15 | 15 |
"""
|
16 | 16 |
Key manager implementation for Barbican
|
17 | 17 |
"""
|
18 | |
from barbicanclient import client as barbican_client
|
19 | |
from barbicanclient import exceptions as barbican_exceptions
|
20 | |
from keystoneclient.auth import token_endpoint
|
|
18 |
import time
|
|
19 |
|
|
20 |
from cryptography.hazmat import backends
|
|
21 |
from cryptography.hazmat.primitives import serialization
|
|
22 |
from cryptography import x509 as cryptography_x509
|
|
23 |
from keystoneclient.auth import identity
|
21 | 24 |
from keystoneclient import session
|
22 | 25 |
from oslo_config import cfg
|
23 | 26 |
from oslo_log import log as logging
|
24 | 27 |
from oslo_utils import excutils
|
25 | 28 |
|
26 | 29 |
from castellan.common import exception
|
|
30 |
from castellan.common.objects import key as key_base_class
|
|
31 |
from castellan.common.objects import opaque_data as op_data
|
|
32 |
from castellan.common.objects import passphrase
|
|
33 |
from castellan.common.objects import private_key as pri_key
|
|
34 |
from castellan.common.objects import public_key as pub_key
|
27 | 35 |
from castellan.common.objects import symmetric_key as sym_key
|
|
36 |
from castellan.common.objects import x_509
|
28 | 37 |
from castellan.key_manager import key_manager
|
29 | 38 |
from castellan.openstack.common import _i18n as u
|
30 | 39 |
|
|
40 |
from barbicanclient import client as barbican_client
|
|
41 |
from barbicanclient import exceptions as barbican_exceptions
|
31 | 42 |
from six.moves import urllib
|
32 | 43 |
|
33 | 44 |
barbican_opts = [
|
34 | 45 |
cfg.StrOpt('barbican_endpoint',
|
35 | |
default='http://localhost:9311/',
|
36 | |
help='Use this endpoint to connect to Barbican'),
|
|
46 |
help='Use this endpoint to connect to Barbican, for example: '
|
|
47 |
'"http://localhost:9311/"'),
|
37 | 48 |
cfg.StrOpt('barbican_api_version',
|
38 | |
default='v1',
|
39 | |
help='Version of the Barbican API'),
|
|
49 |
help='Version of the Barbican API, for example: "v1"'),
|
|
50 |
cfg.StrOpt('auth_endpoint',
|
|
51 |
default='http://localhost:5000/v3',
|
|
52 |
help='Use this endpoint to connect to Keystone'),
|
|
53 |
cfg.IntOpt('retry_delay',
|
|
54 |
default=1,
|
|
55 |
help='Number of seconds to wait before retrying poll for key '
|
|
56 |
'creation completion'),
|
|
57 |
cfg.IntOpt('number_of_retries',
|
|
58 |
default=60,
|
|
59 |
help='Number of times to retry poll for key creation '
|
|
60 |
'completion'),
|
40 | 61 |
]
|
41 | 62 |
|
42 | 63 |
BARBICAN_OPT_GROUP = 'barbican'
|
|
46 | 67 |
|
47 | 68 |
class BarbicanKeyManager(key_manager.KeyManager):
|
48 | 69 |
"""Key Manager Interface that wraps the Barbican client API."""
|
|
70 |
|
|
71 |
_secret_type_dict = {
|
|
72 |
op_data.OpaqueData: 'opaque',
|
|
73 |
passphrase.Passphrase: 'passphrase',
|
|
74 |
pri_key.PrivateKey: 'private',
|
|
75 |
pub_key.PublicKey: 'public',
|
|
76 |
sym_key.SymmetricKey: 'symmetric',
|
|
77 |
x_509.X509: 'certificate'}
|
49 | 78 |
|
50 | 79 |
def __init__(self, configuration):
|
51 | 80 |
self._barbican_client = None
|
|
60 | 89 |
:param context: the user context for authentication
|
61 | 90 |
:return: a Barbican Client object
|
62 | 91 |
:raises Forbidden: if the context is None
|
|
92 |
:raises KeyManagerError: if context is missing tenant or
|
|
93 |
tenant is None
|
63 | 94 |
"""
|
64 | 95 |
|
65 | 96 |
# Confirm context is provided, if not raise forbidden
|
|
68 | 99 |
LOG.error(msg)
|
69 | 100 |
raise exception.Forbidden(msg)
|
70 | 101 |
|
|
102 |
if not hasattr(context, 'tenant') or context.tenant is None:
|
|
103 |
msg = u._("Unable to create Barbican Client without tenant "
|
|
104 |
"attribute in context object.")
|
|
105 |
LOG.error(msg)
|
|
106 |
raise exception.KeyManagerError(msg)
|
|
107 |
|
71 | 108 |
if self._barbican_client and self._current_context == context:
|
72 | |
return self._barbican_client
|
|
109 |
return self._barbican_client
|
73 | 110 |
|
74 | 111 |
try:
|
75 | 112 |
self._current_context = context
|
76 | |
sess = self._get_keystone_session(context)
|
77 | |
|
|
113 |
auth = self._get_keystone_auth(context)
|
|
114 |
sess = session.Session(auth=auth)
|
|
115 |
|
|
116 |
self._barbican_endpoint = self._get_barbican_endpoint(auth, sess)
|
78 | 117 |
self._barbican_client = barbican_client.Client(
|
79 | 118 |
session=sess,
|
80 | 119 |
endpoint=self._barbican_endpoint)
|
|
83 | 122 |
with excutils.save_and_reraise_exception():
|
84 | 123 |
LOG.error(u._LE("Error creating Barbican client: %s"), e)
|
85 | 124 |
|
86 | |
self._base_url = self._create_base_url()
|
|
125 |
self._base_url = self._create_base_url(auth,
|
|
126 |
sess,
|
|
127 |
self._barbican_endpoint)
|
87 | 128 |
|
88 | 129 |
return self._barbican_client
|
89 | 130 |
|
90 | |
def _get_keystone_session(self, context):
|
91 | |
sess = session.Session.load_from_conf_options(
|
92 | |
self.conf, BARBICAN_OPT_GROUP)
|
93 | |
|
94 | |
self._barbican_endpoint = self.conf.barbican.barbican_endpoint
|
95 | |
|
96 | |
auth = token_endpoint.Token(self._barbican_endpoint,
|
97 | |
context.auth_token)
|
98 | |
sess.auth = auth
|
99 | |
return sess
|
100 | |
|
101 | |
def _create_base_url(self):
|
|
131 |
def _get_keystone_auth(self, context):
|
|
132 |
# TODO(kfarr): support keystone v2
|
|
133 |
auth = identity.v3.Token(
|
|
134 |
auth_url=self.conf.barbican.auth_endpoint,
|
|
135 |
token=context.auth_token,
|
|
136 |
project_id=context.tenant,
|
|
137 |
domain_id=context.user_domain,
|
|
138 |
project_domain_id=context.project_domain)
|
|
139 |
return auth
|
|
140 |
|
|
141 |
def _get_barbican_endpoint(self, auth, sess):
|
|
142 |
if self.conf.barbican.barbican_endpoint:
|
|
143 |
return self.conf.barbican.barbican_endpoint
|
|
144 |
else:
|
|
145 |
service_parameters = {'service_type': 'key-manager',
|
|
146 |
'service_name': 'barbican',
|
|
147 |
'interface': 'public'}
|
|
148 |
return auth.get_endpoint(sess, **service_parameters)
|
|
149 |
|
|
150 |
def _create_base_url(self, auth, sess, endpoint):
|
|
151 |
if self.conf.barbican.barbican_api_version:
|
|
152 |
api_version = self.conf.barbican.barbican_api_version
|
|
153 |
else:
|
|
154 |
discovery = auth.get_discovery(sess, url=endpoint)
|
|
155 |
raw_data = discovery.raw_version_data()
|
|
156 |
if len(raw_data) == 0:
|
|
157 |
msg = u._LE(
|
|
158 |
"Could not find discovery information for %s") % endpoint
|
|
159 |
LOG.error(msg)
|
|
160 |
raise exception.KeyManagerError(msg)
|
|
161 |
latest_version = raw_data[-1]
|
|
162 |
api_version = latest_version.get('id')
|
|
163 |
|
102 | 164 |
base_url = urllib.parse.urljoin(
|
103 | |
self._barbican_endpoint, self.conf.barbican.barbican_api_version)
|
|
165 |
endpoint, api_version)
|
104 | 166 |
return base_url
|
105 | 167 |
|
106 | 168 |
def create_key(self, context, algorithm, length, expiration=None):
|
107 | 169 |
"""Creates a symmetric key.
|
108 | 170 |
|
109 | 171 |
:param context: contains information of the user and the environment
|
110 | |
for the request (castellan/context.py)
|
|
172 |
for the request (castellan/context.py)
|
111 | 173 |
:param algorithm: the algorithm associated with the secret
|
112 | 174 |
:param length: the bit length of the secret
|
113 | 175 |
:param expiration: the date the key will expire
|
|
124 | 186 |
bit_length=length,
|
125 | 187 |
expiration=expiration)
|
126 | 188 |
order_ref = key_order.submit()
|
127 | |
order = barbican_client.orders.get(order_ref)
|
|
189 |
order = self._get_active_order(barbican_client, order_ref)
|
128 | 190 |
return self._retrieve_secret_uuid(order.secret_ref)
|
129 | 191 |
except (barbican_exceptions.HTTPAuthError,
|
130 | 192 |
barbican_exceptions.HTTPClientError,
|
|
135 | 197 |
def create_key_pair(self, context, algorithm, length, expiration=None):
|
136 | 198 |
"""Creates an asymmetric key pair.
|
137 | 199 |
|
138 | |
Not implemented yet.
|
139 | |
|
140 | |
:param context: contains information of the user and the environment
|
141 | |
for the request (castellan/context.py)
|
|
200 |
:param context: contains information of the user and the environment
|
|
201 |
for the request (castellan/context.py)
|
142 | 202 |
:param algorithm: the algorithm associated with the secret
|
143 | 203 |
:param length: the bit length of the secret
|
144 | 204 |
:param expiration: the date the key will expire
|
145 | |
:return: TODO: the UUIDs of the new key, in the order (private, public)
|
|
205 |
:return: the UUIDs of the new key, in the order (private, public)
|
146 | 206 |
:raises NotImplementedError: until implemented
|
147 | 207 |
:raises HTTPAuthError: if key creation fails with 401
|
148 | 208 |
:raises HTTPClientError: if key creation failes with 4xx
|
149 | 209 |
:raises HTTPServerError: if key creation fails with 5xx
|
150 | 210 |
"""
|
151 | |
raise NotImplementedError()
|
|
211 |
barbican_client = self._get_barbican_client(context)
|
|
212 |
|
|
213 |
try:
|
|
214 |
key_pair_order = barbican_client.orders.create_asymmetric(
|
|
215 |
algorithm=algorithm,
|
|
216 |
bit_length=length,
|
|
217 |
expiration=expiration)
|
|
218 |
|
|
219 |
order_ref = key_pair_order.submit()
|
|
220 |
order = self._get_active_order(barbican_client, order_ref)
|
|
221 |
container = barbican_client.containers.get(order.container_ref)
|
|
222 |
|
|
223 |
private_key_uuid = self._retrieve_secret_uuid(
|
|
224 |
container.secret_refs['private_key'])
|
|
225 |
public_key_uuid = self._retrieve_secret_uuid(
|
|
226 |
container.secret_refs['public_key'])
|
|
227 |
return private_key_uuid, public_key_uuid
|
|
228 |
except (barbican_exceptions.HTTPAuthError,
|
|
229 |
barbican_exceptions.HTTPClientError,
|
|
230 |
barbican_exceptions.HTTPServerError) as e:
|
|
231 |
with excutils.save_and_reraise_exception():
|
|
232 |
LOG.error(u._LE("Error creating key pair: %s"), e)
|
|
233 |
|
|
234 |
def _get_barbican_object(self, barbican_client, managed_object):
|
|
235 |
"""Converts the Castellan managed_object to a Barbican secret."""
|
|
236 |
try:
|
|
237 |
algorithm = managed_object.algorithm
|
|
238 |
bit_length = managed_object.bit_length
|
|
239 |
except AttributeError:
|
|
240 |
algorithm = None
|
|
241 |
bit_length = None
|
|
242 |
|
|
243 |
secret_type = self._secret_type_dict.get(type(managed_object),
|
|
244 |
'opaque')
|
|
245 |
payload = self._get_normalized_payload(managed_object.get_encoded(),
|
|
246 |
secret_type)
|
|
247 |
secret = barbican_client.secrets.create(payload=payload,
|
|
248 |
algorithm=algorithm,
|
|
249 |
bit_length=bit_length,
|
|
250 |
secret_type=secret_type)
|
|
251 |
return secret
|
|
252 |
|
|
253 |
def _get_normalized_payload(self, encoded_bytes, secret_type):
|
|
254 |
"""Normalizes the bytes of the object.
|
|
255 |
|
|
256 |
Barbican expects certificates, public keys, and private keys in PEM
|
|
257 |
format, but Castellan expects these objects to be DER encoded bytes
|
|
258 |
instead.
|
|
259 |
"""
|
|
260 |
if secret_type == 'public':
|
|
261 |
key = serialization.load_der_public_key(
|
|
262 |
encoded_bytes,
|
|
263 |
backend=backends.default_backend())
|
|
264 |
return key.public_bytes(
|
|
265 |
encoding=serialization.Encoding.PEM,
|
|
266 |
format=serialization.PublicFormat.SubjectPublicKeyInfo)
|
|
267 |
elif secret_type == 'private':
|
|
268 |
key = serialization.load_der_private_key(
|
|
269 |
encoded_bytes,
|
|
270 |
backend=backends.default_backend(),
|
|
271 |
password=None)
|
|
272 |
return key.private_bytes(
|
|
273 |
encoding=serialization.Encoding.PEM,
|
|
274 |
format=serialization.PrivateFormat.PKCS8,
|
|
275 |
encryption_algorithm=serialization.NoEncryption())
|
|
276 |
elif secret_type == 'certificate':
|
|
277 |
cert = cryptography_x509.load_der_x509_certificate(
|
|
278 |
encoded_bytes,
|
|
279 |
backend=backends.default_backend())
|
|
280 |
return cert.public_bytes(encoding=serialization.Encoding.PEM)
|
|
281 |
else:
|
|
282 |
return encoded_bytes
|
152 | 283 |
|
153 | 284 |
def store(self, context, managed_object, expiration=None):
|
154 | 285 |
"""Stores (i.e., registers) an object with the key manager.
|
|
167 | 298 |
barbican_client = self._get_barbican_client(context)
|
168 | 299 |
|
169 | 300 |
try:
|
170 | |
if managed_object.algorithm:
|
171 | |
algorithm = managed_object.algorithm
|
172 | |
else:
|
173 | |
algorithm = None
|
174 | |
encoded_object = managed_object.get_encoded()
|
175 | |
# TODO(kfarr) add support for objects other than symmetric keys
|
176 | |
secret = barbican_client.secrets.create(payload=encoded_object,
|
177 | |
algorithm=algorithm,
|
178 | |
expiration=expiration)
|
|
301 |
secret = self._get_barbican_object(barbican_client,
|
|
302 |
managed_object)
|
|
303 |
secret.expiration = expiration
|
179 | 304 |
secret_ref = secret.store()
|
180 | 305 |
return self._retrieve_secret_uuid(secret_ref)
|
181 | 306 |
except (barbican_exceptions.HTTPAuthError,
|
|
198 | 323 |
base_url += '/'
|
199 | 324 |
return urllib.parse.urljoin(base_url, "secrets/" + key_id)
|
200 | 325 |
|
|
326 |
def _get_active_order(self, barbican_client, order_ref):
|
|
327 |
"""Returns the order when it is active.
|
|
328 |
|
|
329 |
Barbican key creation is done asynchronously, so this loop continues
|
|
330 |
checking until the order is active or a timeout occurs.
|
|
331 |
"""
|
|
332 |
active = u'ACTIVE'
|
|
333 |
number_of_retries = self.conf.barbican.number_of_retries
|
|
334 |
retry_delay = self.conf.barbican.retry_delay
|
|
335 |
order = barbican_client.orders.get(order_ref)
|
|
336 |
time.sleep(.25)
|
|
337 |
for n in range(number_of_retries):
|
|
338 |
if order.status != active:
|
|
339 |
kwargs = {'attempt': n,
|
|
340 |
'total': number_of_retries,
|
|
341 |
'status': order.status,
|
|
342 |
'active': active,
|
|
343 |
'delay': retry_delay}
|
|
344 |
msg = u._LI("Retry attempt #%(attempt)i out of %(total)i: "
|
|
345 |
"Order status is '%(status)s'. Waiting for "
|
|
346 |
"'%(active)s', will retry in %(delay)s "
|
|
347 |
"seconds")
|
|
348 |
LOG.info(msg, kwargs)
|
|
349 |
time.sleep(retry_delay)
|
|
350 |
order = barbican_client.orders.get(order_ref)
|
|
351 |
else:
|
|
352 |
return order
|
|
353 |
msg = u._LE("Exceeded retries: Failed to find '%(active)s' status "
|
|
354 |
"within %(num_retries)i retries") % {'active': active,
|
|
355 |
'num_retries':
|
|
356 |
number_of_retries}
|
|
357 |
LOG.error(msg)
|
|
358 |
raise exception.KeyManagerError(msg)
|
|
359 |
|
201 | 360 |
def _retrieve_secret_uuid(self, secret_ref):
|
202 | 361 |
"""Retrieves the UUID of the secret from the secret_ref.
|
203 | 362 |
|
|
212 | 371 |
return secret_ref.rpartition('/')[2]
|
213 | 372 |
|
214 | 373 |
def _get_secret_data(self, secret):
|
215 | |
"""Retrieves the secret data given a secret and content_type.
|
|
374 |
"""Retrieves the secret data.
|
|
375 |
|
|
376 |
Converts the Barbican secret to bytes suitable for a Castellan object.
|
|
377 |
If the secret is a public key, private key, or certificate, the secret
|
|
378 |
is expected to be in PEM format and will be converted to DER.
|
216 | 379 |
|
217 | 380 |
:param secret: the secret from barbican with the payload of data
|
218 | 381 |
:returns: the secret data
|
219 | 382 |
"""
|
220 | |
# TODO(kfarr) support other types of keys
|
221 | |
return secret.payload
|
|
383 |
if secret.secret_type == 'public':
|
|
384 |
key = serialization.load_pem_public_key(
|
|
385 |
secret.payload,
|
|
386 |
backend=backends.default_backend())
|
|
387 |
return key.public_bytes(
|
|
388 |
encoding=serialization.Encoding.DER,
|
|
389 |
format=serialization.PublicFormat.SubjectPublicKeyInfo)
|
|
390 |
elif secret.secret_type == 'private':
|
|
391 |
key = serialization.load_pem_private_key(
|
|
392 |
secret.payload,
|
|
393 |
backend=backends.default_backend(),
|
|
394 |
password=None)
|
|
395 |
return key.private_bytes(
|
|
396 |
encoding=serialization.Encoding.DER,
|
|
397 |
format=serialization.PrivateFormat.PKCS8,
|
|
398 |
encryption_algorithm=serialization.NoEncryption())
|
|
399 |
elif secret.secret_type == 'certificate':
|
|
400 |
cert = cryptography_x509.load_pem_x509_certificate(
|
|
401 |
secret.payload,
|
|
402 |
backend=backends.default_backend())
|
|
403 |
return cert.public_bytes(encoding=serialization.Encoding.DER)
|
|
404 |
else:
|
|
405 |
return secret.payload
|
|
406 |
|
|
407 |
def _get_castellan_object(self, secret):
|
|
408 |
"""Creates a Castellan managed object given the Barbican secret.
|
|
409 |
|
|
410 |
:param secret: the secret from barbican with the payload of data
|
|
411 |
:returns: the castellan object
|
|
412 |
"""
|
|
413 |
secret_type = op_data.OpaqueData
|
|
414 |
for castellan_type, barbican_type in self._secret_type_dict.items():
|
|
415 |
if barbican_type == secret.secret_type:
|
|
416 |
secret_type = castellan_type
|
|
417 |
|
|
418 |
secret_data = self._get_secret_data(secret)
|
|
419 |
|
|
420 |
if issubclass(secret_type, key_base_class.Key):
|
|
421 |
return secret_type(secret.algorithm,
|
|
422 |
secret.bit_length,
|
|
423 |
secret_data)
|
|
424 |
else:
|
|
425 |
return secret_type(secret_data)
|
222 | 426 |
|
223 | 427 |
def _get_secret(self, context, key_id):
|
224 | 428 |
"""Returns the metadata of the secret.
|
225 | 429 |
|
226 | 430 |
:param context: contains information of the user and the environment
|
227 | |
for the request (castellan/context.py)
|
|
431 |
for the request (castellan/context.py)
|
228 | 432 |
:param key_id: UUID of the secret
|
229 | 433 |
:return: the secret's metadata
|
230 | 434 |
:raises HTTPAuthError: if object retrieval fails with 401
|
|
249 | 453 |
Currently only supports retrieving symmetric keys.
|
250 | 454 |
|
251 | 455 |
:param context: contains information of the user and the environment
|
252 | |
for the request (castellan/context.py)
|
|
456 |
for the request (castellan/context.py)
|
253 | 457 |
:param managed_object_id: the UUID of the object to retrieve
|
254 | 458 |
:return: SymmetricKey representation of the key
|
255 | 459 |
:raises HTTPAuthError: if object retrieval fails with 401
|
|
258 | 462 |
"""
|
259 | 463 |
try:
|
260 | 464 |
secret = self._get_secret(context, managed_object_id)
|
261 | |
secret_data = self._get_secret_data(secret)
|
262 | |
# TODO(kfarr) add support for other objects
|
263 | |
key = sym_key.SymmetricKey(secret.algorithm,
|
264 | |
secret.bit_length,
|
265 | |
secret_data)
|
266 | |
return key
|
|
465 |
return self._get_castellan_object(secret)
|
267 | 466 |
except (barbican_exceptions.HTTPAuthError,
|
268 | 467 |
barbican_exceptions.HTTPClientError,
|
269 | 468 |
barbican_exceptions.HTTPServerError) as e:
|