main/keyring/
hierarchical_keyring.rs

1// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4use super::branch_key_id_supplier::ExampleBranchKeyIdSupplier;
5use crate::test_utils;
6use aws_db_esdk::dynamodb::client as dbesdk_client;
7use aws_db_esdk::dynamodb::types::dynamo_db_encryption_config::DynamoDbEncryptionConfig;
8use aws_db_esdk::dynamodb::types::DynamoDbTableEncryptionConfig;
9use aws_db_esdk::intercept::DbEsdkInterceptor;
10use aws_db_esdk::key_store::client as keystore_client;
11use aws_db_esdk::key_store::types::key_store_config::KeyStoreConfig;
12use aws_db_esdk::key_store::types::KmsConfiguration;
13use aws_db_esdk::material_providers::client as mpl_client;
14use aws_db_esdk::material_providers::types::material_providers_config::MaterialProvidersConfig;
15use aws_db_esdk::CryptoAction;
16use aws_db_esdk::DynamoDbTablesEncryptionConfig;
17use aws_sdk_dynamodb::types::AttributeValue;
18use std::collections::HashMap;
19
20/*
21 This example sets up DynamoDb Encryption for the AWS SDK client
22 using the Hierarchical Keyring, which establishes a key hierarchy
23 where "branch" keys are persisted in DynamoDb.
24 These branch keys are used to protect your data keys,
25 and these branch keys are themselves protected by a root KMS Key.
26
27 Establishing a key hierarchy like this has two benefits:
28
29 First, by caching the branch key material, and only calling back
30 to KMS to re-establish authentication regularly according to your configured TTL,
31 you limit how often you need to call back to KMS to protect your data.
32 This is a performance/security tradeoff, where your authentication, audit, and
33 logging from KMS is no longer one-to-one with every encrypt or decrypt call.
34 However, the benefit is that you no longer have to make a
35 network call to KMS for every encrypt or decrypt.
36
37 Second, this key hierarchy makes it easy to hold multi-tenant data
38 that is isolated per branch key in a single DynamoDb table.
39 You can create a branch key for each tenant in your table,
40 and encrypt all that tenant's data under that distinct branch key.
41 On decrypt, you can either statically configure a single branch key
42 to ensure you are restricting decryption to a single tenant,
43 or you can implement an interface that lets you map the primary key on your items
44 to the branch key that should be responsible for decrypting that data.
45
46 This example then demonstrates configuring a Hierarchical Keyring
47 with a Branch Key ID Supplier to encrypt and decrypt data for
48 two separate tenants.
49
50 Running this example requires access to the DDB Table whose name
51 is provided in CLI arguments.
52 This table must be configured with the following
53 primary key configuration:
54   - Partition key is named "partition_key" with type (S)
55   - Sort key is named "sort_key" with type (S)
56
57 This example also requires using a KMS Key whose ARN
58 is provided in CLI arguments. You need the following access
59 on this key:
60   - GenerateDataKeyWithoutPlaintext
61   - Decrypt
62*/
63pub async fn put_item_get_item(
64    tenant1_branch_key_id: &str,
65    tenant2_branch_key_id: &str,
66) -> Result<(), crate::BoxError> {
67    let ddb_table_name = test_utils::TEST_DDB_TABLE_NAME;
68
69    let keystore_table_name = test_utils::TEST_KEYSTORE_NAME;
70    let logical_keystore_name = test_utils::TEST_LOGICAL_KEYSTORE_NAME;
71    let kms_key_id = test_utils::TEST_KEYSTORE_KMS_KEY_ID;
72
73    // Initial KeyStore Setup: This example requires that you have already
74    // created your KeyStore, and have populated it with two new branch keys.
75    // See the "Create KeyStore Table Example" and "Create KeyStore Key Example"
76    // for an example of how to do this.
77
78    // 1. Configure your KeyStore resource.
79    //    This SHOULD be the same configuration that you used
80    //    to initially create and populate your KeyStore.
81    let sdk_config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await;
82    let key_store_config = KeyStoreConfig::builder()
83        .kms_client(aws_sdk_kms::Client::new(&sdk_config))
84        .ddb_client(aws_sdk_dynamodb::Client::new(&sdk_config))
85        .ddb_table_name(keystore_table_name)
86        .logical_key_store_name(logical_keystore_name)
87        .kms_configuration(KmsConfiguration::KmsKeyArn(kms_key_id.to_string()))
88        .build()?;
89
90    let key_store = keystore_client::Client::from_conf(key_store_config)?;
91
92    // 2. Create a Branch Key ID Supplier. See ExampleBranchKeyIdSupplier in this directory.
93    let dbesdk_config = DynamoDbEncryptionConfig::builder().build()?;
94    let dbesdk = dbesdk_client::Client::from_conf(dbesdk_config)?;
95    let supplier = ExampleBranchKeyIdSupplier::new(tenant1_branch_key_id, tenant2_branch_key_id);
96
97    let branch_key_id_supplier = dbesdk
98        .create_dynamo_db_encryption_branch_key_id_supplier()
99        .ddb_key_branch_key_id_supplier(supplier)
100        .send()
101        .await?
102        .branch_key_id_supplier
103        .unwrap();
104
105    // 3. Create the Hierarchical Keyring, using the Branch Key ID Supplier above.
106    //    With this configuration, the AWS SDK Client ultimately configured will be capable
107    //    of encrypting or decrypting items for either tenant (assuming correct KMS access).
108    //    If you want to restrict the client to only encrypt or decrypt for a single tenant,
109    //    configure this Hierarchical Keyring using `.branchKeyId(tenant1BranchKeyId)` instead
110    //    of `.branchKeyIdSupplier(branchKeyIdSupplier)`.
111    let mpl_config = MaterialProvidersConfig::builder().build()?;
112    let mpl = mpl_client::Client::from_conf(mpl_config)?;
113
114    let hierarchical_keyring = mpl
115        .create_aws_kms_hierarchical_keyring()
116        .branch_key_id_supplier(branch_key_id_supplier)
117        .key_store(key_store)
118        .ttl_seconds(600)
119        .send()
120        .await?;
121
122    // 4. Configure which attributes are encrypted and/or signed when writing new items.
123    //    For each attribute that may exist on the items we plan to write to our DynamoDbTable,
124    //    we must explicitly configure how they should be treated during item encryption:
125    //      - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
126    //      - SIGN_ONLY: The attribute not encrypted, but is still included in the signature
127    //      - DO_NOTHING: The attribute is not encrypted and not included in the signature
128    let attribute_actions_on_encrypt = HashMap::from([
129        ("partition_key".to_string(), CryptoAction::SignOnly), // Our partition attribute must be SIGN_ONLY
130        ("sort_key".to_string(), CryptoAction::SignOnly), // Our sort attribute must be SIGN_ONLY
131        (
132            "tenant_sensitive_data".to_string(),
133            CryptoAction::EncryptAndSign,
134        ),
135    ]);
136
137    // 5. Configure which attributes we expect to be included in the signature
138    //    when reading items. There are two options for configuring this:
139    //
140    //    - (Recommended) Configure `allowedUnsignedAttributesPrefix`:
141    //      When defining your DynamoDb schema and deciding on attribute names,
142    //      choose a distinguishing prefix (such as ":") for all attributes that
143    //      you do not want to include in the signature.
144    //      This has two main benefits:
145    //      - It is easier to reason about the security and authenticity of data within your item
146    //        when all unauthenticated data is easily distinguishable by their attribute name.
147    //      - If you need to add new unauthenticated attributes in the future,
148    //        you can easily make the corresponding update to your `attributeActionsOnEncrypt`
149    //        and immediately start writing to that new attribute, without
150    //        any other configuration update needed.
151    //      Once you configure this field, it is not safe to update it.
152    //
153    //    - Configure `allowedUnsignedAttributes`: You may also explicitly list
154    //      a set of attributes that should be considered unauthenticated when encountered
155    //      on read. Be careful if you use this configuration. Do not remove an attribute
156    //      name from this configuration, even if you are no longer writing with that attribute,
157    //      as old items may still include this attribute, and our configuration needs to know
158    //      to continue to exclude this attribute from the signature scope.
159    //      If you add new attribute names to this field, you must first deploy the update to this
160    //      field to all readers in your host fleet before deploying the update to start writing
161    //      with that new attribute.
162    //
163    //   For this example, we currently authenticate all attributes. To make it easier to
164    //   add unauthenticated attributes in the future, we define a prefix ":" for such attributes.
165    const UNSIGNED_ATTR_PREFIX: &str = ":";
166
167    // 6. Create the DynamoDb Encryption configuration for the table we will be writing to.
168    let table_config = DynamoDbTableEncryptionConfig::builder()
169        .logical_table_name(ddb_table_name)
170        .partition_key_name("partition_key")
171        .sort_key_name("sort_key")
172        .attribute_actions_on_encrypt(attribute_actions_on_encrypt)
173        .keyring(hierarchical_keyring)
174        .allowed_unsigned_attribute_prefix(UNSIGNED_ATTR_PREFIX)
175        .build()?;
176
177    let table_configs = DynamoDbTablesEncryptionConfig::builder()
178        .table_encryption_configs(HashMap::from([(ddb_table_name.to_string(), table_config)]))
179        .build()?;
180
181    // 7. Create a new AWS SDK DynamoDb client using the DynamoDb Encryption Interceptor above
182    let sdk_config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await;
183    let dynamo_config = aws_sdk_dynamodb::config::Builder::from(&sdk_config)
184        .interceptor(DbEsdkInterceptor::new(table_configs)?)
185        .build();
186    let ddb = aws_sdk_dynamodb::Client::from_conf(dynamo_config);
187
188    // 8. Put an item into our table using the above client.
189    //    Before the item gets sent to DynamoDb, it will be encrypted
190    //    client-side, according to our configuration.
191    //    Because the item we are writing uses "tenantId1" as our partition value,
192    //    based on the code we wrote in the ExampleBranchKeySupplier,
193    //    `tenant1BranchKeyId` will be used to encrypt this item.
194    let item = HashMap::from([
195        (
196            "partition_key".to_string(),
197            AttributeValue::S("tenant1Id".to_string()),
198        ),
199        ("sort_key".to_string(), AttributeValue::N("0".to_string())),
200        (
201            "tenant_sensitive_data".to_string(),
202            AttributeValue::S("encrypt and sign me!".to_string()),
203        ),
204    ]);
205
206    ddb.put_item()
207        .table_name(ddb_table_name)
208        .set_item(Some(item.clone()))
209        .send()
210        .await?;
211
212    // 9. Get the item back from our table using the same client.
213    //     The client will decrypt the item client-side, and return
214    //     back the original item.
215    //     Because the returned item's partition value is "tenantId1",
216    //     based on the code we wrote in the ExampleBranchKeySupplier,
217    //     `tenant1BranchKeyId` will be used to decrypt this item.
218    let key_to_get = HashMap::from([
219        (
220            "partition_key".to_string(),
221            AttributeValue::S("tenant1Id".to_string()),
222        ),
223        ("sort_key".to_string(), AttributeValue::N("0".to_string())),
224    ]);
225
226    let resp = ddb
227        .get_item()
228        .table_name(ddb_table_name)
229        .set_key(Some(key_to_get))
230        .consistent_read(true)
231        .send()
232        .await?;
233
234    assert_eq!(resp.item, Some(item));
235    println!("hierarchical_keyring successful.");
236    Ok(())
237}