def __attrs_post_init__(self): """Configure internal keyring.""" key_ids_provided = self._generator_key_id is not None or self._key_ids both = key_ids_provided and self._is_discovery neither = not key_ids_provided and not self._is_discovery if both: raise TypeError("is_discovery cannot be True if key IDs are provided") if neither: raise TypeError("is_discovery cannot be False if no key IDs are provided") if self._is_discovery: self._inner_keyring = _AwsKmsDiscoveryKeyring( client_supplier=self._client_supplier, grant_tokens=self._grant_tokens ) return if self._generator_key_id is None: generator_keyring = None else: generator_keyring = _AwsKmsSingleCmkKeyring( key_id=self._generator_key_id, client_supplier=self._client_supplier, grant_tokens=self._grant_tokens ) child_keyrings = [ _AwsKmsSingleCmkKeyring( key_id=key_id, client_supplier=self._client_supplier, grant_tokens=self._grant_tokens ) for key_id in self._key_ids ] self._inner_keyring = MultiKeyring(generator=generator_keyring, children=child_keyrings)
def test_identity_keyring_as_generator_and_no_data_encryption_key( identity_keyring): test_multi_keyring = MultiKeyring(generator=identity_keyring) with pytest.raises(GenerateKeyError) as exc_info: test_multi_keyring.on_encrypt( encryption_materials=get_encryption_materials_without_data_key()) assert exc_info.match("Unable to generate data encryption key.")
def test_on_encrypt_edk_length_when_keyring_generates_but_does_not_encrypt_encryption_materials_with_data_key( ): test_multi_keyring = MultiKeyring(generator=OnlyGenerateKeyring()) test = test_multi_keyring.on_encrypt( encryption_materials=get_encryption_materials_with_encrypted_data_key( )) assert len(test.encrypted_data_keys) == len( get_encryption_materials_with_encrypted_data_key().encrypted_data_keys)
def test_on_encrypt_when_data_encryption_key_given(mock_generator, mock_child_1, mock_child_2): test_multi_keyring = MultiKeyring(generator=mock_generator, children=[mock_child_1, mock_child_2]) initial_materials = get_encryption_materials_with_data_key() new_materials = test_multi_keyring.on_encrypt( encryption_materials=initial_materials) assert new_materials is not initial_materials for keyring in test_multi_keyring._decryption_keyrings: keyring.on_encrypt.assert_called_once()
def test_on_decrypt_when_data_encryption_key_given(mock_generator, mock_child_1, mock_child_2): test_multi_keyring = MultiKeyring(generator=mock_generator, children=[mock_child_1, mock_child_2]) initial_materials = get_decryption_materials_with_data_key() new_materials = test_multi_keyring.on_decrypt( decryption_materials=initial_materials, encrypted_data_keys=[]) assert new_materials is initial_materials for keyring in test_multi_keyring._decryption_keyrings: assert not keyring.on_decrypt.called
def get_multi_keyring_with_generator_and_children(): private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048, backend=default_backend()) return MultiKeyring( generator=RawAESKeyring( key_namespace=_PROVIDER_ID, key_name=_KEY_ID, wrapping_key=_WRAPPING_KEY_AES, ), children=[ RawRSAKeyring( key_namespace=_PROVIDER_ID, key_name=_KEY_ID, wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, private_wrapping_key=private_key, public_wrapping_key=private_key.public_key(), ), RawRSAKeyring( key_namespace=_PROVIDER_ID, key_name=_KEY_ID, wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, private_wrapping_key=private_key, public_wrapping_key=private_key.public_key(), ), ], )
def get_multi_keyring_with_no_children(): return MultiKeyring(generator=RawRSAKeyring( key_namespace=_PROVIDER_ID, key_name=_KEY_ID, wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, private_wrapping_key=rsa.generate_private_key( public_exponent=65537, key_size=2048, backend=default_backend()), ))
def test_on_decrypt_every_keyring_called_when_data_encryption_key_not_added( mock_generator, mock_child_1, mock_child_2): mock_generator.on_decrypt.side_effect = ( lambda decryption_materials, encrypted_data_keys: get_decryption_materials_without_data_key()) mock_child_1.on_decrypt.return_value = get_decryption_materials_without_data_key( ) mock_child_2.on_decrypt.return_value = get_decryption_materials_without_data_key( ) test_multi_keyring = MultiKeyring(generator=mock_generator, children=[mock_child_1, mock_child_2]) test_multi_keyring.on_decrypt( decryption_materials=get_decryption_materials_without_data_key(), encrypted_data_keys=[]) for keyring in test_multi_keyring._decryption_keyrings: assert keyring.on_decrypt.called
def test_keyring_with_generator_but_no_children(): generator_keyring = RawAESKeyring( key_namespace=_PROVIDER_ID, key_name=_KEY_ID, wrapping_key=_WRAPPING_KEY_AES, ) test_multi_keyring = MultiKeyring(generator=generator_keyring) assert test_multi_keyring.generator is generator_keyring assert not test_multi_keyring.children
def test_no_keyring_called_after_data_encryption_key_added_when_data_encryption_key_not_given( mock_generator, mock_child_1, mock_child_2, mock_child_3): mock_generator.on_decrypt.side_effect = ( lambda decryption_materials, encrypted_data_keys: get_decryption_materials_without_data_key()) test_multi_keyring = MultiKeyring( generator=mock_generator, children=[mock_child_3, mock_child_1, mock_child_2]) initial_materials = get_decryption_materials_without_data_key() new_materials = test_multi_keyring.on_decrypt( decryption_materials=initial_materials, encrypted_data_keys=[]) assert new_materials is not initial_materials assert mock_generator.on_decrypt.called assert mock_child_3.on_decrypt.called assert not mock_child_1.called assert not mock_child_2.called
def test_keyring_with_children_but_no_generator(): children_keyring = [ RawAESKeyring( key_namespace=_PROVIDER_ID, key_name=_KEY_ID, wrapping_key=_WRAPPING_KEY_AES, ) ] test_multi_keyring = MultiKeyring(children=children_keyring) assert test_multi_keyring.children is children_keyring assert test_multi_keyring.generator is None
def run(aws_kms_cmk, aws_kms_additional_cmks, source_plaintext): # type: (str, Sequence[str], bytes) -> None """Demonstrate how to create a keyring that behaves like an AWS KMS master key provider. :param str aws_kms_cmk: The ARN of an AWS KMS CMK that protects data keys :param List[str] aws_kms_additional_cmks: Additional ARNs of secondary AWS KMS CMKs :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", } # This is the master key provider whose behavior we want to reproduce. # # When encrypting, this master key provider generates the data key using the first CMK in the list # and encrypts the data key using all specified CMKs. # However, when decrypting, this master key provider attempts to decrypt # any data keys that were encrypted under an AWS KMS CMK. master_key_provider_cmks = [aws_kms_cmk] + aws_kms_additional_cmks _master_key_provider_to_replicate = KMSMasterKeyProvider( # noqa: intentionally never used key_ids=master_key_provider_cmks, ) # Create a CMK keyring that encrypts and decrypts using the specified AWS KMS CMKs. # # This keyring reproduces the encryption behavior of the AWS KMS master key provider. # # The AWS KMS keyring requires that you explicitly identify the CMK # that you want the keyring to use to generate the data key. cmk_keyring = AwsKmsKeyring(generator_key_id=aws_kms_cmk, key_ids=aws_kms_additional_cmks) # Create an AWS KMS discovery keyring that will attempt to decrypt # any data keys that were encrypted under an AWS KMS CMK. discovery_keyring = AwsKmsKeyring(is_discovery=True) # Combine the CMK and discovery keyrings # to create a keyring that behaves like an AWS KMS master key provider. # # The CMK keyring reproduces the encryption behavior # and the discovery keyring reproduces the decryption behavior. # This also means that it does not matter if the CMK keyring fails to decrypt. # For example, if you configured the CMK keyring with aliases, # it works on encrypt but fails to match any encrypted data keys on decrypt # because the serialized key name is the resulting CMK ARN rather than the alias name. # However, because the discovery keyring attempts to decrypt any AWS KMS-encrypted # data keys that it finds, the message still decrypts successfully. keyring = MultiKeyring(generator=cmk_keyring, children=[discovery_keyring]) # Encrypt your plaintext data. ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( source=source_plaintext, encryption_context=encryption_context, keyring=keyring) # Demonstrate that the ciphertext and plaintext are different. assert ciphertext != source_plaintext # Decrypt your encrypted data using the same keyring 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, keyring=keyring) # 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())
def run(aws_kms_cmk, source_plaintext): # type: (str, bytes) -> None """Demonstrate configuring a keyring to use an AWS KMS CMK and an RSA wrapping key. :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", } # Generate an RSA private key to use with your keyring. # In practice, you should get this key from a secure key management system such as an HSM. # # The National Institute of Standards and Technology (NIST) recommends a minimum of 2048-bit keys for RSA. # https://www.nist.gov/publications/transitioning-use-cryptographic-algorithms-and-key-lengths # # Why did we use this public exponent? # https://crypto.stanford.edu/~dabo/pubs/papers/RSA-survey.pdf private_key = rsa.generate_private_key(public_exponent=65537, key_size=4096, backend=default_backend()) # Collect the public key from the private key. public_key = private_key.public_key() # Create the encrypt keyring that only has access to the public key. escrow_encrypt_keyring = RawRSAKeyring( # The key namespace and key name are defined by you # and are used by the raw RSA keyring # to determine whether it should attempt to decrypt # an encrypted data key. # # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-raw-rsa-keyring key_namespace="some managed raw keys", key_name=b"my RSA wrapping key", public_wrapping_key=public_key, # The wrapping algorithm tells the raw RSA keyring # how to use your wrapping key to encrypt data keys. # # We recommend using RSA_OAEP_SHA256_MGF1. # You should not use RSA_PKCS1 unless you require it for backwards compatibility. wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, ) # Create the decrypt keyring that has access to the private key. escrow_decrypt_keyring = RawRSAKeyring( # The key namespace and key name MUST match the encrypt keyring. key_namespace="some managed raw keys", key_name=b"my RSA wrapping key", private_wrapping_key=private_key, # The wrapping algorithm MUST match the encrypt keyring. wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, ) # Create the AWS KMS keyring that you will use for decryption during normal operations. kms_keyring = AwsKmsKeyring(generator_key_id=aws_kms_cmk) # Combine the AWS KMS keyring and the escrow encrypt keyring using the multi-keyring. encrypt_keyring = MultiKeyring(generator=kms_keyring, children=[escrow_encrypt_keyring]) # Encrypt your plaintext data using the multi-keyring. ciphertext, encrypt_header = aws_encryption_sdk.encrypt( source=source_plaintext, encryption_context=encryption_context, keyring=encrypt_keyring) # Verify that the header contains the expected number of encrypted data keys (EDKs). # It should contain one EDK for AWS KMS and one for the escrow key. assert len(encrypt_header.encrypted_data_keys) == 2 # Demonstrate that the ciphertext and plaintext are different. assert ciphertext != source_plaintext # Decrypt your encrypted data separately using the AWS KMS keyring and the escrow decrypt keyring. # # You do not need to specify the encryption context on decrypt # because the header of the encrypted message includes the encryption context. decrypted_kms, decrypt_header_kms = aws_encryption_sdk.decrypt( source=ciphertext, keyring=kms_keyring) decrypted_escrow, decrypt_header_escrow = aws_encryption_sdk.decrypt( source=ciphertext, keyring=escrow_decrypt_keyring) # Demonstrate that the decrypted plaintext is identical to the original plaintext. assert decrypted_kms == source_plaintext assert decrypted_escrow == 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_kms.encryption_context.items()) assert set(encryption_context.items()) <= set( decrypt_header_escrow.encryption_context.items())
def run(aws_kms_cmk, source_plaintext): # type: (str, bytes) -> None """Demonstrate configuring an AWS KMS discovery-like keyring a particular AWS region and failover to others. :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. # Remember that your encryption context is NOT SECRET. # 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. encrypt_keyring = AwsKmsKeyring(generator_key_id=aws_kms_cmk) # To create our decrypt keyring, we need to know our current default AWS region. # # Create a throw-away boto3 session to discover the default region. local_region = Session().region_name # Now, use that region name to create two AWS KMS discovery keyrings: # # One that only works in the local region local_region_decrypt_keyring = AwsKmsKeyring( is_discovery=True, client_supplier=AllowRegionsClientSupplier(allowed_regions=[local_region]) ) # and one that will work in any other region but NOT the local region. other_regions_decrypt_keyring = AwsKmsKeyring( is_discovery=True, client_supplier=DenyRegionsClientSupplier(denied_regions=[local_region]) ) # Finally, combine those two keyrings into a multi-keyring. # # The multi-keyring steps through its member keyrings in the order that you provide them, # attempting to decrypt every encrypted data key with each keyring before moving on to the next keyring. # Because of this, other_regions_decrypt_keyring will not be called # unless local_region_decrypt_keyring fails to decrypt every encrypted data key. decrypt_keyring = MultiKeyring(children=[local_region_decrypt_keyring, other_regions_decrypt_keyring]) # Encrypt your plaintext data. ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( source=source_plaintext, encryption_context=encryption_context, keyring=encrypt_keyring ) # Demonstrate that the ciphertext and plaintext are different. assert ciphertext != source_plaintext # Decrypt your encrypted data using the multi-keyring. # # 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, keyring=decrypt_keyring) # 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())
class AwsKmsKeyring(Keyring): """Keyring that uses AWS Key Management Service (KMS) Customer Master Keys (CMKs) to manage wrapping keys. Set ``generator_key_id`` to require that the keyring use that CMK to generate the data key. If you do not set ``generator_key_id``, the keyring will not generate a data key. Set ``key_ids`` to specify additional CMKs that the keyring will use to encrypt the data key. The keyring will attempt to use any CMKs identified by CMK ARN in either ``generator_key_id`` or ``key_ids`` on decrypt. You can identify CMKs by any `valid key ID`_ for the keyring to use on encrypt, but for the keyring to attempt to use them on decrypt you MUST specify the CMK ARN. If you specify ``is_discovery=True`` the keyring will be a KMS discovery keyring, doing nothing on encrypt and attempting to decrypt any AWS KMS-encrypted data key on decrypt. .. note:: You must either set ``is_discovery=True`` or provide key IDs. You can use the :class:`ClientSupplier` to customize behavior further, such as to provide different credentials for different regions or to restrict which regions are allowed. See the `AWS KMS Keyring specification`_ for more details. .. _AWS KMS Keyring specification: https://github.com/awslabs/aws-encryption-sdk-specification/blob/master/framework/kms-keyring.md .. _valid key ID: https://docs.aws.amazon.com/kms/latest/APIReference/API_GenerateDataKey.html#API_GenerateDataKey_RequestSyntax .. _discovery mode: https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#kms-keyring-discovery .. versionadded:: 1.5.0 :param ClientSupplier client_supplier: Client supplier that provides AWS KMS clients (optional) :param bool is_discovery: Should this be a discovery keyring (optional) :param str generator_key_id: Key ID of AWS KMS CMK to use when generating data keys (optional) :param List[str] key_ids: Key IDs that will be used to encrypt and decrypt data keys (optional) :param List[str] grant_tokens: AWS KMS grant tokens to include in requests (optional) """ _client_supplier = attr.ib(default=attr.Factory(DefaultClientSupplier), validator=is_callable()) _is_discovery = attr.ib(default=False, validator=instance_of(bool)) _generator_key_id = attr.ib(default=None, validator=optional(instance_of(six.string_types))) _key_ids = attr.ib( default=attr.Factory(tuple), validator=(deep_iterable(member_validator=instance_of(six.string_types)), value_is_not_a_string), ) _grant_tokens = attr.ib( default=attr.Factory(tuple), validator=(deep_iterable(member_validator=instance_of(six.string_types)), value_is_not_a_string), ) def __attrs_post_init__(self): """Configure internal keyring.""" key_ids_provided = self._generator_key_id is not None or self._key_ids both = key_ids_provided and self._is_discovery neither = not key_ids_provided and not self._is_discovery if both: raise TypeError("is_discovery cannot be True if key IDs are provided") if neither: raise TypeError("is_discovery cannot be False if no key IDs are provided") if self._is_discovery: self._inner_keyring = _AwsKmsDiscoveryKeyring( client_supplier=self._client_supplier, grant_tokens=self._grant_tokens ) return if self._generator_key_id is None: generator_keyring = None else: generator_keyring = _AwsKmsSingleCmkKeyring( key_id=self._generator_key_id, client_supplier=self._client_supplier, grant_tokens=self._grant_tokens ) child_keyrings = [ _AwsKmsSingleCmkKeyring( key_id=key_id, client_supplier=self._client_supplier, grant_tokens=self._grant_tokens ) for key_id in self._key_ids ] self._inner_keyring = MultiKeyring(generator=generator_keyring, children=child_keyrings) def on_encrypt(self, encryption_materials): # type: (EncryptionMaterials) -> EncryptionMaterials """Generate a data key using generator keyring and encrypt it using any available wrapping key in any child keyring. :param EncryptionMaterials encryption_materials: Encryption materials for keyring to modify. :returns: Optionally modified encryption materials. :rtype: EncryptionMaterials :raises EncryptKeyError: if unable to encrypt data key. """ return self._inner_keyring.on_encrypt(encryption_materials=encryption_materials) def on_decrypt(self, decryption_materials, encrypted_data_keys): # type: (DecryptionMaterials, Iterable[EncryptedDataKey]) -> DecryptionMaterials """Attempt to decrypt the encrypted data keys. :param DecryptionMaterials decryption_materials: Decryption materials for keyring to modify. :param List[EncryptedDataKey] encrypted_data_keys: List of encrypted data keys. :returns: Optionally modified decryption materials. :rtype: DecryptionMaterials """ return self._inner_keyring.on_decrypt( decryption_materials=decryption_materials, encrypted_data_keys=encrypted_data_keys )
backend=default_backend()) _rsa_private_key_b = rsa.generate_private_key(public_exponent=65537, key_size=2048, backend=default_backend()) _MULTI_KEYRING_WITH_GENERATOR_AND_CHILDREN = MultiKeyring( generator=RawAESKeyring( key_namespace=_PROVIDER_ID, key_name=_KEY_ID, wrapping_key=_WRAPPING_KEY_AES, ), children=[ RawRSAKeyring( key_namespace=_PROVIDER_ID, key_name=_KEY_ID, wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, private_wrapping_key=_rsa_private_key_a, public_wrapping_key=_rsa_private_key_a.public_key(), ), RawRSAKeyring( key_namespace=_PROVIDER_ID, key_name=_KEY_ID, wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, private_wrapping_key=_rsa_private_key_b, public_wrapping_key=_rsa_private_key_b.public_key(), ), ], ) _MULTI_KEYRING_WITHOUT_CHILDREN = MultiKeyring(generator=RawRSAKeyring( key_namespace=_PROVIDER_ID, key_name=_KEY_ID,
) _MULTI_KEYRING_WITH_GENERATOR_AND_CHILDREN = MultiKeyring( generator=RawAESKeyring( key_namespace=_PROVIDER_ID, key_name=_KEY_ID, wrapping_key=_WRAPPING_KEY_AES, ), children=[ RawRSAKeyring( key_namespace=_PROVIDER_ID, key_name=_KEY_ID, wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, private_wrapping_key=rsa.generate_private_key( public_exponent=65537, key_size=2048, backend=default_backend()), ), RawRSAKeyring( key_namespace=_PROVIDER_ID, key_name=_KEY_ID, wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, private_wrapping_key=rsa.generate_private_key( public_exponent=65537, key_size=2048, backend=default_backend()), ), ], ) _MULTI_KEYRING_WITHOUT_CHILDREN = MultiKeyring(generator=RawRSAKeyring(
def test_keyring_with_no_generator_no_children(): with pytest.raises(TypeError) as exc_info: MultiKeyring() assert exc_info.match( "At least one of generator or children must be provided")
def test_keyring_with_invalid_parameters(generator, children): with pytest.raises(TypeError) as exc_info: MultiKeyring(generator=generator, children=children) assert exc_info.match( "('children'|'generator') must be <class 'aws_encryption_sdk.keyrings.base.Keyring'>.*" )