# rorand Ring oscillator-based TRNG for the RP2040 ## What is it `rorand` provides a true random number generator (TRNG) using only hardware that's available inside the [RP2040](https://www.raspberrypi.com/documentation/microcontrollers/rp2040.html) chip. It can be used either in its 'raw' form, or with an extra whitening/compression step based on [Ascon](https://ascon.iaik.tugraz.at/)-Xofa. ## Comparison | Name | Throughput | Entropy assurance | Extra components | Whitening | URL | |:--------------------- |:------------------------ |:------------------------------ |:---------------- |:---------- |:------------- | | rorand (this work) | 40 kbit/s | passes Dieharder (see below) | none | Ascon-Xofa | you are here! | | Adafruit Trinkey TRNG | (40 kbit/s comms. limit) | Common Criteria EAL6+ | Infineon Trust M | ? | https://learn.adafruit.com/trinkey-qt2040-true-random-number-generator/overview | | caprand | ? | ? | one 10nF capacitor | ChaCha20 | https://github.com/mkj/caprand | | rp2040-entropy | 300 kbit/s? | fails a few dieharder tests | none (ADC noise) | none | https://github.com/hashky/rp2040-entropy | | rprand | ~10 kbit/s | ? | none (ROSC+DMA) | CRC32 | https://github.com/alastairpatrick/rprand | | pico-sdk | ? (probably >1Mbit/s) | ? (seed only, RNG is Xorshiro) | none (ROSC seed) | Xorshiro128 | https://github.com/raspberrypi/pico-sdk/blob/master/src/rp2_common/pico_rand/rand.c | | Cornell ECE4760 | ? | convergence to normal distribution | none (ROSC seed) | newlib/RAND48 | https://people.ece.cornell.edu/land/courses/ece4760/RP2040/C_SDK_random/index_random.html | ## Requirements * Pico C SDK * System must not run from ROSC, and the system clock must be at least 48 MHz (80 MHz or higher preferred, 125 MHz or higher ideal). * SysTick not used by something else ## How it works The RP2040 includes a [ring oscillator](https://en.wikipedia.org/wiki/Ring_oscillator) (unimaginatively called ROSC). Such an oscillator is nothing more than a chain of inverters wired up in a loop. If the number of inverters is odd, the circuit will oscillate. Ring oscillators (just like many other oscillators) exhibit a phenomenon called *jitter*: the length of the clock period varies ever so slightly from one period to the next. This stems from various forms of noise (thermal noise, shot noise, etc.) in the semiconductor, which are ultimately caused by the inherent randomness of quantum physics. The RP2040 ROSC has a register called "[RANDOMBIT](https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf#reg-rosc-RANDOMBIT)", which is basically a way of sampling the current state of the oscillator. On first sight, this *sounds* like it would be random. However, much of the perceived 'randomness' would actually stem from [aliasing](https://en.wikipedia.org/wiki/Aliasing), because the code reading this register would be undersampling. Instead, let's use the jitter phenomenon slightly differently: the phase (jitter) noise from multiple oscillator cycles accumulates. Thus, the actual real-world time it takes to complete `N` clock cycles has a larger variance than the jitter of just one clock cycle. This is visualized in the image below: ![A diagram of multiple jittery squarewaves overlaid. They all start at the same time, but their ending time varies a lot (while the first few edges all happen at mostly the same moment). Image from slides by Adriaan Petermans.](img/jitter.png) By using the [COUNT](https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf#reg-rosc-COUNT) register of the RP2040 ROSC, we can time how long it takes for it to reach zero (compared to eg. the ARM SysTick timer running from the CPU clock). We can use the least-significant bit of this timing difference (counted in CPU cycles) as the randomness output! (As a sidenote: 'real professional' ring oscillator-based TRNGs typically use a "transient effect ring oscillator" (TERO) design, which is a bit more robust. Though, it also needs multiple oscillators and counters, and we don't have that available here. So, the basic RO TRNG it is.) Well, almost. We still need to do a crucial final step: [entropy extraction](https://en.wikipedia.org/wiki/Randomness_extractor). There are two types of entropy extractors used most often: the parity filter (which XORs the last N output bits together), and the Von Neumann filter (described in the link). The Von Neumann filter is able to remove all [statistical bias](https://en.wikipedia.org/wiki/Bias_(statistics)), but has more requirements on the probability distribution of the inputs (they must be "Independent and identically distributed", or IID for short). Meanwhile, the parity filter has fewer requirements (only independence of inputs), but it cannot remove all bias in practice (though it converges to this for larger and larger N). For a ring oscillator, one would expect the random bits (generated as described above) to be IID, so we can use the Von Neumann extractor. (This assumption seems to hold in practice.) Oh, and we can still XOR this bit with the RANDOMBIT register, because it's there. This is essentially how the "raw" output is generated. `rorand` also provides a "cooked" output, which feeds the output of the "raw" TRNG into Ascon-Xofa. This creature is a cryptographic PRNG based on what cryptographers call a "[sponge construction](https://en.wikipedia.org/wiki/Sponge_function)". Sponge constructions have all sorts of cool properties (which I won't talk about here because it'd detract from the article, sadly), but one of them is that you can build an *extensible-output function* (XOF) from them. XOFs are like hash functions, but the output length can be made arbitrarily long. This makes it rather easy to make a cryptographically-secure *reseedable* PRNG. Ascon is the finalist for the "lightweight portfolio" of the [CAESAR competition](https://competitions.cr.yp.to/caesar-submissions.html). It's a rather new (but promising, and built from solid foundations) cryptographic cipher, specifically designed for slow embedded devices, such as the RP2040. It should thus be faster than other options like ChaCha20/BLAKE2 or ARC4Random (which is now considered insecure). AEGIS and Deoxys-II, the other two CAESAR finalists for other purposes, are based on the AES round transformation, and would thus be quite a bit slower. ## How good is it ### Entropy The randomness obtained from this TRNG seems to pass the [Dieharder](https://webhome.phy.duke.edu/~rgb/General/dieharder.php) test suite. The output is given below. Note that a few entries are marked as `WEAK`, but those seem to appear and disappear when I try and retry different runs. This seems to happen due to the used sample size: one megabyte of random data takes a few minutes to generate, so much larger samples are quite a bit of a hassle to obtain. Running output from `/dev/random` with the same sizes also has the `WEAK` warnings pop up and disappear at random, so it should be fine. **WARNING**: However, these `WEAK` warnings seem to pop up more often with decreasing RP2040 system clock speed (and, slightly, voltage as well). While no `FAILED` errors have popped up, depending on your needs, only use `rorand` with a high enough system clock frequency (≥80 MHz preferred, ≥125 MHz ideal). Dieharder isn't the most stringent test suite, though. [TestU01](http://simul.iro.umontreal.ca/testu01/tu01.html)'s BigCrush suite is much more comprehensive, but requires ***much*** more data (many gigabytes). Thanks to the limited throughput of a hardware TRNG, using this test isn't very feasible. Real evaluations such as FIPS 140-2, or Common Criteria EAL4 or higher, is completely out of my means. In conclusion, don't trust someone's life --- neither your own nor someone else's --- with this. But otherwise, you should be fine. If you need the assurance (or if you're paranoid), please use a FIPS- or Common Criteria-certified hardware module, though. Dieharder output (raw output, RP2040 running at 250 MHz, core voltage 1.15 V): ``` #=============================================================================# test_name |ntup| tsamples |psamples| p-value |Assessment #=============================================================================# diehard_birthdays| 0| 100| 100|0.22538373| PASSED diehard_operm5| 0| 1000000| 100|0.63794545| PASSED diehard_rank_32x32| 0| 40000| 100|0.73090642| PASSED diehard_rank_6x8| 0| 100000| 100|0.99254745| PASSED diehard_bitstream| 0| 2097152| 100|0.58114397| PASSED diehard_opso| 0| 2097152| 100|0.18319256| PASSED diehard_oqso| 0| 2097152| 100|0.93914840| PASSED diehard_dna| 0| 2097152| 100|0.74871515| PASSED diehard_count_1s_str| 0| 256000| 100|0.20413208| PASSED diehard_count_1s_byt| 0| 256000| 100|0.56507750| PASSED diehard_parking_lot| 0| 12000| 100|0.43004782| PASSED diehard_2dsphere| 2| 8000| 100|0.68150341| PASSED diehard_3dsphere| 3| 4000| 100|0.50961304| PASSED diehard_squeeze| 0| 100000| 100|0.73895828| PASSED diehard_sums| 0| 100| 100|0.89067279| PASSED diehard_runs| 0| 100000| 100|0.88549264| PASSED diehard_runs| 0| 100000| 100|0.17826705| PASSED diehard_craps| 0| 200000| 100|0.74363795| PASSED diehard_craps| 0| 200000| 100|0.92809716| PASSED marsaglia_tsang_gcd| 0| 10000000| 100|0.21033260| PASSED marsaglia_tsang_gcd| 0| 10000000| 100|0.90826850| PASSED sts_monobit| 1| 100000| 100|0.66704052| PASSED sts_runs| 2| 100000| 100|0.82376608| PASSED sts_serial| 1| 100000| 100|0.40789077| PASSED sts_serial| 2| 100000| 100|0.11533197| PASSED sts_serial| 3| 100000| 100|0.57906405| PASSED sts_serial| 3| 100000| 100|0.88247272| PASSED sts_serial| 4| 100000| 100|0.35109410| PASSED sts_serial| 4| 100000| 100|0.06001064| PASSED sts_serial| 5| 100000| 100|0.24597035| PASSED sts_serial| 5| 100000| 100|0.42776664| PASSED sts_serial| 6| 100000| 100|0.34671248| PASSED sts_serial| 6| 100000| 100|0.63443378| PASSED sts_serial| 7| 100000| 100|0.34901523| PASSED sts_serial| 7| 100000| 100|0.87482945| PASSED sts_serial| 8| 100000| 100|0.83333396| PASSED sts_serial| 8| 100000| 100|0.98096154| PASSED sts_serial| 9| 100000| 100|0.18613150| PASSED sts_serial| 9| 100000| 100|0.98222632| PASSED sts_serial| 10| 100000| 100|0.06064720| PASSED sts_serial| 10| 100000| 100|0.53659409| PASSED sts_serial| 11| 100000| 100|0.48783888| PASSED sts_serial| 11| 100000| 100|0.44013196| PASSED sts_serial| 12| 100000| 100|0.89056741| PASSED sts_serial| 12| 100000| 100|0.55084354| PASSED sts_serial| 13| 100000| 100|0.55991536| PASSED sts_serial| 13| 100000| 100|0.79919790| PASSED sts_serial| 14| 100000| 100|0.99559967| WEAK sts_serial| 14| 100000| 100|0.60230091| PASSED sts_serial| 15| 100000| 100|0.91600221| PASSED sts_serial| 15| 100000| 100|0.29385991| PASSED sts_serial| 16| 100000| 100|0.80170010| PASSED sts_serial| 16| 100000| 100|0.89169608| PASSED rgb_bitdist| 1| 100000| 100|0.25864662| PASSED rgb_bitdist| 2| 100000| 100|0.41244241| PASSED rgb_bitdist| 3| 100000| 100|0.80837541| PASSED rgb_bitdist| 4| 100000| 100|0.93133416| PASSED rgb_bitdist| 5| 100000| 100|0.70903269| PASSED rgb_bitdist| 6| 100000| 100|0.29717638| PASSED rgb_bitdist| 7| 100000| 100|0.71361547| PASSED rgb_bitdist| 8| 100000| 100|0.03450956| PASSED rgb_bitdist| 9| 100000| 100|0.14545557| PASSED rgb_bitdist| 10| 100000| 100|0.90299368| PASSED rgb_bitdist| 11| 100000| 100|0.53321027| PASSED rgb_bitdist| 12| 100000| 100|0.65243210| PASSED rgb_minimum_distance| 2| 10000| 1000|0.29295560| PASSED rgb_minimum_distance| 3| 10000| 1000|0.28825100| PASSED rgb_minimum_distance| 4| 10000| 1000|0.84249889| PASSED rgb_minimum_distance| 5| 10000| 1000|0.53807064| PASSED rgb_permutations| 2| 100000| 100|0.72182964| PASSED rgb_permutations| 3| 100000| 100|0.98132372| PASSED rgb_permutations| 4| 100000| 100|0.32426721| PASSED rgb_permutations| 5| 100000| 100|0.94033412| PASSED rgb_lagged_sum| 0| 1000000| 100|0.78964883| PASSED rgb_lagged_sum| 1| 1000000| 100|0.82881744| PASSED rgb_lagged_sum| 2| 1000000| 100|0.90805811| PASSED rgb_lagged_sum| 3| 1000000| 100|0.21428921| PASSED rgb_lagged_sum| 4| 1000000| 100|0.67964064| PASSED rgb_lagged_sum| 5| 1000000| 100|0.64463634| PASSED rgb_lagged_sum| 6| 1000000| 100|0.94268050| PASSED rgb_lagged_sum| 7| 1000000| 100|0.33288741| PASSED rgb_lagged_sum| 8| 1000000| 100|0.79565468| PASSED rgb_lagged_sum| 9| 1000000| 100|0.26291492| PASSED rgb_lagged_sum| 10| 1000000| 100|0.23887951| PASSED rgb_lagged_sum| 11| 1000000| 100|0.75511918| PASSED rgb_lagged_sum| 12| 1000000| 100|0.44806331| PASSED rgb_lagged_sum| 13| 1000000| 100|0.57950849| PASSED rgb_lagged_sum| 14| 1000000| 100|0.37109436| PASSED rgb_lagged_sum| 15| 1000000| 100|0.89758378| PASSED rgb_lagged_sum| 16| 1000000| 100|0.06000037| PASSED rgb_lagged_sum| 17| 1000000| 100|0.87051454| PASSED rgb_lagged_sum| 18| 1000000| 100|0.47509380| PASSED rgb_lagged_sum| 19| 1000000| 100|0.34523765| PASSED rgb_lagged_sum| 20| 1000000| 100|0.98418490| PASSED rgb_lagged_sum| 21| 1000000| 100|0.17652983| PASSED rgb_lagged_sum| 22| 1000000| 100|0.17374595| PASSED rgb_lagged_sum| 23| 1000000| 100|0.63170066| PASSED rgb_lagged_sum| 24| 1000000| 100|0.28953394| PASSED rgb_lagged_sum| 25| 1000000| 100|0.64849257| PASSED rgb_lagged_sum| 26| 1000000| 100|0.78468683| PASSED rgb_lagged_sum| 27| 1000000| 100|0.36484261| PASSED rgb_lagged_sum| 28| 1000000| 100|0.66712700| PASSED rgb_lagged_sum| 29| 1000000| 100|0.48604229| PASSED rgb_lagged_sum| 30| 1000000| 100|0.17591412| PASSED rgb_lagged_sum| 31| 1000000| 100|0.32433586| PASSED rgb_lagged_sum| 32| 1000000| 100|0.12971163| PASSED rgb_kstest_test| 0| 10000| 1000|0.47803450| PASSED dab_bytedistrib| 0| 51200000| 1|0.29203091| PASSED dab_dct| 256| 50000| 1|0.06993184| PASSED Preparing to run test 207. ntuple = 0 dab_filltree| 32| 15000000| 1|0.38387488| PASSED dab_filltree| 32| 15000000| 1|0.55295440| PASSED Preparing to run test 208. ntuple = 0 dab_filltree2| 0| 5000000| 1|0.24922510| PASSED dab_filltree2| 1| 5000000| 1|0.98227801| PASSED Preparing to run test 209. ntuple = 0 dab_monobit2| 12| 65000000| 1|0.71153312| PASSED ``` At 48 MHz and a core voltage of 0.95 V, there would be three or four `WARN`ings. ### Throughput The TRNG is rather CPU-intensive (due to its IRQ-blocking busyloop) and not very fast. The "cooked" (whitened) throughput is about 10 times higher than the "raw" throughput. | System clock | Raw TRNG throughput | |:------------ |:------------------- | | 48 MHz | 10 kbit/s | | 125 MHz | 40 kbit/s | | 250 MHz | 50 kbit/s | ## What is missing? * **Online health tests**: I don't know how to do this. I don't know what a "chi-squared" is. Health tests are rather important for TRNGs (to make sure nothing weird is happening), so this is not a small downside. * **DMA+PIO-based implementation**: to relieve the CPU from some of its load. Currently, it's 100% a spinloop on the CPU, with interrupts disabled. * **Multicore mutex**: currently, both CPU cores could run this code, which would then modify the ROSC `COUNT` register in parallell. This is bad and will lead to bad results. Don't do this. (Or alternatively, add mutex guards, or bug me to do so.) * **More useful `rand()` functions**: currently, the code just gives you bits, and turning these bits into a random integer in a range, or a random float, ..., is left as an exercise for the reader. * **Protections against physical attacks**: if an adversary is in the physical proximity of the device, it's game over. They could attach an SWD debugger to the RP2040 (it has no protection against this), use a power side channel or EM emanations to look at the ROSC doing its oscillate-y thing, or tamper with the environment (e.g. freeze spray or harmonic EM emissions) to slow down the ROSC or make it lock onto a different frequency. I don't plan to add any defenses against such attacks. (Please get a properly certified hardware module if such attacks are part of your threat model, I'm begging you, don't use this.) ## Usage Here's a quick example: ```c #include #include #include "rorand.h" /* raw rng */ #include "rourand.h" /* 'cooked'/whitened rng */ static void health_alarm(const char* msg) { // TRNG health alarm raised! iprintf("TRNG health alarm! %s\n", msg); } int main() { /** raw randomness **/ enum rourand_error r = rorand_init(health_alarm); if (r != rr_ok) { panic("rorand failed to initialize!"); } // get 128 bits of raw randomness uint8_t data[16]; rorand_get(data, sizeof(data)*CHAR_BIT); // specified in bits /** 'cooked'/whitened randomness **/ int rate = 0; // rate at which the CSPRNG will reseed with true random data. 0 is default. // you (the user) must call rorand_init first before calling rourand_init! struct rourand_state* ur = rourand_init(rorand_get, rate); if (!ur) { panic("rourand failed to initialize!"); } rourand_get(ur, data, sizeof(data)); // specified in bytes rourand_free(ur); } ``` ## License License is TBD. The [Ascon reference implementation](https://github.com/ascon/ascon-c) is licensed under CC0 1.0. The [Raspberry Pico C SDK](https://github.com/raspberrypi/pico-sdk) is licensed under the BSD-3-Clause license.