diff --git a/barbican/api/__init__.py b/barbican/api/__init__.py index 6e62f6657..3382afecd 100644 --- a/barbican/api/__init__.py +++ b/barbican/api/__init__.py @@ -22,10 +22,10 @@ import pkgutil from barbican.common import exception from barbican.common import utils -from barbican.crypto import extension_manager as em from barbican.openstack.common import gettextutils as u from barbican.openstack.common import jsonutils as json from barbican.openstack.common import policy +from barbican.plugin.interface import secret_store as s LOG = utils.getLogger(__name__) @@ -111,40 +111,34 @@ def generate_safe_exception_message(operation_name, excep): 'please review your ' 'user/tenant privileges').format(operation_name) status = 403 - except em.CryptoContentTypeNotSupportedException as cctnse: + + except s.SecretContentTypeNotSupportedException as sctnse: reason = u._("content-type of '{0}' not " - "supported").format(cctnse.content_type) + "supported").format(sctnse.content_type) status = 400 - except em.CryptoContentEncodingNotSupportedException as cc: + except s.SecretContentEncodingNotSupportedException as ce: reason = u._("content-encoding of '{0}' not " - "supported").format(cc.content_encoding) + "supported").format(ce.content_encoding) status = 400 - except em.CryptoAcceptNotSupportedException as canse: - reason = u._("accept of '{0}' not " - "supported").format(canse.accept) - status = 406 - except em.CryptoNoPayloadProvidedException: - reason = u._("No payload provided") - status = 400 - except em.CryptoNoSecretOrDataFoundException: - reason = u._("Not Found. Sorry but your secret is in " - "another castle") - status = 404 - except em.CryptoPayloadDecodingError: - reason = u._("Problem decoding payload") - status = 400 - except em.CryptoContentEncodingMustBeBase64: - reason = u._("Text-based binary secret payloads must " - "specify a content-encoding of 'base64'") - status = 400 - except em.CryptoAlgorithmNotSupportedException: - reason = u._("No plugin was found that supports the " - "requested algorithm") - status = 400 - except em.CryptoSupportedPluginNotFound: + except s.SecretStorePluginNotFound: reason = u._("No plugin was found that could support " "your request") status = 400 + except s.SecretPayloadDecodingError: + reason = u._("Problem decoding payload") + status = 400 + except s.SecretContentEncodingMustBeBase64: + reason = u._("Text-based binary secret payloads must " + "specify a content-encoding of 'base64'") + status = 400 + except s.SecretNotFoundException: + reason = u._("Not Found. Sorry but your secret is in " + "another castle") + status = 404 + except s.SecretAlgorithmNotSupportedException: + reason = u._("Requested algorithm is not supported") + status = 400 + except exception.NoDataToProcess: reason = u._("No information provided to process") status = 400 @@ -152,6 +146,7 @@ def generate_safe_exception_message(operation_name, excep): reason = u._("Provided information too large " "to process") status = 413 + except Exception: message = u._('{0} failure seen - please contact site ' 'administrator.').format(operation_name) diff --git a/barbican/api/app.py b/barbican/api/app.py index 84f9873d4..86c12f693 100644 --- a/barbican/api/app.py +++ b/barbican/api/app.py @@ -36,7 +36,6 @@ from barbican.api.controllers import secrets from barbican.api.controllers import transportkeys from barbican.api.controllers import versions from barbican.common import config -from barbican.crypto import extension_manager as ext from barbican.openstack.common import log from barbican import queue @@ -92,15 +91,13 @@ def create_main_app(global_config, **local_conf): config.parse_args() log.setup('barbican') config.setup_remote_pydev_debug() - # Crypto Plugin Manager - crypto_mgr = ext.CryptoExtensionManager() # Queuing initialization CONF = cfg.CONF queue.init(CONF) class RootController(object): - secrets = secrets.SecretsController(crypto_mgr) + secrets = secrets.SecretsController() orders = orders.OrdersController() containers = containers.ContainersController() transport_keys = transportkeys.TransportKeysController() diff --git a/barbican/api/controllers/secrets.py b/barbican/api/controllers/secrets.py index c37203de1..e15219732 100644 --- a/barbican/api/controllers/secrets.py +++ b/barbican/api/controllers/secrets.py @@ -22,9 +22,11 @@ from barbican.common import exception from barbican.common import resources as res from barbican.common import utils from barbican.common import validators -from barbican.crypto import mime_types from barbican.model import repositories as repo from barbican.openstack.common import gettextutils as u +from barbican.plugin import resources as plugin +from barbican.plugin import util as putil + LOG = utils.getLogger(__name__) @@ -51,16 +53,18 @@ def _secret_already_has_data(): class SecretController(object): """Handles Secret retrieval and deletion requests.""" - def __init__(self, secret_id, crypto_manager, + def __init__(self, secret_id, tenant_repo=None, secret_repo=None, datum_repo=None, - kek_repo=None): + kek_repo=None, secret_meta_repo=None): LOG.debug('=== Creating SecretController ===') self.secret_id = secret_id - self.crypto_manager = crypto_manager - self.tenant_repo = tenant_repo or repo.TenantRepo() - self.repo = secret_repo or repo.SecretRepo() - self.datum_repo = datum_repo or repo.EncryptedDatumRepo() - self.kek_repo = kek_repo or repo.KEKDatumRepo() + + #TODO(john-wood-w) Remove passed-in repositories in favor of + # repository factories and patches in unit tests. + self.repos = repo.Repositories(tenant_repo=tenant_repo, + secret_repo=secret_repo, + datum_repo=datum_repo, + kek_repo=kek_repo) @pecan.expose(generic=True) @allow_all_content_types @@ -68,26 +72,26 @@ class SecretController(object): @controllers.handle_rbac('secret:get') def index(self, keystone_id): - secret = self.repo.get(entity_id=self.secret_id, - keystone_id=keystone_id, - suppress_exception=True) + secret = self.repos.secret_repo.get(entity_id=self.secret_id, + keystone_id=keystone_id, + suppress_exception=True) if not secret: _secret_not_found() if controllers.is_json_request_accept(pecan.request): - # Metadata-only response, no decryption necessary. + # Metadata-only response, no secret retrieval is necessary. pecan.override_template('json', 'application/json') - secret_fields = mime_types.augment_fields_with_content_types( + secret_fields = putil.mime_types.augment_fields_with_content_types( secret) return hrefs.convert_to_hrefs(keystone_id, secret_fields) else: - tenant = res.get_or_create_tenant(keystone_id, self.tenant_repo) + tenant = res.get_or_create_tenant(keystone_id, + self.repos.tenant_repo) pecan.override_template('', pecan.request.accept.header_value) - return self.crypto_manager.decrypt( - pecan.request.accept.header_value, - secret, - tenant - ) + + return plugin.get_secret(pecan.request.accept.header_value, + secret, + tenant) @index.when(method='PUT') @allow_all_content_types @@ -104,61 +108,68 @@ class SecretController(object): ) ) - secret = self.repo.get(entity_id=self.secret_id, - keystone_id=keystone_id, - suppress_exception=True) - if not secret: + payload = pecan.request.body + if not payload: + raise exception.NoDataToProcess() + if validators.secret_too_big(payload): + raise exception.LimitExceeded() + + secret_model = self.repos.secret_repo.get(entity_id=self.secret_id, + keystone_id=keystone_id, + suppress_exception=True) + if not secret_model: _secret_not_found() - if secret.encrypted_data: + if secret_model.encrypted_data: _secret_already_has_data() - tenant = res.get_or_create_tenant(keystone_id, self.tenant_repo) + tenant_model = res.get_or_create_tenant(keystone_id, + self.repos.tenant_repo) content_type = pecan.request.content_type content_encoding = pecan.request.headers.get('Content-Encoding') - res.create_encrypted_datum(secret, - pecan.request.body, - content_type, - content_encoding, - tenant, - self.crypto_manager, - self.datum_repo, - self.kek_repo) + plugin.store_secret(payload, content_type, + content_encoding, secret_model.to_dict_fields, + secret_model, tenant_model, self.repos) @index.when(method='DELETE') @controllers.handle_exceptions(u._('Secret deletion')) @controllers.handle_rbac('secret:delete') def on_delete(self, keystone_id, **kwargs): - try: - self.repo.delete_entity_by_id(entity_id=self.secret_id, - keystone_id=keystone_id) - except exception.NotFound: - LOG.exception('Problem deleting secret') + secret_model = self.repos.secret_repo.get(entity_id=self.secret_id, + keystone_id=keystone_id, + suppress_exception=True) + if not secret_model: _secret_not_found() + plugin.delete_secret(secret_model, keystone_id, self.repos) + class SecretsController(object): """Handles Secret creation requests.""" - def __init__(self, crypto_manager, + def __init__(self, tenant_repo=None, secret_repo=None, - tenant_secret_repo=None, datum_repo=None, kek_repo=None): + tenant_secret_repo=None, datum_repo=None, kek_repo=None, + secret_meta_repo=None): LOG.debug('Creating SecretsController') - self.tenant_repo = tenant_repo or repo.TenantRepo() - self.secret_repo = secret_repo or repo.SecretRepo() - self.tenant_secret_repo = tenant_secret_repo or repo.TenantSecretRepo() - self.datum_repo = datum_repo or repo.EncryptedDatumRepo() - self.kek_repo = kek_repo or repo.KEKDatumRepo() - self.crypto_manager = crypto_manager self.validator = validators.NewSecretValidator() + self.repos = repo.Repositories(tenant_repo=tenant_repo, + tenant_secret_repo=tenant_secret_repo, + secret_repo=secret_repo, + datum_repo=datum_repo, + kek_repo=kek_repo, + secret_meta_repo=secret_meta_repo) @pecan.expose() def _lookup(self, secret_id, *remainder): - return SecretController(secret_id, self.crypto_manager, - self.tenant_repo, self.secret_repo, - self.datum_repo, self.kek_repo), remainder + return SecretController(secret_id, + self.repos.tenant_repo, + self.repos.secret_repo, + self.repos.datum_repo, + self.repos.kek_repo, + self.repos.secret_meta_repo), remainder @pecan.expose(generic=True, template='json') @controllers.handle_exceptions(u._('Secret(s) retrieval')) @@ -179,7 +190,7 @@ class SecretsController(object): # the default should be used. bits = 0 - result = self.secret_repo.get_by_create_date( + result = self.repos.secret_repo.get_by_create_date( keystone_id, offset_arg=kw.get('offset', 0), limit_arg=kw.get('limit', None), @@ -196,8 +207,8 @@ class SecretsController(object): secrets_resp_overall = {'secrets': [], 'total': total} else: - secret_fields = lambda s: mime_types\ - .augment_fields_with_content_types(s) + secret_fields = lambda sf: putil.mime_types\ + .augment_fields_with_content_types(sf) secrets_resp = [ hrefs.convert_to_hrefs(keystone_id, secret_fields(s)) for s in secrets @@ -217,13 +228,14 @@ class SecretsController(object): LOG.debug('Start on_post for tenant-ID {0}:...'.format(keystone_id)) data = api.load_body(pecan.request, validator=self.validator) - tenant = res.get_or_create_tenant(keystone_id, self.tenant_repo) + tenant = res.get_or_create_tenant(keystone_id, self.repos.tenant_repo) - new_secret = res.create_secret(data, tenant, self.crypto_manager, - self.secret_repo, - self.tenant_secret_repo, - self.datum_repo, - self.kek_repo) + new_secret = plugin.store_secret(data.get('payload'), + data.get('payload_content_type', + 'application/octet-stream'), + data.get('payload_content_encoding'), + data, None, tenant, + self.repos) pecan.response.status = 201 pecan.response.headers['Location'] = '/{0}/secrets/{1}'.format( diff --git a/barbican/common/validators.py b/barbican/common/validators.py index 4ff4f7963..10fbc1412 100644 --- a/barbican/common/validators.py +++ b/barbican/common/validators.py @@ -21,9 +21,9 @@ import six from barbican.common import exception from barbican.common import utils -from barbican.crypto import mime_types from barbican.openstack.common import gettextutils as u from barbican.openstack.common import timeutils +from barbican.plugin.util import mime_types LOG = utils.getLogger(__name__) diff --git a/barbican/crypto/__init__.py b/barbican/crypto/__init__.py deleted file mode 100644 index 2ddd224e7..000000000 --- a/barbican/crypto/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright (c) 2013-2014 Rackspace, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -""" -Encryption/decryption services for Barbican. -""" diff --git a/barbican/crypto/dogtag_crypto.py b/barbican/crypto/dogtag_crypto.py deleted file mode 100644 index 735321b5b..000000000 --- a/barbican/crypto/dogtag_crypto.py +++ /dev/null @@ -1,237 +0,0 @@ -# Copyright (c) 2014 Red Hat, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import base64 -import os -import uuid - -from oslo.config import cfg -import pki -import pki.client -import pki.cryptoutil as cryptoutil -import pki.key as key -import pki.kraclient - -from barbican.common import exception -from barbican.crypto import plugin -from barbican.openstack.common import gettextutils as u - -CONF = cfg.CONF - -dogtag_crypto_plugin_group = cfg.OptGroup(name='dogtag_crypto_plugin', - title="Dogtag Crypto Plugin Options") -dogtag_crypto_plugin_opts = [ - cfg.StrOpt('pem_path', - help=u._('Path to PEM file for authentication')), - cfg.StrOpt('pem_password', - help=u._('Password to unlock PEM file')), - cfg.StrOpt('drm_host', - default="localhost", - help=u._('Hostname for the DRM')), - cfg.StrOpt('drm_port', - default="8443", - help=u._('Port for the DRM')), - cfg.StrOpt('nss_db_path', - help=u._('Path to the NSS certificate database')), - cfg.StrOpt('nss_password', - help=u._('Password for NSS certificate database')) -] - -CONF.register_group(dogtag_crypto_plugin_group) -CONF.register_opts(dogtag_crypto_plugin_opts, group=dogtag_crypto_plugin_group) - - -class DogtagPluginAlgorithmException(exception.BarbicanException): - message = u._("Invalid algorithm passed in") - - -class DogtagCryptoPlugin(plugin.CryptoPluginBase): - """Dogtag implementation of the crypto plugin with DRM as the backend.""" - - TRANSPORT_NICK = "DRM transport cert" - - def __init__(self, conf=CONF): - """Constructor - create the keyclient.""" - pem_path = conf.dogtag_crypto_plugin.pem_path - if pem_path is None: - raise ValueError(u._("pem_path is required")) - - pem_password = conf.dogtag_crypto_plugin.pem_password - if pem_password is None: - raise ValueError(u._("pem_password is required")) - - crypto = None - create_nss_db = False - - nss_db_path = conf.dogtag_crypto_plugin.nss_db_path - if nss_db_path is not None: - nss_password = conf.dogtag_crypto_plugin.nss_password - if nss_password is None: - raise ValueError(u._("nss_password is required")) - - if not os.path.exists(nss_db_path): - create_nss_db = True - cryptoutil.NSSCryptoUtil.setup_database( - nss_db_path, nss_password, over_write=True) - - crypto = cryptoutil.NSSCryptoUtil(nss_db_path, nss_password) - - # set up connection - connection = pki.client.PKIConnection( - 'https', - conf.dogtag_crypto_plugin.drm_host, - conf.dogtag_crypto_plugin.drm_port, - 'kra') - connection.set_authentication_cert(pem_path) - - # what happened to the password? - # until we figure out how to pass the password to requests, we'll - # just use -nodes to create the admin cert pem file. Any required - # code will end up being in the DRM python client - - #create kraclient - kraclient = pki.kraclient.KRAClient(connection, crypto) - self.keyclient = kraclient.keys - self.systemcert_client = kraclient.system_certs - - if crypto is not None: - if create_nss_db: - # Get transport cert and insert in the certdb - transport_cert = self.systemcert_client.get_transport_cert() - tcert = transport_cert[ - len(pki.CERT_HEADER): - len(transport_cert) - len(pki.CERT_FOOTER)] - crypto.import_cert(DogtagCryptoPlugin.TRANSPORT_NICK, - base64.decodestring(tcert), "u,u,u") - - crypto.initialize() - self.keyclient.set_transport_cert( - DogtagCryptoPlugin.TRANSPORT_NICK) - - def encrypt(self, encrypt_dto, kek_meta_dto, keystone_id): - """Store a secret in the DRM - - This will likely require another parameter which includes the wrapped - session key to be passed. Until that is added, we will call - archive_key() which relies on the DRM python client to create the - session keys. - - We may also be able to be more specific in terms of the data_type - if we know that the data being stored is a symmetric key. Until - then, we need to assume that the secret is pass_phrase_type. - """ - data_type = key.KeyClient.PASS_PHRASE_TYPE - client_key_id = uuid.uuid4().hex - response = self.keyclient.archive_key(client_key_id, - data_type, - encrypt_dto.unencrypted, - key_algorithm=None, - key_size=None) - return plugin.ResponseDTO(response.get_key_id(), None) - - def decrypt(self, decrypt_dto, kek_meta_dto, kek_meta_extended, - keystone_id): - """Retrieve a secret from the DRM - - The encrypted parameter simply contains the plain text key_id by which - the secret is known to the DRM. The remaining parameters are not - used. - - Note: There are two ways to retrieve secrets from the DRM. - - The first, which is implemented here, will call retrieve_key without - a wrapping key. This relies on the DRM client to generate a wrapping - key (and wrap it with the DRM transport cert), and is completely - transparent to the Barbican server. What is returned to the caller - is the unencrypted secret. - - The second way is to provide a wrapping key that ideally would be - generated on the barbican client. That way only the client will be - able to unwrap the secret. This is not yet implemented because - decrypt() and the barbican API still need to be changed to pass the - wrapping key. - """ - key_id = decrypt_dto.encrypted - key = self.keyclient.retrieve_key(key_id) - return key.data - - def bind_kek_metadata(self, kek_meta_dto): - """This function is not used by this plugin.""" - return kek_meta_dto - - def generate_symmetric(self, generate_dto, kek_meta_dto, keystone_id): - """Generate a symmetric key - - This calls generate_symmetric_key() on the DRM passing in the - algorithm, bit_length and id (used as the client_key_id) from - the secret. The remaining parameters are not used. - - Returns a keyId which will be stored in an EncryptedDatum - table for later retrieval. - """ - - usages = [key.SymKeyGenerationRequest.DECRYPT_USAGE, - key.SymKeyGenerationRequest.ENCRYPT_USAGE] - - client_key_id = uuid.uuid4().hex - algorithm = self._map_algorithm(generate_dto.algorithm.lower()) - - if algorithm is None: - raise DogtagPluginAlgorithmException - - response = self.keyclient.generate_symmetric_key( - client_key_id, - algorithm, - generate_dto.bit_length, - usages) - return plugin.ResponseDTO(response.get_key_id(), None) - - def generate_asymmetric(self, generate_dto, kek_meta_dto, keystone_id): - """Generate an asymmetric key.""" - raise NotImplementedError("Feature not implemented for dogtag crypto") - - def supports(self, type_enum, algorithm=None, bit_length=None, - mode=None): - """Specifies what operations the plugin supports.""" - if type_enum == plugin.PluginSupportTypes.ENCRYPT_DECRYPT: - return True - elif type_enum == plugin.PluginSupportTypes.SYMMETRIC_KEY_GENERATION: - return self._is_algorithm_supported(algorithm, - bit_length) - elif type_enum == plugin.PluginSupportTypes.ASYMMETRIC_KEY_GENERATION: - return False - else: - return False - - @staticmethod - def _map_algorithm(algorithm): - """Map Barbican algorithms to Dogtag plugin algorithms.""" - if algorithm == "aes": - return key.KeyClient.AES_ALGORITHM - elif algorithm == "des": - return key.KeyClient.DES_ALGORITHM - elif algorithm == "3des": - return key.KeyClient.DES3_ALGORITHM - else: - return None - - def _is_algorithm_supported(self, algorithm, bit_length=None): - """Check if algorithm and bit length are supported - - For now, we will just check the algorithm. When dogtag adds a - call to check the bit length per algorithm, we can modify to - make that call - """ - return self._map_algorithm(algorithm) is not None diff --git a/barbican/crypto/extension_manager.py b/barbican/crypto/extension_manager.py deleted file mode 100644 index c9eb541b6..000000000 --- a/barbican/crypto/extension_manager.py +++ /dev/null @@ -1,460 +0,0 @@ -# Copyright (c) 2013-2014 Rackspace, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import base64 - -from oslo.config import cfg -from stevedore import named - -from barbican.common import exception -from barbican.common import utils -from barbican.crypto import mime_types -from barbican.crypto import plugin as plugin_mod -from barbican.model import models -from barbican.openstack.common import gettextutils as u - - -CONF = cfg.CONF -DEFAULT_PLUGIN_NAMESPACE = 'barbican.crypto.plugin' -DEFAULT_PLUGINS = ['simple_crypto'] - -crypto_opt_group = cfg.OptGroup(name='crypto', - title='Crypto Plugin Options') -crypto_opts = [ - cfg.StrOpt('namespace', - default=DEFAULT_PLUGIN_NAMESPACE, - help=u._('Extension namespace to search for plugins.') - ), - cfg.MultiStrOpt('enabled_crypto_plugins', - default=DEFAULT_PLUGINS, - help=u._('List of crypto plugins to load.') - ) -] -CONF.register_group(crypto_opt_group) -CONF.register_opts(crypto_opts, group=crypto_opt_group) - - -class CryptoContentTypeNotSupportedException(exception.BarbicanException): - """Raised when support for payload content type is not available.""" - def __init__(self, content_type): - super(CryptoContentTypeNotSupportedException, self).__init__( - u._("Crypto Content Type " - "of '{0}' not supported").format(content_type) - ) - self.content_type = content_type - - -class CryptoContentEncodingNotSupportedException(exception.BarbicanException): - """Raised when support for payload content encoding is not available.""" - def __init__(self, content_encoding): - super(CryptoContentEncodingNotSupportedException, self).__init__( - u._("Crypto Content-Encoding of '{0}' not supported").format( - content_encoding) - ) - self.content_encoding = content_encoding - - -class CryptoAcceptNotSupportedException(exception.BarbicanException): - """Raised when requested decrypted content-type is not available.""" - def __init__(self, accept): - super(CryptoAcceptNotSupportedException, self).__init__( - u._("Crypto Accept of '{0}' not supported").format(accept) - ) - self.accept = accept - - -class CryptoAlgorithmNotSupportedException(exception.BarbicanException): - """Raised when support for an algorithm is not available.""" - def __init__(self, algorithm): - super(CryptoAlgorithmNotSupportedException, self).__init__( - u._("Crypto algorithm of '{0}' not supported").format( - algorithm) - ) - self.algorithm = algorithm - - -class CryptoPayloadDecodingError(exception.BarbicanException): - """Raised when payload could not be decoded.""" - def __init__(self): - super(CryptoPayloadDecodingError, self).__init__( - u._("Problem decoding payload") - ) - - -class CryptoSupportedPluginNotFound(exception.BarbicanException): - """Raised when no plugins are found that support the requested - operation. - """ - message = "Crypto plugin not found for requested operation." - - -class CryptoPluginNotFound(exception.BarbicanException): - """Raised when no plugins are installed.""" - message = u._("Crypto plugin not found.") - - -class CryptoNoPayloadProvidedException(exception.BarbicanException): - """Raised when secret information is not provided.""" - def __init__(self): - super(CryptoNoPayloadProvidedException, self).__init__( - u._('No secret information provided to encrypt.') - ) - - -class CryptoNoSecretOrDataFoundException(exception.BarbicanException): - """Raised when secret information could not be located.""" - def __init__(self, secret_id): - super(CryptoNoSecretOrDataFoundException, self).__init__( - u._('No secret information located for ' - 'secret {0}').format(secret_id) - ) - self.secret_id = secret_id - - -class CryptoContentEncodingMustBeBase64(exception.BarbicanException): - """Raised when encoding must be base64.""" - def __init__(self): - super(CryptoContentEncodingMustBeBase64, self).__init__( - u._("Encoding type must be 'base64' for text-based payloads.") - ) - - -class CryptoKEKBindingException(exception.BarbicanException): - """Raised when the bind_kek_metadata method from a plugin returns None.""" - def __init__(self, plugin_name=u._('Unknown')): - super(CryptoKEKBindingException, self).__init__( - u._('Failed to bind kek metadata for ' - 'plugin: {0}').format(plugin_name) - ) - self.plugin_name = plugin_name - - -class CryptoGeneralException(exception.BarbicanException): - """Raised when a system fault has occurred.""" - def __init__(self, reason=u._('Unknown')): - super(CryptoGeneralException, self).__init__( - u._('Problem seen during crypto processing - ' - 'Reason: {0}').format(reason) - ) - self.reason = reason - - -def normalize_before_encryption(unencrypted, content_type, content_encoding, - enforce_text_only=False): - """Normalize unencrypted prior to plugin encryption processing.""" - if not unencrypted: - raise CryptoNoPayloadProvidedException() - - # Validate and normalize content-type. - normalized_mime = mime_types.normalize_content_type(content_type) - if not mime_types.is_supported(normalized_mime): - raise CryptoContentTypeNotSupportedException(content_type) - - # Process plain-text type. - if normalized_mime in mime_types.PLAIN_TEXT: - # normalize text to binary string - unencrypted = unencrypted.encode('utf-8') - - # Process binary type. - else: - # payload has to be decoded - if mime_types.is_base64_processing_needed(content_type, - content_encoding): - try: - unencrypted = base64.b64decode(unencrypted) - except TypeError: - raise CryptoPayloadDecodingError() - elif enforce_text_only: - # For text-based protocols (such as the one-step secret POST), - # only 'base64' encoding is possible/supported. - raise CryptoContentEncodingMustBeBase64() - elif content_encoding: - # Unsupported content-encoding request. - raise CryptoContentEncodingNotSupportedException(content_encoding) - - return unencrypted, normalized_mime - - -def analyze_before_decryption(content_type): - """Determine support for desired content type.""" - if not mime_types.is_supported(content_type): - raise CryptoAcceptNotSupportedException(content_type) - - -def denormalize_after_decryption(unencrypted, content_type): - """Translate the decrypted data into the desired content type.""" - # Process plain-text type. - if content_type in mime_types.PLAIN_TEXT: - # normalize text to binary string - try: - unencrypted = unencrypted.decode('utf-8') - except UnicodeDecodeError: - raise CryptoAcceptNotSupportedException(content_type) - - # Process binary type. - elif content_type not in mime_types.BINARY: - raise CryptoGeneralException( - u._("Unexpected content-type: '{0}'").format(content_type)) - - return unencrypted - - -class CryptoExtensionManager(named.NamedExtensionManager): - def __init__(self, conf=CONF, invoke_on_load=True, - invoke_args=(), invoke_kwargs={}): - super(CryptoExtensionManager, self).__init__( - conf.crypto.namespace, - conf.crypto.enabled_crypto_plugins, - invoke_on_load=invoke_on_load, - invoke_args=invoke_args, - invoke_kwds=invoke_kwargs - ) - - def encrypt(self, unencrypted, content_type, content_encoding, - secret, tenant, kek_repo, enforce_text_only=False): - """Delegates encryption to first plugin that supports it.""" - - if len(self.extensions) < 1: - raise CryptoPluginNotFound() - - for ext in self.extensions: - if ext.obj.supports(plugin_mod.PluginSupportTypes.ENCRYPT_DECRYPT): - encrypting_plugin = ext.obj - break - else: - raise CryptoSupportedPluginNotFound() - - unencrypted, content_type = normalize_before_encryption( - unencrypted, content_type, content_encoding, - enforce_text_only=enforce_text_only) - - # Find or create a key encryption key metadata. - kek_datum, kek_meta_dto = self._find_or_create_kek_objects( - encrypting_plugin, tenant, kek_repo) - - encrypt_dto = plugin_mod.EncryptDTO(unencrypted) - # Create an encrypted datum instance and add the encrypted cypher text. - datum = models.EncryptedDatum(secret, kek_datum) - datum.content_type = content_type - response_dto = encrypting_plugin.encrypt( - encrypt_dto, kek_meta_dto, tenant.keystone_id - ) - - datum.cypher_text = response_dto.cypher_text - datum.kek_meta_extended = response_dto.kek_meta_extended - - # Convert binary data into a text-based format. - #TODO(jwood) Figure out by storing binary (BYTEA) data in Postgres - # isn't working. - datum.cypher_text = base64.b64encode(datum.cypher_text) - - return datum - - def decrypt(self, content_type, secret, tenant): - """Delegates decryption to active plugins.""" - - if not secret or not secret.encrypted_data: - raise CryptoNoSecretOrDataFoundException(secret.id) - - analyze_before_decryption(content_type) - - for ext in self.extensions: - decrypting_plugin = ext.obj - for datum in secret.encrypted_data: - if self._plugin_supports(decrypting_plugin, - datum.kek_meta_tenant): - # wrap the KEKDatum instance in our DTO - kek_meta_dto = plugin_mod.KEKMetaDTO(datum.kek_meta_tenant) - - # Convert from text-based storage format to binary. - #TODO(jwood) Figure out by storing binary (BYTEA) data in - # Postgres isn't working. - encrypted = base64.b64decode(datum.cypher_text) - decrypt_dto = plugin_mod.DecryptDTO(encrypted) - - # Decrypt the secret. - unencrypted = decrypting_plugin \ - .decrypt(decrypt_dto, - kek_meta_dto, - datum.kek_meta_extended, - tenant.keystone_id) - - # Denormalize the decrypted info per request. - return denormalize_after_decryption(unencrypted, - content_type) - else: - raise CryptoPluginNotFound() - - def generate_symmetric_encryption_key(self, secret, content_type, tenant, - kek_repo): - """Delegates generating a key to the first supported plugin. - - Note that this key can be used by clients for their encryption - processes. This generated key is then be encrypted via - the plug-in key encryption process, and that encrypted datum - is then returned from this method. - """ - encrypting_plugin = \ - self._determine_crypto_plugin(secret.algorithm, - secret.bit_length, - secret.mode) - - kek_datum, kek_meta_dto = self._find_or_create_kek_objects( - encrypting_plugin, tenant, kek_repo) - - # Create an encrypted datum instance and add the created cypher text. - datum = models.EncryptedDatum(secret, kek_datum) - datum.content_type = content_type - - generate_dto = plugin_mod.GenerateDTO(secret.algorithm, - secret.bit_length, - secret.mode, None) - # Create the encrypted meta. - response_dto = encrypting_plugin.generate_symmetric(generate_dto, - kek_meta_dto, - tenant.keystone_id) - - # Convert binary data into a text-based format. - # TODO(jwood) Figure out by storing binary (BYTEA) data in Postgres - # isn't working. - datum.cypher_text = base64.b64encode(response_dto.cypher_text) - datum.kek_meta_extended = response_dto.kek_meta_extended - return datum - - def generate_asymmetric_encryption_keys(self, meta, content_type, tenant, - kek_repo): - """Delegates generating asymmteric keys to the first - supported plugin based on `meta`. meta will provide extra - information to help key generation. - Based on passpharse in meta this method will return a tuple - with two/three objects. - - Note that this key can be used by clients for their encryption - processes. This generated key is then be encrypted via - the plug-in key encryption process, and that encrypted datum - is then returned from this method. - """ - encrypting_plugin = \ - self._determine_crypto_plugin(meta.algorithm, - meta.bit_length, - meta.passphrase) - - kek_datum, kek_meta_dto = self._find_or_create_kek_objects( - encrypting_plugin, tenant, kek_repo) - - generate_dto = plugin_mod.GenerateDTO(meta.algorithm, - meta.bit_length, - None, meta.passphrase) - # generate the secret. - private_key_dto, public_key_dto, passwd_dto = \ - encrypting_plugin.generate_asymmetric( - generate_dto, - kek_meta_dto, - tenant.keystone_id) - - # Create an encrypted datum instances for each secret type - # and add the created cypher text. - priv_datum = models.EncryptedDatum(None, kek_datum) - priv_datum.content_type = content_type - priv_datum.cypher_text = base64.b64encode(private_key_dto.cypher_text) - priv_datum.kek_meta_extended = private_key_dto.kek_meta_extended - - public_datum = models.EncryptedDatum(None, kek_datum) - public_datum.content_type = content_type - public_datum.cypher_text = base64.b64encode(public_key_dto.cypher_text) - public_datum.kek_meta_extended = public_key_dto.kek_meta_extended - - passwd_datum = None - if passwd_dto: - passwd_datum = models.EncryptedDatum(None, kek_datum) - passwd_datum.content_type = content_type - passwd_datum.cypher_text = base64.b64encode(passwd_dto.cypher_text) - passwd_datum.kek_meta_extended = \ - passwd_dto.kek_meta_extended - - return (priv_datum, public_datum, passwd_datum) - - def _determine_type(self, algorithm): - """Determines the type (symmetric and asymmetric for now) - based on algorithm - """ - symmetric_algs = plugin_mod.PluginSupportTypes.SYMMETRIC_ALGORITHMS - asymmetric_algs = plugin_mod.PluginSupportTypes.ASYMMETRIC_ALGORITHMS - if algorithm.lower() in symmetric_algs: - return plugin_mod.PluginSupportTypes.SYMMETRIC_KEY_GENERATION - elif algorithm.lower() in asymmetric_algs: - return plugin_mod.PluginSupportTypes.ASYMMETRIC_KEY_GENERATION - else: - raise CryptoAlgorithmNotSupportedException(algorithm) - - #TODO(atiwari): Use meta object instead of individual attribute - #This has to be done while integration rest resources - def _determine_crypto_plugin(self, algorithm, bit_length=None, - mode=None): - """Determines the generation type and encrypting plug-in - which supports the generation of secret based on - generation type - """ - if len(self.extensions) < 1: - raise CryptoPluginNotFound() - - generation_type = self._determine_type(algorithm) - for ext in self.extensions: - if ext.obj.supports(generation_type, algorithm, - bit_length, - mode): - encrypting_plugin = ext.obj - break - else: - raise CryptoSupportedPluginNotFound() - - return encrypting_plugin - - def _plugin_supports(self, plugin_inst, kek_metadata_tenant): - """Tests for plugin support. - - Tests if the supplied plugin supports operations on the supplied - key encryption key (KEK) metadata. - - :param plugin_inst: The plugin instance to test. - :param kek_metadata: The KEK metadata to test. - :return: True if the plugin can support operations on the KEK metadata. - - """ - plugin_name = utils.generate_fullname_for(plugin_inst) - return plugin_name == kek_metadata_tenant.plugin_name - - def _find_or_create_kek_objects(self, plugin_inst, tenant, kek_repo): - # Find or create a key encryption key. - full_plugin_name = utils.generate_fullname_for(plugin_inst) - kek_datum = kek_repo.find_or_create_kek_datum(tenant, - full_plugin_name) - - # Bind to the plugin's key management. - # TODO(jwood): Does this need to be in a critical section? Should the - # bind operation just be declared idempotent in the plugin contract? - kek_meta_dto = plugin_mod.KEKMetaDTO(kek_datum) - if not kek_datum.bind_completed: - kek_meta_dto = plugin_inst.bind_kek_metadata(kek_meta_dto) - - # By contract, enforce that plugins return a - # (typically modified) DTO. - if kek_meta_dto is None: - raise CryptoKEKBindingException(full_plugin_name) - - plugin_mod.indicate_bind_completed(kek_meta_dto, kek_datum) - kek_repo.save(kek_datum) - - return kek_datum, kek_meta_dto diff --git a/barbican/crypto/p11_crypto.py b/barbican/crypto/p11_crypto.py deleted file mode 100644 index 035f19075..000000000 --- a/barbican/crypto/p11_crypto.py +++ /dev/null @@ -1,192 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -try: - import PyKCS11 -except ImportError: - PyKCS11 = {} # TODO(reaperhulk): remove testing workaround - - -import base64 - -from oslo.config import cfg - -from barbican.common import exception -from barbican.crypto import plugin - -from barbican.openstack.common import gettextutils as u -from barbican.openstack.common import jsonutils as json - - -CONF = cfg.CONF - -p11_crypto_plugin_group = cfg.OptGroup(name='p11_crypto_plugin', - title="PKCS11 Crypto Plugin Options") -p11_crypto_plugin_opts = [ - cfg.StrOpt('library_path', - help=u._('Path to vendor PKCS11 library')), - cfg.StrOpt('login', - help=u._('Password to login to PKCS11 session')) -] -CONF.register_group(p11_crypto_plugin_group) -CONF.register_opts(p11_crypto_plugin_opts, group=p11_crypto_plugin_group) - - -class P11CryptoPluginKeyException(exception.BarbicanException): - message = u._("More than one key found for label") - - -class P11CryptoPluginException(exception.BarbicanException): - message = u._("General exception") - - -class P11CryptoPlugin(plugin.CryptoPluginBase): - """PKCS11 supporting implementation of the crypto plugin. - Generates a key per tenant and encrypts using AES-256-GCM. - This implementation currently relies on an unreleased fork of PyKCS11. - """ - - def __init__(self, conf=cfg.CONF): - self.block_size = 16 # in bytes - self.kek_key_length = 32 # in bytes (256-bit) - self.algorithm = 0x8000011c # CKM_AES_GCM vendor prefixed. - self.pkcs11 = PyKCS11.PyKCS11Lib() - if conf.p11_crypto_plugin.library_path is None: - raise ValueError(u._("library_path is required")) - else: - self.pkcs11.load(conf.p11_crypto_plugin.library_path) - # initialize the library. PyKCS11 does not supply this for free - self._check_error(self.pkcs11.lib.C_Initialize()) - self.session = self.pkcs11.openSession(1) - self.session.login(conf.p11_crypto_plugin.login) - self.rw_session = self.pkcs11.openSession(1, PyKCS11.CKF_RW_SESSION) - self.rw_session.login(conf.p11_crypto_plugin.login) - - def _check_error(self, value): - if value != PyKCS11.CKR_OK: - raise PyKCS11.PyKCS11Error(value) - - def _get_key_by_label(self, key_label): - template = ( - (PyKCS11.CKA_CLASS, PyKCS11.CKO_SECRET_KEY), - (PyKCS11.CKA_KEY_TYPE, PyKCS11.CKK_AES), - (PyKCS11.CKA_LABEL, key_label)) - keys = self.session.findObjects(template) - if len(keys) == 1: - return keys[0] - elif len(keys) == 0: - return None - else: - raise P11CryptoPluginKeyException() - - def _generate_iv(self): - iv = self.session.generateRandom(self.block_size) - iv = b''.join(chr(i) for i in iv) - if len(iv) != self.block_size: - raise P11CryptoPluginException() - return iv - - def _build_gcm_params(self, iv): - gcm = PyKCS11.LowLevel.CK_AES_GCM_PARAMS() - gcm.pIv = iv - gcm.ulIvLen = len(iv) - gcm.ulIvBits = len(iv) * 8 - gcm.ulTagBits = 128 - return gcm - - def _generate_kek(self, kek_label): - # TODO(reaperhulk): review template to ensure it's what we want - template = ( - (PyKCS11.CKA_CLASS, PyKCS11.CKO_SECRET_KEY), - (PyKCS11.CKA_KEY_TYPE, PyKCS11.CKK_AES), - (PyKCS11.CKA_VALUE_LEN, self.kek_key_length), - (PyKCS11.CKA_LABEL, kek_label), - (PyKCS11.CKA_PRIVATE, True), - (PyKCS11.CKA_SENSITIVE, True), - (PyKCS11.CKA_ENCRYPT, True), - (PyKCS11.CKA_DECRYPT, True), - (PyKCS11.CKA_TOKEN, True), - (PyKCS11.CKA_WRAP, True), - (PyKCS11.CKA_UNWRAP, True), - (PyKCS11.CKA_EXTRACTABLE, False)) - ckattr = self.session._template2ckattrlist(template) - - m = PyKCS11.LowLevel.CK_MECHANISM() - m.mechanism = PyKCS11.LowLevel.CKM_AES_KEY_GEN - - key = PyKCS11.LowLevel.CK_OBJECT_HANDLE() - self._check_error( - self.pkcs11.lib.C_GenerateKey( - self.rw_session.session, - m, - ckattr, - key - ) - ) - - def encrypt(self, encrypt_dto, kek_meta_dto, keystone_id): - key = self._get_key_by_label(kek_meta_dto.kek_label) - iv = self._generate_iv() - gcm = self._build_gcm_params(iv) - mech = PyKCS11.Mechanism(self.algorithm, gcm) - encrypted = self.session.encrypt(key, encrypt_dto.unencrypted, mech) - cyphertext = b''.join(chr(i) for i in encrypted) - kek_meta_extended = json.dumps({ - 'iv': base64.b64encode(iv) - }) - - return plugin.ResponseDTO(cyphertext, kek_meta_extended) - - def decrypt(self, decrypt_dto, kek_meta_dto, kek_meta_extended, - keystone_id): - key = self._get_key_by_label(kek_meta_dto.kek_label) - meta_extended = json.loads(kek_meta_extended) - iv = base64.b64decode(meta_extended['iv']) - gcm = self._build_gcm_params(iv) - mech = PyKCS11.Mechanism(self.algorithm, gcm) - decrypted = self.session.decrypt(key, decrypt_dto.encrypted, mech) - secret = b''.join(chr(i) for i in decrypted) - return secret - - def bind_kek_metadata(self, kek_meta_dto): - # Enforce idempotency: If we've already generated a key for the - # kek_label, leave now. - key = self._get_key_by_label(kek_meta_dto.kek_label) - if not key: - self._generate_kek(kek_meta_dto.kek_label) - # To be persisted by Barbican: - kek_meta_dto.algorithm = 'AES' - kek_meta_dto.bit_length = self.kek_key_length * 8 - kek_meta_dto.mode = 'GCM' - kek_meta_dto.plugin_meta = None - - return kek_meta_dto - - def generate_symmetric(self, generate_dto, kek_meta_dto, keystone_id): - byte_length = generate_dto.bit_length / 8 - rand = self.session.generateRandom(byte_length) - if len(rand) != byte_length: - raise P11CryptoPluginException() - return self.encrypt(plugin.EncryptDTO(rand), kek_meta_dto, keystone_id) - - def generate_asymmetric(self, generate_dto, kek_meta_dto, keystone_id): - raise NotImplementedError("Feature not implemented for PKCS11") - - def supports(self, type_enum, algorithm=None, bit_length=None, mode=None): - if type_enum == plugin.PluginSupportTypes.ENCRYPT_DECRYPT: - return True - elif type_enum == plugin.PluginSupportTypes.SYMMETRIC_KEY_GENERATION: - return True - elif type_enum == plugin.PluginSupportTypes.ASYMMETRIC_KEY_GENERATION: - return False - else: - return False diff --git a/barbican/crypto/plugin.py b/barbican/crypto/plugin.py deleted file mode 100644 index 2343d5f38..000000000 --- a/barbican/crypto/plugin.py +++ /dev/null @@ -1,528 +0,0 @@ -# Copyright (c) 2013-2014 Rackspace, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import abc -import os - -from Crypto.PublicKey import DSA -from Crypto.PublicKey import RSA -from Crypto.Util import asn1 -from cryptography import fernet - -from oslo.config import cfg - -import six - -from barbican.common import utils -from barbican.openstack.common import gettextutils as u - -LOG = utils.getLogger(__name__) - -CONF = cfg.CONF - -simple_crypto_plugin_group = cfg.OptGroup(name='simple_crypto_plugin', - title="Simple Crypto Plugin Options") -simple_crypto_plugin_opts = [ - cfg.StrOpt('kek', - default=b'dGhpcnR5X3R3b19ieXRlX2tleWJsYWhibGFoYmxhaGg=', - help=u._('Key encryption key to be used by Simple Crypto ' - 'Plugin')) -] -CONF.register_group(simple_crypto_plugin_group) -CONF.register_opts(simple_crypto_plugin_opts, group=simple_crypto_plugin_group) - - -class PluginSupportTypes(object): - """Class to hold the type enumeration that plugins may support.""" - ENCRYPT_DECRYPT = "ENCRYPT_DECRYPT" - SYMMETRIC_KEY_GENERATION = "SYMMETRIC_KEY_GENERATION" - # A list of symmetric algorithms that are used to determine type of key gen - SYMMETRIC_ALGORITHMS = ['aes', 'des', '3des', 'hmacsha1', - 'hmacsha256', 'hmacsha384', 'hmacsha512'] - SYMMETRIC_KEY_LENGTHS = [64, 128, 192, 256] - - ASYMMETRIC_KEY_GENERATION = "ASYMMETRIC_KEY_GENERATION" - ASYMMETRIC_ALGORITHMS = ['rsa', 'dsa'] - ASYMMETRIC_KEY_LENGTHS = [1024, 2048, 4096] - - -class KEKMetaDTO(object): - """Key Encryption Keys (KEKs) in Barbican are intended to represent a - distinct key that is used to perform encryption on secrets for a particular - project (tenant). - - ``KEKMetaDTO`` objects are provided to cryptographic backends by Barbican - to allow plugins to persist metadata related to the project's (tenant's) - KEK. - - For example, a plugin that interfaces with a Hardware Security Module (HSM) - may want to use a different encryption key for each tenant. Such a plugin - could use the ``KEKMetaDTO`` object to save the key ID used for that - tenant. Barbican will persist the KEK metadata and ensure that it is - provided to the plugin every time a request from that same tenant is - processed. - - .. attribute:: plugin_name - - String attribute used by Barbican to identify the plugin that is bound - to the KEK metadata. Plugins should not change this attribute. - - .. attribute:: kek_label - - String attribute used to label the project's (tenant's) KEK by the - plugin. The value of this attribute should be meaningful to the - plugin. Barbican does not use this value. - - .. attribute:: algorithm - - String attribute used to identify the encryption algorithm used by the - plugin. e.g. "AES", "3DES", etc. This value should be meaningful to - the plugin. Barbican does not use this value. - - .. attribute:: mode - - String attribute used to identify the algorithm mode used by the - plugin. e.g. "CBC", "GCM", etc. This value should be meaningful to - the plugin. Barbican does not use this value. - - .. attribute:: bit_length - - Integer attribute used to identify the bit length of the KEK by the - plugin. This value should be meaningful to the plugin. Barbican does - not use this value. - - .. attribute:: plugin_meta - - String attribute used to persist any additional metadata that does not - fit in any other attribute. The value of this attribute is defined by - the plugin. It could be used to store external system references, such - as Key IDs in an HSM, URIs to an external service, or any other data - that the plugin deems necessary to persist. Because this is just a - plain text field, a plug in may even choose to persist data such as key - value pairs in a JSON object. - """ - - def __init__(self, kek_datum): - """kek_datum is typically a barbican.model.models.EncryptedDatum - instance. Plugins should never have to create their own instance of - this class. - """ - self.kek_label = kek_datum.kek_label - self.plugin_name = kek_datum.plugin_name - self.algorithm = kek_datum.algorithm - self.bit_length = kek_datum.bit_length - self.mode = kek_datum.mode - self.plugin_meta = kek_datum.plugin_meta - - -class GenerateDTO(object): - """Data Transfer Object used to pass all the necessary data for the plugin - to generate a secret on behalf of the user. - - .. attribute:: generation_type - - String attribute used to identify the type of secret that should be - generated. This will be either ``"symmetric"`` or ``"asymmetric"``. - - .. attribute:: algorithm - - String attribute used to specify what type of algorithm the secret will - be used for. e.g. ``"AES"`` for a ``"symmetric"`` type, or ``"RSA"`` - for ``"asymmetric"``. - - .. attribute:: mode - - String attribute used to specify what algorithm mode the secret will be - used for. e.g. ``"CBC"`` for ``"AES"`` algorithm. - - .. attribute:: bit_length - - Integer attribute used to specify the bit length of the secret. For - example, this attribute could specify the key length for an encryption - key to be used in AES-CBC. - """ - - def __init__(self, algorithm, bit_length, mode, passphrase=None): - self.algorithm = algorithm - self.bit_length = bit_length - self.mode = mode - self.passphrase = passphrase - - -class ResponseDTO(object): - """Data transfer object for secret generation response.""" - - def __init__(self, cypher_text, kek_meta_extended=None): - self.cypher_text = cypher_text - self.kek_meta_extended = kek_meta_extended - - -class DecryptDTO(object): - """Data Transfer Object used to pass all the necessary data for the plugin - to perform decryption of a secret. - - Currently, this DTO only contains the data produced by the plugin during - encryption, but in the future this DTO will contain more information, such - as a transport key for secret wrapping back to the client. - - .. attribute:: encrypted - - The data that was produced by the plugin during encryption. For some - plugins this will be the actual bytes that need to be decrypted to - produce the secret. In other implementations, this may just be a - reference to some external system that can produce the unencrypted - secret. - """ - - def __init__(self, encrypted): - self.encrypted = encrypted - - -class EncryptDTO(object): - """Data Transfer Object used to pass all the necessary data for the plugin - to perform encryption of a secret. - - Currently, this DTO only contains the raw bytes to be encrypted by the - plugin, but in the future this may contain more information. - - .. attribute:: unencrypted - - The secret data in Bytes to be encrypted by the plugin. - """ - - def __init__(self, unencrypted): - self.unencrypted = unencrypted - - -def indicate_bind_completed(kek_meta_dto, kek_datum): - """Updates the supplied kek_datum instance per the contents of the supplied - kek_meta_dto instance. This function is typically used once plugins have - had a chance to bind kek_meta_dto to their crypto systems. - - :param kek_meta_dto: - :param kek_datum: - :return: None - - """ - kek_datum.bind_completed = True - kek_datum.algorithm = kek_meta_dto.algorithm - kek_datum.bit_length = kek_meta_dto.bit_length - kek_datum.mode = kek_meta_dto.mode - kek_datum.plugin_meta = kek_meta_dto.plugin_meta - - -@six.add_metaclass(abc.ABCMeta) -class CryptoPluginBase(object): - """Base class for all Crypto plugins. Implementations of this abstract - base class will be used by Barbican to perform cryptographic operations on - secrets. - - Barbican requests operations by invoking the methods on an instance of the - implementing class. Barbican's plugin manager handles the life-cycle of - the Data Transfer Objects (DTOs) that are passed into these methods, and - persist the data that is assigned to these DTOs by the plugin. - """ - - @abc.abstractmethod - def encrypt(self, encrypt_dto, kek_meta_dto, keystone_id): - """This method will be called by Barbican when requesting an encryption - operation on a secret on behalf of a project (tenant). - - :param encrypt_dto: :class:`EncryptDTO` instance containing the raw - secret byte data to be encrypted. - :type encrypt_dto: :class:`EncryptDTO` - :param kek_meta_dto: :class:`KEKMetaDTO` instance containing - information about the project's (tenant's) Key Encryption Key (KEK) - to be used for encryption. Plugins may assume that binding via - :meth:`bind_kek_metadata` has already taken place before this - instance is passed in. - :type kek_meta_dto: :class:`KEKMetaDTO` - :param keystone_id: Project (tenant) ID associated with the unencrypted - data. - :return: A tuple containing two items ``(ciphertext, - kek_metadata_extended)``. In a typical plugin implementation, the - first item in the tuple should be the ciphertext byte data - resulting from the encryption of the secret data. The second item - is an optional String object to be persisted alongside the - ciphertext. - - Barbican guarantees that both the ``ciphertext`` and - ``kek_metadata_extended`` will be persisted and then given back to - the plugin when requesting a decryption operation. - - It should be noted that Barbican does not require that the data - returned for the ``ciphertext`` be the actual encrypted - bytes of the secret data. The only requirement is that the plugin - is able to use whatever data it chooses to return in ``ciphertext`` - to produce the secret data during decryption. This allows more - complex plugins to make decisions regarding the storage of the - encrypted data. For example, the DogTag plugin stores the - encrypted bytes in an external system and uses Barbican to store an - identifier to the external system in ``ciphertext``. During - decryption, Barbican gives the external identifier back to the - DogTag plugin, and then the plugin is able to use the identifier to - retrieve the secret data from the external storage system. - - ``kek_metadata_extended`` takes the idea of Key Encryption Key - (KEK) metadata further by giving plugins the option to store - secret-level KEK metadata. One example of using secret-level KEK - metadata would be plugins that want to use a unique KEK for every - secret that is encrypted. Such a plugin could use - ``kek_metadata_extended`` to store the Key ID for the KEK used to - encrypt this particular secret. - :rtype: tuple - """ - raise NotImplementedError # pragma: no cover - - @abc.abstractmethod - def decrypt(self, decrypt_dto, kek_meta_dto, kek_meta_extended, - keystone_id): - """Decrypt encrypted_datum in the context of the provided tenant. - - :param decrypt_dto: data transfer object containing the cyphertext - to be decrypted. - :param kek_meta_dto: Key encryption key metadata to use for decryption - :param kek_meta_extended: Optional per-secret KEK metadata to use for - decryption. - :param keystone_id: keystone_id associated with the encrypted datum. - :returns: str -- unencrypted byte data - - """ - raise NotImplementedError # pragma: no cover - - @abc.abstractmethod - def bind_kek_metadata(self, kek_meta_dto): - """Bind a key encryption key (KEK) metadata to the sub-system - handling encryption/decryption, updating information about the - key encryption key (KEK) metadata in the supplied 'kek_metadata' - data-transfer-object instance, and then returning this instance. - - This method is invoked prior to the encrypt() method above. - Implementors should fill out the supplied 'kek_meta_dto' instance - (an instance of KEKMetadata above) as needed to completely describe - the kek metadata and to complete the binding process. Barbican will - persist the contents of this instance once this method returns. - - :param kek_meta_dto: Key encryption key metadata to bind, with the - 'kek_label' attribute guaranteed to be unique, and the - and 'plugin_name' attribute already configured. - :returns: kek_meta_dto: Returns the specified DTO, after - modifications. - """ - raise NotImplementedError # pragma: no cover - - @abc.abstractmethod - def generate_symmetric(self, generate_dto, kek_meta_dto, keystone_id): - """Generate a new key. - - :param generate_dto: data transfer object for the record - associated with this generation request. Some relevant - parameters can be extracted from this object, including - bit_length, algorithm and mode - :param kek_meta_dto: Key encryption key metadata to use for decryption - :param keystone_id: keystone_id associated with the data. - :returns: An object of type ResponseDTO containing encrypted data and - kek_meta_extended, the former the resultant cypher text, the latter - being optional per-secret metadata needed to decrypt (over and above - the per-tenant metadata managed outside of the plugins) - """ - raise NotImplementedError # pragma: no cover - - @abc.abstractmethod - def generate_asymmetric(self, generate_dto, - kek_meta_dto, keystone_id): - """Create a new asymmetric key. - - :param generate_dto: data transfer object for the record - associated with this generation request. Some relevant - parameters can be extracted from this object, including - bit_length, algorithm and passphrase - :param kek_meta_dto: Key encryption key metadata to use for decryption - :param keystone_id: keystone_id associated with the data. - :returns: A tuple containing objects for private_key, public_key and - optionally one for passphrase. The objects will be of type ResponseDTO. - Each object containing encrypted data and kek_meta_extended, the former - the resultant cypher text, the latter being optional per-secret - metadata needed to decrypt (over and above the per-tenant metadata - managed outside of the plugins) - """ - raise NotImplementedError # pragma: no cover - - @abc.abstractmethod - def supports(self, type_enum, algorithm=None, bit_length=None, - mode=None): - """Used to determine if the plugin supports the requested operation. - - :param type_enum: Enumeration from PluginSupportsType class - :param algorithm: String algorithm name if needed - """ - raise NotImplementedError # pragma: no cover - - -class SimpleCryptoPlugin(CryptoPluginBase): - """Insecure implementation of the crypto plugin.""" - - def __init__(self, conf=CONF): - self.master_kek = conf.simple_crypto_plugin.kek - - def _get_kek(self, kek_meta_dto): - if not kek_meta_dto.plugin_meta: - raise ValueError('KEK not yet created.') - # the kek is stored encrypted. Need to decrypt. - encryptor = fernet.Fernet(self.master_kek) - # Note : If plugin_meta type is unicode, encode to byte. - if isinstance(kek_meta_dto.plugin_meta, six.text_type): - kek_meta_dto.plugin_meta = kek_meta_dto.plugin_meta.encode('utf-8') - - return encryptor.decrypt(kek_meta_dto.plugin_meta) - - def encrypt(self, encrypt_dto, kek_meta_dto, keystone_id): - kek = self._get_kek(kek_meta_dto) - unencrypted = encrypt_dto.unencrypted - if not isinstance(unencrypted, str): - raise ValueError('Unencrypted data must be a byte type, ' - 'but was {0}'.format(type(unencrypted))) - encryptor = fernet.Fernet(kek) - cyphertext = encryptor.encrypt(unencrypted) - return ResponseDTO(cyphertext, None) - - def decrypt(self, encrypted_dto, kek_meta_dto, kek_meta_extended, - keystone_id): - kek = self._get_kek(kek_meta_dto) - encrypted = encrypted_dto.encrypted - decryptor = fernet.Fernet(kek) - return decryptor.decrypt(encrypted) - - def bind_kek_metadata(self, kek_meta_dto): - kek_meta_dto.algorithm = 'aes' - kek_meta_dto.bit_length = 128 - kek_meta_dto.mode = 'cbc' - if not kek_meta_dto.plugin_meta: - # the kek is stored encrypted in the plugin_meta field - encryptor = fernet.Fernet(self.master_kek) - key = fernet.Fernet.generate_key() - kek_meta_dto.plugin_meta = encryptor.encrypt(key) - return kek_meta_dto - - def generate_symmetric(self, generate_dto, kek_meta_dto, keystone_id): - byte_length = int(generate_dto.bit_length) / 8 - unencrypted = os.urandom(byte_length) - - return self.encrypt(EncryptDTO(unencrypted), - kek_meta_dto, - keystone_id) - - def generate_asymmetric(self, generate_dto, kek_meta_dto, keystone_id): - """Generate asymmetric keys based on below rule - - RSA, with passphrase (supported) - - RSA, without passphrase (supported) - - DSA, without passphrase (supported) - - DSA, with passphrase (not supported) - - Note: PyCrypto is not capable of serializing DSA - keys and DER formated keys. Such keys will be - serialized to Base64 PEM to store in DB. - - TODO (atiwari/reaperhulk): PyCrypto is not capable to serialize - DSA keys and DER formated keys, later we need to pick better - crypto lib. - """ - if generate_dto.algorithm is None\ - or generate_dto.algorithm.lower() == 'rsa': - private_key = RSA.generate( - generate_dto.bit_length, None, None, 65537) - elif generate_dto.algorithm.lower() == 'dsa': - private_key = DSA.generate(generate_dto.bit_length, None, None) - - public_key = private_key.publickey() - - # Note (atiwari): key wrapping format PEM only supported - if generate_dto.algorithm.lower() == 'rsa': - public_key, private_key = self._wrap_key(public_key, private_key, - generate_dto.passphrase) - if generate_dto.algorithm.lower() == 'dsa': - if generate_dto.passphrase: - raise ValueError('Passphrase not supported for DSA key') - public_key, private_key = self._serialize_dsa_key(public_key, - private_key) - private_dto = self.encrypt(EncryptDTO(private_key), - kek_meta_dto, - keystone_id) - - public_dto = self.encrypt(EncryptDTO(public_key), - kek_meta_dto, - keystone_id) - - passphrase_dto = None - if generate_dto.passphrase: - passphrase_dto = self.encrypt(EncryptDTO(generate_dto.passphrase), - kek_meta_dto, - keystone_id) - - return private_dto, public_dto, passphrase_dto - - def supports(self, type_enum, algorithm=None, bit_length=None, - mode=None): - if type_enum == PluginSupportTypes.ENCRYPT_DECRYPT: - return True - - if type_enum == PluginSupportTypes.SYMMETRIC_KEY_GENERATION: - return self._is_algorithm_supported(algorithm, - bit_length) - elif type_enum == PluginSupportTypes.ASYMMETRIC_KEY_GENERATION: - return self._is_algorithm_supported(algorithm, - bit_length) - else: - return False - - def _wrap_key(self, public_key, private_key, - passphrase): - pkcs = 8 - key_wrap_format = 'PEM' - - private_key = private_key.exportKey(key_wrap_format, passphrase, pkcs) - public_key = public_key.exportKey() - - return (public_key, private_key) - - def _serialize_dsa_key(self, public_key, private_key): - - pub_seq = asn1.DerSequence() - pub_seq[:] = [0, public_key.p, public_key.q, - public_key.g, public_key.y] - public_key = "-----BEGIN DSA PUBLIC KEY-----\n%s"\ - "-----END DSA PUBLIC KEY-----" % pub_seq.encode().encode("base64") - - prv_seq = asn1.DerSequence() - prv_seq[:] = [0, private_key.p, private_key.q, - private_key.g, private_key.y, private_key.x] - private_key = "-----BEGIN DSA PRIVATE KEY-----\n%s"\ - "-----END DSA PRIVATE KEY-----" % prv_seq.encode().encode("base64") - - return (public_key, private_key) - - def _is_algorithm_supported(self, algorithm=None, bit_length=None): - """check if algorithm and bit_length combination is supported.""" - if algorithm is None or bit_length is None: - return False - - if algorithm.lower() in PluginSupportTypes.SYMMETRIC_ALGORITHMS \ - and bit_length in PluginSupportTypes.SYMMETRIC_KEY_LENGTHS: - return True - elif algorithm.lower() in PluginSupportTypes.ASYMMETRIC_ALGORITHMS \ - and bit_length in PluginSupportTypes.ASYMMETRIC_KEY_LENGTHS: - return True - else: - return False diff --git a/barbican/model/models.py b/barbican/model/models.py index 21cdf3812..d45553080 100644 --- a/barbican/model/models.py +++ b/barbican/model/models.py @@ -20,6 +20,7 @@ import sqlalchemy as sa from sqlalchemy.ext import compiler from sqlalchemy.ext import declarative from sqlalchemy import orm +from sqlalchemy.orm import collections as col from sqlalchemy import types as sql_types from barbican.common import exception @@ -242,8 +243,12 @@ class Secret(BASE, ModelBase): # metadata is retrieved. # See barbican.api.resources.py::SecretsResource.on_get() encrypted_data = orm.relationship("EncryptedDatum", lazy='joined') - secret_store_metadata = orm.relationship("SecretStoreMetadatum", - lazy='joined') + + secret_store_metadata = orm.\ + relationship("SecretStoreMetadatum", + collection_class=col.attribute_mapped_collection('key'), + backref="secret", + cascade="all, delete-orphan") def __init__(self, parsed_request=None): """Creates secret from a dict.""" @@ -260,14 +265,14 @@ class Secret(BASE, ModelBase): def _do_delete_children(self, session): """Sub-class hook: delete children relationships.""" - for datum in self.secret_store_metadata: - datum.delete(session) + for k, v in self.secret_store_metadata.items(): + v.delete(session) for datum in self.encrypted_data: datum.delete(session) for secret_ref in self.container_secrets: - session.delete(secret_ref) + session.delete(secret_ref) def _do_extra_dict_fields(self): """Sub-class hook method: return dict of fields.""" @@ -290,16 +295,12 @@ class SecretStoreMetadatum(BASE, ModelBase): key = sa.Column(sa.String(255), nullable=False) value = sa.Column(sa.String(255), nullable=False) - def __init__(self, secret, key, value): + def __init__(self, key, value): super(SecretStoreMetadatum, self).__init__() msg = ("Must supply non-None {0} argument " "for SecretStoreMetadatum entry.") - if secret is None: - raise exception.MissingArgumentError(msg.format("secret")) - self.secret_id = secret.id - if key is None: raise exception.MissingArgumentError(msg.format("key")) self.key = key diff --git a/barbican/model/repositories.py b/barbican/model/repositories.py index 2c8e8421b..f0962a46e 100644 --- a/barbican/model/repositories.py +++ b/barbican/model/repositories.py @@ -239,6 +239,38 @@ def clean_paging_values(offset_arg=0, limit_arg=CONF.default_limit_paging): return offset, limit +class Repositories(object): + """Convenient way to pass repositories around. + + Selecting a given repository has 3 choices: + 1) Use a specified repository instance via **kwargs + 2) Create a repository here if it is specified as None via **kwargs + 3) Just use None if no repository is specified + """ + def __init__(self, **kwargs): + if kwargs: + # Enforce that either all arguments are non-None or else all None. + test_set = set(kwargs.values()) + if None in test_set and len(test_set) > 1: + raise NotImplementedError('No support for mixing None ' + 'and non-None repository instances') + + # Only set properties for specified repositories. + self._set_repo('tenant_repo', TenantRepo, kwargs) + self._set_repo('tenant_secret_repo', TenantSecretRepo, kwargs) + self._set_repo('secret_repo', SecretRepo, kwargs) + self._set_repo('datum_repo', EncryptedDatumRepo, kwargs) + self._set_repo('kek_repo', KEKDatumRepo, kwargs) + self._set_repo('secret_meta_repo', SecretStoreMetadatumRepo, + kwargs) + self._set_repo('order_repo', OrderRepo, kwargs) + self._set_repo('transport_key_repo', TransportKeyRepo, kwargs) + + def _set_repo(self, repo_name, repo_cls, specs): + if specs and repo_name in specs: + setattr(self, repo_name, specs[repo_name] or repo_cls()) + + class BaseRepo(object): """Base repository for the barbican entities. @@ -589,6 +621,42 @@ class EncryptedDatumRepo(BaseRepo): pass +class SecretStoreMetadatumRepo(BaseRepo): + """Repository for the SecretStoreMetadatum entity (that stores key/value + information on behalf of a Secret). + """ + + def save(self, metadata, secret_model): + """Saves the the specified metadata for the secret. + + :raises NotFound if entity does not exist. + """ + now = timeutils.utcnow() + session = get_session() + with session.begin(): + for k, v in metadata.items(): + meta_model = models.SecretStoreMetadatum(k, v) + meta_model.updated_at = now + meta_model.secret = secret_model + meta_model.save(session=session) + + def _do_entity_name(self): + """Sub-class hook: return entity name, such as for debugging.""" + return "SecretStoreMetadatum" + + def _do_create_instance(self): + return models.SecretStoreMetadatum() + + def _do_build_get_query(self, entity_id, keystone_id, session): + """Sub-class hook: build a retrieve query.""" + return session.query(models.SecretStoreMetadatum).\ + filter_by(id=entity_id) + + def _do_validate(self, values): + """Sub-class hook: validate values.""" + pass + + class KEKDatumRepo(BaseRepo): """Repository for the KEKDatum entity (that stores key encryption key (KEK) metadata used by crypto plugins to encrypt/decrypt secrets). diff --git a/barbican/plugin/crypto/crypto.py b/barbican/plugin/crypto/crypto.py index b50b2652b..a63906a67 100644 --- a/barbican/plugin/crypto/crypto.py +++ b/barbican/plugin/crypto/crypto.py @@ -1,2 +1,422 @@ -#TODO(john-wood-w) Pull over current crypto package elements: plugin.py and -# lookup parts of extension_manager.py +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +import abc + +from oslo.config import cfg +from stevedore import named + +import six + +from barbican.common import exception +from barbican.common import utils +from barbican.openstack.common import gettextutils as u +from barbican.plugin.interface import secret_store + +LOG = utils.getLogger(__name__) + +CONF = cfg.CONF + +DEFAULT_PLUGIN_NAMESPACE = 'barbican.crypto.plugin' +DEFAULT_PLUGINS = ['simple_crypto'] + +crypto_opt_group = cfg.OptGroup(name='crypto', + title='Crypto Plugin Options') +crypto_opts = [ + cfg.StrOpt('namespace', + default=DEFAULT_PLUGIN_NAMESPACE, + help=u._('Extension namespace to search for plugins.') + ), + cfg.MultiStrOpt('enabled_crypto_plugins', + default=DEFAULT_PLUGINS, + help=u._('List of crypto plugins to load.') + ) +] +CONF.register_group(crypto_opt_group) +CONF.register_opts(crypto_opts, group=crypto_opt_group) + + +class CryptoPluginNotFound(exception.BarbicanException): + """Raised when no plugins are installed.""" + message = u._("Crypto plugin not found.") + + +class CryptoKEKBindingException(exception.BarbicanException): + """Raised when the bind_kek_metadata method from a plugin returns None.""" + def __init__(self, plugin_name=u._('Unknown')): + super(CryptoKEKBindingException, self).__init__( + u._('Failed to bind kek metadata for ' + 'plugin: {0}').format(plugin_name) + ) + self.plugin_name = plugin_name + + +class CryptoPrivateKeyFailureException(exception.BarbicanException): + """Raised when could not generate private key.""" + def __init__(self): + super(CryptoPrivateKeyFailureException, self).__init__( + u._('Could not generate private key') + ) + + +#TODO(john-wood-w) Need to harmonize these lower-level constants with the +# higher level constants in secret_store.py. +class PluginSupportTypes(object): + """Class to hold the type enumeration that plugins may support.""" + ENCRYPT_DECRYPT = "ENCRYPT_DECRYPT" + SYMMETRIC_KEY_GENERATION = "SYMMETRIC_KEY_GENERATION" + # A list of symmetric algorithms that are used to determine type of key gen + SYMMETRIC_ALGORITHMS = ['aes', 'des', '3des', 'hmacsha1', + 'hmacsha256', 'hmacsha384', 'hmacsha512'] + SYMMETRIC_KEY_LENGTHS = [64, 128, 192, 256] + + ASYMMETRIC_KEY_GENERATION = "ASYMMETRIC_KEY_GENERATION" + ASYMMETRIC_ALGORITHMS = ['rsa', 'dsa'] + ASYMMETRIC_KEY_LENGTHS = [1024, 2048, 4096] + + +class KEKMetaDTO(object): + """Key Encryption Keys (KEKs) in Barbican are intended to represent a + distinct key that is used to perform encryption on secrets for a particular + project (tenant). + + ``KEKMetaDTO`` objects are provided to cryptographic backends by Barbican + to allow plugins to persist metadata related to the project's (tenant's) + KEK. + + For example, a plugin that interfaces with a Hardware Security Module (HSM) + may want to use a different encryption key for each tenant. Such a plugin + could use the ``KEKMetaDTO`` object to save the key ID used for that + tenant. Barbican will persist the KEK metadata and ensure that it is + provided to the plugin every time a request from that same tenant is + processed. + + .. attribute:: plugin_name + + String attribute used by Barbican to identify the plugin that is bound + to the KEK metadata. Plugins should not change this attribute. + + .. attribute:: kek_label + + String attribute used to label the project's (tenant's) KEK by the + plugin. The value of this attribute should be meaningful to the + plugin. Barbican does not use this value. + + .. attribute:: algorithm + + String attribute used to identify the encryption algorithm used by the + plugin. e.g. "AES", "3DES", etc. This value should be meaningful to + the plugin. Barbican does not use this value. + + .. attribute:: mode + + String attribute used to identify the algorithm mode used by the + plugin. e.g. "CBC", "GCM", etc. This value should be meaningful to + the plugin. Barbican does not use this value. + + .. attribute:: bit_length + + Integer attribute used to identify the bit length of the KEK by the + plugin. This value should be meaningful to the plugin. Barbican does + not use this value. + + .. attribute:: plugin_meta + + String attribute used to persist any additional metadata that does not + fit in any other attribute. The value of this attribute is defined by + the plugin. It could be used to store external system references, such + as Key IDs in an HSM, URIs to an external service, or any other data + that the plugin deems necessary to persist. Because this is just a + plain text field, a plug in may even choose to persist data such as key + value pairs in a JSON object. + """ + + def __init__(self, kek_datum): + """kek_datum is typically a barbican.model.models.EncryptedDatum + instance. Plugins should never have to create their own instance of + this class. + """ + self.kek_label = kek_datum.kek_label + self.plugin_name = kek_datum.plugin_name + self.algorithm = kek_datum.algorithm + self.bit_length = kek_datum.bit_length + self.mode = kek_datum.mode + self.plugin_meta = kek_datum.plugin_meta + + +class GenerateDTO(object): + """Data Transfer Object used to pass all the necessary data for the plugin + to generate a secret on behalf of the user. + + .. attribute:: generation_type + + String attribute used to identify the type of secret that should be + generated. This will be either ``"symmetric"`` or ``"asymmetric"``. + + .. attribute:: algorithm + + String attribute used to specify what type of algorithm the secret will + be used for. e.g. ``"AES"`` for a ``"symmetric"`` type, or ``"RSA"`` + for ``"asymmetric"``. + + .. attribute:: mode + + String attribute used to specify what algorithm mode the secret will be + used for. e.g. ``"CBC"`` for ``"AES"`` algorithm. + + .. attribute:: bit_length + + Integer attribute used to specify the bit length of the secret. For + example, this attribute could specify the key length for an encryption + key to be used in AES-CBC. + """ + + def __init__(self, algorithm, bit_length, mode, passphrase=None): + self.algorithm = algorithm + self.bit_length = bit_length + self.mode = mode + self.passphrase = passphrase + + +class ResponseDTO(object): + """Data transfer object for secret generation response.""" + + def __init__(self, cypher_text, kek_meta_extended=None): + self.cypher_text = cypher_text + self.kek_meta_extended = kek_meta_extended + + +class DecryptDTO(object): + """Data Transfer Object used to pass all the necessary data for the plugin + to perform decryption of a secret. + + Currently, this DTO only contains the data produced by the plugin during + encryption, but in the future this DTO will contain more information, such + as a transport key for secret wrapping back to the client. + + .. attribute:: encrypted + + The data that was produced by the plugin during encryption. For some + plugins this will be the actual bytes that need to be decrypted to + produce the secret. In other implementations, this may just be a + reference to some external system that can produce the unencrypted + secret. + """ + + def __init__(self, encrypted): + self.encrypted = encrypted + + +class EncryptDTO(object): + """Data Transfer Object used to pass all the necessary data for the plugin + to perform encryption of a secret. + + Currently, this DTO only contains the raw bytes to be encrypted by the + plugin, but in the future this may contain more information. + + .. attribute:: unencrypted + + The secret data in Bytes to be encrypted by the plugin. + """ + + def __init__(self, unencrypted): + self.unencrypted = unencrypted + + +@six.add_metaclass(abc.ABCMeta) +class CryptoPluginBase(object): + """Base class for all Crypto plugins. Implementations of this abstract + base class will be used by Barbican to perform cryptographic operations on + secrets. + + Barbican requests operations by invoking the methods on an instance of the + implementing class. Barbican's plugin manager handles the life-cycle of + the Data Transfer Objects (DTOs) that are passed into these methods, and + persist the data that is assigned to these DTOs by the plugin. + """ + + @abc.abstractmethod + def encrypt(self, encrypt_dto, kek_meta_dto, keystone_id): + """This method will be called by Barbican when requesting an encryption + operation on a secret on behalf of a project (tenant). + + :param encrypt_dto: :class:`EncryptDTO` instance containing the raw + secret byte data to be encrypted. + :type encrypt_dto: :class:`EncryptDTO` + :param kek_meta_dto: :class:`KEKMetaDTO` instance containing + information about the project's (tenant's) Key Encryption Key (KEK) + to be used for encryption. Plugins may assume that binding via + :meth:`bind_kek_metadata` has already taken place before this + instance is passed in. + :type kek_meta_dto: :class:`KEKMetaDTO` + :param keystone_id: Project (tenant) ID associated with the unencrypted + data. + :return: A tuple containing two items ``(ciphertext, + kek_metadata_extended)``. In a typical plugin implementation, the + first item in the tuple should be the ciphertext byte data + resulting from the encryption of the secret data. The second item + is an optional String object to be persisted alongside the + ciphertext. + + Barbican guarantees that both the ``ciphertext`` and + ``kek_metadata_extended`` will be persisted and then given back to + the plugin when requesting a decryption operation. + + ``kek_metadata_extended`` takes the idea of Key Encryption Key + (KEK) metadata further by giving plugins the option to store + secret-level KEK metadata. One example of using secret-level KEK + metadata would be plugins that want to use a unique KEK for every + secret that is encrypted. Such a plugin could use + ``kek_metadata_extended`` to store the Key ID for the KEK used to + encrypt this particular secret. + :rtype: tuple + """ + raise NotImplementedError # pragma: no cover + + @abc.abstractmethod + def decrypt(self, decrypt_dto, kek_meta_dto, kek_meta_extended, + keystone_id): + """Decrypt encrypted_datum in the context of the provided tenant. + + :param decrypt_dto: data transfer object containing the cyphertext + to be decrypted. + :param kek_meta_dto: Key encryption key metadata to use for decryption + :param kek_meta_extended: Optional per-secret KEK metadata to use for + decryption. + :param keystone_id: keystone_id associated with the encrypted datum. + :returns: str -- unencrypted byte data + + """ + raise NotImplementedError # pragma: no cover + + @abc.abstractmethod + def bind_kek_metadata(self, kek_meta_dto): + """Bind a key encryption key (KEK) metadata to the sub-system + handling encryption/decryption, updating information about the + key encryption key (KEK) metadata in the supplied 'kek_metadata' + data-transfer-object instance, and then returning this instance. + + This method is invoked prior to the encrypt() method above. + Implementors should fill out the supplied 'kek_meta_dto' instance + (an instance of KEKMetadata above) as needed to completely describe + the kek metadata and to complete the binding process. Barbican will + persist the contents of this instance once this method returns. + + :param kek_meta_dto: Key encryption key metadata to bind, with the + 'kek_label' attribute guaranteed to be unique, and the + and 'plugin_name' attribute already configured. + :returns: kek_meta_dto: Returns the specified DTO, after + modifications. + """ + raise NotImplementedError # pragma: no cover + + @abc.abstractmethod + def generate_symmetric(self, generate_dto, kek_meta_dto, keystone_id): + """Generate a new key. + + :param generate_dto: data transfer object for the record + associated with this generation request. Some relevant + parameters can be extracted from this object, including + bit_length, algorithm and mode + :param kek_meta_dto: Key encryption key metadata to use for decryption + :param keystone_id: keystone_id associated with the data. + :returns: An object of type ResponseDTO containing encrypted data and + kek_meta_extended, the former the resultant cypher text, the latter + being optional per-secret metadata needed to decrypt (over and above + the per-tenant metadata managed outside of the plugins) + """ + raise NotImplementedError # pragma: no cover + + @abc.abstractmethod + def generate_asymmetric(self, generate_dto, + kek_meta_dto, keystone_id): + """Create a new asymmetric key. + + :param generate_dto: data transfer object for the record + associated with this generation request. Some relevant + parameters can be extracted from this object, including + bit_length, algorithm and passphrase + :param kek_meta_dto: Key encryption key metadata to use for decryption + :param keystone_id: keystone_id associated with the data. + :returns: A tuple containing objects for private_key, public_key and + optionally one for passphrase. The objects will be of type ResponseDTO. + Each object containing encrypted data and kek_meta_extended, the former + the resultant cypher text, the latter being optional per-secret + metadata needed to decrypt (over and above the per-tenant metadata + managed outside of the plugins) + """ + raise NotImplementedError # pragma: no cover + + @abc.abstractmethod + def supports(self, type_enum, algorithm=None, bit_length=None, + mode=None): + """Used to determine if the plugin supports the requested operation. + + :param type_enum: Enumeration from PluginSupportsType class + :param algorithm: String algorithm name if needed + """ + raise NotImplementedError # pragma: no cover + + +class CryptoPluginManager(named.NamedExtensionManager): + def __init__(self, conf=CONF, invoke_on_load=True, + invoke_args=(), invoke_kwargs={}): + super(CryptoPluginManager, self).__init__( + conf.crypto.namespace, + conf.crypto.enabled_crypto_plugins, + invoke_on_load=invoke_on_load, + invoke_args=invoke_args, + invoke_kwds=invoke_kwargs + ) + + def get_plugin_store_generate(self, type_needed, algorithm=None, + bit_length=None, mode=None): + """Gets a secret store or generate plugin that supports provided type. + + :param type_needed: PluginSupportTypes that contains details on the + type of plugin required + :returns: CryptoPluginBase plugin implementation + """ + + if len(self.extensions) < 1: + raise CryptoPluginNotFound() + + for ext in self.extensions: + if ext.obj.supports(type_needed, algorithm, bit_length, mode): + plugin = ext.obj + break + else: + raise secret_store.SecretStorePluginNotFound() + + return plugin + + def get_plugin_retrieve(self, plugin_name_for_store): + """Gets a secret retrieve plugin that supports the provided type. + + :param type_needed: PluginSupportTypes that contains details on the + type of plugin required + :returns: CryptoPluginBase plugin implementation + """ + + if len(self.extensions) < 1: + raise CryptoPluginNotFound() + + for ext in self.extensions: + decrypting_plugin = ext.obj + plugin_name = utils.generate_fullname_for(decrypting_plugin) + if plugin_name == plugin_name_for_store: + break + else: + raise secret_store.SecretStorePluginNotFound() + + return decrypting_plugin diff --git a/barbican/plugin/crypto/p11_crypto.py b/barbican/plugin/crypto/p11_crypto.py index faf441808..452eb0454 100644 --- a/barbican/plugin/crypto/p11_crypto.py +++ b/barbican/plugin/crypto/p11_crypto.py @@ -1 +1,192 @@ -#TODO(john-wood-w) Pull over current crypto package's p11_crypto.py +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +try: + import PyKCS11 +except ImportError: + PyKCS11 = {} # TODO(reaperhulk): remove testing workaround + + +import base64 + +from oslo.config import cfg + +from barbican.common import exception +from barbican.plugin.crypto import crypto as plugin + +from barbican.openstack.common import gettextutils as u +from barbican.openstack.common import jsonutils as json + + +CONF = cfg.CONF + +p11_crypto_plugin_group = cfg.OptGroup(name='p11_crypto_plugin', + title="PKCS11 Crypto Plugin Options") +p11_crypto_plugin_opts = [ + cfg.StrOpt('library_path', + help=u._('Path to vendor PKCS11 library')), + cfg.StrOpt('login', + help=u._('Password to login to PKCS11 session')) +] +CONF.register_group(p11_crypto_plugin_group) +CONF.register_opts(p11_crypto_plugin_opts, group=p11_crypto_plugin_group) + + +class P11CryptoPluginKeyException(exception.BarbicanException): + message = u._("More than one key found for label") + + +class P11CryptoPluginException(exception.BarbicanException): + message = u._("General exception") + + +class P11CryptoPlugin(plugin.CryptoPluginBase): + """PKCS11 supporting implementation of the crypto plugin. + Generates a key per tenant and encrypts using AES-256-GCM. + This implementation currently relies on an unreleased fork of PyKCS11. + """ + + def __init__(self, conf=cfg.CONF): + self.block_size = 16 # in bytes + self.kek_key_length = 32 # in bytes (256-bit) + self.algorithm = 0x8000011c # CKM_AES_GCM vendor prefixed. + self.pkcs11 = PyKCS11.PyKCS11Lib() + if conf.p11_crypto_plugin.library_path is None: + raise ValueError(u._("library_path is required")) + else: + self.pkcs11.load(conf.p11_crypto_plugin.library_path) + # initialize the library. PyKCS11 does not supply this for free + self._check_error(self.pkcs11.lib.C_Initialize()) + self.session = self.pkcs11.openSession(1) + self.session.login(conf.p11_crypto_plugin.login) + self.rw_session = self.pkcs11.openSession(1, PyKCS11.CKF_RW_SESSION) + self.rw_session.login(conf.p11_crypto_plugin.login) + + def _check_error(self, value): + if value != PyKCS11.CKR_OK: + raise PyKCS11.PyKCS11Error(value) + + def _get_key_by_label(self, key_label): + template = ( + (PyKCS11.CKA_CLASS, PyKCS11.CKO_SECRET_KEY), + (PyKCS11.CKA_KEY_TYPE, PyKCS11.CKK_AES), + (PyKCS11.CKA_LABEL, key_label)) + keys = self.session.findObjects(template) + if len(keys) == 1: + return keys[0] + elif len(keys) == 0: + return None + else: + raise P11CryptoPluginKeyException() + + def _generate_iv(self): + iv = self.session.generateRandom(self.block_size) + iv = b''.join(chr(i) for i in iv) + if len(iv) != self.block_size: + raise P11CryptoPluginException() + return iv + + def _build_gcm_params(self, iv): + gcm = PyKCS11.LowLevel.CK_AES_GCM_PARAMS() + gcm.pIv = iv + gcm.ulIvLen = len(iv) + gcm.ulIvBits = len(iv) * 8 + gcm.ulTagBits = 128 + return gcm + + def _generate_kek(self, kek_label): + # TODO(reaperhulk): review template to ensure it's what we want + template = ( + (PyKCS11.CKA_CLASS, PyKCS11.CKO_SECRET_KEY), + (PyKCS11.CKA_KEY_TYPE, PyKCS11.CKK_AES), + (PyKCS11.CKA_VALUE_LEN, self.kek_key_length), + (PyKCS11.CKA_LABEL, kek_label), + (PyKCS11.CKA_PRIVATE, True), + (PyKCS11.CKA_SENSITIVE, True), + (PyKCS11.CKA_ENCRYPT, True), + (PyKCS11.CKA_DECRYPT, True), + (PyKCS11.CKA_TOKEN, True), + (PyKCS11.CKA_WRAP, True), + (PyKCS11.CKA_UNWRAP, True), + (PyKCS11.CKA_EXTRACTABLE, False)) + ckattr = self.session._template2ckattrlist(template) + + m = PyKCS11.LowLevel.CK_MECHANISM() + m.mechanism = PyKCS11.LowLevel.CKM_AES_KEY_GEN + + key = PyKCS11.LowLevel.CK_OBJECT_HANDLE() + self._check_error( + self.pkcs11.lib.C_GenerateKey( + self.rw_session.session, + m, + ckattr, + key + ) + ) + + def encrypt(self, encrypt_dto, kek_meta_dto, keystone_id): + key = self._get_key_by_label(kek_meta_dto.kek_label) + iv = self._generate_iv() + gcm = self._build_gcm_params(iv) + mech = PyKCS11.Mechanism(self.algorithm, gcm) + encrypted = self.session.encrypt(key, encrypt_dto.unencrypted, mech) + cyphertext = b''.join(chr(i) for i in encrypted) + kek_meta_extended = json.dumps({ + 'iv': base64.b64encode(iv) + }) + + return plugin.ResponseDTO(cyphertext, kek_meta_extended) + + def decrypt(self, decrypt_dto, kek_meta_dto, kek_meta_extended, + keystone_id): + key = self._get_key_by_label(kek_meta_dto.kek_label) + meta_extended = json.loads(kek_meta_extended) + iv = base64.b64decode(meta_extended['iv']) + gcm = self._build_gcm_params(iv) + mech = PyKCS11.Mechanism(self.algorithm, gcm) + decrypted = self.session.decrypt(key, decrypt_dto.encrypted, mech) + secret = b''.join(chr(i) for i in decrypted) + return secret + + def bind_kek_metadata(self, kek_meta_dto): + # Enforce idempotency: If we've already generated a key for the + # kek_label, leave now. + key = self._get_key_by_label(kek_meta_dto.kek_label) + if not key: + self._generate_kek(kek_meta_dto.kek_label) + # To be persisted by Barbican: + kek_meta_dto.algorithm = 'AES' + kek_meta_dto.bit_length = self.kek_key_length * 8 + kek_meta_dto.mode = 'GCM' + kek_meta_dto.plugin_meta = None + + return kek_meta_dto + + def generate_symmetric(self, generate_dto, kek_meta_dto, keystone_id): + byte_length = generate_dto.bit_length / 8 + rand = self.session.generateRandom(byte_length) + if len(rand) != byte_length: + raise P11CryptoPluginException() + return self.encrypt(plugin.EncryptDTO(rand), kek_meta_dto, keystone_id) + + def generate_asymmetric(self, generate_dto, kek_meta_dto, keystone_id): + raise NotImplementedError("Feature not implemented for PKCS11") + + def supports(self, type_enum, algorithm=None, bit_length=None, mode=None): + if type_enum == plugin.PluginSupportTypes.ENCRYPT_DECRYPT: + return True + elif type_enum == plugin.PluginSupportTypes.SYMMETRIC_KEY_GENERATION: + return True + elif type_enum == plugin.PluginSupportTypes.ASYMMETRIC_KEY_GENERATION: + return False + else: + return False diff --git a/barbican/plugin/crypto/simple_crypto.py b/barbican/plugin/crypto/simple_crypto.py new file mode 100644 index 000000000..2413860c3 --- /dev/null +++ b/barbican/plugin/crypto/simple_crypto.py @@ -0,0 +1,200 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +import os + +from Crypto.PublicKey import DSA +from Crypto.PublicKey import RSA +from Crypto.Util import asn1 +from cryptography import fernet + +from oslo.config import cfg + +import six + +from barbican.openstack.common import gettextutils as u +from barbican.plugin.crypto import crypto as c + + +CONF = cfg.CONF + +simple_crypto_plugin_group = cfg.OptGroup(name='simple_crypto_plugin', + title="Simple Crypto Plugin Options") +simple_crypto_plugin_opts = [ + cfg.StrOpt('kek', + default=b'dGhpcnR5X3R3b19ieXRlX2tleWJsYWhibGFoYmxhaGg=', + help=u._('Key encryption key to be used by Simple Crypto ' + 'Plugin')) +] +CONF.register_group(simple_crypto_plugin_group) +CONF.register_opts(simple_crypto_plugin_opts, group=simple_crypto_plugin_group) + + +class SimpleCryptoPlugin(c.CryptoPluginBase): + """Insecure implementation of the crypto plugin.""" + + def __init__(self, conf=CONF): + self.master_kek = conf.simple_crypto_plugin.kek + + def _get_kek(self, kek_meta_dto): + if not kek_meta_dto.plugin_meta: + raise ValueError('KEK not yet created.') + # the kek is stored encrypted. Need to decrypt. + encryptor = fernet.Fernet(self.master_kek) + # Note : If plugin_meta type is unicode, encode to byte. + if isinstance(kek_meta_dto.plugin_meta, six.text_type): + kek_meta_dto.plugin_meta = kek_meta_dto.plugin_meta.encode('utf-8') + + return encryptor.decrypt(kek_meta_dto.plugin_meta) + + def encrypt(self, encrypt_dto, kek_meta_dto, keystone_id): + kek = self._get_kek(kek_meta_dto) + unencrypted = encrypt_dto.unencrypted + if not isinstance(unencrypted, str): + raise ValueError('Unencrypted data must be a byte type, ' + 'but was {0}'.format(type(unencrypted))) + encryptor = fernet.Fernet(kek) + cyphertext = encryptor.encrypt(unencrypted) + return c.ResponseDTO(cyphertext, None) + + def decrypt(self, encrypted_dto, kek_meta_dto, kek_meta_extended, + keystone_id): + kek = self._get_kek(kek_meta_dto) + encrypted = encrypted_dto.encrypted + decryptor = fernet.Fernet(kek) + return decryptor.decrypt(encrypted) + + def bind_kek_metadata(self, kek_meta_dto): + kek_meta_dto.algorithm = 'aes' + kek_meta_dto.bit_length = 128 + kek_meta_dto.mode = 'cbc' + if not kek_meta_dto.plugin_meta: + # the kek is stored encrypted in the plugin_meta field + encryptor = fernet.Fernet(self.master_kek) + key = fernet.Fernet.generate_key() + kek_meta_dto.plugin_meta = encryptor.encrypt(key) + return kek_meta_dto + + def generate_symmetric(self, generate_dto, kek_meta_dto, keystone_id): + byte_length = int(generate_dto.bit_length) / 8 + unencrypted = os.urandom(byte_length) + + return self.encrypt(c.EncryptDTO(unencrypted), + kek_meta_dto, + keystone_id) + + def generate_asymmetric(self, generate_dto, kek_meta_dto, keystone_id): + """Generate asymmetric keys based on below rule + - RSA, with passphrase (supported) + - RSA, without passphrase (supported) + - DSA, without passphrase (supported) + - DSA, with passphrase (not supported) + + Note: PyCrypto is not capable of serializing DSA + keys and DER formated keys. Such keys will be + serialized to Base64 PEM to store in DB. + + TODO (atiwari/reaperhulk): PyCrypto is not capable to serialize + DSA keys and DER formated keys, later we need to pick better + crypto lib. + """ + if generate_dto.algorithm is None\ + or generate_dto.algorithm.lower() == 'rsa': + private_key = RSA.generate( + generate_dto.bit_length, None, None, 65537) + elif generate_dto.algorithm.lower() == 'dsa': + private_key = DSA.generate(generate_dto.bit_length, None, None) + else: + raise c.CryptoPrivateKeyFailureException() + + public_key = private_key.publickey() + + # Note (atiwari): key wrapping format PEM only supported + if generate_dto.algorithm.lower() == 'rsa': + public_key, private_key = self._wrap_key(public_key, private_key, + generate_dto.passphrase) + if generate_dto.algorithm.lower() == 'dsa': + if generate_dto.passphrase: + raise ValueError('Passphrase not supported for DSA key') + public_key, private_key = self._serialize_dsa_key(public_key, + private_key) + private_dto = self.encrypt(c.EncryptDTO(private_key), + kek_meta_dto, + keystone_id) + + public_dto = self.encrypt(c.EncryptDTO(public_key), + kek_meta_dto, + keystone_id) + + passphrase_dto = None + if generate_dto.passphrase: + encrypt_dto = c.EncryptDTO(generate_dto.passphrase) + passphrase_dto = self.encrypt(encrypt_dto, + kek_meta_dto, + keystone_id) + + return private_dto, public_dto, passphrase_dto + + def supports(self, type_enum, algorithm=None, bit_length=None, + mode=None): + if type_enum == c.PluginSupportTypes.ENCRYPT_DECRYPT: + return True + + if type_enum == c.PluginSupportTypes.SYMMETRIC_KEY_GENERATION: + return self._is_algorithm_supported(algorithm, + bit_length) + elif type_enum == c.PluginSupportTypes.ASYMMETRIC_KEY_GENERATION: + return self._is_algorithm_supported(algorithm, + bit_length) + else: + return False + + def _wrap_key(self, public_key, private_key, + passphrase): + pkcs = 8 + key_wrap_format = 'PEM' + + private_key = private_key.exportKey(key_wrap_format, passphrase, pkcs) + public_key = public_key.exportKey() + + return public_key, private_key + + def _serialize_dsa_key(self, public_key, private_key): + + pub_seq = asn1.DerSequence() + pub_seq[:] = [0, public_key.p, public_key.q, + public_key.g, public_key.y] + public_key = "-----BEGIN DSA PUBLIC KEY-----\n%s"\ + "-----END DSA PUBLIC KEY-----" % pub_seq.encode().encode("base64") + + prv_seq = asn1.DerSequence() + prv_seq[:] = [0, private_key.p, private_key.q, + private_key.g, private_key.y, private_key.x] + private_key = "-----BEGIN DSA PRIVATE KEY-----\n%s"\ + "-----END DSA PRIVATE KEY-----" % prv_seq.encode().encode("base64") + + return public_key, private_key + + def _is_algorithm_supported(self, algorithm=None, bit_length=None): + """check if algorithm and bit_length combination is supported.""" + if algorithm is None or bit_length is None: + return False + + if algorithm.lower() in c.PluginSupportTypes.SYMMETRIC_ALGORITHMS \ + and bit_length in c.PluginSupportTypes.SYMMETRIC_KEY_LENGTHS: + return True + elif algorithm.lower() in c.PluginSupportTypes.ASYMMETRIC_ALGORITHMS \ + and bit_length in c.PluginSupportTypes.ASYMMETRIC_KEY_LENGTHS: + return True + else: + return False diff --git a/barbican/plugin/interface/secret_store.py b/barbican/plugin/interface/secret_store.py index 54e01696e..d6f8134f6 100644 --- a/barbican/plugin/interface/secret_store.py +++ b/barbican/plugin/interface/secret_store.py @@ -20,6 +20,7 @@ from oslo.config import cfg from stevedore import named from barbican.common import exception +from barbican.common import utils from barbican.openstack.common import gettextutils as u @@ -55,6 +56,87 @@ class SecretStoreSupportedPluginNotFound(exception.BarbicanException): message = "Secret store plugin not found for requested operation." +class SecretContentTypeNotSupportedException(exception.BarbicanException): + """Raised when support for payload content type is not available.""" + def __init__(self, content_type): + super(SecretContentTypeNotSupportedException, self).__init__( + u._("Secret Content Type " + "of '{0}' not supported").format(content_type) + ) + self.content_type = content_type + + +class SecretContentEncodingNotSupportedException(exception.BarbicanException): + """Raised when support for payload content encoding is not available.""" + def __init__(self, content_encoding): + super(SecretContentEncodingNotSupportedException, self).__init__( + u._("Secret Content-Encoding of '{0}' not supported").format( + content_encoding) + ) + self.content_encoding = content_encoding + + +class SecretNoPayloadProvidedException(exception.BarbicanException): + """Raised when secret information is not provided.""" + def __init__(self): + super(SecretNoPayloadProvidedException, self).__init__( + u._('No secret information provided to encrypt.') + ) + + +class SecretContentEncodingMustBeBase64(exception.BarbicanException): + """Raised when encoding must be base64.""" + def __init__(self): + super(SecretContentEncodingMustBeBase64, self).__init__( + u._("Encoding type must be 'base64' for text-based payloads.") + ) + + +class SecretGeneralException(exception.BarbicanException): + """Raised when a system fault has occurred.""" + def __init__(self, reason=u._('Unknown')): + super(SecretGeneralException, self).__init__( + u._('Problem seen during crypto processing - ' + 'Reason: {0}').format(reason) + ) + self.reason = reason + + +class SecretPayloadDecodingError(exception.BarbicanException): + """Raised when payload could not be decoded.""" + def __init__(self): + super(SecretPayloadDecodingError, self).__init__( + u._("Problem decoding payload") + ) + + +class SecretAcceptNotSupportedException(exception.BarbicanException): + """Raised when requested decrypted content-type is not available.""" + def __init__(self, accept): + super(SecretAcceptNotSupportedException, self).__init__( + u._("Secret Accept of '{0}' not supported").format(accept) + ) + self.accept = accept + + +class SecretNotFoundException(exception.BarbicanException): + """Raised when secret information could not be located.""" + def __init__(self): + super(SecretNotFoundException, self).__init__( + u._('No secret information found') + ) + + +class SecretAlgorithmNotSupportedException(exception.BarbicanException): + """Raised when support for an algorithm is not available.""" + def __init__(self, algorithm): + super(SecretAlgorithmNotSupportedException, self).__init__( + u._("Secret algorithm of '{0}' not supported").format( + algorithm) + ) + self.algorithm = algorithm + + class SecretType(object): """Constant to define the symmetric key type. Used by getSecret to retrieve @@ -90,63 +172,79 @@ class KeyAlgorithm(object): DESEDE = "desede" -class KeyFormat(object): - - """Key format that indicates that key value is a bytearray of the raw bytes - of the string. - """ - RAW = "raw" - """PKCS #1 encoding format.""" - PKCS1 = "pkcs1" - """PKCS #8 encoding format.""" - PKCS8 = "pkcs8" - """X.509 encoding format.""" - X509 = "x509" - - class KeySpec(object): """This object specifies the algorithm and bit length for a key.""" - def __init__(self, alg, bit_length): + def __init__(self, alg=None, bit_length=None, mode=None): """Creates a new KeySpec. :param alg:algorithm for the key :param bit_length:bit length of the key + :param mode:algorithm mode for the key """ self.alg = alg self.bit_length = bit_length + self.mode = mode # TODO(john-wood-w) Paul, is 'mode' required? class SecretDTO(object): """This object is a secret data transfer object (DTO). This object encapsulates a key and attributes about the key. The attributes include a KeySpec that contains the algorithm and bit length. The attributes also - include information on the format and encoding of the key. + include information on the encoding of the key. """ - def __init__(self, type, format, secret, key_spec): + #TODO(john-wood-w) Remove 'content_type' once secret normalization work is + # completed. + def __init__(self, type, secret, key_spec, content_type): """Creates a new SecretDTO. - The secret is stored in the secret parameter. The format parameter - indicates the format of the bytes for the secret. In the future this + The secret is stored in the secret parameter. In the future this DTO may include compression and key wrapping information. :param type: SecretType for secret - :param format: KeyFormat key format - :param secret: secret + :param secret: secret, as a base64-encoded string :param key_spec: KeySpec key specifications + :param content_type: Content type of the secret, one of MIME + types such as 'text/plain' or 'application/octet-stream' """ self.type = type - self.format = format self.secret = secret self.key_spec = key_spec + self.content_type = content_type + + +#TODO(john-wood-w) Remove this class once repository factory work is +# completed. +class SecretStoreContext(object): + """Context for secret store plugins. + + Some plugins implementations (such as the crypto implementation) might + require access to core Barbican resources such as datastore repositories. + This object provides access to such storage. + """ + def __init__(self, **kwargs): + if kwargs: + for k, v in kwargs.items(): + setattr(self, k, v) @six.add_metaclass(abc.ABCMeta) class SecretStoreBase(object): + #TODO(john-wood-w) Remove 'context' once repository factory and secret + # normalization work is completed. + #TODO(john-wood-w) Combine generate_symmetric_key() and + # generate_asymmetric_key() into one method: generate_key(), that will + # return a dict with this structure: + # { SecretType.xxxxx: {secret-meta dict} + # So for symmetric keys, this would look like: + # { SecretType.SYMMETRIC: {secret-meta dict} + # And for asymmetric keys: + # { SecretType.PUBLIC: {secret-meta for public}, + # SecretType.PRIVATE: {secret-meta for private}} @abc.abstractmethod - def generate_symmetric_key(self, key_spec): + def generate_symmetric_key(self, key_spec, context): """Generate a new symmetric key and store it. Generates a new symmetric key and stores it in the secret store. @@ -159,12 +257,13 @@ class SecretStoreBase(object): :param key_spec: KeySpec that contains details on the type of key to generate + :param context: SecretStoreContext for secret :returns: a dictionary that contains metadata about the key """ raise NotImplementedError # pragma: no cover @abc.abstractmethod - def generate_asymmetric_key(self, key_spec): + def generate_asymmetric_key(self, key_spec, context): """Generate a new asymmetric key and store it. Generates a new asymmetric key and stores it in the secret store. @@ -177,12 +276,13 @@ class SecretStoreBase(object): :param key_spec: KeySpec that contains details on the type of key to generate + :param context: SecretStoreContext for secret :returns: a dictionary that contains metadata about the key """ raise NotImplementedError # pragma: no cover @abc.abstractmethod - def store_secret(self, secret_dto): + def store_secret(self, secret_dto, context): """Stores a key. The SecretDTO contains the bytes of the secret and properties of the @@ -193,12 +293,13 @@ class SecretStoreBase(object): dictionary may be empty if the SecretStore does not require it. :param secret_dto: SecretDTO for secret + :param context: SecretStoreContext for secret :returns: a dictionary that contains metadata about the secret """ raise NotImplementedError # pragma: no cover @abc.abstractmethod - def get_secret(self, secret_metadata): + def get_secret(self, secret_metadata, context): """Retrieves a secret from the secret store. Retrieves a secret from the secret store and returns a SecretDTO that @@ -209,6 +310,7 @@ class SecretStoreBase(object): the key. :param secret_metadata: secret metadata + :param context: SecretStoreContext for secret :returns: SecretDTO that contains secret """ raise NotImplementedError # pragma: no cover @@ -262,6 +364,24 @@ class SecretStorePluginManager(named.NamedExtensionManager): return self.extensions[0].obj + def get_plugin_retrieve_delete(self, plugin_name): + """Gets a secret retrieve/delete plugin. + + :returns: SecretStoreBase plugin implementation + """ + + if len(self.extensions) < 1: + raise SecretStorePluginNotFound() + + for ext in self.extensions: + if utils.generate_fullname_for(ext.obj) == plugin_name: + retrieve_delete_plugin = ext.obj + break + else: + raise SecretStoreSupportedPluginNotFound() + + return retrieve_delete_plugin + def get_plugin_generate(self, key_spec): """Gets a secret generate plugin. diff --git a/barbican/plugin/resources.py b/barbican/plugin/resources.py new file mode 100644 index 000000000..25d515b3d --- /dev/null +++ b/barbican/plugin/resources.py @@ -0,0 +1,185 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from barbican.common import utils +from barbican.model import models +from barbican.plugin.interface import secret_store +from barbican.plugin.util import translations as tr + + +def store_secret(unencrypted_raw, content_type_raw, content_encoding, + spec, secret_model, tenant_model, + repos): + """Store a provided secret into secure backend.""" + + # Create a secret model is one isn't provided. + # Note: For one-step secret stores, the model is not provided. For + # two-step secrets, the secret entity is already created and should then + # be passed into this function. + if not secret_model: + secret_model = models.Secret(spec) + elif _secret_already_has_stored_data(secret_model): + raise ValueError('Secret already has encrypted data stored for it.') + + # If there is no secret data to store, then just create Secret entity and + # leave. A subsequent call to this method should provide both the Secret + # entity created here *and* the secret data to store into it. + if not unencrypted_raw: + _save_secret(secret_model, tenant_model, repos) + return secret_model + + # Locate a suitable plugin to store the secret. + store_plugin = secret_store.SecretStorePluginManager().get_plugin_store() + + # Normalize inputs prior to storage. + #TODO(john-wood-w) Normalize all secrets to base64, so we don't have to + # pass in 'content' type to the store_secret() call below. + unencrypted, content_type = tr.normalize_before_encryption( + unencrypted_raw, content_type_raw, content_encoding, + enforce_text_only=True) + + # Store the secret securely. + #TODO(john-wood-w) Remove the SecretStoreContext once repository factory + # and unit test patch work is completed. + context = secret_store.SecretStoreContext(secret_model=secret_model, + tenant_model=tenant_model, + repos=repos) + key_spec = secret_store.KeySpec(alg=spec.get('algorithm'), + bit_length=spec.get('bit_length'), + mode=spec.get('mode')) + secret_dto = secret_store.SecretDTO(None, unencrypted, key_spec, + content_type) + secret_metadata = store_plugin.store_secret(secret_dto, context) + + # Save secret and metadata. + _save_secret(secret_model, tenant_model, repos) + _save_secret_metadata(secret_model, secret_metadata, store_plugin, + content_type, repos) + + return secret_model + + +def get_secret(requesting_content_type, secret_model, tenant_model): + tr.analyze_before_decryption(requesting_content_type) + + # Construct metadata dict from data model. + # Note: Must use the dict/tuple format for py2.6 usage. + secret_metadata = dict((k, v.value) for (k, v) in + secret_model.secret_store_metadata.items()) + + # Locate a suitable plugin to store the secret. + retrieve_plugin = secret_store.SecretStorePluginManager()\ + .get_plugin_retrieve_delete(secret_metadata.get('plugin_name')) + + # Retrieve the secret. + #TODO(john-wood-w) Remove the SecretStoreContext once repository factory + # and unit test patch work is completed. + context = secret_store.SecretStoreContext(secret_model=secret_model, + tenant_model=tenant_model) + secret_dto = retrieve_plugin.get_secret(secret_metadata, context) + + # Denormalize the secret. + return tr.denormalize_after_decryption(secret_dto.secret, + requesting_content_type) + + +def generate_secret(spec, content_type, + tenant_model, repos): + """Generate a secret and store into a secure backend.""" + + # Locate a suitable plugin to store the secret. + key_spec = secret_store.KeySpec(alg=spec.get('algorithm'), + bit_length=spec.get('bit_length'), + mode=spec.get('mode')) + generate_plugin = secret_store.SecretStorePluginManager()\ + .get_plugin_generate(key_spec) + + # Create secret model to eventually save metadata to. + secret_model = models.Secret(spec) + + # Generate the secret. + #TODO(john-wood-w) Remove the SecretStoreContext once repository factory + # and unit test patch work is completed. + context = secret_store.SecretStoreContext(content_type=content_type, + secret_model=secret_model, + tenant_model=tenant_model, + repos=repos) + + #TODO(john-wood-w) Replace with single 'generate_key()' call once + # asymmetric and symmetric generation is combined. + secret_metadata = generate_plugin.\ + generate_symmetric_key(key_spec, context) + + # Save secret and metadata. + _save_secret(secret_model, tenant_model, repos) + _save_secret_metadata(secret_model, secret_metadata, generate_plugin, + content_type, repos) + + return secret_model + + +def delete_secret(secret_model, project_id, repos): + """Remove a secret from secure backend.""" + + # Construct metadata dict from data model. + # Note: Must use the dict/tuple format for py2.6 usage. + secret_metadata = dict((k, v.value) for (k, v) in + secret_model.secret_store_metadata.items()) + + # Locate a suitable plugin to delete the secret from. + delete_plugin = secret_store.SecretStorePluginManager()\ + .get_plugin_retrieve_delete(secret_metadata.get('plugin_name')) + + # Delete the secret from plugin storage. + delete_plugin.delete_secret(secret_metadata) + + # Delete the secret from data model. + repos.secret_repo.delete_entity_by_id(entity_id=secret_model.id, + keystone_id=project_id) + + +def _save_secret_metadata(secret_model, secret_metadata, + store_plugin, content_type, repos): + """Add secret metadata to a secret.""" + + if not secret_metadata: + secret_metadata = dict() + + secret_metadata['plugin_name'] = utils\ + .generate_fullname_for(store_plugin) + + secret_metadata['content_type'] = content_type + + repos.secret_meta_repo.save(secret_metadata, secret_model) + + +def _save_secret(secret_model, tenant_model, repos): + """Save a Secret entity.""" + + # Create Secret entities in data store. + if not secret_model.id: + repos.secret_repo.create_from(secret_model) + new_assoc = models.TenantSecret() + new_assoc.tenant_id = tenant_model.id + new_assoc.secret_id = secret_model.id + new_assoc.role = "admin" + new_assoc.status = models.States.ACTIVE + repos.tenant_secret_repo.create_from(new_assoc) + else: + repos.secret_repo.save(secret_model) + + +def _secret_already_has_stored_data(secret_model): + if not secret_model: + return False + return secret_model.encrypted_data or secret_model.secret_store_metadata diff --git a/barbican/plugin/store_crypto.py b/barbican/plugin/store_crypto.py index d8e2f3d62..d42f6bfaa 100644 --- a/barbican/plugin/store_crypto.py +++ b/barbican/plugin/store_crypto.py @@ -1 +1,243 @@ -#TODO(john-wood-w) Add store to crypto adapter logic here. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +import base64 + +from oslo.config import cfg + +from barbican.common import utils +from barbican.model import models +from barbican.plugin.crypto import crypto +from barbican.plugin.interface import secret_store as sstore + +CONF = cfg.CONF + + +class StoreCryptoAdapterPlugin(sstore.SecretStoreBase): + """Secret store plugin adapting to 'crypto' devices as backend. + + HSM-style 'crypto' devices perform encryption/decryption processing but + do not actually store the encrypted information, unlike other 'secret + store' plugins that do provide storage. Hence, this adapter bridges + between these two plugin styles, providing Barbican persistence services + as needed to store information. + """ + + def __init__(self): + super(StoreCryptoAdapterPlugin, self).__init__() + + def store_secret(self, secret_dto, context): + """Store a secret. + + Returns a dict with the relevant metadata (which in this case is just + the key_id + """ + + # Find HSM-style 'crypto' plugin. + encrypting_plugin = crypto.CryptoPluginManager()\ + .get_plugin_store_generate( + crypto.PluginSupportTypes.ENCRYPT_DECRYPT + ) + + # Find or create a key encryption key metadata. + kek_datum_model, kek_meta_dto = self._find_or_create_kek_objects( + encrypting_plugin, context.tenant_model, context.repos.kek_repo) + + encrypt_dto = crypto.EncryptDTO(secret_dto.secret) + + # Create an encrypted datum instance and add the encrypted cyphertext. + datum_model = models.EncryptedDatum(context.secret_model, + kek_datum_model) + datum_model.content_type = secret_dto.content_type + response_dto = encrypting_plugin.encrypt( + encrypt_dto, kek_meta_dto, context.tenant_model.keystone_id + ) + datum_model.kek_meta_extended = response_dto.kek_meta_extended + + # Convert binary data into a text-based format. + #TODO(jwood) Figure out by storing binary (BYTEA) data in Postgres + # isn't working. + datum_model.cypher_text = base64.b64encode(response_dto.cypher_text) + + self._store_secret_and_datum(context.tenant_model, + context.secret_model, + datum_model, context.repos) + + return None + + def get_secret(self, secret_metadata, context): + """Retrieve a secret.""" + if not context.secret_model \ + or not context.secret_model.encrypted_data: + raise sstore.SecretNotFoundException() + + #TODO(john-wood-w) Need to revisit 1 to many datum relationship. + datum_model = context.secret_model.encrypted_data[0] + + # Find HSM-style 'crypto' plugin. + decrypting_plugin = crypto.CryptoPluginManager().get_plugin_retrieve( + datum_model.kek_meta_tenant.plugin_name) + + # wrap the KEKDatum instance in our DTO + kek_meta_dto = crypto.KEKMetaDTO(datum_model.kek_meta_tenant) + + # Convert from text-based storage format to binary. + #TODO(jwood) Figure out by storing binary (BYTEA) data in + # Postgres isn't working. + encrypted = base64.b64decode(datum_model.cypher_text) + decrypt_dto = crypto.DecryptDTO(encrypted) + + # Decrypt the secret. + secret = decrypting_plugin.decrypt(decrypt_dto, + kek_meta_dto, + datum_model.kek_meta_extended, + context.tenant_model.keystone_id) + key_spec = sstore.KeySpec(alg=context.secret_model.algorithm, + bit_length=context.secret_model.bit_length, + mode=context.secret_model.mode) + return sstore.SecretDTO(sstore.SecretType.SYMMETRIC, + secret, key_spec, + datum_model.content_type) + + def delete_secret(self, secret_metadata): + """Delete a secret.""" + pass + + def generate_symmetric_key(self, key_spec, context): + """Generate a symmetric key. + + Returns a metadata object that can be used for retrieving the secret. + """ + + # Find HSM-style 'crypto' plugin. + plugin_type = self._determine_generation_type(key_spec.alg) + if crypto.PluginSupportTypes.SYMMETRIC_KEY_GENERATION != plugin_type: + raise sstore.SecretAlgorithmNotSupportedException(key_spec.alg) + generating_plugin = crypto.CryptoPluginManager()\ + .get_plugin_store_generate(plugin_type, + key_spec.alg, + key_spec.bit_length, + key_spec.mode) + + # Find or create a key encryption key metadata. + kek_datum_model, kek_meta_dto = self._find_or_create_kek_objects( + generating_plugin, context.tenant_model, context.repos.kek_repo) + + # Create an encrypted datum instance and add the created cypher text. + datum_model = models.EncryptedDatum(context.secret_model, + kek_datum_model) + datum_model.content_type = context.content_type + + generate_dto = crypto.GenerateDTO(key_spec.alg, + key_spec.bit_length, + key_spec.mode, None) + # Create the encrypted meta. + response_dto = generating_plugin.\ + generate_symmetric(generate_dto, kek_meta_dto, + context.tenant_model.keystone_id) + + # Convert binary data into a text-based format. + # TODO(jwood) Figure out by storing binary (BYTEA) data in Postgres + # isn't working. + datum_model.cypher_text = base64.b64encode(response_dto.cypher_text) + datum_model.kek_meta_extended = response_dto.kek_meta_extended + + self._store_secret_and_datum(context.tenant_model, + context.secret_model, datum_model, + context.repos) + + return None + + def generate_asymmetric_key(self, key_spec, context): + """Generate an asymmetric key.""" + #TODO(john-wood-w) Pull over https://github.com/openstack/barbican/ + # blob/master/barbican/crypto/extension_manager.py#L336 + raise NotImplementedError('No support for generate_asymmetric_key') + + def generate_supports(self, key_spec): + """Key generation supported? + + Specifies whether the plugin supports key generation with the + given key_spec. + """ + return key_spec and sstore.KeyAlgorithm.AES == key_spec.alg.lower() + + def _find_or_create_kek_objects(self, plugin_inst, tenant_model, kek_repo): + # Find or create a key encryption key. + full_plugin_name = utils.generate_fullname_for(plugin_inst) + kek_datum_model = kek_repo.find_or_create_kek_datum(tenant_model, + full_plugin_name) + + # Bind to the plugin's key management. + # TODO(jwood): Does this need to be in a critical section? Should the + # bind operation just be declared idempotent in the plugin contract? + kek_meta_dto = crypto.KEKMetaDTO(kek_datum_model) + if not kek_datum_model.bind_completed: + kek_meta_dto = plugin_inst.bind_kek_metadata(kek_meta_dto) + + # By contract, enforce that plugins return a + # (typically modified) DTO. + if kek_meta_dto is None: + raise crypto.CryptoKEKBindingException(full_plugin_name) + + self._indicate_bind_completed(kek_meta_dto, kek_datum_model) + kek_repo.save(kek_datum_model) + + return kek_datum_model, kek_meta_dto + + def _store_secret_and_datum(self, tenant_model, secret_model, datum_model, + repos=None): + # Create Secret entities in data store. + if not secret_model.id: + repos.secret_repo.create_from(secret_model) + new_assoc = models.TenantSecret() + new_assoc.tenant_id = tenant_model.id + new_assoc.secret_id = secret_model.id + new_assoc.role = "admin" + new_assoc.status = models.States.ACTIVE + repos.tenant_secret_repo.create_from(new_assoc) + if datum_model: + datum_model.secret_id = secret_model.id + repos.datum_repo.create_from(datum_model) + + def _indicate_bind_completed(self, kek_meta_dto, kek_datum): + """Updates the supplied kek_datum instance + + Updates the the kek_datum per the contents of the supplied + kek_meta_dto instance. This function is typically used once plugins + have had a chance to bind kek_meta_dto to their crypto systems. + + :param kek_meta_dto: + :param kek_datum: + :return: None + + """ + kek_datum.bind_completed = True + kek_datum.algorithm = kek_meta_dto.algorithm + kek_datum.bit_length = kek_meta_dto.bit_length + kek_datum.mode = kek_meta_dto.mode + kek_datum.plugin_meta = kek_meta_dto.plugin_meta + + #TODO(john-wood-w) Move this to the more generic secret_store.py? + def _determine_generation_type(self, algorithm): + """Determines the type (symmetric and asymmetric for now) + based on algorithm + """ + symmetric_algs = crypto.PluginSupportTypes.SYMMETRIC_ALGORITHMS + asymmetric_algs = crypto.PluginSupportTypes.ASYMMETRIC_ALGORITHMS + if algorithm.lower() in symmetric_algs: + return crypto.PluginSupportTypes.SYMMETRIC_KEY_GENERATION + elif algorithm.lower() in asymmetric_algs: + return crypto.PluginSupportTypes.ASYMMETRIC_KEY_GENERATION + else: + raise sstore.SecretAlgorithmNotSupportedException(algorithm) diff --git a/barbican/tests/crypto/__init__.py b/barbican/plugin/util/__init__.py similarity index 100% rename from barbican/tests/crypto/__init__.py rename to barbican/plugin/util/__init__.py diff --git a/barbican/crypto/mime_types.py b/barbican/plugin/util/mime_types.py similarity index 100% rename from barbican/crypto/mime_types.py rename to barbican/plugin/util/mime_types.py diff --git a/barbican/plugin/util/translations.py b/barbican/plugin/util/translations.py new file mode 100644 index 000000000..2e4fea2fd --- /dev/null +++ b/barbican/plugin/util/translations.py @@ -0,0 +1,78 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +import base64 + +from barbican.plugin.interface import secret_store as s +from barbican.plugin.util import mime_types + + +def normalize_before_encryption(unencrypted, content_type, content_encoding, + enforce_text_only=False): + """Normalize unencrypted prior to plugin encryption processing.""" + if not unencrypted: + raise s.SecretNoPayloadProvidedException() + + # Validate and normalize content-type. + normalized_mime = mime_types.normalize_content_type(content_type) + if not mime_types.is_supported(normalized_mime): + raise s.SecretContentTypeNotSupportedException(content_type) + + # Process plain-text type. + if normalized_mime in mime_types.PLAIN_TEXT: + # normalize text to binary string + unencrypted = unencrypted.encode('utf-8') + + # Process binary type. + else: + # payload has to be decoded + if mime_types.is_base64_processing_needed(content_type, + content_encoding): + try: + unencrypted = base64.b64decode(unencrypted) + except TypeError: + raise s.SecretPayloadDecodingError() + elif enforce_text_only: + # For text-based protocols (such as the one-step secret POST), + # only 'base64' encoding is possible/supported. + raise s.SecretContentEncodingMustBeBase64() + elif content_encoding: + # Unsupported content-encoding request. + raise s.SecretContentEncodingNotSupportedException( + content_encoding + ) + + return unencrypted, normalized_mime + + +def analyze_before_decryption(content_type): + """Determine support for desired content type.""" + if not mime_types.is_supported(content_type): + raise s.SecretAcceptNotSupportedException(content_type) + + +def denormalize_after_decryption(unencrypted, content_type): + """Translate the decrypted data into the desired content type.""" + # Process plain-text type. + if content_type in mime_types.PLAIN_TEXT: + # normalize text to binary string + try: + unencrypted = unencrypted.decode('utf-8') + except UnicodeDecodeError: + raise s.SecretAcceptNotSupportedException(content_type) + + # Process binary type. + elif content_type not in mime_types.BINARY: + raise s.SecretContentTypeNotSupportedException(content_type) + + return unencrypted diff --git a/barbican/tasks/resources.py b/barbican/tasks/resources.py index 4325ef92f..ad404e4c2 100644 --- a/barbican/tasks/resources.py +++ b/barbican/tasks/resources.py @@ -21,12 +21,12 @@ import abc import six from barbican import api -from barbican.common import resources as res from barbican.common import utils -from barbican.crypto import extension_manager as em from barbican.model import models from barbican.model import repositories as rep from barbican.openstack.common import gettextutils as u +from barbican.plugin import resources as plugin + LOG = utils.getLogger(__name__) @@ -145,21 +145,21 @@ class BeginOrder(BaseTask): def get_name(self): return u._('Create Secret') - def __init__(self, crypto_manager=None, tenant_repo=None, order_repo=None, + def __init__(self, tenant_repo=None, order_repo=None, secret_repo=None, tenant_secret_repo=None, - datum_repo=None, kek_repo=None): + datum_repo=None, kek_repo=None, secret_meta_repo=None): LOG.debug('Creating BeginOrder task processor') - self.order_repo = order_repo or rep.OrderRepo() - self.tenant_repo = tenant_repo or rep.TenantRepo() - self.secret_repo = secret_repo or rep.SecretRepo() - self.tenant_secret_repo = tenant_secret_repo or rep.TenantSecretRepo() - self.datum_repo = datum_repo or rep.EncryptedDatumRepo() - self.kek_repo = kek_repo or rep.KEKDatumRepo() - self.crypto_manager = crypto_manager or em.CryptoExtensionManager() + self.repos = rep.Repositories(tenant_repo=tenant_repo, + tenant_secret_repo=tenant_secret_repo, + secret_repo=secret_repo, + datum_repo=datum_repo, + kek_repo=kek_repo, + secret_meta_repo=secret_meta_repo, + order_repo=order_repo) def retrieve_entity(self, order_id, keystone_id): - return self.order_repo.get(entity_id=order_id, - keystone_id=keystone_id) + return self.repos.order_repo.get(entity_id=order_id, + keystone_id=keystone_id) def handle_processing(self, order, *args, **kwargs): self.handle_order(order) @@ -169,11 +169,11 @@ class BeginOrder(BaseTask): order.status = models.States.ERROR order.error_status_code = status order.error_reason = message - self.order_repo.save(order) + self.repos.order_repo.save(order) def handle_success(self, order, *args, **kwargs): order.status = models.States.ACTIVE - self.order_repo.save(order) + self.repos.order_repo.save(order) def handle_order(self, order): """Handle secret creation. @@ -188,14 +188,15 @@ class BeginOrder(BaseTask): secret_info = order_info['secret'] # Retrieve the tenant. - tenant = self.tenant_repo.get(order.tenant_id) + tenant = self.repos.tenant_repo.get(order.tenant_id) # Create Secret - new_secret = res.create_secret(secret_info, tenant, - self.crypto_manager, self.secret_repo, - self.tenant_secret_repo, - self.datum_repo, self.kek_repo, - ok_to_generate=True) + new_secret = plugin.\ + generate_secret(secret_info, + secret_info.get('payload_content_type', + 'application/octet-stream'), + tenant, self.repos) + order.secret_id = new_secret.id LOG.debug("...done creating order's secret.") diff --git a/barbican/tests/api/test_resources.py b/barbican/tests/api/test_resources.py index a1d2efbb1..be22f90b5 100644 --- a/barbican/tests/api/test_resources.py +++ b/barbican/tests/api/test_resources.py @@ -20,6 +20,7 @@ resource classes. For RBAC tests of these classes, see the """ import base64 +import logging import urllib import mimetypes @@ -32,11 +33,12 @@ from barbican import api from barbican.api import app from barbican.api import controllers from barbican.common import exception as excep -from barbican.common import utils from barbican.common import validators -from barbican.crypto import extension_manager as em from barbican.model import models -from barbican.tests.crypto import test_plugin as ctp +from barbican.openstack.common import timeutils + + +LOG = logging.getLogger(__name__) def create_secret(id_ref="id", name="name", @@ -166,8 +168,9 @@ class BaseSecretsResource(FunctionalTest): class RootController(object): secrets = controllers.secrets.SecretsController( - self.crypto_mgr, self.tenant_repo, self.secret_repo, - self.tenant_secret_repo, self.datum_repo, self.kek_repo + self.tenant_repo, self.secret_repo, + self.tenant_secret_repo, self.datum_repo, self.kek_repo, + self.secret_meta_repo ) return RootController() @@ -202,8 +205,10 @@ class BaseSecretsResource(FunctionalTest): self.tenant_repo = mock.MagicMock() self.tenant_repo.find_by_keystone_id.return_value = self.tenant + self.secret = models.Secret() + self.secret.id = '123' self.secret_repo = mock.MagicMock() - self.secret_repo.create_from.return_value = None + self.secret_repo.create_from.return_value = self.secret self.tenant_secret_repo = mock.MagicMock() self.tenant_secret_repo.create_from.return_value = None @@ -212,19 +217,22 @@ class BaseSecretsResource(FunctionalTest): self.datum_repo.create_from.return_value = None self.kek_datum = models.KEKDatum() - self.kek_datum.plugin_name = utils.generate_fullname_for( - ctp.TestCryptoPlugin()) self.kek_datum.kek_label = "kek_label" self.kek_datum.bind_completed = False + self.kek_datum.algorithm = '' + self.kek_datum.bit_length = 0 + self.kek_datum.mode = '' + self.kek_datum.plugin_meta = '' + self.kek_repo = mock.MagicMock() - self.kek_repo.find_or_create_kek_metadata.return_value = self.kek_datum + self.kek_repo.find_or_create_kek_datum.return_value = self.kek_datum - self.conf = mock.MagicMock() - self.conf.crypto.namespace = 'barbican.test.crypto.plugin' - self.conf.crypto.enabled_crypto_plugins = ['test_crypto'] - self.crypto_mgr = em.CryptoExtensionManager(conf=self.conf) + self.secret_meta_repo = mock.MagicMock() + + @mock.patch('barbican.plugin.resources.store_secret') + def _test_should_add_new_secret_with_expiration(self, mock_store_secret): + mock_store_secret.return_value = self.secret - def _test_should_add_new_secret_with_expiration(self): expiration = '2114-02-28 12:14:44.180394-05:00' self.secret_req.update({'expiration': expiration}) @@ -235,45 +243,48 @@ class BaseSecretsResource(FunctionalTest): self.assertEqual(resp.status_int, 201) - args, kwargs = self.secret_repo.create_from.call_args - secret = args[0] - expected = expiration[:-6].replace('12', '17', 1) - self.assertEqual(expected, str(secret.expiration)) + # Validation replaces the time. + expected = dict(self.secret_req) + expiration_raw = expected['expiration'] + expiration_raw = expiration_raw[:-6].replace('12', '17', 1) + expiration_tz = timeutils.parse_isotime(expiration_raw.strip()) + expected['expiration'] = timeutils.normalize_time(expiration_tz) + mock_store_secret\ + .assert_called_once_with( + self.secret_req.get('payload'), + self.secret_req.get('payload_content_type', + 'application/octet-stream'), + self.secret_req.get('payload_content_encoding'), + expected, None, self.tenant, mock.ANY + ) - def _test_should_add_new_secret_one_step(self, check_tenant_id=True): + @mock.patch('barbican.plugin.resources.store_secret') + def _test_should_add_new_secret_one_step(self, mock_store_secret, + check_tenant_id=True): """Test the one-step secret creation. :param check_tenant_id: True if the retrieved Tenant id needs to be verified, False to skip this check (necessary for new-Tenant flows). """ + mock_store_secret.return_value = self.secret + resp = self.app.post_json( '/%s/secrets/' % self.keystone_id, self.secret_req ) self.assertEqual(resp.status_int, 201) - args, kwargs = self.secret_repo.create_from.call_args - secret = args[0] - self.assertIsInstance(secret, models.Secret) - self.assertEqual(secret.name, self.name) - self.assertEqual(secret.algorithm, self.secret_algorithm) - self.assertEqual(secret.bit_length, self.secret_bit_length) - self.assertEqual(secret.mode, self.secret_mode) - - args, kwargs = self.tenant_secret_repo.create_from.call_args - tenant_secret = args[0] - self.assertIsInstance(tenant_secret, models.TenantSecret) - if check_tenant_id: - self.assertEqual(tenant_secret.tenant_id, self.tenant_entity_id) - self.assertEqual(tenant_secret.secret_id, secret.id) - - args, kwargs = self.datum_repo.create_from.call_args - datum = args[0] - self.assertIsInstance(datum, models.EncryptedDatum) - self.assertEqual(base64.b64encode('cypher_text'), datum.cypher_text) - self.assertEqual(self.payload_content_type, datum.content_type) - - validate_datum(self, datum) + expected = dict(self.secret_req) + expected['expiration'] = None + mock_store_secret\ + .assert_called_once_with( + self.secret_req.get('payload'), + self.secret_req.get('payload_content_type', + 'application/octet-stream'), + self.secret_req.get('payload_content_encoding'), + expected, None, self.tenant if check_tenant_id else mock.ANY, + mock.ANY + ) def _test_should_add_new_secret_if_tenant_does_not_exist(self): self.tenant_repo.get.return_value = None @@ -305,7 +316,11 @@ class BaseSecretsResource(FunctionalTest): self.assertFalse(self.datum_repo.create_from.called) - def _test_should_add_new_secret_payload_almost_too_large(self): + @mock.patch('barbican.plugin.resources.store_secret') + def _test_should_add_secret_payload_almost_too_large(self, + mock_store_secret): + mock_store_secret.return_value = self.secret + if validators.DEFAULT_MAX_SECRET_BYTES % 4: raise ValueError('Tests currently require max secrets divides by ' '4 evenly, due to base64 encoding.') @@ -382,7 +397,7 @@ class WhenCreatingPlainTextSecretsUsingSecretsResource(BaseSecretsResource): self._test_should_add_new_secret_metadata_without_payload() def test_should_add_new_secret_payload_almost_too_large(self): - self._test_should_add_new_secret_payload_almost_too_large() + self._test_should_add_secret_payload_almost_too_large() def test_should_fail_due_to_payload_too_large(self): self._test_should_fail_due_to_payload_too_large() @@ -405,83 +420,88 @@ class WhenCreatingPlainTextSecretsUsingSecretsResource(BaseSecretsResource): ) self.assertEqual(resp.status_int, 400) - def test_create_secret_content_type_text_plain(self): - # payload_content_type has trailing space - self.secret_req = {'name': self.name, - 'payload_content_type': 'text/plain ', - 'algorithm': self.secret_algorithm, - 'bit_length': self.secret_bit_length, - 'mode': self.secret_mode, - 'payload': self.payload} - - resp = self.app.post_json( - '/%s/secrets/' % self.keystone_id, - self.secret_req - ) - self.assertEqual(resp.status_int, 201) - - self.secret_req = {'name': self.name, - 'payload_content_type': ' text/plain', - 'algorithm': self.secret_algorithm, - 'bit_length': self.secret_bit_length, - 'mode': self.secret_mode, - 'payload': self.payload} - - resp = self.app.post_json( - '/%s/secrets/' % self.keystone_id, - self.secret_req - ) - self.assertEqual(resp.status_int, 201) - - def test_create_secret_content_type_text_plain_space_charset_utf8(self): - # payload_content_type has trailing space - self.secret_req = {'name': self.name, - 'payload_content_type': - 'text/plain; charset=utf-8 ', - 'algorithm': self.secret_algorithm, - 'bit_length': self.secret_bit_length, - 'mode': self.secret_mode, - 'payload': self.payload} - - resp = self.app.post_json( - '/%s/secrets/' % self.keystone_id, - self.secret_req - ) - self.assertEqual(resp.status_int, 201) - - self.secret_req = {'name': self.name, - 'payload_content_type': - ' text/plain; charset=utf-8', - 'algorithm': self.secret_algorithm, - 'bit_length': self.secret_bit_length, - 'mode': self.secret_mode, - 'payload': self.payload} - - resp = self.app.post_json( - '/%s/secrets/' % self.keystone_id, - self.secret_req - ) - self.assertEqual(resp.status_int, 201) - - def test_create_secret_with_only_content_type(self): - # No payload just content_type - self.secret_req = {'payload_content_type': - 'text/plain'} - resp = self.app.post_json( - '/%s/secrets/' % self.keystone_id, - self.secret_req, - expect_errors=True - ) - self.assertEqual(resp.status_int, 400) - - self.secret_req = {'payload_content_type': - 'text/plain', - 'payload': 'somejunk'} - resp = self.app.post_json( - '/%s/secrets/' % self.keystone_id, - self.secret_req - ) - self.assertEqual(resp.status_int, 201) +#TODO(jwood) These tests are integration-style unit tests of the REST -> Pecan +# resources, which are more painful to test now that we have a two-tier +# plugin lookup framework. These tests should instead be re-located to +# unit tests of the secret_store.py and store_crypto.py modules. A separate +# CR will add the additional unit tests. +# def test_create_secret_content_type_text_plain(self): +# # payload_content_type has trailing space +# self.secret_req = {'name': self.name, +# 'payload_content_type': 'text/plain ', +# 'algorithm': self.secret_algorithm, +# 'bit_length': self.secret_bit_length, +# 'mode': self.secret_mode, +# 'payload': self.payload} +# +# resp = self.app.post_json( +# '/%s/secrets/' % self.keystone_id, +# self.secret_req +# ) +# self.assertEqual(resp.status_int, 201) +# +# self.secret_req = {'name': self.name, +# 'payload_content_type': ' text/plain', +# 'algorithm': self.secret_algorithm, +# 'bit_length': self.secret_bit_length, +# 'mode': self.secret_mode, +# 'payload': self.payload} +# +# resp = self.app.post_json( +# '/%s/secrets/' % self.keystone_id, +# self.secret_req +# ) +# self.assertEqual(resp.status_int, 201) +# +# def test_create_secret_content_type_text_plain_space_charset_utf8(self): +# # payload_content_type has trailing space +# self.secret_req = {'name': self.name, +# 'payload_content_type': +# 'text/plain; charset=utf-8 ', +# 'algorithm': self.secret_algorithm, +# 'bit_length': self.secret_bit_length, +# 'mode': self.secret_mode, +# 'payload': self.payload} +# +# resp = self.app.post_json( +# '/%s/secrets/' % self.keystone_id, +# self.secret_req +# ) +# self.assertEqual(resp.status_int, 201) +# +# self.secret_req = {'name': self.name, +# 'payload_content_type': +# ' text/plain; charset=utf-8', +# 'algorithm': self.secret_algorithm, +# 'bit_length': self.secret_bit_length, +# 'mode': self.secret_mode, +# 'payload': self.payload} +# +# resp = self.app.post_json( +# '/%s/secrets/' % self.keystone_id, +# self.secret_req +# ) +# self.assertEqual(resp.status_int, 201) +# +# def test_create_secret_with_only_content_type(self): +# # No payload just content_type +# self.secret_req = {'payload_content_type': +# 'text/plain'} +# resp = self.app.post_json( +# '/%s/secrets/' % self.keystone_id, +# self.secret_req, +# expect_errors=True +# ) +# self.assertEqual(resp.status_int, 400) +# +# self.secret_req = {'payload_content_type': +# 'text/plain', +# 'payload': 'somejunk'} +# resp = self.app.post_json( +# '/%s/secrets/' % self.keystone_id, +# self.secret_req +# ) +# self.assertEqual(resp.status_int, 201) class WhenCreatingBinarySecretsUsingSecretsResource(BaseSecretsResource): @@ -506,7 +526,7 @@ class WhenCreatingBinarySecretsUsingSecretsResource(BaseSecretsResource): self._test_should_add_new_secret_metadata_without_payload() def test_should_add_new_secret_payload_almost_too_large(self): - self._test_should_add_new_secret_payload_almost_too_large() + self._test_should_add_secret_payload_almost_too_large() def test_should_fail_due_to_payload_too_large(self): self._test_should_fail_due_to_payload_too_large() @@ -596,8 +616,9 @@ class WhenGettingSecretsListUsingSecretsResource(FunctionalTest): class RootController(object): secrets = controllers.secrets.SecretsController( - self.crypto_mgr, self.tenant_repo, self.secret_repo, - self.tenant_secret_repo, self.datum_repo, self.kek_repo + self.tenant_repo, self.secret_repo, + self.tenant_secret_repo, self.datum_repo, self.kek_repo, + self.secret_meta_repo ) return RootController() @@ -641,10 +662,7 @@ class WhenGettingSecretsListUsingSecretsResource(FunctionalTest): self.kek_repo = mock.MagicMock() - self.conf = mock.MagicMock() - self.conf.crypto.namespace = 'barbican.test.crypto.plugin' - self.conf.crypto.enabled_crypto_plugins = ['test_crypto'] - self.crypto_mgr = em.CryptoExtensionManager(conf=self.conf) + self.secret_meta_repo = mock.MagicMock() self.params = {'offset': self.offset, 'limit': self.limit, @@ -760,8 +778,9 @@ class WhenGettingPuttingOrDeletingSecretUsingSecretResource(FunctionalTest): class RootController(object): secrets = controllers.secrets.SecretsController( - self.crypto_mgr, self.tenant_repo, self.secret_repo, - self.tenant_secret_repo, self.datum_repo, self.kek_repo + self.tenant_repo, self.secret_repo, + self.tenant_secret_repo, self.datum_repo, self.kek_repo, + self.secret_meta_repo ) return RootController() @@ -784,8 +803,6 @@ class WhenGettingPuttingOrDeletingSecretUsingSecretResource(FunctionalTest): self.kek_tenant.active = True self.kek_tenant.bind_completed = False self.kek_tenant.kek_label = "kek_label" - self.kek_tenant.plugin_name = utils.generate_fullname_for( - ctp.TestCryptoPlugin()) self.datum = models.EncryptedDatum() self.datum.id = datum_id @@ -807,6 +824,7 @@ class WhenGettingPuttingOrDeletingSecretUsingSecretResource(FunctionalTest): self.keystone_id = self.keystone_id self.tenant_repo = mock.MagicMock() self.tenant_repo.get.return_value = self.tenant + self.tenant_repo.find_by_keystone_id.return_value = self.tenant self.secret_repo = mock.MagicMock() self.secret_repo.get.return_value = self.secret @@ -819,10 +837,7 @@ class WhenGettingPuttingOrDeletingSecretUsingSecretResource(FunctionalTest): self.kek_repo = mock.MagicMock() - self.conf = mock.MagicMock() - self.conf.crypto.namespace = 'barbican.test.crypto.plugin' - self.conf.crypto.enabled_crypto_plugins = ['test_crypto'] - self.crypto_mgr = em.CryptoExtensionManager(conf=self.conf) + self.secret_meta_repo = mock.MagicMock() def test_should_get_secret_as_json(self): resp = self.app.get( @@ -841,7 +856,11 @@ class WhenGettingPuttingOrDeletingSecretUsingSecretResource(FunctionalTest): resp.namespace['content_types'].itervalues()) self.assertNotIn('mime_type', resp.namespace) - def test_should_get_secret_as_plain(self): + @mock.patch('barbican.plugin.resources.get_secret') + def test_should_get_secret_as_plain(self, mock_get_secret): + data = 'unencrypted_data' + mock_get_secret.return_value = data + resp = self.app.get( '/%s/secrets/%s/' % (self.keystone_id, self.secret.id), headers={'Accept': 'text/plain'} @@ -853,7 +872,11 @@ class WhenGettingPuttingOrDeletingSecretUsingSecretResource(FunctionalTest): suppress_exception=True) self.assertEqual(resp.status_int, 200) - self.assertIsNotNone(resp.body) + self.assertEqual(resp.body, data) + mock_get_secret\ + .assert_called_once_with('text/plain', + self.secret, + self.tenant) def test_should_get_secret_meta_for_binary(self): self.datum.content_type = "application/octet-stream" @@ -876,7 +899,11 @@ class WhenGettingPuttingOrDeletingSecretUsingSecretResource(FunctionalTest): self.assertIn(self.datum.content_type, resp.namespace['content_types'].itervalues()) - def test_should_get_secret_as_binary(self): + @mock.patch('barbican.plugin.resources.get_secret') + def test_should_get_secret_as_binary(self, mock_get_secret): + data = 'unencrypted_data' + mock_get_secret.return_value = data + self.datum.content_type = "application/octet-stream" self.datum.cypher_text = 'aaaa' @@ -888,7 +915,12 @@ class WhenGettingPuttingOrDeletingSecretUsingSecretResource(FunctionalTest): } ) - self.assertEqual(resp.body, 'unencrypted_data') + self.assertEqual(resp.body, data) + + mock_get_secret\ + .assert_called_once_with('application/octet-stream', + self.secret, + self.tenant) def test_should_throw_exception_for_get_when_secret_not_found(self): self.secret_repo.get.return_value = None @@ -908,17 +940,8 @@ class WhenGettingPuttingOrDeletingSecretUsingSecretResource(FunctionalTest): ) self.assertEqual(resp.status_int, 406) - def test_should_throw_exception_for_get_when_datum_not_available(self): - self.secret.encrypted_data = [] - - resp = self.app.get( - '/%s/secrets/%s/' % (self.keystone_id, self.secret.id), - headers={'Accept': 'text/plain'}, - expect_errors=True - ) - self.assertEqual(resp.status_int, 404) - - def test_should_put_secret_as_plain(self): + @mock.patch('barbican.plugin.resources.store_secret') + def test_should_put_secret_as_plain(self, mock_store_secret): self.secret.encrypted_data = [] resp = self.app.put( @@ -929,14 +952,13 @@ class WhenGettingPuttingOrDeletingSecretUsingSecretResource(FunctionalTest): self.assertEqual(resp.status_int, 204) - args, kwargs = self.datum_repo.create_from.call_args - datum = args[0] - self.assertIsInstance(datum, models.EncryptedDatum) - self.assertEqual(base64.b64encode('cypher_text'), datum.cypher_text) + mock_store_secret\ + .assert_called_once_with('plain text', 'text/plain', + None, self.secret.to_dict_fields, + self.secret, self.tenant, mock.ANY) - validate_datum(self, datum) - - def test_should_put_secret_as_binary(self): + @mock.patch('barbican.plugin.resources.store_secret') + def test_should_put_secret_as_binary(self, mock_store_secret): self.secret.encrypted_data = [] resp = self.app.put( @@ -950,15 +972,18 @@ class WhenGettingPuttingOrDeletingSecretUsingSecretResource(FunctionalTest): self.assertEqual(resp.status_int, 204) - args, kwargs = self.datum_repo.create_from.call_args - datum = args[0] - self.assertIsInstance(datum, models.EncryptedDatum) + mock_store_secret\ + .assert_called_once_with('plain text', 'application/octet-stream', + None, self.secret.to_dict_fields, + self.secret, self.tenant, mock.ANY) - def test_should_put_encoded_secret_as_binary(self): + @mock.patch('barbican.plugin.resources.store_secret') + def test_should_put_encoded_secret_as_binary(self, mock_store_secret): self.secret.encrypted_data = [] + payload = base64.b64encode('plain text') resp = self.app.put( '/%s/secrets/%s/' % (self.keystone_id, self.secret.id), - base64.b64encode('plain text'), + payload, headers={ 'Accept': 'text/plain', 'Content-Type': 'application/octet-stream', @@ -968,6 +993,11 @@ class WhenGettingPuttingOrDeletingSecretUsingSecretResource(FunctionalTest): self.assertEqual(resp.status_int, 204) + mock_store_secret\ + .assert_called_once_with(payload, 'application/octet-stream', + 'base64', self.secret.to_dict_fields, + self.secret, self.tenant, mock.ANY) + def test_should_fail_to_put_secret_with_unsupported_encoding(self): self.secret.encrypted_data = [] resp = self.app.put( @@ -1059,17 +1089,17 @@ class WhenGettingPuttingOrDeletingSecretUsingSecretResource(FunctionalTest): ) self.assertEqual(resp.status_int, 413) - def test_should_delete_secret(self): + @mock.patch('barbican.plugin.resources.delete_secret') + def test_should_delete_secret(self, mock_delete_secret): self.app.delete( '/%s/secrets/%s/' % (self.keystone_id, self.secret.id) ) - self.secret_repo.delete_entity_by_id \ - .assert_called_once_with(entity_id=self.secret.id, - keystone_id=self.keystone_id) + + mock_delete_secret\ + .assert_called_once_with(self.secret, self.keystone_id, mock.ANY) def test_should_throw_exception_for_delete_when_secret_not_found(self): - self.secret_repo.delete_entity_by_id.side_effect = excep.NotFound( - "Test not found exception") + self.secret_repo.get.return_value = None resp = self.app.delete( '/%s/secrets/%s/' % (self.keystone_id, self.secret.id), diff --git a/barbican/tests/api/test_resources_policy.py b/barbican/tests/api/test_resources_policy.py index ca9cb8390..834278e7a 100644 --- a/barbican/tests/api/test_resources_policy.py +++ b/barbican/tests/api/test_resources_policy.py @@ -235,13 +235,13 @@ class WhenTestingSecretsResource(BaseTestCase): ._generate_get_error()) self.secret_repo.get_by_create_date = get_by_create_date - self.resource = SecretsResource(crypto_manager=mock.MagicMock(), - tenant_repo=mock.MagicMock(), + self.resource = SecretsResource(tenant_repo=mock.MagicMock(), secret_repo=self.secret_repo, tenant_secret_repo=mock .MagicMock(), datum_repo=mock.MagicMock(), - kek_repo=mock.MagicMock()) + kek_repo=mock.MagicMock(), + secret_meta_repo=mock.MagicMock()) def test_rules_should_be_loaded(self): self.assertIsNotNone(self.policy_enforcer.rules) @@ -285,7 +285,6 @@ class WhenTestingSecretResource(BaseTestCase): self.secret_repo.delete_entity_by_id = fail_method self.resource = SecretResource(self.secret_id, - crypto_manager=mock.MagicMock(), tenant_repo=mock.MagicMock(), secret_repo=self.secret_repo, datum_repo=mock.MagicMock(), diff --git a/barbican/tests/crypto/test_dogtag_crypto.py b/barbican/tests/crypto/test_dogtag_crypto.py deleted file mode 100644 index 25af09927..000000000 --- a/barbican/tests/crypto/test_dogtag_crypto.py +++ /dev/null @@ -1,203 +0,0 @@ -# Copyright (c) 2014 Red Hat, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import mock -import os -import tempfile -import testtools - -try: - import barbican.crypto.dogtag_crypto as dogtag_import - from barbican.crypto import plugin as plugin_import - from barbican.model import models - imports_ok = True -except ImportError: - # dogtag imports probably not available - imports_ok = False - - -class WhenTestingDogtagCryptoPlugin(testtools.TestCase): - - def setUp(self): - super(WhenTestingDogtagCryptoPlugin, self).setUp() - if not imports_ok: - return - - self.keyclient_mock = mock.MagicMock(name="KeyClient mock") - self.patcher = mock.patch('pki.cryptoutil.NSSCryptoUtil') - self.patcher.start() - - # create nss db for test only - self.nss_dir = tempfile.mkdtemp() - - self.cfg_mock = mock.MagicMock(name='config mock') - self.cfg_mock.dogtag_crypto_plugin = mock.MagicMock( - nss_db_path=self.nss_dir) - self.plugin = dogtag_import.DogtagCryptoPlugin(self.cfg_mock) - self.plugin.keyclient = self.keyclient_mock - - def tearDown(self): - super(WhenTestingDogtagCryptoPlugin, self).tearDown() - if not imports_ok: - return - self.patcher.stop() - os.rmdir(self.nss_dir) - - def test_generate(self): - if not imports_ok: - self.skipTest("Dogtag imports not available") - secret = models.Secret() - secret.bit_length = 128 - secret.algorithm = "AES" - generate_dto = plugin_import.GenerateDTO( - secret.algorithm, - secret.bit_length, - None, - None) - self.plugin.generate_symmetric( - generate_dto, - mock.MagicMock(), - mock.MagicMock() - ) - - self.keyclient_mock.generate_symmetric_key.assert_called_once_with( - mock.ANY, - secret.algorithm.upper(), - secret.bit_length, - mock.ANY) - - def test_generate_non_supported_algorithm(self): - if not imports_ok: - self.skipTest("Dogtag imports not available") - secret = models.Secret() - secret.bit_length = 128 - secret.algorithm = "hmacsha256" - generate_dto = plugin_import.GenerateDTO( - plugin_import.PluginSupportTypes.SYMMETRIC_KEY_GENERATION, - secret.algorithm, - secret.bit_length, - None) - self.assertRaises( - dogtag_import.DogtagPluginAlgorithmException, - self.plugin.generate_symmetric, - generate_dto, - mock.MagicMock(), - mock.MagicMock() - ) - - def test_raises_error_with_no_pem_path(self): - if not imports_ok: - self.skipTest("Dogtag imports not available") - m = mock.MagicMock() - m.dogtag_crypto_plugin = mock.MagicMock(pem_path=None) - self.assertRaises( - ValueError, - dogtag_import.DogtagCryptoPlugin, - m, - ) - - def test_raises_error_with_no_pem_password(self): - if not imports_ok: - self.skipTest("Dogtag imports not available") - m = mock.MagicMock() - m.dogtag_crypto_plugin = mock.MagicMock(pem_password=None) - self.assertRaises( - ValueError, - dogtag_import.DogtagCryptoPlugin, - m, - ) - - def test_raises_error_with_no_nss_password(self): - if not imports_ok: - self.skipTest("Dogtag imports not available") - m = mock.MagicMock() - m.dogtag_crypto_plugin = mock.MagicMock(nss_password=None) - self.assertRaises( - ValueError, - dogtag_import.DogtagCryptoPlugin, - m, - ) - - def test_encrypt(self): - if not imports_ok: - self.skipTest("Dogtag imports not available") - payload = 'encrypt me!!' - encrypt_dto = plugin_import.EncryptDTO(payload) - self.plugin.encrypt(encrypt_dto, - mock.MagicMock(), - mock.MagicMock()) - self.keyclient_mock.archive_key.assert_called_once_with( - mock.ANY, - "passPhrase", - payload, - key_algorithm=None, - key_size=None) - - def test_decrypt(self): - if not imports_ok: - self.skipTest("Dogtag imports not available") - key_id = 'key1' - decrypt_dto = plugin_import.DecryptDTO(key_id) - self.plugin.decrypt(decrypt_dto, - mock.MagicMock(), - mock.MagicMock(), - mock.MagicMock()) - - self.keyclient_mock.retrieve_key.assert_called_once_with(key_id) - - def test_supports_encrypt_decrypt(self): - if not imports_ok: - self.skipTest("Dogtag imports not available") - self.assertTrue( - self.plugin.supports( - plugin_import.PluginSupportTypes.ENCRYPT_DECRYPT - ) - ) - - def test_supports_symmetric_key_generation(self): - if not imports_ok: - self.skipTest("Dogtag imports not available") - self.assertTrue( - self.plugin.supports( - plugin_import.PluginSupportTypes.SYMMETRIC_KEY_GENERATION, - 'aes', 256 - ) - ) - - def test_supports_symmetric_hmacsha256_key_generation(self): - if not imports_ok: - self.skipTest("Dogtag imports not available") - self.assertFalse( - self.plugin.supports( - plugin_import.PluginSupportTypes.SYMMETRIC_KEY_GENERATION, - 'hmacsha256', 128 - ) - ) - - def test_supports_asymmetric_key_generation(self): - if not imports_ok: - self.skipTest("Dogtag imports not available") - self.assertFalse( - self.plugin.supports( - plugin_import.PluginSupportTypes.ASYMMETRIC_KEY_GENERATION - ) - ) - - def test_does_not_support_unknown_type(self): - if not imports_ok: - self.skipTest("Dogtag imports not available") - self.assertFalse( - self.plugin.supports("SOMETHING_RANDOM") - ) diff --git a/barbican/tests/crypto/test_extension_manager.py b/barbican/tests/crypto/test_extension_manager.py deleted file mode 100644 index d3d547e46..000000000 --- a/barbican/tests/crypto/test_extension_manager.py +++ /dev/null @@ -1,477 +0,0 @@ -# Copyright (c) 2013-2014 Rackspace, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import base64 - -import mock -import testtools - -from barbican.crypto import extension_manager as em -from barbican.crypto import mime_types as mt -from barbican.crypto import plugin - - -def get_mocked_kek_repo(): - # For SimpleCryptoPlugin, per-tenant KEKs are stored in - # kek_meta_dto.plugin_meta. SimpleCryptoPlugin does a get-or-create - # on the plugin_meta field, so plugin_meta should be None initially. - kek_datum = mock.MagicMock() - kek_datum.plugin_meta = None - kek_datum.bind_completed = False - kek_repo = mock.MagicMock(name='kek_repo') - kek_repo.find_or_create_kek_datum = mock.MagicMock() - kek_repo.find_or_create_kek_datum.return_value = kek_datum - return kek_repo - - -class TestSupportsCryptoPlugin(plugin.CryptoPluginBase): - """Crypto plugin for testing supports.""" - - def encrypt(self, encrypt_dto, kek_meta_dto, tenant): - raise NotImplementedError() - - def decrypt(self, decrypt_dto, kek_meta_dto, kek_meta_extended, tenant): - raise NotImplementedError() - - def bind_kek_metadata(self, kek_meta_dto): - return None - - def generate_symmetric(self, generate_dto, - kek_meta_dto, keystone_id): - raise NotImplementedError() - - def generate_asymmetric(self, generate_dto, - kek_meta_dto, keystone_id): - raise NotImplementedError("Feature not implemented for PKCS11") - - def supports(self, type_enum, algorithm=None, bit_length=None, mode=None): - return False - - -class WhenTestingNormalizeBeforeEncryptionForBinary(testtools.TestCase): - - def setUp(self): - super(WhenTestingNormalizeBeforeEncryptionForBinary, self).setUp() - self.unencrypted = 'AAAAAAAA' - self.content_type = 'application/octet-stream' - self.content_encoding = 'base64' - self.enforce_text_only = False - - def test_encrypt_binary_from_base64(self): - unenc, content = em.normalize_before_encryption(self.unencrypted, - self.content_type, - self.content_encoding, - self.enforce_text_only) - self.assertEqual(self.content_type, content) - self.assertEqual(base64.b64decode(self.unencrypted), unenc) - - def test_encrypt_binary_directly(self): - self.content_encoding = None - unenc, content = em.normalize_before_encryption(self.unencrypted, - self.content_type, - self.content_encoding, - self.enforce_text_only) - self.assertEqual(self.content_type, content) - self.assertEqual(self.unencrypted, unenc) - - def test_encrypt_fail_binary_unknown_encoding(self): - self.content_encoding = 'gzip' - - ex = self.assertRaises( - em.CryptoContentEncodingNotSupportedException, - em.normalize_before_encryption, - self.unencrypted, - self.content_type, - self.content_encoding, - self.enforce_text_only, - ) - self.assertEqual(self.content_encoding, ex.content_encoding) - - def test_encrypt_fail_binary_force_text_based_no_encoding(self): - self.content_encoding = None - self.enforce_text_only = True - self.assertRaises( - em.CryptoContentEncodingMustBeBase64, - em.normalize_before_encryption, - self.unencrypted, - self.content_type, - self.content_encoding, - self.enforce_text_only, - ) - - def test_encrypt_fail_unknown_content_type(self): - self.content_type = 'bogus' - ex = self.assertRaises( - em.CryptoContentTypeNotSupportedException, - em.normalize_before_encryption, - self.unencrypted, - self.content_type, - self.content_encoding, - self.enforce_text_only, - ) - self.assertEqual(self.content_type, ex.content_type) - - -class WhenTestingNormalizeBeforeEncryptionForText(testtools.TestCase): - - def setUp(self): - super(WhenTestingNormalizeBeforeEncryptionForText, self).setUp() - - self.unencrypted = 'AAAAAAAA' - self.content_type = 'text/plain' - self.content_encoding = 'base64' - self.enforce_text_only = False - - def test_encrypt_text_ignore_encoding(self): - unenc, content = em.normalize_before_encryption(self.unencrypted, - self.content_type, - self.content_encoding, - self.enforce_text_only) - self.assertEqual(self.content_type, content) - self.assertEqual(self.unencrypted, unenc) - - def test_encrypt_text_not_normalized_ignore_encoding(self): - self.content_type = 'text/plain;charset=utf-8' - unenc, content = em.normalize_before_encryption(self.unencrypted, - self.content_type, - self.content_encoding, - self.enforce_text_only) - self.assertEqual(mt.normalize_content_type(self.content_type), - content) - self.assertEqual(self.unencrypted.encode('utf-8'), unenc) - - def test_raises_on_bogus_content_type(self): - content_type = 'text/plain; charset=ISO-8859-1' - - self.assertRaises( - em.CryptoContentTypeNotSupportedException, - em.normalize_before_encryption, - self.unencrypted, - content_type, - self.content_encoding, - self.enforce_text_only - ) - - def test_raises_on_no_payload(self): - content_type = 'text/plain; charset=ISO-8859-1' - self.assertRaises( - em.CryptoNoPayloadProvidedException, - em.normalize_before_encryption, - None, - content_type, - self.content_encoding, - self.enforce_text_only - ) - - -class WhenTestingAnalyzeBeforeDecryption(testtools.TestCase): - - def setUp(self): - super(WhenTestingAnalyzeBeforeDecryption, self).setUp() - - self.content_type = 'application/octet-stream' - - def test_decrypt_fail_bogus_content_type(self): - self.content_type = 'bogus' - - ex = self.assertRaises( - em.CryptoAcceptNotSupportedException, - em.analyze_before_decryption, - self.content_type, - ) - self.assertEqual(self.content_type, ex.accept) - - -class WhenTestingDenormalizeAfterDecryption(testtools.TestCase): - - def setUp(self): - super(WhenTestingDenormalizeAfterDecryption, self).setUp() - - self.unencrypted = 'AAAAAAAA' - self.content_type = 'application/octet-stream' - - def test_decrypt_fail_binary(self): - unenc = em.denormalize_after_decryption(self.unencrypted, - self.content_type) - self.assertEqual(self.unencrypted, unenc) - - def test_decrypt_text(self): - self.content_type = 'text/plain' - unenc = em.denormalize_after_decryption(self.unencrypted, - self.content_type) - self.assertEqual(self.unencrypted.decode('utf-8'), unenc) - - def test_decrypt_fail_unknown_content_type(self): - self.content_type = 'bogus' - self.assertRaises( - em.CryptoGeneralException, - em.denormalize_after_decryption, - self.unencrypted, - self.content_type, - ) - - def test_decrypt_fail_binary_as_plain(self): - self.unencrypted = '\xff' - self.content_type = 'text/plain' - self.assertRaises( - em.CryptoAcceptNotSupportedException, - em.denormalize_after_decryption, - self.unencrypted, - self.content_type, - ) - - -class WhenTestingCryptoExtensionManager(testtools.TestCase): - - def setUp(self): - super(WhenTestingCryptoExtensionManager, self).setUp() - self.manager = em.CryptoExtensionManager() - - def test_create_supported_algorithm(self): - skg = plugin.PluginSupportTypes.SYMMETRIC_KEY_GENERATION - self.assertEqual(skg, self.manager._determine_type('AES')) - self.assertEqual(skg, self.manager._determine_type('aes')) - self.assertEqual(skg, self.manager._determine_type('DES')) - self.assertEqual(skg, self.manager._determine_type('des')) - - def test_create_unsupported_algorithm(self): - self.assertRaises( - em.CryptoAlgorithmNotSupportedException, - self.manager._determine_type, - 'faux_alg', - ) - - def test_create_asymmetric_supported_algorithm(self): - skg = plugin.PluginSupportTypes.ASYMMETRIC_KEY_GENERATION - self.assertEqual(skg, self.manager._determine_type('RSA')) - self.assertEqual(skg, self.manager._determine_type('rsa')) - self.assertEqual(skg, self.manager._determine_type('DSA')) - self.assertEqual(skg, self.manager._determine_type('dsa')) - - def test_encrypt_no_plugin_found(self): - self.manager.extensions = [] - self.assertRaises( - em.CryptoPluginNotFound, - self.manager.encrypt, - 'payload', - 'content_type', - 'content_encoding', - mock.MagicMock(), - mock.MagicMock(), - mock.MagicMock(), - ) - - def test_encrypt_no_supported_plugin(self): - plugin = TestSupportsCryptoPlugin() - plugin_mock = mock.MagicMock(obj=plugin) - self.manager.extensions = [plugin_mock] - self.assertRaises( - em.CryptoSupportedPluginNotFound, - self.manager.encrypt, - 'payload', - 'content_type', - 'content_encoding', - mock.MagicMock(), - mock.MagicMock(), - mock.MagicMock(), - ) - - def test_encrypt_response_dto(self): - plg = plugin.SimpleCryptoPlugin() - plugin_mock = mock.MagicMock(obj=plg) - self.manager.extensions = [plugin_mock] - - kek_repo = get_mocked_kek_repo() - - response_dto = self.manager.encrypt( - 'payload', 'text/plain', None, mock.MagicMock(), mock.MagicMock(), - kek_repo, False - ) - - self.assertIsNotNone(response_dto) - - def test_decrypt_no_plugin_found(self): - """Passing mocks here causes CryptoPluginNotFound because the mock - won't match any of the available plugins. - """ - self.assertRaises( - em.CryptoPluginNotFound, - self.manager.decrypt, - 'text/plain', - mock.MagicMock(), - mock.MagicMock(), - ) - - def test_decrypt_no_supported_plugin_found(self): - """Similar to test_decrypt_no_plugin_found, but in this case - no plugin can be found that supports the specified secret's - encrypted data. - """ - fake_secret = mock.MagicMock() - fake_datum = mock.MagicMock() - fake_datum.kek_meta_tenant = mock.MagicMock() - fake_secret.encrypted_data = [fake_datum] - self.assertRaises( - em.CryptoPluginNotFound, - self.manager.decrypt, - 'text/plain', - fake_secret, - mock.MagicMock(), - ) - - def test_generate_data_encryption_key_no_plugin_found(self): - self.manager.extensions = [] - self.assertRaises( - em.CryptoPluginNotFound, - self.manager.generate_symmetric_encryption_key, - mock.MagicMock(), - mock.MagicMock(), - mock.MagicMock(), - mock.MagicMock(), - ) - - def test_generate_symmetric_encryption_key(self): - secret = mock.MagicMock(algorithm='aes', bit_length=128) - content_type = 'application/octet-stream' - tenant = mock.MagicMock() - kek_repo = get_mocked_kek_repo() - - plg = plugin.SimpleCryptoPlugin() - plugin_mock = mock.MagicMock(obj=plg) - self.manager.extensions = [plugin_mock] - - datum = self.manager.generate_symmetric_encryption_key( - secret, content_type, tenant, kek_repo - ) - self.assertIsNotNone(datum) - - def test_generate_data_encryption_key_no_supported_plugin(self): - plugin = TestSupportsCryptoPlugin() - plugin_mock = mock.MagicMock(obj=plugin) - self.manager.extensions = [plugin_mock] - self.assertRaises( - em.CryptoSupportedPluginNotFound, - self.manager.generate_symmetric_encryption_key, - mock.MagicMock(algorithm='AES'), - mock.MagicMock(), - mock.MagicMock(), - mock.MagicMock(), - ) - - def test_find_or_create_kek_objects_bind_returns_none(self): - plugin = TestSupportsCryptoPlugin() - kek_repo = mock.MagicMock(name='kek_repo') - bind_completed = mock.MagicMock(bind_completed=False) - kek_repo.find_or_create_kek_datum.return_value = bind_completed - self.assertRaises( - em.CryptoKEKBindingException, - self.manager._find_or_create_kek_objects, - plugin, - mock.MagicMock(), - kek_repo, - ) - - def test_find_or_create_kek_objects_saves_to_repo(self): - kek_repo = mock.MagicMock(name='kek_repo') - bind_completed = mock.MagicMock(bind_completed=False) - kek_repo.find_or_create_kek_datum.return_value = bind_completed - self.manager._find_or_create_kek_objects( - mock.MagicMock(), - mock.MagicMock(), - kek_repo - ) - self.assertEqual(1, kek_repo.save.call_count) - - def generate_asymmetric_encryption_keys_no_plugin_found(self): - self.manager.extensions = [] - self.assertRaises( - em.CryptoPluginNotFound, - self.manager.generate_asymmetric_encryption_keys, - mock.MagicMock(), - mock.MagicMock(), - mock.MagicMock(), - mock.MagicMock(), - ) - - def generate_asymmetric_encryption_keys_no_supported_plugin(self): - plugin = TestSupportsCryptoPlugin() - plugin_mock = mock.MagicMock(obj=plugin) - self.manager.extensions = [plugin_mock] - self.assertRaises( - em.CryptoSupportedPluginNotFound, - self.manager.generate_asymmetric_encryption_keys, - mock.MagicMock(algorithm='DSA'), - mock.MagicMock(), - mock.MagicMock(), - mock.MagicMock() - ) - - def generate_asymmetric_encryption_rsa_keys_ensure_encoding(self): - plg = plugin.SimpleCryptoPlugin() - plugin_mock = mock.MagicMock(obj=plg) - self.manager.extensions = [plugin_mock] - - meta = mock.MagicMock(algorithm='rsa', - bit_length=1024, - passphrase=None) - - private_datum, public_datum, passphrase_datum = \ - self.manager.generate_asymmetric_encryption_keys(meta, - mock.MagicMock(), - mock.MagicMock(), - mock.MagicMock()) - self.assertIsNotNone(private_datum) - self.assertIsNotNone(public_datum) - self.assertIsNone(passphrase_datum) - - try: - base64.b64decode(private_datum.cypher_text) - base64.b64decode(public_datum.cypher_text) - if passphrase_datum: - base64.b64decode(passphrase_datum.cypher_text) - isB64Encoding = True - except Exception: - isB64Encoding = False - - self.assertTrue(isB64Encoding) - - def generate_asymmetric_encryption_dsa_keys_ensure_encoding(self): - plg = plugin.SimpleCryptoPlugin() - plugin_mock = mock.MagicMock(obj=plg) - self.manager.extensions = [plugin_mock] - - meta = mock.MagicMock(algorithm='rsa', - bit_length=1024, - passphrase=None) - - private_datum, public_datum, passphrase_datum = \ - self.manager.generate_asymmetric_encryption_keys(meta, - mock.MagicMock(), - mock.MagicMock(), - mock.MagicMock()) - self.assertIsNotNone(private_datum) - self.assertIsNotNone(public_datum) - self.assertIsNone(passphrase_datum) - - try: - base64.b64decode(private_datum.cypher_text) - base64.b64decode(public_datum.cypher_text) - if passphrase_datum: - base64.b64decode(passphrase_datum.cypher_text) - isB64Encoding = True - except Exception: - isB64Encoding = False - - self.assertTrue(isB64Encoding) diff --git a/barbican/tests/plugin/crypto/__init__.py b/barbican/tests/plugin/crypto/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/barbican/tests/crypto/test_plugin.py b/barbican/tests/plugin/crypto/test_crypto.py similarity index 98% rename from barbican/tests/crypto/test_plugin.py rename to barbican/tests/plugin/crypto/test_crypto.py index eed93e43d..9c15208dc 100644 --- a/barbican/tests/crypto/test_plugin.py +++ b/barbican/tests/plugin/crypto/test_crypto.py @@ -24,8 +24,9 @@ import mock import six import testtools -from barbican.crypto import plugin from barbican.model import models +from barbican.plugin.crypto import crypto as plugin +from barbican.plugin.crypto import simple_crypto as simple class TestCryptoPlugin(plugin.CryptoPluginBase): @@ -67,7 +68,7 @@ class WhenTestingSimpleCryptoPlugin(testtools.TestCase): def setUp(self): super(WhenTestingSimpleCryptoPlugin, self).setUp() - self.plugin = plugin.SimpleCryptoPlugin() + self.plugin = simple.SimpleCryptoPlugin() def _get_mocked_kek_meta_dto(self): # For SimpleCryptoPlugin, per-tenant KEKs are stored in diff --git a/barbican/tests/crypto/test_p11_crypto.py b/barbican/tests/plugin/crypto/test_p11_crypto.py similarity index 98% rename from barbican/tests/crypto/test_p11_crypto.py rename to barbican/tests/plugin/crypto/test_p11_crypto.py index 641e48eaf..d3ddfade3 100644 --- a/barbican/tests/crypto/test_p11_crypto.py +++ b/barbican/tests/plugin/crypto/test_p11_crypto.py @@ -17,9 +17,9 @@ import mock import testtools -from barbican.crypto import p11_crypto -from barbican.crypto import plugin as plugin_import from barbican.model import models +from barbican.plugin.crypto import crypto as plugin_import +from barbican.plugin.crypto import p11_crypto class WhenTestingP11CryptoPlugin(testtools.TestCase): @@ -29,7 +29,7 @@ class WhenTestingP11CryptoPlugin(testtools.TestCase): self.p11_mock = mock.MagicMock(CKR_OK=0, CKF_RW_SESSION='RW', name='PyKCS11 mock') - self.patcher = mock.patch('barbican.crypto.p11_crypto.PyKCS11', + self.patcher = mock.patch('barbican.plugin.crypto.p11_crypto.PyKCS11', new=self.p11_mock) self.patcher.start() self.pkcs11 = self.p11_mock.PyKCS11Lib() diff --git a/barbican/tests/plugin/util/__init__.py b/barbican/tests/plugin/util/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/barbican/tests/crypto/test_mime_types.py b/barbican/tests/plugin/util/test_mime_types.py similarity index 99% rename from barbican/tests/crypto/test_mime_types.py rename to barbican/tests/plugin/util/test_mime_types.py index 0832be612..5076e91c6 100644 --- a/barbican/tests/crypto/test_mime_types.py +++ b/barbican/tests/plugin/util/test_mime_types.py @@ -15,8 +15,8 @@ import testtools -from barbican.crypto import mime_types from barbican.model import models +from barbican.plugin.util import mime_types class WhenTestingIsBase64ProcessingNeeded(testtools.TestCase): diff --git a/barbican/tests/tasks/test_resources.py b/barbican/tests/tasks/test_resources.py index 7f673021c..3038e2e0c 100644 --- a/barbican/tests/tasks/test_resources.py +++ b/barbican/tests/tasks/test_resources.py @@ -16,7 +16,6 @@ import mock import testtools -from barbican.crypto import extension_manager as em from barbican.model import models from barbican.openstack.common import gettextutils as u from barbican.openstack.common import timeutils @@ -61,6 +60,8 @@ class WhenBeginningOrder(testtools.TestCase): self.order_repo = mock.MagicMock() self.order_repo.get.return_value = self.order + self.secret = models.Secret() + self.secret_repo = mock.MagicMock() self.secret_repo.create_from.return_value = None @@ -72,18 +73,19 @@ class WhenBeginningOrder(testtools.TestCase): self.kek_repo = mock.MagicMock() - self.conf = mock.MagicMock() - self.conf.crypto.namespace = 'barbican.test.crypto.plugin' - self.conf.crypto.enabled_crypto_plugins = ['test_crypto'] - self.crypto_mgr = em.CryptoExtensionManager(conf=self.conf) + self.secret_meta_repo = mock.MagicMock() - self.resource = resources.BeginOrder(self.crypto_mgr, - self.tenant_repo, self.order_repo, + self.resource = resources.BeginOrder(self.tenant_repo, self.order_repo, self.secret_repo, self.tenant_secret_repo, - self.datum_repo, self.kek_repo) + self.datum_repo, self.kek_repo, + self.secret_meta_repo + ) + + @mock.patch('barbican.plugin.resources.generate_secret') + def test_should_process_order(self, mock_generate_secret): + mock_generate_secret.return_value = self.secret - def test_should_process_order(self): self.resource.process(self.order.id, self.keystone_id) self.order_repo.get \ @@ -91,28 +93,14 @@ class WhenBeginningOrder(testtools.TestCase): keystone_id=self.keystone_id) self.assertEqual(self.order.status, models.States.ACTIVE) - args, kwargs = self.secret_repo.create_from.call_args - secret = args[0] - self.assertIsInstance(secret, models.Secret) - self.assertEqual(secret.name, self.secret_name) - self.assertEqual(secret.expiration, self.secret_expiration.isoformat()) - - args, kwargs = self.tenant_secret_repo.create_from.call_args - tenant_secret = args[0] - self.assertIsInstance(tenant_secret, models.TenantSecret) - self.assertEqual(tenant_secret.tenant_id, self.tenant_id) - self.assertEqual(tenant_secret.secret_id, secret.id) - - args, kwargs = self.datum_repo.create_from.call_args - datum = args[0] - self.assertIsInstance(datum, models.EncryptedDatum) - self.assertIsNotNone(datum.cypher_text) - - self.assertIsNone(datum.kek_meta_extended) - self.assertIsNotNone(datum.kek_meta_tenant) - self.assertTrue(datum.kek_meta_tenant.bind_completed) - self.assertIsNotNone(datum.kek_meta_tenant.plugin_name) - self.assertIsNotNone(datum.kek_meta_tenant.kek_label) + secret_info = self.order.to_dict_fields()['secret'] + mock_generate_secret\ + .assert_called_once_with( + secret_info, + secret_info.get('payload_content_type', + 'application/octet-stream'), + self.tenant, mock.ANY + ) def test_should_fail_during_retrieval(self): # Force an error during the order retrieval phase. @@ -146,7 +134,11 @@ class WhenBeginningOrder(testtools.TestCase): self.assertEqual(u._('Create Secret failure seen - please contact ' 'site administrator.'), self.order.error_reason) - def test_should_fail_during_success_report_fail(self): + @mock.patch('barbican.plugin.resources.generate_secret') + def test_should_fail_during_success_report_fail(self, + mock_generate_secret): + mock_generate_secret.return_value = self.secret + # Force an error during the processing handler phase. self.order_repo.save = mock.MagicMock(return_value=None, side_effect=ValueError()) diff --git a/setup.cfg b/setup.cfg index 681d93cad..70f291afb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,14 +27,14 @@ scripts = bin/barbican-db-manage.py [entry_points] +barbican.secretstore.plugin = + store_crypto = barbican.plugin.store_crypto:StoreCryptoAdapterPlugin + dogtag_crypto = barbican.plugin.dogtag:DogtagPlugin barbican.crypto.plugin = - p11_crypto = barbican.crypto.p11_crypto:P11CryptoPlugin - simple_crypto = barbican.crypto.plugin:SimpleCryptoPlugin - dogtag_crypto = barbican.crypto.dogtag_crypto:DogtagCryptoPlugin + p11_crypto = barbican.plugin.crypto.p11_crypto:P11CryptoPlugin + simple_crypto = barbican.plugin.crypto.simple_crypto:SimpleCryptoPlugin barbican.test.crypto.plugin = test_crypto = barbican.tests.crypto.test_plugin:TestCryptoPlugin -barbican.secretstore.plugin = - dogtag_crypto = barbican.plugin.dogtag:DogtagPlugin [build_sphinx] all_files = 1