# 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.

*Published: 2026-07-02*
*Canonical: https://paragmali.com/blog/a-key-is-only-as-unguessable-as-the-dice-that-made-it-inside*
*© Parag Mali. All rights reserved.*

---
<TLDR>
Every software secret Windows generates -- DPAPI master keys, BitLocker volume keys, TLS session keys, machine-account passwords -- is drawn from one cryptographic random number generator: a NIST SP 800-90A AES `CTR_DRBG` seeded from many mixed entropy sources and reseeded on a clock schedule [@src-win10rng-pdf]. It is not "true" randomness. It is a deterministic generator whose output is only as unguessable as the entropy behind its seed. Windows learned this the hard way -- its first generator, `CryptGenRandom`, was broken for forward secrecy in 2007 [@src-cryptgenrandom2007], and the industry's `Dual_EC_DRBG` scare proved that an algorithm's own constants can be a back door [@src-shumowferguson2007]. The modern design answers both failures: a kernel-resident root `CTR_DRBG` seeds a strict chain of per-processor generators, mixing enough sources that no single one -- not even Intel's `RDRAND` -- can own the pool, delivered to your process through `ProcessPrng` and `BCryptGenRandom` [@src-processprng; @src-bcryptgenrandom]. The one boundary: keys born inside a TPM or a smart card come from the device's own hardware RNG, not this software CSPRNG.
</TLDR>

## 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 [@src-netscape1996]. 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 [@src-debian2008]. 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 [@src-cve20080166].

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.

<Sidenote>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 [@src-netscape1996].</Sidenote>

<Sidenote>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 $2^{15}$, or 32,768 values [@src-debian2008; @src-cve20080166].</Sidenote>

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?

<Mermaid caption="Two decades of randomness failures forced the redesign of the modern Windows generator">
timeline
    title From "looks random" to "is unpredictable"
    1982 : Blum and Micali introduce the unpredictability criterion and first provable PRG
    1982 : Yao proves unpredictability = indistinguishability
    1996 : Netscape SSL seed reconstructed
    2006 to 2008 : Debian OpenSSL entropy stripped
    2007 : CryptGenRandom forward-security break
    2007 : Dual_EC_DRBG back door demonstrated
    2012 : VM Generation ID ships for clone detection
    2014 : NIST removes Dual_EC_DRBG
    2019 : Windows CTR_DRBG tree documented
    2025 : NIST finalizes SP 800-90C
</Mermaid>

## 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 $2^{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" [@src-mt]. The reason is that its internal state is a linear function of its output, held in a working area of just 624 words [@src-mtpaper]. 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.

<Definition term="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.
</Definition>

<Definition term="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 [@src-sp80090a].
</Definition>

<Definition term="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.
</Definition>

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.

| Property | Entropy source (TRNG) | CSPRNG (secure DRBG) | Non-crypto PRNG |
|---|---|---|---|
| Origin of bits | physical noise | a seed plus an algorithm | a seed plus an algorithm |
| Reproducible from state? | no | yes | yes |
| Speed | slow | fast | fast |
| Safe for keys? | yes, but rate-limited | yes | no |
| Example | RDSEED, interrupt jitter | AES `CTR_DRBG`, ChaCha20 | Mersenne Twister, C `rand()` |

> **Note:** `rand()`, `random()`, and the Mersenne Twister are built for simulations and games, not keys. Their state is recoverable from their output [@src-mt]. Anything that must be unpredictable to an adversary needs a CSPRNG, never a general-purpose PRNG.

### 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 [@src-blummicali1984]. 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 [@src-yao1982]. 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.

<Definition term="Min-entropy">
The worst-case measure of unpredictability. For a source $X$, the min-entropy is $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.
</Definition>

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.

<Definition term="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."
</Definition>

<Definition term="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.
</Definition>

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.

