Verifying an AI-Assisted Work Receipt
Companion to: Attested AI-Assisted Work · v0.3
Status: Draft
The verify surface for the receipt format defined in the spec. Anyone can verify; nobody can forge.
What verification proves
Given a receipt, the prompt that was sent, and the output that was returned, verification answers a single question:
Did this prompt produce this output on this model at this time, signed by this key?
It does not prove the prompt was wise, the output was correct, or the model was the best choice. It proves the binding — request, response, model, time, signer — has not been altered.
Inputs
To verify a receipt you need:
- The receipt itself — the signed JSON blob defined in the spec.
- The canonical prompt — the exact request body the client sent, in the issuer's canonicalization.
- The canonical output — the exact response body the issuer returned.
- The issuer's published key set — fetched from
GET /v1/receipts/keyson the model provider's domain.
The receipt's signed bytes are defined normatively in the spec's Canonicalization section: RFC 8785 JCS over the receipt with the signature field omitted, signed with ed25519. Hashes are lowercase-hex sha256. The verifier never needs the model provider's signing key, and never needs to call the issuer at verify time — the key set is cacheable and the comparison is local.
Outcomes
A verification returns one of four statuses:
valid— key known, not revoked as ofissued_at, signature (and any supplied hashes) match.tampered— the signature does not verify over the canonical bytes, or a recomputed hash does not match.unknown_key—key_idis not in the published key set (consult the rotation history).revoked—key_idis present but marked revoked as ofissued_at. A receipt signed before its key's revocation staysvalid— revocation is evaluated atissued_at, not "now".
Statuses resolve in precedence order unknown_key → revoked → tampered → valid. Anything other than valid means the receipt cannot stand on its own.
Verification flow
- Fetch the issuer's key set (or use a cached copy within its declared TTL).
- Hash the canonical prompt; compare to
prompt_hash. - Hash the canonical output; compare to
output_hash. - Look up
key_idin the key set; check it is active atissued_at. - Verify the ed25519 signature over the receipt fields using that public key.
- Return one of the four statuses above.
No step requires contacting the issuer at verify time. A signed key set fetched yesterday is sufficient.
Try it — live, real verification
The verification endpoints are live on this host and backed by real signed receipts — actual ed25519 signatures checked against a real key set, not canned responses. Four demo ids each exercise one spec status and seed the conformance suite:
| Receipt id | Status | Why |
|---|---|---|
demo-valid-001 |
valid |
active key, intact signature |
demo-tampered-002 |
tampered |
output_hash altered after signing |
demo-unknown-key-003 |
unknown_key |
key_id not in the published set |
demo-revoked-004 |
revoked |
signed by the revoked key, after revocation |
Verify a persisted receipt by id (signature + key-status check):
$ curl -s https://gitjob.io/v1/receipts/demo-valid-001/verify
{"status":"valid","key_id":"gitjob-demo-2026q2","issued_at":"2026-04-12T14:32:00Z"}
$ curl -s https://gitjob.io/v1/receipts/demo-tampered-002/verify
{"status":"tampered","key_id":"gitjob-demo-2026q2","issued_at":"2026-04-13T09:15:00Z"}
Unknown receipt ids return 404.
Fetch the public key set (cacheable; no private material is ever served):
$ curl -s https://gitjob.io/v1/receipts/keys
{"keys":[{"key_id":"gitjob-demo-2025q4","public_key":"…","status":"revoked",
"created_at":"2025-09-01T00:00:00Z","rotated_at":"2025-11-01T00:00:00Z"},
{"key_id":"gitjob-demo-2026q2","public_key":"…","status":"active",
"created_at":"2026-04-01T00:00:00Z","rotated_at":null}]}
Verify any receipt offline — POST the receipt, optionally with the prompt and output you were shown, and the hashes are recomputed (this is where tampered is detectable for receipts we never issued):
$ curl -s -X POST https://gitjob.io/v1/receipts/verify \
-H 'content-type: application/json' \
-d '{"receipt": { …the 9 receipt fields… }, "prompt": "…", "output": "…"}'
{"status":"valid","key_id":"gitjob-demo-2026q2","issued_at":"2026-04-12T14:32:00Z"}
Flip a single byte of output_hash or the signature in that body and the response becomes {"status":"tampered",…}.
Reference implementation
internal/attest(Go, MIT) — the canonicalization, signing, and verification this host runs. Pure, unit-tested, no database or network in the core.- Conformance vectors —
internal/attest/testdata/conformance/vectors.json: the key set plus each demo receipt and its expected status, regenerated deterministically. Any verifier must reproduce them. @gitjob/attest(TypeScript, MIT) — forthcoming port of the Go reference, plus a paste-a-receipt verifier dashboard.
Posture: verification is public and reproducible. The same logic runs on this host, in CI, in a CLI, and in any third-party consumer that wants to check a receipt without trusting gitjob.io as a middleman.
Open questions — for implementing verifiers
- Key-set caching: TTL guidance, signed key-set bundles, gossip between verifiers.
- Multi-turn: verify per-turn receipts independently, or require a session bundle?
- Tool-use: when the receipt covers tool inputs/outputs, what hash tree binds them to the parent completion?
(Canonicalization is no longer open — it is fixed normatively in the spec: RFC 8785 JCS, signature omitted, ed25519 over the UTF-8 canonical bytes.)
Justin Higgins · justin.c.higgins@gmail.com · gitjob.io