Docs / KMS

KMS

LocalEmu KMS performs real cryptography. Symmetric Encrypt, Decrypt, ReEncrypt, and GenerateDataKey use AES-256-GCM. Sign / Verify use RSA-PSS, RSA-PKCS1v15, or ECDSA. GenerateMac / VerifyMac use HMAC. Asymmetric key pairs are generated with the Python cryptography library at key-creation time. Grants, aliases, tags, key policies, automatic and on-demand rotation, and multi-region replication are all implemented in the gateway process: no external service or daemon involved.

Operation-level coverage: see the KMS coverage matrix.

Quick start

Terminal
$ awsemu kms create-key --description "demo symmetric key"
{
  "KeyMetadata": {
    "KeyId":     "11111111-2222-3333-4444-555555555555",
    "Arn":       "arn:aws:kms:us-east-1:000000000000:key/11111111-2222-3333-4444-555555555555",
    "KeyUsage":  "ENCRYPT_DECRYPT",
    "KeySpec":   "SYMMETRIC_DEFAULT",
    "KeyState":  "Enabled",
    "Origin":    "AWS_KMS",
    "MultiRegion": false
  }
}

$ KEY_ID=11111111-2222-3333-4444-555555555555

# Encrypt a 16-byte payload
$ awsemu kms encrypt --key-id $KEY_ID --plaintext "$(printf 'hello kms world!' | base64)" \
    --query CiphertextBlob --output text
AQICAHi...long base64...==

$ CIPHERTEXT=$(awsemu kms encrypt --key-id $KEY_ID \
    --plaintext "$(printf 'hello kms world!' | base64)" \
    --query CiphertextBlob --output text)

$ awsemu kms decrypt --ciphertext-blob "$CIPHERTEXT" \
    --query Plaintext --output text | base64 -d
hello kms world!

The CiphertextBlob is a real AES-256-GCM ciphertext, not an opaque token. Format: 32 bytes key ID, 16 bytes IV, 16 bytes auth tag, then the encrypted body. The blob can be decrypted by any LocalEmu process that holds the same KmsStore (so across restarts with PERSISTENCE=1, and across replica keys in other regions).

Architecture

KMS lives at services/kms/. The provider KmsProvider at provider.py:186 owns the request surface; cryptographic state lives in three classes:

The low-level AES routines live in utils/crypto.py:

When PERSISTENCE=1 is set, the KmsStore is serialised through the standard StateVisitor hook and restored across restarts, including all generated key material. Ciphertexts produced before a restart decrypt correctly after.

Key specs and usages

Twelve KeySpec values are validated by KmsCryptoKey.assert_valid at models.py:176-212. Anything else is rejected with ValidationException or UnsupportedOperationException.

KeySpecValid KeyUsageUnderlying primitive
SYMMETRIC_DEFAULTENCRYPT_DECRYPT32 random bytes, used with AES-256-GCM.
RSA_2048, RSA_3072, RSA_4096ENCRYPT_DECRYPT or SIGN_VERIFYrsa.generate_private_key with the matching modulus.
ECC_NIST_P256, ECC_NIST_P384, ECC_NIST_P521, ECC_SECG_P256K1SIGN_VERIFY (P256K1 also supports KEY_AGREEMENT)ec.generate_private_key over SECP256R1 / SECP384R1 / SECP521R1 / SECP256K1.
HMAC_224, HMAC_256, HMAC_384, HMAC_512GENERATE_VERIFY_MACRandom bytes within the per-spec length range (28-64, 32-64, 48-128, 64-128).

The four KeyUsage values are ENCRYPT_DECRYPT, SIGN_VERIFY, GENERATE_VERIFY_MAC, and KEY_AGREEMENT. The pairing rule is enforced at models.py:653-702: e.g. an ECC key cannot be created with ENCRYPT_DECRYPT; an RSA key for signing cannot be used by Encrypt.

Features supported

