First attempt to integrate Paul's HSM plugin into Barbican.

Initial mods to change the plugin interface a little bit, and to introduce
a new 'kek_data' table to house key encryption key (KEK) metadata used for
encryption/decryption. This new table will house per-tenant metadata.

Change-Id: I87cd0d4516fa19209be94cacdb79ac4b7eb740df
Implements: blueprint crypto-implement-hsm-plugin
This commit is contained in:
John Wood 2013-08-08 01:05:55 -05:00
parent 39ba16bea2
commit 5efef9fbce
13 changed files with 473 additions and 126 deletions

View File

@ -266,15 +266,16 @@ class SecretsResource(api.ApiResource):
def __init__(self, crypto_manager,
tenant_repo=None, secret_repo=None,
tenant_secret_repo=None, datum_repo=None,
tenant_secret_repo=None, datum_repo=None, kek_repo=None,
policy_enforcer=None):
LOG.debug('Creating SecretsResource')
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.crypto_manager = crypto_manager
self.kek_repo = kek_repo or repo.KEKDatumRepo()
self.policy = policy_enforcer or Enforcer()
self.crypto_manager = crypto_manager
self.validator = validators.NewSecretValidator()
@handle_exceptions(_('Secret creation'))
@ -288,7 +289,8 @@ class SecretsResource(api.ApiResource):
new_secret = res.create_secret(data, tenant, self.crypto_manager,
self.secret_repo,
self.tenant_secret_repo,
self.datum_repo)
self.datum_repo,
self.kek_repo)
except em.CryptoMimeTypeNotSupportedException as cmtnse:
LOG.exception('Secret creation failed - mime-type not supported')
_secret_mime_type_not_supported(cmtnse.mime_type, req, resp)
@ -343,13 +345,14 @@ class SecretResource(api.ApiResource):
def __init__(self, crypto_manager,
tenant_repo=None, secret_repo=None,
tenant_secret_repo=None, datum_repo=None,
tenant_secret_repo=None, datum_repo=None, kek_repo=None,
policy_enforcer=None):
self.crypto_manager = crypto_manager
self.tenant_repo = tenant_repo or repo.TenantRepo()
self.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.policy = policy_enforcer or Enforcer()
@handle_exceptions(_('Secret retrieval'))
@ -451,7 +454,8 @@ class SecretResource(api.ApiResource):
tenant,
self.crypto_manager,
self.tenant_secret_repo,
self.datum_repo)
self.datum_repo,
self.kek_repo)
except em.CryptoMimeTypeNotSupportedException as cmtnse:
LOG.exception('Secret creation failed - mime-type not supported')
_secret_mime_type_not_supported(cmtnse.mime_type, req, resp)

View File

