diff --git a/extern/pkcs11t.h b/extern/pkcs11t.h index bd3e7eb..f584f21 100644 --- a/extern/pkcs11t.h +++ b/extern/pkcs11t.h @@ -270,6 +270,8 @@ typedef CK_ULONG CK_USER_TYPE; #define CKU_USER 1UL /* Context specific */ #define CKU_CONTEXT_SPECIFIC 2UL +/* Value used in python-pkcs11 as a sentinel value for non-logged in sessions */ +#define CKU_USER_NOBODY 999UL /* CK_STATE enumerates the session states */ typedef CK_ULONG CK_STATE; diff --git a/pkcs11/__init__.py b/pkcs11/__init__.py index cc3a8c2..410218f 100644 --- a/pkcs11/__init__.py +++ b/pkcs11/__init__.py @@ -8,8 +8,7 @@ from .types import * # noqa: F403 from .util import dh, dsa, ec, rsa, x509 # noqa: F401 -_so = None -_lib = None +_loaded = {} def lib(so): @@ -17,20 +16,19 @@ def lib(so): Wrap the main library call coming from Cython with a preemptive dynamic loading. """ - global _lib - global _so + global _loaded - if _lib: - if _so != so: - raise AlreadyInitialized( # noqa: F405 - "Already initialized with %s" % _so - ) - else: - return _lib + try: + _lib = _loaded[so] + if not _lib.initialized: + _lib.initialize() + return _lib + except KeyError: + pass from . import _pkcs11 _lib = _pkcs11.lib(so) - _so = so + _loaded[so] = _lib return _lib diff --git a/pkcs11/_pkcs11.pxd b/pkcs11/_pkcs11.pxd index a5eb9e9..80759af 100644 --- a/pkcs11/_pkcs11.pxd +++ b/pkcs11/_pkcs11.pxd @@ -139,6 +139,8 @@ cdef extern from '../extern/cryptoki.h': CKR_FUNCTION_REJECTED, + CKR_OPERATION_CANCEL_FAILED, + CKR_VENDOR_DEFINED, @@ -146,6 +148,7 @@ cdef extern from '../extern/cryptoki.h': CKU_SO, CKU_USER, CKU_CONTEXT_SPECIFIC, + CKU_USER_NOBODY, cdef enum: CK_TRUE, @@ -589,6 +592,28 @@ cdef extern from '../extern/cryptoki.h': # All other APIs are taken from the CK_FUNCTION_LIST table ctypedef CK_RV (*C_GetFunctionList_ptr) (CK_FUNCTION_LIST **) nogil +ctypedef CK_RV (*KeyOperationInit) ( + CK_SESSION_HANDLE session, + CK_MECHANISM *mechanism, + CK_OBJECT_HANDLE key +) nogil +ctypedef CK_RV (*OperationUpdateWithResult) ( + CK_SESSION_HANDLE session, + CK_BYTE *part_in, + CK_ULONG part_in_len, + CK_BYTE *part_out, + CK_ULONG *part_out_len +) nogil +ctypedef CK_RV (*OperationUpdate) ( + CK_SESSION_HANDLE session, + CK_BYTE *part_in, + CK_ULONG part_in_len +) nogil +ctypedef CK_RV (*OperationWithResult) ( + CK_SESSION_HANDLE session, + CK_BYTE *part_out, + CK_ULONG *part_out_len +) nogil cdef inline CK_BYTE_buffer(length): """Make a buffer for `length` CK_BYTEs.""" diff --git a/pkcs11/_pkcs11.pyx b/pkcs11/_pkcs11.pyx index d2d0125..6b7b410 100644 --- a/pkcs11/_pkcs11.pyx +++ b/pkcs11/_pkcs11.pyx @@ -11,6 +11,8 @@ library loaded. from __future__ import (absolute_import, unicode_literals, print_function, division) +from threading import RLock + from cpython.mem cimport PyMem_Malloc, PyMem_Free from . import types @@ -26,11 +28,14 @@ from .types import ( ) -# _funclist is used to keep the pointer to the list of functions, when lib() is invoked. -# This is a global, as this object cannot be shared between Python and Cython classes -# Due to this limitation, the current implementation limits the loading of the library -# to one instance only, or to several instances of the same kind. -cdef CK_FUNCTION_LIST *_funclist = NULL +cdef class lib(HasFuncList) + +cdef class HasFuncList: + cdef CK_FUNCTION_LIST *funclist + + def __cinit__(self, *args, **kwargs): + self.funclist = NULL + cdef assertRV(rv) with gil: """Check for an acceptable RV value or thrown an exception.""" @@ -47,11 +52,11 @@ cdef assertRV(rv) with gil: elif rv == CKR_ARGUMENTS_BAD: exc = ArgumentsBad() elif rv == CKR_BUFFER_TOO_SMALL: - exc = MemoryError("Buffer was too small. Should never see this.") + exc = PKCS11Error("Buffer was too small. Should never see this.") elif rv == CKR_CRYPTOKI_ALREADY_INITIALIZED: - exc = RuntimeError("Initialisation error (already initialized). Should never see this.") + exc = PKCS11Error("Initialisation error (already initialized). Should never see this.") elif rv == CKR_CRYPTOKI_NOT_INITIALIZED: - exc = RuntimeError("Initialisation error (not initialized). Should never see this.") + exc = PKCS11Error("Initialisation error (not initialized). Should never see this.") elif rv == CKR_DATA_INVALID: exc = DataInvalid() elif rv == CKR_DATA_LEN_RANGE: @@ -135,7 +140,7 @@ cdef assertRV(rv) with gil: elif rv == CKR_SESSION_HANDLE_INVALID: exc = SessionHandleInvalid() elif rv == CKR_SESSION_PARALLEL_NOT_SUPPORTED: - exc = RuntimeError("Parallel not supported. Should never see this.") + exc = PKCS11Error("Parallel not supported. Should never see this.") elif rv == CKR_SESSION_READ_ONLY: exc = SessionReadOnly() elif rv == CKR_SESSION_READ_ONLY_EXISTS: @@ -175,7 +180,7 @@ cdef assertRV(rv) with gil: elif rv == CKR_USER_TOO_MANY_TYPES: exc = UserTooManyTypes() elif rv == CKR_USER_TYPE_INVALID: - exc = RuntimeError("User type invalid. Should never see this.") + exc = PKCS11Error("User type invalid. Should never see this.") elif rv == CKR_WRAPPED_KEY_INVALID: exc = WrappedKeyInvalid() elif rv == CKR_WRAPPED_KEY_LEN_RANGE: @@ -377,25 +382,65 @@ cdef class MechanismWithParam: PyMem_Free(self.param) -class Slot(types.Slot): +cdef class Slot(HasFuncList, types.Slot): """Extend Slot with implementation.""" + cdef readonly CK_SLOT_ID slot_id + """Slot identifier (opaque).""" + cdef readonly str slot_description + """Slot name (:class:`str`).""" + cdef readonly str manufacturer_id + """Slot/device manufacturer's name (:class:`str`).""" + cdef readonly tuple cryptoki_version + cdef CK_FLAGS slot_flags + cdef CK_VERSION hw_version + cdef CK_VERSION fw_version + + @staticmethod + cdef Slot make(CK_FUNCTION_LIST *funclist, CK_SLOT_ID slot_id, CK_SLOT_INFO info, tuple cryptoki_version): + description = info.slotDescription[:sizeof(info.slotDescription)] + manufacturer_id = info.manufacturerID[:sizeof(info.manufacturerID)] + + cdef Slot slot = Slot.__new__(Slot) + slot.funclist = funclist + slot.cryptoki_version = cryptoki_version + + slot.slot_id = slot_id + slot.slot_description = _CK_UTF8CHAR_to_str(description) + slot.manufacturer_id = _CK_UTF8CHAR_to_str(manufacturer_id) + slot.hw_version = info.hardwareVersion + slot.fw_version = info.firmwareVersion + slot.slot_flags = info.flags + return slot + + def __init__(self): + raise TypeError + + @property + def flags(self): + """Capabilities of this slot (:class:`SlotFlag`).""" + return SlotFlag(self.slot_flags) + + @property + def hardware_version(self): + """Hardware version (:class:`tuple`).""" + return _CK_VERSION_to_tuple(self.hw_version) + + @property + def firmware_version(self): + """Firmware version (:class:`tuple`).""" + return _CK_VERSION_to_tuple(self.fw_version) + def get_token(self): cdef CK_SLOT_ID slot_id = self.slot_id cdef CK_TOKEN_INFO info cdef CK_RV retval with nogil: - retval = _funclist.C_GetTokenInfo(slot_id, &info) + retval = self.funclist.C_GetTokenInfo(slot_id, &info) assertRV(retval) - label = info.label[:sizeof(info.label)] - serialNumber = info.serialNumber[:sizeof(info.serialNumber)] - model = info.model[:sizeof(info.model)] - manufacturerID = info.manufacturerID[:sizeof(info.manufacturerID)] - - return Token(self, label, serialNumber, model, manufacturerID, - info.hardwareVersion, info.firmwareVersion, info.flags) + return Token.make(self, info) def get_mechanisms(self): cdef CK_SLOT_ID slot_id = self.slot_id @@ -403,7 +448,7 @@ class Slot(types.Slot): cdef CK_RV retval with nogil: - retval = _funclist.C_GetMechanismList(slot_id, NULL, &count) + retval = self.funclist.C_GetMechanismList(slot_id, NULL, &count) assertRV(retval) if count == 0: @@ -412,7 +457,7 @@ class Slot(types.Slot): cdef CK_MECHANISM_TYPE [:] mechanisms = CK_ULONG_buffer(count) with nogil: - retval = _funclist.C_GetMechanismList(slot_id, &mechanisms[0], &count) + retval = self.funclist.C_GetMechanismList(slot_id, &mechanisms[0], &count) assertRV(retval) return set(map(_CK_MECHANISM_TYPE_to_enum, mechanisms)) @@ -424,15 +469,85 @@ class Slot(types.Slot): cdef CK_RV retval with nogil: - retval = _funclist.C_GetMechanismInfo(slot_id, mech_type, &info) + retval = self.funclist.C_GetMechanismInfo(slot_id, mech_type, &info) assertRV(retval) return types.MechanismInfo(self, mechanism, **info) + def _identity(self): + return Slot.__name__, self.slot_id + + def __str__(self): + return "\n".join( + ( + "Slot Description: %s" % self.slot_description, + "Manufacturer ID: %s" % self.manufacturer_id, + "Hardware Version: %s.%s" % self.hardware_version, + "Firmware Version: %s.%s" % self.firmware_version, + "Flags: %s" % self.flags, + ) + ) + + def __repr__(self): + return "<{klass} (slotID={slot_id} flags={flags})>".format( + klass=type(self).__name__, slot_id=self.slot_id, flags=str(self.flags) + ) + -class Token(types.Token): +cdef class Token(HasFuncList, types.Token): """Extend Token with implementation.""" + cdef readonly Slot slot + """The :class:`Slot` this token is installed in.""" + cdef readonly str label + """Label of this token (:class:`str`).""" + cdef readonly bytes serial + """Serial number of this token (:class:`bytes`).""" + cdef readonly str manufacturer_id + """Manufacturer ID.""" + cdef readonly str model + """Model name.""" + cdef CK_FLAGS token_flags + cdef CK_VERSION hw_version + cdef CK_VERSION fw_version + + @staticmethod + cdef Token make(Slot slot, CK_TOKEN_INFO info): + label = info.label[:sizeof(info.label)] + serial_number = info.serialNumber[:sizeof(info.serialNumber)] + model = info.model[:sizeof(info.model)] + manufacturer_id = info.manufacturerID[:sizeof(info.manufacturerID)] + + cdef Token token = Token.__new__(Token) + token.funclist = slot.funclist + token.slot = slot + token.label = _CK_UTF8CHAR_to_str(label) + token.serial = serial_number.rstrip() + token.manufacturer_id = _CK_UTF8CHAR_to_str(manufacturer_id) + token.model = _CK_UTF8CHAR_to_str(model) + token.hw_version = info.hardwareVersion + token.fw_version = info.firmwareVersion + token.token_flags = info.flags + return token + + def __init__(self): + raise TypeError + + @property + def flags(self): + """Capabilities of this token (:class:`TokenFlag`).""" + return TokenFlag(self.token_flags) + + @property + def hardware_version(self): + """Hardware version (:class:`tuple`).""" + return _CK_VERSION_to_tuple(self.hw_version) + + @property + def firmware_version(self): + """Firmware version (:class:`tuple`).""" + return _CK_VERSION_to_tuple(self.fw_version) + def open(self, rw=False, user_pin=None, so_pin=None, user_type=None): cdef CK_SLOT_ID slot_id = self.slot.slot_id cdef CK_SESSION_HANDLE handle @@ -441,6 +556,7 @@ class Token(types.Token): cdef CK_UTF8CHAR *pin_data cdef CK_ULONG pin_length cdef CK_RV retval + cdef CK_USER_TYPE c_user_type if rw: flags |= CKF_RW_SESSION @@ -449,29 +565,28 @@ class Token(types.Token): raise ArgumentsBad("Set either `user_pin` or `so_pin`") elif user_pin is PROTECTED_AUTH: pin = None - user_type = user_type if user_type is not None else CKU_USER + c_user_type = user_type if user_type is not None else CKU_USER elif so_pin is PROTECTED_AUTH: pin = None - user_type = CKU_SO + c_user_type = CKU_SO elif user_pin is not None: pin = user_pin.encode('utf-8') - user_type = user_type if user_type is not None else CKU_USER + c_user_type = user_type if user_type is not None else CKU_USER elif so_pin is not None: pin = so_pin.encode('utf-8') - user_type = CKU_SO + c_user_type = CKU_SO else: pin = None - user_type = UserType.NOBODY + c_user_type = CKU_USER_NOBODY - final_user_type = user_type with nogil: - retval = _funclist.C_OpenSession(slot_id, flags, NULL, NULL, &handle) + retval = self.funclist.C_OpenSession(slot_id, flags, NULL, NULL, &handle) assertRV(retval) if so_pin is PROTECTED_AUTH or user_pin is PROTECTED_AUTH: if self.flags & TokenFlag.PROTECTED_AUTHENTICATION_PATH: with nogil: - retval = _funclist.C_Login(handle, final_user_type, NULL, 0) + retval = self.funclist.C_Login(handle, c_user_type, NULL, 0) assertRV(retval) else: raise ArgumentsBad("Protected authentication is not supported by loaded module") @@ -480,68 +595,281 @@ class Token(types.Token): pin_length = len(pin) with nogil: - retval = _funclist.C_Login(handle, final_user_type, pin_data, pin_length) + retval = self.funclist.C_Login(handle, c_user_type, pin_data, pin_length) assertRV(retval) - return Session(self, handle, rw=rw, user_type=user_type) + return Session.make(self, handle, rw= rw, user_type=c_user_type) + def __str__(self): + return self.label -class SearchIter: - """Iterate a search for objects on a session.""" + def _identity(self): + return Token.__name__, self.slot - def __init__(self, session, attrs): + def __repr__(self): + return "<{klass} (label='{label}' serial={serial} flags={flags})>".format( + klass=type(self).__name__, label=self.label, serial=self.serial, flags=str(self.flags) + ) + + +cdef class OperationContext: + cdef Session session + cdef bint active + + def __cinit__(self, session, *args, **kwargs): self.session = session + self.active = False - template = AttributeList(attrs) - self.session._operation_lock.acquire() - self._active = True + def __init__(self, session): + pass - cdef CK_SESSION_HANDLE handle = self.session._handle - cdef CK_ATTRIBUTE *attr_data = template.data - cdef CK_ULONG attr_count = template.count + def __enter__(self): + self.session.operation_lock.acquire() + self.active = True + self._initiate() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self._finalize(silent=False) + + cdef _handle_final_retval(self, CK_RV retval) with gil: + self.active = False + self.session.operation_lock.release() + assertRV(retval) + + cdef _operation_aware_assert(self, CK_RV retval) with gil: + if retval != CKR_BUFFER_TOO_SMALL and retval != CKR_OK: + # This is an error that terminated the operation + # We flag the operation as completed on our end as well. + # This is useful to track because there's no way to cleanly cancel + # cryptographic operations in PCKS#11 2.x. + self._handle_final_retval(retval) + + def _initiate(self): + raise NotImplementedError + + def _finalize(self, silent=False): + if self.active: + self.active = False + self.session.operation_lock.release() + + def __del__(self): + self._finalize() + + +cdef class OperationWithBinaryOutput(OperationContext): + + cdef MechanismWithParam mech + + cdef CK_ULONG buffer_size + cdef CK_ULONG buffer_data_length + cdef CK_BYTE [:] output_buf + + @staticmethod + cdef OperationWithBinaryOutput _setup( + type cls, + Session session, + MechanismWithParam mech, + CK_ULONG buffer_size + ) with gil: + cdef OperationWithBinaryOutput op = cls.__new__(cls, session) + op.mech = mech + if buffer_size > 0: + op.output_buf = CK_BYTE_buffer(buffer_size) + op.buffer_size = buffer_size + return op + + cdef resize_buffer(self, CK_ULONG length): + self.output_buf = CK_BYTE_buffer(length) + self.buffer_size = length + + cdef inline bytes current_output(self): + return bytes(self.output_buf[:self.buffer_data_length]) + + cdef CK_RV update_resizing_output( + self, + OperationUpdateWithResult op_update, + CK_BYTE *data, + CK_ULONG data_len, + ) with gil: + + cdef CK_ULONG length = self.buffer_size + cdef CK_BYTE *output_buf_loc = &self.output_buf[0] cdef CK_RV retval with nogil: - retval = _funclist.C_FindObjectsInit(handle, attr_data, attr_count) - assertRV(retval) + retval = op_update( + self.session.handle, data, data_len, output_buf_loc, &length + ) + + if retval == CKR_BUFFER_TOO_SMALL: + self.resize_buffer(length) + output_buf_loc = &self.output_buf[0] + with nogil: + retval = op_update( + self.session.handle, data, data_len, output_buf_loc, &length + ) + self.buffer_data_length = length + return retval + + cdef CK_RV execute_resizing_output(self, OperationWithResult op) with gil: + + cdef CK_ULONG length = self.buffer_size + cdef CK_BYTE *output_buf_loc = &self.output_buf[0] + cdef CK_RV retval + + with nogil: + retval = op(self.session.handle, output_buf_loc, &length) + + if retval == CKR_BUFFER_TOO_SMALL: + self.resize_buffer(length) + output_buf_loc = &self.output_buf[0] + with nogil: + retval = op(self.session.handle, output_buf_loc, &length) + + self.buffer_data_length = length + return retval + + cdef bytes process_fully( + self, + OperationUpdateWithResult op, + CK_BYTE *data, + CK_ULONG data_len + ) with gil: + cdef CK_RV retval = self.update_resizing_output(op, data, data_len) + self._handle_final_retval(retval) + return self.current_output() + + cdef bytes update_with_result( + self, OperationUpdateWithResult op, CK_BYTE *data, CK_ULONG data_len + ) with gil: + cdef CK_RV retval = self.update_resizing_output(op, data, data_len) + self._operation_aware_assert(retval) + return self.current_output() + + cdef update_no_output( + self, OperationUpdate op, CK_BYTE *data, CK_ULONG data_len + ) with gil: + cdef CK_RV retval + with nogil: + retval = op(self.session.handle, data, data_len) + self._operation_aware_assert(retval) + + cdef bytes finish_with_output(self, OperationWithResult op_final) with gil: + cdef CK_RV retval = self.execute_resizing_output(op_final) + self._handle_final_retval(retval) + return self.current_output() + + +cdef class SearchIter(OperationContext): + """Iterate a search for objects on a session.""" + + cdef AttributeList template + + def __init__(self, session, attrs): + cdef AttributeList template = AttributeList(attrs) + self.template = template + super().__init__(session) def __iter__(self): return self def __next__(self): """Get the next object.""" - cdef CK_SESSION_HANDLE handle = self.session._handle + cdef CK_SESSION_HANDLE handle = self.session.handle cdef CK_OBJECT_HANDLE obj cdef CK_ULONG count cdef CK_RV retval with nogil: - retval = _funclist.C_FindObjects(handle, &obj, 1, &count) + retval = self.session.funclist.C_FindObjects(handle, &obj, 1, &count) assertRV(retval) if count == 0: self._finalize() raise StopIteration() else: - return Object._make(self.session, obj) + return make_object(self.session, obj) - def __del__(self): - """Close the search.""" - self._finalize() + def _initiate(self): + cdef CK_SESSION_HANDLE handle = self.session.handle + cdef CK_ATTRIBUTE *attr_data = self.template.data + cdef CK_ULONG attr_count = self.template.count + cdef CK_RV retval + + with nogil: + retval = self.session.funclist.C_FindObjectsInit(handle, attr_data, attr_count) + assertRV(retval) - def _finalize(self): + def _finalize(self, silent=False): """Finish the operation.""" - cdef CK_SESSION_HANDLE handle = self.session._handle + cdef CK_SESSION_HANDLE handle = self.session.handle cdef CK_RV retval - if self._active: - self._active = False - + if self.active: with nogil: - retval = _funclist.C_FindObjectsFinal(handle) - assertRV(retval) + retval = self.session.funclist.C_FindObjectsFinal(handle) + if not silent: + self._handle_final_retval(retval) + + +cdef class DigestOperation(OperationWithBinaryOutput): + + @staticmethod + cdef DigestOperation setup( + Session session, + MechanismWithParam mech, + CK_ULONG buffer_size + ) with gil: + cdef DigestOperation op = OperationWithBinaryOutput._setup( + DigestOperation, session, mech, buffer_size + ) + return op + + def _initiate(self): + cdef CK_RV retval + with nogil: + retval = self.session.funclist.C_DigestInit(self.session.handle, self.mech.data) + self._operation_aware_assert(retval) + + cdef bytes digest_process_fully(self, CK_BYTE *data, CK_ULONG data_len) with gil: + cdef Session session = self.session + return self.process_fully(session.funclist.C_Digest, data, data_len) + + cdef void update_digest(self, CK_BYTE *data, CK_ULONG data_len): + self.update_no_output(self.session.funclist.C_DigestUpdate, data, data_len) + + cdef void update_digest_with_key(self, CK_OBJECT_HANDLE key): + cdef Session session = self.session + with nogil: + retval = session.funclist.C_DigestKey(session.handle, key) + self._operation_aware_assert(retval) - self.session._operation_lock.release() + def ingest_chunks(self, chunks): + cdef CK_BYTE *data_ptr + cdef CK_ULONG data_len + cdef CK_OBJECT_HANDLE key + + for chunk in chunks: + if not chunk: + continue + if isinstance(chunk, types.Key): + key = chunk.handle + self.update_digest_with_key(key) + else: + data_ptr = chunk + data_len = len(chunk) + self.update_digest(data_ptr, data_len) + + cdef bytes finish(self): + return self.finish_with_output(self.session.funclist.C_DigestFinal) + + def _finalize(self, silent=False): + cdef Session session = self.session + if self.active: + self.active = False + session.operation_lock.release() + self.execute_resizing_output(session.funclist.C_DigestFinal) def merge_templates(default_template, *user_templates): @@ -558,39 +886,89 @@ def merge_templates(default_template, *user_templates): } -class Session(types.Session): +cdef class Session(HasFuncList, types.Session): """Extend Session with implementation.""" + cdef CK_SESSION_HANDLE handle + cdef readonly Token token + """:class:`Token` this session is on.""" + cdef readonly bint rw + """True if this is a read/write session.""" + cdef CK_USER_TYPE _user_type + cdef object operation_lock + + @staticmethod + cdef Session make(Token token, CK_SESSION_HANDLE handle, bint rw, CK_USER_TYPE user_type): + cdef Session session = Session.__new__(Session) + + session.funclist = token.funclist + session.token = token + + session.handle = handle + # Big operation lock prevents other threads from entering/reentering + # operations. If the same thread enters the lock, they will get a + # Cryptoki warning + session.operation_lock = RLock() + + session.rw = rw + session._user_type = user_type + return session + + def __init__(self): + raise TypeError + + def _identity(self): + return Session.__name__, self.token, self.handle + + @property + def user_type(self): + """User type for this session (:class:`pkcs11.constants.UserType`).""" + return UserType(self._user_type) + def close(self): - cdef CK_SESSION_HANDLE handle = self._handle + cdef CK_SESSION_HANDLE handle = self.handle cdef CK_RV retval if self.user_type != UserType.NOBODY: with nogil: - retval = _funclist.C_Logout(handle) + retval = self.funclist.C_Logout(handle) assertRV(retval) with nogil: - retval = _funclist.C_CloseSession(handle) + retval = self.funclist.C_CloseSession(handle) assertRV(retval) def get_objects(self, attrs=None): - return SearchIter(self, attrs or {}) + with SearchIter(self, attrs or {}) as op: + yield from op + + def reaffirm_credentials(self, pin): + cdef CK_UTF8CHAR *pin_data + cdef CK_ULONG pin_length + + pin = pin.encode('utf-8') + pin_data = pin + pin_length = len(pin) + user_type = CKU_CONTEXT_SPECIFIC + + with nogil: + retval = self.funclist.C_Login(self.handle, user_type, pin_data, pin_length) + assertRV(retval) def create_object(self, attrs): template = AttributeList(attrs) - cdef CK_OBJECT_HANDLE handle = self._handle + cdef CK_OBJECT_HANDLE handle = self.handle cdef CK_ATTRIBUTE *attr_data = template.data cdef CK_ULONG attr_count = template.count cdef CK_OBJECT_HANDLE new cdef CK_RV retval with nogil: - retval = _funclist.C_CreateObject(handle, attr_data, attr_count, &new) + retval = self.funclist.C_CreateObject(handle, attr_data, attr_count, &new) assertRV(retval) - return Object._make(self, new) + return make_object(self, new) def create_domain_parameters(self, key_type, attrs, local=False, store=False): @@ -603,7 +981,7 @@ class Session(types.Session): attrs[Attribute.TOKEN] = store if local: - return DomainParameters(self, None, attrs) + return LocalDomainParameters(self, attrs) else: return self.create_object(attrs) @@ -627,7 +1005,7 @@ class Session(types.Session): } attrs = AttributeList(merge_templates(template_, template)) - cdef CK_SESSION_HANDLE handle = self._handle + cdef CK_SESSION_HANDLE handle = self.handle cdef CK_MECHANISM *mech_data = mech.data cdef CK_ATTRIBUTE *attr_data = attrs.data cdef CK_ULONG attr_count = attrs.count @@ -635,10 +1013,10 @@ class Session(types.Session): cdef CK_RV retval with nogil: - retval = _funclist.C_GenerateKey(handle, mech_data, attr_data, attr_count, &obj) + retval = self.funclist.C_GenerateKey(handle, mech_data, attr_data, attr_count, &obj) assertRV(retval) - return Object._make(self, obj) + return make_object(self, obj) def generate_key(self, key_type, key_length=None, id=None, label=None, @@ -689,17 +1067,17 @@ class Session(types.Session): attrs = AttributeList(merge_templates(template_, template)) - cdef CK_SESSION_HANDLE handle = self._handle + cdef CK_SESSION_HANDLE handle = self.handle cdef CK_MECHANISM *mech_data = mech.data cdef CK_ATTRIBUTE *attr_data = attrs.data cdef CK_ULONG attr_count = attrs.count cdef CK_OBJECT_HANDLE key with nogil: - retval = _funclist.C_GenerateKey(handle, mech_data, attr_data, attr_count, &key) + retval = self.funclist.C_GenerateKey(handle, mech_data, attr_data, attr_count, &key) assertRV(retval) - return Object._make(self, key) + return make_object(self, key) def _generate_keypair(self, key_type, key_length=None, id=None, label=None, @@ -764,7 +1142,7 @@ class Session(types.Session): } private_attrs = AttributeList(merge_templates(private_template_, private_template)) - cdef CK_SESSION_HANDLE handle = self._handle + cdef CK_SESSION_HANDLE handle = self.handle cdef CK_MECHANISM *mech_data = mech.data cdef CK_ATTRIBUTE *public_attr_data = public_attrs.data cdef CK_ULONG public_attr_count = public_attrs.count @@ -775,159 +1153,79 @@ class Session(types.Session): cdef CK_RV retval with nogil: - retval = _funclist.C_GenerateKeyPair(handle, mech_data, public_attr_data, public_attr_count, private_attr_data, private_attr_count, &public_key, &private_key) + retval = self.funclist.C_GenerateKeyPair(handle, mech_data, public_attr_data, public_attr_count, private_attr_data, private_attr_count, &public_key, &private_key) assertRV(retval) - return (Object._make(self, public_key), - Object._make(self, private_key)) + return (make_object(self, public_key), + make_object(self, private_key)) def seed_random(self, seed): - cdef CK_SESSION_HANDLE handle = self._handle + cdef CK_SESSION_HANDLE handle = self.handle cdef CK_BYTE *seed_data = seed cdef CK_ULONG seed_len = len(seed) cdef CK_RV retval with nogil: - retval = _funclist.C_SeedRandom(handle, seed_data, seed_len) + retval = self.funclist.C_SeedRandom(handle, seed_data, seed_len) assertRV(retval) def generate_random(self, nbits): - cdef CK_SESSION_HANDLE handle = self._handle + cdef CK_SESSION_HANDLE handle = self.handle cdef CK_ULONG length = nbits // 8 cdef CK_CHAR [:] random = CK_BYTE_buffer(length) cdef CK_RV retval with nogil: - retval = _funclist.C_GenerateRandom(handle, &random[0], length) + retval = self.funclist.C_GenerateRandom(handle, &random[0], length) assertRV(retval) return bytes(random) - def _digest(self, data, mechanism=None, mechanism_param=None): - mech = MechanismWithParam(None, {}, mechanism, mechanism_param) + def __digest_operation(self, mechanism, mechanism_param): + mech = MechanismWithParam( + None, {}, + mechanism, mechanism_param) + return DigestOperation.setup(self, mech, 1024) - cdef CK_SESSION_HANDLE handle = self._handle - cdef CK_MECHANISM *mech_data = mech.data + def _digest(self, data, mechanism=None, mechanism_param=None): cdef CK_BYTE *data_ptr = data cdef CK_ULONG data_len = len(data) - cdef CK_BYTE [:] digest - cdef CK_ULONG length cdef CK_RV retval - with self._operation_lock: - with nogil: - retval = _funclist.C_DigestInit(handle, mech_data) - assertRV(retval) - - with nogil: - # Run once to get the length - retval = _funclist.C_Digest(handle, data_ptr, data_len, NULL, &length) - assertRV(retval) - - digest = CK_BYTE_buffer(length) - - with nogil: - retval = _funclist.C_Digest(handle, data_ptr, data_len, &digest[0], &length) - assertRV(retval) - - return bytes(digest[:length]) + cdef DigestOperation op = self.__digest_operation(mechanism, mechanism_param) + with op: + return op.digest_process_fully(data_ptr, data_len) def _digest_generator(self, data, mechanism=None, mechanism_param=None): - mech = MechanismWithParam(None, {}, mechanism, mechanism_param) - - cdef CK_SESSION_HANDLE handle = self._handle - cdef CK_MECHANISM *mech_data = mech.data - cdef CK_OBJECT_HANDLE key - cdef CK_BYTE *data_ptr - cdef CK_ULONG data_len - cdef CK_BYTE [:] digest - cdef CK_ULONG length - cdef CK_RV retval - - with self._operation_lock: - with nogil: - retval = _funclist.C_DigestInit(handle, mech_data) - assertRV(retval) - - for block in data: - if isinstance(block, types.Key): - key = block._handle - - with nogil: - retval = _funclist.C_DigestKey(handle, key) - assertRV(retval) - else: - data_ptr = block - data_len = len(block) - - with nogil: - retval = _funclist.C_DigestUpdate(handle, data_ptr, data_len) - assertRV(retval) - # Run once to get the length - with nogil: - retval = _funclist.C_DigestFinal(handle, NULL, &length) - assertRV(retval) - - digest = CK_BYTE_buffer(length) - - with nogil: - retval = _funclist.C_DigestFinal(handle, &digest[0], &length) - assertRV(retval) - - return bytes(digest[:length]) + cdef DigestOperation op = self.__digest_operation(mechanism, mechanism_param) + with op: + op.ingest_chunks(data) + return op.finish() -class Object(types.Object): - """Expand Object with an implementation.""" +cdef class ObjectHandleWrapper(HasFuncList): + """ + Class implementing generic operations on PKCS#11 objects. + """ - @classmethod - def _make(cls, *args, **kwargs): - """ - Make an object with the right bases for its class and capabilities. - """ + cdef readonly Session session + cdef readonly CK_OBJECT_HANDLE handle - # Make a version of ourselves we can introspect - self = cls(*args, **kwargs) + @staticmethod + cdef ObjectHandleWrapper wrap(Session session, CK_OBJECT_HANDLE handle): + cdef ObjectHandleWrapper obj = ObjectHandleWrapper.__new__(ObjectHandleWrapper) + obj.funclist = session.funclist + obj.session = session + obj.handle = handle + return obj - try: - # Determine a list of base classes to manufacture our class with - # FIXME: we should really request all of these attributes in - # one go - object_class = self[Attribute.CLASS] - bases = (_CLASS_MAP[object_class],) - - # Build a list of mixins for this new class - for attribute, mixin in ( - (Attribute.ENCRYPT, EncryptMixin), - (Attribute.DECRYPT, DecryptMixin), - (Attribute.SIGN, SignMixin), - (Attribute.VERIFY, VerifyMixin), - (Attribute.WRAP, WrapMixin), - (Attribute.UNWRAP, UnwrapMixin), - (Attribute.DERIVE, DeriveMixin), - ): - try: - if self[attribute]: - bases += (mixin,) - # nFast returns FunctionFailed when you request an attribute - # it doesn't like. - except (AttributeTypeInvalid, FunctionFailed): - pass - - bases += (cls,) - - # Manufacture a class with the right capabilities. - klass = type(bases[0].__name__, bases, {}) - - return klass(*args, **kwargs) - - except KeyError: - return self + def __init__(self): + raise TypeError def __getitem__(self, key): - cdef CK_SESSION_HANDLE handle = self.session._handle - cdef CK_OBJECT_HANDLE obj = self._handle + cdef CK_SESSION_HANDLE handle = self.session.handle + cdef CK_OBJECT_HANDLE obj = self.handle cdef CK_ATTRIBUTE template cdef CK_RV retval @@ -937,7 +1235,7 @@ class Object(types.Object): # Find out the attribute size with nogil: - retval = _funclist.C_GetAttributeValue(handle, obj, &template, 1) + retval = self.funclist.C_GetAttributeValue(handle, obj, &template, 1) if retval == CKR_OK and \ template.ulValueLen == CK_UNAVAILABLE_INFORMATION: # The spec prohibits returning CK_UNAVAILABLE_INFORMATION @@ -957,14 +1255,14 @@ class Object(types.Object): # Request the value with nogil: - retval = _funclist.C_GetAttributeValue(handle, obj, &template, 1) + retval = self.funclist.C_GetAttributeValue(handle, obj, &template, 1) assertRV(retval) return _unpack_attributes(key, value) def __setitem__(self, key, value): - cdef CK_SESSION_HANDLE handle = self.session._handle - cdef CK_OBJECT_HANDLE obj = self._handle + cdef CK_SESSION_HANDLE handle = self.session.handle + cdef CK_OBJECT_HANDLE obj = self.handle cdef CK_ATTRIBUTE template cdef CK_RV retval @@ -975,33 +1273,108 @@ class Object(types.Object): template.ulValueLen = len(value) with nogil: - retval = _funclist.C_SetAttributeValue(handle, obj, &template, 1) + retval = self.funclist.C_SetAttributeValue(handle, obj, &template, 1) + assertRV(retval) + + def destroy(self): + cdef CK_SESSION_HANDLE handle = self.session.handle + cdef CK_OBJECT_HANDLE obj = self.handle + cdef CK_RV retval + + with nogil: + retval = self.session.funclist.C_DestroyObject(handle, obj) assertRV(retval) def copy(self, attrs): template = AttributeList(attrs) - cdef CK_SESSION_HANDLE handle = self.session._handle - cdef CK_OBJECT_HANDLE obj = self._handle + cdef CK_SESSION_HANDLE handle = self.session.handle + cdef CK_OBJECT_HANDLE obj = self.handle cdef CK_ATTRIBUTE *attr_data = template.data cdef CK_ULONG attr_count = template.count - cdef CK_OBJECT_HANDLE new + cdef CK_OBJECT_HANDLE new_obj cdef CK_RV retval with nogil: - retval = _funclist.C_CopyObject(handle, obj, attr_data, attr_count, &new) + retval = self.session.funclist.C_CopyObject(handle, obj, attr_data, attr_count, &new_obj) assertRV(retval) + return new_obj + + def identity(self): + return ObjectHandleWrapper.__name__, self.session, self.handle + + +class Object(types.Object): + """Expand Object with an implementation.""" - return Object._make(self.session, new) + def __init__(self, wrapper: ObjectHandleWrapper): + self.wrapper = wrapper + + def __getitem__(self, item): + return self.wrapper[item] + + def __setitem__(self, key, value): + self.wrapper[key] = value + + @property + def session(self): + return self.wrapper.session + + @property + def handle(self): + return self.wrapper.handle + + def copy(self, attrs): + new_obj = self.wrapper.copy(attrs) + return make_object(self.wrapper.session, new_obj) def destroy(self): - cdef CK_SESSION_HANDLE handle = self.session._handle - cdef CK_OBJECT_HANDLE obj = self._handle - cdef CK_RV retval + self.wrapper.destroy() - with nogil: - retval = _funclist.C_DestroyObject(handle, obj) - assertRV(retval) + def _identity(self): + return Object.__name__, self.wrapper.identity() + + +cdef object make_object(Session session, CK_OBJECT_HANDLE handle) with gil: + """ + Make an object with the right bases for its class and capabilities. + """ + wrapper = ObjectHandleWrapper.wrap(session, handle) + + try: + # Determine a list of base classes to manufacture our class with + # FIXME: we should really request all of these attributes in + # one go + object_class = wrapper[Attribute.CLASS] + bases = (_CLASS_MAP[object_class],) + + # Build a list of mixins for this new class + for attribute, mixin in ( + (Attribute.ENCRYPT, EncryptMixin), + (Attribute.DECRYPT, DecryptMixin), + (Attribute.SIGN, SignMixin), + (Attribute.VERIFY, VerifyMixin), + (Attribute.WRAP, WrapMixin), + (Attribute.UNWRAP, UnwrapMixin), + (Attribute.DERIVE, DeriveMixin), + ): + try: + if wrapper[attribute]: + bases += (mixin,) + # nFast returns FunctionFailed when you request an attribute + # it doesn't like. + except (AttributeTypeInvalid, FunctionFailed): + pass + + bases += (Object,) + + # Manufacture a class with the right capabilities. + klass = type(bases[0].__name__, bases, {}) + + return klass(wrapper) + + except KeyError: + return Object(wrapper) class SecretKey(types.SecretKey): @@ -1016,7 +1389,7 @@ class PrivateKey(types.PrivateKey): pass -class DomainParameters(types.DomainParameters): +class GenerateWithParametersMixin(types.DomainParameters): def generate_keypair(self, id=None, label=None, store=False, capabilities=None, @@ -1077,7 +1450,7 @@ class DomainParameters(types.DomainParameters): } private_attrs = AttributeList(merge_templates(private_template_, private_template)) - cdef CK_SESSION_HANDLE handle = self.session._handle + cdef Session session = self.session cdef CK_MECHANISM *mech_data = mech.data cdef CK_ATTRIBUTE *public_attr_data = public_attrs.data cdef CK_ULONG public_attr_count = public_attrs.count @@ -1088,164 +1461,191 @@ class DomainParameters(types.DomainParameters): cdef CK_RV retval with nogil: - retval = _funclist.C_GenerateKeyPair(handle, mech_data, public_attr_data, public_attr_count, private_attr_data, private_attr_count, &public_key, &private_key) + retval = session.funclist.C_GenerateKeyPair(session.handle, mech_data, public_attr_data, public_attr_count, private_attr_data, private_attr_count, &public_key, &private_key) assertRV(retval) - return (Object._make(self.session, public_key), - Object._make(self.session, private_key)) + return (make_object(session, public_key), + make_object(session, private_key)) + +class LocalDomainParameters(GenerateWithParametersMixin, types.LocalDomainParameters): + pass + +class StoredDomainParameters(GenerateWithParametersMixin): + pass class Certificate(types.Certificate): pass -class EncryptMixin(types.EncryptMixin): - """Expand EncryptMixin with an implementation.""" +cdef class KeyOperation(OperationWithBinaryOutput): + cdef CK_OBJECT_HANDLE key + cdef KeyOperationInit op_init - def _encrypt(self, data, - mechanism=None, mechanism_param=None): + @staticmethod + cdef KeyOperation _common_key_setup( + type cls, + Session session, + MechanismWithParam mech, + CK_OBJECT_HANDLE key, + CK_ULONG buffer_size + ) with gil: + + cdef KeyOperation op = OperationWithBinaryOutput._setup( + cls, session, mech, buffer_size + ) + op.key = key + return op + + def unclean_shutdown(self): """ - Non chunking encrypt. Needed for some mechanisms. + Shutdown implementation for 2.x PKCS#11 modules that don't support shutdown signalling """ - mech = MechanismWithParam( - self.key_type, DEFAULT_ENCRYPT_MECHANISMS, - mechanism, mechanism_param) + raise NotImplementedError - cdef CK_SESSION_HANDLE handle = self.session._handle - cdef CK_MECHANISM *mech_data = mech.data - cdef CK_OBJECT_HANDLE key = self._handle - cdef CK_BYTE *data_ptr = data - cdef CK_ULONG data_len = len(data) - cdef CK_BYTE [:] ciphertext - cdef CK_ULONG length + def _initiate(self): cdef CK_RV retval + with nogil: + retval = self.op_init(self.session.handle, self.mech.data, self.key) + self._operation_aware_assert(retval) - with self.session._operation_lock: - with nogil: - retval = _funclist.C_EncryptInit(handle, mech_data, key) - assertRV(retval) - - # Call to find out the buffer length - with nogil: - retval = _funclist.C_Encrypt(handle, data_ptr, data_len, NULL, &length) - assertRV(retval) - - ciphertext = CK_BYTE_buffer(length) - + def _cancel_operation(self, silent): + cdef CK_RV retval + if self.session.token.slot.cryptoki_version >= (3, 0): + # cancel the operation if still active + # This is a PKCS#11 3.x feature with nogil: - retval = _funclist.C_Encrypt(handle, data_ptr, data_len, &ciphertext[0], &length) - assertRV(retval) + retval = self.op_init(self.session.handle, NULL, self.key) + if retval == CKR_OPERATION_CANCEL_FAILED and not silent: + raise PKCS11Error("Failed to cancel operation") + else: + # No official cancel protocol in v2.x of the standard + # Try the poor man's way by making a hail-mary call to C_XYZFinish() and ignoring the response + self.unclean_shutdown() + + def _finalize(self, silent=False): + if self.active: + self.active = False + self.session.operation_lock.release() + self._cancel_operation(silent) + + +cdef class DataCryptOperation(KeyOperation): + + cdef OperationUpdateWithResult op_update + cdef OperationWithResult op_final + cdef OperationUpdateWithResult op_full + + @staticmethod + cdef DataCryptOperation setup_encrypt( + Session session, + MechanismWithParam mech, + CK_OBJECT_HANDLE key, + CK_ULONG buffer_size + ) with gil: + cdef DataCryptOperation op = KeyOperation._common_key_setup( + DataCryptOperation, session, mech, key, buffer_size + ) + op.op_init = session.funclist.C_EncryptInit + op.op_update = session.funclist.C_EncryptUpdate + op.op_final = session.funclist.C_EncryptFinal + op.op_full = session.funclist.C_Encrypt + return op + + @staticmethod + cdef DataCryptOperation setup_decrypt( + Session session, + MechanismWithParam mech, + CK_OBJECT_HANDLE key, + CK_ULONG buffer_size + ) with gil: + cdef DataCryptOperation op = KeyOperation._common_key_setup( + DataCryptOperation, session, mech, key, buffer_size + ) + op.op_init = session.funclist.C_DecryptInit + op.op_update = session.funclist.C_DecryptUpdate + op.op_final = session.funclist.C_DecryptFinal + op.op_full = session.funclist.C_Decrypt + return op + + cdef bytes crypt_process_fully(self, CK_BYTE *data, CK_ULONG data_len) with gil: + return self.process_fully(self.op_full, data, data_len) + + cdef bytes finish(self) with gil: + return self.finish_with_output(self.op_final) + + def unclean_shutdown(self): + self.execute_resizing_output(self.op_final) + + def update_chunks(self, chunks): + cdef CK_BYTE *data_ptr + cdef CK_ULONG data_len - return bytes(ciphertext[:length]) + for chunk in chunks: + if not chunk: + continue + data_ptr = chunk + data_len = len(chunk) + yield self.update_with_result(self.op_update, data_ptr, data_len) - def _encrypt_generator(self, data, - mechanism=None, mechanism_param=None, - buffer_size=8192): - """ - Do chunked encryption. +class EncryptMixin(types.EncryptMixin): + """Expand EncryptMixin with an implementation.""" - Failing to consume the generator will raise GeneratorExit when it - garbage collects. This will release the lock, but you'll still be - in the middle of an operation, and all future operations will raise - OperationActive, see tests/test_iterators.py:test_close_iterators(). + def __encrypt_operation(self, mechanism, mechanism_param, buffer_size): - FIXME: cancel the operation when we exit the generator early. - """ mech = MechanismWithParam( self.key_type, DEFAULT_ENCRYPT_MECHANISMS, mechanism, mechanism_param) - cdef CK_SESSION_HANDLE handle = self.session._handle - cdef CK_MECHANISM *mech_data = mech.data - cdef CK_OBJECT_HANDLE key = self._handle - cdef CK_BYTE *data_ptr - cdef CK_ULONG data_len - cdef CK_ULONG length - cdef CK_BYTE [:] part_out = CK_BYTE_buffer(buffer_size) - cdef CK_RV retval - - with self.session._operation_lock: - with nogil: - retval = _funclist.C_EncryptInit(handle, mech_data, key) - assertRV(retval) - - for part_in in data: - if not part_in: - continue + return DataCryptOperation.setup_encrypt(self.session, mech, self.handle, buffer_size) - data_ptr = part_in - data_len = len(part_in) - length = buffer_size - - with nogil: - retval = _funclist.C_EncryptUpdate(handle, data_ptr, data_len, &part_out[0], &length) - assertRV(retval) - - yield bytes(part_out[:length]) + def _encrypt(self, data, mechanism=None, mechanism_param=None, buffer_size=8192): + """ + Non chunking encrypt. Needed for some mechanisms. + """ + cdef CK_BYTE *data_ptr = data + cdef CK_ULONG data_len = len(data) - # Finalize - # We assume the buffer is much bigger than the block size - length = buffer_size + cdef DataCryptOperation op = self.__encrypt_operation(mechanism, mechanism_param, buffer_size) + with op: + return op.crypt_process_fully(data, data_len) - with nogil: - retval = _funclist.C_EncryptFinal(handle, &part_out[0], &length) - assertRV(retval) - yield bytes(part_out[:length]) + def _encrypt_generator(self, data, + mechanism=None, mechanism_param=None, + buffer_size=8192): + """ + Do chunked encryption. + """ + cdef DataCryptOperation op = self.__encrypt_operation(mechanism, mechanism_param, buffer_size) + with op: + yield from op.update_chunks(data) + yield op.finish() class DecryptMixin(types.DecryptMixin): """Expand DecryptMixin with an implementation.""" - def _decrypt(self, data, - mechanism=None, mechanism_param=None, pin=None): - """Non chunking decrypt.""" + def __decrypt_operation(self, mechanism, mechanism_param, buffer_size): + mech = MechanismWithParam( self.key_type, DEFAULT_ENCRYPT_MECHANISMS, mechanism, mechanism_param) - cdef CK_SESSION_HANDLE handle = self.session._handle - cdef CK_MECHANISM *mech_data = mech.data - cdef CK_OBJECT_HANDLE key = self._handle + return DataCryptOperation.setup_decrypt(self.session, mech, self.handle, buffer_size) + + def _decrypt(self, data, mechanism=None, mechanism_param=None, pin=None, buffer_size=8192): + """Non chunking decrypt.""" + cdef Session session = self.session cdef CK_BYTE *data_ptr = data cdef CK_ULONG data_len = len(data) - cdef CK_BYTE [:] plaintext - cdef CK_ULONG length - cdef CK_USER_TYPE user_type - cdef CK_UTF8CHAR *pin_data - cdef CK_ULONG pin_length - cdef CK_RV retval - - if pin is not None: - pin = pin.encode('utf-8') - pin_data = pin - pin_length = len(pin) - user_type = CKU_CONTEXT_SPECIFIC - - with self.session._operation_lock: - with nogil: - retval = _funclist.C_DecryptInit(handle, mech_data, key) - assertRV(retval) - # Log in if pin provided + cdef DataCryptOperation op = self.__decrypt_operation(mechanism, mechanism_param, buffer_size) + with op: if pin is not None: - with nogil: - retval = _funclist.C_Login(handle, user_type, pin_data, pin_length) - assertRV(retval) - - # Call to find out the buffer length - with nogil: - retval = _funclist.C_Decrypt(handle, data_ptr, data_len, NULL, &length) - assertRV(retval) - - plaintext = CK_BYTE_buffer(length) - - with nogil: - retval = _funclist.C_Decrypt(handle, data_ptr, data_len, &plaintext[0], &length) - assertRV(retval) - - return bytes(plaintext[:length]) + session.reaffirm_credentials(pin) + return op.crypt_process_fully(data, data_len) def _decrypt_generator(self, data, @@ -1253,249 +1653,168 @@ class DecryptMixin(types.DecryptMixin): buffer_size=8192): """ Chunking decrypt. - - Failing to consume the generator will raise GeneratorExit when it - garbage collects. This will release the lock, but you'll still be - in the middle of an operation, and all future operations will raise - OperationActive, see tests/test_iterators.py:test_close_iterators(). - - FIXME: cancel the operation when we exit the generator early. """ - mech = MechanismWithParam( - self.key_type, DEFAULT_ENCRYPT_MECHANISMS, - mechanism, mechanism_param) + cdef Session session = self.session - cdef CK_SESSION_HANDLE handle = self.session._handle - cdef CK_MECHANISM *mech_data = mech.data - cdef CK_OBJECT_HANDLE key = self._handle - cdef CK_BYTE *data_ptr - cdef CK_ULONG data_len - cdef CK_ULONG length - cdef CK_BYTE [:] part_out = CK_BYTE_buffer(buffer_size) - cdef CK_USER_TYPE user_type - cdef CK_UTF8CHAR *pin_data - cdef CK_ULONG pin_length - cdef CK_RV retval + cdef DataCryptOperation op = self.__decrypt_operation(mechanism, mechanism_param, buffer_size) + with op: + if pin is not None: + session.reaffirm_credentials(pin) + yield from op.update_chunks(data) + yield op.finish() - if pin is not None: - pin = pin.encode('utf-8') - pin_data = pin - pin_length = len(pin) - user_type = CKU_CONTEXT_SPECIFIC - with self.session._operation_lock: - with nogil: - retval = _funclist.C_DecryptInit(handle, mech_data, key) - assertRV(retval) +cdef class SignOrVerifyOperation(KeyOperation): + cdef OperationUpdate op_update - # Log in if pin provided - if pin is not None: - with nogil: - retval = _funclist.C_Login(handle, user_type, pin_data, pin_length) - assertRV(retval) + def ingest_chunks(self, chunks): + cdef Session session = self.session + cdef CK_BYTE *data_ptr + cdef CK_ULONG data_len - for part_in in data: - if not part_in: - continue + for chunk in chunks: + if not chunk: + continue + data_ptr = chunk + data_len = len(chunk) + self.update_no_output(self.op_update, data_ptr, data_len) - data_ptr = part_in - data_len = len(part_in) - length = buffer_size - with nogil: - retval = _funclist.C_DecryptUpdate(handle, data_ptr, data_len, &part_out[0], &length) - assertRV(retval) +cdef class DataSignOperation(SignOrVerifyOperation): - yield bytes(part_out[:length]) + @staticmethod + cdef DataSignOperation setup( + Session session, + MechanismWithParam mech, + CK_OBJECT_HANDLE key, + CK_ULONG buffer_size + ) with gil: + cdef DataSignOperation op = KeyOperation._common_key_setup( + DataSignOperation, session, mech, key, buffer_size + ) + op.op_init = session.funclist.C_SignInit + op.op_update = session.funclist.C_SignUpdate + return op - # Finalize - # We assume the buffer is much bigger than the block size - length = buffer_size + cdef bytes sign_process_fully(self, CK_BYTE *data, CK_ULONG data_len) with gil: + cdef Session session = self.session + return self.process_fully(session.funclist.C_Sign, data, data_len) - with nogil: - retval = _funclist.C_DecryptFinal(handle, &part_out[0], &length) - assertRV(retval) + cdef bytes finish(self) with gil: + cdef Session session = self.session + return self.finish_with_output(session.funclist.C_SignFinal) - yield bytes(part_out[:length]) + def unclean_shutdown(self): + cdef Session session = self.session + self.execute_resizing_output(session.funclist.C_SignFinal) class SignMixin(types.SignMixin): """Expand SignMixin with an implementation.""" - def _sign(self, data, - mechanism=None, mechanism_param=None, pin=None): - + def __sign_operation(self, mechanism, mechanism_param, buffer_size): mech = MechanismWithParam( self.key_type, DEFAULT_SIGN_MECHANISMS, mechanism, mechanism_param) + return DataSignOperation.setup(self.session, mech, self.handle, buffer_size) - cdef CK_SESSION_HANDLE handle = self.session._handle - cdef CK_MECHANISM *mech_data = mech.data - cdef CK_OBJECT_HANDLE key = self._handle + def _sign(self, data, + mechanism=None, mechanism_param=None, pin=None, buffer_size=8192): + cdef Session session = self.session cdef CK_BYTE *data_ptr = data cdef CK_ULONG data_len = len(data) - cdef CK_BYTE [:] signature - cdef CK_ULONG length - cdef CK_USER_TYPE user_type - cdef CK_UTF8CHAR *pin_data - cdef CK_ULONG pin_length - cdef CK_RV retval - - if pin is not None: - pin = pin.encode('utf-8') - pin_data = pin - pin_length = len(pin) - user_type = CKU_CONTEXT_SPECIFIC - - with self.session._operation_lock: - with nogil: - retval = _funclist.C_SignInit(handle, mech_data, key) - assertRV(retval) + cdef DataSignOperation op = self.__sign_operation(mechanism, mechanism_param, buffer_size) - # Log in if pin provided + with op: if pin is not None: - with nogil: - retval = _funclist.C_Login(handle, user_type, pin_data, pin_length) - assertRV(retval) - - # Call to find out the buffer length - with nogil: - retval = _funclist.C_Sign(handle, data_ptr, data_len, NULL, &length) - assertRV(retval) - - signature = CK_BYTE_buffer(length) - - with nogil: - retval = _funclist.C_Sign(handle, data_ptr, data_len, &signature[0], &length) - assertRV(retval) - - return bytes(signature[:length]) + session.reaffirm_credentials(pin) + return op.sign_process_fully(data, data_len) def _sign_generator(self, data, - mechanism=None, mechanism_param=None, pin=None): - - mech = MechanismWithParam( - self.key_type, DEFAULT_SIGN_MECHANISMS, - mechanism, mechanism_param) - - cdef CK_SESSION_HANDLE handle = self.session._handle - cdef CK_MECHANISM *mech_data = mech.data - cdef CK_OBJECT_HANDLE key = self._handle - cdef CK_BYTE *data_ptr - cdef CK_ULONG data_len - cdef CK_BYTE [:] signature - cdef CK_ULONG length - cdef CK_USER_TYPE user_type - cdef CK_UTF8CHAR *pin_data - cdef CK_ULONG pin_length - cdef CK_RV retval - - if pin is not None: - pin = pin.encode('utf-8') - pin_data = pin - pin_length = len(pin) - user_type = CKU_CONTEXT_SPECIFIC + mechanism=None, mechanism_param=None, pin=None, buffer_size=8192): - with self.session._operation_lock: - with nogil: - retval = _funclist.C_SignInit(handle, mech_data, key) - assertRV(retval) - - # Log in if pin provided + cdef Session session = self.session + cdef DataSignOperation op = self.__sign_operation(mechanism, mechanism_param, buffer_size) + with op: if pin is not None: - with nogil: - retval = _funclist.C_Login(handle, user_type, pin_data, pin_length) - assertRV(retval) - - for part_in in data: - if not part_in: - continue - - data_ptr = part_in - data_len = len(part_in) - - with nogil: - retval = _funclist.C_SignUpdate(handle, data_ptr, data_len) - assertRV(retval) - - # Finalize - # Call to find out the buffer length - with nogil: - retval = _funclist.C_SignFinal(handle, NULL, &length) - assertRV(retval) - - signature = CK_BYTE_buffer(length) + session.reaffirm_credentials(pin) + op.ingest_chunks(data) + return op.finish() + + +cdef class DataVerifyOperation(SignOrVerifyOperation): + + @staticmethod + cdef DataVerifyOperation setup( + Session session, + MechanismWithParam mech, + CK_OBJECT_HANDLE key + ) with gil: + cdef DataVerifyOperation op = KeyOperation._common_key_setup( + DataVerifyOperation, session, mech, key, 0 + ) + op.op_init = session.funclist.C_VerifyInit + op.op_update = session.funclist.C_VerifyUpdate + return op + + cdef verify_process_fully( + self, + CK_BYTE *data, + CK_ULONG data_len, + CK_BYTE *sig, + CK_ULONG sig_len, + ) with gil: + cdef Session session = self.session + cdef CK_RV retval + with nogil: + retval = session.funclist.C_Verify(session.handle, data, data_len, sig, sig_len) + self._handle_final_retval(retval) - with nogil: - retval = _funclist.C_SignFinal(handle, &signature[0], &length) - assertRV(retval) + cdef finish(self, CK_BYTE *sig, CK_ULONG sig_len) with gil: + cdef Session session = self.session + cdef CK_RV retval + with nogil: + retval = session.funclist.C_VerifyFinal(session.handle, sig, sig_len) + self._handle_final_retval(retval) - return bytes(signature[:length]) + def unclean_shutdown(self): + cdef Session session = self.session + cdef CK_BYTE dummy = 0 + with nogil: + session.funclist.C_VerifyFinal(session.handle, &dummy, 0) class VerifyMixin(types.VerifyMixin): """Expand VerifyMixin with an implementation.""" - def _verify(self, data, signature, - mechanism=None, mechanism_param=None): - + def __verify_operation(self, mechanism, mechanism_param): mech = MechanismWithParam( self.key_type, DEFAULT_SIGN_MECHANISMS, mechanism, mechanism_param) + return DataVerifyOperation.setup(self.session, mech, self.handle) + + def _verify(self, data, signature, + mechanism=None, mechanism_param=None): - cdef CK_SESSION_HANDLE handle = self.session._handle - cdef CK_MECHANISM *mech_data = mech.data - cdef CK_OBJECT_HANDLE key = self._handle cdef CK_BYTE *data_ptr = data cdef CK_ULONG data_len = len(data) cdef CK_BYTE *sig_ptr = signature cdef CK_ULONG sig_len = len(signature) - cdef CK_RV retval + cdef DataVerifyOperation op = self.__verify_operation(mechanism, mechanism_param) - with self.session._operation_lock: - with nogil: - retval = _funclist.C_VerifyInit(handle, mech_data, key) - assertRV(retval) - - with nogil: - retval = _funclist.C_Verify(handle, data_ptr, data_len, sig_ptr, sig_len) - assertRV(retval) + with op: + op.verify_process_fully(data_ptr, data_len, sig_ptr, sig_len) def _verify_generator(self, data, signature, mechanism=None, mechanism_param=None): - mech = MechanismWithParam( - self.key_type, DEFAULT_SIGN_MECHANISMS, - mechanism, mechanism_param) - - cdef CK_SESSION_HANDLE handle = self.session._handle - cdef CK_MECHANISM *mech_data = mech.data - cdef CK_OBJECT_HANDLE key = self._handle - cdef CK_BYTE *data_ptr - cdef CK_ULONG data_len cdef CK_BYTE *sig_ptr = signature cdef CK_ULONG sig_len = len(signature) - cdef CK_RV retval + cdef DataVerifyOperation op = self.__verify_operation(mechanism, mechanism_param) - with self.session._operation_lock: - with nogil: - retval = _funclist.C_VerifyInit(handle, mech_data, key) - assertRV(retval) - - for part_in in data: - if not part_in: - continue - - data_ptr = part_in - data_len = len(part_in) - - with nogil: - retval = _funclist.C_VerifyUpdate(handle, data_ptr, data_len) - assertRV(retval) - - with nogil: - retval = _funclist.C_VerifyFinal(handle, sig_ptr, sig_len) - assertRV(retval) + with op: + op.ingest_chunks(data) + return op.finish(sig_ptr, sig_len) class WrapMixin(types.WrapMixin): @@ -1511,22 +1830,22 @@ class WrapMixin(types.WrapMixin): self.key_type, DEFAULT_WRAP_MECHANISMS, mechanism, mechanism_param) - cdef CK_SESSION_HANDLE handle = self.session._handle + cdef Session session = self.session cdef CK_MECHANISM *mech_data = mech.data - cdef CK_OBJECT_HANDLE wrapping_key = self._handle - cdef CK_OBJECT_HANDLE key_to_wrap = key._handle + cdef CK_OBJECT_HANDLE wrapping_key = self.handle + cdef CK_OBJECT_HANDLE key_to_wrap = key.handle cdef CK_ULONG length cdef CK_RV retval # Find out how many bytes we need to allocate with nogil: - retval = _funclist.C_WrapKey(handle, mech_data, wrapping_key, key_to_wrap, NULL, &length) + retval = session.funclist.C_WrapKey(session.handle, mech_data, wrapping_key, key_to_wrap, NULL, &length) assertRV(retval) cdef CK_BYTE [:] data = CK_BYTE_buffer(length) with nogil: - retval = _funclist.C_WrapKey(handle, mech_data, wrapping_key, key_to_wrap, &data[0], &length) + retval = session.funclist.C_WrapKey(session.handle, mech_data, wrapping_key, key_to_wrap, &data[0], &length) assertRV(retval) return bytes(data[:length]) @@ -1576,9 +1895,9 @@ class UnwrapMixin(types.UnwrapMixin): } attrs = AttributeList(merge_templates(template_, template)) - cdef CK_SESSION_HANDLE handle = self.session._handle + cdef Session session = self.session cdef CK_MECHANISM *mech_data = mech.data - cdef CK_OBJECT_HANDLE unwrapping_key = self._handle + cdef CK_OBJECT_HANDLE unwrapping_key = self.handle cdef CK_BYTE *wrapped_key_ptr = key_data cdef CK_ULONG wrapped_key_len = len(key_data) cdef CK_ATTRIBUTE *attr_data = attrs.data @@ -1587,10 +1906,10 @@ class UnwrapMixin(types.UnwrapMixin): cdef CK_RV retval with nogil: - retval = _funclist.C_UnwrapKey(handle, mech_data, unwrapping_key, wrapped_key_ptr, wrapped_key_len, attr_data, attr_count, &key) + retval = session.funclist.C_UnwrapKey(session.handle, mech_data, unwrapping_key, wrapped_key_ptr, wrapped_key_len, attr_data, attr_count, &key) assertRV(retval) - return Object._make(self.session, key) + return make_object(session, key) class DeriveMixin(types.DeriveMixin): @@ -1640,26 +1959,26 @@ class DeriveMixin(types.DeriveMixin): } attrs = AttributeList(merge_templates(template_, template)) - cdef CK_SESSION_HANDLE handle = self.session._handle + cdef Session session = self.session cdef CK_MECHANISM *mech_data = mech.data - cdef CK_OBJECT_HANDLE src_key = self._handle + cdef CK_OBJECT_HANDLE src_key = self.handle cdef CK_ATTRIBUTE *attr_data = attrs.data cdef CK_ULONG attr_count = attrs.count cdef CK_OBJECT_HANDLE key cdef CK_RV retval with nogil: - retval = _funclist.C_DeriveKey(handle, mech_data, src_key, attr_data, attr_count, &key) + retval = session.funclist.C_DeriveKey(session.handle, mech_data, src_key, attr_data, attr_count, &key) assertRV(retval) - return Object._make(self.session, key) + return make_object(session, key) _CLASS_MAP = { ObjectClass.SECRET_KEY: SecretKey, ObjectClass.PUBLIC_KEY: PublicKey, ObjectClass.PRIVATE_KEY: PrivateKey, - ObjectClass.DOMAIN_PARAMETERS: DomainParameters, + ObjectClass.DOMAIN_PARAMETERS: StoredDomainParameters, ObjectClass.CERTIFICATE: Certificate, } @@ -1672,8 +1991,7 @@ cdef extern from "../extern/load_module.c": int p11_close(P11_HANDLE* handle) - -cdef class lib: +cdef class lib(HasFuncList): """ Main entry point. @@ -1681,11 +1999,12 @@ cdef class lib: pkcs11.types. """ - cdef public str so - cdef public str manufacturer_id - cdef public str library_description - cdef public tuple cryptoki_version - cdef public tuple library_version + cdef readonly str so + cdef readonly str manufacturer_id + cdef readonly str library_description + cdef readonly tuple cryptoki_version + cdef readonly tuple library_version + cdef readonly bint initialized cdef P11_HANDLE *_p11_handle cdef _load_pkcs11_lib(self, so) with gil: @@ -1718,24 +2037,46 @@ cdef class lib: populate_function_list = handle.get_function_list_ptr self._p11_handle = handle - assertRV(populate_function_list(&_funclist)) + assertRV(populate_function_list(&self.funclist)) def __cinit__(self, so): cdef CK_RV retval + self._p11_handle = NULL self._load_pkcs11_lib(so) + self.initialized = False # at this point, _funclist contains all function pointers to the library - with nogil: - retval = _funclist.C_Initialize(NULL) - assertRV(retval) + + cpdef initialize(self): + cdef CK_RV retval + if self.funclist != NULL and not self.initialized: + with nogil: + retval = self.funclist.C_Initialize(NULL) + assertRV(retval) + self.initialized = True + + cpdef finalize(self): + cdef CK_RV retval + if self.funclist != NULL and self.initialized: + with nogil: + retval = self.funclist.C_Finalize(NULL) + assertRV(retval) + self.initialized = False + + def reinitialize(self): + if self.funclist != NULL: + self.finalize() + self.initialize() def __init__(self, so): self.so = so cdef CK_INFO info cdef CK_RV retval + self.initialize() + with nogil: - retval = _funclist.C_GetInfo(&info) + retval = self.funclist.C_GetInfo(&info) assertRV(retval) manufacturerID = info.manufacturerID[:sizeof(info.manufacturerID)] @@ -1768,7 +2109,7 @@ cdef class lib: cdef CK_RV retval with nogil: - retval = _funclist.C_GetSlotList(present, NULL, &count) + retval = self.funclist.C_GetSlotList(present, NULL, &count) assertRV(retval) if count == 0: @@ -1777,7 +2118,7 @@ cdef class lib: cdef CK_SLOT_ID [:] slot_list = CK_ULONG_buffer(count) with nogil: - retval = _funclist.C_GetSlotList(present, &slot_list[0], &count) + retval = self.funclist.C_GetSlotList(present, &slot_list[0], &count) assertRV(retval) cdef CK_SLOT_ID slot_id @@ -1787,15 +2128,11 @@ cdef class lib: for slot_id in slot_list: with nogil: - retval = _funclist.C_GetSlotInfo(slot_id, &info) + retval = self.funclist.C_GetSlotInfo(slot_id, &info) assertRV(retval) - slotDescription = info.slotDescription[:sizeof(info.slotDescription)] - manufacturerID = info.manufacturerID[:sizeof(info.manufacturerID)] - slots.append( - Slot(self, slot_id, slotDescription, manufacturerID, - info.hardwareVersion, info.firmwareVersion, info.flags) + Slot.make(self.funclist, slot_id, info, self.cryptoki_version) ) return slots @@ -1863,13 +2200,13 @@ cdef class lib: flag |= CKF_DONT_BLOCK with nogil: - retval = _funclist.C_WaitForSlotEvent(flag, &slot_id, NULL) + retval = self.funclist.C_WaitForSlotEvent(flag, &slot_id, NULL) assertRV(retval) cdef CK_SLOT_INFO info with nogil: - retval = _funclist.C_GetSlotInfo(slot_id, &info) + retval = self.funclist.C_GetSlotInfo(slot_id, &info) assertRV(retval) slotDescription = info.slotDescription[:sizeof(info.slotDescription)] @@ -1878,18 +2215,8 @@ cdef class lib: return Slot(self, slot_id, slotDescription, manufacturerID, info.hardwareVersion, info.firmwareVersion, info.flags) - def reinitialize(self): - cdef CK_RV retval - if _funclist != NULL: - with nogil: - retval = _funclist.C_Finalize(NULL) - assertRV(retval) - with nogil: - retval = _funclist.C_Initialize(NULL) - assertRV(retval) - def __dealloc__(self): - if _funclist != NULL: - with nogil: - _funclist.C_Finalize(NULL) - p11_close(self._p11_handle) + self.finalize() + self.funclist = NULL + if self._p11_handle != NULL: + p11_close(self._p11_handle) diff --git a/pkcs11/exceptions.py b/pkcs11/exceptions.py index ca6e9f8..0227460 100644 --- a/pkcs11/exceptions.py +++ b/pkcs11/exceptions.py @@ -10,12 +10,6 @@ class PKCS11Error(RuntimeError): """ -class AlreadyInitialized(PKCS11Error): - """ - pkcs11 was already initialized with another library. - """ - - class AnotherUserAlreadyLoggedIn(PKCS11Error): pass diff --git a/pkcs11/types.py b/pkcs11/types.py index 529ecf3..4202b33 100644 --- a/pkcs11/types.py +++ b/pkcs11/types.py @@ -5,20 +5,12 @@ """ from binascii import hexlify -from threading import RLock - -try: - from functools import cached_property -except ImportError: - from cached_property import cached_property +from functools import cached_property from .constants import ( Attribute, MechanismFlag, ObjectClass, - SlotFlag, - TokenFlag, - UserType, ) from .exceptions import ( ArgumentsBad, @@ -34,6 +26,19 @@ """Indicate the pin should be supplied via an external mechanism (e.g. pin pad)""" +class IdentifiedBy: + __slots__ = () + + def _identity(self): + raise NotImplementedError() + + def __eq__(self, other): + return isinstance(other, IdentifiedBy) and self._identity() == other._identity() + + def __hash__(self): + return hash(self._identity()) + + def _CK_UTF8CHAR_to_str(data): """Convert CK_UTF8CHAR to string.""" return data.rstrip(b"\0").decode("utf-8").rstrip() @@ -85,7 +90,7 @@ def __repr__(self): ) -class Slot: +class Slot(IdentifiedBy): """ A PKCS#11 device slot. @@ -94,31 +99,22 @@ class Slot: a physical or software :class:`Token` installed. """ - def __init__( - self, - lib, - slot_id, - slotDescription=None, - manufacturerID=None, - hardwareVersion=None, - firmwareVersion=None, - flags=None, - **kwargs, - ): - self._lib = lib # Hold a reference to the lib to prevent gc - - self.slot_id = slot_id - """Slot identifier (opaque).""" - self.slot_description = _CK_UTF8CHAR_to_str(slotDescription) - """Slot name (:class:`str`).""" - self.manufacturer_id = _CK_UTF8CHAR_to_str(manufacturerID) - """Slot/device manufacturer's name (:class:`str`).""" - self.hardware_version = _CK_VERSION_to_tuple(hardwareVersion) + __slots__ = () + + @property + def flags(self): + """Capabilities of this slot (:class:`SlotFlag`).""" + raise NotImplementedError() + + @property + def hardware_version(self): """Hardware version (:class:`tuple`).""" - self.firmware_version = _CK_VERSION_to_tuple(firmwareVersion) + raise NotImplementedError() + + @property + def firmware_version(self): """Firmware version (:class:`tuple`).""" - self.flags = SlotFlag(flags) - """Capabilities of this slot (:class:`SlotFlag`).""" + raise NotImplementedError() def get_token(self): """ @@ -145,27 +141,8 @@ def get_mechanism_info(self, mechanism): """ raise NotImplementedError() - def __eq__(self, other): - return self.slot_id == other.slot_id - - def __str__(self): - return "\n".join( - ( - "Slot Description: %s" % self.slot_description, - "Manufacturer ID: %s" % self.manufacturer_id, - "Hardware Version: %s.%s" % self.hardware_version, - "Firmware Version: %s.%s" % self.firmware_version, - "Flags: %s" % self.flags, - ) - ) - - def __repr__(self): - return "<{klass} (slotID={slot_id} flags={flags})>".format( - klass=type(self).__name__, slot_id=self.slot_id, flags=str(self.flags) - ) - -class Token: +class Token(IdentifiedBy): """ A PKCS#11 token. @@ -173,37 +150,22 @@ class Token: token, depending on your PKCS#11 library. """ - def __init__( - self, - slot, - label=None, - serialNumber=None, - model=None, - manufacturerID=None, - hardwareVersion=None, - firmwareVersion=None, - flags=None, - **kwargs, - ): - self.slot = slot - """The :class:`Slot` this token is installed in.""" - self.label = _CK_UTF8CHAR_to_str(label) - """Label of this token (:class:`str`).""" - self.serial = serialNumber.rstrip() - """Serial number of this token (:class:`bytes`).""" - self.manufacturer_id = _CK_UTF8CHAR_to_str(manufacturerID) - """Manufacturer ID.""" - self.model = _CK_UTF8CHAR_to_str(model) - """Model name.""" - self.hardware_version = _CK_VERSION_to_tuple(hardwareVersion) + __slots__ = () + + @property + def flags(self): + """Capabilities of this token (:class:`TokenFlag`).""" + raise NotImplementedError() + + @property + def hardware_version(self): """Hardware version (:class:`tuple`).""" - self.firmware_version = _CK_VERSION_to_tuple(firmwareVersion) - """Firmware version (:class:`tuple`).""" - self.flags = TokenFlag(flags) - """Capabilities of this token (:class:`pkcs11.flags.TokenFlag`).""" + raise NotImplementedError() - def __eq__(self, other): - return self.slot == other.slot + @property + def firmware_version(self): + """Firmware version (:class:`tuple`).""" + raise NotImplementedError() def open(self, rw=False, user_pin=None, so_pin=None, user_type=None): """ @@ -231,16 +193,8 @@ def open(self, rw=False, user_pin=None, so_pin=None, user_type=None): """ raise NotImplementedError() - def __str__(self): - return self.label - def __repr__(self): - return "<{klass} (label='{label}' serial={serial} flags={flags})>".format( - klass=type(self).__name__, label=self.label, serial=self.serial, flags=str(self.flags) - ) - - -class Session: +class Session(IdentifiedBy): """ A PKCS#11 :class:`Token` session. @@ -251,26 +205,7 @@ class Session: context manager or closed with :meth:`close`. """ - def __init__(self, token, handle, rw=False, user_type=UserType.NOBODY): - self.token = token - """:class:`Token` this session is on.""" - - self._handle = handle - # Big operation lock prevents other threads from entering/reentering - # operations. If the same thread enters the lock, they will get a - # Cryptoki warning - self._operation_lock = RLock() - - self.rw = rw - """True if this is a read/write session.""" - self.user_type = user_type - """User type for this session (:class:`pkcs11.constants.UserType`).""" - - def __eq__(self, other): - return self.token == other.token and self._handle == other._handle - - def __hash__(self): - return hash(self._handle) + __slots__ = () def __enter__(self): return self @@ -282,6 +217,9 @@ def close(self): """Close the session.""" raise NotImplementedError() + def reaffirm_credentials(self, pin): + raise NotImplementedError() + def get_key(self, object_class=None, key_type=None, label=None, id=None): """ Search for a key with any of `key_type`, `label` and/or `id`. @@ -318,22 +256,17 @@ def get_key(self, object_class=None, key_type=None, label=None, id=None): attrs[Attribute.ID] = id iterator = self.get_objects(attrs) + try: + key = next(iterator) + except StopIteration as ex: + raise NoSuchKey("No key matching %s" % attrs) from ex try: - try: - key = next(iterator) - except StopIteration as ex: - raise NoSuchKey("No key matching %s" % attrs) from ex - - try: - next(iterator) - raise MultipleObjectsReturned("More than 1 key matches %s" % attrs) - except StopIteration: - return key - finally: - # Force finalizing SearchIter rather than waiting for garbage - # collection, so that we release the operation lock. - iterator._finalize() + next(iterator) + raise MultipleObjectsReturned("More than 1 key matches %s" % attrs) + except StopIteration: + pass + return key def get_objects(self, attrs=None): """ @@ -436,7 +369,7 @@ def generate_domain_parameters( but be aware they may be difficult to retrieve. :param KeyType key_type: Key type these parameters are for - :param int params_length: Size of the parameters (e.g. prime length) + :param int param_length: Size of the parameters (e.g. prime length) in bits. :param store: Store these parameters in the HSM :param Mechanism mechanism: Optional generation mechanism (or default) @@ -521,6 +454,9 @@ def generate_keypair(self, key_type, key_length=None, **kwargs): else: return self._generate_keypair(key_type, key_length=key_length, **kwargs) + def _generate_keypair(self, key_type, key_length=None, **kwargs): + raise NotImplementedError() + def seed_random(self, seed): """ Mix additional seed material into the RNG (if supported). @@ -567,8 +503,14 @@ def digest(self, data, **kwargs): return self._digest_generator(data, **kwargs) + def _digest(self, data, mechanism=None, mechanism_param=None): + raise NotImplementedError() + + def _digest_generator(self, data, mechanism=None, mechanism_param=None): + raise NotImplementedError() -class Object: + +class Object(IdentifiedBy): """ A PKCS#11 object residing on a :class:`Token`. @@ -583,16 +525,19 @@ class Object: object_class = None """:class:`pkcs11.constants.ObjectClass` of this Object.""" - def __init__(self, session, handle): - self.session = session - """:class:`Session` this object is valid for.""" - self._handle = handle + @property + def session(self): + raise NotImplementedError() - def __eq__(self, other): - return self.session == other.session and self._handle == other._handle + @property + def handle(self): + raise NotImplementedError() - def __hash__(self): - return hash((self.session, self._handle)) + def __getitem__(self, key): + raise NotImplementedError() + + def __setitem__(self, key, value): + raise NotImplementedError() def copy(self, attrs): """ @@ -637,25 +582,6 @@ class DomainParameters(Object): in DSA and Diffie-Hellman. """ - def __init__(self, session, handle, params=None): - super().__init__(session, handle) - self.params = params - - def __getitem__(self, key): - if self._handle is None: - try: - return self.params[key] - except KeyError as ex: - raise AttributeTypeInvalid from ex - else: - return super().__getitem__(key) - - def __setitem__(self, key, value): - if self._handle is None: - self.params[key] = value - else: - super().__setitem__(key, value) - @cached_property def key_type(self): """ @@ -694,9 +620,44 @@ def generate_keypair( raise NotImplementedError() -class Key(Object): +class LocalDomainParameters(DomainParameters): + def __init__(self, session, params): + self._session = session + self.params = params + + @property + def session(self): + return self._session + + @property + def handle(self): + return None + + def __getitem__(self, key): + try: + return self.params[key] + except KeyError as ex: + raise AttributeTypeInvalid from ex + + def __setitem__(self, key, value): + self.params[key] = value + + +class HasKeyType(Object): + @cached_property + def key_type(self): + """Key type (:class:`pkcs11.mechanisms.KeyType`).""" + return self[Attribute.KEY_TYPE] + + +class Key(HasKeyType): """Base class for all key :class:`Object` types.""" + @property + def key_length(self): + """Key length in bits.""" + raise NotImplementedError + @cached_property def id(self): """Key id (:class:`bytes`).""" @@ -707,11 +668,6 @@ def label(self): """Key label (:class:`str`).""" return self[Attribute.LABEL] - @cached_property - def key_type(self): - """Key type (:class:`pkcs11.mechanisms.KeyType`).""" - return self[Attribute.KEY_TYPE] - @cached_property def _key_description(self): """A description of the key.""" @@ -808,7 +764,7 @@ def certificate_type(self): return self[Attribute.CERTIFICATE_TYPE] -class EncryptMixin(Object): +class EncryptMixin(HasKeyType): """ This :class:`Object` supports the encrypt capability. """ @@ -893,7 +849,7 @@ def encrypt_file(file_in, file_out, buffer_size=8192): return self._encrypt_generator(data, buffer_size=buffer_size, **kwargs) -class DecryptMixin(Object): +class DecryptMixin(HasKeyType): """ This :class:`Object` supports the decrypt capability. """ @@ -925,7 +881,7 @@ def decrypt(self, data, buffer_size=8192, **kwargs): return self._decrypt_generator(data, buffer_size=buffer_size, **kwargs) -class SignMixin(Object): +class SignMixin(HasKeyType): """ This :class:`Object` supports the sign capability. """ @@ -962,7 +918,7 @@ def sign(self, data, **kwargs): return self._sign_generator(data, **kwargs) -class VerifyMixin(Object): +class VerifyMixin(HasKeyType): """ This :class:`Object` supports the verify capability. """ @@ -1006,7 +962,7 @@ def verify(self, data, signature, **kwargs): return False -class WrapMixin(Object): +class WrapMixin(HasKeyType): """ This :class:`Object` supports the wrap capability. """ @@ -1027,7 +983,7 @@ def wrap_key(self, key, mechanism=None, mechanism_param=None): raise NotImplementedError() -class UnwrapMixin(Object): +class UnwrapMixin(HasKeyType): """ This :class:`Object` supports the unwrap capability. """ @@ -1066,7 +1022,7 @@ def unwrap_key( raise NotImplementedError() -class DeriveMixin(Object): +class DeriveMixin(HasKeyType): """ This :class:`Object` supports the derive capability. """ diff --git a/tests/test_aes.py b/tests/test_aes.py index 3c7f2b0..387a465 100644 --- a/tests/test_aes.py +++ b/tests/test_aes.py @@ -32,6 +32,15 @@ def test_encrypt(self): text = self.key.decrypt(crypttext, mechanism_param=iv) self.assertEqual(data, text) + @requires(Mechanism.AES_CBC_PAD) + def test_encrypt_undersized_buffer(self): + data = b"INPUT DATA" * 200 + iv = b"0" * 16 + + crypttext = self.key.encrypt(data, mechanism_param=iv, buffer_size=32) + text = self.key.decrypt(crypttext, mechanism_param=iv, buffer_size=32) + self.assertEqual(data, text) + @requires(Mechanism.AES_CBC_PAD) def test_encrypt_stream(self): data = ( @@ -58,6 +67,72 @@ def test_encrypt_stream(self): text = b"".join(self.key.decrypt(cryptblocks, mechanism_param=iv)) self.assertEqual(b"".join(data), text) + @requires(Mechanism.AES_CBC_PAD) + def test_encrypt_stream_interrupt_releases_operation(self): + data = ( + b"I" * 16, + b"N" * 16, + b"P" * 16, + b"U" * 16, + b"T" * 10, + ) + iv = b"0" * 16 + + def _data_with_error(): + yield data[0] + yield data[1] + yield data[2] + raise ValueError + + def attempt_encrypt(): + list(self.key.encrypt(_data_with_error(), mechanism_param=iv)) + + self.assertRaises(ValueError, attempt_encrypt) + + cryptblocks = list(self.key.encrypt(data, mechanism_param=iv)) + text = b"".join(self.key.decrypt(cryptblocks, mechanism_param=iv)) + self.assertEqual(b"".join(data), text) + + @requires(Mechanism.AES_CBC_PAD) + def test_encrypt_stream_gc_releases_operation(self): + data = ( + b"I" * 16, + b"N" * 16, + b"P" * 16, + b"U" * 16, + b"T" * 10, + ) + iv = b"0" * 16 + + attempt = self.key.encrypt(data, mechanism_param=iv) + next(attempt) + del attempt + + cryptblocks = list(self.key.encrypt(data, mechanism_param=iv)) + text = b"".join(self.key.decrypt(cryptblocks, mechanism_param=iv)) + self.assertEqual(b"".join(data), text) + + @requires(Mechanism.AES_CBC_PAD) + def test_encrypt_stream_undersized_buffers(self): + data = ( + b"I" * 3189, + b"N" * 3284, + b"P" * 5812, + b"U" * 2139, + b"T" * 5851, + ) + iv = b"0" * 16 + + cryptblocks = list(self.key.encrypt(data, mechanism_param=iv, buffer_size=256)) + + self.assertEqual(len(cryptblocks), len(data) + 1) + + crypttext = b"".join(cryptblocks) + + self.assertNotEqual(b"".join(data), crypttext) + text = b"".join(self.key.decrypt(cryptblocks, mechanism_param=iv, buffer_size=5)) + self.assertEqual(b"".join(data), text) + @requires(Mechanism.AES_CBC_PAD) def test_encrypt_whacky_sizes(self): data = [(char * ord(char)).encode("utf-8") for char in "HELLO WORLD"] diff --git a/tests/test_iterators.py b/tests/test_iterators.py index 9456e90..5503ce2 100644 --- a/tests/test_iterators.py +++ b/tests/test_iterators.py @@ -2,8 +2,6 @@ Iterator tests """ -import unittest - import pkcs11 from . import TestCase, requires @@ -31,9 +29,6 @@ def test_partial_decrypt(self): next(iter2) @requires(pkcs11.Mechanism.AES_KEY_GEN, pkcs11.Mechanism.AES_CBC_PAD) - # Ideally deleting iterator #1 would terminate the operation, but it - # currently does not. - @unittest.expectedFailure def test_close_iterators(self): self.session.generate_key(pkcs11.KeyType.AES, 128, label="LOOK ME UP") diff --git a/tests/test_rsa.py b/tests/test_rsa.py index f9e5343..8e5e200 100644 --- a/tests/test_rsa.py +++ b/tests/test_rsa.py @@ -42,7 +42,7 @@ def test_sign_stream(self): b"N" * 16, b"P" * 16, b"U" * 16, - b"T" * 10, # don't align to the blocksize + b"T" * 10, ) signature = self.private.sign(data) @@ -50,6 +50,47 @@ def test_sign_stream(self): self.assertIsInstance(signature, bytes) self.assertTrue(self.public.verify(data, signature)) + @requires(Mechanism.SHA512_RSA_PKCS) + def test_sign_stream_undersized_buffer(self): + data = ( + b"I" * 16, + b"N" * 16, + b"P" * 16, + b"U" * 16, + b"T" * 10, + ) + + signature = self.private.sign(data, buffer_size=16) + self.assertIsNotNone(signature) + self.assertIsInstance(signature, bytes) + self.assertTrue(self.public.verify(data, signature)) + + @requires(Mechanism.SHA512_RSA_PKCS) + def test_sign_stream_interrupt_releases_operation(self): + data = ( + b"I" * 16, + b"N" * 16, + b"P" * 16, + b"U" * 16, + b"T" * 10, + ) + + def _data_with_error(): + yield data[0] + yield data[1] + yield data[2] + raise ValueError + + def attempt_sign(): + self.private.sign(_data_with_error()) + + self.assertRaises(ValueError, attempt_sign) + # ...try again + signature = self.private.sign(data) + self.assertIsNotNone(signature) + self.assertIsInstance(signature, bytes) + self.assertTrue(self.public.verify(data, signature)) + @requires(Mechanism.RSA_PKCS_OAEP) @FIXME.opencryptoki # can't set key attributes def test_key_wrap(self): @@ -110,6 +151,20 @@ def test_sign_pss(self): self.assertTrue(self.public.verify(data, signature, mechanism=Mechanism.SHA1_RSA_PKCS_PSS)) + @requires(Mechanism.SHA1_RSA_PKCS_PSS) + def test_sign_pss_undersized_buffer(self): + data = b"SOME DATA" + + # These are the default params + signature = self.private.sign( + data, + mechanism=Mechanism.SHA1_RSA_PKCS_PSS, + mechanism_param=(Mechanism.SHA_1, MGF.SHA1, 20), + buffer_size=16, + ) + + self.assertTrue(self.public.verify(data, signature, mechanism=Mechanism.SHA1_RSA_PKCS_PSS)) + @requires(Mechanism.RSA_PKCS_OAEP) def test_encrypt_too_much_data(self): data = b"1234" * 128 diff --git a/tests/test_slots_and_tokens.py b/tests/test_slots_and_tokens.py index 2e7b085..3acea2b 100644 --- a/tests/test_slots_and_tokens.py +++ b/tests/test_slots_and_tokens.py @@ -2,6 +2,7 @@ PKCS#11 Slots and Tokens """ +import os import unittest import pkcs11 @@ -11,16 +12,33 @@ class SlotsAndTokensTests(unittest.TestCase): def test_double_initialise(self): - self.assertIsNotNone(pkcs11.lib(LIB)) - self.assertIsNotNone(pkcs11.lib(LIB)) + attempt1 = pkcs11.lib(LIB) + attempt2 = pkcs11.lib(LIB) + self.assertIsNotNone(attempt1) + self.assertIsNotNone(attempt2) + self.assertIs(attempt1, attempt2) def test_nonexistent_lib(self): with self.assertRaises(RuntimeError): pkcs11.lib("thislibdoesntexist.so") + @unittest.skipUnless("PKCS11_MODULE2" in os.environ, "Requires an additional PKCS#11 module") def test_double_initialise_different_libs(self): + lib1 = pkcs11.lib(LIB) + lib2 = pkcs11.lib(os.environ["PKCS11_MODULE2"]) + self.assertIsNotNone(lib1) + self.assertIsNotNone(lib2) + self.assertIsNot(lib1, lib2) + + slots1 = lib1.get_slots() + slots2 = lib2.get_slots() + + self.assertGreater(len(slots1), 0) + self.assertGreater(len(slots2), 0) + + def test_double_initialise_nonexistent_lib(self): self.assertIsNotNone(pkcs11.lib(LIB)) - with self.assertRaises(pkcs11.AlreadyInitialized): + with self.assertRaises(RuntimeError): pkcs11.lib("somethingelse.so") @Only.softhsm2