VeilSwap
← All posts
Explainers2026-10-299 min read

Hardware Wallet Firmware: Verify the Signature, Not the Site

We downloaded Trezor 2.9.1 (1,614,848 bytes) and reproduced its fingerprint byte by byte: the file SHA-256 and the published value differ, and both are right. Full verification, including the 2-of-3 signature.

On 29 October 2026 I pulled Trezor's T2T1 firmware 2.9.1 from data.trezor.io. Size: 1,614,848 bytes. SHA-256: 6a84c200f13b564ffc2f16c2a6806272b0e7561d04d434f55b495d15c5a0c027. The same version listed in the vendor's releases.json carries fingerprint aaa616c14904cf6eb940497d7fd5da8735ab0d0ecdebd3f6ced49fff0ce278a0. Two different values for one file, and both are correct. If you have ever run sha256sum on a firmware image, compared it to the published number, watched it mismatch and concluded you had a tampered download, this is the explanation.

Two hashes, two jobs

The SHA-256 covers the bytes on disk, which is a transport check: it tells you the download was not truncated or mangled in transit. The fingerprint is something else entirely — it is the value the device's bootloader computes over the firmware header, and it is the value the vendor's signature signs. Concretely, the 2.9.1 file is three pieces: a 4,608-byte vendor header (magic TRZV), a 1,024-byte firmware header (magic TRZF at offset 4,608), and 1,609,216 bytes of code. The firmware header carries version 2.9.1.0, fix_version 2.9.0.0, hw_model T2T1, a monotonic counter of 2, an expiry of 0, sixteen 32-byte code-hash slots (thirteen filled: twelve full 128 KiB chunks plus a final 36,352-byte chunk), a 3-slot ECDSA area left empty on this model generation, a sigmask byte and a 64-byte signature.

Reproduce it in five steps

1) Find the byte string TRZF; here it sits at 4,608. 2) Read header_len (1,024) and code_length (1,609,216) from the next 12 bytes. 3) Cut the code into 128 KiB chunks aligned to the start of the *file*, not of the code — so chunk 0 is 131,072 − 5,632 = 125,440 bytes, and hashing them the naive way produces thirteen hashes that match nothing. 4) Rebuild the header: drop those thirteen hashes into the 512-byte slot area at offset 32, zero bytes 544–739 (the empty ECDSA area) and the last 65 bytes (sigmask plus signature). 5) blake2s the 1,024-byte header. You get aaa616c14904cf6eb940497d7fd5da8735ab0d0ecdebd3f6ced49fff0ce278a0, the exact string in releases.json. The same procedure on 2.8.9, 2.8.10 and 2.9.0 lands on each release's published value, which is how I convinced myself the method was right and not lucky.

The signature is the lock, and it is 2-of-3

The header's sigmask byte is 3, meaning keys 0 and 1 of the three vendor public keys stored at 0x20, 0x40 and 0x60. Because Trezor uses CoSi — a threshold scheme where the cosigners' public keys are added as Edwards points — the 64-byte signature verifies as an ordinary Ed25519 signature under the combined key 52ef8f8496c888a45aa526cd980105f1dd6ce7b435f0be8fde86152c244a7805. I implemented the curve arithmetic, checked it against the RFC 8032 test vector first, then ran it on the digest: true. Running the same check against key 0 alone, or key 1 alone, returns false — it is a 2-of-3, not "any one of three". The 4,608-byte vendor header is signed the same way and verifies under two of the three bootloader keys compiled into the firmware tooling, digest abb773af7aa4c475… So the chain is vendor header → firmware, anchored in keys that live on the device, not on the server.

Flip one byte and watch it fail

To check the check, I flipped one bit at offset 100,000, recomputed all thirteen chunk hashes exactly as an attacker who controls the download would, rebuilt the header and re-ran the verifier. The digest changed from aaa616… to 249da0562e5ce9c15f3537cec046894c46c488cd6017850c79ed66053a4e2218, and the signature verification returned false. That is the whole point of the design: recomputing hashes is free, forging the signature is not. Downgrades are covered by a second mechanism — the header's monotonic counter is 2, and an image with a lower value is refused, which is what stops a "roll back to a version with a known bug" attack. One oddity worth naming because it shows the field map is real: the expiry field is 0, and the vendor's own parsing code carries the comment "expiry is not used now".

What an "official channel" actually buys

