fix(wraith): two round-privacy leaks in shipped code - #837
defenwycke wants to merge 2 commits into
Conversation
`GET /api/v1/session/:session_id/round-tx` returned a per-output
`participant_id`. The endpoint is unauthenticated — the router carries no auth
layer, with a comment noting "v1 trusts peers on a private network" — and the
same response serves `prevouts` in registration order, which assembly preserves
because only outputs are shuffled.
Those two together are the complete answer key. Anyone able to reach a
coordinator could read which output belonged to which input, for any round in
signing state. The blind signatures and the ChaCha20 shuffle both exist to make
exactly that unrecoverable.
The field survived because of the comment justifying it:
Diagnostic only — the privacy-relevant property (which mix output belongs
to which input) is preserved by the shuffle, not by hiding this field.
That is inverted. The shuffle randomises output *order* so that no one, the
coordinator explicitly included, can recover the mapping. Publishing per-output
attribution re-attaches the identity the shuffle just detached. `single_round.rs`
says as much three lines above the shuffle call.
The fix costs nothing. Nothing consumes the field — `grep -rn participant_id
apps/wraith-wallet/` returns zero hits, because a participant locates their own
output by matching their own `mixed_output_address`. Structure a participant
genuinely needs (index, kind, amount, fee, txid) is unchanged.
`no_participant_attribution_is_ever_serialised` is a permanent regression test
over the serialised body, checking a banned-name list rather than one field, so
a differently-spelled reintroduction is caught too. Verified it can actually
fail: re-adding the field made it panic with the leaked JSON in the message,
which is the point — a check whose failure produces no observable output is not
a check. `structural_verification_data_is_retained` is its pair, so the fix is
not later "improved" by stripping the endpoint of things wallets need.
⚠ Related, NOT fixed here: inputs are never shuffled, so input order is
registration order. That leaks arrival sequence to anyone who watched
registration, and is the same class of leak one step down. Wants its own issue.
27 lib tests green, clippy and fmt clean.
Claude-Session: https://claude.ai/code/session_01R3zCyPaMpt4tTW8ybbazHp
Follow-up to the round-tx attribution fix. Inputs were emitted in registration
order, which encoded arrival sequence into the transaction. The comment above
the loop explained why that was fine:
Bitcoin doesn't care about input ordering for privacy because the inputs
are observable on chain and in the mempool already. Output ordering is
what matters.
Half right. The inputs are observable; their *order* is not. Order was a
coordinator choice, and it chose arrival order — so anyone who watched
registration, or who correlates submission timing, could map input position
back to a participant. The chain would never have revealed that on its own.
Inputs are now shuffled with ChaCha20, seeded under a **separate domain tag**
(`WraithLite/v1/input_shuffle`). Sharing the output shuffle's seed would
correlate input index i with output index i and hand back exactly the mapping
the output shuffle exists to destroy; `input_and_output_permutations_are_
independent` pins that.
Nothing caught this change, which is its own finding: no test asserted input
order at all, so all 178 stayed green when the ordering flipped.
`inputs_do_not_appear_in_registration_order` now asserts both that the order
differs and that the input *set* is unchanged — a shuffle must permute, never
alter.
The correctness half is the part that would have been nasty. `round-tx` built
`prevouts` by indexing the registration-ordered store positionally, on the
documented assumption that "only outputs get shuffled". With inputs shuffled
that pairs each input with another participant's scriptPubKey and amount, and
because BIP-341 commits to every prevout it fails as an opaque signature error
rather than as a lookup error. Prevouts are now looked up by outpoint, walking
the transaction's own input order, with an explicit 500 if an assembled input
is not in the store rather than a silent mismatch.
`round_tx_full_pipeline_assembles_a_valid_transaction` now asserts prevouts[i]
really describes tx.input[i], and that no participant attribution appears
anywhere in the response body.
288 tests green across both crates, clippy and fmt clean.
Claude-Session: https://claude.ai/code/session_01R3zCyPaMpt4tTW8ybbazHp
|
Superseded by #853, which has now merged. Closing rather than rebasing: both fixes are already on main, so the conflict resolution would produce an empty change. This PR was split out of the larger redesign branch, and that branch is what #853 became — so the fixes travelled with it. Verified on main rather than assumed: 1. 2. Round inputs left in join order. If either had been missing I would have rebased this instead. |
Two independent privacy defects in the Wraith round path, both in code running
today. Split out of a larger redesign branch because neither depends on it —
these are bugs on their own.
Neither is currently exploited at scale:
wraith_election_enabledis off andthe lite round path sees little traffic. Both are unconditional once it does.
1.
/round-txpublished the input-to-output mappingOutputProvenanceWirecarriedparticipant_idon every output. A round's wholepurpose is that an observer cannot say which output belongs to which input, and
the endpoint answered that question directly, for anyone who asked.
Removed the field. Prevouts are now keyed by outpoint —
HashMap<(String, u32), &TxInputRef>— rather than resolved through the participant, so the attributionis gone from the data structure and not merely omitted from the response.
no_participant_attribution_is_ever_serialisedasserts on the serialised JSON,so re-adding the field anywhere in the struct fails rather than passing because
one call site stopped populating it.
2. Round inputs were left in join order
Outputs were shuffled with ChaCha20; inputs were not. Join order is close to
arrival order, so the input list ordered participants by roughly when they
joined, and the coordinator's own view of that order was reconstructable from
the transaction.
Inputs are now shuffled with the same primitive under a separate seed
(
WraithLite/v1/input_shuffle), so the two permutations are independent —sharing one seed would have made the output order recoverable from the input
order, which is the leak restated.
input_and_output_permutations_are_independentpins that.Notes
/round-txloses a field; noshipped client reads it.
by inspection.
https://claude.ai/code/session_01R3zCyPaMpt4tTW8ybbazHp