From ee4a2f7ca3d0f1550d3f3990a63e9fb01158caf9 Mon Sep 17 00:00:00 2001 From: martincostello Date: Sun, 9 Jun 2019 16:08:20 +0100 Subject: [PATCH] Support PKCS 8 keys on macOS and Linux Use new .NET Core 3.0 APIs to support PCKS 8 private keys on Linux and macOS. --- .../AppleAuthenticationOptions.cs | 8 +--- .../AppleAuthenticationOptionsExtensions.cs | 4 +- .../DefaultAppleClientSecretGenerator.cs | 35 +++++++----------- .../Apple/AppleClientSecretGeneratorTests.cs | 17 ++++----- .../Apple/AppleTests.cs | 1 - .../Apple/TestKeys.cs | 26 +++---------- .../Apple/test.cer | 15 -------- .../Apple/test.pfx | Bin 1126 -> 0 bytes ...pNet.Security.OAuth.Providers.Tests.csproj | 3 +- 9 files changed, 28 insertions(+), 81 deletions(-) delete mode 100644 test/AspNet.Security.OAuth.Providers.Tests/Apple/test.cer delete mode 100644 test/AspNet.Security.OAuth.Providers.Tests/Apple/test.pfx diff --git a/src/AspNet.Security.OAuth.Apple/AppleAuthenticationOptions.cs b/src/AspNet.Security.OAuth.Apple/AppleAuthenticationOptions.cs index 8ce0ac49e..31eb6b780 100644 --- a/src/AspNet.Security.OAuth.Apple/AppleAuthenticationOptions.cs +++ b/src/AspNet.Security.OAuth.Apple/AppleAuthenticationOptions.cs @@ -71,16 +71,10 @@ public new AppleAuthenticationEvents Events /// which is passed the value of the property. /// /// - /// On Windows, the private key should be in PKCS #8 (.p8) format. - /// On Linux and macOS, the private key should be PKCS #12 (.pfx) format. + /// The private key should be in PKCS #8 (.p8) format. /// public Func> PrivateKeyBytes { get; set; } - /// - /// Gets or sets the password/passphrase associated with the private key, if any. - /// - public string PrivateKeyPassword { get; set; } = string.Empty; - /// /// Gets or sets the Team ID for your Apple Developer account. /// diff --git a/src/AspNet.Security.OAuth.Apple/AppleAuthenticationOptionsExtensions.cs b/src/AspNet.Security.OAuth.Apple/AppleAuthenticationOptionsExtensions.cs index e34ff3a4e..ca102b066 100644 --- a/src/AspNet.Security.OAuth.Apple/AppleAuthenticationOptionsExtensions.cs +++ b/src/AspNet.Security.OAuth.Apple/AppleAuthenticationOptionsExtensions.cs @@ -17,11 +17,9 @@ public static class AppleAuthenticationOptionsExtensions { public static AppleAuthenticationOptions WithPrivateKey( [NotNull] this AppleAuthenticationOptions options, - [NotNull] Func privateKeyFile, - [CanBeNull] string privateKeyPassword = null) + [NotNull] Func privateKeyFile) { options.GenerateClientSecret = true; - options.PrivateKeyPassword = privateKeyPassword ?? string.Empty; options.PrivateKeyBytes = async (keyId) => { var fileInfo = privateKeyFile(keyId); diff --git a/src/AspNet.Security.OAuth.Apple/Internal/DefaultAppleClientSecretGenerator.cs b/src/AspNet.Security.OAuth.Apple/Internal/DefaultAppleClientSecretGenerator.cs index 340a2b473..cbe9551de 100644 --- a/src/AspNet.Security.OAuth.Apple/Internal/DefaultAppleClientSecretGenerator.cs +++ b/src/AspNet.Security.OAuth.Apple/Internal/DefaultAppleClientSecretGenerator.cs @@ -6,10 +6,8 @@ using System; using System.IdentityModel.Tokens.Jwt; -using System.Runtime.InteropServices; using System.Security.Claims; using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.AspNetCore.Authentication; @@ -81,7 +79,7 @@ public override async Task GenerateAsync([NotNull] AppleGenerateClientSe byte[] keyBlob = await _keyStore.LoadPrivateKeyAsync(context); string clientSecret; - using (var algorithm = CreateAlgorithm(keyBlob, context.Options.PrivateKeyPassword)) + using (var algorithm = CreateAlgorithm(keyBlob)) { tokenDescriptor.SigningCredentials = CreateSigningCredentials(context.Options.KeyId, algorithm); @@ -93,27 +91,20 @@ public override async Task GenerateAsync([NotNull] AppleGenerateClientSe return (clientSecret, expiresAt); } - private ECDsa CreateAlgorithm(byte[] keyBlob, string password) + private ECDsa CreateAlgorithm(byte[] keyBlob) { - // This becomes xplat in .NET Core 3.0: https://github.com/dotnet/corefx/pull/30271 - return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? - CreateAlgorithmWindows(keyBlob) : - CreateAlgorithmLinuxOrMac(keyBlob, password); - } + var algorithm = ECDsa.Create(); - private ECDsa CreateAlgorithmLinuxOrMac(byte[] keyBlob, string password) - { - // Does not support .p8 files in .NET Core 2.x as-per https://github.com/dotnet/corefx/issues/18733#issuecomment-296723615 - // Unlike Linux, macOS does not support empty passwords for .pfx files. - using var cert = new X509Certificate2(keyBlob, password); - return cert.GetECDsaPrivateKey(); - } - - private ECDsa CreateAlgorithmWindows(byte[] keyBlob) - { - // Only Windows supports .p8 files in .NET Core 2.0 as-per https://github.com/dotnet/corefx/issues/18733 - using var privateKey = CngKey.Import(keyBlob, CngKeyBlobFormat.Pkcs8PrivateBlob); - return new ECDsaCng(privateKey) { HashAlgorithm = CngAlgorithm.Sha256 }; + try + { + algorithm.ImportPkcs8PrivateKey(keyBlob, out int _); + return algorithm; + } + catch (Exception) + { + algorithm?.Dispose(); + throw; + } } private SigningCredentials CreateSigningCredentials(string keyId, ECDsa algorithm) diff --git a/test/AspNet.Security.OAuth.Providers.Tests/Apple/AppleClientSecretGeneratorTests.cs b/test/AspNet.Security.OAuth.Providers.Tests/Apple/AppleClientSecretGeneratorTests.cs index 073a163f6..9d25750af 100644 --- a/test/AspNet.Security.OAuth.Providers.Tests/Apple/AppleClientSecretGeneratorTests.cs +++ b/test/AspNet.Security.OAuth.Providers.Tests/Apple/AppleClientSecretGeneratorTests.cs @@ -31,7 +31,6 @@ public static async Task GenerateAsync_Generates_Valid_Signed_Jwt() KeyId = "my-key-id", TeamId = "my-team-id", PrivateKeyBytes = (keyId) => TestKeys.GetPrivateKeyBytesAsync(), - PrivateKeyPassword = TestKeys.GetPrivateKeyPassword(), }; await GenerateTokenAsync(options, async (generator, context) => @@ -80,7 +79,6 @@ public static async Task GenerateAsync_Caches_Jwt_Until_Expired() KeyId = "my-key-id", TeamId = "my-team-id", PrivateKeyBytes = (keyId) => TestKeys.GetPrivateKeyBytesAsync(), - PrivateKeyPassword = TestKeys.GetPrivateKeyPassword(), }; await GenerateTokenAsync(options, async (generator, context) => @@ -114,16 +112,15 @@ public static async Task GenerateAsync_Caches_Jwt_Until_Expired() .AddApple(); }); - using (var host = builder.Build()) - { - var httpContext = new DefaultHttpContext(); - var scheme = new AuthenticationScheme("Apple", "Apple", typeof(AppleAuthenticationHandler)); + using var host = builder.Build(); + + var httpContext = new DefaultHttpContext(); + var scheme = new AuthenticationScheme("Apple", "Apple", typeof(AppleAuthenticationHandler)); - var context = new AppleGenerateClientSecretContext(httpContext, scheme, options); - var generator = host.Services.GetRequiredService(); + var context = new AppleGenerateClientSecretContext(httpContext, scheme, options); + var generator = host.Services.GetRequiredService(); - await actAndAssert(generator, context); - } + await actAndAssert(generator, context); } } } diff --git a/test/AspNet.Security.OAuth.Providers.Tests/Apple/AppleTests.cs b/test/AspNet.Security.OAuth.Providers.Tests/Apple/AppleTests.cs index 658c5510b..c8ced0dd4 100644 --- a/test/AspNet.Security.OAuth.Providers.Tests/Apple/AppleTests.cs +++ b/test/AspNet.Security.OAuth.Providers.Tests/Apple/AppleTests.cs @@ -77,7 +77,6 @@ static void ConfigureServices(IServiceCollection services) options.KeyId = "my-key-id"; options.TeamId = "my-team-id"; options.ValidateTokens = true; - options.PrivateKeyPassword = TestKeys.GetPrivateKeyPassword(); options.PrivateKeyBytes = async (keyId) => { Assert.Equal("my-key-id", keyId); diff --git a/test/AspNet.Security.OAuth.Providers.Tests/Apple/TestKeys.cs b/test/AspNet.Security.OAuth.Providers.Tests/Apple/TestKeys.cs index 9f3c3a4b2..ae73a01ff 100644 --- a/test/AspNet.Security.OAuth.Providers.Tests/Apple/TestKeys.cs +++ b/test/AspNet.Security.OAuth.Providers.Tests/Apple/TestKeys.cs @@ -7,39 +7,23 @@ using System; using System.IO; using System.Linq; -using System.Runtime.InteropServices; using System.Threading.Tasks; namespace AspNet.Security.OAuth.Apple { internal static class TestKeys { - private static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - internal static async Task GetPrivateKeyBytesAsync() { - byte[] privateKey; - - if (IsWindows) - { - string content = await File.ReadAllTextAsync(Path.Combine("Apple", "test.p8")); + string content = await File.ReadAllTextAsync(Path.Combine("Apple", "test.p8")); - if (content.StartsWith("-----BEGIN PRIVATE KEY-----", StringComparison.Ordinal)) - { - string[] keyLines = content.Split('\n'); - content = string.Join(string.Empty, keyLines.Skip(1).Take(keyLines.Length - 2)); - } - - privateKey = Convert.FromBase64String(content); - } - else + if (content.StartsWith("-----BEGIN PRIVATE KEY-----", StringComparison.Ordinal)) { - privateKey = await File.ReadAllBytesAsync(Path.Combine("Apple", "test.pfx")); + string[] keyLines = content.Split('\n'); + content = string.Join(string.Empty, keyLines.Skip(1).Take(keyLines.Length - 2)); } - return privateKey; + return Convert.FromBase64String(content); } - - internal static string GetPrivateKeyPassword() => IsWindows ? string.Empty : "passw0rd"; } } diff --git a/test/AspNet.Security.OAuth.Providers.Tests/Apple/test.cer b/test/AspNet.Security.OAuth.Providers.Tests/Apple/test.cer deleted file mode 100644 index 9f67664d6..000000000 --- a/test/AspNet.Security.OAuth.Providers.Tests/Apple/test.cer +++ /dev/null @@ -1,15 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICYzCCAgmgAwIBAgIJAI8YasGI5F6oMAoGCCqGSM49BAMCMIGNMQswCQYDVQQG -EwJHQjEQMA4GA1UECAwHRW5nbGFuZDEPMA0GA1UEBwwGTG9uZG9uMRcwFQYDVQQK -DA5hc3BuZXQtY29udHJpYjEYMBYGA1UEAwwPTWFydGluIENvc3RlbGxvMSgwJgYJ -KoZIhvcNAQkBFhltYXJ0aW5AbWFydGluY29zdGVsbG8uY29tMB4XDTE5MDYwOTEy -NTgyNVoXDTE5MDcwOTEyNTgyNVowgY0xCzAJBgNVBAYTAkdCMRAwDgYDVQQIDAdF -bmdsYW5kMQ8wDQYDVQQHDAZMb25kb24xFzAVBgNVBAoMDmFzcG5ldC1jb250cmli -MRgwFgYDVQQDDA9NYXJ0aW4gQ29zdGVsbG8xKDAmBgkqhkiG9w0BCQEWGW1hcnRp -bkBtYXJ0aW5jb3N0ZWxsby5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQf -rvDWizEnWAzB2Hx2r/NyvIBO6KGBDL7wkZoKnz4Sm4+1P1dhD9fVEhbsdoq9RKEf -8dvzTOZMaC/iLqZFKSN6o1AwTjAdBgNVHQ4EFgQUZT6ij94pUE1x0rrWWCwswodX -FRMwHwYDVR0jBBgwFoAUZT6ij94pUE1x0rrWWCwswodXFRMwDAYDVR0TBAUwAwEB -/zAKBggqhkjOPQQDAgNIADBFAiAOR5YjLHk5QFPdq9BflblpV4/gwwYcxLM+OuNf -a4PsUQIhAJhh1DNlUH0kvG6/L1gXtd+pi41tjM96RVi4kvrf0xzN ------END CERTIFICATE----- diff --git a/test/AspNet.Security.OAuth.Providers.Tests/Apple/test.pfx b/test/AspNet.Security.OAuth.Providers.Tests/Apple/test.pfx deleted file mode 100644 index 51ed10eb84f8434ffaacebb5ff8fe764a878afdf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1126 zcmXqLVo73RWHxAG(O~1$YV&CO&dbQoxS)welBJ176euic(8SD+kYZcV#Pq|UiRlXy zBLk3ni;!V7kYVG3n!v-zX28e7@qM$ja?bDQY9=NQhK44l1AE>sn(}^*p3JGE)ee<1 z#rv`*`(*yD;&nFcd&YM9$5W0up(m<)+qUKM{Bua%y-m602j_FYU2~6GxzD}F%CL8( z_JL{*;RmM#E{kzWJ_A$u*-R^d6a&&!3-L2|g8-t?q z9~OqUk{a(^6bStK_lTogfz8PWtWQI%12&i+EN(D1Rc!NF8IkE7^7j4f+tE$`H#{_u z=-V0}`nmI|@T2OdlPj+Cc?PrSx&FA-D<)ZcWzvPwk%)Ey}KU%(am4$9=c)q z+o{)1J&=>Sutt7ait$IMgvS#(o;?0{?2Bmf+p~FBrI-KP{GMs&&9n(DhTJ{^zkRtx z44>CF)<~Y3mT~;q>YUq-omU^|dzrrTaO>%J6LDG+6EJUj`5E;&#>^XRm0M~#b{|~z zQ+TiZt<9yAcSx5cKX~SLrf#EXVUEkCrMf3FvuA%56;@%j5H3o%tA5UB)3*J`zousX z*6r38G!WjuB13Dbv%~j!PfC}`R10*;B+N9+d*5`Gb;1R6o-Ov-@+VU-yW8j^wl_J{yXE-c`)| zXb~`Z&fl|9X5ocbKb8hwy~MX%CsxyJmqepYLiDy);b%v3r0Rn&)n8DYYuM=XUh{)N z6C)E+f?v@1g{ASMLE}4v#@B4zu%yq$$h4qwi$UW?l>ER9$`9AXl-r)));$i&4=jxn zmdmeeEPfpG-bo^a@&D%}?T$xJI}HzU&6}2`pwF;ZK3Lb(zE?qnuRO{5@1#Aoaj(iH zxc?>!teM+(-0A<#?b8^gkLvrD-0x%akUjnA(~A}sHiZ!N#0_s2u2^>>>dzwe85d^z z{H$B{>QIC4nh%Or59X$MsQnEUlb$*8 z!@b8TYAD7c^4ZVpjPpA8W3%@OKeL=TdvWwLhaLk%14RQ)HdbvuW+o|C1{RU$VS1vk hG&e3&&YpFFT`-_KXHljG3&%ITioKfVRXQNg0s!Js`*Z*R diff --git a/test/AspNet.Security.OAuth.Providers.Tests/AspNet.Security.OAuth.Providers.Tests.csproj b/test/AspNet.Security.OAuth.Providers.Tests/AspNet.Security.OAuth.Providers.Tests.csproj index 4d3e5ee7d..ee076baa3 100644 --- a/test/AspNet.Security.OAuth.Providers.Tests/AspNet.Security.OAuth.Providers.Tests.csproj +++ b/test/AspNet.Security.OAuth.Providers.Tests/AspNet.Security.OAuth.Providers.Tests.csproj @@ -7,8 +7,7 @@ true - - +