From 5fa614f2b38d366551ebe65bb269ca00d2f486c3 Mon Sep 17 00:00:00 2001 From: M-AlNoaimi <26318936+M-AlNoaimi@users.noreply.github.com> Date: Fri, 23 May 2025 01:05:32 +0100 Subject: [PATCH 1/3] Add PEP 561 type stubs for oqs package The following files have been added: - oqs/py.typed: Marker file for PEP 561 compliance. - oqs/__init__.pyi: Stub for the main package entry point. - oqs/oqs.pyi: Stubs for core OQS functionalities (KeyEncapsulation, Signature, related functions, and exceptions). - oqs/rand.pyi: Stubs for random number generation functions. Signed-off-by: M-AlNoaimi <26318936+M-AlNoaimi@users.noreply.github.com> --- oqs/__init__.pyi | 20 +++++++++++ oqs/oqs.pyi | 94 ++++++++++++++++++++++++++++++++++++++++++++++++ oqs/py.typed | 0 oqs/rand.pyi | 2 ++ 4 files changed, 116 insertions(+) create mode 100644 oqs/__init__.pyi create mode 100644 oqs/oqs.pyi create mode 100644 oqs/py.typed create mode 100644 oqs/rand.pyi diff --git a/oqs/__init__.pyi b/oqs/__init__.pyi new file mode 100644 index 0000000..9b219b6 --- /dev/null +++ b/oqs/__init__.pyi @@ -0,0 +1,20 @@ +# Re-export symbols from oqs/__init__.py for type hinting +from .oqs import ( + KeyEncapsulation, + MechanismNotEnabledError, + MechanismNotSupportedError, + OQS_SUCCESS, + OQS_VERSION, + Signature, + get_enabled_kem_mechanisms, + get_enabled_sig_mechanisms, + get_supported_kem_mechanisms, + get_supported_sig_mechanisms, + is_kem_enabled, + is_sig_enabled, + native, + oqs_python_version, + oqs_version, +) + +__all__: tuple[str, ...] \ No newline at end of file diff --git a/oqs/oqs.pyi b/oqs/oqs.pyi new file mode 100644 index 0000000..4b2fb2f --- /dev/null +++ b/oqs/oqs.pyi @@ -0,0 +1,94 @@ +import ctypes +from types import TracebackType +from typing import Final, TypeVar + +_TKeyEncapsulation = TypeVar("_TKeyEncapsulation", bound="KeyEncapsulation") +_TSignature = TypeVar("_TSignature", bound="Signature") + +OQS_SUCCESS: Final[int] +OQS_ERROR: Final[int] +OQS_VERSION: str | None + +def oqs_python_version() -> str | None: ... +def native() -> ctypes.CDLL: ... +def oqs_version() -> str: ... + +class MechanismNotSupportedError(Exception): + alg_name: str + message: str + def __init__(self, alg_name: str) -> None: ... + +class MechanismNotEnabledError(MechanismNotSupportedError): + # alg_name and message are inherited from MechanismNotSupportedError + def __init__(self, alg_name: str) -> None: ... + +class KeyEncapsulation: + # Attributes from the underlying ctypes.Structure, exposed with Python types + method_name: bytes + alg_version: bytes + claimed_nist_level: int + ind_cca: int + length_public_key: int + length_secret_key: int + length_ciphertext: int + length_shared_secret: int + + # Custom attributes set during initialization + alg_name: str + details: dict[str, str | int | bool] + + def __init__(self, alg_name: str, secret_key: int | bytes | None = None) -> None: ... + def __enter__(self: _TKeyEncapsulation) -> _TKeyEncapsulation: ... + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: ... + def generate_keypair(self) -> bytes: ... + def export_secret_key(self) -> bytes: ... + def encap_secret(self, public_key: int | bytes) -> tuple[bytes, bytes]: ... + def decap_secret(self, ciphertext: int | bytes) -> bytes: ... + def free(self) -> None: ... + def __repr__(self) -> str: ... + +class Signature: + # Attributes from the underlying ctypes.Structure, exposed with Python types + method_name: bytes + alg_version: bytes + claimed_nist_level: int + euf_cma: int + sig_with_ctx_support: int + length_public_key: int + length_secret_key: int + length_signature: int + + # Custom attributes set during initialization + alg_name: str + details: dict[str, str | int | bool] + + def __init__(self, alg_name: str, secret_key: int | bytes | None = None) -> None: ... + def __enter__(self: _TSignature) -> _TSignature: ... + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: ... + def generate_keypair(self) -> bytes: ... + def export_secret_key(self) -> bytes: ... + def sign(self, message: bytes) -> bytes: ... + def verify(self, message: bytes, signature: bytes, public_key: bytes) -> bool: ... + def sign_with_ctx_str(self, message: bytes, context: bytes) -> bytes: ... + def verify_with_ctx_str( + self, message: bytes, signature: bytes, context: bytes, public_key: bytes + ) -> bool: ... + def free(self) -> None: ... + def __repr__(self) -> str: ... + +def is_kem_enabled(alg_name: str) -> bool: ... +def get_enabled_kem_mechanisms() -> tuple[str, ...]: ... +def get_supported_kem_mechanisms() -> tuple[str, ...]: ... +def is_sig_enabled(alg_name: str) -> bool: ... +def get_enabled_sig_mechanisms() -> tuple[str, ...]: ... +def get_supported_sig_mechanisms() -> tuple[str, ...]: ... \ No newline at end of file diff --git a/oqs/py.typed b/oqs/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/oqs/rand.pyi b/oqs/rand.pyi new file mode 100644 index 0000000..f3350f9 --- /dev/null +++ b/oqs/rand.pyi @@ -0,0 +1,2 @@ +def randombytes(bytes_to_read: int) -> bytes: ... +def randombytes_switch_algorithm(alg_name: str) -> None: ... \ No newline at end of file From 43d1c0e99b39b92e543698d559cbfa694c096fea Mon Sep 17 00:00:00 2001 From: M-AlNoaimi <26318936+M-AlNoaimi@users.noreply.github.com> Date: Fri, 23 May 2025 02:08:30 +0100 Subject: [PATCH 2/3] Improved oqs stub to use @final for classes and TypedDict for details attributes & Removed init stub to be more flexible (when making edits directly down the line). Signed-off-by: M-AlNoaimi <26318936+M-AlNoaimi@users.noreply.github.com> --- oqs/__init__.pyi | 20 -------------------- oqs/oqs.pyi | 30 ++++++++++++++++++++++++++---- 2 files changed, 26 insertions(+), 24 deletions(-) delete mode 100644 oqs/__init__.pyi diff --git a/oqs/__init__.pyi b/oqs/__init__.pyi deleted file mode 100644 index 9b219b6..0000000 --- a/oqs/__init__.pyi +++ /dev/null @@ -1,20 +0,0 @@ -# Re-export symbols from oqs/__init__.py for type hinting -from .oqs import ( - KeyEncapsulation, - MechanismNotEnabledError, - MechanismNotSupportedError, - OQS_SUCCESS, - OQS_VERSION, - Signature, - get_enabled_kem_mechanisms, - get_enabled_sig_mechanisms, - get_supported_kem_mechanisms, - get_supported_sig_mechanisms, - is_kem_enabled, - is_sig_enabled, - native, - oqs_python_version, - oqs_version, -) - -__all__: tuple[str, ...] \ No newline at end of file diff --git a/oqs/oqs.pyi b/oqs/oqs.pyi index 4b2fb2f..9315858 100644 --- a/oqs/oqs.pyi +++ b/oqs/oqs.pyi @@ -1,6 +1,6 @@ import ctypes from types import TracebackType -from typing import Final, TypeVar +from typing import Final, TypeVar, final, TypedDict _TKeyEncapsulation = TypeVar("_TKeyEncapsulation", bound="KeyEncapsulation") _TSignature = TypeVar("_TSignature", bound="Signature") @@ -22,6 +22,17 @@ class MechanismNotEnabledError(MechanismNotSupportedError): # alg_name and message are inherited from MechanismNotSupportedError def __init__(self, alg_name: str) -> None: ... +class KeyEncapsulationDetails(TypedDict): + name: str + version: str + claimed_nist_level: int + is_ind_cca: bool + length_public_key: int + length_secret_key: int + length_ciphertext: int + length_shared_secret: int + +@final class KeyEncapsulation: # Attributes from the underlying ctypes.Structure, exposed with Python types method_name: bytes @@ -35,7 +46,7 @@ class KeyEncapsulation: # Custom attributes set during initialization alg_name: str - details: dict[str, str | int | bool] + details: KeyEncapsulationDetails def __init__(self, alg_name: str, secret_key: int | bytes | None = None) -> None: ... def __enter__(self: _TKeyEncapsulation) -> _TKeyEncapsulation: ... @@ -52,6 +63,17 @@ class KeyEncapsulation: def free(self) -> None: ... def __repr__(self) -> str: ... +class SignatureDetails(TypedDict): + name: str + version: str + claimed_nist_level: int + is_euf_cma: bool + sig_with_ctx_support: bool + length_public_key: int + length_secret_key: int + length_signature: int + +@final class Signature: # Attributes from the underlying ctypes.Structure, exposed with Python types method_name: bytes @@ -65,7 +87,7 @@ class Signature: # Custom attributes set during initialization alg_name: str - details: dict[str, str | int | bool] + details: SignatureDetails def __init__(self, alg_name: str, secret_key: int | bytes | None = None) -> None: ... def __enter__(self: _TSignature) -> _TSignature: ... @@ -91,4 +113,4 @@ def get_enabled_kem_mechanisms() -> tuple[str, ...]: ... def get_supported_kem_mechanisms() -> tuple[str, ...]: ... def is_sig_enabled(alg_name: str) -> bool: ... def get_enabled_sig_mechanisms() -> tuple[str, ...]: ... -def get_supported_sig_mechanisms() -> tuple[str, ...]: ... \ No newline at end of file +def get_supported_sig_mechanisms() -> tuple[str, ...]: ... From e78eb7d65b5ccd5e5dd27079b4b9d6daf7b46ba9 Mon Sep 17 00:00:00 2001 From: M-AlNoaimi <26318936+M-AlNoaimi@users.noreply.github.com> Date: Sun, 25 May 2025 00:20:22 +0100 Subject: [PATCH 3/3] Use new C API for context string support detection Signed-off-by: M-AlNoaimi <26318936+M-AlNoaimi@users.noreply.github.com> --- oqs/oqs.py | 12 +++-- oqs/oqs.pyi | 116 ---------------------------------------------- oqs/py.typed | 0 oqs/rand.pyi | 2 - tests/test_sig.py | 27 ++++++++++- 5 files changed, 34 insertions(+), 123 deletions(-) delete mode 100644 oqs/oqs.pyi delete mode 100644 oqs/py.typed delete mode 100644 oqs/rand.pyi diff --git a/oqs/oqs.py b/oqs/oqs.py index 1320a37..d3cc628 100644 --- a/oqs/oqs.py +++ b/oqs/oqs.py @@ -466,6 +466,11 @@ def get_supported_kem_mechanisms() -> tuple[str, ...]: return _supported_KEMs +# Register the OQS_SIG_supports_ctx_str function from the C library +native().OQS_SIG_supports_ctx_str.restype = ct.c_bool +native().OQS_SIG_supports_ctx_str.argtypes = [ct.c_char_p] + + class Signature(ct.Structure): """ An OQS Signature wraps native/C liboqs OQS_SIG structs. @@ -485,7 +490,6 @@ class Signature(ct.Structure): ("alg_version", ct.c_char_p), ("claimed_nist_level", ct.c_ubyte), ("euf_cma", ct.c_ubyte), - ("sig_with_ctx_support", ct.c_ubyte), ("length_public_key", ct.c_size_t), ("length_secret_key", ct.c_size_t), ("length_signature", ct.c_size_t), @@ -515,7 +519,7 @@ def __init__(self, alg_name: str, secret_key: Union[int, bytes, None] = None) -> self.alg_version = self._sig.contents.alg_version self.claimed_nist_level = self._sig.contents.claimed_nist_level self.euf_cma = self._sig.contents.euf_cma - self.sig_with_ctx_support = self._sig.contents.sig_with_ctx_support + self.sig_with_ctx_support = native().OQS_SIG_supports_ctx_str(self.method_name) self.length_public_key = self._sig.contents.length_public_key self.length_secret_key = self._sig.contents.length_secret_key self.length_signature = self._sig.contents.length_signature @@ -634,7 +638,7 @@ def sign_with_ctx_str(self, message: bytes, context: bytes) -> bytes: :param context: the context string. :param message: the message to sign. """ - if context and not self._sig.contents.sig_with_ctx_support: + if context and not self.sig_with_ctx_support: msg = "Signing with context string not supported" raise RuntimeError(msg) @@ -681,7 +685,7 @@ def verify_with_ctx_str( :param context: the context string. :param public_key: the signer's public key. """ - if context and not self._sig.contents.sig_with_ctx_support: + if context and not self.sig_with_ctx_support: msg = "Verifying with context string not supported" raise RuntimeError(msg) diff --git a/oqs/oqs.pyi b/oqs/oqs.pyi deleted file mode 100644 index 9315858..0000000 --- a/oqs/oqs.pyi +++ /dev/null @@ -1,116 +0,0 @@ -import ctypes -from types import TracebackType -from typing import Final, TypeVar, final, TypedDict - -_TKeyEncapsulation = TypeVar("_TKeyEncapsulation", bound="KeyEncapsulation") -_TSignature = TypeVar("_TSignature", bound="Signature") - -OQS_SUCCESS: Final[int] -OQS_ERROR: Final[int] -OQS_VERSION: str | None - -def oqs_python_version() -> str | None: ... -def native() -> ctypes.CDLL: ... -def oqs_version() -> str: ... - -class MechanismNotSupportedError(Exception): - alg_name: str - message: str - def __init__(self, alg_name: str) -> None: ... - -class MechanismNotEnabledError(MechanismNotSupportedError): - # alg_name and message are inherited from MechanismNotSupportedError - def __init__(self, alg_name: str) -> None: ... - -class KeyEncapsulationDetails(TypedDict): - name: str - version: str - claimed_nist_level: int - is_ind_cca: bool - length_public_key: int - length_secret_key: int - length_ciphertext: int - length_shared_secret: int - -@final -class KeyEncapsulation: - # Attributes from the underlying ctypes.Structure, exposed with Python types - method_name: bytes - alg_version: bytes - claimed_nist_level: int - ind_cca: int - length_public_key: int - length_secret_key: int - length_ciphertext: int - length_shared_secret: int - - # Custom attributes set during initialization - alg_name: str - details: KeyEncapsulationDetails - - def __init__(self, alg_name: str, secret_key: int | bytes | None = None) -> None: ... - def __enter__(self: _TKeyEncapsulation) -> _TKeyEncapsulation: ... - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: ... - def generate_keypair(self) -> bytes: ... - def export_secret_key(self) -> bytes: ... - def encap_secret(self, public_key: int | bytes) -> tuple[bytes, bytes]: ... - def decap_secret(self, ciphertext: int | bytes) -> bytes: ... - def free(self) -> None: ... - def __repr__(self) -> str: ... - -class SignatureDetails(TypedDict): - name: str - version: str - claimed_nist_level: int - is_euf_cma: bool - sig_with_ctx_support: bool - length_public_key: int - length_secret_key: int - length_signature: int - -@final -class Signature: - # Attributes from the underlying ctypes.Structure, exposed with Python types - method_name: bytes - alg_version: bytes - claimed_nist_level: int - euf_cma: int - sig_with_ctx_support: int - length_public_key: int - length_secret_key: int - length_signature: int - - # Custom attributes set during initialization - alg_name: str - details: SignatureDetails - - def __init__(self, alg_name: str, secret_key: int | bytes | None = None) -> None: ... - def __enter__(self: _TSignature) -> _TSignature: ... - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: ... - def generate_keypair(self) -> bytes: ... - def export_secret_key(self) -> bytes: ... - def sign(self, message: bytes) -> bytes: ... - def verify(self, message: bytes, signature: bytes, public_key: bytes) -> bool: ... - def sign_with_ctx_str(self, message: bytes, context: bytes) -> bytes: ... - def verify_with_ctx_str( - self, message: bytes, signature: bytes, context: bytes, public_key: bytes - ) -> bool: ... - def free(self) -> None: ... - def __repr__(self) -> str: ... - -def is_kem_enabled(alg_name: str) -> bool: ... -def get_enabled_kem_mechanisms() -> tuple[str, ...]: ... -def get_supported_kem_mechanisms() -> tuple[str, ...]: ... -def is_sig_enabled(alg_name: str) -> bool: ... -def get_enabled_sig_mechanisms() -> tuple[str, ...]: ... -def get_supported_sig_mechanisms() -> tuple[str, ...]: ... diff --git a/oqs/py.typed b/oqs/py.typed deleted file mode 100644 index e69de29..0000000 diff --git a/oqs/rand.pyi b/oqs/rand.pyi deleted file mode 100644 index f3350f9..0000000 --- a/oqs/rand.pyi +++ /dev/null @@ -1,2 +0,0 @@ -def randombytes(bytes_to_read: int) -> bytes: ... -def randombytes_switch_algorithm(alg_name: str) -> None: ... \ No newline at end of file diff --git a/tests/test_sig.py b/tests/test_sig.py index b579e1a..185f6a2 100644 --- a/tests/test_sig.py +++ b/tests/test_sig.py @@ -2,7 +2,7 @@ import random import oqs -from oqs.oqs import Signature +from oqs.oqs import Signature, native # Sigs for which unit testing is disabled disabled_sig_patterns = [] @@ -44,6 +44,31 @@ def check_correctness_with_ctx_str(alg_name: str) -> None: assert sig.verify_with_ctx_str(message, signature, context, public_key) # noqa: S101 +def test_sig_with_ctx_support_detection() -> None: + """ + Test that sig_with_ctx_support matches the C API and that sign_with_ctx_str + raises on unsupported algorithms. + """ + for alg_name in oqs.get_enabled_sig_mechanisms(): + 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 + f"sig_with_ctx_support mismatch for {alg_name}" + ) + # If not supported, sign_with_ctx_str should raise + if not sig.sig_with_ctx_support: + try: + sig.sign_with_ctx_str(b"msg", b"context") + except RuntimeError as e: + if "not supported" not in str(e): + msg = f"Unexpected exception message: {e}" + raise AssertionError(msg) from e + else: + msg = f"sign_with_ctx_str did not raise for {alg_name} without context support" + raise AssertionError(msg) + + def test_wrong_message() -> tuple[None, str]: for alg_name in oqs.get_enabled_sig_mechanisms(): if any(item in alg_name for item in disabled_sig_patterns):