50 min read

A Key Is Only as Unguessable as the Dice That Made It: Inside the Windows CSPRNG

Every software secret Windows makes is drawn from one CSPRNG. Trace the entropy, the SP 800-90A CTR_DRBG, ProcessPrng, and where the OS stops rolling the dice.

Permalink

1. Two Lines of Code That Never Touched the Cipher

In 1996, two Berkeley graduate students broke the encryption on every Netscape SSL session without laying a finger on the cipher. Ian Goldberg and David Wagner never factored a modulus or attacked RC4. They reverse-engineered the browser and found that the "random" seed feeding each session key depended on just three quantities: the time of day, the process ID, and the parent process ID [6]. Guess those three numbers -- and on a shared Unix host you very nearly could -- and you reconstruct the key directly.

Twelve years later, a change to Debian's OpenSSL did the same thing to a large slice of the internet at once. A well-meaning patch commented out the code that mixed extra data into the key generator's entropy pool, so the private keys a machine produced were drawn from little more than its process ID [7]. Every SSH host key, TLS certificate, and OpenVPN key generated on an affected system fell into a tiny, enumerable set, and the flaw sat in shipping code from September 2006 until its disclosure in May 2008 [8].

Notice what did not fail in either case. RSA was fine. RC4 was fine. The math was flawless and the keys were still guessable, because a key is only as unguessable as the randomness underneath it. That is the single idea this article is built on, and it is not a story about Netscape or Debian. It is the story of every secret your operating system makes.

On Windows, that surface is enormous. DPAPI master keys, BitLocker volume keys, TLS session keys, and machine-account passwords are all drawn -- when they are generated in software -- from a single cryptographic random number generator buried in the kernel. Their security bottoms out on how well Windows rolls the dice. Get that generator right and a thousand higher-level protocols inherit unpredictability for free. Get it wrong, once, and every one of them silently weakens at the same time.

Goldberg and Wagner's 1996 write-up derives the figure directly: with only the wall-clock second known (from a packet sniffer) but the microseconds, process ID, and parent process ID unknown, the seed retains "at most, 47 bits of randomness in the secret key," and in practice often far fewer [6]. The Debian break is usually summarized as collapsing keys to "about 32,768 possibilities." That number is a derived consequence, not text from the advisory: the change commented out two MD_Update calls that fed the pool, leaving process-ID as effectively the only varying input, and a 15-bit PID space is 2152^{15}, or 32,768 values [7, 8].

So the questions that organize the rest of this piece almost ask themselves. What would it actually take to build randomness an attacker cannot guess? Has Windows ever failed at it? And when it rebuilt, what design did it land on -- and where does that design stop?

Ctrl + scroll to zoom
Two decades of randomness failures forced the redesign of the modern Windows generator

2. What "Random" Has to Mean

Here is a claim that sounds wrong the first time you hear it: passing every statistical randomness test in existence is not good enough for keys. A sequence can be perfectly balanced, survive every chi-squared and spectral test you throw at it, and still be catastrophic to build a secret on.

The Mersenne Twister makes the point. It is a superb general-purpose generator with a period of 21993712^{19937}-1 and excellent statistical properties, and it is the default rand-style source in countless languages. Its own authors are blunt about the limit: "Mersenne Twister is basically for Monte-Carlo simulations. It is not cryptographically secure as is" [9]. The reason is that its internal state is a linear function of its output, held in a working area of just 624 words [10]. Observe 624 consecutive 32-bit outputs and you can solve for the entire state, then predict every future value. It looks random to a statistician and is transparent to a cryptanalyst.

That gap forces a three-way distinction that the rest of this article leans on constantly.

Entropy source (TRNG)

A physical process that produces genuine unpredictability -- thermal noise, ring-oscillator jitter, the timing of hardware interrupts. A true random number generator (TRNG) samples such a process. It is slow, sometimes biased, and cannot be reproduced from a formula, which is exactly why it is trusted as a source of fresh unpredictability.

Deterministic Random Bit Generator (DRBG)

An algorithm that stretches a short seed into a long stream of output bits. Given the same seed it always produces the same stream, so it creates no new unpredictability; it spreads out the unpredictability already present in the seed. NIST SP 800-90A defines the DRBG mechanisms Windows uses [11].

Cryptographically secure PRNG (CSPRNG)

A DRBG whose output no efficient adversary can distinguish from uniformly random bits, even after seeing any feasible, polynomially bounded amount of prior output. The Mersenne Twister is a PRNG but not a CSPRNG; a CTR_DRBG is designed to be one.

A real system needs both halves: an entropy source to supply unpredictability, and a DRBG to turn a trickle of it into all the fast, uniform bytes applications demand. Confusing the two is how you ship a generator that benchmarks beautifully and leaks keys.

PropertyEntropy source (TRNG)CSPRNG (secure DRBG)Non-crypto PRNG
Origin of bitsphysical noisea seed plus an algorithma seed plus an algorithm
Reproducible from state?noyesyes
Speedslowfastfast
Safe for keys?yes, but rate-limitedyesno
ExampleRDSEED, interrupt jitterAES CTR_DRBG, ChaCha20Mersenne Twister, C rand()

The bar that replaced "looks random"

What separates a CSPRNG from a Mersenne Twister was pinned down theoretically before either Windows generator existed. In 1982, Manuel Blum and Silvio Micali introduced the next-bit unpredictability criterion: a generator is cryptographically strong if no efficient adversary, given any prefix of the output, can predict the next bit with advantage better than negligible. They did more than state the bar -- they showed it was reachable, constructing a PRG whose output meets it as long as a specific hard problem (the discrete logarithm) stays hard, via a hardcore predicate that is provably as difficult to guess as inverting a one-way function [12]. It was the first pseudorandom generator with a security proof rather than a hope.

That same year, Andrew Yao proved why the next-bit test is the right bar: a generator passes it if and only if its output is computationally indistinguishable from a uniform random string -- no efficient statistical test of any kind can tell them apart [13]. Unpredictability and indistinguishability are the same property. Yao's theorem generalized the Blum-Micali criterion into the definition of "cryptographically random," replacing the vague "looks random" with something you can reason about. Every DRBG since, including Windows' CTR_DRBG, is a descendant of that idea.

Min-entropy

The worst-case measure of unpredictability. For a source XX, the min-entropy is H(X)=log2maxxPr[X=x]H_\infty(X) = -\log_2 \max_x \Pr[X = x] -- it counts only the single most likely outcome. If the likeliest value has probability one in a thousand, the source has just under 10 bits of min-entropy, no matter how the rest of the distribution looks. Min-entropy upper-bounds the security of everything drawn from a seed.

Two derived properties matter so much that the entire modern design is organized around them, and every generator in this article is graded on both.

Backtracking resistance (forward secrecy)

If an attacker compromises the generator's state right now, they still cannot recover the output it produced in the past. The state update must be a one-way step. In NIST's vocabulary this is "backtracking resistance"; the academic literature usually calls the same property "forward security."

Prediction resistance

If an attacker learns the state now, fresh entropy mixed in afterward makes future output unpredictable again. Prediction resistance is what lets a generator recover from a compromise, and it is the entire reason a generator reseeds rather than running forever on its first seed.

You can watch why a weak seed is fatal in a few lines. When the seed comes from a small space, an attacker does not attack the cipher; they enumerate the seeds and derive the key directly, exactly as in the Debian case.

JavaScript A strong key on a small seed space is not strong
// A toy key derivation. Pretend the hex output is a real 256-bit key.
function keyFromSeed(seed) {
let h = seed >>> 0;
h = Math.imul(h, 2654435761) >>> 0;   // a deterministic mix
h = (h ^ (h >>> 15)) >>> 0;
return h.toString(16).padStart(8, "0");
}

// The Debian OpenSSL bug effectively drew the seed from the PID space:
// 2^15 = 32768 possibilities per key type, not 2^256.
const PID_SPACE = 32768;
const victimSeed = 20443;                 // the victim's unknown PID
const targetKey  = keyFromSeed(victimSeed);

let guesses = 0, recovered = null;
for (let pid = 0; pid < PID_SPACE && recovered === null; pid++) {
guesses++;
if (keyFromSeed(pid) === targetKey) recovered = pid;
}

console.log("target key:", targetKey);
console.log("recovered the seed in", guesses, "guesses");
console.log("keyspace searched: 2^15 =", PID_SPACE, "-- trivially enumerable");

Press Run to execute.

Windows built a generator on exactly these terms, CryptGenRandom, the generator it shipped in the CryptoAPI. Three researchers later took the Windows 2000 build of it apart -- and found it failed the first property outright.

3. The First Windows Generator, and How It Broke

