Skip to content

rng: TRNG timeout returns zeros silently; no sanity check in src/rng.py #370

Description

@helmchen1968

I reviewed the entropy path after the Coldcard Mk3 advisory (July 2026) to check whether Specter shares that failure class. It does not — and the design is notably more robust:

  • boards/STM32F469DISC/mpconfigboard.h sets MICROPY_HW_ENABLE_RNG (1), so the Yasmarang software fallback in ports/stm32/rng.c is compiled out entirely.
  • There is no second rng_get() implementation competing for the same symbol, which was the root cause on Coldcard.
  • If the macro were ever misconfigured, os.urandom would not exist at all (moduos.c), so src/rng.py would fail loudly instead of silently degrading.
  • Touchscreen entropy is fed automatically on every PRESSING event via the decorators in src/gui/decorators.py, not as an opt-in step.

While reading, I noticed two related gaps that I think are worth closing.

1. rng_get() returns 0 on timeout

In f469-disco/micropython/ports/stm32/rng.c:

while (!(RNG->SR & RNG_SR_DRDY)) {
    if (HAL_GetTick() - start >= RNG_TIMEOUT_MS) {
        return 0;
    }
}

If the RNG peripheral stops responding, rng_get() returns zero without signalling an error. os_urandom() then fills its buffer with those zeros, and src/rng.py receives them as if they were entropy.

This is upstream MicroPython behaviour rather than Specter's code, but it is on the seed-generation path.

2. No sanity check in get_random_bytes()

In src/rng.py, the output of get_trng_bytes() is used and fed into the pool without any check:

def get_random_bytes(nbytes):
    global entropy_pool
    d = get_trng_bytes(nbytes)
    feed(d)  # why not?
    ...

Combined with (1), a dead or stalled peripheral is indistinguishable from a working one.

Impact

Low, and I want to be clear about that. The entropy pool mixes in touchscreen timing (time.ticks_cpu() at 180 MHz), so a failed TRNG would not make seeds predictable — it would silently reduce them to whatever the touch entropy contributed. That is defence in depth working as intended.

But it is a side effect rather than a guaranteed property. entropy_pool starts from the constant b"7" * 64, so the residual strength depends entirely on how many touch events happened to be accumulated beforehand — which is not measured anywhere.

Suggestion

Something minimal in get_random_bytes() would close both gaps, e.g. rejecting all-zero or single-repeated-byte TRNG output and surfacing a visible error rather than continuing.

A second, optional idea: track how many entropy contributions the pool has received and require a minimum before allowing seed generation — perhaps by prompting the user to draw on the screen if the count is low.

Happy to open a PR if the maintainers agree on the approach. Thanks for the careful design here; the layering is what makes this a minor issue rather than a serious one.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions