CID-First Attestation Specification for AT Protocol Records
This specification defines a CID-first attestation workflow for AT Protocol records. It enables any party to attach cryptographic attestations to records, asserting claims such as authorship verification, content approval, or third-party endorsement.
Attestations come in two forms:
signatures arraycom.atproto.repo.strongRefBoth forms share a common CID generation process that deterministically binds the attestation to a specific record, metadata, and repository.
Every attestation is rooted in a CID — a content-addressed identifier computed from the record, attestation metadata, and repository DID. The CID is the signing payload for inline attestations and the binding reference for remote attestations.
The repository DID is injected into the $sig metadata before CID generation.
This means the same record in different repositories produces different CIDs,
preventing replay attacks where a signed record is copied between repositories.
Attestations are stored in a signatures array at the top level of the record.
Each entry is either an inline attestation object or a com.atproto.repo.strongRef.
The signatures array is stripped from the record before CID computation,
so adding attestations does not change the signing payload.
The CID generation process is the foundation of the attestation system. Given a record, metadata, and repository DID:
signatures array.cid and signature fields if present.repository field to the metadata, set to the repository DID.$sig field.| Parameter | Value |
|---|---|
| CID version | CIDv1 |
| Codec | DAG-CBOR (0x71) |
| Hash algorithm | SHA-256 (multihash code 0x12) |
| Hash length | 32 bytes |
| String encoding | Base32lower (prefix bafy) |
$sig Metadata Object
The $sig field is a temporary object inserted into the record during CID computation.
It is not persisted in the final record — its contents are carried in the
attestation entries within the signatures array instead.
com.example.signature) requireddid:key:z...) for inline attestations variesissuer, issuedAt, purpose) are preserved and included in CID calculation optionalDuring CID generation, the cid and signature fields are removed from the metadata before it is inserted as $sig. This allows the same metadata object to serve double duty: the full version lives in the signatures array, while the stripped version is used for CID computation.
An inline attestation embeds ECDSA signature bytes directly in the record. The signer computes the CID and signs the CID bytes with their private key. The signature is normalized to low-S form before encoding.
cid, and signature.signatures array.{
"$type": "com.example.inlineSignature",
"key": "did:key:zQ3sh...",
"issuer": "did:plc:issuer123",
"issuedAt": "2024-01-01T00:00:00.000Z",
"cid": "bafyrei...",
"signature": {
"$bytes": "base64-encoded-normalized-signature"
}
}
$bytes field containing base64-encoded normalized signature auto
A remote attestation creates a separate proof record stored in the attestor's repository.
The original record references the proof via a com.atproto.repo.strongRef,
enabling third-party attestations without requiring the attestor's private key to touch the source record.
cid added.com.atproto.repo.strongRef entry with the proof record's AT-URI and CID.signatures array.This record is stored in the attestor's repository:
{
"$type": "com.example.attestation",
"issuer": "did:plc:issuer123",
"purpose": "verification",
"cid": "bafyrei..."
}
This entry is appended to the source record's signatures array:
{
"$type": "com.atproto.repo.strongRef",
"uri": "at://did:plc:attestor/com.example.attestation/3abc123def",
"cid": "bafyrei..."
}
com.atproto.repo.strongRef required
A record can carry any number of attestations in its signatures array.
Inline and remote attestations can be freely mixed. Each attestation is independently
verifiable and all share the same CID generation logic.
{
"$type": "app.bsky.feed.post",
"text": "Hello world!",
"createdAt": "2024-01-01T00:00:00.000Z",
"signatures": [
{
"$type": "com.example.authorSignature",
"key": "did:key:zQ3sh...",
"cid": "bafyrei...",
"signature": { "$bytes": "..." }
},
{
"$type": "com.atproto.repo.strongRef",
"uri": "at://did:plc:verifier/com.example.approval/3xyz...",
"cid": "bafyrei..."
}
]
}
When appending a new attestation to a record that already has signatures, existing
attestations are validated before the new one is added. The signatures
array is always stripped before CID computation, so the signing payload is stable
regardless of how many attestations are present.
Verification iterates over each entry in the signatures array and validates it independently.
signatures array.$sig metadata from the entry (strip cid and signature, add repository).key field.$bytes wrapper.com.atproto.repo.strongRef entry from signatures.cid in the strongRef.$sig metadata from the proof record (strip cid, add repository).cid stored in the proof record.
The repository field in $sig metadata binds every attestation to a specific repository DID.
Copying a signed record from did:plc:alice to did:plc:mallory invalidates all signatures
because the CID changes when the repository DID changes. This is enforced automatically during both creation and verification.
All ECDSA signatures are normalized to low-S form before encoding. For any ECDSA signature
(r, s), if s > n/2 (where n is the curve order), the signature is
replaced with (r, n - s). This prevents signature malleability attacks where an attacker
creates an alternate valid signature without knowing the private key.
| Curve | Use |
|---|---|
| P-256 (secp256r1) | ECDSA signing and verification |
| P-384 (secp384r1) | ECDSA signing and verification |
| K-256 (secp256k1) | ECDSA signing and verification |
Signature bytes in the $bytes wrapper use standard Base64 alphabet with PKCS#7 padding for encoding.
Decoders accept both padded and unpadded input for maximum compatibility.
Signatures are over CID bytes, not the raw record content. Because the CID is a cryptographic hash
of the DAG-CBOR-encoded record (with $sig metadata), any modification to the record content,
metadata, or repository binding changes the CID and invalidates all signatures.
{
"$type": "app.bsky.feed.post",
"text": "Hello world!",
"createdAt": "2024-01-01T00:00:00.000Z",
"signatures": [
{
"$type": "com.example.inlineSignature",
"key": "did:key:zQ3shNzMp4oaaQ1gQRz...",
"issuer": "did:plc:issuer123",
"issuedAt": "2024-01-01T00:00:00.000Z",
"cid": "bafyreigw5bqvbz6m3c3zjpqhxwl4nj...",
"signature": {
"$bytes": "4pL9s2k7Rm3..."
}
}
]
}
{
"$type": "app.bsky.feed.post",
"text": "Hello world!",
"createdAt": "2024-01-01T00:00:00.000Z",
"signatures": [
{
"$type": "com.atproto.repo.strongRef",
"uri": "at://did:plc:attestor/com.example.attestation/3abc...",
"cid": "bafyreihx7ywnb5ce3f6ol7rv..."
}
]
}
The object serialized to DAG-CBOR for CID computation:
{
"$type": "app.bsky.feed.post",
"text": "Hello world!",
"createdAt": "2024-01-01T00:00:00.000Z",
"$sig": {
"$type": "com.example.inlineSignature",
"key": "did:key:zQ3sh...",
"issuer": "did:plc:issuer123",
"issuedAt": "2024-01-01T00:00:00.000Z",
"repository": "did:plc:repo123"
}
}
Note: signatures is removed, and $sig contains the metadata with repository added and cid/signature removed.
Walk through a complete remote attestation workflow. Edit the inputs below and click Run to see every intermediate step and the final records that would be written to each repository. This runs real DAG-CBOR serialization and SHA-256 hashing in your browser.
signatures from recordcid/signature, add repository)$sig in record
com.atproto.repo.strongRef entry