In 2007, Leo Dorrendorf, Zvi Gutterman, and Benny Pinkas did something Microsoft had never published: they reconstructed the algorithm behind Windows' random number generator by reverse-engineering a Windows 2000 binary, with no help from the vendor. Then they broke it [2]. The target was CryptGenRandom, the generator that at the time sat under essentially every Windows secret, and the flaws they found were structural, not arithmetic.

Three design choices did the damage. First, the generator ran entirely in user mode, with a separate copy of its state living inside each process's own address space. Second, part of that state at startup was simply whatever happened to be sitting on the stack. Third, and most damaging, the generator pulled fresh system entropy into its state only after it had produced 128 KB of output. Between those refreshes, the output was a pure deterministic function of a state that never changed its unpredictability.

With the algorithm in hand, the attack followed. Given the generator's current internal state, the researchers showed the previous state could be recovered in about O(223)O(2^{23}) work -- a break of forward security -- while running the generator forward to predict future output was trivial, O(1)O(1) [2]. Because state was per-process and reseeded so rarely, a single state leak was not a local event. It reached backward and forward across the whole 128 KB window.

"Learning a single state may reveal 128 Kbytes of the past and future output of the generator." -- Dorrendorf, Gutterman, and Pinkas, 2007 [2]

For a process terminating a TLS connection, 128 KB of predictable output is more than enough to expose the session keys it just generated, or is about to. The cipher suite did not matter. The break was in the dice.

Ctrl + scroll to zoom
Why one leaked CryptGenRandom state exposed a process's past and future output
The asymmetry is worth savoring: predicting future output from a known state was O(1)O(1), because that is just running the generator, while recovering past output cost O(223)O(2^{23}) -- the work of inverting one step. A truly forward-secure design makes the second number astronomically large. CryptGenRandom made it a rounding error [2].

Two points of discipline before we move on. This break is strictly a property of the legacy CryptGenRandom, the generator Windows shipped in its CryptoAPI, analyzed here in its Windows 2000 build. It is not the modern CTR_DRBG design that replaced it, and nothing here should be read as a live vulnerability in a current Windows system.

The value of the story is that it names, concretely, the four things a redesign had to fix: get the state out of user-mode process memory, reseed far more often than every 128 KB, stop seeding from uninitialized stack data, and make the state update genuinely one-way so a leak cannot reach into the past.

The fix looked obvious on paper. Move the state into the kernel, reseed on a schedule, make it forward-secure. But at the very moment Microsoft was redesigning, a deeper and more unsettling fear surfaced across the whole field. What if the weak point was not the code at all -- what if the algorithm itself, blessed by a standards body, was the trap?

4. The Dual_EC_DRBG Shadow

In 2006, NIST standardized four DRBG mechanisms in Special Publication 800-90: Hash_DRBG, HMAC_DRBG, CTR_DRBG, and a fourth built on elliptic curves called Dual_EC_DRBG [14, 15]. Three of them were ordinary constructions built from hash functions and block ciphers. The fourth carried a landmine, and in August 2007 two Microsoft cryptographers walked up to the CRYPTO rump session and pointed at it.

Dan Shumow and Niels Ferguson showed that Dual_EC_DRBG is defined by two elliptic-curve points, PP and QQ, whose relationship is a secret. If anyone knows a scalar dd with dP=Qd\cdot P = Q, that number is a skeleton key. In their experiments, "32 bytes of output was sufficient to uniquely identify the internal state" -- after which every future output is predictable [3]. The constants shipped in the standard, and no one outside their authors could say how PP and QQ had been chosen. The generator could be perfect for everyone except the one party holding dd.

They were scrupulous about what they were and were not claiming, and their exact wording is a model of how to raise an alarm honestly.

"WHAT WE ARE NOT SAYING: NIST intentionally put a back door in this PRNG. WHAT WE ARE SAYING: The prediction resistance of this PRNG (...) is dependent on solving one instance of the elliptic curve discrete log problem." -- Shumow and Ferguson, 2007 [3]

For years this stayed a theoretical worry. Then in 2013, reporting by Joseph Menn at Reuters, drawing on the Snowden documents, alleged that RSA Security had accepted a secret $10 million contract that set Dual_EC_DRBG as the default generator in its widely used BSAFE toolkit [16]. The theoretical trapdoor now had a deployment path into real products.

NIST reopened the standard for comment in September 2013 and, in 2014, formally removed Dual_EC_DRBG, keeping "three of the four previously available options" and urging users to "transition (...) as quickly as possible" in response to "the lack of public confidence" [15]. A standards body had withdrawn one of its own algorithms because it could no longer vouch for it.

Where did Windows sit in this? The precise, defensible statement is this: Windows shipped Dual_EC_DRBG as a non-default SP 800-90 option -- available if an application explicitly asked for it, but never the system default -- and later removed it [17]. That is a materially different posture from BSAFE, where the reporting alleges it was the default. Naming this accurately matters more than scoring a point: the lesson of Dual_EC is not "vendor X was careless," it is that trust in a generator bottoms out on its algorithm and its constants, not merely on whether the code has bugs.

So the field learned the same lesson twice in the span of a few years, from two directions. CryptGenRandom proved that a generator's structure could betray it. Dual_EC proved that a generator's algorithm and constants could betray it. When Microsoft rebuilt from scratch, it had to answer both at once: which mechanism do you pick, and how do you arrange the whole system so that no single component -- no algorithm, no constant, no hardware chip -- can quietly own the result?

5. The Modern Architecture: One Root, a Strict Chain of Generators

The redesign that answers both failures fits in a single sentence, and it is worth stating before the diagram. Ferguson's 2019 whitepaper on the Windows generator [19] puts it flatly:

"All PRNGs in the system are SP800-90 AES_CTR_DRBG with 256-bit security strength using the df() function." -- Niels Ferguson, The Windows 10 random number generation infrastructure, 2019 [1]

Every generator in Windows is the same mechanism, an AES CTR_DRBG at the maximum 256-bit strength. What differs is how they are wired together. Before the wiring, here is the road that led to it.

GenerationRepresentative designWhy it was superseded
G0 -- non-crypto PRNGC rand(), Mersenne TwisterLinear state recoverable from output [9]
G1 -- ad-hoc app CSPRNGNetscape SSL seed from clock and PIDToo little entropy; seeds enumerable [6]
G2 -- legacy CryptGenRandomUser-mode, per-process DRBGForward-security break; reseed only per 128 KB [2]
G3 -- SP 800-90 DRBG menuStandardized CTR_DRBG and siblingsA menu is not an architecture; Dual_EC withdrawn [15]
G4 -- modern Windows treeRoot CTR_DRBG seeding a strict chain of per-processor DRBGsCurrent design [1]

The wiring is a strict, one-way chain. At the top sits a single root generator that lives in kernel mode, maintained by the CNG.SYS driver, whose primary job is to seed other generators -- in the rare case a per-processor state cannot be allocated, a kernel-mode request is served directly from the root [1]; it never hands bytes to an application. Ferguson's whitepaper is unambiguous about its reach: "All random bytes in the system are derived in some way from this PRNG" [1]. Below the root are the kernel per-processor generators, also in CNG.SYS, allocated on demand and seeded from the root.

Each user-mode process then gets a process-base generator, maintained by BCryptPrimitives.dll; when that DLL loads it "requests a random seed from kernel mode, where it is produced by the per-CPU states" [1], so it is seeded from the kernel per-processor generators, not directly from the root. Finally, the user-mode per-processor generators that actually service your calls are created on demand and seeded from the process-base generator.

So the seeding lineage runs in exactly one direction, top to bottom, and only that last layer hands bytes to your code. A request for randomness draws from the bottom of the chain; fresh seed material only ever flows downward from the top. There is no diamond and no shortcut -- a user-mode generator can never read the root or the kernel state directly.

Ctrl + scroll to zoom
The modern Windows generator: a root CTR_DRBG seeds a strict one-way chain of per-processor CTR_DRBGs

Why per-processor, and why a root that only seeds? Three reasons, each a direct answer to a past failure. A per-processor generator needs no lock on the hot path, so thousands of threads can draw bytes at once without contending. Each processor's state is isolated from the others, so leaking one tells you nothing about the rest. And because each layer is seeded from the one above rather than sharing its state, a compromise of a user-mode generator on one core cannot walk back up to the kernel generator or across to another core.

The reachability that let one CryptGenRandom state expose 128 KB of a process's keys is designed out.

How a CTR_DRBG actually works

CTR_DRBG

The SP 800-90A DRBG built from a block cipher run in counter mode. Its state is a cipher key KK and a counter VV. To produce output it encrypts successive counter values under KK; after each request it runs an update step that replaces both KK and VV. Windows uses AES with a 256-bit key, giving a combined seed length of 384 bits [20].

