Skip to content

Provision the keys

Two keys, because an agreement key cannot sign its own binding signature.

Create them

aws kms create-key \
  --key-usage KEY_AGREEMENT \
  --key-spec ECC_NIST_P256 \
  --description 'OpenPGP encryption subkey'

aws kms create-key \
  --key-usage SIGN_VERIFY \
  --key-spec RSA_4096 \
  --description 'OpenPGP certification primary'

Give each an alias — the code takes a key id, ARN or alias, and an alias is what makes rotation a policy change rather than a code change.

aws kms create-alias --alias-name alias/security-contact-v1-encrypt --target-key-id <id>
aws kms create-alias --alias-name alias/security-contact-v1-certify --target-key-id <id>

Version the alias (-v1). Rotating an asymmetric KMS key means creating a new key, and a certificate is bound to the key material it was built from — so a rotation is a new certificate, and the alias should say which generation it is.

Grant the two roles

See IAM actions for the exact action lists. In short:

  • reader: GetPublicKey and DeriveSharedSecret on the encryption key.
  • certifier: GetPublicKey on both keys, Sign on the certification key.

The grant most often missed is the certifier's GetPublicKey on the encryption key. Without it there is no subkey packet to emit, so there is nothing to bind.

Check the split actually holds

Assert the refusal, not just the permission:

aws kms sign --key-id alias/security-contact-v1-certify \
  --message <(echo test) --signing-algorithm RSASSA_PKCS1_V15_SHA_256

Run as the reader role, this must fail with AccessDenied. If it succeeds, the split exists only on paper — see the role split.

Confirm the shapes

aws kms describe-key --key-id alias/security-contact-v1-encrypt \
  --query 'KeyMetadata.[KeyUsage,KeySpec,KeyAgreementAlgorithms]'

Expect KEY_AGREEMENT, ECC_NIST_P256, and ["ECDH"]. NewDeriverForKey checks all three, so a mismatch is caught at construction — but catching it here is cheaper than catching it in an application.