Here is the part that unsettles people: the vendor's keys are not in the download at all. The 96 bytes of vendor public keys are byte-identical across 2.8.9, 2.8.10, 2.9.0 and 2.9.1 (their SHA-256 is b56e59ef7eaf8217…), because the vendor header is fixed on the device at manufacture, not fetched per release. So a malicious mirror can serve you a genuine signed image, an old signed image, or a corrupted one — none of which steals funds, since the bootloader will either accept a valid signature or refuse. What a hostile page can steal is your seed: the fake "firmware updater" that asks for 24 words is the realistic attack, and it has no cryptographic surface at all. That is why the download page matters for phishing hygiene, not for trust. Three risks are real: seed-phishing update wizards, the desktop/npm supply chain behind wallets and dApps (Ledger Connect Kit in December 2023, @solana/web3.js in December 2024), and counterfeit or refurbished devices carrying someone else's vendor header. Also worth pinning: 2.9.1 ships a bitcoin-only build with fingerprint 77b53ba1e381fcf8…, so checking the fingerprint also stops you from flashing the wrong variant by accident.

Ledger anchors trust somewhere else

Ledger's model is not a worse version of this; it is a different trade. The secure element holds the keys and verifies the firmware, the companion app performs an attestation-based genuine check, and the application layer is open source while the element's OS is not. What you give up is exactly what I did above: there is no third-party reproducible digest you can recompute and no threshold signature you can check against published keys, so verification is delegated to the device and the vendor. What you get is key material that never sits in a general-purpose microcontroller. Neither model protects against the one attack that actually empties wallets, which is you typing the seed into something that is not the device.

Why a swap does not touch any of this — until you sign

A swap order takes five fields: fromAsset, toAsset, fromAmount, toAddress and refundAddress. No wallet connection, no signature, no seed — which is why firmware risk sits at the moment you move coins out of the wallet, not at the moment you exchange them. For scale, the catalogue this morning listed 516 assets across 74 networks, 415 distinct tickers, 61 stablecoins and 6 assets that need an extra ID. Quotes at 10:07 UTC: 100 USDT-TRC20 gave 0.14687052 XMR (680.87 USDT per XMR, 14.03% above the 597.08 spot, almost all of it the per-order floor), 1 XMR gave 578.800581 USDT (3.06% below spot on the exit side), and 0.05 BTC gave 6.61007527 XMR, or 132.20151 XMR per BTC against 80,072 spot. Treat those as writing-time figures; the page is the only live number. (XMR to USDT exit)

Six checks before you flash

1) Download only from the vendor's own domain, and treat any "firmware updater" that wants your words as an attack. 2) Compute the fingerprint and compare it to releases.json — `trezorctl firmware verify trezor-t2t1-2.9.1.bin --fingerprint aaa616c1…` does both signature and fingerprint in one shot. 3) Confirm the threshold: sigmask 3 with three keys means two cosigners, not one. 4) Check that the version is not a downgrade — the monotonic counter must not go backwards. 5) Flash over USB from the official app and confirm the version string on the device screen, not on the laptop screen. 6) After the reboot, read the version and fingerprint from the device again; if the device reports something other than what you flashed, stop and treat the hardware as suspect.

The threat model compresses into one line: a firmware attack needs either the vendor's private key or your seed, and both live outside the browser. That is why the domain, the padlock and the download page cannot be the control you rely on — they only decide whether you get phished on the way. Verify the artifact once with the arithmetic above, keep the seed on the device, and the scariest part of a hardware wallet stops being a mystery and becomes a checkbox.

Frequently asked questions

Is the published fingerprint the file's SHA-256?

No. For Trezor T2T1 2.9.1 the file SHA-256 is 6a84c200f13b… while the fingerprint is aaa616c14904…, a blake2s digest of the 1,024-byte firmware header after the 16 chunk hashes are recomputed and every signature field is zeroed. That digest is what the signature covers, so compare fingerprint to fingerprint; use SHA-256 only to catch a truncated download (the file must be exactly 1,614,848 bytes).

Can a modified firmware pass the signature check?

No. I flipped one bit at offset 100,000, recomputed all thirteen chunk hashes the way an attacker would, and rebuilt the header: the digest moved from aaa616… to 249da056… and the signature check returned false. Passing requires two of the vendor's three private keys, which never leave the signing machine.

Does a hardware wallet protect me if I type my seed into an app?

No. The seed is the wallet; the device only protects it while it stays inside. A page that asks for 24 words to "complete a firmware update" is phishing with no cryptographic component, and no signature check can catch it. Real updates are signed, flashed over USB from the official app, and confirmed on the device screen — no words required.

Ready to swap?

No account · No KYC · No email

Exchange now
Hardware Wallet Firmware: Verify the Signature, Not the Site