Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ECDsa support in X509SecurityKey and JsonWebKeyConverter.ConvertFromX509SecurityKey #2377

Open
wants to merge 22 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 37 additions & 12 deletions src/Microsoft.IdentityModel.Tokens/JsonWebKeyConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,16 @@ public static JsonWebKey ConvertFromX509SecurityKey(X509SecurityKey key)
if (key == null)
throw LogHelper.LogArgumentNullException(nameof(key));

var kty = key.PublicKey switch
{
RSA => JsonWebAlgorithmsKeyTypes.RSA,
ECDsa => JsonWebAlgorithmsKeyTypes.EllipticCurve,
_ => throw LogHelper.LogExceptionMessage(new NotSupportedException(LogHelper.FormatInvariant(LogMessages.IDX10674, LogHelper.MarkAsNonPII(key.GetType().FullName))))
};

var jsonWebKey = new JsonWebKey
{
Kty = JsonWebAlgorithmsKeyTypes.RSA,
Kty = kty,
Kid = key.KeyId,
X5t = key.X5t,
ConvertedSecurityKey = key
Expand All @@ -123,19 +130,37 @@ public static JsonWebKey ConvertFromX509SecurityKey(X509SecurityKey key)
/// <exception cref="ArgumentNullException">if <paramref name="key"/>is null.</exception>
public static JsonWebKey ConvertFromX509SecurityKey(X509SecurityKey key, bool representAsRsaKey)
{
if (!representAsRsaKey)
return ConvertFromX509SecurityKey(key);

if (key == null)
throw LogHelper.LogArgumentNullException(nameof(key));

RSA rsaKey;
if (!representAsRsaKey)
return ConvertFromX509SecurityKey(key);

if (key.PrivateKeyStatus == PrivateKeyStatus.Exists)
rsaKey = key.PrivateKey as RSA;
else
rsaKey = key.PublicKey as RSA;
{
if (key.PrivateKey is RSA rsaPrivateKey)
{
return ConvertFromRSASecurityKey(new RsaSecurityKey(rsaPrivateKey) { KeyId = key.KeyId });
}
#if NET472 || NETSTANDARD2_0 || NET6_0_OR_GREATER
else if (key.PrivateKey is ECDsa ecdsaPrivateKey)
{
return ConvertFromECDsaSecurityKey(new ECDsaSecurityKey(ecdsaPrivateKey) { KeyId = key.KeyId });
}
#endif
}
else if (key.PublicKey is RSA rsaPublicKey)
{
return ConvertFromRSASecurityKey(new RsaSecurityKey(rsaPublicKey) { KeyId = key.KeyId });
}
#if NET472 || NETSTANDARD2_0 || NET6_0_OR_GREATER
else if (key.PublicKey is ECDsa ecdsaPublicKey)
{
return ConvertFromECDsaSecurityKey(new ECDsaSecurityKey(ecdsaPublicKey) { KeyId = key.KeyId });
}
#endif

return ConvertFromRSASecurityKey(new RsaSecurityKey(rsaKey) { KeyId = key.KeyId });
throw LogHelper.LogExceptionMessage(new NotSupportedException(LogHelper.FormatInvariant(LogMessages.IDX10674, LogHelper.MarkAsNonPII(key.GetType().FullName))));
}

/// <summary>
Expand Down Expand Up @@ -167,15 +192,15 @@ public static JsonWebKey ConvertFromSymmetricSecurityKey(SymmetricSecurityKey ke
/// <exception cref="ArgumentNullException">if <paramref name="key"/>is null.</exception>
public static JsonWebKey ConvertFromECDsaSecurityKey(ECDsaSecurityKey key)
{
if (!ECDsaAdapter.SupportsECParameters())
throw LogHelper.LogExceptionMessage(new PlatformNotSupportedException(LogMessages.IDX10695));

if (key == null)
throw LogHelper.LogArgumentNullException(nameof(key));

if (key.ECDsa == null)
throw LogHelper.LogArgumentNullException(nameof(key.ECDsa));

if (!ECDsaAdapter.SupportsECParameters())
throw LogHelper.LogExceptionMessage(new PlatformNotSupportedException(LogMessages.IDX10695));

ECParameters parameters;
try
{
Expand Down
11 changes: 6 additions & 5 deletions src/Microsoft.IdentityModel.Tokens/X509SecurityKey.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ public AsymmetricAlgorithm PrivateKey
{
if (!_privateKeyAvailabilityDetermined)
{
_privateKey = RSACertificateExtensions.GetRSAPrivateKey(Certificate);

var rsaPrivateKey = Certificate.GetRSAPrivateKey();
_privateKey = rsaPrivateKey != null ? rsaPrivateKey : Certificate.GetECDsaPrivateKey();
_privateKeyAvailabilityDetermined = true;
}
}
Expand All @@ -102,7 +102,8 @@ public AsymmetricAlgorithm PublicKey
{
if (_publicKey == null)
{
_publicKey = RSACertificateExtensions.GetRSAPublicKey(Certificate);
var rsaPublicKey = Certificate.GetRSAPublicKey();
_publicKey = rsaPublicKey != null ? rsaPublicKey : Certificate.GetECDsaPublicKey();
}
}
}
Expand Down Expand Up @@ -156,7 +157,7 @@ public X509Certificate2 Certificate
/// <remarks>https://datatracker.ietf.org/doc/html/rfc7638</remarks>
public override bool CanComputeJwkThumbprint()
{
return (PublicKey as RSA) != null ? true : false;
return PublicKey is RSA || PublicKey is ECDsa;
}

/// <summary>
Expand All @@ -166,7 +167,7 @@ public override bool CanComputeJwkThumbprint()
/// <remarks>https://datatracker.ietf.org/doc/html/rfc7638</remarks>
public override byte[] ComputeJwkThumbprint()
{
return new RsaSecurityKey(PublicKey as RSA).ComputeJwkThumbprint();
return PublicKey is RSA ? new RsaSecurityKey(PublicKey as RSA).ComputeJwkThumbprint() : new ECDsaSecurityKey(PublicKey as ECDsa).ComputeJwkThumbprint();
}

/// <summary>
Expand Down
92 changes: 90 additions & 2 deletions test/Microsoft.IdentityModel.TestUtils/KeyingMaterial.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,16 @@ public static X509Certificate2 AADSigningCert
public static X509SecurityKey DefaultX509Key_2048_With_KeyId = new X509SecurityKey(DefaultCert_2048) { KeyId = DefaultX509Key_2048_KeyId };
public static SigningCredentials DefaultX509SigningCreds_2048_RsaSha2_Sha2 = new SigningCredentials(DefaultX509Key_2048, SecurityAlgorithms.RsaSha256Signature, SecurityAlgorithms.Sha256Digest);
public static X509Certificate2 DefaultAsymmetricCert_2048 = CertificateHelper.LoadX509Certificate(DefaultX509Data_2048, CertPassword, X509KeyStorageFlags.MachineKeySet);

#if NET472 || NET_CORE
// 256 bit ECDSA
public static X509Certificate2 DefaultCert_256ECDSA;
public static string DefaultX509Key_256ECDSA_Thumbprint;
public static string DefaultX509Key_256ECDSA_KeyId = "DefaultX509Key_256ECDSA_KeyId";
public static X509SecurityKey DefaultX509Key_256ECDSA;
public static X509SecurityKey DefaultX509Key_256ECDSA_With_KeyId;
#endif

public static string DefaultX509Data_2048_Public = @"MIICyjCCAbKgAwIBAgIQJPMYqnyiTY1GQYAwZxadMjANBgkqhkiG9w0BAQsFADAhMR8wHQYDVQQDExZBREZTIFNpZ25pbmcgLSBTVFMuY29tMB4XDTEyMTAwOTIyMTA0OVoXDTEzMTAwOTIyMTA0OVowITEfMB0GA1UEAxMWQURGUyBTaWduaW5nIC0gU1RTLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMmeVPJz8o7ayB3AS2dJtsIo/eXqeNhZ+ZqEJgHVHc0JAAgNNwR++moMt8+iIlOKZiAL8dvQBKOuPms+FfqrG1HshnMiLcuadtWUqOntxUdyQLcEKvdaFOqOppqmasqGFtRLPwYKIkZOkj8ikndNzI6PZV46mw18nLaN6rTByMnjVA5n9Lf7Cdu7lmxlKGJOI5F0IfeaW68/kY1bdw3KAEb1aOKHj0r7RJ2joRuHJ+96kw1bA2T6bGC/1LYND3DFsnQQtMBl7LlDrSG1gGoiZxCoQmPCxfrTCrYKGK6y9j6IQ4MCmJpnt0l/INL5i88TjctF4IkJwbJGn9iY2fIIBxMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAq/SyHGCLpBm+Gmh5I7BAWJXvtPaIelt30WgKVXRHccxRVIYpKOfAA2iPuD/CVruFz6pnP4K7o2KLAs+XJptigYzLEjKw6rY4836ZJC8m5kfBVanu45OW39nxzxp1udbxQ5gAdmvnY/2agpFhCFR8M1BtWON6G3SzHwo2dXHh+ettOO2LtK38e1+Uy+KGowRw/m4gprSIvgN3AAo7e0PnFblZn6vRgMsK60QB5D8f+Kxdg2I3ZGQcPBQI2fpjEDQCZVc2LV4ywPX4QDPfmYjn+1IaU9w7unbh+oUGQsrdKw3gsdzWEsX/IMXTDf46FEOjV+JqE7VilzcNuDcQ0x9K8gAA";

public static X509Certificate2 DefaultCert_2048_Public
Expand Down Expand Up @@ -215,7 +225,6 @@ public static RsaSecurityKey RsaSecurityKey2
public static readonly ECDsaSecurityKey Ecdsa521Key_Public;

// SymmetricKeys

public static string DefaultSymmetricKeyEncoded_56 = "bd0Q+Z6Ydw==";
public static byte[] DefaultSymmetricKeyBytes_56 = Convert.FromBase64String(DefaultSymmetricKeyEncoded_56);
public static SymmetricSecurityKey DefaultSymmetricSecurityKey_56 = new SymmetricSecurityKey(DefaultSymmetricKeyBytes_56) { KeyId = "DefaultSymmetricSecurityKey_56" };
Expand Down Expand Up @@ -504,8 +513,12 @@ static KeyingMaterial()
Ecdsa256Key_Public = new ECDsaSecurityKey(Ecdsa256_Public) { KeyId = "ECDsa256Key_Public" };
Ecdsa384Key_Public = new ECDsaSecurityKey(Ecdsa384_Public) { KeyId = "ECDsa384Key_Public" };
Ecdsa521Key_Public = new ECDsaSecurityKey(Ecdsa521_Public) { KeyId = "ECDsa521Key_Public" };
#endif

DefaultCert_256ECDSA = new CertificateRequest("CN=KeyStoreTestCertificate", Ecdsa256, HashAlgorithmName.SHA256).CreateSelfSigned(notBefore: DateTimeOffset.UtcNow, notAfter: DateTimeOffset.UtcNow.AddHours(1));
DefaultX509Key_256ECDSA_Thumbprint = DefaultCert_256ECDSA.Thumbprint;
DefaultX509Key_256ECDSA = new X509SecurityKey(DefaultCert_256ECDSA);
DefaultX509Key_256ECDSA_With_KeyId = new X509SecurityKey(DefaultCert_256ECDSA) { KeyId = DefaultX509Key_256ECDSA_KeyId };
#endif
}

#if NET462
Expand Down Expand Up @@ -1058,6 +1071,81 @@ public static JsonWebKey JsonWebKeyX509_2048_As_RSA_With_KeyId
}
}

