Skip to content

Commit

Permalink
[DEV] random: use a ChaCha20-based internal CSPRNG
Browse files Browse the repository at this point in the history
futils_random_uint{8,16,32,64} return values from a
CSPRNG, aka. cryptographically secure random number
generator, that is, a well seeded, secure RNG, that
don't leak its internal state in its outputs, and
prevent recovering previous generated values, even
if its internal state is leaked somehow.

As it requires doing syscalls to use operating
system's CSPRNG, there's a cost of retrieving a
random value.

A faster, still secure, PRNG would be useful.

Here a likely cryptographically secure PRNG is built
from ChaCha20, following the construct used on
OpenBSD's arc4random().

It uses ChaCha20, as defined by RFC 8439, which is
a stream cipher that uses a 256bits key (+ a 96bits
nonce):

  https://tools.ietf.org/html/rfc8439
  https://cr.yp.to/chacha.html

Here, we're only interested in its key stream, and
skipping the encryption part (thus it can be viewed
as encrypting a strings of 0s).

ChaCha20 is widely used in various CSPRNG. In particular
it's used in OpenBSD's arc4random implementation,
which this implementation is modeled on.

The CSPRNG built here from ChaCha20 should have a
256bits security strength, and a 256 + 96 bits state
(constant and counter in ChaCha20 state don't contribute
much).

The CSPRNG is seeded on first use with the OS CSPRNG
discussed above, ensuring the state contains 256 + 96 bits
of entropy.

No effort is done to prevent data stream to repeat,
because one would expect the PRNG to have a period equal
to 2^256 - 1, meaning an insane number of call to futils_random{8,16,32,64}()
would be needed for the random number sequence actually
repeat.

Below is a crude comparison between previous implementation
and the new one, made on a x86_64 computer running Ubuntu 18.04,
made with dedicated test programs. Results are in MiB/s

                 futils_random8()  futils_random_bytes()
urandom               0.347               220.020
urandom (pool,noplt) 50.923               231.965
getrandom (p,noplt)  60.149               225.354
chacha20             70.939               398.218

Pool buffer size is kept 512 bytes as it's seems to be
the sweet spot again:

                                          PRNG speed
    +-------------------------------------------------------------------------------------+
    |     ++  +   +         +                  +                                     +    |
    |                                                  getrandom (per thread,tls) ***A*** |
    |                                                   ChaCha20 (per thread,tls) ###B### |
    |                                                                                     |
    |                                                             ###################B    |
 70 |-+                               #########B##################                      +-|
    |             B#########B#########                                                    |
    |           ##                                                                        |
    |          #                                                                          |
    |         B                                                                           |
    |       ##                                                                            |
    |      B                                                                              |
    |      #                               ****A*************************************A    |
 60 |-+    #                     **********                                             +-|
    |      #              **A****                                                         |
    |      #         *****                                                                |
    |      #      A**                                                                     |
    |      #    **                                                                        |
    |      #   *                                                                          |
    |     #   A                                                                           |
    |     #   *                                                                           |
    |     #  *                                                                            |
 50 |-+   #  *                                                                          +-|
    |     # *                                                                             |
    |     # *                                                                             |
    |     #*                                                                              |
    |     BA                                                                              |
    |      *                                                                              |
    |      *                                                                              |
    |      *                                                                              |
 40 |-+    *                                                                            +-|
    |     *                                                                               |
    |     *                                                                               |
    |     *                                                                               |
    |     A                                                                               |
    |                                                                                     |
    |     ++  +   +         +                  +                                     +    |
    +-------------------------------------------------------------------------------------+
         64  256 512      1024               2048                                  4096
          128                          pool size (bytes)

BEWARE BEWARE BEWARE BEWARE BEWARE BEWARE BEWARE BEWARE

The state *is not reseeded on fork()*, so child and parent
will produces the same random number sequences.

Also a proper CSPRNG should be reseeded regularly to
ensure the few bits leaked in its output cannot be used
to recover its state. Or in the event its state is fully
leaked somehow, prevent an attacker to recover previously
generated values.

Periodically reseeding would prevent
1) the output to actually repeat, but given the large
   internal state, this should not be a concern for
   common usage;
2) recovering internal state from leaked bits in PRNG's
   output;
3) recovering previously generated value due to full
   internal state leak through unknown mean.

Also this CSPRNG must not be used to generate a string
of random bytes larger than 256GBytes with a single call
futils_random_bytes(), due to internal counter rollover.
Doing otherwise would make the generated string repeat
each 256GBytes.

When the generated data must be kept secret and not
be recoverable through internal state leak, a key for
example, the strong implementation is required, so this
patch makes it available as futils_random_strong(). It
*must* be used when unpredictability is required for the
security to be resistant to an attacker.

BEWARE BEWARE BEWARE BEWARE BEWARE BEWARE BEWARE BEWARE
  • Loading branch information