Mechanically it is simpler than its reputation. The state is a key KK and a counter block VV. To generate output, the DRBG increments the counter before each block and encrypts the result under AES with key KK: the first output block is AES(K,V+1)\text{AES}(K, V+1), the next is AES(K,V+2)\text{AES}(K, V+2), and so on, exactly like running AES in counter mode. Because the increment comes first, the block for the current VV is never emitted. For AES-256 the seed length -- the size of KK and VV together -- is 384 bits: a 256-bit key plus a 128-bit counter block [20].

The step that earns the "cryptographically secure" label comes after the bytes are handed back. The DRBG runs an update that derives fresh values for KK and VV and overwrites the old ones. Once that happens, the state that produced your bytes no longer exists anywhere. Recovering it would mean inverting AES, which is exactly the work the cipher is built to make infeasible. That single overwrite is what gives the whole chain its backtracking resistance -- the property CryptGenRandom lacked.

Ctrl + scroll to zoom
Every CTR_DRBG request ends by overwriting its own key and counter, which is what gives forward secrecy

Before those raw entropy bytes ever become a key KK, they pass through a conditioning step the standard calls the derivation function.

Derivation function (df)

The conditioning step that turns raw entropy of uneven quality into a seed of exactly the length and distribution the DRBG needs. It absorbs more input bits than it emits, spreading whatever unpredictability arrived across every output bit, so no structure in the raw source survives into the seed [11].

You can feel the forward-secrecy property in a few lines. The key move is that the update overwrites the state with the output of a one-way step, so a later leak cannot walk backward.

JavaScript Forward secrecy by erasing the state after every draw
// A toy generator with a one-way update, mirroring the CTR_DRBG idea.
// Real Windows uses AES; here a simple one-way mix stands in for it.
function mix(x) {
let h = x >>> 0;
h = Math.imul(h ^ 0x9e3779b9, 2246822519) >>> 0;
h = (h ^ (h >>> 13)) >>> 0;
return h >>> 0;
}

let state = 123456789;                 // the secret internal state

function generate() {
const output = mix(state ^ 0x0000abcd); // the bytes we hand out
state = mix(state);                      // update: overwrite the state
return output >>> 0;
}

const out1 = generate();
const out2 = generate();
const leakedNow = state;               // attacker compromises the state here

console.log("output 1:", out1.toString(16));
console.log("output 2:", out2.toString(16));
console.log("state leaked after two draws:", leakedNow.toString(16));
console.log("to recover out1 the attacker must invert the update -- that is forward secrecy");

Press Run to execute.

The implementation behind all of this is SymCrypt, which the project describes as "the primary crypto library for all algorithms" in Windows since the Windows 10 1703 release, engineered to "support FIPS 140 certification" and to "provide high assurance" [21]. The generator is not a bespoke one-off; it is part of the same audited library that implements the rest of Windows cryptography.

This chain-of-generators shape now has a standardized name. NIST's SP 800-90C, finalized on 25 September 2025, calls a chain of RBGs on one platform -- each an RBG (a DRBG mechanism plus its seeding interface), one feeding the next -- an RBGC construction, the standardized shape that the Windows root-to-per-processor arrangement instantiates as CTR_DRBGs [22, 23]. That standard was finalized six years after Ferguson's 2019 whitepaper documented the Windows tree, so the correspondence is a retrospective analogy rather than a certified conformance to SP 800-90C. We return to the full SP 800-90 construction family, and what it can and cannot promise, in Section 10.

The architecture facts in this article -- the chain, the 256-bit CTR_DRBG, the df() seeding, and the reseed numbers in the next section -- are quoted from Ferguson's whitepaper, which is a Windows 10 document published in October 2019. They are accurate as documented in 2019; Microsoft has not published a newer primary of comparable detail, so treat the exact figures as of that vintage [1].

Line the redesign up against the two failures. CryptGenRandom ran in user mode; the modern root and kernel generators live in the kernel, out of any process's reach. CryptGenRandom reseeded only every 128 KB; the modern chain reseeds on a clock schedule, which the next section unpacks. CryptGenRandom let a leaked state expose the past; the CTR_DRBG update overwrites KK and VV so the past is gone. And against Dual_EC, the mechanism itself is AES in counter mode, whose security rests on the block cipher rather than on two mysterious curve points nobody can audit.

A DRBG cannot create unpredictability. It can only stretch a seed. So a Windows key is only as unguessable as the entropy behind that seed, which is why the entire architecture is built to protect and diversify the seed rather than to strengthen the cipher.

A CTR_DRBG is deterministic: feed it the same seed and it produces the same "random" stream forever. Everything, then, rests on one question the whole design has been circling. Where does the unpredictability in that seed actually come from?

6. Where the Entropy Comes From

A deterministic generator cannot manufacture unpredictability. It can only spend what it is given. So the design problem is not "how do we make randomness" -- the CTR_DRBG does the stretching -- it is "how do we collect genuine unpredictability, from sources diverse enough that no one of them can quietly poison the well." Windows answers by gathering entropy from everywhere it can reach and trusting no single source in particular.

The whitepaper enumerates the sources, and the list is deliberately not exhaustive [1]. The primary source is the timing jitter of hardware interrupts, sampled with the CPU's cycle counter via the RDTSC instruction: the exact cycle at which each interrupt fires is influenced by physical processes an outside attacker cannot observe or steer.

Alongside it, Windows pulls bytes from the TPM (documented as 40 bytes at boot and 64 bytes per reseed, at most once every 40 minutes or so), from Intel's on-die RDRAND and RDSEED instructions, from a seed file kept in the registry and rewritten on each boot to carry entropy forward across restarts, from UEFI and firmware seeds, and on virtual machines from the Hyper-V ACPI OEM0 table [1].

SourceTypeCadence (as documented in 2019)Trust note
Interrupt timing via RDTSCtiming jittercontinuous; the primary sourceHard to observe or steer remotely [1]
TPMhardware RNG40 bytes at boot, 64 bytes per reseed, at most once per ~40 minSeparate chip, independent of the CPU [1]
RDRAND / RDSEEDon-die hardware RNGon demandRDRAND capped at 128-bit security; RDSEED preferred; never trusted alone [1]
Registry seed filestored entropyread at boot, rewritten for next bootCarries entropy across reboots [1]
UEFI / firmware seedfirmware-providedat bootPlatform-dependent [1]
Hyper-V ACPI OEM0 tablehypervisor-providedat boot on guestsPresent for virtualized machines [1]

Mix many, trust none

This is the direct, engineered answer to the Dual_EC fear. Consider Intel's RDRAND: it is fast, on-die, and convenient, and a lazier design would simply use it as the system's randomness. Windows does not. The whitepaper notes that "the RDRAND instruction only provides random numbers with a 128-bit security level," so Windows uses RDSEED "in preference to RDRAND" and, more importantly, folds it in as one input among many rather than relying on it alone [1].

The underlying Intel design is itself an entropy source feeding a conditioner and a CTR_DRBG [24] -- a whole pipeline sealed inside a chip you cannot audit. Mixing it with five other independent sources means that even if RDRAND were compromised or backdoored, it could not by itself determine the seed.

The 128-bit ceiling is the tell. Windows targets 256-bit security throughout, so a source that tops out at 128 bits can never be the whole seed without halving the system's strength. That is a concrete, non-conspiratorial reason to prefer RDSEED and to always blend -- the mathematics forces diversification before trust ever enters the picture [1].
Ctrl + scroll to zoom
Many independent sources fan into SHA-512 pools, so no single source can own the seed

The mixing itself is a hash. Sources are folded with SHA-512 into as many as 8 entropy pools, and the very first seeding happens early, in the boot loader. During Winload, Windows hashes the available sources with SHA-512, uses the result to seed an AES CTR_DRBG, and hands 48 bytes to CNG so the kernel generator is already seeded before any user code runs [1].

There is no window in which Windows serves keys from an unseeded generator -- the boot-time hole that broke so many Unix devices. The design of these entropy sources, their health tests and min-entropy estimates, is governed by NIST SP 800-90B, the companion standard to the DRBG document [25].

That completes the second shift in how to think about this machine: security does not come from finding one perfect source of randomness. It comes from combining many imperfect ones so that trusting any single component is never necessary. Distrust is the feature.

Collecting entropy once, though, is not enough. A generator that seeds at boot and never refreshes slowly drifts back toward the failure that killed CryptGenRandom -- a long-lived state that, once leaked, stays leaked. So when, and how often, does Windows reseed? And what happens the moment two machines start life from the exact same state?

7. Reseeding, Forward Secrecy, and the VM-Clone Problem

