# 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](https://www.rfc-editor.org/rfc/rfc6962) (Laurie, Langley, Kasper, 2013) and now mandatory for every publicly-trusted TLS certificate, isn't "trust us more" — it's "make tampering mathematically visible."

Whisper runs the same construction for agent identity: every issuance and revocation is a leaf in a Merkle tree, the 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, never got rewritten." This page goes to the bottom of how that works, byte by byte, and verifies 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](https://github.com/C2SP/C2SP/blob/main/tlog-checkpoint.md) — 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:

```bash
# 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:

```bash
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](https://github.com/C2SP/C2SP/blob/main/tlog-tiles.md) — 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:

```bash
curl -s https://rdap.whisper.online/ip/2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4/transparency
```

```json
{
  "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):

```python
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

```bash
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:

```bash
# stock tools
curl -s "https://whisper.online/consistency?old=1400000&new=1481297"
```

```json
{ "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](/docs/opentimestamps) for how that pins a checkpoint to a Bitcoin block height nobody controls. Independent third-party witness co-signing (the model [Sunlight](https://sunlight.dev/) 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](https://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](/docs/transparency) for the product-level guarantee and revocation status list, or [OpenTimestamps](/docs/opentimestamps) for how a checkpoint gets anchored to Bitcoin.
