From 01fcd2af6c24e77574ab9f83b8ba368bf653930b Mon Sep 17 00:00:00 2001 From: Robert de Vries Date: Sun, 5 Apr 2026 23:10:29 +0200 Subject: [PATCH 01/10] Add support for nonce in random number generation. --- scripts/build_ffi.py | 2 ++ tests/test_random.py | 11 +++++++++++ wolfcrypt/random.py | 8 ++++++-- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/scripts/build_ffi.py b/scripts/build_ffi.py index 4d5ad22..68df95d 100644 --- a/scripts/build_ffi.py +++ b/scripts/build_ffi.py @@ -543,6 +543,8 @@ def build_ffi(local_wolfssl, features): typedef struct { ...; } OS_Seed; int wc_InitRng(WC_RNG*); + int wc_InitRngNonce(WC_RNG*, byte*, word32); + int wc_InitRngNonce_ex(WC_RNG*, byte*, word32, void*, int); int wc_RNG_GenerateBlock(WC_RNG*, byte*, word32); int wc_RNG_GenerateByte(WC_RNG*, byte*); int wc_FreeRng(WC_RNG*); diff --git a/tests/test_random.py b/tests/test_random.py index c95847a..bf59f4e 100644 --- a/tests/test_random.py +++ b/tests/test_random.py @@ -37,3 +37,14 @@ def test_bytes(rng): assert len(rng.bytes(1)) == 1 assert len(rng.bytes(8)) == 8 assert len(rng.bytes(128)) == 128 + +@pytest.fixture +def rng_nonce(): + return Random(b"abcdefghijklmnopqrstuv") + +def test_nonce_byte(rng_nonce): + assert len(rng_nonce.byte()) == 1 + +@pytest.mark.parametrize("length", (1, 8, 128)) +def test_nonce_bytes(rng_nonce, length): + assert len(rng_nonce.bytes(length)) == length diff --git a/wolfcrypt/random.py b/wolfcrypt/random.py index c576807..45377bf 100644 --- a/wolfcrypt/random.py +++ b/wolfcrypt/random.py @@ -31,10 +31,14 @@ class Random(object): A Cryptographically Secure Pseudo Random Number Generator - CSPRNG """ - def __init__(self): + def __init__(self, nonce=_ffi.NULL): self.native_object = _ffi.new("WC_RNG *") - ret = _lib.wc_InitRng(self.native_object) + if nonce == _ffi.NULL: + nonce_size = 0 + else: + nonce_size = len(nonce) + ret = _lib.wc_InitRngNonce(self.native_object, nonce, nonce_size) if ret < 0: # pragma: no cover self.native_object = None raise WolfCryptError("RNG init error (%d)" % ret) From 4564459a9e97359aeca2a7efd60ea717cded1e86 Mon Sep 17 00:00:00 2001 From: Robert de Vries Date: Mon, 6 Apr 2026 20:20:21 +0200 Subject: [PATCH 02/10] Prepare python interface for use of device_id callbacks. --- wolfcrypt/random.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wolfcrypt/random.py b/wolfcrypt/random.py index 45377bf..9c9f6b6 100644 --- a/wolfcrypt/random.py +++ b/wolfcrypt/random.py @@ -31,14 +31,14 @@ class Random(object): A Cryptographically Secure Pseudo Random Number Generator - CSPRNG """ - def __init__(self, nonce=_ffi.NULL): + def __init__(self, nonce=_ffi.NULL, device_id=_lib.INVALID_DEVID): self.native_object = _ffi.new("WC_RNG *") if nonce == _ffi.NULL: nonce_size = 0 else: nonce_size = len(nonce) - ret = _lib.wc_InitRngNonce(self.native_object, nonce, nonce_size) + ret = _lib.wc_InitRngNonce_ex(self.native_object, nonce, nonce_size, _ffi.NULL, device_id) if ret < 0: # pragma: no cover self.native_object = None raise WolfCryptError("RNG init error (%d)" % ret) From 1f39cdf700bab7f594e9468d1f054f7581443170 Mon Sep 17 00:00:00 2001 From: Robert de Vries Date: Wed, 8 Apr 2026 15:56:06 +0200 Subject: [PATCH 03/10] Remove unneeded semicolons. Static checker "ruff" complains about these. --- wolfcrypt/ciphers.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/wolfcrypt/ciphers.py b/wolfcrypt/ciphers.py index 105224e..e0d6d47 100644 --- a/wolfcrypt/ciphers.py +++ b/wolfcrypt/ciphers.py @@ -1096,7 +1096,7 @@ if _lib.ECC_ENABLED: qy_size[0] = self.size ret = _lib.wc_ecc_export_public_raw(self.native_object, Qx, - qx_size, Qy, qy_size); + qx_size, Qy, qy_size) if ret != 0: # pragma: no cover raise WolfCryptError("Key encode error (%d)" % ret) @@ -1265,7 +1265,7 @@ if _lib.ECC_ENABLED: d_size[0] = self.size ret = _lib.wc_ecc_export_private_raw(self.native_object, Qx, - qx_size, Qy, qy_size, d, d_size); + qx_size, Qy, qy_size, d, d_size) if ret != 0: # pragma: no cover raise WolfCryptError("Key encode error (%d)" % ret) @@ -1322,8 +1322,8 @@ if _lib.ECC_ENABLED: Returns the signature in its two raw components r, s """ plaintext = t2b(plaintext) - R = _ffi.new("mp_int[1]"); - S = _ffi.new("mp_int[1]"); + R = _ffi.new("mp_int[1]") + S = _ffi.new("mp_int[1]") R_bin = _ffi.new("unsigned char[%d]" % self.size ) S_bin = _ffi.new("unsigned char[%d]" % self.size ) @@ -1478,12 +1478,12 @@ if _lib.ED25519_ENABLED: idx[0] = 0 if pub: ret = _lib.wc_ed25519_import_private_key(key, len(key), pub, - len(pub), self.native_object); + len(pub), self.native_object) if ret < 0: raise WolfCryptError("Key decode error (%d)" % ret) else: ret = _lib.wc_ed25519_import_private_only(key, len(key), - self.native_object); + self.native_object) if ret < 0: raise WolfCryptError("Key decode error (%d)" % ret) pubkey = _ffi.new("byte[%d]" % (self.size * 4)) @@ -1492,7 +1492,7 @@ if _lib.ED25519_ENABLED: if ret < 0: raise WolfCryptError("Public key generate error (%d)" % ret) ret = _lib.wc_ed25519_import_public(pubkey, self.size, - self.native_object); + self.native_object) if self.size <= 0: # pragma: no cover raise WolfCryptError("Key decode error (%d)" % self.size) @@ -1674,12 +1674,12 @@ if _lib.ED448_ENABLED: idx[0] = 0 if pub: ret = _lib.wc_ed448_import_private_key(key, len(key), pub, - len(pub), self.native_object); + len(pub), self.native_object) if ret < 0: raise WolfCryptError("Key decode error (%d)" % ret) else: ret = _lib.wc_ed448_import_private_only(key, len(key), - self.native_object); + self.native_object) if ret < 0: raise WolfCryptError("Key decode error (%d)" % ret) pubkey = _ffi.new("byte[%d]" % (self.size * 4)) @@ -1688,7 +1688,7 @@ if _lib.ED448_ENABLED: if ret < 0: raise WolfCryptError("Public key generate error (%d)" % ret) ret = _lib.wc_ed448_import_public(pubkey, self.size, - self.native_object); + self.native_object) if self.size <= 0: # pragma: no cover raise WolfCryptError("Key decode error (%d)" % self.size) From dc92410de9e54cd37e2a05cafea277f65e0a50d0 Mon Sep 17 00:00:00 2001 From: Robert de Vries Date: Wed, 8 Apr 2026 16:10:07 +0200 Subject: [PATCH 04/10] Fix various boolean expressions involving None among others. Includes also some other minor fixes in boolean expressions involving "in". --- wolfcrypt/ciphers.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/wolfcrypt/ciphers.py b/wolfcrypt/ciphers.py index 105224e..0d0c5a3 100644 --- a/wolfcrypt/ciphers.py +++ b/wolfcrypt/ciphers.py @@ -213,10 +213,9 @@ class _Cipher(object): string = t2b(string) if not string: - raise ValueError( - "empty string not allowed") + raise ValueError("empty string not allowed") - if len(string) % self.block_size and not self.mode == MODE_CTR and not "ChaCha" in self._native_type: + if len(string) % self.block_size and self.mode != MODE_CTR and "ChaCha" not in self._native_type: raise ValueError( "string must be a multiple of %d in length" % self.block_size) @@ -498,7 +497,7 @@ if _lib.CHACHA_ENABLED: self._dec = None self._key = None if len(key) > 0: - if not size in self._key_sizes: + if size not in self._key_sizes: raise ValueError("Invalid key size %d" % size) self._key = t2b(key) self.key_size = size @@ -506,7 +505,7 @@ if _lib.CHACHA_ENABLED: self._IV_counter = 0 def _set_key(self, direction): - if self._key == None: + if self._key is None: return -1 if self._enc: ret = _lib.wc_Chacha_SetKey(self._enc, self._key, len(self._key)) @@ -692,7 +691,7 @@ if _lib.RSA_ENABLED: class RsaPublic(_Rsa): def __init__(self, key=None, hash_type=None): - if key != None: + if key is not None: key = t2b(key) self._hash_type = hash_type @@ -830,7 +829,7 @@ if _lib.RSA_ENABLED: Generates a new key pair of desired length **size**. """ rsa = cls(hash_type=hash_type) - if rsa == None: # pragma: no cover + if rsa is None: # pragma: no cover raise WolfCryptError("Invalid key error (%d)" % ret) ret = _lib.wc_MakeRsaKey(rsa.native_object, size, 65537, @@ -852,7 +851,7 @@ if _lib.RSA_ENABLED: idx = _ffi.new("word32*") idx[0] = 0 - if key != None: + if key is not None: key = t2b(key) ret = _lib.wc_RsaPrivateKeyDecode(key, idx, self.native_object, len(key)) @@ -1622,7 +1621,7 @@ if _lib.ED448_ENABLED: status = _ffi.new("int[1]") ctx_buf = _ffi.NULL ctx_buf_len = 0 - if ctx != None: + if ctx is not None: ctx_buf = t2b(ctx) ctx_buf_len = len(ctx_buf) @@ -1732,7 +1731,7 @@ if _lib.ED448_ENABLED: signature_size[0] = self.max_signature_size ctx_buf = _ffi.NULL ctx_buf_len = 0 - if (ctx != None): + if ctx is not None: ctx_buf = t2b(ctx) ctx_buf_len = len(ctx_buf) From df6fa02e6128123cceb7714a586ec2d8f3366f17 Mon Sep 17 00:00:00 2001 From: Robert de Vries Date: Wed, 8 Apr 2026 16:15:46 +0200 Subject: [PATCH 05/10] Add missing import of WolfCryptError. --- wolfcrypt/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wolfcrypt/__init__.py b/wolfcrypt/__init__.py index 6879589..d7e8721 100644 --- a/wolfcrypt/__init__.py +++ b/wolfcrypt/__init__.py @@ -46,6 +46,7 @@ top_level_py = os.path.basename(sys.argv[0]) if top_level_py not in ["setup.py", "build_ffi.py"]: from wolfcrypt._ffi import ffi as _ffi from wolfcrypt._ffi import lib as _lib + from wolfcrypt.exceptions import WolfCryptError if hasattr(_lib, 'WC_RNG_SEED_CB_ENABLED'): if _lib.WC_RNG_SEED_CB_ENABLED: @@ -53,8 +54,7 @@ if top_level_py not in ["setup.py", "build_ffi.py"]: if ret < 0: raise WolfCryptError("wc_SetSeed_Cb failed (%d)" % ret) if _lib.FIPS_ENABLED and _lib.FIPS_VERSION >= 5: - ret = _lib.wolfCrypt_SetPrivateKeyReadEnable_fips(1, - _lib.WC_KEYTYPE_ALL); + ret = _lib.wolfCrypt_SetPrivateKeyReadEnable_fips(1, _lib.WC_KEYTYPE_ALL) if ret < 0: raise WolfCryptError("wolfCrypt_SetPrivateKeyReadEnable_fips failed" " (%d)" % ret) From 2f630a43534c68e33d94d2a726add9eab8e026fa Mon Sep 17 00:00:00 2001 From: Robert de Vries Date: Thu, 9 Apr 2026 20:02:01 +0200 Subject: [PATCH 06/10] Clean up import statements. * Remove unused imports. * Move import statements together at the top of the file. --- scripts/build_ffi.py | 1 - setup.py | 2 +- tests/test_aesgcmstream.py | 3 +-- tests/test_chacha20poly1305.py | 2 +- tests/test_ciphers.py | 14 +++++--------- tests/test_hashes.py | 1 - 6 files changed, 8 insertions(+), 15 deletions(-) diff --git a/scripts/build_ffi.py b/scripts/build_ffi.py index 68df95d..64f138a 100644 --- a/scripts/build_ffi.py +++ b/scripts/build_ffi.py @@ -26,7 +26,6 @@ from contextlib import contextmanager from distutils.util import get_platform from cffi import FFI import shutil -import glob from wolfcrypt._version import __wolfssl_version__ as version def local_path(path): diff --git a/setup.py b/setup.py index 7c16e74..ed2836e 100755 --- a/setup.py +++ b/setup.py @@ -22,12 +22,12 @@ # pylint: disable=wrong-import-position import os +import re import sys from setuptools import setup, find_packages os.chdir(os.path.dirname(sys.argv[0]) or ".") -import re VERSIONFILE = "wolfcrypt/_version.py" verstrline = open(VERSIONFILE, "rt").read() VSRE = r"^__version__ = ['\"]([^'\"]*)['\"]" diff --git a/tests/test_aesgcmstream.py b/tests/test_aesgcmstream.py index 12c8c04..61ad285 100644 --- a/tests/test_aesgcmstream.py +++ b/tests/test_aesgcmstream.py @@ -23,11 +23,10 @@ from wolfcrypt._ffi import lib as _lib if _lib.AESGCM_STREAM_ENABLED: - from collections import namedtuple import pytest from wolfcrypt.utils import t2b from wolfcrypt.exceptions import WolfCryptError - from binascii import hexlify as b2h, unhexlify as h2b + from binascii import hexlify as b2h from wolfcrypt.ciphers import AesGcmStream def test_encrypt(): diff --git a/tests/test_chacha20poly1305.py b/tests/test_chacha20poly1305.py index c67840e..07616ea 100644 --- a/tests/test_chacha20poly1305.py +++ b/tests/test_chacha20poly1305.py @@ -27,7 +27,7 @@ if _lib.CHACHA20_POLY1305_ENABLED: import pytest from wolfcrypt.utils import t2b from wolfcrypt.exceptions import WolfCryptError - from binascii import hexlify as b2h, unhexlify as h2b + from binascii import unhexlify as h2b from wolfcrypt.ciphers import ChaCha20Poly1305 def test_encrypt_decrypt(): diff --git a/tests/test_ciphers.py b/tests/test_ciphers.py index 79092e2..2cbd483 100644 --- a/tests/test_ciphers.py +++ b/tests/test_ciphers.py @@ -23,8 +23,8 @@ from collections import namedtuple import random import pytest -from wolfcrypt._ffi import ffi as _ffi from wolfcrypt._ffi import lib as _lib +from wolfcrypt.ciphers import MODE_CTR, MODE_ECB, MODE_CBC, WolfCryptError from wolfcrypt.utils import t2b, h2b import os @@ -43,20 +43,16 @@ if _lib.CHACHA_ENABLED: from wolfcrypt.ciphers import ChaCha if _lib.RSA_ENABLED: - from wolfcrypt.ciphers import (RsaPrivate, RsaPublic, HASH_TYPE_SHA256, MGF1SHA256, HASH_TYPE_SHA, MGF1SHA1) + from wolfcrypt.ciphers import RsaPrivate, RsaPublic, HASH_TYPE_SHA256, HASH_TYPE_SHA if _lib.ECC_ENABLED: - from wolfcrypt.ciphers import (EccPrivate, EccPublic) + from wolfcrypt.ciphers import EccPrivate, EccPublic if _lib.ED25519_ENABLED: - from wolfcrypt.ciphers import (Ed25519Private, Ed25519Public) + from wolfcrypt.ciphers import Ed25519Private, Ed25519Public if _lib.ED448_ENABLED: - from wolfcrypt.ciphers import (Ed448Private, Ed448Public) - -from wolfcrypt.ciphers import ( - MODE_CTR, MODE_ECB, MODE_CBC, WolfCryptError -) + from wolfcrypt.ciphers import Ed448Private, Ed448Public @pytest.fixture diff --git a/tests/test_hashes.py b/tests/test_hashes.py index 3f5c2de..09eeb26 100644 --- a/tests/test_hashes.py +++ b/tests/test_hashes.py @@ -22,7 +22,6 @@ from collections import namedtuple import pytest -from wolfcrypt._ffi import ffi as _ffi from wolfcrypt._ffi import lib as _lib from wolfcrypt.utils import t2b From f03380a6444944ff3e1552b97b844b28aaf08047 Mon Sep 17 00:00:00 2001 From: Robert de Vries Date: Thu, 9 Apr 2026 20:12:42 +0200 Subject: [PATCH 07/10] Remove check for None in RsaPrivate.make_key(). The variable rsa is never None if it is successfully created. Under all other circumstances an exception is raised which will skip the execution of the remainder of this method. --- wolfcrypt/ciphers.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/wolfcrypt/ciphers.py b/wolfcrypt/ciphers.py index c86f585..8fa304b 100644 --- a/wolfcrypt/ciphers.py +++ b/wolfcrypt/ciphers.py @@ -829,8 +829,6 @@ if _lib.RSA_ENABLED: Generates a new key pair of desired length **size**. """ rsa = cls(hash_type=hash_type) - if rsa is None: # pragma: no cover - raise WolfCryptError("Invalid key error (%d)" % ret) ret = _lib.wc_MakeRsaKey(rsa.native_object, size, 65537, rng.native_object) From 9fe81ec773973b094a5be4bcdfa5adc6608d4bbe Mon Sep 17 00:00:00 2001 From: Robert de Vries Date: Thu, 9 Apr 2026 20:26:42 +0200 Subject: [PATCH 08/10] Fix missing format argument in exception message. --- scripts/build_ffi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build_ffi.py b/scripts/build_ffi.py index 68df95d..eebf399 100644 --- a/scripts/build_ffi.py +++ b/scripts/build_ffi.py @@ -332,7 +332,7 @@ def get_features(local_wolfssl, features): for d in include_dirs: if not os.path.exists(d): - e = "Invalid wolfSSL include dir: .".format(d) + e = f"Invalid wolfSSL include dir: {d}" raise FileNotFoundError(e) options = os.path.join(d, "wolfssl", "options.h") From e36859cb9c0ee0a6e196ad3d0e675edc6a2ba04d Mon Sep 17 00:00:00 2001 From: Martijn de Milliano Date: Tue, 24 Mar 2026 21:37:00 +0100 Subject: [PATCH 09/10] ML-DSA: Support deterministic signing --- scripts/build_ffi.py | 2 ++ tests/test_mldsa.py | 30 +++++++++++++++++++++++ wolfcrypt/ciphers.py | 57 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+) diff --git a/scripts/build_ffi.py b/scripts/build_ffi.py index d9519ae..1b72481 100644 --- a/scripts/build_ffi.py +++ b/scripts/build_ffi.py @@ -1032,6 +1032,8 @@ def build_ffi(local_wolfssl, features): 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_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); typedef dilithium_key MlDsaKey; int wc_MlDsaKey_GetPrivLen(MlDsaKey* key, int* len); diff --git a/tests/test_mldsa.py b/tests/test_mldsa.py index e664c8b..6e06220 100644 --- a/tests/test_mldsa.py +++ b/tests/test_mldsa.py @@ -28,6 +28,8 @@ if _lib.ML_DSA_ENABLED: from wolfcrypt.ciphers import MlDsaPrivate, MlDsaPublic, MlDsaType from wolfcrypt.random import Random + ML_DSA_SIGNATURE_SEED_LENGTH = 32 + @pytest.fixture def rng(): return Random() @@ -134,3 +136,31 @@ if _lib.ML_DSA_ENABLED: # Verify with wrong message wrong_message = b"This is a wrong message for ML-DSA signature" assert not mldsa_pub.verify(signature, wrong_message) + + 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) + pub_key = mldsa_priv.encode_pub_key() + + # Import public key + mldsa_pub = MlDsaPublic(mldsa_type) + mldsa_pub.decode_key(pub_key) + + # Sign a message + message = b"This is a test message for ML-DSA signature" + signature = mldsa_priv.sign_with_seed(message, signature_seed) + assert len(signature) == mldsa_priv.sig_size + + # Verify the signature using public key + assert mldsa_pub.verify(signature, message) + + # re-generate from the same seed: + signature_from_same_seed = mldsa_priv.sign_with_seed(message, signature_seed) + assert signature == signature_from_same_seed + + # test that the seed size is checked: + with pytest.raises(AssertionError): + _ = mldsa_priv.sign_with_seed(message, signature_seed[:-1]) + + with pytest.raises(AssertionError): + _ = mldsa_priv.sign_with_seed(message, "") diff --git a/wolfcrypt/ciphers.py b/wolfcrypt/ciphers.py index 8fa304b..1e24b0f 100644 --- a/wolfcrypt/ciphers.py +++ b/wolfcrypt/ciphers.py @@ -2149,6 +2149,9 @@ if _lib.ML_DSA_ENABLED: return res[0] == 1 class MlDsaPrivate(_MlDsaBase): + _SIGNATURE_SEED_LENGTH = 32 + """The length of a signature generation seed.""" + @classmethod def make_key(cls, mldsa_type, rng=Random()): """ @@ -2277,6 +2280,60 @@ if _lib.ML_DSA_ENABLED: return _ffi.buffer(signature, out_size[0])[:] + def sign_with_seed(self, message, seed, ctx=None): + """ + :param message: message to be signed + :type message: bytes or str + :param seed: 32-byte seed for deterministic signature generation. + :type seed: bytes + :param ctx: context (optional) + :type ctx: None for no context, str or bytes otherwise + :return: signature + :rtype: bytes + """ + msg_bytestype = t2b(message) + in_size = self.sig_size + signature = _ffi.new(f"byte[{in_size}]") + out_size = _ffi.new("word32 *") + out_size[0] = in_size + + assert isinstance(seed, bytes) and len(seed) == MlDsaPrivate._SIGNATURE_SEED_LENGTH, \ + f"Seed for generating a signature must be {MlDsaPrivate._SIGNATURE_SEED_LENGTH} bytes." + + if ctx is not None: + ctx_bytestype = t2b(ctx) + ret = _lib.wc_dilithium_sign_ctx_msg_with_seed( + _ffi.from_buffer(ctx_bytestype), + len(ctx_bytestype), + _ffi.from_buffer(msg_bytestype), + len(msg_bytestype), + signature, + out_size, + self.native_object, + _ffi.from_buffer(seed), + ) + if ret < 0: # pragma: no cover + raise WolfCryptError("wc_dilithium_sign_ctx_msg_with_seed() error (%d)" % ret) + else: + ret = _lib.wc_dilithium_sign_msg_with_seed( + _ffi.from_buffer(msg_bytestype), + len(msg_bytestype), + signature, + out_size, + self.native_object, + _ffi.from_buffer(seed), + ) + if ret < 0: # pragma: no cover + raise WolfCryptError("wc_dilithium_sign_msg_with_seed() error (%d)" % ret) + + + if in_size != out_size[0]: + raise WolfCryptError( + "in_size=%d and out_size=%d don't match" % (in_size, out_size[0]) + ) + + return _ffi.buffer(signature, out_size[0])[:] + class MlDsaPublic(_MlDsaBase): @property def key_size(self): From ac6eee4f847b16a82eb0f2d06797793192584455 Mon Sep 17 00:00:00 2001 From: Martijn de Milliano Date: Fri, 10 Apr 2026 18:04:13 +0200 Subject: [PATCH 10/10] Process Copilot comments - Use constant from ciphers.py - Raise ValueError or TypeError in sign_with_seed instead of assert - Add missing test case --- tests/test_mldsa.py | 27 ++++++++++++++++++++++----- wolfcrypt/ciphers.py | 32 +++++++++++++++++++++++--------- 2 files changed, 45 insertions(+), 14 deletions(-) diff --git a/tests/test_mldsa.py b/tests/test_mldsa.py index 6e06220..5fe8e0d 100644 --- a/tests/test_mldsa.py +++ b/tests/test_mldsa.py @@ -25,11 +25,9 @@ from wolfcrypt._ffi import lib as _lib if _lib.ML_DSA_ENABLED: import pytest - from wolfcrypt.ciphers import MlDsaPrivate, MlDsaPublic, MlDsaType + from wolfcrypt.ciphers import MlDsaPrivate, MlDsaPublic, MlDsaType, ML_DSA_SIGNATURE_SEED_LENGTH from wolfcrypt.random import Random - ML_DSA_SIGNATURE_SEED_LENGTH = 32 - @pytest.fixture def rng(): return Random() @@ -159,8 +157,27 @@ if _lib.ML_DSA_ENABLED: assert signature == signature_from_same_seed # test that the seed size is checked: - with pytest.raises(AssertionError): + with pytest.raises(ValueError): _ = mldsa_priv.sign_with_seed(message, signature_seed[:-1]) - with pytest.raises(AssertionError): + # test that the seed type is checked (should be bytes-like, not string) + with pytest.raises(TypeError): _ = mldsa_priv.sign_with_seed(message, "") + + def test_sign_with_seed_and_context(mldsa_type, rng): + signature_seed = rng.bytes(ML_DSA_SIGNATURE_SEED_LENGTH) + mldsa_priv = MlDsaPrivate.make_key(mldsa_type, rng) + pub_key = mldsa_priv.encode_pub_key() + + # Import public key + mldsa_pub = MlDsaPublic(mldsa_type) + mldsa_pub.decode_key(pub_key) + + # Sign a message + message = b"This is a test message for ML-DSA signature" + context = b"Some context for the signature" + signature = mldsa_priv.sign_with_seed(message, signature_seed, ctx=context) + assert len(signature) == mldsa_priv.sig_size + # test that the context length is checked (more than 255 bytes is invalid): + with pytest.raises(ValueError): + _ = mldsa_priv.sign_with_seed(message, signature_seed[:-1], ctx=bytes(1000)) diff --git a/wolfcrypt/ciphers.py b/wolfcrypt/ciphers.py index 1e24b0f..b64a2db 100644 --- a/wolfcrypt/ciphers.py +++ b/wolfcrypt/ciphers.py @@ -2028,6 +2028,9 @@ if _lib.ML_KEM_ENABLED: if _lib.ML_DSA_ENABLED: + ML_DSA_SIGNATURE_SEED_LENGTH = 32 + """The length of a signature generation seed.""" + class MlDsaType(IntEnum): """ `MlDsaType` specifies supported ML-DSA types. @@ -2149,9 +2152,7 @@ if _lib.ML_DSA_ENABLED: return res[0] == 1 class MlDsaPrivate(_MlDsaBase): - _SIGNATURE_SEED_LENGTH = 32 - """The length of a signature generation seed.""" - + @classmethod def make_key(cls, mldsa_type, rng=Random()): """ @@ -2286,7 +2287,7 @@ if _lib.ML_DSA_ENABLED: :type message: bytes or str :param seed: 32-byte seed for deterministic signature generation. :type seed: bytes - :param ctx: context (optional) + :param ctx: context (optional, maximum 255 bytes) :type ctx: None for no context, str or bytes otherwise :return: signature :rtype: bytes @@ -2297,20 +2298,33 @@ if _lib.ML_DSA_ENABLED: out_size = _ffi.new("word32 *") out_size[0] = in_size - assert isinstance(seed, bytes) and len(seed) == MlDsaPrivate._SIGNATURE_SEED_LENGTH, \ - f"Seed for generating a signature must be {MlDsaPrivate._SIGNATURE_SEED_LENGTH} bytes." + try: + seed_view = memoryview(seed) + except TypeError as exception: + raise TypeError( + "seed must support the buffer protocol, such as `bytes` or `bytearray`" + ) from exception + if len(seed_view) != ML_DSA_SIGNATURE_SEED_LENGTH: + raise ValueError( + f"Seed for generating a signature must be {ML_DSA_SIGNATURE_SEED_LENGTH}" + "bytes." + ) if ctx is not None: ctx_bytestype = t2b(ctx) + if len(ctx_bytestype) > 255: + raise ValueError( + f"context length {len(ctx_bytestype)} too large: must be 255 or less" + ) ret = _lib.wc_dilithium_sign_ctx_msg_with_seed( _ffi.from_buffer(ctx_bytestype), - len(ctx_bytestype), + len(ctx_bytestype), # length must be < 256 bytes _ffi.from_buffer(msg_bytestype), len(msg_bytestype), signature, out_size, self.native_object, - _ffi.from_buffer(seed), + _ffi.from_buffer(seed_view), ) if ret < 0: # pragma: no cover raise WolfCryptError("wc_dilithium_sign_ctx_msg_with_seed() error (%d)" % ret) @@ -2321,7 +2335,7 @@ if _lib.ML_DSA_ENABLED: signature, out_size, self.native_object, - _ffi.from_buffer(seed), + _ffi.from_buffer(seed_view), ) if ret < 0: # pragma: no cover raise WolfCryptError("wc_dilithium_sign_msg_with_seed() error (%d)" % ret)