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:
Ade Lee 2017-10-17 11:15:51 -04:00
parent 163f5525c9
commit 89cb777941
8 changed files with 484 additions and 0 deletions

View 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

View File

@ -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):

View 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

View 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())

View File

@ -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

View File

@ -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

View File

@ -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 =

View File

@ -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