0d4101fa5d
The PKCS#11 backend key-wraps (encrypts) the project-specific Key Encryption Keys (pKEKs) using the master encryption key (MKEK). The mechanism for wrapping/unwrapping the keys was hard-coded to use CKM_AES_CBC_PAD. This patch refactors the pkcs11 module to make this mechanism configurable. This is necessary to fix Bug #2036506 because some PKCS#11 devices and software implementations no longer allow CKM_AES_CBC_PAD to be used for key wrapping. Supported key wrap mechanisms now include: * CKM_AES_CBC_PAD * CKM_AES_KEY_WRAP_PAD * CKM_AES_KEY_WRAP_KWP Closes-Bug: #2036506 Change-Id: Ic2009a2a55622bb707e884d6a960c044b2248f52
489 lines
19 KiB
Python
489 lines
19 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 base 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('token_serial_number',
|
|
help=u._('Token serial number used to identify the token to be '
|
|
'used.')),
|
|
cfg.ListOpt('token_labels',
|
|
default=[],
|
|
help=u._('List of labels for one or more tokens to be used. '
|
|
'Typically this is a single label, but some HSM '
|
|
'devices may require more than one label for Load '
|
|
'Balancing or High Availability configurations.')),
|
|
cfg.StrOpt('login',
|
|
help=u._('Password (PIN) to login to PKCS11 session'),
|
|
secret=True),
|
|
cfg.StrOpt('mkek_label',
|
|
help=u._('Master KEK label (as stored in the HSM)')),
|
|
cfg.IntOpt('mkek_length',
|
|
default=32,
|
|
min=1,
|
|
help=u._('Master KEK length in bytes.')),
|
|
cfg.StrOpt('hmac_label',
|
|
help=u._('Master HMAC Key label (as stored in the HSM)')),
|
|
cfg.IntOpt('slot_id',
|
|
help=u._('(Optional) HSM Slot ID that contains the token '
|
|
'device to be used.'),
|
|
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('encryption_mechanism',
|
|
help=u._('Secret encryption mechanism'),
|
|
default='CKM_AES_CBC', deprecated_name='algorithm'),
|
|
cfg.StrOpt('hmac_key_type',
|
|
help=u._('HMAC Key Type'),
|
|
default='CKK_AES'),
|
|
cfg.StrOpt('hmac_keygen_mechanism',
|
|
help=u._('HMAC Key Generation Algorithm used to create the '
|
|
'master HMAC Key.'),
|
|
default='CKM_AES_KEY_GEN'),
|
|
cfg.StrOpt('hmac_mechanism',
|
|
help=u._('HMAC algorithm used to sign encrypted data.'),
|
|
default='CKM_SHA256_HMAC',
|
|
deprecated_name='hmac_keywrap_mechanism'),
|
|
cfg.StrOpt('key_wrap_mechanism',
|
|
help=u._('Key Wrapping algorithm used to wrap Project KEKs.'),
|
|
default='CKM_AES_CBC_PAD'),
|
|
cfg.BoolOpt('key_wrap_generate_iv',
|
|
help=u._('Generate IVs for Key Wrapping mechanism.'),
|
|
default=True),
|
|
cfg.StrOpt('seed_file',
|
|
help=u._('File to pull entropy for seeding RNG'),
|
|
default=''),
|
|
cfg.IntOpt('seed_length',
|
|
help=u._('Amount of data to read from file for seed'),
|
|
default=32),
|
|
cfg.StrOpt('plugin_name',
|
|
help=u._('User friendly plugin name'),
|
|
default='PKCS11 HSM'),
|
|
cfg.BoolOpt('aes_gcm_generate_iv',
|
|
help=u._('Generate IVs for CKM_AES_GCM mechanism.'),
|
|
default=True, deprecated_name='generate_iv'),
|
|
cfg.BoolOpt('always_set_cka_sensitive',
|
|
help=u._('Always set CKA_SENSITIVE=CK_TRUE including '
|
|
'CKA_EXTRACTABLE=CK_TRUE keys.'),
|
|
default=True),
|
|
cfg.BoolOpt('os_locking_ok',
|
|
help=u._('Enable CKF_OS_LOCKING_OK flag when initializing the '
|
|
'PKCS#11 client library.'),
|
|
default=False),
|
|
]
|
|
CONF.register_group(p11_crypto_plugin_group)
|
|
CONF.register_opts(p11_crypto_plugin_opts, group=p11_crypto_plugin_group)
|
|
config.parse_args(CONF)
|
|
|
|
|
|
def list_opts():
|
|
yield p11_crypto_plugin_group, p11_crypto_plugin_opts
|
|
|
|
|
|
def register_opts(conf):
|
|
for group, options in list_opts():
|
|
conf.register_opts(options, group)
|
|
|
|
|
|
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
|
|
|
|
# Save conf arguments
|
|
if plugin_conf.library_path is None:
|
|
raise ValueError(u._("library_path is required"))
|
|
self.library_path = plugin_conf.library_path
|
|
self.login = plugin_conf.login
|
|
|
|
self.token_serial_number = plugin_conf.token_serial_number
|
|
self.token_labels = plugin_conf.token_labels
|
|
self.slot_id = plugin_conf.slot_id
|
|
|
|
self.rw_session = plugin_conf.rw_session
|
|
self.seed_file = plugin_conf.seed_file
|
|
self.seed_length = plugin_conf.seed_length
|
|
|
|
self.encryption_mechanism = plugin_conf.encryption_mechanism
|
|
self.encryption_gen_iv = plugin_conf.aes_gcm_generate_iv
|
|
self.cka_sensitive = plugin_conf.always_set_cka_sensitive
|
|
self.mkek_key_type = 'CKK_AES'
|
|
self.mkek_length = plugin_conf.mkek_length
|
|
self.mkek_label = plugin_conf.mkek_label
|
|
self.hmac_key_type = plugin_conf.hmac_key_type
|
|
self.hmac_label = plugin_conf.hmac_label
|
|
self.hmac_mechanism = plugin_conf.hmac_mechanism
|
|
self.key_wrap_mechanism = plugin_conf.key_wrap_mechanism
|
|
self.key_wrap_gen_iv = plugin_conf.key_wrap_generate_iv
|
|
self.os_locking_ok = plugin_conf.os_locking_ok
|
|
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
|
|
|
|
# Use specified or create new pkcs11 object
|
|
self.pkcs11 = pkcs11 or self._create_pkcs11(ffi)
|
|
|
|
self._configure_object_cache()
|
|
|
|
def get_plugin_name(self):
|
|
return self.conf.p11_crypto_plugin.plugin_name
|
|
|
|
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:
|
|
if self.pkcs11 is None:
|
|
self._reinitialize_pkcs11()
|
|
return func(*args, **kwargs)
|
|
except (exception.PKCS11Exception) as pe:
|
|
LOG.warning("Reinitializing PKCS#11 library: %s", 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']),
|
|
'mechanism': self.encryption_mechanism
|
|
})
|
|
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'])
|
|
mech = meta_extended['mechanism']
|
|
|
|
try:
|
|
session = self._get_session()
|
|
pt_data = self.pkcs11.decrypt(
|
|
mech, 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']),
|
|
'mechanism': self.encryption_mechanism}
|
|
)
|
|
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_key_type, self.mkek_label)
|
|
self._get_master_key(self.hmac_key_type, 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, ffi=None):
|
|
seed_random_buffer = None
|
|
if self.seed_file:
|
|
with open(self.seed_file, 'rb') as f:
|
|
seed_random_buffer = f.read(self.seed_length)
|
|
return pkcs11.PKCS11(
|
|
library_path=self.library_path,
|
|
login_passphrase=self.login,
|
|
token_serial_number=self.token_serial_number,
|
|
token_labels=self.token_labels,
|
|
slot_id=self.slot_id,
|
|
rw_session=self.rw_session,
|
|
seed_random_buffer=seed_random_buffer,
|
|
encryption_mechanism=self.encryption_mechanism,
|
|
encryption_gen_iv=self.encryption_gen_iv,
|
|
always_set_cka_sensitive=self.cka_sensitive,
|
|
hmac_mechanism=self.hmac_mechanism,
|
|
key_wrap_mechanism=self.key_wrap_mechanism,
|
|
key_wrap_gen_iv=self.key_wrap_gen_iv,
|
|
os_locking_ok=self.os_locking_ok,
|
|
ffi=ffi
|
|
)
|
|
|
|
def _reinitialize_pkcs11(self):
|
|
if self.pkcs11 is not None:
|
|
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._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, key_type, 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(key_type, 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):
|
|
# If plugin_meta is missing the keywrap_mechanism, we default
|
|
# to the previously hard-coded CKM_AES_CBC_PAD
|
|
_DEFAULT_KEYWRAP_MECHANISM = 'CKM_AES_CBC_PAD'
|
|
meta = json.loads(kek_meta_dto.plugin_meta)
|
|
keywrap_mechanism = meta.get('key_wrap_mechanism',
|
|
_DEFAULT_KEYWRAP_MECHANISM)
|
|
LOG.debug("Key Wrap mechanism: %s", keywrap_mechanism)
|
|
kek = self._load_kek(
|
|
kek_meta_dto.kek_label, meta['iv'], meta['wrapped_key'],
|
|
meta['hmac'], meta['mkek_label'], meta['hmac_label'],
|
|
keywrap_mechanism
|
|
)
|
|
return kek
|
|
|
|
def _load_kek(self, key_label, iv, wrapped_key, hmac,
|
|
mkek_label, hmac_label, keywrap_mechanism):
|
|
with self.pkek_cache_lock:
|
|
kek = self._pkek_cache_get(key_label)
|
|
if kek is None:
|
|
# Decode data
|
|
wrapped_key = base64.b64decode(wrapped_key)
|
|
if iv is None:
|
|
kek_data = wrapped_key
|
|
else:
|
|
iv = base64.b64decode(iv)
|
|
kek_data = iv + wrapped_key
|
|
hmac = base64.b64decode(hmac)
|
|
|
|
with self.caching_session_lock:
|
|
session = self.caching_session
|
|
# Get master keys
|
|
mkek = self._get_master_key(self.mkek_key_type, mkek_label)
|
|
mkhk = self._get_master_key(self.hmac_key_type, hmac_label)
|
|
|
|
# Verify HMAC
|
|
self.pkcs11.verify_hmac(mkhk, hmac, kek_data, session)
|
|
|
|
# Unwrap KEK
|
|
kek = self.pkcs11.unwrap_key(
|
|
keywrap_mechanism,
|
|
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_key_type, self.mkek_label)
|
|
mkhk = self._get_master_key(self.hmac_key_type, self.hmac_label)
|
|
|
|
# Generate KEK
|
|
kek = self.pkcs11.generate_key(
|
|
'CKK_AES', key_length, 'CKM_AES_KEY_GEN', session, encrypt=True
|
|
)
|
|
|
|
# Wrap KEK
|
|
wkek = self.pkcs11.wrap_key(mkek, kek, session)
|
|
|
|
# HMAC Wrapped KEK
|
|
if wkek['iv'] is None:
|
|
wkek_data = wkek['wrapped_key']
|
|
else:
|
|
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': wkek['iv'] and 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,
|
|
'key_wrap_mechanism': wkek['key_wrap_mechanism']
|
|
}
|