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), andkey(DID verification method reference) as required fields - Remote attestations: Strong references (
com.atproto.repo.strongRef) pointing to separate proof records containing acidas 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), andkey(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
$typefield in signatures enables application-specific attestation semantics - The
$sigmetadata 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
- Example:
-
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
- Format:
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):
- Keys sorted by complete CBOR byte representation
- Shorter byte representations sort before longer
- 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
$sigobject is only present during CID generation for both attestation types - It is NOT stored in the final record
- The
$sigmust always contain both$typeandrepositoryfields for attestations - The
repositoryfield 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
repositoryfield in$sigbinds 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
signaturesarray supports multiple types with required$typediscriminator - Unifies CID Generation: Both patterns use the same CID generation process with mandatory
$sig.$typeand$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