# Aperture receipt verification & transparency — open spec

`aperture.receipt.verify/v1` · `aperture.transparency/v1` · `aperture.session/v1` · 2026-07-16

This is the open, versioned specification for verifying an Aperture **signed receipt** (a `/api/read` read
receipt or a `/v1/verify` / calibration certificate) and for the append-only transparency log that makes
receipt suppression third-party detectable. It exists so a stranger — an auditor, a regulator, a customer —
can check our receipts **without trusting us and without calling back to us**. The reference implementation
is [`aperture_verify.py`](aperture_verify.py) and [`transparency_log.py`](transparency_log.py) (Python ≥ 3.9,
`cryptography` + stdlib only). Both are covered by `selftest`.

## 1. What a verified receipt proves — and what it does not

A green check proves **provenance**: the receipt was signed by the Aperture registry key and has not been
altered by a single byte since. It proves *what the lens read* / *what was served*, fixed in time so no one —
including us — can quietly rewrite it.

It does **not** prove the answer is true. A receipt binds provenance, not truth. A verdict of "verified" is
earned elsewhere — registry grounding or cross-model agreement — never by this signature. This boundary is
part of the spec: a verifier that implies "signature valid ⇒ answer correct" is non-conformant.

## 2. Signature scheme

Detached **ed25519** over the canonical JSON of the receipt body. Given a receipt/cert object `R`:

1. Read the signature block `R.signature = {"alg": "ed25519", "pub": <base64>, "sig": <base64>}`.
2. Form the **signed body** = `R` minus the fields `signature` and `id`. (`id` is a content hash minted
   *after* signing and is therefore not part of the signed body.)
3. `canon = json.dumps(body, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")`
4. ed25519-verify `base64decode(sig)` over `canon` using `base64decode(pub)`.
5. **Key pinning is mandatory.** `pub` MUST equal a pinned Aperture registry key. A valid signature under any
   other key is a forgery, not a pass. The current keys (cross-checkable at `/api/cert/pubkey`):

   | base64 public key | key_id | role |
   |---|---|---|
   | `U9pHq3UwrFT5oyLwOnSgt+J6YpCkG1zUEcOO4Sxm7hA=` | `2f48ea75c592` | registry — read receipts + verify/photon primary certs |
   | `h9ee/lLc0OXghSwtB1CtYQEC8mblSTzFoV5lOIJ8bSw=` | `fe8416f4bc70` | lucidia axis-cert — harness sidecar honesty_receipt |

**Read-receipt id binding (optional, read receipts only).** A read id is
`sha256( json.dumps(R_without_id, sort_keys=True, ensure_ascii=False) + "aperture-read" )[:12]` — note the
id blob uses **default** separators (`, ` / `: `), *not* the compact separators of the signature body, and
`R_without_id` includes the signature. Recomputing it binds the short id to the signed content.

## 3. Transparency log — `aperture.transparency/v1`

Self-signing says *we* vouch for a receipt; the log proves we cannot later **suppress** or **swap** it
undetected. It is an RFC 6962 (Certificate Transparency) Merkle tree over receipt leaves.

- **Leaf** for a receipt: `sha256( "aperture.receipt.leaf/v1\x00" + json.dumps(R_without_id, sort_keys, compact, ensure_ascii=False) )`.
  Domain-separated so a receipt leaf can never collide with a raw file hash.
- **Merkle Tree Hash (MTH)**, **inclusion proof (audit path)**, and **consistency proof** follow RFC 6962
  exactly (leaf node prefix `0x00`, internal node prefix `0x01`; split at the largest power of two `< n`).
- **Signed Tree Head (STH)**: `{log, tree_size, root_sha256}`. In production the root is ed25519-signed by
  the log **and co-signed by an independent witness** (or mirrored to a public Sigstore/Rekor instance), so
  neither the operator alone nor Aperture alone can present two divergent histories. That external
  co-signature is the milestone that makes the rung genuinely "outside our trust boundary"; this module is
  the primitive it hangs on.
- **Inclusion** proves a receipt's leaf sits at index *i* under a published root. **Consistency** proves a
  size-*m* tree is a prefix of a size-*n* tree — the log only ever appended; nothing between two STHs a
  client saw was rewritten. A rewritten leaf breaks the consistency check against the earlier root.

