From 46e6ae8dec765185b7e2106bb530eab34262c6cd Mon Sep 17 00:00:00 2001 From: Mathias Ertl Date: Mon, 9 Dec 2024 21:13:10 +0100 Subject: [PATCH 1/9] uv-ize this project --- .github/workflows/quality.yml | 14 +++++++++----- .gitignore | 4 ++-- .python-version | 1 + pyproject.toml | 2 +- uv.lock | 22 ++++++++++++++++++++++ 5 files changed, 35 insertions(+), 8 deletions(-) create mode 100644 .python-version create mode 100644 uv.lock diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 04ea2f1..c496798 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -1,7 +1,8 @@ name: Code quality on: push: - pull_request: +env: + UV_SYSTEM_PYTHON: 1 jobs: @@ -13,6 +14,11 @@ jobs: - name: Acquire sources uses: actions/checkout@v4.1.1 + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + enable-cache: true + - name: Setup Python uses: actions/setup-python@v5.0.0 with: @@ -25,10 +31,8 @@ jobs: path: ~/.cache/pip key: pip-${{ hashFiles('**/requirements-*.txt') }} - - name: Install dependencies - run: | - pip install -U pip setuptools wheel - pip install -r requirements.txt -r dev-requirements.txt + - name: Install dev dependencies + run: uv pip install -r dev-requirements.txt - name: Run ruff run: | diff --git a/.gitignore b/.gitignore index 6506c9e..08eea49 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ *.so *.c __pycache__ - -/build +/.idea/ +/build/ /dist/ /docs/_build /python_pkcs11.egg-info/ diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/pyproject.toml b/pyproject.toml index 9cd3927..5e4e4ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,4 +53,4 @@ extend-select = [ combine-as-imports = true [tool.setuptools.packages.find] -include = ["pkcs11*"] \ No newline at end of file +include = ["pkcs11*"] diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..a520750 --- /dev/null +++ b/uv.lock @@ -0,0 +1,22 @@ +version = 1 +requires-python = ">=3.9" + +[[package]] +name = "asn1crypto" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/de/cf/d547feed25b5244fcb9392e288ff9fdc3280b10260362fc45d37a798a6ee/asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c", size = 121080 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/7f/09065fd9e27da0eda08b4d6897f1c13535066174cc023af248fc2a8d5e5a/asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67", size = 105045 }, +] + +[[package]] +name = "python-pkcs11" +version = "0.7.0" +source = { editable = "." } +dependencies = [ + { name = "asn1crypto" }, +] + +[package.metadata] +requires-dist = [{ name = "asn1crypto", specifier = ">=1.4.0" }] From 0e2a518b2ab7e2c2c21949134ebaf1402261f5cd Mon Sep 17 00:00:00 2001 From: Mathias Ertl Date: Mon, 9 Dec 2024 21:27:00 +0100 Subject: [PATCH 2/9] fix noqa rule --- pkcs11/__init__.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pkcs11/__init__.py b/pkcs11/__init__.py index 5b19b4c..77ef7c0 100644 --- a/pkcs11/__init__.py +++ b/pkcs11/__init__.py @@ -6,11 +6,7 @@ from .exceptions import * # noqa: F403 from .mechanisms import * # noqa: F403 from .types import * # noqa: F403 -from .util import dh # noqa: F403 -from .util import dsa # noqa: F403 -from .util import ec # noqa: F403 -from .util import rsa # noqa: F403 -from .util import x509 # noqa: F403 +from .util import dh, dsa, ec, rsa, x509 # noqa: F401 _so = None _lib = None From 79feb31ef0b3baff02fa7bcc2dd14bd3f884d2ec Mon Sep 17 00:00:00 2001 From: Mathias Ertl Date: Mon, 9 Dec 2024 21:35:26 +0100 Subject: [PATCH 3/9] run test in github actions (and with pytest) --- .github/workflows/quality.yml | 7 ----- .github/workflows/tests.yml | 55 +++++++++++++++++++++++++++++++++++ dev-requirements.txt | 1 + 3 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index c496798..36570a3 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -10,7 +10,6 @@ jobs: runs-on: ubuntu-latest steps: - - name: Acquire sources uses: actions/checkout@v4.1.1 @@ -25,12 +24,6 @@ jobs: python-version: "3.12" architecture: x64 - - name: Apply caching of dependencies - uses: actions/cache@v4.0.0 - with: - path: ~/.cache/pip - key: pip-${{ hashFiles('**/requirements-*.txt') }} - - name: Install dev dependencies run: uv pip install -r dev-requirements.txt diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..82eb770 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,55 @@ +name: Tests +on: + push: +env: + UV_SYSTEM_PYTHON: 1 + SOFTHSM2_CONF: /tmp/softhsm2.conf + +jobs: + + run: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: + - "3.9" + - "3.10" + - "3.11" + - "3.12" + - "3.12" + + steps: + - name: Install APT dependencies + run: sudo apt-get install -y softhsm2 + + - name: env + run: env + + - name: id + run: id + + - name: Create SoftHSM token + run: softhsm2-util --init-token --free --label TEST --pin 1234 --so-pin 5678 + + - name: Acquire sources + uses: actions/checkout@v4.1.1 + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + enable-cache: true + + - name: Setup Python + uses: actions/setup-python@v5.0.0 + with: + python-version: ${{ matrix.python-version }} + architecture: x64 + + - name: Install the project + run: uv sync --all-extras --dev + + - name: Install dev dependencies + run: uv pip install -r dev-requirements.txt + + - name: Run tests + run: uv run pytest -v \ No newline at end of file diff --git a/dev-requirements.txt b/dev-requirements.txt index 9b1cdb3..db6183b 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -5,6 +5,7 @@ setuptools_scm oscrypto cryptography parameterized +pytest==8.3.4 ruff==0.8.2 sphinx sphinx-rtd-theme \ No newline at end of file From 2ba7b722e125c51a21546869bc77319a47cc6010 Mon Sep 17 00:00:00 2001 From: Mathias Ertl Date: Sat, 14 Dec 2024 14:55:13 +0100 Subject: [PATCH 4/9] move tests to pytest --- pyproject.toml | 9 + tests/__init__.py | 168 ------- tests/conftest.py | 154 +++++++ tests/test_aes.py | 643 +++++++++++++------------- tests/test_des.py | 50 ++- tests/test_dh.py | 719 +++++++++++++++--------------- tests/test_digest.py | 129 +++--- tests/test_dsa.py | 70 ++- tests/test_ecc.py | 310 ++++++------- tests/test_iterators.py | 67 ++- tests/test_public_key_external.py | 322 ++++++------- tests/test_rsa.py | 241 +++++----- tests/test_sessions.py | 311 ++++++------- tests/test_slots_and_tokens.py | 139 +++--- tests/test_threading.py | 53 ++- tests/test_x509.py | 463 +++++++++---------- 16 files changed, 1942 insertions(+), 1906 deletions(-) create mode 100644 tests/conftest.py diff --git a/pyproject.toml b/pyproject.toml index 5e4e4ad..c93aff7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,14 @@ Documentation = "http://python-pkcs11.readthedocs.io/en/latest/" Issues = "https://github.com/pyauth/python-pkcs11/issues" Repository = "https://github.com/pyauth/python-pkcs11" +[tool.pytest.ini_options] +markers = [ + "requires: marks tests require support for a certain PKCS11 mechanism.", + "xfail_nfast: Expected failure on nFast.", + "xfail_softhsm: Expected failure on SoftHSMv2.", + "xfail_opencryptoki: Expected failure on OpenCryptoki.", +] + [tool.ruff] line-length = 100 @@ -46,6 +54,7 @@ extend-select = [ "F", # pyflakes "I", # isort "G", # flake8-logging-format + "PT", # flake8-pytest-style "RUF", # ruff specific checks ] diff --git a/tests/__init__.py b/tests/__init__.py index d701e4d..e69de29 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,168 +0,0 @@ -""" -PKCS#11 Tests - -The following environment variables will influence the behaviour of test cases: - - PKCS11_MODULE, mandatory, points to the library/DLL to use for testing - - PKCS11_TOKEN_LABEL, mandatory, contains the token label - - PKCS11_TOKEN_PIN, optional (default is None), contains the PIN/passphrase of the token - - PKCS11_TOKEN_SO_PIN, optional (default is same as PKCS11_TOKEN_PIN), security officer PIN - - OPENSSL_PATH, optional, path to openssl executable (i.e. the folder that contains it) - -""" - -import os -import shutil -import unittest -from functools import wraps -from warnings import warn - -import pkcs11 - -try: - LIB = os.environ["PKCS11_MODULE"] -except KeyError as ex: - raise RuntimeError("Must define `PKCS11_MODULE' to run tests.") from ex - - -try: - TOKEN = os.environ["PKCS11_TOKEN_LABEL"] -except KeyError as ex: - raise RuntimeError("Must define `PKCS11_TOKEN_LABEL' to run tests.") from ex - -TOKEN_PIN = os.environ.get("PKCS11_TOKEN_PIN") # Can be None -if TOKEN_PIN is None: - warn("`PKCS11_TOKEN_PIN' env variable is unset.", stacklevel=2) - -TOKEN_SO_PIN = os.environ.get("PKCS11_TOKEN_SO_PIN") -if TOKEN_SO_PIN is None: - TOKEN_SO_PIN = TOKEN_PIN - warn( - "`PKCS11_TOKEN_SO_PIN' env variable is unset. Using value from `PKCS11_TOKEN_PIN'", - stacklevel=2, - ) - -OPENSSL = shutil.which("openssl", path=os.environ.get("OPENSSL_PATH")) -if OPENSSL is None: - warn("Path to OpenSSL not found. Please adjust `PATH' or define `OPENSSL_PATH'", stacklevel=2) - - -class TestCase(unittest.TestCase): - """Base test case, optionally creates a token and a session.""" - - with_token = True - """Creates a token for this test case.""" - with_session = True - """Creates a session for this test case.""" - - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.lib = lib = pkcs11.lib(LIB) - - if cls.with_token or cls.with_session: - cls.token = lib.get_token(token_label=TOKEN) - - def setUp(self): - super().setUp() - - if self.with_session: - self.session = self.token.open(user_pin=TOKEN_PIN) - - def tearDown(self): - if self.with_session: - self.session.close() - - super().tearDown() - - -def requires(*mechanisms): - """ - Decorates a function or class as requiring mechanisms, else they are - skipped. - """ - - def check_requirements(self): - """Determine what, if any, required mechanisms are unavailable.""" - unavailable = set(mechanisms) - self.token.slot.get_mechanisms() - - if unavailable: - raise unittest.SkipTest("Requires %s" % ", ".join(map(str, unavailable))) - - def inner(func): - @wraps(func) - def wrapper(self, *args, **kwargs): - check_requirements(self) - - return func(self, *args, **kwargs) - - return wrapper - - return inner - - -def xfail(condition): - """Mark a test that's expected to fail for a given condition.""" - - def inner(func): - if condition: - return unittest.expectedFailure(func) - - else: - return func - - return inner - - -class Is: - """ - Test what device we're using. - """ - - # trick: str.endswith() can accept tuples, - # see https://stackoverflow.com/questions/18351951/check-if-string-ends-with-one-of-the-strings-from-a-list - softhsm2 = LIB.lower().endswith( - ("libsofthsm2.so", "libsofthsm2.dylib", "softhsm2.dll", "softhsm2-x64.dll") - ) - nfast = LIB.lower().endswith(("libcknfast.so", "cknfast.dll")) - opencryptoki = LIB.endswith("libopencryptoki.so") - travis = os.environ.get("TRAVIS") == "true" - - -class Avail: - """ - Test if a resource is available - """ - - # openssl is searched across the exec path. Optionally, OPENSSL_PATH env variable can be defined - # in case there is no direct path to it (i.e. PATH does not point to it) - openssl = OPENSSL is not None - - -class Only: - """ - Limit tests to given conditions - """ - - softhsm2 = unittest.skipUnless(Is.softhsm2, "SoftHSMv2 only") - openssl = unittest.skipUnless(Avail.openssl, "openssl not found in the path") - - -class Not: - """ - Ignore tests for given devices - """ - - softhsm2 = unittest.skipIf(Is.softhsm2, "Not supported by SoftHSMv2") - nfast = unittest.skipIf(Is.nfast, "Not supported by nFast") - opencryptoki = unittest.skipIf(Is.opencryptoki, "Not supported by OpenCryptoki") - - -class FIXME: - """ - Tests is broken on this platform. - """ - - softhsm2 = xfail(Is.softhsm2) - nfast = xfail(Is.nfast) - opencryptoki = xfail(Is.opencryptoki) - travis = xfail(Is.travis) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..f9af88c --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,154 @@ +import os +import secrets +import shutil +import string +import subprocess +from pathlib import Path +from typing import Iterator +from unittest import mock +from warnings import warn + +import pytest +from _pytest.fixtures import SubRequest + +import pkcs11 + +ALLOWED_RANDOM_CHARS = string.ascii_letters + string.digits +LIB_PATH = os.environ.get("PKCS11_MODULE", "/usr/lib/softhsm/libsofthsm2.so") + +# trick: str.endswith() can accept tuples, +# see https://stackoverflow.com/questions/18351951/check-if-string-ends-with-one-of-the-strings-from-a-list +IS_SOFTHSM = LIB_PATH.lower().endswith( + ("libsofthsm2.so", "libsofthsm2.dylib", "softhsm2.dll", "softhsm2-x64.dll") +) +IS_NFAST = LIB_PATH.lower().endswith(("libcknfast.so", "cknfast.dll")) +IS_OPENCRYPTOKI = LIB_PATH.endswith("libopencryptoki.so") + +OPENSSL = shutil.which("openssl", path=os.environ.get("OPENSSL_PATH")) +if OPENSSL is None: + warn("Path to OpenSSL not found. Please adjust `PATH' or define `OPENSSL_PATH'", stacklevel=2) + + +def pytest_collection_modifyitems(items) -> None: + for item in items: + markers = [marker.name for marker in item.iter_markers()] + if "xfail_nfast" in markers and IS_NFAST: + item.add_marker( + pytest.mark.xfail(IS_NFAST, reason="Expected failure with nFast.", strict=True) + ) + if "xfail_softhsm" in markers and IS_SOFTHSM: + item.add_marker( + pytest.mark.xfail( + IS_SOFTHSM, reason="Expected failure with SoftHSMvs.", strict=True + ) + ) + if "xfail_opencryptoki" in markers: + item.add_marker( + pytest.mark.xfail( + IS_OPENCRYPTOKI, reason="Expected failure with OpenCryptoki.", strict=True + ) + ) + + +def get_random_string(length): + return "".join(secrets.choice(ALLOWED_RANDOM_CHARS) for i in range(length)) + + +@pytest.fixture(scope="session") +def lib(): + return pkcs11.lib(LIB_PATH) + + +@pytest.fixture +def softhsm_setup(tmp_path: Path) -> Iterator[Path]: # pragma: hsm + """Fixture to set up a unique SoftHSM2 configuration.""" + softhsm_dir = tmp_path / "softhsm" + token_dir = softhsm_dir / "tokens" + token_dir.mkdir(exist_ok=True, parents=True) + + softhsm2_conf = tmp_path / "softhsm2.conf" + print("# SoftHSMv2 conf:", softhsm2_conf) + + with open(softhsm2_conf, "w", encoding="utf-8") as stream: + stream.write(f"""# SoftHSM v2 configuration file + +directories.tokendir = {token_dir} +objectstore.backend = file + +# ERROR, WARNING, INFO, DEBUG +log.level = DEBUG + +# If CKF_REMOVABLE_DEVICE flag should be set +slots.removable = false + +# Enable and disable PKCS#11 mechanisms using slots.mechanisms. +slots.mechanisms = ALL + +# If the library should reset the state on fork +library.reset_on_fork = false""") + + with mock.patch.dict(os.environ, {"SOFTHSM2_CONF": str(softhsm2_conf)}): + yield softhsm_dir + + +@pytest.fixture +def so_pin() -> str: + return get_random_string(12) + + +@pytest.fixture +def pin() -> str: + return get_random_string(12) + + +@pytest.fixture +def softhsm_token(request: "SubRequest", lib, so_pin: str, pin: str) -> pkcs11.Token: + """Get a unique token for the current test.""" + request.getfixturevalue("softhsm_setup") + token = get_random_string(8) + + args = ( + "softhsm2-util", + "--init-token", + "--free", + "--label", + token, + "--so-pin", + so_pin, + "--pin", + pin, + ) + print("+", " ".join(args)) + subprocess.run(args, check=True) + + # Reinitialize library if already loaded (tokens are only seen after (re-)initialization). + lib.reinitialize() + + return lib.get_token(token_label=token) + + +@pytest.fixture +def softhsm_session(softhsm_token: pkcs11.Token, pin: str) -> Iterator[pkcs11.Session]: + session = softhsm_token.open(user_pin=pin) + yield session + session.close() + + +@pytest.fixture +def token(softhsm_token: pkcs11.Token) -> pkcs11.Token: + return softhsm_token + + +@pytest.fixture +def session( + request: "SubRequest", softhsm_session: pkcs11.Session, softhsm_token: pkcs11.Token +) -> pkcs11.Session: + # Skip test if session does not support required mechanisms + requirements = [mark.args[0] for mark in request.node.iter_markers(name="requires")] + if requirements: + unavailable = set(requirements) - softhsm_token.slot.get_mechanisms() + + if unavailable: + pytest.skip("Requires %s" % ", ".join(map(str, unavailable))) + + return softhsm_session diff --git a/tests/test_aes.py b/tests/test_aes.py index 3c7f2b0..f9ca94c 100644 --- a/tests/test_aes.py +++ b/tests/test_aes.py @@ -2,388 +2,397 @@ PKCS#11 AES Secret Keys """ -from parameterized import parameterized +import pytest import pkcs11 from pkcs11 import Mechanism -from . import FIXME, TestCase, requires - - -class AESTests(TestCase): - @requires(Mechanism.AES_KEY_GEN) - def setUp(self): - super().setUp() - self.key = self.session.generate_key(pkcs11.KeyType.AES, 128) - - @requires(Mechanism.AES_CBC_PAD) - def test_encrypt(self): - data = b"INPUT DATA" - iv = b"0" * 16 - - crypttext = self.key.encrypt(data, mechanism_param=iv) - self.assertIsInstance(crypttext, bytes) - self.assertNotEqual(data, crypttext) - # We should be aligned to the block size - self.assertEqual(len(crypttext), 16) - # Ensure we didn't just get 16 nulls - self.assertFalse(all(c == "\0" for c in crypttext)) - - text = self.key.decrypt(crypttext, mechanism_param=iv) - self.assertEqual(data, text) - - @requires(Mechanism.AES_CBC_PAD) - def test_encrypt_stream(self): - data = ( - b"I" * 16, - b"N" * 16, - b"P" * 16, - b"U" * 16, - b"T" * 10, # don't align to the blocksize - ) - iv = b"0" * 16 +pytestmark = [pytest.mark.requires(Mechanism.AES_KEY_GEN)] - cryptblocks = list(self.key.encrypt(data, mechanism_param=iv)) - self.assertEqual(len(cryptblocks), len(data) + 1) +@pytest.fixture +def key(session: pkcs11.Session) -> pkcs11.SecretKey: + return session.generate_key(pkcs11.KeyType.AES, 128) - crypttext = b"".join(cryptblocks) - self.assertNotEqual(b"".join(data), crypttext) - # We should be aligned to the block size - self.assertEqual(len(crypttext) % 16, 0) - # Ensure we didn't just get 16 nulls - self.assertFalse(all(c == "\0" for c in crypttext)) +@pytest.mark.requires(Mechanism.AES_CBC_PAD) +def test_encrypt(key: pkcs11.SecretKey) -> None: + data = b"INPUT DATA" + iv = b"0" * 16 - text = b"".join(self.key.decrypt(cryptblocks, mechanism_param=iv)) - self.assertEqual(b"".join(data), text) + crypttext = key.encrypt(data, mechanism_param=iv) + assert isinstance(crypttext, bytes) + assert data != crypttext + # We should be aligned to the block size + assert len(crypttext) == 16 + # Ensure we didn't just get 16 nulls + assert all(c == "\0" for c in crypttext) is False - @requires(Mechanism.AES_CBC_PAD) - def test_encrypt_whacky_sizes(self): - data = [(char * ord(char)).encode("utf-8") for char in "HELLO WORLD"] - iv = b"0" * 16 + text = key.decrypt(crypttext, mechanism_param=iv) + assert data == text - cryptblocks = list(self.key.encrypt(data, mechanism_param=iv)) - textblocks = list(self.key.decrypt(cryptblocks, mechanism_param=iv)) - self.assertEqual(b"".join(data), b"".join(textblocks)) +@pytest.mark.requires(Mechanism.AES_CBC_PAD) +def test_encrypt_stream(key: pkcs11.SecretKey): + data = ( + b"I" * 16, + b"N" * 16, + b"P" * 16, + b"U" * 16, + b"T" * 10, # don't align to the blocksize + ) + iv = b"0" * 16 - @requires(Mechanism.AES_CBC_PAD) - def test_encrypt_big_string(self): - data = b"HELLO WORLD" * 1024 + cryptblocks = list(key.encrypt(data, mechanism_param=iv)) - iv = self.session.generate_random(128) - crypttext = self.key.encrypt(data, mechanism_param=iv) - text = self.key.decrypt(crypttext, mechanism_param=iv) + assert len(cryptblocks) == len(data) + 1 - self.assertEqual(text, data) + crypttext = b"".join(cryptblocks) - @requires(Mechanism.AES_MAC) - def test_sign(self): - data = b"HELLO WORLD" + assert b"".join(data) != crypttext + # We should be aligned to the block size + assert len(crypttext) % 16 == 0 + # Ensure we didn't just get 16 nulls + assert all(c == "\0" for c in crypttext) is False - signature = self.key.sign(data) - self.assertIsNotNone(signature) - self.assertIsInstance(signature, bytes) - self.assertTrue(self.key.verify(data, signature)) - self.assertFalse(self.key.verify(data, b"1234")) + text = b"".join(key.decrypt(cryptblocks, mechanism_param=iv)) + assert b"".join(data) == text - @requires(Mechanism.AES_MAC) - def test_sign_stream(self): - data = ( - b"I" * 16, - b"N" * 16, - b"P" * 16, - b"U" * 16, - b"T" * 10, # don't align to the blocksize - ) - signature = self.key.sign(data) - self.assertIsNotNone(signature) - self.assertIsInstance(signature, bytes) - self.assertTrue(self.key.verify(data, signature)) +@pytest.mark.requires(Mechanism.AES_CBC_PAD) +def test_encrypt_whacky_sizes(key: pkcs11.SecretKey): + data = [(char * ord(char)).encode("utf-8") for char in "HELLO WORLD"] + iv = b"0" * 16 - @requires(Mechanism.AES_KEY_WRAP) - @FIXME.opencryptoki # can't set key attributes - def test_wrap(self): - key = self.session.generate_key( - pkcs11.KeyType.AES, - 128, - template={ - pkcs11.Attribute.EXTRACTABLE: True, - pkcs11.Attribute.SENSITIVE: False, - }, - ) - data = self.key.wrap_key(key) + cryptblocks = list(key.encrypt(data, mechanism_param=iv)) + textblocks = list(key.decrypt(cryptblocks, mechanism_param=iv)) + + assert b"".join(data) == b"".join(textblocks) + + +@pytest.mark.requires(Mechanism.AES_CBC_PAD) +def test_encrypt_big_string(session: pkcs11.Session, key: pkcs11.SecretKey): + data = b"HELLO WORLD" * 1024 + + iv = session.generate_random(128) + crypttext = key.encrypt(data, mechanism_param=iv) + text = key.decrypt(crypttext, mechanism_param=iv) + + assert text == data - key2 = self.key.unwrap_key( - pkcs11.ObjectClass.SECRET_KEY, - pkcs11.KeyType.AES, - data, - template={ - pkcs11.Attribute.EXTRACTABLE: True, - pkcs11.Attribute.SENSITIVE: False, - }, - ) - self.assertEqual(key[pkcs11.Attribute.VALUE], key2[pkcs11.Attribute.VALUE]) - - @parameterized.expand( - [ - ("POSITIVE_128_BIT", 128, 16, TestCase.assertIsNotNone), - ("POSITIVE_128_BIT_LONG_IV", 128, 32, TestCase.assertIsNotNone), - ("NEGATIVE_128_BIT_BAD_IV", 128, 15, TestCase.assertIsNone), - ("POSITIVE_256_BIT_LONG_IV", 256, 32, TestCase.assertIsNotNone), - ("NEGATIVE_256_BIT_SHORT_IV", 256, 16, TestCase.assertIsNone), - ("NEGATIVE_256_BIT_BAD_IV", 256, 31, TestCase.assertIsNone), - ] +@pytest.mark.requires(Mechanism.AES_MAC) +def test_sign(key: pkcs11.SecretKey): + data = b"HELLO WORLD" + + signature = key.sign(data) + assert isinstance(signature, bytes) + assert key.verify(data, signature) is True + assert key.verify(data, b"1234") is False + + +@pytest.mark.requires(Mechanism.AES_MAC) +def test_sign_stream(key: pkcs11.SecretKey): + data = ( + b"I" * 16, + b"N" * 16, + b"P" * 16, + b"U" * 16, + b"T" * 10, # don't align to the blocksize + ) + + signature = key.sign(data) + assert isinstance(signature, bytes) + assert key.verify(data, signature) + + +@pytest.mark.requires(Mechanism.AES_KEY_WRAP) +@pytest.mark.xfail_opencryptoki # can't set key attributes +def test_wrap(session: pkcs11.Session, key: pkcs11.SecretKey): + key = session.generate_key( + pkcs11.KeyType.AES, + 128, + template={ + pkcs11.Attribute.EXTRACTABLE: True, + pkcs11.Attribute.SENSITIVE: False, + }, + ) + data = key.wrap_key(key) + + key2 = key.unwrap_key( + pkcs11.ObjectClass.SECRET_KEY, + pkcs11.KeyType.AES, + data, + template={ + pkcs11.Attribute.EXTRACTABLE: True, + pkcs11.Attribute.SENSITIVE: False, + }, ) - @requires(Mechanism.AES_ECB_ENCRYPT_DATA) - @FIXME.opencryptoki # can't set key attributes - def test_derive_using_ecb_encrypt(self, test_type, test_key_length, iv_length, assert_fn): - """Function to test AES Key Derivation using the ECB_ENCRYPT Mechanism. - - Refer to Section 2.15 of http://docs.oasis-open.org/pkcs11/pkcs11-curr/v2.40/errata01/os/pkcs11-curr-v2.40-errata01-os-complete.html#_Toc441850521 - """ - - # Create the Master Key - capabilities = pkcs11.defaults.DEFAULT_KEY_CAPABILITIES[pkcs11.KeyType.AES] - capabilities |= pkcs11.MechanismFlag.DERIVE - key = self.session.generate_key( + + assert key[pkcs11.Attribute.VALUE] == key2[pkcs11.Attribute.VALUE] + + +@pytest.mark.parametrize( + ("test_key_length", "iv_length", "is_none"), + [ + (128, 16, False), + (128, 32, False), + (128, 15, True), + (256, 32, False), + (256, 16, True), + (256, 31, True), + ], +) +@pytest.mark.requires(Mechanism.AES_ECB_ENCRYPT_DATA) +@pytest.mark.xfail_opencryptoki # can't set key attributes +def test_derive_using_ecb_encrypt( + session: pkcs11.Session, + key: pkcs11.SecretKey, + test_key_length: int, + iv_length: int, + is_none: bool, +): + """Function to test AES Key Derivation using the ECB_ENCRYPT Mechanism. + + Refer to Section 2.15 of http://docs.oasis-open.org/pkcs11/pkcs11-curr/v2.40/errata01/os/pkcs11-curr-v2.40-errata01-os-complete.html#_Toc441850521 + """ + + # Create the Master Key + capabilities = pkcs11.defaults.DEFAULT_KEY_CAPABILITIES[pkcs11.KeyType.AES] + capabilities |= pkcs11.MechanismFlag.DERIVE + key = session.generate_key( + pkcs11.KeyType.AES, + key_length=test_key_length, + capabilities=capabilities, + template={ + pkcs11.Attribute.EXTRACTABLE: True, + pkcs11.Attribute.DERIVE: True, + pkcs11.Attribute.SENSITIVE: False, + }, + ) + + assert key is not None, "Failed to create {}-bit Master Key".format(test_key_length) + + # Derive a Key from the Master Key + iv = b"0" * iv_length + try: + derived_key = key.derive_key( pkcs11.KeyType.AES, key_length=test_key_length, capabilities=capabilities, + mechanism=Mechanism.AES_ECB_ENCRYPT_DATA, + mechanism_param=iv, template={ pkcs11.Attribute.EXTRACTABLE: True, - pkcs11.Attribute.DERIVE: True, pkcs11.Attribute.SENSITIVE: False, }, ) - - self.assertTrue( - key is not None, "Failed to create {}-bit Master Key".format(test_key_length) - ) - - # Derive a Key from the Master Key - iv = b"0" * iv_length - try: - derived_key = key.derive_key( - pkcs11.KeyType.AES, - key_length=test_key_length, - capabilities=capabilities, - mechanism=Mechanism.AES_ECB_ENCRYPT_DATA, - mechanism_param=iv, - template={ - pkcs11.Attribute.EXTRACTABLE: True, - pkcs11.Attribute.SENSITIVE: False, - }, - ) - except (pkcs11.exceptions.MechanismParamInvalid, pkcs11.exceptions.FunctionFailed): - derived_key = None - - assert_fn(self, derived_key, "{}-bit Key Derivation Failure".format(test_key_length)) - - @parameterized.expand( - [ - ("POSITIVE_128_BIT", 128, 16), - ("POSITIVE_256_BIT_LONG_IV", 256, 32), - ] + except (pkcs11.exceptions.MechanismParamInvalid, pkcs11.exceptions.FunctionFailed): + derived_key = None + + if is_none: + assert derived_key is None, "{}-bit Key Derivation Failure".format(test_key_length) + else: + assert derived_key is not None, "{}-bit Key Derivation Failure".format(test_key_length) + + +@pytest.mark.parametrize(("test_key_length", "iv_length"), [(128, 16), (256, 32)]) +@pytest.mark.requires(Mechanism.AES_ECB_ENCRYPT_DATA) +@pytest.mark.xfail_opencryptoki # can't set key attributes +def test_encrypt_with_key_derived_using_ecb_encrypt( + session: pkcs11.Session, key: pkcs11.SecretKey, test_key_length: int, iv_length: int +) -> None: + """Function to test Data Encryption/Decryption using a Derived AES Key. + + Function to test Data Encryption/Decryption using an AES Key + Derived by the ECB_ENCRYPT Mechanism. + + Refer to Section 2.15 of http://docs.oasis-open.org/pkcs11/pkcs11-curr/v2.40/errata01/os/pkcs11-curr-v2.40-errata01-os-complete.html#_Toc441850521 + """ + + # Create the Master Key + capabilities = pkcs11.defaults.DEFAULT_KEY_CAPABILITIES[pkcs11.KeyType.AES] + capabilities |= pkcs11.MechanismFlag.DERIVE + key = session.generate_key( + pkcs11.KeyType.AES, + key_length=test_key_length, + capabilities=capabilities, + template={ + pkcs11.Attribute.EXTRACTABLE: True, + pkcs11.Attribute.DERIVE: True, + pkcs11.Attribute.SENSITIVE: False, + }, ) - @requires(Mechanism.AES_ECB_ENCRYPT_DATA) - @FIXME.opencryptoki # can't set key attributes - def test_encrypt_with_key_derived_using_ecb_encrypt( - self, test_type, test_key_length, iv_length - ): - """Function to test Data Encryption/Decryption using a Derived AES Key. - - Function to test Data Encryption/Decryption using an AES Key - Derived by the ECB_ENCRYPT Mechanism. - Refer to Section 2.15 of http://docs.oasis-open.org/pkcs11/pkcs11-curr/v2.40/errata01/os/pkcs11-curr-v2.40-errata01-os-complete.html#_Toc441850521 - """ + assert key is not None, "Failed to create {}-bit Master Key".format(test_key_length) - # Create the Master Key - capabilities = pkcs11.defaults.DEFAULT_KEY_CAPABILITIES[pkcs11.KeyType.AES] - capabilities |= pkcs11.MechanismFlag.DERIVE - key = self.session.generate_key( + # Derive a Key from the Master Key + iv = b"0" * iv_length + try: + derived_key = key.derive_key( pkcs11.KeyType.AES, key_length=test_key_length, capabilities=capabilities, + mechanism=Mechanism.AES_ECB_ENCRYPT_DATA, + mechanism_param=iv, template={ pkcs11.Attribute.EXTRACTABLE: True, - pkcs11.Attribute.DERIVE: True, pkcs11.Attribute.SENSITIVE: False, }, ) - - self.assertTrue( - key is not None, "Failed to create {}-bit Master Key".format(test_key_length) - ) - - # Derive a Key from the Master Key - iv = b"0" * iv_length - try: - derived_key = key.derive_key( - pkcs11.KeyType.AES, - key_length=test_key_length, - capabilities=capabilities, - mechanism=Mechanism.AES_ECB_ENCRYPT_DATA, - mechanism_param=iv, - template={ - pkcs11.Attribute.EXTRACTABLE: True, - pkcs11.Attribute.SENSITIVE: False, - }, - ) - except (pkcs11.exceptions.MechanismParamInvalid, pkcs11.exceptions.FunctionFailed): - derived_key = None - - self.assertTrue( - derived_key is not None, "Failed to derive {}-bit Derived Key".format(test_key_length) - ) - - # Test capability of Key to Encrypt/Decrypt data - data = b"HELLO WORLD" * 1024 - - iv = self.session.generate_random(128) - crypttext = self.key.encrypt(data, mechanism_param=iv) - text = self.key.decrypt(crypttext, mechanism_param=iv) - - self.assertEqual(text, data) - - @parameterized.expand( - [ - ("POSITIVE_128_BIT", 128, 16, 16, TestCase.assertIsNotNone), - ("POSITIVE_128_BIT_LONG_DATA", 128, 16, 64, TestCase.assertIsNotNone), - ("NEGATIVE_128_BIT_BAD_IV", 128, 15, 16, TestCase.assertIsNone), - ("NEGATIVE_128_BIT_BAD_DATA", 128, 16, 31, TestCase.assertIsNone), - ("POSITIVE_256_BIT", 256, 16, 32, TestCase.assertIsNotNone), - ("POSITIVE_256_BIT_LONG_DATA", 256, 16, 64, TestCase.assertIsNotNone), - ("NEGATIVE_256_BIT_BAD_IV", 256, 15, 16, TestCase.assertIsNone), - ("NEGATIVE_256_BIT_BAD_DATA", 256, 16, 31, TestCase.assertIsNone), - ("NEGATIVE_256_BIT_SHORT_DATA", 256, 16, 16, TestCase.assertIsNone), - ] + except (pkcs11.exceptions.MechanismParamInvalid, pkcs11.exceptions.FunctionFailed): + derived_key = None + + assert derived_key is not None, "Failed to derive {}-bit Derived Key".format(test_key_length) + + # Test capability of Key to Encrypt/Decrypt data + data = b"HELLO WORLD" * 1024 + + iv = session.generate_random(128) + crypttext = key.encrypt(data, mechanism_param=iv) + text = key.decrypt(crypttext, mechanism_param=iv) + + assert text == data + + +@pytest.mark.parametrize( + ("test_key_length", "iv_length", "data_length", "is_none"), + [ + (128, 16, 16, False), + (128, 16, 64, False), + (128, 15, 16, True), + (128, 16, 31, True), + (256, 16, 32, False), + (256, 16, 64, False), + (256, 15, 16, True), + (256, 16, 31, True), + (256, 16, 16, True), + ], +) +@pytest.mark.requires(Mechanism.AES_CBC_ENCRYPT_DATA) +@pytest.mark.xfail_opencryptoki # can't set key attributes +def test_derive_using_cbc_encrypt( + session: pkcs11.Session, + key: pkcs11.SecretKey, + test_key_length: int, + iv_length: int, + data_length: int, + is_none: bool, +): + """Function to test AES Key Derivation using the CBC_ENCRYPT Mechanism. + + Refer to Section 2.15 of http://docs.oasis-open.org/pkcs11/pkcs11-curr/v2.40/errata01/os/pkcs11-curr-v2.40-errata01-os-complete.html#_Toc441850521 + """ + + # Create the Master Key + capabilities = pkcs11.defaults.DEFAULT_KEY_CAPABILITIES[pkcs11.KeyType.AES] + capabilities |= pkcs11.MechanismFlag.DERIVE + key = session.generate_key( + pkcs11.KeyType.AES, + key_length=test_key_length, + capabilities=capabilities, + template={ + pkcs11.Attribute.EXTRACTABLE: True, + pkcs11.Attribute.DERIVE: True, + pkcs11.Attribute.SENSITIVE: False, + }, ) - @requires(Mechanism.AES_CBC_ENCRYPT_DATA) - @FIXME.opencryptoki # can't set key attributes - def test_derive_using_cbc_encrypt( - self, test_type, test_key_length, iv_length, data_length, assert_fn - ): - """Function to test AES Key Derivation using the CBC_ENCRYPT Mechanism. - Refer to Section 2.15 of http://docs.oasis-open.org/pkcs11/pkcs11-curr/v2.40/errata01/os/pkcs11-curr-v2.40-errata01-os-complete.html#_Toc441850521 - """ + assert key is not None, "Failed to create {}-bit Master Key".format(test_key_length) - # Create the Master Key - capabilities = pkcs11.defaults.DEFAULT_KEY_CAPABILITIES[pkcs11.KeyType.AES] - capabilities |= pkcs11.MechanismFlag.DERIVE - key = self.session.generate_key( + # Derive a Key from the Master Key + iv = b"0" * iv_length + data = b"1" * data_length + try: + derived_key = key.derive_key( pkcs11.KeyType.AES, key_length=test_key_length, capabilities=capabilities, + mechanism=Mechanism.AES_CBC_ENCRYPT_DATA, + mechanism_param=(iv, data), template={ pkcs11.Attribute.EXTRACTABLE: True, - pkcs11.Attribute.DERIVE: True, pkcs11.Attribute.SENSITIVE: False, }, ) - - self.assertTrue( - key is not None, "Failed to create {}-bit Master Key".format(test_key_length) - ) - - # Derive a Key from the Master Key - iv = b"0" * iv_length - data = b"1" * data_length - try: - derived_key = key.derive_key( - pkcs11.KeyType.AES, - key_length=test_key_length, - capabilities=capabilities, - mechanism=Mechanism.AES_CBC_ENCRYPT_DATA, - mechanism_param=(iv, data), - template={ - pkcs11.Attribute.EXTRACTABLE: True, - pkcs11.Attribute.SENSITIVE: False, - }, - ) - except ( - pkcs11.exceptions.MechanismParamInvalid, - pkcs11.exceptions.FunctionFailed, - IndexError, - ): - derived_key = None - - assert_fn(self, derived_key, "{}-bit Key Derivation Failure".format(test_key_length)) - - @parameterized.expand( - [ - ("POSITIVE_128_BIT", 128, 16, 16), - ("POSITIVE_256_BIT", 256, 16, 32), - ("POSITIVE_256_BIT_LONG_DATA", 256, 16, 64), - ] - ) - @requires(Mechanism.AES_CBC_ENCRYPT_DATA) - @FIXME.opencryptoki # can't set key attributes - def test_encrypt_with_key_derived_using_cbc_encrypt( - self, test_type, test_key_length, iv_length, data_length + except ( + pkcs11.exceptions.MechanismParamInvalid, + pkcs11.exceptions.FunctionFailed, + IndexError, ): - """Function to test Data Encryption/Decryption using a Derived AES Key. - - Function to test Data Encryption/Decryption using an AES Key - Derived by the CBC_ENCRYPT Mechanism. + derived_key = None + + if is_none: + assert derived_key is None, "{}-bit Key Derivation Failure".format(test_key_length) + else: + assert derived_key is not None + + +@pytest.mark.parametrize( + ("test_key_length", "iv_length", "data_length"), [(128, 16, 16), (256, 16, 32), (256, 16, 64)] +) +@pytest.mark.requires(Mechanism.AES_CBC_ENCRYPT_DATA) +@pytest.mark.xfail_opencryptoki # can't set key attributes +def test_encrypt_with_key_derived_using_cbc_encrypt( + session: pkcs11.Session, + key: pkcs11.SecretKey, + test_key_length: int, + iv_length: int, + data_length: int, +): + """Function to test Data Encryption/Decryption using a Derived AES Key. + + Function to test Data Encryption/Decryption using an AES Key + Derived by the CBC_ENCRYPT Mechanism. + + Refer to Section 2.15 of http://docs.oasis-open.org/pkcs11/pkcs11-curr/v2.40/errata01/os/pkcs11-curr-v2.40-errata01-os-complete.html#_Toc441850521 + """ + + # Create the Master Key + capabilities = pkcs11.defaults.DEFAULT_KEY_CAPABILITIES[pkcs11.KeyType.AES] + capabilities |= pkcs11.MechanismFlag.DERIVE + key = session.generate_key( + pkcs11.KeyType.AES, + key_length=test_key_length, + capabilities=capabilities, + template={ + pkcs11.Attribute.EXTRACTABLE: True, + pkcs11.Attribute.DERIVE: True, + pkcs11.Attribute.SENSITIVE: False, + }, + ) - Refer to Section 2.15 of http://docs.oasis-open.org/pkcs11/pkcs11-curr/v2.40/errata01/os/pkcs11-curr-v2.40-errata01-os-complete.html#_Toc441850521 - """ + assert key is not None, "Failed to create {}-bit Master Key".format(test_key_length) - # Create the Master Key - capabilities = pkcs11.defaults.DEFAULT_KEY_CAPABILITIES[pkcs11.KeyType.AES] - capabilities |= pkcs11.MechanismFlag.DERIVE - key = self.session.generate_key( + # Derive a Key from the Master Key + iv = b"0" * iv_length + data = b"1" * data_length + try: + derived_key = key.derive_key( pkcs11.KeyType.AES, key_length=test_key_length, capabilities=capabilities, + mechanism=Mechanism.AES_CBC_ENCRYPT_DATA, + mechanism_param=(iv, data), template={ pkcs11.Attribute.EXTRACTABLE: True, - pkcs11.Attribute.DERIVE: True, pkcs11.Attribute.SENSITIVE: False, }, ) + except ( + pkcs11.exceptions.MechanismParamInvalid, + pkcs11.exceptions.FunctionFailed, + IndexError, + ): + derived_key = None - self.assertTrue( - key is not None, "Failed to create {}-bit Master Key".format(test_key_length) - ) - - # Derive a Key from the Master Key - iv = b"0" * iv_length - data = b"1" * data_length - try: - derived_key = key.derive_key( - pkcs11.KeyType.AES, - key_length=test_key_length, - capabilities=capabilities, - mechanism=Mechanism.AES_CBC_ENCRYPT_DATA, - mechanism_param=(iv, data), - template={ - pkcs11.Attribute.EXTRACTABLE: True, - pkcs11.Attribute.SENSITIVE: False, - }, - ) - except ( - pkcs11.exceptions.MechanismParamInvalid, - pkcs11.exceptions.FunctionFailed, - IndexError, - ): - derived_key = None - - self.assertTrue( - derived_key is not None, "Failed to derive {}-bit Derived Key".format(test_key_length) - ) + assert derived_key is not None, "Failed to derive {}-bit Derived Key".format(test_key_length) - # Test capability of Key to Encrypt/Decrypt data - data = b"HELLO WORLD" * 1024 + # Test capability of Key to Encrypt/Decrypt data + data = b"HELLO WORLD" * 1024 - iv = self.session.generate_random(128) - crypttext = self.key.encrypt(data, mechanism_param=iv) - text = self.key.decrypt(crypttext, mechanism_param=iv) + iv = session.generate_random(128) + crypttext = key.encrypt(data, mechanism_param=iv) + text = key.decrypt(crypttext, mechanism_param=iv) - self.assertEqual(text, data) + assert text == data diff --git a/tests/test_des.py b/tests/test_des.py index 0c04436..40a8ace 100644 --- a/tests/test_des.py +++ b/tests/test_des.py @@ -2,39 +2,41 @@ PKCS#11 DES Secret Keys """ +import pytest + import pkcs11 from pkcs11 import KeyType, Mechanism -from . import TestCase, requires +@pytest.mark.requires(Mechanism.DES2_KEY_GEN) +def test_generate_des2_key(session: pkcs11.Session): + key = session.generate_key(KeyType.DES2) + assert isinstance(key, pkcs11.SecretKey) + + +@pytest.mark.requires(Mechanism.DES3_KEY_GEN) +def test_generate_des3_key(session: pkcs11.Session): + key = session.generate_key(KeyType.DES3) + assert isinstance(key, pkcs11.SecretKey) -class DESTests(TestCase): - @requires(Mechanism.DES2_KEY_GEN) - def test_generate_des2_key(self): - key = self.session.generate_key(KeyType.DES2) - self.assertIsInstance(key, pkcs11.SecretKey) - @requires(Mechanism.DES3_KEY_GEN) - def test_generate_des3_key(self): - key = self.session.generate_key(KeyType.DES3) - self.assertIsInstance(key, pkcs11.SecretKey) +@pytest.mark.requires(Mechanism.DES2_KEY_GEN, Mechanism.DES3_CBC_PAD) +def test_encrypt_des2(session: pkcs11.Session): + key = session.generate_key(KeyType.DES2) - @requires(Mechanism.DES2_KEY_GEN, Mechanism.DES3_CBC_PAD) - def test_encrypt_des2(self): - key = self.session.generate_key(KeyType.DES2) + iv = session.generate_random(64) + crypttext = key.encrypt("PLAIN TEXT_", mechanism_param=iv) + plaintext = key.decrypt(crypttext, mechanism_param=iv) - iv = self.session.generate_random(64) - crypttext = key.encrypt("PLAIN TEXT_", mechanism_param=iv) - plaintext = key.decrypt(crypttext, mechanism_param=iv) + assert plaintext == b"PLAIN TEXT_" - self.assertEqual(plaintext, b"PLAIN TEXT_") - @requires(Mechanism.DES3_KEY_GEN, Mechanism.DES3_CBC_PAD) - def test_encrypt_des3(self): - key = self.session.generate_key(KeyType.DES3) +@pytest.mark.requires(Mechanism.DES3_KEY_GEN, Mechanism.DES3_CBC_PAD) +def test_encrypt_des3(session: pkcs11.Session): + key = session.generate_key(KeyType.DES3) - iv = self.session.generate_random(64) - crypttext = key.encrypt("PLAIN TEXT_", mechanism_param=iv) - plaintext = key.decrypt(crypttext, mechanism_param=iv) + iv = session.generate_random(64) + crypttext = key.encrypt("PLAIN TEXT_", mechanism_param=iv) + plaintext = key.decrypt(crypttext, mechanism_param=iv) - self.assertEqual(plaintext, b"PLAIN TEXT_") + assert plaintext == b"PLAIN TEXT_" diff --git a/tests/test_dh.py b/tests/test_dh.py index 59060a5..1ed2035 100644 --- a/tests/test_dh.py +++ b/tests/test_dh.py @@ -4,6 +4,9 @@ import base64 +import pytest + +import pkcs11 from pkcs11 import Attribute, DomainParameters, KeyType, Mechanism from pkcs11.util.dh import ( decode_dh_domain_parameters, @@ -11,377 +14,371 @@ encode_dh_public_key, ) -from . import FIXME, TestCase, requires +@pytest.mark.requires(Mechanism.DH_PKCS_KEY_PAIR_GEN, Mechanism.DH_PKCS_DERIVE) +@pytest.mark.xfail_opencryptoki # AttributeValueInvalid when generating keypair +def test_derive_key(session: pkcs11.Session) -> None: + # Alice and Bob each create a Diffie-Hellman keypair from the + # publicly available DH parameters + # + # E.g. RFC 3526, RFC 5114 or openssl dhparam -C 2236 + prime = [ + 0x0F, + 0x52, + 0xE5, + 0x24, + 0xF5, + 0xFA, + 0x9D, + 0xDC, + 0xC6, + 0xAB, + 0xE6, + 0x04, + 0xE4, + 0x20, + 0x89, + 0x8A, + 0xB4, + 0xBF, + 0x27, + 0xB5, + 0x4A, + 0x95, + 0x57, + 0xA1, + 0x06, + 0xE7, + 0x30, + 0x73, + 0x83, + 0x5E, + 0xC9, + 0x23, + 0x11, + 0xED, + 0x42, + 0x45, + 0xAC, + 0x49, + 0xD3, + 0xE3, + 0xF3, + 0x34, + 0x73, + 0xC5, + 0x7D, + 0x00, + 0x3C, + 0x86, + 0x63, + 0x74, + 0xE0, + 0x75, + 0x97, + 0x84, + 0x1D, + 0x0B, + 0x11, + 0xDA, + 0x04, + 0xD0, + 0xFE, + 0x4F, + 0xB0, + 0x37, + 0xDF, + 0x57, + 0x22, + 0x2E, + 0x96, + 0x42, + 0xE0, + 0x7C, + 0xD7, + 0x5E, + 0x46, + 0x29, + 0xAF, + 0xB1, + 0xF4, + 0x81, + 0xAF, + 0xFC, + 0x9A, + 0xEF, + 0xFA, + 0x89, + 0x9E, + 0x0A, + 0xFB, + 0x16, + 0xE3, + 0x8F, + 0x01, + 0xA2, + 0xC8, + 0xDD, + 0xB4, + 0x47, + 0x12, + 0xF8, + 0x29, + 0x09, + 0x13, + 0x6E, + 0x9D, + 0xA8, + 0xF9, + 0x5D, + 0x08, + 0x00, + 0x3A, + 0x8C, + 0xA7, + 0xFF, + 0x6C, + 0xCF, + 0xE3, + 0x7C, + 0x3B, + 0x6B, + 0xB4, + 0x26, + 0xCC, + 0xDA, + 0x89, + 0x93, + 0x01, + 0x73, + 0xA8, + 0x55, + 0x3E, + 0x5B, + 0x77, + 0x25, + 0x8F, + 0x27, + 0xA3, + 0xF1, + 0xBF, + 0x7A, + 0x73, + 0x1F, + 0x85, + 0x96, + 0x0C, + 0x45, + 0x14, + 0xC1, + 0x06, + 0xB7, + 0x1C, + 0x75, + 0xAA, + 0x10, + 0xBC, + 0x86, + 0x98, + 0x75, + 0x44, + 0x70, + 0xD1, + 0x0F, + 0x20, + 0xF4, + 0xAC, + 0x4C, + 0xB3, + 0x88, + 0x16, + 0x1C, + 0x7E, + 0xA3, + 0x27, + 0xE4, + 0xAD, + 0xE1, + 0xA1, + 0x85, + 0x4F, + 0x1A, + 0x22, + 0x0D, + 0x05, + 0x42, + 0x73, + 0x69, + 0x45, + 0xC9, + 0x2F, + 0xF7, + 0xC2, + 0x48, + 0xE3, + 0xCE, + 0x9D, + 0x74, + 0x58, + 0x53, + 0xE7, + 0xA7, + 0x82, + 0x18, + 0xD9, + 0x3D, + 0xAF, + 0xAB, + 0x40, + 0x9F, + 0xAA, + 0x4C, + 0x78, + 0x0A, + 0xC3, + 0x24, + 0x2D, + 0xDB, + 0x12, + 0xA9, + 0x54, + 0xE5, + 0x47, + 0x87, + 0xAC, + 0x52, + 0xFE, + 0xE8, + 0x3D, + 0x0B, + 0x56, + 0xED, + 0x9C, + 0x9F, + 0xFF, + 0x39, + 0xE5, + 0xE5, + 0xBF, + 0x62, + 0x32, + 0x42, + 0x08, + 0xAE, + 0x6A, + 0xED, + 0x88, + 0x0E, + 0xB3, + 0x1A, + 0x4C, + 0xD3, + 0x08, + 0xE4, + 0xC4, + 0xAA, + 0x2C, + 0xCC, + 0xB1, + 0x37, + 0xA5, + 0xC1, + 0xA9, + 0x64, + 0x7E, + 0xEB, + 0xF9, + 0xD3, + 0xF5, + 0x15, + 0x28, + 0xFE, + 0x2E, + 0xE2, + 0x7F, + 0xFE, + 0xD9, + 0xB9, + 0x38, + 0x42, + 0x57, + 0x03, + ] + parameters = session.create_domain_parameters( + KeyType.DH, {Attribute.PRIME: prime, Attribute.BASE: [0x2]}, local=True + ) + + # Alice generate a keypair + alice_public, alice_private = parameters.generate_keypair() + alice_value = alice_public[Attribute.VALUE] -class DHTests(TestCase): - @requires(Mechanism.DH_PKCS_KEY_PAIR_GEN, Mechanism.DH_PKCS_DERIVE) - @FIXME.opencryptoki # AttributeValueInvalid when generating keypair - def test_derive_key(self): - # Alice and Bob each create a Diffie-Hellman keypair from the - # publicly available DH parameters - # - # E.g. RFC 3526, RFC 5114 or openssl dhparam -C 2236 - prime = [ - 0x0F, - 0x52, - 0xE5, - 0x24, - 0xF5, - 0xFA, - 0x9D, - 0xDC, - 0xC6, - 0xAB, - 0xE6, - 0x04, - 0xE4, - 0x20, - 0x89, - 0x8A, - 0xB4, - 0xBF, - 0x27, - 0xB5, - 0x4A, - 0x95, - 0x57, - 0xA1, - 0x06, - 0xE7, - 0x30, - 0x73, - 0x83, - 0x5E, - 0xC9, - 0x23, - 0x11, - 0xED, - 0x42, - 0x45, - 0xAC, - 0x49, - 0xD3, - 0xE3, - 0xF3, - 0x34, - 0x73, - 0xC5, - 0x7D, - 0x00, - 0x3C, - 0x86, - 0x63, - 0x74, - 0xE0, - 0x75, - 0x97, - 0x84, - 0x1D, - 0x0B, - 0x11, - 0xDA, - 0x04, - 0xD0, - 0xFE, - 0x4F, - 0xB0, - 0x37, - 0xDF, - 0x57, - 0x22, - 0x2E, - 0x96, - 0x42, - 0xE0, - 0x7C, - 0xD7, - 0x5E, - 0x46, - 0x29, - 0xAF, - 0xB1, - 0xF4, - 0x81, - 0xAF, - 0xFC, - 0x9A, - 0xEF, - 0xFA, - 0x89, - 0x9E, - 0x0A, - 0xFB, - 0x16, - 0xE3, - 0x8F, - 0x01, - 0xA2, - 0xC8, - 0xDD, - 0xB4, - 0x47, - 0x12, - 0xF8, - 0x29, - 0x09, - 0x13, - 0x6E, - 0x9D, - 0xA8, - 0xF9, - 0x5D, - 0x08, - 0x00, - 0x3A, - 0x8C, - 0xA7, - 0xFF, - 0x6C, - 0xCF, - 0xE3, - 0x7C, - 0x3B, - 0x6B, - 0xB4, - 0x26, - 0xCC, - 0xDA, - 0x89, - 0x93, - 0x01, - 0x73, - 0xA8, - 0x55, - 0x3E, - 0x5B, - 0x77, - 0x25, - 0x8F, - 0x27, - 0xA3, - 0xF1, - 0xBF, - 0x7A, - 0x73, - 0x1F, - 0x85, - 0x96, - 0x0C, - 0x45, - 0x14, - 0xC1, - 0x06, - 0xB7, - 0x1C, - 0x75, - 0xAA, - 0x10, - 0xBC, - 0x86, - 0x98, - 0x75, - 0x44, - 0x70, - 0xD1, - 0x0F, - 0x20, - 0xF4, - 0xAC, - 0x4C, - 0xB3, - 0x88, - 0x16, - 0x1C, - 0x7E, - 0xA3, - 0x27, - 0xE4, - 0xAD, - 0xE1, - 0xA1, - 0x85, - 0x4F, - 0x1A, - 0x22, - 0x0D, - 0x05, - 0x42, - 0x73, - 0x69, - 0x45, - 0xC9, - 0x2F, - 0xF7, - 0xC2, - 0x48, - 0xE3, - 0xCE, - 0x9D, - 0x74, - 0x58, - 0x53, - 0xE7, - 0xA7, - 0x82, - 0x18, - 0xD9, - 0x3D, - 0xAF, - 0xAB, - 0x40, - 0x9F, - 0xAA, - 0x4C, - 0x78, - 0x0A, - 0xC3, - 0x24, - 0x2D, - 0xDB, - 0x12, - 0xA9, - 0x54, - 0xE5, - 0x47, - 0x87, - 0xAC, - 0x52, - 0xFE, - 0xE8, - 0x3D, - 0x0B, - 0x56, - 0xED, - 0x9C, - 0x9F, - 0xFF, - 0x39, - 0xE5, - 0xE5, - 0xBF, - 0x62, - 0x32, - 0x42, - 0x08, - 0xAE, - 0x6A, - 0xED, - 0x88, - 0x0E, - 0xB3, - 0x1A, - 0x4C, - 0xD3, - 0x08, - 0xE4, - 0xC4, - 0xAA, - 0x2C, - 0xCC, - 0xB1, - 0x37, - 0xA5, - 0xC1, - 0xA9, - 0x64, - 0x7E, - 0xEB, - 0xF9, - 0xD3, - 0xF5, - 0x15, - 0x28, - 0xFE, - 0x2E, - 0xE2, - 0x7F, - 0xFE, - 0xD9, - 0xB9, - 0x38, - 0x42, - 0x57, - 0x03, - ] - parameters = self.session.create_domain_parameters( - KeyType.DH, - { - Attribute.PRIME: prime, - Attribute.BASE: [0x2], - }, - local=True, - ) + # Bob generates a keypair + bob_public, bob_private = parameters.generate_keypair() + bob_value = bob_public[Attribute.VALUE] - # Alice generate a keypair - alice_public, alice_private = parameters.generate_keypair() - alice_value = alice_public[Attribute.VALUE] + assert alice_value != bob_value - # Bob generates a keypair - bob_public, bob_private = parameters.generate_keypair() - bob_value = bob_public[Attribute.VALUE] + # Alice and Bob exchange values and an IV ... + iv = session.generate_random(128) - self.assertNotEqual(alice_value, bob_value) + alice_session = alice_private.derive_key( + KeyType.AES, + 128, + mechanism_param=bob_value, + template={ + Attribute.SENSITIVE: False, + Attribute.EXTRACTABLE: True, + }, + ) + bob_session = bob_private.derive_key( + KeyType.AES, + 128, + mechanism_param=alice_value, + template={ + Attribute.SENSITIVE: False, + Attribute.EXTRACTABLE: True, + }, + ) - # Alice and Bob exchange values and an IV ... - iv = self.session.generate_random(128) + assert alice_session[Attribute.VALUE] == bob_session[Attribute.VALUE] - alice_session = alice_private.derive_key( - KeyType.AES, - 128, - mechanism_param=bob_value, - template={ - Attribute.SENSITIVE: False, - Attribute.EXTRACTABLE: True, - }, - ) - bob_session = bob_private.derive_key( - KeyType.AES, - 128, - mechanism_param=alice_value, - template={ - Attribute.SENSITIVE: False, - Attribute.EXTRACTABLE: True, - }, - ) + crypttext = alice_session.encrypt("HI BOB!", mechanism_param=iv) + plaintext = bob_session.decrypt(crypttext, mechanism_param=iv) + assert plaintext == b"HI BOB!" - self.assertEqual(alice_session[Attribute.VALUE], bob_session[Attribute.VALUE]) - crypttext = alice_session.encrypt("HI BOB!", mechanism_param=iv) - plaintext = bob_session.decrypt(crypttext, mechanism_param=iv) - self.assertEqual(plaintext, b"HI BOB!") +def test_load_params(session: pkcs11.Session) -> None: + # This is RFC5114 #2 + PARAMS = base64.b64decode(""" + MIICKQKCAQEArRB+HpEjqdDWYPqnlVnFH6INZOVoO5/RtUsVl7YdCnXm+hQd+VpW + 26+aPEB7od8V6z1oijCcGA4d5rhaEnSgpm0/gVKtasISkDfJ7e/aTfjZHo/vVbc5 + S3rVt9C2wSIHyfmNEe002/bGugssi7wnvmoA4KC5xJcIs7+KMXCRiDaBKGEwvImF + 2xYC5xRBXZMwJ4Jzx94x79xzEPcSH9WgdBWYfZrcCkhtzfk6zEQyg4cxXXXhmMZB + pIDNhqG55YfovmDmnMkosrnFIXLkEwQumyPxCw4W55djybU9z0uoCinj+3PBa451 + uX7zY+L/ox9xz53lOE5xuBwKxN/+DBDmTwKCAQEArEAy708tmuOd8wtcj/2sUGze + vnuJmYyvdIZqCM/k/+OmgkpOELmm8N2SHwGnDEr6q3OddwDCn1LFfbF8YgqGUr5e + kAGo1mrXwXZpEBmZAkr00CcnWsE0i7inYtBSG8mK4kcVBCLqHtQJk51U2nRgzbX2 + xrJQcXy+8YDrNBGOmNEZUppF1vg0Vm4wJeMWozDvu3eobwwasVsFGuPUKMj4rLcK + gTcVC47rEOGD7dGZY93Z4mPkdwWJ72qiHn9fL/OBtTnM40CdE81Wavu0jWwBkYHh + vP6UswJp7f5y/ptqpL17Wg8ccc//TBnEGOH27AF5gbwIfypwZbOEuJDTGR8r+gId + AIAcDTTFjZP+mXF3EB+AU1pHOM68vziambNjces= + """) - def test_load_params(self): - # This is RFC5114 #2 - PARAMS = base64.b64decode(""" - MIICKQKCAQEArRB+HpEjqdDWYPqnlVnFH6INZOVoO5/RtUsVl7YdCnXm+hQd+VpW - 26+aPEB7od8V6z1oijCcGA4d5rhaEnSgpm0/gVKtasISkDfJ7e/aTfjZHo/vVbc5 - S3rVt9C2wSIHyfmNEe002/bGugssi7wnvmoA4KC5xJcIs7+KMXCRiDaBKGEwvImF - 2xYC5xRBXZMwJ4Jzx94x79xzEPcSH9WgdBWYfZrcCkhtzfk6zEQyg4cxXXXhmMZB - pIDNhqG55YfovmDmnMkosrnFIXLkEwQumyPxCw4W55djybU9z0uoCinj+3PBa451 - uX7zY+L/ox9xz53lOE5xuBwKxN/+DBDmTwKCAQEArEAy708tmuOd8wtcj/2sUGze - vnuJmYyvdIZqCM/k/+OmgkpOELmm8N2SHwGnDEr6q3OddwDCn1LFfbF8YgqGUr5e - kAGo1mrXwXZpEBmZAkr00CcnWsE0i7inYtBSG8mK4kcVBCLqHtQJk51U2nRgzbX2 - xrJQcXy+8YDrNBGOmNEZUppF1vg0Vm4wJeMWozDvu3eobwwasVsFGuPUKMj4rLcK - gTcVC47rEOGD7dGZY93Z4mPkdwWJ72qiHn9fL/OBtTnM40CdE81Wavu0jWwBkYHh - vP6UswJp7f5y/ptqpL17Wg8ccc//TBnEGOH27AF5gbwIfypwZbOEuJDTGR8r+gId - AIAcDTTFjZP+mXF3EB+AU1pHOM68vziambNjces= - """) + params = session.create_domain_parameters( + KeyType.DH, decode_dh_domain_parameters(PARAMS), local=True + ) + assert isinstance(params, DomainParameters) + assert params[Attribute.PRIME][:4] == b"\xad\x10\x7e\x1e" - params = self.session.create_domain_parameters( - KeyType.DH, decode_dh_domain_parameters(PARAMS), local=True - ) - self.assertIsInstance(params, DomainParameters) - self.assertEqual(params[Attribute.PRIME][:4], b"\xad\x10\x7e\x1e") - @requires(Mechanism.DH_PKCS_PARAMETER_GEN, Mechanism.DH_PKCS_KEY_PAIR_GEN) - def test_generate_params(self): - params = self.session.generate_domain_parameters(KeyType.DH, 512) - self.assertIsInstance(params, DomainParameters) - self.assertEqual(params[Attribute.PRIME_BITS], 512) - self.assertEqual(len(params[Attribute.PRIME]) * 8, 512) - encode_dh_domain_parameters(params) +@pytest.mark.requires(Mechanism.DH_PKCS_PARAMETER_GEN, Mechanism.DH_PKCS_KEY_PAIR_GEN) +def test_generate_params(session: pkcs11.Session) -> None: + params = session.generate_domain_parameters(KeyType.DH, 512) + assert isinstance(params, DomainParameters) + assert params[Attribute.PRIME_BITS] == 512 + assert len(params[Attribute.PRIME]) * 8 == 512 + encode_dh_domain_parameters(params) - # Test encoding the public key - public, _ = params.generate_keypair() - encode_dh_public_key(public) + # Test encoding the public key + public, _ = params.generate_keypair() + encode_dh_public_key(public) diff --git a/tests/test_digest.py b/tests/test_digest.py index a6dc1b8..2313fcb 100644 --- a/tests/test_digest.py +++ b/tests/test_digest.py @@ -4,72 +4,67 @@ import hashlib +import pytest + +import pkcs11 from pkcs11 import Attribute, KeyType, Mechanism +from tests.conftest import IS_NFAST + + +@pytest.mark.requires(Mechanism.SHA256) +def test_digest(session: pkcs11.Session) -> None: + data = "THIS IS SOME DATA TO DIGEST" + digest = session.digest(data, mechanism=Mechanism.SHA256) + + assert digest == hashlib.sha256(data.encode("utf-8")).digest() + + +@pytest.mark.requires(Mechanism.SHA256) +def test_digest_generator(session: pkcs11.Session) -> None: + data = (b"This is ", b"some data ", b"to digest.") + + digest = session.digest(data, mechanism=Mechanism.SHA256) + + m = hashlib.sha256() + for d in data: + m.update(d) + + assert digest == m.digest() + + +@pytest.mark.requires(Mechanism.AES_KEY_GEN, Mechanism.SHA256) +@pytest.mark.skipif(IS_NFAST, reason="nFast can't digest keys") +def test_digest_key(session: pkcs11.Session) -> None: + key = session.generate_key( + KeyType.AES, 128, template={Attribute.SENSITIVE: False, Attribute.EXTRACTABLE: True} + ) + + digest = session.digest(key, mechanism=Mechanism.SHA256) + + assert digest == hashlib.sha256(key[Attribute.VALUE]).digest() + + +@pytest.mark.requires(Mechanism.AES_KEY_GEN, Mechanism.SHA256) +@pytest.mark.skipif(IS_NFAST, reason="nFast can't digest keys") +def test_digest_key_data(session: pkcs11.Session) -> None: + key = session.generate_key( + KeyType.AES, + 128, + template={ + Attribute.SENSITIVE: False, + Attribute.EXTRACTABLE: True, + }, + ) + + data = ( + b"Some data", + key, + ) + + digest = session.digest(data, mechanism=Mechanism.SHA256) + + m = hashlib.sha256() + m.update(data[0]) + m.update(data[1][Attribute.VALUE]) -from . import Not, TestCase, requires - - -class DigestTests(TestCase): - @requires(Mechanism.SHA256) - def test_digest(self): - data = "THIS IS SOME DATA TO DIGEST" - digest = self.session.digest(data, mechanism=Mechanism.SHA256) - - self.assertEqual(digest, hashlib.sha256(data.encode("utf-8")).digest()) - - @requires(Mechanism.SHA256) - def test_digest_generator(self): - data = ( - b"This is ", - b"some data ", - b"to digest.", - ) - - digest = self.session.digest(data, mechanism=Mechanism.SHA256) - - m = hashlib.sha256() - for d in data: - m.update(d) - - self.assertEqual(digest, m.digest()) - - @requires(Mechanism.AES_KEY_GEN, Mechanism.SHA256) - @Not.nfast # nFast can't digest keys - def test_digest_key(self): - key = self.session.generate_key( - KeyType.AES, - 128, - template={ - Attribute.SENSITIVE: False, - Attribute.EXTRACTABLE: True, - }, - ) - - digest = self.session.digest(key, mechanism=Mechanism.SHA256) - - self.assertEqual(digest, hashlib.sha256(key[Attribute.VALUE]).digest()) - - @requires(Mechanism.AES_KEY_GEN, Mechanism.SHA256) - @Not.nfast # nFast can't digest keys - def test_digest_key_data(self): - key = self.session.generate_key( - KeyType.AES, - 128, - template={ - Attribute.SENSITIVE: False, - Attribute.EXTRACTABLE: True, - }, - ) - - data = ( - b"Some data", - key, - ) - - digest = self.session.digest(data, mechanism=Mechanism.SHA256) - - m = hashlib.sha256() - m.update(data[0]) - m.update(data[1][Attribute.VALUE]) - - self.assertEqual(digest, m.digest()) + assert digest == m.digest() diff --git a/tests/test_dsa.py b/tests/test_dsa.py index 4a927d5..456ada1 100644 --- a/tests/test_dsa.py +++ b/tests/test_dsa.py @@ -4,14 +4,11 @@ import base64 +import pytest + import pkcs11 from pkcs11 import Attribute, KeyType, Mechanism -from pkcs11.util.dsa import ( - decode_dsa_domain_parameters, - encode_dsa_domain_parameters, -) - -from . import FIXME, TestCase, requires +from pkcs11.util.dsa import decode_dsa_domain_parameters, encode_dsa_domain_parameters DHPARAMS = base64.b64decode(""" MIIBHwKBgQD8jXSat2sk+j0plaMn51AVYBWEyWee3ui3llRUckVceDILsjVdBs1tXCDhU7WC+VZZ @@ -23,33 +20,34 @@ """) -class DSATests(TestCase): - @requires(Mechanism.DSA_PARAMETER_GEN) - @FIXME.nfast # returns Function Failed - def test_generate_params(self): - parameters = self.session.generate_domain_parameters(KeyType.DSA, 1024) - self.assertIsInstance(parameters, pkcs11.DomainParameters) - self.assertEqual(parameters[Attribute.PRIME_BITS], 1024) - - encode_dsa_domain_parameters(parameters) - - @requires(Mechanism.DSA_KEY_PAIR_GEN, Mechanism.DSA_SHA1) - def test_generate_keypair_and_sign(self): - dhparams = self.session.create_domain_parameters( - KeyType.DSA, decode_dsa_domain_parameters(DHPARAMS), local=True - ) - - public, private = dhparams.generate_keypair() - self.assertIsInstance(public, pkcs11.PublicKey) - self.assertIsInstance(private, pkcs11.PrivateKey) - self.assertEqual(len(public[Attribute.VALUE]), 1024 // 8) - - data = "Message to sign" - signature = private.sign(data, mechanism=Mechanism.DSA_SHA1) - self.assertTrue(public.verify(data, signature, mechanism=Mechanism.DSA_SHA1)) - - @requires(Mechanism.DSA_PARAMETER_GEN, Mechanism.DSA_KEY_PAIR_GEN) - @FIXME.nfast # returns Function Failed - def test_generate_keypair_directly(self): - public, private = self.session.generate_keypair(KeyType.DSA, 1024) - self.assertEqual(len(public[Attribute.VALUE]), 1024 // 8) +@pytest.mark.requires(Mechanism.DSA_PARAMETER_GEN) +@pytest.mark.xfail_nfast +def test_generate_params(session: pkcs11.Session) -> None: + parameters = session.generate_domain_parameters(KeyType.DSA, 1024) + assert isinstance(parameters, pkcs11.DomainParameters) + assert parameters[Attribute.PRIME_BITS] == 1024 + + encode_dsa_domain_parameters(parameters) + + +@pytest.mark.requires(Mechanism.DSA_KEY_PAIR_GEN, Mechanism.DSA_SHA1) +def test_generate_keypair_and_sign(session: pkcs11.Session): + dhparams = session.create_domain_parameters( + KeyType.DSA, decode_dsa_domain_parameters(DHPARAMS), local=True + ) + + public, private = dhparams.generate_keypair() + assert isinstance(public, pkcs11.PublicKey) + assert isinstance(private, pkcs11.PrivateKey) + assert len(public[Attribute.VALUE]) == 1024 // 8 + + data = "Message to sign" + signature = private.sign(data, mechanism=Mechanism.DSA_SHA1) + assert public.verify(data, signature, mechanism=Mechanism.DSA_SHA1) is True + + +@pytest.mark.xfail_nfast +@pytest.mark.requires(Mechanism.DSA_PARAMETER_GEN, Mechanism.DSA_KEY_PAIR_GEN) +def test_generate_keypair_directly(session: pkcs11.Session): + public, private = session.generate_keypair(KeyType.DSA, 1024) + assert len(public[Attribute.VALUE]) == 1024 // 8 diff --git a/tests/test_ecc.py b/tests/test_ecc.py index 39c7f29..10394b0 100644 --- a/tests/test_ecc.py +++ b/tests/test_ecc.py @@ -4,6 +4,8 @@ import base64 +import pytest + import pkcs11 from pkcs11 import KDF, Attribute, KeyType, Mechanism from pkcs11.util.ec import ( @@ -14,158 +16,156 @@ encode_named_curve_parameters, ) -from . import TestCase, requires - - -class ECCTests(TestCase): - @requires(Mechanism.EC_KEY_PAIR_GEN, Mechanism.ECDSA) - def test_sign_ecdsa(self): - parameters = self.session.create_domain_parameters( - KeyType.EC, - {Attribute.EC_PARAMS: encode_named_curve_parameters("secp256r1")}, - local=True, - ) - - pub, priv = parameters.generate_keypair() - - mechanism = Mechanism.ECDSA - data = b"HI BOB!" - ecdsa = priv.sign(data, mechanism=mechanism) - self.assertTrue(pub.verify(data, ecdsa, mechanism=mechanism)) - - @requires(Mechanism.EC_KEY_PAIR_GEN, Mechanism.ECDH1_DERIVE) - def test_derive_key(self): - # DER encoded EC params from OpenSSL - # openssl ecparam -out ec_param.der -name prime192v1 - ecparams = base64.b64decode(b"BggqhkjOPQMBAQ==") - - parameters = self.session.create_domain_parameters( - KeyType.EC, - { - Attribute.EC_PARAMS: ecparams, - }, - local=True, - ) - alice_pub, alice_priv = parameters.generate_keypair() - alice_value = alice_pub[Attribute.EC_POINT] - - bob_pub, bob_priv = parameters.generate_keypair() - bob_value = bob_pub[Attribute.EC_POINT] - - self.assertNotEqual(alice_value, bob_value) - - alice_session = alice_priv.derive_key( - KeyType.AES, 128, mechanism_param=(KDF.NULL, None, bob_value) - ) - - bob_session = bob_priv.derive_key( - KeyType.AES, 128, mechanism_param=(KDF.NULL, None, alice_value) - ) - - iv = self.session.generate_random(128) - crypttext = alice_session.encrypt("HI BOB!", mechanism_param=iv) - plaintext = bob_session.decrypt(crypttext, mechanism_param=iv) - self.assertEqual(plaintext, b"HI BOB!") - - @requires(Mechanism.ECDSA) - def test_import_key_params(self): - der = base64.b64decode(""" - MIICXDCCAc8GByqGSM49AgEwggHCAgEBME0GByqGSM49AQECQgH///////////// - //////////////////////////////////////////////////////////////// - /////////zCBngRCAf////////////////////////////////////////////// - ///////////////////////////////////////8BEFRlT65YY4cmh+SmiGgtoVA - 7qLacluZsxXzuLSJkY7xCeFWGTlR7H6TexZSwL07sb8HNXPfiD0sNPHvRR/Ua1A/ - AAMVANCeiAApHLhTlsxnFzkyhKqg2mS6BIGFBADGhY4GtwQE6c2ePstmI5W0Qpxk - gTkFP7Uh+CivYGtNPbqhS1537+dZKP4dwSei/6jeM0izwYVqQpv5fn4xwuW9ZgEY - OSlqeJo7wARcil+0LH0b2Zj1RElXm0RoF6+9Fyc+ZiyX7nKZXvQmQMVQuQE/rQdh - NTxwhqJywkCIvpR2n9FmUAJCAf////////////////////////////////////// - ////+lGGh4O/L5Zrf8wBSPcJpdA7tcm4iZxHrrtvtx6ROGQJAgEBA4GGAAQBMLgt - gTFBGr0f7YrWwZsCPpLxaUQvUKvz2C6ghiFmxc2EzBgxDY+ywnmG4T++EVZhJHTP - eIOnVRcHXXivkRe+YMQBbH/fZyqfCe41vIl39bwhqli839AAj/WoxXZuilpKaXBp - vGbx2380UIhrec1jFjItOOg/Xp9dOecjQZK7Z0wVq1U= - """) - key = self.session.create_object(decode_ec_public_key(der)) - self.assertIsInstance(key, pkcs11.PublicKey) - - # We should get back to identity - self.assertEqual(encode_ec_public_key(key), der) - - @requires(Mechanism.ECDSA_SHA1) - def test_import_key_named_curve(self): - der = base64.b64decode(""" - MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEa6Q5Hs+j71J1lc+VziafH+uL6603 - R8gTAphQD0iLG9Q9RgAvDQdFFpzkvXI+mEGVNRMmT/BA1OtficHcAXTdXA== - """) - key = self.session.create_object(decode_ec_public_key(der)) - self.assertIsInstance(key, pkcs11.PublicKey) - - # Something signed with OpenSSL - signature = base64.b64decode(""" - MEYCIQD1nDlli+uLuGX3eobKJe7PsRYkYJ4F15bjqbbB+MHewwIhAPGFRwyuFOvH - zuj+sxXwk1CsDWN7AXbmHufOlOarXpiq - """) - signature = decode_ecdsa_signature(signature) - - self.assertTrue(key.verify(b"Data to sign", signature, mechanism=Mechanism.ECDSA_SHA1)) - - # We should get back to identity - self.assertEqual(encode_ec_public_key(key), der) - - @requires(Mechanism.ECDSA) - def test_import_key_pair(self): - priv = base64.b64decode(""" - MIICnAIBAQRB9JsyE7khj/d2jm5RkE9T2DKgr/y3gn4Ju+8oWfdIpurNKM4hh3Oo - 0T+ilc0BEy/SfJ5iqUxU5TocdFRpOUzfUIKgggHGMIIBwgIBATBNBgcqhkjOPQEB - AkIB//////////////////////////////////////////////////////////// - //////////////////////////8wgZ4EQgH///////////////////////////// - /////////////////////////////////////////////////////////ARBUZU+ - uWGOHJofkpohoLaFQO6i2nJbmbMV87i0iZGO8QnhVhk5Uex+k3sWUsC9O7G/BzVz - 34g9LDTx70Uf1GtQPwADFQDQnogAKRy4U5bMZxc5MoSqoNpkugSBhQQAxoWOBrcE - BOnNnj7LZiOVtEKcZIE5BT+1Ifgor2BrTT26oUted+/nWSj+HcEnov+o3jNIs8GF - akKb+X5+McLlvWYBGDkpaniaO8AEXIpftCx9G9mY9URJV5tEaBevvRcnPmYsl+5y - mV70JkDFULkBP60HYTU8cIaicsJAiL6Udp/RZlACQgH///////////////////// - //////////////////////pRhoeDvy+Wa3/MAUj3CaXQO7XJuImcR667b7cekThk - CQIBAaGBiQOBhgAEATC4LYExQRq9H+2K1sGbAj6S8WlEL1Cr89guoIYhZsXNhMwY - MQ2PssJ5huE/vhFWYSR0z3iDp1UXB114r5EXvmDEAWx/32cqnwnuNbyJd/W8IapY - vN/QAI/1qMV2bopaSmlwabxm8dt/NFCIa3nNYxYyLTjoP16fXTnnI0GSu2dMFatV - """) - priv = self.session.create_object(decode_ec_private_key(priv)) - - pub = base64.b64decode(""" - MIICXDCCAc8GByqGSM49AgEwggHCAgEBME0GByqGSM49AQECQgH///////////// - //////////////////////////////////////////////////////////////// - /////////zCBngRCAf////////////////////////////////////////////// - ///////////////////////////////////////8BEFRlT65YY4cmh+SmiGgtoVA - 7qLacluZsxXzuLSJkY7xCeFWGTlR7H6TexZSwL07sb8HNXPfiD0sNPHvRR/Ua1A/ - AAMVANCeiAApHLhTlsxnFzkyhKqg2mS6BIGFBADGhY4GtwQE6c2ePstmI5W0Qpxk - gTkFP7Uh+CivYGtNPbqhS1537+dZKP4dwSei/6jeM0izwYVqQpv5fn4xwuW9ZgEY - OSlqeJo7wARcil+0LH0b2Zj1RElXm0RoF6+9Fyc+ZiyX7nKZXvQmQMVQuQE/rQdh - NTxwhqJywkCIvpR2n9FmUAJCAf////////////////////////////////////// - ////+lGGh4O/L5Zrf8wBSPcJpdA7tcm4iZxHrrtvtx6ROGQJAgEBA4GGAAQBMLgt - gTFBGr0f7YrWwZsCPpLxaUQvUKvz2C6ghiFmxc2EzBgxDY+ywnmG4T++EVZhJHTP - eIOnVRcHXXivkRe+YMQBbH/fZyqfCe41vIl39bwhqli839AAj/WoxXZuilpKaXBp - vGbx2380UIhrec1jFjItOOg/Xp9dOecjQZK7Z0wVq1U= - """) - pub = self.session.create_object(decode_ec_public_key(pub)) - - signature = priv.sign(b"Example", mechanism=Mechanism.ECDSA) - self.assertTrue(pub.verify(b"Example", signature, mechanism=Mechanism.ECDSA)) - - @requires(Mechanism.EC_EDWARDS_KEY_PAIR_GEN, Mechanism.EDDSA) - def test_sign_eddsa(self): - parameters = self.session.create_domain_parameters( - KeyType.EC_EDWARDS, - { - # use "Ed25519" once https://github.com/wbond/asn1crypto/pull/134 - # is merged - Attribute.EC_PARAMS: encode_named_curve_parameters("1.3.101.112") - }, - local=True, - ) - - pub, priv = parameters.generate_keypair() - - mechanism = Mechanism.EDDSA - data = b"HI BOB!" - eddsa = priv.sign(data, mechanism=mechanism) - self.assertTrue(pub.verify(data, eddsa, mechanism=mechanism)) + +@pytest.mark.requires(Mechanism.EC_KEY_PAIR_GEN, Mechanism.ECDSA) +def test_sign_ecdsa(session: pkcs11.Session) -> None: + parameters = session.create_domain_parameters( + KeyType.EC, + {Attribute.EC_PARAMS: encode_named_curve_parameters("secp256r1")}, + local=True, + ) + + pub, priv = parameters.generate_keypair() + + mechanism = Mechanism.ECDSA + data = b"HI BOB!" + ecdsa = priv.sign(data, mechanism=mechanism) + assert pub.verify(data, ecdsa, mechanism=mechanism) + + +@pytest.mark.requires(Mechanism.EC_KEY_PAIR_GEN, Mechanism.ECDH1_DERIVE) +def test_derive_key(session: pkcs11.Session) -> None: + # DER encoded EC params from OpenSSL + # openssl ecparam -out ec_param.der -name prime192v1 + ecparams = base64.b64decode(b"BggqhkjOPQMBAQ==") + + parameters = session.create_domain_parameters( + KeyType.EC, {Attribute.EC_PARAMS: ecparams}, local=True + ) + alice_pub, alice_priv = parameters.generate_keypair() + alice_value = alice_pub[Attribute.EC_POINT] + + bob_pub, bob_priv = parameters.generate_keypair() + bob_value = bob_pub[Attribute.EC_POINT] + + assert alice_value != bob_value + + alice_session = alice_priv.derive_key( + KeyType.AES, 128, mechanism_param=(KDF.NULL, None, bob_value) + ) + + bob_session = bob_priv.derive_key( + KeyType.AES, 128, mechanism_param=(KDF.NULL, None, alice_value) + ) + + iv = session.generate_random(128) + crypttext = alice_session.encrypt("HI BOB!", mechanism_param=iv) + plaintext = bob_session.decrypt(crypttext, mechanism_param=iv) + assert plaintext == b"HI BOB!" + + +@pytest.mark.requires(Mechanism.ECDSA) +def test_import_key_params(session: pkcs11.Session) -> None: + der = base64.b64decode(""" + MIICXDCCAc8GByqGSM49AgEwggHCAgEBME0GByqGSM49AQECQgH///////////// + //////////////////////////////////////////////////////////////// + /////////zCBngRCAf////////////////////////////////////////////// + ///////////////////////////////////////8BEFRlT65YY4cmh+SmiGgtoVA + 7qLacluZsxXzuLSJkY7xCeFWGTlR7H6TexZSwL07sb8HNXPfiD0sNPHvRR/Ua1A/ + AAMVANCeiAApHLhTlsxnFzkyhKqg2mS6BIGFBADGhY4GtwQE6c2ePstmI5W0Qpxk + gTkFP7Uh+CivYGtNPbqhS1537+dZKP4dwSei/6jeM0izwYVqQpv5fn4xwuW9ZgEY + OSlqeJo7wARcil+0LH0b2Zj1RElXm0RoF6+9Fyc+ZiyX7nKZXvQmQMVQuQE/rQdh + NTxwhqJywkCIvpR2n9FmUAJCAf////////////////////////////////////// + ////+lGGh4O/L5Zrf8wBSPcJpdA7tcm4iZxHrrtvtx6ROGQJAgEBA4GGAAQBMLgt + gTFBGr0f7YrWwZsCPpLxaUQvUKvz2C6ghiFmxc2EzBgxDY+ywnmG4T++EVZhJHTP + eIOnVRcHXXivkRe+YMQBbH/fZyqfCe41vIl39bwhqli839AAj/WoxXZuilpKaXBp + vGbx2380UIhrec1jFjItOOg/Xp9dOecjQZK7Z0wVq1U= + """) + key = session.create_object(decode_ec_public_key(der)) + assert isinstance(key, pkcs11.PublicKey) + + # We should get back to identity + assert encode_ec_public_key(key) == der + + +@pytest.mark.requires(Mechanism.ECDSA_SHA1) +def test_import_key_named_curve(session: pkcs11.Session) -> None: + der = base64.b64decode(""" + MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEa6Q5Hs+j71J1lc+VziafH+uL6603 + R8gTAphQD0iLG9Q9RgAvDQdFFpzkvXI+mEGVNRMmT/BA1OtficHcAXTdXA== + """) + key = session.create_object(decode_ec_public_key(der)) + assert isinstance(key, pkcs11.PublicKey) + + # Something signed with OpenSSL + signature = base64.b64decode(""" + MEYCIQD1nDlli+uLuGX3eobKJe7PsRYkYJ4F15bjqbbB+MHewwIhAPGFRwyuFOvH + zuj+sxXwk1CsDWN7AXbmHufOlOarXpiq + """) + signature = decode_ecdsa_signature(signature) + + assert key.verify(b"Data to sign", signature, mechanism=Mechanism.ECDSA_SHA1) + + # We should get back to identity + assert encode_ec_public_key(key) == der + + +@pytest.mark.requires(Mechanism.ECDSA) +def test_import_key_pair(session: pkcs11.Session) -> None: + priv = base64.b64decode(""" + MIICnAIBAQRB9JsyE7khj/d2jm5RkE9T2DKgr/y3gn4Ju+8oWfdIpurNKM4hh3Oo + 0T+ilc0BEy/SfJ5iqUxU5TocdFRpOUzfUIKgggHGMIIBwgIBATBNBgcqhkjOPQEB + AkIB//////////////////////////////////////////////////////////// + //////////////////////////8wgZ4EQgH///////////////////////////// + /////////////////////////////////////////////////////////ARBUZU+ + uWGOHJofkpohoLaFQO6i2nJbmbMV87i0iZGO8QnhVhk5Uex+k3sWUsC9O7G/BzVz + 34g9LDTx70Uf1GtQPwADFQDQnogAKRy4U5bMZxc5MoSqoNpkugSBhQQAxoWOBrcE + BOnNnj7LZiOVtEKcZIE5BT+1Ifgor2BrTT26oUted+/nWSj+HcEnov+o3jNIs8GF + akKb+X5+McLlvWYBGDkpaniaO8AEXIpftCx9G9mY9URJV5tEaBevvRcnPmYsl+5y + mV70JkDFULkBP60HYTU8cIaicsJAiL6Udp/RZlACQgH///////////////////// + //////////////////////pRhoeDvy+Wa3/MAUj3CaXQO7XJuImcR667b7cekThk + CQIBAaGBiQOBhgAEATC4LYExQRq9H+2K1sGbAj6S8WlEL1Cr89guoIYhZsXNhMwY + MQ2PssJ5huE/vhFWYSR0z3iDp1UXB114r5EXvmDEAWx/32cqnwnuNbyJd/W8IapY + vN/QAI/1qMV2bopaSmlwabxm8dt/NFCIa3nNYxYyLTjoP16fXTnnI0GSu2dMFatV + """) + priv = session.create_object(decode_ec_private_key(priv)) + + pub = base64.b64decode(""" + MIICXDCCAc8GByqGSM49AgEwggHCAgEBME0GByqGSM49AQECQgH///////////// + //////////////////////////////////////////////////////////////// + /////////zCBngRCAf////////////////////////////////////////////// + ///////////////////////////////////////8BEFRlT65YY4cmh+SmiGgtoVA + 7qLacluZsxXzuLSJkY7xCeFWGTlR7H6TexZSwL07sb8HNXPfiD0sNPHvRR/Ua1A/ + AAMVANCeiAApHLhTlsxnFzkyhKqg2mS6BIGFBADGhY4GtwQE6c2ePstmI5W0Qpxk + gTkFP7Uh+CivYGtNPbqhS1537+dZKP4dwSei/6jeM0izwYVqQpv5fn4xwuW9ZgEY + OSlqeJo7wARcil+0LH0b2Zj1RElXm0RoF6+9Fyc+ZiyX7nKZXvQmQMVQuQE/rQdh + NTxwhqJywkCIvpR2n9FmUAJCAf////////////////////////////////////// + ////+lGGh4O/L5Zrf8wBSPcJpdA7tcm4iZxHrrtvtx6ROGQJAgEBA4GGAAQBMLgt + gTFBGr0f7YrWwZsCPpLxaUQvUKvz2C6ghiFmxc2EzBgxDY+ywnmG4T++EVZhJHTP + eIOnVRcHXXivkRe+YMQBbH/fZyqfCe41vIl39bwhqli839AAj/WoxXZuilpKaXBp + vGbx2380UIhrec1jFjItOOg/Xp9dOecjQZK7Z0wVq1U= + """) + pub = session.create_object(decode_ec_public_key(pub)) + + signature = priv.sign(b"Example", mechanism=Mechanism.ECDSA) + assert pub.verify(b"Example", signature, mechanism=Mechanism.ECDSA) + + +@pytest.mark.requires(Mechanism.EC_EDWARDS_KEY_PAIR_GEN, Mechanism.EDDSA) +def test_sign_eddsa(session: pkcs11.Session) -> None: + parameters = session.create_domain_parameters( + KeyType.EC_EDWARDS, + { + # use "Ed25519" once https://github.com/wbond/asn1crypto/pull/134 + # is merged + Attribute.EC_PARAMS: encode_named_curve_parameters("1.3.101.112") + }, + local=True, + ) + + pub, priv = parameters.generate_keypair() + + mechanism = Mechanism.EDDSA + data = b"HI BOB!" + eddsa = priv.sign(data, mechanism=mechanism) + assert pub.verify(data, eddsa, mechanism=mechanism) diff --git a/tests/test_iterators.py b/tests/test_iterators.py index 9456e90..f9676ea 100644 --- a/tests/test_iterators.py +++ b/tests/test_iterators.py @@ -2,53 +2,48 @@ Iterator tests """ -import unittest +import pytest import pkcs11 -from . import TestCase, requires +@pytest.mark.requires(pkcs11.Mechanism.AES_KEY_GEN, pkcs11.Mechanism.AES_CBC_PAD) +def test_partial_decrypt(session: pkcs11.Session) -> None: + session.generate_key(pkcs11.KeyType.AES, 128, label="LOOK ME UP") -class IteratorTests(TestCase): - @requires(pkcs11.Mechanism.AES_KEY_GEN, pkcs11.Mechanism.AES_CBC_PAD) - def test_partial_decrypt(self): - self.session.generate_key(pkcs11.KeyType.AES, 128, label="LOOK ME UP") + key = session.get_key(label="LOOK ME UP") + data = (b"1234", b"1234") - key = self.session.get_key(label="LOOK ME UP") - data = ( - b"1234", - b"1234", - ) + iv = session.generate_random(128) + encrypted_data = list(key.encrypt(data, mechanism_param=iv)) - iv = self.session.generate_random(128) - encrypted_data = list(key.encrypt(data, mechanism_param=iv)) + iter1 = key.decrypt(encrypted_data, mechanism_param=iv) + next(iter1) - iter1 = key.decrypt(encrypted_data, mechanism_param=iv) - next(iter1) + iter2 = key.decrypt(encrypted_data, mechanism_param=iv) + with pytest.raises(pkcs11.OperationActive): + next(iter2) - with self.assertRaises(pkcs11.OperationActive): - iter2 = key.decrypt(encrypted_data, mechanism_param=iv) - 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") +@pytest.mark.requires(pkcs11.Mechanism.AES_KEY_GEN, pkcs11.Mechanism.AES_CBC_PAD) +# Ideally deleting iterator #1 would terminate the operation, but it +# currently does not. +@pytest.mark.xfail +def test_close_iterators(session: pkcs11.Session) -> None: + session.generate_key(pkcs11.KeyType.AES, 128, label="LOOK ME UP") - key = self.session.get_key(label="LOOK ME UP") - data = ( - b"1234", - b"1234", - ) + key = session.get_key(label="LOOK ME UP") + data = ( + b"1234", + b"1234", + ) - iv = self.session.generate_random(128) - encrypted_data = list(key.encrypt(data, mechanism_param=iv)) + iv = session.generate_random(128) + encrypted_data = list(key.encrypt(data, mechanism_param=iv)) - iter1 = key.decrypt(encrypted_data, mechanism_param=iv) - next(iter1) - del iter1 + iter1 = key.decrypt(encrypted_data, mechanism_param=iv) + next(iter1) + del iter1 - iter2 = key.decrypt(encrypted_data, mechanism_param=iv) - next(iter2) + iter2 = key.decrypt(encrypted_data, mechanism_param=iv) + next(iter2) diff --git a/tests/test_public_key_external.py b/tests/test_public_key_external.py index d211f53..c575a68 100644 --- a/tests/test_public_key_external.py +++ b/tests/test_public_key_external.py @@ -1,3 +1,6 @@ +import pytest + +import pkcs11 from pkcs11 import KDF, Attribute, KeyType, Mechanism, ObjectClass from pkcs11.util.ec import ( decode_ec_public_key, @@ -6,162 +9,163 @@ encode_named_curve_parameters, ) from pkcs11.util.rsa import encode_rsa_public_key - -from . import Is, TestCase, requires - - -class ExternalPublicKeyTests(TestCase): - @requires(Mechanism.RSA_PKCS) - def test_rsa(self): - # A key we generated earlier - self.session.generate_keypair(KeyType.RSA, 1024) - - pub = self.session.get_key(key_type=KeyType.RSA, object_class=ObjectClass.PUBLIC_KEY) - - pub = encode_rsa_public_key(pub) - - from oscrypto.asymmetric import load_public_key, rsa_pkcs1v15_encrypt - - pub = load_public_key(pub) - crypttext = rsa_pkcs1v15_encrypt(pub, b"Data to encrypt") - - priv = self.session.get_key(key_type=KeyType.RSA, object_class=ObjectClass.PRIVATE_KEY) - - plaintext = priv.decrypt(crypttext, mechanism=Mechanism.RSA_PKCS) - - self.assertEqual(plaintext, b"Data to encrypt") - - @requires(Mechanism.ECDSA_SHA1) - def test_ecdsa(self): - # A key we generated earlier - self.session.create_domain_parameters( - KeyType.EC, - { - Attribute.EC_PARAMS: encode_named_curve_parameters("secp256r1"), - }, - local=True, - ).generate_keypair() - - priv = self.session.get_key(key_type=KeyType.EC, object_class=ObjectClass.PRIVATE_KEY) - - signature = priv.sign(b"Data to sign", mechanism=Mechanism.ECDSA_SHA1) - # Encode as ASN.1 for OpenSSL - signature = encode_ecdsa_signature(signature) - - from oscrypto.asymmetric import ecdsa_verify, load_public_key - - pub = self.session.get_key(key_type=KeyType.EC, object_class=ObjectClass.PUBLIC_KEY) - pub = load_public_key(encode_ec_public_key(pub)) - - ecdsa_verify(pub, signature, b"Data to sign", "sha1") - - @requires(Mechanism.ECDH1_DERIVE) - def test_ecdh(self): - # A key we generated earlier - self.session.create_domain_parameters( - KeyType.EC, - { - Attribute.EC_PARAMS: encode_named_curve_parameters("secp256r1"), - }, - local=True, - ).generate_keypair() - - # Retrieve our keypair, with our public key encoded for interchange - alice_priv = self.session.get_key(key_type=KeyType.EC, object_class=ObjectClass.PRIVATE_KEY) - alice_pub = self.session.get_key(key_type=KeyType.EC, object_class=ObjectClass.PUBLIC_KEY) - alice_pub = encode_ec_public_key(alice_pub) - - from cryptography.hazmat.backends import default_backend - from cryptography.hazmat.primitives.asymmetric import ec - from cryptography.hazmat.primitives.serialization import ( - Encoding, - PublicFormat, - load_der_public_key, - ) - - # Bob generates a keypair, with their public key encoded for - # interchange - bob_priv = ec.generate_private_key(ec.SECP256R1(), default_backend()) - bob_pub = bob_priv.public_key().public_bytes( - Encoding.DER, - PublicFormat.SubjectPublicKeyInfo, - ) - - # Bob converts Alice's key to internal format and generates their - # shared key - bob_shared_key = bob_priv.exchange( - ec.ECDH(), - load_der_public_key(alice_pub, default_backend()), - ) - - key = alice_priv.derive_key( - KeyType.GENERIC_SECRET, - 256, - mechanism_param=( - KDF.NULL, - None, - # N.B. it seems like SoftHSMv2 requires an EC_POINT to be - # DER-encoded, which is not what the spec says - decode_ec_public_key(bob_pub, encode_ec_point=Is.softhsm2)[Attribute.EC_POINT], - ), - template={ - Attribute.SENSITIVE: False, - Attribute.EXTRACTABLE: True, - }, - ) - alice_shared_key = key[Attribute.VALUE] - - # We should have the same shared key - self.assertEqual(bob_shared_key, alice_shared_key) - - @requires(Mechanism.RSA_PKCS) - def test_terrible_hybrid_file_encryption_app(self): - # Proof of concept code only! - import io - - from oscrypto.asymmetric import load_public_key, rsa_pkcs1v15_encrypt - from oscrypto.symmetric import ( - aes_cbc_pkcs7_decrypt, - aes_cbc_pkcs7_encrypt, - ) - - # A key we generated earlier - self.session.generate_keypair(KeyType.RSA, 1024) - - pub = self.session.get_key(key_type=KeyType.RSA, object_class=ObjectClass.PUBLIC_KEY) - pub = load_public_key(encode_rsa_public_key(pub)) - - key = self.session.generate_random(256) - iv = self.session.generate_random(128) - - source = b"This is my amazing file" - - with io.BytesIO() as dest: - # Write a 128-byte header containing our key and our IV - # strictly speaking we don't need to keep the IV secure but - # we may as well. - # - # FIXME: Because this is RSA 1.5, we should fill the rest of the - # frame with nonsense - self.assertEqual(dest.write(rsa_pkcs1v15_encrypt(pub, key + iv)), 128) - _, ciphertext = aes_cbc_pkcs7_encrypt(key, source, iv) - dest.write(ciphertext) - - # Time passes - dest.seek(0) - - # Look up our private key - priv = self.session.get_key(key_type=KeyType.RSA, object_class=ObjectClass.PRIVATE_KEY) - # Read the header - header = dest.read(priv.key_length // 8) - header = priv.decrypt(header, mechanism=Mechanism.RSA_PKCS) - - # The first 32 bytes is our key - key, header = header[:32], header[32:] - # The next 16 bytes is the IV - iv = header[:16] - # We can ignore the rest - - plaintext = aes_cbc_pkcs7_decrypt(key, dest.read(), iv) - - self.assertEqual(source, plaintext) +from tests.conftest import IS_SOFTHSM + + +@pytest.mark.requires(Mechanism.RSA_PKCS) +def test_rsa(session: pkcs11.Session) -> None: + # A key we generated earlier + session.generate_keypair(KeyType.RSA, 1024) + + pub = session.get_key(key_type=KeyType.RSA, object_class=ObjectClass.PUBLIC_KEY) + + pub = encode_rsa_public_key(pub) + + from oscrypto.asymmetric import load_public_key, rsa_pkcs1v15_encrypt + + pub = load_public_key(pub) + crypttext = rsa_pkcs1v15_encrypt(pub, b"Data to encrypt") + + priv = session.get_key(key_type=KeyType.RSA, object_class=ObjectClass.PRIVATE_KEY) + + plaintext = priv.decrypt(crypttext, mechanism=Mechanism.RSA_PKCS) + + assert plaintext == b"Data to encrypt" + + +@pytest.mark.requires(Mechanism.ECDSA_SHA1) +def test_ecdsa(session: pkcs11.Session) -> None: + # A key we generated earlier + session.create_domain_parameters( + KeyType.EC, + { + Attribute.EC_PARAMS: encode_named_curve_parameters("secp256r1"), + }, + local=True, + ).generate_keypair() + + priv = session.get_key(key_type=KeyType.EC, object_class=ObjectClass.PRIVATE_KEY) + + signature = priv.sign(b"Data to sign", mechanism=Mechanism.ECDSA_SHA1) + # Encode as ASN.1 for OpenSSL + signature = encode_ecdsa_signature(signature) + + from oscrypto.asymmetric import ecdsa_verify, load_public_key + + pub = session.get_key(key_type=KeyType.EC, object_class=ObjectClass.PUBLIC_KEY) + pub = load_public_key(encode_ec_public_key(pub)) + + ecdsa_verify(pub, signature, b"Data to sign", "sha1") + + +@pytest.mark.requires(Mechanism.ECDH1_DERIVE) +def test_ecdh(session: pkcs11.Session) -> None: + # A key we generated earlier + session.create_domain_parameters( + KeyType.EC, + { + Attribute.EC_PARAMS: encode_named_curve_parameters("secp256r1"), + }, + local=True, + ).generate_keypair() + + # Retrieve our keypair, with our public key encoded for interchange + alice_priv = session.get_key(key_type=KeyType.EC, object_class=ObjectClass.PRIVATE_KEY) + alice_pub = session.get_key(key_type=KeyType.EC, object_class=ObjectClass.PUBLIC_KEY) + alice_pub = encode_ec_public_key(alice_pub) + + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives.asymmetric import ec + from cryptography.hazmat.primitives.serialization import ( + Encoding, + PublicFormat, + load_der_public_key, + ) + + # Bob generates a keypair, with their public key encoded for + # interchange + bob_priv = ec.generate_private_key(ec.SECP256R1(), default_backend()) + bob_pub = bob_priv.public_key().public_bytes( + Encoding.DER, + PublicFormat.SubjectPublicKeyInfo, + ) + + # Bob converts Alice's key to internal format and generates their + # shared key + bob_shared_key = bob_priv.exchange( + ec.ECDH(), + load_der_public_key(alice_pub, default_backend()), + ) + + key = alice_priv.derive_key( + KeyType.GENERIC_SECRET, + 256, + mechanism_param=( + KDF.NULL, + None, + # N.B. it seems like SoftHSMv2 requires an EC_POINT to be + # DER-encoded, which is not what the spec says + decode_ec_public_key(bob_pub, encode_ec_point=IS_SOFTHSM)[Attribute.EC_POINT], + ), + template={ + Attribute.SENSITIVE: False, + Attribute.EXTRACTABLE: True, + }, + ) + alice_shared_key = key[Attribute.VALUE] + + # We should have the same shared key + assert bob_shared_key == alice_shared_key + + +@pytest.mark.requires(Mechanism.RSA_PKCS) +def test_terrible_hybrid_file_encryption_app(session: pkcs11.Session) -> None: + # Proof of concept code only! + import io + + from oscrypto.asymmetric import load_public_key, rsa_pkcs1v15_encrypt + from oscrypto.symmetric import ( + aes_cbc_pkcs7_decrypt, + aes_cbc_pkcs7_encrypt, + ) + + # A key we generated earlier + session.generate_keypair(KeyType.RSA, 1024) + + pub = session.get_key(key_type=KeyType.RSA, object_class=ObjectClass.PUBLIC_KEY) + pub = load_public_key(encode_rsa_public_key(pub)) + + key = session.generate_random(256) + iv = session.generate_random(128) + + source = b"This is my amazing file" + + with io.BytesIO() as dest: + # Write a 128-byte header containing our key and our IV + # strictly speaking we don't need to keep the IV secure but + # we may as well. + # + # FIXME: Because this is RSA 1.5, we should fill the rest of the + # frame with nonsense + assert dest.write(rsa_pkcs1v15_encrypt(pub, key + iv)) == 128 + _, ciphertext = aes_cbc_pkcs7_encrypt(key, source, iv) + dest.write(ciphertext) + + # Time passes + dest.seek(0) + + # Look up our private key + priv = session.get_key(key_type=KeyType.RSA, object_class=ObjectClass.PRIVATE_KEY) + # Read the header + header = dest.read(priv.key_length // 8) + header = priv.decrypt(header, mechanism=Mechanism.RSA_PKCS) + + # The first 32 bytes is our key + key, header = header[:32], header[32:] + # The next 16 bytes is the IV + iv = header[:16] + # We can ignore the rest + + plaintext = aes_cbc_pkcs7_decrypt(key, dest.read(), iv) + + assert source == plaintext diff --git a/tests/test_rsa.py b/tests/test_rsa.py index f9e5343..05579cd 100644 --- a/tests/test_rsa.py +++ b/tests/test_rsa.py @@ -2,119 +2,134 @@ PKCS#11 RSA Public Key Cryptography """ +import pytest + import pkcs11 from pkcs11 import MGF, Attribute, KeyType, Mechanism, ObjectClass -from . import FIXME, TestCase, requires - - -class RSATests(TestCase): - @requires(Mechanism.RSA_PKCS_KEY_PAIR_GEN) - def setUp(self): - super().setUp() - - self.public, self.private = self.session.generate_keypair(KeyType.RSA, 1024) - - @requires(Mechanism.RSA_PKCS) - def test_sign_pkcs_v15(self): - data = b"00000000" - - signature = self.private.sign(data, mechanism=Mechanism.RSA_PKCS) - self.assertIsNotNone(signature) - self.assertIsInstance(signature, bytes) - self.assertTrue(self.public.verify(data, signature, mechanism=Mechanism.RSA_PKCS)) - self.assertFalse(self.public.verify(data, b"1234", mechanism=Mechanism.RSA_PKCS)) - - @requires(Mechanism.SHA512_RSA_PKCS) - def test_sign_default(self): - data = b"HELLO WORLD" * 1024 - - signature = self.private.sign(data) - self.assertIsNotNone(signature) - self.assertIsInstance(signature, bytes) - self.assertTrue(self.public.verify(data, signature)) - self.assertFalse(self.public.verify(data, b"1234")) - - @requires(Mechanism.SHA512_RSA_PKCS) - def test_sign_stream(self): - data = ( - b"I" * 16, - b"N" * 16, - b"P" * 16, - b"U" * 16, - b"T" * 10, # don't align to the blocksize - ) - - 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): - key = self.session.generate_key( - KeyType.AES, - 128, - template={ - Attribute.EXTRACTABLE: True, - Attribute.SENSITIVE: False, - }, - ) - - data = self.public.wrap_key(key) - self.assertNotEqual(data, key[Attribute.VALUE]) - - key2 = self.private.unwrap_key( - ObjectClass.SECRET_KEY, - KeyType.AES, - data, - template={ - Attribute.EXTRACTABLE: True, - Attribute.SENSITIVE: False, - }, - ) - - self.assertEqual(key[Attribute.VALUE], key2[Attribute.VALUE]) - - @requires(Mechanism.RSA_PKCS_OAEP) - def test_encrypt_oaep(self): - data = b"SOME DATA" - - crypttext = self.public.encrypt( - data, - mechanism=Mechanism.RSA_PKCS_OAEP, - mechanism_param=(Mechanism.SHA_1, MGF.SHA1, None), - ) - - self.assertNotEqual(data, crypttext) - - plaintext = self.private.decrypt( - crypttext, - mechanism=Mechanism.RSA_PKCS_OAEP, - mechanism_param=(Mechanism.SHA_1, MGF.SHA1, None), - ) - - self.assertEqual(data, plaintext) - - @requires(Mechanism.SHA1_RSA_PKCS_PSS) - def test_sign_pss(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), - ) - - 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 - - # You can't encrypt lots of data with RSA - # This should ideally throw DataLen but you can't trust it - with self.assertRaises(pkcs11.PKCS11Error): - self.public.encrypt(data) +pytestmark = [pytest.mark.requires(Mechanism.RSA_PKCS_KEY_PAIR_GEN)] + + +@pytest.fixture +def keypair(session: pkcs11.Session) -> tuple[pkcs11.PublicKey, pkcs11.PrivateKey]: + return session.generate_keypair(KeyType.RSA, 1024) + + +@pytest.mark.requires(Mechanism.RSA_PKCS) +def test_sign_pkcs_v15(keypair: tuple[pkcs11.PublicKey, pkcs11.PrivateKey]) -> None: + public_key, private_key = keypair + data = b"00000000" + + signature = private_key.sign(data, mechanism=Mechanism.RSA_PKCS) + assert signature is not None + assert isinstance(signature, bytes) + assert public_key.verify(data, signature, mechanism=Mechanism.RSA_PKCS) + assert not public_key.verify(data, b"1234", mechanism=Mechanism.RSA_PKCS) + + +@pytest.mark.requires(Mechanism.SHA512_RSA_PKCS) +def test_sign_default(keypair: tuple[pkcs11.PublicKey, pkcs11.PrivateKey]) -> None: + public_key, private_key = keypair + data = b"HELLO WORLD" * 1024 + + signature = private_key.sign(data) + assert signature is not None + assert isinstance(signature, bytes) + assert public_key.verify(data, signature) + assert not public_key.verify(data, b"1234") + + +@pytest.mark.requires(Mechanism.SHA512_RSA_PKCS) +def test_sign_stream(keypair: tuple[pkcs11.PublicKey, pkcs11.PrivateKey]) -> None: + public_key, private_key = keypair + data = ( + b"I" * 16, + b"N" * 16, + b"P" * 16, + b"U" * 16, + b"T" * 10, # don't align to the blocksize + ) + + signature = private_key.sign(data) + assert signature is not None + assert isinstance(signature, bytes) + assert public_key.verify(data, signature) + + +@pytest.mark.requires(Mechanism.RSA_PKCS_OAEP) +@pytest.mark.xfail_opencryptoki # can't set key attributes +def test_key_wrap( + session: pkcs11.Session, keypair: tuple[pkcs11.PublicKey, pkcs11.PrivateKey] +) -> None: + public_key, private_key = keypair + key = session.generate_key( + KeyType.AES, + 128, + template={ + Attribute.EXTRACTABLE: True, + Attribute.SENSITIVE: False, + }, + ) + + data = public_key.wrap_key(key) + assert data != key[Attribute.VALUE] + + key2 = private_key.unwrap_key( + ObjectClass.SECRET_KEY, + KeyType.AES, + data, + template={ + Attribute.EXTRACTABLE: True, + Attribute.SENSITIVE: False, + }, + ) + + assert key[Attribute.VALUE] == key2[Attribute.VALUE] + + +@pytest.mark.requires(Mechanism.RSA_PKCS_OAEP) +def test_encrypt_oaep(keypair: tuple[pkcs11.PublicKey, pkcs11.PrivateKey]) -> None: + public_key, private_key = keypair + data = b"SOME DATA" + + crypttext = public_key.encrypt( + data, + mechanism=Mechanism.RSA_PKCS_OAEP, + mechanism_param=(Mechanism.SHA_1, MGF.SHA1, None), + ) + + assert data != crypttext + + plaintext = private_key.decrypt( + crypttext, + mechanism=Mechanism.RSA_PKCS_OAEP, + mechanism_param=(Mechanism.SHA_1, MGF.SHA1, None), + ) + + assert data == plaintext + + +@pytest.mark.requires(Mechanism.SHA1_RSA_PKCS_PSS) +def test_sign_pss(keypair: tuple[pkcs11.PublicKey, pkcs11.PrivateKey]) -> None: + public_key, private_key = keypair + data = b"SOME DATA" + + # These are the default params + signature = private_key.sign( + data, + mechanism=Mechanism.SHA1_RSA_PKCS_PSS, + mechanism_param=(Mechanism.SHA_1, MGF.SHA1, 20), + ) + + assert public_key.verify(data, signature, mechanism=Mechanism.SHA1_RSA_PKCS_PSS) + + +@pytest.mark.requires(Mechanism.RSA_PKCS_OAEP) +def test_encrypt_too_much_data(keypair: tuple[pkcs11.PublicKey, pkcs11.PrivateKey]) -> None: + public_key, private_key = keypair + data = b"1234" * 128 + + # You can't encrypt lots of data with RSA + # This should ideally throw DataLen but you can't trust it + with pytest.raises(pkcs11.PKCS11Error): + public_key.encrypt(data) diff --git a/tests/test_sessions.py b/tests/test_sessions.py index 58c3d2e..a7459bb 100644 --- a/tests/test_sessions.py +++ b/tests/test_sessions.py @@ -2,158 +2,163 @@ PKCS#11 Sessions """ +import pytest + import pkcs11 +from tests.conftest import IS_NFAST, IS_OPENCRYPTOKI, IS_SOFTHSM + + +@pytest.mark.skipif(IS_NFAST or IS_OPENCRYPTOKI, reason="Login is required.") +def test_open_session(token: pkcs11.Token) -> None: + with token.open() as session: + assert isinstance(session, pkcs11.Session) + + +def test_open_session_and_login_user(token: pkcs11.Token, pin: str) -> None: + with token.open(user_pin=pin) as session: + assert isinstance(session, pkcs11.Session) + + +@pytest.mark.skipif( + not IS_SOFTHSM, reason="We don't have credentials to do this for other platforms." +) +def test_open_session_and_login_so(token: pkcs11.Token, so_pin: str) -> None: + with token.open(rw=True, so_pin=so_pin) as session: + assert isinstance(session, pkcs11.Session) + + +@pytest.mark.requires(pkcs11.Mechanism.AES_KEY_GEN) +def test_generate_key(token: pkcs11.Token, pin: str) -> None: + with token.open(user_pin=pin) as session: + key = session.generate_key(pkcs11.KeyType.AES, 128) + assert isinstance(key, pkcs11.Object) + assert isinstance(key, pkcs11.SecretKey) + assert isinstance(key, pkcs11.EncryptMixin) + + assert key.object_class is pkcs11.ObjectClass.SECRET_KEY + + # Test GetAttribute + assert key[pkcs11.Attribute.CLASS] is pkcs11.ObjectClass.SECRET_KEY + assert key[pkcs11.Attribute.TOKEN] is False + assert key[pkcs11.Attribute.LOCAL] is True + assert key[pkcs11.Attribute.MODIFIABLE] is True + assert key[pkcs11.Attribute.LABEL] == "" + + # Test SetAttribute + key[pkcs11.Attribute.LABEL] = "DEMO" + + assert key[pkcs11.Attribute.LABEL] == "DEMO" + + # Create another key with no capabilities + key = session.generate_key( + pkcs11.KeyType.AES, 128, label="MY KEY", id=b"\1\2\3\4", capabilities=0 + ) + assert isinstance(key, pkcs11.Object) + assert isinstance(key, pkcs11.SecretKey) + assert not isinstance(key, pkcs11.EncryptMixin) + + assert key.label == "MY KEY" + + +@pytest.mark.requires(pkcs11.Mechanism.RSA_PKCS_KEY_PAIR_GEN, pkcs11.Mechanism.RSA_PKCS) +def test_generate_keypair(token: pkcs11.Token, pin: str) -> None: + with token.open(user_pin=pin) as session: + pub, priv = session.generate_keypair(pkcs11.KeyType.RSA, 1024) + assert isinstance(pub, pkcs11.PublicKey) + assert isinstance(priv, pkcs11.PrivateKey) + + data = b"HELLO WORLD" + crypttext = pub.encrypt(data, mechanism=pkcs11.Mechanism.RSA_PKCS) + assert data != crypttext + text = priv.decrypt(crypttext, mechanism=pkcs11.Mechanism.RSA_PKCS) + assert data == text + + +@pytest.mark.requires(pkcs11.Mechanism.AES_KEY_GEN) +def test_get_objects(token: pkcs11.Token, pin: str) -> None: + with token.open(user_pin=pin) as session: + key = session.generate_key(pkcs11.KeyType.AES, 128, label="SAMPLE KEY") + + search = list(session.get_objects({pkcs11.Attribute.LABEL: "SAMPLE KEY"})) + + assert len(search) == 1 + assert key == search[0] + + +@pytest.mark.xfail_opencryptoki +def test_create_object(token: pkcs11.Token, pin: str) -> None: + with token.open(user_pin=pin) as session: + key = session.create_object( + { + pkcs11.Attribute.CLASS: pkcs11.ObjectClass.SECRET_KEY, + pkcs11.Attribute.KEY_TYPE: pkcs11.KeyType.AES, + pkcs11.Attribute.VALUE: b"1" * 16, + } + ) + + assert isinstance(key, pkcs11.SecretKey) + assert key.key_length == 128 + + +@pytest.mark.skipif(IS_NFAST, reason="nFast won't destroy objects.") +def test_destroy_object(token: pkcs11.Token, pin: str) -> None: + with token.open(user_pin=pin) as session: + key = session.generate_key(pkcs11.KeyType.AES, 128, label="SAMPLE KEY") + key.destroy() + + assert list(session.get_objects()) == [] + + +@pytest.mark.skipif(IS_NFAST, reason="nFast won't destroy objects.") +def test_copy_object(token: pkcs11.Token, pin: str) -> None: + with token.open(user_pin=pin) as session: + key = session.generate_key(pkcs11.KeyType.AES, 128, label="SAMPLE KEY") + new = key.copy( + { + pkcs11.Attribute.LABEL: "SOMETHING ELSE", + } + ) + + assert set(session.get_objects()) == {key, new} + + +@pytest.mark.requires(pkcs11.Mechanism.AES_KEY_GEN) +def test_get_key(token: pkcs11.Token, pin: str) -> None: + with token.open(user_pin=pin) as session: + session.generate_key(pkcs11.KeyType.AES, 128, label="SAMPLE KEY") + + key = session.get_key( + label="SAMPLE KEY", + ) + assert isinstance(key, pkcs11.SecretKey) + key.encrypt(b"test", mechanism_param=b"IV" * 8) + + +def test_get_key_not_found(token: pkcs11.Token, pin: str) -> None: + with token.open(user_pin=pin) as session: + with pytest.raises(pkcs11.NoSuchKey): + session.get_key(label="SAMPLE KEY") + + +@pytest.mark.requires(pkcs11.Mechanism.AES_KEY_GEN) +def test_get_key_vague(token: pkcs11.Token, pin: str) -> None: + with token.open(user_pin=pin) as session: + session.generate_key(pkcs11.KeyType.AES, 128, label="SAMPLE KEY") + session.generate_key(pkcs11.KeyType.AES, 128, label="SAMPLE KEY 2") + + with pytest.raises(pkcs11.MultipleObjectsReturned): + session.get_key(key_type=pkcs11.KeyType.AES) + + +@pytest.mark.skipif(IS_NFAST or IS_OPENCRYPTOKI, reason="Not supported.") +def test_seed_random(token: pkcs11.Token) -> None: + with token.open() as session: + session.seed_random(b"12345678") + -from . import FIXME, TOKEN_PIN, TOKEN_SO_PIN, Not, Only, TestCase, requires - - -class SessionTests(TestCase): - with_session = False - - @Not.nfast # Login is required - @Not.opencryptoki - def test_open_session(self): - with self.token.open() as session: - self.assertIsInstance(session, pkcs11.Session) - - def test_open_session_and_login_user(self): - with self.token.open(user_pin=TOKEN_PIN) as session: - self.assertIsInstance(session, pkcs11.Session) - - @Only.softhsm2 # We don't have credentials to do this for other platforms - def test_open_session_and_login_so(self): - with self.token.open(rw=True, so_pin=TOKEN_SO_PIN) as session: - self.assertIsInstance(session, pkcs11.Session) - - @requires(pkcs11.Mechanism.AES_KEY_GEN) - def test_generate_key(self): - with self.token.open(user_pin=TOKEN_PIN) as session: - key = session.generate_key(pkcs11.KeyType.AES, 128) - self.assertIsInstance(key, pkcs11.Object) - self.assertIsInstance(key, pkcs11.SecretKey) - self.assertIsInstance(key, pkcs11.EncryptMixin) - - self.assertIs(key.object_class, pkcs11.ObjectClass.SECRET_KEY) - - # Test GetAttribute - self.assertIs(key[pkcs11.Attribute.CLASS], pkcs11.ObjectClass.SECRET_KEY) - self.assertEqual(key[pkcs11.Attribute.TOKEN], False) - self.assertEqual(key[pkcs11.Attribute.LOCAL], True) - self.assertEqual(key[pkcs11.Attribute.MODIFIABLE], True) - self.assertEqual(key[pkcs11.Attribute.LABEL], "") - - # Test SetAttribute - key[pkcs11.Attribute.LABEL] = "DEMO" - - self.assertEqual(key[pkcs11.Attribute.LABEL], "DEMO") - - # Create another key with no capabilities - key = session.generate_key( - pkcs11.KeyType.AES, 128, label="MY KEY", id=b"\1\2\3\4", capabilities=0 - ) - self.assertIsInstance(key, pkcs11.Object) - self.assertIsInstance(key, pkcs11.SecretKey) - self.assertNotIsInstance(key, pkcs11.EncryptMixin) - - self.assertEqual(key.label, "MY KEY") - - @requires(pkcs11.Mechanism.RSA_PKCS_KEY_PAIR_GEN, pkcs11.Mechanism.RSA_PKCS) - def test_generate_keypair(self): - with self.token.open(user_pin=TOKEN_PIN) as session: - pub, priv = session.generate_keypair(pkcs11.KeyType.RSA, 1024) - self.assertIsInstance(pub, pkcs11.PublicKey) - self.assertIsInstance(priv, pkcs11.PrivateKey) - - data = b"HELLO WORLD" - crypttext = pub.encrypt(data, mechanism=pkcs11.Mechanism.RSA_PKCS) - self.assertNotEqual(data, crypttext) - text = priv.decrypt(crypttext, mechanism=pkcs11.Mechanism.RSA_PKCS) - self.assertEqual(data, text) - - @requires(pkcs11.Mechanism.AES_KEY_GEN) - def test_get_objects(self): - with self.token.open(user_pin=TOKEN_PIN) as session: - key = session.generate_key(pkcs11.KeyType.AES, 128, label="SAMPLE KEY") - - search = list( - session.get_objects( - { - pkcs11.Attribute.LABEL: "SAMPLE KEY", - } - ) - ) - - self.assertEqual(len(search), 1) - self.assertEqual(key, search[0]) - - @FIXME.opencryptoki - def test_create_object(self): - with self.token.open(user_pin=TOKEN_PIN) as session: - key = session.create_object( - { - pkcs11.Attribute.CLASS: pkcs11.ObjectClass.SECRET_KEY, - pkcs11.Attribute.KEY_TYPE: pkcs11.KeyType.AES, - pkcs11.Attribute.VALUE: b"1" * 16, - } - ) - - self.assertIsInstance(key, pkcs11.SecretKey) - self.assertEqual(key.key_length, 128) - - @Not.nfast # nFast won't destroy objects - def test_destroy_object(self): - with self.token.open(user_pin=TOKEN_PIN) as session: - key = session.generate_key(pkcs11.KeyType.AES, 128, label="SAMPLE KEY") - key.destroy() - - self.assertEqual(list(session.get_objects()), []) - - @Only.softhsm2 - def test_copy_object(self): - with self.token.open(user_pin=TOKEN_PIN) as session: - key = session.generate_key(pkcs11.KeyType.AES, 128, label="SAMPLE KEY") - new = key.copy( - { - pkcs11.Attribute.LABEL: "SOMETHING ELSE", - } - ) - - self.assertEqual(set(session.get_objects()), {key, new}) - - @requires(pkcs11.Mechanism.AES_KEY_GEN) - def test_get_key(self): - with self.token.open(user_pin=TOKEN_PIN) as session: - session.generate_key(pkcs11.KeyType.AES, 128, label="SAMPLE KEY") - - key = session.get_key( - label="SAMPLE KEY", - ) - self.assertIsInstance(key, pkcs11.SecretKey) - key.encrypt(b"test", mechanism_param=b"IV" * 8) - - def test_get_key_not_found(self): - with self.token.open(user_pin=TOKEN_PIN) as session: - with self.assertRaises(pkcs11.NoSuchKey): - session.get_key(label="SAMPLE KEY") - - @requires(pkcs11.Mechanism.AES_KEY_GEN) - def test_get_key_vague(self): - with self.token.open(user_pin=TOKEN_PIN) as session: - session.generate_key(pkcs11.KeyType.AES, 128, label="SAMPLE KEY") - session.generate_key(pkcs11.KeyType.AES, 128, label="SAMPLE KEY 2") - - with self.assertRaises(pkcs11.MultipleObjectsReturned): - session.get_key(key_type=pkcs11.KeyType.AES) - - @Not.nfast # Not supported - @Not.opencryptoki # Not supported - def test_seed_random(self): - with self.token.open() as session: - session.seed_random(b"12345678") - - def test_generate_random(self): - with self.token.open(user_pin=TOKEN_PIN) as session: - random = session.generate_random(16 * 8) - self.assertEqual(len(random), 16) - # Ensure we didn't get 16 bytes of zeros - self.assertTrue(all(c != "\0" for c in random)) +def test_generate_random(token: pkcs11.Token, pin: str) -> None: + with token.open(user_pin=pin) as session: + random = session.generate_random(16 * 8) + assert len(random) == 16 + # Ensure we didn't get 16 bytes of zeros + assert all(c != "\x00" for c in random) diff --git a/tests/test_slots_and_tokens.py b/tests/test_slots_and_tokens.py index 8e49818..1a9d3c1 100644 --- a/tests/test_slots_and_tokens.py +++ b/tests/test_slots_and_tokens.py @@ -2,72 +2,79 @@ PKCS#11 Slots and Tokens """ -import unittest +import pytest import pkcs11 +from tests.conftest import IS_NFAST, IS_OPENCRYPTOKI, IS_SOFTHSM, LIB_PATH -from . import LIB, TOKEN, Not, Only - - -class SlotsAndTokensTests(unittest.TestCase): - def test_double_initialise(self): - self.assertIsNotNone(pkcs11.lib(LIB)) - self.assertIsNotNone(pkcs11.lib(LIB)) - - def test_double_initialise_different_libs(self): - self.assertIsNotNone(pkcs11.lib(LIB)) - with self.assertRaises(pkcs11.AlreadyInitialized): - pkcs11.lib("somethingelse.so") - - @Only.softhsm2 - def test_get_slots(self): - lib = pkcs11.lib(LIB) - slots = lib.get_slots() - - self.assertEqual(len(slots), 2) - slot1, slot2 = slots - - self.assertIsInstance(slot1, pkcs11.Slot) - self.assertEqual(slot1.flags, pkcs11.SlotFlag.TOKEN_PRESENT) - - def test_get_mechanisms(self): - lib = pkcs11.lib(LIB) - slot, *_ = lib.get_slots() - mechanisms = slot.get_mechanisms() - self.assertIn(pkcs11.Mechanism.RSA_PKCS, mechanisms) - - def test_get_mechanism_info(self): - lib = pkcs11.lib(LIB) - slot, *_ = lib.get_slots() - info = slot.get_mechanism_info(pkcs11.Mechanism.RSA_PKCS_OAEP) - self.assertIsInstance(info, pkcs11.MechanismInfo) - - @Not.nfast # EC not supported - @Not.opencryptoki - def test_get_mechanism_info_ec(self): - lib = pkcs11.lib(LIB) - slot, *_ = lib.get_slots() - info = slot.get_mechanism_info(pkcs11.Mechanism.EC_KEY_PAIR_GEN) - self.assertIsInstance(info, pkcs11.MechanismInfo) - self.assertIn(pkcs11.MechanismFlag.EC_NAMEDCURVE, info.flags) - - @Only.softhsm2 - def test_get_tokens(self): - lib = pkcs11.lib(LIB) - - tokens = lib.get_tokens(token_flags=pkcs11.TokenFlag.RNG) - self.assertEqual(len(list(tokens)), 2) - - tokens = lib.get_tokens(token_label=TOKEN) - self.assertEqual(len(list(tokens)), 1) - - @Only.softhsm2 - def test_get_token(self): - lib = pkcs11.lib(LIB) - slot, *_ = lib.get_slots() - token = slot.get_token() - - self.assertIsInstance(token, pkcs11.Token) - self.assertEqual(token.label, TOKEN) - self.assertIn(pkcs11.TokenFlag.TOKEN_INITIALIZED, token.flags) - self.assertIn(pkcs11.TokenFlag.LOGIN_REQUIRED, token.flags) + +def test_double_initialise() -> None: + assert pkcs11.lib(LIB_PATH) is not None + assert pkcs11.lib(LIB_PATH) is not None + + +def test_double_initialise_different_libs() -> None: + assert pkcs11.lib(LIB_PATH) is not None + with pytest.raises(pkcs11.AlreadyInitialized): + pkcs11.lib("somethingelse.so") + + +@pytest.mark.skipif(not IS_SOFTHSM, reason="Only supported on SoftHSMv2.") +@pytest.mark.usefixtures("softhsm_token") +def test_get_slots() -> None: + lib = pkcs11.lib(LIB_PATH) + slots = lib.get_slots() + print(slots) + + assert len(slots) == 2 + slot1, slot2 = slots + + assert isinstance(slot1, pkcs11.Slot) + assert slot1.flags == pkcs11.SlotFlag.TOKEN_PRESENT + + +def test_get_mechanisms() -> None: + lib = pkcs11.lib(LIB_PATH) + slot, *_ = lib.get_slots() + mechanisms = slot.get_mechanisms() + assert pkcs11.Mechanism.RSA_PKCS in mechanisms + + +def test_get_mechanism_info() -> None: + lib = pkcs11.lib(LIB_PATH) + slot, *_ = lib.get_slots() + info = slot.get_mechanism_info(pkcs11.Mechanism.RSA_PKCS_OAEP) + assert isinstance(info, pkcs11.MechanismInfo) + + +@pytest.mark.skipif(IS_NFAST or IS_OPENCRYPTOKI, reason="EC not supported.") +def test_get_mechanism_info_ec() -> None: + lib = pkcs11.lib(LIB_PATH) + slot, *_ = lib.get_slots() + info = slot.get_mechanism_info(pkcs11.Mechanism.EC_KEY_PAIR_GEN) + assert isinstance(info, pkcs11.MechanismInfo) + assert pkcs11.MechanismFlag.EC_NAMEDCURVE in info.flags + + +@pytest.mark.skipif(not IS_SOFTHSM, reason="Only supported on SoftHSMv2.") +def test_get_tokens(softhsm_token: pkcs11.Token) -> None: + lib = pkcs11.lib(LIB_PATH) + + tokens = list(lib.get_tokens(token_flags=pkcs11.TokenFlag.RNG)) + print(tokens) + assert len(list(tokens)) == 2 + + tokens = lib.get_tokens(token_label=softhsm_token.label) + assert len(list(tokens)) == 1 + + +@pytest.mark.skipif(not IS_SOFTHSM, reason="Only supported on SoftHSMv2.") +def test_get_token(token: pkcs11.Token) -> None: + lib = pkcs11.lib(LIB_PATH) + slot, *_ = lib.get_slots() + actual_token = slot.get_token() + + assert isinstance(actual_token, pkcs11.Token) + assert actual_token.label == token.label + assert pkcs11.TokenFlag.TOKEN_INITIALIZED in actual_token.flags + assert pkcs11.TokenFlag.LOGIN_REQUIRED in actual_token.flags diff --git a/tests/test_threading.py b/tests/test_threading.py index 5ec3a38..88a1d34 100644 --- a/tests/test_threading.py +++ b/tests/test_threading.py @@ -7,37 +7,42 @@ import threading +import pytest + import pkcs11 -from . import Not, TestCase, requires +from .conftest import IS_NFAST + +pytestmark = [ + pytest.mark.skipif(IS_NFAST, reason="Deadlocks nfast ... something wrong with threading?") +] -@Not.nfast # Deadlocks nfast ... something wrong with threading? -class ThreadingTests(TestCase): - @requires(pkcs11.Mechanism.AES_KEY_GEN, pkcs11.Mechanism.AES_CBC_PAD) - def test_concurrency(self): - # Multiplexing a session between processes - self.session.generate_key(pkcs11.KeyType.AES, 128, label="LOOK ME UP") +@pytest.mark.requires(pkcs11.Mechanism.AES_KEY_GEN) +@pytest.mark.requires(pkcs11.Mechanism.AES_CBC_PAD) +def test_concurrency(session: pkcs11.Session) -> None: + # Multiplexing a session between processes + session.generate_key(pkcs11.KeyType.AES, 128, label="LOOK ME UP") - test_passed = [True] + test_passed = [True] - def thread_work(): - try: - data = b"1234" * 1024 * 1024 # Multichunk files - iv = self.session.generate_random(128) - key = self.session.get_key(label="LOOK ME UP") - self.assertIsNotNone(key.encrypt(data, mechanism_param=iv)) - except pkcs11.PKCS11Error: - test_passed[0] = False - raise + def thread_work(): + try: + data = b"1234" * 1024 * 1024 # Multichunk files + iv = session.generate_random(128) + key = session.get_key(label="LOOK ME UP") + assert key.encrypt(data, mechanism_param=iv) is not None + except pkcs11.PKCS11Error: + test_passed[0] = False + raise - threads = [threading.Thread(target=thread_work) for _ in range(10)] + threads = [threading.Thread(target=thread_work) for _ in range(10)] - for thread in threads: - thread.start() + for thread in threads: + thread.start() - # join each thread - for thread in threads: - thread.join() + # join each thread + for thread in threads: + thread.join() - self.assertTrue(test_passed[0]) + assert test_passed[0] diff --git a/tests/test_x509.py b/tests/test_x509.py index b4a7dca..e86fad7 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -5,24 +5,21 @@ import base64 import datetime import subprocess +from pathlib import Path +import pytest from asn1crypto import pem from asn1crypto.csr import CertificationRequest, CertificationRequestInfo from asn1crypto.keys import RSAPublicKey from asn1crypto.x509 import Certificate, Name, TbsCertificate, Time import pkcs11 -from pkcs11 import ( - Attribute, - KeyType, - Mechanism, -) +from pkcs11 import Attribute, KeyType, Mechanism from pkcs11.util.dsa import decode_dsa_signature from pkcs11.util.ec import decode_ecdsa_signature from pkcs11.util.rsa import encode_rsa_public_key from pkcs11.util.x509 import decode_x509_certificate, decode_x509_public_key - -from . import OPENSSL, Not, Only, TestCase, requires +from tests.conftest import IS_NFAST, IS_OPENCRYPTOKI, OPENSSL # X.509 self-signed certificate (generated with OpenSSL) # openssl req -x509 \ @@ -49,233 +46,245 @@ """) -class X509Tests(TestCase): - def test_import_ca_certificate_easy(self): - cert = self.session.create_object(decode_x509_certificate(CERT)) - self.assertIsInstance(cert, pkcs11.Certificate) - - @Not.nfast - @Not.opencryptoki - def test_import_ca_certificate(self): - cert = self.session.create_object(decode_x509_certificate(CERT, extended_set=True)) - self.assertIsInstance(cert, pkcs11.Certificate) - - self.assertEqual( - cert[Attribute.HASH_OF_ISSUER_PUBLIC_KEY], - b"\xf9\xc1\xb6\xe3\x43\xf3\xcf\x4c\xba\x8a" b"\x0b\x66\x86\x79\x35\xfb\x52\x85\xbf\xa8", - ) - # Cert is self signed - self.assertEqual( - cert[Attribute.HASH_OF_SUBJECT_PUBLIC_KEY], - b"\xf9\xc1\xb6\xe3\x43\xf3\xcf\x4c\xba\x8a" b"\x0b\x66\x86\x79\x35\xfb\x52\x85\xbf\xa8", - ) - - @requires(Mechanism.SHA1_RSA_PKCS) - def test_verify_certificate_rsa(self): - # Warning: proof of concept code only! - x509 = Certificate.load(CERT) - key = self.session.create_object(decode_x509_public_key(CERT)) - self.assertIsInstance(key, pkcs11.PublicKey) - - value = x509["tbs_certificate"].dump() - signature = x509.signature - - assert x509.signature_algo == "rsassa_pkcs1v15" - assert x509.hash_algo == "sha1" - - self.assertTrue(key.verify(value, signature, mechanism=Mechanism.SHA1_RSA_PKCS)) - - @requires(Mechanism.DSA_SHA1) - def test_verify_certificate_dsa(self): - # Warning: proof of concept code only! - CERT = base64.b64decode(""" - MIIDbjCCAy6gAwIBAgIJAKPBInGiPjXNMAkGByqGSM44BAMwRTELMAkGA1UEBhMC - QVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdp - dHMgUHR5IEx0ZDAeFw0xNzA3MDMxMjI1MTBaFw0xOTA3MDMxMjI1MTBaMEUxCzAJ - BgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5l - dCBXaWRnaXRzIFB0eSBMdGQwggG3MIIBLAYHKoZIzjgEATCCAR8CgYEA7U0AshA/ - 4MXQ3MHykoeotEoPc+OXFMJ2PHzKfbFD80UC5bloxC9kp908GG3emdqbJuCTfVUD - sex1vEgMj1sEwilBow954zMqncu5lLBIGZKjT6tloW8sFt50sE0l+YnBvAiw9uoL - 9lBOZLKh87zWPZUuORm8lWhZEwjUnZ+3S5ECFQCNJGd68RpctgkA1kDp33NhQhev - lQKBgQCQ6uYkvNpHMtXwyGII4JyOyStbteHjHdKfJfLNRyIEEq/E4e3Do6NGIr26 - Z7u9iBsA5/aU6gKSBrYprxY1hdR4gTRBNzSUDEzf7IX3bfRIbBhjlNBSBba5Fs0z - /kszZbZ8XYGVxs92aWFk/1JIZ0wnToC794+juq72/TvrtvxdowOBhAACgYAjoknQ - kRD0+x3GkbngQCU+VNspZuXboB22CU3bDGVAVhmI5N02M8NmeuN7SqqYZAlw01Ju - rzBF7i9VW4qxBaWszMCwyozerSVjZ2JA/Qubb57v/p7F3FDHq7E33FZzgyhOimds - rzXpVErCGJJ1oBGz5H5fvoKnQmfh0X8N/VHkZqOBpzCBpDAdBgNVHQ4EFgQUQayv - usUnpvRgc9OtXGddqMiwm5cwdQYDVR0jBG4wbIAUQayvusUnpvRgc9OtXGddqMiw - m5ehSaRHMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYD - VQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGSCCQCjwSJxoj41zTAMBgNVHRME - BTADAQH/MAkGByqGSM44BAMDLwAwLAIUNE+zTuFe01v0BRTLarPtGK8ZHHcCFB9Y - YAwtpblAgUEdGuoAtnoEQ2tc - """) - - x509 = Certificate.load(CERT) - key = self.session.create_object(decode_x509_public_key(CERT)) - self.assertIsInstance(key, pkcs11.PublicKey) - - value = x509["tbs_certificate"].dump() - - assert x509.signature_algo == "dsa" - assert x509.hash_algo == "sha1" - - signature = decode_dsa_signature(x509.signature) - - self.assertTrue(key.verify(value, signature, mechanism=Mechanism.DSA_SHA1)) - - @requires(Mechanism.ECDSA_SHA1) - def test_verify_certificate_ecdsa(self): - # Warning: proof of concept code only! - CERT = base64.b64decode(""" - MIIDGjCCAsKgAwIBAgIJAL+PbwiJUZB1MAkGByqGSM49BAEwRTELMAkGA1UEBhMC - QVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdp - dHMgUHR5IEx0ZDAeFw0xNzA3MDMxMTUxMTBaFw0xOTA3MDMxMTUxMTBaMEUxCzAJ - BgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5l - dCBXaWRnaXRzIFB0eSBMdGQwggFLMIIBAwYHKoZIzj0CATCB9wIBATAsBgcqhkjO - PQEBAiEA/////wAAAAEAAAAAAAAAAAAAAAD///////////////8wWwQg/////wAA - AAEAAAAAAAAAAAAAAAD///////////////wEIFrGNdiqOpPns+u9VXaYhrxlHQaw - zFOw9jvOPD4n0mBLAxUAxJ02CIbnBJNqZnjhE50mt4GffpAEQQRrF9Hy4SxCR/i8 - 5uVjpEDydwN9gS3rM6D0oTlF2JjClk/jQuL+Gn+bjufrSnwPnhYrzjNXazFezsu2 - QGg3v1H1AiEA/////wAAAAD//////////7zm+q2nF56E87nKwvxjJVECAQEDQgAE - royPJHkCQMq55egxmQxkFWqiz+yJx0MZP98is99SrkiK5UadFim3r3ZSt5kfh/cc - Ccmy94BZCmihhGJ0F4eB2qOBpzCBpDAdBgNVHQ4EFgQURNXKlYGsAMItf4Ad8fkg - Rg9ATqEwdQYDVR0jBG4wbIAURNXKlYGsAMItf4Ad8fkgRg9ATqGhSaRHMEUxCzAJ - BgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5l - dCBXaWRnaXRzIFB0eSBMdGSCCQC/j28IiVGQdTAMBgNVHRMEBTADAQH/MAkGByqG - SM49BAEDRwAwRAIgAdJp/S9vSjS6EvRy/9zl5k2DBKGI52A3Ygsp1a96UicCIDul - m/eL2OcGdNbzqzsC11alhemJX7Qt9GOcVqQwROIm - """) - - x509 = Certificate.load(CERT) - key = self.session.create_object(decode_x509_public_key(CERT)) - self.assertIsInstance(key, pkcs11.PublicKey) - - value = x509["tbs_certificate"].dump() - - assert x509.signature_algo == "ecdsa" - assert x509.hash_algo == "sha1" - - signature = decode_ecdsa_signature(x509.signature) - - self.assertTrue(key.verify(value, signature, mechanism=Mechanism.ECDSA_SHA1)) - - @Only.openssl - @requires(Mechanism.RSA_PKCS_KEY_PAIR_GEN, Mechanism.SHA1_RSA_PKCS) - def test_self_sign_certificate(self): - # Warning: proof of concept code only! - pub, priv = self.session.generate_keypair(KeyType.RSA, 1024) - - tbs = TbsCertificate( - { - "version": "v1", - "serial_number": 1, - "issuer": Name.build( +def test_import_ca_certificate_easy(session: pkcs11.Session) -> None: + cert = session.create_object(decode_x509_certificate(CERT)) + assert isinstance(cert, pkcs11.Certificate) + + +@pytest.mark.skipif(IS_NFAST or IS_OPENCRYPTOKI, reason="Unknown reason.") +def test_import_ca_certificate(session: pkcs11.Session) -> None: + cert = session.create_object(decode_x509_certificate(CERT, extended_set=True)) + assert isinstance(cert, pkcs11.Certificate) + + assert ( + cert[Attribute.HASH_OF_ISSUER_PUBLIC_KEY] == b"\xf9\xc1\xb6\xe3C\xf3\xcfL\xba\x8a" + b"\x0bf\x86y5\xfbR\x85\xbf\xa8" + ) + # Cert is self signed + assert ( + cert[Attribute.HASH_OF_SUBJECT_PUBLIC_KEY] == b"\xf9\xc1\xb6\xe3C\xf3\xcfL\xba\x8a" + b"\x0bf\x86y5\xfbR\x85\xbf\xa8" + ) + + +@pytest.mark.requires(Mechanism.SHA1_RSA_PKCS) +def test_verify_certificate_rsa(session: pkcs11.Session) -> None: + # Warning: proof of concept code only! + x509 = Certificate.load(CERT) + key = session.create_object(decode_x509_public_key(CERT)) + assert isinstance(key, pkcs11.PublicKey) + + value = x509["tbs_certificate"].dump() + signature = x509.signature + + assert x509.signature_algo == "rsassa_pkcs1v15" + assert x509.hash_algo == "sha1" + + assert key.verify(value, signature, mechanism=Mechanism.SHA1_RSA_PKCS) + + +@pytest.mark.requires(Mechanism.DSA_SHA1) +def test_verify_certificate_dsa(session: pkcs11.Session) -> None: + # Warning: proof of concept code only! + CERT = base64.b64decode(""" + MIIDbjCCAy6gAwIBAgIJAKPBInGiPjXNMAkGByqGSM44BAMwRTELMAkGA1UEBhMC + QVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdp + dHMgUHR5IEx0ZDAeFw0xNzA3MDMxMjI1MTBaFw0xOTA3MDMxMjI1MTBaMEUxCzAJ + BgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5l + dCBXaWRnaXRzIFB0eSBMdGQwggG3MIIBLAYHKoZIzjgEATCCAR8CgYEA7U0AshA/ + 4MXQ3MHykoeotEoPc+OXFMJ2PHzKfbFD80UC5bloxC9kp908GG3emdqbJuCTfVUD + sex1vEgMj1sEwilBow954zMqncu5lLBIGZKjT6tloW8sFt50sE0l+YnBvAiw9uoL + 9lBOZLKh87zWPZUuORm8lWhZEwjUnZ+3S5ECFQCNJGd68RpctgkA1kDp33NhQhev + lQKBgQCQ6uYkvNpHMtXwyGII4JyOyStbteHjHdKfJfLNRyIEEq/E4e3Do6NGIr26 + Z7u9iBsA5/aU6gKSBrYprxY1hdR4gTRBNzSUDEzf7IX3bfRIbBhjlNBSBba5Fs0z + /kszZbZ8XYGVxs92aWFk/1JIZ0wnToC794+juq72/TvrtvxdowOBhAACgYAjoknQ + kRD0+x3GkbngQCU+VNspZuXboB22CU3bDGVAVhmI5N02M8NmeuN7SqqYZAlw01Ju + rzBF7i9VW4qxBaWszMCwyozerSVjZ2JA/Qubb57v/p7F3FDHq7E33FZzgyhOimds + rzXpVErCGJJ1oBGz5H5fvoKnQmfh0X8N/VHkZqOBpzCBpDAdBgNVHQ4EFgQUQayv + usUnpvRgc9OtXGddqMiwm5cwdQYDVR0jBG4wbIAUQayvusUnpvRgc9OtXGddqMiw + m5ehSaRHMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYD + VQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGSCCQCjwSJxoj41zTAMBgNVHRME + BTADAQH/MAkGByqGSM44BAMDLwAwLAIUNE+zTuFe01v0BRTLarPtGK8ZHHcCFB9Y + YAwtpblAgUEdGuoAtnoEQ2tc + """) + + x509 = Certificate.load(CERT) + key = session.create_object(decode_x509_public_key(CERT)) + assert isinstance(key, pkcs11.PublicKey) + + value = x509["tbs_certificate"].dump() + + assert x509.signature_algo == "dsa" + assert x509.hash_algo == "sha1" + + signature = decode_dsa_signature(x509.signature) + + assert key.verify(value, signature, mechanism=Mechanism.DSA_SHA1) + + +@pytest.mark.requires(Mechanism.ECDSA_SHA1) +def test_verify_certificate_ecdsa(session: pkcs11.Session) -> None: + # Warning: proof of concept code only! + CERT = base64.b64decode(""" + MIIDGjCCAsKgAwIBAgIJAL+PbwiJUZB1MAkGByqGSM49BAEwRTELMAkGA1UEBhMC + QVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdp + dHMgUHR5IEx0ZDAeFw0xNzA3MDMxMTUxMTBaFw0xOTA3MDMxMTUxMTBaMEUxCzAJ + BgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5l + dCBXaWRnaXRzIFB0eSBMdGQwggFLMIIBAwYHKoZIzj0CATCB9wIBATAsBgcqhkjO + PQEBAiEA/////wAAAAEAAAAAAAAAAAAAAAD///////////////8wWwQg/////wAA + AAEAAAAAAAAAAAAAAAD///////////////wEIFrGNdiqOpPns+u9VXaYhrxlHQaw + zFOw9jvOPD4n0mBLAxUAxJ02CIbnBJNqZnjhE50mt4GffpAEQQRrF9Hy4SxCR/i8 + 5uVjpEDydwN9gS3rM6D0oTlF2JjClk/jQuL+Gn+bjufrSnwPnhYrzjNXazFezsu2 + QGg3v1H1AiEA/////wAAAAD//////////7zm+q2nF56E87nKwvxjJVECAQEDQgAE + royPJHkCQMq55egxmQxkFWqiz+yJx0MZP98is99SrkiK5UadFim3r3ZSt5kfh/cc + Ccmy94BZCmihhGJ0F4eB2qOBpzCBpDAdBgNVHQ4EFgQURNXKlYGsAMItf4Ad8fkg + Rg9ATqEwdQYDVR0jBG4wbIAURNXKlYGsAMItf4Ad8fkgRg9ATqGhSaRHMEUxCzAJ + BgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5l + dCBXaWRnaXRzIFB0eSBMdGSCCQC/j28IiVGQdTAMBgNVHRMEBTADAQH/MAkGByqG + SM49BAEDRwAwRAIgAdJp/S9vSjS6EvRy/9zl5k2DBKGI52A3Ygsp1a96UicCIDul + m/eL2OcGdNbzqzsC11alhemJX7Qt9GOcVqQwROIm + """) + + x509 = Certificate.load(CERT) + key = session.create_object(decode_x509_public_key(CERT)) + assert isinstance(key, pkcs11.PublicKey) + + value = x509["tbs_certificate"].dump() + + assert x509.signature_algo == "ecdsa" + assert x509.hash_algo == "sha1" + + signature = decode_ecdsa_signature(x509.signature) + + assert key.verify(value, signature, mechanism=Mechanism.ECDSA_SHA1) + + +@pytest.mark.skipif(OPENSSL is None, reason="openssl command not found.") +@pytest.mark.requires(Mechanism.RSA_PKCS_KEY_PAIR_GEN) +@pytest.mark.requires(Mechanism.SHA1_RSA_PKCS) +def test_self_sign_certificate(tmpdir: Path, session: pkcs11.Session) -> None: + # Warning: proof of concept code only! + pub, priv = session.generate_keypair(KeyType.RSA, 1024) + + tbs = TbsCertificate( + { + "version": "v1", + "serial_number": 1, + "issuer": Name.build( + { + "common_name": "Test Certificate", + } + ), + "subject": Name.build( + { + "common_name": "Test Certificate", + } + ), + "signature": { + "algorithm": "sha1_rsa", + "parameters": None, + }, + "validity": { + "not_before": Time( { - "common_name": "Test Certificate", + "utc_time": datetime.datetime( + 2017, 1, 1, 0, 0, tzinfo=datetime.timezone.utc + ), } ), - "subject": Name.build( + "not_after": Time( { - "common_name": "Test Certificate", + "utc_time": datetime.datetime( + 2038, 12, 31, 23, 59, tzinfo=datetime.timezone.utc + ), } ), - "signature": { - "algorithm": "sha1_rsa", - "parameters": None, - }, - "validity": { - "not_before": Time( - { - "utc_time": datetime.datetime( - 2017, 1, 1, 0, 0, tzinfo=datetime.timezone.utc - ), - } - ), - "not_after": Time( - { - "utc_time": datetime.datetime( - 2038, 12, 31, 23, 59, tzinfo=datetime.timezone.utc - ), - } - ), - }, - "subject_public_key_info": { - "algorithm": { - "algorithm": "rsa", - "parameters": None, - }, - "public_key": RSAPublicKey.load(encode_rsa_public_key(pub)), - }, - } - ) - - # Sign the TBS Certificate - value = priv.sign(tbs.dump(), mechanism=Mechanism.SHA1_RSA_PKCS) - - cert = Certificate( - { - "tbs_certificate": tbs, - "signature_algorithm": { - "algorithm": "sha1_rsa", + }, + "subject_public_key_info": { + "algorithm": { + "algorithm": "rsa", "parameters": None, }, - "signature_value": value, - } - ) - - # Pipe our certificate to OpenSSL to verify it - with subprocess.Popen( - (OPENSSL, "verify"), stdin=subprocess.PIPE, stdout=subprocess.DEVNULL - ) as proc: - proc.stdin.write(pem.armor("CERTIFICATE", cert.dump())) - proc.stdin.close() - self.assertEqual(proc.wait(), 0) - - @Only.openssl - @requires(Mechanism.RSA_PKCS_KEY_PAIR_GEN, Mechanism.SHA1_RSA_PKCS) - def test_sign_csr(self): - # Warning: proof of concept code only! - pub, priv = self.session.generate_keypair(KeyType.RSA, 1024) - - info = CertificationRequestInfo( - { - "version": 0, - "subject": Name.build( - { - "common_name": "Test Certificate", - } - ), - "subject_pk_info": { - "algorithm": { - "algorithm": "rsa", - "parameters": None, - }, - "public_key": RSAPublicKey.load(encode_rsa_public_key(pub)), - }, - } - ) - - # Sign the CSR Info - value = priv.sign(info.dump(), mechanism=Mechanism.SHA1_RSA_PKCS) - - csr = CertificationRequest( - { - "certification_request_info": info, - "signature_algorithm": { - "algorithm": "sha1_rsa", + "public_key": RSAPublicKey.load(encode_rsa_public_key(pub)), + }, + } + ) + + # Sign the TBS Certificate + value = priv.sign(tbs.dump(), mechanism=Mechanism.SHA1_RSA_PKCS) + + cert = Certificate( + { + "tbs_certificate": tbs, + "signature_algorithm": { + "algorithm": "sha1_rsa", + "parameters": None, + }, + "signature_value": value, + } + ) + + pem_cert = pem.armor("CERTIFICATE", cert.dump()) + pem_path = tmpdir / "ca.pem" + with open(pem_path, "wb") as stream: + stream.write(pem_cert) + + # Pipe our certificate to OpenSSL to verify it + with subprocess.Popen( + (OPENSSL, "verify", "-CAfile", str(pem_path)), + stdin=subprocess.PIPE, + stdout=subprocess.DEVNULL, + ) as proc: + proc.stdin.write(pem_cert) + proc.stdin.close() + assert proc.wait() == 0 + + +@pytest.mark.skipif(OPENSSL is None, reason="openssl command not found.") +@pytest.mark.requires(Mechanism.RSA_PKCS_KEY_PAIR_GEN, Mechanism.SHA1_RSA_PKCS) +def test_sign_csr(session: pkcs11.Session) -> None: + # Warning: proof of concept code only! + pub, priv = session.generate_keypair(KeyType.RSA, 1024) + + info = CertificationRequestInfo( + { + "version": 0, + "subject": Name.build( + { + "common_name": "Test Certificate", + } + ), + "subject_pk_info": { + "algorithm": { + "algorithm": "rsa", "parameters": None, }, - "signature": value, - } - ) - - # Pipe our CSR to OpenSSL to verify it - with subprocess.Popen( - (OPENSSL, "req", "-inform", "der", "-noout", "-verify"), - stdin=subprocess.PIPE, - stdout=subprocess.DEVNULL, - ) as proc: - proc.stdin.write(csr.dump()) - proc.stdin.close() - - self.assertEqual(proc.wait(), 0) + "public_key": RSAPublicKey.load(encode_rsa_public_key(pub)), + }, + } + ) + + # Sign the CSR Info + value = priv.sign(info.dump(), mechanism=Mechanism.SHA1_RSA_PKCS) + + csr = CertificationRequest( + { + "certification_request_info": info, + "signature_algorithm": { + "algorithm": "sha1_rsa", + "parameters": None, + }, + "signature": value, + } + ) + + # Pipe our CSR to OpenSSL to verify it + with subprocess.Popen( + (OPENSSL, "req", "-inform", "der", "-noout", "-verify"), + stdin=subprocess.PIPE, + stdout=subprocess.DEVNULL, + ) as proc: + proc.stdin.write(csr.dump()) + proc.stdin.close() + + assert proc.wait() == 0 From 7c5c3f12b113a10ad4af16bf2ef15a61dff01b49 Mon Sep 17 00:00:00 2001 From: Mathias Ertl Date: Sat, 14 Dec 2024 20:10:53 +0100 Subject: [PATCH 5/9] use uv dev dependencies, fix test workflow --- .github/workflows/quality.yml | 18 +- .github/workflows/tests.yml | 23 +- .python-version | 2 +- dev-requirements.txt | 11 - pyproject.toml | 19 +- requirements.txt | 1 - uv.lock | 698 ++++++++++++++++++++++++++++++++++ 7 files changed, 729 insertions(+), 43 deletions(-) delete mode 100644 dev-requirements.txt delete mode 100644 requirements.txt diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 36570a3..12cfbd3 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -1,14 +1,9 @@ name: Code quality on: push: -env: - UV_SYSTEM_PYTHON: 1 - jobs: - run: runs-on: ubuntu-latest - steps: - name: Acquire sources uses: actions/checkout@v4.1.1 @@ -21,13 +16,14 @@ jobs: - name: Setup Python uses: actions/setup-python@v5.0.0 with: - python-version: "3.12" + python-version: "3.13" architecture: x64 - name: Install dev dependencies - run: uv pip install -r dev-requirements.txt + run: uv sync --python-preference only-system + + - name: ruff format + run: uv run ruff format --diff . - - name: Run ruff - run: | - ruff format --diff . - ruff check --diff . \ No newline at end of file + - name: ruff check + run: uv run ruff check --diff . \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 82eb770..78690f5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,12 +1,7 @@ name: Tests on: push: -env: - UV_SYSTEM_PYTHON: 1 - SOFTHSM2_CONF: /tmp/softhsm2.conf - jobs: - run: runs-on: ubuntu-latest strategy: @@ -17,20 +12,12 @@ jobs: - "3.11" - "3.12" - "3.12" + - "3.13" steps: - name: Install APT dependencies run: sudo apt-get install -y softhsm2 - - name: env - run: env - - - name: id - run: id - - - name: Create SoftHSM token - run: softhsm2-util --init-token --free --label TEST --pin 1234 --so-pin 5678 - - name: Acquire sources uses: actions/checkout@v4.1.1 @@ -46,10 +33,10 @@ jobs: architecture: x64 - name: Install the project - run: uv sync --all-extras --dev + run: uv sync --all-extras --python-preference only-system --python ${{ matrix.python-version }} - - name: Install dev dependencies - run: uv pip install -r dev-requirements.txt + - name: ls + run: ls /usr/lib/softhsm/libsofthsm2.so - name: Run tests - run: uv run pytest -v \ No newline at end of file + run: uv run --python ${{ matrix.python-version }} pytest -v tests/test_slots_and_tokens.py::test_get_mechanisms \ No newline at end of file diff --git a/.python-version b/.python-version index 24ee5b1..3a4f41e 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.13 +3.13 \ No newline at end of file diff --git a/dev-requirements.txt b/dev-requirements.txt deleted file mode 100644 index db6183b..0000000 --- a/dev-requirements.txt +++ /dev/null @@ -1,11 +0,0 @@ -Cython -setuptools_scm - -# Used for tests -oscrypto -cryptography -parameterized -pytest==8.3.4 -ruff==0.8.2 -sphinx -sphinx-rtd-theme \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index c93aff7..9b0b097 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=68.1", "wheel", "cython"] +requires = ["setuptools>=74.1", "wheel", "cython"] build-backend = "setuptools.build_meta" [project] @@ -22,6 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Security :: Cryptography", ] dependencies = [ @@ -61,5 +62,21 @@ extend-select = [ [tool.ruff.lint.isort] combine-as-imports = true +[tool.setuptools] +ext-modules = [ + {name = "pkcs11._pkcs11", sources = ["pkcs11/_pkcs11.pyx"]} +] + [tool.setuptools.packages.find] include = ["pkcs11*"] + +[dependency-groups] +dev = [ + "cryptography>=44.0.0", + "oscrypto>=1.3.0", + "pytest>=8.3.4", + "ruff>=0.8.3", + "setuptools-scm>=8.1.0", + "sphinx>=7.4.7", + "sphinx-rtd-theme>=3.0.2", +] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index ecf975e..0000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ --e . \ No newline at end of file diff --git a/uv.lock b/uv.lock index a520750..493b887 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,15 @@ version = 1 requires-python = ">=3.9" +[[package]] +name = "alabaster" +version = "0.7.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/3e/13dd8e5ed9094e734ac430b5d0eb4f2bb001708a8b7856cbf8e084e001ba/alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", size = 23776 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92", size = 13511 }, +] + [[package]] name = "asn1crypto" version = "1.5.1" @@ -10,6 +19,425 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c9/7f/09065fd9e27da0eda08b4d6897f1c13535066174cc023af248fc2a8d5e5a/asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67", size = 105045 }, ] +[[package]] +name = "babel" +version = "2.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/74/f1bc80f23eeba13393b7222b11d95ca3af2c1e28edca18af487137eefed9/babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316", size = 9348104 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/20/bc79bc575ba2e2a7f70e8a1155618bb1301eaa5132a8271373a6903f73f8/babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b", size = 9587599 }, +] + +[[package]] +name = "certifi" +version = "2024.12.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/bd/1d41ee578ce09523c81a15426705dd20969f5abf006d1afe8aeff0dd776a/certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db", size = 166010 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/32/8f6669fc4798494966bf446c8c4a162e0b5d893dff088afddf76414f70e1/certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56", size = 164927 }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191 }, + { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592 }, + { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024 }, + { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188 }, + { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571 }, + { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687 }, + { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211 }, + { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325 }, + { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784 }, + { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564 }, + { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804 }, + { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299 }, + { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 }, + { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, + { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, + { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, + { url = "https://files.pythonhosted.org/packages/b9/ea/8bb50596b8ffbc49ddd7a1ad305035daa770202a6b782fc164647c2673ad/cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", size = 182220 }, + { url = "https://files.pythonhosted.org/packages/ae/11/e77c8cd24f58285a82c23af484cf5b124a376b32644e445960d1a4654c3a/cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", size = 178605 }, + { url = "https://files.pythonhosted.org/packages/ed/65/25a8dc32c53bf5b7b6c2686b42ae2ad58743f7ff644844af7cdb29b49361/cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", size = 424910 }, + { url = "https://files.pythonhosted.org/packages/42/7a/9d086fab7c66bd7c4d0f27c57a1b6b068ced810afc498cc8c49e0088661c/cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", size = 447200 }, + { url = "https://files.pythonhosted.org/packages/da/63/1785ced118ce92a993b0ec9e0d0ac8dc3e5dbfbcaa81135be56c69cabbb6/cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", size = 454565 }, + { url = "https://files.pythonhosted.org/packages/74/06/90b8a44abf3556599cdec107f7290277ae8901a58f75e6fe8f970cd72418/cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", size = 435635 }, + { url = "https://files.pythonhosted.org/packages/bd/62/a1f468e5708a70b1d86ead5bab5520861d9c7eacce4a885ded9faa7729c3/cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", size = 445218 }, + { url = "https://files.pythonhosted.org/packages/5b/95/b34462f3ccb09c2594aa782d90a90b045de4ff1f70148ee79c69d37a0a5a/cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", size = 460486 }, + { url = "https://files.pythonhosted.org/packages/fc/fc/a1e4bebd8d680febd29cf6c8a40067182b64f00c7d105f8f26b5bc54317b/cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", size = 437911 }, + { url = "https://files.pythonhosted.org/packages/e6/c3/21cab7a6154b6a5ea330ae80de386e7665254835b9e98ecc1340b3a7de9a/cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", size = 460632 }, + { url = "https://files.pythonhosted.org/packages/cb/b5/fd9f8b5a84010ca169ee49f4e4ad6f8c05f4e3545b72ee041dbbcb159882/cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", size = 171820 }, + { url = "https://files.pythonhosted.org/packages/8c/52/b08750ce0bce45c143e1b5d7357ee8c55341b52bdef4b0f081af1eb248c2/cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", size = 181290 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/8b/825cc84cf13a28bfbcba7c416ec22bf85a9584971be15b21dd8300c65b7f/charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6", size = 196363 }, + { url = "https://files.pythonhosted.org/packages/23/81/d7eef6a99e42c77f444fdd7bc894b0ceca6c3a95c51239e74a722039521c/charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b", size = 125639 }, + { url = "https://files.pythonhosted.org/packages/21/67/b4564d81f48042f520c948abac7079356e94b30cb8ffb22e747532cf469d/charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99", size = 120451 }, + { url = "https://files.pythonhosted.org/packages/c2/72/12a7f0943dd71fb5b4e7b55c41327ac0a1663046a868ee4d0d8e9c369b85/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca", size = 140041 }, + { url = "https://files.pythonhosted.org/packages/67/56/fa28c2c3e31217c4c52158537a2cf5d98a6c1e89d31faf476c89391cd16b/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d", size = 150333 }, + { url = "https://files.pythonhosted.org/packages/f9/d2/466a9be1f32d89eb1554cf84073a5ed9262047acee1ab39cbaefc19635d2/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7", size = 142921 }, + { url = "https://files.pythonhosted.org/packages/f8/01/344ec40cf5d85c1da3c1f57566c59e0c9b56bcc5566c08804a95a6cc8257/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3", size = 144785 }, + { url = "https://files.pythonhosted.org/packages/73/8b/2102692cb6d7e9f03b9a33a710e0164cadfce312872e3efc7cfe22ed26b4/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907", size = 146631 }, + { url = "https://files.pythonhosted.org/packages/d8/96/cc2c1b5d994119ce9f088a9a0c3ebd489d360a2eb058e2c8049f27092847/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b", size = 140867 }, + { url = "https://files.pythonhosted.org/packages/c9/27/cde291783715b8ec30a61c810d0120411844bc4c23b50189b81188b273db/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912", size = 149273 }, + { url = "https://files.pythonhosted.org/packages/3a/a4/8633b0fc1a2d1834d5393dafecce4a1cc56727bfd82b4dc18fc92f0d3cc3/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95", size = 152437 }, + { url = "https://files.pythonhosted.org/packages/64/ea/69af161062166b5975ccbb0961fd2384853190c70786f288684490913bf5/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e", size = 150087 }, + { url = "https://files.pythonhosted.org/packages/3b/fd/e60a9d9fd967f4ad5a92810138192f825d77b4fa2a557990fd575a47695b/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe", size = 145142 }, + { url = "https://files.pythonhosted.org/packages/6d/02/8cb0988a1e49ac9ce2eed1e07b77ff118f2923e9ebd0ede41ba85f2dcb04/charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc", size = 94701 }, + { url = "https://files.pythonhosted.org/packages/d6/20/f1d4670a8a723c46be695dff449d86d6092916f9e99c53051954ee33a1bc/charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749", size = 102191 }, + { url = "https://files.pythonhosted.org/packages/9c/61/73589dcc7a719582bf56aae309b6103d2762b526bffe189d635a7fcfd998/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", size = 193339 }, + { url = "https://files.pythonhosted.org/packages/77/d5/8c982d58144de49f59571f940e329ad6e8615e1e82ef84584c5eeb5e1d72/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", size = 124366 }, + { url = "https://files.pythonhosted.org/packages/bf/19/411a64f01ee971bed3231111b69eb56f9331a769072de479eae7de52296d/charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", size = 118874 }, + { url = "https://files.pythonhosted.org/packages/4c/92/97509850f0d00e9f14a46bc751daabd0ad7765cff29cdfb66c68b6dad57f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", size = 138243 }, + { url = "https://files.pythonhosted.org/packages/e2/29/d227805bff72ed6d6cb1ce08eec707f7cfbd9868044893617eb331f16295/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", size = 148676 }, + { url = "https://files.pythonhosted.org/packages/13/bc/87c2c9f2c144bedfa62f894c3007cd4530ba4b5351acb10dc786428a50f0/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", size = 141289 }, + { url = "https://files.pythonhosted.org/packages/eb/5b/6f10bad0f6461fa272bfbbdf5d0023b5fb9bc6217c92bf068fa5a99820f5/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", size = 142585 }, + { url = "https://files.pythonhosted.org/packages/3b/a0/a68980ab8a1f45a36d9745d35049c1af57d27255eff8c907e3add84cf68f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", size = 144408 }, + { url = "https://files.pythonhosted.org/packages/d7/a1/493919799446464ed0299c8eef3c3fad0daf1c3cd48bff9263c731b0d9e2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", size = 139076 }, + { url = "https://files.pythonhosted.org/packages/fb/9d/9c13753a5a6e0db4a0a6edb1cef7aee39859177b64e1a1e748a6e3ba62c2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", size = 146874 }, + { url = "https://files.pythonhosted.org/packages/75/d2/0ab54463d3410709c09266dfb416d032a08f97fd7d60e94b8c6ef54ae14b/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", size = 150871 }, + { url = "https://files.pythonhosted.org/packages/8d/c9/27e41d481557be53d51e60750b85aa40eaf52b841946b3cdeff363105737/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", size = 148546 }, + { url = "https://files.pythonhosted.org/packages/ee/44/4f62042ca8cdc0cabf87c0fc00ae27cd8b53ab68be3605ba6d071f742ad3/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", size = 143048 }, + { url = "https://files.pythonhosted.org/packages/01/f8/38842422988b795220eb8038745d27a675ce066e2ada79516c118f291f07/charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", size = 94389 }, + { url = "https://files.pythonhosted.org/packages/0b/6e/b13bd47fa9023b3699e94abf565b5a2f0b0be6e9ddac9812182596ee62e4/charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", size = 101752 }, + { url = "https://files.pythonhosted.org/packages/d3/0b/4b7a70987abf9b8196845806198975b6aab4ce016632f817ad758a5aa056/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", size = 194445 }, + { url = "https://files.pythonhosted.org/packages/50/89/354cc56cf4dd2449715bc9a0f54f3aef3dc700d2d62d1fa5bbea53b13426/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", size = 125275 }, + { url = "https://files.pythonhosted.org/packages/fa/44/b730e2a2580110ced837ac083d8ad222343c96bb6b66e9e4e706e4d0b6df/charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", size = 119020 }, + { url = "https://files.pythonhosted.org/packages/9d/e4/9263b8240ed9472a2ae7ddc3e516e71ef46617fe40eaa51221ccd4ad9a27/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", size = 139128 }, + { url = "https://files.pythonhosted.org/packages/6b/e3/9f73e779315a54334240353eaea75854a9a690f3f580e4bd85d977cb2204/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", size = 149277 }, + { url = "https://files.pythonhosted.org/packages/1a/cf/f1f50c2f295312edb8a548d3fa56a5c923b146cd3f24114d5adb7e7be558/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", size = 142174 }, + { url = "https://files.pythonhosted.org/packages/16/92/92a76dc2ff3a12e69ba94e7e05168d37d0345fa08c87e1fe24d0c2a42223/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", size = 143838 }, + { url = "https://files.pythonhosted.org/packages/a4/01/2117ff2b1dfc61695daf2babe4a874bca328489afa85952440b59819e9d7/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", size = 146149 }, + { url = "https://files.pythonhosted.org/packages/f6/9b/93a332b8d25b347f6839ca0a61b7f0287b0930216994e8bf67a75d050255/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", size = 140043 }, + { url = "https://files.pythonhosted.org/packages/ab/f6/7ac4a01adcdecbc7a7587767c776d53d369b8b971382b91211489535acf0/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", size = 148229 }, + { url = "https://files.pythonhosted.org/packages/9d/be/5708ad18161dee7dc6a0f7e6cf3a88ea6279c3e8484844c0590e50e803ef/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", size = 151556 }, + { url = "https://files.pythonhosted.org/packages/5a/bb/3d8bc22bacb9eb89785e83e6723f9888265f3a0de3b9ce724d66bd49884e/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", size = 149772 }, + { url = "https://files.pythonhosted.org/packages/f7/fa/d3fc622de05a86f30beea5fc4e9ac46aead4731e73fd9055496732bcc0a4/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", size = 144800 }, + { url = "https://files.pythonhosted.org/packages/9a/65/bdb9bc496d7d190d725e96816e20e2ae3a6fa42a5cac99c3c3d6ff884118/charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", size = 94836 }, + { url = "https://files.pythonhosted.org/packages/3e/67/7b72b69d25b89c0b3cea583ee372c43aa24df15f0e0f8d3982c57804984b/charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", size = 102187 }, + { url = "https://files.pythonhosted.org/packages/f3/89/68a4c86f1a0002810a27f12e9a7b22feb198c59b2f05231349fbce5c06f4/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", size = 194617 }, + { url = "https://files.pythonhosted.org/packages/4f/cd/8947fe425e2ab0aa57aceb7807af13a0e4162cd21eee42ef5b053447edf5/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", size = 125310 }, + { url = "https://files.pythonhosted.org/packages/5b/f0/b5263e8668a4ee9becc2b451ed909e9c27058337fda5b8c49588183c267a/charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", size = 119126 }, + { url = "https://files.pythonhosted.org/packages/ff/6e/e445afe4f7fda27a533f3234b627b3e515a1b9429bc981c9a5e2aa5d97b6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", size = 139342 }, + { url = "https://files.pythonhosted.org/packages/a1/b2/4af9993b532d93270538ad4926c8e37dc29f2111c36f9c629840c57cd9b3/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", size = 149383 }, + { url = "https://files.pythonhosted.org/packages/fb/6f/4e78c3b97686b871db9be6f31d64e9264e889f8c9d7ab33c771f847f79b7/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", size = 142214 }, + { url = "https://files.pythonhosted.org/packages/2b/c9/1c8fe3ce05d30c87eff498592c89015b19fade13df42850aafae09e94f35/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", size = 144104 }, + { url = "https://files.pythonhosted.org/packages/ee/68/efad5dcb306bf37db7db338338e7bb8ebd8cf38ee5bbd5ceaaaa46f257e6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", size = 146255 }, + { url = "https://files.pythonhosted.org/packages/0c/75/1ed813c3ffd200b1f3e71121c95da3f79e6d2a96120163443b3ad1057505/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", size = 140251 }, + { url = "https://files.pythonhosted.org/packages/7d/0d/6f32255c1979653b448d3c709583557a4d24ff97ac4f3a5be156b2e6a210/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", size = 148474 }, + { url = "https://files.pythonhosted.org/packages/ac/a0/c1b5298de4670d997101fef95b97ac440e8c8d8b4efa5a4d1ef44af82f0d/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", size = 151849 }, + { url = "https://files.pythonhosted.org/packages/04/4f/b3961ba0c664989ba63e30595a3ed0875d6790ff26671e2aae2fdc28a399/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", size = 149781 }, + { url = "https://files.pythonhosted.org/packages/d8/90/6af4cd042066a4adad58ae25648a12c09c879efa4849c705719ba1b23d8c/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482", size = 144970 }, + { url = "https://files.pythonhosted.org/packages/cc/67/e5e7e0cbfefc4ca79025238b43cdf8a2037854195b37d6417f3d0895c4c2/charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", size = 94973 }, + { url = "https://files.pythonhosted.org/packages/65/97/fc9bbc54ee13d33dc54a7fcf17b26368b18505500fc01e228c27b5222d80/charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", size = 102308 }, + { url = "https://files.pythonhosted.org/packages/54/2f/28659eee7f5d003e0f5a3b572765bf76d6e0fe6601ab1f1b1dd4cba7e4f1/charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa", size = 196326 }, + { url = "https://files.pythonhosted.org/packages/d1/18/92869d5c0057baa973a3ee2af71573be7b084b3c3d428fe6463ce71167f8/charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a", size = 125614 }, + { url = "https://files.pythonhosted.org/packages/d6/27/327904c5a54a7796bb9f36810ec4173d2df5d88b401d2b95ef53111d214e/charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0", size = 120450 }, + { url = "https://files.pythonhosted.org/packages/a4/23/65af317914a0308495133b2d654cf67b11bbd6ca16637c4e8a38f80a5a69/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a", size = 140135 }, + { url = "https://files.pythonhosted.org/packages/f2/41/6190102ad521a8aa888519bb014a74251ac4586cde9b38e790901684f9ab/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242", size = 150413 }, + { url = "https://files.pythonhosted.org/packages/7b/ab/f47b0159a69eab9bd915591106859f49670c75f9a19082505ff16f50efc0/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b", size = 142992 }, + { url = "https://files.pythonhosted.org/packages/28/89/60f51ad71f63aaaa7e51a2a2ad37919985a341a1d267070f212cdf6c2d22/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62", size = 144871 }, + { url = "https://files.pythonhosted.org/packages/0c/48/0050550275fea585a6e24460b42465020b53375017d8596c96be57bfabca/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0", size = 146756 }, + { url = "https://files.pythonhosted.org/packages/dc/b5/47f8ee91455946f745e6c9ddbb0f8f50314d2416dd922b213e7d5551ad09/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd", size = 141034 }, + { url = "https://files.pythonhosted.org/packages/84/79/5c731059ebab43e80bf61fa51666b9b18167974b82004f18c76378ed31a3/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be", size = 149434 }, + { url = "https://files.pythonhosted.org/packages/ca/f3/0719cd09fc4dc42066f239cb3c48ced17fc3316afca3e2a30a4756fe49ab/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d", size = 152443 }, + { url = "https://files.pythonhosted.org/packages/f7/0e/c6357297f1157c8e8227ff337e93fd0a90e498e3d6ab96b2782204ecae48/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3", size = 150294 }, + { url = "https://files.pythonhosted.org/packages/54/9a/acfa96dc4ea8c928040b15822b59d0863d6e1757fba8bd7de3dc4f761c13/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742", size = 145314 }, + { url = "https://files.pythonhosted.org/packages/73/1c/b10a63032eaebb8d7bcb8544f12f063f41f5f463778ac61da15d9985e8b6/charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2", size = 94724 }, + { url = "https://files.pythonhosted.org/packages/c5/77/3a78bf28bfaa0863f9cfef278dbeadf55efe064eafff8c7c424ae3c4c1bf/charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca", size = 102159 }, + { url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "cryptography" +version = "44.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/4c/45dfa6829acffa344e3967d6006ee4ae8be57af746ae2eba1c431949b32c/cryptography-44.0.0.tar.gz", hash = "sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02", size = 710657 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/09/8cc67f9b84730ad330b3b72cf867150744bf07ff113cda21a15a1c6d2c7c/cryptography-44.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123", size = 6541833 }, + { url = "https://files.pythonhosted.org/packages/7e/5b/3759e30a103144e29632e7cb72aec28cedc79e514b2ea8896bb17163c19b/cryptography-44.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092", size = 3922710 }, + { url = "https://files.pythonhosted.org/packages/5f/58/3b14bf39f1a0cfd679e753e8647ada56cddbf5acebffe7db90e184c76168/cryptography-44.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f", size = 4137546 }, + { url = "https://files.pythonhosted.org/packages/98/65/13d9e76ca19b0ba5603d71ac8424b5694415b348e719db277b5edc985ff5/cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb", size = 3915420 }, + { url = "https://files.pythonhosted.org/packages/b1/07/40fe09ce96b91fc9276a9ad272832ead0fddedcba87f1190372af8e3039c/cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b", size = 4154498 }, + { url = "https://files.pythonhosted.org/packages/75/ea/af65619c800ec0a7e4034207aec543acdf248d9bffba0533342d1bd435e1/cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543", size = 3932569 }, + { url = "https://files.pythonhosted.org/packages/c7/af/d1deb0c04d59612e3d5e54203159e284d3e7a6921e565bb0eeb6269bdd8a/cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e", size = 4016721 }, + { url = "https://files.pythonhosted.org/packages/bd/69/7ca326c55698d0688db867795134bdfac87136b80ef373aaa42b225d6dd5/cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e", size = 4240915 }, + { url = "https://files.pythonhosted.org/packages/ef/d4/cae11bf68c0f981e0413906c6dd03ae7fa864347ed5fac40021df1ef467c/cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053", size = 2757925 }, + { url = "https://files.pythonhosted.org/packages/64/b1/50d7739254d2002acae64eed4fc43b24ac0cc44bf0a0d388d1ca06ec5bb1/cryptography-44.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd", size = 3202055 }, + { url = "https://files.pythonhosted.org/packages/11/18/61e52a3d28fc1514a43b0ac291177acd1b4de00e9301aaf7ef867076ff8a/cryptography-44.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591", size = 6542801 }, + { url = "https://files.pythonhosted.org/packages/1a/07/5f165b6c65696ef75601b781a280fc3b33f1e0cd6aa5a92d9fb96c410e97/cryptography-44.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7", size = 3922613 }, + { url = "https://files.pythonhosted.org/packages/28/34/6b3ac1d80fc174812486561cf25194338151780f27e438526f9c64e16869/cryptography-44.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc", size = 4137925 }, + { url = "https://files.pythonhosted.org/packages/d0/c7/c656eb08fd22255d21bc3129625ed9cd5ee305f33752ef2278711b3fa98b/cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289", size = 3915417 }, + { url = "https://files.pythonhosted.org/packages/ef/82/72403624f197af0db6bac4e58153bc9ac0e6020e57234115db9596eee85d/cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7", size = 4155160 }, + { url = "https://files.pythonhosted.org/packages/a2/cd/2f3c440913d4329ade49b146d74f2e9766422e1732613f57097fea61f344/cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c", size = 3932331 }, + { url = "https://files.pythonhosted.org/packages/7f/df/8be88797f0a1cca6e255189a57bb49237402b1880d6e8721690c5603ac23/cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64", size = 4017372 }, + { url = "https://files.pythonhosted.org/packages/af/36/5ccc376f025a834e72b8e52e18746b927f34e4520487098e283a719c205e/cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285", size = 4239657 }, + { url = "https://files.pythonhosted.org/packages/46/b0/f4f7d0d0bcfbc8dd6296c1449be326d04217c57afb8b2594f017eed95533/cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417", size = 2758672 }, + { url = "https://files.pythonhosted.org/packages/97/9b/443270b9210f13f6ef240eff73fd32e02d381e7103969dc66ce8e89ee901/cryptography-44.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede", size = 3202071 }, + { url = "https://files.pythonhosted.org/packages/77/d4/fea74422326388bbac0c37b7489a0fcb1681a698c3b875959430ba550daa/cryptography-44.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:37d76e6863da3774cd9db5b409a9ecfd2c71c981c38788d3fcfaf177f447b731", size = 3338857 }, + { url = "https://files.pythonhosted.org/packages/1a/aa/ba8a7467c206cb7b62f09b4168da541b5109838627f582843bbbe0235e8e/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:f677e1268c4e23420c3acade68fac427fffcb8d19d7df95ed7ad17cdef8404f4", size = 3850615 }, + { url = "https://files.pythonhosted.org/packages/89/fa/b160e10a64cc395d090105be14f399b94e617c879efd401188ce0fea39ee/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f5e7cb1e5e56ca0933b4873c0220a78b773b24d40d186b6738080b73d3d0a756", size = 4081622 }, + { url = "https://files.pythonhosted.org/packages/47/8f/20ff0656bb0cf7af26ec1d01f780c5cfbaa7666736063378c5f48558b515/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:8b3e6eae66cf54701ee7d9c83c30ac0a1e3fa17be486033000f2a73a12ab507c", size = 3867546 }, + { url = "https://files.pythonhosted.org/packages/38/d9/28edf32ee2fcdca587146bcde90102a7319b2f2c690edfa627e46d586050/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:be4ce505894d15d5c5037167ffb7f0ae90b7be6f2a98f9a5c3442395501c32fa", size = 4090937 }, + { url = "https://files.pythonhosted.org/packages/cc/9d/37e5da7519de7b0b070a3fedd4230fe76d50d2a21403e0f2153d70ac4163/cryptography-44.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:62901fb618f74d7d81bf408c8719e9ec14d863086efe4185afd07c352aee1d2c", size = 3128774 }, +] + +[[package]] +name = "docutils" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "imagesize" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769 }, +] + +[[package]] +name = "importlib-metadata" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "jinja2" +version = "3.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357 }, + { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393 }, + { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732 }, + { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866 }, + { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964 }, + { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977 }, + { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366 }, + { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091 }, + { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065 }, + { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514 }, + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, + { url = "https://files.pythonhosted.org/packages/a7/ea/9b1530c3fdeeca613faeb0fb5cbcf2389d816072fab72a71b45749ef6062/MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", size = 14344 }, + { url = "https://files.pythonhosted.org/packages/4b/c2/fbdbfe48848e7112ab05e627e718e854d20192b674952d9042ebd8c9e5de/MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", size = 12389 }, + { url = "https://files.pythonhosted.org/packages/f0/25/7a7c6e4dbd4f867d95d94ca15449e91e52856f6ed1905d58ef1de5e211d0/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", size = 21607 }, + { url = "https://files.pythonhosted.org/packages/53/8f/f339c98a178f3c1e545622206b40986a4c3307fe39f70ccd3d9df9a9e425/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", size = 20728 }, + { url = "https://files.pythonhosted.org/packages/1a/03/8496a1a78308456dbd50b23a385c69b41f2e9661c67ea1329849a598a8f9/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", size = 20826 }, + { url = "https://files.pythonhosted.org/packages/e6/cf/0a490a4bd363048c3022f2f475c8c05582179bb179defcee4766fb3dcc18/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", size = 21843 }, + { url = "https://files.pythonhosted.org/packages/19/a3/34187a78613920dfd3cdf68ef6ce5e99c4f3417f035694074beb8848cd77/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", size = 21219 }, + { url = "https://files.pythonhosted.org/packages/17/d8/5811082f85bb88410ad7e452263af048d685669bbbfb7b595e8689152498/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", size = 20946 }, + { url = "https://files.pythonhosted.org/packages/7c/31/bd635fb5989440d9365c5e3c47556cfea121c7803f5034ac843e8f37c2f2/MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", size = 15063 }, + { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506 }, +] + +[[package]] +name = "oscrypto" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asn1crypto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/81/a7654e654a4b30eda06ef9ad8c1b45d1534bfd10b5c045d0c0f6b16fecd2/oscrypto-1.3.0.tar.gz", hash = "sha256:6f5fef59cb5b3708321db7cca56aed8ad7e662853351e7991fcf60ec606d47a4", size = 184590 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl", hash = "sha256:2b2f1d2d42ec152ca90ccb5682f3e051fb55986e1b170ebde472b133713e7085", size = 194553 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, +] + +[[package]] +name = "pygments" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, +] + +[[package]] +name = "pytest" +version = "8.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, +] + [[package]] name = "python-pkcs11" version = "0.7.0" @@ -18,5 +446,275 @@ dependencies = [ { name = "asn1crypto" }, ] +[package.dev-dependencies] +dev = [ + { name = "cryptography" }, + { name = "oscrypto" }, + { name = "pytest" }, + { name = "ruff" }, + { name = "setuptools-scm" }, + { name = "sphinx" }, + { name = "sphinx-rtd-theme" }, +] + [package.metadata] requires-dist = [{ name = "asn1crypto", specifier = ">=1.4.0" }] + +[package.metadata.requires-dev] +dev = [ + { name = "cryptography", specifier = ">=44.0.0" }, + { name = "oscrypto", specifier = ">=1.3.0" }, + { name = "pytest", specifier = ">=8.3.4" }, + { name = "ruff", specifier = ">=0.8.3" }, + { name = "setuptools-scm", specifier = ">=8.1.0" }, + { name = "sphinx", specifier = ">=7.4.7" }, + { name = "sphinx-rtd-theme", specifier = ">=3.0.2" }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "ruff" +version = "0.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/5e/683c7ef7a696923223e7d95ca06755d6e2acbc5fd8382b2912a28008137c/ruff-0.8.3.tar.gz", hash = "sha256:5e7558304353b84279042fc584a4f4cb8a07ae79b2bf3da1a7551d960b5626d3", size = 3378522 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/c4/bfdbb8b9c419ff3b52479af8581026eeaac3764946fdb463dec043441b7d/ruff-0.8.3-py3-none-linux_armv6l.whl", hash = "sha256:8d5d273ffffff0acd3db5bf626d4b131aa5a5ada1276126231c4174543ce20d6", size = 10535860 }, + { url = "https://files.pythonhosted.org/packages/ef/c5/0aabdc9314b4b6f051168ac45227e2aa8e1c6d82718a547455e40c9c9faa/ruff-0.8.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e4d66a21de39f15c9757d00c50c8cdd20ac84f55684ca56def7891a025d7e939", size = 10346327 }, + { url = "https://files.pythonhosted.org/packages/1a/78/4843a59e7e7b398d6019cf91ab06502fd95397b99b2b858798fbab9151f5/ruff-0.8.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c356e770811858bd20832af696ff6c7e884701115094f427b64b25093d6d932d", size = 9942585 }, + { url = "https://files.pythonhosted.org/packages/91/5a/642ed8f1ba23ffc2dd347697e01eef3c42fad6ac76603be4a8c3a9d6311e/ruff-0.8.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c0a60a825e3e177116c84009d5ebaa90cf40dfab56e1358d1df4e29a9a14b13", size = 10797597 }, + { url = "https://files.pythonhosted.org/packages/30/25/2e654bc7226da09a49730a1a2ea6e89f843b362db80b4b2a7a4f948ac986/ruff-0.8.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fb782f4db39501210ac093c79c3de581d306624575eddd7e4e13747e61ba18", size = 10307244 }, + { url = "https://files.pythonhosted.org/packages/c0/2d/a224d56bcd4383583db53c2b8f410ebf1200866984aa6eb9b5a70f04e71f/ruff-0.8.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f26bc76a133ecb09a38b7868737eded6941b70a6d34ef53a4027e83913b6502", size = 11362439 }, + { url = "https://files.pythonhosted.org/packages/82/01/03e2857f9c371b8767d3e909f06a33bbdac880df17f17f93d6f6951c3381/ruff-0.8.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:01b14b2f72a37390c1b13477c1c02d53184f728be2f3ffc3ace5b44e9e87b90d", size = 12078538 }, + { url = "https://files.pythonhosted.org/packages/af/ae/ff7f97b355da16d748ceec50e1604a8215d3659b36b38025a922e0612e9b/ruff-0.8.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53babd6e63e31f4e96ec95ea0d962298f9f0d9cc5990a1bbb023a6baf2503a82", size = 11616172 }, + { url = "https://files.pythonhosted.org/packages/6a/d0/6156d4d1e53ebd17747049afe801c5d7e3014d9b2f398b9236fe36ba4320/ruff-0.8.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ae441ce4cf925b7f363d33cd6570c51435972d697e3e58928973994e56e1452", size = 12919886 }, + { url = "https://files.pythonhosted.org/packages/4e/84/affcb30bacb94f6036a128ad5de0e29f543d3f67ee42b490b17d68e44b8a/ruff-0.8.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7c65bc0cadce32255e93c57d57ecc2cca23149edd52714c0c5d6fa11ec328cd", size = 11212599 }, + { url = "https://files.pythonhosted.org/packages/60/b9/5694716bdefd8f73df7c0104334156c38fb0f77673d2966a5a1345bab94d/ruff-0.8.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5be450bb18f23f0edc5a4e5585c17a56ba88920d598f04a06bd9fd76d324cb20", size = 10784637 }, + { url = "https://files.pythonhosted.org/packages/24/7e/0e8f835103ac7da81c3663eedf79dec8359e9ae9a3b0d704bae50be59176/ruff-0.8.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8faeae3827eaa77f5721f09b9472a18c749139c891dbc17f45e72d8f2ca1f8fc", size = 10390591 }, + { url = "https://files.pythonhosted.org/packages/27/da/180ec771fc01c004045962ce017ca419a0281f4bfaf867ed0020f555b56e/ruff-0.8.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:db503486e1cf074b9808403991663e4277f5c664d3fe237ee0d994d1305bb060", size = 10894298 }, + { url = "https://files.pythonhosted.org/packages/6d/f8/29f241742ed3954eb2222314b02db29f531a15cab3238d1295e8657c5f18/ruff-0.8.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6567be9fb62fbd7a099209257fef4ad2c3153b60579818b31a23c886ed4147ea", size = 11275965 }, + { url = "https://files.pythonhosted.org/packages/79/e9/5b81dc9afc8a80884405b230b9429efeef76d04caead904bd213f453b973/ruff-0.8.3-py3-none-win32.whl", hash = "sha256:19048f2f878f3ee4583fc6cb23fb636e48c2635e30fb2022b3a1cd293402f964", size = 8807651 }, + { url = "https://files.pythonhosted.org/packages/ea/67/7291461066007617b59a707887b90e319b6a043c79b4d19979f86b7a20e7/ruff-0.8.3-py3-none-win_amd64.whl", hash = "sha256:f7df94f57d7418fa7c3ffb650757e0c2b96cf2501a0b192c18e4fb5571dfada9", size = 9625289 }, + { url = "https://files.pythonhosted.org/packages/03/8f/e4fa95288b81233356d9a9dcaed057e5b0adc6399aa8fd0f6d784041c9c3/ruff-0.8.3-py3-none-win_arm64.whl", hash = "sha256:fe2756edf68ea79707c8d68b78ca9a58ed9af22e430430491ee03e718b5e4936", size = 9078754 }, +] + +[[package]] +name = "setuptools" +version = "75.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/54/292f26c208734e9a7f067aea4a7e282c080750c4546559b58e2e45413ca0/setuptools-75.6.0.tar.gz", hash = "sha256:8199222558df7c86216af4f84c30e9b34a61d8ba19366cc914424cdbd28252f6", size = 1337429 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/21/47d163f615df1d30c094f6c8bbb353619274edccf0327b185cc2493c2c33/setuptools-75.6.0-py3-none-any.whl", hash = "sha256:ce74b49e8f7110f9bf04883b730f4765b774ef3ef28f722cce7c273d253aaf7d", size = 1224032 }, +] + +[[package]] +name = "setuptools-scm" +version = "8.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "setuptools" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4f/a4/00a9ac1b555294710d4a68d2ce8dfdf39d72aa4d769a7395d05218d88a42/setuptools_scm-8.1.0.tar.gz", hash = "sha256:42dea1b65771cba93b7a515d65a65d8246e560768a66b9106a592c8e7f26c8a7", size = 76465 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/b9/1906bfeb30f2fc13bb39bf7ddb8749784c05faadbd18a21cf141ba37bff2/setuptools_scm-8.1.0-py3-none-any.whl", hash = "sha256:897a3226a6fd4a6eb2f068745e49733261a21f70b1bb28fce0339feb978d9af3", size = 43666 }, +] + +[[package]] +name = "snowballstemmer" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/7b/af302bebf22c749c56c9c3e8ae13190b5b5db37a33d9068652e8f73b7089/snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", size = 86699 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/dc/c02e01294f7265e63a7315fe086dd1df7dacb9f840a804da846b96d01b96/snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a", size = 93002 }, +] + +[[package]] +name = "sphinx" +version = "7.4.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/be/50e50cb4f2eff47df05673d361095cafd95521d2a22521b920c67a372dcb/sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe", size = 8067911 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/ef/153f6803c5d5f8917dbb7f7fcf6d34a871ede3296fa89c2c703f5f8a6c8e/sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239", size = 3401624 }, +] + +[[package]] +name = "sphinx-rtd-theme" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "sphinx" }, + { name = "sphinxcontrib-jquery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/44/c97faec644d29a5ceddd3020ae2edffa69e7d00054a8c7a6021e82f20335/sphinx_rtd_theme-3.0.2.tar.gz", hash = "sha256:b7457bc25dda723b20b086a670b9953c859eab60a2a03ee8eb2bb23e176e5f85", size = 7620463 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/77/46e3bac77b82b4df5bb5b61f2de98637724f246b4966cfc34bc5895d852a/sphinx_rtd_theme-3.0.2-py2.py3-none-any.whl", hash = "sha256:422ccc750c3a3a311de4ae327e82affdaf59eb695ba4936538552f3b00f4ee13", size = 7655561 }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300 }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530 }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705 }, +] + +[[package]] +name = "sphinxcontrib-jquery" +version = "4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/f3/aa67467e051df70a6330fe7770894b3e4f09436dea6881ae0b4f3d87cad8/sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a", size = 122331 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/85/749bd22d1a68db7291c89e2ebca53f4306c3f205853cf31e9de279034c3c/sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae", size = 121104 }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071 }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743 }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072 }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, +] + +[[package]] +name = "zipp" +version = "3.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/50/bad581df71744867e9468ebd0bcd6505de3b275e06f202c2cb016e3ff56f/zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", size = 24545 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931", size = 9630 }, +] From d65c62f978eb02530aa66d391ca5391519ceeb0a Mon Sep 17 00:00:00 2001 From: Mathias Ertl Date: Sat, 14 Dec 2024 21:27:36 +0100 Subject: [PATCH 6/9] use uv dev dependencies, fix test workflow, final clenaup --- .dockerignore | 6 + .github/workflows/tests.yml | 23 ++- Dockerfile.debian | 21 +++ README.rst => README.md | 290 ++++++++++++++++----------------- pyproject.toml | 1 + setup.py | 26 --- tests/conftest.py | 2 - tests/test_slots_and_tokens.py | 2 - uv.lock | 2 + 9 files changed, 183 insertions(+), 190 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile.debian rename README.rst => README.md (68%) delete mode 100755 setup.py 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..b939009 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,6 +1,8 @@ name: Tests on: push: +env: + PKCS11_MODULE: /home/runner/lib/softhsm/libsofthsm2.so jobs: run: runs-on: ubuntu-latest @@ -15,9 +17,6 @@ jobs: - "3.13" steps: - - name: Install APT dependencies - run: sudo apt-get install -y softhsm2 - - name: Acquire sources uses: actions/checkout@v4.1.1 @@ -32,11 +31,21 @@ 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 + + # Locally compile softhsmv2. For unknown reasons, the version installed by Ubuntu fails on + # Github Actions (while working e.g. in Docker). + - name: Install Softhsm + run: | + curl https://dist.opendnssec.org/source/softhsm-2.6.1.tar.gz | tar -zxv + (cd softhsm-2.6.1 && ./configure --prefix=$HOME --disable-p11-kit --disable-gost && make all install CC="gcc" CXX="g++") + - name: Install the project run: uv sync --all-extras --python-preference only-system --python ${{ matrix.python-version }} - - name: ls - run: ls /usr/lib/softhsm/libsofthsm2.so - - name: Run tests - run: uv run --python ${{ matrix.python-version }} pytest -v tests/test_slots_and_tokens.py::test_get_mechanisms \ No newline at end of file + run: PATH=/home/runner/bin:$PATH uv run --python ${{ matrix.python-version }} pytest -v tests/test_aes.py::test_encrypt \ No newline at end of file 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..83297b9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -67,7 +67,6 @@ def softhsm_setup(tmp_path: Path) -> Iterator[Path]: # pragma: hsm token_dir.mkdir(exist_ok=True, parents=True) softhsm2_conf = tmp_path / "softhsm2.conf" - print("# SoftHSMv2 conf:", softhsm2_conf) with open(softhsm2_conf, "w", encoding="utf-8") as stream: stream.write(f"""# SoftHSM v2 configuration file @@ -118,7 +117,6 @@ def softhsm_token(request: "SubRequest", lib, so_pin: str, pin: str) -> pkcs11.T "--pin", pin, ) - print("+", " ".join(args)) subprocess.run(args, check=True) # Reinitialize library if already loaded (tokens are only seen after (re-)initialization). diff --git a/tests/test_slots_and_tokens.py b/tests/test_slots_and_tokens.py index 1a9d3c1..aba49e0 100644 --- a/tests/test_slots_and_tokens.py +++ b/tests/test_slots_and_tokens.py @@ -24,7 +24,6 @@ def test_double_initialise_different_libs() -> None: def test_get_slots() -> None: lib = pkcs11.lib(LIB_PATH) slots = lib.get_slots() - print(slots) assert len(slots) == 2 slot1, slot2 = slots @@ -61,7 +60,6 @@ def test_get_tokens(softhsm_token: pkcs11.Token) -> None: lib = pkcs11.lib(LIB_PATH) tokens = list(lib.get_tokens(token_flags=pkcs11.TokenFlag.RNG)) - print(tokens) assert len(list(tokens)) == 2 tokens = lib.get_tokens(token_label=softhsm_token.label) 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" }, From b7ec236936e95ec4dce40cd89de21b197ff1540c Mon Sep 17 00:00:00 2001 From: Mathias Ertl Date: Sat, 14 Dec 2024 23:39:49 +0100 Subject: [PATCH 7/9] remove travis configuration --- .travis.yml | 45 --------------------------------------------- 1 file changed, 45 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 58915ca..0000000 --- a/.travis.yml +++ /dev/null @@ -1,45 +0,0 @@ -sudo: false -language: python - -python: - - '3.5' - - '3.6' - - '3.7' - - '3.8' - -env: - global: - - PKCS11_MODULE=/home/travis/lib/softhsm/libsofthsm2.so - - PKCS11_TOKEN_LABEL=TEST - - PKCS11_TOKEN_PIN=1234 - - PKCS11_TOKEN_SO_PIN=5678 - -cache: - - pip - - ccache # For SoftHSMv2 - -before_install: - - pip install -U pip setuptools - - pip install -r dev-requirements.txt - # Install SoftHSMv2 - - curl https://dist.opendnssec.org/source/softhsm-2.5.0.tar.gz | tar -zxv - - (cd softhsm-2.5.0 && ./configure --prefix=$HOME --disable-p11-kit --disable-gost && make all install CC="ccache gcc" CXX="ccache g++") - -before_script: - # Initialise a token on the SoftHSM - - $HOME/bin/softhsm2-util --init-token --free --label TEST --pin 1234 --so-pin 5678 - # Build our extension - - python setup.py build_ext -i - -script: python -m unittest - - -deploy: - provider: pypi - user: danni - password: - secure: "A/W51+GTE9CBAm4m+1AVg11EAF63BUBrCXIonmYCdTT2htEGStk9AJnZGOinHPhwgJoWujBqgqyjqm8wJSvsmhyPSWxGk20lkCJOptcHdExu4FoSnLNNzAgPtZH5lLarkpvxB20J9hUUb4CQbgz5BWeNqFPvKigKFworCksRr9EM4J/Ys8tmkI2zwSTRDH2YAmhI/h8BWGpHMP+pNUsjlp9ZbDaxgNY85r7RloP07N5R0A7TPePH8wJzuGMDOv8dLazdr0epCbvFk+2CyJ7KiEJoX+SlS/2Hi7OKnmuf7QG2z2YyukLlJcP+IhRfzZDVgUeXwJbu24XAUdMoBS8OxId8dOKFla+GJScpWGpA9rO5vgItTAYLG7sd9HuveCtvUZxqbJd5teST4PdcxjjeO5LxYkgXKrLEo1dvDtPOm/veA0axFrXzlberJCKyN2T6grfM5QVUCORQnUYPnOqkYXMHFKSIUfa2mpfJ8NZaHR7jEbddU/PpQTHwlcehMtTx9IKQyfzillmhiXVc+UMOInFbEsU4oD9f0eP2fcs2dDB3ppR+Rdkh8bb80zH2r55Giu4Fv/WGcllwaYvMGkm6TBdod/Hva4sypJaLkLFDH6LQ0jOBHfPdFYKzEn16kZ1F4X+QWl6lKiz4XWAJI8Cf73y9Acj1Q+4MAHobkE/908M=" - skip_upload_docs: true - on: - tags: true - python: 3.6 From 194eb61760fc19029e20d1f8561e33298e99b9ac Mon Sep 17 00:00:00 2001 From: Mathias Ertl Date: Sun, 15 Dec 2024 00:11:29 +0100 Subject: [PATCH 8/9] only use single mechanism per marker, as the fixture only checks the first parameter --- tests/test_des.py | 6 ++++-- tests/test_dh.py | 6 ++++-- tests/test_digest.py | 6 ++++-- tests/test_dsa.py | 6 ++++-- tests/test_ecc.py | 9 ++++++--- tests/test_iterators.py | 6 ++++-- tests/test_sessions.py | 3 ++- tests/test_x509.py | 3 ++- 8 files changed, 30 insertions(+), 15 deletions(-) diff --git a/tests/test_des.py b/tests/test_des.py index 40a8ace..114c9ae 100644 --- a/tests/test_des.py +++ b/tests/test_des.py @@ -20,7 +20,8 @@ def test_generate_des3_key(session: pkcs11.Session): assert isinstance(key, pkcs11.SecretKey) -@pytest.mark.requires(Mechanism.DES2_KEY_GEN, Mechanism.DES3_CBC_PAD) +@pytest.mark.requires(Mechanism.DES2_KEY_GEN) +@pytest.mark.requires(Mechanism.DES3_CBC_PAD) def test_encrypt_des2(session: pkcs11.Session): key = session.generate_key(KeyType.DES2) @@ -31,7 +32,8 @@ def test_encrypt_des2(session: pkcs11.Session): assert plaintext == b"PLAIN TEXT_" -@pytest.mark.requires(Mechanism.DES3_KEY_GEN, Mechanism.DES3_CBC_PAD) +@pytest.mark.requires(Mechanism.DES3_KEY_GEN) +@pytest.mark.requires(Mechanism.DES3_CBC_PAD) def test_encrypt_des3(session: pkcs11.Session): key = session.generate_key(KeyType.DES3) diff --git a/tests/test_dh.py b/tests/test_dh.py index 1ed2035..85052cf 100644 --- a/tests/test_dh.py +++ b/tests/test_dh.py @@ -15,7 +15,8 @@ ) -@pytest.mark.requires(Mechanism.DH_PKCS_KEY_PAIR_GEN, Mechanism.DH_PKCS_DERIVE) +@pytest.mark.requires(Mechanism.DH_PKCS_KEY_PAIR_GEN) +@pytest.mark.requires(Mechanism.DH_PKCS_DERIVE) @pytest.mark.xfail_opencryptoki # AttributeValueInvalid when generating keypair def test_derive_key(session: pkcs11.Session) -> None: # Alice and Bob each create a Diffie-Hellman keypair from the @@ -371,7 +372,8 @@ def test_load_params(session: pkcs11.Session) -> None: assert params[Attribute.PRIME][:4] == b"\xad\x10\x7e\x1e" -@pytest.mark.requires(Mechanism.DH_PKCS_PARAMETER_GEN, Mechanism.DH_PKCS_KEY_PAIR_GEN) +@pytest.mark.requires(Mechanism.DH_PKCS_PARAMETER_GEN) +@pytest.mark.requires(Mechanism.DH_PKCS_KEY_PAIR_GEN) def test_generate_params(session: pkcs11.Session) -> None: params = session.generate_domain_parameters(KeyType.DH, 512) assert isinstance(params, DomainParameters) diff --git a/tests/test_digest.py b/tests/test_digest.py index 2313fcb..6d917fb 100644 --- a/tests/test_digest.py +++ b/tests/test_digest.py @@ -32,7 +32,8 @@ def test_digest_generator(session: pkcs11.Session) -> None: assert digest == m.digest() -@pytest.mark.requires(Mechanism.AES_KEY_GEN, Mechanism.SHA256) +@pytest.mark.requires(Mechanism.AES_KEY_GEN) +@pytest.mark.requires(Mechanism.SHA256) @pytest.mark.skipif(IS_NFAST, reason="nFast can't digest keys") def test_digest_key(session: pkcs11.Session) -> None: key = session.generate_key( @@ -44,7 +45,8 @@ def test_digest_key(session: pkcs11.Session) -> None: assert digest == hashlib.sha256(key[Attribute.VALUE]).digest() -@pytest.mark.requires(Mechanism.AES_KEY_GEN, Mechanism.SHA256) +@pytest.mark.requires(Mechanism.AES_KEY_GEN) +@pytest.mark.requires(Mechanism.SHA256) @pytest.mark.skipif(IS_NFAST, reason="nFast can't digest keys") def test_digest_key_data(session: pkcs11.Session) -> None: key = session.generate_key( diff --git a/tests/test_dsa.py b/tests/test_dsa.py index 456ada1..61f3a72 100644 --- a/tests/test_dsa.py +++ b/tests/test_dsa.py @@ -30,7 +30,8 @@ def test_generate_params(session: pkcs11.Session) -> None: encode_dsa_domain_parameters(parameters) -@pytest.mark.requires(Mechanism.DSA_KEY_PAIR_GEN, Mechanism.DSA_SHA1) +@pytest.mark.requires(Mechanism.DSA_KEY_PAIR_GEN) +@pytest.mark.requires(Mechanism.DSA_SHA1) def test_generate_keypair_and_sign(session: pkcs11.Session): dhparams = session.create_domain_parameters( KeyType.DSA, decode_dsa_domain_parameters(DHPARAMS), local=True @@ -47,7 +48,8 @@ def test_generate_keypair_and_sign(session: pkcs11.Session): @pytest.mark.xfail_nfast -@pytest.mark.requires(Mechanism.DSA_PARAMETER_GEN, Mechanism.DSA_KEY_PAIR_GEN) +@pytest.mark.requires(Mechanism.DSA_PARAMETER_GEN) +@pytest.mark.requires(Mechanism.DSA_KEY_PAIR_GEN) def test_generate_keypair_directly(session: pkcs11.Session): public, private = session.generate_keypair(KeyType.DSA, 1024) assert len(public[Attribute.VALUE]) == 1024 // 8 diff --git a/tests/test_ecc.py b/tests/test_ecc.py index 10394b0..852c4a6 100644 --- a/tests/test_ecc.py +++ b/tests/test_ecc.py @@ -17,7 +17,8 @@ ) -@pytest.mark.requires(Mechanism.EC_KEY_PAIR_GEN, Mechanism.ECDSA) +@pytest.mark.requires(Mechanism.EC_KEY_PAIR_GEN) +@pytest.mark.requires(Mechanism.ECDSA) def test_sign_ecdsa(session: pkcs11.Session) -> None: parameters = session.create_domain_parameters( KeyType.EC, @@ -33,7 +34,8 @@ def test_sign_ecdsa(session: pkcs11.Session) -> None: assert pub.verify(data, ecdsa, mechanism=mechanism) -@pytest.mark.requires(Mechanism.EC_KEY_PAIR_GEN, Mechanism.ECDH1_DERIVE) +@pytest.mark.requires(Mechanism.EC_KEY_PAIR_GEN) +@pytest.mark.requires(Mechanism.ECDH1_DERIVE) def test_derive_key(session: pkcs11.Session) -> None: # DER encoded EC params from OpenSSL # openssl ecparam -out ec_param.der -name prime192v1 @@ -151,7 +153,8 @@ def test_import_key_pair(session: pkcs11.Session) -> None: assert pub.verify(b"Example", signature, mechanism=Mechanism.ECDSA) -@pytest.mark.requires(Mechanism.EC_EDWARDS_KEY_PAIR_GEN, Mechanism.EDDSA) +@pytest.mark.requires(Mechanism.EC_EDWARDS_KEY_PAIR_GEN) +@pytest.mark.requires(Mechanism.EDDSA) def test_sign_eddsa(session: pkcs11.Session) -> None: parameters = session.create_domain_parameters( KeyType.EC_EDWARDS, diff --git a/tests/test_iterators.py b/tests/test_iterators.py index f9676ea..1b3cd8c 100644 --- a/tests/test_iterators.py +++ b/tests/test_iterators.py @@ -7,7 +7,8 @@ import pkcs11 -@pytest.mark.requires(pkcs11.Mechanism.AES_KEY_GEN, pkcs11.Mechanism.AES_CBC_PAD) +@pytest.mark.requires(pkcs11.Mechanism.AES_KEY_GEN) +@pytest.mark.requires(pkcs11.Mechanism.AES_CBC_PAD) def test_partial_decrypt(session: pkcs11.Session) -> None: session.generate_key(pkcs11.KeyType.AES, 128, label="LOOK ME UP") @@ -25,7 +26,8 @@ def test_partial_decrypt(session: pkcs11.Session) -> None: next(iter2) -@pytest.mark.requires(pkcs11.Mechanism.AES_KEY_GEN, pkcs11.Mechanism.AES_CBC_PAD) +@pytest.mark.requires(pkcs11.Mechanism.AES_KEY_GEN) +@pytest.mark.requires(pkcs11.Mechanism.AES_CBC_PAD) # Ideally deleting iterator #1 would terminate the operation, but it # currently does not. @pytest.mark.xfail diff --git a/tests/test_sessions.py b/tests/test_sessions.py index a7459bb..1849e67 100644 --- a/tests/test_sessions.py +++ b/tests/test_sessions.py @@ -60,7 +60,8 @@ def test_generate_key(token: pkcs11.Token, pin: str) -> None: assert key.label == "MY KEY" -@pytest.mark.requires(pkcs11.Mechanism.RSA_PKCS_KEY_PAIR_GEN, pkcs11.Mechanism.RSA_PKCS) +@pytest.mark.requires(pkcs11.Mechanism.RSA_PKCS_KEY_PAIR_GEN) +@pytest.mark.requires(pkcs11.Mechanism.RSA_PKCS) def test_generate_keypair(token: pkcs11.Token, pin: str) -> None: with token.open(user_pin=pin) as session: pub, priv = session.generate_keypair(pkcs11.KeyType.RSA, 1024) diff --git a/tests/test_x509.py b/tests/test_x509.py index e86fad7..cbf3188 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -241,7 +241,8 @@ def test_self_sign_certificate(tmpdir: Path, session: pkcs11.Session) -> None: @pytest.mark.skipif(OPENSSL is None, reason="openssl command not found.") -@pytest.mark.requires(Mechanism.RSA_PKCS_KEY_PAIR_GEN, Mechanism.SHA1_RSA_PKCS) +@pytest.mark.requires(Mechanism.RSA_PKCS_KEY_PAIR_GEN) +@pytest.mark.requires(Mechanism.SHA1_RSA_PKCS) def test_sign_csr(session: pkcs11.Session) -> None: # Warning: proof of concept code only! pub, priv = session.generate_keypair(KeyType.RSA, 1024) From 122e05dfdefc5e790248f768a8eef66cb8fa06f7 Mon Sep 17 00:00:00 2001 From: Mathias Ertl Date: Sun, 15 Dec 2024 00:17:20 +0100 Subject: [PATCH 9/9] fix a few markers --- tests/conftest.py | 2 +- tests/test_sessions.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 83297b9..4b70370 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -42,7 +42,7 @@ def pytest_collection_modifyitems(items) -> None: IS_SOFTHSM, reason="Expected failure with SoftHSMvs.", strict=True ) ) - if "xfail_opencryptoki" in markers: + if "xfail_opencryptoki" in markers and IS_OPENCRYPTOKI: item.add_marker( pytest.mark.xfail( IS_OPENCRYPTOKI, reason="Expected failure with OpenCryptoki.", strict=True diff --git a/tests/test_sessions.py b/tests/test_sessions.py index 1849e67..f704735 100644 --- a/tests/test_sessions.py +++ b/tests/test_sessions.py @@ -110,7 +110,7 @@ def test_destroy_object(token: pkcs11.Token, pin: str) -> None: assert list(session.get_objects()) == [] -@pytest.mark.skipif(IS_NFAST, reason="nFast won't destroy objects.") +@pytest.mark.skipif(not IS_SOFTHSM, reason="Unknown reason.") def test_copy_object(token: pkcs11.Token, pin: str) -> None: with token.open(user_pin=pin) as session: key = session.generate_key(pkcs11.KeyType.AES, 128, label="SAMPLE KEY")