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.
Permalink1. 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 twoMD_Update calls that fed the pool, leaving process-ID as effectively the only varying input, and a 15-bit PID space is , 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?
Diagram source
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 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 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.
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.
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].
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.
| 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() |
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.
The worst-case measure of unpredictability. For a source , the min-entropy is -- 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.
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."
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.
// 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 work -- a break of forward security -- while running the generator forward to predict future output was trivial, [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.
Diagram source
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 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, and , whose relationship is a secret. If anyone knows a scalar with , 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 and had been chosen. The generator could be perfect for everyone except the one party holding .
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.
| Generation | Representative design | Why it was superseded |
|---|---|---|
| G0 -- non-crypto PRNG | C rand(), Mersenne Twister | Linear state recoverable from output [9] |
| G1 -- ad-hoc app CSPRNG | Netscape SSL seed from clock and PID | Too little entropy; seeds enumerable [6] |
G2 -- legacy CryptGenRandom | User-mode, per-process DRBG | Forward-security break; reseed only per 128 KB [2] |
| G3 -- SP 800-90 DRBG menu | Standardized CTR_DRBG and siblings | A menu is not an architecture; Dual_EC withdrawn [15] |
| G4 -- modern Windows tree | Root CTR_DRBG seeding a strict chain of per-processor DRBGs | Current 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.
Diagram source
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"] 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
The SP 800-90A DRBG built from a block cipher run in counter mode. Its state is a cipher key and a counter . To produce output it encrypts successive counter values under ; after each request it runs an update step that replaces both and . 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 and a counter block . To generate output, the DRBG increments the counter before each block and encrypts the result under AES with key : the first output block is , the next is , and so on, exactly like running AES in counter mode. Because the increment comes first, the block for the current is never emitted. For AES-256 the seed length -- the size of and 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 and 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.
Diagram source
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 Before those raw entropy bytes ever become a key , they pass through a conditioning step the standard calls the derivation function.
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.
// 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.
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 and 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].
| 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 [1] |
| TPM | hardware RNG | 40 bytes at boot, 64 bytes per reseed, at most once per ~40 min | Separate chip, independent of the CPU [1] |
RDRAND / RDSEED | on-die hardware RNG | on demand | RDRAND capped at 128-bit security; RDSEED preferred; never trusted alone [1] |
| Registry seed file | stored entropy | read at boot, rewritten for next boot | Carries entropy across reboots [1] |
| UEFI / firmware seed | firmware-provided | at boot | Platform-dependent [1] |
| Hyper-V ACPI OEM0 table | hypervisor-provided | at boot on guests | Present 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.
RDSEED and to always blend -- the mathematics forces diversification before trust ever enters the picture [1].
Diagram source
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"] 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 is drained into a reseed only every -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 generate requests before a reseed is mandatory, though Windows' clock schedule reseeds far more often than that ceiling would ever require [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 and , 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].
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.
Diagram source
flowchart TD
A["VM is cloned or reverted to a snapshot"] --> B["Both instances resume with identical generator state"]
B --> C{"Did the VM Generation ID change?"}
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"] 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.
| 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 [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_DRBGis 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 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].
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.
| Mechanism | Built from | Meets the robustness goal? | Known footgun |
|---|---|---|---|
CTR_DRBG | AES in counter mode | Yes, patched (Hoang and Shen, 2020) [44] | Implementation flexibility enables cache side-channels [45] |
HMAC_DRBG | HMAC over a hash | In the random-oracle model, with a caveat [46] | No forward security if the optional input is omitted [46] |
Hash_DRBG | a hash function | In 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.
| Platform | Call this | Never for secrets |
|---|---|---|
| Windows (native) | BCryptGenRandom with BCRYPT_USE_SYSTEM_PREFERRED_RNG, or ProcessPrng [4, 5] | rand(), legacy CryptGenRandom |
| .NET | System.Security.Cryptography.RandomNumberGenerator [33] | System.Random |
| Linux | getrandom() or getentropy() [34] | /dev/urandom read before init, rand() |
| BSD / macOS | arc4random_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.
// 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?
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?
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
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.
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.
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.
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
- (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. ↩
- (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. ↩
- (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. ↩
- (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. ↩
- 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. ↩
- (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. ↩
- (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. ↩
- (2008). CVE-2008-0166. https://nvd.nist.gov/vuln/detail/CVE-2008-0166 - NVD record anchoring the Debian OpenSSL predictable-key vulnerability. ↩
- 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. ↩
- (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. ↩
- (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. ↩
- (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. ↩
- (1982). Theory and Application of Trapdoor Functions. https://doi.org/10.1109/SFCS.1982.45 - Proves unpredictability and indistinguishability are equivalent for pseudorandom sequences. ↩
- (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). ↩
- (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. ↩
- (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. ↩
- (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. ↩
- Fortuna. https://www.schneier.com/academic/fortuna/ - Fortuna: the Ferguson and Schneier multi-pool accumulator behind the Windows reseed schedule. ↩
- (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. ↩
- (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. ↩
- SymCrypt: the core cryptographic library of Windows. https://github.com/microsoft/SymCrypt - SymCrypt: the core cryptographic library used by Windows. ↩
- (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. ↩
- (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). ↩
- 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. ↩
- (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. ↩
- Yarrow. https://www.schneier.com/academic/yarrow/ - Yarrow: the Schneier and Kelsey pool-and-reseed design that preceded Fortuna. ↩
- (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. ↩
- (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. ↩
- (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. ↩
- (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. ↩
- 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. ↩
- 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. ↩
- (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. ↩
- 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. ↩
- 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. ↩
- (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. ↩
- 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. ↩
- (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. ↩
- 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. ↩
- 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. ↩
- (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. ↩
- (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. ↩
- (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. ↩
- (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. ↩
- (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. ↩
- (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. ↩
- (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. ↩
- (2025). NIST SP 800-90C (DOI). https://doi.org/10.6028/NIST.SP.800-90C - Canonical DOI for NIST SP 800-90C. ↩
- (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. ↩
- (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. ↩
- (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. ↩
- 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. ↩
- (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. ↩
- (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. ↩
- (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. ↩
- (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. ↩