Supporting Custom JWT Signing Algorithms (ES256K) in .NET Core

Scott Brady
Scott Brady
C#

Sometimes you need to use an algorithm that your goto libraries do not support. Whether it’s because your platform’s cryptography libraries don’t implement it yet or because a particular client library doesn’t support it, sometimes you need to go off piece.

In this article, we’re going to look at how to do that when using the Microsoft.IdentityModel JWT libraries, using ES256K as our custom signing algorithm. Example code will both generate and verify a JWT signature.

Adding Support for a Custom Algorithm using ICryptoProvider

To add support for our custom algorithm, we need to create three custom implementations:

ICryptoProvider

Our implementation of ICryptoProvider will be way of modifying behaviour in Microsoft.IdentityModel.Tokens.CryptoProviderFactory, which is responsible for choosing the correct signature provider to use for token signature creation or validation.

SignatureProvider

Our SignatureProvider will be responsible for the actual signing and verification of tokens. It will understand how to take the bytes provided to it and either create a signature or to validate the signature.

SecurityKey

We’ll also be creating a SecurityKey. This object will store our key data for the SignatureProvider to use, such as X509SecurityKey, and could be something custom or built upon existing keys, such as AsymmetricSecurityKey.

ES256K – Our Custom Algorithm

Our custom algorithm for this example is going to be ES256K. ES256K is similar to ES256 (without the K), but it uses the secp256k1 curve as opposed to NIST’s P-256 curve (secp256r1). ES256K still uses SHA-256 as its hashing algorithm.

Curve secp256k1 is popular with the cryptocurrency community (both Bitcoin and Ethereum use it), but it is also in use by FIDO2 (WebAuthn and CTAP2), which makes it worth our time.

At the time of writing, registration of ES256K in the IANA JOSE and COSE registries is in progress.

Test Key

To give us some valid data to play with, I used Nimbus JOSE + JWT to create a key suitable for ES256K. This key has the following base64url encoded d, x & y parameters (d for signature generation, x & y for validation):

d: e8HThqO0wR_Qw4pNIb80Cs0mYuCSqT6BSQj-o-tKTrg
x: A3hkIubgDggcoHzmVdXIm11gZ7UMaOa71JVf1eCifD8
y: ejpRwmCvNMdXMOjR2DodOt09OLPgNUrcKA9hBslaFU0

Otherwise, you could use OpenSSL to generate a key using the secp256k1 curve.

Setup

Currently, .NET does not support ES256K or the secp256k1 curve, which means we’ll be using Bouncy Castle. You can create custom curves in .NET, however I had no luck getting that to work with secp256k1 (this may just be my bad).

So, for this example, install the .NET Standard version of Bouncy Castle as well as the required Microsoft.IdentityModel library:

install-package Portable.BouncyCastle
install-package Microsoft.IdentityModel.JsonWebTokens

Creating a Custom SecurityKey

For our SecurityKey, we’re going to extend AsymmetricSecurityKey:

public class BouncyCastleEcdsaSecurityKey : AsymmetricSecurityKey
{
    public BouncyCastleEcdsaSecurityKey(ECKeyParameters keyParameters)
    {
        KeyParameters = keyParameters;
        CryptoProviderFactory.CustomCryptoProvider = new CustomCryptoProvider();
    }

    public ECKeyParameters KeyParameters { get; }
    public override int KeySize => throw new NotImplementedException();

    [Obsolete("HasPrivateKey method is deprecated, please use PrivateKeyStatus.")]
    public override bool HasPrivateKey => KeyParameters.IsPrivate;

    public override PrivateKeyStatus PrivateKeyStatus 
        => KeyParameters.IsPrivate ? PrivateKeyStatus.Exists : PrivateKeyStatus.DoesNotExist;
}

This SecuityKey has a constructor that accepts ECPublicKeyParameters or ECPrivateKeyParameters and assigns these parameters to a property for later use. It also sets the key’s CustomCryptoProvider to our custom implementation of ICryptoProvider (which you’ll see shortly). You can also set the Key ID if necessary.

For this example, I’ve not put too much effort into the private key methods, but I’ve stubbed out some of it out so that you know what you’re dealing with.

Creating a Custom ICryptoProvider

For our implementation of ICryptoProvider, we need to be able to understand what algorithms we support, and, if we can handle the current algorithm, be able to send the JWT validation logic off to the right place (our upcoming custom SignatureProvider).

public class CustomCryptoProvider : ICryptoProvider
{
    public bool IsSupportedAlgorithm(string algorithm, params object[] args) 
        => algorithm == "ES256K";

    public object Create(string algorithm, params object[] args)
    {
        if (algorithm == "ES256K"
            && args[0] is BouncyCastleEcdsaSecurityKey key)
        {
            return new CustomSignatureProvider(key, algorithm);
        }

        throw new NotSupportedException();
    }

