AER-1 verifier gap: the provenance claim sits outside the commitment

Subject: zambo.dev/verify (AER-1 live MCP receipt verifier, draft-zambo-aer1-09)
Found by: Shin, night desk · first measured 2026-10-05 12:56 UTC · re-measured live 2026-10-06 09:35 UTC
In scope: /verify/aer1-verifier.js, /verify/hosted-verifier.js, /aer-1/schema.json, /aer-1/hosted-profile.schema.json

One line

provenance_class and verification_status are required receipt fields, but they are not inside canonical_bytes and not in the anchor envelope. So the verifier's strongest green result, COMMITMENT MATCH, says nothing about the one field that carries the actual claim ("what ran"). A relayed or copied receipt can therefore carry a provenance claim the bytes never committed to.

What the verifier checks

verifyAER1ReceiptJson returns hashMatches (sha256 over canonical_bytes equals output_hash), schemaValid, and valid (both). It also reports signatureCheck and chainCheck, both not_checked for a receipt consumed this way.

The hashed envelope covers everything except the claim

keys inside canonical_bytes:
caller, output, receipt_schema_version, side_effects, tool

keys inside anchor.envelope:
schema_version, receipt_uuid, output_sha256, evidence_sha256,
evidence_byte_length, evidence_url, anchored_at

provenance_class inside canonical_bytes? false
verification_status inside canonical_bytes? false
timestamp inside canonical_bytes? false
provenance_class in anchor.envelope? false

Reproduction

Take a real receipt returned by the service (I used pdf_extract over a public page). Leave every byte of canonical_bytes untouched. Relabel only the metadata:

r.provenance_class    = "OBSERVED VIA GATEWAY";    // was "EXECUTED BY ZAMBO"
r.verification_status = "asserted_not_observed";   // was "execution_observed"

Run it through verifyAER1ReceiptJson:

BASELINE valid: true  hashMatch: true  schemaValid: true
  provenance_class: EXECUTED BY ZAMBO | verification_status: execution_observed

MUTATED  class: OBSERVED VIA GATEWAY | status: asserted_not_observed
  valid: true  hashMatch: true  schemaValid: true  schemaErrors: []

valid = true, COMMITMENT MATCH, schema conforms. The label moved from "this ran on our infrastructure" to "observed through a gateway, not observed" and the check never flinched.

Why it matters

The service's own stated line is honest: a verifiable receipt proves the saved result was not changed, not that the model was right. The gap is that the verifier's strongest result is displayed adjacent to the claim, and a reader collapses the two, reading a green COMMITMENT MATCH on "EXECUTED BY ZAMBO" as "the execution is verified." The bytes only prove non-alteration. The provenance class is an unauthenticated label riding next to the proof, and when a receipt is relayed between parties (exactly the AER-1 use case) nothing binds the label to the commitment.

Concrete fixes

(a) Bind the claim into the commitment. Add provenance_class and verification_status to the hashed envelope alongside tool/output/side_effects/caller; or, if the envelope shape is frozen, add sha256(provenance_class || verification_status) to anchor.envelope. Then a relabeled class changes the commitment and the verifier fails closed. Schema change; bump receipt_schema_version.

(b) If the fields must stay contextual, stop rendering them as verified. In the verifier UI, render the two fields in a visibly separate block labelled as not covered by the commitment, and keep COMMITMENT MATCH scoped to what it proves: two lines, bytes: match / provenance claim: unverified. Stopgap that removes the misread without a schema break.


Reported publicly on Nostr first (event bc10420e…, 2026-10-05 17:45 UTC). This is the formal write-up plus the patch. Repro script: aer1_gap_test.mjs + sampled receipt pdf_call.json.