Move crypto package contents to the new plugin structure
Move the HSM-style crypto source modules into the new plugin structure, and integrate this into the rest of the Barbican API and worker source flows, via the secret-store to HSM-style plugin adapter store_crypto.py. Removed crypto source and unit test files from the old locations. Implements: blueprint restructure-for-plugins Change-Id: Ie31531ac71a32b14598b903632e8a3127a263e56
This commit is contained in:
parent
68a2309949
commit
82561c3653
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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(
|
||||
|
@ -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__)
|
||||
|
@ -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.
|
||||
"""
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
||||
|
@ -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).
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
200
barbican/plugin/crypto/simple_crypto.py
Normal file
200
barbican/plugin/crypto/simple_crypto.py
Normal file
@ -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
|
@ -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.
|
||||
|
||||
|
185
barbican/plugin/resources.py
Normal file
185
barbican/plugin/resources.py
Normal file
@ -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
|
@ -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)
|
||||
|
78
barbican/plugin/util/translations.py
Normal file
78
barbican/plugin/util/translations.py
Normal file
@ -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
|
@ -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.")
|
||||
|
@ -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),
|
||||
|
@ -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(),
|
||||
|
@ -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")
|
||||
)
|
@ -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)
|
0
barbican/tests/plugin/crypto/__init__.py
Normal file
0
barbican/tests/plugin/crypto/__init__.py
Normal file
@ -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
|
@ -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()
|
0
barbican/tests/plugin/util/__init__.py
Normal file
0
barbican/tests/plugin/util/__init__.py
Normal file
@ -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):
|
@ -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())
|
||||
|
10
setup.cfg
10
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
|
||||
|
Loading…
x
Reference in New Issue
Block a user