Random bytes
sysk's rng driver answers every read of /dev/rng from a generator, not straight from the entropy device: ChaCha20 under a key only sysc's file sees, as OpenBSD's arc4random does it. A generator exists only once it has a seed, so no byte is drawn before one, and no call can fail after. Its key is replaced after every read, so a state stolen later cannot give back what was read before. The figure computes it in the page: the same steps in JavaScript.
ChaCha20's block
RFC 8439 §2.3 makes 64 bytes from sixteen words: four constants ("expand 32-byte k"), the key's eight, a counter and the nonce's three. Twenty rounds of quarter rounds stir a copy of them, each quarter round four additions, four xors and four rotations of 32-bit words, and the input is added back at the end. Nothing looks at a value: every index is a constant and no branch depends on the key, so its time tells nothing about it. crypto/chacha.sysp writes the quarter round and the block as the RFC does, in a buffer of 32 words the caller lends (the input, then the block). Its names are public only because sysp's privacy is a file's: code outside sysc uses the generator, never the block (constitution VI).
The generator
crypto/rnd.sysp keeps ChaCha20's words in Rnd, a struct private to its file. rnd-new makes one from 32 bytes of seed, its first key. rnd-fill writes n bytes, any length, from blocks 1, 2, … of the key, and then replaces the key with the first 32 bytes of block 0: fast key erasure (Bernstein's, as arc4random does it). The key that made the bytes is gone once they are given out. rnd-mix takes fresh bytes from the device, or a seed stored across boots, 32 at a time: the new key is the old key's keystream (block 0, with the nonce's first word 1, so it is never a fill's block) xored with them, as arc4random's _rs_rekey does. Bytes an adversary knows or chose give a key as good as the old one, so a weak source cannot undo a strong one. That is also why a stored seed is mixed in, never used alone: sysf's recovery can bring back an older one. rnd-buf zeroes the words with ct-wipe before giving the buffer back. A caller whose seed never came, sysk's driver on a machine without the device, holds a Rnd from rnd-none instead, and nothing can be drawn from it by proof, not by care: rnd-fill and rnd-mix require rnd-live, which only rnd-new ensures, so a read that does not ask rnd-ready first does not prove (sysc 009). Fill some bytes, mix some in, and watch the key.
In sysk
sysk's rng driver answers every read from a Rnd (sysc 009, carried by sysc into sysk as agreed with sysk and sysn; Devices as files). At start it gathers 32 bytes from the entropy device, lent as often as it takes, and makes the generator in 32 words from its heap's alloc-w32, which since sysk 091 is alloc of 128 bytes and sysp's retype, no extern of its own. A read of /dev/rng that comes before then waits for the seed, rather than be answered an error that only means "not yet" (getrandom's choice). Once a mebibyte has been served, the next read first mixes in 32 fresh device bytes. Every wait is on the device alone, so the driver never waits on a client, and the model sees no new edge. Mixing whenever the device has bytes was not taken: QEMU's device always has them, and the driver would lend without end. The files interface did not change: netd (sysn 012) opens /dev/rng once, reads 16 bytes, and keys TCP's sequence numbers, SYN cookies and DNS ids with them.
Tested against
| What | Vectors | And |
|---|---|---|
| the block | RFC 8439's appendix A.1: five blocks, zero and non-zero keys, nonces and counters; A.2: three messages enciphered, the keystream of successive counters xored with the plaintext | 200 random keys, nonces and counters against tools/chacha.rkt, the block written in Racket from the RFC |
| the generator | none published: the construction is arc4random's, not a standard's | tools/chacha.rkt's generator, written from this page's description: 200 seeds, fills of every length from 0 to 199 and mixes of 0 to 100 bytes; fills of 2,048 bytes; a fill after a fill is not the first fill's next blocks |
Wycheproof has no vectors for ChaCha20 alone; its vectors for the authenticated cipher test ChaCha20 there (Sealed messages, sysc 016). Proven, besides every read and write within its buffer: a fill writes only the bytes it was asked for and leaves the rest of the caller's buffer as it was, which sysk's driver needs to keep its proof of the reply's header; and ct-wipe leaves every word zero.
The vectors are kept as published, in tests/vectors/rfc8439-chacha.txt. A test that could not fail would prove nothing: the block was first run with one rotation changed, and failed every vector.