Reseeding is easy to misunderstand as "topping up" a tank of randomness that slowly drains. It is not. A CTR_DRBG never runs out of output. Reseeding exists for a different reason: it is how a generator recovers from a compromise it may never know occurred. If an attacker somehow learns the state, mixing in fresh entropy afterward restores unpredictability going forward. That is prediction resistance, and it is why reseeding is on a schedule rather than an afterthought.

The schedule, as documented in the 2019 whitepaper, is descended from Fortuna -- the multi-pool accumulator Ferguson co-designed as a successor to Yarrow [26], which itself grew out of a 1998 taxonomy of how real pseudorandom generators get broken in the field [27]. The first reseed is scheduled about 1 second after boot. Each subsequent interval is tripled, up to a cap of 3600 seconds -- one hour -- with roughly 33% jitter so the timing is not perfectly regular [1].

Underneath sits a bank of up to 8 SHA-512 entropy pools, and pool kk is drained into a reseed only every 3k3^k-th time. Pool 0 contributes to every reseed; pool 1 to every third; pool 7 only very rarely. The effect of that geometric schedule is subtle and clever: a fast, low-rate attacker who is quietly injecting predictable "entropy" cannot keep all pools poisoned, because the high-numbered pools accumulate real entropy over long stretches before they are ever used [1].

Map this onto the two properties from Section 2. The CTR_DRBG update gives backtracking resistance: a leaked state cannot reveal past output. Reseeding gives prediction resistance: a leaked state cannot predict output past the next reseed. Together they bound the blast radius of a state compromise in both directions of time. The standard also caps a single CTR_DRBG at 2482^{48} generate requests before a reseed is mandatory, though Windows' clock schedule reseeds far more often than that ceiling would ever require [20].

That 2482^{48} figure is a hard limit in SP 800-90A, not a Windows tuning choice. At any realistic request rate the one-hour clock reseed fires enormously sooner, so the ceiling is a safety backstop rather than an operational parameter [20].

The failure reseeding cannot fully fix

There is one deployment scenario that defeats even a perfect reseed schedule, and it is the sharpest residual edge in the whole design. If you clone a virtual machine, or revert one to a snapshot, you duplicate its memory -- including the generator's state. Two machines that believe they are independent now hold the identical KK and VV, and they will produce the identical "random" stream. They roll the same dice.

This is not hypothetical. Thomas Ristenpart and Scott Yilek demonstrated in 2010 that resuming a VM from a snapshot replays RNG state and causes servers to emit the same "random" TLS and DSA values twice, leaking keys [28, 29]. Everspaugh and colleagues extended it to the Linux system RNG in 2014, measuring generators on Xen, VMware, and EC2 that would "output the exact same sequence of bits each time it is resumed from the same snapshot" [30].

VM Generation ID

A 128-bit value supplied by the hypervisor that changes whenever a virtual machine experiences a time-shift event -- a snapshot restore, a clone, or a backup restore. Guest software reads it, and a change signals that the machine may be a duplicate that must reseed. Introduced with Windows 8 and Windows Server 2012 [31].

Windows' defense is to listen for exactly that event. The VM Generation ID is a 128-bit identifier the hypervisor changes on any time-shift [31]. The whitepaper's "virtual machine rewind" logic detects the change, retrieves "a unique (not random) value from the hypervisor," and forces the root generator to reseed so the two instances diverge [1]. Crucially, Ferguson is candid that this "does not eliminate the vulnerability" [1] -- it narrows it.

Ctrl + scroll to zoom
A VM clone duplicates generator state until a changed Generation ID forces a reseed

Where does the residual risk live? In two gaps. First, in the sliver of time after a clone but before the reseed fires, any secret generated draws on duplicated state. Second, on hypervisors that expose no Generation ID at all, the signal never arrives and the duplication is silent. Precisely quantifying that window for current Windows is not something any primary source pins down -- this is a reasoned consequence of the reseed-on-rewind mechanism and the reset results above, not a measured figure. The honest summary is that the VM-clone problem is mitigated, not solved.

All of this machinery lives in the kernel and the boot loader, far below anything an application sees. So how does ordinary code -- your code -- actually reach down and pull bytes out of it?

8. The User-Mode Path: ProcessPrng and BCryptGenRandom

Everything so far has been under the floorboards. Here is the single line of code that touches it -- and, just as important, the three that you must never write.

The modern entry point is ProcessPrng, exported from bcryptprimitives.dll through the CngRngExt API set. Microsoft's documentation is refreshingly terse: it "retrieves a specified number of random bytes from the user-mode per-processor random number generator," and it "always returns TRUE" [4]. There is no failure path to check and no entropy to supply, because the generator it reads from is one of the user-mode per-processor CTR_DRBGs from Section 5, seeded during your process's startup from the kernel chain (the process-base generator requests a kernel seed when bcryptprimitives.dll loads, and the kernel chain itself was seeded at boot, before any process ran). You ask for bytes; you get unpredictable bytes.

Most code should call BCryptGenRandom, the recommended CNG randomness API. Two flags shape its behavior, and one of them is a trap. Passing BCRYPT_USE_SYSTEM_PREFERRED_RNG selects the system's preferred generator, and when you use it the algorithm handle must be NULL [5]. The trap is BCRYPT_RNG_USE_ENTROPY_IN_BUFFER, which once let a caller stir its own bytes into the request: the documentation states plainly that "this flag is ignored in Windows 8 and later" [5]. Your hand-supplied entropy does nothing, and the API quietly stopped pretending otherwise.

The older names still work, because they were rerouted rather than reinvented. RtlGenRandom -- exported from advapi32.dll as SystemFunction036 -- now carries a Microsoft note telling callers to "use the BCryptGenRandom or ProcessPrng functions" instead [32], and Ferguson's whitepaper states outright that "RtlGenRandom uses the ProcessPrng function" [1]. The legacy CryptGenRandom and the C runtime's rand() are deprecated for anything security-sensitive -- the first for its history, the second because it was never a CSPRNG at all.

On .NET, System.Security.Cryptography.RandomNumberGenerator wraps this same machinery: on Windows its implementation calls BCryptGenRandom(IntPtr.Zero, ..., BCRYPT_USE_SYSTEM_PREFERRED_RNG), so managed code inherits the identical guarantees without touching a P/Invoke [33, 5].

The head-to-head is worth seeing in one place, because it shows that the interesting differences are architectural, not about who has the better cipher.

DimensionWindows CNG chainLinux getrandomOpenBSD/FreeBSD arc4randomFortuna (pattern)
Output coreAES CTR_DRBG, 256-bitChaCha20ChaCha20block cipher
Forward secrecyDRBG update stepfast-key-erasurefast-key-erasureDRBG update
Concurrencyper-processor stateper-CPU stateper-process poolnot specified
VM-reset defenseVM Generation ID reseedimproving over timereseed on fork()not specified
Primary APIBCryptGenRandom / ProcessPrnggetrandom(2)arc4random_bufnot applicable

Sources for the table: the Windows column from Ferguson's whitepaper and the CNG docs [1, 4]; Linux from getrandom(2) [34]; OpenBSD/FreeBSD from arc4random(3) [35, 37]; Fortuna from its design page [18]. The standout row is VM-reset defense: Windows is unusual in having an explicit hypervisor-signaled reseed, a direct consequence of the clone problem in the last section.

One API, one per-processor CTR_DRBG, one shared foundation. Which means we can finally close the loop: every secret Windows makes in software is drawing from the same well. Let us follow the pipes and see exactly which secrets those are -- and the one place the pipes do not reach.

9. How Every Windows Secret Draws on This -- and Where It Doesn't

Now the loop closes. Trace almost any Windows secret back far enough and you arrive at the same per-processor CTR_DRBG from Section 5. The generator is not one feature among many; it is the shared root of the whole key hierarchy.

Here is the claim stated precisely, because precision is what makes it defensible rather than a slogan. BCryptGenRandom and ProcessPrng are the CNG software CSPRNG -- Cryptography API: Next Generation, the subject of the companion post on CNG architecture, has no second random source hiding behind them. And the whitepaper is explicit that the root generator's reach is total: "All random bytes in the system are derived in some way from this PRNG" [1, 4].

So the linkage from any subsystem to the Section 5 chain is architectural, not a claim that each component calls BCryptGenRandom at some named line: on modern Windows, CNG is the sole software CSPRNG, so any key or nonce generated in software bottoms out in that chain by construction.

