.@ Tony Finch – blog


A couple of notable things have happened in recent months:

UUID v4 and v7 are great examples of the need for high performance secure random numbers: you don’t want the performance of your database inserts to be limited by your random number generator! Another example is DNS source port and query ID randomization which help protect DNS resolvers against forged answers.

I was inspired to play with getentropy() by a blog post about getting a few secure random bytes in PostgreSQL without pgcrypto: it struck me that PostgreSQL doesn’t use getentropy(), and I thought it might be fun (and possibly even useful!) to add support for it.

I learned a few things along the way!

what is getentropy()?

A cryptographically secure pseudorandom number generator basically generates bulk random bytes using a stream cipher that is keyed and periodically re-keyed using some source of high-quality randomness. In NIST standards a CSPRNG is often referred to as a DRBG, deterministic random bit generator.

In the kernel, the high-quality randomness comes from things like the unpredictable timing of interrupts, hardware random number generators, or maybe an underlying hypervisor. The word “entropy” has often been used to refer to this distilled essence of randomness. Random bytes are made available to userland via interfaces such as /dev/urandom or getentropy().

A userland CSPRNG such as OpenSSL RAND(7) gets its the high-quality randomness from these kernel interfaces. A notable feature of getentropy() is that it will not produce an arbitrarily large number of bytes: it can provide just enough to securely key a userland CSPRNG.

portability of getentropy()

OpenBSD introduced getentropy() in 2014; it was added to Mac OS X in 2016, glibc in 2017, musl and FreeBSD in 2018, NetBSD and POSIX in 2024.

It’s ubiquitous enough now that my code assumes that getentropy() exists without worrying.

There are a couple of issues that you are likely to encounter:

advantages of getentropy()

There are some annoying issues with /dev/random

Cryptographic algorithms often need nonces that absolutely must never be repeated, otherwise the private key is leaked. So a CSPRNG must also avoid repeating output, which can be difficult when a process fork()s.

While writing this blog post, I discussed this fork() issue with Rich Salz (who re-wrote OpenSSL’s RAND to use a NIST FIPS DRBG algorithm). He said RAND_bytes() uses getpid() to detect when the CSPRNG must be re-keyed because the process fork()ed. (Another way not used by OpenSSL is pthread_atfork().)

The kernel has to deal with the similar issue of ensuring its CSPRNG is re-keyed when a VM is cloned. However there isn’t a way for a userland process to find out its VM has been cloned.

Unlike a stateful userland CSPRNG, if you call getentropy() directly, you don’t have to worry about repeated output due to fork() or VM clones. You also don’t have to worry about linking with a cryptographic library.

disadvantages of getentropy()

In princple, once a userland CSPRNG has been keyed it should be able to produce random bytes very fast. getentropy() is designed to do the keying; it is annoying to use for bulk random bytes because it needs a loop to get 256 bytes at a time.

So one might expect getentropy() to be slower than RAND_bytes().

Is that actually the case? Let’s measure it!

performance of getentropy() vs RAND_bytes()

I wrote a simple benchmark to measure the performance of getentropy() and RAND_bytes() at a few different buffer sizes. It prints a table of nanoseconds per function call. It can be built with different versions of OpenSSL on a few different systems.

The results are more complicated than I expected!

The behaviour of getentropy() on Mac OS is curious. The first time I run bentropy after a pause, getentropy() takes about 1µs. If I run it in quick succession, like bentropy && bentropy && bentropy, then getentropy() speeds up to about 0.5µs - which is what you can see in the table above. This speed-up also affects the OpenSSL timings.

OpenSSL 3.3 is substantially faster than 1.1. The OpenSSL timings are dominated by their startup latency, and not much affected by the buffer size for these relatively small lengths.

The time of one call to getentropy() is not much affected by the buffer size, but large buffers require multiple calls, so the time for 1024 bytes is about 4x the time for 256 bytes.

BoringSSL is from Debian’s android-libboringssl-dev package. I am mainly using it as a representative of more recent versions of OpenSSL to show that RAND_bytes() is a lot faster than it used to be.

It’s weird that getentropy()’s time varies with the buffer size so much. Dunno what’s up there!

This mainly shows the performance numbers for OpenSSL 3.0 on FreeBSD are similar-ish to OpenSSL 1.1.1w on Linux.

conclusions

Maybe I should upgrade my Debian box to a newer kernel so I can try it out!