From e6f46320309f02ea4b6f258eba0a50d8c00cd716 Mon Sep 17 00:00:00 2001 From: Martijn de Milliano Date: Fri, 26 Jun 2026 00:02:59 +0000 Subject: [PATCH 1/3] Random: add DRBG reseed support --- scripts/build_ffi.py | 8 ++++++++ tests/test_random.py | 16 ++++++++++++++++ wolfcrypt/random.py | 10 ++++++++++ 3 files changed, 34 insertions(+) diff --git a/scripts/build_ffi.py b/scripts/build_ffi.py index b0b4f24..5f3edba 100644 --- a/scripts/build_ffi.py +++ b/scripts/build_ffi.py @@ -378,6 +378,7 @@ def get_features(local_wolfssl, features): features["ML_DSA"] = 1 if '#define HAVE_DILITHIUM' 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 + features["HASHDRBG"] = 1 if "#define HAVE_HASHDRBG" in defines else 0 if '#define HAVE_FIPS' in defines: if not fips: @@ -497,6 +498,7 @@ def build_ffi(local_wolfssl, features): int ML_KEM_ENABLED = {features["ML_KEM"]}; int ML_DSA_ENABLED = {features["ML_DSA"]}; int HKDF_ENABLED = {features["HKDF"]}; + int HASHDRBG_ENABLED = {features["HASHDRBG"]}; """ ffibuilder.set_source( "wolfcrypt._ffi", init_source_string, @@ -537,6 +539,7 @@ def build_ffi(local_wolfssl, features): extern int ML_KEM_ENABLED; extern int ML_DSA_ENABLED; extern int HKDF_ENABLED; + extern int HASHDRBG_ENABLED; typedef unsigned char byte; typedef unsigned int word32; @@ -551,6 +554,10 @@ def build_ffi(local_wolfssl, features): int wc_RNG_GenerateByte(WC_RNG*, byte*); int wc_FreeRng(WC_RNG*); """ + if features["HASHDRBG"]: + cdef += """ + int wc_RNG_DRBG_Reseed(WC_RNG*, const byte*, word32); + """ if features["ERROR_STRINGS"]: cdef += """ @@ -1369,6 +1376,7 @@ def main(ffibuilder): "ML_KEM": 1, "ML_DSA": 1, "HKDF": 1, + "HASHDRBG": 1, } # Ed448 requires SHAKE256, which isn't part of the Windows build, yet. diff --git a/tests/test_random.py b/tests/test_random.py index bf59f4e..6daed86 100644 --- a/tests/test_random.py +++ b/tests/test_random.py @@ -21,7 +21,9 @@ # pylint: disable=redefined-outer-name import pytest +from wolfcrypt._ffi import lib as _lib from wolfcrypt.random import Random +from wolfcrypt.exceptions import WolfCryptApiError @pytest.fixture @@ -48,3 +50,17 @@ def test_nonce_byte(rng_nonce): @pytest.mark.parametrize("length", (1, 8, 128)) def test_nonce_bytes(rng_nonce, length): assert len(rng_nonce.bytes(length)) == length + + +@pytest.mark.skipif(not _lib.HASHDRBG_ENABLED, reason="Reseeding only available with hash-DRBG") +def test_reseed(rng): + rng.reseed(b"some seed material for testing") + assert len(rng.bytes(32)) == 32 + + +@pytest.mark.skipif(not _lib.HASHDRBG_ENABLED, reason="Reseeding only available with hash-DRBG") +def test_reseed_empty(rng): + try: + rng.reseed(b"") + except WolfCryptApiError: + pass # acceptable — C rejects zero-length seed diff --git a/wolfcrypt/random.py b/wolfcrypt/random.py index 1c4da84..04fc6c5 100644 --- a/wolfcrypt/random.py +++ b/wolfcrypt/random.py @@ -77,3 +77,13 @@ def bytes(self, length: int) -> __builtins__.bytes: raise WolfCryptApiError("RNG generate block error", ret) return _ffi.buffer(result, length)[:] + + if _lib.HASHDRBG_ENABLED: + def reseed(self, seed: __builtins__.bytes) -> None: + """ + Reseed the DRBG with the provided seed material. + """ + assert self.native_object is not None + ret = _lib.wc_RNG_DRBG_Reseed(self.native_object, seed, len(seed)) + if ret < 0: + raise WolfCryptApiError("RNG reseed error", ret) From 0c1e8f2b6a7627c7be16bf149473500f8241e703 Mon Sep 17 00:00:00 2001 From: Martijn de Milliano Date: Tue, 30 Jun 2026 17:42:27 +0200 Subject: [PATCH 2/3] Update tests for reseeding --- tests/test_random.py | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/tests/test_random.py b/tests/test_random.py index 6daed86..dc26e48 100644 --- a/tests/test_random.py +++ b/tests/test_random.py @@ -23,7 +23,6 @@ import pytest from wolfcrypt._ffi import lib as _lib from wolfcrypt.random import Random -from wolfcrypt.exceptions import WolfCryptApiError @pytest.fixture @@ -53,14 +52,31 @@ def test_nonce_bytes(rng_nonce, length): @pytest.mark.skipif(not _lib.HASHDRBG_ENABLED, reason="Reseeding only available with hash-DRBG") -def test_reseed(rng): - rng.reseed(b"some seed material for testing") - assert len(rng.bytes(32)) == 32 +@pytest.mark.parametrize("seed_size", [0, 1, 32, 1000]) +def test_reseed_sizes(rng, seed_size): + """ + Test that reseeding the random number generator works, for various seed sizes. + """ + # Create seed of required length. + seed = bytes(x % 255 for x in range(seed_size)) + assert len(seed) == seed_size + rng.reseed(seed) + # Pull some bytes from the random number generator to test that it still works. + rng.bytes(32) @pytest.mark.skipif(not _lib.HASHDRBG_ENABLED, reason="Reseeding only available with hash-DRBG") -def test_reseed_empty(rng): - try: - rng.reseed(b"") - except WolfCryptApiError: - pass # acceptable — C rejects zero-length seed +def test_reseed_multiple(rng): + """ + Test that consecutive reseeding of the random number generator works. + """ + # Using our own rng for getting random seed sizes. + for _ in range(10): + # Create seed using random seed size for each call. + seed_size = ord(rng.byte()) + seed = bytes(x % 255 for x in range(seed_size)) + rng.reseed(seed) + + # Pull some bytes from the random number generator to test that it still works. + num_bytes = ord(rng.byte()) + rng.bytes(num_bytes) From 0124b33202400b464184a463aca37e9f02a94be7 Mon Sep 17 00:00:00 2001 From: Martijn de Milliano Date: Tue, 30 Jun 2026 20:47:41 +0200 Subject: [PATCH 3/3] Process review comments for random and test_random - add pragma no cover - format using ruff - fix module factor in test_random --- tests/test_random.py | 9 ++++++--- wolfcrypt/random.py | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/test_random.py b/tests/test_random.py index dc26e48..9b07b71 100644 --- a/tests/test_random.py +++ b/tests/test_random.py @@ -39,13 +39,16 @@ def test_bytes(rng): 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 @@ -58,7 +61,7 @@ def test_reseed_sizes(rng, seed_size): Test that reseeding the random number generator works, for various seed sizes. """ # Create seed of required length. - seed = bytes(x % 255 for x in range(seed_size)) + seed = bytes(x % 256 for x in range(seed_size)) assert len(seed) == seed_size rng.reseed(seed) # Pull some bytes from the random number generator to test that it still works. @@ -74,9 +77,9 @@ def test_reseed_multiple(rng): for _ in range(10): # Create seed using random seed size for each call. seed_size = ord(rng.byte()) - seed = bytes(x % 255 for x in range(seed_size)) + seed = bytes(x % 256 for x in range(seed_size)) rng.reseed(seed) - + # Pull some bytes from the random number generator to test that it still works. num_bytes = ord(rng.byte()) rng.bytes(num_bytes) diff --git a/wolfcrypt/random.py b/wolfcrypt/random.py index 04fc6c5..2948eaf 100644 --- a/wolfcrypt/random.py +++ b/wolfcrypt/random.py @@ -85,5 +85,5 @@ def reseed(self, seed: __builtins__.bytes) -> None: """ assert self.native_object is not None ret = _lib.wc_RNG_DRBG_Reseed(self.native_object, seed, len(seed)) - if ret < 0: + if ret < 0: # pragma: no cover raise WolfCryptApiError("RNG reseed error", ret)