Reproducible Build Hashes
Every released image is a production build. There are twelve, four per target:
signed and unsigned, app-only and full-flash.
Signed
App-only
Waveshare: f0ec469a9537f073b6988f4404fb72dec51fa23a70bedca39a5f094407a887cd
Waveshare-AF: 67d4a50b2c3f9e542e363f9d2fe0fd326e8b0ecae09ed1d6e190e0b6c25538d7
M5Stack: 542f8e1beda0faf8cf3038c4ffb31c0f36ff19dd2a6b5a2a5bba48caff06d2ca
Full-flash
Waveshare: 6356d238d001cf0695497b3d370e71dd6fd87174f26214bd2912495a8429ca3c
Waveshare-AF: df71867e88125252ef1dae9e4815ef2fa69655fb0c4a9d289ef174d450c6e24d
M5Stack: 5aa43c48fa1927a7a6c08061682587141216cf7d2bf9019c41f364b42ecdb67b
Unsigned (what your own build produces, and what you compare against):
App-only
Waveshare: 8cfb544cd4e408f29588035cd24ff8cde6bd81fe37ccd6aa8b32e425a09d4709
Waveshare-AF: 9b42aa904b5cdceb98f1b19f9272daad60a639d13c3919e00de22469d4412c98
M5Stack: 9c9d6a7dffdb72c3f8f3cb0bc444d4037d8d6a1d3210efa2d76f45f6116ed0cb
Full-flash
Waveshare: b3ec20fcfb63fa2958a13812f18edd4681657564e6507cfa7a26caf419bd7c22
Waveshare-AF: 43799d97ca301dcfe81ee5e7a2c23d90c0393a672ffb9991ba014d158f914c9c
M5Stack: f5e82b1f00ca375f832a33d4006622a336f2f3a107cc20321f33d8108924b848
Reproduce the unsigned set:
git clone https://github.com/InKasWeRust/KasSigner.git && cd KasSigner
git checkout v1.0.7
docker build --platform linux/amd64 -f Dockerfile.base -t kassigner-toolchain:v3 .
docker build --platform linux/amd64 -t kassigner-build .
docker run --rm kassigner-buildThe toolchain tag stays v3: same package versions, same Ubuntu snapshot
archive as 1.0.6. The build now runs the core crate's 58 host tests before
producing any image, so a failing known-answer test blocks the release
pipeline rather than shipping.
Until this release, the unsigned images were not the firmware. With no
signing key the compiler could prove the boot never reached the wallet and
delete it, so unsigned builds collapsed to 244-271 kB against 904-912 kB
signed: no BIP39 wordlist, no UI, 47 kB of code. Anyone rebuilding to check a
published unsigned hash reproduced that shell and learned nothing about the
firmware a device runs. Fixed in 1.0.7. The unsigned image is now the complete
firmware, which is why signed and unsigned are the same size to the byte on
two of the three targets.
Signed and unsigned hashes still differ, and by more than the signature.
The signature, the signed flag and the embedded hash are three source
constants, and changing them shifts the compiler's output throughout: measured,
around 623-630 k differing bytes with the first divergence at byte 289. So a
byte-diff of signed against unsigned proves nothing either way. Compare
unsigned against unsigned.
Verify Code Segments
Each build also produces a hash over the executable code segment only, and that
is the number the device displays on its own verification screen. On a
production build it is trustworthy: verification is enforced, so a device that
reaches the UI has already proved the running code matches.
Signed Unsigned
Waveshare: 009ac381d4a45015622127a49908712e22b854aab90f072c672402fcc9a80f18 a9c86ffedd563fd7e619e7bb27f70c361a81409e9b38f86c438d16dce8358767
Waveshare-AF: 2a8be8f4d00918863123d9758bd58cfb02a7e288019b807b0ae4f51cf53b5f86 85ebcf85eab93f306a8512e0731f4a9caa13b28074e5987e4a318cd1e531488a
M5Stack: ecf206a220e8a15c46ae93b441e09cf8117db94c315ac01931f08adce7a784e7 542777f1bf56b2dd196cc1805fcfc6d0d7789bfc919825064af7803aeec55140
Read yours from the container:
docker run --rm kassigner-build cat /build/kassigner-waveshare-unsigned.codehashFlashing Pre-built Binaries
Pick the image that matches your board. Waveshare-AF is for the autofocus
camera module, which is mounted flipped and needs an orientation correction.
M5Stack is the CoreS3 Lite.
Option A: Full-flash image (recommended for new users)
No Rust toolchain needed. Download the -full.bin for your board, install
esptool, and flash at 0x0:
pip3 install esptool
# Waveshare
python3 -m esptool --port <port> --baud 460800 write_flash 0x0 kassigner-waveshare-full.bin
# Waveshare with the AF camera module
python3 -m esptool --port <port> --baud 460800 write_flash 0x0 kassigner-waveshare-af-full.bin
# M5Stack CoreS3 Lite
python3 -m esptool --port <port> --baud 460800 write_flash 0x0 kassigner-m5stack-full.binThe full-flash images include the ESP-IDF bootloader, the partition table, and
the signed firmware. Everything needed to boot from a blank chip.
Option B: App-only image (for developers)
If the device already has a bootloader and partition table from a previous
cargo run, flash just the app at 0x10000:
python3 -m esptool --port <port> --baud 460800 write_flash 0x10000 kassigner-waveshare.binReplace the port with your device's serial port (ls /dev/cu.usb* on macOS,
ls /dev/ttyACM* /dev/ttyUSB* on Linux).
The unsigned images are not for flashing. They exist to be hashed. An
unsigned image is a production build with no valid signature, so it halts at
boot with a failure screen, by design.
Secure Boot boards
On an eFuse-provisioned board, RSA-sign the app image with your own Secure Boot
key first (espsecure sign_data, see docs/EFUSE_RUNBOOK.md) and flash the
result at 0x10000. Both boards take the same procedure.
The macOS one-step installer is gone
Install.sh downloaded a release asset over curl and flashed it with no
checksum, no signature and no tag pin, on a device that at that point has no
Secure Boot burned. A compromised asset or a hijacked DNS answer would have put
attacker firmware on a first-time user's device, and it quietly contradicted
the reproducible-build story it sat next to. Build and flash manually instead:
docs/BUILD_FLASH_GUIDE.md.
After flashing: the USB port goes quiet, and that is correct
Released binaries are production builds. The firmware gates the USB Serial/JTAG
peripheral a second or two into boot, and prints nothing over serial. A dead
port and a silent log after flashing a release is the device working as
intended, not a failed flash.
To reflash, enter download mode first: unplug USB, hold BOOT, plug USB back
in, then release. Both boards have a BOOT button and take the same sequence.
What's New
An auditability release. The security-critical half of the firmware is now
a separate crate anyone can build and test on a laptop, and holding it against
the reference implementation found real defects in signing.
Auditability
core/, ano_stdcrate with no peripheral access: wallet, sighash,
Schnorr, the parsers, FAT32 and the known-answer tests.cd core && cargo testruns 58 tests on any host: no Xtensa toolchain, no ESP-IDF, no
hardware. The firmware consumes it as a path dependency and re-exports the
old paths, so this is a move, not a rewrite. If you are reviewing KasSigner,
this is where to start- Reference vectors from rusty-kaspa 2.0.1: 30 sighash vectors at every
boot across all six sighash types and both transaction versions, the 45'
multisig address checked against an independent implementation, and BIP32
keys carrying Bitcoin version bytes refused - A CI workflow for the crate: builds, tests, a bounded parser fuzz loop,
the cargo-fuzz targets compiled, and clippy with-D warnings. Clippy on the
firmware does not lint a path dependency, so this is the only lint coverage
the wallet and crypto code gets - One toolchain pin per crate: a single pin at the repo root made
cd core && cargo testdemand the Xtensa toolchain on a machine that has no
reason to have it
Fixes that mattered
- Timelocked transactions could not be signed, in either direction. PSKB
minTimewas skipped on parse, so the device signed lock time 0 while the
sender's extractor built the requested value and the network rejected the
mismatch. The emitter had the same gap in mirror, writing"minTime":nullon
every input, so a bundle the device returned reconstructed to lock time 0 and
a second multisig signer saw no lock time on screen at all. Fail closed both
ways, no funds at risk, but the capability did not exist - An absent PSKB
sequencesigned as 0 rather thanu64::MAX, producing
signatures other wallets reject. rusty-kaspa's own committed fixture now
parses - Signing derived every loaded seed slot, which could fill signature
positions with a key the user had not selected. It now derives the active
slot only, which also removes the largest measured cost on that path - Nine issues in the wallet and parser code that no lint had seen since the
crate split, all fixed at cause rather than suppressed - A scanned transaction could halt the device. Values were never bounded at
parse time, so two outputs nearu64::MAXoverflowed the review sums and
trapped before anything was signed. Both parsers now check each value and the
running total against the consensusMAX_SOMPI(a) - A covenant backup could be written corrupt: the hex decode validated only
the first eight characters, so garbage was saved and the failure would have
surfaced at restore (a) - Scratch buffers are cleared on every path, including the failures. A
caller cannot reach a callee's frame, sobase58check_decodeleft a decoded
xprv, private key included, in its own scratch. The session wipe now also
covers the message being signed and the buffers that hold it on the heap (a)
Device and storage
- One FAT32 layer over a
BlockDevicetrait, host-tested against an
in-memory image, replacing two per-board implementations. Batched FAT sector
writes, and the directory entry marked before the chain is freed so a power
cut cannot orphan clusters - The formatter declared space the FAT could not address, 7.5 GiB behind a
FAT covering about 4 GiB, and left a previous filesystem's chains live past
sector 32. Geometry now comes from the card's CSD, both FATs are cleared, and
every format write is read back. Cards formatted by 1.0.6 keep working;
formatting a large card takes longer (a) - Camera lifecycle in one place: the Waveshare scan-exit freeze came from
cleanup that 28 separate exit paths could each forget - Lock time on the review screens: a timelocked transaction shows its lock
time in orange, as a DAA score or a UTC date. The screens disclose; they do
not gate - A warning before scanning a cosigner kpub: a 44' watch-only kpub and a
45' cosigner kpub are byte-indistinguishable on the wire, so nothing at the
scanner can reject the wrong one, and a 44' key builds a wallet that funds
and that nobody can spend. The label on the exporting device is the only
distinction there is
(a) Reported by KodinglsFun; fixed in PRs #14 and
#15, which land in this release.
Docs
- Every root document and the five PDFs regenerated for 1.0.7, with the
reproducible-build sections corrected: comparing a signed image against an
unsigned one byte for byte was never a meaningful check, and the
documentation used to imply it was
Known limitations
- A 45' PSKB from an external coordinator that carries no derivation hints
cannot yet be signed; KasSee always writes them, so this affects other
coordinators only - The multisig descriptor is a second secret with the same backup requirement
as the seed. Losing it loses access to the funds - The lock-time screens disclose but do not gate. A signer who taps through the
orange text signs the lock time as given
Full changelog: CHANGELOG.md
KasSee Web: kassigner.org