JWT signature with AWS KMS
This post explains how you can generate a JWT signature using keys managed by AWS KMS. If you like to see the code first, head to jwt-kms-poc. The code is a port of jwt-poc which is the code repository for JSON Web Token (JWT) Cheat Sheet for Java.
I assume you have basic knowledge of JWT. I will head to the signature part. A JWT has three parts: header, payload, and signature. It looks like header.payload.signature
. The header and payload are base64url
encoded. The signature verifies the authenticity and integrity of the information in the payload.
Signature Algorithm
To create the signature, you need an algorithm. Here are the recommended/required algorithms from rfc7518:
- HMAC using SHA-256. Symmetric algorithm.
- RSASSA-PKCS1-v1_5 using SHA-256. Asymmetric algorithm.
- ECDSA using P-256 and SHA-256. Asymmetric algorithm.
HMAC, a symmetric algorithm, is not recommended for JWT signatures. We are going to use an asymmetric algorithm RSASSA-PKCS1-v1_5 using SHA-256
. You can modify the example to use ECDSA using P-256 and SHA-256
. Check this StackExchange question on why you need to choose an asymmetric algorithm.
Keys Generation
An asymmetric algorithm uses a private key for signing and a public key for verification. Only issuers can access a private key, while the public key can be shared. You can generate these keys using a library like OpenSSL or a key management system like AWS KMS or Google KMS. There are several advantages of using a KMS. Some are:
- Fine-grained access control to who can access the private key, sign payload, verify payload, etc. Check AWS KMS permissions.
- Montor key access.
- Minimise attack surface.
To generate keys in AWS, follow the instructions here. Select Sign and verify
as Key usage
, and RSA_2048
as Key spec
. Note down the Key Id.
Signature Generation
The signature is generated by signing header.payload
. Check this for how to arrive at this. In the header, alg
the value is RS256
. Change the alg
value according to the algorithm you used. For ECDSA, using P-256 and SHA-256, it is ES256
.
Once you have header.payload
you can invoke Sign API using SDK in your favorite language. Here is a sample code in Kotlin,
fun sign(headerBytes: ByteArray, payloadBytes: ByteArray): String {
val contentBytes = headerBytes + '.'.code.toByte() + payloadBytes
val signRequest = SignRequest.builder()
.keyId(signVerifyKeyId)
.messageType(MessageType.RAW)
.message(SdkBytes.fromByteArray(contentBytes))
.signingAlgorithm(SigningAlgorithmSpec.RSASSA_PKCS1_V1_5_SHA_256)
.build()
val signResponse = kmsClient.sign(signRequest)
return base64UrlEncoder.encodeToString(signResponse.signature().asByteArray())
}
Verification
Invoke Verify API with header.payload
and signature
. Here is a sample code,
//Verify signature with KMS
val signableContent = "${jwt.header}.${jwt.payload}".toByteArray(StandardCharsets.UTF_8)
val signature = base64UrlDecoder.decode(jwt.signature)
val kmsVerifyRequest = VerifyRequest.builder()
.keyId(signVerifyKeyId)
.message(SdkBytes.fromByteArray(signableContent))
.signature(SdkBytes.fromByteArray(signature))
.signingAlgorithm(SigningAlgorithmSpec.RSASSA_PKCS1_V1_5_SHA_256)
.build()
kmsClient.verify(kmsVerifyRequest)