attestion_spec.md edited
1122 lines 37 kB view raw view code

ATProtocol Record Structures for Inline and Remote Signature Structures (Attestations)#

Technical Specification v1.0#

Executive Summary#

This specification defines attestation record structures for ATProtocol that enable cryptographic signing of content using CID-based content addressing. Attestations provide verifiable proof that a specific identity has endorsed, verified, or made claims about content within the ATProtocol ecosystem.

The specification supports two minimal patterns within a unified signatures array using union types:

  • Inline attestations: Signature objects with $type, signature (bytes), and key (DID verification method reference) as required fields
  • Remote attestations: Strong references (com.atproto.repo.strongRef) pointing to separate proof records containing a cid as a required field

Both patterns use a $sig metadata object during CID generation that must always include at least a $type field and a repository field. The repository field contains the DID of the repository housing the record, preventing replay attacks where an attacker might attempt to clone records from one repository into their own. For inline attestations, the resulting CID bytes are signed with the specified key. For remote attestations, the resulting CID is stored directly in the proof record in the attestor's repository.

The signatures array uses a union type system, where the $type field is always required to distinguish between inline signatures and strong references to remote attestations.


1. Requirements#

Content Addressability: All attestations must reference content via CID (Content Identifier) to create immutable, verifiable links to specific versions of data. This ensures attestations cannot be replayed, reused, or applied to different content, preventing circumvention of ATProtocol security controls or repository integrity mechanisms.

Replay Attack Prevention: The CID generation process must include the repository DID in the $sig metadata to prevent attackers from cloning records from one repository to another. This binding ensures that attestations are only valid within their original repository context.

Key Management Flexibility: The attestation framework must support both inline and remote patterns to accommodate different operational constraints:

  • Remote attestations enable proof creation using only existing DID verification methods, with no attestation-specific key management required
  • Inline attestations embed signatures directly in records, eliminating the need to create and maintain separate proof records

Radical Simplicity: The attestation structure must minimize complexity while maximizing security:

  • Inline attestations require only three fields: $type, signature (bytes), and key (verification method reference)
  • Remote attestations require only one field in the proof record: cid
  • Fewer fields reduce attack surface and implementation burden

Union Type System: The signatures array must use ATProtocol's union type pattern with required $type field for clear discrimination between inline signatures and strongRef references to remote attestations.

Unified CID Generation: Both attestation types must use the same CID generation process with a $sig metadata object that always includes at least a $type field and a repository field. The CID incorporates this metadata, providing scope control and preventing replay attacks across different contexts and repositories.

Application Extensibility: Applications must be able to define custom attestation types and inject application-specific metadata into the attestation creation process:

  • The $type field in signatures enables application-specific attestation semantics
  • The $sig metadata object allows applications to include contextual information in CID generation, binding attestations to specific use cases and preventing cross-context attacks

DID Integration: The framework must leverage existing ATProtocol DID infrastructure with verification methods for cryptographic operations, eliminating dependency on external key management systems.

Backward Compatibility: Attestation-unaware clients must be able to safely ignore attestation fields. Records must validate against Lexicon schemas with or without signatures, ensuring the framework degrades gracefully in mixed-version environments.


2. Record Structure Definitions#

The signatures array in ATProtocol records uses a union type system that supports both inline attestations and references to remote attestations. The $type field is always required to distinguish between types in the union.

2.1 Inline Attestation Structure#

Inline attestations embed signatures directly in records using a signatures array. The signature is generated by signing the CID bytes of the record content (including a $sig metadata object with repository field).

2.1.1 Example Record#

{
  "$type": "app.example.record",
  "createdAt": "2025-10-14T12:00:00Z",
  "text": "Example content that is being attested",
  "signatures": [
    {
      "$type": "com.example.inlineSignature",
      "signature": {"$bytes": "MzQ2Y2U4ZDNhYmM5NjU0Mzk5NWJmNjJkOGE4..."},
      "key": "did:web:example.com#signing1"
    }
  ]
}

2.1.2 Inline Signature Fields#

When using inline signatures in the signatures array:

