Skip to content

Commit f452d9a

Browse files
committed
ECDH & Hierarchy
1 parent 68ceeb3 commit f452d9a

11 files changed

+2053
-0
lines changed

Examples/runtimes/python/DynamoDBEncryption/src/keyring/__init__.py

Whitespace-only changes.
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""
4+
Example implementation of a branch key ID supplier.
5+
6+
Used in the 'HierarchicalKeyringExample'.
7+
In that example, we have a table where we distinguish multiple tenants
8+
by a tenant ID that is stored in our partition attribute.
9+
The expectation is that this does not produce a confused deputy
10+
because the tenants are separated by partition.
11+
In order to create a Hierarchical Keyring that is capable of encrypting or
12+
decrypting data for either tenant, we implement this interface
13+
to map the correct branch key ID to the correct tenant ID.
14+
"""
15+
from typing import Dict
16+
17+
from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb.references import (
18+
IDynamoDbKeyBranchKeyIdSupplier,
19+
)
20+
from aws_dbesdk_dynamodb.structures.dynamodb import GetBranchKeyIdFromDdbKeyInput, GetBranchKeyIdFromDdbKeyOutput
21+
22+
23+
class ExampleBranchKeyIdSupplier(IDynamoDbKeyBranchKeyIdSupplier):
24+
"""Example implementation of a branch key ID supplier."""
25+
26+
branch_key_id_for_tenant1: str
27+
branch_key_id_for_tenant2: str
28+
29+
def __init__(self, tenant1_id: str, tenant2_id: str):
30+
"""
31+
Example constructor for a branch key ID supplier.
32+
33+
:param tenant1_id: Branch key ID for tenant 1
34+
:param tenant2_id: Branch key ID for tenant 2
35+
"""
36+
self.branch_key_id_for_tenant1 = tenant1_id
37+
self.branch_key_id_for_tenant2 = tenant2_id
38+
39+
def get_branch_key_id_from_ddb_key(
40+
self,
41+
param: GetBranchKeyIdFromDdbKeyInput
42+
) -> GetBranchKeyIdFromDdbKeyOutput:
43+
"""
44+
Returns branch key ID from the tenant ID in input's DDB key.
45+
46+
:param param: Input containing DDB key
47+
:return: Output containing branch key ID
48+
:raises ValueError: If DDB key is invalid or contains invalid tenant ID
49+
"""
50+
# print("Getting branch key ID from DDB key")
51+
# print(param)
52+
# raise ValueError(f'Invalid DDB key: {param}')
53+
key: Dict[str, Dict] = param.ddb_key
54+
55+
if "partition_key" not in key:
56+
raise ValueError(
57+
"Item invalid, does not contain expected partition key attribute."
58+
)
59+
60+
tenant_key_id = key["partition_key"]["S"]
61+
62+
if tenant_key_id == "tenant1Id":
63+
branch_key_id = self.branch_key_id_for_tenant1
64+
elif tenant_key_id == "tenant2Id":
65+
branch_key_id = self.branch_key_id_for_tenant2
66+
else:
67+
raise ValueError("Item does not contain valid tenant ID")
68+
69+
return GetBranchKeyIdFromDdbKeyOutput(
70+
branch_key_id=branch_key_id
71+
)
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""
4+
Example demonstrates DynamoDb Encryption for the AWS SDK client
5+
using the Hierarchical Keyring, which establishes a key hierarchy
6+
where "branch" keys are persisted in DynamoDb.
7+
These branch keys are used to protect your data keys,
8+
and these branch keys are themselves protected by a root KMS Key.
9+
10+
Establishing a key hierarchy like this has two benefits:
11+
12+
First, by caching the branch key material, and only calling back
13+
to KMS to re-establish authentication regularly according to your configured TTL,
14+
you limit how often you need to call back to KMS to protect your data.
15+
This is a performance/security tradeoff, where your authentication, audit, and
16+
logging from KMS is no longer one-to-one with every encrypt or decrypt call.
17+
However, the benefit is that you no longer have to make a
18+
network call to KMS for every encrypt or decrypt.
19+
20+
Second, this key hierarchy makes it easy to hold multi-tenant data
21+
that is isolated per branch key in a single DynamoDb table.
22+
You can create a branch key for each tenant in your table,
23+
and encrypt all that tenant's data under that distinct branch key.
24+
On decrypt, you can either statically configure a single branch key
25+
to ensure you are restricting decryption to a single tenant,
26+
or you can implement an interface that lets you map the primary key on your items
27+
to the branch key that should be responsible for decrypting that data.
28+
29+
This example then demonstrates configuring a Hierarchical Keyring
30+
with a Branch Key ID Supplier to encrypt and decrypt data for
31+
two separate tenants.
32+
33+
Running this example requires access to the DDB Table whose name
34+
is provided in CLI arguments.
35+
This table must be configured with the following
36+
primary key configuration:
37+
- Partition key is named "partition_key" with type (S)
38+
- Sort key is named "sort_key" with type (S)
39+
40+
This example also requires using a KMS Key whose ARN
41+
is provided in CLI arguments. You need the following access
42+
on this key:
43+
- GenerateDataKeyWithoutPlaintext
44+
- Decrypt
45+
"""
46+
import boto3
47+
from aws_cryptographic_material_providers.keystore.client import KeyStore
48+
from aws_cryptographic_material_providers.keystore.config import KeyStoreConfig
49+
from aws_cryptographic_material_providers.keystore.models import KMSConfigurationKmsKeyArn
50+
from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders
51+
from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig
52+
from aws_cryptographic_material_providers.mpl.models import (
53+
CacheTypeDefault,
54+
CreateAwsKmsHierarchicalKeyringInput,
55+
DefaultCache,
56+
)
57+
from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient
58+
from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb.client import DynamoDbEncryption
59+
from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb.config import (
60+
DynamoDbEncryptionConfig,
61+
)
62+
from aws_dbesdk_dynamodb.structures.dynamodb import (
63+
CreateDynamoDbEncryptionBranchKeyIdSupplierInput,
64+
DynamoDbTableEncryptionConfig,
65+
DynamoDbTablesEncryptionConfig,
66+
)
67+
from aws_dbesdk_dynamodb.structures.structured_encryption import (
68+
CryptoAction,
69+
)
70+
71+
from .example_branch_key_id_supplier import ExampleBranchKeyIdSupplier
72+
73+
74+
def hierarchical_keyring_get_item_put_item(
75+
ddb_table_name: str,
76+
tenant1_branch_key_id: str,
77+
tenant2_branch_key_id: str,
78+
keystore_table_name: str,
79+
logical_keystore_name: str,
80+
kms_key_id: str
81+
):
82+
"""
83+
Demonstrate using a hierarchical keyring with multiple tenants.
84+
85+
:param ddb_table_name: The name of the DynamoDB table
86+
:param tenant1_branch_key_id: Branch key ID for tenant 1
87+
:param tenant2_branch_key_id: Branch key ID for tenant 2
88+
:param keystore_table_name: The name of the KeyStore DynamoDB table
89+
:param logical_keystore_name: The logical name for this keystore
90+
:param kms_key_id: The ARN of the KMS key to use
91+
"""
92+
# Initial KeyStore Setup: This example requires that you have already
93+
# created your KeyStore, and have populated it with two new branch keys.
94+
# See the "Create KeyStore Table Example" and "Create KeyStore Key Example"
95+
# for an example of how to do this.
96+
97+
# 1. Configure your KeyStore resource.
98+
# This SHOULD be the same configuration that you used
99+
# to initially create and populate your KeyStore.
100+
keystore = KeyStore(
101+
config=KeyStoreConfig(
102+
ddb_client=boto3.client('dynamodb'),
103+
ddb_table_name=keystore_table_name,
104+
logical_key_store_name=logical_keystore_name,
105+
kms_client=boto3.client('kms'),
106+
kms_configuration=KMSConfigurationKmsKeyArn(kms_key_id),
107+
)
108+
)
109+
110+
# 2. Create a Branch Key ID Supplier. See ExampleBranchKeyIdSupplier in this directory.
111+
ddb_enc = DynamoDbEncryption(
112+
config=DynamoDbEncryptionConfig()
113+
)
114+
branch_key_id_supplier = ddb_enc.create_dynamo_db_encryption_branch_key_id_supplier(
115+
input=CreateDynamoDbEncryptionBranchKeyIdSupplierInput(
116+
ddb_key_branch_key_id_supplier=ExampleBranchKeyIdSupplier(
117+
tenant1_branch_key_id,
118+
tenant2_branch_key_id
119+
)
120+
)
121+
).branch_key_id_supplier
122+
123+
# 3. Create the Hierarchical Keyring, using the Branch Key ID Supplier above.
124+
# With this configuration, the AWS SDK Client ultimately configured will be capable
125+
# of encrypting or decrypting items for either tenant (assuming correct KMS access).
126+
# If you want to restrict the client to only encrypt or decrypt for a single tenant,
127+
# configure this Hierarchical Keyring using `.branch_key_id=tenant1_branch_key_id` instead
128+
# of `.branch_key_id_supplier=branch_key_id_supplier`.
129+
mat_prov = AwsCryptographicMaterialProviders(
130+
config=MaterialProvidersConfig()
131+
)
132+
133+
keyring_input = CreateAwsKmsHierarchicalKeyringInput(
134+
key_store=keystore,
135+
branch_key_id_supplier=branch_key_id_supplier,
136+
ttl_seconds=600, # This dictates how often we call back to KMS to authorize use of the branch keys
137+
cache=CacheTypeDefault( # This dictates how many branch keys will be held locally
138+
value=DefaultCache(
139+
entry_capacity=100
140+
)
141+
)
142+
)
143+
144+
hierarchical_keyring = mat_prov.create_aws_kms_hierarchical_keyring(
145+
input=keyring_input
146+
)
147+
148+
# 4. Configure which attributes are encrypted and/or signed when writing new items.
149+
# For each attribute that may exist on the items we plan to write to our DynamoDbTable,
150+
# we must explicitly configure how they should be treated during item encryption:
151+
# - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
152+
# - SIGN_ONLY: The attribute not encrypted, but is still included in the signature
153+
# - DO_NOTHING: The attribute is not encrypted and not included in the signature
154+
attribute_actions = {
155+
"partition_key": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY
156+
"sort_key": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY
157+
"tenant_sensitive_data": CryptoAction.ENCRYPT_AND_SIGN
158+
}
159+
160+
# 5. Configure which attributes we expect to be included in the signature
161+
# when reading items. There are two options for configuring this:
162+
#
163+
# - (Recommended) Configure `allowed_unsigned_attribute_prefix`:
164+
# When defining your DynamoDb schema and deciding on attribute names,
165+
# choose a distinguishing prefix (such as ":") for all attributes that
166+
# you do not want to include in the signature.
167+
# This has two main benefits:
168+
# - It is easier to reason about the security and authenticity of data within your item
169+
# when all unauthenticated data is easily distinguishable by their attribute name.
170+
# - If you need to add new unauthenticated attributes in the future,
171+
# you can easily make the corresponding update to your `attribute_actions`
172+
# and immediately start writing to that new attribute, without
173+
# any other configuration update needed.
174+
# Once you configure this field, it is not safe to update it.
175+
#
176+
# - Configure `allowed_unsigned_attributes`: You may also explicitly list
177+
# a set of attributes that should be considered unauthenticated when encountered
178+
# on read. Be careful if you use this configuration. Do not remove an attribute
179+
# name from this configuration, even if you are no longer writing with that attribute,
180+
# as old items may still include this attribute, and our configuration needs to know
181+
# to continue to exclude this attribute from the signature scope.
182+
# If you add new attribute names to this field, you must first deploy the update to this
183+
# field to all readers in your host fleet before deploying the update to start writing
184+
# with that new attribute.
185+
#
186+
# For this example, we currently authenticate all attributes. To make it easier to
187+
# add unauthenticated attributes in the future, we define a prefix ":" for such attributes.
188+
unsign_attr_prefix = ":"
189+
190+
# 6. Create the DynamoDb Encryption configuration for the table we will be writing to.
191+
table_config = DynamoDbTableEncryptionConfig(
192+
logical_table_name=ddb_table_name,
193+
partition_key_name="partition_key",
194+
sort_key_name="sort_key",
195+
attribute_actions_on_encrypt=attribute_actions,
196+
keyring=hierarchical_keyring,
197+
allowed_unsigned_attribute_prefix=unsign_attr_prefix
198+
)
199+
200+
table_configs = {ddb_table_name: table_config}
201+
tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs)
202+
203+
# 7. Create the EncryptedClient
204+
ddb_client = boto3.client('dynamodb')
205+
encrypted_ddb_client = EncryptedClient(
206+
client=ddb_client,
207+
encryption_config=tables_config
208+
)
209+
210+
# 8. Put an item into our table using the above client.
211+
# Before the item gets sent to DynamoDb, it will be encrypted
212+
# client-side, according to our configuration.
213+
# Because the item we are writing uses "tenantId1" as our partition value,
214+
# based on the code we wrote in the ExampleBranchKeySupplier,
215+
# `tenant1_branch_key_id` will be used to encrypt this item.
216+
item = {
217+
"partition_key": {"S": "tenant1Id"},
218+
"sort_key": {"N": "0"},
219+
"tenant_sensitive_data": {"S": "encrypt and sign me!"}
220+
}
221+
222+
put_response = encrypted_ddb_client.put_item(
223+
TableName=ddb_table_name,
224+
Item=item
225+
)
226+
227+
# Demonstrate that PutItem succeeded
228+
assert put_response['ResponseMetadata']['HTTPStatusCode'] == 200
229+
230+
# 9. Get the item back from our table using the same client.
231+
# The client will decrypt the item client-side, and return
232+
# back the original item.
233+
# Because the returned item's partition value is "tenantId1",
234+
# based on the code we wrote in the ExampleBranchKeySupplier,
235+
# `tenant1_branch_key_id` will be used to decrypt this item.
236+
key_to_get = {
237+
"partition_key": {"S": "tenant1Id"},
238+
"sort_key": {"N": "0"}
239+
}
240+
241+
get_response = encrypted_ddb_client.get_item(
242+
TableName=ddb_table_name,
243+
Key=key_to_get
244+
)
245+
246+
# Demonstrate that GetItem succeeded and returned the decrypted item
247+
assert get_response['ResponseMetadata']['HTTPStatusCode'] == 200
248+
returned_item = get_response['Item']
249+
assert returned_item["tenant_sensitive_data"]["S"] == "encrypt and sign me!"

0 commit comments

Comments
 (0)