Using AWS KMS via the CLI with Elliptic Curve (ECC) Keys

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 Elliptic Curve Asymmetric Keys support in KMS?

AWS KMS supports message signing and verification operations using an Elliptic Curve key. You can pick between using National Institute of Standards and Technology (NIST) curves with a key size of 256, 384 or 521 bits. Or the secp256k1 curve, commonly known for its use in Bitcoin.

AWS KMS does not currently support encryption / decryption operations with Elliptic Curve keys.

Using KMS from the CLI

Generating an new ECC CMK


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

Note that whilst --key-usage SIGN_VERIFY is the only valid option, it must still be included.

This returns the key details:


{
    "KeyMetadata": {
        "AWSAccountId": "111122223333",
        "KeyId": "024f6b2c-365a-4d86-af2e-9cc5c468afba",
        "Arn": "arn:aws:kms:eu-west-2:111122223333:key/024f6b2c-365a-4d86-af2e-9cc5c468afba",
        "CreationDate": 1590507285,
        "Enabled": true,
        "KeyUsage": "SIGN_VERIFY",
        "KeyState": "Enabled",
        "Origin": "AWS_KMS",
        "KeyManager": "CUSTOMER",
        "CustomerMasterKeySpec": "ECC_NIST_P256",
        "SigningAlgorithms": [
            "ECDSA_SHA_256"
        ]
    }
}
    

We can see that KMS has assigned one, and only one, Signing Algorithm. This algorithm is the only valid option when using the key to sign a message. Each ECC Key Spec maps to the following signing algorithm:

  • ECC_NIST_P256 --> ECDSA_SHA_256
  • ECC_NIST_P384 --> ECDSA_SHA_384
  • ECC_NIST_P521 --> ECDSA_SHA_512
  • ECC_SECG_P256K1 --> ECDSA_SHA_256

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 024f6b2c-365a-4d86-af2e-9cc5c468afba \
--signing-algorithm ECDSA_SHA_256 \
--message Hello
    

The message here is passed in plain text. i.e. not base64 encoded, as the AWS CLI will encode it for us.

Note that I've used the key generated earlier on this page, thus the key spec is ECC_NIST_P256, so I've therefore used the ECDSA_SHA_256 signing algorithm. KMS will automatically hash our message using SHA-256 for us.

This returns


{
    "KeyId": "arn:aws:kms:eu-west-2:111122223333:key/024f6b2c-365a-4d86-af2e-9cc5c468afba",
    "Signature": "MEUCIQDQ1CFlwej64aSAcPtgPBDuVS+HlFSxRfs4CP1O7AhxtgIgX7NHGVyy3hTKP1+jKlw98vQinb7nJ3tAGs8GnldVRu0=",
    "SigningAlgorithm": "ECDSA_SHA_256"
}
    

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.

You can save the signature directly to a file using:


aws kms sign \
--key-id 024f6b2c-365a-4d86-af2e-9cc5c468afba \
--signing-algorithm ECDSA_SHA_256 \
--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 use the correct hashing algorithm. For example, as our key spec is ECC_NIST_P256, our signing algorithm in KMS is therefore ECDSA_SHA_256, so we must hash our message using SHA-256.

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


echo -n Hello | openssl dgst -sha256
    

Which gives us:


185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969
    

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


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

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


aws kms sign \
--key-id 024f6b2c-365a-4d86-af2e-9cc5c468afba \
--signing-algorithm ECDSA_SHA_256 \
--message-type DIGEST \
--message fileb://message.sha256
    

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/024f6b2c-365a-4d86-af2e-9cc5c468afba",
    "Signature": "MEYCIQCBQfufXvIvz0Nis9pdE+8IWI1rEwYwG68KMSl6yLJYlgIhAKipwa6Yy9kuHpCr10ugh1nAhPyRTnrcYtVd7QvFblhV",
    "SigningAlgorithm": "ECDSA_SHA_256"
}
    

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


aws kms sign \
--key-id 024f6b2c-365a-4d86-af2e-9cc5c468afba \
--signing-algorithm ECDSA_SHA_256 \
--message-type DIGEST \
--message fileb://message.sha256 \
--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 024f6b2c-365a-4d86-af2e-9cc5c468afba \
--signing-algorithm ECDSA_SHA_256 \
--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/024f6b2c-365a-4d86-af2e-9cc5c468afba",
    "SignatureValid": true,
    "SigningAlgorithm": "ECDSA_SHA_256"
}
    

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 024f6b2c-365a-4d86-af2e-9cc5c468afba \
--signing-algorithm ECDSA_SHA_256 \
--message-type DIGEST \
--message fileb://message.sha256 \
--signature fileb://signature-digest.sign
    

{
    "KeyId": "arn:aws:kms:eu-west-2:111122223333:key/024f6b2c-365a-4d86-af2e-9cc5c468afba",
    "SignatureValid": true,
    "SigningAlgorithm": "ECDSA_SHA_256"
}
    

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 024f6b2c-365a-4d86-af2e-9cc5c468afba
    

Which gives us:


{
    "KeyId": "arn:aws:kms:eu-west-2:111122223333:key/024f6b2c-365a-4d86-af2e-9cc5c468afba",
    "PublicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuD3O/vISzg4EpFHAFe1jJudnP31YL7MGbxcVwRMnylnA/gw7ty2umsdzluFxwqOGO7xr63pozNjOfSzmJUoDuQ==",
    "CustomerMasterKeySpec": "ECC_NIST_P256",
    "KeyUsage": "SIGN_VERIFY",
    "SigningAlgorithms": [
        "ECDSA_SHA_256"
    ]
}
    

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 024f6b2c-365a-4d86-af2e-9cc5c468afba \
--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 ec -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:


echo -n Hello | openssl dgst -sha256 -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:


openssl pkeyutl -verify -pubin -keyform DER -inkey key.der -in message.sha256 -sigfile signature-digest.sign