Merge "Code to pass through transport_key_id when storing secret"
This commit is contained in:
commit
1926fb13d5
8 files changed
+228
-52
No files matched your search
@@ -113,6 +113,8 @@ class SecretController(object):
|
||||
)
|
||||
)
|
||||
|
||||
transport_key_id = kwargs.get('transport_key_id')
|
||||
|
||||
payload = pecan.request.body
|
||||
if not payload:
|
||||
raise exception.NoDataToProcess()
|
||||
@@ -135,7 +137,8 @@ class SecretController(object):
|
||||
|
||||
plugin.store_secret(payload, content_type,
|
||||
content_encoding, secret_model.to_dict_fields(),
|
||||
secret_model, tenant_model, self.repos)
|
||||
secret_model, tenant_model, self.repos,
|
||||
transport_key_id=transport_key_id)
|
||||
|
||||
@index.when(method='DELETE')
|
||||
@controllers.handle_exceptions(u._('Secret deletion'))
|
||||
@@ -248,7 +251,8 @@ class SecretsController(object):
|
||||
data.get('payload_content_encoding'),
|
||||
data, None, tenant,
|
||||
self.repos,
|
||||
transport_key_needed=transport_key_needed)
|
||||
transport_key_needed=transport_key_needed,
|
||||
transport_key_id=data.get('transport_key_id'))
|
||||
|
||||
pecan.response.status = 201
|
||||
pecan.response.headers['Location'] = '/{0}/secrets/{1}'.format(
|
||||
|
||||
@@ -129,6 +129,7 @@ class NewSecretValidator(ValidatorBase):
|
||||
"type": "string",
|
||||
"enum": ["true", "false"]
|
||||
},
|
||||
"transport_key_id": {"type": "string"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
+31
-13
@@ -128,10 +128,14 @@ class DogtagPlugin(sstore.SecretStoreBase):
|
||||
def store_secret(self, secret_dto, context):
|
||||
"""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.
|
||||
If secret_dto.transport_key is not None, then we expect
|
||||
secret_dto.secret to include a base64 encoded PKIArchiveOptions
|
||||
structure as defined in section 6.4 of RFC 2511. This package contains
|
||||
a transport key wrapped session key, the session key wrapped secret
|
||||
and parameters to specify the symmetric key wrapping.
|
||||
|
||||
Otherwise, the data is unencrypted and we use a call to archive_key()
|
||||
to have the Dogtag DRM client generate the relevant session keys.
|
||||
|
||||
The secret_dto contains additional information on the type of secret
|
||||
that is being stored. We will use that shortly. For, now, lets just
|
||||
@@ -142,11 +146,23 @@ class DogtagPlugin(sstore.SecretStoreBase):
|
||||
"""
|
||||
data_type = key.KeyClient.PASS_PHRASE_TYPE
|
||||
client_key_id = uuid.uuid4().hex
|
||||
response = self.keyclient.archive_key(client_key_id,
|
||||
data_type,
|
||||
secret_dto.secret,
|
||||
key_algorithm=None,
|
||||
key_size=None)
|
||||
if secret_dto.transport_key is not None:
|
||||
# TODO(alee-3) send the transport key with the archival request
|
||||
# once the Dogtag Client API changes.
|
||||
response = self.keyclient.archive_pki_options(
|
||||
client_key_id,
|
||||
data_type,
|
||||
secret_dto.secret,
|
||||
key_algorithm=None,
|
||||
key_size=None)
|
||||
else:
|
||||
response = self.keyclient.archive_key(
|
||||
client_key_id,
|
||||
data_type,
|
||||
secret_dto.secret,
|
||||
key_algorithm=None,
|
||||
key_size=None)
|
||||
|
||||
return {DogtagPlugin.SECRET_TYPE: secret_dto.type,
|
||||
DogtagPlugin.SECRET_KEYSPEC: secret_dto.key_spec,
|
||||
DogtagPlugin.KEY_ID: response.get_key_id()}
|
||||
@@ -176,10 +192,12 @@ class DogtagPlugin(sstore.SecretStoreBase):
|
||||
|
||||
# TODO(alee) remove final field when content_type is removed
|
||||
# from secret_dto
|
||||
ret = sstore.SecretDTO(secret_metadata[DogtagPlugin.SECRET_TYPE],
|
||||
recovered_key,
|
||||
secret_metadata[DogtagPlugin.SECRET_KEYSPEC],
|
||||
None)
|
||||
ret = sstore.SecretDTO(
|
||||
type=secret_metadata[DogtagPlugin.SECRET_TYPE],
|
||||
secret=recovered_key,
|
||||
key_spec=secret_metadata[DogtagPlugin.SECRET_KEYSPEC],
|
||||
content_type=None,
|
||||
transport_key=None)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
@@ -196,7 +196,7 @@ class SecretDTO(object):
|
||||
|
||||
#TODO(john-wood-w) Remove 'content_type' once secret normalization work is
|
||||
# completed.
|
||||
def __init__(self, type, secret, key_spec, content_type):
|
||||
def __init__(self, type, secret, key_spec, content_type, transport_key):
|
||||
"""Creates a new SecretDTO.
|
||||
|
||||
The secret is stored in the secret parameter. In the future this
|
||||
@@ -207,11 +207,15 @@ class SecretDTO(object):
|
||||
: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'
|
||||
:param transport_key: presence of this parameter indicates that the
|
||||
secret has been encrypted using a transport key. The transport
|
||||
key is a base64 encoded x509 transport certificate.
|
||||
"""
|
||||
self.type = type
|
||||
self.secret = secret
|
||||
self.key_spec = key_spec
|
||||
self.content_type = content_type
|
||||
self.transport_key = transport_key
|
||||
|
||||
|
||||
#TODO(john-wood-w) Remove this class once repository factory work is
|
||||
@@ -378,8 +382,9 @@ class SecretStorePluginManager(named.NamedExtensionManager):
|
||||
invoke_kwds=invoke_kwargs
|
||||
)
|
||||
|
||||
def get_plugin_store(self, transport_key_needed=False):
|
||||
def get_plugin_store(self, plugin_name=None, transport_key_needed=False):
|
||||
"""Gets a secret store plugin.
|
||||
:param: plugin_name: set to plugin_name to get specific plugin
|
||||
:param: transport_key_needed: set to True if a transport
|
||||
key is required.
|
||||
:returns: SecretStoreBase plugin implementation
|
||||
@@ -388,6 +393,12 @@ class SecretStorePluginManager(named.NamedExtensionManager):
|
||||
if len(self.extensions) < 1:
|
||||
raise SecretStorePluginNotFound()
|
||||
|
||||
if plugin_name is not None:
|
||||
for ext in self.extensions:
|
||||
if utils.generate_fullname_for(ext.obj) == plugin_name:
|
||||
return ext.obj
|
||||
raise SecretStoreSupportedPluginNotFound()
|
||||
|
||||
if not transport_key_needed:
|
||||
return self.extensions[0].obj
|
||||
|
||||
|
||||
@@ -17,9 +17,50 @@ from barbican.plugin.interface import secret_store
|
||||
from barbican.plugin.util import translations as tr
|
||||
|
||||
|
||||
def get_transport_key_model(repos, transport_key_needed):
|
||||
key_model = None
|
||||
if transport_key_needed:
|
||||
# get_plugin_store() will throw an exception if no suitable
|
||||
# plugin with transport key is found
|
||||
store_plugin = secret_store.SecretStorePluginManager(). \
|
||||
get_plugin_store(transport_key_needed=True)
|
||||
plugin_name = utils.generate_fullname_for(store_plugin)
|
||||
|
||||
key_repo = repos.transport_key_repo
|
||||
key_model = key_repo.get_latest_transport_key(plugin_name)
|
||||
|
||||
if not key_model or not store_plugin.is_transport_key_current(
|
||||
key_model.transport_key):
|
||||
# transport key does not exist or is not current.
|
||||
# need to get a new transport key
|
||||
transport_key = store_plugin.get_transport_key()
|
||||
new_key_model = models.TransportKey(plugin_name, transport_key)
|
||||
key_model = key_repo.create_from(new_key_model)
|
||||
return key_model
|
||||
|
||||
|
||||
def get_plugin_name_and_transport_key(repos, transport_key_id):
|
||||
plugin_name = None
|
||||
transport_key = None
|
||||
if transport_key_id is not None:
|
||||
transport_key_model = repos.transport_key_repo.get(
|
||||
entity_id=transport_key_id)
|
||||
if transport_key_model is None:
|
||||
raise ValueError("Invalid transport key ID provided")
|
||||
|
||||
plugin_name = transport_key_model.plugin_name
|
||||
if plugin_name is None:
|
||||
raise ValueError("Invalid plugin name for transport key")
|
||||
|
||||
transport_key = transport_key_model.transport_key
|
||||
|
||||
return plugin_name, transport_key
|
||||
|
||||
|
||||
def store_secret(unencrypted_raw, content_type_raw, content_encoding,
|
||||
spec, secret_model, tenant_model, repos,
|
||||
transport_key_needed=False):
|
||||
transport_key_needed=False,
|
||||
transport_key_id=None):
|
||||
"""Store a provided secret into secure backend."""
|
||||
|
||||
# Create a secret model is one isn't provided.
|
||||
@@ -35,30 +76,17 @@ def store_secret(unencrypted_raw, content_type_raw, content_encoding,
|
||||
# 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:
|
||||
key_model = None
|
||||
if transport_key_needed:
|
||||
# get_plugin_store() will throw an exception if no suitable
|
||||
# plugin with transport key is found
|
||||
store_plugin = secret_store.SecretStorePluginManager().\
|
||||
get_plugin_store(True)
|
||||
plugin_name = utils.generate_fullname_for(store_plugin)
|
||||
|
||||
key_repo = repos.transport_key_repo
|
||||
key_model = key_repo.get_latest_transport_key(plugin_name)
|
||||
|
||||
if not key_model or not store_plugin.is_transport_key_current(
|
||||
key_model.transport_key):
|
||||
# transport key does not exist or is not current.
|
||||
# need to get a new transport key
|
||||
transport_key = store_plugin.get_transport_key()
|
||||
new_key_model = models.TransportKey(plugin_name, transport_key)
|
||||
key_model = key_repo.create_from(new_key_model)
|
||||
key_model = get_transport_key_model(repos, transport_key_needed)
|
||||
|
||||
_save_secret(secret_model, tenant_model, repos)
|
||||
return secret_model, key_model
|
||||
|
||||
plugin_name, transport_key = get_plugin_name_and_transport_key(
|
||||
repos, transport_key_id)
|
||||
|
||||
# Locate a suitable plugin to store the secret.
|
||||
store_plugin = secret_store.SecretStorePluginManager().get_plugin_store()
|
||||
store_plugin = secret_store.SecretStorePluginManager().\
|
||||
get_plugin_store(plugin_name=plugin_name)
|
||||
|
||||
# Normalize inputs prior to storage.
|
||||
#TODO(john-wood-w) Normalize all secrets to base64, so we don't have to
|
||||
@@ -76,8 +104,11 @@ def store_secret(unencrypted_raw, content_type_raw, content_encoding,
|
||||
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_dto = secret_store.SecretDTO(type=None,
|
||||
secret=unencrypted,
|
||||
key_spec=key_spec,
|
||||
content_type=content_type,
|
||||
transport_key=transport_key)
|
||||
secret_metadata = store_plugin.store_secret(secret_dto, context)
|
||||
|
||||
# Save secret and metadata.
|
||||
|
||||
@@ -232,7 +232,7 @@ class BaseSecretsResource(FunctionalTest):
|
||||
|
||||
self.transport_key = models.TransportKey(
|
||||
'default_plugin_name', 'XXXABCDEF')
|
||||
self.transport_key.id = 'tkey12345'
|
||||
self.transport_key_id = 'tkey12345'
|
||||
self.tkey_url = hrefs.convert_transport_key_to_href(
|
||||
self.keystone_id,
|
||||
self.transport_key.id)
|
||||
@@ -265,7 +265,8 @@ class BaseSecretsResource(FunctionalTest):
|
||||
'application/octet-stream'),
|
||||
self.secret_req.get('payload_content_encoding'),
|
||||
expected, None, self.tenant, mock.ANY,
|
||||
transport_key_needed=False
|
||||
transport_key_needed=False,
|
||||
transport_key_id=None
|
||||
)
|
||||
|
||||
@mock.patch('barbican.plugin.resources.store_secret')
|
||||
@@ -295,7 +296,40 @@ class BaseSecretsResource(FunctionalTest):
|
||||
expected, None,
|
||||
self.tenant if check_tenant_id else mock.ANY,
|
||||
mock.ANY,
|
||||
transport_key_needed=False
|
||||
transport_key_needed=False,
|
||||
transport_key_id=None
|
||||
)
|
||||
|
||||
@mock.patch('barbican.plugin.resources.store_secret')
|
||||
def _test_should_add_new_secret_one_step_with_tkey_id(
|
||||
self, mock_store_secret, check_tenant_id=True):
|
||||
"""Test the one-step secret creation with transport_key_id set
|
||||
|
||||
: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, None
|
||||
self.secret_req['transport_key_id'] = self.transport_key_id
|
||||
|
||||
resp = self.app.post_json(
|
||||
'/%s/secrets/' % self.keystone_id,
|
||||
self.secret_req
|
||||
)
|
||||
self.assertEqual(resp.status_int, 201)
|
||||
|
||||
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,
|
||||
transport_key_needed=False,
|
||||
transport_key_id=self.transport_key_id
|
||||
)
|
||||
|
||||
def _test_should_add_new_secret_if_tenant_does_not_exist(self):
|
||||
@@ -415,6 +449,9 @@ class WhenCreatingPlainTextSecretsUsingSecretsResource(BaseSecretsResource):
|
||||
def test_should_add_new_secret_one_step(self):
|
||||
self._test_should_add_new_secret_one_step()
|
||||
|
||||
def test_should_add_new_secret_one_step_with_tkey_id(self):
|
||||
self._test_should_add_new_secret_one_step_with_tkey_id()
|
||||
|
||||
def test_should_add_new_secret_with_expiration(self):
|
||||
self._test_should_add_new_secret_with_expiration()
|
||||
|
||||
@@ -547,6 +584,9 @@ class WhenCreatingBinarySecretsUsingSecretsResource(BaseSecretsResource):
|
||||
def test_should_add_new_secret_one_step(self):
|
||||
self._test_should_add_new_secret_one_step()
|
||||
|
||||
def test_should_add_new_secret_one_step_with_tkey_id(self):
|
||||
self._test_should_add_new_secret_one_step_with_tkey_id()
|
||||
|
||||
def test_should_add_new_secret_with_expiration(self):
|
||||
self._test_should_add_new_secret_with_expiration()
|
||||
|
||||
@@ -875,6 +915,7 @@ class WhenGettingPuttingOrDeletingSecretUsingSecretResource(FunctionalTest):
|
||||
self.secret_meta_repo = mock.MagicMock()
|
||||
|
||||
self.transport_key_repo = mock.MagicMock()
|
||||
self.transport_key_id = 'tkey12345'
|
||||
|
||||
def test_should_get_secret_as_json(self):
|
||||
resp = self.app.get(
|
||||
@@ -990,9 +1031,29 @@ class WhenGettingPuttingOrDeletingSecretUsingSecretResource(FunctionalTest):
|
||||
self.assertEqual(resp.status_int, 204)
|
||||
|
||||
mock_store_secret\
|
||||
.assert_called_once_with('plain text', 'text/plain',
|
||||
None, self.secret.to_dict_fields(),
|
||||
self.secret, self.tenant, mock.ANY)
|
||||
.assert_called_once_with('plain text', 'text/plain', None,
|
||||
self.secret.to_dict_fields(),
|
||||
self.secret, self.tenant, mock.ANY,
|
||||
transport_key_id=None)
|
||||
|
||||
@mock.patch('barbican.plugin.resources.store_secret')
|
||||
def test_should_put_secret_as_plain_with_tkey_id(self, mock_store_secret):
|
||||
self.secret.encrypted_data = []
|
||||
|
||||
resp = self.app.put(
|
||||
'/%s/secrets/%s/?transport_key_id=%s' %
|
||||
(self.keystone_id, self.secret.id, self.transport_key_id),
|
||||
'plain text',
|
||||
headers={'Accept': 'text/plain', 'Content-Type': 'text/plain'},
|
||||
)
|
||||
|
||||
self.assertEqual(resp.status_int, 204)
|
||||
|
||||
mock_store_secret\
|
||||
.assert_called_once_with('plain text', 'text/plain', None,
|
||||
self.secret.to_dict_fields(),
|
||||
self.secret, self.tenant, mock.ANY,
|
||||
transport_key_id=self.transport_key_id)
|
||||
|
||||
@mock.patch('barbican.plugin.resources.store_secret')
|
||||
def test_should_put_secret_as_binary(self, mock_store_secret):
|
||||
@@ -1011,8 +1072,33 @@ class WhenGettingPuttingOrDeletingSecretUsingSecretResource(FunctionalTest):
|
||||
|
||||
mock_store_secret\
|
||||
.assert_called_once_with('plain text', 'application/octet-stream',
|
||||
None, self.secret.to_dict_fields(),
|
||||
self.secret, self.tenant, mock.ANY)
|
||||
None,
|
||||
self.secret.to_dict_fields(),
|
||||
self.secret, self.tenant, mock.ANY,
|
||||
transport_key_id=None)
|
||||
|
||||
@mock.patch('barbican.plugin.resources.store_secret')
|
||||
def test_should_put_secret_as_binary_with_tkey_id(self, mock_store_secret):
|
||||
self.secret.encrypted_data = []
|
||||
|
||||
resp = self.app.put(
|
||||
'/%s/secrets/%s/?transport_key_id=%s' %
|
||||
(self.keystone_id, self.secret.id, self.transport_key_id),
|
||||
'plain text',
|
||||
headers={
|
||||
'Accept': 'text/plain',
|
||||
'Content-Type': 'application/octet-stream'
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(resp.status_int, 204)
|
||||
|
||||
mock_store_secret\
|
||||
.assert_called_once_with('plain text', 'application/octet-stream',
|
||||
None,
|
||||
self.secret.to_dict_fields(),
|
||||
self.secret, self.tenant, mock.ANY,
|
||||
transport_key_id=self.transport_key_id)
|
||||
|
||||
@mock.patch('barbican.plugin.resources.store_secret')
|
||||
def test_should_put_encoded_secret_as_binary(self, mock_store_secret):
|
||||
@@ -1033,7 +1119,8 @@ class WhenGettingPuttingOrDeletingSecretUsingSecretResource(FunctionalTest):
|
||||
mock_store_secret\
|
||||
.assert_called_once_with(payload, 'application/octet-stream',
|
||||
'base64', self.secret.to_dict_fields(),
|
||||
self.secret, self.tenant, mock.ANY)
|
||||
self.secret, self.tenant, mock.ANY,
|
||||
transport_key_id=None)
|
||||
|
||||
def test_should_raise_to_put_secret_with_unsupported_encoding(self):
|
||||
self.secret.encrypted_data = []
|
||||
|
||||
@@ -137,7 +137,7 @@ class WhenTestingSecretStorePluginManager(testtools.TestCase):
|
||||
self.assertRaises(
|
||||
str.SecretStoreSupportedPluginNotFound,
|
||||
self.manager.get_plugin_store,
|
||||
True,
|
||||
transport_key_needed=True,
|
||||
)
|
||||
|
||||
def test_get_store_with_tkey(self):
|
||||
@@ -146,5 +146,6 @@ class WhenTestingSecretStorePluginManager(testtools.TestCase):
|
||||
plugin2 = TestSecretStoreWithTransportKey(False)
|
||||
plugin2_mock = mock.MagicMock(obj=plugin2)
|
||||
self.manager.extensions = [plugin1_mock, plugin2_mock]
|
||||
self.assertEqual(plugin2,
|
||||
self.manager.get_plugin_store(True))
|
||||
self.assertEqual(
|
||||
plugin2,
|
||||
self.manager.get_plugin_store(transport_key_needed=True))
|
||||
@@ -119,10 +119,12 @@ class WhenTestingDogtagPlugin(testtools.TestCase):
|
||||
key_spec = mock.MagicMock()
|
||||
content_type = mock.MagicMock()
|
||||
context = mock.MagicMock()
|
||||
transport_key = None
|
||||
secret_dto = sstore.SecretDTO(sstore.SecretType.SYMMETRIC,
|
||||
payload,
|
||||
key_spec,
|
||||
content_type)
|
||||
content_type,
|
||||
transport_key)
|
||||
self.plugin.store_secret(secret_dto, context)
|
||||
self.keyclient_mock.archive_key.assert_called_once_with(
|
||||
mock.ANY,
|
||||
@@ -131,6 +133,27 @@ class WhenTestingDogtagPlugin(testtools.TestCase):
|
||||
key_algorithm=None,
|
||||
key_size=None)
|
||||
|
||||
def test_store_secret_with_tkey_id(self):
|
||||
if not imports_ok:
|
||||
self.skipTest("Dogtag imports not available")
|
||||
payload = 'data wrapped in PKIArchiveOptions object'
|
||||
key_spec = mock.MagicMock()
|
||||
content_type = mock.MagicMock()
|
||||
context = mock.MagicMock()
|
||||
transport_key = mock.MagicMock()
|
||||
secret_dto = sstore.SecretDTO(sstore.SecretType.SYMMETRIC,
|
||||
payload,
|
||||
key_spec,
|
||||
content_type,
|
||||
transport_key)
|
||||
self.plugin.store_secret(secret_dto, context)
|
||||
self.keyclient_mock.archive_pki_options.assert_called_once_with(
|
||||
mock.ANY,
|
||||
"passPhrase",
|
||||
payload,
|
||||
key_algorithm=None,
|
||||
key_size=None)
|
||||
|
||||
def test_get_secret(self):
|
||||
if not imports_ok:
|
||||
self.skipTest("Dogtag imports not available")
|
||||
|
||||
Reference in new issue
Block a user