# Transparency log

**Nothing issued in the dark.**

Every agent identity Whisper mints - and every revocation - lands in a public, append-only Merkle log the moment it happens. If we ever tried to show two different histories to two different people, the math catches it and says so.

## The problem this solves

A certificate authority, a registry, or an identity provider is, by default, a black box: it tells you an identity is valid and you take its word for it.

That word can be wrong by accident (a mis-issuance) or on purpose - a compromised operator quietly minting a second, conflicting identity for the same address, or showing an auditor a clean history while showing everyone else a different one (the "split-view" attack).

The web PKI learned this the hard way. After the 2011 DigiNotar breach, browsers forced every public CA onto [Certificate Transparency](https://certificate.transparency.dev/) (RFC 6962): a public log every issued certificate must appear in, so mis-issuance is *detectable*, not just *forbidden by policy*.

Agent identity has the same failure mode and deserves the same fix. A `/128` and its DANE pin are exactly the kind of high-value binding you don't want to take on trust - so we don't ask you to.

## The mechanism: an RFC 6962 Merkle tree

The ledger is a standard [RFC 6962](https://www.rfc-editor.org/rfc/rfc6962) transparency log, served as [C2SP `tlog-tiles`](https://c2sp.org/tlog-tiles) with [C2SP signed-note](https://c2sp.org/signed-note) checkpoints - the same log shape as Certificate Transparency and Sigsum, not a bespoke format. Two hash rules, domain-separated by a leading byte so a leaf can never be mistaken for an interior node:

```
leaf     = SHA-256(0x00 ‖ commitment)
interior = SHA-256(0x01 ‖ left ‖ right)
```

Adding a leaf changes the root; any leaf can prove its own membership with a short **inclusion proof** (the sibling hashes on the path to the root), and any two tree sizes can prove one is a strict extension of the other with a **consistency proof** - the log can only grow, never rewrite.

**What's actually in a leaf.** Not the `/128`, not the FQDN, not the owner - a bare, opaque commitment: `commitment = SHA-256(salt ‖ canonical-event)`, where `salt` is a fresh 256-bit secret minted per event and the event is a deterministic encoding of "this `/128`+name was issued to this owner at time T" (or revoked, or rotated). The tree itself is unreadable - that's deliberate. It reconciles two things that normally fight: public verifiability and GDPR Art. 17 erasure. The `(salt, event)` pair lives outside the append-only tree, in a store that *can* delete. Verifying what a commitment means requires the subject (or an auditor they've authorized) to disclose that pair; you then recompute the hash and check it folds to the signed root - **selective disclosure, not global readability.** Erase the salt and the leaf's meaning becomes unrecoverable forever, while every prior inclusion proof stays mathematically valid - the log doesn't have to lie about its own history to honor a right to be forgotten.

## Signed checkpoints

The current state of the tree is published as a **signed checkpoint** - a plain-text [C2SP note](https://c2sp.org/signed-note): line one is the log origin, line two the tree size, line three the base64 root hash, then one Ed25519 signature line per cosigner (the log itself, and each witness). No key required to fetch it, ever. A copy pinned in a doc goes stale the moment the tree grows, so fetch the live one and the key it is signed under rather than trust a printed sample:

```
# the current signed checkpoint: origin, size, root, then the signature lines
curl -s https://whisper.online/checkpoint

# the dedicated log key that signs it
curl -s https://whisper.online/checkpoint/key
# {"object":"whisper-ledger-key","origin":"whisper.online/ledger/g2","alg":"Ed25519",
#  "key_id":"d40e573a","public_key":"7pSYYInKXm2TMjwAqEsoKon/d3E8SDq34V1VUrgTAm0=",
#  "public_key_spki":"MCowBQYDK2VwAyEA7pSYYInKXm2TMjwAqEsoKon/d3E8SDq34V1VUrgTAm0="}
```

The signing key is **dedicated** - it signs ledger checkpoints and nothing else (not RDAP, not the identity document), so a compromise or rotation of one never touches the others. The log runs on its second genesis today, origin `whisper.online/ledger/g2`, signed by key id `d40e573a`. That key, and the full set the log may sign under, are published and DNSSEC-anchored as `_whisper-ledger.whisper.online` TXT records, so you pin the set from signed DNS once and verify forever without re-trusting a web fetch each time.

## Anchored to Bitcoin, honestly

A signature proves *Whisper* attests to a root; it doesn't prove *when*, and it doesn't stop us from moving our own clock. So every signed checkpoint's 32-byte root is submitted to public [OpenTimestamps](https://opentimestamps.org/) calendars, which batch many commitments into one aggregate and periodically timestamp that aggregate into a real Bitcoin transaction. Once the transaction is buried a few blocks deep, the proof upgrades from a *calendar promise* ("pending") to a *Bitcoin fact* ("confirmed") - anyone can verify the upgraded proof offline against raw Bitcoin block headers, with no OpenTimestamps service and no trust in Whisper's clock. Only the 32-byte root ever touches the chain - no event count, no identity data, nothing to link.

> **Honest status.** The ledger is **tamper-evident and Ed25519-signed**, with checkpoints anchored to Bitcoin, and it is **independently witnessed by MarkovianProtocol** (C2SP `tlog-witness`): a genuinely separate party, not an availability check between our own boxes. The served claim reaches *publicly verifiable / split-view-resistant* once a quorum of at least two genuinely independent witnesses is cosigning the served checkpoint (gated in code, self-reverting); MarkovianProtocol is one such witness today, and the live `X-Whisper-Ledger-Claim` header is the source of truth for the current level. We speak the open [C2SP `tlog-witness`](https://c2sp.org/tlog-witness) protocol, so any Sigsum/transparency.dev-style witness (a $5 VPS is plenty) can co-sign our checkpoints for free and bring the quorum. Full policy: [nic.whisper.online/policy#transparency](https://nic.whisper.online/policy#transparency).

## The freshest confirmed anchor

The newest checkpoint's OpenTimestamps proof sits *calendar-pending* for a few hours before it upgrades to a Bitcoin confirmation, so the freshest root you can anchor a continuity check against is the newest **confirmed** one, which lags the head. `GET /checkpoint/ots/latest-confirmed` returns exactly that, keyless:

```bash
curl -s https://whisper.online/checkpoint/ots/latest-confirmed
#   {
#     "object": "whisper-ledger-ots-latest-confirmed",
#     "tree_size": 18114,
#     "root": "dCibwNKApBOjN/HdbzX3XpCM7XOXl5R/3fHCmxz5XwA=",
#     "bitcoin_block": 959234,
#     "stamped_at_millis": 1784794550778,
#     "confirmed_at_millis": 1784797492799,
#     "ots": "/ots/18114",
#     "consistency": "/consistency?from=18114"
#   }
```

Then `GET /consistency?from=18114` from that confirmed size up to the current head proves, with no trust in our clock, that the live tree is an append-only extension of a root already buried in Bitcoin block 959234. Response headers carry the same facts (`X-Whisper-OTS-Status: confirmed`, `X-Whisper-OTS-Bitcoin-Block`, `X-Whisper-Ledger-Tree-Size`); an empty store answers a clean `404`, never a `500`.

## Try it: fetch and verify a checkpoint by hand

**With stock tools** - `curl`, `openssl`, `jq`, `python3`. No Whisper software anywhere in this path:

```bash
# 1. the signed checkpoint (a C2SP note: origin / size / root / signature)
curl -s https://whisper.online/checkpoint

# 2. the log's public key + key id, to verify against - pin it from signed DNS,
#    which is DNSSEC-anchored, so you trust the chain and not this fetch
dig +short TXT _whisper-ledger.whisper.online
# "v=whisper1; k=ed25519; n=whisper.online/ledger/g2; p=MCowBQYDK2VwAyEA7pSYYInKXm2TMjwAqEsoKon/d3E8SDq34V1VUrgTAm0=" …
curl -s https://whisper.online/checkpoint/key
# {"origin":"whisper.online/ledger/g2","alg":"Ed25519","key_id":"d40e573a",
#  "public_key":"7pSYYInKXm2TMjwAqEsoKon/d3E8SDq34V1VUrgTAm0=", …}

# 3. verify the Ed25519 signature (OpenSSL ≥3.0 verifies Ed25519 with -rawin)
openssl pkeyutl -verify -rawin -in checkpoint.body \
  -pubin -inkey ledger.pub -sigfile checkpoint.sig

# 4. any leaf's inclusion proof, by index: leaf_hash + sibling path + the signed
#    checkpoint it folds to, one keyless call (a bad leaf is a clear 400, never a 500)
curl -s "https://whisper.online/inclusion?leaf=0"
# {"object":"ledger-inclusion","leaf":0,"tree_size":429,
#  "leaf_hash":"9e47e79ecfecbce8babb888fc7f7e3481b45fb5b1d5c4492671e69a0042e119a",
#  "proof":["82f522c46fcaad3f3aa070b468be09dbf1b532e98aa1615b5b1141bf81ca281f",
#           "b19e49843367422013a66b799da22f495683d6c9a78e7087e5a5119071dc9647", …],
#  "checkpoint":"…"}

# 5. one agent's inclusion proof - no key needed, RFC 9083 RDAP arm
curl -s https://rdap.whisper.online/ip/2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4/transparency | jq .ledger
# {"tree_size":39858,"checkpoint":"…",
#  "leaves":[{"index":57,"leaf_hash":"b21f…","inclusion_proof":["9ac3…","5e10…"]}]}

# 6. fold the proof to the signed root yourself
python3 - <<'PY'
import hashlib
def node(l, r): return hashlib.sha256(b"\x01" + l + r).digest()
leaf = bytes.fromhex("b21f...")          # from step 4 or 5
proof = [bytes.fromhex(h) for h in ["9ac3...", "5e10..."]]
h, idx, size = leaf, 57, 39858        # size = the tree_size from your own fetch
for sib in proof:
    h = node(sib, h) if idx % 2 else node(h, sib)
    idx //= 2
assert h.hex() == "<root from step 1>"    # matches the signed checkpoint
PY
```

The Merkle-fold logic is public domain math (RFC 6962 §2.1.1) - nothing here needs a client library. The same checkpoint's witness cosignature is verifiable independently at [witness.markovianprotocol.com/checkpoints](https://witness.markovianprotocol.com/checkpoints) - a separate party vouching for the very root you just folded to.

**With Whisper** - one command per step, same math, done for you:

```bash
# fetch + verify the latest checkpoint under the published key
whisper ledger checkpoint

# prove a disclosed (salt, event) is included in the signed tree
whisper ledger verify 2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4 \
  --salt 9f1e4c2a... --event-file event.bin

# the full trustless verdict - DNSSEC root → DANE pin → this ledger → identity
# document, chained, with Whisper's API trusted for nothing:
whisper verify --trustless 2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4
```

`whisper ledger checkpoint` fetches the checkpoint and key, verifies the Ed25519 signature, and prints `VERIFIED`. `whisper ledger verify` additionally fetches the inclusion proof and folds it to the root for you - exit code 0 means cryptographically included, exit 1 means it is not. Full source for both: [github.com/whisper-sec/whisper-cli](https://github.com/whisper-sec/whisper-cli), `internal/cli/ledger.go`.

On the write side, identity events never call the ledger directly - every `whisper.agents({op:'register'|'identity'|'revoke'})` call appends the corresponding event automatically as part of issuing or tearing down a `/128`. The log is a side effect of using the control plane honestly, not a separate thing you have to remember to do. The tree does have two deliberate, API-key-gated write doors for your own material: [`POST /entries`](/docs/register-a-statement) registers a COSE Signed Statement you signed, and [`POST /attest`](/docs/compliance#5-attest-a-control-in-force-typed-compliance-attestations) commits a typed compliance attestation. Both land as the same opaque, keyless-verifiable leaves as everything else.

## SCITT receipts (RFC 9943 / RFC 9942)

This same log **supports SCITT**: every leaf can be pulled, keyless, as an [RFC 9942](https://www.rfc-editor.org/rfc/rfc9942) COSE receipt under the [RFC 9943](https://www.rfc-editor.org/rfc/rfc9943) architecture (`GET /entries/{leaf}`, key discovery at `/.well-known/scitt-keys`), signed by the same Ed25519 key that signs the checkpoint above. Fold the receipt's inclusion proof and it byte-equals the checkpoint root at the same tree size: one tree, two envelopes, verifiable with stock COSE tooling and no Whisper code. The receipt inherits this page's honest gate, log-signed inclusion, and now independent witnessing. Full walkthrough: [SCITT receipts](/docs/scitt-receipts).

## Point-in-time RDAP, anchored in this same log

The same tree also holds a second leaf kind: not identity events, an **observation**, *"we saw subject X have operator-of-record Y at time T."* A keyless "operator of record as of T" endpoint returns an RFC 9083 object plus a SCITT receipt for that observation, so the same fold that proves an identity leaf or a receipt is in this tree proves an RDAP observation is too: one tree, three surfaces. The `?at=` query parameter is Whisper's own extension, not a standardized RDAP param, and the honest ceiling is an observation of what we saw, never a claim about the registry's own ground truth. Full walkthrough: [Point-in-time RDAP](/docs/point-in-time-rdap).

## Next

[DANE & DNSSEC](/docs/dane) covers the cryptographic pin the ledger's inclusion proofs sit alongside; [Compliance & audit](/docs/compliance) covers how crypto-shredding a ledger entry satisfies GDPR Art. 17 without breaking any prior proof.
