Harden the Linux entropy source - #267
Conversation
On Linux, entropy() called getrandom() once and fell back to /dev/urandom whenever that call did not return all requested bytes, including when a signal interrupted it (EINTR) while it was still waiting for the kernel's random pool to be initialized. /dev/urandom does not wait for that initialization, so the fallback could read from a pool that is not yet seeded. - Call getrandom() in a loop that retries on EINTR and continues after short reads. - Before reading /dev/urandom, wait until the pool is initialized by polling /dev/random for readability, as libsodium does. - Apply the existing all-zero check to the getrandom() and CCRandomGenerateBytes() results as well. - Remove the SYS_getentropy branch; Linux has no such system call.
|
Hi Markus,
I'm fine with the changes introduced in this PR. And therefore I merged it, after having inspected the changes in detail.
For the typical use case of SQLite3MC the latter is unlikely to impose a real problem. However, several years ago we struggled with the random number generation on a virtual machine for similar reasons. So, it is really good to improve the code.
I'm considering moving the code for generating random data into a separate source file. That should make future maintenance easier. |
Hi Ulrich,
this is one of the follow-ups I mentioned in #262. It changes
entropy()insrc/chacha20poly1305.c, which you reworked recently, so please tell me if you'd prefer a different approach.Type: Bug fix
Background
We plan to use SQLite3 Multiple Ciphers in projects that may have to pass a security audit, for example in critical infrastructure or for public authorities. Whether or not that happens, we'd like the random number generation to be done right, because salts, IVs and nonces are only as good as the random source behind them. For Linux, the BSI technical guideline BSI TR-02102-1 (section 8.5.1, remark 8.2) specifically warns that
/dev/urandomcan return data before the kernel's random pool is initialized.The problem
On Linux,
entropy()callsgetrandom()once. If that call doesn't return all requested bytes, it falls back to/dev/urandom. This also happensgetrandom()(EINTR) while it is still waiting for the random pool, which can happen shortly after boot, orgetrandom()returns fewer bytes than requested.In exactly these cases,
/dev/urandommay be read from a pool that isn't seeded yet. TheRNDGETENTCNTcheck inread_urandom()confirms that the device is a real random device, but not that the pool is initialized.The changes (only
src/chacha20poly1305.c)The changes follow what established crypto libraries do:
getrandom()is called in a loop (read_getrandom()) that retries afterEINTRand continues after short reads, like libsodium and BoringSSL./dev/urandomis only used ifgetrandom()fails for another reason, for exampleENOSYSon kernels older than 3.17 orEPERMunder a seccomp filter. Before reading from it,read_urandom()waits until/dev/randomis readable, which means that the pool is initialized. libsodium and the Rustgetrandomcrate do the same, and OpenSSL does it on older kernels. If/dev/randomcan't be opened, it reads/dev/urandomas before, like libsodium.read_urandom()already does now also applies to the results ofgetrandom()andCCRandomGenerateBytes()(new helperis_all_zero()).SYS_getentropybranch. Linux has no such system call, so it was never compiled.In the normal case, nothing changes: it's still a single
getrandom()call. Apple, Windows, WebAssembly and the other Unix systems keep their random sources, and the file format is not affected.How serious is it? Low in practice. It only matters shortly after boot, mainly on embedded devices and virtual machines, and only if a signal interrupts the waiting call or
getrandom()is unavailable. But it's exactly the kind of detail an audit of the random number generation looks at. With this change,entropy()handles these cases the way libsodium and OpenSSL do.How I tested it
Failure injection: a small test program includes
src/chacha20poly1305.cand interceptsgetrandom(),open()andpoll(). It injects failures and records the order in which/dev/randomand/dev/urandomare used. The results are the same on Linux aarch64 and x86_64 (glibc, GCC 13 and Clang 18), Alpine Linux (musl) and Android (NDK r28, bionic):getrandom()behaviourgetrandom()onlygetrandom()onlyEINTR/dev/urandomwithout waiting/dev/urandomwithout waitingENOSYSorEPERM/dev/urandomwithout waiting/dev/random, then reads/dev/urandomENOSYS, and/dev/randomcan't be opened/dev/urandom/dev/urandom, as beforeOther checks:
autoreconf,./configure,make, the SQL tests andcryptotest) passes on Linux aarch64 and x86_64 with GCC and Clang.cryptotestpasses. macOS uses the same random source as before.-Wsign-comparewarning of the oldgetrandom()line is gone.hwaccel.Best regards,
Markus