<RunnableCode lang="js" title="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");
`}</RunnableCode>

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 [@src-cryptgenrandom2007]. 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(2^{23})$ work -- a break of forward security -- while running the generator *forward* to predict future output was trivial, $O(1)$ [@src-cryptgenrandom2007]. 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.

<PullQuote>
"Learning a single state may reveal 128 Kbytes of the past and future output of the generator." -- Dorrendorf, Gutterman, and Pinkas, 2007 [@src-cryptgenrandom2007]
</PullQuote>

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.

<Mermaid caption="Why one leaked CryptGenRandom state exposed a process's past and future output">
flowchart TD
    A["CryptGenRandom runs in user mode, one state per process"] --> B["Generator state lives in the process address space"]
    B --> C["System entropy mixed in only after 128 KB of output"]
    C --> D["Attacker leaks a single internal state"]
    D --> E["Recover the previous state in about 2 to the 23 work"]
    D --> F["Run forward to predict future output in constant work"]
    E --> G["Reconstruct the process's SSL and session keys"]
    F --> G
</Mermaid>

<Sidenote>The asymmetry is worth savoring: predicting *future* output from a known state was $O(1)$, because that is just running the generator, while recovering *past* output cost $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 [@src-cryptgenrandom2007].</Sidenote>

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` [@src-sp80090-2006; @src-nistremoval2014]. 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, $P$ and $Q$, whose relationship is a secret. If anyone knows a scalar $d$ with $d\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 [@src-shumowferguson2007]. The constants shipped in the standard, and no one outside their authors could say how $P$ and $Q$ had been chosen. The generator could be perfect for everyone except the one party holding $d$.

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.

<PullQuote>
"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 [@src-shumowferguson2007]
</PullQuote>

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 [@src-reuters2013]. 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" [@src-nistremoval2014]. 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 [@src-msdualec]. 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.

<Aside label="The through-line: the man who warned about the back door built the fix">
Niels Ferguson is not a bystander in this story; he is the thread that ties it together. He co-presented the Dual_EC back-door result in 2007. Years earlier he co-designed Fortuna, the multi-pool reseeding generator whose pattern Windows would adopt [@src-fortuna]. And in 2019 he authored the whitepaper documenting the modern Windows generator [@src-win10rng-pdf]. The person who showed most vividly that a standardized DRBG could betray you is the same person who later wrote down how Windows arranges never to trust any single one. That is not a coincidence -- it is the design philosophy made personal.
</Aside>

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 [@src-win10rng] puts it flatly:

<PullQuote>
"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 [@src-win10rng-pdf]
</PullQuote>

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.

| Generation | Representative design | Why it was superseded |
|---|---|---|
| G0 -- non-crypto PRNG | C `rand()`, Mersenne Twister | Linear state recoverable from output [@src-mt] |
| G1 -- ad-hoc app CSPRNG | Netscape SSL seed from clock and PID | Too little entropy; seeds enumerable [@src-netscape1996] |
| G2 -- legacy `CryptGenRandom` | User-mode, per-process DRBG | Forward-security break; reseed only per 128 KB [@src-cryptgenrandom2007] |
| G3 -- SP 800-90 DRBG menu | Standardized `CTR_DRBG` and siblings | A menu is not an architecture; Dual_EC withdrawn [@src-nistremoval2014] |
| G4 -- modern Windows tree | Root `CTR_DRBG` seeding a strict chain of per-processor DRBGs | Current design [@src-win10rng-pdf] |

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 [@src-win10rng-pdf]; 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" [@src-win10rng-pdf]. 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" [@src-win10rng-pdf], 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.

<Mermaid caption="The modern Windows generator: a root CTR_DRBG seeds a strict one-way chain of per-processor CTR_DRBGs">
flowchart TD
    ES["Mixed entropy sources"] --> ROOT["Root CTR_DRBG in CNG.SYS, seeds the generators below it"]
    ROOT --> KP["Kernel per-processor CTR_DRBGs, also in CNG.SYS"]
    KP --> PB["Process-base CTR_DRBG in BCryptPrimitives.dll"]
    PB --> UP["User-mode per-processor CTR_DRBGs"]
    UP --> APP["ProcessPrng hands bytes to your process"]
</Mermaid>

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.

> **Note:** The two structural moves -- a root generator whose main job is to seed the generators below it, and a separate `CTR_DRBG` per processor -- are not performance tricks that happen to help security. They are the security. They ensure no single leaked state, and no single hot lock, sits on the path between your key and everyone else's [@src-win10rng-pdf].