Yves-Marie Morgan committed May 5, 2021
1 parent 90fb65b commit 0fdff8e
Show file tree
Hide file tree
Showing 2 changed files with 239 additions and 21 deletions.
15 changes: 14 additions & 1 deletion include/futils/random.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
*
* @file random.h
*
* @brief strong random functions.
* @brief fast, secure random functions.
* and strong random function.
*
******************************************************************************/

Expand All @@ -39,6 +40,18 @@
extern "C" {
#endif

/**
* @brief Fill a buffer with random bytes
*
* @param buffer buffer to fill
* @param len number of bytes to fill
*
* @return 0 buffer filled with len random bytes
* @return -EINVAL Invalid parameter
* @return other negative errno on internal error
*/
int futils_random_strong(void *buffer, size_t len);

/**
* @brief Fill a buffer with random bytes
*
Expand Down
245 changes: 225 additions & 20 deletions src/random.c
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
*
* @file random.c
*
* @brief strong random functions.
* @brief fast, secure random functions.
* and strong random function.
*
******************************************************************************/

Expand Down Expand Up @@ -151,9 +152,150 @@ static inline unsigned int ilog2plus1(uint64_t v)
return r;
}

static inline uint32_t rotl(uint32_t x, unsigned int k)
{
return (x << k) | (x >> (32 - k));
}

static inline void read_32le(const uint8_t *d, uint32_t *v)
{
*v = 0;
*v |= (uint32_t)d[0] << 0;
*v |= (uint32_t)d[1] << 8;
*v |= (uint32_t)d[2] << 16;
*v |= (uint32_t)d[3] << 24;
}

static inline void write_32le(const uint32_t v, uint8_t *d)
{
d[0] = (v >> 0);
d[1] = (v >> 8);
d[2] = (v >> 16);
d[3] = (v >> 24);
}

#define CHACHA_KEY_SIZE 32
#define CHACHA_NONCE_SIZE 12
#define CHACHA_KEY_NONCE_SIZE (CHACHA_KEY_SIZE + CHACHA_NONCE_SIZE)
#define CHACHA_BLOCK_SIZE 64

#define CHACHA_ROUNDS 20

static inline void chacha_quarterround(uint32_t x[16],
const unsigned int a,
const unsigned int b,
const unsigned int c,
const unsigned int d)
{
x[a] += x[b]; x[d] = rotl(x[d] ^ x[a], 16);
x[c] += x[d]; x[b] = rotl(x[b] ^ x[c], 12);
x[a] += x[b]; x[d] = rotl(x[d] ^ x[a], 8);
x[c] += x[d]; x[b] = rotl(x[b] ^ x[c], 7);
}

static inline void chacha_block(const uint32_t in[16],
uint8_t out[CHACHA_BLOCK_SIZE])
{
unsigned int i;
uint32_t x[16];

for (i = 0; i < 16; i++)
x[i] = in[i];

for (i = 0; i < CHACHA_ROUNDS; i += 2) {
chacha_quarterround(x, 0, 4, 8, 12);
chacha_quarterround(x, 1, 5, 9, 13);
chacha_quarterround(x, 2, 6, 10, 14);
chacha_quarterround(x, 3, 7, 11, 15);
chacha_quarterround(x, 0, 5, 10, 15);
chacha_quarterround(x, 1, 6, 11, 12);
chacha_quarterround(x, 2, 7, 8, 13);
chacha_quarterround(x, 3, 4, 9, 14);
}

for (i = 0; i < 16; i++)
x[i] += in[i];

for (i = 0; i < 16; i++)
write_32le(x[i], &out[i * 4]);
}

/* chacha20 state */
struct chacha {
uint32_t x[16];
};

static void chacha_init(struct chacha *chacha,
const uint8_t k[CHACHA_KEY_NONCE_SIZE])
{
static const uint8_t c[16] = "expand 32-byte k";

/* constant */
read_32le(&c[0], &chacha->x[0]);
read_32le(&c[4], &chacha->x[1]);
read_32le(&c[8], &chacha->x[2]);
read_32le(&c[12], &chacha->x[3]);

/* key */
read_32le(&k[0], &chacha->x[4]);
read_32le(&k[4], &chacha->x[5]);
read_32le(&k[8], &chacha->x[6]);
read_32le(&k[12], &chacha->x[7]);
read_32le(&k[16], &chacha->x[8]);
read_32le(&k[20], &chacha->x[9]);
read_32le(&k[24], &chacha->x[10]);
read_32le(&k[28], &chacha->x[11]);

/* counter */
chacha->x[12] = 0;

/* nonce */
read_32le(&k[32], &chacha->x[13]);
read_32le(&k[36], &chacha->x[14]);
read_32le(&k[40], &chacha->x[15]);
}

/* get one block of chacha20 keystream */
static inline void chacha_get(struct chacha *chacha,
uint8_t out[CHACHA_BLOCK_SIZE])
{
chacha_block(chacha->x, out);

/* counter */
chacha->x[12]++;
}

