Post-quantum token signing with Dilithium using Duende Identity Server

Β· 2084 words Β· 10 minutes to read

On March 12th, a new IETF draft JOSE and COSE Encoding for Dilithium was published. It describes JSON serializations for CRYSTALS-Dilithium, a post-quantum cryptography suite. This in turn allows using post-quantum cryptography for tasks like signing JSON Web Tokens in a standardized fashion.

I previously blogged about using CRYSTALS-Dilithium from .NET applications, so in this post let’s see how we can apply this new draft to one of the most popular .NET OpenID Connect / OAuth 2.0 servers, Duende Identity Server

The new spec πŸ”—

The spec defines how information about being signed or encrypted with CRYSTALS-Dilithium should be conveyed when performing JSON Object Signing and Encryption (JOSE).

We shall focus here on the use case of JSON Web Tokens, and their signatures. For that, the public key of the Dilithium pair must be exposed in a following JSON format:

{
  "kty": "LWE",
  "alg": "CRYDI<Dilithium parameter set>",
  "x": "<base64url encoded public key bytes>"
}

This is the bare required minimum in terms of the properties describing the public key. The possible values for alg are CRYDI2, CRYDI3 and CRYDI5, corresponding to the Dilithium parameter set. For example CRYDI3 is Dilithium3 which offers security level comparable to 128-bit AES against both classical and quantum threats.

When publishing JSON Web Keys from an identity provider, there may also be a need to report a use property, which should be set to sig. Additionally, it’s possible to include a key_ops array, which could have values sign or verify

The specification also mentions that the appropriate CRYDI value (as mentioned above) needs to be included in the alg property in the header of a distributed a JSON Web Token.

Outlining the integration into Duende Identity Server πŸ”—

I already covered using Dilithium with .NET applications via BouncyCastle implementation before, so I will not go through that again - if needed, I recommend you have a look at that article.

In this post we will instead focus on integration into Duende Identity Server. The server relies on SigningCredentials from the Microsoft.IdentityModel.Tokens library for performing cryptographic operations. This means that the standard way of loading new signing credentials for tokens is limited to the cryptographic features of that library, and it’s not possible to support third-party implementation, like the BouncyCastle one, this way.

However, Duende Identity Server is also very flexible and large parts of its functionality can be swapped with custom implementations. Almost all internal services it uses are overridaable, and almost every public method used by the server is virtual.

From that perspective, in order to add support for Dilithium, we need to provide custom implementations of:

  • token creation service - ITokenCreationService, responsible for serializing and signing the actual JWT
  • discovery document generator - IDiscoveryResponseGenerator, responsible, among other things, for generating the JSON Web Keys response

Before we get there, however, let us start by creating a custom class that will actually hold our public and private keys, which we will call DilithiumCredentials. It will end up being used in both places I mentioned above.

public class DilithiumCredentials
{
    public DilithiumCredentials()
    {
        var random = new SecureRandom();
        var keyGenParameters = new DilithiumKeyGenerationParameters(random, DilithiumParameters.Dilithium3);
        var dilithiumKeyPairGenerator = new DilithiumKeyPairGenerator();
        dilithiumKeyPairGenerator.Init(keyGenParameters);

        var keyPair = dilithiumKeyPairGenerator.GenerateKeyPair();

        PublicKey = (DilithiumPublicKeyParameters)keyPair.Public;
        PrivateKey = (DilithiumPrivateKeyParameters)keyPair.Private;
        KeyId = BitConverter.ToString(SecureRandom.GetNextBytes(random, 16)).Replace("-", "");
    }

    public DilithiumPublicKeyParameters PublicKey { get; }
    public DilithiumPrivateKeyParameters PrivateKey { get; }
    public string KeyId { get; }
    public string Alg { get; } = "CRYDI3";
}

The class creates a new Dilithium private/public pair and generates a key ID which will be published in the JWKS document and which will be embedded into the token.

We will use it as a singleton, so the key creation can happen in the constructor. Of course this means that the lifetime will be limited to the lifetime of the server application, but this is fine for our demo purposes - in real life the pair would be generated upfront and loaded from some secure storage like a key vault.

