From 41f7bcb78a4eb6291350a0408531d6e480a3f6cd Mon Sep 17 00:00:00 2001 From: Vlad Gheorghiu Date: Wed, 15 Jan 2025 13:00:33 -0500 Subject: [PATCH] Version 0.12.0 (#100) * Version 0.12.0 Signed-off-by: Vlad Gheorghiu * update * Update Signed-off-by: Vlad Gheorghiu * Update Signed-off-by: Vlad Gheorghiu * Update Signed-off-by: Vlad Gheorghiu * update Signed-off-by: Vlad Gheorghiu * Update Signed-off-by: Vlad Gheorghiu * update Signed-off-by: Vlad Gheorghiu * Update Signed-off-by: Vlad Gheorghiu * Update Signed-off-by: Vlad Gheorghiu * Updates Signed-off-by: Vlad Gheorghiu * c_int->c_size_t Signed-off-by: Vlad Gheorghiu * Fixes Signed-off-by: Vlad Gheorghiu * Update Signed-off-by: Vlad Gheorghiu * Version 0.12 Signed-off-by: Vlad Gheorghiu * Update Signed-off-by: Vlad Gheorghiu * Updated CHANGES Signed-off-by: Vlad Gheorghiu --------- Signed-off-by: Vlad Gheorghiu --- CHANGES.md | 16 +++++ LICENSE | 2 +- README.md | 11 ++-- RELEASE.md | 8 +-- examples/kem.py | 2 +- examples/sig.py | 2 +- oqs/oqs.py | 150 ++++++++++++++++++++++++++++++++++++---------- oqs/rand.py | 2 +- pyproject.toml | 2 +- tests/test_kem.py | 24 ++++---- tests/test_sig.py | 35 ++++++++--- 11 files changed, 188 insertions(+), 66 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 6159675..e1aec45 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,19 @@ +# Version 0.12.0 - January 15, 2025 + +- Fixes https://github.com/open-quantum-safe/liboqs-python/issues/98. The API + that NIST has introduced in + [FIPS 204](https://csrc.nist.gov/pubs/fips/204/final) + for ML-DSA includes a context string of length >= 0. Added new API for + signing with a context string + - `Signature.sign_with_ctx_str(self, message, context)` + - `Signature.verify_with_ctx_str(self, message, signature, context, +public_key)` +- When operations fail (i.e., `OQS_SUCCESS != 0`) in functions returning + non-boolean objects, a `RuntimeError` is now raised, instead of returning 0 +- Bugfix on Linux, `c_int` -> `c_size_t` for buffer sizes +- Pyright type checking fixes +- Updated examples to use `ML-KEM` and `ML-DSA` as the defaults + # Version 0.10.0 - April 1, 2024 - Replaced CHANGES by diff --git a/LICENSE b/LICENSE index 1fe6778..9eff3c3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018-2024 Open Quantum Safe +Copyright (c) 2018-2025 Open Quantum Safe Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index da26f31..2a5d267 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ you have access to a Python 3 interpreter. liboqs-python has been extensively tested on Linux, macOS and Windows platforms. Continuous integration is provided via GitHub actions. -The project contains the following files and directories: +The project contains the following files and directories - **`oqs/oqs.py`: a Python 3 module wrapper for the liboqs C library.** - `oqs/rand.py`: a Python 3 module supporting RNGs from `` @@ -84,7 +84,8 @@ an alternative path, e.g., `C:\liboqs`, by passing the cmake -S liboqs -B liboqs/build -DCMAKE_INSTALL_PREFIX="C:\liboqs" -DCMAKE_WINDOWS_EXPORT_ALL_SYMBOLS=TRUE -DBUILD_SHARED_LIBS=ON ``` -Alternatively you can set the `OQS_INSTALL_PATH` environment variable to point to the installation directory, e.g., on a UNIX-like system execute: +Alternatively, you can set the `OQS_INSTALL_PATH` environment variable to point +to the installation directory, e.g., on a UNIX-like system, execute ```shell export OQS_INSTALL_PATH=/path/to/liboqs @@ -148,7 +149,7 @@ python3 liboqs-python/examples/rand.py Execute ```shell -nose2 --verbose liboqs-python +nose2 --verbose ``` --- @@ -205,7 +206,7 @@ docker run -it oqs-python sh -c ". venv/bin/activate && python liboqs-python/exa Or, run the unit tests with ```shell -docker run -it oqs-python sh -c ". venv/bin/activate && nose2 --verbose liboqs-python" +docker run -it oqs-python sh -c ". venv/bin/activate && nose2 --verbose" ``` In case you want to use the Docker container as a development environment, @@ -265,7 +266,7 @@ Waterloo. ### Contributors -Contributors to the liboqs-python wrapper include: +Contributors to the liboqs-python wrapper include - Ben Davies (University of Waterloo) - Vlad Gheorghiu ([softwareQ Inc.](https://www.softwareq.ca) and the University diff --git a/RELEASE.md b/RELEASE.md index 6fcfdb6..65de4f2 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,4 +1,4 @@ -# liboqs-python version 0.10.0 +# liboqs-python version 0.12.0 --- @@ -24,13 +24,13 @@ See in particular limitations on intended use. ## Release notes -This release of liboqs-python was released on April 1, 2024. Its release +This release of liboqs-python was released on January 15, 2025. Its release page on GitHub is -https://github.com/open-quantum-safe/liboqs-python/releases/tag/0.10.0. +https://github.com/open-quantum-safe/liboqs-python/releases/tag/0.12.0. --- ## What's New -This is the 10th release of liboqs-python. For a list of changes see +This is the 11th 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/kem.py b/examples/kem.py index a9deb2d..dbbaefb 100644 --- a/examples/kem.py +++ b/examples/kem.py @@ -10,7 +10,7 @@ pprint(kems, compact=True) # Create client and server with sample KEM mechanisms -kemalg = "Kyber512" +kemalg = "ML-KEM-512" with oqs.KeyEncapsulation(kemalg) as client: with oqs.KeyEncapsulation(kemalg) as server: print("\nKey encapsulation details:") diff --git a/examples/sig.py b/examples/sig.py index ea07508..3a20bfd 100644 --- a/examples/sig.py +++ b/examples/sig.py @@ -12,7 +12,7 @@ message = "This is the message to sign".encode() # Create signer and verifier with sample signature mechanisms -sigalg = "Dilithium2" +sigalg = "ML-DSA-44" with oqs.Signature(sigalg) as signer: with oqs.Signature(sigalg) as verifier: print("\nSignature details:") diff --git a/oqs/oqs.py b/oqs/oqs.py index c0ab485..e57ce89 100644 --- a/oqs/oqs.py +++ b/oqs/oqs.py @@ -45,7 +45,7 @@ def _countdown(seconds): def _load_shared_obj(name, additional_searching_paths=None): """Attempts to load shared library.""" paths = [] - dll = ct.windll if platform.system() == "Windows" else ct.cdll + dll = ct.windll if platform.system() == "Windows" else ct.cdll # type: ignore # Search additional path, if any if additional_searching_paths: @@ -138,7 +138,6 @@ def _load_liboqs(): assert _liboqs except RuntimeError: sys.exit("Could not load liboqs shared library") - return _liboqs @@ -162,7 +161,7 @@ def native(): def oqs_version(): """liboqs version string.""" native().OQS_version.restype = ct.c_char_p - return ct.c_char_p(native().OQS_version()).value.decode("UTF-8") + return ct.c_char_p(native().OQS_version()).value.decode() # type: ignore # Warn the user if the liboqs version differs from liboqs-python version @@ -262,7 +261,7 @@ def __init__(self, alg_name, secret_key=None): def __enter__(self): return self - def __exit__(self, ctx_type, ctx_value, ctx_traceback): + def __exit__(self, _ctx_type, _ctx_value, _ctx_traceback): self.free() def generate_keypair(self): @@ -276,7 +275,10 @@ def generate_keypair(self): rv = native().OQS_KEM_keypair( self._kem, ct.byref(public_key), ct.byref(self.secret_key) ) - return bytes(public_key) if rv == OQS_SUCCESS else 0 + if rv == OQS_SUCCESS: + return bytes(public_key) + else: + raise RuntimeError("Can not generate keypair") def export_secret_key(self): """Exports the secret key.""" @@ -288,15 +290,18 @@ def encap_secret(self, public_key): :param public_key: the peer's public key. """ - my_public_key = ct.create_string_buffer( + c_public_key = ct.create_string_buffer( public_key, self._kem.contents.length_public_key ) ciphertext = ct.create_string_buffer(self._kem.contents.length_ciphertext) shared_secret = ct.create_string_buffer(self._kem.contents.length_shared_secret) rv = native().OQS_KEM_encaps( - self._kem, ct.byref(ciphertext), ct.byref(shared_secret), my_public_key + self._kem, ct.byref(ciphertext), ct.byref(shared_secret), c_public_key ) - return bytes(ciphertext), bytes(shared_secret) if rv == OQS_SUCCESS else 0 + if rv == OQS_SUCCESS: + return bytes(ciphertext), bytes(shared_secret) + else: + raise RuntimeError("Can not encapsulate secret") def decap_secret(self, ciphertext): """ @@ -304,14 +309,17 @@ def decap_secret(self, ciphertext): :param ciphertext: the ciphertext received from the peer. """ - my_ciphertext = ct.create_string_buffer( + c_ciphertext = ct.create_string_buffer( ciphertext, self._kem.contents.length_ciphertext ) shared_secret = ct.create_string_buffer(self._kem.contents.length_shared_secret) rv = native().OQS_KEM_decaps( - self._kem, ct.byref(shared_secret), my_ciphertext, self.secret_key + self._kem, ct.byref(shared_secret), c_ciphertext, self.secret_key ) - return bytes(shared_secret) if rv == OQS_SUCCESS else 0 + if rv == OQS_SUCCESS: + return bytes(shared_secret) + else: + raise RuntimeError("Can not decapsulate secret") def free(self): """Releases the native resources.""" @@ -374,6 +382,7 @@ 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), @@ -404,6 +413,7 @@ def __init__(self, alg_name, secret_key=None): "version": self._sig.contents.alg_version.decode(), "claimed_nist_level": int(self._sig.contents.claimed_nist_level), "is_euf_cma": bool(self._sig.contents.euf_cma), + "sig_with_ctx_support": bool(self._sig.contents.sig_with_ctx_support), "length_public_key": int(self._sig.contents.length_public_key), "length_secret_key": int(self._sig.contents.length_secret_key), "length_signature": int(self._sig.contents.length_signature), @@ -417,7 +427,7 @@ def __init__(self, alg_name, secret_key=None): def __enter__(self): return self - def __exit__(self, ctx_type, ctx_value, ctx_traceback): + def __exit__(self, _ctx_type, _ctx_value, _ctx_traceback): self.free() def generate_keypair(self): @@ -431,7 +441,10 @@ def generate_keypair(self): rv = native().OQS_SIG_keypair( self._sig, ct.byref(public_key), ct.byref(self.secret_key) ) - return bytes(public_key) if rv == OQS_SUCCESS else 0 + if rv == OQS_SUCCESS: + return bytes(public_key) + else: + raise RuntimeError("Can not generate keypair") def export_secret_key(self): """Exports the secret key.""" @@ -444,22 +457,25 @@ def sign(self, message): :param message: the message to sign. """ # Provide length to avoid extra null char - my_message = ct.create_string_buffer(message, len(message)) - message_len = ct.c_int(len(my_message)) - signature = ct.create_string_buffer(self._sig.contents.length_signature) - sig_len = ct.c_int( - self._sig.contents.length_signature - ) # initialize to maximum signature size + c_message = ct.create_string_buffer(message, len(message)) + c_message_len = ct.c_size_t(len(c_message)) + c_signature = ct.create_string_buffer(self._sig.contents.length_signature) + + # Initialize to maximum signature size + signature_len = ct.c_size_t(self._sig.contents.length_signature) + rv = native().OQS_SIG_sign( self._sig, - ct.byref(signature), - ct.byref(sig_len), - my_message, - message_len, + ct.byref(c_signature), + ct.byref(signature_len), + c_message, + c_message_len, self.secret_key, ) - - return bytes(signature[: sig_len.value]) if rv == OQS_SUCCESS else 0 + if rv == OQS_SUCCESS: + return bytes(c_signature[: signature_len.value]) + else: + raise RuntimeError("Can not sign message") def verify(self, message, signature, public_key): """ @@ -470,17 +486,85 @@ def verify(self, message, signature, public_key): :param public_key: the signer's public key. """ # Provide length to avoid extra null char - my_message = ct.create_string_buffer(message, len(message)) - message_len = ct.c_int(len(my_message)) - - # Provide length to avoid extra null char in sig - my_signature = ct.create_string_buffer(signature, len(signature)) - sig_len = ct.c_int(len(my_signature)) - my_public_key = ct.create_string_buffer( + c_message = ct.create_string_buffer(message, len(message)) + c_message_len = ct.c_size_t(len(c_message)) + c_signature = ct.create_string_buffer(signature, len(signature)) + signature_len = ct.c_size_t(len(c_signature)) + c_public_key = ct.create_string_buffer( public_key, self._sig.contents.length_public_key ) + rv = native().OQS_SIG_verify( - self._sig, my_message, message_len, my_signature, sig_len, my_public_key + self._sig, + c_message, + c_message_len, + c_signature, + signature_len, + c_public_key, + ) + return True if rv == OQS_SUCCESS else False + + def sign_with_ctx_str(self, message, context): + """ + Signs the provided message with context string and returns the signature. + + :param context: the context string. + :param message: the message to sign. + """ + # Provide length to avoid extra null char + c_message = ct.create_string_buffer(message, len(message)) + c_message_len = ct.c_size_t(len(c_message)) + c_context = ct.create_string_buffer(context, len(context)) + context_len = ct.c_size_t(len(c_context)) + c_signature = ct.create_string_buffer(self._sig.contents.length_signature) + + # Initialize to maximum signature size + c_signature_len = ct.c_size_t(self._sig.contents.length_signature) + + rv = native().OQS_SIG_sign_with_ctx_str( + self._sig, + ct.byref(c_signature), + ct.byref(c_signature_len), + c_message, + c_message_len, + c_context, + context_len, + self.secret_key, + ) + if rv == OQS_SUCCESS: + return bytes(c_signature[: c_signature_len.value]) + else: + raise RuntimeError("Can not sign message with context string") + + def verify_with_ctx_str(self, message, signature, context, public_key): + """ + Verifies the provided signature on the message with context string; returns True if valid. + + :param message: the signed message. + :param signature: the signature on the message. + :param context: the context string. + :param public_key: the signer's public key. + """ + # Provide length to avoid extra null char + c_message = ct.create_string_buffer(message, len(message)) + c_message_len = ct.c_size_t(len(c_message)) + c_signature = ct.create_string_buffer(signature, len(signature)) + c_signature_len = ct.c_size_t(len(c_signature)) + c_context = ct.create_string_buffer(context, len(context)) + c_context_len = ct.c_size_t(len(c_context)) + c_public_key = ct.create_string_buffer( + public_key, self._sig.contents.length_public_key + ) + + rv = native().OQS_SIG_verify_with_ctx_str( + self._sig, + c_message, + c_message_len, + c_signature, + c_signature_len, + c_context, + c_context_len, + c_public_key, ) return True if rv == OQS_SUCCESS else False diff --git a/oqs/rand.py b/oqs/rand.py index 5e7fd77..b96c26d 100644 --- a/oqs/rand.py +++ b/oqs/rand.py @@ -19,7 +19,7 @@ def randombytes(bytes_to_read): :return: random bytes. """ result = oqs.ct.create_string_buffer(bytes_to_read) - oqs.native().OQS_randombytes(result, oqs.ct.c_int(bytes_to_read)) + oqs.native().OQS_randombytes(result, oqs.ct.c_size_t(bytes_to_read)) return bytes(result) diff --git a/pyproject.toml b/pyproject.toml index fd558c0..5ec9ae0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ build-backend = "setuptools.build_meta" [project] name = "liboqs-python" requires-python = ">=3.8" -version = "0.10.0" +version = "0.12.0" description = "Python bindings for liboqs, providing post-quantum public key cryptography algorithms" authors = [ { name = "Open Quantum Safe project", email = "contact@openquantumsafe.org" }, diff --git a/tests/test_kem.py b/tests/test_kem.py index cd13640..7733666 100644 --- a/tests/test_kem.py +++ b/tests/test_kem.py @@ -36,18 +36,23 @@ def check_wrong_ciphertext(alg_name): public_key = kem.generate_keypair() ciphertext, shared_secret_server = kem.encap_secret(public_key) wrong_ciphertext = bytes(random.getrandbits(8) for _ in range(len(ciphertext))) - shared_secret_client = kem.decap_secret(wrong_ciphertext) - assert shared_secret_client != shared_secret_server + try: + shared_secret_client = kem.decap_secret(wrong_ciphertext) + assert shared_secret_client != shared_secret_server + except RuntimeError: + pass + except Exception as ex: + raise AssertionError(f"An unexpected exception was raised: {ex}") def test_not_supported(): try: - with oqs.KeyEncapsulation("bogus") as kem: + with oqs.KeyEncapsulation("bogus"): raise AssertionError("oqs.MechanismNotSupportedError was not raised.") except oqs.MechanismNotSupportedError: pass except Exception as ex: - raise AssertionError("An unexpected exception was raised. " + ex) + raise AssertionError(f"An unexpected exception was raised {ex}") def test_not_enabled(): @@ -56,12 +61,12 @@ def test_not_enabled(): if alg_name not in oqs.get_enabled_kem_mechanisms(): # Found a non-enabled but supported alg try: - with oqs.KeyEncapsulation(alg_name) as kem: + with oqs.KeyEncapsulation(alg_name): raise AssertionError("oqs.MechanismNotEnabledError was not raised.") except oqs.MechanismNotEnabledError: pass except Exception as ex: - raise AssertionError("An unexpected exception was raised. " + ex) + raise AssertionError(f"An unexpected exception was raised: {ex}") if __name__ == "__main__": @@ -69,8 +74,7 @@ def test_not_enabled(): import nose2 nose2.main() - except ImportError: - import nose - - nose.runmodule() + raise RuntimeError( + "nose2 module not found. Please install it with 'pip install node2'." + ) diff --git a/tests/test_sig.py b/tests/test_sig.py index 5053df4..f84ef4b 100644 --- a/tests/test_sig.py +++ b/tests/test_sig.py @@ -6,7 +6,7 @@ disabled_sig_patterns = [] if platform.system() == "Windows": - disabled_sig_patterns = ["Rainbow-V"] + disabled_sig_patterns = [""] def test_correctness(): @@ -16,6 +16,15 @@ def test_correctness(): yield check_correctness, alg_name +def test_correctness_with_ctx_str(): + for alg_name in oqs.get_enabled_sig_mechanisms(): + if not alg_name.startswith("ML-DSA"): + continue + if any(item in alg_name for item in disabled_sig_patterns): + continue + yield check_correctness_with_ctx_str, alg_name + + def check_correctness(alg_name): with oqs.Signature(alg_name) as sig: message = bytes(random.getrandbits(8) for _ in range(100)) @@ -24,6 +33,15 @@ def check_correctness(alg_name): assert sig.verify(message, signature, public_key) +def check_correctness_with_ctx_str(alg_name): + with oqs.Signature(alg_name) as sig: + message = bytes(random.getrandbits(8) for _ in range(100)) + context = "some context".encode() + public_key = sig.generate_keypair() + signature = sig.sign_with_ctx_str(message, context) + assert sig.verify_with_ctx_str(message, signature, context, public_key) + + def test_wrong_message(): for alg_name in oqs.get_enabled_sig_mechanisms(): if any(item in alg_name for item in disabled_sig_patterns): @@ -74,12 +92,12 @@ def check_wrong_public_key(alg_name): def test_not_supported(): try: - with oqs.Signature("bogus") as sig: + with oqs.Signature("bogus"): raise AssertionError("oqs.MechanismNotSupportedError was not raised.") except oqs.MechanismNotSupportedError: pass except Exception as ex: - raise AssertionError("An unexpected exception was raised. " + ex) + raise AssertionError(f"An unexpected exception was raised: {ex}") def test_not_enabled(): @@ -88,12 +106,12 @@ def test_not_enabled(): if alg_name not in oqs.get_enabled_sig_mechanisms(): # Found a non-enabled but supported alg try: - with oqs.Signature(alg_name) as sig: + with oqs.Signature(alg_name): raise AssertionError("oqs.MechanismNotEnabledError was not raised.") except oqs.MechanismNotEnabledError: pass except Exception as ex: - raise AssertionError("An unexpected exception was raised. " + ex) + raise AssertionError(f"An unexpected exception was raised: {ex}") if __name__ == "__main__": @@ -101,8 +119,7 @@ def test_not_enabled(): import nose2 nose2.main() - except ImportError: - import nose - - nose.runmodule() + raise RuntimeError( + "nose2 module not found. Please install it with 'pip install node2'." + )