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
24 changes: 23 additions & 1 deletion ChangeLog.rst
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
2 changes: 1 addition & 1 deletion lib/wolfssl
Submodule wolfssl updated 1639 files
12 changes: 9 additions & 3 deletions scripts/build_ffi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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"]};
"""

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

Expand Down
11 changes: 9 additions & 2 deletions tests/test_aesgcmstream.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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():
Expand Down
38 changes: 21 additions & 17 deletions tests/test_mldsa.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions wolfcrypt/_version.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# 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.
# https://www.python.org/dev/peps/pep-0440/#implicit-post-releases
#
# MAJOR.MINOR.BUILD-POST

__version__ = "5.8.4-0"
__version__ = "5.9.1-0"

10 changes: 8 additions & 2 deletions wolfcrypt/ciphers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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):
"""
Expand Down Expand Up @@ -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),
Expand All @@ -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")

Expand Down Expand Up @@ -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),
Expand Down
Loading