Certificate Transparency (RFC 6962)
Every identity Whisper issues is a leaf in a public, append-only Merkle tree — so "we revoked it silently" or "we backdated that certificate" become things the math simply won't let us do.
Registries and CAs have a long history of getting quietly rewritten — a record edited, an issuance backdated, a revocation that never shows up in the log anyone actually checks. The fix, worked out for Web PKI in RFC 6962 (Laurie, Langley, Kasper, 2013) and now mandatory for every publicly-trusted TLS certificate, is not "trust us more" — it's "make tampering mathematically visible." Whisper runs the same construction for agent identity: every issuance and every revocation is a leaf in a Merkle tree, the tree's root is signed and published on a schedule, and anyone — no account, no key — can ask "prove this leaf is really in there" and "prove the tree only grew, it never got rewritten." This page goes to the bottom of how that works, byte by byte, and shows you how to verify it with nothing but sha256sum.
The Merkle tree: leaves are hashes, not secrets
A Merkle tree is a binary tree where every leaf is a hash and every interior node is the hash of its two children. RFC 6962 §2.1 fixes the exact preimages, and Whisper uses the same domain-separated construction:
leaf = SHA-256( 0x00 || salt || event )
interior = SHA-256( 0x01 || left_hash || right_hash )
The 0x00/0x01 prefix bytes are the whole trick: they stop a second-preimage attack where an attacker relabels an interior node as a leaf (or vice versa) to forge a proof — a leaf hash and an interior hash can never collide by construction (RFC 6962 §2.1, §2.1's second-preimage resistance note). The salt means the leaf is an opaque commitment: it proves an issuance event happened without revealing what it was to anyone who doesn't already know the salt — which is also how a record can later be crypto-shredded (GDPR Art. 17 — destroy the salt, the leaf becomes an unopenable hash forever) without breaking the tree or any proof already issued against it.
An odd node with no sibling at some level is promoted straight up unchanged (RFC 6962 §2.1) — there's no padding, no fake leaves. The tree is built strictly append-only, left to right; a new issuance is always the new rightmost leaf, so tree_size only ever grows.
The signed checkpoint (RFC 6962's STH, in C2SP form)
RFC 6962 calls the periodic attestation a Signed Tree Head (STH): tree size, root hash, timestamp, signature. Whisper publishes it as a C2SP signed-note checkpoint — same three facts, a text format anyone can eyeball and any witness can co-sign:
whisper.online/ledger
1481297
NcQ2mF3q9c8Jv1lXk0yqzB8mQ3H1yq9m2vQm7z5xY0k=
— whisper.online/ledger Ay3f9k...Ed25519sig...==
Line 1 is the origin (which log), line 2 the tree size (leaf count), line 3 the base64 root hash, and the trailer is an Ed25519 signature over the first three lines. It is keyless and public:
# stock tools — no Whisper code, just curl + your eyes
curl -s https://whisper.online/checkpoint
curl -s https://whisper.online/checkpoint/key # the Ed25519 pubkey + key id, to verify the signature above
The pubkey is also nailed to DNS under DNSSEC, so you don't even have to trust the HTTPS cert to fetch it:
dig +short TXT _whisper-ledger.whisper.online +dnssec # AD=YES: the log key, anchored at the IANA root
The tree itself is served as C2SP tlog-tiles — fixed-size chunks of the tree (GET /tile/<L>/<N> for level L, tile index N) instead of one hash at a time, which is what makes it cheap to serve a tree with over a million leaves without a database round-trip per proof node. Tiles are a transport optimization; the math underneath is still exactly RFC 6962.
Inclusion proof: proving one leaf is in the tree
An inclusion proof (RFC 6962 §2.1.1, the "Merkle audit path") is the minimal set of sibling hashes you need to recompute the root from one leaf. For a tree of size N and a leaf at index i, the path has ⌈log₂ N⌉ entries — for a million-leaf tree, about 20 hashes, each 32 bytes. Fetch one for the demo agent's identity record:
curl -s https://rdap.whisper.online/ip/2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4/transparency
{
"leaf_index": 1481203,
"tree_size": 1481297,
"leaf_hash": "b4f9...c31a",
"audit_path": ["7e2c...", "a901...", "5f3d...", "...18 more"],
"checkpoint_root": "NcQ2mF3q9c8Jv1lXk0yqzB8mQ3H1yq9m2vQm7z5xY0k="
}
Verifying it by hand (no Whisper code at all)
Walking the audit path is nothing but repeated SHA-256(0x01 || left || right), choosing left-vs-right at each level from the bit pattern of the leaf index (RFC 6962 §2.1.1):
import hashlib, base64
def h(prefix, *parts):
m = hashlib.sha256(); m.update(bytes([prefix]))
for p in parts: m.update(p)
return m.digest()
def verify_inclusion(leaf_hash, index, tree_size, audit_path):
# fn tracks our node, sn the rightmost node, at each level. The border
# case (fn == sn, an odd node with no sibling) is promoted straight up.
fn, sn, node = index, tree_size - 1, leaf_hash
for sibling in audit_path:
if fn & 1 or fn == sn:
node = h(0x01, sibling, node) # our subtree is the right child
if not (fn & 1): # rightmost node: climb past promotions
while not (fn & 1) and fn != 0:
fn >>= 1; sn >>= 1
else:
node = h(0x01, node, sibling) # our subtree is the left child
fn >>= 1; sn >>= 1
return node
root = verify_inclusion(
bytes.fromhex("b4f9...c31a"),
1481203, 1481297,
[bytes.fromhex(x) for x in ["7e2c...", "a901...", "5f3d..."]],
)
assert base64.b64encode(root).decode() == "NcQ2mF3q9c8Jv1lXk0yqzB8mQ3H1yq9m2vQm7z5xY0k="
That's the entire trust model for "this identity was really issued": 32-byte hashes and one comparison against a signed checkpoint you fetched yourself over DNSSEC-anchored DNS.
With Whisper
whisper verify --trustless 2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4
# re-derives the audit path, recomputes the root, checks it against the
# live checkpoint AND the DNSSEC-pinned log key — the API is not trusted,
# only arithmetic and the IANA root are
CALL whisper.agents({op:'identity', args:{agent:'my-agent'}})
returns the same leaf_index/audit_path fields as the keyless RDAP call above — the control-plane path and the keyless path prove the identical claim.
Consistency proof: proving the tree only grew
An inclusion proof says "leaf X is in tree of size N." A consistency proof (RFC 6962 §2.1.2) says something stronger over time: "tree of size N₁ is a strict, untouched prefix of tree of size N₂" — nothing before N₁ was edited, reordered, or deleted; growth only ever appended. It's O(log N) hashes, same construction, computed from the shared subtrees between the two sizes:
# stock tools
curl -s "https://whisper.online/consistency?old=1400000&new=1481297"
{ "old_size": 1400000, "new_size": 1481297, "proof": ["3ab1...", "9e04...", "..."] }
Run that between every checkpoint you've ever seen from the log and the day someone shows you a stale one, or splits the tree to show two different histories to two different verifiers (the classic CT "split-view" attack), and the hashes simply won't reconcile. That's the whole guarantee RFC 6962 was written to give the Web PKI, applied here to 2a04:2a01::/32 identities instead of X.509 certs.
How tampering actually breaks
Flip one bit in one historical leaf — say, quietly un-revoke an agent — and the interior hash directly above it changes, which changes its parent, all the way to the root (avalanche effect of SHA-256). The new root will not match any previously-published, Ed25519-signed checkpoint. Anyone holding an old checkpoint (a client, an auditor, a co-signing witness) who runs a consistency proof against the new tree will get a proof that fails to verify — not a soft warning, a hard cryptographic mismatch. There is no way to edit history and keep old proofs valid; that's the property, not an implementation detail.
Honest status: the log is tamper-evident and Ed25519-signed today, with each checkpoint's OpenTimestamps Bitcoin anchor fetchable at GET /checkpoint/ots — see OpenTimestamps for how that pins a checkpoint to a Bitcoin block height nobody controls. Independent third-party witness co-signing (the model Sunlight popularized for CT logs) is being recruited next; until then, treat "signed + Bitcoin-anchored" as the current guarantee and "witnessed" as the one still in progress. Full policy at nic.whisper.online/policy#transparency.
Nothing above needs an API key. Fetch the checkpoint, pull an inclusion proof, verify it with a 20-line Python script and stock curl — that's the point of a transparency log: you don't have to trust the operator's word, only arithmetic.
Next: Transparency log for the product-level guarantee and revocation status list, or OpenTimestamps for how a checkpoint gets anchored to Bitcoin.