feat(coldcard): verified firmware download and export for Coldcard Q and Mk4 - #2447
Open
BullishNode wants to merge 5 commits into
Open
feat(coldcard): verified firmware download and export for Coldcard Q and Mk4#2447BullishNode wants to merge 5 commits into
BullishNode wants to merge 5 commits into
Conversation
New workspace package packages/coldcard_firmware: discovers the latest firmware on coldcard.com per model, downloads it streamed with a size cap, and verifies it against Coinkite's PGP clear-signed signatures.txt using a compile-time-pinned signing key (DocHex, 4589779ADFC14F3327534EA8A3A31BAD5A2A5B10). Improves on rust-coldcard, which checks SHA-256 but never the manifest signature. DownloadedFirmware and VerifiedFirmware are constructible only by the client: verify() recomputes the SHA-256 over a private copy of the bytes and returns them unmodifiable, so a verified result can be neither forged nor mutated. No security knob is exported — trust anchor, verifier and endpoints are fixed in the public constructor, with @VisibleForTesting overrides reserved for this package's own tests. Metadata fetches are size-capped and the default client has connect/receive timeouts. No Flutter dependency. dart_pg pinned exact (2.0.0, because 2.1.0 requires pointycastle 4 which conflicts with ledger_bitcoin). 48 offline tests against snapshotted real manifest and download pages plus a throwaway test key, including forge, tamper, oversized-input and wrong-key coverage; verified live end to end against coldcard.com for Q and Mk4. Plan: plans/coldcard-firmware-update.md (PR 1 of 3 for issue #1260).
Settings > Bitcoin settings > Coldcard update: pick a model (Q or Mk4), fetch and display the latest firmware from the signed manifest, download with live progress, verify (PGP manifest signature by the pinned Coinkite key + SHA-256), then export the verified .dfu to any folder or microSD via the system picker, with install instructions in a bottom sheet. Core module lib/core/coldcard_firmware wraps the coldcard_firmware package behind a domain boundary (entities, sealed ColdcardFirmwareFailure, repository + three usecases); only a verified entity ever reaches the export path and every failure fails closed to an error state. A fresh package client is created per flow so the cached signed manifest can never go stale in a long-lived session, and leaving the screen aborts an in-flight download via CancelToken. Feature module lib/features/coldcard_firmware follows the ledger shape: freezed state + cubit (unit-tested, 11 tests), router-provided cubit, failure l10n extension, screens reusing BorderedTappableTile/BBButton/BlurredBottomSheet and the success-tick animation. Verified end to end on the Android emulator: Mk4 v5.5.1 and Q v1.4.1Q discovered, downloaded, verified, and the exported file's SHA-256 matches Coinkite's signed manifest.
|
Claude finished @BullishNode's task in 9m 59s —— View job Todo
So far: the embedded PGP key in |
BullishNode
marked this pull request as draft
July 14, 2026 01:46
Contributor
Author
|
I’m reworking the branch in follow-up commits based on review: package test-surface hardening, feature-owned flow-scoped firmware session, and safe route handling. The PR is staying draft and existing history will not be rewritten; I’ll request re-review after the focused tests and full checks are green. |
BullishNode
marked this pull request as ready for review
July 14, 2026 02:57
Contributor
Author
|
@thibistaken ready for review (reviewed by Fable and GPT 5.6 SOL ULTRA already) |
Coinkite ships one firmware image for the Mk5 and Mk4 (the -mk- suffix line, listed as Mk5/Mk4 on coldcard.com since v5.5.0), so the model card labeled Coldcard Mk4 silently excluded Mk5 owners. Rename the two models to Q and MK, matching Coinkite's line naming, and say on the MK card that it covers Mk5 and Mk4. Presentation only: enum values, page paths, filename parsing and version spaces are unchanged.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Part of #1260.
Why
Keeping Coldcard firmware up to date currently requires the command line. Coinkite's own instructions are: download the
.dfufrom coldcard.com, fetchsignatures.txt, verify its PGP signature withgpg, then check the file's SHA-256 by hand. Realistically almost nobody does this, people download the file from a browser and copy it to the microSD unverified, trusting that the device's own signature check will catch anything wrong. That on-device check is real, but it is the last line of defense, not a reason to skip supply-chain verification entirely.This PR gives users the whole verified pipeline in the app: pick your Coldcard model, see the latest firmware, download it, have the signature and hash checked automatically, and export the verified file to any folder or microSD card. No terminal, no gpg, no manual hash comparison.
The flow
Settings → Bitcoin settings → Coldcard update.
Any failure at any step lands on an error state with a retry. There is no path to the export button except through successful verification.
How verification works
Coinkite publishes
signatures.txtin the firmware repo: a PGP clear-signed manifest listing the SHA-256 of every release ever shipped, signed by Peter Gray's release key. The app bundles that public key at compile time (fingerprint4589 779A DFC1 4F33 2753 4EA8 A3A3 1BAD 5A2A 5B10) and never fetches it at runtime, since fetching the key over the same channel as the manifest would make the verification circular. Verification requires a valid signature by exactly that pinned key, then an SHA-256 match between the downloaded bytes and the manifest entry for that exact filename.The coldcard.com downloads page is scraped only to learn which release is currently offered; nothing is shown to the user unless that filename also appears in the verified manifest. If the page layout ever changes, discovery falls back to the newest manifest entry for the model.
For comparison, rust-coldcard's upgrade tool checks the SHA-256 against signatures.txt but never verifies the manifest's signature, so a compromised endpoint could feed it a matching bad hash and bad file together. Checking the signature closes that hole. The Coldcard's bootloader check against factory keys remains the final authority; this is the gate in front of it.
Some implementation details that matter for review:
DownloadedFirmwareandVerifiedFirmwarehave private constructors. The only way to obtain a verified result isverify(), which recomputes the hash over a private copy of the bytes and returns that copy as an unmodifiable list. A lying hash field or later mutation cannot produce or corrupt a verified result, and the app's export path only accepts the verified type.@visibleForTesting.X) and-factorybuilds are filtered out at the parser level and can never be offered.Structure
Two layers, per the monorepo direction in ARCHITECTURE.md:
packages/coldcard_firmware— a pure Dart workspace package (no Flutter) that owns discovery, download and verification. Second workspace member afterbull_ui. Usable from a CLI or another app as-is.lib/core/coldcard_firmware+lib/features/coldcard_firmware— the app integration, following the ledger module shape: domain entities and sealedColdcardFirmwareFailurebehind a repository interface, one usecase per intent, a freezed cubit provided at the route, failure l10n extension, and screens reusing the existing widget kit (BorderedTappableTile,BBButton,BlurredBottomSheet, the success-tick animation). Package types never cross the repository boundary.Dependencies
New dependency, package level:
dart_pg(pinned exact at2.0.0) — the only maintained pure-Dart OpenPGP implementation, used solely to verify the clear-signed manifest. Two things to know: it is pinned exact rather than caret because it is the trust anchor of the feature and unaudited upstream, so bumps should be deliberate and reviewed; and it is 2.0.0 rather than 2.1.0 because 2.1.0 requires pointycastle 4.x, which conflicts withledger_bitcoin(pointycastle 3.x) — the 2.1.0 diff is only that bump plus removal of cipher engines we never touch. The alternatives were theopenpgppackage (gopenpgp compiled to native binaries — a large opaque blob per platform, and it breaks the pure-Dart goal) or hand-rolling the RSA signature check with pointycastle (smaller surface, more custom crypto code to review; still available as a fallback if we ever want to drop dart_pg). As defense in depth against a bug in dart_pg, the SHA-256 comparison is computed independently withcrypto, so the PGP layer alone can never bless tampered bytes.crypto,dio,meta— already direct dependencies of the app, reused at the same versions:cryptofor streaming SHA-256,dioas the HTTP client (and so the app can later hand the package a Tor-configured instance),metafor@useResult/@visibleForTesting.App level: no new dependencies at all. Export uses the existing
file_pickersaveFile, dates useintl, state usesfreezed/flutter_bloc, all already in the tree.Testing
signatures.txtsnapshot, real downloads-page HTML, and a throwaway signing key generated for the test suite). Coverage includes: one flipped hash character in the manifest, an injected manifest line, a manifest signed by the wrong key, tampered firmware bytes, oversized responses, hostile filenames, and the forged/mutated-verdict bypasses (which now fail to compile or fail verification).1139f219…af95e7) matches Coinkite's signed manifest byte for byte.Review focus
packages/coldcard_firmware/lib/src/firmware/trusted_key.dartis the file that deserves the most scrutiny. The armored key was obtained independently from keybase.io/dochex and keyserver.ubuntu.com (identical), andgpg --verifyof the live signatures.txt reports a good signature from it. Please re-derive the fingerprint from those sources yourself before approving, and treat any future diff to that file the same way.Out of scope
The rest of #1260 (USB device listing, bag number, automated USB upgrade, PSBT flow) needs the CKCC USB protocol and is Android-only territory; it will come as separate packages/PRs. Mk3 support (frozen at v4.1.9) was deliberately left out of the model list for now.