The Random Number Degenerator

The Random Number Degenerator

•

Episode description

Why is a mistake in randomness so hard to catch?

An audio edition of The Random Number Degenerator, based on episode 101 of Bitcoin, Explained with Aaron van Wirdum and Sjors Provoost.

COLDCARD’s randomness failure left wallets looking functional while making their seeds easier to guess. This article follows the firmware’s build configuration, fallback generators and reseeding bottleneck, then examines why ordinary output tests can miss predictable randomness.

Sjorsnado, passed through a transformer. AI-assisted article written in Sjors Provoost’s voice, with synthetic narration using ElevenLabs: Sarah for the introduction and an original custom male voice for the body. This is an article narration; the original conversation is linked below.

Sources and further reading:

Download transcript (.vtt)
0:00

The Random Number Degenerator.

0:01

Why is a mistake in randomness so hard to catch?

0:04

From the blog: Sjorsnado passed through a transformer.

0:08

Aaron and I recently discussed the COLDCARD vulnerability on episode 101 of Bitcoin, Explained. A firmware integration mistake weakened the randomness used to generate wallet seeds, without producing an obvious malfunction.

0:20

Some mistakes would be easy to spot. If a bug caused every hardware wallet to generate the same seed, a tester setting up two devices would get identical seed phrases. Customers might discover that their new wallet already had transactions.

0:34

COLDCARD's mistake was harder to spot. The affected devices could still generate different seeds, receive bitcoin, and spend it normally. A tester setting up a handful of wallets might see no duplicate seed phrases or failed transactions. Yet the seeds came from a much smaller set of possibilities than intended, making them easier for an attacker to guess.

0:55

A uniformly random 256-bit secret has 2²⁵⁶ possibilities. Merely storing a number in 256 bits does not give it that much unpredictability.

1:06

Imagine choosing one number between zero and 255, hashing it, and using the resulting 256-bit digest as a secret. Each digest would look quite different. But an attacker could reproduce the entire collection with 256 hash calculations. Encoding the results as seed words changes nothing.

1:26

A cryptographically secure pseudorandom generator can expand unpredictable input into the bytes an application needs. A general-purpose generator for simulations need not keep its output unpredictable to an attacker.

1:39

Coinkite's technical account describes a migration to libNgU that changed which random-byte function wallet creation called. That function ultimately reached MicroPython's general-purpose Yasmarang generator instead of the intended STM32 hardware random-number generator (RNG).

1:56

LibNgU wraps libsecp256k1 and other cryptographic operations. Correct elliptic-curve arithmetic does not settle how the surrounding application generates its seed. For background on software libraries, chapter 4 of Bitcoin: A Work in Progress, libsecp256k1 (Software Libraries), explains why Bitcoin Core replaced OpenSSL and the risks of changing cryptographic libraries.

2:22

In the podcast, we described the danger of simulator code reaching a real device. Here, the specific failure involved a runtime library's fallback, selected when building the firmware. It was not a runtime switch after hardware failure.

2:37

The build checks confused a setting's existence with its value. COLDCARD defined MICROPY_HW_ENABLE_RNG as zero, intentionally using its own hardware implementation. LibNgU's #ifndef guard accepted a defined setting, while MicroPython's #if selected its software generator because the value was zero.

2:57

The board implementation also lacked the global rng_get() function libNgU expected; the fallback supplied it. The linker found a function and reported no error. Fixing the guard would expose the configuration problem; the call also needed connecting to the hardware implementation.

3:14

Hardware-RNG code could therefore be present without wallet creation ever using it.

3:19

The fallback implementation initialized its state using part of the chip identifier and timing registers, then evolved it deterministically. These inputs can make devices and boots produce different outputs, but identifiers are fixed manufacturing information, and timing values may be correlated or constrained. Neither guarantees an unguessable secret.

3:38

There were two copies of Yasmarang in this path. LibNgU XORed the MicroPython fallback's output with output from its own Yasmarang generator, which started from public constants. If an attacker can reproduce both streams, XORing them leaves the result reproducible too.

3:55

