diff --git a/Examples/runtimes/java/DynamoDbEncryption/src/main/java/software/amazon/cryptography/examples/keyring/SharedCacheAcrossHierarchicalKeyringsExample.java b/Examples/runtimes/java/DynamoDbEncryption/src/main/java/software/amazon/cryptography/examples/keyring/SharedCacheAcrossHierarchicalKeyringsExample.java index dfed4b84d..62fb6d711 100644 --- a/Examples/runtimes/java/DynamoDbEncryption/src/main/java/software/amazon/cryptography/examples/keyring/SharedCacheAcrossHierarchicalKeyringsExample.java +++ b/Examples/runtimes/java/DynamoDbEncryption/src/main/java/software/amazon/cryptography/examples/keyring/SharedCacheAcrossHierarchicalKeyringsExample.java @@ -180,7 +180,7 @@ public static void SharedCacheAcrossHierarchicalKeyringsGetItemPutItem( final IKeyring hierarchicalKeyring1 = matProv.CreateAwsKmsHierarchicalKeyring(keyringInput1); - // 4. Configure which attributes are encrypted and/or signed when writing new items. + // 5. Configure which attributes are encrypted and/or signed when writing new items. // For each attribute that may exist on the items we plan to write to our DynamoDbTable, // we must explicitly configure how they should be treated during item encryption: // - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature @@ -194,14 +194,14 @@ public static void SharedCacheAcrossHierarchicalKeyringsGetItemPutItem( CryptoAction.ENCRYPT_AND_SIGN ); - // 5. Get the DDB Client for Hierarchical Keyring 1. + // 6. Get the DDB Client for Hierarchical Keyring 1. final DynamoDbClient ddbClient1 = GetDdbClient( ddbTableName, hierarchicalKeyring1, attributeActionsOnEncrypt ); - // 6. Encrypt Decrypt roundtrip with ddbClient1 + // 7. Encrypt Decrypt roundtrip with ddbClient1 PutGetItems(ddbTableName, ddbClient1); // Through the above encrypt and decrypt roundtrip, the cache will be populated and @@ -210,7 +210,7 @@ public static void SharedCacheAcrossHierarchicalKeyringsGetItemPutItem( // - Same Logical Key Store Name of the Key Store for the Hierarchical Keyring // - Same Branch Key ID - // 7. Configure your KeyStore resource keystore2. + // 8. Configure your KeyStore resource keystore2. // This SHOULD be the same configuration that you used // to initially create and populate your physical KeyStore. // Note that keyStoreTableName is the physical Key Store, @@ -243,7 +243,7 @@ public static void SharedCacheAcrossHierarchicalKeyringsGetItemPutItem( ) .build(); - // 8. Create the Hierarchical Keyring HK2 with Key Store instance K2, the shared Cache + // 9. Create the Hierarchical Keyring HK2 with Key Store instance K2, the shared Cache // and the same partitionId and BranchKeyId used in HK1 because we want to share cache entries // (and experience cache HITS). @@ -262,14 +262,14 @@ public static void SharedCacheAcrossHierarchicalKeyringsGetItemPutItem( final IKeyring hierarchicalKeyring2 = matProv.CreateAwsKmsHierarchicalKeyring(keyringInput2); - // 9. Get the DDB Client for Hierarchical Keyring 2. + // 10. Get the DDB Client for Hierarchical Keyring 2. final DynamoDbClient ddbClient2 = GetDdbClient( ddbTableName, hierarchicalKeyring2, attributeActionsOnEncrypt ); - // 10. Encrypt Decrypt roundtrip with ddbClient2 + // 11. Encrypt Decrypt roundtrip with ddbClient2 PutGetItems(ddbTableName, ddbClient2); } diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/create_keystore_key_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/create_keystore_key_example.py new file mode 100644 index 000000000..6957290f8 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/create_keystore_key_example.py @@ -0,0 +1,49 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Example for creating a new key in a KeyStore. + +The Hierarchical Keyring Example and Searchable Encryption Examples rely on the +existence of a DDB-backed key store with pre-existing branch key material or +beacon key material. + +See the "Create KeyStore Table Example" for how to first set up the DDB Table +that will back this KeyStore. + +Demonstrates configuring a KeyStore and using a helper method to create a branch +key and beacon key that share the same Id. A new beacon key is always created +alongside a new branch key, even if searchable encryption is not being used. + +Note: This key creation should occur within your control plane. +""" + +import boto3 +from aws_cryptographic_material_providers.keystore.client import KeyStore +from aws_cryptographic_material_providers.keystore.config import KeyStoreConfig +from aws_cryptographic_material_providers.keystore.models import ( + CreateKeyInput, + KMSConfigurationKmsKeyArn, +) + + +def keystore_create_key(key_store_table_name: str, logical_key_store_name: str, kms_key_arn: str) -> str: + """Create a new branch key and beacon key in our KeyStore.""" + # 1. Configure your KeyStore resource. + # This SHOULD be the same configuration that was used to create the DDB table + # in the "Create KeyStore Table Example". + keystore: KeyStore = KeyStore( + KeyStoreConfig( + ddb_table_name=key_store_table_name, + kms_configuration=KMSConfigurationKmsKeyArn(kms_key_arn), + logical_key_store_name=logical_key_store_name, + kms_client=boto3.client("kms"), + ddb_client=boto3.client("dynamodb"), + ) + ) + + # 2. Create a new branch key and beacon key in our KeyStore. + # Both the branch key and the beacon key will share an Id. + # This creation is eventually consistent. + branch_key_id = keystore.create_key(CreateKeyInput()).branch_key_identifier + + return branch_key_id diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/create_keystore_table_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/create_keystore_table_example.py new file mode 100644 index 000000000..d154e33c2 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/create_keystore_table_example.py @@ -0,0 +1,59 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Example for creating a DynamoDB table for use as a KeyStore. + +The Hierarchical Keyring Example and Searchable Encryption Examples rely on the +existence of a DDB-backed key store with pre-existing branch key material or +beacon key material. + +Shows how to configure a KeyStore and use a helper method to create the DDB table +that will be used to persist branch keys and beacons keys for this KeyStore. + +This table creation should occur within your control plane and only needs to occur +once. While not demonstrated in this example, you should additionally use the +`VersionKey` API on the KeyStore to periodically rotate your branch key material. +""" + +import boto3 +from aws_cryptographic_material_providers.keystore.client import KeyStore +from aws_cryptographic_material_providers.keystore.config import KeyStoreConfig +from aws_cryptographic_material_providers.keystore.models import ( + CreateKeyStoreInput, + KMSConfigurationKmsKeyArn, +) + + +def keystore_create_table(keystore_table_name: str, logical_keystore_name: str, kms_key_arn: str): + """ + Create KeyStore Table Example. + + :param keystore_table_name: The name of the DynamoDB table to create + :param logical_keystore_name: The logical name for this keystore + :param kms_key_arn: The ARN of the KMS key to use for protecting branch keys + """ + # 1. Configure your KeyStore resource. + # `ddb_table_name` is the name you want for the DDB table that + # will back your keystore. + # `kms_key_arn` is the KMS Key that will protect your branch keys and beacon keys + # when they are stored in your DDB table. + keystore = KeyStore( + config=KeyStoreConfig( + ddb_client=boto3.client("dynamodb"), + ddb_table_name=keystore_table_name, + logical_key_store_name=logical_keystore_name, + kms_client=boto3.client("kms"), + kms_configuration=KMSConfigurationKmsKeyArn(kms_key_arn), + ) + ) + + # 2. Create the DynamoDb table that will store the branch keys and beacon keys. + # This checks if the correct table already exists at `ddb_table_name` + # by using the DescribeTable API. If no table exists, + # it will create one. If a table exists, it will verify + # the table's configuration and will error if the configuration is incorrect. + keystore.create_key_store(input=CreateKeyStoreInput()) + # It may take a couple of minutes for the table to become ACTIVE, + # at which point it is ready to store branch and beacon keys. + # See the Create KeyStore Key Example for how to populate + # this table. diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/item_encryptor/encrypt_decrypt_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/item_encryptor/encrypt_decrypt_example.py index 40e5e15b0..daf8082f0 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/src/item_encryptor/encrypt_decrypt_example.py +++ b/Examples/runtimes/python/DynamoDBEncryption/src/item_encryptor/encrypt_decrypt_example.py @@ -45,7 +45,7 @@ ) -def encrypt_decrypt_example(kms_key_id: str, ddb_table_name: str) -> None: +def encrypt_decrypt_example(kms_key_id: str, ddb_table_name: str): """Encrypt and decrypt an item with an ItemEncryptor.""" # 1. Create a Keyring. This Keyring will be responsible for protecting the data keys that protect your data. # For this example, we will create a AWS KMS Keyring with the AWS KMS Key we want to use. diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/__init__.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/example_branch_key_id_supplier.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/example_branch_key_id_supplier.py new file mode 100644 index 000000000..f16218a46 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/example_branch_key_id_supplier.py @@ -0,0 +1,61 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Example implementation of a branch key ID supplier. + +Used in the 'HierarchicalKeyringExample'. +In that example, we have a table where we distinguish multiple tenants +by a tenant ID that is stored in our partition attribute. +The expectation is that this does not produce a confused deputy +because the tenants are separated by partition. +In order to create a Hierarchical Keyring that is capable of encrypting or +decrypting data for either tenant, we implement this interface +to map the correct branch key ID to the correct tenant ID. +""" +from typing import Dict + +from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb.references import ( + IDynamoDbKeyBranchKeyIdSupplier, +) +from aws_dbesdk_dynamodb.structures.dynamodb import GetBranchKeyIdFromDdbKeyInput, GetBranchKeyIdFromDdbKeyOutput + + +class ExampleBranchKeyIdSupplier(IDynamoDbKeyBranchKeyIdSupplier): + """Example implementation of a branch key ID supplier.""" + + branch_key_id_for_tenant1: str + branch_key_id_for_tenant2: str + + def __init__(self, tenant1_id: str, tenant2_id: str): + """ + Initialize a branch key ID supplier. + + :param tenant1_id: Branch key ID for tenant 1 + :param tenant2_id: Branch key ID for tenant 2 + """ + self.branch_key_id_for_tenant1 = tenant1_id + self.branch_key_id_for_tenant2 = tenant2_id + + def get_branch_key_id_from_ddb_key(self, param: GetBranchKeyIdFromDdbKeyInput) -> GetBranchKeyIdFromDdbKeyOutput: + """ + Get branch key ID from the tenant ID in input's DDB key. + + :param param: Input containing DDB key + :return: Output containing branch key ID + :raises ValueError: If DDB key is invalid or contains invalid tenant ID + """ + key: Dict[str, Dict] = param.ddb_key + + if "partition_key" not in key: + raise ValueError("Item invalid, does not contain expected partition key attribute.") + + tenant_key_id = key["partition_key"]["S"] + + if tenant_key_id == "tenant1Id": + branch_key_id = self.branch_key_id_for_tenant1 + elif tenant_key_id == "tenant2Id": + branch_key_id = self.branch_key_id_for_tenant2 + else: + raise ValueError("Item does not contain valid tenant ID") + + return GetBranchKeyIdFromDdbKeyOutput(branch_key_id=branch_key_id) diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example.py new file mode 100644 index 000000000..9c68125e6 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example.py @@ -0,0 +1,229 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Example demonstrating DynamoDb Encryption using a Hierarchical Keyring. + +This example sets up DynamoDb Encryption for the AWS SDK client +using the Hierarchical Keyring, which establishes a key hierarchy +where "branch" keys are persisted in DynamoDb. +These branch keys are used to protect your data keys, +and these branch keys are themselves protected by a root KMS Key. + +Establishing a key hierarchy like this has two benefits: + +First, by caching the branch key material, and only calling back +to KMS to re-establish authentication regularly according to your configured TTL, +you limit how often you need to call back to KMS to protect your data. +This is a performance/security tradeoff, where your authentication, audit, and +logging from KMS is no longer one-to-one with every encrypt or decrypt call. +However, the benefit is that you no longer have to make a +network call to KMS for every encrypt or decrypt. + +Second, this key hierarchy makes it easy to hold multi-tenant data +that is isolated per branch key in a single DynamoDb table. +You can create a branch key for each tenant in your table, +and encrypt all that tenant's data under that distinct branch key. +On decrypt, you can either statically configure a single branch key +to ensure you are restricting decryption to a single tenant, +or you can implement an interface that lets you map the primary key on your items +to the branch key that should be responsible for decrypting that data. + +This example then demonstrates configuring a Hierarchical Keyring +with a Branch Key ID Supplier to encrypt and decrypt data for +two separate tenants. + +Running this example requires access to the DDB Table whose name +is provided in CLI arguments. +This table must be configured with the following +primary key configuration: + - Partition key is named "partition_key" with type (S) + - Sort key is named "sort_key" with type (S) + +This example also requires using a KMS Key whose ARN +is provided in CLI arguments. You need the following access +on this key: + - GenerateDataKeyWithoutPlaintext + - Decrypt +""" + +import boto3 +from aws_cryptographic_material_providers.keystore.client import KeyStore +from aws_cryptographic_material_providers.keystore.config import KeyStoreConfig +from aws_cryptographic_material_providers.keystore.models import KMSConfigurationKmsKeyArn +from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders +from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig +from aws_cryptographic_material_providers.mpl.models import ( + CacheTypeDefault, + CreateAwsKmsHierarchicalKeyringInput, + DefaultCache, +) +from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient +from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb.client import DynamoDbEncryption +from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb.config import ( + DynamoDbEncryptionConfig, +) +from aws_dbesdk_dynamodb.structures.dynamodb import ( + CreateDynamoDbEncryptionBranchKeyIdSupplierInput, + DynamoDbTableEncryptionConfig, + DynamoDbTablesEncryptionConfig, +) +from aws_dbesdk_dynamodb.structures.structured_encryption import ( + CryptoAction, +) + +from .example_branch_key_id_supplier import ExampleBranchKeyIdSupplier + + +def hierarchical_keyring_get_item_put_item( + ddb_table_name: str, + tenant1_branch_key_id: str, + tenant2_branch_key_id: str, + keystore_table_name: str, + logical_keystore_name: str, + kms_key_id: str, +): + """ + Demonstrate using a hierarchical keyring with multiple tenants. + + :param ddb_table_name: The name of the DynamoDB table + :param tenant1_branch_key_id: Branch key ID for tenant 1 + :param tenant2_branch_key_id: Branch key ID for tenant 2 + :param keystore_table_name: The name of the KeyStore DynamoDB table + :param logical_keystore_name: The logical name for this keystore + :param kms_key_id: The ARN of the KMS key to use + """ + # Initial KeyStore Setup: This example requires that you have already + # created your KeyStore, and have populated it with two new branch keys. + # See the "Create KeyStore Table Example" and "Create KeyStore Key Example" + # for an example of how to do this. + + # 1. Configure your KeyStore resource. + # This SHOULD be the same configuration that you used + # to initially create and populate your KeyStore. + keystore = KeyStore( + config=KeyStoreConfig( + ddb_client=boto3.client("dynamodb"), + ddb_table_name=keystore_table_name, + logical_key_store_name=logical_keystore_name, + kms_client=boto3.client("kms"), + kms_configuration=KMSConfigurationKmsKeyArn(kms_key_id), + ) + ) + + # 2. Create a Branch Key ID Supplier. See ExampleBranchKeyIdSupplier in this directory. + ddb_enc = DynamoDbEncryption(config=DynamoDbEncryptionConfig()) + branch_key_id_supplier = ddb_enc.create_dynamo_db_encryption_branch_key_id_supplier( + input=CreateDynamoDbEncryptionBranchKeyIdSupplierInput( + ddb_key_branch_key_id_supplier=ExampleBranchKeyIdSupplier(tenant1_branch_key_id, tenant2_branch_key_id) + ) + ).branch_key_id_supplier + + # 3. Create the Hierarchical Keyring, using the Branch Key ID Supplier above. + # With this configuration, the AWS SDK Client ultimately configured will be capable + # of encrypting or decrypting items for either tenant (assuming correct KMS access). + # If you want to restrict the client to only encrypt or decrypt for a single tenant, + # configure this Hierarchical Keyring using `.branch_key_id=tenant1_branch_key_id` instead + # of `.branch_key_id_supplier=branch_key_id_supplier`. + mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) + + keyring_input = CreateAwsKmsHierarchicalKeyringInput( + key_store=keystore, + branch_key_id_supplier=branch_key_id_supplier, + ttl_seconds=600, # This dictates how often we call back to KMS to authorize use of the branch keys + cache=CacheTypeDefault( # This dictates how many branch keys will be held locally + value=DefaultCache(entry_capacity=100) + ), + ) + + hierarchical_keyring = mat_prov.create_aws_kms_hierarchical_keyring(input=keyring_input) + + # 4. Configure which attributes are encrypted and/or signed when writing new items. + # For each attribute that may exist on the items we plan to write to our DynamoDbTable, + # we must explicitly configure how they should be treated during item encryption: + # - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature + # - SIGN_ONLY: The attribute not encrypted, but is still included in the signature + # - DO_NOTHING: The attribute is not encrypted and not included in the signature + attribute_actions = { + "partition_key": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY + "sort_key": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY + "tenant_sensitive_data": CryptoAction.ENCRYPT_AND_SIGN, + } + + # 5. Configure which attributes we expect to be included in the signature + # when reading items. There are two options for configuring this: + # + # - (Recommended) Configure `allowed_unsigned_attribute_prefix`: + # When defining your DynamoDb schema and deciding on attribute names, + # choose a distinguishing prefix (such as ":") for all attributes that + # you do not want to include in the signature. + # This has two main benefits: + # - It is easier to reason about the security and authenticity of data within your item + # when all unauthenticated data is easily distinguishable by their attribute name. + # - If you need to add new unauthenticated attributes in the future, + # you can easily make the corresponding update to your `attribute_actions` + # and immediately start writing to that new attribute, without + # any other configuration update needed. + # Once you configure this field, it is not safe to update it. + # + # - Configure `allowed_unsigned_attributes`: You may also explicitly list + # a set of attributes that should be considered unauthenticated when encountered + # on read. Be careful if you use this configuration. Do not remove an attribute + # name from this configuration, even if you are no longer writing with that attribute, + # as old items may still include this attribute, and our configuration needs to know + # to continue to exclude this attribute from the signature scope. + # If you add new attribute names to this field, you must first deploy the update to this + # field to all readers in your host fleet before deploying the update to start writing + # with that new attribute. + # + # For this example, we currently authenticate all attributes. To make it easier to + # add unauthenticated attributes in the future, we define a prefix ":" for such attributes. + unsign_attr_prefix = ":" + + # 6. Create the DynamoDb Encryption configuration for the table we will be writing to. + table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="partition_key", + sort_key_name="sort_key", + attribute_actions_on_encrypt=attribute_actions, + keyring=hierarchical_keyring, + allowed_unsigned_attribute_prefix=unsign_attr_prefix, + ) + + table_configs = {ddb_table_name: table_config} + tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) + + # 7. Create the EncryptedClient + ddb_client = boto3.client("dynamodb") + encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) + + # 8. Put an item into our table using the above client. + # Before the item gets sent to DynamoDb, it will be encrypted + # client-side, according to our configuration. + # Because the item we are writing uses "tenantId1" as our partition value, + # based on the code we wrote in the ExampleBranchKeySupplier, + # `tenant1_branch_key_id` will be used to encrypt this item. + item = { + "partition_key": {"S": "tenant1Id"}, + "sort_key": {"N": "0"}, + "tenant_sensitive_data": {"S": "encrypt and sign me!"}, + } + + put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item) + + # Demonstrate that PutItem succeeded + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 9. Get the item back from our table using the same client. + # The client will decrypt the item client-side, and return + # back the original item. + # Because the returned item's partition value is "tenantId1", + # based on the code we wrote in the ExampleBranchKeySupplier, + # `tenant1_branch_key_id` will be used to decrypt this item. + key_to_get = {"partition_key": {"S": "tenant1Id"}, "sort_key": {"N": "0"}} + + get_response = encrypted_ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + returned_item = get_response["Item"] + assert returned_item["tenant_sensitive_data"]["S"] == "encrypt and sign me!" diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_ecdh_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_ecdh_keyring_example.py new file mode 100644 index 000000000..3af215b1f --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_ecdh_keyring_example.py @@ -0,0 +1,451 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +These examples set up DynamoDb Encryption for the AWS SDK client using the AWS KMS ECDH Keyring. + +This keyring, depending on its KeyAgreement scheme, +takes in the sender's KMS ECC Key ARN, and the recipient's ECC Public Key to derive a shared secret. +The keyring uses the shared secret to derive a data key to protect the +data keys that encrypt and decrypt DynamoDb table items. + +Running these examples require access to the DDB Table whose name +is provided in CLI arguments. +This table must be configured with the following +primary key configuration: + - Partition key is named "partition_key" with type (S) + - Sort key is named "sort_key" with type (S) +""" + +import pathlib + +import boto3 +from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders +from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig +from aws_cryptographic_material_providers.mpl.models import ( + CreateAwsKmsEcdhKeyringInput, + DBEAlgorithmSuiteId, + KmsEcdhStaticConfigurationsKmsPrivateKeyToStaticPublicKey, + KmsEcdhStaticConfigurationsKmsPublicKeyDiscovery, + KmsPrivateKeyToStaticPublicKeyInput, + KmsPublicKeyDiscoveryInput, +) +from aws_cryptographic_material_providers.mpl.references import IKeyring +from aws_cryptography_primitives.smithygenerated.aws_cryptography_primitives.models import ECDHCurveSpec +from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient +from aws_dbesdk_dynamodb.structures.dynamodb import ( + DynamoDbTableEncryptionConfig, + DynamoDbTablesEncryptionConfig, +) +from aws_dbesdk_dynamodb.structures.structured_encryption import ( + CryptoAction, +) +from cryptography.hazmat.primitives import serialization + +EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME = "KmsEccKeyringKeyringExamplePublicKeySender.pem" +EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME = "KmsEccKeyringKeyringExamplePublicKeyRecipient.pem" + + +def kms_ecdh_keyring_get_item_put_item( + ddb_table_name: str, + ecc_key_arn: str, + ecc_public_key_sender_filename: str = EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME, + ecc_public_key_recipient_filename: str = EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME, +): + """ + Demonstrate using a KMS ECDH keyring with static keys. + + This example takes in the sender's KMS ECC key ARN, the sender's public key, + the recipient's public key, and the algorithm definition where the ECC keys lie. + The ecc_key_arn parameter takes in the sender's KMS ECC key ARN, + the ecc_public_key_sender_filename parameter takes in the sender's public key that corresponds to the + ecc_key_arn, the ecc_public_key_recipient_filename parameter takes in the recipient's public key, + and the Curve Specification where the keys lie. + + Both public keys MUST be UTF8 PEM-encoded X.509 public key, also known as SubjectPublicKeyInfo (SPKI) + + This example encrypts a test item using the provided ECC keys and puts the + encrypted item to the provided DynamoDb table. Then, it gets the + item from the table and decrypts it. + + Running this example requires access to the DDB Table whose name + is provided in CLI arguments. + This table must be configured with the following + primary key configuration: + - Partition key is named "partition_key" with type (S) + - Sort key is named "sort_key" with type (S) + This example also requires access to a KMS ECC key. + Our tests provide a KMS ECC Key ARN that anyone can use, but you + can also provide your own KMS ECC key. + To use your own KMS ECC key, you must have either: + - Its public key downloaded in a UTF-8 encoded PEM file + - kms:GetPublicKey permissions on that key. + If you do not have the public key downloaded, running this example + through its main method will download the public key for you + by calling kms:GetPublicKey. + You must also have kms:DeriveSharedSecret permissions on the KMS ECC key. + This example also requires a recipient ECC Public Key that lies on the same + curve as the sender public key. This examples uses another distinct + KMS ECC Public Key, it does not have to be a KMS key; it can be a + valid SubjectPublicKeyInfo (SPKI) Public Key. + + :param ddb_table_name: The name of the DynamoDB table + :param ecc_key_arn: The ARN of the KMS ECC key to use + :param ecc_public_key_sender_filename: The filename containing the sender's public key + :param ecc_public_key_recipient_filename: The filename containing the recipient's public key + """ + # Load UTF-8 encoded public key PEM files as DER encoded bytes. + # You may provide your own PEM files to use here. If you provide this, it MUST + # be a key on curve P256. + # If not, the main method in this class will call + # the KMS ECC key, retrieve its public key, and store it + # in a PEM file for example use. + public_key_recipient_bytes = load_public_key_bytes(ecc_public_key_recipient_filename) + public_key_sender_bytes = load_public_key_bytes(ecc_public_key_sender_filename) + + # Create a KMS ECDH keyring. + # This keyring uses the KmsPrivateKeyToStaticPublicKey configuration. This configuration calls for both of + # the keys to be on the same curve (P256, P384, P521). + # On encrypt, the keyring calls AWS KMS to derive the shared secret from the sender's KMS ECC Key ARN + # and the recipient's public key. + # For this example, on decrypt, the keyring calls AWS KMS to derive the shared secret from the + # sender's KMS ECC Key ARN and the recipient's public key; + # however, on decrypt, the recipient can construct a keyring such that the shared secret is calculated with + # the recipient's private key and the sender's public key. In both scenarios the shared secret will be the same. + # For more information on this configuration see: + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/use-kms-ecdh-keyring.html#kms-ecdh-create + # The DynamoDb encryption client uses this keyring to encrypt and decrypt items. + # This keyring takes in: + # - kms_client + # - kms_key_id: Must be an ARN representing a KMS ECC key meant for KeyAgreement + # - curve_spec: The curve name where the public keys lie + # - sender_public_key: A ByteBuffer of a UTF-8 encoded public + # key for the key passed into kms_key_id in DER format + # - recipient_public_key: A ByteBuffer of a UTF-8 encoded public + # key for the key passed into kms_key_id in DER format + mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) + + keyring_input = CreateAwsKmsEcdhKeyringInput( + kms_client=boto3.client("kms"), + curve_spec=ECDHCurveSpec.ECC_NIST_P256, + key_agreement_scheme=KmsEcdhStaticConfigurationsKmsPrivateKeyToStaticPublicKey( + KmsPrivateKeyToStaticPublicKeyInput( + sender_kms_identifier=ecc_key_arn, + # Must be a DER-encoded X.509 public key + sender_public_key=public_key_sender_bytes, + # Must be a DER-encoded X.509 public key + recipient_public_key=public_key_recipient_bytes, + ) + ), + ) + + kms_ecdh_keyring = mat_prov.create_aws_kms_ecdh_keyring(input=keyring_input) + + put_get_item_with_keyring(kms_ecdh_keyring, ddb_table_name) + + +def kms_ecdh_discovery_get_item(ddb_table_name: str, ecc_recipient_key_arn: str): + """ + Demonstrate using a KMS ECDH keyring with discovery. + + This example takes in the recipient's KMS ECC key ARN via + the ecc_recipient_key_arn parameter. + + This example attempts to decrypt a test item using the provided ecc_recipient_key_arn, + it does so by checking if the message header contains the recipient's public key. + + Running this example requires access to the DDB Table whose name + is provided in CLI arguments. + This table must be configured with the following + primary key configuration: + - Partition key is named "partition_key" with type (S) + - Sort key is named "sort_key" with type (S) + This example also requires access to a KMS ECC key. + Our tests provide a KMS ECC Key ARN that anyone can use, but you + can also provide your own KMS ECC key. + To use your own KMS ECC key, you must have: + - kms:GetPublicKey permissions on that key. + This example will call kms:GetPublicKey on keyring creation. + You must also have kms:DeriveSharedSecret permissions on the KMS ECC key. + + :param ddb_table_name: The name of the DynamoDB table + :param ecc_recipient_key_arn: The ARN of the recipient's KMS ECC key + """ + # Create a KMS ECDH keyring. + # This keyring uses the KmsPublicKeyDiscovery configuration. + # On encrypt, the keyring will fail as it is not allowed to encrypt data under this configuration. + # On decrypt, the keyring will check if its corresponding public key is stored in the message header. It + # will AWS KMS to derive the shared from the recipient's KMS ECC Key ARN and the sender's public key; + # For more information on this configuration see: + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/use-kms-ecdh-keyring.html#kms-ecdh-discovery + # The DynamoDb encryption client uses this to encrypt and decrypt items. + # This keyring takes in: + # - kms_client + # - recipient_kms_identifier: Must be an ARN representing a KMS ECC key meant for KeyAgreement + # - curve_spec: The curve name where the public keys lie + mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) + + keyring_input = CreateAwsKmsEcdhKeyringInput( + kms_client=boto3.client("kms"), + curve_spec=ECDHCurveSpec.ECC_NIST_P256, + key_agreement_scheme=KmsEcdhStaticConfigurationsKmsPublicKeyDiscovery( + KmsPublicKeyDiscoveryInput(recipient_kms_identifier=ecc_recipient_key_arn) + ), + ) + + kms_ecdh_keyring = mat_prov.create_aws_kms_ecdh_keyring(input=keyring_input) + + get_item_with_keyring(kms_ecdh_keyring, ddb_table_name) + + +def get_item_with_keyring(kms_ecdh_keyring: IKeyring, ddb_table_name: str): + """ + Demonstrate get operation with a KMS ECDH keyring. + + :param kms_ecdh_keyring: The KMS ECDH keyring to use + :param ddb_table_name: The name of the DynamoDB table + """ + # Configure which attributes are encrypted and/or signed when writing new items. + # For each attribute that may exist on the items we plan to write to our DynamoDbTable, + # we must explicitly configure how they should be treated during item encryption: + # - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature + # - SIGN_ONLY: The attribute not encrypted, but is still included in the signature + # - DO_NOTHING: The attribute is not encrypted and not included in the signature + attribute_actions = { + "partition_key": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY + "sort_key": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY + "sensitive_data": CryptoAction.ENCRYPT_AND_SIGN, + } + + # Configure which attributes we expect to be included in the signature + # when reading items. There are two options for configuring this: + # + # - (Recommended) Configure `allowed_unsigned_attribute_prefix`: + # When defining your DynamoDb schema and deciding on attribute names, + # choose a distinguishing prefix (such as ":") for all attributes that + # you do not want to include in the signature. + # This has two main benefits: + # - It is easier to reason about the security and authenticity of data within your item + # when all unauthenticated data is easily distinguishable by their attribute name. + # - If you need to add new unauthenticated attributes in the future, + # you can easily make the corresponding update to your `attribute_actions` + # and immediately start writing to that new attribute, without + # any other configuration update needed. + # Once you configure this field, it is not safe to update it. + # + # - Configure `allowed_unsigned_attributes`: You may also explicitly list + # a set of attributes that should be considered unauthenticated when encountered + # on read. Be careful if you use this configuration. Do not remove an attribute + # name from this configuration, even if you are no longer writing with that attribute, + # as old items may still include this attribute, and our configuration needs to know + # to continue to exclude this attribute from the signature scope. + # If you add new attribute names to this field, you must first deploy the update to this + # field to all readers in your host fleet before deploying the update to start writing + # with that new attribute. + # + # For this example, we currently authenticate all attributes. To make it easier to + # add unauthenticated attributes in the future, we define a prefix ":" for such attributes. + unsign_attr_prefix = ":" + + # Create the DynamoDb Encryption configuration for the table we will be writing to. + # Note: To use the KMS RSA keyring, your table config must specify an algorithmSuite + # that does not use asymmetric signing. + table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="partition_key", + sort_key_name="sort_key", + attribute_actions_on_encrypt=attribute_actions, + keyring=kms_ecdh_keyring, + allowed_unsigned_attribute_prefix=unsign_attr_prefix, + # Specify algorithmSuite without asymmetric signing here + # As of v3.0.0, the only supported algorithmSuite without asymmetric signing is + # ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_SYMSIG_HMAC_SHA384. + algorithm_suite_id=DBEAlgorithmSuiteId.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_SYMSIG_HMAC_SHA384, + ) + + table_configs = {ddb_table_name: table_config} + tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) + + # Create the EncryptedClient + ddb_client = boto3.client("dynamodb") + encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) + + # Get the item back from our table using the client. + # The client will decrypt the item client-side using the ECDH keyring + # and return the original item. + key_to_get = {"partition_key": {"S": "awsKmsEcdhKeyringItem"}, "sort_key": {"N": "0"}} + + get_response = encrypted_ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + returned_item = get_response["Item"] + assert returned_item["sensitive_data"]["S"] == "encrypt and sign me!" + + +def put_get_item_with_keyring(aws_kms_ecdh_keyring: IKeyring, ddb_table_name: str): + """ + Demonstrate put and get operations with a KMS ECDH keyring. + + :param aws_kms_ecdh_keyring: The KMS ECDH keyring to use + :param ddb_table_name: The name of the DynamoDB table + """ + # Configure which attributes are encrypted and/or signed when writing new items. + # For each attribute that may exist on the items we plan to write to our DynamoDbTable, + # we must explicitly configure how they should be treated during item encryption: + # - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature + # - SIGN_ONLY: The attribute not encrypted, but is still included in the signature + # - DO_NOTHING: The attribute is not encrypted and not included in the signature + attribute_actions = { + "partition_key": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY + "sort_key": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY + "sensitive_data": CryptoAction.ENCRYPT_AND_SIGN, + } + + # Configure which attributes we expect to be included in the signature + # when reading items. There are two options for configuring this: + # + # - (Recommended) Configure `allowed_unsigned_attribute_prefix`: + # When defining your DynamoDb schema and deciding on attribute names, + # choose a distinguishing prefix (such as ":") for all attributes that + # you do not want to include in the signature. + # This has two main benefits: + # - It is easier to reason about the security and authenticity of data within your item + # when all unauthenticated data is easily distinguishable by their attribute name. + # - If you need to add new unauthenticated attributes in the future, + # you can easily make the corresponding update to your `attribute_actions` + # and immediately start writing to that new attribute, without + # any other configuration update needed. + # Once you configure this field, it is not safe to update it. + # + # - Configure `allowed_unsigned_attributes`: You may also explicitly list + # a set of attributes that should be considered unauthenticated when encountered + # on read. Be careful if you use this configuration. Do not remove an attribute + # name from this configuration, even if you are no longer writing with that attribute, + # as old items may still include this attribute, and our configuration needs to know + # to continue to exclude this attribute from the signature scope. + # If you add new attribute names to this field, you must first deploy the update to this + # field to all readers in your host fleet before deploying the update to start writing + # with that new attribute. + # + # For this example, we currently authenticate all attributes. To make it easier to + # add unauthenticated attributes in the future, we define a prefix ":" for such attributes. + unsign_attr_prefix = ":" + + # Create the DynamoDb Encryption configuration for the table we will be writing to. + # Note: To use the KMS RSA keyring, your table config must specify an algorithmSuite + # that does not use asymmetric signing. + table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="partition_key", + sort_key_name="sort_key", + attribute_actions_on_encrypt=attribute_actions, + keyring=aws_kms_ecdh_keyring, + allowed_unsigned_attribute_prefix=unsign_attr_prefix, + # Specify algorithmSuite without asymmetric signing here + # As of v3.0.0, the only supported algorithmSuite without asymmetric signing is + # ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_SYMSIG_HMAC_SHA384. + algorithm_suite_id=DBEAlgorithmSuiteId.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_SYMSIG_HMAC_SHA384, + ) + + table_configs = {ddb_table_name: table_config} + tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) + + # Create the EncryptedClient + ddb_client = boto3.client("dynamodb") + encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) + + # Put an item into our table using the above client. + # Before the item gets sent to DynamoDb, it will be encrypted + # client-side, according to our configuration. + item = { + "partition_key": {"S": "awsKmsEcdhKeyringItem"}, + "sort_key": {"N": "0"}, + "sensitive_data": {"S": "encrypt and sign me!"}, + } + + put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item) + + # Demonstrate that PutItem succeeded + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # Get the item back from our table using the client. + # The client will decrypt the item client-side using the RSA keyring + # and return the original item. + key_to_get = {"partition_key": {"S": "awsKmsEcdhKeyringItem"}, "sort_key": {"N": "0"}} + + get_response = encrypted_ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + returned_item = get_response["Item"] + assert returned_item["sensitive_data"]["S"] == "encrypt and sign me!" + + +def load_public_key_bytes(ecc_public_key_filename: str) -> bytes: + """ + Load public key bytes from a PEM file. + + :param ecc_public_key_filename: The filename containing the public key + :return: The public key bytes + """ + try: + with open(ecc_public_key_filename, "rb") as f: + public_key_file_bytes = f.read() + public_key = serialization.load_pem_public_key(public_key_file_bytes) + return public_key.public_bytes( + encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo + ) + except IOError as e: + raise OSError("IOError while reading public key from file") from e + + +def should_get_new_public_keys() -> bool: + """ + Check if new public keys should be generated. + + :return: True if new keys should be generated, False otherwise + """ + # Check if public keys already exist + sender_public_key_file = pathlib.Path(EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME) + recipient_public_key_file = pathlib.Path(EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME) + + if sender_public_key_file.exists() or recipient_public_key_file.exists(): + return False + + if not sender_public_key_file.exists() and recipient_public_key_file.exists(): + raise FileNotFoundError(f"Missing public key sender file at {EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME}") + + if not recipient_public_key_file.exists() and sender_public_key_file.exists(): + raise FileNotFoundError(f"Missing public key recipient file at {EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME}") + + return True + + +def write_public_key_pem_for_ecc_key(ecc_key_arn: str, ecc_public_key_filename: str): + """ + Write a public key PEM file for an ECC key. + + :param ecc_key_arn: The ARN of the KMS ECC key + :param ecc_public_key_filename: The filename to write the public key to + """ + # Safety check: Validate file is not present + public_key_file = pathlib.Path(ecc_public_key_filename) + if public_key_file.exists(): + raise FileExistsError("writePublicKeyPemForEccKey will not overwrite existing PEM files") + + # This code will call KMS to get the public key for the KMS ECC key. + # You must have kms:GetPublicKey permissions on the key for this to succeed. + # The public key will be written to the file EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME + # or EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME. + kms_client = boto3.client("kms") + response = kms_client.get_public_key(KeyId=ecc_key_arn) + public_key_bytes = response["PublicKey"] + + # Write the public key to a PEM file + public_key = serialization.load_der_public_key(public_key_bytes) + pem_data = public_key.public_bytes( + encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo + ) + + with open(ecc_public_key_filename, "wb") as f: + f.write(pem_data) diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_rsa_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_rsa_keyring_example.py new file mode 100644 index 000000000..dd18e9e48 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_rsa_keyring_example.py @@ -0,0 +1,230 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Example demonstrating DynamoDb Encryption using a KMS RSA Keyring. + +The KMS RSA Keyring uses a KMS RSA key pair to encrypt and decrypt records. The client +uses the downloaded public key to encrypt items it adds to the table. The keyring +uses the private key to decrypt existing table items it retrieves by calling +KMS' decrypt API. + +Running this example requires access to the DDB Table whose name is provided +in CLI arguments. This table must be configured with the following primary key +configuration: + - Partition key is named "partition_key" with type (S) + - Sort key is named "sort_key" with type (S) + +The example also requires access to a KMS RSA key. Our tests provide a KMS RSA +ARN that anyone can use, but you can also provide your own KMS RSA key. +To use your own KMS RSA key, you must have either: + - Its public key downloaded in a UTF-8 encoded PEM file + - kms:GetPublicKey permissions on that key + +If you do not have the public key downloaded, running this example through its +main method will download the public key for you by calling kms:GetPublicKey. +You must also have kms:Decrypt permissions on the KMS RSA key. +""" + +import os + +import boto3 +from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders +from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig +from aws_cryptographic_material_providers.mpl.models import ( + CreateAwsKmsRsaKeyringInput, + DBEAlgorithmSuiteId, +) +from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient +from aws_dbesdk_dynamodb.structures.dynamodb import ( + DynamoDbTableEncryptionConfig, + DynamoDbTablesEncryptionConfig, +) +from aws_dbesdk_dynamodb.structures.structured_encryption import ( + CryptoAction, +) +from cryptography.hazmat.primitives import serialization + +DEFAULT_EXAMPLE_RSA_PUBLIC_KEY_FILENAME = "KmsRsaKeyringExamplePublicKey.pem" + + +def kms_rsa_keyring_example( + ddb_table_name: str, rsa_key_arn: str, rsa_public_key_filename: str = DEFAULT_EXAMPLE_RSA_PUBLIC_KEY_FILENAME +): + """ + Create a KMS RSA keyring and use it to encrypt/decrypt DynamoDB items. + + :param ddb_table_name: The name of the DynamoDB table + :param rsa_key_arn: ARN of the KMS RSA key + :param rsa_public_key_filename: Path to the public key PEM file + """ + # 1. Load UTF-8 encoded public key PEM file. + # You may have an RSA public key file already defined. + # If not, the main method in this class will call + # the KMS RSA key, retrieve its public key, and store it + # in a PEM file for example use. + try: + with open(rsa_public_key_filename, "rb") as f: + public_key_utf8_encoded = f.read() + except IOError as e: + raise RuntimeError("IOError while reading public key from file") from e + + # 2. Create a KMS RSA keyring. + # This keyring takes in: + # - kms_client + # - kms_key_id: Must be an ARN representing a KMS RSA key + # - public_key: A ByteBuffer of a UTF-8 encoded PEM file representing the public + # key for the key passed into kms_key_id + # - encryption_algorithm: Must be either RSAES_OAEP_SHA_256 or RSAES_OAEP_SHA_1 + mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) + + keyring_input = CreateAwsKmsRsaKeyringInput( + kms_key_id=rsa_key_arn, + kms_client=boto3.client("kms"), + public_key=public_key_utf8_encoded, + encryption_algorithm="RSAES_OAEP_SHA_256", + ) + + kms_rsa_keyring = mat_prov.create_aws_kms_rsa_keyring(input=keyring_input) + + # 3. Configure which attributes are encrypted and/or signed when writing new items. + # For each attribute that may exist on the items we plan to write to our DynamoDbTable, + # we must explicitly configure how they should be treated during item encryption: + # - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature + # - SIGN_ONLY: The attribute not encrypted, but is still included in the signature + # - DO_NOTHING: The attribute is not encrypted and not included in the signature + attribute_actions = { + "partition_key": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY + "sort_key": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY + "sensitive_data": CryptoAction.ENCRYPT_AND_SIGN, + } + + # 4. Configure which attributes we expect to be included in the signature + # when reading items. There are two options for configuring this: + # + # - (Recommended) Configure `allowed_unsigned_attribute_prefix`: + # When defining your DynamoDb schema and deciding on attribute names, + # choose a distinguishing prefix (such as ":") for all attributes that + # you do not want to include in the signature. + # This has two main benefits: + # - It is easier to reason about the security and authenticity of data within your item + # when all unauthenticated data is easily distinguishable by their attribute name. + # - If you need to add new unauthenticated attributes in the future, + # you can easily make the corresponding update to your `attribute_actions` + # and immediately start writing to that new attribute, without + # any other configuration update needed. + # Once you configure this field, it is not safe to update it. + # + # - Configure `allowed_unsigned_attributes`: You may also explicitly list + # a set of attributes that should be considered unauthenticated when encountered + # on read. Be careful if you use this configuration. Do not remove an attribute + # name from this configuration, even if you are no longer writing with that attribute, + # as old items may still include this attribute, and our configuration needs to know + # to continue to exclude this attribute from the signature scope. + # If you add new attribute names to this field, you must first deploy the update to this + # field to all readers in your host fleet before deploying the update to start writing + # with that new attribute. + # + # For this example, we currently authenticate all attributes. To make it easier to + # add unauthenticated attributes in the future, we define a prefix ":" for such attributes. + unsign_attr_prefix = ":" + + # 5. Create the DynamoDb Encryption configuration for the table we will be writing to. + # Note: To use the KMS RSA keyring, your table config must specify an algorithmSuite + # that does not use asymmetric signing. + table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="partition_key", + sort_key_name="sort_key", + attribute_actions_on_encrypt=attribute_actions, + keyring=kms_rsa_keyring, + allowed_unsigned_attribute_prefix=unsign_attr_prefix, + # Specify algorithmSuite without asymmetric signing here + # As of v3.0.0, the only supported algorithmSuite without asymmetric signing is + # ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_SYMSIG_HMAC_SHA384. + algorithm_suite_id=DBEAlgorithmSuiteId.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_SYMSIG_HMAC_SHA384, + ) + + table_configs = {ddb_table_name: table_config} + tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) + + # 6. Create the EncryptedClient + ddb_client = boto3.client("dynamodb") + encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) + + # 7. Put an item into our table using the above client. + # Before the item gets sent to DynamoDb, it will be encrypted + # client-side using the KMS RSA keyring. + item = { + "partition_key": {"S": "awsKmsRsaKeyringItem"}, + "sort_key": {"N": "0"}, + "sensitive_data": {"S": "encrypt and sign me!"}, + } + + put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item) + + # Demonstrate that PutItem succeeded + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 8. Get the item back from our table using the client. + # The client will decrypt the item client-side using the RSA keyring + # and return the original item. + key_to_get = {"partition_key": {"S": "awsKmsRsaKeyringItem"}, "sort_key": {"N": "0"}} + + get_response = encrypted_ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + returned_item = get_response["Item"] + assert returned_item["sensitive_data"]["S"] == "encrypt and sign me!" + + +def should_get_new_public_key(rsa_public_key_filename: str = DEFAULT_EXAMPLE_RSA_PUBLIC_KEY_FILENAME) -> bool: + """ + Check if we need to get a new public key. + + :param rsa_public_key_filename: Path to the public key PEM file + :return: True if we need to get a new public key, False otherwise + """ + # Check if a public key file already exists + public_key_file = os.path.exists(rsa_public_key_filename) + + # If a public key file already exists: do not overwrite existing file + if public_key_file: + return False + + # If file is not present, generate a new key pair + return True + + +def write_public_key_pem_for_rsa_key( + rsa_key_arn: str, rsa_public_key_filename: str = DEFAULT_EXAMPLE_RSA_PUBLIC_KEY_FILENAME +): + """ + Get the public key from KMS and write it to a PEM file. + + :param rsa_key_arn: The ARN of the KMS RSA key + :param rsa_public_key_filename: Path to write the public key PEM file + """ + # Safety check: Validate file is not present + if os.path.exists(rsa_public_key_filename): + raise RuntimeError("getRsaPublicKey will not overwrite existing PEM files") + + # This code will call KMS to get the public key for the KMS RSA key. + # You must have kms:GetPublicKey permissions on the key for this to succeed. + # The public key will be written to the file EXAMPLE_RSA_PUBLIC_KEY_FILENAME. + kms_client = boto3.client("kms") + response = kms_client.get_public_key(KeyId=rsa_key_arn) + public_key_bytes = response["PublicKey"] + + # Convert the public key to PEM format + public_key = serialization.load_der_public_key(public_key_bytes) + pem_data = public_key.public_bytes( + encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo + ) + + # Write the PEM file + try: + with open(rsa_public_key_filename, "wb") as f: + f.write(pem_data) + except IOError as e: + raise RuntimeError("IOError while writing public key PEM") from e diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_discovery_multi_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_discovery_multi_keyring_example.py new file mode 100644 index 000000000..7d6c77357 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_discovery_multi_keyring_example.py @@ -0,0 +1,188 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Example demonstrating DynamoDb Encryption using a MRK discovery multi-keyring. + +A discovery keyring is not provided with any wrapping keys; instead, it recognizes +the KMS key that was used to encrypt a data key, and asks KMS to decrypt with that +KMS key. Discovery keyrings cannot be used to encrypt data. + +For more information on discovery keyrings, see: +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/use-kms-keyring.html#kms-keyring-discovery + +The example encrypts an item using an MRK multi-keyring and puts the encrypted +item to the configured DynamoDb table. Then, it gets the item from the table and +decrypts it using the discovery keyring. + +Running this example requires access to the DDB Table whose name is provided in +CLI arguments. This table must be configured with the following primary key +configuration: + - Partition key is named "partition_key" with type (S) + - Sort key is named "sort_key" with type (S) +""" +from typing import List + +import boto3 +from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders +from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig +from aws_cryptographic_material_providers.mpl.models import ( + CreateAwsKmsMrkDiscoveryMultiKeyringInput, + CreateAwsKmsMrkMultiKeyringInput, + DiscoveryFilter, +) +from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient +from aws_dbesdk_dynamodb.structures.dynamodb import ( + DynamoDbTableEncryptionConfig, + DynamoDbTablesEncryptionConfig, +) +from aws_dbesdk_dynamodb.structures.structured_encryption import ( + CryptoAction, +) + + +def multi_mrk_discovery_keyring_get_item_put_item( + ddb_table_name: str, key_arn: str, account_ids: List[str], regions: List[str] +): + """ + Demonstrate using a MRK discovery multi-keyring. + + :param ddb_table_name: The name of the DynamoDB table + :param key_arn: The ARN of the KMS key to use for encryption + :param account_ids: List of AWS account IDs for discovery filter + :param regions: List of AWS regions for discovery keyring + """ + # 1. Create a single MRK multi-keyring using the key arn. + # Although this example demonstrates use of the MRK discovery multi-keyring, + # a discovery keyring cannot be used to encrypt. So we will need to construct + # a non-discovery keyring for this example to encrypt. For more information on MRK + # multi-keyrings, see the MultiMrkKeyringExample in this directory. + # Though this is an "MRK multi-keyring", we do not need to provide multiple keys, + # and can use single-region KMS keys. We will provide a single key here; this + # can be either an MRK or a single-region key. + mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) + + encrypt_keyring = mat_prov.create_aws_kms_mrk_multi_keyring( + input=CreateAwsKmsMrkMultiKeyringInput(generator=key_arn) + ) + + # 2. Configure which attributes are encrypted and/or signed when writing new items. + # For each attribute that may exist on the items we plan to write to our DynamoDbTable, + # we must explicitly configure how they should be treated during item encryption: + # - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature + # - SIGN_ONLY: The attribute not encrypted, but is still included in the signature + # - DO_NOTHING: The attribute is not encrypted and not included in the signature + attribute_actions_on_encrypt = { + "partition_key": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY + "sort_key": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY + "sensitive_data": CryptoAction.ENCRYPT_AND_SIGN, + } + + # 3. Configure which attributes we expect to be included in the signature + # when reading items. There are two options for configuring this: + # + # - (Recommended) Configure `allowed_unsigned_attribute_prefix`: + # When defining your DynamoDb schema and deciding on attribute names, + # choose a distinguishing prefix (such as ":") for all attributes that + # you do not want to include in the signature. + # This has two main benefits: + # - It is easier to reason about the security and authenticity of data within your item + # when all unauthenticated data is easily distinguishable by their attribute name. + # - If you need to add new unauthenticated attributes in the future, + # you can easily make the corresponding update to your `attribute_actions_on_encrypt` + # and immediately start writing to that new attribute, without + # any other configuration update needed. + # Once you configure this field, it is not safe to update it. + # + # - Configure `allowed_unsigned_attributes`: You may also explicitly list + # a set of attributes that should be considered unauthenticated when encountered + # on read. Be careful if you use this configuration. Do not remove an attribute + # name from this configuration, even if you are no longer writing with that attribute, + # as old items may still include this attribute, and our configuration needs to know + # to continue to exclude this attribute from the signature scope. + # If you add new attribute names to this field, you must first deploy the update to this + # field to all readers in your host fleet before deploying the update to start writing + # with that new attribute. + # + # For this example, we currently authenticate all attributes. To make it easier to + # add unauthenticated attributes in the future, we define a prefix ":" for such attributes. + unsign_attr_prefix = ":" + + # 4. Create the DynamoDb Encryption configuration for the table we will be writing to. + table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="partition_key", + sort_key_name="sort_key", + attribute_actions_on_encrypt=attribute_actions_on_encrypt, + keyring=encrypt_keyring, + allowed_unsigned_attribute_prefix=unsign_attr_prefix, + ) + + table_configs = {ddb_table_name: table_config} + tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) + + # 5. Create the EncryptedClient + ddb_client = boto3.client("dynamodb") + encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) + + # 6. Put an item into our table using the above client. + # Before the item gets sent to DynamoDb, it will be encrypted + # client-side using the MRK multi-keyring. + item = { + "partition_key": {"S": "awsKmsMrkDiscoveryMultiKeyringItem"}, + "sort_key": {"N": "0"}, + "sensitive_data": {"S": "encrypt and sign me!"}, + } + + put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item) + + # Demonstrate that PutItem succeeded + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 7. Construct a discovery filter. + # A discovery filter limits the set of encrypted data keys + # the keyring can use to decrypt data. + # We will only let the keyring use keys in the selected AWS accounts + # and in the `aws` partition. + # This is the suggested config for most users; for more detailed config, see + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/use-kms-keyring.html#kms-keyring-discovery + discovery_filter = DiscoveryFilter(partition="aws", account_ids=account_ids) + + # 8. Construct a discovery keyring. + # Note that we choose to use the MRK discovery multi-keyring, even though + # our original keyring used a single KMS key. + decrypt_keyring = mat_prov.create_aws_kms_mrk_discovery_multi_keyring( + input=CreateAwsKmsMrkDiscoveryMultiKeyringInput(discovery_filter=discovery_filter, regions=regions) + ) + + # 9. Create new DDB config and client using the decrypt discovery keyring. + # This is the same as the above config, except we pass in the decrypt keyring. + table_config_for_decrypt = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="partition_key", + sort_key_name="sort_key", + attribute_actions_on_encrypt=attribute_actions_on_encrypt, + # Add decrypt keyring here + keyring=decrypt_keyring, + allowed_unsigned_attribute_prefix=unsign_attr_prefix, + ) + + table_configs_for_decrypt = {ddb_table_name: table_config_for_decrypt} + tables_config_for_decrypt = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs_for_decrypt) + + encrypted_ddb_client_for_decrypt = EncryptedClient(client=ddb_client, encryption_config=tables_config_for_decrypt) + + # 10. Get the item back from our table using the client. + # The client will retrieve encrypted items from the DDB table, then + # detect the KMS key that was used to encrypt their data keys. + # The client will make a request to KMS to decrypt with the encrypting KMS key. + # If the client has permission to decrypt with the KMS key, + # the client will decrypt the item client-side using the keyring + # and return the original item. + key_to_get = {"partition_key": {"S": "awsKmsMrkDiscoveryMultiKeyringItem"}, "sort_key": {"N": "0"}} + + get_response = encrypted_ddb_client_for_decrypt.get_item(TableName=ddb_table_name, Key=key_to_get) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + returned_item = get_response["Item"] + assert returned_item["sensitive_data"]["S"] == "encrypt and sign me!" diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_multi_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_multi_keyring_example.py new file mode 100644 index 000000000..dbc774763 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_multi_keyring_example.py @@ -0,0 +1,245 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Example demonstrating DynamoDb Encryption using an MRK multi-keyring configuration. + +The MRK multi-keyring accepts multiple AWS KMS MRKs (multi-region keys) or regular +AWS KMS keys (single-region keys) and uses them to encrypt and decrypt data. Data +encrypted using an MRK multi-keyring can be decrypted using any of its component +keys. If a component key is an MRK with a replica in a second region, the replica +key can also be used to decrypt data. + +For more information on MRKs and multi-keyrings, see: +- MRKs: https://docs.aws.amazon.com/kms/latest/developerguide/multi-region-keys-overview.html +- Multi-keyrings: https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/use-multi-keyring.html + +The example creates a new MRK multi-keyring consisting of one MRK (labeled as the +"generator keyring") and one single-region key (labeled as the only "child keyring"). +The MRK also has a replica in a second region. + +The example encrypts a test item using the MRK multi-keyring and puts the encrypted +item to the provided DynamoDb table. Then, it gets the item from the table and +decrypts it using three different configs: + 1. The MRK multi-keyring, where the MRK key is used to decrypt + 2. Another MRK multi-keyring, where the replica MRK key is used to decrypt + 3. Another MRK multi-keyring, where the single-region key that was present + in the original MRK multi-keyring is used to decrypt + +Running this example requires access to the DDB Table whose name is provided in +CLI arguments. This table must be configured with the following primary key +configuration: + - Partition key is named "partition_key" with type (S) + - Sort key is named "sort_key" with type (S) + +Since this example demonstrates multi-region use cases, it requires a default +region set in your AWS client. You can set a default region through the AWS CLI: + aws configure set region [region-name] +For example: + aws configure set region us-west-2 + +For more information on using AWS CLI to set config, see: +https://awscli.amazonaws.com/v2/documentation/api/latest/reference/configure/set.html +""" +import boto3 +from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders +from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig +from aws_cryptographic_material_providers.mpl.models import ( + CreateAwsKmsMrkMultiKeyringInput, +) +from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient +from aws_dbesdk_dynamodb.structures.dynamodb import ( + DynamoDbTableEncryptionConfig, + DynamoDbTablesEncryptionConfig, +) +from aws_dbesdk_dynamodb.structures.structured_encryption import ( + CryptoAction, +) + + +def multi_mrk_keyring_get_item_put_item(ddb_table_name: str, mrk_key_arn: str, key_arn: str, mrk_replica_key_arn: str): + """ + Demonstrate using a MRK multi-keyring. + + :param ddb_table_name: The name of the DynamoDB table + :param mrk_key_arn: The ARN of the MRK key to use as generator + :param key_arn: The ARN of the single-region key to use as child + :param mrk_replica_key_arn: The ARN of the MRK replica key + """ + # 1. Create a single MRK multi-keyring using the MRK arn and the single-region key arn. + mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) + + # Create the multi-keyring, using the MRK as the generator key, + # and the single-region key as a child key. + # Note that the generator key will generate and encrypt a plaintext data key + # and all child keys will only encrypt that same plaintext data key. + # As such, you must have permission to call KMS:GenerateDataKey on your generator key + # and permission to call KMS:Encrypt on all child keys. + # For more information, see the AWS docs on multi-keyrings above. + aws_kms_mrk_multi_keyring = mat_prov.create_aws_kms_mrk_multi_keyring( + input=CreateAwsKmsMrkMultiKeyringInput(generator=mrk_key_arn, kms_key_ids=[key_arn]) + ) + + # 2. Configure which attributes are encrypted and/or signed when writing new items. + # For each attribute that may exist on the items we plan to write to our DynamoDbTable, + # we must explicitly configure how they should be treated during item encryption: + # - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature + # - SIGN_ONLY: The attribute not encrypted, but is still included in the signature + # - DO_NOTHING: The attribute is not encrypted and not included in the signature + attribute_actions_on_encrypt = { + "partition_key": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY + "sort_key": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY + "sensitive_data": CryptoAction.ENCRYPT_AND_SIGN, + } + + # 3. Configure which attributes we expect to be included in the signature + # when reading items. There are two options for configuring this: + # + # - (Recommended) Configure `allowed_unsigned_attribute_prefix`: + # When defining your DynamoDb schema and deciding on attribute names, + # choose a distinguishing prefix (such as ":") for all attributes that + # you do not want to include in the signature. + # This has two main benefits: + # - It is easier to reason about the security and authenticity of data within your item + # when all unauthenticated data is easily distinguishable by their attribute name. + # - If you need to add new unauthenticated attributes in the future, + # you can easily make the corresponding update to your `attribute_actions_on_encrypt` + # and immediately start writing to that new attribute, without + # any other configuration update needed. + # Once you configure this field, it is not safe to update it. + # + # - Configure `allowed_unsigned_attributes`: You may also explicitly list + # a set of attributes that should be considered unauthenticated when encountered + # on read. Be careful if you use this configuration. Do not remove an attribute + # name from this configuration, even if you are no longer writing with that attribute, + # as old items may still include this attribute, and our configuration needs to know + # to continue to exclude this attribute from the signature scope. + # If you add new attribute names to this field, you must first deploy the update to this + # field to all readers in your host fleet before deploying the update to start writing + # with that new attribute. + # + # For this example, we currently authenticate all attributes. To make it easier to + # add unauthenticated attributes in the future, we define a prefix ":" for such attributes. + unsign_attr_prefix = ":" + + # 4. Create the DynamoDb Encryption configuration for the table we will be writing to. + table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="partition_key", + sort_key_name="sort_key", + attribute_actions_on_encrypt=attribute_actions_on_encrypt, + keyring=aws_kms_mrk_multi_keyring, + allowed_unsigned_attribute_prefix=unsign_attr_prefix, + ) + + table_configs = {ddb_table_name: table_config} + tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) + + # 5. Create the EncryptedClient + ddb_client = boto3.client("dynamodb") + encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) + + # 6. Put an item into our table using the above client. + # Before the item gets sent to DynamoDb, it will be encrypted + # client-side using the MRK multi-keyring. + # The data key protecting this item will be encrypted + # with all the KMS Keys in this keyring, so that it can be + # decrypted with any one of those KMS Keys. + item = { + "partition_key": {"S": "awsKmsMrkMultiKeyringItem"}, + "sort_key": {"N": "0"}, + "sensitive_data": {"S": "encrypt and sign me!"}, + } + + put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item) + + # Demonstrate that PutItem succeeded + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 7. Get the item back from our table using the client. + # The client will decrypt the item client-side using the MRK + # and return back the original item. + # Since the generator key is the first available key in the keyring, + # that is the KMS Key that will be used to decrypt this item. + key_to_get = {"partition_key": {"S": "awsKmsMrkMultiKeyringItem"}, "sort_key": {"N": "0"}} + + get_response = encrypted_ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + returned_item = get_response["Item"] + assert returned_item["sensitive_data"]["S"] == "encrypt and sign me!" + + # 8. Create a MRK keyring using the replica MRK arn. + # We will use this to demonstrate that the replica MRK + # can decrypt data created with the original MRK, + # even when the replica MRK was not present in the + # encrypting multi-keyring. + only_replica_key_mrk_multi_keyring = mat_prov.create_aws_kms_mrk_multi_keyring( + input=CreateAwsKmsMrkMultiKeyringInput(kms_key_ids=[mrk_replica_key_arn]) + ) + + # 9. Create a new config and client using the MRK keyring. + # This is the same setup as above, except we provide the MRK keyring to the config. + only_replica_key_table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="partition_key", + sort_key_name="sort_key", + attribute_actions_on_encrypt=attribute_actions_on_encrypt, + keyring=only_replica_key_mrk_multi_keyring, # Only replica keyring added here + allowed_unsigned_attribute_prefix=unsign_attr_prefix, + ) + + only_replica_key_table_configs = {ddb_table_name: only_replica_key_table_config} + only_replica_key_tables_config = DynamoDbTablesEncryptionConfig( + table_encryption_configs=only_replica_key_table_configs + ) + + only_replica_key_encrypted_ddb_client = EncryptedClient( + client=ddb_client, encryption_config=only_replica_key_tables_config + ) + + # 10. Get the item back from our table using the client configured with the replica. + # The client will decrypt the item client-side using the replica MRK + # and return back the original item. + only_replica_key_get_response = only_replica_key_encrypted_ddb_client.get_item( + TableName=ddb_table_name, Key=key_to_get + ) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert only_replica_key_get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + only_replica_key_returned_item = only_replica_key_get_response["Item"] + assert only_replica_key_returned_item["sensitive_data"]["S"] == "encrypt and sign me!" + + # 11. Create an AWS KMS keyring using the single-region key ARN. + # We will use this to demonstrate that the single-region key + # can decrypt data created with the MRK multi-keyring, + # since it is present in the keyring used to encrypt. + only_srk_keyring = mat_prov.create_aws_kms_mrk_multi_keyring( + input=CreateAwsKmsMrkMultiKeyringInput(kms_key_ids=[key_arn]) + ) + + # 12. Create a new config and client using the AWS KMS keyring. + # This is the same setup as above, except we provide the AWS KMS keyring to the config. + only_srk_table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="partition_key", + sort_key_name="sort_key", + attribute_actions_on_encrypt=attribute_actions_on_encrypt, + keyring=only_srk_keyring, # Only single-region key keyring added here + allowed_unsigned_attribute_prefix=unsign_attr_prefix, + ) + + only_srk_table_configs = {ddb_table_name: only_srk_table_config} + only_srk_tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=only_srk_table_configs) + + only_srk_encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=only_srk_tables_config) + + # 13. Get the item back from our table using the client configured with the AWS KMS keyring. + # The client will decrypt the item client-side using the single-region key + # and return back the original item. + only_srk_get_response = only_srk_encrypted_ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert only_srk_get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + only_srk_returned_item = only_srk_get_response["Item"] + assert only_srk_returned_item["sensitive_data"]["S"] == "encrypt and sign me!" diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/multi_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/multi_keyring_example.py new file mode 100644 index 000000000..5cb67df61 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/multi_keyring_example.py @@ -0,0 +1,211 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Example demonstrating DynamoDb Encryption using a multi-keyring configuration. + +A multi-keyring accepts multiple keyrings and uses them to encrypt and decrypt data. +Data encrypted with a multi-keyring can be decrypted with any of its component keyrings. + +For more information on multi-keyrings, see: +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/use-multi-keyring.html + +The example creates a multi-keyring consisting of an AWS KMS keyring (labeled the +"generator keyring") and a raw AES keyring (labeled as the only "child keyring"). +It encrypts a test item using the multi-keyring and puts the encrypted item to the +provided DynamoDb table. Then, it gets the item from the table and decrypts it +using only the raw AES keyring. + +The example takes an `aes_key_bytes` parameter representing a 256-bit AES key. +If run through the class's main method, it will create a new key. In practice, +users should not randomly generate a key, but instead retrieve an existing key +from a secure key management system (e.g. an HSM). + +Running this example requires access to the DDB Table whose name is provided in +CLI arguments. This table must be configured with the following primary key +configuration: + - Partition key is named "partition_key" with type (S) + - Sort key is named "sort_key" with type (S) +""" +import boto3 +from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders +from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig +from aws_cryptographic_material_providers.mpl.models import ( + AesWrappingAlg, + CreateAwsKmsMrkMultiKeyringInput, + CreateMultiKeyringInput, + CreateRawAesKeyringInput, +) +from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient +from aws_dbesdk_dynamodb.structures.dynamodb import ( + DynamoDbTableEncryptionConfig, + DynamoDbTablesEncryptionConfig, +) +from aws_dbesdk_dynamodb.structures.structured_encryption import ( + CryptoAction, +) + + +def multi_keyring_get_item_put_item(ddb_table_name: str, key_arn: str, aes_key_bytes: bytes): + """ + Demonstrate using a multi-keyring. + + :param ddb_table_name: The name of the DynamoDB table + :param key_arn: The ARN of the KMS key to use + :param aes_key_bytes: The AES key bytes to use + """ + # 1. Create the raw AES keyring. + mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) + + raw_aes_keyring_input = CreateRawAesKeyringInput( + key_name="my-aes-key-name", + key_namespace="my-key-namespace", + wrapping_key=aes_key_bytes, + wrapping_alg=AesWrappingAlg.ALG_AES256_GCM_IV12_TAG16, + ) + + raw_aes_keyring = mat_prov.create_raw_aes_keyring(input=raw_aes_keyring_input) + + # 2. Create the AWS KMS keyring. + # We create a MRK multi keyring, as this interface also supports + # single-region KMS keys (standard KMS keys), + # and creates the KMS client for us automatically. + aws_kms_mrk_multi_keyring = mat_prov.create_aws_kms_mrk_multi_keyring( + input=CreateAwsKmsMrkMultiKeyringInput(generator=key_arn) + ) + + # 3. Create the multi-keyring. + # We will label the AWS KMS keyring as the generator and the raw AES keyring as the + # only child keyring. + # You must provide a generator keyring to encrypt data. + # You may provide additional child keyrings. Each child keyring will be able to + # decrypt data encrypted with the multi-keyring on its own. It does not need + # knowledge of any other child keyrings or the generator keyring to decrypt. + multi_keyring = mat_prov.create_multi_keyring( + input=CreateMultiKeyringInput(generator=aws_kms_mrk_multi_keyring, child_keyrings=[raw_aes_keyring]) + ) + + # 4. Configure which attributes are encrypted and/or signed when writing new items. + # For each attribute that may exist on the items we plan to write to our DynamoDbTable, + # we must explicitly configure how they should be treated during item encryption: + # - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature + # - SIGN_ONLY: The attribute not encrypted, but is still included in the signature + # - DO_NOTHING: The attribute is not encrypted and not included in the signature + attribute_actions = { + "partition_key": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY + "sort_key": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY + "sensitive_data": CryptoAction.ENCRYPT_AND_SIGN, + } + + # 5. Configure which attributes we expect to be included in the signature + # when reading items. There are two options for configuring this: + # + # - (Recommended) Configure `allowed_unsigned_attribute_prefix`: + # When defining your DynamoDb schema and deciding on attribute names, + # choose a distinguishing prefix (such as ":") for all attributes that + # you do not want to include in the signature. + # This has two main benefits: + # - It is easier to reason about the security and authenticity of data within your item + # when all unauthenticated data is easily distinguishable by their attribute name. + # - If you need to add new unauthenticated attributes in the future, + # you can easily make the corresponding update to your `attribute_actions` + # and immediately start writing to that new attribute, without + # any other configuration update needed. + # Once you configure this field, it is not safe to update it. + # + # - Configure `allowed_unsigned_attributes`: You may also explicitly list + # a set of attributes that should be considered unauthenticated when encountered + # on read. Be careful if you use this configuration. Do not remove an attribute + # name from this configuration, even if you are no longer writing with that attribute, + # as old items may still include this attribute, and our configuration needs to know + # to continue to exclude this attribute from the signature scope. + # If you add new attribute names to this field, you must first deploy the update to this + # field to all readers in your host fleet before deploying the update to start writing + # with that new attribute. + # + # For this example, we currently authenticate all attributes. To make it easier to + # add unauthenticated attributes in the future, we define a prefix ":" for such attributes. + unsign_attr_prefix = ":" + + # 6. Create the DynamoDb Encryption configuration for the table we will be writing to. + # Note that this example creates one config/client combination for PUT, and another + # for GET. The PUT config uses the multi-keyring, while the GET config uses the + # raw AES keyring. This is solely done to demonstrate that a keyring included as + # a child of a multi-keyring can be used to decrypt data on its own. + table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="partition_key", + sort_key_name="sort_key", + attribute_actions_on_encrypt=attribute_actions, + keyring=multi_keyring, # Multi-keyring is added here + allowed_unsigned_attribute_prefix=unsign_attr_prefix, + ) + + table_configs = {ddb_table_name: table_config} + tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) + + # 7. Create the EncryptedClient + ddb_client = boto3.client("dynamodb") + encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) + + # 8. Put an item into our table using the above client. + # Before the item gets sent to DynamoDb, it will be encrypted + # client-side using the multi-keyring. + # The item will be encrypted with all wrapping keys in the keyring, + # so that it can be decrypted with any one of the keys. + item = { + "partition_key": {"S": "multiKeyringItem"}, + "sort_key": {"N": "0"}, + "sensitive_data": {"S": "encrypt and sign me!"}, + } + + put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item) + + # Demonstrate that PutItem succeeded + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 9. Get the item back from our table using the above client. + # The client will decrypt the item client-side using the AWS KMS + # keyring, and return back the original item. + # Since the generator key is the first available key in the keyring, + # that is the key that will be used to decrypt this item. + key_to_get = {"partition_key": {"S": "multiKeyringItem"}, "sort_key": {"N": "0"}} + + get_response = encrypted_ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + returned_item = get_response["Item"] + assert returned_item["sensitive_data"]["S"] == "encrypt and sign me!" + + # 10. Create a new config and client with only the raw AES keyring to GET the item + # This is the same setup as above, except the config uses the `raw_aes_keyring`. + only_aes_keyring_table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="partition_key", + sort_key_name="sort_key", + attribute_actions_on_encrypt=attribute_actions, + keyring=raw_aes_keyring, # Raw AES keyring is added here + allowed_unsigned_attribute_prefix=unsign_attr_prefix, + ) + + only_aes_keyring_table_configs = {ddb_table_name: only_aes_keyring_table_config} + only_aes_keyring_tables_config = DynamoDbTablesEncryptionConfig( + table_encryption_configs=only_aes_keyring_table_configs + ) + + only_aes_keyring_encrypted_ddb_client = EncryptedClient( + client=ddb_client, encryption_config=only_aes_keyring_tables_config + ) + + # 11. Get the item back from our table using the client + # configured with only the raw AES keyring. + # The client will decrypt the item client-side using the raw + # AES keyring, and return back the original item. + only_aes_keyring_get_response = only_aes_keyring_encrypted_ddb_client.get_item( + TableName=ddb_table_name, Key=key_to_get + ) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert only_aes_keyring_get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + only_aes_keyring_returned_item = only_aes_keyring_get_response["Item"] + assert only_aes_keyring_returned_item["sensitive_data"]["S"] == "encrypt and sign me!" diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_aes_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_aes_keyring_example.py new file mode 100644 index 000000000..2b4993e26 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_aes_keyring_example.py @@ -0,0 +1,145 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Example demonstrating DynamoDb Encryption using a raw AES Keyring. + +The raw AES Keyring takes in an AES key and uses that key to protect the data +keys that encrypt and decrypt DynamoDb table items. + +This example takes an `aes_key_bytes` parameter representing a 256-bit AES key. +If run through the script's main method, it will create a new key. In practice, +users should not randomly generate a key, but instead retrieve an existing key +from a secure key management system (e.g. an HSM). + +This example encrypts a test item using the provided AES key and puts the encrypted +item to the provided DynamoDb table. Then, it gets the item from the table and +decrypts it. + +Running this example requires access to the DDB Table whose name is provided in +CLI arguments. This table must be configured with the following primary key +configuration: + - Partition key is named "partition_key" with type (S) + - Sort key is named "sort_key" with type (S) +""" + +import boto3 +from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders +from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig +from aws_cryptographic_material_providers.mpl.models import ( + AesWrappingAlg, + CreateRawAesKeyringInput, +) +from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient +from aws_dbesdk_dynamodb.structures.dynamodb import ( + DynamoDbTableEncryptionConfig, + DynamoDbTablesEncryptionConfig, +) +from aws_dbesdk_dynamodb.structures.structured_encryption import ( + CryptoAction, +) + + +def raw_aes_keyring_get_item_put_item(ddb_table_name: str, aes_key_bytes: bytes): + """ + Demonstrate using a raw AES keyring. + + :param ddb_table_name: The name of the DynamoDB table + :param aes_key_bytes: The AES key bytes to use + """ + # 1. Create the keyring. + # The DynamoDb encryption client uses this to encrypt and decrypt items. + mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) + + keyring_input = CreateRawAesKeyringInput( + key_name="my-aes-key-name", + key_namespace="my-key-namespace", + wrapping_key=aes_key_bytes, + wrapping_alg=AesWrappingAlg.ALG_AES256_GCM_IV12_TAG16, + ) + + raw_aes_keyring = mat_prov.create_raw_aes_keyring(input=keyring_input) + + # 2. Configure which attributes are encrypted and/or signed when writing new items. + # For each attribute that may exist on the items we plan to write to our DynamoDbTable, + # we must explicitly configure how they should be treated during item encryption: + # - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature + # - SIGN_ONLY: The attribute not encrypted, but is still included in the signature + # - DO_NOTHING: The attribute is not encrypted and not included in the signature + attribute_actions = { + "partition_key": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY + "sort_key": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY + "sensitive_data": CryptoAction.ENCRYPT_AND_SIGN, + } + + # 3. Configure which attributes we expect to be included in the signature + # when reading items. There are two options for configuring this: + # + # - (Recommended) Configure `allowed_unsigned_attribute_prefix`: + # When defining your DynamoDb schema and deciding on attribute names, + # choose a distinguishing prefix (such as ":") for all attributes that + # you do not want to include in the signature. + # This has two main benefits: + # - It is easier to reason about the security and authenticity of data within your item + # when all unauthenticated data is easily distinguishable by their attribute name. + # - If you need to add new unauthenticated attributes in the future, + # you can easily make the corresponding update to your `attribute_actions` + # and immediately start writing to that new attribute, without + # any other configuration update needed. + # Once you configure this field, it is not safe to update it. + # + # - Configure `allowed_unsigned_attributes`: You may also explicitly list + # a set of attributes that should be considered unauthenticated when encountered + # on read. Be careful if you use this configuration. Do not remove an attribute + # name from this configuration, even if you are no longer writing with that attribute, + # as old items may still include this attribute, and our configuration needs to know + # to continue to exclude this attribute from the signature scope. + # If you add new attribute names to this field, you must first deploy the update to this + # field to all readers in your host fleet before deploying the update to start writing + # with that new attribute. + # + # For this example, we currently authenticate all attributes. To make it easier to + # add unauthenticated attributes in the future, we define a prefix ":" for such attributes. + unsign_attr_prefix = ":" + + # 4. Create the DynamoDb Encryption configuration for the table we will be writing to. + table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="partition_key", + sort_key_name="sort_key", + attribute_actions_on_encrypt=attribute_actions, + keyring=raw_aes_keyring, + allowed_unsigned_attribute_prefix=unsign_attr_prefix, + ) + + table_configs = {ddb_table_name: table_config} + tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) + + # 5. Create the EncryptedClient + ddb_client = boto3.client("dynamodb") + encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) + + # 6. Put an item into our table using the above client. + # Before the item gets sent to DynamoDb, it will be encrypted + # client-side, according to our configuration. + item = { + "partition_key": {"S": "rawAesKeyringItem"}, + "sort_key": {"N": "0"}, + "sensitive_data": {"S": "encrypt and sign me!"}, + } + + put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item) + + # Demonstrate that PutItem succeeded + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 7. Get the item back from our table using the same client. + # The client will decrypt the item client-side, and return + # back the original item. + key_to_get = {"partition_key": {"S": "rawAesKeyringItem"}, "sort_key": {"N": "0"}} + + get_response = encrypted_ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + returned_item = get_response["Item"] + assert returned_item["sensitive_data"]["S"] == "encrypt and sign me!" diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example.py new file mode 100644 index 000000000..54f79a46d --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example.py @@ -0,0 +1,564 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +These examples set up DynamoDb Encryption for the AWS SDK client using the raw ECDH Keyring. + +This keyring, depending on its KeyAgreement scheme, +takes in the sender's ECC private key, and the recipient's ECC Public Key to derive a shared secret. +The keyring uses the shared secret to derive a data key to protect the +data keys that encrypt and decrypt DynamoDb table items. + +Running these examples require access to the DDB Table whose name +is provided in CLI arguments. +This table must be configured with the following +primary key configuration: + - Partition key is named "partition_key" with type (S) + - Sort key is named "sort_key" with type (S) +""" +import pathlib + +import boto3 +from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders +from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig +from aws_cryptographic_material_providers.mpl.models import ( + CreateRawEcdhKeyringInput, + EphemeralPrivateKeyToStaticPublicKeyInput, + PublicKeyDiscoveryInput, + RawEcdhStaticConfigurationsEphemeralPrivateKeyToStaticPublicKey, + RawEcdhStaticConfigurationsPublicKeyDiscovery, + RawEcdhStaticConfigurationsRawPrivateKeyToStaticPublicKey, + RawPrivateKeyToStaticPublicKeyInput, +) +from aws_cryptographic_material_providers.mpl.references import IKeyring +from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient +from aws_dbesdk_dynamodb.structures.dynamodb import ( + DynamoDbTableEncryptionConfig, + DynamoDbTablesEncryptionConfig, +) +from aws_dbesdk_dynamodb.structures.structured_encryption import ( + CryptoAction, +) +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import ec + +EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER = "RawEcdhKeyringExamplePrivateKeySender.pem" +EXAMPLE_ECC_PRIVATE_KEY_FILENAME_RECIPIENT = "RawEcdhKeyringExamplePrivateKeyRecipient.pem" +EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT = "RawEcdhKeyringExamplePublicKeyRecipient.pem" + + +def raw_ecdh_keyring_get_item_put_item(ddb_table_name: str, curve_spec: str): + """ + Demonstrate using a raw ECDH keyring with static keys. + + This example takes in the sender's private key as a + UTF8 PEM-encoded (PKCS #8 PrivateKeyInfo structures) + located at the file location defined in EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER, + the recipient's public key as a UTF8 PEM-encoded X.509 public key, also known as SubjectPublicKeyInfo (SPKI), + located at the file location defined in EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT, + and the Curve Specification where the keys lie. + + This example encrypts a test item using the provided ECC keys and puts the + encrypted item to the provided DynamoDb table. Then, it gets the + item from the table and decrypts it. + + This examples creates a RawECDH keyring with the RawPrivateKeyToStaticPublicKey key agreement scheme. + For more information on this configuration see: + https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/use-raw-ecdh-keyring.html#raw-ecdh-RawPrivateKeyToStaticPublicKey + + On encrypt, the shared secret is derived from the sender's private key and the recipient's public key. + On decrypt, the shared secret is derived from the sender's private key and the recipient's public key; + however, on decrypt the recipient can construct a keyring such that the shared secret is calculated with + the recipient's private key and the sender's public key. In both scenarios the shared secret will be the same. + + :param ddb_table_name: The name of the DynamoDB table + :param curve_spec: The curve specification to use + """ + # Load key pair from UTF-8 encoded PEM files. + # You may provide your own PEM files to use here. If you provide this, it MUST + # be a key on curve P256. + # If you do not, the main method in this class will generate PEM + # files for example use. Do not use these files for any other purpose. + try: + with open(EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER, "rb") as f: + private_key_utf8_encoded = f.read() + except IOError as e: + raise OSError("IOError while reading the private key from file") from e + + try: + with open(EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT, "rb") as f: + public_key_utf8_encoded = f.read() + public_key = serialization.load_pem_public_key(public_key_utf8_encoded) + public_key_bytes = public_key.public_bytes( + encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo + ) + except IOError as e: + raise OSError("IOError while reading the public key from file") from e + + # Create the keyring. + # This keyring uses static sender and recipient keys. This configuration calls for both of + # the keys to be on the same curve (P256, P384, P521). + # On encrypt, the shared secret is derived from the sender's private key and the recipient's public key. + # For this example, on decrypt, the shared secret is derived from the sender's private key + # and the recipient's public key; + # however, on decrypt the recipient can construct a keyring such that the shared secret is calculated with + # the recipient's private key and the sender's public key. In both scenarios the shared secret will be the same. + # The DynamoDb encryption client uses this to encrypt and decrypt items. + mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) + + keyring_input = CreateRawEcdhKeyringInput( + curve_spec=curve_spec, + key_agreement_scheme=RawEcdhStaticConfigurationsRawPrivateKeyToStaticPublicKey( + RawPrivateKeyToStaticPublicKeyInput( + # Must be a UTF8 PEM-encoded private key + sender_static_private_key=private_key_utf8_encoded, + # Must be a DER-encoded X.509 public key + recipient_public_key=public_key_bytes, + ) + ), + ) + + raw_ecdh_keyring = mat_prov.create_raw_ecdh_keyring(input=keyring_input) + + put_get_example_with_keyring(raw_ecdh_keyring, ddb_table_name) + + +def ephemeral_raw_ecdh_keyring_put_item(ddb_table_name: str, curve_spec: str): + """ + Demonstrate using a raw ECDH keyring with ephemeral keys. + + This example takes in the recipient's public key located at EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT + as a UTF8 PEM-encoded X.509 public key, and the Curve Specification where the key lies. + + This examples creates a RawECDH keyring with the EphemeralPrivateKeyToStaticPublicKey key agreement scheme. + This configuration will always create a new key pair as the sender key pair for the key agreement operation. + The ephemeral configuration can only encrypt data and CANNOT decrypt messages. + For more information on this configuration see: + https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/use-raw-ecdh-keyring.html#raw-ecdh-EphemeralPrivateKeyToStaticPublicKey + + :param ddb_table_name: The name of the DynamoDB table + :param curve_spec: The curve specification to use + """ + # Load public key from UTF-8 encoded PEM files into a DER encoded public key. + # You may provide your own PEM files to use here. If you provide this, it MUST + # be a key on curve P256. + # If you do not, the main method in this class will generate PEM + # files for example use. Do not use these files for any other purpose. + try: + with open(EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT, "rb") as f: + public_key_utf8_encoded = f.read() + public_key = serialization.load_pem_public_key(public_key_utf8_encoded) + public_key_bytes = public_key.public_bytes( + encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo + ) + except IOError as e: + raise OSError("IOError while reading the public key from file") from e + + # Create the keyring. + # This keyring uses an ephemeral configuration. This configuration will always create a new + # key pair as the sender key pair for the key agreement operation. The ephemeral configuration can only + # encrypt data and CANNOT decrypt messages. + # The DynamoDb encryption client uses this to encrypt items. + mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) + + keyring_input = CreateRawEcdhKeyringInput( + curve_spec=curve_spec, + key_agreement_scheme=RawEcdhStaticConfigurationsEphemeralPrivateKeyToStaticPublicKey( + EphemeralPrivateKeyToStaticPublicKeyInput(recipient_public_key=public_key_bytes) + ), + ) + + raw_ecdh_keyring = mat_prov.create_raw_ecdh_keyring(input=keyring_input) + + # A raw ecdh keyring with Ephemeral configuration cannot decrypt data since the key pair + # used as the sender is ephemeral. This means that at decrypt time it does not have + # the private key that corresponds to the public key that is stored on the message. + put_example_with_keyring(raw_ecdh_keyring, ddb_table_name) + + +def discovery_raw_ecdh_keyring_get_item(ddb_table_name: str, curve_spec: str): + """ + Demonstrate using a raw ECDH keyring with discovery. + + This example takes in the recipient's private key located at EXAMPLE_ECC_PRIVATE_KEY_FILENAME_RECIPIENT + as a UTF8 PEM-encoded (PKCS #8 PrivateKeyInfo structures) private key, + and the Curve Specification where the key lies. + + This examples creates a RawECDH keyring with the PublicKeyDiscovery key agreement scheme. + This scheme is only available on decrypt. + For more information on this configuration see: + https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/use-raw-ecdh-keyring.html#raw-ecdh-PublicKeyDiscovery + + :param ddb_table_name: The name of the DynamoDB table + :param curve_spec: The curve specification to use + """ + # Load key pair from UTF-8 encoded PEM files. + # You may provide your own PEM files to use here. If you provide this, it MUST + # be a key on curve P256. + # If you do not, the main method in this class will generate PEM + # files for example use. Do not use these files for any other purpose. + try: + with open(EXAMPLE_ECC_PRIVATE_KEY_FILENAME_RECIPIENT, "rb") as f: + private_key_utf8_encoded = f.read() + except IOError as e: + raise OSError("IOError while reading the private key from file") from e + + # Create the keyring. + # This keyring uses a discovery configuration. This configuration will check on decrypt + # if it is meant to decrypt the message by checking if the configured public key is stored on the message. + # The discovery configuration can only decrypt messages and CANNOT encrypt messages. + # The DynamoDb encryption client uses this to decrypt items. + mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) + + keyring_input = CreateRawEcdhKeyringInput( + curve_spec=curve_spec, + key_agreement_scheme=RawEcdhStaticConfigurationsPublicKeyDiscovery( + PublicKeyDiscoveryInput(recipient_static_private_key=private_key_utf8_encoded) + ), + ) + + raw_ecdh_keyring = mat_prov.create_raw_ecdh_keyring(input=keyring_input) + + # A raw ecdh keyring with discovery configuration cannot encrypt data since the keyring + # looks for its configured public key on the message. + get_example_with_keyring(raw_ecdh_keyring, ddb_table_name) + + +def put_get_example_with_keyring(raw_ecdh_keyring: IKeyring, ddb_table_name: str): + """ + Demonstrate put and get operations with a raw ECDH keyring. + + :param raw_ecdh_keyring: The raw ECDH keyring to use + :param ddb_table_name: The name of the DynamoDB table + """ + # Configure which attributes are encrypted and/or signed when writing new items. + # For each attribute that may exist on the items we plan to write to our DynamoDbTable, + # we must explicitly configure how they should be treated during item encryption: + # - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature + # - SIGN_ONLY: The attribute not encrypted, but is still included in the signature + # - DO_NOTHING: The attribute is not encrypted and not included in the signature + attribute_actions = { + "partition_key": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY + "sort_key": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY + "sensitive_data": CryptoAction.ENCRYPT_AND_SIGN, + } + + # Configure which attributes we expect to be included in the signature + # when reading items. There are two options for configuring this: + # + # - (Recommended) Configure `allowed_unsigned_attribute_prefix`: + # When defining your DynamoDb schema and deciding on attribute names, + # choose a distinguishing prefix (such as ":") for all attributes that + # you do not want to include in the signature. + # This has two main benefits: + # - It is easier to reason about the security and authenticity of data within your item + # when all unauthenticated data is easily distinguishable by their attribute name. + # - If you need to add new unauthenticated attributes in the future, + # you can easily make the corresponding update to your `attribute_actions` + # and immediately start writing to that new attribute, without + # any other configuration update needed. + # Once you configure this field, it is not safe to update it. + # + # - Configure `allowed_unsigned_attributes`: You may also explicitly list + # a set of attributes that should be considered unauthenticated when encountered + # on read. Be careful if you use this configuration. Do not remove an attribute + # name from this configuration, even if you are no longer writing with that attribute, + # as old items may still include this attribute, and our configuration needs to know + # to continue to exclude this attribute from the signature scope. + # If you add new attribute names to this field, you must first deploy the update to this + # field to all readers in your host fleet before deploying the update to start writing + # with that new attribute. + # + # For this example, we currently authenticate all attributes. To make it easier to + # add unauthenticated attributes in the future, we define a prefix ":" for such attributes. + unsign_attr_prefix = ":" + + # Create the DynamoDb Encryption configuration for the table we will be writing to. + table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="partition_key", + sort_key_name="sort_key", + attribute_actions_on_encrypt=attribute_actions, + keyring=raw_ecdh_keyring, + allowed_unsigned_attribute_prefix=unsign_attr_prefix, + ) + + table_configs = {ddb_table_name: table_config} + tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) + + # Create the EncryptedClient + ddb_client = boto3.client("dynamodb") + encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) + + # Put an item into our table using the above client. + # Before the item gets sent to DynamoDb, it will be encrypted + # client-side, according to our configuration. + item = { + "partition_key": {"S": "rawEcdhKeyringItem"}, + "sort_key": {"N": "0"}, + "sensitive_data": {"S": "encrypt and sign me!"}, + } + + put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item) + + # Demonstrate that PutItem succeeded + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # Get the item back from our table using the client. + # The client will decrypt the item client-side using the RSA keyring + # and return the original item. + key_to_get = {"partition_key": {"S": "rawEcdhKeyringItem"}, "sort_key": {"N": "0"}} + + get_response = encrypted_ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + returned_item = get_response["Item"] + assert returned_item["sensitive_data"]["S"] == "encrypt and sign me!" + + +def put_example_with_keyring(raw_ecdh_keyring: IKeyring, ddb_table_name: str): + """ + Demonstrate put operation with a raw ECDH keyring. + + :param raw_ecdh_keyring: The raw ECDH keyring to use + :param ddb_table_name: The name of the DynamoDB table + """ + # Configure which attributes are encrypted and/or signed when writing new items. + # For each attribute that may exist on the items we plan to write to our DynamoDbTable, + # we must explicitly configure how they should be treated during item encryption: + # - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature + # - SIGN_ONLY: The attribute not encrypted, but is still included in the signature + # - DO_NOTHING: The attribute is not encrypted and not included in the signature + attribute_actions = { + "partition_key": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY + "sort_key": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY + "sensitive_data": CryptoAction.ENCRYPT_AND_SIGN, + } + + # Configure which attributes we expect to be included in the signature + # when reading items. There are two options for configuring this: + # + # - (Recommended) Configure `allowed_unsigned_attribute_prefix`: + # When defining your DynamoDb schema and deciding on attribute names, + # choose a distinguishing prefix (such as ":") for all attributes that + # you do not want to include in the signature. + # This has two main benefits: + # - It is easier to reason about the security and authenticity of data within your item + # when all unauthenticated data is easily distinguishable by their attribute name. + # - If you need to add new unauthenticated attributes in the future, + # you can easily make the corresponding update to your `attribute_actions` + # and immediately start writing to that new attribute, without + # any other configuration update needed. + # Once you configure this field, it is not safe to update it. + # + # - Configure `allowed_unsigned_attributes`: You may also explicitly list + # a set of attributes that should be considered unauthenticated when encountered + # on read. Be careful if you use this configuration. Do not remove an attribute + # name from this configuration, even if you are no longer writing with that attribute, + # as old items may still include this attribute, and our configuration needs to know + # to continue to exclude this attribute from the signature scope. + # If you add new attribute names to this field, you must first deploy the update to this + # field to all readers in your host fleet before deploying the update to start writing + # with that new attribute. + # + # For this example, we currently authenticate all attributes. To make it easier to + # add unauthenticated attributes in the future, we define a prefix ":" for such attributes. + unsign_attr_prefix = ":" + + # Create the DynamoDb Encryption configuration for the table we will be writing to. + table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="partition_key", + sort_key_name="sort_key", + attribute_actions_on_encrypt=attribute_actions, + keyring=raw_ecdh_keyring, + allowed_unsigned_attribute_prefix=unsign_attr_prefix, + ) + + table_configs = {ddb_table_name: table_config} + tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) + + # Create the EncryptedClient + ddb_client = boto3.client("dynamodb") + encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) + + # Put an item into our table using the above client. + # Before the item gets sent to DynamoDb, it will be encrypted + # client-side, according to our configuration. + item = { + "partition_key": {"S": "rawEcdhKeyringItem"}, + "sort_key": {"N": "0"}, + "sensitive_data": {"S": "encrypt and sign me!"}, + } + + put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item) + + # Demonstrate that PutItem succeeded + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + +def get_example_with_keyring(raw_ecdh_keyring: IKeyring, ddb_table_name: str): + """ + Demonstrate get operation with a raw ECDH keyring. + + :param raw_ecdh_keyring: The raw ECDH keyring to use + :param ddb_table_name: The name of the DynamoDB table + """ + # Configure which attributes are encrypted and/or signed when writing new items. + # For each attribute that may exist on the items we plan to write to our DynamoDbTable, + # we must explicitly configure how they should be treated during item encryption: + # - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature + # - SIGN_ONLY: The attribute not encrypted, but is still included in the signature + # - DO_NOTHING: The attribute is not encrypted and not included in the signature + attribute_actions = { + "partition_key": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY + "sort_key": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY + "sensitive_data": CryptoAction.ENCRYPT_AND_SIGN, + } + + # Configure which attributes we expect to be included in the signature + # when reading items. There are two options for configuring this: + # + # - (Recommended) Configure `allowed_unsigned_attribute_prefix`: + # When defining your DynamoDb schema and deciding on attribute names, + # choose a distinguishing prefix (such as ":") for all attributes that + # you do not want to include in the signature. + # This has two main benefits: + # - It is easier to reason about the security and authenticity of data within your item + # when all unauthenticated data is easily distinguishable by their attribute name. + # - If you need to add new unauthenticated attributes in the future, + # you can easily make the corresponding update to your `attribute_actions` + # and immediately start writing to that new attribute, without + # any other configuration update needed. + # Once you configure this field, it is not safe to update it. + # + # - Configure `allowed_unsigned_attributes`: You may also explicitly list + # a set of attributes that should be considered unauthenticated when encountered + # on read. Be careful if you use this configuration. Do not remove an attribute + # name from this configuration, even if you are no longer writing with that attribute, + # as old items may still include this attribute, and our configuration needs to know + # to continue to exclude this attribute from the signature scope. + # If you add new attribute names to this field, you must first deploy the update to this + # field to all readers in your host fleet before deploying the update to start writing + # with that new attribute. + # + # For this example, we currently authenticate all attributes. To make it easier to + # add unauthenticated attributes in the future, we define a prefix ":" for such attributes. + unsign_attr_prefix = ":" + + # Create the DynamoDb Encryption configuration for the table we will be writing to. + table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="partition_key", + sort_key_name="sort_key", + attribute_actions_on_encrypt=attribute_actions, + keyring=raw_ecdh_keyring, + allowed_unsigned_attribute_prefix=unsign_attr_prefix, + ) + + table_configs = {ddb_table_name: table_config} + tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) + + # Create the EncryptedClient + ddb_client = boto3.client("dynamodb") + encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) + + # Get the item back from our table using the client. + # The client will decrypt the item client-side using the RSA keyring + # and return the original item. + key_to_get = {"partition_key": {"S": "rawEcdhKeyringItem"}, "sort_key": {"N": "0"}} + + get_response = encrypted_ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + returned_item = get_response["Item"] + assert returned_item["sensitive_data"]["S"] == "encrypt and sign me!" + + +def should_generate_new_ecc_key_pairs() -> bool: + """ + Check if new ECC key pairs should be generated. + + :return: True if new key pairs should be generated, False otherwise + """ + private_key_file_sender = pathlib.Path(EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER) + private_key_file_recipient = pathlib.Path(EXAMPLE_ECC_PRIVATE_KEY_FILENAME_RECIPIENT) + public_key_file_recipient = pathlib.Path(EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT) + + # If keys already exist: do not overwrite existing keys + return ( + not private_key_file_sender.exists() + and not public_key_file_recipient.exists() + and not private_key_file_recipient.exists() + ) + + +def generate_ecc_key_pairs(): + """ + Generate new ECC key pairs. + + This code will generate new ECC key pairs for example use. + The keys will be written to the files: + - public_sender: EXAMPLE_ECC_PUBLIC_KEY_FILENAME_SENDER + - private_sender: EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER + - public_recipient: EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT + This example uses cryptography's EllipticCurve to generate the key pairs. + In practice, you should not generate this in your code, and should instead + retrieve this key from a secure key management system (e.g. HSM). + These examples only demonstrate using the P256 curve while the keyring accepts + P256, P384, or P521. + These keys are created here for example purposes only. + """ + private_key_file_sender = pathlib.Path(EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER) + private_key_file_recipient = pathlib.Path(EXAMPLE_ECC_PRIVATE_KEY_FILENAME_RECIPIENT) + public_key_file_recipient = pathlib.Path(EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT) + + if private_key_file_sender.exists() or public_key_file_recipient.exists() or private_key_file_recipient.exists(): + raise FileExistsError("generateEccKeyPairs will not overwrite existing PEM files") + + # Generate sender key pair + sender_private_key = ec.generate_private_key(ec.SECP256R1()) + + # Generate recipient key pair + recipient_private_key = ec.generate_private_key(ec.SECP256R1()) + recipient_public_key = recipient_private_key.public_key() + + # Write private keys + write_private_key(sender_private_key, EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER) + write_private_key(recipient_private_key, EXAMPLE_ECC_PRIVATE_KEY_FILENAME_RECIPIENT) + + # Write public key + write_public_key(recipient_public_key, EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT) + + +def write_private_key(private_key: ec.EllipticCurvePrivateKey, filename: str): + """ + Write a private key to a PEM file. + + :param private_key: The private key to write + :param filename: The filename to write to + """ + pem_data = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + + with open(filename, "wb") as f: + f.write(pem_data) + + +def write_public_key(public_key: ec.EllipticCurvePublicKey, filename: str): + """ + Write a public key to a PEM file. + + :param public_key: The public key to write + :param filename: The filename to write to + """ + pem_data = public_key.public_bytes( + encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo + ) + + with open(filename, "wb") as f: + f.write(pem_data) diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_rsa_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_rsa_keyring_example.py new file mode 100644 index 000000000..f0de43eee --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_rsa_keyring_example.py @@ -0,0 +1,245 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Example demonstrating DynamoDb Encryption using a raw RSA Keyring. + +The raw RSA Keyring uses an RSA key pair to encrypt and decrypt records. +The keyring accepts PEM encodings of the key pair as UTF-8 interpreted bytes. +The client uses the public key to encrypt items it adds to the table and +uses the private key to decrypt existing table items it retrieves. + +The example loads a key pair from PEM files with paths defined in: + - EXAMPLE_RSA_PRIVATE_KEY_FILENAME + - EXAMPLE_RSA_PUBLIC_KEY_FILENAME + +If you do not provide these files, running this example through the main method +will generate these files for you in the directory where the example is run. +In practice, users of this library should not generate new key pairs like this, +and should instead retrieve an existing key from a secure key management system +(e.g. an HSM). + +You may also provide your own key pair by placing PEM files in the directory +where the example is run or modifying the paths in the code below. These files +must be valid PEM encodings of the key pair as UTF-8 encoded bytes. If you do +provide your own key pair, or if a key pair already exists, this class' main +method will not generate a new key pair. + +The example loads a key pair from disk, encrypts a test item, and puts the +encrypted item to the provided DynamoDb table. Then, it gets the item from +the table and decrypts it. + +Running this example requires access to the DDB Table whose name is provided +in CLI arguments. This table must be configured with the following primary +key configuration: + - Partition key is named "partition_key" with type (S) + - Sort key is named "sort_key" with type (S) +""" +import os + +import boto3 +from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders +from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig +from aws_cryptographic_material_providers.mpl.models import ( + CreateRawRsaKeyringInput, + PaddingScheme, +) +from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient +from aws_dbesdk_dynamodb.structures.dynamodb import ( + DynamoDbTableEncryptionConfig, + DynamoDbTablesEncryptionConfig, +) +from aws_dbesdk_dynamodb.structures.structured_encryption import ( + CryptoAction, +) +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa + +EXAMPLE_RSA_PRIVATE_KEY_FILENAME = "RawRsaKeyringExamplePrivateKey.pem" +EXAMPLE_RSA_PUBLIC_KEY_FILENAME = "RawRsaKeyringExamplePublicKey.pem" + + +def raw_rsa_keyring_example(ddb_table_name: str): + """ + Create a Raw RSA keyring and use it to encrypt/decrypt DynamoDB items. + + :param ddb_table_name: The name of the DynamoDB table + """ + # 1. Load key pair from UTF-8 encoded PEM files. + # You may provide your own PEM files to use here. + # If you do not, the main method in this class will generate PEM + # files for example use. Do not use these files for any other purpose. + try: + with open(EXAMPLE_RSA_PUBLIC_KEY_FILENAME, "rb") as f: + public_key_utf8_encoded = f.read() + except IOError as e: + raise RuntimeError("IOError while reading public key from file") from e + + try: + with open(EXAMPLE_RSA_PRIVATE_KEY_FILENAME, "rb") as f: + private_key_utf8_encoded = f.read() + except IOError as e: + raise RuntimeError("IOError while reading private key from file") from e + + # 2. Create the keyring. + # The DynamoDb encryption client uses this to encrypt and decrypt items. + mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) + + keyring_input = CreateRawRsaKeyringInput( + key_name="my-rsa-key-name", + key_namespace="my-key-namespace", + padding_scheme=PaddingScheme.OAEP_SHA256_MGF1, + public_key=public_key_utf8_encoded, + private_key=private_key_utf8_encoded, + ) + + raw_rsa_keyring = mat_prov.create_raw_rsa_keyring(input=keyring_input) + + # 3. Configure which attributes are encrypted and/or signed when writing new items. + # For each attribute that may exist on the items we plan to write to our DynamoDbTable, + # we must explicitly configure how they should be treated during item encryption: + # - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature + # - SIGN_ONLY: The attribute not encrypted, but is still included in the signature + # - DO_NOTHING: The attribute is not encrypted and not included in the signature + attribute_actions = { + "partition_key": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY + "sort_key": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY + "sensitive_data": CryptoAction.ENCRYPT_AND_SIGN, + } + + # 4. Configure which attributes we expect to be included in the signature + # when reading items. There are two options for configuring this: + # + # - (Recommended) Configure `allowed_unsigned_attribute_prefix`: + # When defining your DynamoDb schema and deciding on attribute names, + # choose a distinguishing prefix (such as ":") for all attributes that + # you do not want to include in the signature. + # This has two main benefits: + # - It is easier to reason about the security and authenticity of data within your item + # when all unauthenticated data is easily distinguishable by their attribute name. + # - If you need to add new unauthenticated attributes in the future, + # you can easily make the corresponding update to your `attribute_actions` + # and immediately start writing to that new attribute, without + # any other configuration update needed. + # Once you configure this field, it is not safe to update it. + # + # - Configure `allowed_unsigned_attributes`: You may also explicitly list + # a set of attributes that should be considered unauthenticated when encountered + # on read. Be careful if you use this configuration. Do not remove an attribute + # name from this configuration, even if you are no longer writing with that attribute, + # as old items may still include this attribute, and our configuration needs to know + # to continue to exclude this attribute from the signature scope. + # If you add new attribute names to this field, you must first deploy the update to this + # field to all readers in your host fleet before deploying the update to start writing + # with that new attribute. + # + # For this example, we currently authenticate all attributes. To make it easier to + # add unauthenticated attributes in the future, we define a prefix ":" for such attributes. + unsign_attr_prefix = ":" + + # 5. Create the DynamoDb Encryption configuration for the table we will be writing to. + table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="partition_key", + sort_key_name="sort_key", + attribute_actions_on_encrypt=attribute_actions, + keyring=raw_rsa_keyring, + allowed_unsigned_attribute_prefix=unsign_attr_prefix, + ) + + table_configs = {ddb_table_name: table_config} + tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) + + # 6. Create the EncryptedClient + ddb_client = boto3.client("dynamodb") + encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) + + # 7. Put an item into our table using the above client. + # Before the item gets sent to DynamoDb, it will be encrypted + # client-side using the Raw RSA keyring. + item = { + "partition_key": {"S": "rawRsaKeyringItem"}, + "sort_key": {"N": "0"}, + "sensitive_data": {"S": "encrypt and sign me!"}, + } + + put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item) + + # Demonstrate that PutItem succeeded + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 8. Get the item back from our table using the same client. + # The client will decrypt the item client-side using the Raw RSA keyring + # and return the original item. + key_to_get = {"partition_key": {"S": "rawRsaKeyringItem"}, "sort_key": {"N": "0"}} + + get_response = encrypted_ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + returned_item = get_response["Item"] + assert returned_item["sensitive_data"]["S"] == "encrypt and sign me!" + + +def should_generate_new_rsa_key_pair() -> bool: + """ + Check if we need to generate a new RSA key pair. + + :return: True if we need to generate a new key pair, False otherwise + """ + # Check if a key pair already exists + private_key_file = os.path.exists(EXAMPLE_RSA_PRIVATE_KEY_FILENAME) + public_key_file = os.path.exists(EXAMPLE_RSA_PUBLIC_KEY_FILENAME) + + # If a key pair already exists: do not overwrite existing key pair + if private_key_file and public_key_file: + return False + + # If only one file is present: throw exception + if private_key_file and not public_key_file: + raise ValueError(f"Missing public key file at {EXAMPLE_RSA_PUBLIC_KEY_FILENAME}") + if not private_key_file and public_key_file: + raise ValueError(f"Missing private key file at {EXAMPLE_RSA_PRIVATE_KEY_FILENAME}") + + # If neither file is present, generate a new key pair + return True + + +def generate_rsa_key_pair(): + """Generate a new RSA key pair and save to PEM files.""" + # Safety check: Validate neither file is present + if os.path.exists(EXAMPLE_RSA_PRIVATE_KEY_FILENAME) or os.path.exists(EXAMPLE_RSA_PUBLIC_KEY_FILENAME): + raise FileExistsError("generateRsaKeyPair will not overwrite existing PEM files") + + # This code will generate a new RSA key pair for example use. + # The public and private key will be written to the files: + # - public: EXAMPLE_RSA_PUBLIC_KEY_FILENAME + # - private: EXAMPLE_RSA_PRIVATE_KEY_FILENAME + # In practice, you should not generate this in your code, and should instead + # retrieve this key from a secure key management system (e.g. HSM) + # This key is created here for example purposes only. + private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + + # Write private key PEM file + private_key_pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + + try: + with open(EXAMPLE_RSA_PRIVATE_KEY_FILENAME, "wb") as f: + f.write(private_key_pem) + except IOError as e: + raise OSError("IOError while writing private key PEM") from e + + # Write public key PEM file + public_key = private_key.public_key() + public_key_pem = public_key.public_bytes( + encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo + ) + + try: + with open(EXAMPLE_RSA_PUBLIC_KEY_FILENAME, "wb") as f: + f.write(public_key_pem) + except IOError as e: + raise RuntimeError("IOError while writing public key PEM") from e diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/shared_cache_across_hierarchical_keyrings_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/shared_cache_across_hierarchical_keyrings_example.py new file mode 100644 index 000000000..a7a2338e2 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/shared_cache_across_hierarchical_keyrings_example.py @@ -0,0 +1,352 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Example demonstrates how to use a shared cache across multiple Hierarchical Keyrings in single-threaded environments. + +IMPORTANT: This example and the shared cache functionality should ONLY be used in single-threaded environments. +The AWS Cryptographic Material Providers Library (MPL) for Python does not support multithreading for +components that interact with KMS. For more information about multithreading limitations, see: +https://github.com/aws/aws-cryptographic-material-providers-library/blob/main/AwsCryptographicMaterialProviders/runtimes/python/README.rst + +With this functionality, users only need to maintain one common shared cache across multiple +Hierarchical Keyrings with different Key Stores instances/KMS Clients/KMS Keys in a single-threaded environment. + +There are three important parameters that users need to carefully set while providing the shared cache: + +1. Partition ID - Partition ID is an optional parameter provided to the Hierarchical Keyring input, +which distinguishes Cryptographic Material Providers (i.e: Keyrings) writing to a cache. +- If the Partition ID is set and is the same for two Hierarchical Keyrings (or another Material Provider), + they CAN share the same cache entries in the cache. +- If the Partition ID is set and is different for two Hierarchical Keyrings (or another Material Provider), + they CANNOT share the same cache entries in the cache. +- If the Partition ID is not set by the user, it is initialized as a random 16-byte UUID which makes + it unique for every Hierarchical Keyring, and two Hierarchical Keyrings (or another Material Provider) + CANNOT share the same cache entries in the cache. + +2. Logical Key Store Name - This parameter is set by the user when configuring the Key Store for +the Hierarchical Keyring. This is a logical name for the branch key store. +Suppose you have a physical Key Store (K). You create two instances of K (K1 and K2). Now, you create +two Hierarchical Keyrings (HK1 and HK2) with these Key Store instances (K1 and K2 respectively). +- If you want to share cache entries across these two keyrings, you should set the Logical Key Store Names + for both the Key Store instances (K1 and K2) to be the same. +- If you set the Logical Key Store Names for K1 and K2 to be different, HK1 (which uses Key Store instance K1) + and HK2 (which uses Key Store instance K2) will NOT be able to share cache entries. + +3. Branch Key ID - Choose an effective Branch Key ID Schema + +This is demonstrated in the example below. +Notice that both K1 and K2 are instances of the same physical Key Store (K). +You MUST NEVER have two different physical Key Stores with the same Logical Key Store Name. + +Important Note: If you have two or more Hierarchy Keyrings with: +- Same Partition ID +- Same Logical Key Store Name of the Key Store for the Hierarchical Keyring +- Same Branch Key ID +then they WILL share the cache entries in the Shared Cache. +Please make sure that you set all of Partition ID, Logical Key Store Name and Branch Key ID +to be the same for two Hierarchical Keyrings if and only if you want them to share cache entries. + +This example sets up DynamoDb Encryption for the AWS SDK client using the Hierarchical +Keyring, which establishes a key hierarchy where "branch" keys are persisted in DynamoDb. +These branch keys are used to protect your data keys, and these branch keys are themselves +protected by a root KMS Key. + +This example first creates a shared cache that you can use across multiple Hierarchical Keyrings. +The example then configures a Hierarchical Keyring (HK1 and HK2) with the shared cache, +a Branch Key ID and two instances (K1 and K2) of the same physical Key Store (K) respectively, +i.e. HK1 with K1 and HK2 with K2. The example demonstrates that if you set the same Partition ID +for HK1 and HK2, the two keyrings can share cache entries. +If you set different Partition ID of the Hierarchical Keyrings, or different +Logical Key Store Names of the Key Store instances, then the keyrings will NOT +be able to share cache entries. + +Running this example requires access to the DDB Table whose name +is provided in CLI arguments. +This table must be configured with the following +primary key configuration: + - Partition key is named "partition_key" with type (S) + - Sort key is named "sort_key" with type (S) + +This example also requires using a KMS Key whose ARN +is provided in CLI arguments. You need the following access +on this key: + - GenerateDataKeyWithoutPlaintext + - Decrypt +""" +from typing import Dict + +import boto3 +from aws_cryptographic_material_providers.keystore import KeyStore +from aws_cryptographic_material_providers.keystore.config import KeyStoreConfig +from aws_cryptographic_material_providers.keystore.models import KMSConfigurationKmsKeyArn +from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders +from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig +from aws_cryptographic_material_providers.mpl.models import ( + CacheTypeDefault, + CacheTypeShared, + CreateAwsKmsHierarchicalKeyringInput, + CreateCryptographicMaterialsCacheInput, + DefaultCache, +) +from aws_cryptographic_material_providers.mpl.references import IKeyring +from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient +from aws_dbesdk_dynamodb.structures.dynamodb import ( + DynamoDbTableEncryptionConfig, + DynamoDbTablesEncryptionConfig, +) +from aws_dbesdk_dynamodb.structures.structured_encryption import ( + CryptoAction, +) + + +def get_ddb_client( + ddb_table_name: str, hierarchical_keyring: IKeyring, attribute_actions_on_encrypt: Dict[str, CryptoAction] +) -> boto3.client: + """ + Get a DynamoDB client configured with encryption using the given keyring. + + :param ddb_table_name: The name of the DynamoDB table + :param hierarchical_keyring: The hierarchical keyring to use + :param attribute_actions_on_encrypt: The attribute actions for encryption + :return: The configured DynamoDB client + """ + # Configure which attributes we expect to be included in the signature + # when reading items. There are two options for configuring this: + # + # - (Recommended) Configure `allowedUnsignedAttributesPrefix`: + # When defining your DynamoDb schema and deciding on attribute names, + # choose a distinguishing prefix (such as ":") for all attributes that + # you do not want to include in the signature. + # This has two main benefits: + # - It is easier to reason about the security and authenticity of data within your item + # when all unauthenticated data is easily distinguishable by their attribute name. + # - If you need to add new unauthenticated attributes in the future, + # you can easily make the corresponding update to your `attributeActionsOnEncrypt` + # and immediately start writing to that new attribute, without + # any other configuration update needed. + # Once you configure this field, it is not safe to update it. + # + # - Configure `allowedUnsignedAttributes`: You may also explicitly list + # a set of attributes that should be considered unauthenticated when encountered + # on read. Be careful if you use this configuration. Do not remove an attribute + # name from this configuration, even if you are no longer writing with that attribute, + # as old items may still include this attribute, and our configuration needs to know + # to continue to exclude this attribute from the signature scope. + # If you add new attribute names to this field, you must first deploy the update to this + # field to all readers in your host fleet before deploying the update to start writing + # with that new attribute. + # + # For this example, we currently authenticate all attributes. To make it easier to + # add unauthenticated attributes in the future, we define a prefix ":" for such attributes. + unsign_attr_prefix = ":" + + # Create the DynamoDb Encryption configuration for the table we will be writing to. + table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="partition_key", + sort_key_name="sort_key", + attribute_actions_on_encrypt=attribute_actions_on_encrypt, + keyring=hierarchical_keyring, + allowed_unsigned_attribute_prefix=unsign_attr_prefix, + ) + + table_configs = {ddb_table_name: table_config} + tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) + + # Create the EncryptedClient + ddb_client = boto3.client("dynamodb") + encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) + + return encrypted_ddb_client + + +def put_get_items(ddb_table_name: str, ddb_client: boto3.client): + """ + Put and get items using the given DynamoDB client. + + :param ddb_table_name: The name of the DynamoDB table + :param ddb_client: The DynamoDB client to use + """ + # Put an item into our table using the given ddb client. + # Before the item gets sent to DynamoDb, it will be encrypted + # client-side, according to our configuration. + # This example creates a Hierarchical Keyring for a single BranchKeyId. You can, however, use a + # BranchKeyIdSupplier as per your use-case. See the HierarchicalKeyringsExample.java for more + # information. + item = {"partition_key": {"S": "id"}, "sort_key": {"N": "0"}, "sensitive_data": {"S": "encrypt and sign me!"}} + + put_response = ddb_client.put_item(TableName=ddb_table_name, Item=item) + + # Demonstrate that PutItem succeeded + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # Get the item back from our table using the same client. + # The client will decrypt the item client-side, and return + # back the original item. + # This example creates a Hierarchical Keyring for a single BranchKeyId. You can, however, use a + # BranchKeyIdSupplier as per your use-case. See the HierarchicalKeyringsExample.java for more + # information. + key_to_get = {"partition_key": {"S": "id"}, "sort_key": {"N": "0"}} + + get_response = ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + returned_item = get_response["Item"] + assert returned_item["sensitive_data"]["S"] == "encrypt and sign me!" + + +def shared_cache_across_hierarchical_keyrings_example( + ddb_table_name: str, + branch_key_id: str, + key_store_table_name: str, + logical_key_store_name: str, + partition_id: str, + kms_key_id: str, +): + """ + Create multiple hierarchical keyrings sharing a cache and use them to encrypt/decrypt DynamoDB items. + + :param ddb_table_name: The name of the DynamoDB table + :param branch_key_id: The branch key ID to use + :param key_store_table_name: The name of the KeyStore DynamoDB table + :param logical_key_store_name: The logical name for the KeyStore + :param partition_id: The partition ID for cache sharing + :param kms_key_id: ARN of the KMS key + """ + # 1. Create the CryptographicMaterialsCache (CMC) to share across multiple Hierarchical Keyrings + # using the Material Providers Library in a single-threaded environment. + # IMPORTANT: This shared cache must only be used in single-threaded environments as the + # MPL for Python does not support multithreading for KMS operations. + # This CMC takes in: + # - CacheType + mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) + + cache = CacheTypeDefault(DefaultCache(entry_capacity=100)) + + cryptographic_materials_cache_input = CreateCryptographicMaterialsCacheInput(cache=cache) + + shared_cryptographic_materials_cache = mat_prov.create_cryptographic_materials_cache( + input=cryptographic_materials_cache_input + ) + + # 2. Create a CacheType object for the sharedCryptographicMaterialsCache + # Note that the `cache` parameter in the Hierarchical Keyring Input takes a `CacheType` as input + shared_cache = CacheTypeShared( + # This is the `Shared` CacheType that passes an already initialized shared cache + shared_cryptographic_materials_cache + ) + + # Initial KeyStore Setup: This example requires that you have already + # created your KeyStore, and have populated it with a new branch key. + + # 3. Configure your KeyStore resource keystore1. + # This SHOULD be the same configuration that you used + # to initially create and populate your KeyStore. + # Note that key_store_table_name is the physical Key Store, + # and keystore1 is instances of this physical Key Store. + keystore1 = KeyStore( + config=KeyStoreConfig( + ddb_client=boto3.client("dynamodb"), + ddb_table_name=key_store_table_name, + logical_key_store_name=logical_key_store_name, + kms_client=boto3.client("kms"), + kms_configuration=KMSConfigurationKmsKeyArn(kms_key_id), + ) + ) + + # 4. Create the Hierarchical Keyring HK1 with Key Store instance K1, partitionId, + # the shared Cache and the BranchKeyId. + # Note that we are now providing an already initialized shared cache instead of just mentioning + # the cache type and the Hierarchical Keyring initializing a cache at initialization. + + # This example creates a Hierarchical Keyring for a single BranchKeyId. You can, however, use a + # BranchKeyIdSupplier as per your use-case. See the HierarchicalKeyringsExample.java for more + # information. + + # Please make sure that you read the guidance on how to set Partition ID, Logical Key Store Name and + # Branch Key ID at the top of this example before creating Hierarchical Keyrings with a Shared Cache. + # partitionId for this example is a random UUID + keyring_input1 = CreateAwsKmsHierarchicalKeyringInput( + key_store=keystore1, + branch_key_id=branch_key_id, + ttl_seconds=600, # This dictates how often we call back to KMS to authorize use of the branch keys + cache=shared_cache, + partition_id=partition_id, + ) + + hierarchical_keyring1 = mat_prov.create_aws_kms_hierarchical_keyring(input=keyring_input1) + + # 5. Configure which attributes are encrypted and/or signed when writing new items. + # For each attribute that may exist on the items we plan to write to our DynamoDbTable, + # we must explicitly configure how they should be treated during item encryption: + # - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature + # - SIGN_ONLY: The attribute not encrypted, but is still included in the signature + # - DO_NOTHING: The attribute is not encrypted and not included in the signature + attribute_actions_on_encrypt = { + "partition_key": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY + "sort_key": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY + "sensitive_data": CryptoAction.ENCRYPT_AND_SIGN, + } + + # 6. Get the DDB Client for Hierarchical Keyring 1. + ddb_client1 = get_ddb_client(ddb_table_name, hierarchical_keyring1, attribute_actions_on_encrypt) + + # 7. Encrypt Decrypt roundtrip with ddb_client1 + put_get_items(ddb_table_name, ddb_client1) + + # Through the above encrypt and decrypt roundtrip, the cache will be populated and + # the cache entries can be used by another Hierarchical Keyring with the + # - Same Partition ID + # - Same Logical Key Store Name of the Key Store for the Hierarchical Keyring + # - Same Branch Key ID + + # 8. Configure your KeyStore resource keystore2. + # This SHOULD be the same configuration that you used + # to initially create and populate your physical KeyStore. + # Note that key_store_table_name is the physical Key Store, + # and keystore2 is instances of this physical Key Store. + + # Note that for this example, keystore2 is identical to keystore1. + # You can optionally change configurations like KMS Client or KMS Key ID based + # on your use-case. + # Make sure you have the required permissions to use different configurations. + + # - If you want to share cache entries across two keyrings HK1 and HK2, + # you should set the Logical Key Store Names for both + # Key Store instances (K1 and K2) to be the same. + # - If you set the Logical Key Store Names for K1 and K2 to be different, + # HK1 (which uses Key Store instance K1) and HK2 (which uses Key Store + # instance K2) will NOT be able to share cache entries. + keystore2 = KeyStore( + config=KeyStoreConfig( + ddb_client=boto3.client("dynamodb"), + ddb_table_name=key_store_table_name, + logical_key_store_name=logical_key_store_name, + kms_client=boto3.client("kms"), + kms_configuration=KMSConfigurationKmsKeyArn(kms_key_id), + ) + ) + + # 9. Create the Hierarchical Keyring HK2 with Key Store instance K2, the shared Cache + # and the same partitionId and BranchKeyId used in HK1 because we want to share cache entries + # (and experience cache HITS). + + # Please make sure that you read the guidance on how to set Partition ID, Logical Key Store Name and + # Branch Key ID at the top of this example before creating Hierarchical Keyrings with a Shared Cache. + # partitionId for this example is a random UUID + keyring_input2 = CreateAwsKmsHierarchicalKeyringInput( + key_store=keystore2, + branch_key_id=branch_key_id, + ttl_seconds=600, # This dictates how often we call back to KMS to authorize use of the branch keys + cache=shared_cache, + partition_id=partition_id, + ) + + hierarchical_keyring2 = mat_prov.create_aws_kms_hierarchical_keyring(input=keyring_input2) + + # 10. Get the DDB Client for Hierarchical Keyring 2. + ddb_client2 = get_ddb_client(ddb_table_name, hierarchical_keyring2, attribute_actions_on_encrypt) + + # 11. Encrypt Decrypt roundtrip with ddb_client2 + put_get_items(ddb_table_name, ddb_client2) diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/scan_error_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/scan_error_example.py new file mode 100644 index 000000000..b844b573a --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/scan_error_example.py @@ -0,0 +1,149 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Example demonstrating error handling for failed decryption during DynamoDB Scan operations. + +Uses the Scan operation to show how to retrieve error messages from the +returned CollectionOfErrors when some of the Scan results do not decrypt successfully. + +Running this example requires access to the DDB Table whose name is provided in +CLI arguments. This table must be configured with the following primary key +configuration: + - Partition key is named "partition_key" with type (S) + - Sort key is named "sort_key" with type (N) +""" + +import sys + +import boto3 +from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders +from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig +from aws_cryptographic_material_providers.mpl.errors import CollectionOfErrors +from aws_cryptographic_material_providers.mpl.models import CreateAwsKmsMrkMultiKeyringInput, DBEAlgorithmSuiteId +from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient +from aws_dbesdk_dynamodb.structures.dynamodb import ( + DynamoDbTableEncryptionConfig, + DynamoDbTablesEncryptionConfig, +) +from aws_dbesdk_dynamodb.structures.structured_encryption import ( + CryptoAction, +) + + +def print_exception(e: Exception, indent: str = ""): + """ + Print exception and any nested CollectionOfErrors. + + :param e: Exception to print + :param indent: Indentation string for nested errors + """ + print(indent + str(e), file=sys.stderr) + if isinstance(e.__cause__, CollectionOfErrors): + print(indent + str(e.__cause__), file=sys.stderr) + for err in e.__cause__.list(): + print_exception(err, indent + " ") + elif isinstance(e, CollectionOfErrors): + for err in e.list(): + print_exception(err, indent + " ") + + +def scan_error(kms_key_id: str, ddb_table_name: str): + """ + Demonstrate handling scan errors. + + :param kms_key_id: The ARN of the KMS key to use + :param ddb_table_name: The name of the DynamoDB table + """ + # 1. Create a Keyring. This Keyring will be responsible for protecting the data keys that protect your data. + # For this example, we will create a AWS KMS Keyring with the AWS KMS Key we want to use. + # We will use the `create_aws_kms_mrk_multi_keyring` method to create this keyring, + # as it will correctly handle both single region and Multi-Region KMS Keys. + mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) + + kms_keyring = mat_prov.create_aws_kms_mrk_multi_keyring( + input=CreateAwsKmsMrkMultiKeyringInput(generator=kms_key_id) + ) + + # 2. Configure which attributes are encrypted and/or signed when writing new items. + # For each attribute that may exist on the items we plan to write to our DynamoDbTable, + # we must explicitly configure how they should be treated during item encryption: + # - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature + # - SIGN_ONLY: The attribute not encrypted, but is still included in the signature + # - DO_NOTHING: The attribute is not encrypted and not included in the signature + attribute_actions = { + "partition_key": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY + "sort_key": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY + "attribute1": CryptoAction.ENCRYPT_AND_SIGN, + "attribute2": CryptoAction.SIGN_ONLY, + ":attribute3": CryptoAction.DO_NOTHING, + } + + # 3. Configure which attributes we expect to be included in the signature + # when reading items. There are two options for configuring this: + # + # - (Recommended) Configure `allowed_unsigned_attribute_prefix`: + # When defining your DynamoDb schema and deciding on attribute names, + # choose a distinguishing prefix (such as ":") for all attributes that + # you do not want to include in the signature. + # This has two main benefits: + # - It is easier to reason about the security and authenticity of data within your item + # when all unauthenticated data is easily distinguishable by their attribute name. + # - If you need to add new unauthenticated attributes in the future, + # you can easily make the corresponding update to your `attribute_actions` + # and immediately start writing to that new attribute, without + # any other configuration update needed. + # Once you configure this field, it is not safe to update it. + # + # - Configure `allowed_unsigned_attributes`: You may also explicitly list + # a set of attributes that should be considered unauthenticated when encountered + # on read. Be careful if you use this configuration. Do not remove an attribute + # name from this configuration, even if you are no longer writing with that attribute, + # as old items may still include this attribute, and our configuration needs to know + # to continue to exclude this attribute from the signature scope. + # If you add new attribute names to this field, you must first deploy the update to this + # field to all readers in your host fleet before deploying the update to start writing + # with that new attribute. + # + # For this example, we have designed our DynamoDb table such that any attribute name with + # the ":" prefix should be considered unauthenticated. + unsign_attr_prefix = ":" + + # 4. Create the DynamoDb Encryption configuration for the table we will be writing to. + table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="partition_key", + sort_key_name="sort_key", + attribute_actions_on_encrypt=attribute_actions, + keyring=kms_keyring, + allowed_unsigned_attribute_prefix=unsign_attr_prefix, + # Specifying an algorithm suite is not required, + # but is done here to demonstrate how to do so. + # We suggest using the + # `ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_ECDSA_P384_SYMSIG_HMAC_SHA384` suite, + # which includes AES-GCM with key derivation, signing, and key commitment. + # This is also the default algorithm suite if one is not specified in this config. + # For more information on supported algorithm suites, see: + # https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/supported-algorithms.html + algorithm_suite_id=DBEAlgorithmSuiteId.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_ECDSA_P384_SYMSIG_HMAC_SHA384, + ) + + table_configs = {ddb_table_name: table_config} + tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) + + # 5. Create the EncryptedClient + ddb_client = boto3.client("dynamodb") + encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) + + # 6. Perform a Scan for which some records will not decrypt + expression_attribute_values = {":prefix": {"S": "Broken"}} + + try: + encrypted_ddb_client.scan( + TableName=ddb_table_name, + FilterExpression="begins_with(partition_key, :prefix)", + ExpressionAttributeValues=expression_attribute_values, + ) + assert False, "scan should have failed" + except Exception as e: + print_exception(e) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/cleanup.py b/Examples/runtimes/python/DynamoDBEncryption/test/cleanup.py new file mode 100644 index 000000000..4ae8a3014 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/cleanup.py @@ -0,0 +1,82 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Test cleanup utilities for DynamoDB Encryption SDK. + +This module provides utilities for cleaning up resources after running tests. + +WARNING: Please be careful. This is only a test utility and should NOT be used in production code. +It is specifically designed for cleaning up test resources after test execution. +- Running this code on production resources or any data you want to keep could result + in cryptographic shredding (permanent loss of access to encrypted data). +- Only use this on test resources that you are willing to permanently delete. +- Never run this against any production DynamoDB tables. Ensure you have backups + of any important data before running cleanup operations. +""" +import boto3 + +BRANCH_KEY_IDENTIFIER_FIELD = "branch-key-id" +TYPE_FIELD = "type" + + +def delete_branch_key( + identifier: str, + table_name: str, + ddb_client: boto3.client, +) -> bool: + """ + Delete all branch key items with the given identifier. + + Args: + identifier: Branch key identifier to delete + table_name: DynamoDB table name + ddb_client: DynamoDB client to use + + Returns: + True if all items were deleted, False if more than 100 items exist + + Raises: + ValueError: If an item is not a branch key + + """ + if ddb_client is None: + ddb_client = boto3.client("dynamodb") + + # Query for items with matching identifier + query_response = ddb_client.query( + TableName=table_name, + KeyConditionExpression="#pk = :pk", + ExpressionAttributeNames={"#pk": BRANCH_KEY_IDENTIFIER_FIELD}, + ExpressionAttributeValues={":pk": {"S": identifier}}, + ) + + items = query_response.get("Items", []) + if not items: + return True + + # Create delete requests for each item + delete_items = [] + for item in items: + if TYPE_FIELD not in item: + raise ValueError("Item is not a branch key") + + delete_item = { + "Delete": { + "Key": {BRANCH_KEY_IDENTIFIER_FIELD: {"S": identifier}, TYPE_FIELD: item[TYPE_FIELD]}, + "TableName": table_name, + } + } + delete_items.append(delete_item) + + if not delete_items: + return True + + # DynamoDB transactions are limited to 100 items + if len(delete_items) > 100: + delete_items = delete_items[:100] + + # Execute the delete transaction + ddb_client.transact_write_items(TransactItems=delete_items) + + # Return False if we had to truncate the deletion + return len(items) <= 100 diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/__init__.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_hierarchical_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_hierarchical_keyring_example.py new file mode 100644 index 000000000..61cdb10a0 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_hierarchical_keyring_example.py @@ -0,0 +1,37 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test hierarchical keyring example.""" +import time + +import pytest + +from ...src.create_keystore_key_example import keystore_create_key +from ...src.keyring.hierarchical_keyring_example import hierarchical_keyring_get_item_put_item +from ..cleanup import delete_branch_key +from ..test_utils import ( + TEST_DDB_TABLE_NAME, + TEST_KEYSTORE_KMS_KEY_ID, + TEST_KEYSTORE_NAME, + TEST_LOGICAL_KEYSTORE_NAME, +) + +pytestmark = [pytest.mark.examples] + + +def test_hierarchical_keyring_example(): + """Test hierarchical_keyring_example.""" + # Create new branch keys for test + key_id1 = keystore_create_key(TEST_KEYSTORE_NAME, TEST_LOGICAL_KEYSTORE_NAME, TEST_KEYSTORE_KMS_KEY_ID) + key_id2 = keystore_create_key(TEST_KEYSTORE_NAME, TEST_LOGICAL_KEYSTORE_NAME, TEST_KEYSTORE_KMS_KEY_ID) + + # Key creation is eventually consistent, so wait 5 seconds to decrease the likelihood + # our test fails due to eventual consistency issues. + time.sleep(5) + + hierarchical_keyring_get_item_put_item( + TEST_DDB_TABLE_NAME, key_id1, key_id2, TEST_KEYSTORE_NAME, TEST_LOGICAL_KEYSTORE_NAME, TEST_KEYSTORE_KMS_KEY_ID + ) + + # Cleanup Branch Key + delete_branch_key(key_id1, TEST_KEYSTORE_NAME, None) + delete_branch_key(key_id2, TEST_KEYSTORE_NAME, None) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_kms_ecdh_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_kms_ecdh_keyring_example.py new file mode 100644 index 000000000..d54bd273e --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_kms_ecdh_keyring_example.py @@ -0,0 +1,40 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test KMS ECDH keyring examples.""" +import pytest + +from ...src.keyring.kms_ecdh_keyring_example import ( + EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME, + EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME, + kms_ecdh_discovery_get_item, + kms_ecdh_keyring_get_item_put_item, + should_get_new_public_keys, + write_public_key_pem_for_ecc_key, +) +from ..test_utils import TEST_DDB_TABLE_NAME, TEST_KMS_ECDH_KEY_ID_P256_RECIPIENT, TEST_KMS_ECDH_KEY_ID_P256_SENDER + +pytestmark = [pytest.mark.examples] + + +def test_kms_ecdh_keyring_example_static(): + """Test kms_ecdh_keyring_example with static configuration.""" + # You may provide your own ECC public keys at + # - EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME + # - EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME. + # If you provide these, the keys MUST be on curve P256 + # This must be the public key for the ECC key represented at eccKeyArn + # If this file is not present, this will write a UTF-8 encoded PEM file for you. + if should_get_new_public_keys(): + write_public_key_pem_for_ecc_key(TEST_KMS_ECDH_KEY_ID_P256_SENDER, EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME) + write_public_key_pem_for_ecc_key(TEST_KMS_ECDH_KEY_ID_P256_RECIPIENT, EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME) + + kms_ecdh_keyring_get_item_put_item(TEST_DDB_TABLE_NAME, TEST_KMS_ECDH_KEY_ID_P256_SENDER) + + +def test_kms_ecdh_keyring_example_discovery(): + """Test kms_ecdh_keyring_example with discovery configuration.""" + # In this example you do not need to provide the recipient ECC Public Key. + # On initialization, the keyring will call KMS:getPublicKey on the configured + # recipientKmsIdentifier set on the keyring. This example uses the previous example + # to write an item meant for the recipient. + kms_ecdh_discovery_get_item(TEST_DDB_TABLE_NAME, TEST_KMS_ECDH_KEY_ID_P256_RECIPIENT) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_kms_rsa_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_kms_rsa_keyring_example.py new file mode 100644 index 000000000..908531d5f --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_kms_rsa_keyring_example.py @@ -0,0 +1,27 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test for the KMS RSA keyring example.""" +import pytest + +from ...src.keyring.kms_rsa_keyring_example import ( + kms_rsa_keyring_example, + should_get_new_public_key, + write_public_key_pem_for_rsa_key, +) +from ..test_utils import ( + TEST_DDB_TABLE_NAME, + TEST_KMS_RSA_KEY_ID, +) + +pytestmark = [pytest.mark.examples] + + +def test_kms_rsa_keyring_example(): + """Test the KMS RSA keyring example.""" + # You may provide your own RSA public key at EXAMPLE_RSA_PUBLIC_KEY_FILENAME. + # This must be the public key for the RSA key represented at rsa_key_arn. + # If this file is not present, this will write a UTF-8 encoded PEM file for you. + if should_get_new_public_key(): + write_public_key_pem_for_rsa_key(TEST_KMS_RSA_KEY_ID) + + kms_rsa_keyring_example(TEST_DDB_TABLE_NAME, TEST_KMS_RSA_KEY_ID) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_mrk_discovery_multi_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_mrk_discovery_multi_keyring_example.py new file mode 100644 index 000000000..d2238bfa7 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_mrk_discovery_multi_keyring_example.py @@ -0,0 +1,22 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test MRK discovery multi-keyring example.""" +import pytest + +from ...src.keyring.mrk_discovery_multi_keyring_example import multi_mrk_discovery_keyring_get_item_put_item +from ..test_utils import ( + TEST_AWS_ACCOUNT_ID, + TEST_AWS_REGION, + TEST_DDB_TABLE_NAME, + TEST_MRK_KEY_ID, +) + +pytestmark = [pytest.mark.examples] + + +def test_mrk_discovery_multi_keyring_example(): + """Test mrk_discovery_multi_keyring_example.""" + accounts = [TEST_AWS_ACCOUNT_ID] + regions = [TEST_AWS_REGION] + + multi_mrk_discovery_keyring_get_item_put_item(TEST_DDB_TABLE_NAME, TEST_MRK_KEY_ID, accounts, regions) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_mrk_multi_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_mrk_multi_keyring_example.py new file mode 100644 index 000000000..e63ccc323 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_mrk_multi_keyring_example.py @@ -0,0 +1,21 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test MRK multi-keyring example.""" +import pytest + +from ...src.keyring.mrk_multi_keyring_example import multi_mrk_keyring_get_item_put_item +from ..test_utils import ( + TEST_DDB_TABLE_NAME, + TEST_KMS_KEY_ID, + TEST_MRK_KEY_ID, + TEST_MRK_REPLICA_KEY_ID_US_EAST_1, +) + +pytestmark = [pytest.mark.examples] + + +def test_mrk_multi_keyring_example(): + """Test mrk_multi_keyring_example.""" + multi_mrk_keyring_get_item_put_item( + TEST_DDB_TABLE_NAME, TEST_MRK_KEY_ID, TEST_KMS_KEY_ID, TEST_MRK_REPLICA_KEY_ID_US_EAST_1 + ) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_multi_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_multi_keyring_example.py new file mode 100644 index 000000000..0db6f3d80 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_multi_keyring_example.py @@ -0,0 +1,19 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test multi-keyring example.""" +import secrets + +import pytest + +from ...src.keyring.multi_keyring_example import multi_keyring_get_item_put_item +from ..test_utils import TEST_DDB_TABLE_NAME, TEST_KMS_KEY_ID + +pytestmark = [pytest.mark.examples] + + +def test_multi_keyring_example(): + """Test multi_keyring_example.""" + # Generate a new AES key + aes_key_bytes = secrets.token_bytes(32) # 32 bytes = 256 bits + + multi_keyring_get_item_put_item(TEST_DDB_TABLE_NAME, TEST_KMS_KEY_ID, aes_key_bytes) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_raw_aes_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_raw_aes_keyring_example.py new file mode 100644 index 000000000..93da5bddf --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_raw_aes_keyring_example.py @@ -0,0 +1,19 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test raw AES keyring example.""" +import secrets + +import pytest + +from ...src.keyring.raw_aes_keyring_example import raw_aes_keyring_get_item_put_item +from ..test_utils import TEST_DDB_TABLE_NAME + +pytestmark = [pytest.mark.examples] + + +def test_raw_aes_keyring_example(): + """Test raw_aes_keyring_example.""" + # Generate a new AES key + aes_key_bytes = secrets.token_bytes(32) # 32 bytes = 256 bits + + raw_aes_keyring_get_item_put_item(TEST_DDB_TABLE_NAME, aes_key_bytes) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_raw_ecdh_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_raw_ecdh_keyring_example.py new file mode 100644 index 000000000..6a7676d89 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_raw_ecdh_keyring_example.py @@ -0,0 +1,74 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test raw ECDH keyring examples.""" +import pytest +from aws_cryptography_primitives.smithygenerated.aws_cryptography_primitives.models import ECDHCurveSpec + +from ...src.keyring.raw_ecdh_keyring_example import ( + discovery_raw_ecdh_keyring_get_item, + ephemeral_raw_ecdh_keyring_put_item, + generate_ecc_key_pairs, + raw_ecdh_keyring_get_item_put_item, + should_generate_new_ecc_key_pairs, +) +from ..test_utils import TEST_DDB_TABLE_NAME + +pytestmark = [pytest.mark.examples] + + +def test_static_raw_ecdh_keyring_example(): + """Test raw_ecdh_keyring_example with static configuration.""" + # You may provide your own ECC Key pairs in the files located at + # - EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER + # - EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT + # If you provide this, the keys MUST be on curve P256 + # If these files are not present, this will generate a pair for you. + # For this example we will use the curve P256. + if should_generate_new_ecc_key_pairs(): + generate_ecc_key_pairs() + + # Part of using these keyrings is knowing which curve the keys used in the key agreement + # lie on. The keyring will fail if the keys do not lie on the configured curve. + raw_ecdh_keyring_get_item_put_item(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256) + + +def test_ephemeral_raw_ecdh_keyring_example(): + """Test raw_ecdh_keyring_example with ephemeral configuration.""" + # You may provide your own ECC Public Key in the files located at + # - EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT + # If you provide this, the keys MUST be on curve P256 + # If these files are not present, this will generate a pair for you. + # For this example we will use the curve P256. + if should_generate_new_ecc_key_pairs(): + generate_ecc_key_pairs() + + # Part of using these keyrings is knowing which curve the keys used in the key agreement + # lie on. The keyring will fail if the keys do not lie on the configured curve. + ephemeral_raw_ecdh_keyring_put_item(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256) + + +def test_discovery_raw_ecdh_keyring_example(): + """Test raw_ecdh_keyring_example with discovery configuration.""" + # You may provide your own ECC Public Key in the files located at + # - EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT + # - EXAMPLE_ECC_PRIVATE_KEY_FILENAME_RECIPIENT + # If you provide this, the keys MUST be on curve P256 + # If these files are not present, this will generate a pair for you. + # For this example we will use the curve P256. + if should_generate_new_ecc_key_pairs(): + generate_ecc_key_pairs() + + # The discovery configuration is not allowed to encrypt + # To understand this example best, we will write a record with the ephemeral configuration + # in the previous example. This means that the recipient public key configured on + # both keyrings is the same. This means that the other party has the recipient public key + # and is writing messages meant only for the owner of the recipient public key to decrypt. + + # In this call we are writing a record that is written with an ephemeral sender key pair. + # The recipient will be able to decrypt the message + ephemeral_raw_ecdh_keyring_put_item(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256) + + # In this call we are reading a record that was written with the recipient's public key. + # It will use the recipient's private key and the sender's public key stored in the message to + # calculate the appropriate shared secret to successfully decrypt the message. + discovery_raw_ecdh_keyring_get_item(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_raw_rsa_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_raw_rsa_keyring_example.py new file mode 100644 index 000000000..590a7948e --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_raw_rsa_keyring_example.py @@ -0,0 +1,25 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test for the Raw RSA keyring example.""" +import pytest + +from ...src.keyring.raw_rsa_keyring_example import ( + generate_rsa_key_pair, + raw_rsa_keyring_example, + should_generate_new_rsa_key_pair, +) +from ..test_utils import TEST_DDB_TABLE_NAME + +pytestmark = [pytest.mark.examples] + + +def test_raw_rsa_keyring_example(): + """Test the Raw RSA keyring example.""" + # You may provide your own RSA key pair in the files located at + # - EXAMPLE_RSA_PRIVATE_KEY_FILENAME + # - EXAMPLE_RSA_PUBLIC_KEY_FILENAME + # If these files are not present, this will generate a pair for you + if should_generate_new_rsa_key_pair(): + generate_rsa_key_pair() + + raw_rsa_keyring_example(TEST_DDB_TABLE_NAME) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_shared_cache_across_hierarchical_keyrings_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_shared_cache_across_hierarchical_keyrings_example.py new file mode 100644 index 000000000..2cfc10fe7 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_shared_cache_across_hierarchical_keyrings_example.py @@ -0,0 +1,43 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test for the shared cache across hierarchical keyrings example.""" +import time + +import pytest + +from ...src.create_keystore_key_example import keystore_create_key +from ...src.keyring.shared_cache_across_hierarchical_keyrings_example import ( + shared_cache_across_hierarchical_keyrings_example, +) +from ..cleanup import delete_branch_key +from ..test_utils import ( + TEST_DDB_TABLE_NAME, + TEST_KEYSTORE_KMS_KEY_ID, + TEST_KEYSTORE_NAME, + TEST_LOGICAL_KEYSTORE_NAME, + TEST_PARTITION_ID, +) + +pytestmark = [pytest.mark.examples] + + +def test_shared_cache_across_hierarchical_keyrings_example(): + """Test the shared cache across hierarchical keyrings example.""" + # Create new branch key for test + key_id = keystore_create_key(TEST_KEYSTORE_NAME, TEST_LOGICAL_KEYSTORE_NAME, TEST_KEYSTORE_KMS_KEY_ID) + + # Key creation is eventually consistent, so wait 5 seconds to decrease the likelihood + # our test fails due to eventual consistency issues. + time.sleep(5) + + shared_cache_across_hierarchical_keyrings_example( + TEST_DDB_TABLE_NAME, + key_id, + TEST_KEYSTORE_NAME, + TEST_LOGICAL_KEYSTORE_NAME, + TEST_PARTITION_ID, + TEST_KEYSTORE_KMS_KEY_ID, + ) + + # Cleanup Branch Key + delete_branch_key(key_id, TEST_KEYSTORE_NAME, None) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/test_create_keystore_key_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/test_create_keystore_key_example.py new file mode 100644 index 000000000..10a3c2cad --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/test_create_keystore_key_example.py @@ -0,0 +1,20 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test create key store key example.""" +import pytest + +from ..src.create_keystore_key_example import keystore_create_key +from .cleanup import delete_branch_key +from .test_utils import TEST_KEYSTORE_KMS_KEY_ID, TEST_KEYSTORE_NAME, TEST_LOGICAL_KEYSTORE_NAME + +pytestmark = [pytest.mark.examples] + + +def test_create_keystore_key_example(): + """Test create_key_store_key_example.""" + key_id = keystore_create_key(TEST_KEYSTORE_NAME, TEST_LOGICAL_KEYSTORE_NAME, TEST_KEYSTORE_KMS_KEY_ID) + + assert key_id is not None + + # Cleanup Branch Key + delete_branch_key(key_id, TEST_KEYSTORE_NAME, None) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/test_create_keystore_table_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/test_create_keystore_table_example.py new file mode 100644 index 000000000..fd030a0f7 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/test_create_keystore_table_example.py @@ -0,0 +1,14 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test create key store table example.""" +import pytest + +from ..src.create_keystore_table_example import keystore_create_table +from .test_utils import TEST_KEYSTORE_KMS_KEY_ID, TEST_KEYSTORE_NAME, TEST_LOGICAL_KEYSTORE_NAME + +pytestmark = [pytest.mark.examples] + + +def test_create_keystore_table_example(): + """Test create_key_store_table_example.""" + keystore_create_table(TEST_KEYSTORE_NAME, TEST_LOGICAL_KEYSTORE_NAME, TEST_KEYSTORE_KMS_KEY_ID) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/test_scan_error_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/test_scan_error_example.py new file mode 100644 index 000000000..ee0c3710d --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/test_scan_error_example.py @@ -0,0 +1,14 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test scan error example.""" +import pytest + +from ..src.scan_error_example import scan_error +from .test_utils import TEST_DDB_TABLE_NAME, TEST_KMS_KEY_ID + +pytestmark = [pytest.mark.examples] + + +def test_scan_error(): + """Test scan_error.""" + scan_error(TEST_KMS_KEY_ID, TEST_DDB_TABLE_NAME)