The problem in the field

I spent a career handing evidence to people who had every reason to doubt it. Chain of custody was the answer: who had it, when, and what they did with it, written down in a way a stranger could check. Agents are now doing things that used to need a person with a badge. Unlocking a door. Moving a camera. Tagging a clip that ends up in a case file. The question I keep coming back to is what must be independently provable about what an agent saw, inferred, was authorized to do, and actually did, before its output triggers physical-world action or becomes evidence.

Why the usual answers fail

The audit trail lives in the platform that did the work, so a compromised or careless platform grades its own homework. Reading the trail often needs an account and a license from the vendor. And JSON logs are easy to write and hard to sign, because two encoders will serialize the same record two ways.

What I built

A small wire spec and a reference verifier. Receipts are deterministic CBOR with a detached COSE_Sign1 signature (ES256), closed schemas, and normative CDDL. The verifier, pyref, is Python 3.11 standard library only. No network, no wall clock, no dependencies. Verifying receipts is free, forever, offline, with no account and no license from anyone. The conformance vectors are byte-pinned known-answer tests generated by a second, clean-room implementation in TypeScript, which I also wrote, and the threat model, v0.1, lists 30 findings. The reference producer is onvif-mcp. Since October 12 it writes v0.2.3, and its first two live bundles, snapshots from two of my cameras, verify as conformant in pyref. Its older receipts stay in the August pre-release format they were signed in.

How it works

A producer signs each receipt and links it to the previous one. The verifier runs twenty checks in a fixed order, from the size of the bundle to the signed verdict, and names the first one that fails. This is a test bundle from the repo, so you can run it yourself:

$ python3 -B -m pyref verify kats/positive/bundle-valid-subset.cbor --at 1735689800
AAR offline verification report
Bundle: kats/positive/bundle-valid-subset.cbor
Evaluation time: 1735689800
Network: not used (all inputs are local)
Trust policy: producer-declared only
Result: conformant
Steps:
  01 PASS          Bundle byte limit
  02 PASS          Bundle CBOR
  03 PASS          Bundle schema
  04 PASS          Static resource counts
  05 PASS          Trust-policy input
  06 PASS          Envelope mechanics
  07 PASS          Content commitments and IDs
  08 PASS          Credential lifecycle
  09 PASS          Emission identity
  10 PASS          Receipt schema semantics
  11 PASS          Replay and freshness
  12 PASS          Referential closure and graph
  13 PASS          Authorization dominance
  14 PASS          Epoch state machine
  15 PASS          Manifest index
  16 PASS          Merkle batches
  17 PASS          Anchors
  18 PASS          Bundle ranges and coverage
  19 PASS          Evidence-class qualification
  20 PASS          Verdict
First failure: none

What that gives you: attribution to a signer, tamper evidence after emission, and tamper evidence over the declared contents of an epoch. What it does not give you: it can't stop a compromised signer from lying before it signs, it can't prove an inference was correct, and it doesn't establish legal compliance with anything. I'd rather say that here than have someone find out in a hearing.

Where it stands

A stable experimental spec, not an adopted standard. Both implementations are mine, so nobody independent has built one yet, and that gap matters, because the best bug I've found so far came from a cross-check. The reference CBOR encoder wrote every unsigned integer between 2^24 and 2^32 wrong. That range covers every real Unix timestamp. All 188 fixtures stayed green because they came from the same encoder. The clean-room second implementation caught it on its first pass. One implementation agreeing with itself proves nothing. The two still differ. An October 8 audit found 40 of 232 test fixtures where they sign different values in a failure verdict, and one where they disagree on the result itself. Closing that gap blocks a v1.0.

What's next

v0.2.3 is tagged, as of October 12. Since v0.2.0 in September the wire has had three numbered changes, each closing a hole a review found: the signed verdict now commits to the prior history it was judged against (v0.2.1), every signed object has to be signed by the key it names (v0.2.2), and a receipt can declare where an action's inputs came from, such as a camera, the operator or a third party, which the verifier passes on as a stated limit rather than a fact (v0.2.3). The conformance suite runs in CI on every change. What's left is the part I can't do myself: one external producer emitting receipts that pyref verifies without my help. That external run, or an acknowledgement from the people writing the adjacent standards, is the signal I'm watching for. The wire may still change in response to it.