cryptoadvance / cryptoadvance/specter-diy

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

Open
#370 5 comments 2 reactions 0 assignees View on GitHub
Dominant language
Python
Stars
585
Forks
108
Avg merge
6d 1h
Merged PRs (30d)
5

Description

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`:

```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:

```python
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.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start by tracing the timeout path in f469-disco/micropython/ports/stm32/rng.c and the get_trng_bytes()/get_random_bytes() entry points in src/rng.py. Review the entropy_pool feed path and agree on the failure behavior; done means a stalled or visibly invalid TRNG result is no longer accepted silently, with the chosen behavior verified at the seed-generation path.

Written by the indexing model from the issue text.

Assessment

Tech stack
c, python
Domain
embedded-iot, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.