Hello, this is Daiki Tanaka. I'm a member of CTO Office, AI Forward Division. This page describes things to keep in mind when you create JSON Web Tokens (JWT) with signatures using ECDSA_SHA algorithms with AWS KMS keys. Sample codes here are all writen in python 3.8.
JSON Web Token
JSON Web Token (JWT) is a standard that defines a method for signing and encrypting JSON data.
As a practical use case, let's consider a request from a client to an API server.
The followings are the steps needed when requesting to the API server with JWT. Here, we assume the API user has already created a public/private key pair. 1. Send the public key to the API server administrator in advance. 2. When the cluent makes a request, the private key is used to sign the request and create a JWT. 3. Set the created JWT in the header and make a request to the API. 4. The API server receives the request and verifies the validity of the signature using the public key. 5. If the signature is valid, the request is processed and the response is returned to the client from the API.
How to create JWT
Here is a brief introduction to the procedure for creating a JWT. Basically, it is based on simple string conversion and concatenation.
- Prepare Header and Payload in JSON format
- Encode Header and Payload with Base64 Url Safe Encoding
- Encoded Header is
encoded_header
and encoded Payload isencoded_payload
.
- Encoded Header is
- Let the text:
{encoded_header}.{encoded_payload}
asmessage
(Just combine them by.
) - Sign the value of
message
with the private key, and let it be assignature
. - Base64 Url Safe Encoding of
signatature
, and call itencoded_signature
. - Text:
{encoded_header}.{enoded_payload}.{encoded_signature}
is JWT.
Signatures using AWS KMS keys
For example, keys for JWT can be prepared in a local environment as follows.
openssl ecparam -genkey -name prime256v1 -out key-pair.pem # public key openssl ec -in key-pair.pem -outform PEM -pubout -out public.pem # private key openssl ec -in key-pair.pem -outform PEM -out private.pem
On the other hand, AWS provides a service called Key Management Service (KMS), which generates and manages keys on AWS. With this service, key issuance and usage can be managed on AWS.
We can create signatures with the private key like:
import boto3 kms_client = boto3.client("kms") signature: bytes = kms_client.sign( KeyId="KMS_KEY_ARN", Message="Message to sign".encode(), SigningAlgorithm="ECDSA_SHA_256", MessageType="RAW" )["Signature"]
We don't need to think about managing keys with KMS. We just need to manage key access permissions by IAM policies, so if you are using AWS, I think it's a good option to create keys in KMS.
Creation of JWT with signatures with KMS key
JWT can be created using a signature created with a KMS key like:
import time import json import base64 def create_jwt() -> str: issued_at = int(time.time()) expiration_time = issued_at + 3600 header = {"alg": "RS256", "typ": "JWT"} payload = {"iat": issued_at, "exp": expiration_time} token_components = { "header": base64.urlsafe_b64encode(json.dumps(header).encode()).decode().rstrip("="), "payload": base64.urlsafe_b64encode(json.dumps(payload).encode()).decode().rstrip("="), } kms_client = boto3.client("kms") signature: bytes = kms_client.sign( KeyId="KMS_KEY_ARN", Message=message.encode(), SigningAlgorithm="RSASSA_PKCS1_V1_5_SHA_256", MessageType="RAW" )["Signature"] token_components["signature"] = base64.urlsafe_b64encode(signature).decode().rstrip("=") return f'{token_components["header"]}.{token_components["payload"]}.{token_components["signature"]}'
Notes on signing with KMS Key using elliptic curve cryptography
If you use the RSA cryptosystem, you can create a JWT as above, but if you use an elliptic curve cryptosystem (ECDSA_SHA_256
, ECDSA_SHA_384
, or ECDSA_SHA_512
), the signature is encoded with the DER method. For now, the following key specifications employ elliptic curve cryptosystem method (For more details, please see the official document):
- ECC_NIST_P256 (secp256r1)
- ECC_NIST_P384 (secp384r1)
- ECC_NIST_P521 (secp521r1)
Creating a JWT using such DER-encoded signatures will result in authentication errors. Therefore, we must first DER-decode the DER-encoded signature from KMS, extract the necessary information, and then create the signature which is valid for JWT.
In elliptic curve cryptography, a signature is equivalent to two positive integers (R, S). But in JWT, a concatenation of these integers in byte sequence must be specified as the signature. (For more information, please see: https://www.rfc-editor.org/rfc/rfc7515#appendix-A.3.1)
Thus, we need to add the following operations: - Decodes DER-encoded signatures signed by KMS - Obtain a positive integer pair (R, S) from the decoded byte sequence - Concatenate the R and S byte sequences - Encode it with BASE64URLSafeEncoding
For DER decoding, you can use libraries such as python-asn1
for python.
Based on these, modify the previous function:
import time import json import base64 import asn1 def convert_signature(sig: bytes, key_bytes: int = 256) -> bytes: """ Convert DER-decoded signature into R || S format defined in JWT spec. https://www.rfc-editor.org/rfc/rfc7515#appendix-A.3.1 :param sig: The DER-encoded signature in bytes :param key_bytes: The number of bytes for the key :return: """ decoder = asn1.Decoder() decoder.start(sig) # DER decoded signature is type:Constructed decoder.enter() # read (r, s) _, r = decoder.read() # the first one represents r _, s = decoder.read() # the second one represents s # Convert int to bytes (big-endian) r_bytes: bytes = r.to_bytes(int(key_bytes / 8), "big") s_bytes: bytes = s.to_bytes(int(key_bytes / 8), "big") # Combine r_bytes and s_bytes return b"".join([r_bytes, s_bytes]) def create_jwt() -> str: issued_at = int(time.time()) expiration_time = issued_at + 3600 header = {"alg": "ES256", "typ": "JWT"} payload = {"iat": issued_at, "exp": expiration_time} token_components = { "header": base64.urlsafe_b64encode(json.dumps(header).encode()).decode().rstrip("="), "payload": base64.urlsafe_b64encode(json.dumps(payload).encode()).decode().rstrip("="), } kms_client = boto3.client("kms") signature: bytes = kms_client.sign( KeyId="KMS_KEY_ARN", Message=message.encode(), SigningAlgorithm="ECDSA_SHA_256", MessageType="RAW" )["Signature"] # Convert DER-encoded signater into R || S signature token_components["signature"] = base64.urlsafe_b64encode(convert_signature(signature)).decode().rstrip("=") return f'{token_components["header"]}.{token_components["payload"]}.{token_components["signature"]}'
Here we have added a function convert_signature()
that DER-decodes a DER-encoded signature signed by the KMS key, and converts it to a valid signature format for JWT. Just apply this function to the DER-encoded signature before Base64 Url Safe Encoding the signature.
You can verify created JWT using the public key with the library PyJWT
as:
import jwt jwt_token = create_jwt() # create JWT using the above create_jwt(). public_key = "INSERT_YOUR_PUBLIC_KEY" decoded_data = jwt.decode(jwt_token, public_key, algorithms=["ES256"])
Conclusion
We've discussed the points to be considered when creating a JWT with a signature by kms key using the elliptic curve cryptosystem method. Since DER-encoded signatures were returned by KMS, they had to first be DER-decoded and converted into the valid format for JWT. I have explained the way to address that issue with code examples in python.
References
You can also refer to the following useful links.
- https://stackoverflow.com/questions/69619620/convert-aws-kms-ecdsa-sha-256-signature-from-der-encoded-ans-1-format-to-jwt-bas
- A StackOverFlow question about how to convert signatures using the elliptic curve cryptosystem method.
- https://stackoverflow.com/questions/66170120/aws-kms-signature-returns-invalid-signature-for-my-jwt?rq=1
- A StackOverFlow question about how to convert signatures using the elliptic curve cryptosystem method.
- https://github.com/sufiyanghori/Python-Asymmetric-JWT-Signing-using-AWS-KMS
- https://github.com/matelang/jwt-go-aws-kms
- Sample code for Go to create JWT using signatures signed with a KMS key with ECDSA_SHA or RSA-SHA algorithms
Money Forward is looking for an engineer. We look forward to hearing from you.
【会社情報】 ■Wantedly ■株式会社マネーフォワード ■福岡開発拠点 ■関西開発拠点(大阪/京都)
【SNS】 ■マネーフォワード公式note ■Twitter - 【公式】マネーフォワード ■Twitter - Money Forward Developers ■connpass - マネーフォワード ■YouTube - Money Forward Developers