    public void Release(object cryptoInstance)
    {
        if (cryptoInstance is IDisposable disposableObject)
            disposableObject.Dispose();
    }
}

In our implementation we are only supporting ES256K, but it could handle multiple algorithms. Just remember that only one implementation of ICryptoProvider can be used at a time.

Creating a Custom CustomSignatureProvider

And now for the signature validation itself with our custom implementation of SignatureProvider:

public class CustomSignatureProvider : SignatureProvider 
{
    public CustomSignatureProvider(BouncyCastleEcdsaSecurityKey key, string algorithm) 
        : base(key, algorithm) { }

    protected override void Dispose(bool disposing) { }

    public override byte[] Sign(byte[] input)
    {
        var ecDsaSigner = new ECDsaSigner();
        BouncyCastleEcdsaSecurityKey key = Key as BouncyCastleEcdsaSecurityKey;
        ecDsaSigner.Init(true, key.KeyParameters);

        byte[] hashedInput;
        using (var hasher = SHA256.Create())
        {
            hashedInput = hasher.ComputeHash(input);
        }

        var output = ecDsaSigner.GenerateSignature(hashedInput);

        var r = output[0].ToByteArrayUnsigned();
        var s = output[1].ToByteArrayUnsigned();

        var signature = new byte[r.Length + s.Length];
        r.CopyTo(signature, 0);
        s.CopyTo(signature, r.Length);

        return signature;
    }

    public override bool Verify(byte[] input, byte[] signature)
    {
        var ecDsaSigner = new ECDsaSigner();
        BouncyCastleEcdsaSecurityKey key = Key as BouncyCastleEcdsaSecurityKey;
        ecDsaSigner.Init(false, key.KeyParameters);

        byte[] hashedInput;
        using (var hasher = SHA256.Create())
        {
            hashedInput = hasher.ComputeHash(input);
        }

        var r = new BigInteger(1, signature.Take(32).ToArray());
        var s = new BigInteger(1, signature.Skip(32).ToArray());

        return ecDsaSigner.VerifySignature(hashedInput, r, s);
    }
}

The Final Product

Bringing it all together, lets load in our key, generate a JWT with our newly supported algorithm, and then validate it.

X9ECParameters secp256k1 = ECNamedCurveTable.GetByName("secp256k1");
ECDomainParameters domainParams = new ECDomainParameters(
    secp256k1.Curve, secp256k1.G, secp256k1.N, secp256k1.H, secp256k1.GetSeed());

byte[] d = Base64UrlEncoder.DecodeBytes("e8HThqO0wR_Qw4pNIb80Cs0mYuCSqT6BSQj-o-tKTrg");
byte[] x = Base64UrlEncoder.DecodeBytes("A3hkIubgDggcoHzmVdXIm11gZ7UMaOa71JVf1eCifD8");
byte[] y = Base64UrlEncoder.DecodeBytes("ejpRwmCvNMdXMOjR2DodOt09OLPgNUrcKA9hBslaFU0");

var handler = new JsonWebTokenHandler();

var token = handler.CreateToken(new SecurityTokenDescriptor
{
    Issuer = "me",
    Audience = "you",
    SigningCredentials = new SigningCredentials(
        new BouncyCastleEcdsaSecurityKey(
            new ECPrivateKeyParameters(new BigInteger(1, d), domainParams)) {KeyId = "123"},
        "ES256K")
});

var point = secp256k1.Curve.CreatePoint(
    new BigInteger(1, x),
    new BigInteger(1, y));

var result = handler.ValidateToken(
    token,
    new TokenValidationParameters
    {
        ValidIssuer = "me",
        ValidAudience = "you",
        IssuerSigningKey = new BouncyCastleEcdsaSecurityKey(
            new ECPublicKeyParameters(point, domainParams)) {KeyId = "123"}
    });

Thanks to our custom ICryptoProvider, the CryptoProviderFactory in SigningCredentials and TokenValidationParameters will be able to handle our custom signing algorithm and successfully validate the incoming token.

Our JWT should look something like:

eyJhbGciOiJFUzI1NksiLCJraWQiOiIxMjMiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJ5b3UiLCJpc3MiOiJtZSIsImV4cCI6MTU3NjQ0Mzg2NS4wLCJpYXQiOjE1NzY0NDAyNjUsIm5iZiI6MTU3NjQ0MDI2NX0.nngSyreix-Ri0H1lC4PRGYLNEktMDUag22VmSYe_SRJFd_Oh-Qag1XSLr1Pq0puym8KSVVuPYCzIh5rsuAFH6g

Which decodes to:

{
  "alg": "ES256K",
  "kid": "123",
  "typ": "JWT"
}
{
  "aud": "you",
  "iss": "me",
  "exp": 1576443865,
  "iat": 1576440265,
  "nbf": 1576440265
}

Source Code

You can find the full source code for this sample on GitHub in a simple console app.