Create JSON Web Tokens with signatures by ECDSA_SHA algorithms signed by AWS KMS keys with python

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 is encoded_payload.
  • Let the text: {encoded_header}.{encoded_payload} as message (Just combine them by .)
  • Sign the value of message with the private key, and let it be as signature.
  • Base64 Url Safe Encoding of signatature, and call it encoded_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.


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

Pocket