Whisper · Docs
Standards

SCITT receipts (RFC 9943 / RFC 9942)

Every leaf in Whisper's transparency log doubles as a SCITT receipt: a COSE-signed inclusion proof you validate with stock CBOR and COSE tooling, with no Whisper code in the trust path.

SCITT, the IETF's Supply Chain Integrity, Transparency and Trust architecture, standardizes how a transparency service accepts signed statements and hands back receipts that prove registration. It shipped as RFCs in 2026: RFC 9943 defines the architecture, RFC 9942 defines the COSE receipt format. Only the HTTP API is still a draft (draft-ietf-scitt-scrapi).

Whisper supports SCITT: the transparency log behind /checkpoint emits real RFC 9942 receipts, keyless, for every leaf it holds. Not a parallel system, not a second tree; the same RFC 6962 Merkle tree wearing a second, standard envelope.

Why a second envelope

The log already speaks C2SP: checkpoint notes, tiles, consistency proofs, the dialect of Certificate Transparency and Sigsum. SCITT is the dialect of supply-chain and audit tooling: a self-contained COSE object a relying party verifies offline with any COSE library, then files next to an SBOM or an attestation bundle.

Same math, same root, same key. If your tooling folds RFC 6962 proofs, use /checkpoint; if it verifies COSE, use /entries. Both resolve to the identical 32 bytes.

The three shapes on the wire

Everything here is a COSE_Sign1 (CBOR tag 18): [protected, unprotected, payload, signature].

Signed Statement (you, to the log). Your artifact, or its hash, as the payload, signed by your key. The protected header carries alg (1), optionally content_type (3), and kid (4) unless an x5chain (33) or x5t (34) names the key. The one strictly mandatory field is CWT_Claims (15): a map with iss (1) and sub (2), who is saying it and what it is about (RFC 9943 §6).

Receipt (the log, to anyone). The transparency service's counter-signature proving a leaf is in the tree:

protected   (deterministic CBOR map):
   1  (alg)        => -8                            ; EdDSA (RFC 9053)
 395  (vds)        => 1                             ; RFC9162_SHA256 (RFC 9942 §5.1)
  15  (CWT_Claims) => { 1: "whisper.online/ledger", ; iss = the log identity
                        2: <subject> }              ; sub = the attested subject
unprotected (map):
 396  (vdp)        => { -1: [ bstr .cbor [tree_size, leaf_index, [+ bstr path]] ] }
payload:    nil                                     ; detached: the verifier recomputes the root
signature:  EdDSA over Sig_structure("Signature1", protected, h'', recomputed_root)

The payload is detached on purpose: the bytes the signature covers are the Merkle root you fold out of the inclusion proof. A receipt cannot vouch for a root you did not derive yourself.

Transparent Statement (statement plus receipt, one file). The registered Signed Statement with the Receipt slotted into its unprotected header at receipts (394): { 394: [ bstr .cbor Receipt ] }. The protected bytes and your signature stay byte-untouched, so both signatures keep verifying (RFC 9943 §7). When the node holds your registered statement bytes, GET /entries/{id} serves this shape; otherwise it serves the bare Receipt.

One tree, two envelopes

The receipt is signed by the same Ed25519 key that signs the C2SP /checkpoint note, and the kid in the published COSE_Key is the same four bytes that name the checkpoint's signature line. Fold a receipt's inclusion proof and you get, byte for byte, the base64-decoded root on line 3 of /checkpoint at the same tree size. The C2SP note and the COSE receipt are two signed envelopes over the identical (root, tree_size): nothing new to trust, and no way for the two surfaces to quietly disagree.

The SCITT entry (the bytes a stock RFC 9162 verifier folds) is the leaf's 32-byte opaque commitment, served in the X-Whisper-Scitt-Entry response header. leaf_hash = SHA-256(0x00 ‖ entry) is exactly the ledger's leaf rule, so a stock verifier folds it with zero Whisper knowledge, and the receipt stays verifiable even after a GDPR Art. 17 crypto-shred: the erasable salt is what gets destroyed, never the commitment.

The endpoints

Endpoint Auth Returns
GET /entries/{id} keyless the RFC 9942 Receipt (or Transparent Statement), application/cose. {id} is the decimal leaf index; a 64-hex statement digest or leaf hash also resolves
GET /.well-known/scitt-keys keyless the log key as a COSE Key Set, application/cbor
GET /.well-known/scitt-keys/{kid} keyless one COSE_Key
POST /entries API key register a Signed Statement: 202 Accepted + Location: /entries/{id}

Errors are RFC 9290 concise problem details (application/concise-problem-details+cbor), never an opaque 500. A 204 on GET means the entry is registered but not yet provable against a served checkpoint; retry shortly.

Validate a receipt with stock tooling

