From 1e20bc9b8002324683fe27d91c9feb7a7d35310e Mon Sep 17 00:00:00 2001 From: Martijn de Milliano Date: Thu, 4 Jun 2026 16:59:24 +0200 Subject: [PATCH 1/3] Prepare for v5.9.1 release --- ChangeLog.rst | 24 +++++++++++++++++++++++- lib/wolfssl | 2 +- wolfcrypt/_version.py | 4 ++-- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/ChangeLog.rst b/ChangeLog.rst index 7046f3a..3e85793 100644 --- a/ChangeLog.rst +++ b/ChangeLog.rst @@ -1,7 +1,29 @@ -wolfCrypt-py Release next (TBD, 2026) +wolfCrypt-py Release 5.9.1 (Jun 4, 2026) +========================================== +* Make t2b support other types +* Fix ChaCha20Poly1305 to be singleshot +* Fix padding of small ECC signatures +* ML-DSA: Add support for generating private key deterministically from seed +* ML-DSA: Add support for deterministic signing +* ML-DSA: Add support for signing and verifying with context * Drop support for end-of-life Python versions (<= 3.9) * Add extra nonce parameter to Random generator +* Add type annotations to utils and random +* Add pyproject.toml for modern Python packaging +* Fix AES-SIV silently mangling associated data +* Fix ChaCha decrypt stream state wiping on first call +* Fix Random AttributeError on builds without ML-KEM/ML-DSA +* Fix Ed448 ctx cdef to match wolfSSL header signature +* Fix wc_ecc_import_unsigned cdef to match wolfSSL header +* Fix wc_RsaPSS_Verify const qualifier in cdef +* Fix wc_GetPkcs8TraditionalOffset non-const bytes handling +* Validate raw element lengths in EccPublic/EccPrivate.decode_key_raw +* Improve WolfCryptError exception to include error return value +* Code modernization: f-strings, remove unicode prefix, remove redundant code +* Fix issue in AES-GCM tag verification +* Address many small issues found by Fenrir +* Update to wolfSSL version 5.9.1 wolfCrypt-py Release 5.8.4 (Jan 7, 2026) diff --git a/lib/wolfssl b/lib/wolfssl index 59f4fa5..1d363f3 160000 --- a/lib/wolfssl +++ b/lib/wolfssl @@ -1 +1 @@ -Subproject commit 59f4fa568615396fbf381b073b220d1e8d61e4c2 +Subproject commit 1d363f3adceba9d1478230ede476a37b0dcdef24 diff --git a/wolfcrypt/_version.py b/wolfcrypt/_version.py index 7a7fe33..5a03bf3 100644 --- a/wolfcrypt/_version.py +++ b/wolfcrypt/_version.py @@ -1,6 +1,6 @@ # When bumping the C library version, reset the POST count to 0 -__wolfssl_version__ = "v5.8.4-stable" +__wolfssl_version__ = "v5.9.1-stable" # We're using implicit post releases [PEP 440] to bump package version # while maintaining the C library version intact for better reference. @@ -8,5 +8,5 @@ # # MAJOR.MINOR.BUILD-POST -__version__ = "5.8.4-0" +__version__ = "5.9.1-0" From 52886c4034a8b705392f8890f7e17154ce0b21de Mon Sep 17 00:00:00 2001 From: Martijn de Milliano Date: Thu, 4 Jun 2026 18:03:35 +0200 Subject: [PATCH 2/3] ML-DSA: Deal with signing without context not always supported In the newer wolfSSL signing and verifying without context is not available unless it is explicitly enabled. This change modifies the Python binding and test suite to accommodate this. --- scripts/build_ffi.py | 12 +++++++++--- tests/test_mldsa.py | 38 +++++++++++++++++++++----------------- wolfcrypt/ciphers.py | 10 ++++++++-- 3 files changed, 38 insertions(+), 22 deletions(-) diff --git a/scripts/build_ffi.py b/scripts/build_ffi.py index ec170d5..1586e45 100644 --- a/scripts/build_ffi.py +++ b/scripts/build_ffi.py @@ -376,6 +376,7 @@ def get_features(local_wolfssl, features): features["RSA_PSS"] = 1 if '#define WC_RSA_PSS' in defines else 0 features["CHACHA20_POLY1305"] = 1 if ('#define HAVE_CHACHA' in defines and '#define HAVE_POLY1305' in defines) else 0 features["ML_DSA"] = 1 if '#define HAVE_DILITHIUM' in defines else 0 + features["ML_DSA_NO_CTX"] = 1 if '#define WOLFSSL_DILITHIUM_NO_CTX' in defines else 0 features["ML_KEM"] = 1 if '#define WOLFSSL_HAVE_MLKEM' in defines else 0 features["HKDF"] = 1 if "#define HAVE_HKDF" in defines else 0 @@ -496,6 +497,7 @@ def build_ffi(local_wolfssl, features): int CHACHA20_POLY1305_ENABLED = {features["CHACHA20_POLY1305"]}; int ML_KEM_ENABLED = {features["ML_KEM"]}; int ML_DSA_ENABLED = {features["ML_DSA"]}; + int ML_DSA_NO_CTX = {features["ML_DSA_NO_CTX"]}; int HKDF_ENABLED = {features["HKDF"]}; """ @@ -536,6 +538,7 @@ def build_ffi(local_wolfssl, features): extern int CHACHA20_POLY1305_ENABLED; extern int ML_KEM_ENABLED; extern int ML_DSA_ENABLED; + extern int ML_DSA_NO_CTX; extern int HKDF_ENABLED; typedef unsigned char byte; @@ -1323,17 +1326,20 @@ def build_ffi(local_wolfssl, features): int wc_dilithium_import_private(const byte* priv, word32 privSz, dilithium_key* key); int wc_dilithium_export_public(dilithium_key* key, byte* out, word32* outLen); int wc_dilithium_import_public(const byte* in, word32 inLen, dilithium_key* key); - int wc_dilithium_sign_msg(const byte* msg, word32 msgLen, byte* sig, word32* sigLen, dilithium_key* key, WC_RNG* rng); int wc_dilithium_sign_ctx_msg(const byte* ctx, byte ctxLen, const byte* msg, word32 msgLen, byte* sig, word32* sigLen, dilithium_key* key, WC_RNG* rng); - int wc_dilithium_sign_msg_with_seed(const byte* msg, word32 msgLen, byte* sig, word32* sigLen, dilithium_key* key, const byte* seed); int wc_dilithium_sign_ctx_msg_with_seed(const byte* ctx, byte ctxLen, const byte* msg, word32 msgLen, byte* sig, word32* sigLen, dilithium_key* key, const byte* seed); - int wc_dilithium_verify_msg(const byte* sig, word32 sigLen, const byte* msg, word32 msgLen, int* res, dilithium_key* key); int wc_dilithium_verify_ctx_msg(const byte* sig, word32 sigLen, const byte* ctx, word32 ctxLen, const byte* msg, word32 msgLen, int* res, dilithium_key* key); typedef dilithium_key MlDsaKey; int wc_MlDsaKey_GetPrivLen(MlDsaKey* key, int* len); int wc_MlDsaKey_GetPubLen(MlDsaKey* key, int* len); int wc_MlDsaKey_GetSigLen(MlDsaKey* key, int* len); """ + if features["ML_DSA_NO_CTX"]: + cdef += """ + int wc_dilithium_sign_msg(const byte* msg, word32 msgLen, byte* sig, word32* sigLen, dilithium_key* key, WC_RNG* rng); + int wc_dilithium_sign_msg_with_seed(const byte* msg, word32 msgLen, byte* sig, word32* sigLen, dilithium_key* key, const byte* seed); + int wc_dilithium_verify_msg(const byte* sig, word32 sigLen, const byte* msg, word32 msgLen, int* res, dilithium_key* key); + """ ffibuilder.cdef(cdef) diff --git a/tests/test_mldsa.py b/tests/test_mldsa.py index 023b734..9bb31c7 100644 --- a/tests/test_mldsa.py +++ b/tests/test_mldsa.py @@ -121,24 +121,26 @@ def test_sign_verify(mldsa_type, rng): # Sign a message message = b"This is a test message for ML-DSA signature" - signature = mldsa_priv.sign(message, rng) - assert len(signature) == mldsa_priv.sig_size + ctx = b"This is a test context for ML-DSA signature" + wrong_ctx = b"This is a wrong context for ML-DSA signature" - # Verify the signature by MlDsaPrivate - assert mldsa_priv.verify(signature, message) + if _lib.ML_DSA_NO_CTX: + signature = mldsa_priv.sign(message, rng) + assert len(signature) == mldsa_priv.sig_size - # Verify the signature by MlDsaPublic - assert mldsa_pub.verify(signature, message) + # Verify the signature by MlDsaPrivate + assert mldsa_priv.verify(signature, message) - # Verify with wrong message - wrong_message = b"This is a wrong message for ML-DSA signature" - assert not mldsa_pub.verify(signature, wrong_message) - - # Verify a signature generated without a context but where a context - # is provided during verify - ctx = b"This is a test context for ML-DSA signature" - wrong_ctx = b"This is a wrong context for ML-DSA signature" - assert not mldsa_pub.verify(signature, message, ctx=wrong_ctx) + # Verify the signature by MlDsaPublic + assert mldsa_pub.verify(signature, message) + + # Verify with wrong message + wrong_message = b"This is a wrong message for ML-DSA signature" + assert not mldsa_pub.verify(signature, wrong_message) + + # Verify a signature generated without a context but where a context + # is provided during verify + assert not mldsa_pub.verify(signature, message, ctx=wrong_ctx) # Sign a message with context signature = mldsa_priv.sign(message, rng, ctx=ctx) @@ -150,12 +152,14 @@ def test_sign_verify(mldsa_type, rng): # Verify the signature by MlDsaPublic assert mldsa_pub.verify(signature, message, ctx=ctx) - # Verify but do not provide a context - assert not mldsa_pub.verify(signature, message, ctx=None) + if _lib.ML_DSA_NO_CTX: + # Verify but do not provide a context + assert not mldsa_pub.verify(signature, message, ctx=None) # Verify with wrong context assert not mldsa_pub.verify(signature, message, ctx=wrong_ctx) + @pytest.mark.skipif(not _lib.ML_DSA_NO_CTX, reason="Requires support for signing without context") def test_sign_with_seed(mldsa_type, rng): signature_seed = rng.bytes(ML_DSA_SIGNATURE_SEED_LENGTH) mldsa_priv = MlDsaPrivate.make_key(mldsa_type, rng) diff --git a/wolfcrypt/ciphers.py b/wolfcrypt/ciphers.py index 562aabd..337aa5d 100644 --- a/wolfcrypt/ciphers.py +++ b/wolfcrypt/ciphers.py @@ -2264,6 +2264,8 @@ def verify(self, signature, message, ctx=None): ) if ret < 0: # pragma: no cover raise WolfCryptApiError("wc_dilithium_verify_ctx_msg() error", ret) + elif not _lib.ML_DSA_NO_CTX: + raise WolfCryptError("support verifying without context is disabled") else: ret = _lib.wc_dilithium_verify_msg( _ffi.from_buffer(sig_bytestype), @@ -2279,7 +2281,7 @@ def verify(self, signature, message, ctx=None): return res[0] == 1 class MlDsaPrivate(_MlDsaBase): - + @classmethod def make_key(cls, mldsa_type, rng=None): """ @@ -2440,6 +2442,8 @@ def sign(self, message, rng=None, ctx=None): ) if ret < 0: # pragma: no cover raise WolfCryptApiError("wc_dilithium_sign_ctx_msg() error", ret) + elif not _lib.ML_DSA_NO_CTX: + raise WolfCryptError("support for signing without context is disabled") else: ret = _lib.wc_dilithium_sign_msg( _ffi.from_buffer(msg_bytestype), @@ -2451,7 +2455,7 @@ def sign(self, message, rng=None, ctx=None): ) if ret < 0: # pragma: no cover raise WolfCryptApiError("wc_dilithium_sign_msg() error", ret) - + if in_size != out_size[0]: raise WolfCryptError(f"{in_size=} and {out_size[0]=} don't match") @@ -2504,6 +2508,8 @@ def sign_with_seed(self, message, seed, ctx=None): ) if ret < 0: # pragma: no cover raise WolfCryptApiError("wc_dilithium_sign_ctx_msg_with_seed() error", ret) + elif not _lib.ML_DSA_NO_CTX: + raise WolfCryptError("support for signing without context is disabled") else: ret = _lib.wc_dilithium_sign_msg_with_seed( _ffi.from_buffer(msg_bytestype), From eb93f657bf85a7be3ef2564184d6f129f289753e Mon Sep 17 00:00:00 2001 From: Martijn de Milliano Date: Thu, 4 Jun 2026 18:05:58 +0200 Subject: [PATCH 3/3] Test AESGCM stream: ignore failures for small auth tags Smaller authentication tags may not be supported by the library. This fix makes the test work for the default case that tags should be minimum 12 bytes in size. --- tests/test_aesgcmstream.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/test_aesgcmstream.py b/tests/test_aesgcmstream.py index c22858b..24e89cd 100644 --- a/tests/test_aesgcmstream.py +++ b/tests/test_aesgcmstream.py @@ -25,7 +25,7 @@ if _lib.AESGCM_STREAM_ENABLED: import pytest from wolfcrypt.utils import t2b - from wolfcrypt.exceptions import WolfCryptError + from wolfcrypt.exceptions import WolfCryptError, WolfCryptApiError from binascii import hexlify as b2h from wolfcrypt.ciphers import AesGcmStream @@ -141,7 +141,14 @@ def test_invalid_tag_bytes(): for good in (4, 8, 12, 13, 14, 15, 16): gcm = AesGcmStream(key, iv, tag_bytes=good) gcm.encrypt("hello world") - tag = gcm.final() + try: + tag = gcm.final() + except WolfCryptApiError as exception: + if "(-173)" in str(exception) and good < 12: + # Probably this authentication tag size is not supported, + # the minimum is 12 by default. Ignore. + continue + raise assert len(tag) == good def test_decrypt_rejects_wrong_tag_length():