With that framing, the dependencies fan out, and each one has a primary source confirming its key material is randomly generated.

  • DPAPI and DPAPI-NG, the credential vault under nearly every stored Windows secret: classic DPAPI builds on a MasterKey that Microsoft documents as "512 bits of random data," and each protection operation derives a session key from that MasterKey plus "16 bytes of random data" [38]. DPAPI-NG's key hierarchy differs, but it likewise draws its key material from the same CNG CSPRNG. Either way, that randomness is CNG's.
  • BitLocker generates its Volume Master Key and Full Volume Encryption Key when a volume is first encrypted in software, wrapping the FVEK under the VMK in the key hierarchy Microsoft documents [39]; that key material, like all software randomness on the system, is drawn from the CNG CSPRNG [1].
  • Schannel, the Windows TLS stack [40], pulls per-handshake randomness and ephemeral (EC)DHE secrets on every connection through the same CryptoAPI/CNG plumbing [1].
  • Machine-account passwords and computer-object secrets bottom out in the same place. Randomly-generated (version-4) GUIDs, when produced through CNG, inherit its randomness, but GUIDs are not in general guaranteed to be unpredictable and should not be relied on as security tokens [41].

Break the generator and you do not break one of these -- you weaken all of them at once, which is precisely why the design invests so heavily in protecting the seed. The DPAPI, BitLocker, and Schannel companion posts trace each of these key hierarchies in detail.

That is the thesis, and it would be an overstatement if it stopped there. So here is the boundary that keeps it honest, and naming it is what makes the universal claim true rather than sloppy.

Every software secret Windows generates draws on one CSPRNG. But keys born inside a TPM or a smart card are generated by that device's own hardware random number generator -- the CNG software CTR_DRBG is not in that path. That boundary is exactly where "the OS rolls the dice" stops and the hardware rolls its own.

When you generate a key through the Platform Crypto Provider -- the TPM key storage provider -- or through a smart-card provider, the private key can be created and kept on the device and never exist as software-accessible bytes. The randomness for that key comes from the TPM's or the card's on-chip RNG, governed by its own certification, not from the Windows CTR_DRBG; the software chain the whitepaper describes simply is not on that path [1]. The companion post on the TPM in Windows follows it in detail.

The distinction is not academic: a TPM-resident key does not inherit the VM-clone caveat from Section 7 the way a software key does, because its generation never touched the cloned software state. Conversely, it inherits whatever assurance -- or opacity -- the device's own RNG carries.

This is why the thesis is scoped to software secrets. It is not a hedge; it is a map. The set of things that draw on the CNG generator is enormous and includes essentially everything an application, a protocol stack, or the OS itself produces in memory. The set that does not is small, specific, and deliberate: the keys you asked the hardware to make and keep. A precise claim tells you both where to look when you reason about randomness and where that reasoning hands off to a different chip.

So the OS rolls the dice for everything it makes in software, and -- as the last several sections argued -- it rolls them well: kernel-resident, forward-secure, diversely seeded, clock-reseeded, clone-aware. That is a genuinely strong position. But there is a limit no amount of good engineering can cross, and it is not a bug to be patched. It is a fact about information itself.

10. Theoretical Limits: You Cannot Conjure Entropy

Here is the uncomfortable truth beneath all of it. A CTR_DRBG cannot create a single bit of unpredictability. It can only spread out what it was seeded with. Run AES a billion times and the output is exactly as unpredictable as the entropy in its seed, and never beyond its 256-bit security strength -- no more. The generator is a magnificent amplifier of entropy and a producer of none.

That has a hard consequence. The min-entropy of the seed is an upper bound on the security of everything drawn from it. A 256-bit key generated from a seed carrying only 30 bits of real min-entropy has 30 bits of security, not 256, no matter how flawless the cipher is. The attacker does not fight AES; they guess among 2302^{30} possible seeds. This is the same failure as Netscape and Debian, stated as a theorem rather than a bug: unpredictability out cannot exceed unpredictability in.

The theory takes this seriously by splitting a secure generator into two phases with different characters. The first, entropy extraction, is information-theoretic -- it is about genuinely accumulating unpredictability. The second, output generation, is computational -- it stretches the accumulated seed using a cipher. Barak and Halevi made that separation rigorous in 2005, showing the extraction step "is information-theoretic in nature" while the generation step can be built from any standard cryptographic PRG [42].

Robustness (for PRNGs)

The modern security goal for a generator with input. A generator meeting this goal must keep output unpredictable, recover its security after a state compromise once enough fresh entropy is absorbed (prediction resistance), and protect past output (backtracking resistance) -- all at once, even against an adversary who partly controls the entropy source. Formalized by Dodis and colleagues in 2013 [43].

That model, robustness, is now "the standard security goal for PRNGs" [44]. It folds resilience, prediction resistance, and backtracking resistance into one game an adversary plays against the whole generator, entropy source included. And the good news is real: the mechanisms Windows relies on have been analyzed against it.

MechanismBuilt fromMeets the robustness goal?Known footgun
CTR_DRBGAES in counter modeYes, patched (Hoang and Shen, 2020) [44]Implementation flexibility enables cache side-channels [45]
HMAC_DRBGHMAC over a hashIn the random-oracle model, with a caveat [46]No forward security if the optional input is omitted [46]
Hash_DRBGa hash functionIn the random-oracle model, with a caveat [46]An over-flexible standard

Read the table honestly, though, and the caveats are the point. Hoang and Shen proved the patched CTR_DRBG satisfies robustness, and only under the assumption that AES behaves as an ideal cipher [44]. Woodage and Shumow proved Hash_DRBG and HMAC_DRBG satisfy robustness in the random-oracle model, and found that HMAC_DRBG loses forward security entirely if an optional input the standard treats as discretionary is omitted [46]. Every one of these results holds up to an assumption: an idealized primitive and a leak-free, constant-time implementation.

Reality violates that assumption. Cohney and colleagues turned CTR_DRBG's implementation flexibility into practical cache side-channel attacks on deployed code, recovering state that the security proofs assumed was hidden [45]. The proof says "secure if the code does not leak." The attack says "the code leaks." Both are true, and the gap between them is where real vulnerabilities live.

The construction-level standard meant to tie entropy sources to DRBGs, SP 800-90C, is now finished. NIST finalized it on 25 September 2025, calling it "the final document in the SP 800-90 series," and it specifies four RBG constructions: RBG1, RBG2, RBG3, and the chained RBGC [22, 48, 23].

RBG3 constructions target full-entropy output, the continuous-fresh-entropy ideal; RBGC names the chain-of-RBGs shape -- each RBG (a DRBG mechanism plus its seeding interface) on one platform seeding the next -- that the Windows root-to-per-processor arrangement instantiates as CTR_DRBGs. Beneath it, SP 800-90B governs the design and health testing of the entropy sources feeding the whole construction [25]. The framework for reasoning about the full stack, in other words, is now complete on paper -- which throws the remaining gap into sharper relief.

So the ceiling is the seed's min-entropy, and there is no way to run a tape measure over a live source and read off its true entropy. Every security proof in this article is conditioned on a "sufficiently seeded" premise that can be assumed but never fully verified. That is not a flaw in Windows; it is the shape of the problem. Which raises the obvious question: if you cannot measure entropy and cannot prove code leak-free, what exactly is still open -- and how much of it should keep you up at night?

11. Open Problems and Live Concerns

The generator is, for practical purposes, solved. The things around it are not. Five honest frontiers remain, and knowing them tells you exactly where to be careful.

Trusting hardware entropy you cannot audit. RDRAND and RDSEED are sealed pipelines inside a chip: an entropy source, a conditioner, and a CTR_DRBG, none of which you can inspect [24]. Windows' mix-many-trust-none design means a single compromised hardware source cannot own the seed [1], which is the right defensive posture. But "cannot dominate" is not "proven honest." No amount of mixing can demonstrate that a hardware source is behaving; it can only ensure that its misbehavior is survivable.

Entropy estimation is essentially unsolvable. There is no sound, general way to measure the true min-entropy of a running source online -- to look at a stream and certify "this carries 200 bits." Fortuna's design philosophy is to avoid estimation entirely, reseeding on a schedule rather than when an estimator declares "enough" [18]. Dodis and colleagues showed that flawed estimators are themselves an attack surface, adding that "it remains unclear if these attacks lead to actual exploitable vulnerabilities in practice" [43]. SP 800-90B specifies health tests, but a health test detects failure; it does not measure abundance [25, 49].

This is why "add your own entropy" is bad advice: you would be trusting an entropy estimate no one can validate.

The boot, headless, container, and VM residual window. The most damaging real-world randomness failures have all happened when a system generated keys before it had gathered enough entropy. "Mining Your Ps and Qs" scanned the internet and found tens of thousands of hosts sharing or exposing factorable keys, tracing the cause to a "boot-time entropy hole" on headless and embedded devices that generated keys at first boot [50, 51].