#if NET472 || NET_CORE
public static JsonWebKey JsonWebKeyX509_256ECDSA
{
get
{
var jsonWebKey = new JsonWebKey
{
Kty = JsonWebAlgorithmsKeyTypes.EllipticCurve,
Kid = DefaultX509Key_256ECDSA_Thumbprint,
X5t = Base64UrlEncoder.Encode(DefaultCert_256ECDSA.GetCertHash())
};

jsonWebKey.X5c.Add(Convert.ToBase64String(DefaultCert_256ECDSA.RawData));
return jsonWebKey;
}
}

public static JsonWebKey JsonWebKeyX509_256ECDSA_With_KeyId
{
get
{
var jsonWebKey = new JsonWebKey
{
Kty = JsonWebAlgorithmsKeyTypes.EllipticCurve,
Kid = DefaultX509Key_256ECDSA_KeyId,
X5t = Base64UrlEncoder.Encode(DefaultCert_256ECDSA.GetCertHash())
};

jsonWebKey.X5c.Add(Convert.ToBase64String(DefaultCert_256ECDSA.RawData));
return jsonWebKey;
}
}

public static JsonWebKey JsonWebKeyX509_256ECDSA_As_ECDSA
{
get
{
var ecdsa = DefaultX509Key_256ECDSA.PrivateKey as ECDsa;
var ecParams = ecdsa.ExportParameters(true);

var jsonWebKey = new JsonWebKey
{
Kty = JsonWebAlgorithmsKeyTypes.EllipticCurve,
Kid = DefaultX509Key_256ECDSA_Thumbprint,
Crv = JsonWebKeyECTypes.P256,
D = Base64UrlEncoder.Encode(ecParams.D),
X = Base64UrlEncoder.Encode(ecParams.Q.X),
Y = Base64UrlEncoder.Encode(ecParams.Q.Y)
};

return jsonWebKey;
}
}

