Using AWS KMS via the CLI with RSA Keys for Message Signing

Off the back of local-kms, I've been getting a few questions regarding how to interact with it via the CLI. So here are a few examples of how you can use AWS KMS (or local-kms) via the CLI.

The examples here focus on demonstrating how to use AWS KMS, not as examples of how to perform 'good' encryption. Please don't use these snippets in production systems unless you know what you're doing.

Related articles

What do RSA Asymmetric Keys support in KMS?

AWS KMS supports both message signing and encryption operations using RSA keys; however a single key can only support one of these two operations. RSA keys can be created with a key size of 2048, 3072 or 4096 bits.

Using KMS from the CLI

Generating an new RSA CMK


aws kms create-key \
--key-usage SIGN_VERIFY \
--customer-master-key-spec RSA_2048
    

You must select the --key-usage at the point of creation. You will only be able to use the generated key for the selected usage. You cannot change this once the create has been created.

This returns the key details:


{
    "KeyMetadata": {
        "AWSAccountId": "111122223333",
        "KeyId": "5211d781-06f0-4062-82a1-0b5e8a99c468",
        "Arn": "arn:aws:kms:eu-west-2:111122223333:key/5211d781-06f0-4062-82a1-0b5e8a99c468",
        "CreationDate": 1591088481.619,
        "Enabled": true,
        "Description": "",
        "KeyUsage": "SIGN_VERIFY",
        "KeyState": "Enabled",
        "Origin": "AWS_KMS",
        "KeyManager": "CUSTOMER",
        "CustomerMasterKeySpec": "RSA_2048",
        "SigningAlgorithms": [
            "RSASSA_PKCS1_V1_5_SHA_256",
            "RSASSA_PKCS1_V1_5_SHA_384",
            "RSASSA_PKCS1_V1_5_SHA_512",
            "RSASSA_PSS_SHA_256",
            "RSASSA_PSS_SHA_384",
            "RSASSA_PSS_SHA_512"
        ]
    }
}
    

All RSA key specs support multiple Signing Algorithms (unlike ECC keys). An explanation of them can be found in AWS' documentation here: RSA key specs for signing and verification

Signing a message

In all cases, when KMS is signing a message, it is in fact always signing the digest of that message, generated via a SHA hash function.

KMS supports two options for generating the digest of a message - you can generate it yourself in advance or, if your message is less than or equal to 4096 bytes, you can have KMS generate the digest for you.

Having KMS generate the digest for you (--message-type RAW)

To have KMS sign the message Hello, you can call:


aws kms sign \
--key-id 5211d781-06f0-4062-82a1-0b5e8a99c468 \
--signing-algorithm RSASSA_PSS_SHA_384 \
--message Hello
    

Note that we must pass a signing algorithm that was listed under SigningAlgorithms when the key was created. KMS will automatically hash our message using SHA-384 for us in this example.

This returns


{
    "KeyId": "arn:aws:kms:eu-west-2:111122223333:key/5211d781-06f0-4062-82a1-0b5e8a99c468",
    "Signature": "sgZbDf3jCCU0dbbYucn+YXOCX7UVCTMb+13OVcNvOLeo36IueNu6v7zUYtSjd4B7cAaprgyQ6wiOD+St2q6R9Ggbnhm3SGWKQdMNu1FC8+MK8yQRLlnX6CtYZDnAaosGqh7yBZHi8625Maj0sqC8I8Lel6f0s4fthBAfaJyuD+i53LHOTdCDNnHbbLMHCDtDjZO1j0XpYB9eIkeaq4d7YDjpNjqF7LzHO2pA6EDm7HlJZHGwKrZ02laEjtm2LYajlqHBvZCDeNfiW6l88ZugtENOKO4s/ti8+VSGRbTgh2eRCjgyC5ZGMaHiuN6andcgHJyfdxPmK0oKMYDoUIzwRQ==",
    "SigningAlgorithm": "RSASSA_PSS_SHA_384"
}

The message's signature is returned, along with the Key ARN and the Signing Algorithm. The Signature is always returned as a base64 encoded string. i.e. the AWS CLI does not base64 decode it for us.

It's important to note that you'll need to pass the KeyId and SigningAlgorithm when Verifying the message with KMS, so keep a note of these too. It's especially important to record the SigningAlgorithm used with RSA keys as, unlike ECC keys, it cannot be derived from the KeyId.

