badge.blue

CID-First Attestation Specification for AT Protocol Records

1. Overview

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:

Both forms share a common CID generation process that deterministically binds the attestation to a specific record, metadata, and repository.

2. Core Concepts

Content Identifiers (CIDs)

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.

Repository Binding

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.

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 is stripped from the record before CID computation, so adding attestations does not change the signing payload.

3. CID Generation

The CID generation process is the foundation of the attestation system. Given a record, metadata, and repository DID:

  1. Start with the record as a JSON object. Remove any existing signatures array.
  2. Prepare the metadata object. Remove the cid and signature fields if present.
  3. Add the repository field to the metadata, set to the repository DID.
  4. Insert the prepared metadata into the record as the $sig field.
  5. Serialize the entire object to DAG-CBOR (deterministic CBOR).
  6. Compute a SHA-256 hash of the DAG-CBOR bytes.
  7. Wrap the hash as a CIDv1 with DAG-CBOR codec.

CID Parameters

ParameterValue
CID versionCIDv1
CodecDAG-CBOR (0x71)
Hash algorithmSHA-256 (multihash code 0x12)
Hash length32 bytes
String encodingBase32lower (prefix bafy)
Determinism: Identical record + metadata + repository inputs always produce the same CID. Different repository DIDs produce different CIDs for the same record content.

4. The $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.

$type
The attestation type identifier (e.g., com.example.signature) required
repository
Repository DID that houses the record. Added automatically during CID generation. auto
key
Public key reference (e.g., did:key:z...) for inline attestations varies
custom fields
Any additional fields (e.g., issuer, issuedAt, purpose) are preserved and included in CID calculation optional

During 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.

5. Inline 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 is normalized to low-S form before encoding.

Creation Workflow

  1. Compute the attestation CID from the record, metadata, and repository DID.
  2. Sign the CID bytes using the private key (ECDSA).
  3. Normalize the signature to low-S form.
  4. Base64-encode the signature bytes (standard alphabet, with padding).
  5. Build the attestation entry with the metadata fields, cid, and signature.
  6. Append the entry to the record's signatures array.

Inline Attestation Entry Structure

{
  "$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 $bytes field containing base64-encoded normalized signature auto

6. Remote Attestations

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.

Creation Workflow

  1. Compute the attestation CID from the record, metadata, and source repository DID.
  2. Build the proof record: the metadata object with the computed cid added.
  3. Compute the DAG-CBOR CID of the proof record itself.
  4. Build a com.atproto.repo.strongRef entry with the proof record's AT-URI and CID.
  5. Append the strongRef to the source record's signatures array.

Proof Record Structure

This record is stored in the attestor's repository:

{
  "$type": "com.example.attestation",
  "issuer": "did:plc:issuer123",
  "purpose": "verification",
  "cid": "bafyrei..."
}

strongRef Entry Structure

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..."
}
$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

7. Multiple Attestations

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.

8. Verification

Verification iterates over each entry in the signatures array and validates it independently.

Inline Attestation Verification

  1. Extract the attestation entry from the signatures array.
  2. Rebuild the $sig metadata from the entry (strip cid and signature, add repository).
  3. Recompute the attestation CID using the record, rebuilt metadata, and repository DID.
  4. Resolve the public key from the key field.
  5. Base64-decode the signature bytes from the $bytes wrapper.
  6. Verify the ECDSA signature against the recomputed CID bytes using the resolved public key.

Remote Attestation Verification

  1. Extract the com.atproto.repo.strongRef entry from signatures.
  2. Fetch the proof record from the attestor's repository using the AT-URI.
  3. Verify the fetched proof record's DAG-CBOR CID matches the cid in the strongRef.
  4. Rebuild $sig metadata from the proof record (strip cid, add repository).
  5. Recompute the attestation CID using the record, rebuilt metadata, and repository DID.
  6. Verify the recomputed CID matches the cid stored in the proof record.
Repository binding check: 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 will fail.

9. Security 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 all signatures 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 where an attacker creates an alternate valid signature without knowing the private key.

Supported Key Types

CurveUse
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 in the $bytes wrapper use standard Base64 alphabet with PKCS#7 padding for encoding. Decoders accept both padded and unpadded input for maximum compatibility.

Content Integrity

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.

10. Quick Reference

Signed Record (Inline)

{
  "$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..."
      }
    }
  ]
}

Signed Record (Remote)

{
  "$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..."
    }
  ]
}

CID Generation Input (Conceptual)

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.

11. Interactive Example

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.

Phase 1: Compute Attestation CID

1 Strip signatures from record
2 Prepare metadata (strip cid/signature, add repository)
3 Insert metadata as $sig in record
4 Serialize to DAG-CBOR
5 SHA-256 hash → CIDv1
Attestation CID

Phase 2: Build Proof Record

6 Create proof record (metadata + attestation CID)
7 Compute proof record CID (DAG-CBOR → SHA-256 → CIDv1)
Proof Record CID

Phase 3: Build strongRef

8 Create com.atproto.repo.strongRef entry

Output Records

Attestor's Repository
Source Repository