In short. Our archived v0.1 verifier passed a receipt holding two retained values, 99 then 0, because its parser kept the last. A signature can vouch for bytes, never for the events those bytes describe, so a result should list the checks it ran and stop there. If a maintained signed-JSON library can print a result that plain, use it.
What this is. A code review of our archived v0.1 receipt verifier, and a short account of what v0.2 changed. We read the v0.1 draft and source, built an eight-leaf tree and malformed inputs, and reran the shipped fixtures on Node 22.22.3. Our account of v0.2 rests on its published spec and README and one internal review record.
Contents
Our v0.1 checker printed VERIFIED for a near-empty receipt
On 2 November we gave our own v0.1 checker a near-empty agent receipt, a signed record of an AI agent’s work, and it printed VERIFIED. The receipt held three fields: a version, a one-leaf log entry and retained: 0, a claim that zero bytes were kept. Every check the program ran came out true for that receipt, and it had checked only a version string, two signatures, a proof and one number [8] [10].
The word sounded like a verdict on the whole receipt. Its full line read VERIFIED (signature and log, against the trust store); attestation root pending. The signatures were correct, made with keys we’d told the checker to trust [8] [10].
The checker was the reference verifier for Attested Agent Receipt v0.1, our first receipt draft, since archived [4]. It’s an offline Node program that sees only the files it’s given [5] [6]. We kept its code and failure records and wrote a stricter format, Signed Agent Receipt v0.2 [11].
The v0.1 checks were too few, and one read proof directions backwards
The checker did less than the word VERIFIED suggests, and some of what it did was wrong. It runs six steps, in this order [5] [6]:
- It checks that the version field,
aar, equals"0.1". - It writes the body in one fixed byte form.
- It checks the Ed25519 signature against a key from the supplied trust store.
- It computes the leaf, the receipt’s entry in the log, from a hash of the canonical body without its log field.
- It checks the log proof against a supplied signed head, the log’s signed root and size.
- It checks that the retained field is zero.
That’s the whole list, apart from a ledger summary that only counts the entries it was handed and always prints PASS. Nothing checks a policy or observes an effect [5] [6]. The code requires none of the draft’s other fields, so the near-empty receipt passed without a run ID, issue time, runtime, model, policy, attestation, tools or ledger [6] [8].
We wrote the same receipt with two retained fields, 99 then 0, and it also returned VERIFIED, because the JSON parser kept the last value [6] [8]. JCS (RFC 8785) forbids duplicate field names [1]. The signature covered the parsed object, so this is a missing rejection rule rather than a forged signature [8].
In the draft, one sentence says the signature covers the body hash, while other passages, and the code, use the canonical body bytes. The code computes the hash only to display it [4] [6]. The draft’s sort rule is off too: JCS sorts member names by UTF-16 code units, the draft says code point, and the orders differ for some characters [1] [4].
The canonicalizer keeps a lone surrogate, where JCS requires an error, and writes a numeric overflow as null [1] [6]. The code’s own comment calls its canonicalizer a subset, so it makes no claim of full RFC 8785 conformance [6].
The proof check holds the clearest case of a test passing for the wrong reason. A log proof gives one sibling hash per level. The leaf’s position and the tree size fix whether each sibling sits left or right, a list of turns read from the bottom up [2].
The v0.1 code took the siblings from the bottom but the turns from the top [6]. We built an eight-leaf tree and its bottom-up proofs ourselves, without the code’s sample generator. Every proof was valid, yet in that one tree the code returned the wrong root for half of them [7]:
- positions 1, 3, 4 and 6 gave the wrong root;
- positions 0, 2, 5 and 7 gave the right one.
The four that passed have turns that read the same both ways. The shipped sample uses position 5, whose turns run right, left, right, so it could never have shown the bug [6] [7].
In the v0.1 code, the bytes a log signs for its head, sthBytes, cover only the log ID, root and tree size [4] [6]. We added a timestamp to a head without re-signing it, and the checker still accepted it [8]. The body’s own issue time, issued_at, is signed, but it’s still only the issuer’s claim. The program offers neither trusted event time nor replay detection [6].
The dated check record and its raw result transcript are published [8] [10]. So is the portable runner that produced the transcript [9].
Each output line reports one check; the label claimed more
Each line the checker prints reports one check, and only the final label speaks for the whole receipt [5] [6]. A passing line means at most that its check came out true for these inputs. A skipped check stays skipped, however many other lines pass.
| Output | What it reports | What can’t follow |
|---|---|---|
| PASS | At most, one check came out true. The trust-store line passes when its file parses, and the ledger line always passes. | That it works for every input, or that an event happened. |
| SKIP | Not run against trusted inputs, or not implemented. The attestation root is always skipped. | Anything about that check. |
| FAIL | At least one check came out false. | Intent or wrongdoing. The code or trust files may be at fault. |
| UNVERIFIED | No check failed, but required trust backing was missing. | That the receipt is authentic, or that its account is false. |
| VERIFIED | Signature, signed head, proof math and stated-field rules all came out true. | Full schema or JCS conformance, proof code correct for every input, attestation appraisal, fresh or complete observation, or erasure. |
Even VERIFIED, the label the near-empty receipt got, vouches for nothing in its last cell.
We reran the checker’s own fixtures read-only on Node 22.22.3 [10]:
- the sample receipt with trust and head exits 0;
- the same receipt without trust exits 3;
- the tampered and forged-root fixtures exit 1.
The forged-root fixture is weaker than its name suggests. It changes the receipt’s signed copy of the root without re-signing, so the signature fails. The log check still passes, since the checker treats that copy as informational and checks the proof against an independently signed head. So the fixture is no independent test that every root an attacker might choose gets rejected [6] [7].
Our near-empty and duplicate receipts also exited 0. We chose their keys, so they show missing input rules, and they don’t show a forgery against someone else’s key [8]. Some malformed inputs crash the program instead, a sixth outcome the table leaves out [6]. A usage error, exit 2, sits outside it too [5].
The recipient picks the keys, and a key isn’t a witness
A receipt can’t choose its own judge, so whoever relies on it has to pick the trusted keys. Picture a job applicant who supplies a reference’s phone number. The call reaches whoever the applicant picked.
Trust a key found inside a receipt, and a valid signature shows only that its maker held the matching private key. A root it supplies is no better, since a proof can match it yet place the entry in no log you accept.
Our first design got this right, and v0.2 keeps it [11]. The v0.1 trust store maps each runtime-key ID and log ID to a public key, and the signed head arrives separately. One thing the v0.1 checker takes on faith is the trust-store file itself, so those keys need an independent source that keeps them current [4] [5].
No JSON document can decide which issuers and logs to accept, who hands out their keys, how to rotate them, what to do after a compromise or which policy applies. Even a genuine signed head from a log you accept leaves open whether the log is complete, whether others saw the same history and whether the event happened.
RFC 9162, the Certificate Transparency (CT) specification, separates inclusion proofs from consistency proofs, describes log monitors and warns of split views [2]. One proof against one signed root can’t see a log’s history, and neither of our versions implements CT [6] [11]. Our spec essay covers what one root can show.
A trusted key can still sign a false or incomplete account. Had the near-empty receipt listed every field, it would still be only the issuer’s word. The W3C’s Verifiable Credentials model draws the same line, between checking a secured statement and judging whether its claims are true [3].
The v0.1 program never looks past the bytes. It appraises no hardware evidence and only compares a stated retained count with zero [5] [6]. Proof that an event happened needs an observer that states what it could see and ties its record to the receipt and policy version. Our private-verification essay takes up that observer.
v0.2 requires every field, and a pass claims less
The v0.2 spec answers with stricter rules and a narrower claim. Every field is required, so the spec rules out a receipt as bare as ours. A field that appears twice is rejected, even when one copy hides its name behind an escape sequence, and plain JSON numbers are rejected. Each signature covers exact bytes that start with a label naming its role, issuer or log, and the issuer key and the log key must be different keys [11] [12].
The one positive result, ARTIFACT CHECKS PASSED, means three things and no more. The receipt matches a fixed schema, keys the recipient chose made its signatures, and it sits under one signed log root. Hardware attestation, execution, complete observation, retention, deletion, time and log history all stay outside that result [11].
A receipt with all four inventory lists empty still passes, and the result no longer says anything an empty list would contradict [11]. Our spec essay walks through the format and why it dropped the word attested.
Our evidence for v0.2 is one internal review, done with AI agents’ help, whose adversarial tests were written separately from the code. We publish a summary of its record, but not the v0.2 source code, tests or fixtures, and no outside cryptographer has reviewed v0.2 [12] [13].
A checker should say which checks it ran, and stop
A receipt checker should list the checks it ran and claim nothing more, whatever format carries the record. Our first checker failed that test, and v0.2’s result passes it by claiming less [11].
The strongest objection is that our format shouldn’t exist. Ordinary signed JSON, checked with well-maintained libraries, already ties bytes to a key. We haven’t tested whether a custom format adds anything over the same fields in plain signed JSON. We also have no performance measurements that would justify a custom format.
If a well-maintained signed-JSON tool can print a result that lists its checks and stops, use it. More confidence needs an outside cryptographer’s review and a second checker, maintained by someone else, that agrees with ours on shared test vectors [12] [13].
Once the bytes are handled well, the hard question moves to events: signed lies, omitted effects, controlled bypasses and hidden histories that no reviewer could tell apart. A checker’s output can’t sort them. Our proposed-studies page sets out a study for them.
Scope and limits
- What we did
- We read the v0.1 draft and source, built an eight-leaf tree and its proofs without the code’s sample generator, wrote a near-empty receipt and one with two retained values, added an unsigned timestamp to a signed head, and reran the shipped fixtures. No outside party has reviewed these checks.
- What we didn’t do
- Every receipt, key and log here is synthetic: no real agent run, production key or public log. We didn’t test forgery against someone else’s key, invalid-proof acceptance or a failure rate. We didn’t run v0.2 on our test receipts, and v0.2 is incompatible with v0.1 receipts by design.
- What this can’t tell you
- How the v0.1 proof code handles other trees or invalid proofs, or how fully its parser conforms to JCS. Whether any event inside a receipt happened, since neither version has an observer. Whether a custom format adds anything over plain signed JSON.
- Confidence
- High on each v0.1 failure: the v0.1 source, fixtures, runner and transcript are published, and the runner reproduces our checks. High that the v0.2 spec states the rules we describe; moderate that the checker enforces them. That rests on one internal review, and we publish only its summary record.
- What would change our view
- A second checker, maintained by someone else, that disagrees with ours on shared test vectors. That test needs v0.2 vectors we haven’t published. An outside cryptographer’s review that finds a flaw in v0.2. A maintained signed-JSON tool whose result lists its checks and stops, which would argue for using it.
- Corrections
- We log every correction, with its date, on How we work. Send one through a research enquiry.
The open question
Would an independently maintained implementation, run on shared test vectors, agree with our checker on every pass and every rejection?
The v0.2 specification, README and internal review record, published beside the preserved v0.1 failures and the runner and transcript that reproduce them.
Source materials
These documents describe the receipt format and its checker, for closer review. The source code, tests and fixtures are on GitHub, at Lugano-AI/signed-agent-receipt. These are research artifacts, not evidence of a deployed service.
References
A selected review of published sources, with Lugano’s analysis and proposed work identified in the text. Source dates and conditions matter; historical results are not current-model rankings.
- 1
RFC 8785: JSON Canonicalization Scheme
Defines deterministic JSON canonicalization, input constraints and validation guidance for signature applications.
Scope. Does not establish semantic truth or general conformance of this prototype.Back to the essay
- 2
RFC 9162: Certificate Transparency Version 2.0
Defines Merkle inclusion and consistency proofs, signed tree heads and log monitoring in Certificate Transparency.
Scope. This prototype is not a Certificate Transparency implementation; one head does not establish append-only history.Back to the essay
- 3
Verifiable Credentials Data Model v2.0
States that credential verification does not imply truth of encoded claims.
Scope. The model is not a runtime observation system.Back to the essay
- 4
Historical v0.1 — Attested Agent Receipt (AAR) v0.1
Local proposed draft specifies intended receipt, canonicalization, log and trust boundaries.
Scope. A draft, not evidence of a deployed runtime, public log, hardware attestation or erasure.Back to the essay
- 5
Historical v0.1 — AAR reference verifier README
Local README describes offline prototype, requirements, synthetic fixtures, checks and explicit limits.
Scope. Does not establish broad security, conformance or real-world behavior.Back to the essay
- 6
Historical v0.1 — AAR reference verifier source
Local implementation of parsing, key lookup, signature, tree head, proof, field and result logic.
Scope. Code review and a few fixtures do not establish correctness across all inputs.Back to the essay
- 7
Independent synthetic check of receipt-verifier inclusion paths
Read-only source audit and independently constructed eight-leaf synthetic proof check, with reproduction code, expected and actual roots, and fixture outputs.
Scope. One synthetic tree and a few fixtures are not a security assessment, invalid-proof acceptance test, failure-rate estimate or broad conformance study.Back to the essay
- 8
Internal synthetic verifier artifact checks, 2 November 2026
Dated method/results record with a portable read-only runner and machine-readable transcript, Node version, unchanged verifier hash, explicit local-key assumptions, fixture outcomes and selected input-constraint checks.
Scope. Internal bounded synthetic checks; no external human review, full conformance or security audit, invalid-proof acceptance study, real execution, trusted event time or retention observation.Back to the essay
- 9
Portable synthetic verifier-check runner
Self-relative Node runner for the dated local checks; only its sibling transcript is written, while verifier and supplied fixtures are left unchanged.
Scope. Bounded internal synthetic record; not a complete conformance/security assessment, invalid-proof acceptance study, real-run observation or external human review.Back to the essay
- 10
Synthetic verifier-check result transcript
Raw per-check exits and output, eight expected/actual roots, Node version and subject hash; contains no generated private keys or machine-specific paths.
Scope. Bounded internal synthetic record; not a complete conformance/security assessment, invalid-proof acceptance study, real-run observation or external human review.Back to the essay
- 11
Signed Agent Receipt Reference Profile v0.2
Defines the restricted canonical JSON profile, exact signing bytes, required schema, external trust, bounded Merkle inclusion, and unsupported properties.
Scope. A format contract; it does not establish real agent execution, privacy, or a deployed service.Back to the essay
- 12
Signed Agent Receipt v0.2: README and checker guide
Describes the offline reference checker, its command line, its four result states and exit codes, its trust boundary and its deterministic synthetic fixtures. The source code, tests and fixtures it describes are on GitHub, at Lugano-AI/signed-agent-receipt.
Scope. The fixtures are test inputs, not observed tasks or a completed research study. No production readiness or external audit is claimed.Back to the essay
- 13
Signed Agent Receipt v0.2: internal review record
Commands, coverage counts, defects found and fixes from an internal implementation review and a separately authored adversarial harness. The hashes and per-case results it cites sit in files this site doesn’t publish, and rerunning the commands needs source code and tests that it doesn’t publish either.
Scope. Agent-assisted internal review, not independent external certification, formal verification, or evidence of runtime behavior.Back to the essay