11.4 Using the Standard
|
11.5 Using an Application-Level Generator11.5.1 Problem
You are in an environment where you do not have access to a built-in,
11.5.2 Solution
For general-purpose use, we recommend a pseudo-random number generator based on the AES encryption algorithm run in counter (CTR) mode (see Recipe 5.9). This generator has the best theoretical security assurance,
In addition, the
11.5.3 Discussion
Stream ciphers are actually cryptographic pseudo-random number generators. One major practical differentiator between the two terms is whether you are using the output of the generator to perform encryption. If you are, it is a stream cipher;
Another difference is that, when you are using a stream cipher to encrypt data, you need to be able to reproduce the same stream of output to decrypt the encrypted data. With a cryptographic PRNG, there is
The primary concern with a good cryptographic PRNG at the application level is internal state compromise, which would allow an attacker to predict its output. As long as the cryptographic algorithms used by a PRNG are not broken and the generator is not used to produce more output than it is designed to support, state compromise is generally not
The risk of state compromise is generally not a big deal when dealing with something like /dev/random , where the generator is in the kernel. The only way to compromise the state is to be inside the kernel. If that's possible, there are much bigger problems than /dev/urandom or CryptGenRandom( ) producing data that an attacker can guess.
In the
If state compromise is a potential issue, you might have to worry about more than an attacker guessing future outputs. You might also have to worry about an attacker
backtracking
, which means
In practice, few people should have to worry very much about state compromise of their cryptographic PRNG. As was the case at the operating system level, if such attacks are a realistic threat, you will usually have far bigger threats, and mitigating those threats will help mitigate this one as well. There is a lot that can go wrong when using a pseudo-random number generator. Coming up with a good construct turns out to be the easy part. Here are some things you should closely consider:
In the following three subsections, we will look at three different techniques for pseudo-random number generators: using a block cipher such as AES, using a stream cipher directly, and using a cryptographic hash function such as SHA1. 11.5.3.1 Using generators based on block ciphersIf you are in an environment where you have use of a good block cipher such as AES, you have the makings of a cryptographically strong pseudo-random number generator. Many of the encryption modes for turning a block cipher into a stream cipher are useful for this task, but CTR mode has the nicest properties. Essentially, you create random outputs one block at a time by encrypting a counter that is incremented after every encryption operation. The seed should be at least as large as the key size of the cipher, because it will be used to key a block cipher. In addition, it is useful to have additional seed data that sets the first plaintext (counter) value. Our implementation is based on the code in Recipe 5.5 and has two exported routines. The first initializes a random number generator:
void spc_bcprng_init(SPC_BCPRNG_CTX *prng, unsigned char *key, int kl,
unsigned char *x, int xl);
This function has the following arguments:
Once you have an
unsigned char *spc_bcprng_rand(SPC_BCPRNG_CTX *prng, unsigned char *buf, size_t l); This function has the following arguments:
This function never fails (save for a catastrophic error in encryption), and it returns the address of the output buffer. Here is an implementation of this generator API, which makes use of the block cipher interface we developed in Recipe 5.5:
/* NOTE: This code should be augmented to reseed after each request
/* for pseudo-random data, as discussed in Recipe 11.6
/*
#ifndef WIN32
#include <string.h>
#include <pthread.h>
#else
#include <windows.h>
#endif
/* if encryption operations fail, you passed in a bad key size or are using a
* hardware API that failed. In that case, be sure to perform error checking.
*/
typedef struct {
SPC_KEY_SCHED ks;
unsigned char ctr[SPC_BLOCK_SZ];
unsigned char lo[SPC_BLOCK_SZ]; /* Leftover block of output */
int ix; /* index into lo */
int kl; /* The length of key used to key the cipher */
} SPC_BCPRNG_CTX;
#ifndef WIN32
static pthread_mutex_t spc_bcprng_mutex = PTHREAD_MUTEX_INITIALIZER;
#define SPC_BCPRNG_LOCK( ) pthread_mutex_lock(&spc_bcprng_mutex);
#define SPC_BCPRNG_UNLOCK( ) pthread_mutex_unlock(&spc_bcprng_mutex);
#else
static HANDLE hSpcBCPRNGMutex;
#define SPC_BCPRNG_LOCK( ) WaitForSingleObject(hSpcBCPRNGMutex, INFINITE)
#define SPC_BCPRNG_UNLOCK( ) ReleaseMutex(hSpcBCPRNGMutex)
#endif
static void spc_increment_counter(SPC_BCPRNG_CTX *prng) {
int i = SPC_BLOCK_SZ;
while (i--)
if (++prng->ctr[i]) return;
}
void spc_bcprng_init(SPC_BCPRNG_CTX *prng, unsigned char *key, int kl,
unsigned char *x, int xl) {
int i = 0;
SPC_BCPRNG_LOCK( );
SPC_ENCRYPT_INIT(&(prng->ks), key, kl);
memset(prng->ctr, 0, SPC_BLOCK_SZ);
while (xl-- && i < SPC_BLOCK_SZ)
prng->ctr[i++] = *x++;
prng->ix = 0;
prng->kl = kl;
SPC_BCPRNG_UNLOCK( );
}
unsigned char *spc_bcprng_rand(SPC_BCPRNG_CTX *prng, unsigned char *buf, size_t l) {
unsigned char *p;
SPC_BCPRNG_LOCK( );
for (p = buf; prng->ix && l; l--) {
*p++ = prng->lo[prng->ix++];
prng->ix %= SPC_BLOCK_SZ;
}
while (l >= SPC_BLOCK_SZ) {
SPC_DO_ENCRYPT(&(prng->ks), prng->ctr, p);
spc_increment_counter(prng);
p += SPC_BLOCK_SZ;
l -= SPC_BLOCK_SZ;
}
if (l) {
SPC_DO_ENCRYPT(&(prng->ks), prng->ctr, prng->lo);
spc_increment_counter(prng);
prng->ix = l;
while (l--) p[l] = prng->lo[l];
}
SPC_BCPRNG_UNLOCK( );
return buf;
}
If your block cipher has 64-bit blocks and has no practical weaknesses, do not use this generator for more than 2 35 bytes of output (2 32 block cipher calls). If the cipher has 128-bit blocks, do not exceed 2 68 bytes of output (2 64 block cipher calls). If using a 128-bit block cipher, it is generally acceptable not to check for this condition, as you generally would not reasonably expect to ever use that many bytes of output.
To bind this cryptographic PRNG to the API in Recipe 11.2, you can use a single global generator context that you seed in
spc_rand_init( )
, requiring you to get a secure seed. Once that's done (assuming the generator variable is a statically allocated global variable named
spc_prng
), you can simply implement
spc_rand( )
as
unsigned char *spc_rand(unsigned char *buf, size_t l) {
return spc_bcprng_rand(&spc_prng, buf, l);
}
Note that you should probably be sure to check that the generator is seeded before calling spc_bcprng_rand( ) . 11.5.3.2 Using a stream cipher as a generator
As we mentioned, stream ciphers are
Because of the popularity of the RC4 cipher, we expect that people will prefer to use RC4, even though it does not look as good as SNOW. The RC4 stream cipher does make an acceptable pseudo-random number generator, and it is incredibly fast if you do not rekey frequently (that is particularly useful if you expect to need a heck of a lot of
RC4 requires a little bit of work to use properly, given a standard API. First, most APIs want you to pass in data to encrypt. Because you want only the raw keystream, you must always pass in zeros. Second, be sure to use RC4 in a secure manner, as discussed in Recipe 5.23. If your RC4 implementation has the API discussed in Recipe 5.23, seeding it as a pseudo-random number generator is the same as keying the algorithm. RC4 can accept keys up to 256 bytes in length.
After encrypting 256 bytes and throwing the results away, you can then, given an RC4 context, get random data by encrypting zeros. Assuming the RC4 API from Recipe 5.23 and assuming you have a context statically allocated in a global variable named spc_prng , here's a binding of RC4 to the spc_rand( ) function that we introduced in Recipe 11.2:
/* NOTE: This code should be augmented to reseed after each request
/* for pseudo-random data, as discussed in Recipe 11.6
/*
#ifndef WIN32
#include <pthread.h>
static pthread_mutex_t spc_rc4rng_mutex = PTHREAD_MUTEX_INITIALIZER;
#define SPC_RC4RNG_LOCK( ) pthread_mutex_lock(&spc_rc4rng_mutex)
#define SPC_RC4RNG_UNLOCK( ) pthread_mutex_unlock(&spc_rc4rng_mutex)
#else
#include <windows.h>
static HANDLE hSpcRC4RNGMutex;
#define SPC_RC4RNG_LOCK( ) WaitForSingleObject(hSpcRC4RNGMutex, INFINITE)
#define SPC_RC4RNG_UNLOCK( ) ReleaseMutex(hSpcRC4RNGMutex)
#endif
#define SPC_ARBITRARY_SIZE 16
unsigned char *spc_rand(unsigned char *buf, size_t l) {
static unsigned char zeros[SPC_ARBITRARY_SIZE] = {0,};
unsigned char *p = buf;
#ifdef WIN32
if (!hSpcRC4RNGMutex) hSpcRC4RNGMutex = CreateMutex(0, FALSE, 0);
#endif
SPC_RC4RNG_LOCK( );
while (l >= SPC_ARBITRARY_SIZE) {
RC4(&spc_prng, SPC_ARBITRARY_SIZE, zeros, p);
l -= SPC_ARBITRARY_SIZE;
p += SPC_ARBITRARY_SIZE;
}
if (l) RC4(&spc_prng, l, zeros, p);
SPC_RC4RNG_UNLOCK( );
return buf;
}
Note that, although we don't show it in this code, you should ensure that the generator is
Because using this RC4 API requires encrypting zero bytes to get the keystream output, in order to be able to generate data of arbitrary sizes, you must either dynamically allocate and zero out memory every time or iteratively call RC4 in
11.5.3.3 Using a generator based on a cryptographic hash function
The most common mistake made when trying to use a hash function as a cryptographic pseudo-random number generator is to continually hash a piece of data. Such an approach gives away the generator's internal state with every output. For example, suppose that your internal state is some value X, and you generate and output Y by hashing X. The next time you need random data, rehashing X will give the same results, and any attacker who
One very safe way to use a cryptographic hash function in a cryptographic pseudo-random number generator is to use HMAC in counter mode, as discussed in Recipe 6.10. Here we implement a generator based on the HMAC-SHA1 implementation from Recipe 6.10. You should be able to adapt this code easily to any HMAC implementation you want to use.
/* NOTE: This code should be augmented to reseed after each request
/* for pseudo-random data, as discussed in Recipe 11.6
/*
#ifndef WIN32
#include <string.h>
#include <pthread.h>
#else
#include <windows.h>
#endif
/* If MAC operations fail, you passed in a bad key size or you are using a hardware
* API that failed. In that case, be sure to perform error checking.
*/
#define MAC_OUT_SZ 20
typedef struct {
SPC_HMAC_CTX ctx;
unsigned char ctr[MAC_OUT_SZ];
unsigned char lo[MAC_OUT_SZ]; /* Leftover block of output */
int ix; /* index into lo. */
} SPC_MPRNG_CTX;
#ifndef WIN32
static pthread_mutex_t spc_mprng_mutex = PTHREAD_MUTEX_INITIALIZER;
#define SPC_MPRNG_LOCK( ) pthread_mutex_lock(&spc_mprng_mutex)
#define SPC_MPRNG_UNLOCK( ) pthread_mutex_unlock(&spc_mprng_mutex)
#else
static HANDLE hSpcMPRNGMutex;
#define SPC_MPRNG_LOCK( ) WaitForSingleObject(hSpcMPRNGMutex, INFINITE)
#define SPC_MPRNG_UNLOCK( ) ReleaseMutex(hSpcMPRNGMutex)
#endif
static void spc_increment_mcounter(SPC_MPRNG_CTX *prng) {
int i = MAC_OUT_SZ;
while (i--)
if (++prng->ctr[i])
return;
}
void spc_mprng_init(SPC_MPRNG_CTX *prng, unsigned char *seed, int l) {
SPC_MPRNG_LOCK( );
SPC_HMAC_Init(&(prng->ctx), seed, l);
memset(prng->ctr, 0, MAC_OUT_SZ);
prng->ix = 0;
SPC_MPRNG_UNLOCK( );
}
unsigned char *spc_mprng_rand(SPC_MPRNG_CTX *prng, unsigned char *buf, size_t l) {
unsigned char *p;
SPC_MPRNG_LOCK( );
for (p = buf; prng->ix && l; l--) {
*p++ = prng->lo[prng->ix++];
prng->ix %= MAC_OUT_SZ;
}
while (l >= MAC_OUT_SZ) {
SPC_HMAC_Reset(&(prng->ctx));
SPC_HMAC_Update(&(prng->ctx), prng->ctr, sizeof(prng->ctr));
SPC_HMAC_Final(p, &(prng->ctx));
spc_increment_mcounter(prng);
p += MAC_OUT_SZ;
l -= MAC_OUT_SZ;
}
if (l) {
SPC_HMAC_Reset(&(prng->ctx));
SPC_HMAC_Update(&(prng->ctx), prng->ctr, sizeof(prng->ctr));
SPC_HMAC_Final(prng->lo, &(prng->ctx));
spc_increment_mcounter(prng);
prng->ix = l;
while (l--) p[l] = prng->lo[l];
}
SPC_MPRNG_UNLOCK( );
return buf;
}
This implementation has two
void spc_mprng_init(SPC_MPRNG_CTX *prng, unsigned char *seed, int l); This function has the following arguments:
The second function actually produces random data: unsigned char *spc_mprng_rand(SPC_MPRNG_CTX *prng, unsigned char *buf, size_t l); This function has the following arguments:
If your hash function produces n -bit outputs and has no practical weaknesses, do not use the generator after you run the MAC more than 2 n /2 times. For example, with SHA1, this generator should be not be a problem for at least 2 80 x 20 bytes. In practice, you probably will not have to worry about this issue. To bind this cryptographic pseudo-random number generator to the API in Recipe 11.2, you can use a single global generator context that you seed in spc_rand_init( ) , requiring you to get a secure seed. Once that is done (assuming the generator variable is a statically allocated global variable named spc_prng ), you can simply implement spc_rand( ) as follows:
unsigned char *spc_rand(unsigned char *buf, size_t l) {
return spc_bcprng_rand(&spc_prng, buf, l);
}
Note that, although we don't show it in the previous code, you should ensure that the generator is initialized before giving output. 11.5.4 See AlsoRecipe 5.2, Recipe 5.5, Recipe 5.9, Recipe 5.23, Recipe 6.10, Recipe 11.2, Recipe 11.6, Recipe 11.8, Recipe 11.16 |