@ -44,7 +44,7 @@ def get_or_create_tenant(keystone_id, tenant_repo):
def create_secret(data, tenant, crypto_manager,
secret_repo, tenant_secret_repo, datum_repo,
secret_repo, tenant_secret_repo, datum_repo, kek_repo,
ok_to_generate=False):
"""
Common business logic to create a secret.
@ -53,10 +53,11 @@ def create_secret(data, tenant, crypto_manager,
new_secret = models.Secret(data)
time_keeper.mark('after Secret model create')
new_datum = None
content_type = data.get('payload_content_type',
'application/octet-stream') # TODO: Add to Order!
if 'payload' in data:
payload = data.get('payload')
content_type = data.get('payload_content_type')
content_encoding = data.get('payload_content_encoding')
if not payload:
raise exception.NoDataToProcess()
@ -81,13 +82,16 @@ def create_secret(data, tenant, crypto_manager,
new_datum = crypto_manager.encrypt(payload,
content_type,
new_secret,
tenant)
tenant,
kek_repo)
time_keeper.mark('after encrypt')
elif ok_to_generate:
LOG.debug('Generating new secret...')
new_datum = crypto_manager.generate_data_encryption_key(new_secret,
tenant)
content_type,
tenant,
kek_repo)
time_keeper.mark('after secret generate')
else:
@ -118,7 +122,7 @@ def create_secret(data, tenant, crypto_manager,
def create_encrypted_datum(secret, payload,
content_type, content_encoding,
tenant, crypto_manager,
tenant_secret_repo, datum_repo):
tenant_secret_repo, datum_repo, kek_repo):
"""
Modifies the secret to add the plain_text secret information.
@ -130,6 +134,7 @@ def create_encrypted_datum(secret, payload,
:param crypto_manager: the crypto plugin manager
:param tenant_secret_repo: the tenant/secret association repository
:param datum_repo: the encrypted datum repository
:param kek_repo: the KEK metadata repository
:retval The response body, None if N/A
"""
if not payload:
@ -149,7 +154,8 @@ def create_encrypted_datum(secret, payload,
new_datum = crypto_manager.encrypt(payload,
content_type,
secret,
tenant)
tenant,
kek_repo)
datum_repo.create_from(new_datum)
# Create Tenant/Secret entity.

View File

@ -90,6 +90,23 @@ def get_accepted_encodings(req):
cmp=lambda a, b: cmp(b[1], a[1]))]
def generate_fullname_for(o):
"""
Produce a fully qualified class name for the specified instance.
:param o: The instance to generate information from.
:return: A string providing the package.module information for the
instance.
"""
if not o:
return 'None'
module = o.__class__.__module__
if module is None or module == str.__class__.__module__:
return o.__class__.__name__
return module + '.' + o.__class__.__name__
class TimeKeeper(object):
"""
Keeps track of elapsed times and then allows for dumping a smmary to

View File

@ -17,7 +17,9 @@ from oslo.config import cfg
from stevedore import named
from barbican.common.exception import BarbicanException
from barbican.common import utils
from barbican.crypto import mime_types
from barbican.crypto import plugin as plugin_mod
from barbican.model.models import EncryptedDatum
from barbican.openstack.common.gettextutils import _
@ -89,20 +91,26 @@ class CryptoExtensionManager(named.NamedExtensionManager):
invoke_kwds=invoke_kwargs
)
def encrypt(self, unencrypted, content_type, secret, tenant):
def encrypt(self, unencrypted, content_type, secret, tenant, kek_repo):
"""Delegates encryption to first active plugin."""
if len(self.extensions) < 1:
raise CryptoPluginNotFound()
encrypting_plugin = self.extensions[0].obj
# TODO: Need to test if the plugin supports 'secret's requirements.
if content_type in mime_types.PLAIN_TEXT:
# normalize text to binary string
unencrypted = unencrypted.encode('utf-8')
datum = EncryptedDatum(secret)
# Find or create a key encryption key metadata.
kek_datum, kek_metadata = self._find_or_create_kek_metadata(
encrypting_plugin, tenant, kek_repo)
# Create an encrypted datum instance and add the encrypted cypher text.
datum = EncryptedDatum(secret, kek_datum)
datum.content_type = content_type
datum.cypher_text, datum.kek_metadata = encrypting_plugin.encrypt(
unencrypted, tenant
datum.cypher_text, datum.kek_meta_extended = encrypting_plugin.encrypt(
unencrypted, kek_metadata, tenant
)
return datum
@ -116,19 +124,23 @@ class CryptoExtensionManager(named.NamedExtensionManager):
raise CryptoAcceptNotSupportedException(accept)
for ext in self.extensions:
plugin = ext.obj
decrypting_plugin = ext.obj
for datum in secret.encrypted_data:
if plugin.supports(datum.kek_metadata):
unencrypted = plugin.decrypt(datum.cypher_text,
datum.kek_metadata,
tenant)
if self._plugin_supports(decrypting_plugin,
datum.kek_meta_tenant):
unencrypted = decrypting_plugin \
.decrypt(datum.cypher_text,
datum.kek_meta_tenant,
datum.kek_meta_extended,
tenant)
if datum.content_type in mime_types.PLAIN_TEXT:
unencrypted = unencrypted.decode('utf-8')
return unencrypted
else:
raise CryptoPluginNotFound()
def generate_data_encryption_key(self, secret, tenant):
def generate_data_encryption_key(self, secret, content_type, tenant,
kek_repo):
"""
Delegates generating a data-encryption key to first active plugin.
@ -141,15 +153,39 @@ class CryptoExtensionManager(named.NamedExtensionManager):
raise CryptoPluginNotFound()
encrypting_plugin = self.extensions[0].obj
# TODO: Call plugin's key generation processes.
# Note: It could be the *data* key to generate (for the
# secret algo type) uses a different plug in than that
# used to encrypted the key.
# Create the secret.
data_key = encrypting_plugin.create(secret.algorithm,
secret.bit_length)
datum = EncryptedDatum(secret)
datum.cypher_text, datum.kek_metadata = encrypting_plugin.encrypt(
data_key, tenant
)
return datum
# Encrypt the secret.
return self.encrypt(data_key, content_type, secret, tenant, kek_repo)
def _plugin_supports(self, plugin_inst, kek_metadata_tenant):
"""
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_metadata(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_metadata(tenant,
full_plugin_name)
# Bind to the plugin's key management.
# TODO: Does this need to be in a critical section? Should the bind
# operation just be declared idempotent in the plugin contract?
kek_metadata = plugin_mod.KEKMetadata(kek_datum)
if not kek_datum.bind_completed:
plugin_inst.bind_kek_metadata(kek_metadata)
plugin_mod.indicate_bind_completed(kek_metadata, kek_datum)
kek_repo.save(kek_datum)
return (kek_datum, kek_metadata)

View File

@ -1,5 +1,6 @@
import PyKCS11
import uuid
# TODO: Restore this: import PyKCS11
# This code is disabled just enough to pass tox tests, but once full
# integration into Barbican is achieved, this code should re-enabled.
from oslo.config import cfg
@ -10,6 +11,10 @@ from barbican.openstack.common import jsonutils as json
from barbican.openstack.common.gettextutils import _
# TODO: Remove this:
PyKCS11 = {}
class P11CryptoPluginException(exception.BarbicanException):
message = _("TODO") # TODO
@ -60,7 +65,7 @@ class P11CryptoPlugin(CryptoPluginBase):
if len(keys) == 1:
return keys[0]
elif len(keys) == 0:
return None
return None, None
elif len(keys) > 1:
raise P11CryptoPluginException() # TODO:make this a mega exception
@ -68,32 +73,35 @@ class P11CryptoPlugin(CryptoPluginBase):
key_label = self.repo.get_key(tenant) # TODO
return key_label
def _generate_key_for_tenant(self, tenant):
# TODO: uuid generation from sufficient entropy?
key_label = "tenant-{0}-key-{1}".format(tenant.tenant_id, uuid.uuid4())
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, key_label),
(PyKCS11.CKA_PRIVATE, True),
(PyKCS11.CKA_SENSITIVE, True),
(PyKCS11.CKA_ENCRYPT, True),
(PyKCS11.CKA_DECRYPT, True),
#(PyKCS11.CKA_TOKEN, True), # TODO: enable this (saves to HSM)
(PyKCS11.CKA_WRAP, True),
(PyKCS11.CKA_UNWRAP, True),
# TODO: make these unextractable if feasible
(PyKCS11.CKA_EXTRACTABLE, True))
ckattr = self.session._template2ckattrlist(template)
m = PyKCS11.Mechanism(PyKCS11.CKM_AES_KEY_GEN, None)
key = PyKCS11.CK_OBJECT_HANDLE()
self._check_error(
self.pkcs11.lib.C_GenerateKey(self.session.session, m, ckattr, key)
)
self.repo.write_key(key_label, tenant) # TODO: write key
return (key, key_label)
# TODO: jwood: No longer needed...see bind_kek_metadata() below...
# def _generate_key_for_tenant(self, tenant):
# # TODO: uuid generation from sufficient entropy?
# key_label = "tenant-{0}-key-{1}".format(tenant.tenant_id,
# uuid.uuid4())
# 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, key_label),
# (PyKCS11.CKA_PRIVATE, True),
# (PyKCS11.CKA_SENSITIVE, True),
# (PyKCS11.CKA_ENCRYPT, True),
# (PyKCS11.CKA_DECRYPT, True),
# #(PyKCS11.CKA_TOKEN, True), # TODO: enable this (saves to HSM)
# (PyKCS11.CKA_WRAP, True),
# (PyKCS11.CKA_UNWRAP, True),
# # TODO: make these unextractable if feasible
# (PyKCS11.CKA_EXTRACTABLE, True))
# ckattr = self.session._template2ckattrlist(template)
#
# m = PyKCS11.Mechanism(PyKCS11.CKM_AES_KEY_GEN, None)
# key = PyKCS11.CK_OBJECT_HANDLE()
# self._check_error(
# self.pkcs11.lib.C_GenerateKey(self.session.session, m, ckattr,
# key)
# )
# self.repo.write_key(key_label, tenant) # TODO: write key
# return (key, key_label)
def _build_kek_metadata(self, mechanism, key_label, iv):
# TODO: CBC, default (exception?)
@ -112,32 +120,77 @@ class P11CryptoPlugin(CryptoPluginBase):
})
return kek_metadata
def encrypt(self, unencrypted, tenant):
def encrypt(self, unencrypted, kek_metadata, tenant):
padded_data = self._pad(unencrypted)
key_label = self._get_current_key_label_for_tenant(tenant)
if key_label:
key = self._get_key_by_label(key_label)
else:
key, key_label = self._generate_key_for_tenant(tenant)
key = self._get_key_by_label(kek_metadata.kek_label)
# TODO: jwood: No longer needed:
# key_label = self._get_current_key_label_for_tenant(tenant)
# if key_label:
# key = self._get_key_by_label(key_label)
# else:
# key, key_label = self._generate_key_for_tenant(tenant)
iv = self.session.generateRandom(self.block_size)
mech = PyKCS11.Mechanism(PyKCS11.CKM_AES_CBC_PAD, iv)
encrypted = self.session.encrypt(key, padded_data, mech)
cyphertext = b''.join(chr(i) for i in encrypted)
kek_metadata = self._build_kek_metadata(mech, key_label, iv)
# TODO: jwood No longer needed???: kek_metadata = self
# ._build_kek_metadata(mech, key_label, iv)
return cyphertext, kek_metadata
return cyphertext, None # TODO: jwood kek_metadata return not needed?
def decrypt(self, encrypted, kek_metadata, tenant):
kek_info = json.loads(kek_metadata)
key, iv = self._get_key_by_label(kek_info['kek']) # TODO: get IV
def decrypt(self, encrypted, kek_meta_tenant, kek_meta_extended, tenant):
# TODO: jwood Metadata coming in now...kek_info = json.loads(
# kek_metadata)
key, iv = self._get_key_by_label(
kek_meta_tenant.kek_label) # TODO: jwood
# kek_info['kek']) # TODO: get IV
mech = PyKCS11.Mechanism(PyKCS11.CKM_AES_CBC_PAD, iv)
decrypted = self.session.decrypt(key, encrypted, mech)
padded_secret = b''.join(chr(i) for i in decrypted)
return self._strip_pad(padded_secret)
# TODO: jwood: This is a new method, to generate a key in the HSM for the
# metadata kek_label.
def bind_kek_metadata(self, kek_metadata):
# Enforce idempotency: If we've already generated a key for the
# kek_label, leave now.
key, iv = self._get_key_by_label(kek_metadata.kek_label)
if key:
return
# To be persisted by Barbican:
kek_metadata.algorithm = 'AES CBC PAD'
kek_metadata.bit_length = self.kek_key_length
kek_metadata.mode = None
kek_metadata.plugin_meta = None
# Generate the key.
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_metadata.kek_label),
(PyKCS11.CKA_PRIVATE, True),
(PyKCS11.CKA_SENSITIVE, True),
(PyKCS11.CKA_ENCRYPT, True),
(PyKCS11.CKA_DECRYPT, True),
#(PyKCS11.CKA_TOKEN, True), # TODO: enable this (saves to HSM)
(PyKCS11.CKA_WRAP, True),
(PyKCS11.CKA_UNWRAP, True),
# TODO: make these unextractable if feasible
(PyKCS11.CKA_EXTRACTABLE, True))
ckattr = self.session._template2ckattrlist(template)
m = PyKCS11.Mechanism(PyKCS11.CKM_AES_KEY_GEN, None)
key = PyKCS11.CK_OBJECT_HANDLE()
self._check_error(
self.pkcs11.lib.C_GenerateKey(self.session.session, m, ckattr, key)
)
def create(self, algorithm, bit_length):
if bit_length % 8 != 0:
raise ValueError('Bit lengths must be divisible by 8')