You can save the signature directly to a file using:


aws kms sign \
--key-id 5211d781-06f0-4062-82a1-0b5e8a99c468 \
--signing-algorithm RSASSA_PSS_SHA_384 \
--message Hello \
--output text --query Signature | base64 --decode > signature-raw.sign
    

The reason we base64 decode the message here is that when passing it back to KMS for verification, the AWS CLI expects it unencoded.

Generating the digest yourself (--message-type DIGEST)

We need to perform the extra hashing step ourselves in this context. This has two advantages:

  • There's no KMS imposed limit on the size of the message/file; as opposed to the 4096 byte limit when KMS performs the hashing.
  • If the message itself is sensitive, it never needs to be sent to AWS.

When we hash the message ourselves, we must match the hash size with that of the signing algorithm we wish to use.

  • RSASSA_*_SHA_256 --> SHA-256
  • RSASSA_*_SHA_384 --> SHA-384
  • RSASSA_*_SHA_512 --> SHA-512

So for example, we can use OpenSSL to hash our message with:


echo -n Hello | openssl dgst -sha384
    

Which gives us:


3519fe5ad2c596efe3e276a6f351b8fc0b03db861782490d45f7598ebd0ab5fd5520ed102f38c4a5ec834e98668035fc
    

The AWS CLI actually wants the digest in unencoded binary however, so we can do:


echo -n Hello | openssl dgst -sha384 -binary > message.sha384
    

We can then sign the message similarly to how we did before:


aws kms sign \
--key-id 5211d781-06f0-4062-82a1-0b5e8a99c468 \
--signing-algorithm RSASSA_PSS_SHA_384 \
--message-type DIGEST \
--message fileb://message.sha384
    

In this instance we must specify that the message type as DIGEST, and we pass the binary digest in from the pre-generated file as the message.

This gives us:


{
    "KeyId": "arn:aws:kms:eu-west-2:111122223333:key/5211d781-06f0-4062-82a1-0b5e8a99c468",
    "Signature": "o2rp8HioAuih5dbSHTRxrTFzEUULx/mZr+xOUUpR08gW/iSb3hUbDrml+M7noXvxOqb3LeDd4EdEER3RbCaqHEq2XJw+HwYZ7SYNcinda4AUnp7EsQDbsJNPJr/bDnFC26CrCpVinHYKgzVdYrkOZ84p08yRrAR0i3hQYET3pIexcKDyCGfGNX1QSkTgVoLsqw6vixTau8sNcyRI0WWVwmdHP5q7/4CBw7b7Xs7MyRjoCS6vxGkFJvARUfDop6eEH7jHlyRPjgpTUJRheEjr0BNdLjXL0KMt3+eEFbLiaysmV8zPqfe6nJFRiRFspRQshbMVWCd7WYziPGe0F7sPhw==",
    "SigningAlgorithm": "RSASSA_PSS_SHA_384"
}
    

As before we can generate the signature and save it directly to a file:


aws kms sign \
--key-id 5211d781-06f0-4062-82a1-0b5e8a99c468 \
--signing-algorithm RSASSA_PSS_SHA_384 \
--message-type DIGEST \
--message fileb://message.sha384 \
--output text --query Signature | base64 --decode > signature-digest.sign
    

Verifying a message's signature

Having KMS generate the digest for you (--message-type RAW)

To verify a message we must pass the same KeyId, Signing Algorithm and Message passed when we signed the message, plus the signature that was returned:


aws kms verify \
--key-id 5211d781-06f0-4062-82a1-0b5e8a99c468 \
--signing-algorithm RSASSA_PSS_SHA_384 \
--message Hello \
--signature fileb://signature-raw.sign
    

KMS will then verify that the signature passed is valid for the given combination of Key and Message:


{
    "KeyId": "arn:aws:kms:eu-west-2:111122223333:key/5211d781-06f0-4062-82a1-0b5e8a99c468",
    "SignatureValid": true,
    "SigningAlgorithm": "RSASSA_PSS_SHA_384"
}
    

If the verification fails for any reason, KMS throws a KMSInvalidSignatureException.

With a self generated digest (--message-type DIGEST)

