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..e69de29bb 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..8d53f5b9e --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/example_branch_key_id_supplier.py @@ -0,0 +1,71 @@ +# 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): + """ + Example constructor for 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: + """ + Returns 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 + """ + # print("Getting branch key ID from DDB key") + # print(param) + # raise ValueError(f'Invalid DDB key: {param}') + 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 + ) \ No newline at end of file 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..87b27792e --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example.py @@ -0,0 +1,249 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Example demonstrates 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..11e0ad475 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_ecdh_keyring_example.py @@ -0,0 +1,500 @@ +# 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 + +# TODO: Resolve dependencies +# from aws_cryptographic_primitives.model 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='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='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 RuntimeError("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 RuntimeError( + 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 RuntimeError( + 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 RuntimeError( + "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) \ No newline at end of file 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..4a1636480 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example.py @@ -0,0 +1,630 @@ +# 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 RuntimeError("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 RuntimeError("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 RuntimeError("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 RuntimeError("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.ECC_NIST_P256()) + sender_public_key = sender_private_key.public_key() + + # Generate recipient key pair + recipient_private_key = ec.generate_private_key(ec.ECC_NIST_P256()) + 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/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..609ee6db2 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/shared_cache_across_hierarchical_keyrings_example.py @@ -0,0 +1,381 @@ +# 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. + +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. + +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 + # 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/test/keyring/__init__.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/__init__.py new file mode 100644 index 000000000..622962d60 --- /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.""" \ No newline at end of file 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..d50a7bfbe --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_hierarchical_keyring_example.py @@ -0,0 +1,44 @@ +# 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 ..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 + ) 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..828ca1460 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_kms_ecdh_keyring_example.py @@ -0,0 +1,48 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test KMS ECDH keyring examples.""" +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 + + +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_raw_ecdh_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_raw_ecdh_keyring_example.py new file mode 100644 index 000000000..5e0930cc7 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_raw_ecdh_keyring_example.py @@ -0,0 +1,84 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test raw ECDH keyring examples.""" +# TODO: Resolve dependencies +# from aws_cryptographic_material_providers.mpl.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 + + +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, + "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, + '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, + '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, + 'ECC_NIST_P256' + ) 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..f4da1e790 --- /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 + +pytestmark = [pytest.mark.examples] + +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 ..test_utils import ( + TEST_DDB_TABLE_NAME, + TEST_KEYSTORE_KMS_KEY_ID, + TEST_KEYSTORE_NAME, + TEST_LOGICAL_KEYSTORE_NAME, + TEST_PARTITION_ID, +) + + +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 + )