Required Fields:

  • $type (string, NSID format): Type identifier for the signature object (required for union types)

    • Example: "com.example.inlineSignature"
    • Must NOT be "com.atproto.repo.strongRef" (that's for remote attestations)
    • Allows applications to define custom signature types
  • signature (object with $bytes): Base64-encoded signature bytes

    • Generated from signing the CID bytes of the record
    • ECDSA signature using P-256 or K-256 curve
    • Must use "low-S" variant (per BIP-0062)
    • Format: {"$bytes": "base64-encoded-signature"}
  • key (string): Full verification method reference within a DID document

    • Format: did:{method}:{identifier}#{fragment}
    • Example: did:web:example.com#signing1
    • The DID component identifies who signed the record
    • The fragment identifies which verification method in the DID document was used

2.1.3 Signatures Array Lexicon Schema#

{
  "signatures": {
    "type": "array",
    "description": "Array of cryptographic signatures attesting to record",
    "items": {
      "type": "union",
      "refs": [
        "com.atproto.repo.strongRef",
        "com.example.inlineSignature"
      ]
    }
  }
}

Inline Signature Type Definition:

{
  "lexicon": 1,
  "id": "com.example.inlineSignature",
  "defs": {
    "main": {
      "type": "object",
      "required": ["signature", "key"],
      "properties": {
        "signature": {
          "type": "bytes",
          "description": "Signature bytes"
        },
        "key": {
          "type": "string",
          "description": "Full verification method reference (DID#fragment)"
        }
      }
    }
  }
}

2.2 Remote Attestation Structure#

Remote attestations are separate records that contain a CID of the content being attested.

2.2.1 Remote Attestation Record#

{
  "$type": "com.example.proof",
  "cid": "bafyreifsqhrnlciktfxkz4yiqw5wtx6xvods67aicqt5tc7cly24dmhv3e"
}

The CID is created from a DAG-CBOR encoding of the record content that includes a $sig metadata object (with at least a $type field and repository field) and excludes the "signatures" field.

2.2.2 Attested Record with Remote Reference#

Records can reference remote attestations through strong-refs in their signatures array:

{
  "$type": "app.bsky.feed.post",
  "createdAt": "2025-10-14T20:09:00.733Z",
  "text": "Content that is being attested",
  "signatures": [
    {
      "$type": "com.atproto.repo.strongRef",
      "cid": "bafyreig5ug2vj63ag5b6okth3roujv2lngxnyssxeylfcmmqiznfje4enu",
      "uri": "at://did:plc:cbkjy5n7bk3ax2wplmtjofq2/com.example.proof/3m3i7e3uhrocj"
    }
  ]
}

The proof record's CID was generated from the content with a $sig object containing attestation metadata including the repository DID.

2.2.3 Remote Attestation Lexicon Schema#

Proof Record Schema:

{
  "lexicon": 1,
  "id": "com.example.proof",
  "defs": {
    "main": {
      "type": "record",
      "description": "Remote attestation proof containing CID of attested content",
      "key": "tid",
      "record": {
        "type": "object",
        "required": ["cid"],
        "properties": {
          "cid": {
            "type": "string",
            "format": "cid",
            "description": "CID of the attested content (record without signatures field)"
          }
        }
      }
    }
  }
}

Note: The reference to a proof record from the signatures array uses the standard com.atproto.repo.strongRef type, which is already defined in the ATProtocol specification.

2.2.4 Pattern Comparison#

Aspect Inline Attestation Remote Attestation
Location Signature in same record as content Separate proof record with CID
Storage Recipient's repository Attester's repository
Fields signature + key only cid only
CID Usage CID bytes are signed CID is stored directly
$sig Required Yes (with $type + repository) Yes (with $type + repository)
Use Case Direct attestations, self-signing Revocation, external proofs
Discoverability Direct (in record) Requires query/index or strong-ref
Reference N/A Strong-ref from attested record

3. CID Generation Algorithms and Requirements#

3.1 CID Format Specification#

ATProtocol attestations use CIDv1 with the following "blessed" format:

CIDv1:
  Multibase:  base32 (for strings), raw binary (for DAG-CBOR)
  Multicodec: dag-cbor (0x71)
  Multihash:  sha2-256 (0x12), 32 bytes

Binary Structure:

0x01                    # CID version 1
0x71                    # dag-cbor codec
0x12                    # SHA-256 hash function
0x20                    # Hash length (32 bytes)
<32 bytes of hash>      # SHA-256 digest

3.2 CID Generation Algorithm#

3.2.1 Standard Process#

Step 1: Prepare Record Data
  - Remove "signatures" field from record
  - Include "$sig" metadata object (required for attestations)
    * Must always include "$type" field
    * Must always include "repository" field (DID of housing repository)
    * Additional fields based on attestation type
  - Ensure all fields in canonical form

Step 2: Encode to DAG-CBOR
  - Apply canonical serialization rules:
    * Sort map keys by length, then lexically
    * Use shortest integer encodings
    * Definite-length items only
    * Tag 42 for CID links
  - Output: Binary CBOR bytes

Step 3: Hash with SHA-256
  - Input: DAG-CBOR bytes from Step 2
  - Output: 32-byte binary hash digest

Step 4: Construct Multihash
  - Prepend hash function code: 0x12 (SHA-256)
  - Prepend hash length: 0x20 (32 bytes)
  - Result: 34-byte multihash

Step 5: Construct CID
  - Prepend CID version: 0x01
  - Prepend multicodec: 0x71 (dag-cbor)
  - Append multihash from Step 4
  - Result: 36-byte CID

Step 6: Usage by Attestation Type
  - For inline attestations: Sign the CID bytes with private key
  - For remote attestations: Store the CID in proof record
  - For string representation: Base32-encode with 'b' prefix

3.2.2 Pseudocode Implementation#

FUNCTION generateRecordCID(recordData, sigMetadata, repositoryDID):
    # Step 1: Prepare data with $sig
    encodingData = copy(recordData)
    
    # Remove signatures field if present
    DELETE encodingData["signatures"]
    
    # Add $sig metadata (must have $type and repository)
    IF NOT sigMetadata["$type"]:
        THROW Error("$sig must include $type field")
    
    # CRITICAL: Always set repository field to prevent replay attacks
    sigMetadata["repository"] = repositoryDID
    encodingData["$sig"] = sigMetadata
    
    # Step 2: Encode to DAG-CBOR
    cborBytes = encodeDAGCBORCanonical(encodingData)
    
    # Step 3: Hash
    hashDigest = SHA256(cborBytes)  # 32 bytes
    
    # Step 4: Construct multihash
    multihash = CONCAT(
        [0x12],          # SHA-256 code
        [0x20],          # 32 bytes length
        hashDigest       # 32-byte hash
    )
    
    # Step 5: Construct CID
    cidBytes = CONCAT(
        [0x01],          # CIDv1
        [0x71],          # dag-cbor
        multihash        # 34 bytes
    )
    
    RETURN cidBytes  # 36 bytes total
END FUNCTION

# Wrapper function used in attestation generation
FUNCTION generateCIDFromCBOR(cborBytes):
    hashDigest = SHA256(cborBytes)
    multihash = CONCAT([0x12], [0x20], hashDigest)
    cidBytes = CONCAT([0x01], [0x71], multihash)
    RETURN cidBytes
END FUNCTION

FUNCTION encodeDAGCBORCanonical(data):
    # Recursive encoding with strict rules
    
    IF data is NULL:
        RETURN encodeCBORNull()
    
    IF data is BOOLEAN:
        RETURN encodeCBORBool(data)
    
    IF data is INTEGER:
        RETURN encodeCBORInt(data)  # Shortest encoding
    
    IF data is STRING:
        utf8Bytes = encodeUTF8(data)
        RETURN CONCAT(
            majorType3WithLength(LENGTH(utf8Bytes)),
            utf8Bytes
        )
    
    IF data is BYTES:
        RETURN CONCAT(
            majorType2WithLength(LENGTH(data)),
            data
        )
    
    IF data is CID:
        # Tag 42 encoding
        cidWithPrefix = CONCAT([0x00], data.toBytes())
        RETURN CONCAT(
            [0xd8, 0x2a],  # Tag 42
            majorType2WithLength(LENGTH(cidWithPrefix)),
            cidWithPrefix
        )
    
    IF data is ARRAY:
        encoded = []
        FOR item IN data:
            encoded.APPEND(encodeDAGCBORCanonical(item))
        
        RETURN CONCAT(
            majorType4WithLength(LENGTH(data)),
            CONCATENATE_ALL(encoded)
        )
    
    IF data is MAP:
        # CRITICAL: Sort keys before encoding
        sortedKeys = sortKeysCanonical(data.KEYS())
        
        encoded = []
        FOR key IN sortedKeys:
            encodedKey = encodeDAGCBORCanonical(key)
            encodedValue = encodeDAGCBORCanonical(data[key])
            encoded.APPEND(CONCAT(encodedKey, encodedValue))
        
        RETURN CONCAT(
            majorType5WithLength(LENGTH(sortedKeys)),
            CONCATENATE_ALL(encoded)
        )
    
    THROW Error("Unsupported type for DAG-CBOR")
END FUNCTION

FUNCTION sortKeysCanonical(keys):
    # Convert keys to CBOR bytes first
    keyPairs = []
    FOR key IN keys:
        cborBytes = encodeDAGCBORCanonical(key)
        keyPairs.APPEND((key, cborBytes))
    
    # Sort by length first, then lexically
    sortedPairs = SORT(keyPairs, COMPARATOR=lambda a, b:
        IF LENGTH(a.cborBytes) != LENGTH(b.cborBytes):
            RETURN COMPARE(LENGTH(a.cborBytes), LENGTH(b.cborBytes))
        ELSE:
            RETURN byteWiseCompare(a.cborBytes, b.cborBytes)
    )
    
    RETURN [pair.key FOR pair IN sortedPairs]
END FUNCTION

3.3 DAG-CBOR Strictness Requirements#

Map Key Sorting (RFC 7049 Section 3.9):

  1. Keys sorted by complete CBOR byte representation
  2. Shorter byte representations sort before longer
  3. Same-length representations sorted lexically (byte-wise)

Example Sorting:

Original:  {"text": ..., "$type": ..., "repository": ..., "createdAt": ...}

CBOR bytes:
  "text"       → 0x64 74 65 78 74        (5 bytes)
  "$type"      → 0x65 24 74 79 70 65     (6 bytes)
  "createdAt"  → 0x69 63 72 65 61 74 65 64 41 74  (11 bytes)
  "repository" → 0x6a 72 65 70 6f 73 69 74 6f 72 79  (11 bytes)

Sorted:     ["text", "$type", "createdAt", "repository"]

Integer Encoding:

  • 0-23: Encoded in initial byte
  • 24-255: 1-byte extension (0x18 prefix)
  • 256-65535: 2-byte extension (0x19 prefix)
  • Must use shortest possible encoding

Tag 42 (CID Links):

  • Only valid form: 0xd8 0x2a
  • Must not use alternative encodings (0xf8 0x2a is invalid)

Floating Point:

  • ATProtocol records should avoid floats
  • If required, use 64-bit double precision only
  • NaN, Infinity, -Infinity not supported

Forbidden:

  • Tags other than 42
  • Indefinite-length items
  • Non-string map keys
  • Simple values except true (21), false (20), null (22)

4. Signature Format and Key Resolution#

4.1 The $sig Metadata Object#

The $sig object is a temporary metadata structure included during CID generation for both inline and remote attestations. It provides scope control and prevents signature replay attacks.

4.1.1 Structure#

{
  "$sig": {
    "$type": "com.example.attestation.type",
    "repository": "did:plc:abc123",  // Always required for attestations
    "key": "did:plc:signer123#method"  // For inline attestations
  }
}

4.1.2 Required and Optional Fields#

Required Fields:

  • $type (string, NSID): Type identifier for the attestation (always required)
  • repository (string): DID of the repository housing the record (always required for attestations)
    • Must be a valid DID format string
    • Prevents replay attacks where an attacker clones records to a different repository
    • Value must be overwritten if present to ensure correct repository binding

Common Fields:

  • key (string): The full verification method reference (for inline attestations)

Important:

  • The $sig object is only present during CID generation for both attestation types
  • It is NOT stored in the final record
  • The $sig must always contain both $type and repository fields for attestations
  • The repository field ensures the CID is bound to a specific repository, preventing cross-repository replay attacks
  • For inline attestations: The CID (including $sig) is signed to create the signature
  • For remote attestations: The CID (including $sig) is stored directly in the proof record

4.2 Signature Generation Process (Inline Attestations)#

FUNCTION generateInlineSignature(record, repositoryDID, signerDID, signingKey, fragmentID, signatureType):
    # Step 1: Add $sig metadata for CID generation
    sigMetadata = {
        "$type": "com.example.inlineSignature",  # Always required
        "repository": repositoryDID,  # Always required - prevents replay attacks
        "key": signerDID + "#" + fragmentID,
    }
    
    # Step 2: Prepare record for CID generation
    recordForCID = copy(record)
    DELETE recordForCID["signatures"]
    
    # Step 3: Generate CID (which includes $sig with repository)
    recordForCID["$sig"] = sigMetadata
    cborBytes = encodeDAGCBORCanonical(recordForCID)
    cidBytes = generateCIDFromCBOR(cborBytes)
    
    # Step 4: Sign the CID bytes (not the CBOR directly)
    signature = ECDSA_Sign(signingKey, cidBytes)
    
    # Step 5: Ensure low-S format
    IF NOT isLowS(signature):
        signature = convertToLowS(signature)
    
    # Step 6: Create signature block with required $type
    signatureBlock = {
        "$type": "com.example.inlineSignature",
        "signature": {"$bytes": base64Encode(signature)},
        "key": signerDID + "#" + fragmentID
    }
    
    # Step 7: Add to record (without $sig)
    finalRecord = copy(record)
    IF NOT finalRecord.signatures:
        finalRecord["signatures"] = []
    finalRecord["signatures"].APPEND(signatureBlock)
    
    RETURN finalRecord
END FUNCTION

4.3 Remote Attestation Generation Process#

FUNCTION generateRemoteProof(record, repositoryDID, proverDID):
    # Step 1: Add $sig metadata for CID generation
    sigMetadata = {
        "$type": "com.example.proof",  # Always required
        "repository": repositoryDID  # Always required - prevents replay attacks
    }
    
    # Step 2: Prepare record for CID generation
    recordForCID = copy(record)
    DELETE recordForCID["signatures"]
    
    # Step 3: Generate CID (which includes $sig with repository)
    recordForCID["$sig"] = sigMetadata
    cborBytes = encodeDAGCBORCanonical(recordForCID)
    cid = generateCIDFromCBOR(cborBytes)
    
    # Step 4: Create minimal proof record
    proofRecord = {
        "$type": "com.example.proof",
        "cid": cid
    }
    
    # Step 5: Store proof record and return reference
    proofURI = storeRecord(proofRecord)
    
    RETURN {
        proofRecord: proofRecord,
        proofURI: proofURI,
        contentCID: cid
    }
END FUNCTION

4.4 Key Resolution Process#

4.4.1 DID Document Structure#

{
  "@context": ["https://www.w3.org/ns/did/v1"],
  "id": "did:plc:abc123",
  "alsoKnownAs": ["at://alice.bsky.social"],
  "verificationMethod": [
    {
      "id": "did:plc:abc123#atproto",
      "type": "Multikey",
      "controller": "did:plc:abc123",
      "publicKeyMultibase": "zQ3shqwJEJyMBsBXCWyCBpUBMqxcon9oHB7mCvx4sSpMdLJwc"
    }
  ],
  "assertionMethod": [
    {
      "id": "did:plc:abc123#attesting",
      "type": "Multikey",
      "controller": "did:plc:abc123",
      "publicKeyMultibase": "zDnaembgSGUhZULN2Caob4HLJPaxBh92N7rtH21TErzqf8HQo"
    }
  ],
  "service": [
    {
      "id": "#atproto_pds",
      "type": "AtprotoPersonalDataServer",
      "serviceEndpoint": "https://pds.example.com"
    }
  ]
}

4.4.2 Resolution Algorithm#

FUNCTION resolveVerificationMethod(keyReference):
    # Parse DID and fragment
    [did, fragment] = splitDIDFragment(keyReference)
    
    # Resolve DID document
    didDocument = resolveDID(did)
    IF didDocument is NULL:
        THROW Error("Cannot resolve DID from key reference")
    
    # Find verification method
    FOR method IN didDocument.verificationMethod:
        IF method.id == keyReference:
            RETURN method
        
        # Also check short form (without DID prefix)
        IF method.id == "#" + fragment:
            RETURN method
    FOR method IN didDocument.assertionMethod:
        IF method.id == keyReference:
            RETURN method
        
        # Also check short form (without DID prefix)
        IF method.id == "#" + fragment:
            RETURN method
    
    THROW Error("Verification method not found: " + keyReference)
END FUNCTION

FUNCTION parsePublicKey(verificationMethod):
    # Extract public key from multibase encoding
    multibaseKey = verificationMethod.publicKeyMultibase
    
    # Decode multibase (base58btc with 'z' prefix)
    IF NOT multibaseKey.startsWith("z"):
        THROW Error("Invalid multibase encoding")
    
    keyBytes = base58Decode(multibaseKey.substring(1))
    
    # Extract multicodec
    [codecVarint, offset] = decodeVarint(keyBytes)
    
    # Determine key type
    IF codecVarint == 0xE701:  # secp256k1-pub
        keyType = "K-256"
        compressedKey = keyBytes[offset:]
    ELSE IF codecVarint == 0x1200:  # p256-pub
        keyType = "P-256"
        compressedKey = keyBytes[offset:]
    ELSE:
        THROW Error("Unsupported key type")
    
    # Decompress public key (curve-specific)
    publicKey = decompressPublicKey(compressedKey, keyType)
    
    RETURN (publicKey, keyType)
END FUNCTION

4.5 Supported Cryptographic Algorithms#

Elliptic Curves:

  • P-256 (secp256r1, NIST P-256): Widely supported, WebCrypto compatible
  • K-256 (secp256k1): Bitcoin/Ethereum curve, default in ATProtocol

Signature Algorithm:

  • ECDSA with CID bytes as input (not raw hash)
  • Low-S signature variant mandatory (BIP-0062)
  • Signs the complete 36-byte CID, not the underlying SHA-256 hash

Hash Function:

  • SHA-256 (256-bit output) for CID generation

Encoding:

  • Public keys: Multibase (base58btc) with multicodec prefix, compressed format
  • Signatures: Raw ECDSA bytes (r, s) encoded as base64 in JSON
  • CIDs: Base32-encoded for strings, raw bytes for signing

5. Verification Workflows#

5.1 Inline Attestation Verification#

FUNCTION verifyInlineAttestation(record, repositoryDID):
    IF NOT record.signatures OR LENGTH(record.signatures) == 0:
        RETURN {valid: false, reason: "No signatures present"}
    
    results = []
    
    FOR signatureBlock IN record.signatures:
        # Skip strongRefs (they're remote attestations)
        IF signatureBlock["$type"] == "com.atproto.repo.strongRef":
            CONTINUE
        
        # Verify inline signature
        IF signatureBlock.signature AND signatureBlock.key:
            result = verifySingleSignature(record, signatureBlock, repositoryDID)
            results.APPEND(result)
    
    RETURN results
END FUNCTION

FUNCTION verifySingleSignature(record, signatureBlock, repositoryDID):
    # Step 1: Verify required fields
    IF NOT signatureBlock["$type"]:
        RETURN {valid: false, reason: "Missing $type field"}
    
    # Step 2: Resolve verification method from key field
    verificationMethod = resolveVerificationMethod(signatureBlock.key)
    IF verificationMethod is NULL:
        RETURN {valid: false, reason: "Cannot resolve verification method"}
    
    (publicKey, keyType) = parsePublicKey(verificationMethod)
    
    # Step 3: Reconstruct signed content with $sig
    recordForSigning = copy(record)
    DELETE recordForSigning["signatures"]
    
    # Add $sig metadata (must include $type and repository)
    sigMetadata = copy(signatureBlock)
    DELETE sigMetadata["signature"]
    
    # CRITICAL: Always set repository field for replay attack prevention
    sigMetadata["repository"] = repositoryDID
    recordForSigning["$sig"] = sigMetadata
    
    # Step 4: Generate CID (same process as signing)
    cborBytes = encodeDAGCBORCanonical(recordForSigning)
    cidBytes = generateCIDFromCBOR(cborBytes)
    
    # Step 5: Decode signature
    signatureBytes = base64Decode(signatureBlock.signature["$bytes"])
    
    # Step 6: Verify signature is low-S
    IF NOT isLowS(signatureBytes):
        RETURN {valid: false, reason: "Signature not in low-S format"}
    
    # Step 7: Cryptographic verification against CID bytes
    isValid = ECDSA_Verify(publicKey, cidBytes, signatureBytes, keyType)
    
    IF isValid:
        RETURN {
            valid: true,
            type: signatureBlock["$type"],
            key: signatureBlock.key,
            signer: extractDIDFromKey(signatureBlock.key),
            repository: repositoryDID
        }
    ELSE:
        RETURN {valid: false, reason: "Signature verification failed"}
END FUNCTION

5.2 Remote Attestation Verification#

FUNCTION verifyRemoteAttestation(proofRecord, subjectRecord, repositoryDID):
    # Step 1: Verify the proof record exists and has CID
    IF NOT proofRecord.cid:
        RETURN {valid: false, reason: "Proof record missing CID"}
    
    # Step 2: Reconstruct subject record with $sig for CID generation
    subjectRecordForCID = copy(subjectRecord)
    DELETE subjectRecordForCID["signatures"]
    
    # Add $sig metadata (must include $type and repository)
    # Note: The exact $sig content must match what was used during proof creation
    sigMetadata = copy(proofRecord)
    DELETE sigMetadata["cid"]
    
    # CRITICAL: Always set repository field for replay attack prevention
    sigMetadata["repository"] = repositoryDID
    subjectRecordForCID["$sig"] = sigMetadata
    
    # Step 3: Generate CID from subject record
    cborBytes = encodeDAGCBORCanonical(subjectRecordForCID)
    subjectCID = generateCIDFromCBOR(cborBytes)
    
    # Step 4: Verify CID match
    IF proofRecord.cid != subjectCID:
        RETURN {
            valid: false,
            reason: "CID mismatch - possible replay attack or incorrect repository",
            expected: proofRecord.cid,
            actual: subjectCID
        }
    
    # Step 5: Extract proof metadata
    proofURI = constructATURI(proofRecord)
    proofDID = extractDIDFromURI(proofURI)
    
    RETURN {
        valid: true,
        proofCID: proofRecord.cid,
        proofURI: proofURI,
        prover: proofDID,
        subjectCID: subjectCID,
        repository: repositoryDID
    }
END FUNCTION

5.3 CID Chain Verification#

For verifying content integrity through repository Merkle tree:

FUNCTION verifyRecordInRepository(commit, recordPath):
    # Step 1: Verify commit signature
    IF NOT verifyCommitSignature(commit):
        RETURN {valid: false, reason: "Invalid commit signature"}
    
    # Step 2: Extract repository DID from commit
    repositoryDID = commit.did
    
    # Step 3: Fetch MST root
    mstRoot = fetchBlock(commit.data)
    
    # Step 4: Navigate MST to find record
    (record, proof) = findRecordInMST(mstRoot, recordPath)
    IF record is NULL:
        RETURN {valid: false, reason: "Record not found in MST"}
    
    # Step 5: Verify CID chain (with repository binding)
    recordCID = generateRecordCID(record, getRecordSigMetadata(record), repositoryDID)
    IF NOT verifyMSTPath(mstRoot, recordPath, recordCID):
        RETURN {valid: false, reason: "MST path verification failed"}
    
    # Step 6: Verify root CID matches commit
    rootCID = generateRecordCID(mstRoot)
    IF rootCID != commit.data:
        RETURN {valid: false, reason: "Root CID mismatch"}
    
    RETURN {
        valid: true,
        record: record,
        recordCID: recordCID,
        proof: proof,
        repository: repositoryDID
    }
END FUNCTION

6. Integration Patterns with ATProtocol Infrastructure#

6.1 PDS OAuth Integration#

Signature Request Flow:

1. Client requests OAuth scope: "identity:attestation"
2. PDS prompts user for consent
3. User approves specific verification method usage
4. Client receives scoped token
5. Client calls com.atproto.attestation.sign with repository DID
6. PDS signs record using authorized verification method
7. Returns signed record to client

XRPC Method for Inline Signing:

{
  "lexicon": 1,
  "id": "com.atproto.attestation.sign",
  "defs": {
    "main": {
      "type": "procedure",
      "description": "Sign a CID with repository binding",
      "input": {
        "encoding": "application/json",
        "schema": {
          "type": "object",
          "required": ["cid"],
          "properties": {
            "cid": {
              "type": "string",
              "format": "cid",
              "description": "The CID to sign."
            },
            "key": {
              "type": "string",
              "description": "Fragment ID of verification method"
            }
          }
        }
      },
      "output": {
        "encoding": "application/json",
        "schema": {
          "type": "object",
          "required": ["signature"],
          "properties": {
            "signature": {
              "type": "bytes",
              "description": "The signature"
            }
          }
        }
      }
    }
  }
}

7. Examples and Use Cases#

7.1 Verification (Remote Attestation)#

Scenario: An organization provides profile verification records.

Proof Record:

{
  "$type": "network.bsky.verification.proof",
  "cid": "bafyreig7w5q432clkzxn5azlybqi37lnuvxvl3uucbqojgew4cujyoamzq",
  "type": "individual"
}

Note: The CID was generated with $sig containing:

{
  "$type": "network.bsky.verification.proof",
  "repository": "did:plc:cbkjy5n7bk3ax2wplmtjofq2",
  "type": "individual"
}

Identity's Profile Record with Reference:

{
  "$type": "app.bsky.actor.profile",
  "avatar": {
    "$type": "blob",
    "mimeType": "image/jpeg",
    "ref": {
      "$link": "bafkreie4cpq6eisks4gojzfypjqjqyetaqc53rq224vu2ic7wht3wgdtem"
    },
    "size": 622579
  },
  "banner": {
    "$type": "blob",
    "mimeType": "image/jpeg",
    "ref": {
      "$link": "bafkreigecokmzaufpshelpgtlbguq3fgcq5qxnemgjd4l3sfdkuggumsum"
    },
    "size": 758665
  },
  "description": "he/him; Protocols, platforms, and machine learning; worker at GitHub; Check out @smokesignal.events @atwork.place\n\nProfile: myself with a thick mustache, septum ring, and hat",
  "displayName": "Nick Gerakines",
  "signatures": [
    {
      "$type": "com.atproto.repo.strongRef",
      "cid": "bafyreigk73rnjpjfjjeeii25w2cczdq7tpzwrv4xeyo7gs47m75pqshbau",
      "uri": "at://did:plc:verify.bsky.network/network.bsky.verification.proof/3m3i7it5ya7cq"
    }
  ]
}

7.2 Badge Award (Inline Attestation)#

Scenario: Issue achievement badge to user

{
  "$type": "community.lexicon.badge.award",
  "badge": {
    "$type": "com.atproto.repo.strongRef",
    "cid": "bafyreibnfpriilyjmssycvlkcp46cmoscwon7okbfvhjmobggisinerj5e",
    "uri": "at://did:plc:tgudj2fjm77pzkuawquqhsxm/community.lexicon.badge.definition/3ltwfsgx3vu2a"
  },
  "did": "did:plc:cbkjy5n7bk3ax2wplmtjofq2",
  "issued": "2025-07-14T12:00:00.000Z",
  "signatures": [
    {
      "$type": "community.lexicon.badge.proof",
      "key": "did:plc:tgudj2fjm77pzkuawquqhsxm#badge",
      "signature": {
        "$bytes": "JjLKuf35PstZQhef36SHtGrPrlvWy6+Qt6xI2zINOBNAxh4pAAaq5X3tZdJc/3Y9wDZp1X7Sk97fMERexYMvBQ=="
      }
    }
  ]
}

Note: The signature was created from a CID generated with $sig containing:

{
  "$type": "community.lexicon.badge.proof",
  "repository": "did:plc:cbkjy5n7bk3ax2wplmtjofq2",
  "key": "did:plc:tgudj2fjm77pzkuawquqhsxm#badge"
}

7.3 Event RSVP with Capacity Control#

Scenario: Ticket information is back-referenced to RSVPs

Proof Record:

{
  "$type": "org.atmosphereconf.ticketProof",
  "cid": "bafyreid57no4elksfm3m5axi6tacs65rtjholez4qgdd3p5k5lfzeinwhm",
  "role": "organizer",
  "ticket": "full"
}

Note: The CID was generated with $sig containing:

{
  "$type": "org.atmosphereconf.ticketProof",
  "repository": "did:plc:lehcqqkwzcwvjvw66uthu5oq",
  "role": "organizer",
  "ticket": "full"
}

RSVP Record:

{
  "$type": "events.smokesignal.calendar.rsvp",
  "status": "events.smokesignal.calendar.rsvp#going",
  "subject": {
    "cid": "bafyreifwvuqm4hhkmrsaj3zxvpfqj7byjefucrb36burwbuu5uv5sk5jqa",
    "uri": "at://did:plc:lehcqqkwzcwvjvw66uthu5oq/events.smokesignal.calendar.event/3lcyaxnwvau2f"
  },
  "signatures": [
    {
      "$type": "com.atproto.repo.strongRef",
      "cid": "bafyreieo2yfcqvrkatitxhqyz54pmxvrafisnoocrpx5py5y3cawzh5slm",
      "uri": "at://did:plc:verify.bsky.network/org.atmosphereconf.ticketProof/3m3ia2cz7vqvo"
    }
  ]
}

7.4 Multi-Signer Document#

Scenario: Multiple authors and editors sign-off on their collaboration roles.

Article Record:

{
  "$type": "publishing.awesome.article",
  "createdAt": "2025-10-14T09:00:00Z",
  "parties": [
    "did:plc:party1",
    "did:plc:party2",
    "did:plc:party3"
  ],
  "signatures": [
    {
      "$type": "publishing.awesome.collaborationProof",
      "key": "did:plc:party1#authoring",
      "role": "editor",
      "signature": {
        "$bytes": "jQR7uDmHE5fNbY7Z/W0Y20LK4fFDLFmJtDHjFSMhoe9EbR8Bbo6jMACMm5mERGbWyqVFFkc8rGRqpdyj51ymHA=="
      }
    },
    {
      "$type": "com.atproto.repo.strongRef",
      "cid": "bafyreifryor4vmbibmtauvb2dre2uobsi7nguf75cm4fpnrvdlelwodyby",
      "uri": "at://did:plc:party2/publishing.awesome.collaborationProof/3m3ia64sghb4g"
    },
    {
      "$type": "com.atproto.repo.strongRef",
      "cid": "bafyreifryor4vmbibmtauvb2dre2uobsi7nguf75cm4fpnrvdlelwodyby",
      "uri": "at://did:plc:party3/publishing.awesome.collaborationProof/3m3ia7ke7fz27"
    }
  ],
  "text": "This is a multi-author article"
}

Note: All signatures and proof records were created with $sig containing the repository field set to the DID of the repository housing the article record (did:plc:articlerepo123).

Party2 Proof Record:

{
  "$type": "publishing.awesome.collaborationProof",
  "cid": "bafyreigppx6wwhoeuf25crbkwbjyqkqj5lnk2zuyjf7okmyai6ym7obnda",
  "role": "co-author"
}

Party3 Proof Record:

{
  "$type": "publishing.awesome.collaborationProof",
  "cid": "bafyreigppx6wwhoeuf25crbkwbjyqkqj5lnk2zuyjf7okmyai6ym7obnda",
  "role": "co-author"
}

8. Conclusion#

This specification defines a minimal yet comprehensive attestation framework for ATProtocol that:

  • Prevents Replay Attacks: The mandatory repository field in $sig binds attestations to specific repositories
  • Leverages Content Addressing: CIDs provide immutable, verifiable references
  • Embraces Simplicity: Inline attestations require only $type + signature + key; remote attestations only CID
  • Uses Union Types: The signatures array supports multiple types with required $type discriminator
  • Unifies CID Generation: Both patterns use the same CID generation process with mandatory $sig.$type and $sig.repository
  • Differentiates Usage: Inline attestations sign CID bytes; remote attestations store CID directly
  • Integrates with DIDs: Uses existing cryptographic infrastructure
  • Provides Security: Repository binding, scope control via $sig, robust verification, and replay protection