Skip to content

About

Turns your Digispark into an USB-pluggable Random Number Generator

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

25 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

DigisparkRNG

Hardware random number generator on a Digispark (ATtiny85): watchdog timer jitter and ADC noise, conditioned with the Ascon permutation. The design notes are in the header comment of clockdrift_rng/clockdrift_rng.ino.

Every output bit is backed by credited entropy: the sketch estimates how much each sample is worth, and squeezes 64 bits out only once 128 have been credited. That makes it an entropy source rather than a random number generator -- about 285 bytes/s, which is the rate the sources actually produce, not a limit imposed by USB.

A 64 MB capture of x output passed PractRand (RNG_stdin8, 2^26 bytes) with no anomalies in 95 test results, over 65 hours at the entropy rate. Note what that does and does not say: any competent conditioner makes any input look uniform -- hash a counter and every test suite ever written passes -- so the statistical run tests the plumbing. The claim about the entropy rests on the SP 800-90B assessment of the raw samples below.

Try it in the browser: the UART build runs in the Wokwi simulator at https://wokwi.com/projects/475707136206905345 -- the s stream and the status LED, no hardware needed. See wokwi/ for how it is wired and what the simulator can and cannot do.

Layout

  • clockdrift_rng/: the sketch. ~/Arduino/clockdrift_rng is a symlink to it, so the Arduino IDE finds it and the source lives here.
    • clockdrift_rng.ino, ascon_permute.S: firmware
    • rawdump.py: convert raw dumps for SP 800-90B, compute health test cutoffs
    • analyze.py: statistics on hex output (x mode), a check for bugs only
  • tools/
    • build.sh [SKETCH_DIR]: compile for the Digispark at 16.5 MHz into build/
    • flash.sh [HEX]: Ctrl-C x3 into the bootloader, flash, wait for the port
    • stump.py: capture a mode, jump to the bootloader, reboot, wait for the port
    • rngread.c: read the random bytes over libusb (see Two transports)
    • rng2random.c: add conditioned output to /dev/random with an entropy credit (RNDADDENTROPY); autodetects hex/binary; for cron (see Typical use)
    • 60-digispark-rng.rules: udev rule so rngread needs no root
    • assess.sh [ADC_MINUTES] [INTERVAL_MINUTES]: raw d and r captures, the SP 800-90B assessment and health test cutoffs, all in captures/<date-time>/
    • build-sp800-90b.sh: rebuild ea_non_iid and its libraries in third_party/
  • wokwi/: the Wokwi simulator diagram and notes for running the UART build in a browser (see the link above)

Not in the repository, because they belong to other projects: arduino-cli (copied from the Arduino IDE AppImage), arduino-lint, ea_non_iid (built by build-sp800-90b.sh), third_party/, build/ and captures/.

Needs DigiCDCFast in ~/Arduino/libraries -- 1.4.0 or newer for the libusb transport, which uses a hook that release added; the serial build works with any version. The Digistump AVR core provides the board.

Typical use

tools/build.sh && tools/flash.sh
python3 tools/stump.py capture x 60 out.txt             # conditioned output
nohup tools/assess.sh 6 0 > captures/assess.log 2>&1 &  # ADC only, ~8 minutes

Board: micronucleus 2.6 bootloader (6650 bytes free). If no sketch is running, flash.sh waits 60 s for the board to be plugged in.

Feed the kernel's entropy pool

Pipe the RNG into /dev/random so the CRNG mixes in real hardware entropy. On a current kernel a plain copy once a minute -- board left in a conditioned mode (x/X), no root needed -- is more than enough:

* * * * *  bash -c 'dd if=/dev/ttyACM0 bs=1 count=32 of=/dev/random 2>/dev/null'

tools/rng2random.c does the same but also credits the entropy (RNDADDENTROPY), which older kernels needed and current ones ignore. See tools/README.md for when that credit still matters and how to use it.

Build options

Pass them through EXTRA_FLAGS, as in EXTRA_FLAGS="-DRNG_VENDOR=1" tools/build.sh.

option
RNG_VENDOR 0 1: vendor control transfers instead of the serial port (see Transports)
RNG_UART 0 1: a plain UART on PB2 (TX)/PB0 (RX) instead of USB (see Transports)
RNG_UART_BAUD 115200 UART baud; the send blocks per byte, so lower rates block longer (see Transports)
DEFAULT_MODE s the power-up mode before any has been stored (see Modes); fixes a headless build to one output
LED_STATUS 1 0: no status LED
LED_PIN PB1 the onboard LED; some clones wire it to PB0
STREAM_MODE 1 0: no s/S streaming mode, 140 bytes less flash
MODE_EEPROM 1 1: remember the last mode in EEPROM byte 0 across power cycles; 0: always boot DEFAULT_MODE
RNG_CDC_ECHO RNG_VENDOR 1: echo the serial port back, to prove it still works while the bytes go out over libusb
ADC_BATCH 16 ADC readings per loop pass. Tuned; the transmitter's timing was measured with it
STACK_CHECK 0 1: report never-used RAM after each x line

