Verifying an AI-Assisted Work Receipt

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'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:

Statuses resolve in precedence order unknown_keyrevokedtamperedvalid. Anything other than valid means the receipt cannot stand on its own.


Verification flow

  1. Fetch the issuer's key set (or use a cached copy within its declared TTL).
  2. Hash the canonical prompt; compare to prompt_hash.
  3. Hash the canonical output; compare to output_hash.
  4. Look up key_id in the key set; check it is active at issued_at.
  5. Verify the ed25519 signature over the receipt fields using that public key.
  6. 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

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

(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