View File

@ -19,7 +19,6 @@ from Crypto.Cipher import AES
from Crypto import Random
from oslo.config import cfg
from barbican.openstack.common import jsonutils as json
from barbican.openstack.common.gettextutils import _
@ -36,33 +35,99 @@ CONF.register_group(simple_crypto_plugin_group)
CONF.register_opts(simple_crypto_plugin_opts, group=simple_crypto_plugin_group)
class KEKMetadata(object):
"""
Data transfer object to support key encryption key (KEK) definition.
Instances are passed into third-party plugins rather than passing in
KekDatum instances directly. This provides a level of isolation from
these third party systems and Barbican's data model.
"""
def __init__(self, kek_datum):
"""
kek_datum is typically a barbican.model.models.EncryptedDatum instance.
"""
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
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_metadata 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
class CryptoPluginBase(object):
"""Base class for Crypto plugins."""
__metaclass__ = abc.ABCMeta
@abc.abstractmethod
def encrypt(self, unencrypted, tenant):
def encrypt(self, unencrypted, kek_metadata, tenant):
"""Encrypt unencrypted data in the context of the provided tenant.
:param unencrypted: byte data to be encrypted.
:param kek_metadata: Key encryption key metadata to use for encryption.
:param tenant: Tenant associated with the unencrypted data.
:returns: tuple -- contains the encrypted data and kek metadata.
:raises: ValueError if unencrypted is not byte data.
:returns: 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)
"""
@abc.abstractmethod
def decrypt(self, encrypted, kek_metadata, tenant):
def decrypt(self, encrypted, kek_meta_tenant, kek_meta_extended, tenant):
"""Decrypt encrypted_datum in the context of the provided tenant.
:param encrypted: cyphertext to be decrypted.
:param kek_metadata: metadata that was created by encryption.
:param kek_meta_tenant: Per-tenant key encryption key (KEK) metadata
to use for decryption.
:param kek_meta_extended: Optional per-secret KEK metadata to use for
decryption.
:param tenant: Tenant associated with the encrypted datum.
:returns: str -- unencrypted byte data
"""
@abc.abstractmethod
def bind_kek_metadata(self, kek_metadata):
"""
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'
instance.
This method is invoked prior to the encrypt() method above.
Implementors should fill out the supplied 'kek_metadata' 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_metadata: Key encryption key metadata to bind, with the
'kek_label' attribute guaranteed to be unique, and the
and 'plugin_name' attribute already configured.
:returns: None
"""
@abc.abstractmethod
def create(self, algorithm, bit_length):
"""Create a new key."""
@ -91,7 +156,7 @@ class SimpleCryptoPlugin(CryptoPluginBase):
unpadded = unencrypted[:-pad_length]
return unpadded
def encrypt(self, unencrypted, tenant):
def encrypt(self, unencrypted, kek_metadata, tenant):
if not isinstance(unencrypted, str):
raise ValueError('Unencrypted data must be a byte type, '
'but was {0}'.format(type(unencrypted)))
@ -100,21 +165,22 @@ class SimpleCryptoPlugin(CryptoPluginBase):
encryptor = AES.new(self.kek, AES.MODE_CBC, iv)
cyphertext = iv + encryptor.encrypt(padded_data)
kek_metadata = json.dumps({
'plugin': 'SimpleCryptoPlugin',
'encryption': 'aes-128-cbc',
'kek': 'kek_id'
})
return cyphertext, kek_metadata
return cyphertext, None
def decrypt(self, encrypted, kek_metadata, tenant):
def decrypt(self, encrypted, kek_meta_tenant, kek_meta_extended, tenant):
iv = encrypted[:self.block_size]
cypher_text = encrypted[self.block_size:]
decryptor = AES.new(self.kek, AES.MODE_CBC, iv)
padded_secret = decryptor.decrypt(cypher_text)
return self._strip_pad(padded_secret)
def bind_kek_metadata(self, kek_metadata):
kek_metadata.algorithm = 'aes'
kek_metadata.bit_length = 128
kek_metadata.mode = 'cbc'
kek_metadata.plugin_meta = None
def create(self, algorithm, bit_length):
# TODO: do this right
# return Random.get_random_bytes(bit_length/8)
@ -131,5 +197,7 @@ class SimpleCryptoPlugin(CryptoPluginBase):
'bit length')
def supports(self, kek_metadata):
metadata = json.loads(kek_metadata)
return metadata['plugin'] == 'SimpleCryptoPlugin'
return True # TODO: Revisit what 'supports' means...it really should
# be if the plugin can perform the required task or not
# metadata = json.loads(kek_metadata)
# return metadata['plugin'] == 'SimpleCryptoPlugin'

