diff --git a/.github/workflows/python_simplified.yml b/.github/workflows/python_simplified.yml index 18a0f8f..def3d8e 100644 --- a/.github/workflows/python_simplified.yml +++ b/.github/workflows/python_simplified.yml @@ -2,11 +2,11 @@ name: GitHub actions simplified on: push: - branches: [ "**" ] + branches: ["**"] pull_request: - branches: [ "**" ] + branches: ["**"] repository_dispatch: - types: [ "**" ] + types: ["**"] permissions: contents: read @@ -15,7 +15,7 @@ jobs: build: strategy: matrix: - os: [ ubuntu-latest, macos-latest, windows-latest ] + os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: diff --git a/CHANGES.md b/CHANGES.md index 0e9fd4e..dbf5d35 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,4 @@ -# Pre-release +# Version 0.14.0 - August xx, 2025 - Added type checking and automatic linting/formatting, https://github.com/open-quantum-safe/liboqs-python/pull/97 - Added a utility function for de-structuring version strings in `oqs.py` @@ -6,6 +6,12 @@ containing the (major, minor, patch) versions - A warning is issued only if the liboqs-python version's major and minor numbers differ from those of liboqs, ignoring the patch version +- Added stateful signature support via the `StatefulSignature` class +- New enumeration helpers `get_enabled_stateful_sig_mechanisms()` and + `get_supported_stateful_sig_mechanisms()` +- ML-KEM keys can be generated from a seed via + `KeyEncapsulation.generate_keypair_seed()`. +- Minimum required Python 3 version bumped to 3.11 # Version 0.12.0 - January 15, 2025 diff --git a/RELEASE.md b/RELEASE.md index bdebfa6..1da57c9 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,14 +1,6 @@ # liboqs-python version 0.14.0 --- -# Added in version 0.14.0 July 2025 - -- Added stateful signature support via the `StatefulSignature` class. -- New enumeration helpers `get_enabled_stateful_sig_mechanisms()` and - `get_supported_stateful_sig_mechanisms()`. -- Updated to liboqs 0.14.0. -- ML-KEM keys can be generated from a seed via - `KeyEncapsulation.generate_keypair_seed()`. ## About @@ -32,7 +24,7 @@ See in particular limitations on intended use. ## Release notes -This release of liboqs-python was released on July 10, 2025. Its release +This release of liboqs-python was released on August xx, 2025. Its release page on GitHub is https://github.com/open-quantum-safe/liboqs-python/releases/tag/0.14.0. @@ -40,5 +32,5 @@ https://github.com/open-quantum-safe/liboqs-python/releases/tag/0.14.0. ## What's New -This is the 11th release of liboqs-python. For a list of changes see +This is the 12th release of liboqs-python. For a list of changes see [CHANGES.md](https://github.com/open-quantum-safe/liboqs-python/blob/main/CHANGES.md). diff --git a/examples/stfl_sig.py b/examples/stfl_sig.py index da312a2..1878215 100644 --- a/examples/stfl_sig.py +++ b/examples/stfl_sig.py @@ -22,7 +22,10 @@ # Create signer and verifier with sample signature mechanisms stfl_sigalg = "XMSS-SHA2_10_256" -with StatefulSignature(stfl_sigalg) as signer, StatefulSignature(stfl_sigalg) as verifier: +with ( + StatefulSignature(stfl_sigalg) as signer, + StatefulSignature(stfl_sigalg) as verifier, +): logger.info("Signature details:\n%s", pformat(signer.details)) # Signer generates its keypair diff --git a/oqs/oqs.py b/oqs/oqs.py index 9a24f0a..0f88dc6 100644 --- a/oqs/oqs.py +++ b/oqs/oqs.py @@ -36,6 +36,7 @@ cast, Optional, ) + if TYPE_CHECKING: from collections.abc import Sequence, Iterable from types import TracebackType @@ -244,7 +245,9 @@ def _load_liboqs() -> ct.CDLL: assert liboqs # noqa: S101 except RuntimeError: # We don't have liboqs, so we try to install it automatically - _install_liboqs(target_directory=oqs_install_dir, oqs_version_to_install=OQS_VERSION) + _install_liboqs( + target_directory=oqs_install_dir, oqs_version_to_install=OQS_VERSION + ) # Try loading it again try: liboqs = _load_shared_obj( @@ -283,9 +286,13 @@ def oqs_version() -> str: oqs_python_ver = oqs_python_version() if oqs_python_ver: - oqs_python_ver_major, oqs_python_ver_minor, oqs_python_ver_patch = version(oqs_python_ver) + oqs_python_ver_major, oqs_python_ver_minor, oqs_python_ver_patch = version( + oqs_python_ver + ) # Warn the user if the liboqs version differs from liboqs-python version - if not (oqs_ver_major == oqs_python_ver_major and oqs_ver_minor == oqs_python_ver_minor): + if not ( + oqs_ver_major == oqs_python_ver_major and oqs_ver_minor == oqs_python_ver_minor + ): warnings.warn( f"liboqs version (major, minor) {oqs_version()} differs from liboqs-python version " f"{oqs_python_version()}", @@ -296,7 +303,9 @@ def oqs_version() -> str: class MechanismNotSupportedError(Exception): """Exception raised when an algorithm is not supported by OQS.""" - def __init__(self, alg_name: str, supported: Optional[Iterable[str]] = None) -> None: + def __init__( + self, alg_name: str, supported: Optional[Iterable[str]] = None + ) -> None: """ Initialize the exception. @@ -363,7 +372,9 @@ class KeyEncapsulation(ct.Structure): ("decaps_cb", ct.c_void_p), ] - def __init__(self, alg_name: str, secret_key: Union[int, bytes, None] = None) -> None: + def __init__( + self, alg_name: str, secret_key: Union[int, bytes, None] = None + ) -> None: """ Create new KeyEncapsulation with the given algorithm. @@ -552,9 +563,13 @@ def is_kem_enabled(alg_name: str) -> bool: return native().OQS_KEM_alg_is_enabled(ct.create_string_buffer(alg_name.encode())) -_KEM_alg_ids = [native().OQS_KEM_alg_identifier(i) for i in range(native().OQS_KEM_alg_count())] -_supported_KEMs: tuple[str, ...] = tuple([i.decode() for i in _KEM_alg_ids]) # noqa: N816 -_enabled_KEMs: tuple[str, ...] = tuple([i for i in _supported_KEMs if is_kem_enabled(i)]) # noqa: N816 +_KEM_alg_ids = [ + native().OQS_KEM_alg_identifier(i) for i in range(native().OQS_KEM_alg_count()) +] +_supported_KEMs: tuple[str, ...] = tuple([i.decode() for i in _KEM_alg_ids]) +_enabled_KEMs: tuple[str, ...] = tuple( + [i for i in _supported_KEMs if is_kem_enabled(i)] +) def get_enabled_kem_mechanisms() -> tuple[str, ...]: @@ -603,7 +618,9 @@ class Signature(ct.Structure): ("verify_with_ctx_cb", ct.c_void_p), ] - def __init__(self, alg_name: str, secret_key: Union[int, bytes, None] = None) -> None: + def __init__( + self, alg_name: str, secret_key: Union[int, bytes, None] = None + ) -> None: """ Create new Signature with the given algorithm. @@ -746,8 +763,10 @@ def sign_with_ctx_str(self, message: bytes, context: bytes) -> bytes: :param message: the message to sign. """ if context and not self._sig.contents.sig_with_ctx_support: - msg = (f"Signing with context is not supported for: " - f"{self._sig.contents.method_name.decode()}") + msg = ( + f"Signing with context is not supported for: " + f"{self._sig.contents.method_name.decode()}" + ) raise RuntimeError(msg) # Provide length to avoid extra null char @@ -859,12 +878,18 @@ def sig_supports_context(alg_name: str) -> bool: :param alg_name: A signature mechanism algorithm name. """ - return bool(native().OQS_SIG_supports_ctx_str(ct.create_string_buffer(alg_name.encode()))) + return bool( + native().OQS_SIG_supports_ctx_str(ct.create_string_buffer(alg_name.encode())) + ) -_sig_alg_ids = [native().OQS_SIG_alg_identifier(i) for i in range(native().OQS_SIG_alg_count())] +_sig_alg_ids = [ + native().OQS_SIG_alg_identifier(i) for i in range(native().OQS_SIG_alg_count()) +] _supported_sigs: tuple[str, ...] = tuple([i.decode() for i in _sig_alg_ids]) -_enabled_sigs: tuple[str, ...] = tuple([i for i in _supported_sigs if is_sig_enabled(i)]) +_enabled_sigs: tuple[str, ...] = tuple( + [i for i in _supported_sigs if is_sig_enabled(i)] +) def get_enabled_sig_mechanisms() -> tuple[str, ...]: @@ -883,7 +908,9 @@ def get_supported_sig_mechanisms() -> tuple[str, ...]: def is_stateful_sig_enabled(alg_name: str) -> bool: """Check if a stateful signature algorithm is enabled.""" - return native().OQS_SIG_STFL_alg_is_enabled(ct.create_string_buffer(alg_name.encode())) + return native().OQS_SIG_STFL_alg_is_enabled( + ct.create_string_buffer(alg_name.encode()) + ) _supported_stateful_sigs: tuple[str, ...] = tuple( @@ -971,7 +998,9 @@ def __init__(self, alg_name: str, secret_key: Optional[bytes] = None) -> None: super().__init__() _check_alg(alg_name) - self._sig = native().OQS_SIG_STFL_new(ct.create_string_buffer(alg_name.encode())) + self._sig = native().OQS_SIG_STFL_new( + ct.create_string_buffer(alg_name.encode()) + ) if not self._sig: msg = f"Could not allocate OQS_SIG_STFL for {alg_name}" raise RuntimeError(msg) @@ -1008,7 +1037,9 @@ def _cb(buf: bytes, length: int, _: ct.c_void_p) -> int: return OQS_SUCCESS self._store_cb = _cb # keep ref - native().OQS_SIG_STFL_SECRET_KEY_SET_store_cb(self._secret_key, self._store_cb, None) + native().OQS_SIG_STFL_SECRET_KEY_SET_store_cb( + self._secret_key, self._store_cb, None + ) def _new_secret_key(self) -> None: """Create a new secret key for the stateful signature.""" @@ -1022,7 +1053,9 @@ def _load_secret_key(self, data: bytes) -> None: """Load a secret key from bytes.""" self._new_secret_key() buf = ct.create_string_buffer(data, len(data)) - rc = native().OQS_SIG_STFL_SECRET_KEY_deserialize(self._secret_key, buf, len(data), None) + rc = native().OQS_SIG_STFL_SECRET_KEY_deserialize( + self._secret_key, buf, len(data), None + ) if rc != OQS_SUCCESS: msg = "Secret‑key deserialization failed" raise RuntimeError(msg) @@ -1098,7 +1131,9 @@ def verify(self, message: bytes, signature: bytes, public_key: bytes) -> bool: msg = ct.create_string_buffer(message, len(message)) sig = ct.create_string_buffer(signature, len(signature)) pk = ct.create_string_buffer(public_key, len(public_key)) - rc = native().OQS_SIG_STFL_verify(self._sig, msg, len(message), sig, len(signature), pk) + rc = native().OQS_SIG_STFL_verify( + self._sig, msg, len(message), sig, len(signature), pk + ) return rc == OQS_SUCCESS def export_secret_key(self) -> bytes: @@ -1126,7 +1161,9 @@ def export_secret_key(self) -> bytes: def sigs_total(self) -> int: """Get the total number of signatures that can be made with the secret key.""" total = ct.c_uint64() - rc = native().OQS_SIG_STFL_sigs_total(self._sig, ct.byref(total), self._secret_key) + rc = native().OQS_SIG_STFL_sigs_total( + self._sig, ct.byref(total), self._secret_key + ) if rc != OQS_SUCCESS: msg = "Failed to get total signature count" raise RuntimeError(msg) @@ -1138,7 +1175,9 @@ def sigs_remaining(self) -> int: msg = "Secret key not initialised – call generate_keypair() first" raise ValueError(msg) remain = ct.c_uint64() - rc = native().OQS_SIG_STFL_sigs_remaining(self._sig, ct.byref(remain), self._secret_key) + rc = native().OQS_SIG_STFL_sigs_remaining( + self._sig, ct.byref(remain), self._secret_key + ) if rc != OQS_SUCCESS: msg = "Failed to get remaining signature count" raise ValueError(msg) @@ -1173,8 +1212,16 @@ def free(self) -> None: native().OQS_SIG_STFL_new.restype = ct.POINTER(StatefulSignature) native().OQS_SIG_STFL_SECRET_KEY_new.restype = ct.c_void_p native().OQS_SIG_STFL_SECRET_KEY_new.argtypes = [ct.c_char_p] -native().OQS_SIG_STFL_SECRET_KEY_SET_store_cb.argtypes = [ct.c_void_p, ct.c_void_p, ct.c_void_p] -native().OQS_SIG_STFL_keypair.argtypes = [ct.POINTER(StatefulSignature), ct.c_void_p, ct.c_void_p] +native().OQS_SIG_STFL_SECRET_KEY_SET_store_cb.argtypes = [ + ct.c_void_p, + ct.c_void_p, + ct.c_void_p, +] +native().OQS_SIG_STFL_keypair.argtypes = [ + ct.POINTER(StatefulSignature), + ct.c_void_p, + ct.c_void_p, +] native().OQS_SIG_STFL_sign.argtypes = [ ct.POINTER(StatefulSignature), ct.c_void_p, diff --git a/tests/test_sig.py b/tests/test_sig.py index 185f6a2..2ff86b4 100644 --- a/tests/test_sig.py +++ b/tests/test_sig.py @@ -53,9 +53,9 @@ def test_sig_with_ctx_support_detection() -> None: with Signature(alg_name) as sig: # Check Python attribute matches C API c_api_result = native().OQS_SIG_supports_ctx_str(sig.method_name) - assert bool(sig.sig_with_ctx_support) == bool(c_api_result), ( # noqa: S101 + assert bool(sig.sig_with_ctx_support) == bool(c_api_result), ( f"sig_with_ctx_support mismatch for {alg_name}" - ) + ) # noqa: S101 # If not supported, sign_with_ctx_str should raise if not sig.sig_with_ctx_support: try: diff --git a/tests/test_stfl_sig.py b/tests/test_stfl_sig.py index c2459ed..433aa87 100644 --- a/tests/test_stfl_sig.py +++ b/tests/test_stfl_sig.py @@ -4,7 +4,11 @@ import oqs -_skip_names = ["LMS_SHA256_H20_W8_H10_W8", "LMS_SHA256_H20_W8_H15_W8", "LMS_SHA256_H20_W8_H20_W8"] +_skip_names = [ + "LMS_SHA256_H20_W8_H10_W8", + "LMS_SHA256_H20_W8_H15_W8", + "LMS_SHA256_H20_W8_H20_W8", +] # Sigs for which unit testing is disabled