static void chacha_keystream(struct chacha *chacha,
void *buffer, size_t len)
{
uint8_t *p = buffer;

/* full blocks */
while (len >= CHACHA_BLOCK_SIZE) {

chacha_get(chacha, p);

p += CHACHA_BLOCK_SIZE;
len -= CHACHA_BLOCK_SIZE;
}

/* last partial block */
if (len) {
uint8_t tmp[CHACHA_BLOCK_SIZE];

chacha_get(chacha, tmp);

memcpy(p, tmp, len);

memset(tmp, 0, sizeof(tmp));
}
}

static int rand_fetch(void *buffer, size_t len);

struct pool {
struct chacha chacha;
unsigned int seeded;
unsigned int available;
uint8_t buffer[512];
};
Expand Down Expand Up @@ -193,6 +335,7 @@ static struct pool *pool_new(void)
if (!pool)
return NULL;

pool->seeded = 0;
pool->available = 0;

return pool;
Expand Down Expand Up @@ -258,42 +401,92 @@ static inline void pool_buffer_consume(struct pool *pool,
pool->available -= len;
}

static int pool_reload(struct pool *pool)
static int pool_seed(struct pool *pool)
{
size_t consumed;
uint8_t key[CHACHA_KEY_NONCE_SIZE];
int err;

err = rand_fetch(key, sizeof(key));
if (err) {
ULOG_ERRNO("rand_fetch()",
-err);
return err;
}

chacha_init(&pool->chacha, key);

pool->seeded = 1;

memset(key, 0, sizeof(key));

return 0;
}

static inline int pool_seed_if_needed(struct pool *pool)
{
/* seed if needed */
if (pool->seeded)
return 0;

return pool_seed(pool);
}

static void pool_reload(struct pool *pool)
{
size_t consumed;
const uint8_t *key;

consumed = sizeof(pool->buffer) - pool->available;

/* bring remaining bytes to front */
memmove(pool->buffer,
&pool->buffer[consumed],
pool->available);

err = rand_fetch(&pool->buffer[pool->available],
/* fill pool buffer with pseudorandom bytes */
chacha_keystream(&pool->chacha,
&pool->buffer[pool->available],
consumed);
if (err < 0)
return err;

pool->available = sizeof(pool->buffer);

return 0;
/* apply a new key for backtracking protection */
key = pool_buffer_get(pool, CHACHA_KEY_NONCE_SIZE);

chacha_init(&pool->chacha, key);

pool_buffer_consume(pool, key, CHACHA_KEY_NONCE_SIZE);
}

static inline int pool_reload_if_needed(struct pool *pool, size_t required)
static inline void pool_reload_if_needed(struct pool *pool,
size_t required)
{
int err;

if (pool->available >= required)
return 0;
return;

err = pool_reload(pool);
if (err < 0)
return err;
pool_reload(pool);

assert(pool->available >= required);
}

return 0;
static void pool_stir(struct pool *pool, void *buffer, size_t len)
{
struct chacha chacha;
const uint8_t *key;

/* get enough bytes for a new dedicated key */
pool_reload_if_needed(pool, CHACHA_KEY_NONCE_SIZE);

key = pool_buffer_get(pool, CHACHA_KEY_NONCE_SIZE);

chacha_init(&chacha, key);

pool_buffer_consume(pool, key, CHACHA_KEY_NONCE_SIZE);

/* stir key to fill buffer */
chacha_keystream(&chacha, buffer, len);

memset(&chacha, 0, sizeof(chacha));
}

static int pool_rand(struct pool *pool, void *buffer, size_t len)
Expand All @@ -304,16 +497,20 @@ static int pool_rand(struct pool *pool, void *buffer, size_t len)
if (!pool)
return -ENOMEM;

err = pool_seed_if_needed(pool);
if (err < 0)
return err;

/* if request is too large, write
directly to the output buffer */
if (len >= sizeof(pool->buffer))
return rand_fetch(buffer, len);
if (len >= sizeof(pool->buffer) - CHACHA_KEY_NONCE_SIZE) {
pool_stir(pool, buffer, len);
return 0;
}

/* append more bytes in the pool if
there's not enough byte remaining */
err = pool_reload_if_needed(pool, len);
if (err < 0)
return err;
pool_reload_if_needed(pool, len);

/* extract random bytes from the random pool */
ptr = pool_buffer_get(pool, len);
Expand Down Expand Up @@ -767,6 +964,14 @@ static int rand_fetch(void *buffer, size_t len)
#endif
}

int futils_random_strong(void *buffer, size_t len)
{
if (!buffer || len == 0)
return -EINVAL;

return rand_fetch(buffer, len);
}

int futils_random_bytes(void *buffer, size_t len)
{
struct pool *pool = pool_get();
Expand Down

0 comments on commit 0fdff8e

Please sign in to comment.