Update Barbican wrapper

Changes to Barbican key manager to support all managed objects and
API changes.

Updates  to the functional tests will follow in another patch in the
interest of shorter code reviews.

Change-Id: I2af3e7c2f16f31dcd2b4484a6537d3114bc4b3bb
This commit is contained in:
Kaitlin Farr 2015-08-26 18:32:17 -04:00
parent 82a0ded5c8
commit 73e9601095
3 changed files with 393 additions and 74 deletions

View File

@ -16,28 +16,49 @@
"""
Key manager implementation for Barbican
"""
from barbicanclient import client as barbican_client
from barbicanclient import exceptions as barbican_exceptions
from keystoneclient.auth import token_endpoint
import time
from cryptography.hazmat import backends
from cryptography.hazmat.primitives import serialization
from cryptography import x509 as cryptography_x509
from keystoneclient.auth import identity
from keystoneclient import session
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import excutils
from castellan.common import exception
from castellan.common.objects import key as key_base_class
from castellan.common.objects import opaque_data as op_data
from castellan.common.objects import passphrase
from castellan.common.objects import private_key as pri_key
from castellan.common.objects import public_key as pub_key
from castellan.common.objects import symmetric_key as sym_key
from castellan.common.objects import x_509
from castellan.key_manager import key_manager
from castellan.openstack.common import _i18n as u
from barbicanclient import client as barbican_client
from barbicanclient import exceptions as barbican_exceptions
from six.moves import urllib
barbican_opts = [
cfg.StrOpt('barbican_endpoint',
default='http://localhost:9311/',
help='Use this endpoint to connect to Barbican'),
help='Use this endpoint to connect to Barbican, for example: '
'"http://localhost:9311/"'),
cfg.StrOpt('barbican_api_version',
default='v1',
help='Version of the Barbican API'),
help='Version of the Barbican API, for example: "v1"'),
cfg.StrOpt('auth_endpoint',
default='http://localhost:5000/v3',
help='Use this endpoint to connect to Keystone'),
cfg.IntOpt('retry_delay',
default=1,
help='Number of seconds to wait before retrying poll for key '
'creation completion'),
cfg.IntOpt('number_of_retries',
default=60,
help='Number of times to retry poll for key creation '
'completion'),
]
BARBICAN_OPT_GROUP = 'barbican'
@ -48,6 +69,14 @@ LOG = logging.getLogger(__name__)
class BarbicanKeyManager(key_manager.KeyManager):
"""Key Manager Interface that wraps the Barbican client API."""
_secret_type_dict = {
op_data.OpaqueData: 'opaque',
passphrase.Passphrase: 'passphrase',
pri_key.PrivateKey: 'private',
pub_key.PublicKey: 'public',
sym_key.SymmetricKey: 'symmetric',
x_509.X509: 'certificate'}
def __init__(self, configuration):
self._barbican_client = None
self._base_url = None
@ -61,6 +90,8 @@ class BarbicanKeyManager(key_manager.KeyManager):
:param context: the user context for authentication
:return: a Barbican Client object
:raises Forbidden: if the context is None
:raises KeyManagerError: if context is missing tenant or
tenant is None
"""
# Confirm context is provided, if not raise forbidden
@ -69,13 +100,21 @@ class BarbicanKeyManager(key_manager.KeyManager):
LOG.error(msg)
raise exception.Forbidden(msg)
if not hasattr(context, 'tenant') or context.tenant is None:
msg = u._("Unable to create Barbican Client without tenant "
"attribute in context object.")
LOG.error(msg)
raise exception.KeyManagerError(msg)
if self._barbican_client and self._current_context == context:
return self._barbican_client
return self._barbican_client
try:
self._current_context = context
sess = self._get_keystone_session(context)
auth = self._get_keystone_auth(context)
sess = session.Session(auth=auth)
self._barbican_endpoint = self._get_barbican_endpoint(auth, sess)
self._barbican_client = barbican_client.Client(
session=sess,
endpoint=self._barbican_endpoint)
@ -84,31 +123,54 @@ class BarbicanKeyManager(key_manager.KeyManager):
with excutils.save_and_reraise_exception():
LOG.error(u._LE("Error creating Barbican client: %s"), e)
self._base_url = self._create_base_url()
self._base_url = self._create_base_url(auth,
sess,
self._barbican_endpoint)
return self._barbican_client
def _get_keystone_session(self, context):
sess = session.Session.load_from_conf_options(
self.conf, BARBICAN_OPT_GROUP)
def _get_keystone_auth(self, context):
# TODO(kfarr): support keystone v2
auth = identity.v3.Token(
auth_url=self.conf.barbican.auth_endpoint,
token=context.auth_token,
project_id=context.tenant,
domain_id=context.user_domain,
project_domain_id=context.project_domain)
return auth
self._barbican_endpoint = self.conf.barbican.barbican_endpoint
def _get_barbican_endpoint(self, auth, sess):
if self.conf.barbican.barbican_endpoint:
return self.conf.barbican.barbican_endpoint
else:
service_parameters = {'service_type': 'key-manager',
'service_name': 'barbican',
'interface': 'public'}
return auth.get_endpoint(sess, **service_parameters)
auth = token_endpoint.Token(self._barbican_endpoint,
context.auth_token)
sess.auth = auth
return sess
def _create_base_url(self, auth, sess, endpoint):
if self.conf.barbican.barbican_api_version:
api_version = self.conf.barbican.barbican_api_version
else:
discovery = auth.get_discovery(sess, url=endpoint)
raw_data = discovery.raw_version_data()
if len(raw_data) == 0:
msg = u._LE(
"Could not find discovery information for %s") % endpoint
LOG.error(msg)
raise exception.KeyManagerError(msg)
latest_version = raw_data[-1]
api_version = latest_version.get('id')
def _create_base_url(self):
base_url = urllib.parse.urljoin(
self._barbican_endpoint, self.conf.barbican.barbican_api_version)
endpoint, api_version)
return base_url
def create_key(self, context, algorithm, length, expiration=None):
"""Creates a symmetric key.
:param context: contains information of the user and the environment
for the request (castellan/context.py)
for the request (castellan/context.py)
:param algorithm: the algorithm associated with the secret
:param length: the bit length of the secret
:param expiration: the date the key will expire
@ -125,7 +187,7 @@ class BarbicanKeyManager(key_manager.KeyManager):
bit_length=length,
expiration=expiration)
order_ref = key_order.submit()
order = barbican_client.orders.get(order_ref)
order = self._get_active_order(barbican_client, order_ref)
return self._retrieve_secret_uuid(order.secret_ref)
except (barbican_exceptions.HTTPAuthError,
barbican_exceptions.HTTPClientError,
@ -136,20 +198,89 @@ class BarbicanKeyManager(key_manager.KeyManager):
def create_key_pair(self, context, algorithm, length, expiration=None):
"""Creates an asymmetric key pair.
Not implemented yet.
:param context: contains information of the user and the environment
for the request (castellan/context.py)
for the request (castellan/context.py)
:param algorithm: the algorithm associated with the secret
:param length: the bit length of the secret
:param expiration: the date the key will expire
:return: TODO: the UUIDs of the new key, in the order (private, public)
:return: the UUIDs of the new key, in the order (private, public)
:raises NotImplementedError: until implemented
:raises HTTPAuthError: if key creation fails with 401
:raises HTTPClientError: if key creation failes with 4xx
:raises HTTPServerError: if key creation fails with 5xx
"""
raise NotImplementedError()
barbican_client = self._get_barbican_client(context)
try:
key_pair_order = barbican_client.orders.create_asymmetric(
algorithm=algorithm,
bit_length=length,
expiration=expiration)
order_ref = key_pair_order.submit()
order = self._get_active_order(barbican_client, order_ref)
container = barbican_client.containers.get(order.container_ref)
private_key_uuid = self._retrieve_secret_uuid(
container.secret_refs['private_key'])
public_key_uuid = self._retrieve_secret_uuid(
container.secret_refs['public_key'])
return private_key_uuid, public_key_uuid
except (barbican_exceptions.HTTPAuthError,
barbican_exceptions.HTTPClientError,
barbican_exceptions.HTTPServerError) as e:
with excutils.save_and_reraise_exception():
LOG.error(u._LE("Error creating key pair: %s"), e)
def _get_barbican_object(self, barbican_client, managed_object):
"""Converts the Castellan managed_object to a Barbican secret."""
try:
algorithm = managed_object.algorithm
bit_length = managed_object.bit_length
except AttributeError:
algorithm = None
bit_length = None
secret_type = self._secret_type_dict.get(type(managed_object),
'opaque')
payload = self._get_normalized_payload(managed_object.get_encoded(),
secret_type)
secret = barbican_client.secrets.create(payload=payload,
algorithm=algorithm,
bit_length=bit_length,
secret_type=secret_type)
return secret
def _get_normalized_payload(self, encoded_bytes, secret_type):
"""Normalizes the bytes of the object.
Barbican expects certificates, public keys, and private keys in PEM
format, but Castellan expects these objects to be DER encoded bytes
instead.
"""
if secret_type == 'public':
key = serialization.load_der_public_key(
encoded_bytes,
backend=backends.default_backend())
return key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo)
elif secret_type == 'private':
key = serialization.load_der_private_key(
encoded_bytes,
backend=backends.default_backend(),
password=None)
return key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption())
elif secret_type == 'certificate':
cert = cryptography_x509.load_der_x509_certificate(
encoded_bytes,
backend=backends.default_backend())
return cert.public_bytes(encoding=serialization.Encoding.PEM)
else:
return encoded_bytes
def store(self, context, managed_object, expiration=None):
"""Stores (i.e., registers) an object with the key manager.
@ -168,15 +299,9 @@ class BarbicanKeyManager(key_manager.KeyManager):
barbican_client = self._get_barbican_client(context)
try:
if managed_object.algorithm:
algorithm = managed_object.algorithm
else:
algorithm = None
encoded_object = managed_object.get_encoded()
# TODO(kfarr) add support for objects other than symmetric keys
secret = barbican_client.secrets.create(payload=encoded_object,
algorithm=algorithm,
expiration=expiration)
secret = self._get_barbican_object(barbican_client,
managed_object)
secret.expiration = expiration
secret_ref = secret.store()
return self._retrieve_secret_uuid(secret_ref)
except (barbican_exceptions.HTTPAuthError,
@ -199,6 +324,40 @@ class BarbicanKeyManager(key_manager.KeyManager):
base_url += '/'
return urllib.parse.urljoin(base_url, "secrets/" + key_id)
def _get_active_order(self, barbican_client, order_ref):
"""Returns the order when it is active.
Barbican key creation is done asynchronously, so this loop continues
checking until the order is active or a timeout occurs.
"""
active = u'ACTIVE'
number_of_retries = self.conf.barbican.number_of_retries
retry_delay = self.conf.barbican.retry_delay
order = barbican_client.orders.get(order_ref)
time.sleep(.25)
for n in range(number_of_retries):
if order.status != active:
kwargs = {'attempt': n,
'total': number_of_retries,
'status': order.status,
'active': active,
'delay': retry_delay}
msg = u._LI("Retry attempt #%(attempt)i out of %(total)i: "
"Order status is '%(status)s'. Waiting for "
"'%(active)s', will retry in %(delay)s "
"seconds")
LOG.info(msg, kwargs)
time.sleep(retry_delay)
order = barbican_client.orders.get(order_ref)
else:
return order
msg = u._LE("Exceeded retries: Failed to find '%(active)s' status "
"within %(num_retries)i retries") % {'active': active,
'num_retries':
number_of_retries}
LOG.error(msg)
raise exception.KeyManagerError(msg)
def _retrieve_secret_uuid(self, secret_ref):
"""Retrieves the UUID of the secret from the secret_ref.
@ -213,19 +372,64 @@ class BarbicanKeyManager(key_manager.KeyManager):
return secret_ref.rpartition('/')[2]
def _get_secret_data(self, secret):
"""Retrieves the secret data given a secret and content_type.
"""Retrieves the secret data.
Converts the Barbican secret to bytes suitable for a Castellan object.
If the secret is a public key, private key, or certificate, the secret
is expected to be in PEM format and will be converted to DER.
:param secret: the secret from barbican with the payload of data
:returns: the secret data
"""
# TODO(kfarr) support other types of keys
return secret.payload
if secret.secret_type == 'public':
key = serialization.load_pem_public_key(
secret.payload,
backend=backends.default_backend())
return key.public_bytes(
encoding=serialization.Encoding.DER,
format=serialization.PublicFormat.SubjectPublicKeyInfo)
elif secret.secret_type == 'private':
key = serialization.load_pem_private_key(
secret.payload,
backend=backends.default_backend(),
password=None)
return key.private_bytes(
encoding=serialization.Encoding.DER,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption())
elif secret.secret_type == 'certificate':
cert = cryptography_x509.load_pem_x509_certificate(
secret.payload,
backend=backends.default_backend())
return cert.public_bytes(encoding=serialization.Encoding.DER)
else:
return secret.payload
def _get_castellan_object(self, secret):
"""Creates a Castellan managed object given the Barbican secret.
:param secret: the secret from barbican with the payload of data
:returns: the castellan object
"""
secret_type = op_data.OpaqueData
for castellan_type, barbican_type in self._secret_type_dict.items():
if barbican_type == secret.secret_type:
secret_type = castellan_type
secret_data = self._get_secret_data(secret)
if issubclass(secret_type, key_base_class.Key):
return secret_type(secret.algorithm,
secret.bit_length,
secret_data)
else:
return secret_type(secret_data)
def _get_secret(self, context, key_id):
"""Returns the metadata of the secret.
:param context: contains information of the user and the environment
for the request (castellan/context.py)
for the request (castellan/context.py)
:param key_id: UUID of the secret
:return: the secret's metadata
:raises HTTPAuthError: if object retrieval fails with 401
@ -250,7 +454,7 @@ class BarbicanKeyManager(key_manager.KeyManager):
Currently only supports retrieving symmetric keys.
:param context: contains information of the user and the environment
for the request (castellan/context.py)
for the request (castellan/context.py)
:param managed_object_id: the UUID of the object to retrieve
:return: SymmetricKey representation of the key
:raises HTTPAuthError: if object retrieval fails with 401
@ -259,12 +463,7 @@ class BarbicanKeyManager(key_manager.KeyManager):
"""
try:
secret = self._get_secret(context, managed_object_id)
secret_data = self._get_secret_data(secret)
# TODO(kfarr) add support for other objects
key = sym_key.SymmetricKey(secret.algorithm,
secret.bit_length,
secret_data)
return key
return self._get_castellan_object(secret)
except (barbican_exceptions.HTTPAuthError,
barbican_exceptions.HTTPClientError,
barbican_exceptions.HTTPServerError) as e:

View File

@ -17,11 +17,12 @@
Test cases for the barbican key manager.
"""
from barbicanclient import exceptions as barbican_exceptions
import mock
from oslo_config import cfg
from castellan.common import exception
from castellan.common.objects import symmetric_key as key_manager_key
from castellan.common.objects import symmetric_key as sym_key
from castellan.key_manager import barbican_key_manager
from castellan.tests.unit.key_manager import test_key_manager
@ -48,12 +49,16 @@ class BarbicanKeyManagerTestCase(test_key_manager.KeyManagerTestCase):
self.hex = ("0080f1429dbefae01b29a4d50cc5c5608bbc3c8ba0246aa42b424baa4"
"534ae16")
self.key_mgr._base_url = "http://host:9311/v1/"
self.key_mgr.conf.barbican.number_of_retries = 3
self.key_mgr.conf.barbican.retry_delay = 1
self.addCleanup(self._restore)
def _restore(self):
try:
getattr(self, 'original_key')
key_manager_key.SymmetricKey = self.original_key
sym_key.SymmetricKey = self.original_key
except AttributeError:
return None
@ -80,6 +85,7 @@ class BarbicanKeyManagerTestCase(test_key_manager.KeyManagerTestCase):
# Create order and assign return value
order = mock.Mock()
order.secret_ref = self.secret_ref
order.status = u'ACTIVE'
self.mock_barbican.orders.get.return_value = order
# Create the key, get the UUID
@ -90,11 +96,72 @@ class BarbicanKeyManagerTestCase(test_key_manager.KeyManagerTestCase):
self.mock_barbican.orders.get.assert_called_once_with(order_ref_url)
self.assertEqual(self.key_id, returned_uuid)
def test_create_null_context(self):
def test_create_key_null_context(self):
self.key_mgr._barbican_client = None
self.assertRaises(exception.Forbidden,
self.key_mgr.create_key, None, 'AES', 256)
def test_create_key_with_error(self):
key_order = mock.Mock()
self.mock_barbican.orders.create_key.return_value = key_order
key_order.submit = mock.Mock(
side_effect=barbican_exceptions.HTTPClientError('test error'))
self.assertRaises(barbican_exceptions.HTTPClientError,
self.key_mgr.create_key, self.ctxt, 'AES', 256)
def test_create_key_pair(self):
# Create order_ref_url and assign return value
order_ref_url = ("http://localhost:9311/v1/orders/"
"f45bf211-a917-4ead-9aec-1c91e52609df")
asym_order = mock.Mock()
self.mock_barbican.orders.create_asymmetric.return_value = asym_order
asym_order.submit.return_value = order_ref_url
# Create order and assign return value
order = mock.Mock()
container_id = "16caa8f4-dd34-4fb3-bf67-6c20533a30e4"
container_ref = ("http://localhost:9311/v1/containers/" + container_id)
order.container_ref = container_ref
order.status = u'ACTIVE'
self.mock_barbican.orders.get.return_value = order
# Create container and assign return value
container = mock.Mock()
public_key_id = "43ed09c3-e551-4c24-b612-e619abe9b534"
pub_key_ref = ("http://localhost:9311/v1/secrets/" + public_key_id)
private_key_id = "32a0bc60-4e10-4269-9f17-f49767e99586"
priv_key_ref = ("http://localhost:9311/v1/secrets/" + private_key_id)
container.secret_refs = {'public_key': pub_key_ref,
'private_key': priv_key_ref}
self.mock_barbican.containers.get.return_value = container
# Create the keys, get the UUIDs
returned_private_uuid, returned_public_uuid = (
self.key_mgr.create_key_pair(self.ctxt,
algorithm='RSA',
length=2048))
self.mock_barbican.orders.get.assert_called_once_with(order_ref_url)
self.mock_barbican.containers.get.assert_called_once_with(
container_ref)
self.mock_barbican.orders.get.assert_called_once_with(order_ref_url)
self.assertEqual(private_key_id, returned_private_uuid)
self.assertEqual(public_key_id, returned_public_uuid)
def test_create_key_pair_null_context(self):
self.key_mgr._barbican_client = None
self.assertRaises(exception.Forbidden,
self.key_mgr.create_key_pair, None, 'RSA', 2048)
def test_create_key_pair_with_error(self):
asym_order = mock.Mock()
self.mock_barbican.orders.create_asymmetric.return_value = asym_order
asym_order.submit = mock.Mock(
side_effect=barbican_exceptions.HTTPClientError('test error'))
self.assertRaises(barbican_exceptions.HTTPClientError,
self.key_mgr.create_key_pair, self.ctxt, 'RSA', 2048)
def test_delete_null_context(self):
self.key_mgr._barbican_client = None
self.assertRaises(exception.Forbidden,
@ -108,18 +175,25 @@ class BarbicanKeyManagerTestCase(test_key_manager.KeyManagerTestCase):
self.assertRaises(exception.KeyManagerError,
self.key_mgr.delete, self.ctxt, None)
def test_delete_with_error(self):
self.mock_barbican.secrets.delete = mock.Mock(
side_effect=barbican_exceptions.HTTPClientError('test error'))
self.assertRaises(barbican_exceptions.HTTPClientError,
self.key_mgr.delete, self.ctxt, self.key_id)
def test_get_key(self):
original_secret_metadata = mock.Mock()
original_secret_metadata.algorithm = mock.sentinel.alg
original_secret_metadata.bit_length = mock.sentinel.bit
original_secret_data = mock.Mock()
original_secret_metadata.secret_type = 'symmetric'
original_secret_data = b'test key'
original_secret_metadata.payload = original_secret_data
self.mock_barbican.secrets.get.return_value = original_secret_metadata
key = self.key_mgr.get(self.ctxt, self.key_id)
self.get.assert_called_once_with(self.secret_ref)
self.assertEqual(key.get_encoded(), original_secret_data)
self.assertEqual(original_secret_data, key.get_encoded())
def test_get_null_context(self):
self.key_mgr._barbican_client = None
@ -130,12 +204,19 @@ class BarbicanKeyManagerTestCase(test_key_manager.KeyManagerTestCase):
self.assertRaises(exception.KeyManagerError,
self.key_mgr.get, self.ctxt, None)
def test_store_key_base64(self):
def test_get_with_error(self):
self.mock_barbican.secrets.get = mock.Mock(
side_effect=barbican_exceptions.HTTPClientError('test error'))
self.assertRaises(barbican_exceptions.HTTPClientError,
self.key_mgr.get, self.ctxt, self.key_id)
def test_store_key(self):
# Create Key to store
secret_key = bytes(b'\x01\x02\xA0\xB3')
_key = key_manager_key.SymmetricKey('AES',
len(secret_key) * 8,
secret_key)
key_length = len(secret_key) * 8
_key = sym_key.SymmetricKey('AES',
key_length,
secret_key)
# Define the return values
secret = mock.Mock()
@ -146,25 +227,66 @@ class BarbicanKeyManagerTestCase(test_key_manager.KeyManagerTestCase):
returned_uuid = self.key_mgr.store(self.ctxt, _key)
self.create.assert_called_once_with(algorithm='AES',
bit_length=key_length,
payload=secret_key,
expiration=None)
secret_type='symmetric')
self.assertEqual(self.key_id, returned_uuid)
def test_store_key_plaintext(self):
# Create the plaintext key
secret_key_text = "This is a test text key."
_key = key_manager_key.SymmetricKey('AES',
len(secret_key_text) * 8,
secret_key_text)
# Store the Key
self.key_mgr.store(self.ctxt, _key)
self.create.assert_called_once_with(algorithm='AES',
payload=secret_key_text,
expiration=None)
self.assertEqual(0, self.store.call_count)
def test_store_null_context(self):
self.key_mgr._barbican_client = None
self.assertRaises(exception.Forbidden,
self.key_mgr.store, None, None)
def test_store_with_error(self):
self.mock_barbican.secrets.create = mock.Mock(
side_effect=barbican_exceptions.HTTPClientError('test error'))
secret_key = bytes(b'\x01\x02\xA0\xB3')
key_length = len(secret_key) * 8
_key = sym_key.SymmetricKey('AES',
key_length,
secret_key)
self.assertRaises(barbican_exceptions.HTTPClientError,
self.key_mgr.store, self.ctxt, _key)
def test_get_active_order(self):
order_ref_url = ("http://localhost:9311/v1/orders/"
"4fe939b7-72bc-49aa-bd1e-e979589858af")
pending_order = mock.Mock()
pending_order.status = u'PENDING'
pending_order.order_ref = order_ref_url
active_order = mock.Mock()
active_order.secret_ref = self.secret_ref
active_order.status = u'ACTIVE'
active_order.order_ref = order_ref_url
self.mock_barbican.orders.get.side_effect = [pending_order,
active_order]
self.key_mgr._get_active_order(self.mock_barbican, order_ref_url)
self.assertEqual(2, self.mock_barbican.orders.get.call_count)
calls = [mock.call(order_ref_url), mock.call(order_ref_url)]
self.mock_barbican.orders.get.assert_has_calls(calls)
def test_get_active_order_timeout(self):
order_ref_url = ("http://localhost:9311/v1/orders/"
"4fe939b7-72bc-49aa-bd1e-e979589858af")
number_of_retries = self.key_mgr.conf.barbican.number_of_retries
pending_order = mock.Mock()
pending_order.status = u'PENDING'
pending_order.order_ref = order_ref_url
self.mock_barbican.orders.get.return_value = pending_order
self.assertRaises(exception.KeyManagerError,
self.key_mgr._get_active_order,
self.mock_barbican,
order_ref_url)
self.assertEqual(number_of_retries + 1,
self.mock_barbican.orders.get.call_count)

View File

@ -14,9 +14,7 @@
# under the License.
"""
These utilility functions are borrowed from Barbican's testing utilites.
"""
"""These utility functions are borrowed from Barbican's testing utilities."""
def get_certificate_der():