From 69459a0ecf278410835e469347c5f32f5b43ffd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Douglas=20Mendiz=C3=A1bal?= Date: Wed, 14 Oct 2020 16:04:47 -0500 Subject: [PATCH] Use serial number or label for PKCS#11 tokens The PKCS#11 standard does not make any guarantees about slot numbering, so the slot ID alone should not be used to identify a token. Instead, the token's Serial Number or Label should be used to ensure the correct token is being used. This patch adds two new config options to the p11_crypto plugin: token_serial_number and token_label. These new options allow for more flexibility in configuring the PKCS#11 module. The config may include either the token's serial number or its label. Serial numbers should be unique, so they take higher precedence. Some devices allow tokens to have the same label, so this patch ensures that only one token with the specified label is present. If both serial number and label are given, only the serial number will be checked and an error will be raised if it is not found. slot_id continues to work as expected, although its use is discouraged and may be deprecated in a future patch. If the conf contains only the slot_id, it will be used. If the serial number or label are also provided, the new logic will ignore the slot_id and search for the serial number or label instead. Change-Id: I115cf1a7006a6c85f37c5e50ded13134a3dfd1a3 --- barbican/plugin/crypto/p11_crypto.py | 15 ++- barbican/plugin/crypto/pkcs11.py | 123 +++++++++++++++++- .../tests/plugin/crypto/test_p11_crypto.py | 4 + barbican/tests/plugin/crypto/test_pkcs11.py | 70 ++++++++++ doc/source/install/barbican-backend.rst | 53 +++++++- ...d-new-pkcs11-options-fc7bb625998e91fc.yaml | 14 ++ 6 files changed, 269 insertions(+), 10 deletions(-) create mode 100644 releasenotes/notes/add-new-pkcs11-options-fc7bb625998e91fc.yaml diff --git a/barbican/plugin/crypto/p11_crypto.py b/barbican/plugin/crypto/p11_crypto.py index cb2614a43..48056087c 100644 --- a/barbican/plugin/crypto/p11_crypto.py +++ b/barbican/plugin/crypto/p11_crypto.py @@ -36,6 +36,14 @@ p11_crypto_plugin_group = cfg.OptGroup(name='p11_crypto_plugin', 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. Required when the device has multiple tokens ' + 'with the same label.')), + cfg.StrOpt('token_label', + help=u._('Token label used to identify the token to ' + 'be used. Required when token_serial_number is ' + 'not specified.')), cfg.StrOpt('login', help=u._('Password to login to PKCS11 session'), secret=True), @@ -46,7 +54,8 @@ p11_crypto_plugin_opts = [ cfg.StrOpt('hmac_label', help=u._('Master HMAC Key label (as stored in the HSM)')), cfg.IntOpt('slot_id', - help=u._('HSM 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'), @@ -310,7 +319,9 @@ class P11CryptoPlugin(plugin.CryptoPluginBase): seed_random_buffer=seed_random_buffer, generate_iv=plugin_conf.aes_gcm_generate_iv, always_set_cka_sensitive=plugin_conf.always_set_cka_sensitive, - hmac_keywrap_mechanism=plugin_conf.hmac_keywrap_mechanism + hmac_keywrap_mechanism=plugin_conf.hmac_keywrap_mechanism, + token_serial_number=plugin_conf.token_serial_number, + token_label=plugin_conf.token_label ) def _reinitialize_pkcs11(self): diff --git a/barbican/plugin/crypto/pkcs11.py b/barbican/plugin/crypto/pkcs11.py index dbe4289be..e851a609e 100644 --- a/barbican/plugin/crypto/pkcs11.py +++ b/barbican/plugin/crypto/pkcs11.py @@ -12,6 +12,7 @@ # limitations under the License. import collections +import itertools import textwrap import cffi @@ -27,8 +28,10 @@ LOG = utils.getLogger(__name__) Attribute = collections.namedtuple("Attribute", ["type", "value"]) CKAttributes = collections.namedtuple("CKAttributes", ["template", "cffivals"]) CKMechanism = collections.namedtuple("CKMechanism", ["mech", "cffivals"]) +Token = collections.namedtuple("Token", ["slot_id", "label", "serial_number"]) CKR_OK = 0 +CK_TRUE = 1 CKF_RW_SESSION = (1 << 1) CKF_SERIAL_SESSION = (1 << 2) CKU_SO = 0 @@ -263,11 +266,15 @@ def build_ffi(): ffi = cffi.FFI() ffi.cdef(textwrap.dedent(""" typedef unsigned char CK_BYTE; + typedef CK_BYTE CK_CHAR; + typedef CK_BYTE CK_UTF8CHAR; + typedef CK_BYTE CK_BBOOL; typedef unsigned long CK_ULONG; typedef unsigned long CK_RV; typedef unsigned long CK_SESSION_HANDLE; typedef unsigned long CK_OBJECT_HANDLE; typedef unsigned long CK_SLOT_ID; + typedef CK_SLOT_ID * CK_SLOT_ID_PTR; typedef unsigned long CK_FLAGS; typedef unsigned long CK_STATE; typedef unsigned long CK_USER_TYPE; @@ -294,6 +301,44 @@ def build_ffi(): typedef CK_BYTE *CK_BYTE_PTR; typedef CK_ULONG *CK_ULONG_PTR; + typedef struct CK_VERSION { + CK_BYTE major; + CK_BYTE minor; + } CK_VERSION; + + typedef struct CK_SLOT_INFO { + CK_UTF8CHAR slotDescription[64]; + CK_UTF8CHAR manufacturerID[32]; + CK_FLAGS flags; + + CK_VERSION hardwareVersion; + CK_VERSION firmwareVersion; + } CK_SLOT_INFO; + typedef CK_SLOT_INFO * CK_SLOT_INFO_PTR; + + typedef struct CK_TOKEN_INFO { + CK_UTF8CHAR label[32]; + CK_UTF8CHAR manufacturerID[32]; + CK_UTF8CHAR model[16]; + CK_CHAR serialNumber[16]; + CK_FLAGS flags; + + CK_ULONG ulMaxSessionCount; + CK_ULONG ulSessionCount; + CK_ULONG ulMaxRwSessionCount; + CK_ULONG ulRwSessionCount; + CK_ULONG ulMaxPinLen; + CK_ULONG ulMinPinLen; + CK_ULONG ulTotalPublicMemory; + CK_ULONG ulFreePublicMemory; + CK_ULONG ulTotalPrivateMemory; + CK_ULONG ulFreePrivateMemory; + CK_VERSION hardwareVersion; + CK_VERSION firmwareVersion; + CK_CHAR utcTime[16]; + } CK_TOKEN_INFO; + typedef CK_TOKEN_INFO * CK_TOKEN_INFO_PTR; + typedef struct ck_session_info { CK_SLOT_ID slot_id; CK_STATE state; @@ -321,6 +366,9 @@ def build_ffi(): CK_RV C_GetSessionInfo(CK_SESSION_HANDLE, CK_SESSION_INFO_PTR); CK_RV C_Login(CK_SESSION_HANDLE, CK_USER_TYPE, CK_UTF8CHAR_PTR, CK_ULONG); + CK_RV C_GetSlotList(CK_BBOOL, CK_SLOT_ID_PTR, CK_ULONG_PTR); + CK_RV C_GetSlotInfo(CK_SLOT_ID, CK_SLOT_INFO_PTR); + CK_RV C_GetTokenInfo(CK_SLOT_ID, CK_TOKEN_INFO_PTR); CK_RV C_GetAttributeValue(CK_SESSION_HANDLE, CK_OBJECT_HANDLE, CK_ATTRIBUTE *, CK_ULONG); CK_RV C_SetAttributeValue(CK_SESSION_HANDLE, CK_OBJECT_HANDLE, @@ -365,7 +413,9 @@ class PKCS11(object): ffi=None, algorithm=None, seed_random_buffer=None, generate_iv=None, always_set_cka_sensitive=None, - hmac_keywrap_mechanism='CKM_SHA256_HMAC'): + hmac_keywrap_mechanism='CKM_SHA256_HMAC', + token_serial_number=None, + token_label=None): if algorithm: LOG.warning("WARNING: Using deprecated 'algorithm' argument.") encryption_mechanism = encryption_mechanism or algorithm @@ -389,7 +439,10 @@ class PKCS11(object): # Session options self.login_passphrase = _to_bytes(login_passphrase) self.rw_session = rw_session - self.slot_id = slot_id + self.slot_id = self._get_slot_id( + token_serial_number, + token_label, + slot_id) # Algorithm options self.algorithm = CKM_NAMES[encryption_mechanism] @@ -406,6 +459,72 @@ class PKCS11(object): self._seed_random(session, seed_random_buffer) self._rng_self_test(session) self.return_session(session) + LOG.debug("Connected to PCKS11 sn: %s label: %s slot: %s", + token_serial_number, token_label, self.slot_id) + + def _get_slot_id(self, token_serial_number, token_label, slot_id): + # First find out how many slots with tokens are available + slots_ptr = self.ffi.new("CK_ULONG_PTR") + rv = self.lib.C_GetSlotList(CK_TRUE, self.ffi.NULL, slots_ptr) + self._check_error(rv) + + # Next get the Slot IDs for each of the available slots + slot_ids_ptr = self.ffi.new("CK_SLOT_ID[{}]".format(slots_ptr[0])) + rv = self.lib.C_GetSlotList(CK_TRUE, slot_ids_ptr, slots_ptr) + self._check_error(rv) + + # Gather details from each token + tokens = list() + for id in slot_ids_ptr: + token_info_ptr = self.ffi.new("CK_TOKEN_INFO_PTR") + rv = self.lib.C_GetTokenInfo(id, token_info_ptr) + self._check_error(rv) + tokens.append(Token( + id, + self.ffi.string(token_info_ptr.label).decode("UTF-8").strip(), + self.ffi.string( + token_info_ptr.serialNumber + ).decode("UTF-8").strip() + )) + + # Matching serial number gets highest priority + if token_serial_number: + for token in tokens: + if token.serial_number == token_serial_number: + LOG.debug("Found token sn: %s in slot %s", + token.serial_number, + token.slot_id) + if token_label: + LOG.warning( + "Ignoring token_label: %s from barbican.conf", + token_label + ) + if slot_id: + LOG.warning("Ignoring slot_id: %s from barbican.conf", + slot_id) + return token.slot_id + raise ValueError("Token Serial Number not found.") + + # Label match is next, raises an error if there's not exactly one match + if token_label: + matched = list(itertools.dropwhile( + lambda x: x.label != token_label, + tokens + )) + if len(matched) > 1: + raise ValueError("More than one matching token label found") + if len(matched) < 1: + raise ValueError("Token Label not found.") + + token = matched.pop() + LOG.debug("Found token label: %s in slot %s", token.label, + token.slot_id) + if slot_id: + LOG.warning("Ignoring slot_id: %s from barbican.conf", slot_id) + return token.slot_id + + # If we got this far, slot_id was the only param given, so we return it + return slot_id def get_session(self): session = self._open_session(self.slot_id) diff --git a/barbican/tests/plugin/crypto/test_p11_crypto.py b/barbican/tests/plugin/crypto/test_p11_crypto.py index afae19cc6..f9044c0ea 100644 --- a/barbican/tests/plugin/crypto/test_p11_crypto.py +++ b/barbican/tests/plugin/crypto/test_p11_crypto.py @@ -54,6 +54,8 @@ class WhenTestingP11CryptoPlugin(utils.BaseTestCase): self.cfg_mock.p11_crypto_plugin.hmac_label = 'hmac_label' self.cfg_mock.p11_crypto_plugin.mkek_length = 32 self.cfg_mock.p11_crypto_plugin.slot_id = 1 + self.cfg_mock.p11_crypto_plugin.token_serial_number = None + self.cfg_mock.p11_crypto_plugin.token_label = None self.cfg_mock.p11_crypto_plugin.rw_session = True self.cfg_mock.p11_crypto_plugin.pkek_length = 32 self.cfg_mock.p11_crypto_plugin.pkek_cache_ttl = 900 @@ -286,6 +288,8 @@ class WhenTestingP11CryptoPlugin(utils.BaseTestCase): return pkcs11.CKR_OK lib = mock.Mock() lib.C_Initialize.return_value = pkcs11.CKR_OK + lib.C_GetSlotList.return_value = pkcs11.CKR_OK + lib.C_GetTokenInfo.return_value = pkcs11.CKR_OK lib.C_OpenSession.return_value = pkcs11.CKR_OK lib.C_CloseSession.return_value = pkcs11.CKR_OK lib.C_GetSessionInfo.return_value = pkcs11.CKR_OK diff --git a/barbican/tests/plugin/crypto/test_pkcs11.py b/barbican/tests/plugin/crypto/test_pkcs11.py index fbc01fbb3..f0a74a742 100644 --- a/barbican/tests/plugin/crypto/test_pkcs11.py +++ b/barbican/tests/plugin/crypto/test_pkcs11.py @@ -27,6 +27,8 @@ class WhenTestingPKCS11(utils.BaseTestCase): self.lib = mock.Mock() self.lib.C_Initialize.return_value = pkcs11.CKR_OK self.lib.C_Finalize.return_value = pkcs11.CKR_OK + self.lib.C_GetSlotList.side_effect = self._get_slot_list + self.lib.C_GetTokenInfo.side_effect = self._get_token_info self.lib.C_OpenSession.side_effect = self._open_session self.lib.C_CloseSession.return_value = pkcs11.CKR_OK self.lib.C_GetSessionInfo.side_effect = self._get_session_user @@ -59,6 +61,10 @@ class WhenTestingPKCS11(utils.BaseTestCase): self.cfg_mock.encryption_mechanism = 'CKM_AES_CBC' self.cfg_mock.hmac_keywrap_mechanism = 'CKM_SHA256_HMAC' + self.token_mock = mock.MagicMock() + self.token_mock.label = b'myLabel' + self.token_mock.serial_number = b'111111' + self.pkcs11 = pkcs11.PKCS11( self.cfg_mock.library_path, self.cfg_mock.login_passphrase, self.cfg_mock.rw_session, self.cfg_mock.slot_id, @@ -71,6 +77,31 @@ class WhenTestingPKCS11(utils.BaseTestCase): self.ffi.buffer(buf)[:] = b'0' * length return pkcs11.CKR_OK + def _get_slot_list(self, token_present, slot_ids_ptr, slots_ptr): + # default to mocking only one slot (ID: 1) + if slot_ids_ptr is not self.ffi.NULL: + slot_ids_ptr[0] = 1 + slots_ptr[0] = 1 + return pkcs11.CKR_OK + + def _get_token_info(self, id, token_info_ptr): + token_info_ptr.serialNumber = self.token_mock.serial_number + token_info_ptr.label = self.token_mock.label + return pkcs11.CKR_OK + + def _get_two_slot_list(self, token_present, slot_ids_ptr, slots_ptr): + # mock two slots (IDs: 1, 2) + if slot_ids_ptr is not self.ffi.NULL: + slot_ids_ptr[0] = 1 + slot_ids_ptr[1] = 2 + slots_ptr[0] = 2 + return pkcs11.CKR_OK + + def _get_two_token_info_same_label(self, id, token_info_ptr): + token_info_ptr.serialNumber = (str(id) * 6).encode('UTF-8') + token_info_ptr.label = self.token_mock.label + return pkcs11.CKR_OK + def _get_session_public(self, session, session_info_ptr): if self.cfg_mock.rw_session: session_info_ptr[0].state = pkcs11.CKS_RW_PUBLIC_SESSION @@ -143,6 +174,45 @@ class WhenTestingPKCS11(utils.BaseTestCase): def _verify(self, *args, **kwargs): return pkcs11.CKR_OK + def test_get_slot_id_from_serial_number(self): + slot_id = self.pkcs11._get_slot_id('111111', None, 1) + self.assertEqual(1, slot_id) + + def test_get_slot_id_from_label(self): + slot_id = self.pkcs11._get_slot_id(None, 'myLabel', 1) + self.assertEqual(1, slot_id) + + def test_get_slot_id_backwards_compatibility(self): + slot_id = self.pkcs11._get_slot_id(None, None, 5) + self.assertEqual(5, slot_id) + + def test_get_slot_id_from_serial_ignores_label(self): + slot_id = self.pkcs11._get_slot_id('111111', 'badLabel', 1) + self.assertEqual(1, slot_id) + + def test_get_slot_id_from_serial_ignores_given_slot(self): + slot_id = self.pkcs11._get_slot_id('111111', None, 3) + self.assertEqual(1, slot_id) + + def test_get_slot_id_from_label_ignores_given_slot(self): + slot_id = self.pkcs11._get_slot_id(None, 'myLabel', 3) + self.assertEqual(1, slot_id) + + def test_get_slot_id_serial_not_found(self): + self.assertRaises(ValueError, + self.pkcs11._get_slot_id, '222222', None, 1) + + def test_get_slot_id_label_not_found(self): + self.assertRaises(ValueError, + self.pkcs11._get_slot_id, None, 'badLabel', 1) + + def test_get_slot_id_two_tokens_same_label(self): + self.lib.C_GetSlotList.side_effect = self._get_two_slot_list + self.lib.C_GetTokenInfo.side_effect = \ + self._get_two_token_info_same_label + self.assertRaises(ValueError, + self.pkcs11._get_slot_id, None, 'myLabel', 1) + def test_public_get_session(self): self.lib.C_GetSessionInfo.side_effect = self._get_session_public sess = self.pkcs11.get_session() diff --git a/doc/source/install/barbican-backend.rst b/doc/source/install/barbican-backend.rst index f67df033c..81e0ebb79 100644 --- a/doc/source/install/barbican-backend.rst +++ b/doc/source/install/barbican-backend.rst @@ -98,6 +98,15 @@ The PKCS#11 plugin configuration looks like: # Path to vendor PKCS11 library library_path = '/usr/lib/libCryptoki2_64.so' + # Token serial number used to identify the token to be used. Required + # when the device has multiple tokens with the same label. (string + # value) + token_serial_number = 12345678 + + # Token label used to identify the token to be used. Required when + # token_serial_number is not specified. (string value) + #token_label = + # Password to login to PKCS11 session login = 'mypassword' @@ -110,8 +119,10 @@ The PKCS#11 plugin configuration looks like: # Label to identify HMAC key in the HSM (must not be the same as MKEK label) hmac_label = 'my_hmac_label' - # HSM Slot id (Should correspond to a configured PKCS11 slot). Default: 1 - # slot_id = 1 + # (Optional) HSM Slot ID that contains the token device to be used. + # (integer value) + #slot_id = 1 + # Enable Read/Write session with the HSM? # rw_session = True @@ -141,6 +152,15 @@ For a nCipher nShield Connect XC, the plugin configuration looks like: # Path to vendor PKCS11 library library_path = '/opt/nfast/toolkits/pkcs11/libcknfast.so' + # Token serial number used to identify the token to be used. Required + # when the device has multiple tokens with the same label. (string + # value) + token_serial_number = 12345678 + + # Token label used to identify the token to be used. Required when + # token_serial_number is not specified. (string value) + #token_label = + # Password to login to PKCS11 session login = 'XXX' @@ -153,7 +173,8 @@ For a nCipher nShield Connect XC, the plugin configuration looks like: # Label to identify HMAC key in the HSM (must not be the same as MKEK label) hmac_label = 'thales_hmac_0' - # HSM Slot id (Should correspond to a configured PKCS11 slot). Default: 1 + # (Optional) HSM Slot ID that contains the token device to be used. + # (integer value) # slot_id = 1 # Enable Read/Write session with the HSM? @@ -220,6 +241,15 @@ For an ATOS Bull HSM, the plugin configuration looks like: # Path to vendor PKCS11 library library_path = '/usr/lib64/libnethsm.so' + # Token serial number used to identify the token to be used. Required + # when the device has multiple tokens with the same label. (string + # value) + token_serial_number = 12345678 + + # Token label used to identify the token to be used. Required when + # token_serial_number is not specified. (string value) + #token_label = + # Password to login to PKCS11 session login = 'XXX' @@ -232,7 +262,8 @@ For an ATOS Bull HSM, the plugin configuration looks like: # Label to identify HMAC key in the HSM (must not be the same as MKEK label) hmac_label = 'atos_hmac_0' - # HSM Slot id (Should correspond to a configured PKCS11 slot). Default: 1 + # (Optional) HSM Slot ID that contains the token device to be used. + # (integer value) # slot_id = 1 # Enable Read/Write session with the HSM? @@ -293,6 +324,15 @@ The PKCS#11 plugin configuration looks like: # Path to vendor PKCS11 library (string value) library_path = '/opt/utimaco/lib/libcs_pkcs11_R2.so' + # Token serial number used to identify the token to be used. Required + # when the device has multiple tokens with the same label. (string + # value) + token_serial_number = 12345678 + + # Token label used to identify the token to be used. Required when + # token_serial_number is not specified. (string value) + #token_label = + # Password to login to PKCS11 session (string value) login = '$up3r$e