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

ECDsa Signed JWT Support with Identity Server #1154

Closed
danlarson opened this issue Mar 5, 2024 · 6 comments
Closed

ECDsa Signed JWT Support with Identity Server #1154

danlarson opened this issue Mar 5, 2024 · 6 comments

Comments

@danlarson
Copy link

Which version of Duende IdentityServer are you using?
7.0.1

Which version of .NET are you using?
8.0

Describe the bug

I cannot get ECDsa certificate signed JWTs to work with Identity Server. It works fine with RSA but I need this to work with EC keys. To get this working, I started with the sample code, which adds the following in Startup:
builder.AddJwtBearerClientAuthentication();

I did not add anything special in IdentityServer to add EDCsa support, so perhaps that's something that's needed? I did test with an RSA signed JWT and it worked, but then fails with EC keys.

To Reproduce

Configure the public key of the ECDsa certificate as a client secret in IdentityServer.
Create a JWT and sign it using a ECDsa certificate.
Send that to request an auth token from Identity Server.

Expected behavior

Expect it to work. Instead get a failure in JwtTokenHandler, as it doesn't seem to understand EC keys.

Log output/exception with stacktrace

{"IDX10503: Signature validation failed. Token does not have a kid. Keys tried: 'Microsoft.IdentityModel.Tokens.X509SecurityKey, KeyId: '1416B34A2B9B4D4E20F3237EEB0D1297A5C00691', InternalId: 'FBazSiubTU4g8yN-6w0Sl6XABpE'. , KeyId: 1416B34A2B9B4D4E20F3237EEB0D1297A5C00691\r\n'. Number of keys in TokenValidationParameters: '1'. \nNumber of keys in Configuration: '0'. \nExceptions caught:\n ''.\ntoken: '[Security Artifact of type 'Microsoft.IdentityModel.JsonWebTokens.JsonWebToken' is hidden. For more details, see https://aka.ms/IdentityModel/SecurityArtifactLogging.]'. See https://aka.ms/IDX10503 for details."}
    Data: {System.Collections.ListDictionaryInternal}
    HResult: -2146233088
    HelpLink: null
    InnerException: null
    Message: "IDX10503: Signature validation failed. Token does not have a kid. Keys tried: 'Microsoft.IdentityModel.Tokens.X509SecurityKey, KeyId: '1416B34A2B9B4D4E20F3237EEB0D1297A5C00691', InternalId: 'FBazSiubTU4g8yN-6w0Sl6XABpE'. , KeyId: 1416B34A2B9B4D4E20F3237EEB0D1297A5C00691\r\n'. Number of keys in TokenValidationParameters: '1'. \nNumber of keys in Configuration: '0'. \nExceptions caught:\n ''.\ntoken: '[Security Artifact of type 'Microsoft.IdentityModel.JsonWebTokens.JsonWebToken' is hidden. For more details, see https://aka.ms/IdentityModel/SecurityArtifactLogging.]'. See https://aka.ms/IDX10503 for details."
    Source: "Microsoft.IdentityModel.JsonWebTokens"
    StackTrace: "   at Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.ValidateSignature(JsonWebToken jwtToken, TokenValidationParameters validationParameters, BaseConfiguration configuration)\r\n   at Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.ValidateSignatureAndIssuerSecurityKey(JsonWebToken jsonWebToken, TokenValidationParameters validationParameters, BaseConfiguration configuration)\r\n   at Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.<ValidateJWSAsync>d__67.MoveNext()"
    TargetSite: {Microsoft.IdentityModel.JsonWebTokens.JsonWebToken ValidateSignature(Microsoft.IdentityModel.JsonWebTokens.JsonWebToken, Microsoft.IdentityModel.Tokens.TokenValidationParameters, Microsoft.IdentityModel.Tokens.BaseConfiguration)}

Additional context

