diff --git a/scripts/build_ffi.py b/scripts/build_ffi.py index b0b4f24..02f9588 100644 --- a/scripts/build_ffi.py +++ b/scripts/build_ffi.py @@ -378,6 +378,9 @@ 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 + # Unlike the other fatures, HASHDRBG is enabled by default in random.h, unless WC_NO_HASHDRBG or + # CUSTOM_RAND_GENERATE_BLOCK is defined. + features["HASHDRBG"] = 0 if ("#define WC_NO_HASHDRBG" in defines or "#define CUSTOM_RAND_GENERATE_BLOCK" in defines) else 1 if '#define HAVE_FIPS' in defines: if not fips: @@ -497,6 +500,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 +541,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 +556,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 +1378,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..5f1de3d 100644 --- a/tests/test_random.py +++ b/tests/test_random.py @@ -21,6 +21,7 @@ # pylint: disable=redefined-outer-name import pytest +from wolfcrypt._ffi import lib as _lib from wolfcrypt.random import Random @@ -38,13 +39,44 @@ 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 + + +@pytest.mark.skipif(not _lib.HASHDRBG_ENABLED, reason="Reseeding only available with hash-DRBG") +@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 % 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. + rng.bytes(32) + + +@pytest.mark.skipif(not _lib.HASHDRBG_ENABLED, reason="Reseeding only available with hash-DRBG") +def test_reseed_multiple(rng): + """ + Test that consecutive reseeding of the random number generator works. + """ + for _ in range(10): + # Create seed of typical size. Testing with various seed sizes done in `test_reseed_sizes`. + seed = bytes(x % 256 for x in range(32)) + rng.reseed(seed) + + # Pull some bytes from the random number generator to test that it still works. + rng.bytes(100) diff --git a/windows/non_fips/user_settings.h b/windows/non_fips/user_settings.h index ab81e47..ef41da3 100644 --- a/windows/non_fips/user_settings.h +++ b/windows/non_fips/user_settings.h @@ -6,6 +6,7 @@ #endif #define WOLFCRYPT_ONLY +#define HAVE_HASHDRBG #define WOLFSSL_AESGCM_STREAM #define HAVE_AESGCM #define GCM_TABLE_4BIT diff --git a/wolfcrypt/_ffi/lib.pyi b/wolfcrypt/_ffi/lib.pyi index 047e5cf..38426e4 100644 --- a/wolfcrypt/_ffi/lib.pyi +++ b/wolfcrypt/_ffi/lib.pyi @@ -31,6 +31,7 @@ SHA256_ENABLED: int SHA384_ENABLED: int SHA512_ENABLED: int WC_RNG_SEED_CB_ENABLED: int +HASHDRBG_ENABLED: int FIPS_VERSION: int @@ -73,4 +74,5 @@ RNG: TypeAlias = FFI.CData def wc_InitRngNonce_ex(rng: RNG, nonce: bytes, nonce_size: int, heap: FFI.CData, device_id: int) -> int: ... def wc_RNG_GenerateByte(rng: RNG, buffer: FFI.CData) -> int: ... def wc_RNG_GenerateBlock(rng: RNG, buffer: FFI.CData, len: int) -> int: ... +def wc_RNG_DRBG_Reseed(rng: RNG, seed: bytes, seed_size: int) -> int: ... def wc_FreeRng(rng: RNG) -> None: ... diff --git a/wolfcrypt/random.py b/wolfcrypt/random.py index 1c4da84..2948eaf 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: # pragma: no cover + raise WolfCryptApiError("RNG reseed error", ret)