Skip to content
Permalink
Browse files

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.
  • Loading branch information...
martincostello committed Jun 9, 2019
1 parent b06a10e commit ee4a2f7ca3d0f1550d3f3990a63e9fb01158caf9
@@ -71,16 +71,10 @@ public new AppleAuthenticationEvents Events
/// which is passed the value of the <see cref="KeyId"/> property.
/// </summary>
/// <remarks>
/// On Windows, the private key should be in PKCS #8 (<c>.p8</c>) format.
/// On Linux and macOS, the private key should be PKCS #12 (<c>.pfx</c>) format.
/// The private key should be in PKCS #8 (<c>.p8</c>) format.
/// </remarks>
public Func<string, Task<byte[]>> PrivateKeyBytes { get; set; }

/// <summary>
/// Gets or sets the password/passphrase associated with the private key, if any.
/// </summary>
public string PrivateKeyPassword { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the Team ID for your Apple Developer account.
/// </summary>
@@ -17,11 +17,9 @@ public static class AppleAuthenticationOptionsExtensions
{
public static AppleAuthenticationOptions WithPrivateKey(
[NotNull] this AppleAuthenticationOptions options,
[NotNull] Func<string, IFileInfo> privateKeyFile,
[CanBeNull] string privateKeyPassword = null)
[NotNull] Func<string, IFileInfo> privateKeyFile)
{
options.GenerateClientSecret = true;
options.PrivateKeyPassword = privateKeyPassword ?? string.Empty;
options.PrivateKeyBytes = async (keyId) =>
{
var fileInfo = privateKeyFile(keyId);
@@ -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<string> 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<string> 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)
@@ -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<AppleClientSecretGenerator>();
var context = new AppleGenerateClientSecretContext(httpContext, scheme, options);
var generator = host.Services.GetRequiredService<AppleClientSecretGenerator>();

await actAndAssert(generator, context);
}
await actAndAssert(generator, context);
}
}
}
@@ -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);
@@ -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<byte[]> 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";
}
}

This file was deleted.

Binary file not shown.
@@ -7,8 +7,7 @@
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<Content Include="xunit.runner.json;**\bundle.json" Exclude="bin\**\bundle.json" CopyToOutputDirectory="PreserveNewest" />
<Content Include="Apple\test.p8;Apple\test.pfx" CopyToOutputDirectory="PreserveNewest" />
<Content Include="xunit.runner.json;**\bundle.json;Apple\test.p8" Exclude="bin\**\bundle.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\AspNet.Security.OAuth.*\*.csproj" />

0 comments on commit ee4a2f7

Please sign in to comment.
You can’t perform that action at this time.