Skip to contents

Creates the counter-mode DRBG of NIST SP 800-90A Rev. 1, section 10.2.1, with AES-256 and no derivation function, implemented in this package (no system library). From the same entropy, personalization and additional inputs it returns the same bytes, which is what reproducible key generation and known-answer tests need; for keys meant to stay secret, seed it from random_bytes() (the default) or use random_bytes() directly.

Usage

drbg_new(entropy = NULL, personalization = NULL)

Arguments

entropy

Entropy input: exactly 48 bytes, as a raw vector or one string of 96 hexadecimal digits. NULL (the default) draws them from the operating system's CSPRNG through random_bytes().

personalization

Optional personalization string of up to 48 bytes, raw or hex; NULL (the default) is the empty string. Different personalization strings give independent streams from the same entropy.

Value

An object of class bricklayer_drbg (an environment holding the 32-byte Key, the 16-byte V, the request count and the process id). Its state (Key, V) is not printed.

Details

It reproduces all 720 AES-256 no-df vectors of NIST's DRBG validation suite (without reseeding, with reseeding, and with prediction resistance; the package's tests check a sample of them), and it is the randombytes() of NIST's rng.c, with which most post-quantum submissions wrote their known-answer files: drbg_new(as.raw(0:47)) followed by drbg_generate(d, 48) gives the seed of their vector 0. AES runs in constant time: the S-box is computed (an inversion in GF(2^8) and the affine map) rather than looked up, and on x86-64 processors with AES-NI the rounds use those instructions. Both paths are checked under valgrind memcheck with the entropy input marked secret (inst/ctcheck, case drbg-portable and drbg-aesni).

The generator is an object that changes as it is used: every drbg_generate() and drbg_reseed() advances it in place.

The generator is an environment, so copies share one state. It is not meant to be saved: a generator restored from saveRDS() or inherited by a forked process reseeds itself from the operating system on its next use, so two processes never replay the same stream (and such a stream is then no longer reproducible). Errors when entropy is not exactly 48 bytes or personalization is longer than 48 bytes, or either is neither a raw vector nor one hexadecimal string.

Security

A hand-written implementation. The standardised schemes are checked byte for byte against OpenSSL 3.5 and NIST known-answer vectors in the test suite. Side channels are checked, not assumed: inst/ctcheck runs every operation that touches a secret under valgrind memcheck with the secret marked undefined (the ctgrind method), so a branch or a memory address that depends on it is a reported error, and afterwards scans the dead stack for copies of the secret. Both checks run in CI on every change, with GCC and with Clang. Timing is also measured on x86-64 and arm64 hardware (inst/dudect) and power leakage is assessed in simulation under the value and the transition models (inst/tvla) for the masked ML-KEM and ML-DSA kernels; this scheme is not masked. These are checks of this code on those compilers; no third-party security audit has been commissioned, and the README's security section lists what each check does and does not cover.

References

NIST SP 800-90A Rev. 1 (2015). Recommendation for Random Number Generation Using Deterministic Random Bit Generators, section 10.2.1. doi:10.6028/NIST.SP.800-90Ar1

NIST FIPS 197 (2001, updated 2023). Advanced Encryption Standard (AES). doi:10.6028/NIST.FIPS.197-upd1

Examples

# NIST's rng.c, as the post-quantum known-answer files use it: vector 0's seed
d <- drbg_new(as.raw(0:47))
drbg_generate(d, 48)
#>  [1] 06 15 50 23 4d 15 8c 5e c9 55 95 fe 04 ef 7a 25 76 7f 2e 24 cc 2b c4 79 d0
#> [26] 9d 86 dc 9a bc fd e7 05 6a 8c 26 6f 9e f9 7e d0 85 41 db d2 e1 ff a1

# the same inputs give the same bytes; a personalization string separates streams
a <- drbg_new(as.raw(1:48), personalization = charToRaw("session 1"))
b <- drbg_new(as.raw(1:48), personalization = charToRaw("session 2"))
identical(drbg_generate(a, 16), drbg_generate(b, 16))
#> [1] FALSE

# seeded from the operating system
d <- drbg_new()
length(drbg_generate(d, 32))
#> [1] 32

# the entropy must be exactly 48 bytes
try(drbg_new(as.raw(1:16)))
#> Error : `entropy` must be exactly 48 bytes, raw or hex