Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/python_simplified.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ name: GitHub actions simplified

on:
push:
branches: [ "**" ]
branches: ["**"]
pull_request:
branches: [ "**" ]
branches: ["**"]
repository_dispatch:
types: [ "**" ]
types: ["**"]

permissions:
contents: read
Expand All @@ -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:
Expand Down
8 changes: 7 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
# 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`
- `version(version_str: str) -> tuple[str, str, str]:` - Returns a tuple
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

Expand Down
12 changes: 2 additions & 10 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -32,13 +24,13 @@ 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.

---

## 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).
5 changes: 4 additions & 1 deletion examples/stfl_sig.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
93 changes: 70 additions & 23 deletions oqs/oqs.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
cast,
Optional,
)

if TYPE_CHECKING:
from collections.abc import Sequence, Iterable
from types import TracebackType
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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()}",
Expand All @@ -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.

Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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, ...]:
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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, ...]:
Expand All @@ -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(
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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."""
Expand All @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions tests/test_sig.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
6 changes: 5 additions & 1 deletion tests/test_stfl_sig.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading