packages feed

crypton-2.2.0: cbits/crypton_sysdrg.c

/*
 * The generator behind MonadRandom IO.
 *
 * A ChaCha20 DRBG per operating system thread, seeded from a process-wide
 * DRBG, which is itself seeded from the system entropy pool.  This is the
 * shape RFC 9180's neighbours and the other libraries have settled on, and
 * it was asked for in #298.
 *
 * Per operating system thread and not per Haskell thread: a forkIO thread
 * moves between capabilities, so state kept against it would be shared by
 * threads running at the same time.  That is why the state is here and
 * reached through pthread_getspecific rather than held in Haskell.
 *
 * Three things force a reseed:
 *
 *   - a thread has produced CRYPTON_THREAD_RESEED bytes,
 *   - the process DRBG has issued CRYPTON_GLOBAL_RESEED bytes of seed,
 *   - the process has forked.
 *
 * The last one is the one that bites.  A child inherits its parent's state
 * and would otherwise produce the same stream; the generation counter below
 * is bumped in the child by a pthread_atfork handler, and every generator
 * compares against it before it answers.
 */

#include <stdint.h>
#include <string.h>
#include <stdlib.h>

#include "crypton_chacha.h"
#include "crypton_sha512.h"

#ifdef _WIN32
#include <windows.h>
#else
#include <pthread.h>
#include <unistd.h>
#endif

/* from crypton_sysrandom.c */
int crypton_sysrandom_available(void);
int crypton_sysrandom_bytes(uint8_t *buf, int len);

#define CHACHA_ROUNDS      20
#define SEED_KEY           32
#define SEED_IV             8
#define SEED_LEN           (SEED_KEY + SEED_IV)

#define CRYPTON_THREAD_RESEED  (1u << 20)
#define CRYPTON_GLOBAL_RESEED  (1u << 20)

typedef struct {
	crypton_chacha_context ctx;
	uint64_t used;
	uint32_t generation;
	int seeded;
} drg_t;

static drg_t global_drg;
static volatile uint32_t fork_generation = 0;

#ifdef _WIN32
static CRITICAL_SECTION global_lock;
/* Fls and not Tls: TlsAlloc has no destructor, so the state of every thread
 * that ever drew a byte would be left allocated and unscrubbed when the
 * thread ended.  FlsAlloc takes the callback that pthread_key_create does. */
static DWORD thread_slot;
static INIT_ONCE init_once = INIT_ONCE_STATIC_INIT;
#else
static pthread_mutex_t global_lock = PTHREAD_MUTEX_INITIALIZER;
static pthread_key_t thread_slot;
static pthread_once_t init_once = PTHREAD_ONCE_INIT;
#endif

static void scrub(void *p, size_t n)
{
	volatile uint8_t *q = (volatile uint8_t *) p;
	while (n--) *q++ = 0;
}

/*
 * Seed material for the process DRBG.
 *
 * What the system gives goes through SHA-512 rather than into the key
 * directly, so that the key is a fixed size whatever the call returns, and
 * so that a second source could be added without any of this changing shape.
 * There is one source today and the note below says why.
 */
static int seed_from_system(uint8_t out[SEED_LEN])
{
	struct sha512_ctx h;
	uint8_t buf[64];
	uint8_t digest[64];
	int got;

	crypton_sha512_init(&h);

	/*
	 * The system call only.  Where there is none -- an old kernel, a BSD
	 * this does not know -- seeding fails and the caller keeps the path
	 * it has, rather than this file growing a second copy of the device
	 * reading that Crypto.Random.Entropy.Unix already does.
	 *
	 * And RDRAND is not mixed in beside it, which it was until
	 * @vdukhovni pointed out that it cannot add anything: every system
	 * this code runs on already feeds RDRAND into the pool the call below
	 * draws from.  Linux does that whatever random.trust_cpu says -- that
	 * setting decides whether the contribution is *credited*, not whether
	 * it is mixed -- so the instruction's output is in this buffer
	 * already.
	 *
	 * The argument for keeping it was defence in depth against that pool
	 * having gone wrong.  It does not survive the above: a pool that has
	 * gone wrong has gone wrong with RDRAND already in it, so asking the
	 * instruction a second time covers only the case where the kernel's
	 * mixing is broken and the instruction is not.  That is not worth a
	 * second code path, a flag, and the standing invitation to read this
	 * as a second source when it is the same one twice.
	 */
	got = crypton_sysrandom_available() ? crypton_sysrandom_bytes(buf, 64) : 0;
	if (got <= 0) {
		/* Nothing else here is a seed on its own, so there is nothing to
		 * go on with: return before drawing anything that would only be
		 * thrown away. */
		scrub(&h, sizeof h);
		scrub(buf, sizeof buf);
		return 0;
	}
	crypton_sha512_update(&h, buf, (uint32_t) got);


	crypton_sha512_finalize(&h, digest);
	memcpy(out, digest, SEED_LEN);

	scrub(&h, sizeof h);
	scrub(buf, sizeof buf);
	scrub(digest, sizeof digest);
	return 1;
}