I replaced the secret validator and my own token handler to try to mimic the Microsoft code and debug it to try to see what's happening, and find that Microsoft's implementation fails in this method of JwtTokenHandler:

	internal static bool ValidateSignature(JsonWebToken jsonWebToken, SecurityKey key, TokenValidationParameters validationParameters)
	{
		var cryptoProviderFactory = validationParameters.CryptoProviderFactory ?? key.CryptoProviderFactory;
		if (!cryptoProviderFactory.IsSupportedAlgorithm(jsonWebToken.Alg, key))

That returns false, which causes the validation to fail, which results in a more generic error message.

In the PrivateKeyJwtSecretValidator given the following code:

var handler = new JsonWebTokenHandler() { MaximumTokenSizeInBytes = _options.InputLengthRestrictions.Jwt };
var result = await handler.ValidateTokenAsync(jwtTokenString, tokenValidationParameters);

I can see that I have the expected matching signing key, and it is able to resolve it using the InternalId from the x5t header.

tokenValidationParameters.IssuerSigningKeys
Count = 1
    [0]: {Microsoft.IdentityModel.Tokens.X509SecurityKey, KeyId: '1416B34A2B9B4D4E20F3237EEB0D1297A5C00691', InternalId: 'FBazSiubTU4g8yN-6w0Sl6XABpE'.}

Deep in Microsoft's code I see the following:

internal static bool ValidateSignature(JsonWebToken jsonWebToken, SecurityKey key, TokenValidationParameters validationParameters)
{
var cryptoProviderFactory = validationParameters.CryptoProviderFactory ?? key.CryptoProviderFactory;
// The cryptoProviderFactory.IsSupportedAlgorithm method returns false which returns a failure:
if (!cryptoProviderFactory.IsSupportedAlgorithm(jsonWebToken.Alg, key))

In Clients.cs (using sample/starter code) I added the following secret (which works with a RSA key and RSA signed JWTs):

new Secret
{
    Description = "EC key from sample code",
    Type = IdentityServerConstants.SecretTypes.X509CertificateBase64,
    Value = "PEM content from the EC key here"
}

On the client, I'm using the following code for the header:
var tokenHeader = new Dictionary<string, string> { { "alg", "ES384" }, { "typ", "JWT" } }; tokenHeader.Add("x5t", Base64UrlEncoder.Encode(cert.GetCertHash()));
In Identityserver, this seems to resolve to the signing key, but then it doesnt seem to know that it should be using ECDsa and fails on the call to IsSupportedAlgorithm inside of Microsoft's library.

@danlarson
Copy link
Author

I think the issue may be that the SecurityKey key that gets passed to JsonWebTokenHandler needs to be a Microsoft.IdentityModel.Tokens.ECDsaSecurityKey but instead gets passed as a Microsoft.IdentityModel.Tokens.X509SecurityKey. Am I missing something in identity server configuration?

@josephdecock
Copy link
Member

Thanks for opening this issue. I've verified that RSA keys stored in X.509 certificates and as JWK strings work, and also EC DSA keys as a JWK string, but you may be on to something with the combination of an X.509 certificate used as the container for an EC DSA key. We'll update here with more details as we find out more.

@danlarson
Copy link
Author

Thanks Joe! Do you have a sample I could see of how to store EC DSA keys as a JWK string? Our use case is a device that has EC keys baked into the trust chip, and we want to export the public key to identity server so the device can authenticate. I haven't seen a good example on how to do this.

@josephdecock
Copy link
Member

josephdecock commented Mar 12, 2024

Sure! Within IdentityServer's main repo, we have a collection of clients that we use internally for development and testing, and that's where I verified the EC DSA as a JWK. Those clients aren't as straightforward to use as the code we package up as an official sample in our samples repo, but I think it shows what you need to do. I've just opened a PR - take a look: DuendeSoftware/IdentityServer#1528.

@danlarson
Copy link
Author

Thanks Joe-- we got it working from that! That totally unblocked me. Except for embedding it in identity server secrets, we're using the following code to create the EC keys from the public keys, and roughly the same for the private keys:
`// Create pub key:
var publicKey = Convert.FromBase64String("redacted");

var x = publicKey.Skip(1).Take(32).ToArray();
var y = publicKey.Skip(33).ToArray();

var publicEcdsa = ECDsa.Create(new ECParameters
{
Curve = ECCurve.NamedCurves.nistP256,
Q = new ECPoint
{
X = x,
Y = y
}
});
var jwk = JsonWebKeyConverter.ConvertFromECDsaSecurityKey(new ECDsaSecurityKey(publicEcdsa));
`
This seems to be a bit more stable and reproducible with multiple EC certs. The public key we can read from the PEM. You can then use JsonSerializer to cram this into identity server's client secrets.

It would be nice if this was just a new parsable secret, and instead of using the JWK we could just use the public key and tell it to use an EC signature...

@josephdecock
Copy link
Member

Glad to hear you're unblocked. I've added the issue linked above to investigate how we can make this scenario more convenience. For now, I'll go ahead and close this issue, but feel free to follow up if you have more thoughts/questions/concerns!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants