Skip to content

fix(governance-api): require the signed message to match the submitted body#779

Closed
frankmeds wants to merge 1 commit into
mainfrom
fix/governance-signed-message-binding
Closed

fix(governance-api): require the signed message to match the submitted body#779
frankmeds wants to merge 1 commit into
mainfrom
fix/governance-signed-message-binding

Conversation

@frankmeds

Copy link
Copy Markdown
Contributor

Problem (pre-existing authenticity gap)

In POST /api/message, the signature is verified over body.sig.message, but every downstream check — message validation, the gZIL gate, the IPFS pin, and the DB record — operates on body.msg (JSON.parse(body.msg)). Nothing ties the two together.

A client could therefore sign one string and submit a different proposal/vote body: the signature check passes (over sig.message), while the unsigned body.msg is accepted and persisted as authentic. This affects both the EVM and Schnorr paths and predates the EVM work (surfaced during the #778 security review).

Fix

Reject the request when body.sig.message !== body.msg, before signature verification:

if (body.sig.message !== body.msg) {
  return res.status(400).json({ code: ErrorCodes.INCORRECT_SIGNATURE, error_description: "incorrect signature" });
}

Byte-equality is the correct check: the signature authenticates the exact bytes of sig.message, so those bytes must equal the body.msg bytes that produce the content.

Safety (verified against real data, no behaviour change for legitimate clients)

The current frontend builds both from the same string (msg.msg), so they're already identical. Confirmed empirically: for all sampled prod gZIL proposals (ZilPay/Schnorr) and staging duck proposals (EVM), sig.message parses to exactly the stored msg content. So legitimate submissions on both paths pass unchanged; only true mismatches are rejected.

Testing

  • tsc --noEmit clean; existing governance-api test suite green.
  • This is an inline route guard (consistent with the handler's other inline validations — type/version/timestamp/etc., which likewise have no route-level test harness). Its correctness rests on the byte-equality semantics plus the empirical verification above.

Scope

Independent of #778 (different region of message.ts); either can merge first.

…d body

The signature was verified over body.sig.message, but every downstream check (validation, gZIL gate, IPFS pin, DB record) uses body.msg. A client could sign one string and submit a different proposal/vote body, which would then be treated as authentic.

Reject when body.sig.message !== body.msg, before signature verification. Applies to both EVM and Schnorr paths. Verified safe against real prod (ZilPay) and staging (EVM) data: sig.message already equals body.msg for all legitimate submissions.
@frankmeds frankmeds closed this Jun 5, 2026
@frankmeds frankmeds deleted the fix/governance-signed-message-binding branch June 5, 2026 09:12
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