Mk3 Firmware Lab — Information

← Back to the emulator

This page lists everything the emulator models, everything it assumes, and everything it deliberately leaves out. The main page keeps the Coldcard centered, with firmware provenance on the left and live RNG registers and call history on the right.


What this project is

An interactive, source-reflective walkthrough of Coldcard Mk3 firmware v4.1.9, pinned to the 2023-06-26T1241-v4.1.9 tag. It reproduces the source menu order and predicates, four-row navigation, story scrolling, boot/PIN and wallet-state branches, settings choices, redacted new-wallet and backup quizzes, and source-anchored RNG call sites. It is an audit and teaching tool, not a wallet-recovery utility.

What this project is not

There is no BIP39 word conversion, no private-key derivation, no address generation, no target matching and no batch scanning. Those are omitted on purpose and will not be added. The emulator will never ask you for a mnemonic or a passphrase, and you should never type a real one into any website.

The defect in one paragraph

When the STM32 hardware RNG is unavailable, the affected firmware falls back to a small software generator called Yasmarang. That generator is seeded once per MCU reset from three values that are far more predictable than they look: the low 32 bits of the chip UID, the current SysTick counter value, and two RTC registers. Everything drawn after that point is a deterministic function of those inputs plus the exact sequence of RNG calls the firmware happened to make beforehand.

The initializer

On the first fallback call after a reset, the generator state is set from pad = UID_low32 XOR SysTick, n = RTC_TR, d = RTC_SSR and dat = 0. The capture is lazy: the values are read when the first fallback call happens, not at boot.

The UID coordinate model

Word 0 of the STM32 UID contains the wafer X and Y coordinates of the die. This lab maps X to the low half of the word and Y to the high half, then assumes each coordinate ranges uniformly over 0…255. That is a research population model. It is not a measured dataset of sold Coldcards, and it is not Coinkite's broader preliminary estimate of roughly 40 bits.

The RTC-zero assumption

The lab defaults to RTC_TR = RTC_SSR = 0. This is an explicit research assumption about the power-on state of those registers, not a fact established by reading the firmware. You can override both registers from the right-hand register rail. These are counterfactual lab controls, not live writes to a physical Mk3: the existing modeled call sequence is replayed from the newly selected initializer and any stale displayed output is cleared.

The Yasmarang step function

Both generator instances use the same state machine. One step is:

sum = pad + dat + (d * n)
pad = rotl32(sum, 3)
n   = pad | 2
d   = d XOR rotr32(pad, 1)
dat = (dat XOR (pad & 0xff) XOR ((d >> 8) & 0xff) XOR 1) & 0xff
out = pad XOR (d << 5) XOR (pad >> 18) XOR (dat << 1)

Two generators, not one

There are two independent Yasmarang instances. The fallback instance stands in for the hardware RNG and is seeded as described above. The public mixer instance lives in the ngu.random module and always starts from the same fixed constants: pad = 0x0A8CE26F, n = 69, d = 233, dat = 0. Because the public mixer starts identically on every device, it adds no unpredictability at all — it only permutes what the fallback generator produced. The lab counts the two call streams separately, shown as F and P.

random.bytes(n)

Bytes are produced four at a time. Each word takes one fallback output and one public-mixer output and XORs them, then packs the result little-endian. Before mixing, the raw fallback word is compared against the previous raw fallback word within the same call; if they are equal, the call raises EFAULT. That duplicate check is scoped per call, so it resets every time bytes() is invoked again. EFAULT aborts that call, but it does not permanently poison either C generator; a later separate call resumes from the state already advanced by the failed attempt.

ckcc.rng_bytes(buffer)

Some firmware paths call the lower-level Coldcard RNG buffer function directly. Those calls advance the fallback stream without advancing the ngu.random public mixer. The encrypted-backup password, 7z salt and IV, user secrets, internal-flash overwrite and MicroSD format paths use this interface. Its consecutive-word fault check crosses call boundaries through the C last_value static; the lab tracks that state separately.

random.uniform(max)

One fallback word and one public word are XORed, then masked. If the masked value is at or above max, the code draws only from the public mixer again and XORs that in, repeating until the value falls in range. This is why the two call counters drift apart: rejection sampling advances the public mixer without advancing the fallback generator.

shuffle(length)