Sizes on the Digispark, of the 6650 bytes micronucleus leaves and 512 of RAM:

build flash RAM
default (USB CDC) 6376 377
RNG_VENDOR=1 6206 381
RNG_UART=1 3712 157
LED_STATUS=0 6242 374

The UART build is half the size because it carries no USB stack at all.

Modes

Send the character over the serial port, or ask for it with rngread --mode:

Lowercase is hex text (readable on a terminal); uppercase is binary.

mode output
s / S conditioned, free-running at ~5700 bytes/s, hex / binary
x / X conditioned bytes, hex / binary
r / R raw watchdog intervals, low 16 bits of CPU cycles, hex / binary
d / D raw ADC readings, signed bytes, hex / binary

The default is s: on power-up it gathers entropy until seeded, then streams hex at the chip's full rate, so the first thing you see in a serial terminal is readable random data. Send S for the same stream in binary to feed a program, x/X for entropy-rate output, or the assessment tools drive the uppercase R/D.

The last mode you select is remembered in EEPROM byte 0 and restored on the next power-up, so a board keeps whatever you set it to. A blank or unrecognised byte (0xFF on a fresh chip) falls back to DEFAULT_MODE, which is written back so the byte is valid from then on. Turn this off with MODE_EEPROM=0, and the board always boots DEFAULT_MODE.