JWKS document πŸ”—

JSON Web Key Set is used by the identity servers to announce the public keys it uses to sign the tokens. The JWKS URL is published by the server in the OIDC discovery document. For Duende Identity Server it’s normally /.well-known/openid-configuration/jwks.

If we could add our Dilithium credential as SigningCredentials instance, the JWKS document would be updated automatically for us. However, since this is not possible, we have to update the document manually. To do that we will extend the DiscoveryResponseGenerator (no need to even directly implement IDiscoveryResponseGenerator interface!), and we shall override the method responsible for creating JWKS.

class DilithiumAwareDiscoveryResponseGenerator : DiscoveryResponseGenerator
{
    private readonly DilithiumCredentials _dilithiumCredentials;

    public DilithiumAwareDiscoveryResponseGenerator(
      IdentityServerOptions options, 
      IResourceStore resourceStore, 
      IKeyMaterialService keys, 
      ExtensionGrantValidator extensionGrants, 
      ISecretsListParser secretParsers, 
      IResourceOwnerPasswordValidator resourceOwnerValidator, 
      ILogger<DiscoveryResponseGenerator> logger, 
      DilithiumCredentials dilithiumCredentials)
       : base(options, resourceStore, keys, extensionGrants, secretParsers, resourceOwnerValidator, logger)
    {
        _dilithiumCredentials = dilithiumCredentials;
    }

    public override async Task<IEnumerable<JsonWebKey>> CreateJwkDocumentAsync()
    {
        var current = await base.CreateJwkDocumentAsync();
        current = current.Append(new JsonWebKey
        {
            kty = "LWE",
            kid = _dilithiumCredentials.KeyId,
            x = Base64Url.Encode(_dilithiumCredentials.PublicKey.GetEncoded()),
            alg = _dilithiumCredentials.Alg,
            use = "sig"
        });

        return current;
    }
}

We then simply call the base implementation, after which we manually add the Dilithium public key, based on our injected DilithiumCredentials. We have to make sure we set kty to LWE, that the public key is base64 URL encoded and that we publish the key ID, which the clients can use to locate the correct key for signature verification.

The constructor of our class is quite frightening, but that is just all of the stuff that the base class requires, so we only pass it over to the base. The only thing we really need is our own DilithiumCredentials instance.

Signing the token πŸ”—

Once the JWKS is taken care of, we need to add support for token signing. This can be done by extending DefaultTokenCreationService (again, no need to implement the entire ITokenCreationService manually) and overriding just a single method, CreateJwtAsync. This is a method that is responsible to converting a prepared model of a token into a JWT representation. This is shown next.

public class DilithiumCompatibleTokenCreationService : DefaultTokenCreationService
{
    private readonly DilithiumCredentials _dilithiumCredentials;
    private readonly DilithiumSigner _signer;
    private readonly JsonWebTokenHandler _handler;

    static DilithiumCompatibleTokenCreationService() {
        var defaultHeaderParameters = new List<string>()
        {
            JwtHeaderParameterNames.X5t,
            JwtHeaderParameterNames.Enc,
            JwtHeaderParameterNames.Zip
        };

        typeof(JwtTokenUtilities)
          .GetField("DefaultHeaderParameters", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)
          .SetValue(null, defaultHeaderParameters);
    }

    public DilithiumCompatibleTokenCreationService(
      ISystemClock clock, 
      IKeyMaterialService keys, 
      IdentityServerOptions options, 
      ILogger<DefaultTokenCreationService> logger, 
      DilithiumCredentials dilithiumCredentials)
       : base(clock, keys, options, logger)
    {
        _dilithiumCredentials = dilithiumCredentials;
        _signer = new DilithiumSigner();
        _signer.Init(true, _dilithiumCredentials.PrivateKey);
        _handler = new JsonWebTokenHandler { SetDefaultTimesOnTokenCreation = false };
    }

