Skip to content

master key provider keyring #209

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions src/aws_encryption_sdk/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down
2 changes: 1 addition & 1 deletion src/aws_encryption_sdk/internal/formatting/serialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
8 changes: 6 additions & 2 deletions src/aws_encryption_sdk/key_providers/raw.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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,
)
Expand Down
222 changes: 222 additions & 0 deletions src/aws_encryption_sdk/keyrings/master_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This date is wrong, but according to yesterday's communication we can remove the date anyway

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I think what I'll do here is update the date here, then in a separate PR update all the copyright notices per aws/crypto-tools#15

#
# 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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this the same issue that prevented the Java ESDK from using a MasterKeyProviderKeyring? You aren't providing the plaintext_rostream or plaintext_length to the MKP, but the Default CMM used to. I think we may have discussed this, but why isn't that a potential breaking change for customers who have written custom MKPs and used the plaintext_rostream or plaintext_length inputs?

)
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."
Comment on lines +124 to +126
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the multi keyring validate that the children keyrings are not MKP keyrings upon creation? This behavior is different than all the other keyrings, so it might be confusing

)

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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would cause this to happen, a buggy MKP?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically, yes.

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
2 changes: 2 additions & 0 deletions src/aws_encryption_sdk/keyrings/multi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions src/aws_encryption_sdk/keyrings/raw.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
10 changes: 8 additions & 2 deletions src/aws_encryption_sdk/materials_managers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down
Loading