Skip to content

Bare metal: one ChaCha20 generator, keyed from the hardware, behind every reader - #1059

Merged
pdfinn merged 4 commits into
masterfrom
feat/kernel-csprng
Nov 5, 2026
Merged

pdfinn merged 4 commits into
masterfrom
feat/kernel-csprng

Conversation

@pdfinn

@pdfinn pdfinn commented Nov 3, 2026 •

Copy link
Copy Markdown
Member

#1051 made the rule that a secret comes only from a hardware generator. It kept the rule by reading the generator for every byte asked for. That had three problems:

  • Readers got the hardware's raw output, with nothing behind it if the hardware were wrong in a way its own health tests miss.
  • It ran at the hardware's speed: an RNG200 gives about 190 KB/s, and /dev/random gave 45 KB/s.
  • A key that waited for a generator held the Dis VM. On a Pi 5 with the generator absent, VM 0 froze and the console with it.

This puts one deterministic generator between the hardware and every reader, as Linux, the BSDs and 9front do. It is in os/port/random.c, so it covers every board. Readers are #c/random, #c/notquiterandom, genrandom, prng, prngtry and truerand.

The generator

  • Construction. ChaCha20 (RFC 8439) with fast key erasure, the same construction as libsec's hosted genrandom.c. Each request first takes 32 bytes of keystream as the next key, and its output comes after them. The state that produced a request is gone once it returns.
  • Keyed from hwrandom alone. key' = SHA-256(32 bytes of the old stream || 32 bytes from the hardware). It reseeds every second or every megabyte, whichever comes first. Nothing is returned before the first 32 hardware bytes; readers wait, and the console says why every five seconds.
  • Dry generator. The old key keeps serving for at most ten minutes, then readers wait again.
  • Refused seeds. 32 hardware bytes that are one 32-bit word eight times (an unmapped register, a stuck FIFO), or that repeat the previous seed, are not used.
  • Self-check. ChaCha20 is checked against RFC 8439's test vector, on an unaligned buffer, before its first use. A wrong result panics.
  • The VM. A Keyring builtin that waits for a key lets go of the Dis VM while it waits.
  • insecurerandom. It keys the generator from the labelled counter, as before, and the label is now printed at boot.

ndb/dns

ndb/dns read one id from #c/random at startup and counted up from it. With no generator, that kept os/init/profile waiting forever, and the console shell with it; I found this on the Pi 5. #1051's run with no generator stalled the same way. dns now reads a random id for each query, at query time. The shell comes up, and lookups wait.

Tests

tests/host/kernrandom_test.sh (added to CI shard 2) compiles random.c on the host against a simulated generator and clock. It checks, in nine cases:

  • the output against a model written from the RFC;
  • nothing is written when there is no generator;
  • the VM is released for every wait and taken back after it;
  • reseeds by time and by bytes;
  • the 100 ms retry and the ten-minute dry window;
  • stuck and repeated seeds are refused;
  • insecurerandom;
  • 4096 requests, no two alike.

I tried these mutations, and each one fails the test: no key erasure, a slower reseed, a zero-padded short seed, a wrong test vector, the VM held while waiting, and a longer dry window.

Pi 5 board (BCM2712, RNG200)

Run on the integration branch's kernel. Its random.c and dns.b are this PR's, except for the boot-time label hunk, which does nothing on a board that has a generator.

Throughput 64 MB in 0.284 s (about 236 MB/s; it was 45 KB/s); 20000 × 32-byte reads in 143 ms (7 µs each)
16 MB stream, two boots entropy 7.99999 bits/byte; chi² 220 and 253; no repeated 16-byte block; 32-bit collisions 2022 and 2035 (about 2048 expected); FIPS 140-2 failures 6 and 5 of 6710 blocks (about 7 expected)
Four VMs at once, 2 MB no repeated 16-byte block; FIPS 1 of 838 blocks
Different boots the first 32 bytes differ
Keys and TLS RSA-2048 in 2.4 s; two keys differ; TLS live test 4/4
Injected health-test failures, and the generator dry for 8 s readers never stopped; the 16 MB stream passes the same statistics
No generator the console shell runs every command; VM 1 answers; a 1024-bit RSA key waits, saying why every 5 s, and no key is written; reboot on command works

QEMU

All from this branch.

Machine Result
virt (cortex-a53, virtio-rng) 195 passed, 0 failed. Includes the no-generator checks: the shell comes up, key material and #c/random wait and say why, nothing is made, and insecurerandom answers with the label.
raspi4b (no RNG200 model, so insecurerandom) 43 passed, 0 failed
raspi3b 289 passed, 0 failed
RISC-V: riscvvirt, mpfs both kernels build; no qemu-system-riscv64 here, so they are not booted (CI boots them)

Not done yet

  • The Pi 4 and the Pi 3 have not run this on hardware. The Pi 4 is in another soak, and the Pi 3 is off the bench. Their QEMU runs are above.
  • The Pi 5 24-hour soak is running, with the generator under load: reads with a duplicate check, an RSA key every minute and TLS every 10 minutes. I will post the result here.

…very reader

#1051 made the rule that a secret comes only from a hardware generator,
and kept it by reading the generator for every byte. That gave readers
the hardware's raw output with nothing behind it, at its speed (an
RNG200 is about 190 KB/s), and a key that waited for a generator held
the Dis VM, so the console and every program on that VM stopped too.

os/port/random.c is now a deterministic generator between the hardware
and every reader -- #c/random, #c/notquiterandom, genrandom, prng,
prngtry, truerand -- as in Linux, the BSDs and 9front:

- ChaCha20 (RFC 8439) with fast key erasure, as libsec's hosted
  genrandom.c: each request takes 32 bytes of keystream as the next key
  first, and its output follows them; the state it came from is gone.
- Keyed from hwrandom alone: key' = SHA-256(32 bytes of the old stream
  || 32 bytes from the hardware), every second or megabyte. Nothing is
  returned before the first 32 hardware bytes; readers wait, saying why.
- A dry generator: the old key serves for at most ten minutes, then
  readers wait again. 32 hardware bytes that are one word eight times,
  or repeat the previous seed, are refused.
- ChaCha20 checked against RFC 8439's test vector before first use.
- A Keyring builtin waiting for a key lets go of the Dis VM.
- insecurerandom keys it from the labelled counter, as before.

ndb/dns read one id from #c/random at startup and counted up from it.
With no generator that kept os/init/profile, and so the console shell,
waiting for ever. It now reads a random id for each query, when it
makes the query: the shell comes up, and lookups wait.

tests/host/kernrandom_test.sh compiles random.c on the host against a
simulated generator and clock: the output against a model written from
the RFC, fail-closed, VM release, reseed by time and bytes, the dry
window, refused seeds, insecurerandom. Each mutation tried (no key
erasure, slower reseed, zero-padded seed, wrong test vector, VM held,
longer dry window) fails it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0195u6z8JjsAy9yrE5TqKxzx
# Conflicts:
#	.github/workflows/ci.yml
#	CLAUDE.md
@pdfinn
pdfinn marked this pull request as ready for review November 5, 2026 08:46
@pdfinn
pdfinn merged commit d2ab405 into master Nov 5, 2026
48 of 50 checks passed
@pdfinn
pdfinn deleted the feat/kernel-csprng branch November 5, 2026 13:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants