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
$ 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:
- •
KmsCryptoKeyatmodels.py:159: holds the actual key material. ForSYMMETRIC_DEFAULT, 32 bytes fromos.urandom. ForRSA_*, anRSAPrivateKey. ForECC_*, anEllipticCurvePrivateKey. ForHMAC_*, random bytes of the spec-appropriate length. - •
KmsKeyatmodels.py:285: the user-visible key entity. Owns theKmsCryptoKey, theKeyMetadataenvelope (key ID, ARN, state, policies, tags, aliases, rotation schedule, multi-region config), and theencrypt/decrypt/sign/verifymethods atmodels.py:358-451. - •
KmsStoreatmodels.py:825: per-account-per-region container of allKmsKeyinstances, allKmsAlias(models.py:793) andKmsGrant(models.py:746) records, and the key-import staging area.
The low-level AES routines live in utils/crypto.py:
- •
encrypt()atutils/crypto.py:178:Cipher(AES(key), GCM(iv)), returns ciphertext + 16-byte auth tag. - •
decrypt()atutils/crypto.py:187: same cipher, raisesInvalidTagif the ciphertext was tampered with. - •PKCS#7 envelope encryption for
ImportKeyMaterial: AES-256-CBC content key wrapped with RSA-OAEP-SHA-256, atutils/crypto.py:200-220.
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.
| KeySpec | Valid KeyUsage | Underlying primitive |
|---|---|---|
SYMMETRIC_DEFAULT | ENCRYPT_DECRYPT | 32 random bytes, used with AES-256-GCM. |
RSA_2048, RSA_3072, RSA_4096 | ENCRYPT_DECRYPT or SIGN_VERIFY | rsa.generate_private_key with the matching modulus. |
ECC_NIST_P256, ECC_NIST_P384, ECC_NIST_P521, ECC_SECG_P256K1 | SIGN_VERIFY (P256K1 also supports KEY_AGREEMENT) | ec.generate_private_key over SECP256R1 / SECP384R1 / SECP521R1 / SECP256K1. |
HMAC_224, HMAC_256, HMAC_384, HMAC_512 | GENERATE_VERIFY_MAC | Random 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
| Feature | Notes |
|---|---|
| Symmetric encrypt / decrypt | Encrypt, Decrypt, ReEncrypt. AES-256-GCM with 16-byte IV and 16-byte tag. Ciphertext is portable across replica keys in different regions. |
| Asymmetric encrypt / decrypt | RSA keys with ENCRYPT_DECRYPT usage. Algorithms RSAES_OAEP_SHA_1, RSAES_OAEP_SHA_256. |
| Envelope encryption | GenerateDataKey, GenerateDataKeyWithoutPlaintext, GenerateDataKeyPair, GenerateDataKeyPairWithoutPlaintext. Plaintext is real key material; the wrapped form is encrypted under the parent KMS key. |
| Sign / Verify | Sign, 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. |
| MAC | GenerateMac, VerifyMac. HMAC_SHA_224, HMAC_SHA_256, HMAC_SHA_384, HMAC_SHA_512. |
| Key agreement | DeriveSharedSecret on ECC keys using ECDH. |
| Random | GenerateRandom at provider.py:851-871: bytes from os.urandom. |
| Public-key export | GetPublicKey returns the DER-encoded SubjectPublicKeyInfo for RSA / ECC keys. |
| Grants | CreateGrant, ListGrants, RetireGrant, RevokeGrant, ListRetirableGrants. Grant constraints, grantee principal, retiring principal, and operations list are all stored. |
| Aliases | CreateAlias, UpdateAlias, DeleteAlias, ListAliases. Aliases resolve transparently as KeyId in every operation that takes one. |
| Key policies | PutKeyPolicy, GetKeyPolicy, ListKeyPolicies. Policies are stored verbatim; access decisions are not made from them. |
| Key rotation | EnableKeyRotation, DisableKeyRotation, GetKeyRotationStatus, RotateKeyOnDemand. Custom rotation period via RotationPeriodInDays attribute (90-2 560). |
| Key lifecycle | DescribeKey, ListKeys, EnableKey, DisableKey, UpdateKeyDescription, ScheduleKeyDeletion, CancelKeyDeletion. Deletion window 7-30 days, state transitions Enabled → Disabled → PendingDeletion. |
| Tags | TagResource, UntagResource, ListResourceTags. |
| Import own key material | GetParametersForImport returns a wrapping public key and an import token; ImportKeyMaterial unwraps and installs. DeleteImportedKeyMaterial wipes it. |
| Multi-region keys | CreateKey --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. |
| Persistence | Full 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.
# 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.
$ 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.
$ 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.
# 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.
| Service | Where KMS is used |
|---|---|
| S3 SSE-KMS | Bucket-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-rest | The 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 Manager | Each secret's KmsKeyId is used by the provider when wrapping and unwrapping secret versions. |
| SNS, SQS | Topic and queue KMS attributes are stored and returned. Message bodies are stored in cleartext at rest. |
| EventBridge | Buses 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:
| Variable | Default | Purpose |
|---|---|---|
S3_SKIP_KMS_KEY_VALIDATION | false | When 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
- •Custom Key Stores are not implemented. The six operations
CreateCustomKeyStore,DescribeCustomKeyStores,ConnectCustomKeyStore,DisconnectCustomKeyStore,UpdateCustomKeyStore, andDeleteCustomKeyStorereturn 501 Not Implemented. CloudHSM-backed and external-HSM-backed key stores cannot be created. - •
UpdatePrimaryRegionis not implemented. A multi-region key created in region A cannot have its primary promoted to region B. Both regions remain reachable; only the bookkeeping op is missing. - •
ListKeyRotationsis not implemented. Rotation status is reported byGetKeyRotationStatusand on-demand rotation works viaRotateKeyOnDemand; the historical rotation listing op is the one that returns 501. - •Key policies are stored but not enforced.
PutKeyPolicyupdates the policy andGetKeyPolicyreturns it, but the gateway does not deny KMS calls based on policy statements. - •Encryption context is captured but not enforced on
Decrypt. The context is serialised into the GCM additional-authenticated-data so any tampering is caught, but the request-side strict equality check that real KMS performs is not duplicated. - •
DryRunrequests are executed. When a request carriesDryRun=true, the operation still runs and the side effects still apply. - •Grant constraints are stored, not enforced.
EncryptionContextSubsetandEncryptionContextEqualsvalues are accepted onCreateGrantand returned byListGrants, but operations performed under a grant do not check the constraint. - •Encryption-at-rest in dependent services is metadata only. S3, DynamoDB, SNS, SQS, and EventBridge accept and return KMS key IDs, but stored payloads on disk are not encrypted under the referenced key.
- •
KMS_PROVIDERenvironment variable is deprecated. Marked deprecated since LocalStack 1.4.0 (config.py:1381) and has no effect: LocalEmu always uses the in-process cryptography backend.