## 3a. Session trails — the flight recorder for an autonomous run

You cannot watch a 12-hour agent; you can verify its **trail**. A session is an *ordered* list of receipts
from one agent run. Its tamper-evident head is the Merkle root over the receipt leaves (`session_root`). To
verify a session:

1. verify **every** receipt's signature (§2), and
2. recompute the Merkle root over the leaves in order and match it to the head the customer recorded (ideally
   also witnessed in the public log).

Altering, reordering, dropping, or inserting a receipt breaks (1) or (2). No extra key signs the leaves — they
are already bound to Aperture-signed receipts; the root binds their **sequence**. `verify-session <root>
r1.json r2.json …`. This is the primitive an SDK produces client-side: chain the customer's own agent-loop
receipts into a session head, and publish that head to the witness log.

## 3b. The session head — what the SDK emits · `aperture.session/v1`

The recorder ([`aperture_session.py`](aperture_session.py)) runs on the *customer's* side: the agent loop
feeds each Aperture receipt to `record()` (verified as it enters, so a forged one never joins the trail) and
at the end emits a small **session head** to publish and witness:

```json
{ "alg": "aperture.session/v1", "session_id": "sess_…", "n": 3,
  "merkle_root": "<hex>", "chain_tip": "<hex>" }
```

The head binds the trail two independent ways:

- **`merkle_root`** — the RFC 6962 root over the receipt leaves (§3a). Binds content and order; supports
  inclusion proofs against a witness log. It is *session-id-agnostic* — two runs with the same receipts in
  the same order share a root.
- **`chain_tip`** — a domain-separated rolling hash, `h₀ = sha256("aperture.session.chain/v1\x00" ‖
  session_id)` then `hᵢ = sha256("aperture.session.chain/v1\x00" ‖ hᵢ₋₁ ‖ leafᵢ)` (a `prev_receipt_hash`
  linked list). Binds content, order, **and the session identity**, and extends incrementally as receipts
  arrive without recomputing a tree.

Carrying both is deliberate: lifting an *intact* trail under a different `session_id` moves the `chain_tip`
but not the `merkle_root`. `verify_bundle` checks every signature, the root, the chain-tip, and the claimed
count. The recorder signs nothing new — its security rests on the Aperture signatures already on each
receipt; the head only fixes their sequence and session.

## 4. Conformance

An implementation is conformant if, for the vectors in `selftest`:
- it verifies a genuine receipt, **rejects a one-byte tamper**, and **rejects a valid signature under an
  unpinned key**;
- its Merkle root, inclusion proofs, and consistency proofs match the reference for a non-power-of-two tree,
  and a forged leaf / rewritten history fail their proofs;
- a multi-receipt **session** verifies against its root, and tampering, reordering, or dropping a receipt in
  the trail is caught (signature failure or root mismatch);
- a recorded **session head** verifies (signatures + root + chain-tip + count), a forged receipt is refused
  at record time, and re-labeling the `session_id` of an otherwise-intact trail is caught by the chain-tip
  even though the Merkle root still matches;
- and it never reports truth — only provenance and inclusion.

## 5. Quickstart

```bash
# offline — nothing leaves your machine:
curl -s https://www.honesty.tools/api/cert/<id> | python3 -c 'import sys,json;json.dump(json.load(sys.stdin)["cert"],open("r.json","w"))'
python3 aperture_verify.py verify r.json

python3 aperture_verify.py selftest          # 3/3
python3 transparency_log.py selftest         # 8/8 (RFC6962 inclusion+consistency + session)
python3 transparency_log.py add r.json       # append the leaf; print the STH
python3 transparency_log.py check r.json     # prove it's included under the current root

# a whole autonomous-agent run — the flight recorder:
ROOT=$(python3 transparency_log.py session-root r1.json r2.json r3.json)
python3 transparency_log.py verify-session "$ROOT" r1.json r2.json r3.json
# → ✓ SESSION VERIFIED — every signature valid, root matches; tamper/reorder/drop all fail

# or record the trail as your agent runs, then verify the whole bundle:
python3 aperture_session.py open  trail.json
python3 aperture_session.py record trail.json r1.json     # append + re-seal the head, per receipt
python3 aperture_session.py record trail.json r2.json
python3 aperture_session.py verify trail.json             # sigs + merkle_root + chain_tip + count
```