static void drg_seed(drg_t *d, const uint8_t seed[SEED_LEN])
{
	crypton_chacha_init(&d->ctx, CHACHA_ROUNDS, SEED_KEY, seed,
	                    SEED_IV, seed + SEED_KEY);
	d->used = 0;
	d->generation = fork_generation;
	d->seeded = 1;
}

/*
 * Forget the key that produced the bytes just handed out.
 *
 * Without this the key stands until the next reseed, and anyone who reads a
 * generator's state can wind the counter back and reproduce everything it
 * has issued since -- up to CRYPTON_THREAD_RESEED bytes that were meant to
 * be secret.  Taking the next forty bytes of keystream as the new key and
 * nonce, and dropping the old ones, puts that out of reach one step after
 * it is issued: ChaCha20 does not run backwards, and the key that would
 * have been needed is gone.  It is what arc4random does.
 *
 * crypton_chacha_init memsets the whole context, so the counter returns to
 * zero and the tail of a part-used block goes with the old key rather than
 * being handed out under the new one.
 *
 * d->used is not touched.  It counts what callers were given, which is what
 * the reseed interval is written in terms of; these forty bytes are the
 * cost of the rekey and not an answer to anybody.
 */
static void drg_rekey(drg_t *d)
{
	uint8_t next[SEED_LEN];

	crypton_chacha_generate(next, &d->ctx, SEED_LEN);
	crypton_chacha_init(&d->ctx, CHACHA_ROUNDS, SEED_KEY, next,
	                    SEED_IV, next + SEED_KEY);
	scrub(next, sizeof next);
}

static int drg_stale(const drg_t *d, uint64_t limit)
{
	return !d->seeded || d->used >= limit || d->generation != fork_generation;
}

/* Bytes from the process DRBG, which is only ever asked for seed material. */
static int global_bytes(uint8_t *out, uint32_t len)
{
	int ok = 1;

#ifdef _WIN32
	EnterCriticalSection(&global_lock);
#else
	pthread_mutex_lock(&global_lock);
#endif
	if (drg_stale(&global_drg, CRYPTON_GLOBAL_RESEED)) {
		uint8_t seed[SEED_LEN];
		ok = seed_from_system(seed);
		if (ok)
			drg_seed(&global_drg, seed);
		scrub(seed, sizeof seed);
	}
	if (ok) {
		crypton_chacha_generate(out, &global_drg.ctx, len);
		global_drg.used += len;
		drg_rekey(&global_drg);
	}
#ifdef _WIN32
	LeaveCriticalSection(&global_lock);
#else
	pthread_mutex_unlock(&global_lock);
#endif
	return ok;
}

#ifdef _WIN32
static void WINAPI thread_free(void *p)
#else
static void thread_free(void *p)
#endif
{
	if (p) {
		scrub(p, sizeof(drg_t));
		free(p);
	}
}

#ifndef _WIN32
/* All three handlers, not just the child's.  The child's first draw has to
 * reseed -- the generation has changed -- and reseeding takes global_lock.
 * A fork made while another thread held it would hand the child a mutex
 * locked by a thread that did not come across, and the child would wait on
 * it for ever.  So the lock is taken before the fork and released on both
 * sides of it, which is the state the child needs it in. */
static void before_fork(void)
{
	pthread_mutex_lock(&global_lock);
}

static void after_fork_in_parent(void)
{
	pthread_mutex_unlock(&global_lock);
}

static void after_fork_in_child(void)
{
	pthread_mutex_unlock(&global_lock);
	fork_generation++;
}
#endif