This works exactly as the RAW message type, other than we set --message-type DIGEST, and we pass in the binary message digest.


aws kms verify \
--key-id 5211d781-06f0-4062-82a1-0b5e8a99c468 \
--signing-algorithm RSASSA_PSS_SHA_384 \
--message-type DIGEST \
--message fileb://message.sha384 \
--signature fileb://signature-digest.sign
    

{
    "KeyId": "arn:aws:kms:eu-west-2:111122223333:key/5211d781-06f0-4062-82a1-0b5e8a99c468",
    "SignatureValid": true,
    "SigningAlgorithm": "RSASSA_PSS_SHA_384"
}
    

Exporting and using the Public Key

Whilst there is no way to export the Private Key from KMS, you can export the Public Key and use it to verify messages locally that have been signed by KMS.

To export the public key we use:


aws kms get-public-key \
--key-id 5211d781-06f0-4062-82a1-0b5e8a99c468
    

Which gives us:


{
    "KeyId": "arn:aws:kms:eu-west-2:111122223333:key/5211d781-06f0-4062-82a1-0b5e8a99c468",
    "PublicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1q5Pb4c+6eJe+iwkaC8V3ZLrJdZrf6xm+8+OApl25tjAh5ewj8OebriPM/8LzHDKJ66pekGR2AAv/PHcmIPkmJPui5iGxjpvJZrS+Yzwg4jBb3da5wqenZ/xYpw6aIZv1AI7F2vWUtS6Om/4MiZEr0AarA5UC05rEN9GdJGB25Rkozfh+Okqn1vDz9cnMUUdPDN4fJF4r+Qno/e0Jk8tN69ofkbx2IWCFPcy/SyeSnoKyJ4ebdTz70rbijdTYxp4XZ2z7nJsGgmU827y/ntmDJYyYrYpIiz/ZIqHS8zynhkci5l87n032OJCizHfIQ/stGuWlGSzhbQtoJgA9lFeMwIDAQAB",
    "CustomerMasterKeySpec": "RSA_2048",
    "KeyUsage": "SIGN_VERIFY",
    "SigningAlgorithms": [
        "RSASSA_PKCS1_V1_5_SHA_256",
        "RSASSA_PKCS1_V1_5_SHA_384",
        "RSASSA_PKCS1_V1_5_SHA_512",
        "RSASSA_PSS_SHA_256",
        "RSASSA_PSS_SHA_384",
        "RSASSA_PSS_SHA_512"
    ]
}
    

PublicKey here is a DER-encoded X.509 public key, base64 encoded.

We can save this to a file using:


aws kms get-public-key \
--key-id 5211d781-06f0-4062-82a1-0b5e8a99c468 \
--output text --query PublicKey | base64 --decode > key.der
    

Many applications for verifying signatures support DER encoded keys, however if needed you can convert it to a PEM encoding using:


openssl rsa -inform DER -in key.der -pubin > key.pem
    

Verifying locally with OpenSSL

If you used KMS to generate the message's digest originally, you will now need to generate a version yourself. OpenSSL's dgst function directly supports generating a digest and verifying it in a single command. There are two variations, one for each RSA padding option, as specific in the signing algorithm.

For RSASSA_PSS_SHA_384:


echo -n Hello | openssl dgst -sha384 -sigopt rsa_padding_mode:pss -keyform DER -verify key.der -signature signature-raw.sign
    

For RSASSA_PKCS1_V1_5_SHA_384:


echo -n Hello | openssl dgst -sha384 -sigopt rsa_padding_mode:pkcs1 -keyform DER -verify key.der -signature signature-raw.sign
    

If you generated the digest yourself originally, and still have it, you can use OpenSSL's pkeyutl function.

For RSASSA_PSS_SHA_384:


openssl pkeyutl -verify -pubin -keyform DER -inkey key.der \
-pkeyopt digest:sha384 -pkeyopt rsa_padding_mode:pss \
-in message.sha384 -sigfile signature-digest.sign
    

For RSASSA_PKCS1_V1_5_SHA_384:


openssl pkeyutl -verify -pubin -keyform DER -inkey key.der \
-pkeyopt digest:sha384 -pkeyopt rsa_padding_mode:pkcs1 \
-in message.sha384 -sigfile signature-digest.sign