Evidence bundles.
GAAn evidence bundle is a portable, self-contained file holding a leaf, its inclusion path, the checkpoint it was proven against, and the witness cosignatures — everything needed to verify the record with zero network access. It is the artifact you hand an auditor, a court, or an air-gapped reviewer, and it keeps working when the reviewer has no account, no token, and no connectivity.
Self-contained, or it isn't evidence
A proof that requires calling our API to check is a proof that depends on us being reachable, truthful, and still in business at verification time. Evidence bundles remove that dependency: the file carries the claim and every cryptographic input needed to check it. The verifier brings exactly one thing of their own — the pinned public keys from a trust file — and that is deliberate: keys must come from somewhere the bundle’s author cannot touch, or the bundle would be vouching for itself.
Together, the checkpoint signature, the witness cosignatures, and the inclusion path make the bundle tamper-evident: any modification to any byte of the leaf, the path, or the checkpoint makes verification fail, deterministically.
The bundle JSON
{
"tillnotary_evidence": 1,
"origin": "notary.tilldev.dev/acme-releases",
"suite": "sig-ed25519-mldsa65-sha256.v1",
"leaf": {
"index": 5,
"value_hex": "c53d57898e94df8d3fd05e41456bb17357992da9c36bab87edda8fe75f9acafb",
"bytes_b64": null, // the attested bytes, for --attest submissions
"content_class": "hash-only" // or "public-attestation"
},
"inclusion": {
"tree_size": 8,
"path_hex": [ // sibling hashes, leaf → root
"b153ffc56f36cbfd7bb208f43603fe704c27a0b9424c0424c94fd99f2cc539a3",
"ed3fcf4624cb77b4775ec5717351c2907a92635984536725bb9056e89b0e2394",
"a1d55691a4764f1c797f8849de205858215b2113e6f0dca650bc5c85f428a743"
]
},
"checkpoint": {
"tree_size": 8,
"root_hash": "96b66e36e50a3a61fc37f08bf11e12d2b6ea922068183a8d24e1ae613b98402d",
"timestamp": "2026-09-27T05:44:12.622Z",
"key_name": "tillnotary-log/acme-releases",
"signature_b64": "mBhb35vfRMRqJBGq…"
},
"cosignatures": [
{ "witness": "tillnotary-witness/w1", "observed_at": 1790487852, "signature_b64": "4LNgxgE5WTXeLCRK…" },
{ "witness": "acme-witness/primary", "observed_at": 1790487853, "signature_b64": "vv0siERecad3wp+q…" }
]
}tillnotary_evidence: 1— format version; verifiers refuse versions they do not understand rather than guess.leaf.value_hex— the hash that was logged.leaf.bytes_b64carries the raw content only when it was attested at submit time (capped at 4096 bytes) and isnullotherwise;content_classishash-onlyorpublic-attestationaccordingly.inclusion.path_hex— the Merkle sibling hashes from the leaf up to the root, for the tree ofinclusion.tree_size.checkpoint+cosignatures— the signed head the path resolves to, and the cosignatures from the witnesses you pinned, which make it trustable.
Getting a bundle out
Export from the CLI with proof … --out, or capture one at submit time:
# CLI — verify leaf 5 and export its bundle
$ tilldev notary proof acme-releases 5 --out bundle.json
OK VERIFIED — leaf 5 of notary.tilldev.dev/acme-releases
leaf c53d57898e94df8d3fd05e41456bb17357992da9c36bab87edda8fe75f9acafb
content class hash-only
origin notary.tilldev.dev/acme-releases
tree size 8
root 96b66e36e50a3a61fc37f08bf11e12d2b6ea922068183a8d24e1ae613b98402d
timestamp 2026-09-27T05:44:12.622Z
cosignatures tillnotary-witness/w1, acme-witness/primary
status COMPLETE (witness-cosigned)
inclusion path (3 nodes, leaf→root):
b153ffc56f36cbfd7bb208f43603fe704c27a0b9424c0424c94fd99f2cc539a3
ed3fcf4624cb77b4775ec5717351c2907a92635984536725bb9056e89b0e2394
a1d55691a4764f1c797f8849de205858215b2113e6f0dca650bc5c85f428a743
OK Evidence bundle written to bundle.json
# a submit writes its receipt as a bundle in the same step
$ tilldev notary submit acme-releases --file dist/app-v2.4.1.tar.gz --out receipt.jsonOr from the Node SDK:
// SDK — same artifact, programmatically
const bundle = await notary.exportEvidence('acme-releases', 5)
await fs.writeFile('bundle.json', JSON.stringify(bundle))Both paths verify the bundle themselves before writing it. Until witnesses have cosigned a head that includes the leaf, the SDK refuses to export, and the CLI still writes the file but warns that it will not verify offline yet; re-run proof after the witnesses’ next pass.
Offline verification: what is checked, what is refused
# no network, no token, no server — only the bundle and your pinned trust file
$ tilldev notary verify bundle.json
OK VERIFIED (offline) — leaf 5 of notary.tilldev.dev/acme-releases
leaf c53d57898e94df8d3fd05e41456bb17357992da9c36bab87edda8fe75f9acafb
origin notary.tilldev.dev/acme-releases
tree size 8
root 96b66e36e50a3a61fc37f08bf11e12d2b6ea922068183a8d24e1ae613b98402d
timestamp 2026-09-27T05:44:12.622Z
cosignatures tillnotary-witness/w1, acme-witness/primary
status COMPLETE (witness-cosigned)import { verifyEvidence } from '@tillstack/sdk-notary-node'
// standalone function — needs no client, no baseUrl, no token
const checkpoint = verifyEvidence(bundle, trust) // throws NotaryVerificationError on any failureVerification recomputes everything from first principles:
- the bundle’s
originmatches a log pinned in your trust store; - hashing the leaf and folding in
inclusion.path_hexreproduces exactlycheckpoint.root_hashatcheckpoint.tree_size; - the checkpoint signature verifies under the pinned log key — both halves of the hybrid signature;
- enough cosignatures verify under pinned witness keys to meet your configured threshold;
- if
bytes_b64is present, the bytes hash tovalue_hexunder the log’s suite.
| Refused | Why |
|---|---|
| Wrong origin | The bundle names a log that is not in your trust store — an unpinned log cannot be verified at all, only trusted blindly, and the tooling does not do blind. |
| Tampered leaf or root | Any altered byte breaks the hash chain from leaf to root — the recomputed root will not match the signed one. |
| Cosignatures below your threshold | The log alone vouching for itself is not sufficient; your required_cosignatures floor is enforced client-side. |
| Bytes that don’t hash to the logged leaf | Attested content must reproduce value_hex exactly — substituted content is detected, not accepted. |
Keeping a bundle valid for the long term
A bundle is anchored to the checkpoint inside it, so it never expires and never needs the log to still be online. Two habits make decade-scale evidence stronger:
- Keep the bundle and its checkpoint together, in your own archive. The checkpoint is your comparison point against the log’s public history — and, if it ever came to it, one half of a non-repudiable equivocation proof.
- Re-export later to pick up additional cosignatures. Witness sets grow over time; re-running
tilldev notary proofagainst the same leaf yields a bundle carrying every cosignature available today. The old bundle stays valid — the new one is simply harder to argue with.
The bundle’s anatomy leans on Checkpoints & signatures and Witnesses & refusals. To automate export and verification, see the Node SDK.