Search code examples
amazon-web-services.net-corejwt.net-core-3.1amazon-kms

AWS KMS signature returns Invalid Signature for my JWT


I am trying to generate a simple JWT, using ES256 using KMS. Everything looks fine to the naked eye. But I get "Invalid signature" when I test it through jwt.io. The code is quite simple:

public async Task<string> GenerateJwt(object payload)
{
    var encodedHeader = Base64Encoder.EncodeBase64Url(JsonSerializer.Serialize(_header));
    var encodedPayload = Base64Encoder.EncodeBase64Url(JsonSerializer.Serialize(payload));

    var signatureData = Encoding.ASCII.GetBytes(encodedHeader + "." + encodedPayload);

    var signature = await _signingService.Sign(signatureData);
    var encodedSignature = Base64Encoder.ReplaceSpecialUrlCharacters(Convert.ToBase64String(signature));

    return encodedHeader + "." + encodedPayload + "." + encodedSignature;
}

The SigningService looks something like this:

public async Task<byte[]> Sign(byte[] signatureData)
{
    using var memoryStream = new MemoryStream(signatureData, 0, signatureData.Length);
    var signRequest = new SignRequest()
    {
        KeyId = _signingKeyId,
        Message = memoryStream,
        SigningAlgorithm = SigningAlgorithmSpec.ECDSA_SHA_256
    };
    SignResponse signResponse = await _keyManagementService.SignAsync(signRequest);
    return signResponse.Signature.ToArray();
}

_keyManagementService is an IAmazonKeyManagementService from AWSSDK.KeyManagementService 3.5.2.6

In KMS the key is set up like this:

  • Key Spec: ECC_NIST_P256
  • Key Usage: Sign and verify
  • Signing algorithms: ECDSA_SHA_256

Public key in KMS (using localstack)

MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEj7Cy/Gbx3jnuPdanuBSjuUmdnr7tQ/BcOytDlFoHWdNA1scc6RwNwqnNbmRE0BmwnlFNDVGxWU5oycTig8p0KQ==

Example on generated output

eyJhbGciOiJFUzI1NiJ9.eyJ0ZXN0MSI6MSwidGVzdDIiOiJ0d28iLCJ0ZXN0MyI6ZmFsc2V9.MEYCIQCiatnRhYGBKgdJj9LECe7mJ4bhhkVTvFSgpVI3Dm14pwIhAOrHAu0vqKvVwdgpAhaU7KOhiIBZdcEOuzfXrdXldCFQ

If it sign it with the built in tools (by just replacing the signatureData row above) with the same input I get it to validate in jwt.io.

var ecdsa = ECDsa.Create();
ecdsa.GenerateKey(ECCurve.NamedCurves.nistP256);
var signatureData = ecdsa.SignData(signatureData, HashAlgorithmName.SHA256);

Any input would be welcome as it feels like I've tested everything...


Solution

  • The problem was that KMS returns the signature in another format:

    DER-encoded object as defined by ANS X9.62–2005

    While the JWT should in the format R || S according to https://www.rfc-editor.org/rfc/rfc7515#appendix-A.3.1

    So what I did as to add a convert method using BouncyCastle and called that before converting it to base64url:

    private byte[] ConvertSignature(byte[] signature)
    {
        var asn1 = Asn1Object.FromByteArray(signature) as DerSequence;
        if (asn1 == null)
            return Array.Empty<byte>();
    
        if (asn1.Count < 2)
            return Array.Empty<byte>();
    
        var r = asn1[0] as DerInteger;
        var s = asn1[1] as DerInteger;
    
        if (r == null || s == null)
            return Array.Empty<byte>();
    
        return Array.Empty<byte>()
            .Concat(r.Value.ToByteArrayUnsigned())
            .Concat(s.Value.ToByteArrayUnsigned())
            .ToArray();
    }