# Register a statement: sign your own objects

**Whisper's transparency log is an append-only registry you can write to. Sign a statement about any object you hold (a document, a build artifact, a model, a dataset), register it, and get back a COSE receipt that proves it was logged, verifiable by anyone with stock CBOR and COSE tooling and no Whisper code.**

The log behind [`/checkpoint`](/docs/transparency) is a SCITT Transparency Service ([RFC 9943](https://www.rfc-editor.org/rfc/rfc9943)). This page is the producer walkthrough: sign, register, receipt, verify, in four copy-paste steps. If you want the receipt wire format and the Merkle fold in depth, that reference is [SCITT receipts](/docs/scitt-receipts); this page is the "how do I get MY object in there" recipe.

It is a close cousin of [Sign agent outputs](/docs/sign-outputs), with one addition. That recipe binds a signature to a key you publish in DNS, so a recipient checks it against your name; nothing is logged. Here you also drop the signed statement into a shared, append-only log and get an inclusion proof back, so a third party can later show your statement existed and has not been altered since, without ever contacting you. The two compose: publish the key with one, prove the record with the other.

## Keyless to read, one key to write

Two tiers, Postel style. Reading and verifying any entry is keyless, and always will be. Writing into the shared tree takes an API key, and that is the only thing the key is for here: an anti-spam gate on a single log everyone shares. One tree, one root, one signing key. The receipt you get back is the same envelope [SCITT receipts](/docs/scitt-receipts) folds, and it resolves to the same root as [the checkpoint](/docs/transparency).

What you register is a claim about your object, not the object itself. The tree is a log, not a blob store (statements cap at 64 KiB), so you sign a small statement over your object's hash and leave the object wherever it already lives.

## The shape of a Signed Statement

A Signed Statement is a `COSE_Sign1` (CBOR tag 18): `[protected, unprotected, payload, signature]`, signed by YOUR key. The registration policy is small, standard ([RFC 9943 §6](https://www.rfc-editor.org/rfc/rfc9943)), and it is everything the log checks on the way in:

- the `protected` header carries `alg (1)`, a key identifier `kid (4)` (required unless an `x5chain (33)` or `x5t (34)` names your key by certificate), and, mandatory, `CWT_Claims (15)`: a map with `iss (1)`, who is saying it, and `sub (2)`, what it is about. `CWT_Claims` must sit in the protected header so your signature covers it.
- the `payload` is your object's hash, or a small manifest about it. Sign the hash, not the file.
- the `signature` is over the COSE `Sig_structure`, which is exactly `["Signature1", protected, h'', payload]` ([RFC 9052 §4.4](https://www.rfc-editor.org/rfc/rfc9052#section-4.4)).

The log stores your bytes exactly as received and never re-encodes them, because your signature covers those exact protected-header bytes. It does not verify your signature and does not resolve your key, and that is deliberate: the log proves your statement was registered, while whether the key named in `kid` is genuinely yours is proven separately, by publishing that key at your [DNSSEC-anchored name](/docs/sign-outputs) so any verifier can resolve it. Naming the key is your job; checking it is the relying party's.

## 1. Sign your object

Stock Python, `cbor2` plus `cryptography`, no Whisper code. This uses Ed25519 (`alg -8`), whose signatures are raw 64-byte values, so there is no DER-versus-raw footgun:

```bash
pip install cbor2 cryptography
```

```python
import cbor2, hashlib
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey

# 1. Your object, reduced to its SHA-256. Sign the hash; the file stays where it is.
digest = hashlib.sha256(open("model.safetensors", "rb").read()).digest()

# 2. Your own signing key. Generate once, keep the private half; publish the public
#    half at your name so a verifier can resolve the kid (see /docs/sign-outputs).
sk = Ed25519PrivateKey.generate()

name = "acef2002a323d40d4.demo.agents.whisper.online"
protected = {
    1:  -8,                                  # alg = EdDSA (RFC 9053)
    4:  name.encode(),                       # kid = names your verification key
    15: {1: name, 2: "model.safetensors"},   # CWT_Claims: iss (who), sub (what)
}
protected_bstr = cbor2.dumps(protected)

# 3. Sign the COSE Sig_structure: ["Signature1", protected, external_aad=b"", payload]
sig_structure = cbor2.dumps(["Signature1", protected_bstr, b"", digest])
signature = sk.sign(sig_structure)

# 4. The COSE_Sign1 Signed Statement (tag 18): [protected, unprotected, payload, signature]
statement = cbor2.dumps(cbor2.CBORTag(18, [protected_bstr, {}, digest, signature]))
open("statement.cose", "wb").write(statement)

# The entry id is the SHA-256 of these exact bytes; the log commits to them.
print("entry id:", hashlib.sha256(statement).hexdigest())
```

`model.safetensors` is just an example; substitute a document, a container image digest, an SBOM, or a dataset manifest. `name` is any identifier a verifier can tie back to a key. A Whisper agent already has a [DNSSEC-anchored name](/docs/identity) that resolves to a key, which is what makes it an ideal `iss` and `kid`.

## 2. Register it

`POST /entries` with your API key and the raw COSE bytes:

```bash
curl -sS -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/9f2b7c...e41a          (the entry id: SHA-256 of your statement bytes)
```

`202 Accepted` means the statement is committed and the sequencer is assigning it a Merkle leaf. The `Location` id is the 64-hex SHA-256 of the exact bytes you sent, the same value the sign script printed. Registration is idempotent: re-POST the identical statement and you get `201 Created` with the final leaf index, never a second leaf. Anything malformed comes back as an [RFC 9290](https://www.rfc-editor.org/rfc/rfc9290) problem document that says exactly what is wrong (a missing `CWT_Claims`, an absent `kid`, a body over 64 KiB), never an opaque 500.

## 3. Get your receipt

`GET` the id from the `Location`. This is keyless, so anyone you hand the id to can do it too:

```bash
curl -sD headers.txt "https://whisper.online/entries/9f2b7c...e41a" -o receipt.cose
grep -i '^x-whisper-scitt-entry:' headers.txt | tr -d '\r' | awk '{print $2}' > entry.hex
```

While the leaf is being sequenced you get `204 No Content` with a `Retry-After`; poll until `200`. The `200` body is `application/cose`. Because the server holds the bytes you registered, it serves the full **Transparent Statement** ([RFC 9943 §7](https://www.rfc-editor.org/rfc/rfc9943)): your original envelope, byte for byte, with the log's Receipt appended to its unprotected `receipts (394)` header. Both signatures, yours over the object and the log's over the tree, keep verifying on that one file. If the server does not hold your bytes it serves the bare Receipt instead; either way the proof is identical.

The response headers carry the pieces a verifier folds:

| Header | Meaning |
|---|---|
| `X-Whisper-Scitt-Entry-Id` | the decimal Merkle leaf index |
| `X-Whisper-Scitt-Entry` | the 32-byte opaque commitment, the SCITT "entry": `leaf_hash = SHA-256(0x00 ‖ entry)` |
| `X-Whisper-Ledger-Claim` | the honest claim level of the served checkpoint (see the last section) |

## 4. Verify it yourself, offline

Two independent checks fall out of that one file. The Receipt proves **inclusion** (the log logged it); your own signature proves **authorship** (your key made it). Both use stock `cbor2` plus `cryptography`; the inclusion fold is identical to the one on [SCITT receipts](/docs/scitt-receipts).

**Inclusion, against the log's key and the checkpoint root:**

```bash
curl -s https://whisper.online/.well-known/scitt-keys -o scitt-keys.cbor
curl -s https://whisper.online/checkpoint > checkpoint.txt
```

```python
import base64, cbor2, hashlib
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey

ts = cbor2.loads(open("receipt.cose", "rb").read())        # your Transparent Statement (tag 18)
s_protected, s_unprot, s_payload, s_sig = ts.value
receipt = cbor2.loads(s_unprot[394][0])                    # the log's Receipt, out of receipts(394)
r_protected, r_unprot, r_payload, r_sig = receipt.value
rhdr = cbor2.loads(r_protected)
assert rhdr[1] == -8 and rhdr[395] == 1, "EdDSA + RFC9162_SHA256 receipt"
assert r_payload is None, "detached: the verifier recomputes the signed root"

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

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

# fold the path to the root (RFC 9162 section 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

# the log's EdDSA signature under the published COSE_Key (kty OKP, crv Ed25519)
keys = cbor2.loads(open("scitt-keys.cbor", "rb").read())
k = next(x for x in keys if x[1] == 1 and x[-1] == 6)
Ed25519PublicKey.from_public_bytes(k[-2]).verify(
    r_sig, cbor2.dumps(["Signature1", r_protected, b"", root]))
print("inclusion: OK (log-signed against the checkpoint root)")

# and the root byte-matches the C2SP checkpoint at this tree size
lines = open("checkpoint.txt").read().splitlines()
assert int(lines[1]) == tree_size and base64.b64decode(lines[2]) == root
print("one tree, one root:", lines[2], "at size", tree_size)
```

**Authorship, against the key you named.** The receipt says nothing about who made the statement; that is what your signature is for. A relying party resolves the public key from the `kid` and `iss` you set (published at your name, see [Sign agent outputs](/docs/sign-outputs)), then:

```python
# resolve agent_pub from the kid/iss name in the wild; here it is the key from step 1
Ed25519PublicKey.from_public_bytes(agent_pub).verify(
    s_sig, cbor2.dumps(["Signature1", s_protected, b"", s_payload]))
assert s_payload == hashlib.sha256(open("model.safetensors", "rb").read()).digest()
print("authorship: OK (your key signed this exact object hash)")
```

Three facts now stand on their own, no Whisper account in the trust path: the statement is **in the log** (inclusion), **you made it** (authorship), and the object is the **exact bytes** the statement names (the hash compare). Any one failing means reject.

## What a receipt proves, and what it does not

> A receipt asserts one precise thing: **log-signed inclusion**. Your exact signed statement is a leaf in the tree whose root the log's key signed, and, while the `X-Whisper-Ledger-Claim` header reads `publicly verifiable / split-view-resistant`, an independent witness is cosigning that checkpoint, so the log cannot show you one history and someone else another. That claim level is a property of the artifact you fetched and self-revokes if the witness goes away, so a verifier never takes our word for it; it reads the live header.

What a receipt does **not** do: it does not certify that your object's contents are true, correct, complete, or lawful. The log never reads your object, and it does not vouch for your key or your claim. Authorship is what your own signature proves; the truth of the claim is between you and your relying party. A receipt is evidence that a specific signed statement existed and has not been altered since it was logged, no more and no less. It also records registration order, not wall-clock time: if you need to prove WHEN, anchor the checkpoint to Bitcoin with [OpenTimestamps](/docs/opentimestamps).

---

**Next:** [SCITT receipts](/docs/scitt-receipts) for the full receipt wire format and the fold in depth, [Transparency log](/docs/transparency) for the checkpoint chain your receipt anchors to, [Sign agent outputs](/docs/sign-outputs) to publish the key that signs your statements, or [OpenTimestamps](/docs/opentimestamps) to add a Bitcoin-anchored proof of time.