    protected override Task<string> CreateJwtAsync(Token token, string payload, Dictionary<string, object> headerElements)
    {
        if (!token.AllowedSigningAlgorithms.Contains(_dilithiumCredentials.Alg)) 
          return base.CreateJwtAsync(token, payload, headerElements);

        headerElements["kid"] = _dilithiumCredentials.KeyId;
        headerElements["alg"] = _dilithiumCredentials.Alg;

        // strip last "." as the handler generates <header>.<payload>.<empty> becasue we did not ask it to sign
        var jwt = _handler.CreateToken(payload, headerElements);
        jwt = jwt.TrimEnd('.');

        var signature = _signer.GenerateSignature(Encoding.UTF8.GetBytes(jwt));
        return Task.FromResult($"{jwt}.{Base64Url.Encode(signature)}");
    }
}

There is quite a lot going on here, so let’s unpack that. First, we will focus on the overridden method to create the JWT.

We will need to check if the current token supports being issued with Dilithium signature (more on that later), and if not, we yield back to the base implementation. In other case, we can take over the task of preparing the JWT, and do it manually.

In order to achieve this, we inject the kid and alg values into the header - based on our DilithiumCredentials instance. Next, we can actually use JsonWebTokenHandler from the Microsoft.IdentityModel.JsonWebTokens library to create the JWT. Now, while the library does not support Dilithium, we can have it create the token without any signature - we simply do not pass any signing credentials to it.

At this stage the library produces a JWT token in the format [header].[payload].. We strip the trailing dot, because the JWT specification actually requires us to sign a string [header].[payload] and sign the remainder using Dilithium’s private key. Then we can simply append the dot and the signature to the previously generated JWT and that’s it!

The class contains, like the previous one, quite a lot of constructor parameters but yet again, those are there to satisfy the base class - we only need our DilithiumCredentials.

There is one additional trick here, namely in the static constructor. Since we’d like JsonWebTokenHandler to create the unsigned JWT for us, we also have to add kid and alg properties to it. Those are normally populated during signing process and the library does not allow them to be manually set. However, since that disallow list is maintained in a static field, we can bypass that restriction by removing them from that list using reflection.

Wiring everything in πŸ”—

In order to make all these pieces work, we have to make sure we register our custom service implementations before Identity Server is bootstrapped. So typically it would be something like this:

public static WebApplication ConfigureServices(thisWebApplicationBuilder builder)
{
    builder.Services.AddSingleton<DilithiumCredentials>();
    builder.Services.AddTransient<ITokenCreationService, DilithiumCompatibleTokenCreationService>();
    builder.Services.AddTransient<IDiscoveryResponseGenerator, DilithiumAwareDiscoveryResponseGenerator>();
    
    builder.Services.AddIdentityServer()
        .AddInMemoryApiScopes(Config.ApiScopes)
        .AddInMemoryApiResources(Config.ApiResources)
        .AddInMemoryClients(Config.Clients);
    return builder.Build();
}

With such setup, Identity Server will see that both ITokenCreationService and IDiscoveryResponseGenerator are already registered and will no register its own defaults anymore.

We also have to make sure that the Dilithium signing is actually being used for the tokens - remember the check against AllowedSigningAlgorithms we had when creating the JWT?

This is done when defining API Resources. We can restrict a specific API resource (and thus a specific scope) to be only available via tokens signed with a specific algorithm. This of course makes sense, because the resource maps to a certain resource server which will have to be able to verify the signature.

An example of such setup (in memory - normally it would be in the Identity Server storage) is show below:

public static class Config
{
    public static IEnumerable<ApiScope> ApiScopes =>
       new List<ApiScope>
        {
            new ApiScope(name: "scope1", displayName: "Scope 1")
        };

    public static IEnumerable<ApiResource> ApiResources =>
       new List<ApiResource>
        {
            new ApiResource(name: "api1", displayName: "MyAPI") {
                Scopes = new HashSet<string> { "scope1" },
                AllowedAccessTokenSigningAlgorithms = new HashSet<string> { "CRYDI3" }
            }
        };

