Skip to content

fix(documents): 400, not 500, for malformed input to the public verify endpoints - #416

Open
QSchlegel wants to merge 1 commit into
preprodfrom
claude/daily-2026-10-18-verify-endpoints-400
Open

QSchlegel wants to merge 1 commit into
preprodfrom
claude/daily-2026-10-18-verify-endpoints-400

Conversation

@QSchlegel

@QSchlegel QSchlegel commented Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator

What and why

document.verifyProof and document.verifyAttestationChain are public tRPC mutations (no account, no DB). Malformed JSON reached the verifiers and threw, so callers got INTERNAL_SERVER_ERROR. verifyAudit (hardened in #413) already had a shape check, caps and a catch. This brings the other two up to the same standard.

Reproduced on preprod (758f737) via createCaller, all 8 → 500:

Input Before After
verifyAttestationChain, payload null / missing / 2nd record null 500 Cannot read properties of null 400
verifyProof, version: "x" / reviews: null / contentHash: 5 500 value.trim is not a function 400 with the field named
signed review payload "null" 500 invalid review: payload is not a JSON object
attestation publicKeyId: "constructor" "signature does not verify" "signed by unknown key"

Changes

  • proof.ts: proofPackageProblem now checks every field verifyProofPackage reads. The check is factored out as proofShapeProblem, and audit.ts's per-version check now delegates to it, so audit error messages and limits are unchanged. Reviews are capped at PROOF_LIMITS.reviews = 200, the same per-version cap the audit package already used.
  • verifyReview: a signed payload that isn't an object, or whose contentHash isn't a string, is now an invalid review instead of an exception.
  • verifyAttestationChain: the input now requires object payloads. They are checked with z.custom, not parsed, so a payload carrying an extra field still fails its signature rather than being stripped back into one that verifies (there's a test for this). The chain is capped at AUDIT_LIMITS.chain = 500, and key ids are looked up as own properties only.
  • Both mutations catch anything the shape check can't see (for example a signed field that can't be stringified) and return 400. They console.warn the cause so a real verifier bug stays visible.

Signing and spending logic is untouched. The verifiers only gained early-outs that make a result invalid. No previously failing input can now verify.

Tests

  • New src/__tests__/trpc/documentVerifyEndpoints.test.ts (11): an honest chain verifies; an extra payload field breaks the signature; null/string/array/missing payload → 400; a 501-record chain → 400; three malformed proofs → 400; the catch branch → 400 and logs. The 400 cases fail on preprod.
  • documentSignoff.test.ts (+20) and documentAttestation.test.ts (+1): field-type matrix, review cap, user-facing message, non-object signed payloads, non-string signed hash, inherited key ids. All but the well-formed-package case fail on preprod.
  • npx tsc --noEmit: clean.
  • node scripts/run-tests.mjs: 1676 + 97 passed, 2 skipped (pre-existing).
  • tRPC suites against local Postgres 16 (-c jest.trpc.config.mjs --runInBand): 15 suites, 122 passed.
  • An independent cold review found nothing blocking. Its suggestions (type-check partyId/comment, sentence-style message for /verify, log in the catch, test the catch branch) are all in this PR.

Not verified / behaviour notes

  • A proof with more than 200 reviews, or a chain with more than 500 records, is now refused (400, and the /verify paste box rejects such a proof up front). These limits come from AUDIT_LIMITS; no real wallet gets near them.
  • I did not re-run the bundle build. The only client-side change is in proof.ts (used by /verify, 328.6 KB against a 450 KB budget on preprod) and adds a few hundred bytes. CI's budget job will confirm.

🤖 Generated with Claude Code

https://claude.ai/code/session_011icbSAD1vGxmDnJQ4fByEk

… with 400, not 500

verifyProof and verifyAttestationChain need no account, but malformed JSON
reached the verifiers and threw: a null attestation payload, a numeric
version.contentHash, null reviews, or a signed payload of "null" all came
back as INTERNAL_SERVER_ERROR. verifyAudit already had a shape check, caps
and a catch; these two now match it.

- proofPackageProblem checks every field verifyProofPackage reads (shared
  with the audit check as proofShapeProblem) and caps reviews at 200, the
  per-version cap the audit package already applied.
- verifyReview treats a non-object signed payload, or a non-string signed
  contentHash, as an invalid review instead of throwing.
- verifyAttestationChain requires object payloads (checked, not parsed, so
  an extra field still breaks the signature) and caps the chain at
  AUDIT_LIMITS.chain (500); key ids are looked up as own properties only.
- Both mutations catch what the shape check cannot see and return 400,
  logging the cause.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011icbSAD1vGxmDnJQ4fByEk
@vercel

vercel Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
multisig Ready Ready Preview Oct 18, 2026 5:10am UTC

Request Review

This branch was successfully deployed

1 active deployment
Preview — 6078a996 Deployed Oct 18, 2026 by vercel[bot]
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.

2 participants