### How a CTR_DRBG actually works

<Definition term="CTR_DRBG">
The SP 800-90A DRBG built from a block cipher run in counter mode. Its state is a cipher key $K$ and a counter $V$. To produce output it encrypts successive counter values under $K$; after each request it runs an update step that replaces both $K$ and $V$. Windows uses AES with a 256-bit key, giving a combined seed length of 384 bits [@src-sp80090a-pdf].
</Definition>

Mechanically it is simpler than its reputation. The state is a key $K$ and a counter block $V$. To generate output, the DRBG **increments the counter before each block** and encrypts the result under AES with key $K$: the first output block is $\text{AES}(K, V+1)$, the next is $\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 $V$ is never emitted. For AES-256 the seed length -- the size of $K$ and $V$ together -- is 384 bits: a 256-bit key plus a 128-bit counter block [@src-sp80090a-pdf].

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 $K$ and $V$ 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.

<Mermaid caption="Every CTR_DRBG request ends by overwriting its own key and counter, which is what gives forward secrecy">
sequenceDiagram
    participant App as Caller
    participant D as CTR_DRBG state K and V
    App->>D: Request random bytes
    D->>D: Increment the counter, then encrypt under AES to produce each output block
    D-->>App: Return the output bytes
    D->>D: Update step derives and overwrites a fresh K and V
    Note over D: The old K and V are destroyed, so past output cannot be recomputed
</Mermaid>

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

<Definition term="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 [@src-sp80090a].
</Definition>

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.

<RunnableCode lang="js" title="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");
`}</RunnableCode>

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" [@src-symcrypt]. 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_DRBG`s [@src-sp80090c; @src-nist90cnews2025]. 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.

<Sidenote>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 [@src-win10rng-pdf].</Sidenote>

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 $K$ and $V$ 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.

