
When using the p11_crypto module with an HSM, certain errors can be thrown by the device that currently require the Barbican application to be restarted to recover. This CR adds to work already done to the pkcs11 module that will trap known errors and will raise a specific exception that can be handled gracefully without the need to restart the entire application. In addition, the p11_crypto module has been enhanced to use a retry mechanism when these known errors are raised after reinitializing the pkcs11 library. This was done specifically to trap the CKR_TOKEN_NOT_PRESENT error from an HSM, but can be enhanced further in the future to handle additional error conditions that are recoverable with a simple reinitialization of the library to prevent the need to restart the entire Barbican application. Change-Id: Ic43f3729bff00560d4a344f785416546c019e016 Closes-Bug: 1582884
398 lines
15 KiB
Python
398 lines
15 KiB
Python
# 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 base64
|
|
import collections
|
|
import threading
|
|
import time
|
|
|
|
from oslo_config import cfg
|
|
from oslo_serialization import jsonutils as json
|
|
|
|
from barbican.common import config
|
|
from barbican.common import exception
|
|
from barbican.common import utils
|
|
from barbican import i18n as u
|
|
from barbican.plugin.crypto import crypto as plugin
|
|
from barbican.plugin.crypto import pkcs11
|
|
|
|
CONF = config.new_config()
|
|
LOG = utils.getLogger(__name__)
|
|
|
|
CachedKEK = collections.namedtuple("CachedKEK", ["kek", "expires"])
|
|
|
|
p11_crypto_plugin_group = cfg.OptGroup(name='p11_crypto_plugin',
|
|
title="PKCS11 Crypto Plugin Options")
|
|
p11_crypto_plugin_opts = [
|
|
cfg.StrOpt('library_path',
|
|
help=u._('Path to vendor PKCS11 library')),
|
|
cfg.StrOpt('login',
|
|
help=u._('Password to login to PKCS11 session'),
|
|
secret=True),
|
|
cfg.StrOpt('mkek_label',
|
|
help=u._('Master KEK label (used in the HSM)')),
|
|
cfg.IntOpt('mkek_length',
|
|
help=u._('Master KEK length in bytes.')),
|
|
cfg.StrOpt('hmac_label',
|
|
help=u._('HMAC label (used in the HSM)')),
|
|
cfg.IntOpt('slot_id',
|
|
help=u._('HSM Slot ID'),
|
|
default=1),
|
|
cfg.BoolOpt('rw_session',
|
|
help=u._('Flag for Read/Write Sessions'),
|
|
default=True),
|
|
cfg.IntOpt('pkek_length',
|
|
help=u._('Project KEK length in bytes.'),
|
|
default=32),
|
|
cfg.IntOpt('pkek_cache_ttl',
|
|
help=u._('Project KEK Cache Time To Live, in seconds'),
|
|
default=900),
|
|
cfg.IntOpt('pkek_cache_limit',
|
|
help=u._('Project KEK Cache Item Limit'),
|
|
default=100),
|
|
cfg.StrOpt('algorithm',
|
|
help=u._('Secret encryption algorithm'),
|
|
default='VENDOR_SAFENET_CKM_AES_GCM'),
|
|
]
|
|
CONF.register_group(p11_crypto_plugin_group)
|
|
CONF.register_opts(p11_crypto_plugin_opts, group=p11_crypto_plugin_group)
|
|
config.parse_args(CONF)
|
|
|
|
|
|
def json_dumps_compact(data):
|
|
return json.dumps(data, separators=(',', ':'))
|
|
|
|
|
|
class P11CryptoPlugin(plugin.CryptoPluginBase):
|
|
"""PKCS11 supporting implementation of the crypto plugin.
|
|
|
|
"""
|
|
|
|
def __init__(self, conf=CONF, ffi=None, pkcs11=None):
|
|
self.conf = conf
|
|
plugin_conf = conf.p11_crypto_plugin
|
|
if plugin_conf.library_path is None:
|
|
raise ValueError(u._("library_path is required"))
|
|
|
|
# Use specified or create new pkcs11 object
|
|
self.pkcs11 = pkcs11 or self._create_pkcs11(plugin_conf, ffi)
|
|
|
|
# Save conf arguments
|
|
self.mkek_length = plugin_conf.mkek_length
|
|
self.mkek_label = plugin_conf.mkek_label
|
|
self.hmac_label = plugin_conf.hmac_label
|
|
self.pkek_length = plugin_conf.pkek_length
|
|
self.pkek_cache_ttl = plugin_conf.pkek_cache_ttl
|
|
self.pkek_cache_limit = plugin_conf.pkek_cache_limit
|
|
self.algorithm = plugin_conf.algorithm
|
|
|
|
self._configure_object_cache()
|
|
|
|
def encrypt(self, encrypt_dto, kek_meta_dto, project_id):
|
|
return self._call_pkcs11(self._encrypt, encrypt_dto, kek_meta_dto,
|
|
project_id)
|
|
|
|
def decrypt(self, decrypt_dto, kek_meta_dto, kek_meta_extended,
|
|
project_id):
|
|
return self._call_pkcs11(self._decrypt, decrypt_dto, kek_meta_dto,
|
|
kek_meta_extended, project_id)
|
|
|
|
def bind_kek_metadata(self, kek_meta_dto):
|
|
return self._call_pkcs11(self._bind_kek_metadata, kek_meta_dto)
|
|
|
|
def generate_symmetric(self, generate_dto, kek_meta_dto, project_id):
|
|
return self._call_pkcs11(self._generate_symmetric, generate_dto,
|
|
kek_meta_dto, project_id)
|
|
|
|
def generate_asymmetric(self, generate_dto, kek_meta_dto, project_id):
|
|
raise NotImplementedError(u._("Feature not implemented for PKCS11"))
|
|
|
|
def supports(self, type_enum, algorithm=None, bit_length=None, mode=None):
|
|
if type_enum == plugin.PluginSupportTypes.ENCRYPT_DECRYPT:
|
|
return True
|
|
elif type_enum == plugin.PluginSupportTypes.SYMMETRIC_KEY_GENERATION:
|
|
return True
|
|
elif type_enum == plugin.PluginSupportTypes.ASYMMETRIC_KEY_GENERATION:
|
|
return False
|
|
else:
|
|
return False
|
|
|
|
def _call_pkcs11(self, func, *args, **kwargs):
|
|
# Wrap pkcs11 calls to enable a single retry when exceptions are raised
|
|
# that can be fixed by reinitializing the pkcs11 library
|
|
try:
|
|
return func(*args, **kwargs)
|
|
except (exception.PKCS11Exception) as pe:
|
|
LOG.warn("Reinitializing PKCS#11 library: {e}".format(e=pe))
|
|
self._reinitialize_pkcs11()
|
|
return func(*args, **kwargs)
|
|
|
|
def _encrypt(self, encrypt_dto, kek_meta_dto, project_id):
|
|
kek = self._load_kek_from_meta_dto(kek_meta_dto)
|
|
try:
|
|
session = self._get_session()
|
|
ct_data = self.pkcs11.encrypt(
|
|
kek, encrypt_dto.unencrypted, session
|
|
)
|
|
finally:
|
|
if 'session' in locals():
|
|
self._return_session(session)
|
|
|
|
kek_meta_extended = json_dumps_compact(
|
|
{'iv': base64.b64encode(ct_data['iv'])}
|
|
)
|
|
return plugin.ResponseDTO(ct_data['ct'], kek_meta_extended)
|
|
|
|
def _decrypt(self, decrypt_dto, kek_meta_dto, kek_meta_extended,
|
|
project_id):
|
|
kek = self._load_kek_from_meta_dto(kek_meta_dto)
|
|
meta_extended = json.loads(kek_meta_extended)
|
|
iv = base64.b64decode(meta_extended['iv'])
|
|
|
|
try:
|
|
session = self._get_session()
|
|
pt_data = self.pkcs11.decrypt(
|
|
kek, iv, decrypt_dto.encrypted, session
|
|
)
|
|
finally:
|
|
if 'session' in locals():
|
|
self._return_session(session)
|
|
|
|
return pt_data
|
|
|
|
def _bind_kek_metadata(self, kek_meta_dto):
|
|
if not kek_meta_dto.plugin_meta:
|
|
# Generate wrapped kek and jsonify
|
|
wkek = self._generate_wrapped_kek(
|
|
self.pkek_length, kek_meta_dto.kek_label
|
|
)
|
|
|
|
# Persisted by Barbican
|
|
kek_meta_dto.plugin_meta = json_dumps_compact(wkek)
|
|
kek_meta_dto.algorithm = 'AES'
|
|
kek_meta_dto.bit_length = self.pkek_length * 8
|
|
kek_meta_dto.mode = 'CBC'
|
|
return kek_meta_dto
|
|
|
|
def _generate_symmetric(self, generate_dto, kek_meta_dto, project_id):
|
|
kek = self._load_kek_from_meta_dto(kek_meta_dto)
|
|
byte_length = int(generate_dto.bit_length) // 8
|
|
|
|
try:
|
|
session = self._get_session()
|
|
buf = self.pkcs11.generate_random(byte_length, session)
|
|
ct_data = self.pkcs11.encrypt(kek, buf, session)
|
|
finally:
|
|
if 'session' in locals():
|
|
self._return_session(session)
|
|
|
|
kek_meta_extended = json_dumps_compact(
|
|
{'iv': base64.b64encode(ct_data['iv'])}
|
|
)
|
|
return plugin.ResponseDTO(ct_data['ct'], kek_meta_extended)
|
|
|
|
def _configure_object_cache(self):
|
|
# Master Key cache
|
|
self.mk_cache = {}
|
|
self.mk_cache_lock = threading.RLock()
|
|
|
|
# Project KEK cache
|
|
self.pkek_cache = collections.OrderedDict()
|
|
self.pkek_cache_lock = threading.RLock()
|
|
|
|
# Session for object caching
|
|
self.caching_session = self._get_session()
|
|
self.caching_session_lock = threading.RLock()
|
|
|
|
# Cache master keys
|
|
self._get_master_key(self.mkek_label)
|
|
self._get_master_key(self.hmac_label)
|
|
|
|
def _pkek_cache_add(self, kek, label):
|
|
with self.pkek_cache_lock:
|
|
if label in self.pkek_cache:
|
|
raise ValueError('{0} is already in the cache'.format(label))
|
|
now = int(time.time())
|
|
ckek = CachedKEK(kek, now + self.pkek_cache_ttl)
|
|
if len(self.pkek_cache) >= self.pkek_cache_limit:
|
|
with self.caching_session_lock:
|
|
session = self.caching_session
|
|
self._pkek_cache_expire(now, session)
|
|
# Test again if call above didn't remove any items
|
|
if len(self.pkek_cache) >= self.pkek_cache_limit:
|
|
(l, k) = self.pkek_cache.popitem(last=False)
|
|
self.pkcs11.destroy_object(k.kek, session)
|
|
self.pkek_cache[label] = ckek
|
|
|
|
def _pkek_cache_get(self, label, default=None):
|
|
kek = default
|
|
with self.pkek_cache_lock:
|
|
ckek = self.pkek_cache.get(label)
|
|
if ckek is not None:
|
|
if int(time.time()) < ckek.expires:
|
|
kek = ckek.kek
|
|
else:
|
|
with self.caching_session_lock:
|
|
self.pkcs11.destroy_object(ckek.kek,
|
|
self.caching_session)
|
|
del self.pkek_cache[label]
|
|
return kek
|
|
|
|
def _pkek_cache_expire(self, now, session):
|
|
# Look for expired items, starting from oldest
|
|
for (label, kek) in self.pkek_cache.items():
|
|
if now >= kek.expires:
|
|
self.pkcs11.destroy_object(kek.kek, session)
|
|
del self.pkek_cache[label]
|
|
else:
|
|
break
|
|
|
|
def _create_pkcs11(self, plugin_conf, ffi=None):
|
|
return pkcs11.PKCS11(
|
|
library_path=plugin_conf.library_path,
|
|
login_passphrase=plugin_conf.login,
|
|
rw_session=plugin_conf.rw_session,
|
|
slot_id=plugin_conf.slot_id,
|
|
ffi=ffi,
|
|
algorithm=plugin_conf.algorithm
|
|
)
|
|
|
|
def _reinitialize_pkcs11(self):
|
|
self.pkcs11.finalize()
|
|
self.pkcs11 = None
|
|
|
|
with self.caching_session_lock:
|
|
self.caching_session = None
|
|
|
|
with self.pkek_cache_lock:
|
|
self.pkek_cache.clear()
|
|
|
|
with self.mk_cache_lock:
|
|
self.mk_cache.clear()
|
|
|
|
self.pkcs11 = self._create_pkcs11(self.conf.p11_crypto_plugin)
|
|
self._configure_object_cache()
|
|
|
|
def _get_session(self):
|
|
return self.pkcs11.get_session()
|
|
|
|
def _return_session(self, session):
|
|
self.pkcs11.return_session(session)
|
|
|
|
def _get_master_key(self, label):
|
|
with self.mk_cache_lock:
|
|
session = self.caching_session
|
|
key = self.mk_cache.get(label, None)
|
|
if key is None:
|
|
with self.caching_session_lock:
|
|
key = self.pkcs11.get_key_handle(label, session)
|
|
if key is None:
|
|
raise exception.P11CryptoKeyHandleException(
|
|
u._("Could not find key labeled {0}").format(label)
|
|
)
|
|
self.mk_cache[label] = key
|
|
return key
|
|
|
|
def _load_kek_from_meta_dto(self, kek_meta_dto):
|
|
meta = json.loads(kek_meta_dto.plugin_meta)
|
|
kek = self._load_kek(
|
|
kek_meta_dto.kek_label, meta['iv'], meta['wrapped_key'],
|
|
meta['hmac'], meta['mkek_label'], meta['hmac_label']
|
|
)
|
|
return kek
|
|
|
|
def _load_kek(self, key_label, iv, wrapped_key, hmac,
|
|
mkek_label, hmac_label):
|
|
with self.pkek_cache_lock:
|
|
kek = self._pkek_cache_get(key_label)
|
|
if kek is None:
|
|
# Decode data
|
|
iv = base64.b64decode(iv)
|
|
wrapped_key = base64.b64decode(wrapped_key)
|
|
hmac = base64.b64decode(hmac)
|
|
kek_data = iv + wrapped_key
|
|
|
|
with self.caching_session_lock:
|
|
session = self.caching_session
|
|
# Get master keys
|
|
mkek = self._get_master_key(mkek_label)
|
|
mkhk = self._get_master_key(hmac_label)
|
|
|
|
# Verify HMAC
|
|
self.pkcs11.verify_hmac(mkhk, hmac, kek_data, session)
|
|
|
|
# Unwrap KEK
|
|
kek = self.pkcs11.unwrap_key(mkek, iv, wrapped_key,
|
|
session)
|
|
|
|
self._pkek_cache_add(kek, key_label)
|
|
|
|
return kek
|
|
|
|
def _generate_wrapped_kek(self, key_length, key_label):
|
|
with self.caching_session_lock:
|
|
session = self.caching_session
|
|
# Get master keys
|
|
mkek = self._get_master_key(self.mkek_label)
|
|
mkhk = self._get_master_key(self.hmac_label)
|
|
|
|
# Generate KEK
|
|
kek = self.pkcs11.generate_key(key_length, session, encrypt=True)
|
|
|
|
# Wrap KEK
|
|
wkek = self.pkcs11.wrap_key(mkek, kek, session)
|
|
|
|
# HMAC Wrapped KEK
|
|
wkek_data = wkek['iv'] + wkek['wrapped_key']
|
|
wkek_hmac = self.pkcs11.compute_hmac(mkhk, wkek_data, session)
|
|
|
|
# Cache KEK
|
|
self._pkek_cache_add(kek, key_label)
|
|
|
|
return {
|
|
'iv': base64.b64encode(wkek['iv']),
|
|
'wrapped_key': base64.b64encode(wkek['wrapped_key']),
|
|
'hmac': base64.b64encode(wkek_hmac),
|
|
'mkek_label': self.mkek_label,
|
|
'hmac_label': self.hmac_label
|
|
}
|
|
|
|
def _generate_mkek(self, key_length, key_label):
|
|
with self.mk_cache_lock, self.caching_session_lock:
|
|
session = self.caching_session
|
|
if key_label in self.mk_cache or \
|
|
self.pkcs11.get_key_handle(key_label, session) is not None:
|
|
raise exception.P11CryptoPluginKeyException(
|
|
u._("A master key with that label already exists")
|
|
)
|
|
mk = self.pkcs11.generate_key(
|
|
key_length, session, key_label,
|
|
encrypt=True, wrap=True, master_key=True
|
|
)
|
|
self.mk_cache[key_label] = mk
|
|
return mk
|
|
|
|
def _generate_mkhk(self, key_length, key_label):
|
|
with self.mk_cache_lock, self.caching_session_lock:
|
|
session = self.caching_session
|
|
if key_label in self.mk_cache or \
|
|
self.pkcs11.get_key_handle(key_label, session) is not None:
|
|
raise exception.P11CryptoPluginKeyException(
|
|
u._("A master key with that label already exists")
|
|
)
|
|
mk = self.pkcs11.generate_key(
|
|
key_length, session, key_label, sign=True, master_key=True
|
|
)
|
|
self.mk_cache[key_label] = mk
|
|
return mk
|