badge.blue
A CID-first attestation workflow for AT Protocol records — inline and remote cryptographic signatures with repository-bound replay-attack prevention.
Abstract
This specification defines how any party may attach cryptographic attestations to an AT Protocol record. Attestations travel either inline (ECDSA signatures embedded in the record's signatures array) or remotely (as separate proof records referenced by com.atproto.repo.strongRef). Both forms share a deterministic, repository-bound CID-generation process that makes the signing payload stable, independent of how many signatures have been attached, and non-replayable across repositories.
§ 1Overview
This document specifies a CID-first attestation workflow for records in the AT Protocol. It enables any party to attach cryptographic attestations to records, asserting claims such as authorship verification, content approval, or third-party endorsement.
Attestations take one of two forms:
- Inline attestations — ECDSA signatures embedded directly in the record's
signaturesarray. - Remote attestations — separate proof records, referenced via
com.atproto.repo.strongRef, stored in the attestor's repository.
Both forms share a common CID-generation process that deterministically binds the attestation to a specific record, metadata, and repository DID.
Every attestation is bound to a record, a metadata object, and a repository DID. Change any of the three, and the CID — and therefore every signature over it — is invalidated.
§ 2Core concepts
Content identifiers
Every attestation is rooted in a CID — a content-addressed identifier computed from the record, the attestation metadata, and the repository DID. The CID serves as the signing payload for inline attestations, and as the binding reference for remote attestations.
Repository binding
The repository DID is injected into the $sig metadata before CID generation. Consequently, the same record in different repositories produces different CIDs — preventing replay attacks in which a signed record is copied between repositories.
Signatures array
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 must be stripped from the record before CID computation, so adding attestations does not change the signing payload.
§ 3CID generation
CID generation is the foundation of the attestation system. Given a record, a metadata object, and a repository DID, an implementation must perform the following procedure in order:
- Start with the record as a JSON object. Remove any existing
signaturesarray. - Prepare the metadata object. Remove the
cidandsignaturefields if present. - Add a
repositoryfield to the metadata, set to the repository DID. - Insert the prepared metadata into the record as the
$sigfield. - Serialize the entire object to DAG-CBOR (deterministic CBOR).
- Compute a SHA-256 hash over the DAG-CBOR bytes.
- Wrap the hash as a CIDv1 with DAG-CBOR codec.
CID parameters
| 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) |
Identical record + metadata + repository inputs always produce the same CID. Different repository DIDs produce different CIDs for the same record content.
§ 4The $sig metadata object
$sig is a transient 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.
- $type
- Attestation type identifier, e.g.
com.example.signature. required - repository
- Repository DID housing the record. Added automatically during CID generation. auto
- key
- Public-key reference (e.g.,
did:key:z…) for inline attestations. varies - custom
- Any additional fields (
issuer,issuedAt,purpose, …) are preserved and included in CID calculation. optional
During CID generation, the cid and signature fields are stripped from the metadata before it is inserted as $sig. This lets a single metadata object serve double duty: the full version lives in the signatures array, while the stripped version drives CID computation.
§ 5Inline attestations
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 must be normalized to low-S form before encoding.
Creation workflow
- Compute the attestation CID from the record, metadata, and repository DID.
- Sign the CID bytes using the private key (ECDSA).
- Normalise the signature to low-S form.
- Base64-encode the signature bytes (standard alphabet, with padding).
- Build the attestation entry with the metadata fields,
cid, andsignature. - Append the entry to the record's
signaturesarray.
{
"$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"
}
}
Required fields
- $type
- Attestation type identifier. required
- key
- Public-key reference for signature verification. required
- cid
- The computed attestation CID (base32-encoded CIDv1). auto
- signature
- Object with
$bytesfield containing base64-encoded normalized signature. auto
§ 6Remote attestations
A remote attestation creates a separate proof record stored in the attestor's repository. The source record references that proof via a com.atproto.repo.strongRef. This enables third-party attestations without the attestor's private key ever touching the source record.
Creation workflow
- Compute the attestation CID from the record, metadata, and source repository DID.
- Build the proof record: the metadata object with the computed
cidadded. - Compute the DAG-CBOR CID of the proof record itself.
- Build a
com.atproto.repo.strongRefentry with the proof record's AT-URI and CID. - Append the strongRef to the source record's
signaturesarray.
{
"$type": "com.example.attestation",
"issuer": "did:plc:issuer123",
"purpose": "verification",
"cid": "bafyrei..."
}
signatures{
"$type": "com.atproto.repo.strongRef",
"uri": "at://did:plc:attestor/com.example.attestation/3abc123def",
"cid": "bafyrei..."
}
- $type
com.atproto.repo.strongRef. required- uri
- AT-URI pointing to the proof record in the attestor's repository. required
- cid
- DAG-CBOR CID of the proof record (for content integrity). required
§ 7Multiple attestations
A record may 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 should be 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.
§ 8Verification
Verification iterates over each entry in the signatures array and validates it independently.
Inline attestation verification
- Extract the attestation entry from the
signaturesarray. - Rebuild the
$sigmetadata from the entry: stripcidandsignature, then addrepository. - Recompute the attestation CID using the record, rebuilt metadata, and repository DID.
- Resolve the public key from the
keyfield. - Base64-decode the signature bytes from the
$byteswrapper. - Verify the ECDSA signature against the recomputed CID bytes using the resolved public key.
Remote attestation verification
- Extract the
com.atproto.repo.strongRefentry fromsignatures. - Fetch the proof record from the attestor's repository using the AT-URI.
- Verify the fetched proof record's DAG-CBOR CID matches the
cidin the strongRef. - Rebuild
$sigmetadata from the proof record: stripcid, addrepository. - Recompute the attestation CID using the record, rebuilt metadata, and repository DID.
- Verify the recomputed CID matches the
cidstored in the proof record.
Verification must use the repository DID where the record is actually stored. If an attacker copies a record to a different repository, the recomputed CID will not match, and verification must fail.
§ 9Security properties
Replay attack prevention
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 every signature, because the CID changes when the repository DID changes. This is enforced automatically during both creation and verification.
Signature normalization
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 in which an alternate valid signature is produced without knowing the private key.
Supported key types
| Curve | Use |
|---|---|
| P-256 (secp256r1) | ECDSA signing and verification |
| P-384 (secp384r1) | ECDSA signing and verification |
| K-256 (secp256k1) | ECDSA signing and verification |
Base64 encoding
Signature bytes inside the $bytes wrapper use the standard Base64 alphabet with PKCS #7 padding for encoding. Decoders should accept both padded and unpadded input for maximum compatibility.
Content integrity
Signatures cover CID bytes, not raw record content. Because the CID is a cryptographic hash of the DAG-CBOR-encoded record (including $sig metadata), any modification to the record content, metadata, or repository binding changes the CID and invalidates every signature.
§ 10Quick reference
{
"$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..."
}
]
}
{
"$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.
§ 11Interactive example
Walk through a complete remote attestation workflow below. Edit the inputs and click Run to see every intermediate step and the final records that would be written to each repository. The demo performs real DAG-CBOR serialization and SHA-256 hashing in your browser.
signatures from the recordcid/signature, add repository$sig in the recordcom.atproto.repo.strongRef entry