#ifdef CRYPTON_SYSDRG_TESTING
/* For cbits/tests/sysdrg and nothing else: hold and release the process
 * generator's lock, so that a fork can be made to happen while another
 * thread holds it.  Nothing outside that test declares these, and the
 * library is never built with this defined. */
void crypton_sysdrg_test_lock(void);
void crypton_sysdrg_test_unlock(void);

static drg_t *this_thread(void);

/* The calling thread's ChaCha key, which is d[4..11] of the state.  A test
 * uses it to ask whether the key that produced a draw is still there
 * afterwards; see "a draw replaces the key that made it" in
 * cbits/tests/sysdrg. */
void crypton_sysdrg_test_key(uint8_t out[32]);

void crypton_sysdrg_test_key(uint8_t out[32])
{
	drg_t *d = this_thread();
	int i;

	if (!d)
		return;
	for (i = 0; i < 8; i++) {
		uint32_t w = d->ctx.st.d[4 + i];
		out[i * 4 + 0] = (uint8_t) (w);
		out[i * 4 + 1] = (uint8_t) (w >> 8);
		out[i * 4 + 2] = (uint8_t) (w >> 16);
		out[i * 4 + 3] = (uint8_t) (w >> 24);
	}
}

void crypton_sysdrg_test_lock(void)
{
#ifndef _WIN32
	pthread_mutex_lock(&global_lock);
#endif
}

void crypton_sysdrg_test_unlock(void)
{
#ifndef _WIN32
	pthread_mutex_unlock(&global_lock);
#endif
}
#endif

#ifdef _WIN32
static BOOL CALLBACK init_slot(PINIT_ONCE o, PVOID p, PVOID *c)
{
	(void) o; (void) p; (void) c;
	InitializeCriticalSection(&global_lock);
	thread_slot = FlsAlloc(thread_free);
	return TRUE;
}
#else
static void init_slot(void)
{
	pthread_key_create(&thread_slot, thread_free);
	pthread_atfork(before_fork, after_fork_in_parent, after_fork_in_child);
}
#endif

static drg_t *this_thread(void)
{
	drg_t *d;

#ifdef _WIN32
	InitOnceExecuteOnce(&init_once, init_slot, NULL, NULL);
	if (thread_slot == FLS_OUT_OF_INDEXES)
		return NULL;
	d = (drg_t *) FlsGetValue(thread_slot);
#else
	pthread_once(&init_once, init_slot);
	d = (drg_t *) pthread_getspecific(thread_slot);
#endif
	if (!d) {
		d = (drg_t *) calloc(1, sizeof(drg_t));
		if (!d)
			return NULL;
#ifdef _WIN32
		if (!FlsSetValue(thread_slot, d)) {
			free(d);
			return NULL;
		}
#else
		if (pthread_setspecific(thread_slot, d) != 0) {
			free(d);
			return NULL;
		}
#endif
	}
	return d;
}

/* Returns the number of bytes written, which is len unless there was no
 * seed to be had -- and then it is 0, so that the caller can say so rather
 * than hand back a buffer it cannot vouch for. */
int crypton_sysdrg_bytes(uint8_t *out, int len)
{
	drg_t *d;

	if (len < 0)
		return 0;
	d = this_thread();
	if (!d)
		return 0;

	if (drg_stale(d, CRYPTON_THREAD_RESEED)) {
		uint8_t seed[SEED_LEN];
		int ok = global_bytes(seed, SEED_LEN);
		if (ok)
			drg_seed(d, seed);
		scrub(seed, sizeof seed);
		if (!ok)
			return 0;
	}

	crypton_chacha_generate(out, &d->ctx, (uint32_t) len);
	d->used += (uint64_t) len;
	drg_rekey(d);
	return len;
}

/* For the tests: how many times the process has been seen to fork. */
uint32_t crypton_sysdrg_generation(void)
{
	return fork_generation;
}

/* For the tests: bytes this thread's generator has produced since it was
 * last seeded.  A generator shared between threads would carry the first
 * thread's count into the second; a per-thread one starts again at zero,
 * and that is the only way from outside to tell the two apart. */
uint64_t crypton_sysdrg_thread_used(void)
{
	drg_t *d = this_thread();
	return d ? d->used : 0;
}