FeatureNotes
Symmetric encrypt / decryptEncrypt, Decrypt, ReEncrypt. AES-256-GCM with 16-byte IV and 16-byte tag. Ciphertext is portable across replica keys in different regions.
Asymmetric encrypt / decryptRSA keys with ENCRYPT_DECRYPT usage. Algorithms RSAES_OAEP_SHA_1, RSAES_OAEP_SHA_256.
Envelope encryptionGenerateDataKey, GenerateDataKeyWithoutPlaintext, GenerateDataKeyPair, GenerateDataKeyPairWithoutPlaintext. Plaintext is real key material; the wrapped form is encrypted under the parent KMS key.
Sign / VerifySign, Verify. RSASSA-PSS and RSASSA-PKCS1v15 (SHA-256/384/512) for RSA keys; ECDSA-SHA-256/384/512 for ECC keys. Signature validation actually checks the cryptographic signature.
MACGenerateMac, VerifyMac. HMAC_SHA_224, HMAC_SHA_256, HMAC_SHA_384, HMAC_SHA_512.
Key agreementDeriveSharedSecret on ECC keys using ECDH.
RandomGenerateRandom at provider.py:851-871: bytes from os.urandom.
Public-key exportGetPublicKey returns the DER-encoded SubjectPublicKeyInfo for RSA / ECC keys.
GrantsCreateGrant, ListGrants, RetireGrant, RevokeGrant, ListRetirableGrants. Grant constraints, grantee principal, retiring principal, and operations list are all stored.
AliasesCreateAlias, UpdateAlias, DeleteAlias, ListAliases. Aliases resolve transparently as KeyId in every operation that takes one.
Key policiesPutKeyPolicy, GetKeyPolicy, ListKeyPolicies. Policies are stored verbatim; access decisions are not made from them.
Key rotationEnableKeyRotation, DisableKeyRotation, GetKeyRotationStatus, RotateKeyOnDemand. Custom rotation period via RotationPeriodInDays attribute (90-2 560).
Key lifecycleDescribeKey, ListKeys, EnableKey, DisableKey, UpdateKeyDescription, ScheduleKeyDeletion, CancelKeyDeletion. Deletion window 7-30 days, state transitions EnabledDisabledPendingDeletion.
TagsTagResource, UntagResource, ListResourceTags.
Import own key materialGetParametersForImport returns a wrapping public key and an import token; ImportKeyMaterial unwraps and installs. DeleteImportedKeyMaterial wipes it.
Multi-region keysCreateKey --multi-region and ReplicateKey. The replica shares the parent's key material via replicate_metadata at models.py:453, so ciphertext encrypted in one region decrypts in another.
PersistenceFull KmsStore (keys, aliases, grants, import staging) is restored when PERSISTENCE=1.

Envelope encryption with data keys

GenerateDataKey returns a freshly generated symmetric key in two forms: Plaintext (use it once, then discard) and CiphertextBlob (store alongside the data). To read the data back, call Decrypt on the wrapped blob to recover the plaintext key, then decrypt locally. This is the same pattern S3 SSE-KMS, EBS encryption, and the AWS Encryption SDK rely on.

Terminal
# Envelope encryption: GenerateDataKey returns both the plaintext and the wrapped key
$ awsemu kms generate-data-key --key-id $KEY_ID --key-spec AES_256
{
  "CiphertextBlob": "AQIDAHi...",
  "Plaintext":      "5w8...32 bytes of base64...=",
  "KeyId":          "arn:aws:kms:us-east-1:000000000000:key/11111111-..."
}

# Save the wrapped key alongside your data; throw away the plaintext after encrypting locally
$ awsemu kms generate-data-key-without-plaintext --key-id $KEY_ID --key-spec AES_256 \
    --query CiphertextBlob --output text > wrapped.key

# Unwrap on demand
$ awsemu kms decrypt --ciphertext-blob "$(cat wrapped.key)" \
    --query Plaintext --output text | base64 -d | xxd | head -2
00000000: 5b6c 3f1e 8c2a ...32 random bytes...

Asymmetric keys: sign and verify

RSA and ECC sign / verify flows produce real signatures that any external library can validate against the exported public key. Sign at models.py:397 calls cryptography's RSAPrivateKey.sign or EllipticCurvePrivateKey.sign directly; Verify at models.py:410 checks the signature and raises KMSInvalidSignatureException on mismatch.

Terminal
$ awsemu kms create-key --key-spec RSA_2048 --key-usage SIGN_VERIFY \
    --query 'KeyMetadata.{KeyId:KeyId,SigningAlgorithms:SigningAlgorithms}'
{
  "KeyId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
  "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"
  ]
}

$ SIGN_KEY=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
$ MSG=$(printf 'payload-to-sign' | base64)

