Castellan based secret store
This will provide a Castellan based secret store, which will allow secret stores which have a castellan backend to be used behind barbican. The initial example of this is the Vault backend. Unit tests have been added. In local tests, most of the functional tests do in fact pass with a local Vault backend, though this will need to be demonstrated with a later review which establishes a Vault based gate. Change-Id: Ib30fb79304014592bfc37938839d60a4c10c244d
This commit is contained in:
parent
163f5525c9
commit
89cb777941
165
barbican/plugin/castellan_secret_store.py
Normal file
165
barbican/plugin/castellan_secret_store.py
Normal file
@ -0,0 +1,165 @@
|
||||
# Copyright (c) 2018 Red Hat Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import abc
|
||||
import six
|
||||
|
||||
from castellan.common.objects import opaque_data
|
||||
from castellan import key_manager
|
||||
from oslo_context import context
|
||||
from oslo_log import log
|
||||
|
||||
from barbican.plugin.interface import secret_store as ss
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class CastellanSecretStore(ss.SecretStoreBase):
|
||||
|
||||
KEY_ID = "key_id"
|
||||
ALG = "alg"
|
||||
BIT_LENGTH = "bit_length"
|
||||
|
||||
def _set_params(self, conf):
|
||||
self.key_manager = key_manager.API(conf)
|
||||
self.context = context.get_current()
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_conf(self, conf):
|
||||
"""Get plugin configuration
|
||||
|
||||
This method is supposed to be implemented by the relevant
|
||||
subclass. This method reads in the config for the plugin
|
||||
in barbican.conf -- which should look like the way other
|
||||
barbican plugins are configured, and convert them to the
|
||||
proper oslo.config object to be passed to the keymanager
|
||||
API. (keymanager.API(conf)
|
||||
|
||||
@returns oslo.config object
|
||||
"""
|
||||
raise NotImplementedError # pragma: no cover
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_plugin_name(self):
|
||||
"""Get plugin name
|
||||
|
||||
This method is implemented by the subclass.
|
||||
Note that this name must be unique across the deployment.
|
||||
"""
|
||||
raise NotImplementedError # pragma: no cover
|
||||
|
||||
def get_secret(self, secret_type, secret_metadata):
|
||||
secret_ref = secret_metadata[CastellanSecretStore.KEY_ID]
|
||||
try:
|
||||
secret = self.key_manager.get(
|
||||
self.context,
|
||||
secret_ref)
|
||||
return secret.get_encoded()
|
||||
except Exception as e:
|
||||
LOG.exception("Error retrieving secret {}: {}".format(
|
||||
secret_ref, six.text_type(e)))
|
||||
raise ss.SecretGeneralException(e)
|
||||
|
||||
def store_secret(self, secret_dto):
|
||||
if not self.store_secret_supports(secret_dto.key_spec):
|
||||
raise ss.SecretAlgorithmNotSupportedException(
|
||||
secret_dto.key_spec.alg)
|
||||
|
||||
try:
|
||||
secret_ref = self.key_manager.store(
|
||||
self.context,
|
||||
opaque_data.OpaqueData(secret_dto.secret)
|
||||
)
|
||||
return {CastellanSecretStore.KEY_ID: secret_ref}
|
||||
except Exception as e:
|
||||
LOG.exception("Error storing secret: {}".format(
|
||||
six.text_type(e)))
|
||||
raise ss.SecretGeneralException(e)
|
||||
|
||||
def delete_secret(self, secret_metadata):
|
||||
secret_ref = secret_metadata[CastellanSecretStore.KEY_ID]
|
||||
try:
|
||||
self.key_manager.delete(
|
||||
self.context,
|
||||
secret_ref)
|
||||
except KeyError:
|
||||
LOG.warning("Attempting to delete a non-existent secret {}".format(
|
||||
secret_ref))
|
||||
except Exception as e:
|
||||
LOG.exception("Error deleting secret: {}".format(
|
||||
six.text_type(e)))
|
||||
raise ss.SecretGeneralException(e)
|
||||
|
||||
def generate_symmetric_key(self, key_spec):
|
||||
if not self.generate_supports(key_spec):
|
||||
raise ss.SecretAlgorithmNotSupportedException(
|
||||
key_spec.alg)
|
||||
try:
|
||||
secret_ref = self.key_manager.create_key(
|
||||
self.context,
|
||||
key_spec.alg,
|
||||
key_spec.bit_length
|
||||
)
|
||||
return {CastellanSecretStore.KEY_ID: secret_ref}
|
||||
except Exception as e:
|
||||
LOG.exception("Error generating symmetric key: {}".format(
|
||||
six.text_type(e)))
|
||||
raise ss.SecretGeneralException(e)
|
||||
|
||||
def generate_asymmetric_key(self, key_spec):
|
||||
if not self.generate_supports(key_spec):
|
||||
raise ss.SecretAlgorithmNotSupportedException(
|
||||
key_spec.alg)
|
||||
|
||||
if key_spec.passphrase:
|
||||
raise ss.GeneratePassphraseNotSupportedException()
|
||||
|
||||
try:
|
||||
private_ref, public_ref = self.key_manager.create_key_pair(
|
||||
self.context,
|
||||
key_spec.alg,
|
||||
key_spec.bit_length
|
||||
)
|
||||
|
||||
private_key_metadata = {
|
||||
CastellanSecretStore.ALG: key_spec.alg,
|
||||
CastellanSecretStore.BIT_LENGTH: key_spec.bit_length,
|
||||
CastellanSecretStore.KEY_ID: private_ref
|
||||
}
|
||||
|
||||
public_key_metadata = {
|
||||
CastellanSecretStore.ALG: key_spec.alg,
|
||||
CastellanSecretStore.BIT_LENGTH: key_spec.bit_length,
|
||||
CastellanSecretStore.KEY_ID: public_ref
|
||||
}
|
||||
|
||||
return ss.AsymmetricKeyMetadataDTO(
|
||||
private_key_metadata,
|
||||
public_key_metadata,
|
||||
None
|
||||
)
|
||||
except Exception as e:
|
||||
LOG.exception("Error generating asymmetric key: {}".format(
|
||||
six.text_type(e)))
|
||||
raise ss.SecretGeneralException(e)
|
||||
|
||||
@abc.abstractmethod
|
||||
def store_secret_supports(self, key_spec):
|
||||
raise NotImplementedError # pragma: no cover
|
||||
|
||||
@abc.abstractmethod
|
||||
def generate_supports(self, key_spec):
|
||||
raise NotImplementedError # pragma: no cover
|
@ -227,6 +227,20 @@ class SecretAlgorithmNotSupportedException(exception.BarbicanHTTPException):
|
||||
self.algorithm = algorithm
|
||||
|
||||
|
||||
class GeneratePassphraseNotSupportedException(exception.BarbicanHTTPException):
|
||||
"""Raised when generating keys encrypted by passphrase is not supported."""
|
||||
|
||||
client_message = (
|
||||
u._("Generating keys encrypted with passphrases is not supported")
|
||||
)
|
||||
status_code = 400
|
||||
|
||||
def __init__(self):
|
||||
super(GeneratePassphraseNotSupportedException, self).__init__(
|
||||
self.client_message
|
||||
)
|
||||
|
||||
|
||||
class SecretStorePluginsNotConfigured(exception.BarbicanException):
|
||||
"""Raised when there are no secret store plugins configured."""
|
||||
def __init__(self):
|
||||
|
82
barbican/plugin/vault_secret_store.py
Normal file
82
barbican/plugin/vault_secret_store.py
Normal file
@ -0,0 +1,82 @@
|
||||
# Copyright (c) 2018 Red Hat Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from barbican.common import config
|
||||
import barbican.plugin.castellan_secret_store as css
|
||||
from castellan.i18n import _
|
||||
from castellan import options
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
DEFAULT_VAULT_URL = "http://127.0.0.1:8200"
|
||||
|
||||
vault_opt_group = cfg.OptGroup(name='vault_plugin', title='Vault Plugin')
|
||||
vault_opts = [
|
||||
cfg.StrOpt('root_token_id',
|
||||
help='root token for vault'),
|
||||
cfg.StrOpt('vault_url',
|
||||
default=DEFAULT_VAULT_URL,
|
||||
help='Use this endpoint to connect to Vault, for example: '
|
||||
'"%s"' % DEFAULT_VAULT_URL),
|
||||
cfg.StrOpt('ssl_ca_crt_file',
|
||||
help='Absolute path to ca cert file'),
|
||||
cfg.BoolOpt('use_ssl',
|
||||
default=False,
|
||||
help=_('SSL Enabled/Disabled')),
|
||||
]
|
||||
|
||||
CONF = config.new_config()
|
||||
CONF.register_group(vault_opt_group)
|
||||
CONF.register_opts(vault_opts, group=vault_opt_group)
|
||||
config.parse_args(CONF)
|
||||
|
||||
|
||||
def list_opts():
|
||||
yield vault_opt_group, vault_opts # pragma: no cover
|
||||
|
||||
|
||||
class VaultSecretStore(css.CastellanSecretStore):
|
||||
|
||||
def __init__(self, conf=CONF):
|
||||
"""Constructor - create the vault secret store."""
|
||||
vault_conf = self.get_conf(conf)
|
||||
self._set_params(vault_conf)
|
||||
|
||||
def get_plugin_name(self):
|
||||
return "VaultSecretStore"
|
||||
|
||||
def get_conf(self, conf=CONF):
|
||||
"""Convert secret store conf into oslo conf
|
||||
|
||||
Returns an oslo.config() object to pass to keymanager.API(conf)
|
||||
"""
|
||||
vault_conf = cfg.ConfigOpts()
|
||||
options.set_defaults(
|
||||
vault_conf,
|
||||
backend='vault',
|
||||
vault_root_token_id=conf.vault_plugin.root_token_id,
|
||||
vault_url=conf.vault_plugin.vault_url,
|
||||
vault_ssl_ca_crt_file=conf.vault_plugin.ssl_ca_crt_file,
|
||||
vault_use_ssl=conf.vault_plugin.use_ssl
|
||||
)
|
||||
return vault_conf
|
||||
|
||||
def store_secret_supports(self, key_spec):
|
||||
return True
|
||||
|
||||
def generate_supports(self, key_spec):
|
||||
return True
|
218
barbican/tests/plugin/test_castellan_secret_store.py
Normal file
218
barbican/tests/plugin/test_castellan_secret_store.py
Normal file
@ -0,0 +1,218 @@
|
||||
# Copyright (c) 2018 Red Hat, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from castellan.common import exception
|
||||
from castellan.common.objects import opaque_data
|
||||
import mock
|
||||
|
||||
import barbican.plugin.castellan_secret_store as css
|
||||
import barbican.plugin.interface.secret_store as ss
|
||||
import barbican.plugin.vault_secret_store as vss
|
||||
from barbican.tests import utils
|
||||
|
||||
key_ref1 = 'aff825be-6ede-4b1d-aeb0-aaec8e62aec6'
|
||||
key_ref2 = '9c94c9c7-16ea-43e8-8ebe-0de282c0e6d5'
|
||||
secret_passphrase = 'secret passphrase'
|
||||
|
||||
|
||||
class WhenTestingVaultSecretStore(utils.BaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(WhenTestingVaultSecretStore, self).setUp()
|
||||
self.key_manager_mock = mock.MagicMock(name="key manager mock")
|
||||
self.key_manager_mock.create_key_pair.return_value = (
|
||||
key_ref1, key_ref2
|
||||
)
|
||||
self.key_manager_mock.create_key.return_value = key_ref1
|
||||
self.key_manager_mock.store.return_value = key_ref1
|
||||
|
||||
secret_object = opaque_data.OpaqueData(secret_passphrase)
|
||||
self.key_manager_mock.get.return_value = secret_object
|
||||
|
||||
self.cfg_mock = mock.MagicMock(name='config mock')
|
||||
self.cfg_mock.vault_plugin = mock.MagicMock(
|
||||
use_ssl=False,
|
||||
root_token_id='12345'
|
||||
)
|
||||
|
||||
self.plugin = vss.VaultSecretStore(self.cfg_mock)
|
||||
self.plugin.key_manager = self.key_manager_mock
|
||||
self.plugin_name = "VaultSecretStore"
|
||||
|
||||
def test_generate_symmetric_key(self):
|
||||
key_spec = ss.KeySpec(ss.KeyAlgorithm.AES, 128)
|
||||
response = self.plugin.generate_symmetric_key(key_spec)
|
||||
|
||||
self.plugin.key_manager.create_key.assert_called_once_with(
|
||||
mock.ANY,
|
||||
ss.KeyAlgorithm.AES,
|
||||
128
|
||||
)
|
||||
|
||||
expected_response = {css.CastellanSecretStore.KEY_ID: key_ref1}
|
||||
self.assertEqual(response, expected_response)
|
||||
|
||||
def test_generate_symmetric_key_raises_exception(self):
|
||||
key_spec = ss.KeySpec(ss.KeyAlgorithm.AES, 128)
|
||||
self.plugin.key_manager.create_key.side_effect = exception.Forbidden()
|
||||
self.assertRaises(
|
||||
ss.SecretGeneralException,
|
||||
self.plugin.generate_symmetric_key,
|
||||
key_spec
|
||||
)
|
||||
|
||||
def test_generate_asymmetric_key(self):
|
||||
key_spec = ss.KeySpec(ss.KeyAlgorithm.RSA, 2048)
|
||||
response = self.plugin.generate_asymmetric_key(key_spec)
|
||||
|
||||
self.plugin.key_manager.create_key_pair.assert_called_once_with(
|
||||
mock.ANY,
|
||||
ss.KeyAlgorithm.RSA,
|
||||
2048)
|
||||
|
||||
self.assertIsInstance(response, ss.AsymmetricKeyMetadataDTO)
|
||||
self.assertEqual(
|
||||
response.public_key_meta[css.CastellanSecretStore.KEY_ID],
|
||||
key_ref2
|
||||
)
|
||||
self.assertEqual(
|
||||
response.private_key_meta[css.CastellanSecretStore.KEY_ID],
|
||||
key_ref1
|
||||
)
|
||||
|
||||
def test_generate_asymmetric_throws_exception(self):
|
||||
key_spec = ss.KeySpec(ss.KeyAlgorithm.RSA, 2048)
|
||||
self.plugin.key_manager.create_key_pair.side_effect = (
|
||||
exception.Forbidden()
|
||||
)
|
||||
self.assertRaises(
|
||||
ss.SecretGeneralException,
|
||||
self.plugin.generate_asymmetric_key,
|
||||
key_spec
|
||||
)
|
||||
|
||||
def test_generate_asymmetric_throws_passphrase_exception(self):
|
||||
key_spec = ss.KeySpec(
|
||||
alg=ss.KeyAlgorithm.RSA,
|
||||
bit_length=2048,
|
||||
passphrase="some passphrase"
|
||||
)
|
||||
|
||||
self.assertRaises(
|
||||
ss.GeneratePassphraseNotSupportedException,
|
||||
self.plugin.generate_asymmetric_key,
|
||||
key_spec
|
||||
)
|
||||
|
||||
def test_store_secret(self):
|
||||
payload = 'encrypt me!!'
|
||||
key_spec = mock.MagicMock()
|
||||
content_type = mock.MagicMock()
|
||||
transport_key = None
|
||||
secret_dto = ss.SecretDTO(ss.SecretType.SYMMETRIC,
|
||||
payload,
|
||||
key_spec,
|
||||
content_type,
|
||||
transport_key)
|
||||
response = self.plugin.store_secret(secret_dto)
|
||||
|
||||
data = opaque_data.OpaqueData(secret_dto.secret)
|
||||
self.plugin.key_manager.store.assert_called_once_with(
|
||||
mock.ANY,
|
||||
data
|
||||
)
|
||||
expected_response = {css.CastellanSecretStore.KEY_ID: key_ref1}
|
||||
self.assertEqual(response, expected_response)
|
||||
|
||||
def test_store_secret_raises_exception(self):
|
||||
payload = 'encrypt me!!'
|
||||
key_spec = mock.MagicMock()
|
||||
content_type = mock.MagicMock()
|
||||
transport_key = None
|
||||
secret_dto = ss.SecretDTO(ss.SecretType.SYMMETRIC,
|
||||
payload,
|
||||
key_spec,
|
||||
content_type,
|
||||
transport_key)
|
||||
|
||||
self.plugin.key_manager.store.side_effect = exception.Forbidden()
|
||||
self.assertRaises(
|
||||
ss.SecretGeneralException,
|
||||
self.plugin.store_secret,
|
||||
secret_dto
|
||||
)
|
||||
|
||||
def test_get_secret(self):
|
||||
secret_metadata = {css.CastellanSecretStore.KEY_ID: key_ref1}
|
||||
response = self.plugin.get_secret(
|
||||
ss.SecretType.SYMMETRIC,
|
||||
secret_metadata
|
||||
)
|
||||
|
||||
self.plugin.key_manager.get.assert_called_once_with(
|
||||
mock.ANY,
|
||||
key_ref1
|
||||
)
|
||||
|
||||
self.assertEqual(response, secret_passphrase)
|
||||
|
||||
def test_get_secret_throws_exception(self):
|
||||
secret_metadata = {css.CastellanSecretStore.KEY_ID: key_ref1}
|
||||
self.plugin.key_manager.get.side_effect = exception.Forbidden()
|
||||
self.assertRaises(
|
||||
ss.SecretGeneralException,
|
||||
self.plugin.get_secret,
|
||||
ss.SecretType.SYMMETRIC,
|
||||
secret_metadata
|
||||
)
|
||||
|
||||
def test_delete_secret(self):
|
||||
secret_metadata = {css.CastellanSecretStore.KEY_ID: key_ref1}
|
||||
self.plugin.delete_secret(secret_metadata)
|
||||
self.plugin.key_manager.delete.assert_called_once_with(
|
||||
mock.ANY,
|
||||
key_ref1
|
||||
)
|
||||
|
||||
def test_delete_secret_throws_exception(self):
|
||||
secret_metadata = {css.CastellanSecretStore.KEY_ID: key_ref1}
|
||||
self.plugin.key_manager.delete.side_effect = exception.Forbidden()
|
||||
self.assertRaises(
|
||||
ss.SecretGeneralException,
|
||||
self.plugin.delete_secret,
|
||||
secret_metadata
|
||||
)
|
||||
|
||||
def test_delete_secret_throws_key_error(self):
|
||||
secret_metadata = {css.CastellanSecretStore.KEY_ID: key_ref1}
|
||||
self.plugin.key_manager.delete.side_effect = KeyError()
|
||||
self.plugin.delete_secret(secret_metadata)
|
||||
self.plugin.key_manager.delete.assert_called_once_with(
|
||||
mock.ANY,
|
||||
key_ref1
|
||||
)
|
||||
|
||||
def test_store_secret_supports(self):
|
||||
self.assertTrue(
|
||||
self.plugin.generate_supports(mock.ANY)
|
||||
)
|
||||
|
||||
def test_generate_supports(self):
|
||||
self.assertTrue(
|
||||
self.plugin.generate_supports(mock.ANY)
|
||||
)
|
||||
|
||||
def test_get_plugin_name(self):
|
||||
self.assertEqual(self.plugin_name, self.plugin.get_plugin_name())
|
@ -8,6 +8,7 @@ bandit==1.1.0
|
||||
bcrypt==3.1.4
|
||||
beautifulsoup4==4.6.0
|
||||
cachetools==2.0.1
|
||||
castellan==0.17
|
||||
certifi==2018.1.18
|
||||
cffi==1.7.0
|
||||
chardet==3.0.4
|
||||
|
@ -30,3 +30,4 @@ six>=1.10.0 # MIT
|
||||
SQLAlchemy!=1.1.5,!=1.1.6,!=1.1.7,!=1.1.8,>=1.0.10 # MIT
|
||||
stevedore>=1.20.0 # Apache-2.0
|
||||
WebOb>=1.7.1 # MIT
|
||||
castellan >= 0.17 # Apache-2.0
|
||||
|
@ -53,6 +53,7 @@ barbican.secretstore.plugin =
|
||||
store_crypto = barbican.plugin.store_crypto:StoreCryptoAdapterPlugin
|
||||
dogtag_crypto = barbican.plugin.dogtag:DogtagKRAPlugin
|
||||
kmip_plugin = barbican.plugin.kmip_secret_store:KMIPSecretStore
|
||||
vault_plugin = barbican.plugin.vault_secret_store:VaultSecretStore
|
||||
barbican.crypto.plugin =
|
||||
p11_crypto = barbican.plugin.crypto.p11_crypto:P11CryptoPlugin
|
||||
simple_crypto = barbican.plugin.crypto.simple_crypto:SimpleCryptoPlugin
|
||||
@ -73,6 +74,7 @@ oslo.config.opts =
|
||||
barbican.plugin.dogtag = barbican.plugin.dogtag_config_opts:list_opts
|
||||
barbican.plugin.crypto.p11 = barbican.plugin.crypto.p11_crypto:list_opts
|
||||
barbican.plugin.secret_store.kmip = barbican.plugin.kmip_secret_store:list_opts
|
||||
barbican.plugin.secret_store.vault = barbican.plugin.vault_secret_store:list_opts
|
||||
barbican.certificate.plugin = barbican.plugin.interface.certificate_manager:list_opts
|
||||
barbican.certificate.plugin.snakeoil = barbican.plugin.snakeoil_ca:list_opts
|
||||
oslo.config.opts.defaults =
|
||||
|
@ -27,3 +27,4 @@ sphinx!=1.6.6,!=1.6.7,>=1.6.2 # BSD
|
||||
os-api-ref>=1.4.0 # Apache-2.0
|
||||
reno>=2.5.0 # Apache-2.0
|
||||
openstackdocstheme>=1.18.1 # Apache-2.0
|
||||
castellan >= 0.17 # Apache-2.0
|
||||
|
Loading…
Reference in New Issue
Block a user