[PATCH v2] random: reseed more often immediately after booting

From: Jason A. Donenfeld
Date: Thu Mar 10 2022 - 16:11:11 EST


In order to chip away at the "premature first" problem, we augment our
existing entropy accounting with more frequent reseedings at boot.

The idea is that at boot, we're getting entropy from various places, and
we're not very sure which of early boot entropy is good and which isn't.
Even when we're crediting the entropy, we're still not totally certain
that it's any good. Since boot is the one time (aside from a compromise)
that we have zero entropy, it's important that we shephard entropy into
the crng fairly often.

At the same time, we don't want a "premature next" problem, whereby an
attacker can brute force individual bits of added entropy. In lieu of
going full-on Fortuna (for now), we can pick a simpler strategy of just
reseeding more often during the first 5 minutes after boot. This is
still bounded by the 256-bit entropy credit requirement, so we'll skip a
reseeding if we haven't reached that, but in case entropy /is/ coming
in, this ensures that it makes its way into the crng rather rapidly
during these early stages.

For this we start at 5 seconds after boot, and double that interval
until it's more than 5 minutes. After that, we then move to our normal
schedule of reseeding not more than once per 5 minutes.

One interesting caveat is that reseeding is presently on-demand, so
we're not reseeding at precisely the 5, 10, 20, 40, 80, 160 marks, but
rather whenever the crng is actually used and we've matched or exceeded
one of those marks. This means that we might wind up trying to reseed at
both second 79 and second 80, or at second 41 and second 80, or anywhere
in between. In that sense, this isn't quite imposing a minimum frequency
on reseeds during early boot, but simply bounding a maximum frequency.
Since we're still requiring 256 bits of entropy credits, this shouldn't
be a super large issue in terms of "premature next", and in the event
that we reseed a bit late and then miss the next reseeding due to not
enough entropy credits, we still have acquired more entropy by virtue of
having reseeded later.

Cc: Theodore Ts'o <tytso@xxxxxxx>
Cc: Dominik Brodowski <linux@xxxxxxxxxxxxxxxxxxxx>
Cc: Eric Biggers <ebiggers@xxxxxxxxxx>
Signed-off-by: Jason A. Donenfeld <Jason@xxxxxxxxx>
---
Changes v1->v2:
- Note "on-demand" funkiness in commit message.
- Always use READ_ONCE() with next_init_secs.

drivers/char/random.c | 28 +++++++++++++++++++++++++---
1 file changed, 25 insertions(+), 3 deletions(-)

diff --git a/drivers/char/random.c b/drivers/char/random.c
index d94d5ba414ee..27d91cfc3cd9 100644
--- a/drivers/char/random.c
+++ b/drivers/char/random.c
@@ -333,6 +333,28 @@ static void crng_fast_key_erasure(u8 key[CHACHA_KEY_SIZE],
memzero_explicit(first_block, sizeof(first_block));
}

+/*
+ * Return whether the crng seed is considered to be sufficiently
+ * old that a reseeding might be attempted. This is the case 5,
+ * 10, 20, 40, 80, and 160 seconds after boot, and after if the
+ * last reseeding was CRNG_RESEED_INTERVAL ago.
+ */
+static bool crng_has_old_seed(void)
+{
+ static unsigned int next_init_secs = 5;
+ unsigned int this_init_secs = READ_ONCE(next_init_secs);
+
+ if (unlikely(this_init_secs < CRNG_RESEED_INTERVAL / HZ)) {
+ unsigned int uptime = min_t(u64, INT_MAX, ktime_get_seconds());
+ if (uptime >= this_init_secs) {
+ WRITE_ONCE(next_init_secs, 5U << fls(uptime / 5));
+ return true;
+ }
+ return false;
+ }
+ return time_after(jiffies, READ_ONCE(base_crng.birth) + CRNG_RESEED_INTERVAL);
+}
+
/*
* This function returns a ChaCha state that you may use for generating
* random data. It also returns up to 32 bytes on its own of random data
@@ -366,10 +388,10 @@ static void crng_make_state(u32 chacha_state[CHACHA_STATE_WORDS],
}

/*
- * If the base_crng is more than 5 minutes old, we reseed, which
- * in turn bumps the generation counter that we check below.
+ * If the base_crng is old enough, we try to reseed, which in turn
+ * bumps the generation counter that we check below.
*/
- if (unlikely(time_after(jiffies, READ_ONCE(base_crng.birth) + CRNG_RESEED_INTERVAL)))
+ if (unlikely(crng_has_old_seed()))
crng_reseed(false);

local_lock_irqsave(&crngs.lock, flags);
--
2.35.1