$ SIG=$(awsemu kms sign --key-id $SIGN_KEY --message "$MSG" \
    --signing-algorithm RSASSA_PSS_SHA_256 \
    --query Signature --output text)

$ awsemu kms verify --key-id $SIGN_KEY --message "$MSG" \
    --signature "$SIG" --signing-algorithm RSASSA_PSS_SHA_256 \
    --query SignatureValid
true

# Export the public key so external systems can verify offline
$ awsemu kms get-public-key --key-id $SIGN_KEY \
    --query PublicKey --output text | base64 -d > pub.der

Aliases

An alias is a friendly pointer at a key; it lets you rotate the underlying key ID without changing application code. Every KMS operation that takes a KeyId also accepts alias/.... Resolution happens in the provider on every call.

Terminal
$ awsemu kms create-alias --alias-name alias/payments --target-key-id $KEY_ID
$ awsemu kms list-aliases --query 'Aliases[?AliasName==`alias/payments`].TargetKeyId' --output text
11111111-2222-3333-4444-555555555555

# Aliases resolve transparently in every other KMS call
$ awsemu kms encrypt --key-id alias/payments \
    --plaintext "$(printf 'card-token' | base64)" \
    --query KeyId --output text
arn:aws:kms:us-east-1:000000000000:key/11111111-2222-3333-4444-555555555555

Multi-region keys

A multi-region key has a primary in one region and zero or more replicas in others. ReplicateKey copies the metadata and the KmsCryptoKey material into the target region's KmsStore. Because both regions share the underlying key bytes, a ciphertext produced in one region decrypts in the other without an extra ReEncrypt hop.

Terminal
# Create a primary multi-region key in us-east-1
$ awsemu kms create-key --multi-region \
    --query 'KeyMetadata.{KeyId:KeyId,MultiRegion:MultiRegion,MultiRegionConfiguration:MultiRegionConfiguration}'
{
  "KeyId":        "mrk-1234...",
  "MultiRegion":  true,
  "MultiRegionConfiguration": {
    "MultiRegionKeyType": "PRIMARY",
    "PrimaryKey":   {"Arn": "arn:aws:kms:us-east-1:000000000000:key/mrk-1234...", "Region": "us-east-1"},
    "ReplicaKeys":  []
  }
}

# Replicate the key to eu-west-1 with the same key material
$ awsemu kms replicate-key --key-id mrk-1234... --replica-region eu-west-1 \
    --query 'ReplicaKeyMetadata.{Region:MultiRegionConfiguration.PrimaryKey.Region,Replicas:MultiRegionConfiguration.ReplicaKeys[].Region}'
{
  "Region":   "us-east-1",
  "Replicas": ["eu-west-1"]
}

# A ciphertext encrypted in us-east-1 can be decrypted by the eu-west-1 replica
$ C=$(awsemu kms encrypt --key-id mrk-1234... \
    --plaintext "$(printf 'cross-region payload' | base64)" \
    --query CiphertextBlob --output text)

$ awsemu --region eu-west-1 kms decrypt --ciphertext-blob "$C" \
    --query Plaintext --output text | base64 -d
cross-region payload

Integration points

Other LocalEmu services depend on KMS for at-rest and in-transit encryption.

ServiceWhere KMS is used
S3 SSE-KMSBucket-default and per-object KMS keys are resolved through get_kms_key_arn() at services/s3/utils.py:652. The object body is encrypted with a data key generated via GenerateDataKey.
DynamoDB encryption-at-restThe SSESpecification on tables stores a KMS key ARN; the table reads it on each access. Server-side encryption metadata is honoured; storage stays cleartext on disk.
Secrets ManagerEach secret's KmsKeyId is used by the provider when wrapping and unwrapping secret versions.
SNS, SQSTopic and queue KMS attributes are stored and returned. Message bodies are stored in cleartext at rest.
EventBridgeBuses with a KmsKeyIdentifier store the reference; the underlying event payload is not encrypted.

Configuration

KMS has no service-specific configuration knobs. One related variable lives on the S3 side:

VariableDefaultPurpose
S3_SKIP_KMS_KEY_VALIDATIONfalseWhen true, S3 stops calling KMS to verify that a bucket-default or per-object KMS key exists in the same region. Useful for cross-account fixture setups where the key lives outside LocalEmu's view. Defined at config.py:1108.

Known limitations