> **Key idea:** 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 [@src-win10rng-pdf]. 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](/blog/the-tpm-in-windows-one-primitive-twenty-five-years-and-the-c/)** (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 [@src-win10rng-pdf].

| Source | Type | Cadence (as documented in 2019) | Trust note |
|---|---|---|---|
| Interrupt timing via `RDTSC` | timing jitter | continuous; the primary source | Hard to observe or steer remotely [@src-win10rng-pdf] |
| TPM | hardware RNG | 40 bytes at boot, 64 bytes per reseed, at most once per ~40 min | Separate chip, independent of the CPU [@src-win10rng-pdf] |
| `RDRAND` / `RDSEED` | on-die hardware RNG | on demand | `RDRAND` capped at 128-bit security; `RDSEED` preferred; never trusted alone [@src-win10rng-pdf] |
| Registry seed file | stored entropy | read at boot, rewritten for next boot | Carries entropy across reboots [@src-win10rng-pdf] |
| UEFI / firmware seed | firmware-provided | at boot | Platform-dependent [@src-win10rng-pdf] |
| Hyper-V ACPI OEM0 table | hypervisor-provided | at boot on guests | Present for virtualized machines [@src-win10rng-pdf] |

### 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 [@src-win10rng-pdf].

The underlying Intel design is itself an entropy source feeding a conditioner and a `CTR_DRBG` [@src-inteldrng] -- 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.

<Sidenote>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 [@src-win10rng-pdf].</Sidenote>

<Mermaid caption="Many independent sources fan into SHA-512 pools, so no single source can own the seed">
flowchart LR
    A["Interrupt and RDTSC timing jitter (primary)"] --> P["SHA-512 entropy pools, up to 8"]
    B["TPM random bytes"] --> P
    C["RDRAND and RDSEED"] --> P
    D["Boot seed file from the registry"] --> P
    E["UEFI and firmware seed"] --> P
    F["Hyper-V ACPI OEM0 table"] --> P
    P --> R["Seed for the root CTR_DRBG"]
</Mermaid>

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 [@src-win10rng-pdf].

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 [@src-sp80090b].

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 [@src-yarrow], which itself grew out of a 1998 taxonomy of how real pseudorandom generators get broken in the field [@src-prngattacks1998]. 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 [@src-win10rng-pdf].

Underneath sits a bank of up to **8 SHA-512 entropy pools**, and pool $k$ is drained into a reseed only every $3^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 [@src-win10rng-pdf].

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 $2^{48}$ generate requests before a reseed is mandatory, though Windows' clock schedule reseeds far more often than that ceiling would ever require [@src-sp80090a-pdf].

<Sidenote>That $2^{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 [@src-sp80090a-pdf].</Sidenote>

### 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 $K$ and $V$, 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 [@src-ristenpart2010; @src-ristenpart2010-pdf]. 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" [@src-whirlwind2014].

<Definition term="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 [@src-vmgenid].
</Definition>

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 [@src-vmgenid]. 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 [@src-win10rng-pdf]. Crucially, Ferguson is candid that this "does not eliminate the vulnerability" [@src-win10rng-pdf] -- it narrows it.

<Mermaid caption="A VM clone duplicates generator state until a changed Generation ID forces a reseed">
flowchart TD
    A["VM is cloned or reverted to a snapshot"] --> B["Both instances resume with identical generator state"]
    B --> C&#123;"Did the VM Generation ID change?"&#125;
    C -->|Yes| D["Root generator reseeds, the two states diverge"]
    C -->|No| E["Both instances roll identical dice"]
    E --> F["Residual window: any secret made here may repeat on the twin"]
</Mermaid>

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.

> **Note:** A virtual machine that is cloned or rolled back on a hypervisor that does not expose a VM Generation ID can emit the same "random" values more than once -- a direct route to duplicated TLS, SSH, or signing keys. If you snapshot and clone Windows VMs, confirm your hypervisor surfaces a Generation ID [@src-whirlwind2014; @src-win10rng-pdf].

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" [@src-processprng]. 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_DRBG`s 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` [@src-bcryptgenrandom]. 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" [@src-bcryptgenrandom]. 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 [@src-rtlgenrandom], and Ferguson's whitepaper states outright that "RtlGenRandom uses the ProcessPrng function" [@src-win10rng-pdf]. 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 [@src-dotnet-rng; @src-bcryptgenrandom].

<Aside label="How the other systems do it">
Windows is not alone in converging on a two-layer, forward-secure system RNG. Linux exposes `getrandom(2)`, which draws from the kernel urandom source and blocks until the pool is initialized unless `GRND_NONBLOCK` is set [@src-getrandom]. OpenBSD's `arc4random` family "was replaced with the ChaCha20 cipher" in OpenBSD 5.5 and is "re-seeded from the kernel random(4) subsystem using getentropy(2) on a regular basis, and also upon fork(2)"; its `arc4random_uniform` avoids the modulo bias that trips up naive `% n` reductions [@src-arc4random]. Both lean on the fast-key-erasure construction Daniel J. Bernstein describes, which overwrites the key immediately after use -- "in NIST terminology, it is a failure of backtracking resistance" that erasure prevents [@src-djbrandom]. Different cipher, same shape: collect entropy, key a fast generator, erase forward.
</Aside>

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.

| Dimension | Windows CNG chain | Linux `getrandom` | OpenBSD/FreeBSD `arc4random` | Fortuna (pattern) |
|---|---|---|---|---|
| Output core | AES `CTR_DRBG`, 256-bit | ChaCha20 | ChaCha20 | block cipher |
| Forward secrecy | DRBG update step | fast-key-erasure | fast-key-erasure | DRBG update |
| Concurrency | per-processor state | per-CPU state | per-process pool | not specified |
| VM-reset defense | VM Generation ID reseed | improving over time | reseed on `fork()` | not specified |
| Primary API | `BCryptGenRandom` / `ProcessPrng` | `getrandom(2)` | `arc4random_buf` | not applicable |

Sources for the table: the Windows column from Ferguson's whitepaper and the CNG docs [@src-win10rng-pdf; @src-processprng]; Linux from `getrandom(2)` [@src-getrandom]; OpenBSD/FreeBSD from `arc4random(3)` [@src-arc4random; @src-arc4random-freebsd]; Fortuna from its design page [@src-fortuna]. 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](/blog/cng-architecture-bcrypt-ncrypt-ksps/), 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" [@src-win10rng-pdf; @src-processprng].

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](/blog/dpapi-and-dpapi-ng-the-credential-vault-under-everything/)**, 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" [@src-dpapi]. 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](/blog/bitlocker-on-windows-architecture-attacks-and-the-limits-of-/)** 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 [@src-bitlocker]; that key material, like all software randomness on the system, is drawn from the CNG CSPRNG [@src-win10rng-pdf].
- **[Schannel](/blog/rotating-every-cipher-schannel-and-the-twenty-year-algorithm/)**, the Windows TLS stack [@src-schannel], pulls per-handshake randomness and ephemeral (EC)DHE secrets on every connection through the same CryptoAPI/CNG plumbing [@src-win10rng-pdf].
- **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 [@src-rfc4122].

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.

> **Key idea:** 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 [@src-win10rng-pdf]. 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 $2^{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 [@src-barakhalevi2005].

<Definition term="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 [@src-dodis2013].
</Definition>

That model, **robustness**, is now "the standard security goal for PRNGs" [@src-hoangshen2020]. 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.

| Mechanism | Built from | Meets the robustness goal? | Known footgun |
|---|---|---|---|
| `CTR_DRBG` | AES in counter mode | Yes, patched (Hoang and Shen, 2020) [@src-hoangshen2020] | Implementation flexibility enables cache side-channels [@src-cohney2019] |
| `HMAC_DRBG` | HMAC over a hash | In the random-oracle model, with a caveat [@src-woodageshumow2018] | No forward security if the optional input is omitted [@src-woodageshumow2018] |
| `Hash_DRBG` | a hash function | In the random-oracle model, with a caveat [@src-woodageshumow2018] | 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 [@src-hoangshen2020]. 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 [@src-woodageshumow2018]. 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 [@src-cohney2019]. 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.

<Aside label="Why statistical test suites cannot save you">
It is tempting to think you can validate a generator by running its output through Dieharder or the NIST statistical test suite. You cannot, at least not for the property that matters. Those suites detect gross bias and obvious structure -- a generator that is broken in a way a statistician would notice. They say nothing about cryptographic unpredictability. `Dual_EC_DRBG` would sail through every one of them, because its output passes standard statistical test batteries; the *catastrophic* weakness is a trapdoor, not the small, known output bias [@src-dualecbias]. Passing the tests is necessary and nowhere near sufficient.
</Aside>

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** [@src-sp80090c; @src-sp80090c-doi; @src-nist90cnews2025].

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_DRBG`s. Beneath it, SP 800-90B governs the design and health testing of the entropy sources feeding the whole construction [@src-sp80090b]. 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 [@src-inteldrng]. Windows' mix-many-trust-none design means a single compromised hardware source cannot own the seed [@src-win10rng-pdf], 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" [@src-fortuna]. 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" [@src-dodis2013]. SP 800-90B specifies health tests, but a health test detects failure; it does not measure abundance [@src-sp80090b; @src-sp80090b-pdf].

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

**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 [@src-heninger2012; @src-factorable].

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") [@src-random7], 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* [@src-cohney2019]. 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 [@src-symcrypt], 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.

<Aside label="FIPS mode, and what it changes">
Enabling FIPS mode does not swap in a different generator. Windows already uses an SP 800-90A `CTR_DRBG` implemented in SymCrypt, which is built for FIPS 140 validation [@src-symcrypt]. What FIPS mode does is constrain the system to the validated set of mechanisms and enforce the associated self-tests and boundaries. The architecture -- the chain, the mixing, the reseed schedule -- is the same with FIPS mode on or off. It is a compliance and configuration control, not a security upgrade to the RNG itself.
</Aside>

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 [@src-shor1997]; it does not make a well-seeded `CTR_DRBG` predictable, and a good CSPRNG needs no [post-quantum](/blog/post-quantum-cryptography-on-windows-the-thirty-year-migrati/) 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 [@src-fips203] and SLH-DSA between 48 and 96 bytes [@src-fips205], against roughly the 32-byte scalar an elliptic-curve or EdDSA key needs, while ML-DSA draws about the same, a 32-byte seed [@src-fips204].

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 [@src-sp80090b].

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.

| Platform | Call this | Never for secrets |
|---|---|---|
| Windows (native) | `BCryptGenRandom` with `BCRYPT_USE_SYSTEM_PREFERRED_RNG`, or `ProcessPrng` [@src-processprng; @src-bcryptgenrandom] | `rand()`, legacy `CryptGenRandom` |
| .NET | `System.Security.Cryptography.RandomNumberGenerator` [@src-dotnet-rng] | `System.Random` |
| Linux | `getrandom()` or `getentropy()` [@src-getrandom] | `/dev/urandom` read before init, `rand()` |
| BSD / macOS | `arc4random_buf`, `arc4random_uniform` [@src-arc4random] | `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 [@src-mt]. 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.

<RunnableCode lang="js" title="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");
`}</RunnableCode>

> **Note:** The instinct to "strengthen" a key by stirring in your own randomness is almost always counterproductive. The OS pool is conditioned from many independent sources; app-supplied entropy is usually worse and can only dilute. On Windows 8 and later, the flag that once allowed it is ignored outright [@src-bcryptgenrandom]. Call the system generator and trust the architecture the last eleven sections described.

<Spoiler kind="solution" label="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 [@src-dotnet-rng]. On Linux, a program calling `getrandom(buf, 32, 0)` blocks only if the pool is not yet initialized, then fills the buffer [@src-getrandom]. On OpenBSD or macOS, `arc4random_buf(buf, 32)` never blocks and never fails [@src-arc4random]. In every case you are reaching the same two-layer design: an entropy-fed pool keying a fast, forward-secure generator.
</Spoiler>

<FAQ title="Frequently asked questions">
<FAQItem question="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 [@src-msdualec]. 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 [@src-nistremoval2014; @src-reuters2013].
</FAQItem>
<FAQItem question="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 [@src-win10rng-pdf].
</FAQItem>
<FAQItem question="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 [@src-win10rng-pdf].
</FAQItem>
<FAQItem question="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 [@src-processprng; @src-bcryptgenrandom; @src-dotnet-rng].
</FAQItem>
<FAQItem question="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 [@src-mt].
</FAQItem>
<FAQItem question="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 [@src-win10rng-pdf; @src-getrandom].
</FAQItem>
<FAQItem question="Should I add my own entropy or reseed by hand?">
No. See the "Do not add your own entropy" callout above.
</FAQItem>
</FAQ>

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_DRBG`s 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.

<StudyGuide slug="windows-rng-cng-csprng" keyTerms={[
  { term: "Min-entropy", definition: "The worst-case measure of unpredictability of a source; it upper-bounds the security of every key drawn from a seed." },
  { term: "DRBG", definition: "Deterministic Random Bit Generator: an algorithm that stretches a seed into many output bits but creates no new unpredictability." },
  { term: "CSPRNG", definition: "A DRBG whose output no efficient adversary can distinguish from uniform random bits, even given prior output." },
  { term: "CTR_DRBG", definition: "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." },
  { term: "Backtracking resistance", definition: "Forward secrecy: a state compromise cannot reveal past output, because the state update is one-way." },
  { term: "Prediction resistance", definition: "After fresh entropy is mixed in, output becomes unpredictable again even if the prior state leaked; this is why generators reseed." },
  { term: "Entropy source (TRNG)", definition: "A physical process producing genuine unpredictability, such as interrupt-timing jitter, RDSEED, or a TPM, as opposed to an algorithm." },
  { term: "VM Generation ID", definition: "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." },
  { term: "ProcessPrng", definition: "The user-mode CNG entry point that returns bytes from the user-mode per-processor CTR_DRBG, seeded from the kernel chain." },
  { term: "BCryptGenRandom", definition: "The recommended CNG randomness API; with BCRYPT_USE_SYSTEM_PREFERRED_RNG it draws from the system generator." }
]} questions={[
  { q: "Why is passing every statistical randomness test not enough for a key generator?", a: "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." },
  { q: "What exactly was the 2007 CryptGenRandom break?", a: "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." },
  { q: "Why does Windows mix many entropy sources instead of trusting one good one?", a: "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." },
  { q: "Where does the claim 'the OS rolls the dice for every secret' stop being true?", a: "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." }
]} />