curl plus cbor2 plus cryptography (or pycose); no Whisper code anywhere:

pip install cbor2 cryptography

curl -sD hdr.txt -H 'Accept: application/cose' \
     "https://whisper.online/entries/166" -o receipt.cose
grep -i '^x-whisper-scitt-entry:' hdr.txt | tr -d '\r' | awk '{print $2}' > entry.hex
curl -s https://whisper.online/.well-known/scitt-keys -o scitt-keys.cbor
curl -s https://whisper.online/checkpoint > checkpoint.txt
import base64, cbor2, hashlib
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey

# 1. The receipt is a COSE_Sign1 (CBOR tag 18): [protected, unprotected, payload, signature]
tagged = cbor2.loads(open("receipt.cose", "rb").read())
protected_bstr, unprotected, payload, sig = tagged.value
phdr = cbor2.loads(protected_bstr)
assert phdr[1] == -8, "alg must be EdDSA (RFC 9053)"
assert phdr[395] == 1, "vds must be RFC9162_SHA256 (RFC 9942)"
assert payload is None, "payload is detached: the verifier recomputes the signed root"

# 2. The inclusion proof rides vdp(396){-1}: [tree_size, leaf_index, path]
tree_size, leaf_index, path = cbor2.loads(unprotected[396][-1][0])

# 3. Leaf hash from the entry (the X-Whisper-Scitt-Entry header)
entry = bytes.fromhex(open("entry.hex").read().strip())
node = hashlib.sha256(b"\x00" + entry).digest()

# 4. Fold the path to the root (RFC 9162 §2.1.3.2)
fn, sn = leaf_index, tree_size - 1
for sib in path:
    if fn & 1 or fn == sn:
        node = hashlib.sha256(b"\x01" + sib + node).digest()
        if not fn & 1:
            while fn & 1 == 0 and fn != 0:
                fn >>= 1; sn >>= 1
    else:
        node = hashlib.sha256(b"\x01" + node + sib).digest()
    fn >>= 1; sn >>= 1
root = node

# 5. Verify the EdDSA signature under the published COSE_Key (kty OKP, crv Ed25519)
keys = cbor2.loads(open("scitt-keys.cbor", "rb").read())
key = next(k for k in keys if k[1] == 1 and k[-1] == 6)
sig_structure = cbor2.dumps(["Signature1", protected_bstr, b"", root])
Ed25519PublicKey.from_public_bytes(key[-2]).verify(sig, sig_structure)
print("receipt signature: OK (log-signed inclusion)")

# 6. Byte-equality with the C2SP checkpoint root at the same tree size
lines = open("checkpoint.txt").read().splitlines()
assert int(lines[1]) == tree_size, "checkpoint moved on; re-pull the receipt"
assert base64.b64decode(lines[2]) == root
print("one tree, two envelopes: root ==", lines[2], "at size", tree_size)
receipt signature: OK (log-signed inclusion)
one tree, two envelopes: root == W1RW56xR1zcO4y3LHiK2EimIaDtxYRFU00FxQN+jOAc= at size 221

Step 4 is the same fold as the Certificate Transparency walkthrough; step 6 is the same root as the checkpoint walkthrough. That is the whole point.

Register your own Signed Statement

Reading and verifying is keyless, always. Writing into the shared tree takes an API key (the anti-spam gate on the single tree):

# a COSE_Sign1 Signed Statement, signed by YOUR key, with CWT_Claims{iss,sub} set
curl -s -X POST "https://whisper.online/entries" \
     -H "X-API-Key: whisper_live_xxx" \
     -H "Content-Type: application/cose" \
     --data-binary @statement.cose -D -
# HTTP/1.1 202 Accepted
# Location: /entries/<id>

The exact bytes you sent are what gets committed (a COSE envelope is byte-sensitive; re-encoding would break your signature), and the async sequencer assigns the leaf. GET the Location until the 204 becomes a 200; the body is then your Transparent Statement, your envelope plus our receipt. Statements are capped at 64 KiB: the tree is a log, not a blob store, so register the hash of a large artifact rather than the artifact itself.

The honest gate

A receipt asserts log-signed inclusion against the checkpoint root: this leaf is in the tree whose root the log key signed. That claim is gated, in code, on a genuinely independent witness cosigning the served checkpoint. With MarkovianProtocol now cosigning, the claim header on every receipt and checkpoint reads publicly verifiable / split-view-resistant, so a receipt inherits non-equivocation while that cosignature is fresh (self-revoking; the live header is the source of truth). Same gate, same wording, as the transparency log page. The receipts themselves did not change.


Next: Certificate Transparency for the Merkle math the receipt folds, Transparency log for the checkpoint chain it anchors to, or OpenTimestamps for the Bitcoin anchor above them both.