public static JsonWebKey JsonWebKeyX509_256ECDSA_As_ECDSA_With_KeyId
{
get
{
var ecdsa = DefaultX509Key_256ECDSA_With_KeyId.PrivateKey as ECDsa;
var ecParams = ecdsa.ExportParameters(true);

var jsonWebKey = new JsonWebKey
{
Kty = JsonWebAlgorithmsKeyTypes.EllipticCurve,
Kid = DefaultX509Key_256ECDSA_KeyId,
Crv = JsonWebKeyECTypes.P256,
D = Base64UrlEncoder.Encode(ecParams.D),
X = Base64UrlEncoder.Encode(ecParams.Q.X),
Y = Base64UrlEncoder.Encode(ecParams.Q.Y)
};

return jsonWebKey;
}
}
#endif
private static SecureString ConvertToSecureString(string password)
{
if (password == null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace Microsoft.IdentityModel.Tokens.Tests
{
public class JsonWebKeyConverterTest
{
[Theory, MemberData(nameof(ConvertSecurityKeyToJsonWebKeyTheoryData), DisableDiscoveryEnumeration = true)]
[Theory, MemberData(nameof(ConversionKeyTheoryData), DisableDiscoveryEnumeration = true)]
public void ConvertSecurityKeyToJsonWebKey(JsonWebKeyConverterTheoryData theoryData)
{
var context = TestUtilities.WriteHeader($"{this}.ConvertSecurityKeyToJsonWebKey", theoryData);
Expand All @@ -32,7 +32,7 @@ public void ConvertSecurityKeyToJsonWebKey(JsonWebKeyConverterTheoryData theoryD
TestUtilities.AssertFailIfErrors(context);
}

[Theory, MemberData(nameof(ConvertToJsonWebKeyToSecurityKeyTheoryData), DisableDiscoveryEnumeration = true)]
[Theory, MemberData(nameof(ConversionKeyTheoryData), DisableDiscoveryEnumeration = true)]
public void ConvertJsonWebKeyToSecurityKey(JsonWebKeyConverterTheoryData theoryData)
{
var context = TestUtilities.WriteHeader($"{this}.ConvertJsonWebKeyToSecurityKey", theoryData);
Expand Down Expand Up @@ -63,9 +63,8 @@ public void ConvertX509SecurityKeyAsRsaSecurityKeyToJsonWebKey(JsonWebKeyConvert
theoryData.ExpectedException.ProcessNoException(context);
IdentityComparer.AreEqual(convertedKey, theoryData.JsonWebKey, context);

var expectedConvertedKeyType = theoryData.RepresentAsRsaKey == true ? typeof(RsaSecurityKey) : typeof(X509SecurityKey);
if (convertedKey.ConvertedSecurityKey.GetType() != expectedConvertedKeyType)
context.AddDiff($"convertedKey.ConvertedSecurityKey.GetType(): '{convertedKey.ConvertedSecurityKey.GetType()}' != expectedConvertedKeyType: '{expectedConvertedKeyType}'.");
if (!theoryData.RepresentAsRsaKey && convertedKey.ConvertedSecurityKey.GetType() != typeof(X509SecurityKey))
context.AddDiff($"convertedKey.ConvertedSecurityKey.GetType() should be {typeof(X509SecurityKey)}.");
}
catch (Exception ex)
{
Expand All @@ -75,32 +74,6 @@ public void ConvertX509SecurityKeyAsRsaSecurityKeyToJsonWebKey(JsonWebKeyConvert
TestUtilities.AssertFailIfErrors(context);
}

public static TheoryData<JsonWebKeyConverterTheoryData> ConvertSecurityKeyToJsonWebKeyTheoryData
{
get
{
var theoryData = ConversionKeyTheoryData;
#if !NET472 && !NET_CORE
theoryData.Add(new JsonWebKeyConverterTheoryData
{
SecurityKey = KeyingMaterial.Ecdsa256Key,
JsonWebKey = KeyingMaterial.JsonWebKeyP256_Public,
ExpectedException = ExpectedException.NotSupportedException("IDX10674"),
TestId = "SecurityKeyNotSupported"
});
#endif
return theoryData;
}
}

public static TheoryData<JsonWebKeyConverterTheoryData> ConvertToJsonWebKeyToSecurityKeyTheoryData
{
get
{
return ConversionKeyTheoryData;
}
}

public static TheoryData<JsonWebKeyConverterTheoryData> ConversionKeyTheoryData
{
get
Expand Down Expand Up @@ -235,7 +208,37 @@ public static TheoryData<JsonWebKeyConverterTheoryData> ConvertX509SecurityKeyTo
JsonWebKey = KeyingMaterial.JsonWebKeyX509_2048_Public_As_RSA,
TestId = nameof(KeyingMaterial.DefaultX509Key_2048_Public) + nameof(JsonWebKeyConverterTheoryData.RepresentAsRsaKey)
});
#if NET472 || NET_CORE
theoryData.Add(new JsonWebKeyConverterTheoryData
{
SecurityKey = KeyingMaterial.DefaultX509Key_256ECDSA,
JsonWebKey = KeyingMaterial.JsonWebKeyX509_256ECDSA,
TestId = nameof(KeyingMaterial.DefaultX509Key_256ECDSA)
});

theoryData.Add(new JsonWebKeyConverterTheoryData
{
SecurityKey = KeyingMaterial.DefaultX509Key_256ECDSA_With_KeyId,
JsonWebKey = KeyingMaterial.JsonWebKeyX509_256ECDSA_With_KeyId,
TestId = nameof(KeyingMaterial.DefaultX509Key_256ECDSA_With_KeyId)
});

theoryData.Add(new JsonWebKeyConverterTheoryData
{
SecurityKey = KeyingMaterial.DefaultX509Key_256ECDSA,
RepresentAsRsaKey = true,
JsonWebKey = KeyingMaterial.JsonWebKeyX509_256ECDSA_As_ECDSA,
TestId = nameof(KeyingMaterial.DefaultX509Key_256ECDSA_With_KeyId) + nameof(JsonWebKeyConverterTheoryData.RepresentAsRsaKey)
});

theoryData.Add(new JsonWebKeyConverterTheoryData
{
SecurityKey = KeyingMaterial.DefaultX509Key_256ECDSA_With_KeyId,
RepresentAsRsaKey = true,
JsonWebKey = KeyingMaterial.JsonWebKeyX509_256ECDSA_As_ECDSA_With_KeyId,
TestId = nameof(KeyingMaterial.DefaultX509Key_256ECDSA) + nameof(JsonWebKeyConverterTheoryData.RepresentAsRsaKey)
});
#endif
return theoryData;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,25 @@ public void Constructor()
TestUtilities.AssertFailIfErrors(context);
}

#if NET472 || NET_CORE
[Fact]
public void Constructor_WithECDsa()
{
var certificate = KeyingMaterial.DefaultCert_256ECDSA;
var x509SecurityKey = new X509SecurityKey(certificate);
var context = new CompareContext();
IdentityComparer.AreEqual(x509SecurityKey.KeyId, certificate.Thumbprint, context);
IdentityComparer.AreEqual(x509SecurityKey.X5t, Base64UrlEncoder.Encode(certificate.GetCertHash()), context);
IdentityComparer.AreEqual(certificate, x509SecurityKey.Certificate, context);
Assert.NotNull(x509SecurityKey.PublicKey);
Assert.NotNull(x509SecurityKey.PrivateKey);
Assert.Equal(PrivateKeyStatus.Exists, x509SecurityKey.PrivateKeyStatus);
Assert.True(x509SecurityKey.CanComputeJwkThumbprint());
Assert.NotEmpty(x509SecurityKey.ComputeJwkThumbprint());
TestUtilities.AssertFailIfErrors(context);
}
#endif

[Fact]
public void CanComputeJwkThumbprint()
{
Expand Down
Loading