Skip to content

Commit 44d9192

Browse files
feat: Required encryption context CMM (#645)
1 parent ac79bc8 commit 44d9192

18 files changed

+1045
-65
lines changed

examples/src/keyrings/hierarchical_keyring.py

+32-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,36 @@
11
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
22
# SPDX-License-Identifier: Apache-2.0
3-
"""Example showing basic encryption and decryption of a value already in memory."""
3+
"""
4+
This example sets up the Hierarchical Keyring, which establishes a key hierarchy where "branch"
5+
keys are persisted in DynamoDb. These branch keys are used to protect your data keys, and these
6+
branch keys are themselves protected by a KMS Key.
7+
8+
Establishing a key hierarchy like this has two benefits:
9+
First, by caching the branch key material, and only calling KMS to re-establish authentication
10+
regularly according to your configured TTL, you limit how often you need to call KMS to protect
11+
your data. This is a performance security tradeoff, where your authentication, audit, and logging
12+
from KMS is no longer one-to-one with every encrypt or decrypt call. Additionally, KMS Cloudtrail
13+
cannot be used to distinguish Encrypt and Decrypt calls, and you cannot restrict who has
14+
Encryption rights from who has Decryption rights since they both ONLY need KMS:Decrypt. However,
15+
the benefit is that you no longer have to make a network call to KMS for every encrypt or
16+
decrypt.
17+
18+
Second, this key hierarchy facilitates cryptographic isolation of a tenant's data in a
19+
multi-tenant data store. Each tenant can have a unique Branch Key, that is only used to protect
20+
the tenant's data. You can either statically configure a single branch key to ensure you are
21+
restricting access to a single tenant, or you can implement an interface that selects the Branch
22+
Key based on the Encryption Context.
23+
24+
This example demonstrates configuring a Hierarchical Keyring with a Branch Key ID Supplier to
25+
encrypt and decrypt data for two separate tenants.
26+
27+
This example requires access to the DDB Table where you are storing the Branch Keys. This
28+
table must be configured with the following primary key configuration: - Partition key is named
29+
"partition_key" with type (S) - Sort key is named "sort_key" with type (S).
30+
31+
This example also requires using a KMS Key. You need the following access on this key: -
32+
GenerateDataKeyWithoutPlaintext - Decrypt
33+
"""
434
import sys
535

636
import boto3
@@ -25,6 +55,7 @@
2555

2656
from .example_branch_key_id_supplier import ExampleBranchKeyIdSupplier
2757

58+
# TODO-MPL: Remove this as part of removing PYTHONPATH hacks.
2859
module_root_dir = '/'.join(__file__.split("/")[:-1])
2960

3061
sys.path.append(module_root_dir)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""
4+
Demonstrate an encrypt/decrypt cycle using a Required Encryption Context CMM.
5+
A required encryption context CMM asks for required keys in the encryption context field
6+
on encrypt such that they will not be stored on the message, but WILL be included in the header signature.
7+
On decrypt, the client MUST supply the key/value pair(s) that were not stored to successfully decrypt the message.
8+
"""
9+
import sys
10+
11+
import boto3
12+
# Ignore missing MPL for pylint, but the MPL is required for this example
13+
# noqa pylint: disable=import-error
14+
from aws_cryptographic_materialproviders.mpl import AwsCryptographicMaterialProviders
15+
from aws_cryptographic_materialproviders.mpl.config import MaterialProvidersConfig
16+
from aws_cryptographic_materialproviders.mpl.models import (
17+
CreateAwsKmsKeyringInput,
18+
CreateDefaultCryptographicMaterialsManagerInput,
19+
CreateRequiredEncryptionContextCMMInput,
20+
)
21+
from aws_cryptographic_materialproviders.mpl.references import ICryptographicMaterialsManager, IKeyring
22+
from typing import Dict, List
23+
24+
import aws_encryption_sdk
25+
from aws_encryption_sdk import CommitmentPolicy
26+
from aws_encryption_sdk.exceptions import AWSEncryptionSDKClientError
27+
28+
# TODO-MPL: Remove this as part of removing PYTHONPATH hacks
29+
module_root_dir = '/'.join(__file__.split("/")[:-1])
30+
31+
sys.path.append(module_root_dir)
32+
33+
EXAMPLE_DATA: bytes = b"Hello World"
34+
35+
36+
def encrypt_and_decrypt_with_keyring(
37+
kms_key_id: str
38+
):
39+
"""Creates a hierarchical keyring using the provided resources, then encrypts and decrypts a string with it."""
40+
# 1. Instantiate the encryption SDK client.
41+
# This builds the client with the REQUIRE_ENCRYPT_REQUIRE_DECRYPT commitment policy,
42+
# which enforces that this client only encrypts using committing algorithm suites and enforces
43+
# that this client will only decrypt encrypted messages that were created with a committing
44+
# algorithm suite.
45+
# This is the default commitment policy if you were to build the client as
46+
# `client = aws_encryption_sdk.EncryptionSDKClient()`.
47+
48+
client = aws_encryption_sdk.EncryptionSDKClient(
49+
commitment_policy=CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT
50+
)
51+
52+
# 2. Create an encryption context.
53+
# Most encrypted data should have an associated encryption context
54+
# to protect integrity. This sample uses placeholder values.
55+
# For more information see:
56+
# blogs.aws.amazon.com/security/post/Tx2LZ6WBJJANTNW/How-to-Protect-the-Integrity-of-Your-Encrypted-Data-by-Using-AWS-Key-Management # noqa: E501
57+
encryption_context: Dict[str, str] = {
58+
"key1": "value1",
59+
"key2": "value2",
60+
"requiredKey1": "requiredValue1",
61+
"requiredKey2": "requiredValue2",
62+
}
63+
64+
# 3. Create list of required encryption context keys.
65+
# This is a list of keys that must be present in the encryption context.
66+
required_encryption_context_keys: List[str] = ["requiredKey1", "requiredKey2"]
67+
68+
# 4. Create the AWS KMS keyring.
69+
mpl: AwsCryptographicMaterialProviders = AwsCryptographicMaterialProviders(
70+
config=MaterialProvidersConfig()
71+
)
72+
keyring_input: CreateAwsKmsKeyringInput = CreateAwsKmsKeyringInput(
73+
kms_key_id=kms_key_id,
74+
kms_client=boto3.client('kms', region_name="us-west-2")
75+
)
76+
kms_keyring: IKeyring = mpl.create_aws_kms_keyring(keyring_input)
77+
78+
# 5. Create the required encryption context CMM.
79+
underlying_cmm: ICryptographicMaterialsManager = \
80+
mpl.create_default_cryptographic_materials_manager(
81+
CreateDefaultCryptographicMaterialsManagerInput(
82+
keyring=kms_keyring
83+
)
84+
)
85+
86+
required_ec_cmm: ICryptographicMaterialsManager = \
87+
mpl.create_required_encryption_context_cmm(
88+
CreateRequiredEncryptionContextCMMInput(
89+
required_encryption_context_keys=required_encryption_context_keys,
90+
underlying_cmm=underlying_cmm,
91+
)
92+
)
93+
94+
# 6. Encrypt the data
95+
ciphertext, _ = client.encrypt(
96+
source=EXAMPLE_DATA,
97+
materials_manager=required_ec_cmm,
98+
encryption_context=encryption_context
99+
)
100+
101+
# 7. Reproduce the encryption context.
102+
# The reproduced encryption context MUST contain a value for
103+
# every key in the configured required encryption context keys during encryption with
104+
# Required Encryption Context CMM.
105+
reproduced_encryption_context: Dict[str, str] = {
106+
"requiredKey1": "requiredValue1",
107+
"requiredKey2": "requiredValue2",
108+
}
109+
110+
# 8. Decrypt the data
111+
plaintext_bytes_A, _ = client.decrypt(
112+
source=ciphertext,
113+
materials_manager=required_ec_cmm,
114+
encryption_context=reproduced_encryption_context
115+
)
116+
assert plaintext_bytes_A == EXAMPLE_DATA
117+
118+
# We can also decrypt using the underlying CMM,
119+
# but must also provide the reproduced encryption context
120+
plaintext_bytes_A, _ = client.decrypt(
121+
source=ciphertext,
122+
materials_manager=underlying_cmm,
123+
encryption_context=reproduced_encryption_context
124+
)
125+
assert plaintext_bytes_A == EXAMPLE_DATA
126+
127+
# 9. Extra: Demonstrate that if we don't provide the reproduced encryption context,
128+
# decryption will fail.
129+
try:
130+
plaintext_bytes_A, _ = client.decrypt(
131+
source=ciphertext,
132+
materials_manager=required_ec_cmm,
133+
# No reproduced encryption context for required EC CMM-produced message makes decryption fail.
134+
)
135+
raise Exception("If this exception is raised, decryption somehow succeeded!")
136+
except AWSEncryptionSDKClientError:
137+
# Swallow specific expected exception.
138+
# We expect decryption to fail with an AWSEncryptionSDKClientError
139+
# since we did not provide reproduced encryption context when decrypting
140+
# a message encrypted with the requried encryption context CMM.
141+
pass
142+
143+
# Same for the default CMM;
144+
# If we don't provide the reproduced encryption context, decryption will fail.
145+
try:
146+
plaintext_bytes_A, _ = client.decrypt(
147+
source=ciphertext,
148+
materials_manager=required_ec_cmm,
149+
# No reproduced encryption context for required EC CMM-produced message makes decryption fail.
150+
)
151+
raise Exception("If this exception is raised, decryption somehow succeeded!")
152+
except AWSEncryptionSDKClientError:
153+
# Swallow specific expected exception.
154+
# We expect decryption to fail with an AWSEncryptionSDKClientError
155+
# since we did not provide reproduced encryption context when decrypting
156+
# a message encrypted with the requried encryption context CMM,
157+
# even though we are using a default CMM on decrypt.
158+
pass

examples/test/keyrings/test_i_hierarchical_keyring.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
22
# SPDX-License-Identifier: Apache-2.0
3-
"""Unit test suite for the hierarchical keyring example."""
3+
"""Test suite for the hierarchical keyring example."""
44
import pytest
55

66
from ...src.keyrings.hierarchical_keyring import encrypt_and_decrypt_with_keyring
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""Test suite for the required encryption context CMM example."""
4+
import pytest
5+
6+
from ...src.keyrings.required_encryption_context_cmm import encrypt_and_decrypt_with_keyring
7+
8+
pytestmark = [pytest.mark.examples]
9+
10+
11+
def test_encrypt_and_decrypt_with_keyring():
12+
key_arn = "arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f"
13+
encrypt_and_decrypt_with_keyring(key_arn)

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def get_requirements():
4040
license="Apache License 2.0",
4141
install_requires=get_requirements(),
4242
# pylint: disable=fixme
43-
# TODO: Point at PyPI once MPL is released.
43+
# TODO-MPL: Point at PyPI once MPL is released.
4444
# This blocks releasing ESDK-Python MPL integration.
4545
extras_require={
4646
"MPL": ["aws-cryptographic-material-providers @" \

src/aws_encryption_sdk/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,9 @@ def decrypt(self, **kwargs):
185185
If source_length is not provided and read() is called, will attempt to seek()
186186
to the end of the stream and tell() to find the length of source data.
187187
188+
:param dict encryption_context: Dictionary defining encryption context to validate
189+
on decrypt. This is ONLY validated on decrypt if using a CMM from the
190+
aws-cryptographic-material-providers library.
188191
:param int max_body_length: Maximum frame size (or content length for non-framed messages)
189192
in bytes to read from ciphertext message.
190193
:returns: Tuple containing the decrypted plaintext and the message header object

0 commit comments

Comments
 (0)