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
8 changes: 8 additions & 0 deletions scripts/build_ffi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [Medium] HASHDRBG feature not detected on Windows non-FIPS build; reseed() silently unavailable

The new detection features["HASHDRBG"] = 1 if "#define HAVE_HASHDRBG" in defines else 0 does a literal, exact-line match against the defines pulled from options.h/user_settings.h. On Linux/macOS the autotools build emits #define HAVE_HASHDRBG into the generated options.h (verified in lib/wolfssl/.../include/wolfssl/options.h), so detection works. On Windows non-FIPS the build copies windows/non_fips/user_settings.h as the sole defines source, and that file does NOT contain #define HAVE_HASHDRBG — Hash-DRBG is left enabled implicitly via wolfSSL's default settings.h (no WC_NO_HASHDRBG). As a result, features["HASHDRBG"] evaluates to 0 on Windows non-FIPS even though wc_RNG_DRBG_Reseed is actually compiled into the library. Consequently HASHDRBG_ENABLED is 0, wc_RNG_DRBG_Reseed is omitted from the cdef, and Random.reseed() is never defined on the default Windows build — the very feature this PR adds ships as unavailable there. Note windows/fips_ready/user_settings.h:17 already defines HAVE_HASHDRBG, and windows/non_fips/user_settings.h:23 already lists HAVE_HKDF for exactly this reason, so the omission looks accidental.

Fix: Add #define HAVE_HASHDRBG to windows/non_fips/user_settings.h (mirroring the existing HAVE_HKDF define and the fips_ready variant) so the new detection fires and reseed() is exposed on the default Windows build. Alternatively, document that reseed is Linux/macOS-only for now.


if '#define HAVE_FIPS' in defines:
if not fips:
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand All @@ -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 += """
Expand Down Expand Up @@ -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.
Expand Down
35 changes: 35 additions & 0 deletions tests/test_random.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
# pylint: disable=redefined-outer-name

import pytest
from wolfcrypt._ffi import lib as _lib
from wolfcrypt.random import Random


Expand All @@ -38,13 +39,47 @@ 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")
Comment thread
dgarske marked this conversation as resolved.
@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")
Comment thread
dgarske marked this conversation as resolved.
def test_reseed_multiple(rng):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 [Low] test_reseed_multiple is non-deterministic

The test derives both the per-iteration seed size and the final byte count from live RNG output (seed_size = ord(rng.byte()), num_bytes = ord(rng.byte())). This makes the test path non-deterministic: a failure would not reproduce with a fixed input, and coverage varies run to run. Values are bounded to 0-255 so there is no resource risk, but reproducibility suffers. The success-path coverage itself is otherwise good (test_reseed_sizes already exercises 0/1/32/1000).

Fix: Use fixed, explicit seed sizes and byte counts so the test is deterministic and reproducible; keep the loop to prove consecutive reseeds work.

"""
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 % 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)
10 changes: 10 additions & 0 deletions wolfcrypt/random.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Comment thread
dgarske marked this conversation as resolved.
def reseed(self, seed: __builtins__.bytes) -> None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 [Low] reseed() accepts bytes only with no input normalization

reseed passes seed straight to the CFFI call. If a caller passes a str instead of bytes, CFFI raises an opaque type error rather than a clear message. Other modules (e.g. hkdf.py) normalize inputs via t2b(). This is consistent with Random.__init__ (which also takes nonce as raw bytes without t2b), so within random.py the convention is followed; flagging only for awareness since the type hint (__builtins__.bytes) is not enforced at runtime.

Fix: Optional: either leave as-is to match Random.init's bytes-only convention, or run seed through t2b() for consistency with the rest of the bindings and friendlier errors on str input.

"""
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)
Loading