Repository navigation
Releases
smartcard: SeedSigner + Satochip, unified opt-in sighash
Pre-release. This is the smartcard fork, not stock SeedSigner. For stock, use v0.8.7-unified-sighash.6, which is a normal release and has been run on hardware. The two are separate lines and the images are not interchangeable.
3rdIteration/seedsigner at SeSi-0.8.7+ShSi-B13, signing with the unified opt-in signature hash as Bitcoin Knots v29.4.1.knots20260508 defines it in doc/unified-sighash.md. Unofficial, and affiliated with neither project.
What it adds
- Signs the unified message when the transaction asks for it, from a seed and from a Satochip or Keycard alike. A PSBT declaring
0x21is signed with the hardfork's message; one declaring nothing is signed the standard way, exactly as before. - The signature hash type is on the approval screen,
Unified sighash (0x21)orStandard sighash (0x01), so a host that quietly rewrites the request cannot do it without you seeing. For seed signing, what the screen names is checked against every signature before anything leaves the device. - A transaction it can only sign part of is refused, rather than signed in part and reported as complete.
- PSBTs declaring
0x21are accepted at load. The fork refused every hash type butSIGHASH_ALLbefore the review screens, so the opt-in never reached them.
A card is handed 32 bytes and signs them blind, so nothing on it decides which message it just committed to. The type is chosen on the SeedSigner, used to build the digest, and appended to the signature there. That is why the approval screen is the thing to read.
Why this is a pre-release
No card has produced a signature yet. The card signer is covered against a simulated card whose signatures a Bitcoin Knots regtest node accepts and mines, and the full suite is green (2067 tests), but it has never run on a real Satochip or Keycard.
What a tester has confirmed on hardware: loading a seed from a SeedKeeper card, an approval screen reading Unified sighash (0x21), and a signed transaction that broadcast, relayed and arrived at another wallet. That is the whole path end to end, on a live network, for a seed the device holds.
It leaves one gap, and it is the gap this branch added code for. A SeedKeeper stores the seed and hands it to the device, which then signs in software, the same as any other loaded seed. Signing on the card, where the device sends a digest and the card returns the signature, is a different code path and is the one still unproven.
If you have a Satochip or a Keycard, the useful test is the one that exercises it: on the seed screen choose Use Satochip card or Use Keycard rather than loading a seed, sign a PSBT declaring 0x21, confirm the screen reads Unified sighash (0x21), and confirm a node accepts the broadcast. Please report what you see.
No warranty
Unofficial, unaudited firmware provided as is, with no warranty and no liability, under the same MIT terms as the LICENSE.md it inherits. It is not a SeedSigner release, not a 3rdIteration release, and is supported by neither project. Flashing it is entirely at your own risk, and you alone bear any loss of funds or loss of access to your device.
Which file for which board
| file | hardware |
|---|---|
…pi0-smartcard.img.zip |
Raspberry Pi Zero v1.3, Raspberry Pi Zero W |
…pi02w-smartcard.img.zip |
Raspberry Pi Zero 2 W, Raspberry Pi 3 Model B |
…pi2-smartcard.img.zip |
Raspberry Pi 2 Model B |
…pi4-smartcard.img.zip |
Raspberry Pi 4 Model B |
…lafrite-smartcard.img.zip |
Libre Computer La Frite (AML-S805X-AC) |
seedsigner-luckfox-pico-mini-sd-….img |
Luckfox Pico Mini, booting from MicroSD |
seedsigner-luckfox-pico-max-sd-….img |
Luckfox Pico Pro / Pico Max, booting from MicroSD |
seedsigner-luckfox-pico-mini-nand-files-….zip |
Luckfox Pico Mini, onboard NAND |
seedsigner-luckfox-pico-max-nand-files-….zip |
Luckfox Pico Pro / Pico Max, onboard NAND |
seedsigner-luckfox-pico-pi-emmc-files-….zip |
Luckfox Pico Pi, onboard eMMC |
Flashing
MicroSD (every Pi, La Frite, and the two Luckfox -sd- images):
- Download the file for your board, and the three verification files below.
- Verify it, using the commands in the next section. Do this before flashing, not after.
- Unzip it if it ends in
.zip. The Luckfox-sd-images are already raw.img. - Write the
.imgto a MicroSD card with Balena Etcher, Raspberry Pi Imager, ordd. Copying the file onto a card does nothing; it has to be written as a disk image.
The Pi and La Frite images are 512 MiB and the Luckfox -sd- images are 838 MiB, so any card of 1 GB or more is enough.
Onboard NAND or eMMC (the Luckfox -nand-files- and -emmc-files- bundles): unzip and use update.img with the Luckfox or Rockchip upgrade tooling, or sd_update.txt / tftp_update.txt with a U-Boot workflow. Each bundle carries a README.txt saying the same. These paths are the fork's, and I have not run them.
Verify what you downloaded
gpg --import privkeyio-signing-key.asc
gpg --verify SHA256SUMS.asc SHA256SUMS
sha256sum --ignore-missing -c SHA256SUMS
Look for Good signature from "Kyle Santiago <kyle@privkey.io>" on key A47D99B6DB0D715D40C59A2023AE8A8EA7E24E38, and OK for the file you downloaded. The warning that the key is not certified is expected: it means only that you have not marked it trusted yourself.
Build it yourself
Unaudited firmware is worth more if you do not have to take my word for what is in it. These are the exact commands the published artifacts came from.
Raspberry Pi and La Frite:
git clone https://github.com/privkeyio/seedsigner-os
cd seedsigner-os
git checkout unified-sighash-smartcard
git submodule update --init opt/buildroot
./build.sh --all --smartcard \
--app-repo=https://github.com/privkeyio/seedsigner \
--app-branch=SeSi-0.8.7+ShSi-B13+US.1-pre
Luckfox Pico:
SEEDSIGNER_REPO_URL=https://github.com/privkeyio/seedsigner \
SEEDSIGNER_REF=SeSi-0.8.7+ShSi-B13+US.1-pre \
./build.sh --luckfox build --all
SeSi-0.8.7+ShSi-B13+US.1-pre and smartcard/SeSi-0.8.7+ShSi-B13+US.1-pre are the same commit. The build stamps whichever string you pass into the image filename and into /etc/seedsigner-os-release, so the unprefixed one is what reproduces these files.
Both builds fetch embit from the tag and refuse to continue unless it hashes to bc8540ecc4a893a24651b071e01054443db8efb3c6a5439515311f21fb86b3d2, which is the tarball published with embit v0.8.0-unified-sighash.4.
On reproducibility: the fork reports its Pi images as reproducible and says La Frite and Luckfox are not yet. I have not verified reproducibility of these builds either way, so treat a matching hash as evidence and a differing one as a question worth raising rather than proof of anything.
SeedSigner 0.8.7-unified-sighash.6 - the committed script type comes from the script
SeedSigner signing with the unified opt-in signature hash as Bitcoin Knots v29.4.1.knots20260508 defines it, with flashable images for all four Pi targets.
Everything in .5, plus one correctness fix in the signing library. Nothing about how the device is used changes.
A legacy input could be signed with the wrong message
The unified message commits a byte naming the script type, which separates the four so a signature made for one can never verify as another. embit derived that byte from which PSBT fields were present rather than from the spent output's own script.
A coordinator is allowed to carry a legacy input as witness_utxo, and consensus can only read that output as bare. Carried that way it was signed as segwit v0 instead, and the node refuses the result. Two signers handed different forms of the same PSBT also disagreed with each other, and both differed from every other implementation of this message, which classify from the script.
Classified from the script now. This device inherits the fix through its pinned embit.
Verified
Against a Bitcoin Knots v29.4.1.knots20260508 regtest node, spending the same P2PKH output through each form a PSBT can carry it:
| carried as | before | after |
|---|---|---|
non_witness_utxo |
mined, 0x21 |
mined, 0x21 |
witness_utxo |
node refused the signature | mined, 0x21 |
The digest also matches Knots' own test_framework.script.UnifiedSignatureHash, which is written from the specification rather than by mirroring an implementation, for both forms and for a wrapped segwit input that must stay segwit v0.
The images carry that library and nothing else changed: the build fetched embit-0.8.0-unified-sighash.4.tar.gz, sha256 bc8540ecc4a893a24651b071e01054443db8efb3c6a5439515311f21fb86b3d2, which is byte-identical to the tarball published with that release, and the build verifies the hash before it will use it.
433 tests, and all 166 cross-implementation vectors.
On hardware: this image has been run on a SeedSigner and used to sign a mainnet transaction carrying the opt-in.
No warranty
This is unofficial, unaudited firmware provided as is, with no warranty and no liability, under the same MIT terms as the LICENSE.md it inherits. It is not a SeedSigner release and is neither affiliated with nor supported by the SeedSigner project. Flashing it is entirely at your own risk, and you alone bear any loss of funds or loss of access to your device.
Which file for which Raspberry Pi
| file | hardware |
|---|---|
…pi0.img |
Raspberry Pi Zero v1.3, Raspberry Pi Zero W |
…pi02w.img |
Raspberry Pi Zero 2 W, Raspberry Pi 3 Model B |
…pi2.img |
Raspberry Pi 2 Model B |
…pi4.img |
Raspberry Pi 4 Model B |
Flashing
- Download the
.imgfor your board, along withSHA256SUMS,SHA256SUMS.asc, andprivkeyio-signing-key.asc. - Verify it, using the commands below. Do this before flashing, not after.
- Write the
.imgto a MicroSD card (64 MB or larger) with Balena Etcher, Raspberry Pi Imager, ordd. Copying the file onto the card does nothing; it has to be written as a disk image.
Verify what you downloaded
gpg --import privkeyio-signing-key.asc
gpg --verify SHA256SUMS.asc SHA256SUMS
sha256sum --ignore-missing -c SHA256SUMS
Look for Good signature from "Kyle Santiago <kyle@privkey.io>" with Primary key fingerprint: A47D 99B6 DB0D 715D 40C5 9A20 23AE 8A8E A7E2 4E38, and OK for the image you downloaded. The warning that the key is not certified is expected: it means only that you have not marked it trusted yourself.
Build it yourself
Unaudited firmware is worth more if you do not have to take my word for what is in it. This is the command the published images came from:
git clone https://github.com/privkeyio/seedsigner-os
cd seedsigner-os
git checkout 793a723
git submodule update --init opt/buildroot
SS_ARGS="--all --app-repo=https://github.com/privkeyio/seedsigner --app-branch=v0.8.7-unified-sighash.6" \
docker compose up --force-recreate --build
--all builds all four Pi targets. The build fetches embit from the tag and refuses to continue unless it hashes to the bc8540ec… above, so a tampered library stops the build rather than shipping quietly.
Rebuilt this way from a fresh clone on 2026-10-20, all four images match SHA256SUMS byte for byte.
Also available
smartcard, the same signing change on the 3rdIteration Satochip fork, for anyone signing with a smartcard. That line is a pre-release and has not been run on card hardware. These images are the stock ones and are the right choice unless you have a card.
SeedSigner 0.8.7-unified-sighash.5 - superseded by .6
Superseded by .6. The signing library derived the committed script type from which PSBT fields were present rather than from the spent output's script, so a legacy input carried as witness_utxo was signed with a byte consensus cannot accept and the node refused it. The images below stay in place for anyone already on them.
SeedSigner signing with the unified opt-in signature hash as Bitcoin Knots v29.4.1.knots20260508 defines it, with flashable images for all four Pi targets.
The hash type is now on screen
The device signs the signature hash type the PSBT declares and displayed none of it. The allowlist added in .3 bounds that to types committing to every output and input, so the theft cases are refused, but it cannot bound the choice of algorithm: a wallet intending the unified message writes 0x21, a host that rewrites it to 0x01 gets a standard signature, and the device reported success identically.
The approval screen now names the message it is about to produce, in both states: Unified sighash (0x21) or Standard sighash (0x01). Labelling only the opt-in would make its absence indistinguishable from the screen never having said anything, which is the case that matters.
What it shows is the hash type each input is actually signed with, which is not the same as the type the transaction asks for: an input declaring nothing is signed with 0x01 off taproot, and a screen naming 0x00 there would describe a byte no signature carries. Where the inputs cannot be described by one type, the transaction is refused rather than labelled with something untrue, and the screen says which of the two reasons applies.
Also fixed
- A transaction this device can only partly sign is refused. It names one hash type for the whole signing call, so an input declaring one that disagrees was skipped. The signature count still rose, which is what decided success, so a partly signed transaction reported exactly like a fully signed one and handed back something that cannot be broadcast. Inputs that would be skipped are now found before the approval screen, so you are not asked to approve a transaction that will not be signed.
- The declared hash type survives to a co-signer. A signature carries its own type in its last byte, so a lone signer never needed the field, but a co-signer reading the trimmed PSBT does: without it the next signer was not told the opt-in was asked for and signed the standard message instead.
- Signing cannot leave a half-signed transaction behind. It runs on a copy, so a malformed PSBT that fails partway no longer leaves the signatures it already made on the transaction the device is holding. This one predates the unified sighash work and is reachable with a PSBT that declares no hash type at all.
- Every CI dependency is installed by hash, transitive ones included.
Verified
The screen's claim is not a prediction. After signing, the hash type of every signature the device just made is read back off the transaction and compared with what was displayed; nothing reaches the QR unless they match. That holds however the signer chose what to sign, including paths this firmware does not model.
Checked against the images themselves rather than the source they were built from: every embit module byte-identical to the published tarball, Knots' 166 cross-implementation vectors reproduced from each image's own bytecode, and the feature's own tests run against the code extracted from each image. The app and embit lifted out of the pi0 image were then driven against a Knots v29.4.1.knots20260508 node, where the byte on the screen matched the byte in the mined transaction for the unified, standard and undeclared cases.
433 tests, including taproot, multisig with a co-signer who signed first, and a host that plants a signature in the slot the device is about to fill.
No warranty
This is unofficial, unaudited firmware provided as is, with no warranty and no liability, under the same MIT terms as the LICENSE.md it inherits. It is not a SeedSigner release and is neither affiliated with nor supported by the SeedSigner project. Flashing it is entirely at your own risk, and you alone bear any loss of funds or loss of access to your device.
Flashing
pi0 for the Pi Zero, pi02w for the Zero 2 W and Pi 3, pi2 for the Pi 2, pi4 for the Pi 4 and CM IO.
gpg --import privkeyio-signing-key.asc
gpg --verify SHA256SUMS.asc SHA256SUMS
sha256sum --ignore-missing -c SHA256SUMS
SeedSigner 0.8.7-unified-sighash.4 - superseded by .5
Superseded by 0.8.7-unified-sighash.5. The images below are left in place because nothing here puts funds at risk and removing them would break verification for anyone who has already pinned their hashes. Flash .5 instead.
What .5 fixes, none of which can move coins to someone else:
- This release displays nothing about the signature hash type, so a host that rewrites a request for the unified message down to the standard one gets a standard signature and this device reports success identically.
- A transaction whose inputs ask to be signed in ways this device cannot satisfy at once is signed in part and reported as fully signed. The result cannot be broadcast.
- The declared hash type does not survive to a co-signer, so the next signer in a multisig signs a different message and the two signatures do not agree.
- A malformed PSBT that fails partway through signing leaves the signatures it already made on the transaction the device is holding.
SeedSigner signing with the unified opt-in signature hash as Bitcoin Knots v29.4.1.knots20260508 defines it, with flashable images for all four Pi targets.
Signing fix
A PSBT could be signed with a hash type that does not commit to its outputs. One declaring SIGHASH_NONE was signed, and that signature commits to no outputs, so whoever supplied the PSBT could rewrite them and take the coins. SIGHASH_SINGLE and ANYONECANPAY commit to less than the screen shows in the same way. Nothing on the device displays the hash type, so these reviewed as ordinary sends.
Reaching it needs a wallet that asks for one of those types. No ordinary wallet does, so the risk was to anyone scanning a PSBT from software built to exploit it.
Affects 0.8.7-unified-sighash.2 only. Earlier releases and upstream SeedSigner are not affected.
Only the hash types that commit to every output are now signed: undeclared, SIGHASH_DEFAULT, SIGHASH_ALL, and the unified opt-in 0x21. Anything else is refused, as it was before this fork added the opt-in.
Still outstanding: the device shows nothing about the hash type, so a host that rewrites a 0x21 request to 0x01 gets a legacy signature and the same success screen. That needs a display, which is the next work.
Dependency
embit moves to 0.8.0-unified-sighash.3, which fixes six defects in how a hash type is negotiated before signing. None of them are reachable from this device, whose allowlist above already bounds what it asks for; the pin advances so the two agree about what is refused rather than relying on that.
Verified
Every hash type byte a host can declare, 0x00 to 0x1ff and out of range, uniform and mixed, across eight script types: only types committing to every output and input sign. Each signature re-checked after six mutations a host could make to the returned PSBT; none survived, against a control where the unfixed code's survive four of six.
166 cross-implementation vectors, the UI flow asserting hash type 0x21, end to end against a Knots v29.4.1.knots20260508 node, and CVE-2020-14199 blocked under the opt-in. 388 unit tests.
No warranty
This is unofficial, unaudited firmware provided as is, with no warranty and no liability, under the same MIT terms as the LICENSE.md it inherits. It is not a SeedSigner release and is neither affiliated with nor supported by the SeedSigner project. Flashing it is entirely at your own risk, and you alone bear any loss of funds or loss of access to your device.
Flashing
pi0 for the Pi Zero, pi02w for the Zero 2 W and Pi 3, pi2 for the Pi 2, pi4 for the Pi 4 and CM IO.
gpg --import privkeyio-signing-key.asc
gpg --verify SHA256SUMS.asc SHA256SUMS
sha256sum --ignore-missing -c SHA256SUMS
SeedSigner 0.8.7-unified-sighash.2 - withdrawn, use .4
Withdrawn. Use 0.8.7-unified-sighash.4 instead.
This release signed PSBTs declaring a hash type that does not commit to their outputs. One declaring SIGHASH_NONE was signed, and that signature commits to no outputs, so whoever supplied the PSBT could rewrite them and take the coins. SIGHASH_SINGLE and ANYONECANPAY commit to less than the screen shows in the same way. Nothing on the device displayed the hash type, so these reviewed as ordinary sends.
Reaching it needs a wallet that asks for one of those types. No ordinary wallet does, so the risk was to anyone scanning a PSBT from software built to exploit it. Earlier releases and upstream SeedSigner are not affected.
The images have been removed so they cannot be flashed by mistake. If you flashed one, reflash from .4. No key material is at risk: the defect is in what a signature commits to, not in how keys are handled, so seeds that only ever sat on a device running this build do not need to be moved.
No warranty
This is unofficial, unaudited firmware provided as is, with no warranty and no liability, under the same MIT terms as the LICENSE.md it inherits. It is not a SeedSigner release and is neither affiliated with nor supported by the SeedSigner project. Flashing it is entirely at your own risk, and you alone bear any loss of funds or loss of access to your device.