View File

@ -164,6 +164,7 @@ class Tenant(BASE, ModelBase):
orders = relationship("Order", backref="tenant")
secrets = relationship("TenantSecret", backref="tenants")
keks = relationship("KEKDatum", backref="tenant")
def _do_extra_dict_fields(self):
"""Sub-class hook method: return dict of fields."""
@ -197,7 +198,10 @@ class Secret(BASE, ModelBase):
cypher_type = Column(String(255))
# TODO: Performance - Consider avoiding full load of all
# datum attributes here.
# datum attributes here. This is only being done to support the
# building of the list of supported content types when secret
# metadata is retrieved.
# See barbican.api.resources.py::SecretsResource.on_get()
encrypted_data = relationship("EncryptedDatum", lazy='joined')
def __init__(self, parsed_request):
@ -241,26 +245,74 @@ class EncryptedDatum(BASE, ModelBase):
secret_id = Column(String(36), ForeignKey('secrets.id'),
nullable=False)
kek_id = Column(String(36), ForeignKey('kek_data.id'),
nullable=False)
content_type = Column(String(255))
mime_type = Column(String(255))
cypher_text = Column(LargeBinary)
kek_metadata = Column(Text)
kek_meta_extended = Column(Text)
kek_meta_tenant = relationship("KEKDatum")
def __init__(self, secret=None):
"""Creates encrypted datum from a secret."""
def __init__(self, secret=None, kek_datum=None):
"""Creates encrypted datum from a secret and KEK metadata."""
super(EncryptedDatum, self).__init__()
if secret:
self.secret_id = secret.id
if kek_datum:
self.kek_id = kek_datum.id
self.kek_meta_tenant = kek_datum
self.status = States.ACTIVE
def _do_extra_dict_fields(self):
"""Sub-class hook method: return dict of fields."""
return {'name': self.name,
'cypher_text': self.secret,
'content_type': self.content_type,
'kek_metadata': self.kek_metadata}
return {'cypher_text': self.secret,
'content_type': self.content_type}
class KEKDatum(BASE, ModelBase):
"""
Represents the key encryption key (KEK) metadata associated with a process
used to encrypt/decrypt secret information.
When a secret is encrypted, in addition to the cypher text, the Barbican
encryption process produces a KEK metadata object. The cypher text is
stored via the EncryptedDatum model above, whereas the metadata is stored
within this model. Decryption processes utilize this KEK metadata
to decrypt the associated cypher text.
Note that this model is intended to be agnostic to the specific means used
to encrypt/decrypt the secret information, so please do not place vendor-
specific attributes here.
Note as well that each Tenant will have at most one 'active=True' KEKDatum
instance at a time, representing the most recent KEK metadata instance
to use for encryption processes performed on behalf of the Tenant.
KEKDatum instances that are 'active=False' are associated to previously
used encryption processes for the Tenant, that eventually should be
rotated and deleted with the Tenant's active KEKDatum.
"""
__tablename__ = 'kek_data'
plugin_name = Column(String(255))
kek_label = Column(String(255))
tenant_id = Column(String(36), ForeignKey('tenants.id'),
nullable=False)
active = Column(Boolean, nullable=False, default=True)
bind_completed = Column(Boolean, nullable=False, default=False)
algorithm = Column(String(255))
bit_length = Column(Integer)
mode = Column(String(255))
plugin_meta = Column(Text)
def _do_extra_dict_fields(self):
"""Sub-class hook method: return dict of fields."""
return {'algorithm': self.algorithm}
class Order(BASE, ModelBase):

