Skip to content

fix: F-2026-18199 | [Dual Defense] TSS Setup Message Signs an Unverified Hash (Payload vs SigningHash Split) - #309

Merged
Aman035 merged 6 commits into
audit-fixesfrom
F-2026-18199
Aug 19, 2026
Merged

fix: F-2026-18199 | [Dual Defense] TSS Setup Message Signs an Unverified Hash (Payload vs SigningHash Split)#309
Aman035 merged 6 commits into
audit-fixesfrom
F-2026-18199

Conversation

@Aman035

@Aman035 Aman035 commented Aug 18, 2026

Copy link
Copy Markdown
Member

F-2026-18199 Setup message signs an unverified hash

Issue

  • Followers verify UnsignedSigningReq.SigningHash, but DKLS signs the hash embedded in Message.Payload. The two arrive unbound.
  • messageHash is only checked in GetResult, after shares are exchanged. So a coordinator can present a legitimate hash for verification and embed an attacker-chosen one in the setup.
  • Same split in keygen, keyrefresh and quorumchange: we validate participants and derive a threshold, but the session runs on the values inside Payload.

Approach taken

  • Added three decoders in dkls/utils.go: SetupMessageHash, SetupParticipants, SetupThreshold.
  • Added one entry point, verifySetupMatchesValidated, at step 6c of handleSetupMessage. It binds participants for every protocol, the hash for sign types, and the threshold for key-lifecycle types.
  • Runs before the ACK, so no shares are produced for a setup we did not verify.
  • Participant comparison is order sensitive. The coordinator sorts once and uses the same partyIDs for both the blob and Message.Participants, and party index determines protocol role.

Why decode and compare, not rebuild locally

  • The recommendation's first option was to rebuild the setup locally. That is not possible here.
  • Setup construction is non-deterministic: identical inputs produce different bytes on every call.
  • The blob carries ephemeral ed25519 keys, one signing key per party plus everyone's verifying keys, used to authenticate protocol messages between parties.
  • If each follower built its own, no two would share keys and every session would fail. There must be exactly one setup, from the coordinator, so the only defence is verifying its contents.

Threshold

  • The wrapper exposes no threshold decoder, and the session constructors take a threshold argument and ignore it, so the embedded value is what the protocol runs with.
  • From the DKLS source, a setup is a tag-length-value list after a 36 byte header, and tags::T is the threshold stored as one byte. SetupThreshold parses that in Go. No Rust change needed.
  • Tradeoff: this reads a library-internal encoding. TestSetupThreshold builds real setups at thresholds 2 and 3 and asserts the value round-trips, so a format change fails in CI.

Tests

  • dkls/utils_test.go: each decoder returns what the blob contains, substituted or downgraded setups report the attacker value, all error on malformed input.
  • sessionmanager: rejects a substituted hash, substituted, dropped or reordered participants, and a downgraded threshold.
  • End to end: a payload whose hash differs from the verified one is refused, no session created, no ACK or share emitted.
  • Mutation checked: neutering each comparison fails its tests, and removing only the step 6c wiring passes every unit test but fails the end to end test.

@Aman035 Aman035 changed the title fix: bind verified signing hash to the DKLS setup message (F-2026-18199) fix: F-2026-18199 | [Dual Defense] TSS Setup Message Signs an Unverified Hash (Payload vs SigningHash Split) Aug 19, 2026
@Aman035
Aman035 merged commit 6ae6dcb into audit-fixes Aug 19, 2026
9 checks passed
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