The mitigations -- seed files carried across reboots, Linux's getrandom() blocking until initialized (its random(7) manual warns that early urandom output "may be low entropy and unsuitable") [52], and Windows' Winload seeding plus VM Generation ID -- have narrowed this dramatically. They have not closed it. A freshly provisioned container or a cloned VM in the wrong instant is still the scenario to watch.

Constant-time, side-channel-free implementations. The Cohney cache attacks from the last section were not breaks of the mathematics; they were breaks of the code [45]. Keeping every step of a generator constant-time and free of state-dependent memory access is an ongoing engineering discipline, not a solved problem.

End-to-end formal verification. SymCrypt is explicitly engineered to "provide high assurance" and to support FIPS 140 certification [21], and pieces of the cryptographic stack have been formally analyzed. But a single machine-checked proof covering the entire path -- from interrupt-timing collection through the SHA-512 pools to the per-processor CTR_DRBG and out through ProcessPrng -- is not yet reality.

A final clarification, because it comes up constantly. The arrival of quantum computing does not threaten the quality of Windows' randomness. Shor's algorithm attacks the hard problems behind RSA and elliptic curves [53]; it does not make a well-seeded CTR_DRBG predictable, and a good CSPRNG needs no post-quantum redesign.

If anything, randomness becomes more foundational in a post-quantum world, because most of the new key-generation algorithms draw more fresh randomness per key than an elliptic-curve key does: ML-KEM consumes 64 bytes of RBG output [54] and SLH-DSA between 48 and 96 bytes [55], against roughly the 32-byte scalar an elliptic-curve or EdDSA key needs, while ML-DSA draws about the same, a 32-byte seed [56].

This is not a blanket claim against all classical schemes -- RSA key generation already rejection-samples large primes and consumes substantial randomness of its own -- but for the elliptic-curve keys these algorithms replace, the fixed draw is as large or larger. The generator does not need to change; it just gets asked for more, more often. And with SP 800-90C now final as of 2025, the remaining standards work is not the constructions themselves but validating the entropy sources that feed them under SP 800-90B [25].

None of this should scare you off the platform. It should do the opposite: tell you precisely what the OS handles for you, and what small set of things are yours to get right.

12. A Practical Guide

The whole argument reduces to a short list of rules, and most of them are about what not to do. The generator is the OS's job; your job is to call it correctly and stay out of its way.

PlatformCall thisNever for secrets
Windows (native)BCryptGenRandom with BCRYPT_USE_SYSTEM_PREFERRED_RNG, or ProcessPrng [4, 5]rand(), legacy CryptGenRandom
.NETSystem.Security.Cryptography.RandomNumberGenerator [33]System.Random
Linuxgetrandom() or getentropy() [34]/dev/urandom read before init, rand()
BSD / macOSarc4random_buf, arc4random_uniform [35]rand(), random()

The DO side is short because the right answer is almost always "ask the OS." The DON'T side is where the real bugs live. Never use rand() or the Mersenne Twister for anything an adversary should not predict [9]. Never reach for legacy CryptGenRandom. Never hand-seed the generator with your own entropy (see the callout below).

And watch three subtler traps: modulo bias when reducing random bytes into a range, generator state inherited across a fork() or a VM clone, and key generation that happens too early in boot.

That first trap -- modulo bias -- is the one people rediscover constantly, so it is worth seeing fixed. Reducing a uniform byte with % n skews the result whenever n does not divide the range evenly. Rejection sampling fixes it by discarding the few values that would skew things.

JavaScript One secure call, then reject to avoid modulo bias
// In real code the bytes come from the OS CSPRNG:
//   Windows: BCryptGenRandom / ProcessPrng
//   .NET:    RandomNumberGenerator.Fill
//   Linux:   getrandom()   BSD/macOS: arc4random_buf
// Here we simulate a source of uniform bytes.
function secureRandomByte() {
return Math.floor(Math.random() * 256);   // stand-in for the OS CSPRNG
}

// WRONG: 'byte % 6' is biased, because 256 is not a multiple of 6.
function rollBiased() {
return (secureRandomByte() % 6) + 1;
}

// RIGHT: rejection sampling discards the few skewing values.
function rollFair() {
const limit = 256 - (256 % 6);            // 252, the largest multiple of 6 below 256
let b;
do { b = secureRandomByte(); } while (b >= limit);
return (b % 6) + 1;
}

const biased = [0,0,0,0,0,0,0];
const fair   = [0,0,0,0,0,0,0];
for (let i = 0; i < 600000; i++) {
biased[rollBiased()]++;
fair[rollFair()]++;
}
console.log("biased % 6 :", biased.slice(1));
console.log("fair reject:", fair.slice(1));
console.log("low faces are over-represented in the biased row");

Press Run to execute.

Try it: one line of correct randomness per platform

On Windows, in PowerShell, [System.Security.Cryptography.RandomNumberGenerator]::GetBytes(32) returns 32 cryptographically secure bytes through the CNG path [33]. On Linux, a program calling getrandom(buf, 32, 0) blocks only if the pool is not yet initialized, then fills the buffer [34]. On OpenBSD or macOS, arc4random_buf(buf, 32) never blocks and never fails [35]. In every case you are reaching the same two-layer design: an entropy-fed pool keying a fast, forward-secure generator.

Frequently asked questions

Was Windows backdoored through Dual_EC_DRBG?

No. Windows shipped Dual_EC_DRBG only as a non-default SP 800-90 option -- available if an application explicitly requested it, never the system default -- and later removed it [17]. The case where it was reportedly the default is RSA's BSAFE toolkit, per Reuters reporting of a $10 million contract, and NIST withdrew the algorithm in 2014 citing a lack of public confidence [15, 16].

Does Windows just trust Intel's RDRAND?

No. Windows treats RDRAND as one input among many, notes that it provides only a 128-bit security level, prefers RDSEED, and never relies on any single hardware source alone. Everything is mixed through SHA-512 pools so no one source can determine the seed [1].

Do TPM and smart-card keys come from the OS RNG?

No. Keys generated and held inside a TPM or a smart card come from that device's own hardware random number generator. The CNG software CTR_DRBG is not in that path. This is the explicit boundary of the claim that the OS rolls the dice for software secrets [1].

Which randomness API should I actually call?

On native Windows, BCryptGenRandom (with BCRYPT_USE_SYSTEM_PREFERRED_RNG) or ProcessPrng. On .NET, RandomNumberGenerator, which wraps BCryptGenRandom on Windows. Never rand() or legacy CryptGenRandom for anything secret [4, 5, 33].

Can I validate the generator myself with statistical tests?

You can detect gross failure, but statistical suites cannot prove cryptographic security. Dual_EC_DRBG would pass Dieharder and the NIST test suite while remaining predictable to whoever holds its trapdoor. Statistical quality is necessary, not sufficient [9].

Does Windows block waiting for entropy, like the old /dev/random?

No. Windows seeds its generator in the Winload boot loader before any user code runs, so there is no blocking wait at the API. Contrast Linux getrandom(), which blocks until the kernel pool is initialized unless you pass a non-blocking flag [1, 34].

Should I add my own entropy or reseed by hand?

No. See the "Do not add your own entropy" callout above.

Which brings the argument back to where it started, now with the evidence behind it. Netscape and Debian did not lose to better cryptanalysis; they lost because their keys were drawn from dice an attacker could read. Windows lost the same way once, with CryptGenRandom, and the industry nearly lost a third time to a standardized algorithm whose own constants were the weapon.

The answer Windows built is not a better cipher -- AES was never the problem -- but a better way to roll: kernel-resident, forward-secure CTR_DRBGs in a per-processor chain, seeded from many sources so no single one can cheat, reseeded on a clock, and watching for the clone that would make two machines roll alike.

Every software secret the OS makes inherits that. The one place it hands the dice to someone else is the TPM and the smart card, which roll their own. Know where that line is, call the system generator, and the unguessability of your keys takes care of itself.

Study guide

Key terms

Min-entropy
The worst-case measure of unpredictability of a source; it upper-bounds the security of every key drawn from a seed.
DRBG
Deterministic Random Bit Generator: an algorithm that stretches a seed into many output bits but creates no new unpredictability.
CSPRNG
A DRBG whose output no efficient adversary can distinguish from uniform random bits, even given prior output.
CTR_DRBG
The SP 800-90A DRBG built from AES in counter mode with a state (key K and counter V) that is overwritten after every request. Windows uses it at 256-bit strength.
Backtracking resistance
Forward secrecy: a state compromise cannot reveal past output, because the state update is one-way.
Prediction resistance
After fresh entropy is mixed in, output becomes unpredictable again even if the prior state leaked; this is why generators reseed.
Entropy source (TRNG)
A physical process producing genuine unpredictability, such as interrupt-timing jitter, RDSEED, or a TPM, as opposed to an algorithm.
VM Generation ID
A 128-bit hypervisor value that changes on a snapshot or clone, signaling the guest RNG to reseed so cloned machines do not roll identical dice.
ProcessPrng
The user-mode CNG entry point that returns bytes from the user-mode per-processor CTR_DRBG, seeded from the kernel chain.
BCryptGenRandom
The recommended CNG randomness API; with BCRYPT_USE_SYSTEM_PREFERRED_RNG it draws from the system generator.