View File

@ -23,6 +23,7 @@ quite intense for sqlalchemy, and maybe could be simplified.
import time
import logging
import uuid
from oslo.config import cfg
@ -554,7 +555,7 @@ class SecretRepo(BaseRepo):
class EncryptedDatumRepo(BaseRepo):
"""
Repository for the EncryptedDatum entity (that stores encrypted
information on behalf of a Secret.
information on behalf of a Secret).
"""
def _do_entity_name(self):
@ -573,6 +574,61 @@ class EncryptedDatumRepo(BaseRepo):
pass
class KEKDatumRepo(BaseRepo):
"""
Repository for the KEKDatum entity (that stores key encryption key (KEK)
metadata used by crypto plugins to encrypt/decrypt secrets).
"""
def find_or_create_kek_metadata(self, tenant,
plugin_name,
suppress_exception=False,
session=None):
""" Find or create a KEK metadata instance. """
kek_datum = None
session = self.get_session(session)
# TODO: Reverse this...attempt insert first, then get on fail.
try:
query = session.query(models.KEKDatum) \
.filter_by(tenant_id=tenant.id) \
.filter_by(plugin_name=plugin_name) \
.filter_by(active=True) \
.filter_by(deleted=False)
kek_datum = query.one()
except sa_orm.exc.NoResultFound:
kek_datum = models.KEKDatum()
kek_datum.kek_label = "tenant-{0}-key-{1}".format(
tenant.keystone_id, uuid.uuid4())
kek_datum.tenant_id = tenant.id
kek_datum.plugin_name = plugin_name
kek_datum.status = models.States.ACTIVE
self.save(kek_datum)
return kek_datum
def _do_entity_name(self):
"""Sub-class hook: return entity name, such as for debugging."""
return "KEKDatum"
def _do_create_instance(self):
return models.KEKDatum()
def _do_build_get_query(self, entity_id, keystone_id, session):
"""Sub-class hook: build a retrieve query."""
return session.query(models.KEKDatum).filter_by(id=entity_id)
def _do_validate(self, values):
"""Sub-class hook: validate values."""
pass
class TenantSecretRepo(BaseRepo):
"""Repository for the TenantSecret entity."""