    public static IEnumerable<Client> Clients =>
        new Client[]
            {
                new Client
                {
                    ClientId = "client",
                    AllowedGrantTypes = GrantTypes.ClientCredentials,
                    ClientSecrets =
                    {
                        new Secret("secret".Sha256())
                    },
                    AllowedScopes = { "scope1" }
                }
            };
}

In this setup, the client with ID client is allowed to access scope1 which belongs to api1 resource. That resource only allows CRYDI3 signatures.

Fetching the JWKS, the token and verifying it πŸ”—

Now that everything is in place we can run our server. When accessing the JWKS endpoint at /.well-known/openid-configuration/jwks we should now see two keys being reported, the default RSA Signature with SHA-256 key and our new Dilithium key.

The output should resemble:

{
    "keys": [
        {
            "kty": "RSA",
            "use": "sig",
            "kid": "E8415AAC9E39F39F208BD8FA9F4C71CE",
            "e": "AQAB",
            "n": "<base64url encoded N>",
            "alg": "RS256"
        },
        {
            "kty": "LWE",
            "kid": "3197ECF1718F7C88BF96F35F8DF1C6FD",
            "alg": "CRYDI3",
            "x": "<base64url encoded public key>"
        }
    ]
}

So when this works as expected, we can try an fetch the token:

POST /connect/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded

client_id=client&
client_secret=secret&
grant_type=client_credentials

The response should now be the access token, signed with Dilithium, although that might not be obvious from the response yet:

{
    "access_token": "<JWT>",
    "expires_in": 3600,
    "token_type": "Bearer",
    "scope": "scope1"
}

However, we can grab the token and decode it using a JWT decoding tool such as jwt.io. This should be able to decode the header to the following value:

{
  "alg": "CRYDI3",
  "typ": "at+jwt",
  "kid": "3197ECF1718F7C88BF96F35F8DF1C6FD"
}

where the kid should match our key from the JWKS document, and the alg should be equal to CRYDI3.

The last thing to check is to make sure that the token signature can be verified using the published public key. To achieve that, let’s use a simple .NET console application that will fetch the token, fetch JWKS, select the correct key based on kid in the token and then use the Dilithium implementation locally to verify the signature.

The application code (assuming the Identity Server is running on https://localhost:5001) is shown below:

using System.IdentityModel.Tokens.Jwt;
using System.Text;
using IdentityModel;
using IdentityModel.Client;
using Org.BouncyCastle.Pqc.Crypto.Crystals.Dilithium;

var client = new HttpClient();

var req = new ClientCredentialsTokenRequest
{
    Address = "https://localhost:5001/connect/token",
    ClientId = "client",
    ClientSecret = "secret"
};

var token = await client.RequestClientCredentialsTokenAsync(req);
var parsedToken = new JwtSecurityToken(token.AccessToken);
var jsonWebKeySetResponse = await client.GetJsonWebKeySetAsync("https://localhost:5001/.well-known/openid-configuration/jwks");
var key = jsonWebKeySetResponse.KeySet.Keys.FirstOrDefault(k => k.Kid == parsedToken.Header["kid"]?.ToString()) ?? throw new Exception("No matching key found in JWKS!");

// verify signature
var signer = new DilithiumSigner();
var publicKey = new DilithiumPublicKeyParameters(DilithiumParameters.Dilithium3, Base64Url.Decode(key.X));

signer.Init(false, publicKey);

var signedPart = $"{parsedToken.RawHeader}.{parsedToken.RawPayload}";
var verified = signer.VerifySignature(Encoding.UTF8.GetBytes(signedPart), Base64Url.Decode(parsedToken.RawSignature));
Console.WriteLine($"Successfully verified? {verified}");

The output should of course be:

Successfully verified? True

The source for this blog post can be found on Github.

About


Hi! I'm Filip W., a cloud architect from ZΓΌrich πŸ‡¨πŸ‡­. I like Toronto Maple Leafs πŸ‡¨πŸ‡¦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁒󠁳󠁣󠁴󠁿.

You can find me on Github and on Mastodon.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP