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/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_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(): 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/_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" 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),