Repository navigation
NUT-11: typed, length-framed SIG_ALL message over output hashes - #404
robwoodgate wants to merge 9 commits into
Conversation
|
Here is a worked collision under the old unframed transcript that length-prefixing removes The old transcript concatenates Two output allocations, both summing to 205:
Both produce the byte-identical output transcript: So a single signature over the approved allocation also authorises the reallocation, moving value to a different blinded output while conservation still holds. Under the v1 framing in this PR ( Note this specific example uses non-power-of-2 amounts; against a mint that only issues power-of-2 denominations I could not construct a collision (searched the two-output case up to 2^64), but denominations are not mandated by the protocol, so the framing closes it regardless. |
The digests object is computable from the rest of the package, so carrying it added nothing a signer could not derive itself. Worse, a signer that trusts a supplied digest signs a message it has never checked against the transaction it is approving. The legacy entry hashed the pre-amount-binding format (secret||C||B_), which NUT-11 no longer specifies. Without amount binding a signature commits to which blinded outputs exist but not to their values, letting amounts be reordered across outputs while the signature and the mint's balance check both still pass. Carrying that digest gave a superseded format renewed standing. The current entry hashed today's aggregation format, a label that goes stale the moment the message format changes (see cashubtc#404). Dropping both leaves the package independent of any aggregation scheme. Adds a normative requirement that signers derive the message to sign from the package contents themselves, and regenerates the sigallA vector without digests. Addresses review feedback from @robwoodgate. Serialized vector matches the one pinned in cashubtc/cashu-ts#947.
Wallets sign v1 and current; the pre-0.21 message (secrets then B_ fields) is no longer produced, as it does not commit to C values or output amounts (cashubtc/nuts#404). Mint verification is unchanged.
Wallets sign v1 and current; the pre-0.14 message (secrets then B_ values) is no longer produced, as it does not commit to C values or output amounts (cashubtc/nuts#404). Mint verification is unchanged.
The digests object is computable from the rest of the package, so carrying it added nothing a signer could not derive itself. Worse, a signer that trusts a supplied digest signs a message it has never checked against the transaction it is approving. The legacy entry hashed the pre-amount-binding format (secret||C||B_), which NUT-11 no longer specifies. Without amount binding a signature commits to which blinded outputs exist but not to their values, letting amounts be reordered across outputs while the signature and the mint's balance check both still pass. Carrying that digest gave a superseded format renewed standing. The current entry hashed today's aggregation format, a label that goes stale the moment the message format changes (see cashubtc#404). Dropping both leaves the package independent of any aggregation scheme. Adds a normative requirement that signers derive the message to sign from the package contents themselves, and regenerates the sigallA vector without digests. Also clarifies the witness encoding. NUT-11 defines Proof.witness as a serialized JSON string, but the package schema showed witness as a JSON object without saying which representation applied. An implementer could serialize it before putting it in the package, or pass the package's object straight into Proof.witness; both parse as valid JSON and fail only later. The object form is correct and is what the vector already encodes, so this documents the field rather than changing it. The accompanying note also states that inputs[].secret keeps the representation it has in Proof.secret, since documenting only witness invites the opposite mistake. Addresses review feedback from @robwoodgate and Copilot. Serialized vector matches the one pinned in cashubtc/cashu-ts#947.
Wallets sign v1 and current; the pre-0.14 message (secrets then B_ values) is no longer produced, as it does not commit to C values or output amounts (cashubtc/nuts#404). Mint verification is unchanged.
NUT-11 (cashubtc/nuts#404) now signs tagged_hash("Cashu_SigAllSig_v1", message) rather than a plain SHA-256 of a tag-prefixed message. Accepted SIG_ALL formats are carried as 32-byte digests and signed and verified directly; canonical vectors regenerated.
…er its BIP-340 tagged hash NUT-11 (cashubtc/nuts#404) now signs tagged_hash("Cashu_SigAllSig_v1", message) rather than a plain SHA-256 of a tag-prefixed message. The accepted-format list carries prehashed digests: MessageInput gains { digest } so the tagged hash flows through the existing sign and verify helpers. Canonical NUT-11 and NUT-14 vectors regenerated.
NUT-11 (cashubtc/nuts#404) now signs tagged_hash("Cashu_SigAllSig_v1", message) rather than a plain SHA-256 of a tag-prefixed message. Accepted SIG_ALL formats are carried as 32-byte digests and signed and verified directly via sign_digest/verify_digest; canonical vectors regenerated.
Re-encodes the SIG_ALL message to the domain-separated, length-framed byte format used by NUT-20/29 (tag "Cashu_SigAllSig_v1"). The quote id is framed directly after the tag and is empty for swaps, so one layout serves both transaction types. Adds a normative signature validation rule (ignore signatures that do not verify, count unique public keys) and canonical message vectors in tests/11-test.md.
Removes the superseded string-concatenation SIG_ALL vectors so the spec carries one canonical message aggregation, retitles the length-framed vector set, and notes why the output keyset id is not part of the message.
The SIG_ALL example witness now carries one signature per accepted format, matching what wallets produce.
0bfafaa to
73a911a
Compare
# NUTS: - cashubtc/nuts#404 The P2PK and HTLC signing and verification functions now take the 32-byte digest that is signed instead of a message string, so every SIG_ALL format hashes in one place and the signing path never sees a message. ## Why The `message?: string` parameter on `signP2PKProofs`, `hasP2PKSignedProof`, `verifyP2PKSpendingConditions` and the HTLC wrappers exists only for SIG_ALL, where the signed value is not the proof secret. None of those functions read the message: they resolve keys from the secret and hand the message to the Schnorr helpers, which SHA-256 it. That ties the API to one specific derivation. The framed [NUT-11](https://github.com/cashubtc/nuts/blob/main/11.md) SIG_ALL format signs a BIP-340 tagged hash rather than a plain SHA-256, so it cannot pass through a string parameter at all. `signMintQuote` already works digest-first for the same reason. ## Changes - `signP2PKProofs`, `signP2PKProof`, `hasP2PKSignedProof`, `verifyP2PKSpendingConditions`, `isP2PKSpendAuthorised`, `verifyHTLCSpendingConditions`, `isHTLCSpendAuthorised`, `getValidSigners` and `meetsSignerThreshold` take `digest?: DigestInput` (the existing hex-or-bytes type of `schnorrSignDigest`). With no digest they hash the proof secret, so SIG_INPUTS behaviour is unchanged. - `Wallet.signP2PKProofs` passes `computeMessageDigest(v0Message)`; the v0 message builder itself is untouched. - Migration entry added to `migration-5.0.0.md`. ## Impact Breaking for callers that pass a SIG_ALL message string to the functions above, which is the mint-side use of this library; they hash the message first. No wire or storage change. Not for backport to v4.
Each input, output and melt quote is now a typed record of length-framed fields, and outputs commit SHA256(B_) in place of the blinded point, so a signer can approve outputs from their amounts and hashes alone. A swap has no melt record. Vectors are regenerated from real proofs and outputs under a derived keyset, and the HTLC SIG_ALL vector moves here from the NUT-14 vectors so every vector on this message lives in one file.
Preimage and signature, and keyless hashlock cases. The HTLC SIG_ALL vector lives with the NUT-11 SIG_ALL vectors.
Moves the vector from tests/11-test.md into tests/14-test.md, now that the NUT-14 vectors are part of this PR.
This PR re-encodes the NUT-11 SIG_ALL message as typed records using the length-framed field encoding of NUT-20/29, hashed with a BIP-340 tagged hash as in #443, and adds canonical test vectors for NUT-11 and NUT-14.
Notes:
h_j = SHA256(B_j), not the blinded point, so a signer can approve outputs from their amounts and hashes alone: the party that will unblind them can keepB_to itself until submission.meltrecord) cannot collide with a melt. The type codes follow the input, output and melt quote containers of the NUT-10: Nutroot secrets (v3 keysets) #443 transaction transcript.idremains unbound, also as in NUT-20/29: it is already pinned by mint invariants (outputs from active keysets only, one unit per transaction), and leaving it out keeps pre-signed transactions valid across keyset rotation.tests/11-test.mduse real proofs and outputs under a derived keyset, signed by the well-known test key.tests/14-test.mdadds the NUT-14 vectors (preimage, keyless hashlock and an HTLC SIG_ALL case). They are pinned byte-for-byte in the implementation PRs above.History
tagged_hash("Cashu_SigAllSig_v1", message)in place of a plaintext prefix.SHA256(B_), the quote moved to a trailingmeltrecord. Vectors regenerated, and the NUT-14 vectors from NUT-14: add HTLC test vectors #407 folded in. The implementation PRs above are being updated to match.