Keeping the device offline cannot prevent an attacker from reconstructing its seed and testing candidates against a known address. The attacker still needs to reproduce or search the relevant generator state and execution history. Block's analysis and Wizardsardine's reconstruction examine the device, timing, and execution-history assumptions behind attack estimates.

4:16

Later devices added another input, from the secure elements. This was a chance to compensate for the broken STM32 path: retaining and securely mixing enough independent, unpredictable input could have kept the seed secure despite that first mistake. More precisely, the Mk4 reseeding code hashed the collected material and retained just four bytes of the digest before passing it to ngu.random.reseed(). That function replaced one 32-bit state word in libNgU's generator. It did not reseed MicroPython's fallback.

4:48

Four bytes can hold at most 2³² distinct values. For any fixed values of the generator's other state and call history, varying that four-byte input can therefore select at most 2³² output streams. Producing more bytes afterward cannot undo that bottleneck. This is a limit on what that input contributes, rather than a claim that every affected wallet can be recovered by trying exactly 2³² candidates.

5:15

Why didn't testing catch the problem? Consider a test that requests random bytes and checks that successive requests differ. A deterministic generator can pass. The affected seed-generation code checked that its 32-byte output contained more than four distinct byte values, then hashed it twice with SHA-256. A predictable sequence can pass that check too. Even tests that examine the statistical distribution of output do not establish that an attacker cannot reproduce it.

5:44

Testing many devices for duplicate seeds would not necessarily expose the problem either. In a toy system with 2³² equally likely seeds, 1,000 independent draws have only about a 0.012% chance of producing any duplicate. Yet an attacker would have at most 2³² candidates to enumerate. This is a birthday-paradox example, not an estimate of COLDCARD's actual seed space.

6:11

Aaron and I discussed the same dangerous middle ground in episode 83, The Milk Sad Vulnerability. Libbitcoin's developers describe bx seed as intended for demonstrations, but people used it to create real wallets. It produced long, varied-looking outputs using Mersenne Twister initialized from only 32 bits of clock data. A tutorial could seem to work: generate a seed, derive an address, receive bitcoin. The weakness did not announce itself. A deliberately fixed dummy seed would have made the distinction between a working example and a secure wallet much harder to overlook.

6:48

For COLDCARD, Coinkite says the fix added a build-time check that rng_get() comes from the board's hardware-RNG code.

6:56

In the episode, Aaron and I explore making test implementations return an obvious fixed value, and combining sources so any one can supply enough unpredictability on its own. Our discussion needs one qualification: XORing a value with an identical copy gives zero. At least one source must remain unpredictable to the attacker, and the combination must preserve that uncertainty. Either approach still needs checks of what the production build actually uses.

7:25

Audits and open source create opportunities to find these mistakes, but scope matters. Security labs have demonstrated how to extract seeds from hardware wallets. Ledger's Donjon published a physical seed-extraction attack on Trezor, and Kraken Security Labs used voltage glitching to extract an encrypted seed and then crack its PIN. These attacks ask whether someone who gets hold of your device can recover its secrets.

7:49

My theory in the podcast was that researchers may have concentrated on that question and missed the generation problem. You can spend weeks trying to read a seed out of a chip without checking whether it was unpredictable when created. I don't know what each researcher examined on COLDCARD; this is a possible explanation, not a finding about those labs' work.

8:09

Generation does get scrutiny elsewhere: Donjon also discovered weak seed generation in the Trust Wallet browser extension. The lesson is to check both how a seed is created and how it is protected afterward.

8:22

The review question is simple: where do these bytes actually come from? Follow it through the build configuration, linked implementation, reseeding interface, and hardware call. Put those checks in the build and test suite, so a refactor cannot silently change the answer.

8:38

For more on testing, combining randomness sources, and the limits of audits, listen to episode 101 of Bitcoin, Explained. Aaron and I also discuss the trade-offs of hardware-wallet multisig and, toward the end, our responsibilities as podcasters who had accepted Coinkite as a sponsor. Subscribe in your podcast app for future technical Bitcoin discussions.