From 139fc5c81ef54a345d4f47910b5afd8e3039ca4d Mon Sep 17 00:00:00 2001 From: Thomas Haller Date: Thu, 8 Jul 2021 18:16:54 +0200 Subject: [PATCH 1/3] glib-aux: rework fallback random generator to use sha256 nm_utils_random_bytes() tries to get good randomness. If it fails, we still try our own approach, but also signal that the returned numbers are bad. In practice, none of the callers cares about the return value, because they wouldn't know what to do in case of bad randomness (abort() is not an option and retry is not expected to help and sending an email to the admin isn't gonna help either). So the fallback case really should try its best. The fallback case depends on a good random seed and a good pseudorandom number generator. Getting a good seed is in reality impossible, after kernel let us down. That is part of the problem, but we try our best. The other part is to use a cryptographic pseudorandom number generator. GRand uses a Mersenne Twister, so that is not good enough. In this low level code we also cannot call gnutls/nss, because usually we don't have that dependency. Maybe we could copy&paste the chacha20 implementation, it's simple enough and a compatible license. That might be good, but instead cock our own by adding some sha256 into the mix. This is fallback code after all, and we want to try hard, but not *that* hard to add chacha20 to NetworkManager. So, what we do is to use a well seeded GRand instance, and XOR that output with a sha256 digest of the state. It's probably slow, but performance is not the issue in this code path. (cherry picked from commit c22c3ce9f910982c81459a00fa01ddc25a938f35) --- src/libnm-glib-aux/nm-random-utils.c | 244 +++++++++++++++------------ 1 file changed, 134 insertions(+), 110 deletions(-) diff --git a/src/libnm-glib-aux/nm-random-utils.c b/src/libnm-glib-aux/nm-random-utils.c index 4f861be055..4b06f807fb 100644 --- a/src/libnm-glib-aux/nm-random-utils.c +++ b/src/libnm-glib-aux/nm-random-utils.c @@ -21,94 +21,146 @@ /*****************************************************************************/ -#define SEED_ARRAY_SIZE (16 + 2 + 4 + 2 + 3) +#define STATIC_SALT "l6z5vMBldDlCD6na" -static guint32 -_pid_hash(pid_t id) -{ - if (sizeof(pid_t) > sizeof(guint32)) - return (((guint64) id) >> 32) ^ ((guint64) id); - return id; -} +typedef struct _nm_packed { + uintptr_t heap_ptr; + uintptr_t stack_ptr; + gint64 now_bootime; + gint64 now_real; + pid_t pid; + pid_t ppid; + pid_t tid; + guint32 grand[16]; + guint8 auxval[16]; + char static_salt[NM_STRLEN(STATIC_SALT)]; +} BadRandSeed; + +typedef struct _nm_packed { + guint64 counter; + union { + guint8 full[NM_UTILS_CHECKSUM_LENGTH_SHA256]; + struct { + guint8 half_1[NM_UTILS_CHECKSUM_LENGTH_SHA256 / 2]; + guint8 half_2[NM_UTILS_CHECKSUM_LENGTH_SHA256 / 2]; + }; + } sha_digest; + union { + guint8 u8[NM_UTILS_CHECKSUM_LENGTH_SHA256 / 2]; + guint32 u32[((NM_UTILS_CHECKSUM_LENGTH_SHA256 / 2) + 3) / 4]; + } rand_vals; + GRand *rand; +} BadRandState; static void -_rand_init_seed(guint32 seed_array[static SEED_ARRAY_SIZE], GRand *rand) +_bad_random_init_seed(BadRandSeed *seed) { - int seed_idx; const guint8 *p_at_random; - guint64 now_nsec; + int seed_idx; + GRand * rand; - /* Get some seed material from the provided GRand. */ - for (seed_idx = 0; seed_idx < 16; seed_idx++) - seed_array[seed_idx] = g_rand_int(rand); + memcpy(seed->static_salt, STATIC_SALT, NM_STRLEN(STATIC_SALT)); - /* Add an address from the heap. */ - seed_array[seed_idx++] = ((guint64) ((uintptr_t) ((gpointer) rand))) >> 32; - seed_array[seed_idx++] = ((guint64) ((uintptr_t) ((gpointer) rand))); + /* g_rand_new() reads /dev/urandom, but we already noticed that + * /dev/urandom fails to give us good randomness (which is why + * we hit the "bad randomness" code path). So this may not be as + * good as we wish, but let's hope that it it does something smart + * to give some extra entropy... */ + rand = g_rand_new(); + + /* Get some seed material from a GRand. */ + for (seed_idx = 0; seed_idx < (int) G_N_ELEMENTS(seed->grand); seed_idx++) + seed->grand[seed_idx] = g_rand_int(rand); + + /* Add an address from the heap and stack, maybe ASLR helps a bit? */ + seed->heap_ptr = (uintptr_t) ((gpointer) rand); + seed->stack_ptr = (uintptr_t) ((gpointer) &rand); + + g_rand_free(rand); /* Add the per-process, random number. */ p_at_random = ((gpointer) getauxval(AT_RANDOM)); if (p_at_random) { - memcpy(&seed_array[seed_idx], p_at_random, 16); - } else - memset(&seed_array[seed_idx], 0, 16); - G_STATIC_ASSERT_EXPR(sizeof(guint32) == 4); - seed_idx += 4; - - /* Add the current timestamp, the pid and ppid. */ - now_nsec = nm_utils_clock_gettime_nsec(CLOCK_BOOTTIME); - seed_array[seed_idx++] = ((guint64) now_nsec) >> 32; - seed_array[seed_idx++] = ((guint64) now_nsec); - seed_array[seed_idx++] = _pid_hash(getpid()); - seed_array[seed_idx++] = _pid_hash(getppid()); - seed_array[seed_idx++] = _pid_hash(nm_utils_gettid()); - - nm_assert(seed_idx == SEED_ARRAY_SIZE); -} - -static GRand * -_rand_create_thread_local(void) -{ - G_LOCK_DEFINE_STATIC(global_rand); - static GRand *global_rand = NULL; - guint32 seed_array[SEED_ARRAY_SIZE]; - - /* We use thread-local instances of GRand to create a series of - * "random" numbers. We use thread-local instances, so that we don't - * require additional locking except the first time. - * - * We trust that once seeded, a GRand gives us a good enough stream of - * random numbers. If that wouldn't be the case, then maybe GRand should - * be fixed. - * Also, we tell our callers that the numbers from GRand are not good. - * But that isn't gonna help, because callers have no other way to get - * better random numbers, so usually the just ignore the failure and make - * the best of it. - * - * That means, the remaining problem is to seed the instance well. - * Note that we are already in a situation where getrandom() failed - * to give us good random numbers. So we can not do much to get reliably - * good entropy for the seed. */ - - G_LOCK(global_rand); - - if (G_UNLIKELY(!global_rand)) { - GRand *rand1; - - /* g_rand_new() reads /dev/urandom, but we already noticed that - * /dev/urandom fails to give us good randomness (which is why - * we hit this code path). So this may not be as good as we wish, - * but let's add it to the mix. */ - rand1 = g_rand_new(); - _rand_init_seed(seed_array, rand1); - global_rand = g_rand_new_with_seed_array(seed_array, SEED_ARRAY_SIZE); - g_rand_free(rand1); + G_STATIC_ASSERT(sizeof(seed->auxval) == 16); + memcpy(&seed->auxval, p_at_random, 16); } - _rand_init_seed(seed_array, global_rand); - G_UNLOCK(global_rand); + seed->now_bootime = nm_utils_clock_gettime_nsec(CLOCK_BOOTTIME); + seed->now_real = g_get_real_time(); + seed->pid = getpid(); + seed->ppid = getppid(); + seed->tid = nm_utils_gettid(); +} - return g_rand_new_with_seed_array(seed_array, SEED_ARRAY_SIZE); +static void +_bad_random_bytes(guint8 *buf, gsize n) +{ + nm_auto_free_checksum GChecksum *sum = g_checksum_new(G_CHECKSUM_SHA256); + + nm_assert(n > 0); + + /* We are in the fallback code path, where getrandom() (and /dev/urandom) failed + * to give us good randomness. Try our best. + * + * Our ability to get entropy for the CPRNG is very limited and thus the overall + * result will not be good randomness. See _bad_random_init_seed(). + * + * Once we have some seed material, we combine GRand (which is not a cryptographically + * secure PRNG) with some iterative sha256 hashing. It would be nice if we had + * easy access to chacha20, but it's probably more cumbersome to fork those + * implementations than hack a bad CPRNG by using sha256 hashing. After all, this + * is fallback code to get *some* randomness. And with the inability to get a good + * seed, the CPRNG is not going to give us truly good randomness. */ + + { + static BadRandState gl_state; + static GRand * gl_rand; + static GMutex gl_mutex; + NM_G_MUTEX_LOCKED(&gl_mutex); + + if (G_UNLIKELY(!gl_rand)) { + union { + BadRandSeed d_seed; + guint32 d_u32[(sizeof(BadRandSeed) + 3) / 4]; + } data = { + .d_u32 = {0}, + }; + + _bad_random_init_seed(&data.d_seed); + + gl_rand = g_rand_new_with_seed_array(data.d_u32, G_N_ELEMENTS(data.d_u32)); + + g_checksum_update(sum, (const guchar *) &data, sizeof(data)); + nm_utils_checksum_get_digest(sum, gl_state.sha_digest.full); + } + + while (TRUE) { + int i; + + gl_state.counter++; + for (i = 0; i < G_N_ELEMENTS(gl_state.rand_vals.u32); i++) + gl_state.rand_vals.u32[i] = g_rand_int(gl_rand); + g_checksum_reset(sum); + g_checksum_update(sum, (const guchar *) &gl_state, sizeof(gl_state)); + nm_utils_checksum_get_digest(sum, gl_state.sha_digest.full); + + /* gl_state.sha_digest.full and gl_state.rand_vals contain now our + * random values, but they are also the state for the next iteration. + * We must not directly expose that state to the caller, so XOR the values. + * + * That means, per iteration we can generate 16 bytes of randomness. That + * is for example required to generate a random UUID. */ + for (i = 0; i < (int) (NM_UTILS_CHECKSUM_LENGTH_SHA256 / 2); i++) { + nm_assert(n > 0); + buf[0] = gl_state.sha_digest.half_1[i] ^ gl_state.sha_digest.half_2[i] + ^ gl_state.rand_vals.u8[i]; + buf++; + n--; + if (n == 0) + return; + } + } + } } /** @@ -156,8 +208,8 @@ nm_utils_random_bytes(void *p, size_t n) return TRUE; /* no or partial read. There is not enough entropy. - * Fill the rest reading from urandom, and remember that - * some bits are not high quality. */ + * Fill the rest reading with the fallback code and remember + * that some bits are not high quality. */ nm_assert(r < n); buf += r; n -= r; @@ -167,9 +219,10 @@ nm_utils_random_bytes(void *p, size_t n) * the entropy pool is low (early boot?), and asking for more * entropy causes kernel messages to be logged. * - * We use our fallback via GRand. Note that g_rand_new() also - * tries to seed itself with data from /dev/urandom, but since - * we reuse the instance, it shouldn't matter. */ + * Note that we fall back to _bad_random_bytes(), which (among others) seeds + * itself with g_rand_new(). That also will read /dev/urandom, but as + * we do that only once, we don't care. But in general, we are here in + * a situation where we want to avoid reading /dev/urandom too much. */ avoid_urandom = TRUE; } else { if (errno == ENOSYS) { @@ -203,42 +256,13 @@ fd_open: } if (!urandom_success) { - static _nm_thread_local GRand *rand_tls = NULL; - GRand * rand; - gsize i; - int j; - /* we failed to fill the bytes reading from urandom. - * Fill the bits using GRand pseudo random numbers. + * Fill the bits using our pseudo random numbers. * * We don't have good quality. */ has_high_quality = FALSE; - - rand = rand_tls; - if (G_UNLIKELY(!rand)) { - rand = _rand_create_thread_local(); - rand_tls = rand; - nm_utils_thread_local_register_destroy(rand, (GDestroyNotify) g_rand_free); - } - - nm_assert(n > 0); - i = 0; - for (;;) { - const union { - guint32 v32; - guint8 v8[4]; - } v = { - .v32 = g_rand_int(rand), - }; - - for (j = 0; j < 4;) { - buf[i++] = v.v8[j++]; - if (i >= n) - goto done; - } - } -done:; + _bad_random_bytes(buf, n); } return has_high_quality; From f84ebc461132c8acb88efa64b5c47091e26a46c4 Mon Sep 17 00:00:00 2001 From: Thomas Haller Date: Fri, 9 Jul 2021 07:56:29 +0200 Subject: [PATCH 2/3] glib-aux: simplify logic in nm_utils_random_bytes() There should be no change in behavior. (cherry picked from commit 62c60653a72b828f2cc1f2070a305eff03ad96ad) --- src/libnm-glib-aux/nm-random-utils.c | 65 ++++++++++++---------------- 1 file changed, 27 insertions(+), 38 deletions(-) diff --git a/src/libnm-glib-aux/nm-random-utils.c b/src/libnm-glib-aux/nm-random-utils.c index 4b06f807fb..aaeec62cc1 100644 --- a/src/libnm-glib-aux/nm-random-utils.c +++ b/src/libnm-glib-aux/nm-random-utils.c @@ -190,9 +190,7 @@ nm_utils_random_bytes(void *p, size_t n) int fd; int r; gboolean has_high_quality = TRUE; - gboolean urandom_success; - guint8 * buf = p; - gboolean avoid_urandom = FALSE; + guint8 * buf = p; g_return_val_if_fail(p, FALSE); g_return_val_if_fail(n > 0, FALSE); @@ -213,7 +211,6 @@ nm_utils_random_bytes(void *p, size_t n) nm_assert(r < n); buf += r; n -= r; - has_high_quality = FALSE; /* At this point, we don't want to read /dev/urandom, because * the entropy pool is low (early boot?), and asking for more @@ -223,47 +220,39 @@ nm_utils_random_bytes(void *p, size_t n) * itself with g_rand_new(). That also will read /dev/urandom, but as * we do that only once, we don't care. But in general, we are here in * a situation where we want to avoid reading /dev/urandom too much. */ - avoid_urandom = TRUE; + goto out_bad_random; + } + if (errno == ENOSYS) { + /* no support for getrandom(). We don't know whether + * we urandom will give us good quality. Assume yes. */ + have_syscall = FALSE; } else { - if (errno == ENOSYS) { - /* no support for getrandom(). We don't know whether - * we urandom will give us good quality. Assume yes. */ - have_syscall = FALSE; - } else { - /* unknown error. We'll read urandom below, but we don't have - * high-quality randomness. */ - has_high_quality = FALSE; - } + /* unknown error. We'll read urandom below, but we don't have + * high-quality randomness. */ + has_high_quality = FALSE; } } } #endif - urandom_success = FALSE; - if (!avoid_urandom) { fd_open: - fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC | O_NOCTTY); - if (fd < 0) { - r = errno; - if (r == EINTR) - goto fd_open; - } else { - r = nm_utils_fd_read_loop_exact(fd, buf, n, TRUE); - nm_close(fd); - if (r >= 0) - urandom_success = TRUE; - } + fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC | O_NOCTTY); + if (fd < 0) { + if (errno == EINTR) + goto fd_open; + goto out_bad_random; } + r = nm_utils_fd_read_loop_exact(fd, buf, n, TRUE); + nm_close(fd); + if (r >= 0) + return has_high_quality; - if (!urandom_success) { - /* we failed to fill the bytes reading from urandom. - * Fill the bits using our pseudo random numbers. - * - * We don't have good quality. - */ - has_high_quality = FALSE; - _bad_random_bytes(buf, n); - } - - return has_high_quality; +out_bad_random: + /* we failed to fill the bytes reading from urandom. + * Fill the bits using our pseudo random numbers. + * + * We don't have good quality. + */ + _bad_random_bytes(buf, n); + return FALSE; } From e427f53b8fa4ddb43d9f21828aa56680348d3dae Mon Sep 17 00:00:00 2001 From: Thomas Haller Date: Fri, 9 Jul 2021 08:18:57 +0200 Subject: [PATCH 3/3] glib-aux: add getrandom() syscall wrapper as fallback We make an effort to get a better fallback case with _bad_random_bytes(). Also make an effort to get good randomness in the first place. Even if we compile against libc headers that don't provide getrandom(). Also, this isn't really ugly, because for a long time glibc was reluctant to add getrandom() wrapper and using syscall() was the way to go. (cherry picked from commit 05a6936befed8806d42a988b777d3013f15f6683) --- src/libnm-glib-aux/nm-random-utils.c | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/libnm-glib-aux/nm-random-utils.c b/src/libnm-glib-aux/nm-random-utils.c index aaeec62cc1..e8fc47e109 100644 --- a/src/libnm-glib-aux/nm-random-utils.c +++ b/src/libnm-glib-aux/nm-random-utils.c @@ -9,6 +9,7 @@ #include #include +#include #if USE_SYS_RANDOM_H #include @@ -21,6 +22,26 @@ /*****************************************************************************/ +#if !defined(SYS_getrandom) && defined(__NR_getrandom) + #define SYS_getrandom __NR_getrandom +#endif + +#ifndef GRND_NONBLOCK + #define GRND_NONBLOCK 0x01 +#endif + +#if !HAVE_GETRANDOM && defined(SYS_getrandom) +static int +getrandom(void *buf, size_t buflen, unsigned flags) +{ + return syscall(SYS_getrandom, buf, buflen, flags); +} + #undef HAVE_GETRANDOM + #define HAVE_GETRANDOM 1 +#endif + +/*****************************************************************************/ + #define STATIC_SALT "l6z5vMBldDlCD6na" typedef struct _nm_packed {