Anatomy of the Coldcard Firmware PRNG Vulnerability: When Hardware Security Silently Falls Back
In hardware security, physical isolation is often treated as the ultimate security boundary. Hardware wallets—air-gapped, purpose-built microcontrollers designed to store cryptographic keys—are considered the gold standard for securing digital assets. By isolating private key generation and signing operations from compromised host operating systems, these devices aim to guarantee that unauthenticated entities cannot extract secrets or force malicious state changes.
However, hardware security is only as robust as the software executing on top of it. A critical vulnerability in the firmware powering Coldcard hardware wallets illustrates a stark reality: physical isolation and dedicated hardware random number generators (TRNGs) offer zero protection when the underlying software silently drops its security guarantees.
At the center of this flaw was a classic software engineering mistake tucked inside a C preprocessor conditional check within the libngu library. Rather than utilizing the secure, multi-source hardware entropy engine intended by the hardware designers, production devices silently defaulted to MicroPython’s built-in software Pseudorandom Number Generator (PRNG)—a deterministic engine called Yasmarang.
Instead of generating keys with 128 to 256 bits of cryptographic entropy, affected Coldcard devices derived seed phrases from a drastically reduced search space. Attackers could then systematically compute and drain wallet balances offline using simple brute-force scripts.
Understanding how a single macro evaluation error bypassed hardware-level security requires examining the interactions between firmware compilation flags, low-level preprocessor logic, and fallback entropy mechanisms in embedded systems.
The Anatomy of a Macro Mismatch: #ifdef vs #if
To understand how the firmware’s true random number generation was bypassed, we must look at how C preprocessor directives interact with build configuration files in embedded C and MicroPython environments.
In C development, preprocessor macros manage platform-specific code paths, enabling drivers, and toggling features at compile time. Two common preprocessor directives for conditional compilation are #ifdef and #if:
#ifdef MACROchecks strictly whetherMACROhas been defined in the build environment, regardless of the value assigned to it.#if MACROevaluates the numerical value assigned toMACRO. IfMACROis defined as0,#if MACROevaluates tofalse.
In Coinkite’s Coldcard firmware build setup, production configurations set the macro MICROPY_HW_ENABLE_RNG explicitly to 0. The architectural intention was clear: disable MicroPython’s default, generic hardware RNG driver so that Coldcard’s proprietary security library (libngu) could take total control of entropy generation via Coinkite’s custom hardware RNG wrapper.
/* Build Configuration Setup */
#define MICROPY_HW_ENABLE_RNG 0 /* Intent: Disable MicroPython default driver */
However, inside the libngu library, the preprocessor guard checking whether to route entropy calls to the custom driver or to the software fallback contained a fatal logic error. Instead of testing the numerical value of MICROPY_HW_ENABLE_RNG using #if, the code utilized #ifdef.
The Vulnerable Code Structure
The preprocessor evaluation proceeded as follows:
/* Vulnerable conditional compilation logic in libngu */
#ifdef MICROPY_HW_ENABLE_RNG
/*
* Because MICROPY_HW_ENABLE_RNG was defined as 0,
* #ifdef evaluates to TRUE!
*
* The compiler selects this path, assuming the standard
* MicroPython driver path is active, misdirecting execution
* into the Yasmarang PRNG fallback sequence.
*/
return default_micropython_rng_read();
#else
/*
* Coinkite's custom hardware RNG wrapper was placed here,
* intended to run when MicroPython's default driver was disabled.
* This execution path was silently unreachable in production.
*/
return coinkite_custom_trng_wrapper_read();
#endif
Because MICROPY_HW_ENABLE_RNG was explicitly #defined in the header files (even though its value was set to 0), the preprocessor directive #ifdef MICROPY_HW_ENABLE_RNG evaluated to true.
As a result, the compiler stripped out Coinkite’s dedicated hardware entropy routines entirely during build compilation. Instead, it linked the firmware directly to MicroPython’s standard internal RNG dispatch logic. But because the hardware RNG underlying MicroPython’s driver was turned off by the zero value setting (MICROPY_HW_ENABLE_RNG=0), the internal MicroPython dispatch logic silently fell back to its default software-based PRNG: Yasmarang.
| Macro Definition | Directive Used | Intended Evaluation | Actual Preprocessor Result | Selected Code Path |
|---|---|---|---|---|
#define MICROPY_HW_ENABLE_RNG 0 |
#if MICROPY_HW_ENABLE_RNG |
false (Use Custom TRNG) |
false |
Coinkite Custom TRNG Wrapper |
#define MICROPY_HW_ENABLE_RNG 0 |
#ifdef MICROPY_HW_ENABLE_RNG |
false (Use Custom TRNG) |
true |
Yasmarang PRNG Fallback |
This macro mismatch bypassed the hardware entropy sources without raising a compiler warning, triggering an execution error, or alerting the end user. The device initialized normally, rendered its UI, and presented users with valid BIP-39 mnemonic phrases—phrases that appeared secure but were generated using low-entropy state logic.
Deterministic Entropy: Inside the Yasmarang PRNG Fallback
When the preprocessor logic directed seed generation away from the custom hardware wrapper, execution fell back to MicroPython’s internal software PRNG driver, which relies on the Yasmarang algorithm.
In resource-constrained microcontrollers, lightweight algorithms like Yasmarang are designed for non-critical pseudo-random operations, such as scheduling, UI animations, or non-cryptographic networking backoffs. They are explicitly not designed to generate 256-bit cryptographic keys or standard BIP-39 master seeds.
The primary architectural flaw was not just the choice of algorithm, but how the PRNG state was seeded and maintained during operation.
[ MicroPython Boot Sequence ]
|
v
+------------------------------------+
| Retrieve STM32 96-Bit Unique ID |
| Read Low-Resolution Timer Registers|
+------------------------------------+
|
v
+------------------------------------+
| Seed Yasmarang Software PRNG State |
+------------------------------------+
|
v
+------------------------------------+
| Generate BIP-39 Master Seed |
| (Zero Fresh Entropy Added at Run) |
+------------------------------------+
Static Initialization Sources
During boot, the MicroPython engine initialized the Yasmarang PRNG state using only two primary inputs from the host STM32 microcontroller:
- The STM32 96-bit Unique Device ID (UID): A factory-programmed register burnt into the silicon during chip fabrication.
- STM32 Hardware Timer Registers: Low-resolution system tick registers sampled during the early boot sequence.
Crucially, no fresh physical entropy was collected post-initialization. Once the device finished booting, calling the PRNG function to generate a BIP-39 seed phrase derived bytes exclusively from the Yasmarang state array without injecting additional, unpredictable environmental noise.
The Entropy Collapse
Standard cryptographic seed generation relies on uniform, unpredictable entropy sources. A 128-bit BIP-39 seed phrase requires $2^{128}$ possible combinations, while a 24-word (256-bit) seed requires $2^{256}$ combinations.
Under the Yasmarang fallback execution path, the total state space collapsed:
- Static Silicon Identifiers: The STM32 96-bit UID is fixed per physical device. While unique across chips, silicon UIDs are not secret. They are assigned sequentially or in predictable batches during wafer manufacturing.
- Predictable Boot Timing: The execution time of microcontrollers from cold boot to initialization is deterministic. Timer registers sampled at boot show minimal variance across execution cycles, often varying by only a few hundred clock ticks.
Because the system collected zero dynamic runtime entropy during seed generation, the effective state space available to the generator dropped from a cryptographically secure range down to a search space small enough for practical brute-force enumeration.
Key Takeaway: A PRNG initialized solely with fixed device IDs and low-variance boot timers converts a key-generation process into a deterministic output generator. If an attacker can bound the timer variance and survey the chip ID ranges, the target key space becomes tractable.
Attack Vectors and Mitigation Realities
The deterministic nature of the Yasmarang fallback transformed key extraction from a theoretical cryptographic problem into a high-speed search problem.
Offline Brute-Forcing Dynamics
Because BIP-39 seed phrases, account key derivation (BIP-32/BIP-44), and Bitcoin address generation follow public, standardized specifications, an attacker does not need physical access to the target Coldcard device to execute an exploit. The attack vector is completely offline:
- Search Space Reduction: Attackers construct candidate state sets based on known STM32 UID allocation patterns and standard boot-timer offset ranges.
- Deterministic Sequence Generation: Using the Yasmarang algorithm, an automated script seeds a local PRNG instance with a candidate state and computes the resulting master seed.
- Key and Address Derivation: The script derives the corresponding public key tree and target addresses (e.g., Native SegWit
bc1q...or Taprootbc1p...addresses). - Blockchain Scanning: The generated addresses are cross-referenced against public blockchain indexers to identify unspent transaction outputs (UTXOs).
When a match is found, the attacker reconstructs the private key and signs transactions to transfer assets out of the compromised wallet. Because the search space was constrained by the static inputs of the Yasmarang PRNG, high-performance GPUs and automated cloud clusters could scan billions of derived wallet addresses per second.
# Simplified Conceptual Illustration of the Deterministic Seed Reconstruction Loop
import hashlib
def simulate_vulnerable_seed_derivation(stm32_uid_bytes, boot_timer_ticks):
# Reconstruct the deterministic Yasmarang initial state
combined_state = stm32_uid_bytes + boot_timer_ticks.to_bytes(4, byteorder='little')
# Internal state initialization (simplified for illustration)
prng_state = hashlib.sha256(combined_state).digest()
# Generate bytes without subsequent physical hardware entropy
master_entropy = hashlib.sha256(prng_state).digest()
return master_entropy
# Search loop over predictable timer offsets
known_uid = bytes.fromhex("363150013033323700000000") # Sample STM32 UID pattern
for timer_offset in range(1000, 5000): # Small variance window at boot
candidate_entropy = simulate_vulnerable_seed_derivation(known_uid, timer_offset)
# Check if derived seed yields target wallet address...
The Firmware Update Limitation
When security vulnerabilities of this magnitude surface, a common misconception is that flashing updated firmware renders the device secure. In this scenario, understanding the distinction between code-level remediation and state remediation is vital.
+-----------------------------------------------------------------------+
| FIRMWARE UPDATE APPLIED |
+-----------------------------------------------------------------------+
|
+--------------------------+--------------------------+
| |
v v
[ Future Seed Creations ] [ Existing Wallet Seeds ]
| |
v v
Pre-processor bug fixed. Seed was derived from
Hardware TRNG now active. low-entropy PRNG.
New seeds are SECURE. Seed remains WEAK forever.
ACTION: Migrate funds!
Applying a firmware patch updates the preprocessor logic, substituting the correct #if conditionals and directing future requests to Coinkite’s true hardware RNG driver.
However:
- Updating device firmware stops future vulnerable seed creation.
- Updating device firmware DOES NOT secure existing seeds.
Any seed phrase derived while running the vulnerable firmware version was permanently generated from low-entropy initial states. Correcting the firmware code after the fact does not retroactively add entropy to a seed phrase that already exists on the blockchain. Users with seeds derived during the vulnerable window must generate an entirely new seed phrase under patched firmware and migrate all assets to the new addresses immediately.
The Dice Roll Exception
There is one critical architectural exception to this vulnerability: seeds created using manual user-supplied entropy.
Coldcard firmware includes a feature allowing users to supply physical entropy manually during device setup by rolling standard six-sided dice.
[ Dice Roll Sequence ]
|
v
+-----------------------+ +-----------------------+ +-----------------------+
| Roll 1: Output [1-6] | ---> | Roll 2: Output [1-6] | ---> | Roll 50+: Minimum 256 |
| Mix into SHA-256 State| | Mix into SHA-256 State| | Bits Entropy Reached |
+-----------------------+ +-----------------------+ +-----------------------+
|
v
+-----------------------+
| BIP-39 Master Seed |
| (FULLY SECURE) |
+-----------------------+
If a user created their seed phrase by executing at least 50 fair, independent, and private dice rolls, the resulting seed is unaffected by this PRNG flaw alone.
When the dice roll interface is used, the firmware collects the user’s manual inputs, converts each roll into entropy bits, and mixes them directly into the target state buffer via SHA-256 hashing functions. Even if the underlying software PRNG provided zero security guarantees, 50 rolls of a fair six-sided die supply over 129 bits of pure physical entropy ($\log_2(6) \approx 2.585$ bits per roll), exceeding the minimum threshold needed to secure a 12-word BIP-39 seed.
This mechanical mitigation highlights the resilience of zero-trust user entropy mechanisms when software layers fail.
Broader Implications for Embedded Systems Design
The Coldcard PRNG vulnerability is not an isolated edge case; it reveals systemic risks present across low-level software engineering, embedded systems architecture, and security-critical software design.
Silent-Fail Security Primitives
The most dangerous aspect of the libngu bug was its silence. In standard application development, a missing dependency or invalid configuration parameter produces runtime exceptions, log entries, or failed builds. In secure embedded systems, falling back to a secondary execution path without halting execution violates the principle of safe-fail engineering.
When a security-critical primitive—such as a True Random Number Generator—cannot be verified or initialized properly, the system must fail closed. It should halt execution, throw an unrecoverable hardware exception, and refuse to proceed with key generation.
/* Defensive Failure Strategy: Fail Closed */
if (!hardware_rng_is_verified()) {
/*
* DO NOT fallback to software PRNGs during key creation.
* Force a system halt and notify the user.
*/
display_critical_error("TRNG Hardware Initialization Failed");
system_halt();
}
Allowing a cryptographic function to fail open or fail soft by falling back to a lower-security mode ensures that security guarantees are degraded precisely when they are needed most.
Contextual Parallels in System Integrity
Maintaining hardware integrity across variable software states is a challenge shared across many domains of technology infrastructure.
For instance, software stack misconfigurations and subtle preprocessor failures resemble the edge cases found in higher-level software frameworks, such as file handling bugs like CVE-2026-66066 in Ruby on Rails Active Storage, where implicit execution assumptions break security boundaries.
Similar concerns over hardware reliability, software configuration management, and supply chain verification appear in global infrastructure technology, from high-performance processing hardware such as the DeepSeek architecture navigating AI compute constraints to hardware integrity mandates seen in regulatory policy like the FCC ban on foreign robotics and power inverters.
Whether managing high-density power systems, compute clusters, or air-gapped cryptographic signing devices, relying on unverified assumptions about low-level hardware states introduces critical vulnerabilities.
Defensive Coding Standards in Embedded C and MicroPython
To prevent similar preprocessor and architectural failures in embedded C and MicroPython firmware, engineering teams should implement strict defensive coding standards:
- Avoid
#ifdeffor Numeric Toggles: Never use#ifdefor#ifndefto check flags that hold boolean or numeric values (0or1). Use#if defined(MACRO) && (MACRO != 0)or enforce#if MACROwith explicit compiler flags like-Wundef.
/* Vulnerable */
#ifdef FEATURE_ENABLED
// Executes even if FEATURE_ENABLED is defined as 0
#endif
/* Defensive */
#if defined(FEATURE_ENABLED) && (FEATURE_ENABLED != 0)
// Executes ONLY if FEATURE_ENABLED is explicitly non-zero
#else
#error "FEATURE_ENABLED is explicitly disabled or undefined!"
#endif
-
Enforce Static Analysis Rules for Preprocessors: Standard static analysis tools often parse C code after macro expansion, missing preprocessor logic errors entirely. Security pipelines must run specialized preprocessor linter rules that analyze raw AST conditional blocks.
-
Runtime Entropy Health Tests: Firmware should continuously run continuous random number generator tests (such as NIST SP 800-90B or FIPS 140-2 entropy checks) on data drawn from entropy APIs before feeding it into key derivation routines. If the output stream exhibits deterministic patterns or low entropy scores, execution halts immediately.
Future Outlook: Raising the Bar for Open-Source Hardware Security
The dissection of the Coldcard firmware vulnerability has driven significant shifts in how open-source hardware wallet vendors design, verify, and build security systems.
+------------------------------------------------------------------+
| HARDWARE FIRMWARE SECURITY ARCHITECTURE |
+------------------------------------------------------------------+
|
+-----------------------------+-----------------------------+
| | |
v v v
[ Preprocessor Audits ] [ Mandatory User Entropy ] [ Verifiable Builds ]
| | |
v v v
Static pre-compilation Multi-source mixing via Deterministic, public
linter checks for #ifdef manual physical dice rolls CI static verification
Preprocessor and Build Pipeline Scrutiny
Security teams across the embedded landscape are re-auditing C preprocessor dependencies within core cryptography libraries. Automated static analysis tooling is being updated to flag any instance where a macro defined as zero can alter security execution logic. Modern build systems are increasingly adopting stricter compiler flags (-Wundef, -Werror) to treat any undefined or ambiguously evaluated macro as a build breaker.
Mandatory Multi-Source Entropy Systems
Hardware wallet vendors are moving away from single-source entropy models. Modern firmware architectures are adopting mandatory multi-source entropy pipelines that combine:
- Internal Secure Element TRNGs
- Microcontroller mainboard TRNGs
- Environmental noise registers
- User-supplied physical entropy (e.g., manual dice rolls)
By requiring user-supplied physical entropy to be combined into every seed derivation sequence via cryptographic hash functions, devices ensure that even if a hardware driver or software PRNG is compromised, the seed remains secure due to the external entropy provided by the user.
Verifiable Builds and Continuous Formal Verification
The open-source community is placing greater emphasis on reproducible build setups and formal verification pipelines. Security audits are shifting focus from high-level application logic to include low-level build scripts, platform-specific preprocessor macro maps, and compiler flags.
As embedded hardware continues to secure high-value systems, the industry is learning a clear lesson: true hardware security is not defined by physical isolation alone, but by the rigor and verification applied to every line of underlying code.