Skip to content

feat: independent Agent Commerce receipt verification (#2) - #3

Merged
dkitchell merged 2 commits into
mainfrom
feat/receipt-verification
Aug 20, 2026
Merged

dkitchell merged 2 commits into
mainfrom
feat/receipt-verification

Conversation

@dkitchell

Copy link
Copy Markdown
Contributor

Summary

Implements #2certifieddata-verify <receipt-id> verifies Agent Commerce payment receipts locally: fetch the payload + signature envelope, fetch the Agent Commerce PEM from .well-known, RFC 8785 JCS-canonicalize (the repo's existing zero-dep canonicalizer), verify Ed25519 with Node crypto, recompute SHA-256 against the stored hash. Zero new dependencies.

The server's booleans never determine the verdict — proven by a lying-server test where {valid: true, signatureValid: true} accompanies a tampered payload and the CLI still says INVALID.

Artifact-kind resolution (per spec)

src/resolve.ts: prefix hints → endpoint-URL hints → local JSON schema sniff → dual-endpoint probe for bare UUIDs. Exactly-one wins; both → AMBIGUOUS (exit 64, requires --type); neither → NOT_FOUND; transport failure → NETWORK, never "not found". --type certificate|receipt overrides everything.

Fail-closed key handling

PEM endpoint 503/unreachable → UNKNOWN_KEY exit with an explicit "the server's own verdict is NOT accepted as a substitute" message. --offline receipts require --key <pem>.

Acceptance criteria

  • certificate behavior backward compatible — all 34 pre-existing tests pass untouched
  • --type receipt / --type certificate override detection
  • receipt signature verified with Node crypto, not the server verdict
  • canonicalization = RFC 8785 JCS (same canonicalizeToBytes used for certs)
  • payload SHA-256 recomputed locally vs stored hash
  • key from /.well-known/certifieddata-public-key.pem
  • 503/missing key → non-success exit, no silent fallback
  • tampering with any signed field fails — 9-field tamper matrix (amount, policy_hash, artifact_hash, agent_id, purpose, status, settlement_state, timestamp, receipt_id)
  • live end-to-end against a production receipt — blocked on platform deploys landing: platform#318 (exposes signature + canonicalization + public_key_url on the verify endpoint) and platform#319 (fixes the PEM endpoint's ESM dynamic-require 503). Both merged; will confirm and post the terminal transcript here once production serves them.

Tests: 49/49 (15 new). Lint + typecheck clean.

🤖 Generated with Claude Code

dkitchell and others added 2 commits August 20, 2026 00:00
certifieddata-verify <receipt-id> now verifies payment receipts locally:
fetch the payload+signature envelope from the public verify endpoint,
fetch the Agent Commerce PEM from .well-known, RFC 8785 JCS-canonicalize
(the repo's existing zero-dep canonicalizer), verify Ed25519 with Node
crypto, and recompute SHA-256 against the stored receipt hash. The
server's valid/signatureValid booleans are surfaced as informational
metadata only — they never determine the verdict, proven by a lying-
server test.

Artifact-kind resolution per the issue spec (src/resolve.ts): prefix and
URL-path hints, local JSON schema sniffing, and for bare UUIDs a dual
probe of both public endpoints — exactly-one wins, both -> AMBIGUOUS
(exit 64, require --type), neither -> NOT_FOUND, transport failure ->
NETWORK (a server failure is never treated as absence). --type
certificate|receipt overrides everything. Certificate behavior is
unchanged (all 34 pre-existing tests pass untouched).

Key handling fails closed: PEM endpoint 503/unreachable -> UNKNOWN_KEY
exit with an explicit 'the server's own verdict is NOT accepted as a
substitute' message; offline receipt verification requires --key <pem>.

Tests: 15 new (49 total) — real Ed25519 over real JCS bytes; 9-field
tamper matrix (amount, policy_hash, artifact_hash, agent, purpose,
status, settlement_state, timestamp, receipt_id) each fails; stored-hash
mismatch fails even with a valid signature; missing signature and wrong
schema are MALFORMED; RSA key is UNKNOWN_KEY.

Depends on platform PR #318 exposing signature + canonicalization +
public_key_url on GET /api/payments/verify/:receiptId.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dkitchell

Copy link
Copy Markdown
Contributor Author

Live end-to-end against production is now confirmed — both platform deploys (#318 signature exposure, #319 PEM ESM fix) landed. Fresh receipt from a fully anonymous stranger run tonight:

$ certifieddata-verify 2a37d835-e50d-4a59-99ad-5fe1d346871f
✓ VALID  receipt 2a37d835-e50d-4a59-99ad-5fe1d346871f
  signed by    (published Agent Commerce key) (CertifiedData.io)
  signature    pass
  payload hash pass
  public key   /.well-known/certifieddata-public-key.pem
  settlement   simulated_sandbox
  The verdict above was computed locally — not taken from the server.

Auto-detection also confirmed live: the bare UUID resolved via the dual-endpoint probe (receipt exists, certificate doesn't → receipt path) with no --type needed.

The receipt itself carries the full evidence chain from #315: non-null policy_hash matching the policy snapshot at authorization time, truthful settlement_state: simulated_sandbox, settled_at bound. All acceptance criteria now pass.

@dkitchell
dkitchell merged commit af3c080 into main Aug 20, 2026
6 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