View File

@ -29,13 +29,15 @@ class BeginOrder(object):
"""Handles beginning processing an Order"""
def __init__(self, crypto_manager=None, tenant_repo=None, order_repo=None,
secret_repo=None, tenant_secret_repo=None, datum_repo=None):
secret_repo=None, tenant_secret_repo=None,
datum_repo=None, kek_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 CryptoExtensionManager()
def process(self, order_id, keystone_id):
@ -71,7 +73,8 @@ class BeginOrder(object):
# Create Secret
new_secret = create_secret(secret_info, tenant,
self.crypto_manager, self.secret_repo,
self.tenant_secret_repo, self.datum_repo,
self.tenant_secret_repo,
self.datum_repo, self.kek_repo,
ok_to_generate=True)
order.secret_id = new_secret.id

View File

@ -22,9 +22,11 @@ import mock
from mock import MagicMock
from barbican.api import resources as res
from barbican.common import utils
from barbican.common import exception as excep
from barbican.common.validators import DEFAULT_MAX_SECRET_BYTES
from barbican.crypto.extension_manager import CryptoExtensionManager
from barbican.tests.crypto.test_plugin import TestCryptoPlugin
from barbican.model import models
from barbican.openstack.common import jsonutils
@ -73,6 +75,14 @@ def create_order(id="id",
return order
def validate_datum(test, datum):
test.assertIsNone(datum.kek_meta_extended)
test.assertIsNotNone(datum.kek_meta_tenant)
test.assertTrue(datum.kek_meta_tenant.bind_completed)
test.assertIsNotNone(datum.kek_meta_tenant.plugin_name)
test.assertIsNotNone(datum.kek_meta_tenant.kek_label)
class WhenTestingVersionResource(unittest.TestCase):
def setUp(self):
self.policy = MagicMock()
@ -128,6 +138,14 @@ class WhenCreatingSecretsUsingSecretsResource(unittest.TestCase):
self.datum_repo = MagicMock()
self.datum_repo.create_from.return_value = None
self.kek_datum = models.KEKDatum()
self.kek_datum.plugin_name = utils.generate_fullname_for(
TestCryptoPlugin())
self.kek_datum.kek_label = "kek_label"
self.kek_datum.bind_completed = False
self.kek_repo = MagicMock()
self.kek_repo.find_or_create_kek_metadata.return_value = self.kek_datum
self.policy = MagicMock()
self.stream = MagicMock()
@ -146,7 +164,9 @@ class WhenCreatingSecretsUsingSecretsResource(unittest.TestCase):
self.tenant_repo,
self.secret_repo,
self.tenant_secret_repo,
self.datum_repo, self.policy)
self.datum_repo,
self.kek_repo,
self.policy)
def test_should_add_new_secret(self):
self.resource.on_post(self.req, self.resp, self.keystone_id)
@ -172,7 +192,8 @@ class WhenCreatingSecretsUsingSecretsResource(unittest.TestCase):
self.assertIsInstance(datum, models.EncryptedDatum)
self.assertEqual('cypher_text', datum.cypher_text)
self.assertEqual(self.payload_content_type, datum.content_type)
self.assertIsNotNone(datum.kek_metadata)
validate_datum(self, datum)
def test_should_add_new_secret_with_expiration(self):
expiration = '2114-02-28 12:14:44.180394-05:00'
@ -209,7 +230,8 @@ class WhenCreatingSecretsUsingSecretsResource(unittest.TestCase):
self.assertTrue(isinstance(datum, models.EncryptedDatum))
self.assertEqual('cypher_text', datum.cypher_text)
self.assertEqual(self.payload_content_type, datum.content_type)
self.assertIsNotNone(datum.kek_metadata)
validate_datum(self, datum)
def test_should_add_new_secret_metadata_without_payload(self):
self.stream.read.return_value = json.dumps({'name': self.name})
@ -311,7 +333,8 @@ class WhenCreatingSecretsUsingSecretsResource(unittest.TestCase):
self.assertIsInstance(datum, models.EncryptedDatum)
self.assertEqual('cypher_text', datum.cypher_text)
self.assertEqual('application/octet-stream', datum.content_type)
self.assertIsNotNone(datum.kek_metadata)
validate_datum(self, datum)
def test_create_secret_fails_with_binary_payload_no_encoding(self):
self.stream.read.return_value = json.dumps(
@ -389,7 +412,10 @@ class WhenGettingSecretsListUsingSecretsResource(unittest.TestCase):
self.datum_repo = MagicMock()
self.datum_repo.create_from.return_value = None
self.kek_repo = MagicMock()
self.policy = MagicMock()
self.policy.read.return_value = None
self.conf = MagicMock()
self.conf.crypto.namespace = 'barbican.test.crypto.plugin'
@ -404,7 +430,9 @@ class WhenGettingSecretsListUsingSecretsResource(unittest.TestCase):
self.resource = res.SecretsResource(self.crypto_mgr, self.tenant_repo,
self.secret_repo,
self.tenant_secret_repo,
self.datum_repo, self.policy)
self.datum_repo,
self.kek_repo,
self.policy)
def test_should_get_list_secrets(self):
self.resource.on_get(self.req, self.resp, self.keystone_id)
@ -466,17 +494,27 @@ class WhenGettingPuttingOrDeletingSecretUsingSecretResource(unittest.TestCase):
secret_id = "idsecret1"
datum_id = "iddatum1"
kek_id = "idkek1"
self.secret_algorithm = "AES"
self.secret_bit_length = 256
self.secret_cypher_type = "CBC"
self.kek_tenant = models.KEKDatum()
self.kek_tenant.id = kek_id
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(
TestCryptoPlugin())
self.datum = models.EncryptedDatum()
self.datum.id = datum_id
self.datum.secret_id = secret_id
self.datum.kek_id = kek_id
self.datum.kek_meta_tenant = self.kek_tenant
self.datum.content_type = "text/plain"
self.datum.cypher_text = "cypher_text"
self.datum.kek_metadata = json.dumps({'plugin': 'TestCryptoPlugin'})
self.secret = create_secret(id=secret_id,
name=self.name,
@ -501,6 +539,8 @@ class WhenGettingPuttingOrDeletingSecretUsingSecretResource(unittest.TestCase):
self.datum_repo = MagicMock()
self.datum_repo.create_from.return_value = None
self.kek_repo = MagicMock()
self.policy = MagicMock()
self.req = MagicMock()
@ -517,7 +557,9 @@ class WhenGettingPuttingOrDeletingSecretUsingSecretResource(unittest.TestCase):
self.tenant_repo,
self.secret_repo,
self.tenant_secret_repo,
self.datum_repo, self.policy)
self.datum_repo,
self.kek_repo,
self.policy)
def test_should_get_secret_as_json(self):
self.resource.on_get(self.req, self.resp, self.keystone_id,
@ -629,7 +671,8 @@ class WhenGettingPuttingOrDeletingSecretUsingSecretResource(unittest.TestCase):
datum = args[0]
self.assertIsInstance(datum, models.EncryptedDatum)
self.assertEqual('cypher_text', datum.cypher_text)
self.assertIsNotNone(datum.kek_metadata)
validate_datum(self, datum)
def test_should_put_secret_as_binary(self):
self._setup_for_puts()

View File

@ -24,14 +24,19 @@ from barbican.openstack.common import jsonutils as json
class TestCryptoPlugin(CryptoPluginBase):
"""Crypto plugin implementation for testing the plugin manager."""
def encrypt(self, unencrypted, tenant):
def encrypt(self, unencrypted, kek_metadata, tenant):
cypher_text = 'cypher_text'
kek_metadata = json.dumps({'plugin': 'TestCryptoPlugin'})
return cypher_text, kek_metadata
return cypher_text, None
def decrypt(self, encrypted, kek_metadata, tenant):
def decrypt(self, encrypted, kek_meta_tenant, kek_meta_extended, tenant):
return b'unencrypted_data'
def bind_kek_metadata(self, kek_metadata):
kek_metadata.algorithm = 'aes'
kek_metadata.bit_length = 128
kek_metadata.mode = 'cbc'
kek_metadata.plugin_meta = None
def create(self, algorithm, bit_length):
return "insecure_key"
@ -76,18 +81,23 @@ class WhenTestingSimpleCryptoPlugin(unittest.TestCase):
secret = MagicMock()
secret.mime_type = 'text/plain'
with self.assertRaises(ValueError):
self.plugin.encrypt(unencrypted, MagicMock())
self.plugin.encrypt(unencrypted, MagicMock(), MagicMock())
def test_byte_string_encryption(self):
unencrypted = b'some_secret'
encrypted, kek_metadata = self.plugin.encrypt(unencrypted, MagicMock())
decrypted = self.plugin.decrypt(encrypted, kek_metadata, MagicMock())
encrypted, kek_ext = self.plugin.encrypt(unencrypted,
MagicMock(),
MagicMock())
decrypted = self.plugin.decrypt(encrypted, MagicMock(),
kek_ext, MagicMock())
self.assertEqual(unencrypted, decrypted)
def test_random_bytes_encryption(self):
unencrypted = Random.get_random_bytes(10)
encrypted, kek_metadata = self.plugin.encrypt(unencrypted, MagicMock())
decrypted = self.plugin.decrypt(encrypted, kek_metadata, MagicMock())
encrypted, kek_meta_ext = self.plugin.encrypt(unencrypted,
MagicMock(), MagicMock())
decrypted = self.plugin.decrypt(encrypted, MagicMock(),
kek_meta_ext, MagicMock())
self.assertEqual(unencrypted, decrypted)
def test_create_256_bit_key(self):
@ -113,11 +123,3 @@ class WhenTestingSimpleCryptoPlugin(unittest.TestCase):
'kek': 'kek_id'
})
self.assertTrue(self.plugin.supports(kek_metadata))
def test_does_not_support_decoding_metadata(self):
kek_metadata = json.dumps({
'plugin': 'MuchFancierPlugin',
'encryption': 'aes-128-cbc',
'kek': 'kek_id'
})
self.assertFalse(self.plugin.supports(kek_metadata))

