Ilya Maximets <[email protected]> writes:
> On 10/12/26 5:11 PM, Aaron Conole wrote:
>> Until now, utilities wishing access to a quality source of
>> cryptographically secure random numbers would have to either
>> forego security and just use the xorshift, or use one of a
>> few very slow entropy options like get_entropy_or_die or
>> OpenSSL's RAND_bytes() calls. Both of these are quality
>> tested mechanisms, but the drawback is a very long time to
>> execute (measured 10m iterations of rand_bytes taking
>> roughly 5.14s to complete).
>>
>> This change adds a ChaCha20 based CSPRNG, which is known
>> to be a high quality CSPRNG. The benefits are very fast
>> execution time (the same 10m iterations only take 0.059s
>> vs. the PRNG taking 0.018s).
>>
>> The implementation heavily relies on referencing the
>> following:
>> * RFC 8439, "ChaCha20 and Poly1305 for IETF Protocols"
>> * libevent/arc4random.c, "Arc4 random number generator for OpenBSD."
>> * glibc/stdlib/arc4random.c, "Psuedo random number generator"
>> * Ben Tasker, "Writing (and backdooring) a ChaCha20 based CSPRNG"
>>
>> Signed-off-by: Aaron Conole <[email protected]>
>> ---
> A different LLM had the following to say about this patch
> (some parts seem reasonable to a human posting this, but not
> fully validated):
>
> ────────────────────────────────────────
> REVIEW: 5b2647092 "random: Implement a ChaCha20 based CSPRNG."
> ────────────────────────────────────────
> commit 5b26470924cfe117d22c7fee5115e2b3e3fdd66c ("random: Implement a
> ChaCha20 based CSPRNG.")
> Author: Aaron Conole <[email protected]>
>
> This patch adds a per-thread ChaCha20-based CSPRNG to lib/random.c and
> exposes it as cs_random_bytes() and cs_random_uint32(). When OpenSSL is
> available it uses EVP instead. It also adds an RFC 8439 block selftest
> and a benchmark to tests/test-random.c.
Well, the HAVE_OPENSSL check isn't in this patch. :-/
>> +void
>> +cs_random_bytes(void *b, size_t l)
>> +{
>> + struct csprng *c = cs_state_get();
> [...]
>> + if (!c->initialized || c->served >= CSPRNG_RESEED) {
>> + csprng_seed(c);
>> + }
>
>> +static void
>> +csprng_seed(struct csprng *c)
>> +{
>> + uint8_t key[CHACHA_KEYLEN];
>> +
>> + get_entropy_or_die(key, sizeof key);
>
> Can this take down the process at runtime? Every CSPRNG_RESEED bytes
> (about 409600 cs_random_uint32() calls per thread), and on first use in
> each new thread, cs_random_bytes() calls csprng_seed(). That calls
> get_entropy_or_die(). In lib/entropy.c, get_entropy() opens
> /dev/urandom on each call, and get_entropy_or_die() does VLOG_FATAL on
> any error.
Yes, it can. We could be more relaxed about it, but basically, would
have to reserve some seed from a previous run. But if /dev/urandom
isn't available for read() then there is a bigger issue.
> The call chain is:
>
> cs_random_uint32()
> cs_random_bytes()
> csprng_seed()
> get_entropy_or_die()
> get_entropy() -> open("/dev/urandom") fails -> VLOG_FATAL
>
> Before this patch, get_entropy_or_die() was only reached from one-time
> initialization. With the follow-up conntrack NAT change, datapath
> threads reach this path while handling packets. A transient EMFILE or
> ENFILE at reseed time would then exit ovs-vswitchd.
>
> Since csprng_refill() already rekeys for forward secrecy, could a
> failed reseed keep the current key and log a warning instead?
>
> ---
>
> The commit message quotes timings for "10m iterations", but the
> benchmark loop runs N_ROUNDS * N_ROUNDS times:
Yes, the benchmark is a different utility. I will update with this
version if that seems more appropriate.
>> + enum { N_ROUNDS = 10000 };
> [...]
>> + for (i = 0; i < N_ROUNDS * N_ROUNDS; i++) {
>> + x += cs_random_uint32();
>> + }
>
> That is 100,000,000 iterations, not 10M. Should the commit message or
> the loop count be updated so the figures match?
I can do that, but above the loop count is for a test-bench file I have,
not the committed benchmark. The test-bench is a bit too synthetic
because it has a bunch of implementations bundled together. That made
some strange performance data that didn't hold up when I ported things
to ovs code base. I can re-run with the ovstest benchmark if needed.
> The header comment in lib/random.c also cites a source that the commit
> message's reference list leaves out:
>
>> +/* This is the 32-bit CSPRNG adapted from the ChaCha20 algorithm from
>> + * arc4random(3) and from the Linux kernel's get_random_u32().
>
> Should the Linux kernel be added to the references in the commit
> message?
Addressed when responding to Ilya's licensing concern.
> There is also an unused include in tests/test-random.c, since only
> ovs_assert() is called:
>
>> #include "random.h"
>> +#include <assert.h>
>
> ---
>
>> +#define CHACHA_K_INPUT 16 /* ChaCha20 inblock size in bytes. */
>
>> +void chacha20_block(const uint32_t input[CHACHA_K_INPUT],
>> + uint8_t out[CHACHA_BLOCK]);
>
> Is the comment on CHACHA_K_INPUT correct? chacha20_block() uses it as
> a count of 16 uint32_t words, which is 64 bytes. A caller who trusts
> the header comment and passes a 16-byte buffer would have it read past
> the end.
Yes, my mistake.
> The same macro sizes a string here:
>
>> +static const uint8_t chacha_sigma[CHACHA_K_INPUT + 1] = "expand 32-byte k";
> That only works because the string happens to be 16 characters long.
>
> The comment in cs_random_bytes() also seems to misdescribe the
> reserved bytes:
>
>> + /* c->buf isn't fully available for reading due to
>> + * the initial 32-bytes from the block being used as a
>> + * seeding counter. In the case of the OpenSSL EVP
>
> csprng_refill() uses those 32 bytes as the next key via
> csprng_rekey(c, c->buf). The counter is in state[12..13]. Should this
> say "next key" instead of "seeding counter"?
This got some weird hallucination. That hunk is weird. Still, I can
make the description a bit cleaner, probably.
> ---
>
>> + ovs_assert(l < sizeof c->buf - CHACHA_KEYLEN);
>
> Is this bound off by one? After csprng_refill(), c->pos is 32. With
> l == 4064, the check c->pos + l > sizeof c->buf is 4096 > 4096, which
> is false, so the memcpy() of buf[32..4095] stays in bounds. The assert
> still aborts.
This looks wrong. l == 4064 won't abort when the ovs_assert runs. And
the csprng_refill() won't abort unless get_entropy_or_die fails.
> Also, the prototype in random.h documents no maximum length:
I can document the length. But if someone is really needing to pull 4k
of csprng data in OVS somewhere, we could be in trouble.
>> +void cs_random_bytes(void *, size_t);
>
> random_bytes() accepts any length. Would it be better to loop over
> refills for larger requests, or at least document the cap?
>
> ---
>
>> + get_entropy_or_die(key, sizeof key);
>> + csprng_rekey(c, key);
>> + memset(key, 0, sizeof key);
>
> key is never read after this memset(), so the compiler is free to
> remove it as a dead store. Is the key actually scrubbed from the stack
> here?
Hrrm.. this is a real problem - we may need something like (untested):
void ovs_memzero(uint8_t *ptr, size_t l)
{
size_t s;
for (s = 0; s < l; s++) {
volatile uint8_t *p = (volatile uint8_t *)(ptr + s);
*p = 0;
}
}
That should force the compiler NOT to remove the assignment to 'p'.
> Similarly, chacha20_block() leaves its working array on the stack:
>
>> + uint32_t x[CHACHA_K_INPUT];
> [...]
>> + ovs_store_le32(out + i * 4, x[i] + input[i]);
>
> Because out = x + input, anyone holding both x[] and the output block
> can recover the key words input[4..11]. Should x[] be cleared before
> returning?
chacha20_block may leave around data. I can erase this.
> On reseed, csprng_seed() sets c->pos = sizeof c->buf but leaves the old
> unserved keystream in c->buf until the next refill. Should that be
> cleared too, given the forward secrecy goal stated in the
> csprng_refill() comment?
csprng_seed() leaving the buffer around isn't an issue. Either the
buffer is still csrandom data, or is uninitialized data.
> ---
>
>> +#define QUARTERROUND(a, b, c, d) \
>> + a += b; d ^= a; d = ROTL32(d, 16); \
>> + c += d; b ^= c; b = ROTL32(b, 12); \
>> + a += b; d ^= a; d = ROTL32(d, 8); \
>> + c += d; b ^= c; b = ROTL32(b, 7)
>
> This is a minor point. The macro expands to several statements with no
> do { } while (0) wrapper. All current uses are inside the braced loop
> in chacha20_block(), but if it were used as the body of an unbraced
> if, only "a += b" would be conditional. Would wrapping it be worth it?
no It isn't intended for anywhere else.
_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev