From 03acddfb68d16dfce17fd83a574ab51158f63737 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Fri, 14 Feb 2020 15:17:23 -0800 Subject: [PATCH 1/6] feat: add optional key_id to MasterKeyInfo for when it differs from provider_info --- .../internal/formatting/serialize.py | 2 +- src/aws_encryption_sdk/key_providers/raw.py | 8 +++++-- .../materials_managers/__init__.py | 10 +++++++-- src/aws_encryption_sdk/structures.py | 22 ++++++++++++++++++- 4 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/aws_encryption_sdk/internal/formatting/serialize.py b/src/aws_encryption_sdk/internal/formatting/serialize.py index e7c86a0cb..775da85d3 100644 --- a/src/aws_encryption_sdk/internal/formatting/serialize.py +++ b/src/aws_encryption_sdk/internal/formatting/serialize.py @@ -316,6 +316,6 @@ def serialize_wrapped_key(key_provider, wrapping_algorithm, wrapping_key_id, enc ) key_ciphertext = encrypted_wrapped_key.ciphertext + encrypted_wrapped_key.tag return EncryptedDataKey( - key_provider=MasterKeyInfo(provider_id=key_provider.provider_id, key_info=key_info), + key_provider=MasterKeyInfo(provider_id=key_provider.provider_id, key_info=key_info, key_id=wrapping_key_id), encrypted_data_key=key_ciphertext, ) diff --git a/src/aws_encryption_sdk/key_providers/raw.py b/src/aws_encryption_sdk/key_providers/raw.py index 57a1d5edf..4696d345e 100644 --- a/src/aws_encryption_sdk/key_providers/raw.py +++ b/src/aws_encryption_sdk/key_providers/raw.py @@ -23,7 +23,7 @@ from aws_encryption_sdk.identifiers import EncryptionType from aws_encryption_sdk.internal.crypto.wrapping_keys import WrappingKey from aws_encryption_sdk.key_providers.base import MasterKey, MasterKeyConfig, MasterKeyProvider, MasterKeyProviderConfig -from aws_encryption_sdk.structures import DataKey, RawDataKey +from aws_encryption_sdk.structures import DataKey, MasterKeyInfo, RawDataKey _LOGGER = logging.getLogger(__name__) @@ -182,7 +182,11 @@ def _decrypt_data_key(self, encrypted_data_key, algorithm, encryption_context): ) # Raw key string to DataKey return DataKey( - key_provider=encrypted_data_key.key_provider, + key_provider=MasterKeyInfo( + provider_id=encrypted_data_key.key_provider.provider_id, + key_info=encrypted_data_key.key_provider.key_info, + key_id=self.key_id, + ), data_key=plaintext_data_key, encrypted_data_key=encrypted_data_key.encrypted_data_key, ) diff --git a/src/aws_encryption_sdk/materials_managers/__init__.py b/src/aws_encryption_sdk/materials_managers/__init__.py index 0a6dcd2f0..d9459c7e2 100644 --- a/src/aws_encryption_sdk/materials_managers/__init__.py +++ b/src/aws_encryption_sdk/materials_managers/__init__.py @@ -133,7 +133,10 @@ def _validate_data_encryption_key(self, data_encryption_key, keyring_trace, requ if flag not in keyring_trace.flags: raise InvalidKeyringTraceError("Keyring flags do not match action.") - if keyring_trace.wrapping_key != data_encryption_key.key_provider: + if not ( + keyring_trace.wrapping_key.provider_id == data_encryption_key.key_provider.provider_id + and keyring_trace.wrapping_key.key_id == data_encryption_key.key_provider.key_id + ): raise InvalidKeyringTraceError("Keyring trace does not match data key provider.") if len(data_encryption_key.data_key) != self.algorithm.kdf_input_len: @@ -302,7 +305,10 @@ def add_encrypted_data_key(self, encrypted_data_key, keyring_trace): if KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY not in keyring_trace.flags: raise InvalidKeyringTraceError("Keyring flags do not match action.") - if keyring_trace.wrapping_key != encrypted_data_key.key_provider: + if not ( + keyring_trace.wrapping_key.provider_id == encrypted_data_key.key_provider.provider_id + and keyring_trace.wrapping_key.key_id == encrypted_data_key.key_provider.key_id + ): raise InvalidKeyringTraceError("Keyring trace does not match data key encryptor.") self._encrypted_data_keys.append(encrypted_data_key) diff --git a/src/aws_encryption_sdk/structures.py b/src/aws_encryption_sdk/structures.py index ea9253b39..8f8192e18 100644 --- a/src/aws_encryption_sdk/structures.py +++ b/src/aws_encryption_sdk/structures.py @@ -15,7 +15,7 @@ import attr import six -from attr.validators import deep_iterable, deep_mapping, instance_of +from attr.validators import deep_iterable, deep_mapping, instance_of, optional from aws_encryption_sdk.identifiers import Algorithm, ContentType, KeyringTraceFlag, ObjectType, SerializationVersion from aws_encryption_sdk.internal.str_ops import to_bytes, to_str @@ -25,12 +25,32 @@ class MasterKeyInfo(object): """Contains information necessary to identify a Master Key. + ``key_id`` is optional because ``key_id`` and ``key_info`` SHOULD + always be the same except in the case of the Raw AES Keyring/MasterKey. + This gives the option to specify a different ``key_id`` value if needed. + :param str provider_id: MasterKey provider_id value :param bytes key_info: MasterKey key_info value + :param bytes key_id: MasterKey key_id value (optional) """ provider_id = attr.ib(hash=True, validator=instance_of((six.string_types, bytes)), converter=to_str) key_info = attr.ib(hash=True, validator=instance_of((six.string_types, bytes)), converter=to_bytes) + _key_id = attr.ib( + hash=True, + eq=False, + default=None, + validator=optional(instance_of((six.string_types, bytes))), + converter=to_bytes, + ) + + @property + def key_id(self): + """Return the key ID if separately specified, or the key info if not.""" + if self._key_id is None: + return self.key_info + + return self._key_id @attr.s(hash=True) From c3ba9b00874dcb1d6da6cfe1cc36f3453b19c633 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Fri, 14 Feb 2020 15:17:54 -0800 Subject: [PATCH 2/6] fix: fix integ test to run when default region is set --- test/integration/test_client.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/integration/test_client.py b/test/integration/test_client.py index 26df431dc..183f9d49f 100644 --- a/test/integration/test_client.py +++ b/test/integration/test_client.py @@ -64,12 +64,13 @@ def test_encrypt_verify_user_agent_kms_master_key(caplog): def test_remove_bad_client(): test = KMSMasterKeyProvider() - test.add_regional_client("us-fakey-12") + fake_region = "us-fakey-12" + test.add_regional_client(fake_region) with pytest.raises(BotoCoreError): - test._regional_clients["us-fakey-12"].list_keys() + test._regional_clients[fake_region].list_keys() - assert not test._regional_clients + assert fake_region not in test._regional_clients def test_regional_client_does_not_modify_botocore_session(caplog): From 162aa0a4c59ce3a7695ffe8f8006ea3f3ec1cb50 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Fri, 14 Feb 2020 15:18:23 -0800 Subject: [PATCH 3/6] chore: add verionadded flag --- src/aws_encryption_sdk/keyrings/multi.py | 2 ++ src/aws_encryption_sdk/keyrings/raw.py | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/src/aws_encryption_sdk/keyrings/multi.py b/src/aws_encryption_sdk/keyrings/multi.py index 4274c1c4d..048dbe846 100644 --- a/src/aws_encryption_sdk/keyrings/multi.py +++ b/src/aws_encryption_sdk/keyrings/multi.py @@ -36,6 +36,8 @@ class MultiKeyring(Keyring): """Public class for Multi Keyring. + .. versionadded:: 1.5.0 + :param generator: Generator keyring used to generate data encryption key (optional) :type generator: Keyring :param list children: List of keyrings used to encrypt the data encryption key (optional) diff --git a/src/aws_encryption_sdk/keyrings/raw.py b/src/aws_encryption_sdk/keyrings/raw.py index dea924eae..7095e8296 100644 --- a/src/aws_encryption_sdk/keyrings/raw.py +++ b/src/aws_encryption_sdk/keyrings/raw.py @@ -91,6 +91,8 @@ class RawAESKeyring(Keyring): """Generate an instance of Raw AES Keyring which encrypts using AES-GCM algorithm using wrapping key provided as a byte array + .. versionadded:: 1.5.0 + :param str key_namespace: String defining the keyring. :param bytes key_name: Key ID :param bytes wrapping_key: Encryption key with which to wrap plaintext data key. @@ -240,6 +242,8 @@ class RawRSAKeyring(Keyring): """Generate an instance of Raw RSA Keyring which performs asymmetric encryption and decryption using public and private keys provided + .. versionadded:: 1.5.0 + :param str key_namespace: String defining the keyring ID :param bytes key_name: Key ID :param private_wrapping_key: Private encryption key with which to wrap plaintext data key (optional) From 74c5ba1a22ba50358f291b2ace964382ccb2852e Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Fri, 14 Feb 2020 15:18:57 -0800 Subject: [PATCH 4/6] feat: add master key provider keyring and tests --- src/aws_encryption_sdk/exceptions.py | 7 + src/aws_encryption_sdk/keyrings/master_key.py | 222 ++++++++++++++ test/unit/keyrings/test_master_key.py | 286 ++++++++++++++++++ test/unit/unit_test_utils.py | 96 +++++- 4 files changed, 610 insertions(+), 1 deletion(-) create mode 100644 src/aws_encryption_sdk/keyrings/master_key.py create mode 100644 test/unit/keyrings/test_master_key.py diff --git a/src/aws_encryption_sdk/exceptions.py b/src/aws_encryption_sdk/exceptions.py index cd60ab6bd..fb699e25b 100644 --- a/src/aws_encryption_sdk/exceptions.py +++ b/src/aws_encryption_sdk/exceptions.py @@ -87,6 +87,13 @@ class SignatureKeyError(AWSEncryptionSDKClientError): """ +class InvalidCryptographicMaterialsError(AWSEncryptionSDKClientError): + """Exception class for errors encountered when attempting to validate cryptographic materials. + + .. versionadded:: 1.5.0 + """ + + class ActionNotAllowedError(AWSEncryptionSDKClientError): """Exception class for errors encountered when attempting to perform unallowed actions.""" diff --git a/src/aws_encryption_sdk/keyrings/master_key.py b/src/aws_encryption_sdk/keyrings/master_key.py new file mode 100644 index 000000000..5f157a1b3 --- /dev/null +++ b/src/aws_encryption_sdk/keyrings/master_key.py @@ -0,0 +1,222 @@ +# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Keyring for use with :class:`MasterKey`s and :class:`MasterKeyProvider`s.""" +import attr +from attr.validators import instance_of + +from aws_encryption_sdk.exceptions import ( + DecryptKeyError, + IncorrectMasterKeyError, + InvalidCryptographicMaterialsError, + MasterKeyProviderError, + UnknownIdentityError, +) +from aws_encryption_sdk.identifiers import EncryptionKeyType +from aws_encryption_sdk.key_providers.base import MasterKey, MasterKeyProvider +from aws_encryption_sdk.key_providers.kms import KMSMasterKey +from aws_encryption_sdk.key_providers.raw import RawMasterKey +from aws_encryption_sdk.keyrings.base import Keyring +from aws_encryption_sdk.materials_managers import DecryptionMaterials, EncryptionMaterials +from aws_encryption_sdk.structures import EncryptedDataKey, KeyringTrace, KeyringTraceFlag, RawDataKey + +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Iterable, Set # noqa pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + +__all__ = ("MasterKeyProviderKeyring",) + + +def _signs_encryption_context(master_key): + # type: (MasterKey) -> bool + if isinstance(master_key, KMSMasterKey): + return True + + if isinstance(master_key, RawMasterKey): + if master_key.config.wrapping_key.wrapping_key_type is EncryptionKeyType.SYMMETRIC: + return True + + return False + + +def _generate_flags(): + # type: () -> Set[KeyringTraceFlag] + """Build the keyring trace flags for a generate data key operation. + + :return: Set of keyring trace flags + :rtype: set of :class:`KeyringTraceFlag` + """ + return {KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY} + + +def _encrypt_flags(master_key): + # type: (MasterKey) -> Set[KeyringTraceFlag] + """Build the keyring trace flags for an encrypt data key operation. + + :param MasterKey master_key: Master key that encrypted the key + :return: Set of keyring trace flags + :rtype: set of :class:`KeyringTraceFlag` + """ + flags = {KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY} + if _signs_encryption_context(master_key): + flags.add(KeyringTraceFlag.WRAPPING_KEY_SIGNED_ENC_CTX) + return flags + + +def _decrypt_flags(master_key): + # type: (MasterKey) -> Set[KeyringTraceFlag] + """Build the keyring trace flags for a decrypt data key operation. + + :param MasterKey master_key: Master key that decrypted the key + :return: Set of keyring trace flags + :rtype: set of :class:`KeyringTraceFlag` + """ + flags = {KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY} + if _signs_encryption_context(master_key): + flags.add(KeyringTraceFlag.WRAPPING_KEY_VERIFIED_ENC_CTX) + return flags + + +@attr.s +class MasterKeyProviderKeyring(Keyring): + """Keyring compatibility layer for use with master key providers. + + .. versionadded:: 1.5.0 + + :param MasterKeyProvider master_key_provider: Master key provider to use + """ + + _master_key_provider = attr.ib(validator=instance_of(MasterKeyProvider)) + + def on_encrypt(self, encryption_materials): + # type: (EncryptionMaterials) -> EncryptionMaterials + """Generate a data key if not present and encrypt it using any available wrapping key. + + :param encryption_materials: Encryption materials for the keyring to modify. + :type encryption_materials: aws_encryption_sdk.materials_managers.EncryptionMaterials + :returns: Optionally modified encryption materials. + :rtype: aws_encryption_sdk.materials_managers.EncryptionMaterials + :raises NotImplementedError: if method is not implemented + """ + primary_master_key, master_keys = self._master_key_provider.master_keys_for_encryption( + encryption_context=encryption_materials.encryption_context, plaintext_rostream=None, plaintext_length=None, + ) + if not master_keys: + raise MasterKeyProviderError("No Master Keys available from Master Key Provider") + if primary_master_key not in master_keys: + raise MasterKeyProviderError("Primary Master Key not in provided Master Keys") + + if encryption_materials.data_encryption_key is not None: + # Because the default CMM used to require that the primary MKP was the generator, + # this keyring cannot accept encryption materials that already have a data key. + raise InvalidCryptographicMaterialsError( + "Unable to use master keys with encryption materials that already contain a data key." + " You are probably trying to mix master key providers and keyrings." + " If you want to do that, the master key provider MUST be the generator." + ) + + data_encryption_key = primary_master_key.generate_data_key( + algorithm=encryption_materials.algorithm, encryption_context=encryption_materials.encryption_context, + ) + + encryption_materials.add_data_encryption_key( + data_encryption_key=RawDataKey( + data_key=data_encryption_key.data_key, key_provider=data_encryption_key.key_provider, + ), + keyring_trace=KeyringTrace(wrapping_key=primary_master_key.key_provider, flags=_generate_flags(),), + ) + encryption_materials.add_encrypted_data_key( + encrypted_data_key=EncryptedDataKey( + key_provider=data_encryption_key.key_provider, + encrypted_data_key=data_encryption_key.encrypted_data_key, + ), + keyring_trace=KeyringTrace( + wrapping_key=primary_master_key.key_provider, flags=_encrypt_flags(primary_master_key), + ), + ) + + # Go through all of the other master keys and encrypt + for child in master_keys: + if child is primary_master_key: + # The additional master keys returned by MasterKeyProvider.master_keys_for_encryption + # can include the primary master key. + # We already have the encrypted data key from the primary, so skip it. + continue + + encrypted_data_key = child.encrypt_data_key( + data_key=data_encryption_key, + algorithm=encryption_materials.algorithm, + encryption_context=encryption_materials.encryption_context, + ) + encryption_materials.add_encrypted_data_key( + encrypted_data_key=EncryptedDataKey( + key_provider=encrypted_data_key.key_provider, + encrypted_data_key=encrypted_data_key.encrypted_data_key, + ), + keyring_trace=KeyringTrace(wrapping_key=child.key_provider, flags=_encrypt_flags(child)), + ) + + return encryption_materials + + def on_decrypt(self, decryption_materials, encrypted_data_keys): + # type: (DecryptionMaterials, Iterable[EncryptedDataKey]) -> DecryptionMaterials + """Attempt to decrypt the encrypted data keys. + + :param decryption_materials: Decryption materials for the keyring to modify. + :type decryption_materials: aws_encryption_sdk.materials_managers.DecryptionMaterials + :param encrypted_data_keys: List of encrypted data keys. + :type: Iterable of :class:`aws_encryption_sdk.structures.EncryptedDataKey` + :returns: Optionally modified decryption materials. + :rtype: aws_encryption_sdk.materials_managers.DecryptionMaterials + :raises NotImplementedError: if method is not implemented + """ + # If the plaintext data key is set, just return. + if decryption_materials.data_encryption_key is not None: + return decryption_materials + + # Use the master key provider to decrypt. + try: + decrypted_data_key = self._master_key_provider.decrypt_data_key_from_list( + encrypted_data_keys=encrypted_data_keys, + algorithm=decryption_materials.algorithm, + encryption_context=decryption_materials.encryption_context, + ) + # MasterKeyProvider.decrypt_data_key throws DecryptKeyError + # but MasterKey.decrypt_data_key throws IncorrectMasterKeyError + except (IncorrectMasterKeyError, DecryptKeyError): + # Don't fail here for master key providers. + # The default CMM will fail if no keyrings can decrypt. + return decryption_materials + + # Find the master key object that was used for decryption + # This is important because we need the key ID for the trace, + # not the provider info, which is what is in the returned data key. + try: + decrypting_master_key = list(self._master_key_provider.master_keys_for_data_key(decrypted_data_key))[0] + except IndexError: + raise UnknownIdentityError( + "Unable to locate master key for {}".format(repr(decrypted_data_key.key_provider)) + ) + + # Add the plaintext data to the decryption materials, along with a trace. + decryption_materials.add_data_encryption_key( + data_encryption_key=RawDataKey( + key_provider=decrypted_data_key.key_provider, data_key=decrypted_data_key.data_key, + ), + keyring_trace=KeyringTrace( + wrapping_key=decrypting_master_key.key_provider, flags=_decrypt_flags(decrypting_master_key), + ), + ) + + return decryption_materials diff --git a/test/unit/keyrings/test_master_key.py b/test/unit/keyrings/test_master_key.py new file mode 100644 index 000000000..79c50855b --- /dev/null +++ b/test/unit/keyrings/test_master_key.py @@ -0,0 +1,286 @@ +# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Test suite for ``aws_encryption_sdk.keyrings.master_key``.""" +import itertools + +import pytest + +from aws_encryption_sdk.exceptions import ( + InvalidCryptographicMaterialsError, + MasterKeyProviderError, + UnknownIdentityError, +) +from aws_encryption_sdk.identifiers import KeyringTraceFlag +from aws_encryption_sdk.internal.defaults import ALGORITHM +from aws_encryption_sdk.keyrings.master_key import MasterKeyProviderKeyring +from aws_encryption_sdk.materials_managers import DecryptionMaterials, EncryptionMaterials +from aws_encryption_sdk.structures import MasterKeyInfo, RawDataKey + +from ..unit_test_utils import ( + DisjointMasterKeyProvider, + EmptyMasterKeyProvider, + EphemeralRawMasterKeyProvider, + FailingDecryptMasterKeyProvider, + UnknownDataKeyInfoMasterKeyProvider, + ephemeral_raw_aes_master_key, + ephemeral_raw_rsa_master_key, +) + +pytestmark = [pytest.mark.unit, pytest.mark.local] + + +def _encryption_contexts(): + yield pytest.param({}, id="no encryption context") + yield pytest.param({"foo": "bar"}, id="some encryption context") + + +@pytest.mark.parametrize("encryption_context", _encryption_contexts()) +def test_cycle(encryption_context): + mkp = ephemeral_raw_rsa_master_key() + keyring = MasterKeyProviderKeyring(master_key_provider=mkp) + + encryption_materials = EncryptionMaterials(algorithm=ALGORITHM, encryption_context=encryption_context) + + final_encryption_materials = keyring.on_encrypt(encryption_materials=encryption_materials) + + decryption_materials = DecryptionMaterials(algorithm=ALGORITHM, encryption_context=encryption_context) + + final_decryption_materials = keyring.on_decrypt( + decryption_materials=decryption_materials, encrypted_data_keys=final_encryption_materials.encrypted_data_keys + ) + + assert ( + final_encryption_materials.data_encryption_key.data_key + == final_decryption_materials.data_encryption_key.data_key + ) + + +def _master_key_flags_on_encrypt(): + single_raw_rsa_mkp = ephemeral_raw_rsa_master_key() + yield pytest.param( + single_raw_rsa_mkp, + single_raw_rsa_mkp.key_provider, + [KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY, KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY], + id="single master key : raw RSA", + ) + + single_raw_aes_mkp = ephemeral_raw_aes_master_key() + yield pytest.param( + single_raw_aes_mkp, + single_raw_aes_mkp.key_provider, + [ + KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY, + KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY, + KeyringTraceFlag.WRAPPING_KEY_SIGNED_ENC_CTX, + ], + id="single master key : raw AES", + ) + + raw_provider = EphemeralRawMasterKeyProvider() + raw_provider.add_master_key(b"aes-256") + raw_provider.add_master_key(b"rsa-4096") + yield pytest.param( + raw_provider, + raw_provider.master_key(b"aes-256").key_provider, + [ + KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY, + KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY, + KeyringTraceFlag.WRAPPING_KEY_SIGNED_ENC_CTX, + ], + id="multiple master keys : raw AES generate and encrypt", + ) + yield pytest.param( + raw_provider, + raw_provider.master_key(b"rsa-4096").key_provider, + [KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY], + id="multiple master keys : raw RSA encrypt only", + ) + + +@pytest.mark.parametrize("master_key_provider, master_key_for_flags, expected_flags", _master_key_flags_on_encrypt()) +@pytest.mark.parametrize("encryption_context", _encryption_contexts()) +def test_keyring_flags_on_encrypt(master_key_provider, master_key_for_flags, expected_flags, encryption_context): + keyring = MasterKeyProviderKeyring(master_key_provider=master_key_provider) + + encryption_materials = EncryptionMaterials(algorithm=ALGORITHM, encryption_context=encryption_context) + + final_encryption_materials = keyring.on_encrypt(encryption_materials=encryption_materials) + + actual_flags = list( + itertools.chain.from_iterable( + ( + trace.flags + for trace in final_encryption_materials.keyring_trace + if trace.wrapping_key == master_key_for_flags + ) + ) + ) + assert len(actual_flags) == len(expected_flags) + assert set(actual_flags) == set(expected_flags) + + +def _master_key_flags_on_decrypt(): + single_raw_rsa_mkp = ephemeral_raw_rsa_master_key() + yield pytest.param( + single_raw_rsa_mkp, + single_raw_rsa_mkp, + [KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY], + id="single master key : raw RSA", + ) + + single_raw_aes_mkp = ephemeral_raw_aes_master_key() + yield pytest.param( + single_raw_aes_mkp, + single_raw_aes_mkp, + [KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY, KeyringTraceFlag.WRAPPING_KEY_VERIFIED_ENC_CTX,], + id="single master key : raw AES", + ) + + raw_provider = EphemeralRawMasterKeyProvider() + raw_provider.add_master_key(b"aes-256") + raw_provider.add_master_key(b"rsa-4096") + yield pytest.param( + raw_provider, + raw_provider.master_key(b"aes-256"), + [KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY, KeyringTraceFlag.WRAPPING_KEY_VERIFIED_ENC_CTX,], + id="multiple master key encrypt : raw AES decrypt", + ) + yield pytest.param( + raw_provider, + raw_provider.master_key(b"rsa-4096"), + [KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY], + id="multiple master key encrypt : raw RSA decrypt", + ) + + +@pytest.mark.parametrize( + "master_key_provider_for_encrypt, master_key_for_decrypt, expected_flags", _master_key_flags_on_decrypt() +) +@pytest.mark.parametrize("encryption_context", _encryption_contexts()) +def test_keyring_flags_on_decrypt( + master_key_provider_for_encrypt, master_key_for_decrypt, expected_flags, encryption_context +): + keyring = MasterKeyProviderKeyring(master_key_provider=master_key_provider_for_encrypt) + + encryption_materials = EncryptionMaterials(algorithm=ALGORITHM, encryption_context=encryption_context) + + final_encryption_materials = keyring.on_encrypt(encryption_materials=encryption_materials) + + decrypt_keyring = MasterKeyProviderKeyring(master_key_provider=master_key_for_decrypt) + + decryption_materials = DecryptionMaterials(algorithm=ALGORITHM, encryption_context=encryption_context) + + final_decryption_materials = decrypt_keyring.on_decrypt( + decryption_materials=decryption_materials, encrypted_data_keys=final_encryption_materials.encrypted_data_keys + ) + + actual_flags = list( + itertools.chain.from_iterable( + ( + trace.flags + for trace in final_decryption_materials.keyring_trace + if trace.wrapping_key == master_key_for_decrypt.key_provider + ) + ) + ) + assert len(actual_flags) == len(expected_flags) + assert set(actual_flags) == set(expected_flags) + + +def test_on_encrypt_no_master_keys(): + keyring = MasterKeyProviderKeyring(master_key_provider=EmptyMasterKeyProvider()) + + encryption_materials = EncryptionMaterials(algorithm=ALGORITHM, encryption_context={}) + + with pytest.raises(MasterKeyProviderError) as excinfo: + keyring.on_encrypt(encryption_materials=encryption_materials) + + excinfo.match("No Master Keys available from Master Key Provider") + + +def test_on_encrypt_primary_master_key_not_in_master_keys(): + keyring = MasterKeyProviderKeyring(master_key_provider=DisjointMasterKeyProvider()) + + encryption_materials = EncryptionMaterials(algorithm=ALGORITHM, encryption_context={}) + + with pytest.raises(MasterKeyProviderError) as excinfo: + keyring.on_encrypt(encryption_materials=encryption_materials) + + excinfo.match("Primary Master Key not in provided Master Keys") + + +@pytest.mark.parametrize("encryption_context", _encryption_contexts()) +def test_on_encrypt_with_existing_data_key(encryption_context): + keyring = MasterKeyProviderKeyring(master_key_provider=ephemeral_raw_aes_master_key()) + + encryption_materials = EncryptionMaterials( + algorithm=ALGORITHM, + encryption_context=encryption_context, + data_encryption_key=RawDataKey(key_provider=MasterKeyInfo(provider_id="foo", key_info=b"bar"), data_key=b""), + ) + + with pytest.raises(InvalidCryptographicMaterialsError): + keyring.on_encrypt(encryption_materials=encryption_materials) + + +def test_on_decrypt_with_existing_data_key(): + keyring = MasterKeyProviderKeyring(master_key_provider=ephemeral_raw_aes_master_key()) + + decryption_materials = DecryptionMaterials( + algorithm=ALGORITHM, + encryption_context={}, + data_encryption_key=RawDataKey(key_provider=MasterKeyInfo(provider_id="foo", key_info=b"bar"), data_key=b""), + ) + + final_decryption_materials = keyring.on_decrypt(decryption_materials=decryption_materials, encrypted_data_keys=[]) + + assert not final_decryption_materials.keyring_trace + + +def test_on_decrypt_master_key_throws_error(): + mkp = FailingDecryptMasterKeyProvider() + mkp.add_master_key(b"aes-256") + mkp.add_master_key(b"rsa-4096") + keyring = MasterKeyProviderKeyring(master_key_provider=mkp) + + encryption_materials = EncryptionMaterials(algorithm=ALGORITHM, encryption_context={}) + + final_encryption_materials = keyring.on_encrypt(encryption_materials=encryption_materials) + + decryption_materials = DecryptionMaterials(algorithm=ALGORITHM, encryption_context={}) + + final_decryption_materials = keyring.on_decrypt( + decryption_materials=decryption_materials, encrypted_data_keys=final_encryption_materials.encrypted_data_keys, + ) + assert final_decryption_materials.data_encryption_key is None + + +def test_on_decrypt_master_key_not_in_keyring_trace(): + mkp = UnknownDataKeyInfoMasterKeyProvider() + mkp.add_master_key(b"aes-256") + mkp.add_master_key(b"rsa-4096") + keyring = MasterKeyProviderKeyring(master_key_provider=mkp) + + encryption_materials = EncryptionMaterials(algorithm=ALGORITHM, encryption_context={}) + + final_encryption_materials = keyring.on_encrypt(encryption_materials=encryption_materials) + + decryption_materials = DecryptionMaterials(algorithm=ALGORITHM, encryption_context={}) + + with pytest.raises(UnknownIdentityError) as excinfo: + keyring.on_decrypt( + decryption_materials=decryption_materials, + encrypted_data_keys=final_encryption_materials.encrypted_data_keys, + ) + + excinfo.match(r"Unable to locate master key for *") diff --git a/test/unit/unit_test_utils.py b/test/unit/unit_test_utils.py index 2bf1bc838..ca344f436 100644 --- a/test/unit/unit_test_utils.py +++ b/test/unit/unit_test_utils.py @@ -17,10 +17,15 @@ import os from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa -from aws_encryption_sdk.identifiers import Algorithm, KeyringTraceFlag, WrappingAlgorithm +from aws_encryption_sdk.exceptions import DecryptKeyError +from aws_encryption_sdk.identifiers import Algorithm, EncryptionKeyType, KeyringTraceFlag, WrappingAlgorithm +from aws_encryption_sdk.internal.crypto.wrapping_keys import WrappingKey from aws_encryption_sdk.internal.utils.streams import InsistentReaderBytesIO +from aws_encryption_sdk.key_providers.base import MasterKeyProvider, MasterKeyProviderConfig +from aws_encryption_sdk.key_providers.raw import RawMasterKey, RawMasterKeyProvider from aws_encryption_sdk.keyrings.base import Keyring from aws_encryption_sdk.keyrings.multi import MultiKeyring from aws_encryption_sdk.keyrings.raw import RawAESKeyring, RawRSAKeyring @@ -401,3 +406,92 @@ def assert_prepped_stream_identity(prepped_stream, wrapped_type): assert isinstance(prepped_stream, wrapped_type) # Check the wrapping streams assert isinstance(prepped_stream, InsistentReaderBytesIO) + + +def ephemeral_raw_rsa_master_key(size=4096): + private_key = rsa.generate_private_key(public_exponent=65537, key_size=size, backend=default_backend()) + key_bytes = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + return RawMasterKey( + provider_id="fake", + key_id="rsa-{}".format(size).encode("utf-8"), + wrapping_key=WrappingKey( + wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + wrapping_key=key_bytes, + wrapping_key_type=EncryptionKeyType.PRIVATE, + ), + ) + + +def ephemeral_raw_aes_master_key(size=256): + key = os.urandom(size // 8) + return RawMasterKey( + provider_id="fake", + key_id="aes-{}".format(size).encode("utf-8"), + wrapping_key=WrappingKey( + wrapping_algorithm=WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING, + wrapping_key=key, + wrapping_key_type=EncryptionKeyType.SYMMETRIC, + ), + ) + + +class EphemeralRawMasterKeyProvider(RawMasterKeyProvider): + """Master key provider with raw master keys that are generated on each initialization.""" + + provider_id = "fake" + + def __init__(self): + self.__keys = {b"aes-256": ephemeral_raw_aes_master_key(256), b"rsa-4096": ephemeral_raw_rsa_master_key(4096)} + + def _get_raw_key(self, key_id): + return self.__keys[key_id].config.wrapping_key + + +class EmptyMasterKeyProvider(MasterKeyProvider): + """Master key provider that provides no master keys.""" + + provider_id = "empty" + _config_class = MasterKeyProviderConfig + vend_masterkey_on_decrypt = False + + def _new_master_key(self, key_id): + raise Exception("How did this happen??") + + def master_keys_for_encryption(self, encryption_context, plaintext_rostream, plaintext_length=None): + return ephemeral_raw_aes_master_key(), [] + + +class DisjointMasterKeyProvider(MasterKeyProvider): + """Master key provider that does not provide the primary master key in the additional master keys.""" + + provider_id = "disjoint" + _config_class = MasterKeyProviderConfig + vend_masterkey_on_decrypt = False + + def _new_master_key(self, key_id): + raise Exception("How did this happen??") + + def master_keys_for_encryption(self, encryption_context, plaintext_rostream, plaintext_length=None): + return ephemeral_raw_aes_master_key(), [ephemeral_raw_rsa_master_key()] + + +class UnknownDataKeyInfoMasterKeyProvider(EphemeralRawMasterKeyProvider): + """EphemeralRawMasterKeyProvider that cannot locate a master key from a decrypted data key.""" + + def master_keys_for_data_key(self, data_key): + """Only fail on post-decryption check.""" + if hasattr(data_key, "data_key"): + return [] + + return super(UnknownDataKeyInfoMasterKeyProvider, self).master_keys_for_data_key(data_key) + + +class FailingDecryptMasterKeyProvider(EphemeralRawMasterKeyProvider): + """EphemeralRawMasterKeyProvider that cannot decrypt.""" + + def decrypt_data_key(self, encrypted_data_key, algorithm, encryption_context): + raise DecryptKeyError("FailingDecryptMasterKeyProvider cannot decrypt!") From c4d1276cf6d6e6672d75ad03b20379fb46de9a3c Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Fri, 14 Feb 2020 15:30:28 -0800 Subject: [PATCH 5/6] feat: update default and caching CMMs to accept keyrings and automatically wrap MKPs in keyrings --- .../materials_managers/caching.py | 62 +++++++----- .../materials_managers/default.py | 98 ++++++++++++------- test/unit/materials_managers/test_caching.py | 2 +- test/unit/materials_managers/test_default.py | 98 ++++++++----------- 4 files changed, 146 insertions(+), 114 deletions(-) diff --git a/src/aws_encryption_sdk/materials_managers/caching.py b/src/aws_encryption_sdk/materials_managers/caching.py index 992a39a7a..20e36a6a0 100644 --- a/src/aws_encryption_sdk/materials_managers/caching.py +++ b/src/aws_encryption_sdk/materials_managers/caching.py @@ -16,6 +16,7 @@ import attr import six +from attr.validators import instance_of, optional from ..caches import ( CryptoMaterialsCacheEntryHints, @@ -27,6 +28,7 @@ from ..internal.defaults import MAX_BYTES_PER_KEY, MAX_MESSAGES_PER_KEY from ..internal.str_ops import to_bytes from ..key_providers.base import MasterKeyProvider +from ..keyrings.base import Keyring from . import EncryptionMaterialsRequest from .base import CryptoMaterialsManager from .default import DefaultCryptoMaterialsManager @@ -36,10 +38,14 @@ @attr.s(hash=False) class CachingCryptoMaterialsManager(CryptoMaterialsManager): + # pylint: disable=too-many-instance-attributes """Crypto material manager which caches results from an underlying material manager. .. versionadded:: 1.3.0 + .. versionadded:: 1.5.0 + The *keyring* parameter. + >>> import aws_encryption_sdk >>> kms_key_provider = aws_encryption_sdk.KMSMasterKeyProvider(key_ids=[ ... 'arn:aws:kms:us-east-1:2222222222222:key/22222222-2222-2222-2222-222222222222', @@ -62,14 +68,14 @@ class CachingCryptoMaterialsManager(CryptoMaterialsManager): Either `backing_materials_manager` or `master_key_provider` must be provided. `backing_materials_manager` will always be used if present. - :param cache: Crypto cache to use with material manager - :type cache: aws_encryption_sdk.caches.base.CryptoMaterialsCache - :param backing_materials_manager: Crypto material manager to back this caching material manager + :param CryptoMaterialsCache cache: Crypto cache to use with material manager + :param CryptoMaterialsManager backing_materials_manager: + Crypto material manager to back this caching material manager (either `backing_materials_manager` or `master_key_provider` required) - :type backing_materials_manager: aws_encryption_sdk.materials_managers.base.CryptoMaterialsManager - :param master_key_provider: Master key provider to use (either `backing_materials_manager` or - `master_key_provider` required) - :type master_key_provider: aws_encryption_sdk.key_providers.base.MasterKeyProvider + :param MasterKeyProvider master_key_provider: Master key provider to use + (either `backing_materials_manager`, `keyring`, or `master_key_provider` is required) + :param Keyring keyring: Keyring to use + (either `backing_materials_manager`, `keyring`, or `master_key_provider` is required) :param float max_age: Maximum time in seconds that a cache entry may be kept in the cache :param int max_messages_encrypted: Maximum number of messages that may be encrypted under a cache entry (optional) @@ -78,21 +84,14 @@ class CachingCryptoMaterialsManager(CryptoMaterialsManager): :param bytes partition_name: Partition name to use for this instance (optional) """ - cache = attr.ib(validator=attr.validators.instance_of(CryptoMaterialsCache)) - max_age = attr.ib(validator=attr.validators.instance_of(float)) - max_messages_encrypted = attr.ib( - default=MAX_MESSAGES_PER_KEY, validator=attr.validators.instance_of(six.integer_types) - ) - max_bytes_encrypted = attr.ib(default=MAX_BYTES_PER_KEY, validator=attr.validators.instance_of(six.integer_types)) - partition_name = attr.ib( - default=None, converter=to_bytes, validator=attr.validators.optional(attr.validators.instance_of(bytes)) - ) - master_key_provider = attr.ib( - default=None, validator=attr.validators.optional(attr.validators.instance_of(MasterKeyProvider)) - ) - backing_materials_manager = attr.ib( - default=None, validator=attr.validators.optional(attr.validators.instance_of(CryptoMaterialsManager)) - ) + cache = attr.ib(validator=instance_of(CryptoMaterialsCache)) + max_age = attr.ib(validator=instance_of(float)) + max_messages_encrypted = attr.ib(default=MAX_MESSAGES_PER_KEY, validator=instance_of(six.integer_types)) + max_bytes_encrypted = attr.ib(default=MAX_BYTES_PER_KEY, validator=instance_of(six.integer_types)) + partition_name = attr.ib(default=None, converter=to_bytes, validator=optional(instance_of(bytes))) + master_key_provider = attr.ib(default=None, validator=optional(instance_of(MasterKeyProvider))) + keyring = attr.ib(default=None, validator=optional(instance_of(Keyring))) + backing_materials_manager = attr.ib(default=None, validator=optional(instance_of(CryptoMaterialsManager))) def __attrs_post_init__(self): """Applies post-processing which cannot be handled by attrs.""" @@ -111,10 +110,23 @@ def __attrs_post_init__(self): if self.max_age <= 0.0: raise ValueError("max_age cannot be less than or equal to 0") + exactly_one = ( + len([i for i in (self.master_key_provider, self.keyring, self.backing_materials_manager) if i is not None]) + == 1 + ) + + if not exactly_one: + raise TypeError( + "Exactly one of 'backing_materials_manager', 'keyring', or 'master_key_provider' must be provided." + ) + if self.backing_materials_manager is None: - if self.master_key_provider is None: - raise TypeError("Either backing_materials_manager or master_key_provider must be defined") - self.backing_materials_manager = DefaultCryptoMaterialsManager(self.master_key_provider) + if self.keyring is not None: + self.backing_materials_manager = DefaultCryptoMaterialsManager(keyring=self.keyring) + else: + self.backing_materials_manager = DefaultCryptoMaterialsManager( + master_key_provider=self.master_key_provider + ) if self.partition_name is None: self.partition_name = to_bytes(str(uuid.uuid4())) diff --git a/src/aws_encryption_sdk/materials_managers/default.py b/src/aws_encryption_sdk/materials_managers/default.py index 6d10465a9..a80c8e48d 100644 --- a/src/aws_encryption_sdk/materials_managers/default.py +++ b/src/aws_encryption_sdk/materials_managers/default.py @@ -14,15 +14,17 @@ import logging import attr +from attr.validators import instance_of, optional -from ..exceptions import MasterKeyProviderError, SerializationError +from ..exceptions import InvalidCryptographicMaterialsError, SerializationError from ..internal.crypto.authentication import Signer, Verifier from ..internal.crypto.elliptic_curve import generate_ecc_signing_key from ..internal.defaults import ALGORITHM, ENCODED_SIGNER_KEY from ..internal.str_ops import to_str -from ..internal.utils import prepare_data_keys from ..key_providers.base import MasterKeyProvider -from . import DecryptionMaterials, EncryptionMaterials +from ..keyrings.base import Keyring +from ..keyrings.master_key import MasterKeyProviderKeyring +from . import DecryptionMaterials, DecryptionMaterialsRequest, EncryptionMaterials, EncryptionMaterialsRequest from .base import CryptoMaterialsManager _LOGGER = logging.getLogger(__name__) @@ -34,12 +36,29 @@ class DefaultCryptoMaterialsManager(CryptoMaterialsManager): .. versionadded:: 1.3.0 - :param master_key_provider: Master key provider to use - :type master_key_provider: aws_encryption_sdk.key_providers.base.MasterKeyProvider + .. versionadded:: 1.5.0 + The *keyring* parameter. + + :param MasterKeyProvider master_key_provider: Master key provider to use + (either `keyring` or `master_key_provider` is required) + :param Keyring keyring: Keyring to use + (either `keyring` or `master_key_provider` is required) """ algorithm = ALGORITHM - master_key_provider = attr.ib(validator=attr.validators.instance_of(MasterKeyProvider)) + master_key_provider = attr.ib(default=None, validator=optional(instance_of(MasterKeyProvider))) + keyring = attr.ib(default=None, validator=optional(instance_of(Keyring))) + + def __attrs_post_init__(self): + """Make sure that exactly one key provider is set and prep the keyring if needed.""" + both = self.keyring is not None and self.master_key_provider is not None + neither = self.keyring is None and self.master_key_provider is None + + if both or neither: + raise TypeError("Exactly one of 'master_key_provider' or 'keyring' must be provided.") + + if self.keyring is None: + self.keyring = MasterKeyProviderKeyring(master_key_provider=self.master_key_provider) def _generate_signing_key_and_update_encryption_context(self, algorithm, encryption_context): """Generates a signing key based on the provided algorithm. @@ -59,6 +78,7 @@ def _generate_signing_key_and_update_encryption_context(self, algorithm, encrypt return signer.key_bytes() def get_encryption_materials(self, request): + # type: (EncryptionMaterialsRequest) -> EncryptionMaterials """Creates encryption materials using underlying master key provider. :param request: encryption materials request @@ -74,32 +94,28 @@ def get_encryption_materials(self, request): signing_key = self._generate_signing_key_and_update_encryption_context(algorithm, encryption_context) - primary_master_key, master_keys = self.master_key_provider.master_keys_for_encryption( - encryption_context=encryption_context, - plaintext_rostream=request.plaintext_rostream, - plaintext_length=request.plaintext_length, - ) - if not master_keys: - raise MasterKeyProviderError("No Master Keys available from Master Key Provider") - if primary_master_key not in master_keys: - raise MasterKeyProviderError("Primary Master Key not in provided Master Keys") - - data_encryption_key, encrypted_data_keys = prepare_data_keys( - primary_master_key=primary_master_key, - master_keys=master_keys, - algorithm=algorithm, - encryption_context=encryption_context, + expected_encryption_context = encryption_context.copy() + + encryption_materials = EncryptionMaterials( + algorithm=algorithm, encryption_context=encryption_context, signing_key=signing_key, ) - _LOGGER.debug("Post-encrypt encryption context: %s", encryption_context) + final_materials = self.keyring.on_encrypt(encryption_materials=encryption_materials) + + if not final_materials.is_complete: + raise InvalidCryptographicMaterialsError("Encryption materials are incomplete!") - return EncryptionMaterials( - algorithm=algorithm, - data_encryption_key=data_encryption_key, - encrypted_data_keys=encrypted_data_keys, - encryption_context=encryption_context, - signing_key=signing_key, + materials_are_valid = ( + final_materials.algorithm is algorithm, + final_materials.encryption_context == expected_encryption_context, + final_materials.signing_key is signing_key, ) + if not all(materials_are_valid): + raise InvalidCryptographicMaterialsError("Encryption materials do not match request!") + + _LOGGER.debug("Post-encrypt encryption context: %s", final_materials.encryption_context) + + return final_materials def _load_verification_key_from_encryption_context(self, algorithm, encryption_context): """Loads the verification key from the encryption context if used by algorithm suite. @@ -125,6 +141,7 @@ def _load_verification_key_from_encryption_context(self, algorithm, encryption_c return verifier.key_bytes() def decrypt_materials(self, request): + # type: (DecryptionMaterialsRequest) -> DecryptionMaterials """Obtains a plaintext data key from one or more encrypted data keys using underlying master key provider. @@ -133,13 +150,28 @@ def decrypt_materials(self, request): :returns: decryption materials :rtype: aws_encryption_sdk.materials_managers.DecryptionMaterials """ - data_key = self.master_key_provider.decrypt_data_key_from_list( - encrypted_data_keys=request.encrypted_data_keys, + verification_key = self._load_verification_key_from_encryption_context( + algorithm=request.algorithm, encryption_context=request.encryption_context + ) + decryption_materials = DecryptionMaterials( algorithm=request.algorithm, encryption_context=request.encryption_context, + verification_key=verification_key, ) - verification_key = self._load_verification_key_from_encryption_context( - algorithm=request.algorithm, encryption_context=request.encryption_context + + final_materials = self.keyring.on_decrypt( + decryption_materials=decryption_materials, encrypted_data_keys=request.encrypted_data_keys + ) + + if not final_materials.is_complete: + raise InvalidCryptographicMaterialsError("Materials are incomplete!") + + materials_are_valid = ( + final_materials.algorithm is request.algorithm, + final_materials.encryption_context == request.encryption_context, + final_materials.verification_key is verification_key, ) + if not all(materials_are_valid): + raise InvalidCryptographicMaterialsError("Decryption materials do not match request!") - return DecryptionMaterials(data_key=data_key, verification_key=verification_key) + return final_materials diff --git a/test/unit/materials_managers/test_caching.py b/test/unit/materials_managers/test_caching.py index 833d6aa53..59b63e13d 100644 --- a/test/unit/materials_managers/test_caching.py +++ b/test/unit/materials_managers/test_caching.py @@ -96,7 +96,7 @@ def test_mkp_to_default_cmm(mocker): ) aws_encryption_sdk.materials_managers.caching.DefaultCryptoMaterialsManager.assert_called_once_with( - mock_mkp + master_key_provider=mock_mkp ) # noqa pylint: disable=line-too-long assert ( test.backing_materials_manager diff --git a/test/unit/materials_managers/test_default.py b/test/unit/materials_managers/test_default.py index 32fdc953a..3965adee2 100644 --- a/test/unit/materials_managers/test_default.py +++ b/test/unit/materials_managers/test_default.py @@ -12,17 +12,23 @@ # language governing permissions and limitations under the License. """Test suite for aws_encryption_sdk.materials_managers.default""" import pytest -from mock import MagicMock, sentinel -from pytest_mock import mocker # noqa pylint: disable=unused-import +from mock import MagicMock, patch, sentinel import aws_encryption_sdk.materials_managers.default from aws_encryption_sdk.exceptions import MasterKeyProviderError, SerializationError from aws_encryption_sdk.identifiers import Algorithm from aws_encryption_sdk.internal.defaults import ALGORITHM, ENCODED_SIGNER_KEY from aws_encryption_sdk.key_providers.base import MasterKeyProvider -from aws_encryption_sdk.materials_managers import EncryptionMaterials +from aws_encryption_sdk.keyrings.base import Keyring +from aws_encryption_sdk.materials_managers import ( + DecryptionMaterialsRequest, + EncryptionMaterials, + EncryptionMaterialsRequest, +) from aws_encryption_sdk.materials_managers.default import DefaultCryptoMaterialsManager -from aws_encryption_sdk.structures import DataKey, EncryptedDataKey, MasterKeyInfo, RawDataKey +from aws_encryption_sdk.structures import DataKey, EncryptedDataKey, MasterKeyInfo + +from ..unit_test_utils import ephemeral_raw_rsa_master_key pytestmark = [pytest.mark.unit, pytest.mark.local] @@ -39,11 +45,9 @@ def patch_for_dcmm_encrypt(mocker): mocker.patch.object(DefaultCryptoMaterialsManager, "_generate_signing_key_and_update_encryption_context") mock_signing_key = b"ex_signing_key" DefaultCryptoMaterialsManager._generate_signing_key_and_update_encryption_context.return_value = mock_signing_key - mocker.patch.object(aws_encryption_sdk.materials_managers.default, "prepare_data_keys") mock_data_encryption_key = _DATA_KEY mock_encrypted_data_keys = (_ENCRYPTED_DATA_KEY,) result_pair = mock_data_encryption_key, mock_encrypted_data_keys - aws_encryption_sdk.materials_managers.default.prepare_data_keys.return_value = result_pair yield result_pair, mock_signing_key @@ -56,18 +60,15 @@ def patch_for_dcmm_decrypt(mocker): def build_cmm(): - mock_mkp = MagicMock(__class__=MasterKeyProvider) - mock_mkp.decrypt_data_key_from_list.return_value = _DATA_KEY - mock_mkp.master_keys_for_encryption.return_value = ( - sentinel.primary_mk, - set([sentinel.primary_mk, sentinel.mk_a, sentinel.mk_b]), - ) - return DefaultCryptoMaterialsManager(master_key_provider=mock_mkp) + return DefaultCryptoMaterialsManager(master_key_provider=ephemeral_raw_rsa_master_key()) -def test_attributes_fail(): +@pytest.mark.parametrize( + "mkp, keyring", ((None, None), (MagicMock(__class__=MasterKeyProvider), MagicMock(__class__=Keyring))) +) +def test_attributes_fail(mkp, keyring): with pytest.raises(TypeError): - DefaultCryptoMaterialsManager(master_key_provider=None) + DefaultCryptoMaterialsManager(master_key_provider=mkp, keyring=keyring) def test_attributes_default(): @@ -115,33 +116,23 @@ def test_generate_signing_key_and_update_encryption_context(mocker): def test_get_encryption_materials(patch_for_dcmm_encrypt): encryption_context = {"a": "b"} - mock_request = MagicMock(algorithm=None, encryption_context=encryption_context) + request = EncryptionMaterialsRequest(encryption_context=encryption_context, frame_length=128) cmm = build_cmm() - test = cmm.get_encryption_materials(request=mock_request) + test = cmm.get_encryption_materials(request=request) - cmm.master_key_provider.master_keys_for_encryption.assert_called_once_with( - encryption_context=encryption_context, - plaintext_rostream=mock_request.plaintext_rostream, - plaintext_length=mock_request.plaintext_length, - ) - cmm._generate_signing_key_and_update_encryption_context.assert_called_once_with(cmm.algorithm, encryption_context) - aws_encryption_sdk.materials_managers.default.prepare_data_keys.assert_called_once_with( - primary_master_key=cmm.master_key_provider.master_keys_for_encryption.return_value[0], - master_keys=cmm.master_key_provider.master_keys_for_encryption.return_value[1], - algorithm=cmm.algorithm, - encryption_context=encryption_context, - ) assert isinstance(test, EncryptionMaterials) assert test.algorithm is cmm.algorithm - assert test.data_encryption_key == RawDataKey.from_data_key(patch_for_dcmm_encrypt[0][0]) - assert test.encrypted_data_keys == patch_for_dcmm_encrypt[0][1] + assert test.data_encryption_key.data_key + assert test.data_encryption_key.key_provider.provider_id == "fake" + assert test.data_encryption_key.key_provider.key_id == b"rsa-4096" + assert len(test.encrypted_data_keys) == 1 assert test.encryption_context == encryption_context assert test.signing_key == patch_for_dcmm_encrypt[1] def test_get_encryption_materials_override_algorithm(patch_for_dcmm_encrypt): - mock_request = MagicMock(algorithm=MagicMock(__class__=Algorithm), encryption_context={}) + mock_request = MagicMock(algorithm=Algorithm.AES_128_GCM_IV12_TAG16, encryption_context={}) cmm = build_cmm() test = cmm.get_encryption_materials(request=mock_request) @@ -150,26 +141,28 @@ def test_get_encryption_materials_override_algorithm(patch_for_dcmm_encrypt): def test_get_encryption_materials_no_mks(patch_for_dcmm_encrypt): - mock_request = MagicMock(algorithm=MagicMock(__class__=Algorithm), encryption_context={}) + mock_request = MagicMock(algorithm=ALGORITHM, encryption_context={}) cmm = build_cmm() - cmm.master_key_provider.master_keys_for_encryption.return_value = (None, set([])) - with pytest.raises(MasterKeyProviderError) as excinfo: - cmm.get_encryption_materials(request=mock_request) + with patch.object(cmm.master_key_provider, "master_keys_for_encryption", return_value=(None, set([]))): + + with pytest.raises(MasterKeyProviderError) as excinfo: + cmm.get_encryption_materials(request=mock_request) excinfo.match(r"No Master Keys available from Master Key Provider") def test_get_encryption_materials_primary_mk_not_in_mks(patch_for_dcmm_encrypt): - mock_request = MagicMock(algorithm=MagicMock(__class__=Algorithm), encryption_context={}) + mock_request = MagicMock(algorithm=ALGORITHM, encryption_context={}) cmm = build_cmm() - cmm.master_key_provider.master_keys_for_encryption.return_value = ( - sentinel.primary_mk, - {sentinel.mk_a, sentinel.mk_b}, - ) + with patch.object( + cmm.master_key_provider, + "master_keys_for_encryption", + return_value=(sentinel.primary_mk, {sentinel.mk_a, sentinel.mk_b},), + ): - with pytest.raises(MasterKeyProviderError) as excinfo: - cmm.get_encryption_materials(request=mock_request) + with pytest.raises(MasterKeyProviderError) as excinfo: + cmm.get_encryption_materials(request=mock_request) excinfo.match(r"Primary Master Key not in provided Master Keys") @@ -225,19 +218,14 @@ def test_load_verification_key_from_encryption_context_key_is_needed_and_is_foun assert test is mock_verifier.key_bytes.return_value -def test_decrypt_materials(mocker, patch_for_dcmm_decrypt): - mock_request = MagicMock() +def test_decrypt_materials(patch_for_dcmm_decrypt): cmm = build_cmm() + dk = cmm.master_key_provider.generate_data_key(algorithm=ALGORITHM, encryption_context={}) + edk = EncryptedDataKey(key_provider=dk.key_provider, encrypted_data_key=dk.encrypted_data_key) - test = cmm.decrypt_materials(request=mock_request) - - cmm.master_key_provider.decrypt_data_key_from_list.assert_called_once_with( - encrypted_data_keys=mock_request.encrypted_data_keys, - algorithm=mock_request.algorithm, - encryption_context=mock_request.encryption_context, - ) - cmm._load_verification_key_from_encryption_context.assert_called_once_with( - algorithm=mock_request.algorithm, encryption_context=mock_request.encryption_context + test = cmm.decrypt_materials( + request=DecryptionMaterialsRequest(algorithm=ALGORITHM, encryption_context={}, encrypted_data_keys={edk}) ) - assert test.data_key == RawDataKey.from_data_key(cmm.master_key_provider.decrypt_data_key_from_list.return_value) + + assert test.data_key.data_key == dk.data_key assert test.verification_key == patch_for_dcmm_decrypt From 31930c48e58e75bfb05ec22400822d0fd6089306 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Fri, 14 Feb 2020 15:30:45 -0800 Subject: [PATCH 6/6] chore: include keyrings in docs --- doc/index.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/index.rst b/doc/index.rst index 10957074e..1ea8cc0b2 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -14,6 +14,10 @@ Modules aws_encryption_sdk.caches.base aws_encryption_sdk.caches.local aws_encryption_sdk.caches.null + aws_encryption_sdk.keyrings.base + aws_encryption_sdk.keyrings.master_key + aws_encryption_sdk.keyrings.multi + aws_encryption_sdk.keyrings.raw aws_encryption_sdk.key_providers.base aws_encryption_sdk.key_providers.kms aws_encryption_sdk.key_providers.raw