This commit makes the ChaCha20 CSPRNG prefer using OpenSSL's SIMD optimized ChaCha20 on platforms which are compiled with such support. This represents a major improvement on performance, without impacting the underlying CSPRNG functionality.
The EVP backend re-keys from the first bytes of every refill, just as the built-in implementation does, so that the fast key erasure - and thus the forward secrecy - of the generator is the same for both. The built-in ChaCha20 implementation is always built so that it can serve as a fallback when OpenSSL is unable to provide ChaCha20, for example when it only offers approved algorithms in FIPS mode. Assisted-by: LLM (claude, opencode, sonnet, opus, qwen3.8) Signed-off-by: Aaron Conole <[email protected]> --- v2->v3: * Update the the csprng to provide local ChaCha20 when openssl isn't available for many reasons. * Updated the cs_random_bytes() max comment (since the OpenSSL version will shrink the max a bit more). * Updated the rekey to provide forward key erasure and forward secrecy. lib/random.c | 217 ++++++++++++++++++++++++++++++++++++++++++--------- lib/random.h | 2 +- 2 files changed, 182 insertions(+), 37 deletions(-) diff --git a/lib/random.c b/lib/random.c index d2f28866a2..27d57530f7 100644 --- a/lib/random.c +++ b/lib/random.c @@ -18,15 +18,24 @@ #include "random.h" #include <errno.h> +#include <stdbool.h> #include <stdlib.h> #include <sys/time.h> +#if defined(HAVE_OPENSSL) && !defined(OPENSSL_NO_CHACHA) +#include <openssl/evp.h> +#define HAVE_CHACHA20_EVP 1 +#endif + #include "byte-order.h" #include "entropy.h" #include "hash.h" #include "ovs-thread.h" #include "timeval.h" #include "util.h" +#include "openvswitch/vlog.h" + +VLOG_DEFINE_THIS_MODULE(random); /* This is the 32-bit PRNG recommended in G. Marsaglia, "Xorshift RNGs", * _Journal of Statistical Software_ 8:14 (July 2003). According to the paper, @@ -120,6 +129,39 @@ random_next(void) } +/* Cryptographic secure pseudo-random number generator. */ + +/* Refill a page worth of keystream at a time to amortize the block cost. */ +#define CSPRNG_BUFSZ 4096 + +#define CHACHA_KEYLEN 32 /* 256-bit key. */ +#define CHACHA_IVLEN 16 /* ChaCha20 IV: counter and nonce. */ + +/* Reseed from the OS entropy pool after this many bytes served, matching + * the arc4random policy of periodic reseeding. */ +#define CSPRNG_RESEED (1600 * 1024) + +#ifdef HAVE_CHACHA20_EVP +/* The OpenSSL EVP backend consumes an IV in addition to a key. */ +#define CSPRNG_SECRETS (CHACHA_KEYLEN + CHACHA_IVLEN) +#else +#define CSPRNG_SECRETS CHACHA_KEYLEN +#endif + +struct csprng { +#ifdef HAVE_CHACHA20_EVP + EVP_CIPHER_CTX *ctx; /* OpenSSL EVP state, once created. */ + bool use_evp; /* Serve from OpenSSL rather than the + * built-in implementation. */ +#endif + uint32_t state[16]; /* ChaCha20 state: + * constant|key|counter|nonce. */ + uint8_t buf[CSPRNG_BUFSZ]; /* Buffered keystream. */ + size_t pos; /* Next unused byte in 'buf'. */ + size_t served; /* Bytes served since last reseed. */ + int initialized; +}; + /* This is a 32-bit CSPRNG adapted from the ChaCha20 algorithm described in * RFC 7539 and RFC 8439, similar to arc4random(3) and the Linux kernel's * get_random_u32(). @@ -130,14 +172,6 @@ random_next(void) */ #define CHACHA_ROUNDS 20 -#define CHACHA_KEYLEN 32 /* 256-bit key. */ - -/* Refill a page worth of keystream at a time to amortize the block cost. */ -#define CSPRNG_BUFSZ 4096 - -/* Reseed from the OS entropy pool after this many bytes served, matching - * the arc4random policy of periodic reseeding. */ -#define CSPRNG_RESEED (1600 * 1024) #define ROTL32(v, n) (((v) << (n)) | ((v) >> (32 - (n)))) @@ -147,20 +181,13 @@ random_next(void) a += b; d ^= a; d = ROTL32(d, 8); \ c += d; b ^= c; b = ROTL32(b, 7) -struct csprng { - uint32_t state[16]; /* ChaCha20 state: - * constant|key|counter|nonce. */ - uint8_t buf[CSPRNG_BUFSZ]; /* Buffered keystream. */ - size_t pos; /* Next unused byte in 'buf'. */ - size_t served; /* Bytes served since last reseed. */ - int initialized; -}; - /* ChaCha20 constant: "expand 32-byte k". Must be at least 16 bytes + NUL. */ static const uint8_t chacha_sigma[] = "expand 32-byte k"; +#ifndef HAVE_CHACHA20_EVP /* Current random state. */ DEFINE_STATIC_PER_THREAD_DATA(struct csprng, cs_state, { .initialized = 0 }); +#endif /* Produces one 64-byte ChaCha20 block from 'input' (16 words) into 'out'. */ void @@ -211,22 +238,8 @@ csprng_rekey(struct csprng *c, const uint8_t key[CHACHA_KEYLEN]) } } -static void -csprng_seed(struct csprng *c) -{ - uint8_t key[CHACHA_KEYLEN]; - - get_entropy_or_die(key, sizeof key); - csprng_rekey(c, key); - ovs_memzero(key, sizeof key); - - c->pos = sizeof c->buf; - c->served = 0; - c->initialized = 1; -} - -/* Fills csprng with fresh keystream, reserving the first 32 bytes to re-key - * the generator (fast key erasure / forward secrecy). */ +/* Fills 'c' with fresh keystream, reserving the first 32 bytes to re-key the + * generator (fast key erasure / forward secrecy). */ static void csprng_refill(struct csprng *c) { @@ -246,21 +259,153 @@ csprng_refill(struct csprng *c) c->pos = CHACHA_KEYLEN; } +#ifdef HAVE_CHACHA20_EVP +/* This is the EVP based CSPRNG using the OpenSSL EVP framework to create a + * long stream of bytes, and pull those. The OpenSSL EVP manages the + * underlying bytes, all this does is manage the policy. It re-keys from the + * beginning of every refill, as the built-in implementation above does, so + * that both provide the same fast key erasure. + */ + +static void +csprng_state_destroy(void *ctx) +{ + struct csprng *csp = ctx; + + if (csp->ctx) { + EVP_CIPHER_CTX_free(csp->ctx); + csp->ctx = NULL; + } + + free(csp); +} + +static struct csprng * +cs_state_get(void) +{ + static struct ovsthread_once once = OVSTHREAD_ONCE_INITIALIZER; + static ovsthread_key_t cskey; + struct csprng *csp; + + if (ovsthread_once_start(&once)) { + ovsthread_key_create(&cskey, csprng_state_destroy); + ovsthread_once_done(&once); + } + + csp = ovsthread_getspecific(cskey); + if (!csp) { + csp = xzalloc(sizeof *csp); + csp->use_evp = true; + ovsthread_setspecific(cskey, csp); + } + return csp; +} + +/* (Re)keys the OpenSSL EVP state from 'key' (32 bytes) and 'iv' (16 bytes). */ +static bool +csprng_evp_rekey(struct csprng *c, const uint8_t key[CHACHA_KEYLEN], + const uint8_t iv[CHACHA_IVLEN]) +{ + return EVP_EncryptInit_ex(c->ctx, EVP_chacha20(), NULL, key, iv) == 1; +} + +/* Seeds the OpenSSL EVP state from fresh entropy, returning false if OpenSSL + * cannot provide ChaCha20. */ +static bool +csprng_evp_seed(struct csprng *c, const uint8_t key[CHACHA_KEYLEN]) +{ + uint8_t iv[CHACHA_IVLEN]; + bool ok; + + if (!c->ctx) { + c->ctx = EVP_CIPHER_CTX_new(); + if (!c->ctx) { + VLOG_WARN_ONCE("cs_random: cannot create cipher context."); + return false; + } + } + + get_entropy_or_die(iv, sizeof iv); + ok = csprng_evp_rekey(c, key, iv); + ovs_memzero(iv, sizeof iv); + + if (!ok) { + /* OpenSSL may not provide ChaCha20 at all, for example when it only + * offers approved algorithms in FIPS mode. Fall back to the built-in + * implementation rather than failing to provide randomness. */ + VLOG_WARN_ONCE("cs_random: OpenSSL ChaCha20 unavailable, using the " + "built-in implementation."); + } + return ok; +} + +/* Fills 'c' with fresh keystream from OpenSSL, reserving the first bytes to + * re-key the generator (fast key erasure / forward secrecy). */ +static void +csprng_evp_refill(struct csprng *c) +{ + static const uint8_t zero[CSPRNG_BUFSZ]; + int outl; + + if (EVP_EncryptUpdate(c->ctx, c->buf, &outl, zero, sizeof c->buf) != 1 || + outl != sizeof c->buf) { + VLOG_FATAL("cs_random: EVP fill failure."); + } + + csprng_evp_rekey(c, c->buf, c->buf + CHACHA_KEYLEN); + ovs_memzero(c->buf, CSPRNG_SECRETS); + c->pos = CSPRNG_SECRETS; +} +#endif /* HAVE_CHACHA20_EVP */ + +static void +csprng_seed(struct csprng *c) +{ + uint8_t key[CHACHA_KEYLEN]; + + get_entropy_or_die(key, sizeof key); + +#ifdef HAVE_CHACHA20_EVP + if (c->use_evp && !csprng_evp_seed(c, key)) { + c->use_evp = false; + } + if (!c->use_evp) { + csprng_rekey(c, key); + } +#else + csprng_rekey(c, key); +#endif + + ovs_memzero(key, sizeof key); + + c->pos = sizeof c->buf; + c->served = 0; + c->initialized = 1; +} + void cs_random_bytes(void *b, size_t l) { struct csprng *c = cs_state_get(); - /* c->buf isn't fully available for reading because the first 32 bytes - * of every refill are consumed as the next key. */ - ovs_assert(l <= sizeof c->buf - CHACHA_KEYLEN); + /* c->buf isn't fully available for reading because the first bytes of + * every refill are consumed as the next key and IV. */ + ovs_assert(l <= sizeof c->buf - CSPRNG_SECRETS); if (!c->initialized || c->served >= CSPRNG_RESEED) { csprng_seed(c); } if (c->pos + l > sizeof c->buf) { +#ifdef HAVE_CHACHA20_EVP + if (c->use_evp) { + csprng_evp_refill(c); + } else { + csprng_refill(c); + } +#else csprng_refill(c); +#endif } memcpy(b, c->buf + c->pos, l); diff --git a/lib/random.h b/lib/random.h index 77792e28f1..76f165c7a8 100644 --- a/lib/random.h +++ b/lib/random.h @@ -53,7 +53,7 @@ void chacha20_block(const uint32_t input[CHACHA_K_INPUT], uint8_t out[CHACHA_BLOCK]); /* Fills 'b' with 'l' cryptographically secure random bytes. 'l' must not be - * larger than 4064; callers needing more bytes should make multiple calls. */ + * larger than 4048; callers needing more bytes should make multiple calls. */ void cs_random_bytes(void *, size_t); uint32_t cs_random_uint32(void); -- 2.56.0 _______________________________________________ dev mailing list [email protected] https://mail.openvswitch.org/mailman/listinfo/ovs-dev
