barbican/barbican/plugin/crypto/p11_crypto.py
Douglas Mendizábal 0d4101fa5d Configure mechanism for wrapping pKEKs
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
2024-11-13 15:42:30 -05:00

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']
}