diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..aa51dde --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +**/__pycache__ +**/*.egg-info +/pkcs11/*.c +/pkcs11/*.so +/build/ +/dist/ \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 78690f5..e3f706e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -3,7 +3,7 @@ on: push: jobs: run: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 strategy: matrix: python-version: @@ -16,7 +16,7 @@ jobs: steps: - name: Install APT dependencies - run: sudo apt-get install -y softhsm2 + run: sudo apt-get install -y gcc python3 python3-dev softhsm2 openssl - name: Acquire sources uses: actions/checkout@v4.1.1 @@ -32,6 +32,12 @@ jobs: python-version: ${{ matrix.python-version }} architecture: x64 + - name: Create venv + run: uv venv --python-preference only-system --python ${{ matrix.python-version }} + + - name: Update setuptools + run: uv pip install setuptools + - name: Install the project run: uv sync --all-extras --python-preference only-system --python ${{ matrix.python-version }} diff --git a/Dockerfile.debian b/Dockerfile.debian new file mode 100644 index 0000000..9202f8b --- /dev/null +++ b/Dockerfile.debian @@ -0,0 +1,21 @@ +ARG IMAGE=debian:stable +FROM $IMAGE + +RUN apt-get update && \ + DEBIAN_FRONTEND="noninteractive" apt-get install -y gcc python3 python3-dev softhsm2 openssl && \ + rm -rf /var/lib/apt/lists/* + +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ + +WORKDIR /test + +ADD uv.lock pyproject.toml . +ADD pkcs11/ pkcs11/ +ADD extern/ extern/ + +ENV UV_LINK_MODE=copy +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync --all-extras + +ADD tests/ tests/ +CMD ["uv", "run", "pytest", "-v"] \ No newline at end of file diff --git a/README.rst b/README.md similarity index 68% rename from README.rst rename to README.md index c0b6eff..4db168c 100644 --- a/README.rst +++ b/README.md @@ -1,8 +1,11 @@ -.. image:: https://travis-ci.org/danni/python-pkcs11.svg?branch=master - :target: https://travis-ci.org/danni/python-pkcs11 +# Python PKCS#11 - High Level Wrapper API -Python PKCS#11 - High Level Wrapper API -======================================= +![image](https://github.com/pyauth/python-pkcs11/workflows/Tests/badge.svg) +![image](https://github.com/pyauth/python-pkcs11/workflows/Code%20quality/badge.svg) +![image](https://img.shields.io/pypi/v/python-pkcs11.svg) +![image](https://img.shields.io/pypi/dm/python-pkcs11.svg) +![image](https://img.shields.io/pypi/pyversions/python-pkcs11.svg) +![image](https://img.shields.io/pypi/status/python-pkcs11.svg) A high level, "more Pythonic" interface to the PKCS#11 (Cryptoki) standard to support HSM and Smartcard devices in Python. @@ -26,198 +29,187 @@ Source: https://github.com/danni/python-pkcs11 Documentation: http://python-pkcs11.readthedocs.io/en/latest/ -Getting Started ---------------- +## Getting Started Install from Pip: -:: - - pip install python-pkcs11 - +``` +pip install python-pkcs11 +``` Or build from source: -:: - - python setup.py build - -Assuming your PKCS#11 library is set as `PKCS11_MODULE` and contains a -token named `DEMO`: - -AES -~~~ - -:: +``` +python setup.py build +``` - import pkcs11 +Assuming your PKCS#11 library is set as `PKCS11_MODULE` and contains a token named `DEMO`: - # Initialise our PKCS#11 library - lib = pkcs11.lib(os.environ['PKCS11_MODULE']) - token = lib.get_token(token_label='DEMO') +### AES - data = b'INPUT DATA' +```python +import os, pkcs11 - # Open a session on our token - with token.open(user_pin='1234') as session: - # Generate an AES key in this session - key = session.generate_key(pkcs11.KeyType.AES, 256) +# Initialise our PKCS#11 library +lib = pkcs11.lib(os.environ['PKCS11_MODULE']) +token = lib.get_token(token_label='DEMO') - # Get an initialisation vector - iv = session.generate_random(128) # AES blocks are fixed at 128 bits - # Encrypt our data - crypttext = key.encrypt(data, mechanism_param=iv) +data = b'INPUT DATA' -3DES -~~~~ +# Open a session on our token +with token.open(user_pin='1234') as session: + # Generate an AES key in this session + key = session.generate_key(pkcs11.KeyType.AES, 256) -:: + # Get an initialisation vector + iv = session.generate_random(128) # AES blocks are fixed at 128 bits + # Encrypt our data + crypttext = key.encrypt(data, mechanism_param=iv) +``` - import pkcs11 +### 3DES - # Initialise our PKCS#11 library - lib = pkcs11.lib(os.environ['PKCS11_MODULE']) - token = lib.get_token(token_label='DEMO') +```python +import os, pkcs11 - data = b'INPUT DATA' +# Initialise our PKCS#11 library +lib = pkcs11.lib(os.environ['PKCS11_MODULE']) +token = lib.get_token(token_label='DEMO') - # Open a session on our token - with token.open(user_pin='1234') as session: - # Generate a DES key in this session - key = session.generate_key(pkcs11.KeyType.DES3) +data = b'INPUT DATA' - # Get an initialisation vector - iv = session.generate_random(64) # DES blocks are fixed at 64 bits - # Encrypt our data - crypttext = key.encrypt(data, mechanism_param=iv) +# Open a session on our token +with token.open(user_pin='1234') as session: + # Generate a DES key in this session + key = session.generate_key(pkcs11.KeyType.DES3) -RSA -~~~ + # Get an initialisation vector + iv = session.generate_random(64) # DES blocks are fixed at 64 bits + # Encrypt our data + crypttext = key.encrypt(data, mechanism_param=iv) +``` -:: +### RSA - import pkcs11 +```python +import os, pkcs11 - lib = pkcs11.lib(os.environ['PKCS11_MODULE']) - token = lib.get_token(token_label='DEMO') +lib = pkcs11.lib(os.environ['PKCS11_MODULE']) +token = lib.get_token(token_label='DEMO') - data = b'INPUT DATA' +data = b'INPUT DATA' - # Open a session on our token - with token.open(user_pin='1234') as session: - # Generate an RSA keypair in this session - pub, priv = session.generate_keypair(pkcs11.KeyType.RSA, 2048) +# Open a session on our token +with token.open(user_pin='1234') as session: + # Generate an RSA keypair in this session + pub, priv = session.generate_keypair(pkcs11.KeyType.RSA, 2048) - # Encrypt as one block - crypttext = pub.encrypt(data) + # Encrypt as one block + crypttext = pub.encrypt(data) +``` -DSA -~~~ +### DSA -:: +```python +import os, pkcs11 - import pkcs11 +lib = pkcs11.lib(os.environ['PKCS11_MODULE']) +token = lib.get_token(token_label='DEMO') - lib = pkcs11.lib(os.environ['PKCS11_MODULE']) - token = lib.get_token(token_label='DEMO') +data = b'INPUT DATA' - data = b'INPUT DATA' +# Open a session on our token +with token.open(user_pin='1234') as session: + # Generate an DSA keypair in this session + pub, priv = session.generate_keypair(pkcs11.KeyType.DSA, 1024) - # Open a session on our token - with token.open(user_pin='1234') as session: - # Generate an DSA keypair in this session - pub, priv = session.generate_keypair(pkcs11.KeyType.DSA, 1024) + # Sign + signature = priv.sign(data) +``` - # Sign - signature = priv.sign(data) +### ECDSA -ECDSA -~~~~~ +```python +import pkcs11 -:: +lib = pkcs11.lib(os.environ['PKCS11_MODULE']) +token = lib.get_token(token_label='DEMO') - import pkcs11 +data = b'INPUT DATA' - lib = pkcs11.lib(os.environ['PKCS11_MODULE']) - token = lib.get_token(token_label='DEMO') +# Open a session on our token +with token.open(user_pin='1234') as session: + # Generate an EC keypair in this session from a named curve + ecparams = session.create_domain_parameters( + pkcs11.KeyType.EC, { + pkcs11.Attribute.EC_PARAMS: pkcs11.util.ec.encode_named_curve_parameters('secp256r1'), + }, local=True) + pub, priv = ecparams.generate_keypair() - data = b'INPUT DATA' + # Sign + signature = priv.sign(data) +``` - # Open a session on our token - with token.open(user_pin='1234') as session: - # Generate an EC keypair in this session from a named curve - ecparams = session.create_domain_parameters( - pkcs11.KeyType.EC, { - pkcs11.Attribute.EC_PARAMS: pkcs11.util.ec.encode_named_curve_parameters('secp256r1'), - }, local=True) - pub, priv = ecparams.generate_keypair() +### Diffie-Hellman - # Sign - signature = priv.sign(data) +```python +import os, pkcs11 -Diffie-Hellman -~~~~~~~~~~~~~~ +lib = pkcs11.lib(os.environ['PKCS11_MODULE']) +token = lib.get_token(token_label='DEMO') -:: +with token.open() as session: + # Given shared Diffie-Hellman parameters + parameters = session.create_domain_parameters(pkcs11.KeyType.DH, { + pkcs11.Attribute.PRIME: prime, # Diffie-Hellman parameters + pkcs11.Attribute.BASE: base, + }) - import pkcs11 + # Generate a DH key pair from the public parameters + public, private = parameters.generate_keypair() - lib = pkcs11.lib(os.environ['PKCS11_MODULE']) - token = lib.get_token(token_label='DEMO') + # Share the public half of it with our other party. + _network_.write(public[Attribute.VALUE]) + # And get their shared value + other_value = _network_.read() - with token.open() as session: - # Given shared Diffie-Hellman parameters - parameters = session.create_domain_parameters(pkcs11.KeyType.DH, { - pkcs11.Attribute.PRIME: prime, # Diffie-Hellman parameters - pkcs11.Attribute.BASE: base, - }) + # Derive a shared session key with perfect forward secrecy + session_key = private.derive_key( + pkcs11.KeyType.AES, 128, + mechanism_param=other_value) +``` - # Generate a DH key pair from the public parameters - public, private = parameters.generate_keypair() - # Share the public half of it with our other party. - _network_.write(public[Attribute.VALUE]) - # And get their shared value - other_value = _network_.read() +### Elliptic-Curve Diffie-Hellman - # Derive a shared session key with perfect forward secrecy - session_key = private.derive_key( - pkcs11.KeyType.AES, 128, - mechanism_param=other_value) +```python +import os, pkcs11 +lib = pkcs11.lib(os.environ['PKCS11_MODULE']) +token = lib.get_token(token_label='DEMO') -Elliptic-Curve Diffie-Hellman -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +with token.open() as session: + # Given DER encocded EC parameters, e.g. from + # openssl ecparam -outform der -name + parameters = session.create_domain_parameters(pkcs11.KeyType.EC, { + pkcs11.Attribute.EC_PARAMS: ecparams, + }) -:: + # Generate a DH key pair from the public parameters + public, private = parameters.generate_keypair() - import pkcs11 + # Share the public half of it with our other party. + _network_.write(public[pkcs11.Attribute.EC_POINT]) + # And get their shared value + other_value = _network_.read() - lib = pkcs11.lib(os.environ['PKCS11_MODULE']) - token = lib.get_token(token_label='DEMO') + # Derive a shared session key + session_key = private.derive_key( + pkcs11.KeyType.AES, 128, + mechanism_param=(pkcs11.KDF.NULL, None, other_value)) +``` - with token.open() as session: - # Given DER encocded EC parameters, e.g. from - # openssl ecparam -outform der -name - parameters = session.create_domain_parameters(pkcs11.KeyType.EC, { - pkcs11.Attribute.EC_PARAMS: ecparams, - }) - - # Generate a DH key pair from the public parameters - public, private = parameters.generate_keypair() - - # Share the public half of it with our other party. - _network_.write(public[pkcs11.Attribute.EC_POINT]) - # And get their shared value - other_value = _network_.read() - - # Derive a shared session key - session_key = private.derive_key( - pkcs11.KeyType.AES, 128, - mechanism_param=(pkcs11.KDF.NULL, None, other_value)) - -Tested Compatibility --------------------- +## Tested Compatibility +------------------------------+--------------+-----------------+--------------+-------------------+ | Functionality | SoftHSMv2 | Thales nCipher | Opencryptoki | OpenSC (Nitrokey) | @@ -304,12 +296,6 @@ Tested Compatibility .. [8] `store` parameter is ignored, all keys are stored. .. [9] Encryption/verify not supported, extract the public key -Python version: - -* 3.4 (with `aenum`) -* 3.5 (with `aenum`) -* 3.6 - PKCS#11 versions: * 2.11 @@ -322,8 +308,7 @@ straight-forward way. If you want your device supported, get in touch! -More info on PKCS #11 ---------------------- +## More info on PKCS #11 The latest version of the PKCS #11 spec is available from OASIS: @@ -334,8 +319,7 @@ Many implementations expose additional vendor options configurable in your environment, including alternative features, modes and debugging information. -License -------- +## License MIT License diff --git a/pyproject.toml b/pyproject.toml index 9b0b097..241b146 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,6 +76,7 @@ dev = [ "oscrypto>=1.3.0", "pytest>=8.3.4", "ruff>=0.8.3", + "setuptools>=75.6.0", "setuptools-scm>=8.1.0", "sphinx>=7.4.7", "sphinx-rtd-theme>=3.0.2", diff --git a/setup.py b/setup.py deleted file mode 100755 index 1ff33c0..0000000 --- a/setup.py +++ /dev/null @@ -1,26 +0,0 @@ -# Add cython extension module to build configuration. -# -# See also: https://setuptools.pypa.io/en/latest/userguide/ext_modules.html - -import platform - -from setuptools import Extension, setup - -libraries = [] - -# if compiling using MSVC, we need to link against user32 library -if platform.system() == "Windows": - libraries.append("user32") - - -setup( - ext_modules=[ - Extension( - name="pkcs11._pkcs11", - sources=[ - "pkcs11/_pkcs11.pyx", - ], - libraries=libraries, - ), - ], -) diff --git a/tests/conftest.py b/tests/conftest.py index f9af88c..08f2c05 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -56,6 +56,7 @@ def get_random_string(length): @pytest.fixture(scope="session") def lib(): + print(LIB_PATH, os.path.exists(LIB_PATH)) return pkcs11.lib(LIB_PATH) diff --git a/tests/test_slots_and_tokens.py b/tests/test_slots_and_tokens.py index 1a9d3c1..a53968c 100644 --- a/tests/test_slots_and_tokens.py +++ b/tests/test_slots_and_tokens.py @@ -2,6 +2,8 @@ PKCS#11 Slots and Tokens """ +import os + import pytest import pkcs11 @@ -34,6 +36,7 @@ def test_get_slots() -> None: def test_get_mechanisms() -> None: + print(LIB_PATH, os.path.exists(LIB_PATH)) lib = pkcs11.lib(LIB_PATH) slot, *_ = lib.get_slots() mechanisms = slot.get_mechanisms() diff --git a/uv.lock b/uv.lock index 493b887..2da0ef3 100644 --- a/uv.lock +++ b/uv.lock @@ -452,6 +452,7 @@ dev = [ { name = "oscrypto" }, { name = "pytest" }, { name = "ruff" }, + { name = "setuptools" }, { name = "setuptools-scm" }, { name = "sphinx" }, { name = "sphinx-rtd-theme" }, @@ -466,6 +467,7 @@ dev = [ { name = "oscrypto", specifier = ">=1.3.0" }, { name = "pytest", specifier = ">=8.3.4" }, { name = "ruff", specifier = ">=0.8.3" }, + { name = "setuptools", specifier = ">=75.6.0" }, { name = "setuptools-scm", specifier = ">=8.1.0" }, { name = "sphinx", specifier = ">=7.4.7" }, { name = "sphinx-rtd-theme", specifier = ">=3.0.2" },