Openiddict-core: Use ECCurve.Oid.Value when comparing EC curves

Created on 11 Oct 2020  路  29Comments  路  Source: openiddict/openiddict-core

Hosted on Ubuntu, NGINX, Cloudflare DNS with proxy
OpenIdDict v2.0.1 and _other STS_ are working fine in production.
Dev build works fine.
Being new to OpenIdDict, I'm probably missing something obvious.

Api side:

services
   .AddOpenIddict()
   .AddValidation(options =>
   {       
   // Note: the validation handler uses OpenID Connect discovery
   // to retrieve the issuer signing keys used to validate tokens.
       options.SetIssuer(stsOptions.Authority);         
       options.AddAudiences(CLIENT_ID);          
       options.UseSystemNetHttp();          
       options.UseAspNetCore();
   });

Everything works using JwtBearer auth instead OpenIdDict

services.AddAuthentication(options => 
   .AddJwtBearer(options =>
   {
       options.Authority = stsOptions.Authority;
       options.Audience = CLIENT_ID;       
       options.TokenValidationParameters = new TokenValidationParameters
       {
           IssuerSigningKey = ECDsaKey,           
       };
});

Api stack:

System.InvalidOperationException: IDX20803: Unable to obtain configuration from: 'https://***/.well-known/openid-configuration'.
 ---> OpenIddict.Abstractions.OpenIddictExceptions+GenericException: An error occurred while handling the cryptography response.
  Error: server_error
  Error description: A JWKS response containing an invalid key was returned.
  Error URI:
   at OpenIddict.Validation.OpenIddictValidationService.<>c__DisplayClass3_0.<<GetSecurityKeysAsync>g__HandleCryptographyResponseAsync|3>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at OpenIddict.Validation.OpenIddictValidationService.GetSecurityKeysAsync(Uri address, CancellationToken cancellationToken)
   at OpenIddict.Validation.OpenIddictValidationService.GetSecurityKeysAsync(Uri address, CancellationToken cancellationToken)
   at OpenIddict.Validation.OpenIddictValidationRetriever.Microsoft.IdentityModel.Protocols.IConfigurationRetriever<Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfiguration>.GetConfigur
ationAsync(String address, IDocumentRetriever retriever, CancellationToken cancel)
   at Microsoft.IdentityModel.Protocols.ConfigurationManager`1.GetConfigurationAsync(CancellationToken cancel)
   --- End of inner exception stack trace ---
   at Microsoft.IdentityModel.Protocols.ConfigurationManager`1.GetConfigurationAsync(CancellationToken cancel)
   at OpenIddict.Validation.OpenIddictValidationHandlers.ValidateIdentityModelToken.HandleAsync(ProcessAuthenticationContext context)
   at OpenIddict.Validation.OpenIddictValidationDispatcher.DispatchAsync[TContext](TContext context)
   at OpenIddict.Validation.OpenIddictValidationDispatcher.DispatchAsync[TContext](TContext context)
   at OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandler.HandleAuthenticateAsync()
   at Microsoft.AspNetCore.Authentication.AuthenticationHandler`1.AuthenticateAsync()
   at Microsoft.AspNetCore.Authentication.AuthenticationService.AuthenticateAsync(HttpContext context, String scheme)
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
   at Serilog.AspNetCore.RequestLoggingMiddleware.Invoke(HttpContext httpContext)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

Server side:

services.AddOpenIddict()
    .AddCore(options => ...
    .AddServer(options =>
        {
         options
            .SetTokenEndpointUris("/connect/token")
            .SetAuthorizationEndpointUris("/connect/authorize")
            .SetLogoutEndpointUris("/connect/logout")
            .SetUserinfoEndpointUris("/api/userinfo");
            ...
            .UseAspNetCore()
            .EnableAuthorizationEndpointPassthrough()
            .EnableVerificationEndpointPassthrough()
            .EnableTokenEndpointPassthrough()
        .AddSigningKey(ECDsaKey);

    .AddValidation(options =>
    {
        options.UseLocalServer();   
        options.UseAspNetCore();
    });

Server log (Api is calling http:// instead https://):

[16:55:23 Information] Microsoft.AspNetCore.Hosting.Diagnostics
Request starting HTTP/1.1 GET http://***/.well-known/jwks

[16:55:23 Debug] Microsoft.AspNetCore.Routing.Matching.DfaMatcher
No candidates found for the request path '/.well-known/jwks'

[16:55:23 Debug] Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware
Request did not match any endpoints

[16:55:23 Information] OpenIddict.Server.OpenIddictServerDispatcher
The request address matched a server endpoint: Cryptography.
...
[16:55:23 Information] OpenIddict.Server.OpenIddictServerDispatcher
The response was successfully returned as a JSON document: {
  "keys":...
}.

...

[16:56:12 Information] Microsoft.AspNetCore.Hosting.Diagnostics
Request starting HTTP/1.1 GET http://***/.well-known/openid-configuration

[16:56:12 Debug] Microsoft.AspNetCore.Routing.Matching.DfaMatcher
No candidates found for the request path '/.well-known/openid-configuration'

[16:56:12 Information] OpenIddict.Server.OpenIddictServerDispatcher
The response was successfully returned as a JSON document: {
  "issuer": "https://***/",
  ...
}.
...
[16:56:14 Information] Microsoft.AspNetCore.Hosting.Diagnostics
Request starting HTTP/1.1 GET http://***/.well-known/jwks

[16:56:14 Debug] Microsoft.AspNetCore.Routing.Matching.DfaMatcher
No candidates found for the request path '/.well-known/jwks'

[16:56:14 Debug] Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware
Request did not match any endpoints

[16:56:14 Information] OpenIddict.Server.OpenIddictServerDispatcher
The request address matched a server endpoint: Cryptography.

bug

All 29 comments

Can you share the complete response returned by the /.well-known/jwks endpoint? Without that, it's hard to say what's going on.

Can you share the complete response returned by the /.well-known/jwks endpoint? Without that, it's hard to say what's going on.

Sure.

[16:55:23 Information] OpenIddict.Server.OpenIddictServerDispatcher
The response was successfully returned as a JSON document: {
  "keys": [
    {
      "kid": "BDFOWTSVUCKCTMHFJMBNKNK1V4FQFIHTRUAG1HUU",
      "use": "sig",
      "kty": "EC",
      "alg": "ES256",
      "x": "bdFOwTsvuCKcTMHfJMBNknK1v4fQfIhTruag1hUuSPc",
      "y": "oe6QQOQbqjUzLHovr2LLxQeBzPZAudObtSKUO8kBKBU"
    }
  ]
}.

As I suspected, the error you see is due to the fact there's no crv node returned by the JWKS endpoint, which seems to indicate it's not one of the supported NIST curves:

https://github.com/openiddict/openiddict-core/blob/725265e04ea18a4db4c789b08637a80a0d02ad91/src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs#L1201-L1209

How did you create the key?

How did you create the key?

On the fly for this instance.

var bytes = Convert.FromBase64String(...);
            var ecdsa = ECDsa.Create();
            ecdsa?.ImportECPrivateKey(bytes, out _);

            var ECDsaKey = new ECDsaSecurityKey(ecdsa);

Super weird. If you export the parameters using var parameters = ecdsa.ExportParameters(includePrivateParameters: false);, what's the value of parameters.Curve.Oid.FriendlyName and parameters.Curve.Oid.Value?

FriendlyName: nistP256
Value: 1.2.840.10045.3.1.7

Does ECCurve.NamedCurves.nistP256 give you the same values?

It does.

I'm out of ideas. If it gives you exactly the same value, then the string.Equals(parameters.Curve.Oid.FriendlyName, ECCurve.NamedCurves.nistP256.Oid.FriendlyName, StringComparison.Ordinal); check made by OpenIddict should return true and a crv parameter should be returned to indicate it's a P256 curve.

Just to be sure, what's returned by string.Equals(parameters.Curve.Oid.FriendlyName, ECCurve.NamedCurves.nistP256.Oid.FriendlyName, StringComparison.Ordinal); on your Ubuntu machine?

Ubuntu: False
Dev Windows: True

This is insane.

And string.Equals("nistP256", ECCurve.NamedCurves.nistP256.Oid.FriendlyName, StringComparison.Ordinal);?
And string.Equals("nistP256, "nistP256", StringComparison.Ordinal);?

Trueon both cases on both machines.

Heh. Let's try something else:

var result1 = Convert.ToBase64String(Encoding.UTF8.GetBytes(parameters.Curve.Oid.FriendlyName));
var result2 = Convert.ToBase64String(Encoding.UTF8.GetBytes(ECCurve.NamedCurves.nistP256.Oid.FriendlyName));
var result3 = Convert.ToBase64String(Encoding.UTF8.GetBytes(parameters.Curve.Oid.Value));
var result4 = Convert.ToBase64String(Encoding.UTF8.GetBytes(ECCurve.NamedCurves.nistP256.Oid.Value));

Windows:

Result1: bmlzdFAyNTY=
Result2: bmlzdFAyNTY=
Result3: MS4yLjg0MC4xMDA0NS4zLjEuNw==
Result4: MS4yLjg0MC4xMDA0NS4zLjEuNw==

Ubuntu:

Result1: RUNEU0FfUDI1Ng==
Result2: bmlzdFAyNTY=
Result3: MS4yLjg0MC4xMDA0NS4zLjEuNw==
Result4: MS4yLjg0MC4xMDA0NS4zLjEuNw==

You said earlier that FriendlyName was returning nistP256 for your key. Here, it's ECDSA_P256. Does the value change when you restart your app?

Let me reboot server, just to be sure.

Name: ECDSA_P256
Value: 1.2.840.10045.3.1.7

I messed up, was checking values on local machine first time, sorry.

I messed up, was checking values on local machine first time, sorry.

馃槄

So: OpenSSL can return two different friendly names for nistP256. Fun...

I'll update the code to use the OID value.

Thank you.
Should I close it?

Please leave it open, I'll take care of closing it when the bug is fixed in beta6.

Alright, have a nice one.

Thanks, you too!

If you like the project, consider encouraging your employer to sponsor it so I can dedicate more time on it: https://github.com/sponsors/kevinchalet 鉂わ笍

I sure will.

This won't be as easy as expected: on .NET Framework 4.x and .NET Core 2.1, the Oid.Value attached to ECParameters returned by ExportParameters() is typically null (for reasons I ignore), which means we can't remove the FriendlyName check (at least on these platforms). This also explains why I had used FriendlyName instead of Value when adding ECDSA support.

Updated packages were pushed to MyGet.org to fix this issue. Can you please give them a try?

Of course, as soon as I get home.

Everything seems to be fine 馃憤

"keys": [
    {
      "kid": "BDFOWTSVUCKCTMHFJMBNKNK1V4FQFIHTRUAG1HUU",
      "use": "sig",
      "kty": "EC",
      "alg": "ES256",
      "crv": "P-256",
      "x": "bdFOwTsvuCKcTMHfJMBNknK1v4fQfIhTruag1hUuSPc",
      "y": "oe6QQOQbqjUzLHovr2LLxQeBzPZAudObtSKUO8kBKBU"
    }
  ]

@rvlajcev thanks!

Was this page helpful?
0 / 5 - 0 ratings