Skip to content

Feature: Add SLIP-19 support for safe remote-signing of WabiSabi coinjoins#685

Draft
kravens wants to merge 4 commits into
Coldcard:masterfrom
kravens:feature/slip19-coinjoin
Draft

Feature: Add SLIP-19 support for safe remote-signing of WabiSabi coinjoins#685
kravens wants to merge 4 commits into
Coldcard:masterfrom
kravens:feature/slip19-coinjoin

Conversation

@kravens

@kravens kravens commented Jul 11, 2026

Copy link
Copy Markdown

SLIP-19 ownership proofs for coinjoin remote signing

What

Adds a USB command slp9 that returns a SLIP-19 ownership proof (BIP-322-style) for a given derivation
path + commitment, and an HSM policy field slip19_paths gating which paths may be proven while unattended.
This is the one primitive a coinjoin coordinator (e.g. Wasabi WabiSabi) needs that no existing Coldcard
command provides: a signature over SHA256(proofBody ‖ cs(spk) ‖ spk ‖ cs(commitment) ‖ commitment), which
is not a transaction sighash, so PSBT/BIP-322/message-signing cannot produce it.

Changes (3 files, ~84 lines)

  • shared/slip19.py (new) — make_ownership_proof(subpath, flags, commitment): derives the key,
    builds the proof body (magic SL\x00\x19 ‖ flags ‖ varint(count) ‖ 32-byte ownership id), computes the
    digest, ECDSA-signs (P2WPKH), and returns the fully serialized proof (proof body ‖ empty scriptSig ‖
    witness stack). Taproot (P2TR, BIP-340 keyspend over the same digest) is stubbed — the ngu primitives
    (sign_schnorr, xonly_pubkey_tweak_add, tagged-sha256) are already present.
  • shared/usb.pyslp9 command handler mirroring smsg/xpub: unpacks <III> (flags, len_subpath,
    len_commitment) + payloads, gates on hsm_active.approve_slip19(subpath) when in HSM mode, returns
    b'biny' + proof.
  • shared/hsm.pyslip19_paths policy field (mirrors msg_paths), included in policy JSON/text, and
    approve_slip19(subpath) (mirrors approve_xpub_share).

Verified (simulator)

The real slp9 command returns a proof that Wasabi's own verifier accepts — see the consumer tests
WalletWasabi.Tests/.../ColdcardProofSpikeTests.cs (OwnershipProof.VerifyOwnership passes on both the
reconstructed proof and the full wire bytes off the slp9 command).

Before upstream submission (follow-ups)

  • Implement the taproot branch (BIP86 output-key tweak + sign_schnorr).
  • Real SLIP-19 ownership id via the SLIP-21 node (currently zeros — accepted by the coordinator, which does
    not bind the id, but should be correct).
  • Decide the non-HSM UX: currently returns synchronously like xpub; upstream may want an on-device
    confirmation for interactive use (in HSM the slip19_paths whitelist is the gate).
  • testing/test_hsm.py coverage (whitelisted path signs, non-whitelisted denied) + a docs page.

kravens added 4 commits July 8, 2026 15:53
Taproot key-spend proof: BIP-86 tweak the internal key (libsecp
keypair_xonly_tweak_add handles internal even-Y + output parity), then a
BIP-340 schnorr signature over the same SLIP-19 digest. Simulator-verified
against Wasabi OwnershipProof.VerifyOwnership with an independent NBitcoin
BIP-86 derivation. Note: a full taproot coinjoin round also needs taproot
PSBT spend-signing, which is EDGE-firmware only.
The stm32 frozen-module manifest lists files explicitly; slip19.py was
missing, so real hardware raised ImportError on the first slp9 command
(err_Confused at the import in usb.py). The simulator loads shared/
from the filesystem and never hit it. Found on a Mk4 running the dev
build.
…ubpath

Two issues found testing HSM mode end-to-end in the simulator:

- slp9 was never added to HSM_WHITELIST, so HSM mode rejected the
  command before the slip19_paths policy gate could run. That defeats
  the whole point: ownership proofs are needed during unattended
  coinjoin operation.
- the handler passed the raw subpath string to approve_slip19, but
  match_deriv_path requires both sides in canonical hard notation
  (84h not 84-prime), so every path was denied. Clean it with
  cleanup_deriv_path first (which also validates junk input).

Note: building this tree also requires the ckcc-protocol submodule at
its recorded pin (3d1dfa8) — an older checkout lacks
PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE in ckcc/constants.py (symlinked as
shared/public_constants.py), which breaks the auth/hsm_ux import chain
at runtime and crashes the HSM menu.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant