Skip to content

KasSigner v1.0.7 - New "core" crate. An auditability release

Latest

Choose a tag to compare

@InKasWeRust InKasWeRust released this 27 Aug 18:46
· 4 commits to main since this release
528f379

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-build

The 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.codehash

Flashing 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.bin

The 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.bin

Replace 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/, a no_std crate with no peripheral access: wallet, sighash,
    Schnorr, the parsers, FAT32 and the known-answer tests. cd core && cargo test runs 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 test demand 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
    minTime was 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":null on
    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 sequence signed as 0 rather than u64::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 near u64::MAX overflowed the review sums and
    trapped before anything was signed. Both parsers now check each value and the
    running total against the consensus MAX_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, so base58check_decode left 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 BlockDevice trait, 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