| # Entropy quality tests |
| |
| This document describes how we test the quality of the entropy sources used to |
| seed the Zircon CPRNG. |
| |
| [TOC] |
| |
| ## Theoretical concerns |
| |
| Approximately speaking, it's sometimes easy to tell that a stream of numbers is |
| not random by recognizing a pattern in it. It's impossible to be sure that the |
| numbers are truly random. The state of the art seems to be running several |
| statistical tests on the data, and hoping to detect any exploitable weaknesses. |
| |
| The problem of testing for randomness gets more difficult when the random |
| numbers aren't perfectly random (when their distributions aren't uniform, or |
| when there are some limited correlations between numbers in the sequence). A |
| stream of non-perfect random numbers still contains some randomness, but it's |
| hard to determine how random it is. |
| |
| For our purposes, a good measure of how much randomness is contained in a stream |
| of non-perfectly random numbers is the min-entropy. This is related to the |
| Shannon entropy used in information theory, but is always takes a smaller value. |
| The min-entropy controls how much randomness we can reliably extract from the |
| entropy source; see, for example |
| <https://en.wikipedia.org/wiki/Randomness_extractor#Formal_definition_of_extractors> |
| |
| From a practical standpoint, we can use the test suite described in US NIST |
| SP800-90B to analyze samples of random from an entropy source. A prototype |
| implementation for the tests is available from |
| <https://github.com/usnistgov/SP800-90B_EntropyAssessment>. The suite takes a |
| sample data file (say, 1MB of random bytes) as input. The nice thing about this |
| test suite is that it can handle non-perfect RNGs, and it reports an estimate |
| for how much min-entropy is contained in each byte of the random data stream. |
| |
| ### The importance of testing unprocessed data |
| |
| After drawing entropy from our entropy sources, we will mix it into the CPRNG in |
| a "safe" way that basically gets rid of detectable correlations and |
| distributional imperfections in the raw random byte stream from the entropy |
| source. This is a very important thing to do when actually generating random |
| numbers to use, but we must avoid this mixing and processing phase when testing |
| the entropy source itself. |
| |
| For a stark example of why it's important to test unprocessed data if we want to |
| test our actual entropy sources, here's an experiment. It should run on any |
| modern linux system with OpenSSL installed. |
| |
| head -c 1000000 /dev/zero >zero.bin |
| openssl enc -aes-256-ctr -in zero.bin -out random.bin -nosalt -k "password" |
| |
| This takes one million bytes from /dev/zero, encrypts them via AES-256, with a |
| weak password and no salt (a terrible crypto scheme, of course!). The fact that |
| the output looks like good random data is a sign that AES is working as |
| intended, but this demonstrates the risk of estimating entropy content from |
| processed data: together, /dev/zero and "password" provide ~0 bits of entropy, |
| but our tests are way more optimistic about the resulting data! |
| |
| For a more concrete Zircon-related example, consider jitterentropy (the RNG |
| discussed here: <http://www.chronox.de/jent/doc/CPU-Jitter-NPTRNG.html>). |
| Jitterentropy draws entropy from variations in CPU timing. The unprocessed data |
| are how long it took to run a certain block of CPU- and memory-intensive code |
| (in nanoseconds). Naturally, these time data are not perfectly random: there's |
| an average value that they center around, with some fluctuations. Each |
| individual data sample might be several bits (e.g. a 64-bit integer) but only |
| contribute 1 bit or less of min-entropy. |
| |
| The full jitterentropy RNG code takes several raw time data samples and |
| processes them into a single random output (by shifting through a LFSR, among |
| other things). If we test the processed output, we're seeing apparent randomness |
| both from the actual timing variations and from the LFSR. We want to focus on |
| just the timing variation, so we should test the raw time samples. Note that |
| jitterentropy's built-in processing can be turned on and off via the |
| `kernel.jitterentropy.raw` cmdline. |
| |
| ## Quality test implementation |
| |
| As mentioned above, the NIST test suite takes a file full of random bytes as |
| input. We collect those bytes on a Zircon system (possibly with a thin Fuchsia |
| layer on top), then usually export them to a more capable workstation to run the |
| test suite. |
| |
| ## Boot-time tests |
| |
| Some of our entropy sources are read during boot, before userspace is started. |
| To test these entropy sources in a realistic environment, we run the tests |
| during boot. The relevant code is in |
| `kernel/lib/crypto/entropy/quality\_test.cpp`, but the basic idea is that the |
| kernel allocates a large static buffer to hold test data during early boot |
| (before the VMM is up, so before it's possible to allocate a VMO). Later on, the |
| data is copied into a VMO, and the VMO is passed to userboot and devmgr, where |
| it's presented as a pseudo-file at `/boot/kernel/debug/entropy.bin`. Userspace |
| apps can read this file and export the data (by copying to persistent storage or |
| using the network, for example). |
| |
| In theory, you should be able to build Zircon with entropy collector testing |
| enabled using `scripts/entropy-test/make-parallel`, and then you should be able |
| to run a single boot-time test with the script |
| `scripts/entropy-test/run-boot-test`. The `run-boot-test` script is mostly |
| intended to be invoked by other scripts, so it's a little bit rough around the |
| edges (for example, most of its arguments are passed via command line options |
| like `-a x86-64`, but many of these "options" are in fact mandatory). |
| |
| Assuming the `run-boot-test` script succeeds, it should produce two files in the |
| output directory: `entropy.000000000.bin` and `entropy.000000000.meta`. The |
| first is the raw data collected from the entropy source, and the second is a |
| simple text file, where each line is a key-value pair. The keys are single words |
| matching `/[a-zA-Z0-9_-]+/`, and the values are separated by whitespace matching |
| `/[ \t]+/`. This file can be pretty easily parsed via `read` in Bash, |
| `str.split()` in Python, or (with the usual caution about buffer overruns) |
| `scanf` in C. |
| |
| In practice, I'm nervous about bit-rot in these scripts, so the next couple |
| sections document what the scripts are supposed to do, to make it easier to run |
| the tests manually or fix the scripts if/when they break. |
| |
| ### Boot-time tests: building |
| |
| Since the boot-time entropy test requires that a large block of memory be |
| permanently reserved (for the temporary, pre-VMM buffer), we don't usually build |
| the entropy test mode into the kernel. The tests are enabled by passing the |
| `ENABLE_ENTROPY_COLLECTOR_TEST` flag at build time, e.g. by adding the line |
| |
| ``` |
| EXTERNAL_DEFINES += ENABLE_ENTROPY_COLLECTOR_TEST=1 |
| ``` |
| |
| to `local.mk`. Currently, there's also a build-time constant, |
| `ENTROPY_COLLECTOR_TEST_MAXLEN`, which (if provided) is the size of the |
| statically allocated buffer. The default value if unspecified is 1MiB. |
| |
| ### Boot-time tests: configuring |
| |
| The boot-time tests are controlled via kernel cmdlines. The relevant cmdlines |
| are `kernel.entropy-test.*`, documented in |
| [kernel\_cmdline.md](kernel_cmdline.md). |
| |
| Some entropy sources, notably jitterentropy, have parameter values that can be |
| tweaked via kernel cmdline. Again, see [kernel\_cmdline.md](kernel_cmdline.md) |
| for further details. |
| |
| ### Boot-time tests: running |
| |
| The boot-time tests will run automatically during boot, as long as the correct |
| kernel cmdlines are passed (if there are problems with the cmdlines, error |
| messages will be printed instead). The tests run just before the first stage of |
| RNG seeding, which happens at LK\_INIT\_LEVEL\_PLATFORM\_EARLY, shortly before |
| the heap the VMM are brought up. If running a large test, boot will often slow |
| down noticeably. For example, collecting 128kB of data from jitterentropy on |
| rpi3 can take around a minute, depending on the parameter values. |
| |
| ## Run-time tests |
| |
| *TODO(SEC-29): discuss actual user-mode test process* |
| |
| *Current rough ideas: only the kernel can trigger hwrng reads. To test, |
| userspace issues a kernel command (e.g. `k hwrng test`), with some arguments to |
| specify the test source and length. The kernel collects random bytes into the |
| existing VMO-backed pseudo-file at `/boot/kernel/debug/entropy.bin`, assuming |
| that this is safely writeable. Currently unimplemented; blocked by lack of a |
| userspace HWRNG driver. Can test the VMO-rewriting mechanism first.* |
| |
| ## Test data export |
| |
| Test data is saved in `/boot/kernel/debug/entropy.bin` in the Zircon system |
| under test. So far I've usually exported the data file manually via `netcp`. |
| Other options include `scp` if you build with the correct Fuchsia packages, or |
| saving to persistent storage. |
| |
| ## Running the NIST test suite |
| |
| *Note: the NIST tests aren't actually mirrored in Fuchsia yet. Today, you need |
| to clone the tests from the repo at |
| <https://github.com/usnistgov/SP800-90B_EntropyAssessment>.* |
| |
| The NIST test suite has three entry points (as of the version committed on Oct. |
| 25, 2016): `iid_main.py`, `noniid_main.py`, and `restart.py`. The two "main" |
| scripts perform the bulk of the work. The `iid_main.py` script is meant for |
| entropy sources that produce independent, identically distributed data samples. |
| Most of the testing is to validate the iid condition. Many entropy sources will |
| not be iid, so the `noniid_main.py` test implements several entropy estimators |
| that don't require iid data. |
| |
| Note that the test binaries from the NIST repo are Python scripts without a |
| shebang line, so you probably need to explicitly call `python` on the command |
| line when invoking them. |
| |
| The first two scripts take two arguments, both mandatory: the data file to read, |
| and the number of significant bits per sample (if less than 8, only the low `N` |
| bits will be used from each byte). They optionally accept a `-v` flag to produce |
| verbose output or `-h` for help. |
| |
| The `noniid_main.py` also optionally accepts a `-u <int>` flag that can reduce |
| the number of bits below the `N` value passed in the second mandatory argument. |
| I'm not entirely sure why this flag is provided; it seems functionally |
| redundant, but passing it does change the verbose output slightly. My best guess |
| is that this is provided because the noniid Markov test only works on samples of |
| at most 6 bits, so 7- or 8-bit datasets will be reduced to their low 6 bits for |
| this test. In contrast, all the iid tests can run on 8-bit samples. |
| |
| A sample invocation of the `iid_main.py` script: |
| |
| ``` |
| python2 -- $FUCHSIA_DIR/third_party/sp800-90b-entropy-assessment/iid_main.py -v /path/to/datafile.bin 8 |
| ``` |
| |
| The `restart.py` script takes the same two arguments, plus a third argument: the |
| min-entropy estimate returned by a previous run of `iid_main.py` or |
| `noniid_main.py`. This document doesn't describe restart tests. For now, see |
| NIST SP800-90B for more details. |
| |
| ## Future directions |
| |
| ### Automation |
| |
| It would be nice to automate the process of building, configuring, and running a |
| quality test. As a first step, it should be easy to write a shell script to |
| perform these steps. Even better would be to use the testing infrastructure to |
| run entropy collector quality tests this automatically, mostly to reduce bit-rot |
| in the test code. Failing automation, we have to rely on humans to periodically |
| run the tests (or to fix the tests when they break). |