Files
barbican/barbican/plugin/kmip_secret_store.py
Kevin Bishop 5687cd9b93 Centralize Barbican API Config
This centralizes all config options into config.py,
eliminating a race condition where not all config options
from modules may have been registered by the time the
config file is parsed. Plugins are now provided a
separate config object, to which they can add their
own individual options before parsing the config file themselves.

Change-Id: I737bfe092ff71a1275f27de5b9328e9d23032a24
Closes-Bug: #1459448
2015-06-02 15:08:21 -05:00

627 lines
25 KiB
Python

# Copyright (c) 2014 Johns Hopkins University Applied Physics Laboratory
#
# 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.
"""
An implementation of the SecretStore that uses the KMIP backend.
"""
from kmip.services import kmip_client
import base64
import os
import stat
from kmip.core import enums
from kmip.core.factories import attributes
from kmip.core.factories import credentials
from kmip.core.factories import secrets
from kmip.core import misc
from kmip.core import objects as kmip_objects
from oslo_config import cfg
from oslo_log import log
from barbican.common import config
from barbican import i18n as u # noqa
from barbican.plugin.interface import secret_store as ss
from barbican.plugin.util import translations
LOG = log.getLogger(__name__)
CONF = config.new_config()
kmip_opt_group = cfg.OptGroup(name='kmip_plugin', title='KMIP Plugin')
kmip_opts = [
cfg.StrOpt('username',
default=None,
help=u._('Username for authenticating with KMIP server')
),
cfg.StrOpt('password',
default=None,
help=u._('Password for authenticating with KMIP server')
),
cfg.StrOpt('host',
default='localhost',
help=u._('Address of the KMIP server')
),
cfg.StrOpt('port',
default='5696',
help=u._('Port for the KMIP server'),
),
cfg.StrOpt('ssl_version',
default='PROTOCOL_TLSv1',
help=u._('SSL version, maps to the module ssl\'s constants'),
),
cfg.StrOpt('ca_certs',
default=None,
help=u._('File path to concatenated "certification authority" '
'certificates'),
),
cfg.StrOpt('certfile',
default=None,
help=u._('File path to local client certificate'),
),
cfg.StrOpt('keyfile',
default=None,
help=u._('File path to local client certificate keyfile'),
)
]
CONF.register_group(kmip_opt_group)
CONF.register_opts(kmip_opts, group=kmip_opt_group)
config.parse_args(CONF)
attribute_debug_msg = "Created attribute type %s with value %s"
class KMIPSecretStoreError(Exception):
def __init__(self, what):
super(KMIPSecretStoreError, self).__init__(what)
class KMIPSecretStore(ss.SecretStoreBase):
KEY_UUID = "key_uuid"
VALID_BIT_LENGTHS = "valid_bit_lengths"
KMIP_ALGORITHM_ENUM = "kmip_algorithm_enum"
def __init__(self, conf=CONF):
"""Initializes KMIPSecretStore
Creates a dictionary of mappings between SecretStore enum values
and pyKMIP enum values. Initializes the KMIP client with credentials
needed to connect to the KMIP server.
"""
super(KMIPSecretStore, self).__init__()
self.valid_alg_dict = {
ss.KeyAlgorithm.AES: {
KMIPSecretStore.VALID_BIT_LENGTHS: [128, 192, 256],
KMIPSecretStore.KMIP_ALGORITHM_ENUM:
enums.CryptographicAlgorithm.AES},
ss.KeyAlgorithm.DES: {
KMIPSecretStore.VALID_BIT_LENGTHS: [56],
KMIPSecretStore.KMIP_ALGORITHM_ENUM:
enums.CryptographicAlgorithm.DES},
ss.KeyAlgorithm.DESEDE: {
KMIPSecretStore.VALID_BIT_LENGTHS:
[56, 64, 112, 128, 168, 192],
KMIPSecretStore.KMIP_ALGORITHM_ENUM:
enums.CryptographicAlgorithm.TRIPLE_DES},
ss.KeyAlgorithm.DSA: {
KMIPSecretStore.VALID_BIT_LENGTHS:
[1024, 2048, 3072],
KMIPSecretStore.KMIP_ALGORITHM_ENUM:
enums.CryptographicAlgorithm.DSA},
ss.KeyAlgorithm.RSA: {
KMIPSecretStore.VALID_BIT_LENGTHS:
[1024, 2048, 3072, 4096],
KMIPSecretStore.KMIP_ALGORITHM_ENUM:
enums.CryptographicAlgorithm.RSA},
}
if conf.kmip_plugin.keyfile is not None:
self._validate_keyfile_permissions(conf.kmip_plugin.keyfile)
if (conf.kmip_plugin.username is None) and (
conf.kmip_plugin.password is None):
self.credential = None
else:
credential_type = credentials.CredentialType.USERNAME_AND_PASSWORD
credential_value = {'Username': conf.kmip_plugin.username,
'Password': conf.kmip_plugin.password}
self.credential = (
credentials.CredentialFactory().create_credential(
credential_type,
credential_value))
self.client = kmip_client.KMIPProxy(
host=conf.kmip_plugin.host,
port=int(conf.kmip_plugin.port),
ssl_version=conf.kmip_plugin.ssl_version,
ca_certs=conf.kmip_plugin.ca_certs,
certfile=conf.kmip_plugin.certfile,
keyfile=conf.kmip_plugin.keyfile,
username=conf.kmip_plugin.username,
password=conf.kmip_plugin.password)
def generate_symmetric_key(self, key_spec):
"""Generate a symmetric key.
Creates KMIP attribute objects based on the given KeySpec to send to
the server.
:param key_spec: KeySpec with symmetric algorithm and bit_length
:returns: dictionary holding key_id returned by server
:raises: SecretGeneralException, SecretAlgorithmNotSupportedException
"""
LOG.debug("Starting symmetric key generation with KMIP plugin")
if not self.generate_supports(key_spec):
raise ss.SecretAlgorithmNotSupportedException(
key_spec.alg)
if key_spec.alg.lower() not in ss.KeyAlgorithm.SYMMETRIC_ALGORITHMS:
raise KMIPSecretStoreError(
u._("An unsupported algorithm {algorithm} was passed to the "
"'generate_symmetric_key' method").format(
algorithm=key_spec.alg))
object_type = enums.ObjectType.SYMMETRIC_KEY
algorithm = self._create_cryptographic_algorithm_attribute(
key_spec.alg)
usage_mask = self._create_usage_mask_attribute()
length = self._create_cryptographic_length_attribute(
key_spec.bit_length)
attribute_list = [algorithm, usage_mask, length]
template_attribute = kmip_objects.TemplateAttribute(
attributes=attribute_list)
try:
self.client.open()
LOG.debug("Opened connection to KMIP client for secret " +
"generation")
result = self.client.create(object_type=object_type,
template_attribute=template_attribute,
credential=self.credential)
except Exception as e:
LOG.exception("Error opening or writing to client")
raise ss.SecretGeneralException(str(e))
else:
if result.result_status.enum == enums.ResultStatus.SUCCESS:
LOG.debug("SUCCESS: Symmetric key generated with "
"uuid: %s", result.uuid.value)
return {KMIPSecretStore.KEY_UUID: result.uuid.value}
else:
self._raise_secret_general_exception(result)
finally:
self.client.close()
LOG.debug("Closed connection to KMIP client for secret " +
"generation")
def generate_asymmetric_key(self, key_spec):
"""Generate an asymmetric key pair.
Creates KMIP attribute objects based on the given KeySpec to send to
the server. The KMIP Secret Store currently does not support
protecting the private key with a passphrase.
:param key_spec: KeySpec with asymmetric algorithm and bit_length
:returns: AsymmetricKeyMetadataDTO with the key UUIDs
:raises: SecretGeneralException, SecretAlgorithmNotSupportedException
"""
LOG.debug("Starting asymmetric key generation with KMIP plugin")
if not self.generate_supports(key_spec):
raise ss.SecretAlgorithmNotSupportedException(
key_spec.alg)
if key_spec.alg.lower() not in ss.KeyAlgorithm.ASYMMETRIC_ALGORITHMS:
raise KMIPSecretStoreError(
u._("An unsupported algorithm {algorithm} was passed to "
"the 'generate_asymmetric_key' method").format(
algorithm=key_spec.alg))
if key_spec.passphrase:
raise KMIPSecretStoreError(
u._('KMIP plugin does not currently support protecting the '
'private key with a passphrase'))
algorithm = self._create_cryptographic_algorithm_attribute(
key_spec.alg)
usage_mask = self._create_usage_mask_attribute()
length = self._create_cryptographic_length_attribute(
key_spec.bit_length)
attributes = [algorithm, usage_mask, length]
common = kmip_objects.CommonTemplateAttribute(
attributes=attributes)
try:
self.client.open()
LOG.debug("Opened connection to KMIP client for asymmetric " +
"secret generation")
result = self.client.create_key_pair(
common_template_attribute=common,
credential=self.credential)
except Exception as e:
LOG.exception("Error opening or writing to client")
raise ss.SecretGeneralException(str(e))
else:
if result.result_status.enum == enums.ResultStatus.SUCCESS:
LOG.debug("SUCCESS: Asymmetric key pair generated with "
"public key uuid: %s and private key uuid: %s",
result.public_key_uuid.value,
result.private_key_uuid.value)
private_key_metadata = {
KMIPSecretStore.KEY_UUID:
result.private_key_uuid.value}
public_key_metadata = {
KMIPSecretStore.KEY_UUID:
result.public_key_uuid.value}
passphrase_metadata = None
return ss.AsymmetricKeyMetadataDTO(private_key_metadata,
public_key_metadata,
passphrase_metadata)
else:
self._raise_secret_general_exception(result)
finally:
self.client.close()
LOG.debug("Closed connection to KMIP client for asymmetric "
"secret generation")
def store_secret(self, secret_dto):
"""Stores a secret
To store a secret in KMIP, the attributes must be known.
:param secret_dto: SecretDTO of the secret to be stored
:returns: Dictionary holding the key_uuid assigned by KMIP
:raises: SecretGeneralException, SecretAlgorithmNotSupportedException
"""
LOG.debug("Starting secret storage with KMIP plugin")
if not self.store_secret_supports(secret_dto.key_spec):
raise ss.SecretAlgorithmNotSupportedException(
secret_dto.key_spec.alg)
secret_type = secret_dto.type
object_type, key_format_type = (
self._map_type_ss_to_kmip(secret_type))
if object_type is None:
raise KMIPSecretStoreError(
u._('Secret object type {object_type} is '
'not supported').format(object_type=object_type))
algorithm_value = self._map_algorithm_ss_to_kmip(
secret_dto.key_spec.alg)
usage_mask = self._create_usage_mask_attribute()
attribute_list = [usage_mask]
template_attribute = kmip_objects.TemplateAttribute(
attributes=attribute_list)
normalized_secret = self._normalize_secret(secret_dto.secret,
secret_type)
secret_features = {
'key_format_type': key_format_type,
'key_value': normalized_secret,
'cryptographic_algorithm': algorithm_value,
'cryptographic_length': secret_dto.key_spec.bit_length
}
secret = secrets.SecretFactory().create(object_type, secret_features)
LOG.debug("Created secret object to be stored: %s, %s, %s",
secret_features.get('key_format_type'),
secret_features.get('cryptographic_algorithm'),
secret_features.get('cryptographic_length'))
try:
self.client.open()
LOG.debug("Opened connection to KMIP client for secret storage")
result = self.client.register(
object_type=object_type,
template_attribute=template_attribute,
secret=secret,
credential=self.credential)
except Exception as e:
LOG.exception(u._LE("Error opening or writing to client"))
raise ss.SecretGeneralException(str(e))
else:
if result.result_status.enum == enums.ResultStatus.SUCCESS:
LOG.debug("SUCCESS: Key stored with uuid: %s",
result.uuid.value)
return {KMIPSecretStore.KEY_UUID: result.uuid.value}
else:
self._raise_secret_general_exception(result)
finally:
self.client.close()
LOG.debug("Closed connection to KMIP client for secret storage")
def get_secret(self, secret_type, secret_metadata):
"""Gets a secret
:param secret_type: secret type
:param secret_metadata: Dictionary of key metadata, requires:
{'key_uuid': <uuid of key>}
:returns: SecretDTO of the retrieved Secret
:raises: SecretGeneralException
"""
LOG.debug("Starting secret retrieval with KMIP plugin")
uuid = str(secret_metadata[KMIPSecretStore.KEY_UUID])
object_type, key_format_enum = self._map_type_ss_to_kmip(secret_type)
if key_format_enum is not None:
key_format_type = misc.KeyFormatType(key_format_enum)
else:
key_format_type = None
try:
self.client.open()
LOG.debug("Opened connection to KMIP client for secret " +
"retrieval")
result = self.client.get(uuid=uuid,
key_format_type=key_format_type,
credential=self.credential)
except Exception as e:
LOG.exception(u._LE("Error opening or writing to client"))
raise ss.SecretGeneralException(str(e))
else:
if result.result_status.enum == enums.ResultStatus.SUCCESS:
secret_block = result.secret.key_block
key_value_type = type(secret_block.key_value.key_material)
if (key_value_type == kmip_objects.KeyMaterialStruct or
key_value_type == kmip_objects.KeyMaterial):
secret_value = self._denormalize_secret(
secret_block.key_value.key_material.value,
secret_type)
else:
msg = u._(
"Unknown key value type received from KMIP "
"server, expected {key_value_struct} or "
"{key_value_string}, received: {key_value_type}"
).format(
key_value_struct=kmip_objects.KeyValue,
key_value_string=kmip_objects.KeyMaterial,
key_value_type=key_value_type
)
LOG.exception(msg)
raise ss.SecretGeneralException(msg)
secret_alg = self._map_algorithm_kmip_to_ss(
secret_block.cryptographic_algorithm.value)
secret_bit_length = secret_block.cryptographic_length.value
ret_secret_dto = ss.SecretDTO(
secret_type,
secret_value,
ss.KeySpec(secret_alg, secret_bit_length),
content_type=None,
transport_key=None)
LOG.debug("SUCCESS: Key retrieved with uuid: %s",
uuid)
return ret_secret_dto
else:
self._raise_secret_general_exception(result)
finally:
self.client.close()
LOG.debug("Closed connection to KMIP client for secret " +
"retrieval")
def generate_supports(self, key_spec):
"""Key generation supported?
Specifies whether the plugin supports key generation with the
given key_spec. Currently, asymmetric key pair generation does not
support encrypting the private key with a passphrase.
Checks both the algorithm and the bit length. Only symmetric
algorithms are currently supported.
:param key_spec: KeySpec for secret to be generates
:returns: boolean indicating if secret can be generated
"""
alg_dict_entry = self.valid_alg_dict.get(key_spec.alg.lower())
if (alg_dict_entry and key_spec.bit_length in
alg_dict_entry.get(KMIPSecretStore.VALID_BIT_LENGTHS)):
return True
return False
def delete_secret(self, secret_metadata):
"""Deletes the secret whose metadata is included in the dictionary.
Returns nothing if successful, raises an exception if an error occurs
:param secret_metadata: Dictionary of key metadata, requires:
{'key_uuid': <uuid of key>}
:raises: SecretGeneralException
"""
LOG.debug("Starting secret deletion with KMIP plugin")
uuid = str(secret_metadata[KMIPSecretStore.KEY_UUID])
try:
self.client.open()
LOG.debug("Opened connection to KMIP client for secret deletion")
result = self.client.destroy(uuid=uuid,
credential=self.credential)
except Exception as e:
LOG.exception(u._LE("Error opening or writing to client"))
raise ss.SecretGeneralException(str(e))
else:
if result.result_status.enum == enums.ResultStatus.SUCCESS:
LOG.debug("SUCCESS: Key with uuid %s deleted", uuid)
else:
self._raise_secret_general_exception(result)
finally:
self.client.close()
LOG.debug("Closed connection to KMIP client for secret deletion")
def store_secret_supports(self, key_spec):
"""Key storage supported?
Specifies whether the plugin supports storage of the secret given
the attributes included in the KeySpec.
For now, only symmetric and asymmetric keys are supported.
:param key_spec: KeySpec of secret to be stored
:returns: boolean indicating if secret can be stored
"""
return self.generate_supports(key_spec)
def _create_cryptographic_algorithm_attribute(self, alg):
"""Creates a KMIP Cryptographic Algorithm attribute.
This attribute is used when telling the KMIP server what kind of
key to generate.
:param algorithm: A SecretStore KeyAlgorithm enum value
:returns: A KMIP Cryptographic Algorithm attribute
"""
attribute_type = enums.AttributeType.CRYPTOGRAPHIC_ALGORITHM
algorithm_name = self._map_algorithm_ss_to_kmip(alg.lower())
algorithm = attributes.AttributeFactory().create_attribute(
attribute_type,
algorithm_name)
LOG.debug(attribute_debug_msg,
attribute_type.value,
algorithm_name.name)
return algorithm
def _create_usage_mask_attribute(self):
"""Creates a KMIP Usage Mask attribute.
For now, we assume the key will only be used for encryption and
decryption. This attribute is used when telling the KMIP server
what kind of key to generate or store.
:returns: A KMIP Usage Mask attribute with values ENCRYPT and DECRYPT
"""
attribute_type = enums.AttributeType.CRYPTOGRAPHIC_USAGE_MASK
mask_flags = [enums.CryptographicUsageMask.ENCRYPT,
enums.CryptographicUsageMask.DECRYPT]
usage_mask = attributes.AttributeFactory().create_attribute(
attribute_type,
mask_flags)
LOG.debug(attribute_debug_msg,
attribute_type.value,
', '.join(map(str, mask_flags)))
return usage_mask
def _create_cryptographic_length_attribute(self, bit_length):
"""Creates a KMIP Cryptographic Length attribute.
This attribute is used when telling the KMIP server what kind of
key to generate.
:param bit_length: Bit length of the secret's algorithm
:returns: KMIP Cryptographic Length attribute
"""
attribute_type = enums.AttributeType.CRYPTOGRAPHIC_LENGTH
length = attributes.AttributeFactory().create_attribute(
attribute_type,
int(bit_length))
LOG.debug(attribute_debug_msg,
attribute_type.value,
bit_length)
return length
def _map_type_ss_to_kmip(self, object_type):
"""Map SecretType to KMIP type enum
Returns None if the type is not supported. The KMIP plugin only
supports symmetric and asymmetric keys for now.
:param object_type: SecretType enum value
:returns: KMIP type enums if supported, None if not supported
"""
if object_type == ss.SecretType.SYMMETRIC:
return enums.ObjectType.SYMMETRIC_KEY, enums.KeyFormatType.RAW
elif object_type == ss.SecretType.PRIVATE:
return enums.ObjectType.PRIVATE_KEY, enums.KeyFormatType.PKCS_8
elif object_type == ss.SecretType.PUBLIC:
return enums.ObjectType.PUBLIC_KEY, enums.KeyFormatType.X_509
else:
return None, None
def _map_algorithm_ss_to_kmip(self, algorithm):
"""Map SecretStore enum value to the KMIP algorithm enum
Returns None if the algorithm is not supported.
:param algorithm: SecretStore algorithm enum value
:returns: KMIP algorithm enum value if supported, None if not
supported
"""
alg_dict_entry = self.valid_alg_dict.get(algorithm, None)
if alg_dict_entry:
return alg_dict_entry.get(KMIPSecretStore.KMIP_ALGORITHM_ENUM)
else:
return None
def _map_algorithm_kmip_to_ss(self, algorithm):
"""Map KMIP algorithm enum to SecretStore algorithm enum
Returns None if the algorithm is not supported.
:param algorithm: KMIP algorithm enum
:returns: SecretStore algorithm enum value if supported, None if not
supported
"""
for ss_alg, ss_dict in self.valid_alg_dict.iteritems():
if ss_dict.get(KMIPSecretStore.KMIP_ALGORITHM_ENUM) == algorithm:
return ss_alg
return None
def _raise_secret_general_exception(self, result):
msg = u._(
"Status: {status}, Reason: {reason}, "
"Message: {message}"
).format(
status=result.result_status,
reason=result.result_reason,
message=result.result_message
)
LOG.debug("ERROR from KMIP server: %s", msg)
raise ss.SecretGeneralException(msg)
def _validate_keyfile_permissions(self, path):
"""Check that file has permissions appropriate for a sensitive key
Key files are extremely sensitive, they should be owned by the user
who they relate to. They should be readable only (to avoid accidental
changes). They should not be readable or writeable by any other user.
:raises: KMIPSecretStoreError
"""
expected = (stat.S_IRUSR | stat.S_IFREG) # 0o100400
st = os.stat(path)
if st.st_mode != expected:
raise KMIPSecretStoreError(
u._('Bad key file permissions found, expected 400 '
'for path: {file_path}').format(file_path=path)
)
def _normalize_secret(self, secret, secret_type):
"""Normalizes secret for use by KMIP plugin"""
data = base64.b64decode(secret)
if secret_type in [ss.SecretType.PUBLIC,
ss.SecretType.PRIVATE,
ss.SecretType.CERTIFICATE]:
data = translations.convert_pem_to_der(data, secret_type)
return data
def _denormalize_secret(self, secret, secret_type):
"""Converts secret back to the format expected by Barbican core"""
data = secret
if secret_type in [ss.SecretType.PUBLIC,
ss.SecretType.PRIVATE,
ss.SecretType.CERTIFICATE]:
data = translations.convert_der_to_pem(data, secret_type)
return base64.b64encode(data)