Export format v1
This page specifies the interchange contract between the writer and the verifier: the
run_*.jsonl export file that behalf verify checks offline. A later revision of the format will
add checkpoint and inclusion-proof material from the transparency log under a new format string;
files in this format remain readable.
Two invariants from the receipt schema govern everything: stored bytes are the signed bytes are the hashed bytes (no canonicalization step exists — DSSE/PAE), and the verifier needs no network and no call to behalf.
One deliberate deviation from standard DSSE: the payload is embedded as plaintext JSON, not base64, so that tampering with the file directly edits receipt content — which the verifier must catch. Signing and hashing are still over exact bytes — the byte span of the payload value as it appears in the file — so there is still no canonicalization: the writer emits the payload bytes it signed, verbatim, and the verifier extracts that exact span from the raw line before doing anything else.
1. File layout
Section titled “1. File layout”UTF-8, one JSON object per line (\n separators, no CR). Three line kinds, in order:
- Header — exactly one, first line.
- Leaf — one per receipt,
indexstrictly ascending from 0, no gaps. - Head — exactly one, last line.
1.1 Header line
Section titled “1.1 Header line”{"kind":"header","format":"behalf.sh/export/v1","log_origin":"<origin string>","keys":[{"jkt":"<RFC 7638 thumbprint, base64url>","jwk":{"kty":"OKP","crv":"Ed25519","x":"<base64url>"}}]}keys carries every Ed25519 public key referenced by any signature in the file, keyed by its
RFC 7638 JWK thumbprint (SHA-256, base64url, no padding). Verification checks signatures against
these embedded keys; key provenance (a published key log) is a later milestone, and the
verifier’s output must not claim it.
1.2 Leaf line
Section titled “1.2 Leaf line”{"kind":"leaf","index":31,"payloadType":"application/vnd.behalf.receipt+json","payload":{…receipt JSON…},"sig":{"keyid":"<emitter jkt>","sig":"<base64std>"},"leaf_hash":"<hex>"}payloadis the receipt object perreceipt-v1.schema.json, embedded as plaintext.payload_bytesis defined as the exact byte span of thepayloadvalue in the raw line — from its opening{to its matching closing}inclusive. The writer MUST splice the payload bytes it signed into the line unmodified (no re-serialization, no re-indentation). The verifier MUST extract the span from the raw line bytes with a scanner that respects JSON strings and escapes — it MUST NOT parse-and-reserialize.pae = PAE(payloadType, payload_bytes)per the DSSE spec:"DSSEv1" SP LEN(payloadType) SP payloadType SP LEN(payload_bytes) SP payload_byteswhere LEN is the decimal ASCII byte length and SP is a single 0x20.sig.sig= Ed25519 signature overpae, base64 (standard alphabet, padded), by the emitter key identified bysig.keyid.leaf_hash= lowercase hex SHA-256 overpae. (This is the v1 leaf definition; when the format gains transparency-log material, the leaf hashes the stored envelope per the receipt schema — theformatstring is the switch.)
1.3 Head line
Section titled “1.3 Head line”{"kind":"head","head":{"format":"behalf.sh/export/v1","log_origin":"<origin>","count":47,"chain":"<hex>"},"sig":{"keyid":"<jkt>","sig":"<base64std>"}}- Chain rule:
chain_start = SHA-256("behalf.sh/chain/v1\n" + log_origin);chain_i = SHA-256(chain_{i-1} || leaf_hash_i_raw)for i = 0…count−1, whereleaf_hash_i_rawis the 32 raw bytes (not hex).head.chainis the final value, hex. head_bytes= the exact byte span of theheadvalue in the raw line (same span rule).sig.sig= Ed25519 overPAE("application/vnd.behalf.chain-head+json", head_bytes).
2. Verifier contract (behalf verify <file>)
Section titled “2. Verifier contract (behalf verify <file>)”Checks, in order, reporting the first failure class but continuing where meaningful:
- Parse header; unknown
format→ unverifiable. Unknown extra JSON fields anywhere MUST be ignored (production logs grease unknown line and field types; verifiers must tolerate them). - Per leaf: extract
payload_bytesspan, recomputepae, checkleaf_hash, verifysigagainst the header key forkeyid. A mismatch is content tampering at index N; every later receipt is reported unverifiable (chain breaks at N; receipts N+1..count-1 unverifiable). - Index sequence: a gap is a dropped receipt; a non-ascending index is reordering.
head.countvs actual leaf count: fewer leaves is truncation. This check runs before the chain compare — a truncated file must classify as truncation, not as the trivially-mismatched chain it also implies. More leaves thanhead.countclassifies as chain.- Recompute the chain (over the header’s
log_origin); mismatch withhead.chain(with counts matching and all leaves individually valid) is a chain mismatch. An editedhead.chaintherefore surfaces as classchain, since this runs before the head signature check. - Verify the head signature; failure is head tampering. An unknown or undecodable
sig.keyid/signature on a leaf classifies as content at that leaf (a swapped-in foreign key is tampering, not unverifiability); on the head, as head. - Missing head line entirely is truncation.
Malformed JSON, wrong types, or missing required fields anywhere in the file — not just the
header — make the export unreadable: exit 2. A duplicated top-level payload/head key
inside a line (span-smuggling) is likewise exit 2.
Duplicate receipt_id inside payload is classified as duplicate, never as tampering —
report it, exit 0 if everything else verifies.
Exit codes (stable, documented, load-bearing for CI)
Section titled “Exit codes (stable, documented, load-bearing for CI)”| code | meaning |
|---|---|
| 0 | verified: all receipts intact, chain and head verify |
| 1 | tampering detected (any class above: content, drop, reorder, chain, truncation, head) |
| 2 | unverifiable: not a readable export (bad args, missing file, malformed header/JSON) |
3. Output formats
Section titled “3. Output formats”Human output on success: ✔ 47/47 receipts intact chain head <first4>…<last4> (e.g.
4f0c…a19e); on tampering: the class, the index, and the unverifiable range.
The verifier also emits a machine-readable line per failure on stderr:
class=<content|drop|reorder|chain|truncation|head|duplicate> index=<N>, with these index
conventions: content/sig failures → the tampered leaf’s index; drop → the first missing
index; reorder → the first position where ascending order breaks; truncation → the first
missing index; chain/head/missing-head → -1 (no leaf index).