Skip to content

Commit 93860fe

Browse files
committed
ECDH & Hierarchy
1 parent a44ef3f commit 93860fe

9 files changed

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

0 commit comments

Comments
 (0)