s/S (the same stream, hex or binary) are a different thing from x/X, and the difference is the whole point of this project, so it is worth stating plainly. In X every bit is backed by credited entropy, and the output is unpredictable even against an adversary with unlimited computation. S keeps squeezing whether or not entropy has been credited for it, so its output is unpredictable because Ascon is -- a DRBG (an RBG2 construction, in SP 800-90C's terms) rather than an entropy source. That is the ordinary architecture of every OS random generator and perfectly sound; it is simply a different claim.

Two things keep it honest. Nothing is emitted until the capacity has been seeded four times over, so a stream never comes from a state that was never filled; and entropy keeps being absorbed, so a compromised state heals. The backtracking ratchet still runs, but only where fresh entropy paid for it, so a captured state exposes at most one credit's worth of earlier output rather than the whole stream.

The failure mode you must know about: if a source dies, S keeps streaming perfect-looking bytes from a stale seed, forever and silently. In X the rate visibly collapses. So a consumer of S should check the source flags and seeded (rngread --info) rather than trusting the stream on its own. Compile it out with STREAM_MODE=0 if you would rather it not exist; it costs 140 bytes of flash.

Measured: 5650 bytes/s, twenty times X's 284, against a theoretical 8000 that low-speed USB allows with 8-byte packets. The permutation is not what stops it -- raw d mode, with no Ascon at all, reaches 3800 by the same path. What is left is the shape of the sending: the sketch fills a 39-byte burst, sends it in five USB frames and then goes quiet, so the pipe idles between bursts. Keeping it full would want the output topped up continuously rather than in bursts, which is a larger change than the remaining 2 kB/s is worth. S also samples the ADC in twos rather than sixteens, since a conversion is about 100 us and sampling in sixteens left the pipe idle half the time (3900 -> 5650 bytes/s); X's sampling cadence is untouched.

r and d are the un-conditioned sources, for the assessment below. They will fail PractRand, and should: a source carrying about one bit of entropy per byte is not meant to look uniform, only to be unpredictable. Those are different properties and only the second one matters here.

Read the binary modes with the tty in raw mode

A tty starts cooked, with echo on, and a serial port's echo goes back to the device. In the binary modes X, S, R and D that means the host echoes random bytes back at the sketch, and about one byte in 256 is a mode character -- so the mode changes by itself, and a stream turns into a trickle of something else. The hex modes (x, s, r, d) are immune, hex digits being no command, and the default s is one of them, which is why this went unnoticed for so long.

stty -F /dev/ttyACM0 raw -echo

tools/rngread.c is unaffected (it never opens the tty) and so is anything that sets raw mode itself. A cat or a plain open() is not. (Found by the rngtacho session, on a device that had quietly switched itself to r.)

Status LED

An LED on PB1 (the onboard one on most Digisparks) reports what the RNG is doing, so a standalone device with no serial monitor still tells you it is alive and honest:

LED meaning
solid on starting up -- sources not yet validated, no output yet
brief blink, once a second generating normally, both sources passing
fast blink, 5 times a second a source has failed its health tests

The fast blink takes precedence: it means at least one source is dead even if the other is still producing output. Single-bit writes drive the pin, so it cannot disturb V-USB on the USB build. LED_STATUS=0 removes it; LED_PIN moves it (some clones wire the LED to PB0). It is why the UART's TX is on PB2 rather than PB1.

Transports

Three ways the bytes leave the chip, chosen at build time:

  • USB CDC serial (default): a serial port, read with any terminal or with stump.py.

  • USB vendor requests (RNG_VENDOR=1): control requests on endpoint 0, read with tools/rngread.c and libusb. The serial port stays enumerated and completely unused, so another function of the same sketch can have it -- this chip's USB driver has no endpoint to spare for a second one. Needs USB_CFG_VENDOR_HOOK set to 1 in DigiCDCFast's src/usbconfig.h.

      EXTRA_FLAGS="-DRNG_VENDOR=1 -DUSB_CFG_VENDOR_HOOK=1" tools/build.sh
      cc -O2 -o tools/rngread tools/rngread.c -lusb-1.0
      tools/rngread --info
      tools/rngread | pv > /dev/null
    

    rngread needs permission on the USB device: install tools/60-digispark-rng.rules in /etc/udev/rules.d/, or run it as root. It learns how long a burst takes and sleeps through most of it rather than polling hard, because each request steals cycles from the timing the entropy comes from.

    In vendor builds the diagnostics that x mode prints as # comments become requests instead (rngread --info): protocol, mode, the Ascon self-test, whether each source still passes its health tests, bursts dropped because nobody read them, and whether S mode has been seeded.

A plain UART, with RNG_UART=1: 8N1 on PB2 (TX) and PB0 (RX), no USB. It reads on any terminal through a USB-serial cable, or feeds another microcontroller's UART directly -- the RNG as a serial entropy peripheral, with no host that has to speak CDC. The receiver is the ATtiny85's USI, so a byte costs two short interrupts rather than a busy-wait, and Ctrl-C over RX reaches the bootloader. Three things to know:

  • Timing. The 16.5 MHz clock is held precise, in the USB builds, by tuning against the USB frame; a UART build has no such reference and rides the internal RC oscillator, set once at boot and then drifting with temperature. The default is 115200 (so a send blocks for only ~87 us -- see below); it works here at room temperature but sits nearer the oscillator's margin than a slower rate, so drop RNG_UART_BAUD (9600, ...) if a board runs hot or cold.
  • Half duplex. A byte is transmitted with interrupts off so the watchdog cannot stretch it into a framing error, which means the port is deaf while a burst goes out. Typed commands land in the gaps between bursts -- fine for the slower modes, but switching out of s/S (which stream almost continuously) may take a few tries. A received byte briefly borrows Timer0, the fine-timestamp clock, to run the USI: it disturbs at most the one interval that spans the keystroke, and the next sample is clean -- the act of talking to the RNG nudging the jitter it measures, but only while you type.
  • Simulator. The USI receiver is clocked by a Timer0 compare match, which Wokwi's avr8js does not implement, so reception -- and mode switching over the UART -- does not work in the simulator, though the transmit stream and the LED do. See wokwi/. On real hardware the receiver works.

The USB transports both measure the same 285 bytes/s; the UART's throughput is set by its baud (11.5 kB/s at 115200), well above the entropy rate.

Assessing the entropy

tools/assess.sh captures raw samples and runs NIST SP 800-90B's non-IID estimator over them. What the sketch credits is deliberately below what the estimator returns:

source estimated samples credited
watchdog intervals 6.71 bits/sample 180,291 1
ADC readings 1.10 bits/sample 1,244,295 1

The estimate moves from run to run: the interval source has measured 6.44, 6.55, 6.71, 6.76 and 6.82 across an hour-long run each, on this firmware and on rngtacho's two-port variant of it. That spread is why the health test cutoffs below are computed from the lowest of them rather than the latest, and why the credit is 1.

The intervals are cut hardest because part of their jitter is USB polling latency, which the host controls rather than physics.

SP 800-90B's continuous health tests (repetition count, adaptive proportion) run on every sample with cutoffs for alpha = 2^-40; a source that fails stops being credited, and rngread --info says so. A higher assessed entropy gives tighter cutoffs, so taking them from the lowest estimate seen keeps false alarms at or under alpha in every condition measured; taking them from the latest run would be chasing the spread.

Not yet done: the restart test (SP 800-90B section 3.1.4) -- the first samples after each of many power-ups, checked for column-wise predictability. It is the one test that catches a source which looks fine within a run but starts from the same place every boot, and within-run testing cannot see that at all.

License and credits

GPL version 2, in LICENSE. The firmware links V-USB through DigiCDCFast, and V-USB is distributed by Objective Development under the GPL version 2 or version 3 (with a commercial license offered separately), so the combined work is covered by the GPL.

clockdrift_rng/ascon_permute.S is not mine: it is the AVR assembler permutation from ascon/ascon-c (crypto_hash/asconxof128/avr_lowsize, commit 446347f) by L. Cardoso and J. Großschädl of the University of Luxembourg, under CC0-1.0, with only the function name changed. Its header says so and should stay.

Ascon is the NIST lightweight cryptography standard (SP 800-232). The entropy assessment uses NIST's SP 800-90B ea_non_iid, built by tools/build-sp800-90b.sh. tools/rngread.c links libusb (LGPL-2.1+).

There is no circuit diagram because there is no circuit: the entropy comes from the watchdog oscillator's drift against the CPU clock and from the ADC's own amplifier noise, with nothing attached to the board.

About

Turns your Digispark into an USB-pluggable Random Number Generator

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages