Verifying data integrity with KMS asymmetric keys
Tags: aws, security
Published: 2022-12-08 06:26:41

We can use asymmetric KMS keys to verify if our classified data have changed in transit. Asymmetric keys also come in handy when we want to check if the data come from a reliable source. The AWS SDK provides a programmatic way for applications to sign and verify messages.

## 1. Data integrity

When we transfer data from one application to another we want to ensure that:

<ul>
  <li>no one can read our data</li>
  <li>the sender is one who says they are</li>
  <li>no one can change the data in transit.</li>
</ul>

TLS and data encryption can address the first bullet point. <a href="https://aws.amazon.com/kms/" class="link" target="_blank" rel="noopener noreferrer">Key Management Service (KMS)</a> provides both **symmetric** keys with [envelope encryption](https://arpadt.com/articles/envelope-encryption) and asymmetric keypairs to encrypt and decrypt data.

But we can use **asymmetric keys** to fulfilling the requirements of the last two points, namely sign and verify data.

## 2. About asymmetric keys

With symmetric keys, we use the **same key** for both encryption and decryption. On the other hand, an asymmetric key pair consists of a private and a public key, and we should use them together.

We use the **private** key to **sign** (and encrypt) data. It's like our secret ID, so we should never share it with anyone. The signature ensures that the message's sender is the one we expect them to be because they have the private key.

**Public** keys, on the other hand, are what their name indicates: public. We use the public key for **verifying** the sender's identity and the integrity of the data.

We must use both keys together. We derive the public key from the private key using complicated mathematical operations that include large prime numbers and other scary concepts. We can share the public key because it's impossible to reverse-engineer the private key from it.

## 3. Asymmetric keys in KMS

Aside from symmetric keys, we can create asymmetric keypairs in KMS too. It's straightforward to generate an asymmetric keypair in the Console. We should select **Asymmetric** and **Sign and verify** when prompted to create a new key.

The private key never leaves KMS, so we can only use it via an API.

On the other hand, we can see the public key in the Console or view it using the <a href="https://aws.amazon.com/cli/" class="link" target="_blank" rel="noopener noreferrer">AWS CLI</a> with the `get-public-key` command:

```bash
aws kms get-public-key --key-id KEY_ID
```

The response will be something like this:

```json
{
  "KeyId": "arn:aws:kms:us-east-1:124556789012:key/KEY_ID",
  "PublicKey": "PUBLIC_KEY",
  "CustomerMasterKeySpec": "ECC_NIST_P256",
  "KeySpec": "ECC_NIST_P256",
  "KeyUsage": "SIGN_VERIFY",
  "SigningAlgorithms": [
      "ECDSA_SHA_256"
  ]
}
```

We can use the public key in our application using the SDK. This way, we can perform the data verification locally instead of letting KMS do it for us.

## 4. Let's code it

The pre-requisite for this little exercise is to have a public/private key pair (single or multi-region) in KMS, which we have configured to sign and verify messages.

I used two <a href="https://aws.amazon.com/lambda/" class="link" target="_blank" rel="noopener noreferrer">Lambda</a> functions, one for signing the message and another one for verifying it. In this example, I'll send the signed data to an <a href="https://aws.amazon.com/sqs/" class="link" target="_blank" rel="noopener noreferrer">SQS</a> queue. The verification function will poll the queue for available messages. It will then verify if the data comes from the expected sender and if no one has tampered with it.

We'll provide the queue URL and the **KMS key ID** as environment variables for the functions. The code uses Node.js v18 and the <a href="https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/" class="link" target="_blank" rel="noopener noreferrer">AWS SDK for JavaScript for Node.js v3</a> to interact with the AWS APIs.

### 4.1. The sign side

The sign function can have the following code:

```js
import { KMSClient, SignCommand, MessageType, SigningAlgorithmSpec } from '@aws-sdk/client-kms';
import { SQSClient, SendMessageCommand } from '@aws-sdk/client-sqs';
import { createHash } from 'crypto';

const { QUEUE_URL, KMS_KEY_ID } = process.env;

const kmsClient = new KMSClient();
const sqsClient = new SQSClient();

export async function handler(event) {
  // 1. Get the object which we want to sign
  const data = event.data;

  // 2. Create a digest from the message
  const hash = createHash('sha256');
  hash.update(Buffer.from(JSON.stringify(data)));
  const digest = hash.digest();

  // 3. Sign the message
  const signCommandInput = {
    KeyId: KMS_KEY_ID,
    MessageType: 'DIGEST',
    SigningAlgorithm: 'ECDSA_SHA_256',
    Message: digest,
  };

  const signCommand = new SignCommand(signCommandInput);

  let signature;
  try {
    const response = await kmsClient.send(signCommand);
    signature = response.Signature;
  } catch (error) {
    throw error;
  }

  // 4. Send the message to the recipient
  const sendMessageInput = {
    QueueUrl: QUEUE_URL,
    MessageBody: JSON.stringify({ signature: signature.toString(), data }),
  };
  const sendMessageCommand = new SendMessageCommand(sendMessageInput);

  try {
    const response = await sqsClient.send(sendMessageCommand);
    console.log('message sent to queue: ', response);
  } catch (error) {
    throw error;
  }
}
```

The main points are as follows.

The function receives the data from the `event` object, but it can come from any source.

We'll then create a **digest** of the message. This operation is mandatory if the data size is more than 4kB. If it's less than 4kB, then KMS will create it for us.

It basically means that we'll create a **hash** of the data, which will have the same length regardless of the original data size.

In <a href="https://nodejs.org/en/" class="link" target="_blank" rel="noopener noreferrer">Node.js</a> we can do it in the following way:

```js
const hash = createHash('sha256');
hash.update(Buffer.from(JSON.stringify(data)));
const digest = hash.digest();
```

In this example, we use the `sha256` algorithm. First, we have to `JSON.stringify` the object and then turn it into binary data.

Next, we'll **sign** the message. The value of the `MessageType` property is `DIGEST` since we want to sign the digest and not the raw data. We can choose from multiple `SigningAlgorithm` values, and let's choose `ECDSA_SHA_256` here. The acronym refers to the Elliptic Curve Digital Signature Algorithm, and I'll have a link to an article that explains it in detail at the bottom.

We'll call the `sign` method in the KMS API to (surprise) sign the message. KMS will use the private key for this operation. KMS stores the private key **inside** a hardware security module, so the key never leaves the service.

If the signing process is successful, the `sign` method will return the `Signature` along with a few other properties.

The last step is to `stringify` the object that contains **both** the data and the signature.

### 4.2. The verify side

The second function performs the verification. The code is similar to the first function, and it can look like this:

```js
import { KMSClient, VerifyCommand } from '@aws-sdk/client-kms';
import { createHash } from 'crypto';

const { KMS_KEY_ID } = process.env;

const kmsClient = new KMSClient();

export async function handler(event) {
  // 1. unwrap the message
  const record = event.Records[0];
  const message = JSON.parse(record.body);
  const data = message.data;

  // 2. Create the same digest
  const hash = createHash('sha256');
  hash.update(Buffer.from(JSON.stringify(data)));
  const digest = hash.digest();

  // 3. Verify the integrity of the data
  const verifyCommandInput = {
    KeyId: KMS_KEY_ID,
    MessageType: 'DIGEST',
    SigningAlgorithm: 'ECDSA_SHA_256',
    Message: digest,
    Signature: new Uint8Array(message.signature.split(','))
  };

  const verifyCommand = new VerifyCommand(verifyCommandInput);

  try {
    const response = await kmsClient.send(verifyCommand);
    console.log('verification response: ', response);
  } catch (error) {
    throw error
  }
}
```

First, we extract `data` and `signature` from the message. It's a standard SQS-Lambda trigger, and it's not part of this post to explain how it works.

Next, we'll create the **same** hash as in the sign function. We should do it so that KMS can compare the original signed message to the one we submit for verification.

We'll then call the `verify` endpoint with the data's digest and the signature. It's the same signature we received when the `sign` function signed the message. Originally it was a `Uint8Array`, so when we submit it for verification, the signature's format should be the same. We can use the `Uint8Array` Node.js constructor here.

 KMS will use the public key to decide if the message is still the same as at the point of the signature. It will also verify if the private key is valid, i.e., it belongs to the same KMS key pair (the public key is a mathematic derivation of the private key).

If the verification is successful, the endpoint returns `SignatureValid: true`:

```js
{
  '$metadata': {
    httpStatusCode: 200,
    requestId: '69a98201-67a9-4ae2-bd67-d5f01c388e9b',
    extendedRequestId: undefined,
    cfId: undefined,
    attempts: 1,
    totalRetryDelay: 0
  },
  KeyId: 'arn:aws:kms:us-east-1:123456789012:key/KEY_ID',
  SignatureValid: true,
  SigningAlgorithm: 'ECDSA_SHA_256'
}
```

Otherwise, it will throw an error.

## 5. Errors

I found it hard to prepare this exercise partly because the AWS documentation doesn't always explain the steps well. So it came as no surprise to me that I encountered some errors while playing with the keys. I'll share the most interesting ones and their resolutions here.

**Digest already called** - This error comes from the `hash` object when we invoke the Lambda function more than once. We should move the `hash` object creation inside the handler. If not, Lambda will reuse the already existing object. We can't verify a digest more than once, so in this case, it's ok to have more logic inside the Lambda handler.

**Digest is invalid length for algorithm ECDSA_SHA_256** - The digest we send to KMS for signing must be of a Buffer type.

**Cannot read properties of undefined (reading 'byteLength')** - This error is interesting. Most sources indicate that the error comes up when we don't configure the credentials. I received this error when I had `Message` in `SignCommandInput` in a string format when it should be Buffer.

**ValidationException: 1 validation error detected: Value at 'signature' failed to satisfy constraint: Member must have length greater than or equal to 1** - The error can occur while we try to verify the signature. It means that the `Uint8Array` is empty. The error is probably SQS-specific in how we send messages to the queue. The `Uint8Array` won't be of the same format after parsing the stringified object. In this case, we can use `toString()` on the `sign` end and `split` the string into the new `Uint8Array` before we submit the signature for verification.

## 6. Summary

We can generate not only symmetric keys but asymmetric keypairs in KMS. We can use asymmetric keypairs to encrypt and decrypt data so as to sign and verify messages.

Signing the message with the private key and verifying it with its public pair ensures that we'll receive the original data as intended. We can also be sure that the sender is a trusted entity.

## 7. Further reading

<a href="https://cryptobook.nakov.com/digital-signatures/ecdsa-sign-verify-messages" class="link" target="_blank" rel="noopener noreferrer">ECDSA: Elliptic Curve Signatures</a> - Explanation of the ECDSA signature process - hard core mathematics fans only

<a href="https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#asymmetric-keys-concept" class="link" target="_blank" rel="noopener noreferrer">Asymmetric key concepts</a> - Basic information about public/private key pairs

<a href="https://docs.aws.amazon.com/kms/latest/developerguide/symmetric-asymmetric.html" class="link" target="_blank" rel="noopener noreferrer">Asymmetric keys in AWS KMS</a> - The title says it all

<a href="https://aws.amazon.com/blogs/security/digital-signing-asymmetric-keys-aws-kms/" class="link" target="_blank" rel="noopener noreferrer">Digital signing with the new asymmetric keys feature of AWS KMS</a> - Use the CLI to sign and verify data