A Fisher–Yates shuffle over a list of length items performs length − 1 calls to uniform() with descending maxima. Most of the firmware's incidental RNG consumption is shuffles.

ngu.random.reseed(value)

Replaces only the public mixer's pad. It leaves the public n, d and dat alone and does not touch the fallback generator at all. Production v4.1.9 has no call site for it; the lab exposes it because it is part of the disclosed API surface.


Call history: what advances the state

"Call history" is shorthand for the unobserved sequence of RNG calls that advanced the generator before the seed was drawn. The Mk3 does not store or authenticate such a log. Each item below is a real v4.1.9 call site and lists what it costs in fallback words.

Terms acceptance / first settings save — 31 F

Fisher–Yates shuffle of the 32 settings slots in flash.

Existing settings save — 30 F

Same shuffle, but the currently occupied slot is excluded, so 31 items.

Randomized two-part PIN keypad — 18 F

Two independent shuffles of ten keys, one per PIN half.

Paper-wallet keypair — 8 to 16 F

A 32-byte secp256k1 candidate. If the scalar is zero or above the curve order, the code retries exactly once and then fails.

USB encrypted session — 8 to 16 F

An ephemeral secp256k1 candidate with the same single-retry rule.

Completely erased settings flash — 3,103 F

A shuffle of 32 slots followed by 48 separate random.bytes(256) calls. This is by far the largest single consumer.

Encrypted-backup filename — 2 F

One uniform(2048) followed by one uniform(1000).

Encrypted-backup password — 8 F, 0 P

One direct ckcc.rng_bytes(32) call creates the entropy for the 12-word backup password.

Encrypted-backup salt and IV — 8 F, 0 P

The 7z builder makes two separate direct calls: ckcc.rng_bytes(16) for the salt and another 16 bytes for the IV.

Backup-password quiz order — 13 F plus questions

The limited four-question quiz shuffles 11 candidate positions, keeps three plus the final word, then shuffles those four. Each displayed question normally costs four more fallback calls: uniform(12), uniform(2048), and shuffle(3).

MicroSD format overwrite — 131,072 F, 0 P

The source overwrites 1,024 blocks, each using a direct ckcc.rng_bytes(512) call, before formatting the card. The lab models the calls but never touches a card or local file.

HSM confirmation character — 1 F

A single uniform(5).

HSM local authorization code — 4 F

One random.bytes(15), which rounds up to four words.

USB MITM-check delay — 1 F

A single uniform(100).

Random XOR-seed part — 8 F

One random.bytes(32).

Membrane keypad scan window — 3 F

Each fresh scan window randomizes the order in which the four keypad rows are driven, using shuffle(4). In the emulator every key press charges one scan window. On real hardware, presses close together in time can land inside one still-open window and share a single shuffle, so this is an upper bound rather than an exact count.

ngu.random.uint32() — 1 F

Advances the public mixer and then the fallback generator once each and XORs them. No duplicate test is applied on this path.

Seed generation itself

Generating a 24-word wallet draws random.bytes(32), so eight fallback words and eight public words. The firmware then asserts that the 32 bytes contain more than four distinct byte values and refuses the seed if they do not. Both that assertion and the EFAULT duplicate check are modeled, and either one will halt that generation attempt in the emulator. The v4.1.9 EmptyWallet menu invokes this operation directly; there is no extra “24 Words” submenu. The lab then shows 24 redacted placeholders, performs the source's 24-position quiz-order shuffle, and walks all 24 redacted questions. It never shows the raw 32 bytes—only a truncated audit fingerprint—and never performs BIP39 conversion, key derivation or address generation.

For an ordinary question, the source calls uniform(24), then uniform(2048), then shuffle(3), for a minimum of four fallback calls. Either uniform loop repeats if it picks a duplicate word. Since the lab deliberately does not materialize or expose the mnemonic, it models the first-draw-distinct path and labels this boundary. A wrong answer or a detour to view the words reshuffles the three choices and costs two more fallback calls.

After a passed or explicitly skipped quiz, v4.1.9 calls set_seed_value() and then goto_top_menu(). Despite a stale comment in seed.py saying “reboot,” this function contains no machine.reset() call in the pinned tag. The RNG call history therefore continues into NormalSystem. By contrast, destroying the seed really does call machine.reset(), so that path clears the C RNG statics.


Reset semantics: where history goes

Power-on or hardware MCU reset