View File

@ -73,6 +73,8 @@ class WhenBeginningOrder(unittest.TestCase):
self.datum_repo = MagicMock()
self.datum_repo.create_from.return_value = None
self.kek_repo = MagicMock()
self.conf = MagicMock()
self.conf.crypto.namespace = 'barbican.test.crypto.plugin'
self.conf.crypto.enabled_crypto_plugins = ['test_crypto']
@ -81,7 +83,7 @@ class WhenBeginningOrder(unittest.TestCase):
self.resource = BeginOrder(self.crypto_mgr,
self.tenant_repo, self.order_repo,
self.secret_repo, self.tenant_secret_repo,
self.datum_repo)
self.datum_repo, self.kek_repo)
def test_should_process_order(self):
self.resource.process(self.order.id, self.keystone_id)
@ -107,7 +109,12 @@ class WhenBeginningOrder(unittest.TestCase):
datum = args[0]
self.assertIsInstance(datum, EncryptedDatum)
self.assertIsNotNone(datum.cypher_text)
self.assertIsNotNone(datum.kek_metadata)
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)
if __name__ == '__main__':

View File

@ -17,4 +17,4 @@ jsonschema>=2.0.0
SQLAlchemy>=0.8.1
alembic>=0.5.0
psycopg2>=2.5.1
PyKCS11>=1.2.4
# TODO: Get this working again...PyKCS11>=1.2.4