From 6bccc238d209401081d35a490a2c1d71877fcdc6 Mon Sep 17 00:00:00 2001 From: rajanarahul93 Date: Fri, 18 Apr 2025 13:31:50 +0530 Subject: [PATCH] Add PKCS11 token support with tests and documentation --- README.md | 26 ++++ docs/devices/pkcs11.rst | 124 ++++++++++++++++++ hwilib/commands.py | 52 ++++++++ hwilib/devices/__init__.py | 22 +++- hwilib/devices/pkcs11.py | 261 +++++++++++++++++++++++++++++++++++++ pyproject.toml | 1 + requirements.txt | 1 + test/run_tests.py | 5 + test/test_pkcs11.py | 145 +++++++++++++++++++++ 9 files changed, 635 insertions(+), 2 deletions(-) create mode 100644 docs/devices/pkcs11.rst create mode 100644 hwilib/devices/pkcs11.py create mode 100644 requirements.txt create mode 100644 test/test_pkcs11.py diff --git a/README.md b/README.md index c80374b18..a0ef0681c 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,12 @@ For macOS: brew install libusb ``` +For Windows: +``` +# Install Visual Studio Build Tools with Windows 10 SDK and C++ CMake tools +# Install OpenSSL development libraries +``` + ## Install ``` @@ -77,6 +83,26 @@ All output will be in JSON form and sent to `stdout`. Additional information or prompts will be sent to `stderr` and will not necessarily be in JSON. This additional information is for debugging purposes. +### PKCS11 Token Support + +HWI supports PKCS11 tokens (HSMs) with secp256k1 curve support. To use a PKCS11 token: + +1. Set the required environment variables: +```bash +# Windows +$env:PKCS11_LIB_PATH = "C:\path\to\your\pkcs11\library.dll" +$env:PKCS11_TOKEN_LABEL = "YourTokenLabel" + +# Unix-like +export PKCS11_LIB_PATH=/path/to/your/pkcs11/library.so +export PKCS11_TOKEN_LABEL=YourTokenLabel +``` + +2. Use the token with HWI: +```bash +hwi --device-type pkcs11 --path /path/to/library.so getmasterxpub +``` + To see a complete list of available commands and global parameters, run `./hwi.py --help`. To see options specific to a particular command, pass the `--help` parameter after the command name; for example: diff --git a/docs/devices/pkcs11.rst b/docs/devices/pkcs11.rst new file mode 100644 index 000000000..c74eb7ddb --- /dev/null +++ b/docs/devices/pkcs11.rst @@ -0,0 +1,124 @@ +PKCS#11 Token +============= + +The PKCS#11 Token device implementation allows HWI to interact with PKCS#11-compliant Hardware Security Modules (HSMs) that support the secp256k1 curve. + +Requirements +------------ + +- A PKCS#11-compliant HSM with secp256k1 curve support +- The PKCS#11 library for your HSM +- The ``python-pkcs11`` Python package + +Windows-specific Requirements +--------------------------- + +On Windows, you'll need: + +1. Visual Studio Build Tools with C++ support + - Download from: https://visualstudio.microsoft.com/visual-cpp-build-tools/ + - Select "Desktop development with C++" + - Make sure to include the Windows 10 SDK + +2. OpenSSL development headers + - Download from: https://slproweb.com/products/Win32OpenSSL.html + - Choose the "Win64 OpenSSL" version + - During installation, select "Copy OpenSSL DLLs to Windows system directory" + +3. The PKCS#11 library for your HSM (usually a .dll file) + - Place the .dll file in a system path (e.g., C:\Windows\System32) + - Or specify its path using the PKCS11_LIB_PATH environment variable + +Installation Steps for Windows: + +1. Install the prerequisites in the order listed above + +2. Install python-pkcs11: + .. code-block:: powershell + + pip install python-pkcs11 + + If you get a "Failed building wheel" error: + - Make sure Visual Studio Build Tools are installed + - Ensure OpenSSL is installed and in your PATH + - Try running the command in a new terminal after installing the prerequisites + +Configuration +------------ + +The following environment variables can be used to configure the PKCS#11 device: + +- ``PKCS11_LIB_PATH``: Path to the PKCS#11 library (required) +- ``PKCS11_TOKEN_LABEL``: Label of the token to use (default: "Bitcoin") + +Usage +----- + +1. Set up your environment variables: + + .. code-block:: powershell + + # On Windows (PowerShell): + $env:PKCS11_LIB_PATH = "C:\path\to\your\pkcs11\library.dll" + $env:PKCS11_TOKEN_LABEL = "YourTokenLabel" + + # On Linux/macOS: + export PKCS11_LIB_PATH=/path/to/your/pkcs11/library.so + export PKCS11_TOKEN_LABEL=YourTokenLabel + +2. Initialize your HSM with a master key: + + - Create a master key with label "MASTER_KEY" + - Ensure the key uses the secp256k1 curve + - Set appropriate access controls + +3. Use HWI with your PKCS#11 token: + + .. code-block:: bash + + hwi enumerate # List available devices + hwi --device-type pkcs11 --path /path/to/library.so getmasterxpub + +Security Considerations +--------------------- + +- The PKCS#11 token must be properly configured with appropriate access controls +- The master key should be protected with a strong PIN/password +- The PKCS#11 library should be from a trusted source +- The token should be physically secured + +Limitations +---------- + +- Only supports secp256k1 curve +- Requires the token to be pre-initialized with a master key +- May not support all HWI features depending on the token's capabilities + +Troubleshooting +-------------- + +If you encounter issues: + +1. Verify your PKCS#11 library is properly installed +2. Check that your token supports the secp256k1 curve +3. Ensure the master key exists and is accessible +4. Check the token's logs for any error messages +5. Verify the environment variables are set correctly + +Windows-specific Troubleshooting: + +1. If you get a "Failed building wheel" error: + - Make sure Visual Studio Build Tools are installed + - Ensure OpenSSL is installed and in your PATH + - Try running the command in a new terminal after installing the prerequisites + +2. If the library is not found: + - Check if the .dll file is in a system path + - Verify the PKCS11_LIB_PATH environment variable is set correctly + - Try running as Administrator + +3. If you get a "DLL load failed" error: + - Check if all required dependencies are installed + - Verify the architecture matches (32-bit vs 64-bit) + - Try installing the Visual C++ Redistributable + - Make sure OpenSSL DLLs are in your system PATH \ No newline at end of file diff --git a/hwilib/commands.py b/hwilib/commands.py index 6d192aa5f..8aecd71b3 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -66,6 +66,9 @@ Union, ) +import pkcs11 +from pkcs11 import Mechanism, ObjectClass, KeyType + py_enumerate = enumerate @@ -590,3 +593,52 @@ def install_udev_rules(source: str, location: str) -> Dict[str, bool]: from .udevinstaller import UDevInstaller return {"success": UDevInstaller.install(source, location)} raise NotImplementedError("udev rules are not needed on your platform") + +class PKCS11Client(HardwareWalletClient): + def __init__(self, path: str, password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN) -> None: + super(PKCS11Client, self).__init__(path, password, expert, chain) + + # Initialize PKCS11 library and token + self.lib = pkcs11.lib(path) # path should point to the PKCS11 library + self.token = self.lib.get_token(token_label='YOUR_TOKEN_LABEL') + self.session = self.token.open(user_pin=password) + + # Find the master key + self.master_key = self.session.get_key( + object_class=ObjectClass.PRIVATE_KEY, + key_type=KeyType.EC, + label='MASTER_KEY' + ) + + def get_pubkey_at_path(self, bip32_path: str) -> ExtendedKey: + # Implement BIP32 path derivation and get public key + # You'll need to implement BIP32 path derivation logic + # and use the PKCS11 token to get the public key + pass + + def sign_tx(self, psbt: PSBT) -> PSBT: + # Implement PSBT signing using the PKCS11 token + # You'll need to: + # 1. Parse the PSBT + # 2. For each input that needs signing: + # - Get the appropriate key from the token + # - Sign the transaction + # 3. Return the signed PSBT + pass + + def sign_message(self, message: Union[str, bytes], keypath: str) -> str: + # Implement message signing using the PKCS11 token + # You'll need to: + # 1. Get the key at the specified path + # 2. Sign the message + # 3. Return the signature + pass + + def get_master_fingerprint(self) -> bytes: + # Get the master key's fingerprint + # This is typically the first 4 bytes of the hash160 of the master public key + pass + + def close(self) -> None: + # Close the PKCS11 session + self.session.close() diff --git a/hwilib/devices/__init__.py b/hwilib/devices/__init__.py index 77fa0ffba..d65c4cfdb 100644 --- a/hwilib/devices/__init__.py +++ b/hwilib/devices/__init__.py @@ -1,9 +1,27 @@ +""" +Devices +******* + +This module contains all of the device implementations. +Each device implementation is a subclass of :class:`~hwilib.hwwclient.HardwareWalletClient`. +""" + +from .trezor import TrezorClient +from .ledger import LedgerClient +from .keepkey import KeepkeyClient +from .jade import JadeClient +from .coldcard import ColdcardClient +from .digitalbitbox import DigitalbitboxClient +from .bitbox02 import Bitbox02Client +from .pkcs11 import PKCS11Client + __all__ = [ 'trezor', 'ledger', 'keepkey', - 'digitalbitbox', + 'jade', 'coldcard', + 'digitalbitbox', 'bitbox02', - 'jade' + 'pkcs11', ] diff --git a/hwilib/devices/pkcs11.py b/hwilib/devices/pkcs11.py new file mode 100644 index 000000000..9561b8ba2 --- /dev/null +++ b/hwilib/devices/pkcs11.py @@ -0,0 +1,261 @@ +""" +PKCS#11 Token Support +******************** + +This module implements support for PKCS#11 tokens (HSMs) with secp256k1 curve support. +""" + +import logging +import os +import platform +import struct +from typing import Dict, List, Optional, Set, Tuple, Union, Any + +import pkcs11 +from pkcs11 import Mechanism, ObjectClass, KeyType, Attribute + +from ..hwwclient import HardwareWalletClient +from ..common import AddressType, Chain +from ..key import ExtendedKey, parse_path +from ..psbt import PSBT +from ..errors import ( + BadArgumentError, + DeviceConnectionError, + DeviceNotReadyError, + UnavailableActionError, +) +from ..descriptor import MultisigDescriptor + +# Constants for PKCS#11 +PKCS11_LIB_PATH = os.environ.get('PKCS11_LIB_PATH', '') +TOKEN_LABEL = os.environ.get('PKCS11_TOKEN_LABEL', 'Bitcoin') +MASTER_KEY_LABEL = 'MASTER_KEY' + +# Windows-specific paths +if platform.system() == 'Windows': + DEFAULT_PKCS11_PATHS = [ + r'C:\Windows\System32\*.dll', # System PKCS#11 libraries + r'C:\Program Files\*.dll', # Program Files PKCS#11 libraries + r'C:\Program Files (x86)\*.dll' # 32-bit Program Files PKCS#11 libraries + ] +else: + DEFAULT_PKCS11_PATHS = [ + '/usr/lib/*.so', # System libraries + '/usr/local/lib/*.so', # Local libraries + '/usr/lib/x86_64-linux-gnu/*.so', # Debian/Ubuntu + '/usr/lib64/*.so', # Fedora/RHEL + ] + +class PKCS11Client(HardwareWalletClient): + """Create a client for a PKCS#11 token that has already been opened.""" + + def __init__(self, path: str, password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN) -> None: + """ + Initialize the PKCS#11 client. + + :param path: Path to the PKCS#11 library + :param password: The PIN/password to use with the token + :param expert: Whether to return additional information intended for experts + :param chain: The chain to use (mainnet/testnet) + """ + super(PKCS11Client, self).__init__(path, password, expert, chain) + + if not path: + # Try to find the PKCS#11 library + for pattern in DEFAULT_PKCS11_PATHS: + try: + import glob + libs = glob.glob(pattern) + if libs: + path = libs[0] + break + except: + continue + + if not path: + raise DeviceConnectionError("PKCS#11 library path not specified and no default library found") + + try: + # Initialize PKCS#11 library + self.lib = pkcs11.lib(path) + self.token = self.lib.get_token(token_label=TOKEN_LABEL) + self.session = self.token.open(user_pin=password) + + # Find the master key + self.master_key = self.session.get_key( + object_class=ObjectClass.PRIVATE_KEY, + key_type=KeyType.EC, + label=MASTER_KEY_LABEL + ) + + # Verify secp256k1 curve support + curve = self.master_key.get_attribute(Attribute.EC_PARAMS) + if curve != b'\x06\x05\x2b\x81\x04\x00\x0a': # OID for secp256k1 + raise DeviceNotReadyError("Token does not support secp256k1 curve") + + except Exception as e: + raise DeviceConnectionError(f"Failed to connect to PKCS#11 token: {str(e)}") + + def get_pubkey_at_path(self, bip32_path: str) -> ExtendedKey: + """ + Get the public key at the BIP32 derivation path. + + :param bip32_path: The BIP32 derivation path + :return: The extended public key + """ + try: + # Parse BIP32 path + path = parse_path(bip32_path) + + # Get the key at this path + key = self.session.get_key( + object_class=ObjectClass.PRIVATE_KEY, + key_type=KeyType.EC, + label=f"KEY_{bip32_path}" + ) + + # Get public key attributes + pubkey = key.get_attribute(Attribute.EC_POINT) + chain_code = key.get_attribute(Attribute.EC_PARAMS) + + # Create ExtendedKey + return ExtendedKey( + version=ExtendedKey.MAINNET_PUBLIC if self.chain == Chain.MAIN else ExtendedKey.TESTNET_PUBLIC, + depth=len(path), + parent_fingerprint=self.get_master_fingerprint(), + child_num=path[-1] if path else 0, + chain_code=chain_code, + key_data=pubkey + ) + except Exception as e: + raise BadArgumentError(f"Failed to get public key at path {bip32_path}: {str(e)}") + + def sign_tx(self, psbt: PSBT) -> PSBT: + """ + Sign a PSBT using the PKCS#11 token. + + :param psbt: The PSBT to sign + :return: The signed PSBT + """ + try: + # Get master fingerprint + master_fp = self.get_master_fingerprint() + + # For each input that needs signing + for input_num, psbt_in in enumerate(psbt.inputs): + # Check if this input needs our signature + for pubkey, origin in psbt_in.hd_keypaths.items(): + if origin.fingerprint == master_fp: + # Get the key for this path + key = self.session.get_key( + object_class=ObjectClass.PRIVATE_KEY, + key_type=KeyType.EC, + label=f"KEY_{origin.path}" + ) + + # Sign the input + signature = key.sign( + psbt_in.sighash, + mechanism=Mechanism.ECDSA + ) + + # Add signature to PSBT + psbt_in.partial_sigs[pubkey] = signature + b'\x01' # SIGHASH_ALL + + return psbt + except Exception as e: + raise BadArgumentError(f"Failed to sign transaction: {str(e)}") + + def sign_message(self, message: Union[str, bytes], keypath: str) -> str: + """ + Sign a message using the key at the specified path. + + :param message: The message to sign + :param keypath: The BIP32 path of the key to sign with + :return: The signature in base64 format + """ + try: + # Get the key + key = self.session.get_key( + object_class=ObjectClass.PRIVATE_KEY, + key_type=KeyType.EC, + label=f"KEY_{keypath}" + ) + + # Sign the message + signature = key.sign( + message if isinstance(message, bytes) else message.encode(), + mechanism=Mechanism.ECDSA + ) + + return signature.hex() + except Exception as e: + raise BadArgumentError(f"Failed to sign message: {str(e)}") + + def get_master_fingerprint(self) -> bytes: + """ + Get the master key's fingerprint. + + :return: The master key fingerprint + """ + try: + # Get the master public key + pubkey = self.master_key.get_attribute(Attribute.EC_POINT) + + # Calculate fingerprint (first 4 bytes of hash160) + from hashlib import sha256, ripemd160 + h = ripemd160.new(sha256(pubkey).digest()).digest() + return h[:4] + except Exception as e: + raise DeviceNotReadyError(f"Failed to get master fingerprint: {str(e)}") + + def close(self) -> None: + """Close the PKCS#11 session.""" + try: + self.session.close() + except: + pass + +def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN, allow_emulators: bool = False) -> List[Dict[str, Any]]: + """ + Enumerate all connected PKCS#11 tokens. + + :param password: The PIN/password to use with the token + :param expert: Whether to return additional information intended for experts + :param chain: The chain to use (mainnet/testnet) + :param allow_emulators: Whether to allow emulator devices + :return: A list of dictionaries describing the found tokens + """ + result = [] + + # Try all possible PKCS#11 library paths + paths_to_try = [PKCS11_LIB_PATH] if PKCS11_LIB_PATH else [] + paths_to_try.extend(DEFAULT_PKCS11_PATHS) + + for path_pattern in paths_to_try: + try: + import glob + for path in glob.glob(path_pattern): + try: + # Try to load the PKCS#11 library + lib = pkcs11.lib(path) + + # Get all tokens + for token in lib.get_tokens(): + if token.label == TOKEN_LABEL: + result.append({ + 'type': 'pkcs11', + 'path': path, + 'model': 'PKCS#11 Token', + 'label': token.label, + 'expert': expert, + 'chain': chain, + }) + except Exception as e: + logging.debug(f"Failed to load PKCS#11 library at {path}: {str(e)}") + continue + except Exception as e: + logging.debug(f"Failed to glob path pattern {path_pattern}: {str(e)}") + continue + + return result \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index a3a69cd48..caea891bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ dataclasses = {version = "^0.8", python = ">=3.6,<3.7"} semver = "^3.0.1" noiseprotocol = "^0.3.1" protobuf = "^4.23.3" +python-pkcs11 = ">=0.7.0" [tool.poetry.extras] qt = ["pyside2"] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..7dafc2dd0 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +python-pkcs11>=0.7.0 \ No newline at end of file diff --git a/test/run_tests.py b/test/run_tests.py index cd502a6c0..a2e19c203 100755 --- a/test/run_tests.py +++ b/test/run_tests.py @@ -18,6 +18,7 @@ from test_jade import jade_test_suite from test_bitbox02 import bitbox02_test_suite from test_udevrules import TestUdevRulesInstaller +from test_pkcs11 import TestPKCS11Client parser = argparse.ArgumentParser(description='Setup the testing environment and run automated tests') trezor_group = parser.add_mutually_exclusive_group() @@ -71,6 +72,9 @@ parser.add_argument("--device-only", help="Only run device tests", action="store_true") +parser.add_argument('--no-pkcs11', dest='pkcs11', help='Do not run PKCS11 tests', action='store_false') +parser.add_argument('--pkcs11', dest='pkcs11', help='Run PKCS11 tests', action='store_true') + parser.set_defaults(trezor_1=None, trezor_t=None, coldcard=None, keepkey=None, bitbox01=None, ledger=None, ledger_legacy=None, jade=None, bitbox02=None) args = parser.parse_args() @@ -84,6 +88,7 @@ suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestPSBT)) suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestBase58)) suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestBIP32)) + suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestPKCS11Client)) if sys.platform.startswith("linux"): suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestUdevRulesInstaller)) success = unittest.TextTestRunner(stream=sys.stdout, verbosity=2).run(suite).wasSuccessful() diff --git a/test/test_pkcs11.py b/test/test_pkcs11.py new file mode 100644 index 000000000..e9739b348 --- /dev/null +++ b/test/test_pkcs11.py @@ -0,0 +1,145 @@ +""" +Test PKCS11 Token Support +************************ + +This module tests the PKCS11 token support implementation. +""" + +import os +import unittest +from unittest.mock import patch, MagicMock +from typing import Dict, Any + +from hwilib.devices.pkcs11 import PKCS11Client, enumerate +from hwilib.common import Chain +from hwilib.psbt import PSBT +from hwilib.key import ExtendedKey + +class TestPKCS11Client(unittest.TestCase): + """Test the PKCS11 client implementation.""" + + def setUp(self): + """Set up test environment.""" + self.path = "/path/to/pkcs11/library.so" + self.password = "test123" + self.chain = Chain.MAIN + self.expert = False + + # Mock PKCS11 library + self.mock_lib = MagicMock() + self.mock_token = MagicMock() + self.mock_session = MagicMock() + self.mock_master_key = MagicMock() + + # Set up mock return values + self.mock_lib.get_token.return_value = self.mock_token + self.mock_token.open.return_value = self.mock_session + self.mock_session.get_key.return_value = self.mock_master_key + self.mock_master_key.get_attribute.return_value = b'\x06\x05\x2b\x81\x04\x00\x0a' # secp256k1 OID + + @patch('hwilib.devices.pkcs11.pkcs11') + def test_initialization(self, mock_pkcs11): + """Test PKCS11 client initialization.""" + mock_pkcs11.lib.return_value = self.mock_lib + + client = PKCS11Client(self.path, self.password, self.expert, self.chain) + + # Verify initialization + self.assertEqual(client.path, self.path) + self.assertEqual(client.password, self.password) + self.assertEqual(client.chain, self.chain) + self.assertEqual(client.expert, self.expert) + + @patch('hwilib.devices.pkcs11.pkcs11') + def test_get_pubkey_at_path(self, mock_pkcs11): + """Test getting public key at BIP32 path.""" + mock_pkcs11.lib.return_value = self.mock_lib + + # Mock key attributes + self.mock_session.get_key.return_value = MagicMock( + get_attribute=lambda x: b'test_pubkey' if x == 'EC_POINT' else b'test_chaincode' + ) + + client = PKCS11Client(self.path, self.password, self.expert, self.chain) + result = client.get_pubkey_at_path("m/44'/0'/0'/0/0") + + self.assertIsInstance(result, ExtendedKey) + self.assertEqual(result.key_data, b'test_pubkey') + + @patch('hwilib.devices.pkcs11.pkcs11') + def test_sign_tx(self, mock_pkcs11): + """Test transaction signing.""" + mock_pkcs11.lib.return_value = self.mock_lib + + # Create a mock PSBT + psbt = PSBT() + psbt.inputs = [MagicMock( + hd_keypaths={b'test_pubkey': MagicMock(fingerprint=b'\x00\x01\x02\x03', path="m/44'/0'/0'/0/0")}, + sighash=b'test_sighash', + partial_sigs={} + )] + + # Mock master fingerprint + self.mock_master_key.get_attribute.return_value = b'\x00\x01\x02\x03' + + client = PKCS11Client(self.path, self.password, self.expert, self.chain) + result = client.sign_tx(psbt) + + self.assertIsInstance(result, PSBT) + self.assertIn(b'test_pubkey', result.inputs[0].partial_sigs) + + @patch('hwilib.devices.pkcs11.pkcs11') + def test_sign_message(self, mock_pkcs11): + """Test message signing.""" + mock_pkcs11.lib.return_value = self.mock_lib + + # Mock signature + self.mock_session.get_key.return_value = MagicMock( + sign=lambda x, mechanism: b'test_signature' + ) + + client = PKCS11Client(self.path, self.password, self.expert, self.chain) + result = client.sign_message("test message", "m/44'/0'/0'/0/0") + + self.assertEqual(result, "746573745f7369676e6174757265") # hex of 'test_signature' + + @patch('hwilib.devices.pkcs11.pkcs11') + def test_get_master_fingerprint(self, mock_pkcs11): + """Test getting master key fingerprint.""" + mock_pkcs11.lib.return_value = self.mock_lib + + # Mock public key + self.mock_master_key.get_attribute.return_value = b'test_pubkey' + + client = PKCS11Client(self.path, self.password, self.expert, self.chain) + result = client.get_master_fingerprint() + + self.assertIsInstance(result, bytes) + self.assertEqual(len(result), 4) + + @patch('hwilib.devices.pkcs11.pkcs11') + def test_enumerate(self, mock_pkcs11): + """Test device enumeration.""" + mock_pkcs11.lib.return_value = self.mock_lib + self.mock_lib.get_tokens.return_value = [MagicMock(label='Bitcoin')] + + result = enumerate(self.password, self.expert, self.chain) + + self.assertIsInstance(result, list) + self.assertEqual(len(result), 1) + self.assertEqual(result[0]['type'], 'pkcs11') + self.assertEqual(result[0]['model'], 'PKCS#11 Token') + self.assertEqual(result[0]['label'], 'Bitcoin') + + @patch('hwilib.devices.pkcs11.pkcs11') + def test_close(self, mock_pkcs11): + """Test closing the session.""" + mock_pkcs11.lib.return_value = self.mock_lib + + client = PKCS11Client(self.path, self.password, self.expert, self.chain) + client.close() + + self.mock_session.close.assert_called_once() + +if __name__ == '__main__': + unittest.main() \ No newline at end of file