All C statics are initialized again. The fallback generator's seeded flag becomes false and the public mixer returns to its constants. UID, SysTick and RTC are captured lazily on the next fallback call. "History zero" here means generator state, not an erased audit log — no log ever existed.

Startup actions

Between the reset and the seed draw, the settings filler, PIN-map shuffles, keypad scans, paper wallets and USB sessions all advance the state. This is the history the attacker has to guess or enumerate.

MicroPython soft reset

The interpreter restarts without an MCU reset, so file-scope and function-static C generator state continues from wherever it was. A soft reset does not give you a fresh generator.

Clear call trace

This lab-only control clears the visible trace while preserving the modeled C state and total call counters. It is not a reset and it is not an operation a physical Mk3 can perform: the device never stored an authenticated call-history log in the first place.


Entropy geometry

Under the model above — X and Y each uniform over 0…255, SysTick over 80,000 reachable states — the number of distinct reachable initial pads is 224. The lab counts this exactly rather than estimating it, by enumerating which 32-bit pad values the XOR can actually produce for contiguous coordinate ranges and SysTick values counted from zero.

Support size is not entropy

224 is a support size: the count of distinct values the initial pad can take inside the stated model. It is not a measured entropy estimate for sold Coldcards, and it is not a claim about the real-world distribution of wafer coordinates or boot timing. Treat it as an upper bound on how much work a brute-force search over that model needs, under assumptions you can change from the emulator.

Why enumerating histories does not add entropy

If you multiply both the single-device candidate count and the global candidate count by the same set of, say, 1,000 candidate histories, the coverage fraction between them is unchanged. Enumerating histories can reveal the target's real history if it happens to be in your search set, but it does not let a fixed UID reach initial pads it could not otherwise reach. These are candidate (pad, history) pairs, not measured unique outputs. A wrong-history trace that matched the same 256-bit seed would require an independent output collision, which is a different and much stronger claim.


Claims and provenance

Importing a known seed proves nothing about origin

It works on any device with any UID. It demonstrates present knowledge of the seed, not which device generated it and not who historically controlled the funds.

A compatible native generation is possible but timing-bound

Showing a UID, timer value and history that reproduce a given entropy proves the stock algorithm could have produced it. Such a trace can be constructed after the disclosure, so on its own it establishes compatibility, not history.

Historical attribution requires earlier evidence

Pre-dispute descriptors, xpubs, several derived addresses, purchase records, funding records and timestamped commitments. "Could have generated" is not "did generate."

Forensic rule

Never accept a single address, a current signature, or possession of a physical device as proof of seed origin. Verify a complete descriptor together with contemporaneous evidence that predates the compromise.


What is exact and what is a model

Exact and checked against an independent C reference: the initializer, both Yasmarang state machines, the call ordering, the per-call duplicate check, little-endian byte packing, rejection sampling, the keypair retry rule, the seed byte-diversity assertion and the final SHA-256.

Explicit assumptions you can change: the RTC-zero power-on state, the uniform 256 × 256 wafer-coordinate population, and the 80,000 reachable SysTick states.

Source-exact within the pinned Mk3 profile: the static menu labels and order, Mk3 predicates such as ATECC608 countdown PIN and FAT-RAM user management, the four-row menu rules, chooser values, 17-character story wrapping, five-row story scrolling, and the modeled RNG primitive semantics.

Source-reflective but deliberately substituted: the visible demo-only PIN and prefix confirmation in place of hardware-backed anti-phishing words, redacted seed/backup word screens, compressed dramatic pause timing, and state transitions that would normally cross the bootloader or secure element. Hardware-only USB, MicroSD, secure-element, bootloader, signing, key derivation, address generation and destructive operations are described and never executed. This is not QEMU or a cycle-accurate STM32 emulator.

Emulator key map

5 moves up and 8 moves down, matching the Coldcard convention. 7 and 9 page up and down, and 0 returns to the top of the current menu. OK selects, X goes back. On a numeric entry screen the digit keys type, X deletes a digit and OK saves. Your physical arrow keys, Enter, Escape and number keys work too. Every key press charges one membrane scan window against the RNG, which is why the call trace grows as you navigate.

Sources


Independent educational research interface. Not affiliated with Coinkite. Never enter a real mnemonic or passphrase into this or any other website.

← Back to the emulator