Comprehension questions

  1. Why is passing every statistical randomness test not enough for a key generator?

    Statistical tests detect bias and structure but not predictability. A generator like the Mersenne Twister or Dual_EC_DRBG can produce statistically excellent output whose state or future values are recoverable, so it fails as a CSPRNG despite passing the tests.

  2. What exactly was the 2007 CryptGenRandom break?

    Dorrendorf, Gutterman, and Pinkas reverse-engineered the legacy generator and showed it ran in user mode with per-process state reseeded only every 128 KB. Given one state, the previous state was recoverable in about 2^23 work, so a single leak exposed roughly 128 KB of past and future output.

  3. Why does Windows mix many entropy sources instead of trusting one good one?

    Because trust bottoms out on both the algorithm and its sources. Mixing many independent sources through SHA-512 pools means that even a compromised or capped source, such as RDRAND, cannot by itself determine the seed. Distrust is designed in.

  4. Where does the claim 'the OS rolls the dice for every secret' stop being true?

    At hardware-resident keys. Keys generated inside a TPM or a smart card come from the device's own hardware RNG, not the CNG software CTR_DRBG, so the software generator is not in that path.

References

  1. Niels Ferguson (2019). The Windows 10 random number generation infrastructure. https://download.microsoft.com/download/1/c/9/1c9813b8-089c-4fef-b2ad-ad80e79403ba/Whitepaper%20-%20The%20Windows%2010%20random%20number%20generation%20infrastructure.pdf - Ferguson 2019 whitepaper: root and per-processor CTR_DRBG tree, reseed schedule, entropy sources.
  2. Leo Dorrendorf, Zvi Gutterman, & Benny Pinkas (2007). Cryptanalysis of the Random Number Generator of the Windows Operating System. https://eprint.iacr.org/2007/419 - Cryptanalysis of legacy CryptGenRandom: state recovery that breaks forward security.
  3. Dan Shumow & Niels Ferguson (2007). On the Possibility of a Back Door in the NIST SP800-90 Dual Ec Prng. https://rump2007.cr.yp.to/15-shumow.pdf - Crypto 2007 rump-session slides demonstrating the possible Dual_EC_DRBG back door.
  4. Microsoft (2025). ProcessPrng function. https://learn.microsoft.com/en-us/windows/win32/seccng/processprng - Microsoft docs for ProcessPrng, the user-mode per-processor RNG delivery surface.
  5. Microsoft BCryptGenRandom function (bcrypt.h). https://learn.microsoft.com/en-us/windows/win32/api/bcrypt/nf-bcrypt-bcryptgenrandom - Microsoft docs for BCryptGenRandom, the CNG RNG API.
  6. Ian Goldberg & David Wagner (1996). Randomness and the Netscape Browser. https://people.eecs.berkeley.edu/~daw/papers/ddj-netscape.html - Reverse-engineered Netscape SSL seed: time of day + PID + parent PID.
  7. Debian Security Team (2008). DSA-1571-1 openssl -- predictable random number generator. https://lists.debian.org/debian-security-announce/2008/msg00152.html - Debian advisory DSA-1571-1: the predictable OpenSSL RNG that produced guessable keys.
  8. (2008). CVE-2008-0166. https://nvd.nist.gov/vuln/detail/CVE-2008-0166 - NVD record anchoring the Debian OpenSSL predictable-key vulnerability.
  9. Makoto Matsumoto & Takuji Nishimura Mersenne Twister: A random number generator. https://www.math.sci.hiroshima-u.ac.jp/m-mat/MT/emt.html - Mersenne Twister home page: fast non-cryptographic PRNG, period 2^19937-1, not secure as is.
  10. Makoto Matsumoto & Takuji Nishimura (1998). Mersenne Twister: A 623-Dimensionally Equidistributed Uniform Pseudo-Random Number Generator. https://doi.org/10.1145/272991.272995 - ACM TOMACS 8(1):3-30. Working area of only 624 words over the two-element field, so 624 consecutive 32-bit outputs recover the full state.
  11. Elaine Barker & John Kelsey (2015). NIST SP 800-90A Rev. 1: Recommendation for Random Number Generation Using Deterministic Random Bit Generators. https://csrc.nist.gov/pubs/sp/800/90/a/r1/final - SP 800-90A Rev. 1 landing page: the standardized CTR, HMAC, and Hash DRBG mechanisms.
  12. Manuel Blum & Silvio Micali (1984). How to Generate Cryptographically Strong Sequences of Pseudo-Random Bits. https://doi.org/10.1137/0213053 - Introduces the next-bit test and the first provably secure pseudorandom generator.
  13. Andrew C. Yao (1982). Theory and Application of Trapdoor Functions. https://doi.org/10.1109/SFCS.1982.45 - Proves unpredictability and indistinguishability are equivalent for pseudorandom sequences.
  14. Elaine Barker & John Kelsey (2006). NIST SP 800-90 (original, June 2006): Recommendation for Random Number Generation Using Deterministic Random Bit Generators. https://csrc.nist.gov/pubs/sp/800/90/final - Original 2006 edition specifying four DRBG mechanisms, including Dual_EC_DRBG (later removed in Rev. 1).
  15. NIST (2014). NIST Removes Cryptography Algorithm from Random Number Generator Recommendations. https://www.nist.gov/news-events/news/2014/04/nist-removes-cryptography-algorithm-random-number-generator-recommendations - NIST announcement removing Dual_EC_DRBG from SP 800-90A and urging transition.
  16. Joseph Menn (2013). Exclusive: Secret contract tied NSA and security industry pioneer (Reuters, archived). http://web.archive.org/web/20250325215921/https://www.reuters.com/article/us-usa-security-rsa-idUSBRE9BJ1C220131220/ - Reuters report on the alleged RSA BSAFE Dual_EC default contract; used strictly as journalism.
  17. Microsoft (2025). CNG Algorithm Identifiers (Bcrypt.h): BCRYPT_RNG_DUAL_EC_ALGORITHM. https://learn.microsoft.com/en-us/windows/win32/seccng/cng-algorithm-identifiers - CNG algorithm identifiers page showing Dual_EC exposed as a non-default RNG option.
  18. Niels Ferguson & Bruce Schneier Fortuna. https://www.schneier.com/academic/fortuna/ - Fortuna: the Ferguson and Schneier multi-pool accumulator behind the Windows reseed schedule.
  19. Niels Ferguson (2019). The Windows 10 random number generation infrastructure (aka.ms short link). https://aka.ms/win10rng - aka.ms short link that redirects to the Ferguson Windows 10 RNG whitepaper.
  20. Elaine Barker & John Kelsey (2015). NIST SP 800-90A Rev. 1 (PDF). https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-90Ar1.pdf - SP 800-90A Rev. 1 full text: the CTR_DRBG generate and update algorithm and 384-bit seedlen.
  21. Microsoft SymCrypt: the core cryptographic library of Windows. https://github.com/microsoft/SymCrypt - SymCrypt: the core cryptographic library used by Windows.
  22. Elaine Barker, John Kelsey, Kerry McKay, Allen Roginsky, & Meltem Sonmez Turan (2025). NIST SP 800-90C: Recommendation for Random Bit Generator (RBG) Constructions. https://csrc.nist.gov/pubs/sp/800/90/c/final - Finalized 25 September 2025; specifies RBG1, RBG2, RBG3, and RBGC constructions.
  23. NIST (2025). Recommendation for Random Bit Generator (RBG) Constructions: NIST Publishes SP 800-90C. https://www.nist.gov/news-events/news/2025/09/recommendation-random-bit-generator-constructions-nist-publishes-sp-800-90c - NIST announcement of SP 800-90C, the final document in the SP 800-90 series (2025).
  24. Intel Intel Digital Random Number Generator (DRNG) Software Implementation Guide. https://www.intel.com/content/www/us/en/developer/articles/guide/intel-digital-random-number-generator-drng-software-implementation-guide.html - Intel DRNG guide: the RDRAND and RDSEED on-die entropy-to-CTR_DRBG pipeline.
  25. Meltem Sonmez Turan, Elaine Barker, John Kelsey, & Kerry McKay (2018). NIST SP 800-90B: Recommendation for the Entropy Sources Used for Random Bit Generation. https://csrc.nist.gov/pubs/sp/800/90/b/final - SP 800-90B landing page: entropy-source validation and min-entropy estimation.
  26. Bruce Schneier & John Kelsey Yarrow. https://www.schneier.com/academic/yarrow/ - Yarrow: the Schneier and Kelsey pool-and-reseed design that preceded Fortuna.
  27. John Kelsey, Bruce Schneier, David Wagner, & Chris Hall (1998). Cryptanalytic Attacks on Pseudorandom Number Generators. https://www.schneier.com/wp-content/uploads/2017/10/paper-prngs.pdf - Kelsey et al. 1998 taxonomy of cryptanalytic attacks on pseudorandom number generators.
  28. Thomas Ristenpart & Scott Yilek (2010). When Good Randomness Goes Bad: Virtual Machine Reset Vulnerabilities and Hedging Deployed Cryptography. https://www.ndss-symposium.org/ndss2010/when-good-randomness-goes-bad-virtual-machine-reset-vulnerabilities-and-hedging-deployed/ - NDSS 2010 landing page for the VM-reset randomness-reuse work by Ristenpart and Yilek.
  29. Thomas Ristenpart & Scott Yilek (2010). When Good Randomness Goes Bad (open PDF). https://pages.cs.wisc.edu/~rist/papers/sslhedge.pdf - Open-access PDF of the VM-reset randomness vulnerability paper.
  30. Adam Everspaugh, Yan Zhai, Robert Jellinek, Thomas Ristenpart, & Michael Swift (2014). Not-So-Random Numbers in Virtualized Linux and the Whirlwind RNG. https://www.ieee-security.org/TC/SP2014/papers/Not-So-RandomNumbersinVirtualizedLinuxandtheWhirlwindRNG.pdf - IEEE S&P 2014 study measuring VM-reset RNG reuse and proposing the Whirlwind RNG.
  31. Microsoft Virtual Machine Generation ID. https://learn.microsoft.com/en-us/windows/win32/hyperv_v2/virtual-machine-generation-identifier - Microsoft docs on the VM Generation ID used to detect clone and time-shift events.
  32. Microsoft RtlGenRandom function (ntsecapi.h). https://learn.microsoft.com/en-us/windows/win32/api/ntsecapi/nf-ntsecapi-rtlgenrandom - Microsoft docs for the legacy RtlGenRandom (SystemFunction036) wrapper.
  33. .NET Runtime contributors (2024). RandomNumberGeneratorImplementation.Windows.cs (dotnet/runtime). https://github.com/dotnet/runtime/blob/e1979b72ccb5f916649f1d9949ef663254790c25/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/RandomNumberGeneratorImplementation.Windows.cs - dotnet runtime source showing RandomNumberGenerator routes through BCryptGenRandom.
  34. getrandom(2) -- Linux manual page. https://man7.org/linux/man-pages/man2/getrandom.2.html - Linux getrandom(2) man page: blocks until the urandom source is initialized.
  35. arc4random(3) -- OpenBSD manual page. https://man.openbsd.org/arc4random.3 - OpenBSD arc4random(3): ChaCha20-based per-process RNG, reseeded on a schedule and on fork.
  36. Daniel J. Bernstein (2017). Fast-key-erasure random-number generators. https://blog.cr.yp.to/20170723-random.html - Bernstein on fast-key-erasure RNGs, the forward-secrecy technique behind modern Unix CSPRNGs.
  37. arc4random(3) -- FreeBSD manual page. https://man.freebsd.org/cgi/man.cgi?query=arc4random&sektion=3 - FreeBSD arc4random(3) man page for the same ChaCha20-based interface.
  38. Microsoft (2001). Windows Data Protection (DPAPI). https://learn.microsoft.com/en-us/previous-versions/ms995355(v=msdn.10) - Windows Data Protection paper: DPAPI master and session keys are randomly generated.
  39. Microsoft BitLocker overview. https://learn.microsoft.com/en-us/windows/security/operating-system-security/data-protection/bitlocker/ - BitLocker overview: the VMK and FVEK key hierarchy generated from the CNG RNG.
  40. Microsoft Secure Channel (Schannel). https://learn.microsoft.com/en-us/windows/win32/secauthn/secure-channel - Schannel overview: the Windows TLS stack that draws handshake randomness from CNG.
  41. Paul Leach, Michael Mealling, & Rich Salz (2005). RFC 4122: A Universally Unique IDentifier (UUID) URN Namespace. https://www.rfc-editor.org/rfc/rfc4122 - Sec. 6: UUIDs should not be used as security capabilities and are not to be assumed hard to guess. Sec. 4.4 defines version-4 UUIDs from truly-random or pseudo-random numbers, so the format itself does not guarantee unpredictability.
  42. Boaz Barak & Shai Halevi (2005). A Model and Architecture for Pseudo-Random Generation with Applications to /dev/random. https://eprint.iacr.org/2005/029 - Barak and Halevi robustness model: the extraction-then-generation split for RNGs.
  43. Yevgeniy Dodis, David Pointcheval, Sylvain Ruhault, Damien Vergnaud, & Daniel Wichs (2013). Security Analysis of Pseudo-Random Number Generators with Input. https://eprint.iacr.org/2013/338 - Dodis et al. formalize entropy-accumulation robustness and the premature-next problem.
  44. Viet Tung Hoang & Yaobin Shen (2020). Security Analysis of NIST CTR-DRBG. https://eprint.iacr.org/2020/619 - Hoang and Shen prove the patched NIST CTR_DRBG robust under an ideal-cipher model.
  45. Shaanan Cohney, Andrew Kwong, Shahar Paz, Daniel Genkin, Nadia Heninger, Eyal Ronen, & Yuval Yarom (2019). Pseudorandom Black Swans: Cache Attacks on CTR_DRBG. https://eprint.iacr.org/2019/996 - Cohney et al. mount cache side-channel attacks on deployed CTR_DRBG implementations.
  46. Joanne Woodage & Dan Shumow (2018). An Analysis of the NIST SP 800-90A Standard. https://eprint.iacr.org/2018/349 - Woodage and Shumow analyze SP 800-90A DRBGs and their forward-security caveats.
  47. Berry Schoenmakers & Andrey Sidorenko (2006). Cryptanalysis of the Dual Elliptic Curve Pseudorandom Generator. https://eprint.iacr.org/2006/190 - Dual_EC output is distinguishable from uniform, a small known bias separate from the trapdoor.
  48. (2025). NIST SP 800-90C (DOI). https://doi.org/10.6028/NIST.SP.800-90C - Canonical DOI for NIST SP 800-90C.
  49. (2018). NIST SP 800-90B (PDF). https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-90B.pdf - SP 800-90B full text: noise-source and conditioning requirements for entropy sources.
  50. Nadia Heninger, Zakir Durumeric, Eric Wustrow, & J. Alex Halderman (2012). Mining Your Ps and Qs: Detection of Widespread Weak Keys in Network Devices. https://www.usenix.org/conference/usenixsecurity12/technical-sessions/presentation/heninger - USENIX Security 2012 study finding widespread weak keys from low boot-time entropy.
  51. Nadia Heninger, Zakir Durumeric, Eric Wustrow, & J. Alex Halderman (2012). Mining Your Ps and Qs (factorable.net). https://factorable.net/paper.html - Companion data for Mining Your Ps and Qs: shared TLS keys from a boot-time entropy hole.
  52. random(7) -- Linux manual page. https://man7.org/linux/man-pages/man7/random.7.html - Linux random(7) man page: overview of the kernel CSPRNG interface.
  53. Peter W. Shor (1997). Polynomial-Time Algorithms for Prime Factorization and Discrete Logarithms on a Quantum Computer. https://doi.org/10.1137/S0097539795293172 - SIAM J. Comput. 26(5):1484-1509. Quantum algorithm for integer factorization and discrete logarithms, the hard problems behind RSA and elliptic-curve cryptography.
  54. NIST (2024). FIPS 203: Module-Lattice-Based Key-Encapsulation Mechanism Standard. https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.203.pdf - FIPS 203 (ML-KEM): key generation consumes 64 bytes of fresh randomness.
  55. NIST (2024). FIPS 205: Stateless Hash-Based Digital Signature Standard. https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.205.pdf - FIPS 205 (SLH-DSA): key generation consumes 48 to 96 bytes of fresh randomness.
  56. NIST (2024). FIPS 204: Module-Lattice-Based Digital Signature Standard. https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.204.pdf - FIPS 204 (ML-DSA): key generation consumes 32 bytes of fresh randomness.