Whisper · Docs
Trust & cryptography

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 (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 transparency log, served as C2SP tlog-tiles with C2SP 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: origin, tree size, base64 root hash, and an Ed25519 signature line naming the key that made it. No key required to fetch it, ever:

whisper.online/ledger
134
fWOenkL0m3rQx9s2VbC7pXk4hT1uJ8dNzYqR6oAsE2c=

— whisper-ledger-log+8a3a5df0 Ay7Jf3Kd0aBnQe2Ls9V…

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 public key and key ID (8a3a5df0, ce79119a…) are themselves published and DNSSEC-anchored (_whisper-ledger.whisper.online TXT), so you can pin it 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 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. Today the ledger is tamper-evident and Ed25519-signed, with checkpoints anchored to Bitcoin. It is not yet independently witnessed — ns1 and ns2 co-signing each other is an availability check, not independence, since both are Whisper's own infrastructure. We speak the open C2SP tlog-witness protocol so any Sigsum/transparency.dev-style witness (a $5 VPS is plenty) can co-sign our checkpoints for free; the day one does, this page updates to "publicly verifiable." Full policy: nic.whisper.online/policy#transparency.

Try it: fetch and verify a checkpoint by hand

With stock toolscurl, openssl, jq, python3. No Whisper software anywhere in this path:

# 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
curl -s https://whisper.online/checkpoint/key
# {"key_id":"8a3a5df0","algorithm":"ed25519","public_key":"ce79119a…"}

# 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. 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":134,"checkpoint":"…",
#  "leaves":[{"index":57,"leaf_hash":"b21f…","inclusion_proof":["9ac3…","5e10…"]}]}

# 5. 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
proof = [bytes.fromhex(h) for h in ["9ac3...", "5e10..."]]
h, idx, size = leaf, 57, 134
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.

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

# 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, internal/cli/ledger.go.

On the write side, you 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.

Next

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