diff --git a/examples/README.md b/examples/README.md index c4c0e67ec..081e62fab 100644 --- a/examples/README.md +++ b/examples/README.md @@ -59,6 +59,12 @@ We start with AWS KMS examples, then show how to use other wrapping keys. * How to combine AWS KMS with an offline escrow key * [with keyrings](./src/keyring/multi/aws_kms_with_escrow.py) * [with master key providers](./src/master_key_provider/multi/aws_kms_with_escrow.py) +* How to reuse data keys across multiple messages + * [with the caching cryptographic materials manager](./src/crypto_materials_manager/caching/simple_cache.py) +* How to restrict algorithm suites + * [with a custom cryptographic materials manager](src/crypto_materials_manager/custom/algorithm_suite_enforcement.py) +* How to require encryption context fields + * [with a custom cryptographic materials manager](src/crypto_materials_manager/custom/requiring_encryption_context_fields.py) ### Keyrings diff --git a/examples/src/crypto_materials_manager/__init__.py b/examples/src/crypto_materials_manager/__init__.py new file mode 100644 index 000000000..f413e63bd --- /dev/null +++ b/examples/src/crypto_materials_manager/__init__.py @@ -0,0 +1,7 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Cryptographic materials manager examples. + +These examples show how to create and use cryptographic materials managers. +""" diff --git a/examples/src/crypto_materials_manager/caching/__init__.py b/examples/src/crypto_materials_manager/caching/__init__.py new file mode 100644 index 000000000..2c55faad8 --- /dev/null +++ b/examples/src/crypto_materials_manager/caching/__init__.py @@ -0,0 +1,7 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Caching cryptographic materials manager examples. + +These examples show how to configure and use the caching cryptographic materials manager. +""" diff --git a/examples/src/crypto_materials_manager/caching/simple_cache.py b/examples/src/crypto_materials_manager/caching/simple_cache.py new file mode 100644 index 000000000..bb14b25fe --- /dev/null +++ b/examples/src/crypto_materials_manager/caching/simple_cache.py @@ -0,0 +1,94 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +The default cryptographic materials manager (CMM) +creates new encryption and decryption materials +on every call. +This means every encrypted message is protected by a unique data key, +but it also means that you need to interact with your key management system +in order to process any message. +If this causes performance, operations, or cost issues for you, +you might benefit from data key caching. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/data-key-caching.html + +This example shows how to configure the caching CMM +to reuse data keys across multiple encrypted messages. + +In this example, we use an AWS KMS customer master key (CMK), +but you can use other key management options with the AWS Encryption SDK. +For examples that demonstrate how to use other key management configurations, +see the ``keyring`` and ``master_key_provider`` directories. + +In this example, we use the one-step encrypt and decrypt APIs. +""" +import aws_encryption_sdk +from aws_encryption_sdk.caches.local import LocalCryptoMaterialsCache +from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring +from aws_encryption_sdk.materials_managers.caching import CachingCryptoMaterialsManager + + +def run(aws_kms_cmk, source_plaintext): + # type: (str, bytes) -> None + """Demonstrate an encrypt/decrypt cycle using the caching cryptographic materials manager. + + :param str aws_kms_cmk: The ARN of an AWS KMS CMK that protects data keys + :param bytes source_plaintext: Plaintext to encrypt + """ + # Prepare your encryption context. + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + encryption_context = { + "encryption": "context", + "is not": "secret", + "but adds": "useful metadata", + "that can help you": "be confident that", + "the data you are handling": "is what you think it is", + } + + # Create the keyring that determines how your data keys are protected. + keyring = KmsKeyring(generator_key_id=aws_kms_cmk) + + # Create the caching cryptographic materials manager using your keyring. + cmm = CachingCryptoMaterialsManager( + keyring=keyring, + # The cache is where the caching CMM stores the materials. + # + # LocalCryptoMaterialsCache gives you a local, in-memory, cache. + cache=LocalCryptoMaterialsCache(capacity=100), + # max_age determines how long the caching CMM will reuse materials. + # + # This example uses two minutes. + # In production, always choose as small a value as possible + # that works for your requirements. + max_age=120.0, + # max_messages_encrypted determines how many messages + # the caching CMM will protect with the same materials. + # + # In production, always choose as small a value as possible + # that works for your requirements. + max_messages_encrypted=10, + ) + + # Encrypt your plaintext data. + ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, materials_manager=cmm + ) + + # Demonstrate that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Decrypt your encrypted data using the same cryptographic materials manager you used on encrypt. + # + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. + decrypted, decrypt_header = aws_encryption_sdk.decrypt(source=ciphertext, materials_manager=cmm) + + # Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert decrypted == source_plaintext + + # Verify that the encryption context used in the decrypt operation includes + # the encryption context that you specified when encrypting. + # The AWS Encryption SDK can add pairs, so don't require an exact match. + # + # In production, always use a meaningful encryption context. + assert set(encryption_context.items()) <= set(decrypt_header.encryption_context.items()) diff --git a/examples/src/crypto_materials_manager/custom/__init__.py b/examples/src/crypto_materials_manager/custom/__init__.py new file mode 100644 index 000000000..202647480 --- /dev/null +++ b/examples/src/crypto_materials_manager/custom/__init__.py @@ -0,0 +1,10 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Custom cryptographic materials manager (CMM) examples. + +The AWS Encryption SDK includes CMMs for common use cases, +but you might need to do something else. + +These examples show how you could create your own CMM for some specific requirements. +""" diff --git a/examples/src/crypto_materials_manager/custom/algorithm_suite_enforcement.py b/examples/src/crypto_materials_manager/custom/algorithm_suite_enforcement.py new file mode 100644 index 000000000..d7385fc76 --- /dev/null +++ b/examples/src/crypto_materials_manager/custom/algorithm_suite_enforcement.py @@ -0,0 +1,138 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +The AWS Encryption SDK supports several different algorithm suites +that offer different security properties. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/supported-algorithms.html + +By default, the AWS Encryption SDK will let you use any of these, +but you might want to restrict that further. + +We recommend that you use the default algorithm suite, +which uses AES-GCM with 256-bit keys, HKDF, and ECDSA message signing. +If your readers and writers have the same permissions, +you might want to omit the message signature for faster operation. +For more information about choosing a signed or unsigned algorithm suite, +see the AWS Encryption SDK developer guide: + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/supported-algorithms.html#other-algorithms + +This example shows how you can make a custom cryptographic materials manager (CMM) +that only allows encrypt requests that either specify one of these two algorithm suites +or do not specify an algorithm suite, in which case the default CMM uses the default algorithm suite. +""" +import aws_encryption_sdk +from aws_encryption_sdk.identifiers import AlgorithmSuite +from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring +from aws_encryption_sdk.keyrings.base import Keyring +from aws_encryption_sdk.materials_managers import ( + DecryptionMaterials, + DecryptionMaterialsRequest, + EncryptionMaterials, + EncryptionMaterialsRequest, +) +from aws_encryption_sdk.materials_managers.base import CryptoMaterialsManager +from aws_encryption_sdk.materials_managers.default import DefaultCryptoMaterialsManager + + +class UnapprovedAlgorithmSuite(Exception): + """Indicate that an unsupported algorithm suite was requested.""" + + +class RequireApprovedAlgorithmSuitesCryptoMaterialsManager(CryptoMaterialsManager): + """Only allow encryption requests for approved algorithm suites.""" + + def __init__(self, keyring): + # type: (Keyring) -> None + """Set up the inner cryptographic materials manager using the provided keyring. + + :param Keyring keyring: Keyring to use in the inner cryptographic materials manager + """ + self._allowed_algorithm_suites = { + None, # no algorithm suite in the request + AlgorithmSuite.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, # the default algorithm suite + AlgorithmSuite.AES_256_GCM_IV12_TAG16_HKDF_SHA256, # the recommended unsigned algorithm suite + } + # Wrap the provided keyring in the default cryptographic materials manager (CMM). + # + # This is the same thing that the encrypt and decrypt APIs, as well as the caching CMM, + # do if you provide a keyring instead of a CMM. + self._cmm = DefaultCryptoMaterialsManager(keyring=keyring) + + def get_encryption_materials(self, request): + # type: (EncryptionMaterialsRequest) -> EncryptionMaterials + """Block any requests that include an unapproved algorithm suite.""" + if request.algorithm not in self._allowed_algorithm_suites: + raise UnapprovedAlgorithmSuite("Unapproved algorithm suite requested!") + + return self._cmm.get_encryption_materials(request) + + def decrypt_materials(self, request): + # type: (DecryptionMaterialsRequest) -> DecryptionMaterials + """Be more permissive on decrypt and just pass through.""" + return self._cmm.decrypt_materials(request) + + +def run(aws_kms_cmk, source_plaintext): + # type: (str, bytes) -> None + """Demonstrate an encrypt/decrypt cycle using a custom cryptographic materials manager that filters requests. + + :param str aws_kms_cmk: The ARN of an AWS KMS CMK that protects data keys + :param bytes source_plaintext: Plaintext to encrypt + """ + # Prepare your encryption context. + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + encryption_context = { + "encryption": "context", + "is not": "secret", + "but adds": "useful metadata", + "that can help you": "be confident that", + "the data you are handling": "is what you think it is", + } + + # Create the keyring that determines how your data keys are protected. + keyring = KmsKeyring(generator_key_id=aws_kms_cmk) + + # Create the algorithm suite restricting cryptographic materials manager using your keyring. + cmm = RequireApprovedAlgorithmSuitesCryptoMaterialsManager(keyring=keyring) + + # Demonstrate that the algorithm suite restricting CMM will not let you use an unapproved algorithm suite. + try: + aws_encryption_sdk.encrypt( + source=source_plaintext, + encryption_context=encryption_context, + materials_manager=cmm, + algorithm=AlgorithmSuite.AES_256_GCM_IV12_TAG16, + ) + except UnapprovedAlgorithmSuite: + # You asked for an unapproved algorithm suite. + # Reaching this point means everything is working as expected. + pass + else: + # The algorithm suite restricting CMM keeps this from happening. + raise AssertionError("The algorithm suite restricting CMM does not let this happen!") + + # Encrypt your plaintext data. + ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, materials_manager=cmm + ) + + # Demonstrate that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Decrypt your encrypted data using the same cryptographic materials manager you used on encrypt. + # + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. + decrypted, decrypt_header = aws_encryption_sdk.decrypt(source=ciphertext, materials_manager=cmm) + + # Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert decrypted == source_plaintext + + # Verify that the encryption context used in the decrypt operation includes + # the encryption context that you specified when encrypting. + # The AWS Encryption SDK can add pairs, so don't require an exact match. + # + # In production, always use a meaningful encryption context. + assert set(encryption_context.items()) <= set(decrypt_header.encryption_context.items()) diff --git a/examples/src/crypto_materials_manager/custom/requiring_encryption_context_fields.py b/examples/src/crypto_materials_manager/custom/requiring_encryption_context_fields.py new file mode 100644 index 000000000..c797849bb --- /dev/null +++ b/examples/src/crypto_materials_manager/custom/requiring_encryption_context_fields.py @@ -0,0 +1,153 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Encryption context is a powerful tool for access and audit controls +because it lets you tie *non-secret* metadata about a plaintext value to the encrypted message. +Within the AWS Encryption SDK, +you can use cryptographic materials managers to analyse the encryption context +to provide logical controls and additional metadata. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + +If you are using the AWS Encryption SDK with AWS KMS, +you can use AWS KMS to provide additional powerful controls using the encryption context. +For more information on that, see the KMS developer guide: + +https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#encrypt_context + +This example shows how to create a custom cryptographic materials manager (CMM) +that requires a particular field in the encryption context. +""" +import aws_encryption_sdk +from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring +from aws_encryption_sdk.keyrings.base import Keyring +from aws_encryption_sdk.materials_managers import ( + DecryptionMaterials, + DecryptionMaterialsRequest, + EncryptionMaterials, + EncryptionMaterialsRequest, +) +from aws_encryption_sdk.materials_managers.base import CryptoMaterialsManager +from aws_encryption_sdk.materials_managers.default import DefaultCryptoMaterialsManager + + +class MissingClassificationError(Exception): + """Indicates that an encryption context was found that lacked a classification identifier.""" + + +class ClassificationRequiringCryptoMaterialsManager(CryptoMaterialsManager): + """Only allow requests when the encryption context contains a classification identifier.""" + + def __init__(self, keyring): + # type: (Keyring) -> None + """Set up the inner cryptographic materials manager using the provided keyring. + + :param Keyring keyring: Keyring to use in the inner cryptographic materials manager + """ + self._classification_field = "classification" + self._classification_error = MissingClassificationError("Encryption context does not contain classification!") + # Wrap the provided keyring in the default cryptographic materials manager (CMM). + # + # This is the same thing that the encrypt and decrypt APIs, as well as the caching CMM, + # do if you provide a keyring instead of a CMM. + self._cmm = DefaultCryptoMaterialsManager(keyring=keyring) + + def get_encryption_materials(self, request): + # type: (EncryptionMaterialsRequest) -> EncryptionMaterials + """Block any requests that do not contain a classification identifier in the encryption context.""" + if self._classification_field not in request.encryption_context: + raise self._classification_error + + return self._cmm.get_encryption_materials(request) + + def decrypt_materials(self, request): + # type: (DecryptionMaterialsRequest) -> DecryptionMaterials + """Block any requests that do not contain a classification identifier in the encryption context.""" + if self._classification_field not in request.encryption_context: + raise self._classification_error + + return self._cmm.decrypt_materials(request) + + +def run(aws_kms_cmk, source_plaintext): + # type: (str, bytes) -> None + """Demonstrate an encrypt/decrypt cycle using a custom cryptographic materials manager that filters requests. + + :param str aws_kms_cmk: The ARN of an AWS KMS CMK that protects data keys + :param bytes source_plaintext: Plaintext to encrypt + """ + # Prepare your encryption context. + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + encryption_context = { + "encryption": "context", + "is not": "secret", + "but adds": "useful metadata", + "that can help you": "be confident that", + "the data you are handling": "is what you think it is", + } + + # Create the keyring that determines how your data keys are protected. + keyring = KmsKeyring(generator_key_id=aws_kms_cmk) + + # Create the classification requiring cryptographic materials manager using your keyring. + cmm = ClassificationRequiringCryptoMaterialsManager(keyring=keyring) + + # Demonstrate that the classification requiring CMM will not let you encrypt without a classification identifier. + try: + aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, materials_manager=cmm, + ) + except MissingClassificationError: + # Your encryption context did not contain a classification identifier. + # Reaching this point means everything is working as expected. + pass + else: + # The classification requiring CMM keeps this from happening. + raise AssertionError("The classification requiring CMM does not let this happen!") + + # Encrypt your plaintext data. + classified_ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, + encryption_context=dict(classification="secret", **encryption_context), + materials_manager=cmm, + ) + + # Demonstrate that the ciphertext and plaintext are different. + assert classified_ciphertext != source_plaintext + + # Decrypt your encrypted data using the same cryptographic materials manager you used on encrypt. + # + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. + decrypted, decrypt_header = aws_encryption_sdk.decrypt(source=classified_ciphertext, materials_manager=cmm) + + # Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert decrypted == source_plaintext + + # Verify that the encryption context used in the decrypt operation includes + # the encryption context that you specified when encrypting. + # The AWS Encryption SDK can add pairs, so don't require an exact match. + # + # In production, always use a meaningful encryption context. + assert set(encryption_context.items()) <= set(decrypt_header.encryption_context.items()) + + # Now demonstrate the decrypt path of the classification requiring cryptographic materials manager. + + # Encrypt your plaintext using the keyring and do not include a classification identifier. + unclassified_ciphertext, encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, keyring=keyring + ) + + assert "classification" not in encrypt_header.encryption_context + + # Demonstrate that the classification requiring CMM + # will not let you decrypt messages without classification identifiers. + try: + aws_encryption_sdk.decrypt(source=unclassified_ciphertext, materials_manager=cmm) + except MissingClassificationError: + # Your encryption context did not contain a classification identifier. + # Reaching this point means everything is working as expected. + pass + else: + # The classification requiring CMM keeps this from happening. + raise AssertionError("The classification requiring CMM does not let this happen!") diff --git a/test/unit/keyrings/test_aws_kms.py b/test/unit/keyrings/test_aws_kms.py index 78f84635e..2adf0f656 100644 --- a/test/unit/keyrings/test_aws_kms.py +++ b/test/unit/keyrings/test_aws_kms.py @@ -42,10 +42,7 @@ def test_kms_keyring_builds_correct_inner_keyring_multikeyring(): supplier = DefaultClientSupplier() test = KmsKeyring( - generator_key_id=generator_id, - key_ids=(child_id_1, child_id_2), - grant_tokens=grants, - client_supplier=supplier, + generator_key_id=generator_id, key_ids=(child_id_1, child_id_2), grant_tokens=grants, client_supplier=supplier, ) # We specified a generator and child IDs, so the inner keyring MUST be a multikeyring