attestion_spec.md
edited
1# ATProtocol Record Structures for Inline and Remote Signature Structures (Attestations)
2
3## Technical Specification v1.0
4
5## Executive Summary
6
7This 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.
8
9The specification supports two minimal patterns within a unified `signatures` array using union types:
10- **Inline attestations**: Signature objects with `$type`, `signature` (bytes), and `key` (DID verification method reference) as required fields
11- **Remote attestations**: Strong references (`com.atproto.repo.strongRef`) pointing to separate proof records containing a `cid` as a required field
12
13Both 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.
14
15The `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.
16
17---
18
19## 1. Requirements
20
21**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.
22
23**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.
24
25**Key Management Flexibility**: The attestation framework must support both inline and remote patterns to accommodate different operational constraints:
26- **Remote attestations** enable proof creation using only existing DID verification methods, with no attestation-specific key management required
27- **Inline attestations** embed signatures directly in records, eliminating the need to create and maintain separate proof records
28
29**Radical Simplicity**: The attestation structure must minimize complexity while maximizing security:
30- Inline attestations require only three fields: `$type`, `signature` (bytes), and `key` (verification method reference)
31- Remote attestations require only one field in the proof record: `cid`
32- Fewer fields reduce attack surface and implementation burden
33
34**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.
35
36**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.
37
38**Application Extensibility**: Applications must be able to define custom attestation types and inject application-specific metadata into the attestation creation process:
39- The `$type` field in signatures enables application-specific attestation semantics
40- The `$sig` metadata object allows applications to include contextual information in CID generation, binding attestations to specific use cases and preventing cross-context attacks
41
42**DID Integration**: The framework must leverage existing ATProtocol DID infrastructure with verification methods for cryptographic operations, eliminating dependency on external key management systems.
43
44**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.
45
46---
47
48## 2. Record Structure Definitions
49
50The `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.
51
52### 2.1 Inline Attestation Structure
53
54Inline 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).
55
56#### 2.1.1 Example Record
57
58```json
59{
60 "$type": "app.example.record",
61 "createdAt": "2025-10-14T12:00:00Z",
62 "text": "Example content that is being attested",
63 "signatures": [
64 {
65 "$type": "com.example.inlineSignature",
66 "signature": {"$bytes": "MzQ2Y2U4ZDNhYmM5NjU0Mzk5NWJmNjJkOGE4..."},
67 "key": "did:web:example.com#signing1"
68 }
69 ]
70}
71```
72
73#### 2.1.2 Inline Signature Fields
74
75When using inline signatures in the `signatures` array:
76
77**Required Fields:**
78
79- **`$type`** (string, NSID format): Type identifier for the signature object (required for union types)
80 - Example: `"com.example.inlineSignature"`
81 - Must NOT be `"com.atproto.repo.strongRef"` (that's for remote attestations)
82 - Allows applications to define custom signature types
83
84- **`signature`** (object with `$bytes`): Base64-encoded signature bytes
85 - Generated from signing the CID bytes of the record
86 - ECDSA signature using P-256 or K-256 curve
87 - Must use "low-S" variant (per BIP-0062)
88 - Format: `{"$bytes": "base64-encoded-signature"}`
89
90- **`key`** (string): Full verification method reference within a DID document
91 - Format: `did:{method}:{identifier}#{fragment}`
92 - Example: `did:web:example.com#signing1`
93 - The DID component identifies who signed the record
94 - The fragment identifies which verification method in the DID document was used
95
96#### 2.1.3 Signatures Array Lexicon Schema
97
98```json
99{
100 "signatures": {
101 "type": "array",
102 "description": "Array of cryptographic signatures attesting to record",
103 "items": {
104 "type": "union",
105 "refs": [
106 "com.atproto.repo.strongRef",
107 "com.example.inlineSignature"
108 ]
109 }
110 }
111}
112```
113
114**Inline Signature Type Definition:**
115```json
116{
117 "lexicon": 1,
118 "id": "com.example.inlineSignature",
119 "defs": {
120 "main": {
121 "type": "object",
122 "required": ["signature", "key"],
123 "properties": {
124 "signature": {
125 "type": "bytes",
126 "description": "Signature bytes"
127 },
128 "key": {
129 "type": "string",
130 "description": "Full verification method reference (DID#fragment)"
131 }
132 }
133 }
134 }
135}
136```
137
138### 2.2 Remote Attestation Structure
139
140Remote attestations are **separate records** that contain a CID of the content being attested.
141
142#### 2.2.1 Remote Attestation Record
143
144```json
145{
146 "$type": "com.example.proof",
147 "cid": "bafyreifsqhrnlciktfxkz4yiqw5wtx6xvods67aicqt5tc7cly24dmhv3e"
148}
149```
150
151The 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.
152
153#### 2.2.2 Attested Record with Remote Reference
154
155Records can reference remote attestations through strong-refs in their signatures array:
156
157```json
158{
159 "$type": "app.bsky.feed.post",
160 "createdAt": "2025-10-14T20:09:00.733Z",
161 "text": "Content that is being attested",
162 "signatures": [
163 {
164 "$type": "com.atproto.repo.strongRef",
165 "cid": "bafyreig5ug2vj63ag5b6okth3roujv2lngxnyssxeylfcmmqiznfje4enu",
166 "uri": "at://did:plc:cbkjy5n7bk3ax2wplmtjofq2/com.example.proof/3m3i7e3uhrocj"
167 }
168 ]
169}
170```
171
172The proof record's CID was generated from the content with a `$sig` object containing attestation metadata including the repository DID.
173
174#### 2.2.3 Remote Attestation Lexicon Schema
175
176**Proof Record Schema:**
177```json
178{
179 "lexicon": 1,
180 "id": "com.example.proof",
181 "defs": {
182 "main": {
183 "type": "record",
184 "description": "Remote attestation proof containing CID of attested content",
185 "key": "tid",
186 "record": {
187 "type": "object",
188 "required": ["cid"],
189 "properties": {
190 "cid": {
191 "type": "string",
192 "format": "cid",
193 "description": "CID of the attested content (record without signatures field)"
194 }
195 }
196 }
197 }
198 }
199}
200```
201
202**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.
203
204#### 2.2.4 Pattern Comparison
205
206| Aspect | Inline Attestation | Remote Attestation |
207|--------|-------------------|-------------------|
208| **Location** | Signature in same record as content | Separate proof record with CID |
209| **Storage** | Recipient's repository | Attester's repository |
210| **Fields** | `signature` + `key` only | `cid` only |
211| **CID Usage** | CID bytes are signed | CID is stored directly |
212| **$sig Required** | Yes (with `$type` + `repository`) | Yes (with `$type` + `repository`) |
213| **Use Case** | Direct attestations, self-signing | Revocation, external proofs |
214| **Discoverability** | Direct (in record) | Requires query/index or strong-ref |
215| **Reference** | N/A | Strong-ref from attested record |
216
217---
218
219## 3. CID Generation Algorithms and Requirements
220
221### 3.1 CID Format Specification
222
223ATProtocol attestations use **CIDv1** with the following "blessed" format:
224
225```
226CIDv1:
227 Multibase: base32 (for strings), raw binary (for DAG-CBOR)
228 Multicodec: dag-cbor (0x71)
229 Multihash: sha2-256 (0x12), 32 bytes
230```
231
232**Binary Structure:**
233```
2340x01 # CID version 1
2350x71 # dag-cbor codec
2360x12 # SHA-256 hash function
2370x20 # Hash length (32 bytes)
238<32 bytes of hash> # SHA-256 digest
239```
240
241### 3.2 CID Generation Algorithm
242
243#### 3.2.1 Standard Process
244
245```
246Step 1: Prepare Record Data
247 - Remove "signatures" field from record
248 - Include "$sig" metadata object (required for attestations)
249 * Must always include "$type" field
250 * Must always include "repository" field (DID of housing repository)
251 * Additional fields based on attestation type
252 - Ensure all fields in canonical form
253
254Step 2: Encode to DAG-CBOR
255 - Apply canonical serialization rules:
256 * Sort map keys by length, then lexically
257 * Use shortest integer encodings
258 * Definite-length items only
259 * Tag 42 for CID links
260 - Output: Binary CBOR bytes
261
262Step 3: Hash with SHA-256
263 - Input: DAG-CBOR bytes from Step 2
264 - Output: 32-byte binary hash digest
265
266Step 4: Construct Multihash
267 - Prepend hash function code: 0x12 (SHA-256)
268 - Prepend hash length: 0x20 (32 bytes)
269 - Result: 34-byte multihash
270
271Step 5: Construct CID
272 - Prepend CID version: 0x01
273 - Prepend multicodec: 0x71 (dag-cbor)
274 - Append multihash from Step 4
275 - Result: 36-byte CID
276
277Step 6: Usage by Attestation Type
278 - For inline attestations: Sign the CID bytes with private key
279 - For remote attestations: Store the CID in proof record
280 - For string representation: Base32-encode with 'b' prefix
281```
282
283#### 3.2.2 Pseudocode Implementation
284
285```pseudocode
286FUNCTION generateRecordCID(recordData, sigMetadata, repositoryDID):
287 # Step 1: Prepare data with $sig
288 encodingData = copy(recordData)
289
290 # Remove signatures field if present
291 DELETE encodingData["signatures"]
292
293 # Add $sig metadata (must have $type and repository)
294 IF NOT sigMetadata["$type"]:
295 THROW Error("$sig must include $type field")
296
297 # CRITICAL: Always set repository field to prevent replay attacks
298 sigMetadata["repository"] = repositoryDID
299 encodingData["$sig"] = sigMetadata
300
301 # Step 2: Encode to DAG-CBOR
302 cborBytes = encodeDAGCBORCanonical(encodingData)
303
304 # Step 3: Hash
305 hashDigest = SHA256(cborBytes) # 32 bytes
306
307 # Step 4: Construct multihash
308 multihash = CONCAT(
309 [0x12], # SHA-256 code
310 [0x20], # 32 bytes length
311 hashDigest # 32-byte hash
312 )
313
314 # Step 5: Construct CID
315 cidBytes = CONCAT(
316 [0x01], # CIDv1
317 [0x71], # dag-cbor
318 multihash # 34 bytes
319 )
320
321 RETURN cidBytes # 36 bytes total
322END FUNCTION
323
324# Wrapper function used in attestation generation
325FUNCTION generateCIDFromCBOR(cborBytes):
326 hashDigest = SHA256(cborBytes)
327 multihash = CONCAT([0x12], [0x20], hashDigest)
328 cidBytes = CONCAT([0x01], [0x71], multihash)
329 RETURN cidBytes
330END FUNCTION
331
332FUNCTION encodeDAGCBORCanonical(data):
333 # Recursive encoding with strict rules
334
335 IF data is NULL:
336 RETURN encodeCBORNull()
337
338 IF data is BOOLEAN:
339 RETURN encodeCBORBool(data)
340
341 IF data is INTEGER:
342 RETURN encodeCBORInt(data) # Shortest encoding
343
344 IF data is STRING:
345 utf8Bytes = encodeUTF8(data)
346 RETURN CONCAT(
347 majorType3WithLength(LENGTH(utf8Bytes)),
348 utf8Bytes
349 )
350
351 IF data is BYTES:
352 RETURN CONCAT(
353 majorType2WithLength(LENGTH(data)),
354 data
355 )
356
357 IF data is CID:
358 # Tag 42 encoding
359 cidWithPrefix = CONCAT([0x00], data.toBytes())
360 RETURN CONCAT(
361 [0xd8, 0x2a], # Tag 42
362 majorType2WithLength(LENGTH(cidWithPrefix)),
363 cidWithPrefix
364 )
365
366 IF data is ARRAY:
367 encoded = []
368 FOR item IN data:
369 encoded.APPEND(encodeDAGCBORCanonical(item))
370
371 RETURN CONCAT(
372 majorType4WithLength(LENGTH(data)),
373 CONCATENATE_ALL(encoded)
374 )
375
376 IF data is MAP:
377 # CRITICAL: Sort keys before encoding
378 sortedKeys = sortKeysCanonical(data.KEYS())
379
380 encoded = []
381 FOR key IN sortedKeys:
382 encodedKey = encodeDAGCBORCanonical(key)
383 encodedValue = encodeDAGCBORCanonical(data[key])
384 encoded.APPEND(CONCAT(encodedKey, encodedValue))
385
386 RETURN CONCAT(
387 majorType5WithLength(LENGTH(sortedKeys)),
388 CONCATENATE_ALL(encoded)
389 )
390
391 THROW Error("Unsupported type for DAG-CBOR")
392END FUNCTION
393
394FUNCTION sortKeysCanonical(keys):
395 # Convert keys to CBOR bytes first
396 keyPairs = []
397 FOR key IN keys:
398 cborBytes = encodeDAGCBORCanonical(key)
399 keyPairs.APPEND((key, cborBytes))
400
401 # Sort by length first, then lexically
402 sortedPairs = SORT(keyPairs, COMPARATOR=lambda a, b:
403 IF LENGTH(a.cborBytes) != LENGTH(b.cborBytes):
404 RETURN COMPARE(LENGTH(a.cborBytes), LENGTH(b.cborBytes))
405 ELSE:
406 RETURN byteWiseCompare(a.cborBytes, b.cborBytes)
407 )
408
409 RETURN [pair.key FOR pair IN sortedPairs]
410END FUNCTION
411```
412
413### 3.3 DAG-CBOR Strictness Requirements
414
415**Map Key Sorting (RFC 7049 Section 3.9):**
4161. Keys sorted by complete CBOR byte representation
4172. Shorter byte representations sort before longer
4183. Same-length representations sorted lexically (byte-wise)
419
420**Example Sorting:**
421```
422Original: {"text": ..., "$type": ..., "repository": ..., "createdAt": ...}
423
424CBOR bytes:
425 "text" → 0x64 74 65 78 74 (5 bytes)
426 "$type" → 0x65 24 74 79 70 65 (6 bytes)
427 "createdAt" → 0x69 63 72 65 61 74 65 64 41 74 (11 bytes)
428 "repository" → 0x6a 72 65 70 6f 73 69 74 6f 72 79 (11 bytes)
429
430Sorted: ["text", "$type", "createdAt", "repository"]
431```
432
433**Integer Encoding:**
434- 0-23: Encoded in initial byte
435- 24-255: 1-byte extension (0x18 prefix)
436- 256-65535: 2-byte extension (0x19 prefix)
437- Must use shortest possible encoding
438
439**Tag 42 (CID Links):**
440- Only valid form: `0xd8 0x2a`
441- Must not use alternative encodings (0xf8 0x2a is invalid)
442
443**Floating Point:**
444- ATProtocol records should avoid floats
445- If required, use 64-bit double precision only
446- NaN, Infinity, -Infinity not supported
447
448**Forbidden:**
449- Tags other than 42
450- Indefinite-length items
451- Non-string map keys
452- Simple values except true (21), false (20), null (22)
453
454---
455
456## 4. Signature Format and Key Resolution
457
458### 4.1 The $sig Metadata Object
459
460The **`$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.
461
462#### 4.1.1 Structure
463
464```json
465{
466 "$sig": {
467 "$type": "com.example.attestation.type",
468 "repository": "did:plc:abc123", // Always required for attestations
469 "key": "did:plc:signer123#method" // For inline attestations
470 }
471}
472```
473
474#### 4.1.2 Required and Optional Fields
475
476**Required Fields:**
477- **`$type`** (string, NSID): Type identifier for the attestation (always required)
478- **`repository`** (string): DID of the repository housing the record (always required for attestations)
479 - Must be a valid DID format string
480 - Prevents replay attacks where an attacker clones records to a different repository
481 - Value must be overwritten if present to ensure correct repository binding
482
483**Common Fields:**
484- **`key`** (string): The full verification method reference (for inline attestations)
485
486**Important**:
487- The `$sig` object is **only present during CID generation** for both attestation types
488- It is **NOT stored** in the final record
489- The `$sig` must always contain both `$type` and `repository` fields for attestations
490- The `repository` field ensures the CID is bound to a specific repository, preventing cross-repository replay attacks
491- For inline attestations: The CID (including $sig) is signed to create the signature
492- For remote attestations: The CID (including $sig) is stored directly in the proof record
493
494### 4.2 Signature Generation Process (Inline Attestations)
495
496```pseudocode
497FUNCTION generateInlineSignature(record, repositoryDID, signerDID, signingKey, fragmentID, signatureType):
498 # Step 1: Add $sig metadata for CID generation
499 sigMetadata = {
500 "$type": "com.example.inlineSignature", # Always required
501 "repository": repositoryDID, # Always required - prevents replay attacks
502 "key": signerDID + "#" + fragmentID,
503 }
504
505 # Step 2: Prepare record for CID generation
506 recordForCID = copy(record)
507 DELETE recordForCID["signatures"]
508
509 # Step 3: Generate CID (which includes $sig with repository)
510 recordForCID["$sig"] = sigMetadata
511 cborBytes = encodeDAGCBORCanonical(recordForCID)
512 cidBytes = generateCIDFromCBOR(cborBytes)
513
514 # Step 4: Sign the CID bytes (not the CBOR directly)
515 signature = ECDSA_Sign(signingKey, cidBytes)
516
517 # Step 5: Ensure low-S format
518 IF NOT isLowS(signature):
519 signature = convertToLowS(signature)
520
521 # Step 6: Create signature block with required $type
522 signatureBlock = {
523 "$type": "com.example.inlineSignature",
524 "signature": {"$bytes": base64Encode(signature)},
525 "key": signerDID + "#" + fragmentID
526 }
527
528 # Step 7: Add to record (without $sig)
529 finalRecord = copy(record)
530 IF NOT finalRecord.signatures:
531 finalRecord["signatures"] = []
532 finalRecord["signatures"].APPEND(signatureBlock)
533
534 RETURN finalRecord
535END FUNCTION
536```
537
538### 4.3 Remote Attestation Generation Process
539
540```pseudocode
541FUNCTION generateRemoteProof(record, repositoryDID, proverDID):
542 # Step 1: Add $sig metadata for CID generation
543 sigMetadata = {
544 "$type": "com.example.proof", # Always required
545 "repository": repositoryDID # Always required - prevents replay attacks
546 }
547
548 # Step 2: Prepare record for CID generation
549 recordForCID = copy(record)
550 DELETE recordForCID["signatures"]
551
552 # Step 3: Generate CID (which includes $sig with repository)
553 recordForCID["$sig"] = sigMetadata
554 cborBytes = encodeDAGCBORCanonical(recordForCID)
555 cid = generateCIDFromCBOR(cborBytes)
556
557 # Step 4: Create minimal proof record
558 proofRecord = {
559 "$type": "com.example.proof",
560 "cid": cid
561 }
562
563 # Step 5: Store proof record and return reference
564 proofURI = storeRecord(proofRecord)
565
566 RETURN {
567 proofRecord: proofRecord,
568 proofURI: proofURI,
569 contentCID: cid
570 }
571END FUNCTION
572```
573
574### 4.4 Key Resolution Process
575
576#### 4.4.1 DID Document Structure
577
578```json
579{
580 "@context": ["https://www.w3.org/ns/did/v1"],
581 "id": "did:plc:abc123",
582 "alsoKnownAs": ["at://alice.bsky.social"],
583 "verificationMethod": [
584 {
585 "id": "did:plc:abc123#atproto",
586 "type": "Multikey",
587 "controller": "did:plc:abc123",
588 "publicKeyMultibase": "zQ3shqwJEJyMBsBXCWyCBpUBMqxcon9oHB7mCvx4sSpMdLJwc"
589 }
590 ],
591 "assertionMethod": [
592 {
593 "id": "did:plc:abc123#attesting",
594 "type": "Multikey",
595 "controller": "did:plc:abc123",
596 "publicKeyMultibase": "zDnaembgSGUhZULN2Caob4HLJPaxBh92N7rtH21TErzqf8HQo"
597 }
598 ],
599 "service": [
600 {
601 "id": "#atproto_pds",
602 "type": "AtprotoPersonalDataServer",
603 "serviceEndpoint": "https://pds.example.com"
604 }
605 ]
606}
607```
608
609#### 4.4.2 Resolution Algorithm
610
611```pseudocode
612FUNCTION resolveVerificationMethod(keyReference):
613 # Parse DID and fragment
614 [did, fragment] = splitDIDFragment(keyReference)
615
616 # Resolve DID document
617 didDocument = resolveDID(did)
618 IF didDocument is NULL:
619 THROW Error("Cannot resolve DID from key reference")
620
621 # Find verification method
622 FOR method IN didDocument.verificationMethod:
623 IF method.id == keyReference:
624 RETURN method
625
626 # Also check short form (without DID prefix)
627 IF method.id == "#" + fragment:
628 RETURN method
629 FOR method IN didDocument.assertionMethod:
630 IF method.id == keyReference:
631 RETURN method
632
633 # Also check short form (without DID prefix)
634 IF method.id == "#" + fragment:
635 RETURN method
636
637 THROW Error("Verification method not found: " + keyReference)
638END FUNCTION
639
640FUNCTION parsePublicKey(verificationMethod):
641 # Extract public key from multibase encoding
642 multibaseKey = verificationMethod.publicKeyMultibase
643
644 # Decode multibase (base58btc with 'z' prefix)
645 IF NOT multibaseKey.startsWith("z"):
646 THROW Error("Invalid multibase encoding")
647
648 keyBytes = base58Decode(multibaseKey.substring(1))
649
650 # Extract multicodec
651 [codecVarint, offset] = decodeVarint(keyBytes)
652
653 # Determine key type
654 IF codecVarint == 0xE701: # secp256k1-pub
655 keyType = "K-256"
656 compressedKey = keyBytes[offset:]
657 ELSE IF codecVarint == 0x1200: # p256-pub
658 keyType = "P-256"
659 compressedKey = keyBytes[offset:]
660 ELSE:
661 THROW Error("Unsupported key type")
662
663 # Decompress public key (curve-specific)
664 publicKey = decompressPublicKey(compressedKey, keyType)
665
666 RETURN (publicKey, keyType)
667END FUNCTION
668```
669
670### 4.5 Supported Cryptographic Algorithms
671
672**Elliptic Curves:**
673- **P-256** (secp256r1, NIST P-256): Widely supported, WebCrypto compatible
674- **K-256** (secp256k1): Bitcoin/Ethereum curve, default in ATProtocol
675
676**Signature Algorithm:**
677- ECDSA with CID bytes as input (not raw hash)
678- Low-S signature variant mandatory (BIP-0062)
679- Signs the complete 36-byte CID, not the underlying SHA-256 hash
680
681**Hash Function:**
682- SHA-256 (256-bit output) for CID generation
683
684**Encoding:**
685- Public keys: Multibase (base58btc) with multicodec prefix, compressed format
686- Signatures: Raw ECDSA bytes (r, s) encoded as base64 in JSON
687- CIDs: Base32-encoded for strings, raw bytes for signing
688
689---
690
691## 5. Verification Workflows
692
693### 5.1 Inline Attestation Verification
694
695```pseudocode
696FUNCTION verifyInlineAttestation(record, repositoryDID):
697 IF NOT record.signatures OR LENGTH(record.signatures) == 0:
698 RETURN {valid: false, reason: "No signatures present"}
699
700 results = []
701
702 FOR signatureBlock IN record.signatures:
703 # Skip strongRefs (they're remote attestations)
704 IF signatureBlock["$type"] == "com.atproto.repo.strongRef":
705 CONTINUE
706
707 # Verify inline signature
708 IF signatureBlock.signature AND signatureBlock.key:
709 result = verifySingleSignature(record, signatureBlock, repositoryDID)
710 results.APPEND(result)
711
712 RETURN results
713END FUNCTION
714
715FUNCTION verifySingleSignature(record, signatureBlock, repositoryDID):
716 # Step 1: Verify required fields
717 IF NOT signatureBlock["$type"]:
718 RETURN {valid: false, reason: "Missing $type field"}
719
720 # Step 2: Resolve verification method from key field
721 verificationMethod = resolveVerificationMethod(signatureBlock.key)
722 IF verificationMethod is NULL:
723 RETURN {valid: false, reason: "Cannot resolve verification method"}
724
725 (publicKey, keyType) = parsePublicKey(verificationMethod)
726
727 # Step 3: Reconstruct signed content with $sig
728 recordForSigning = copy(record)
729 DELETE recordForSigning["signatures"]
730
731 # Add $sig metadata (must include $type and repository)
732 sigMetadata = copy(signatureBlock)
733 DELETE sigMetadata["signature"]
734
735 # CRITICAL: Always set repository field for replay attack prevention
736 sigMetadata["repository"] = repositoryDID
737 recordForSigning["$sig"] = sigMetadata
738
739 # Step 4: Generate CID (same process as signing)
740 cborBytes = encodeDAGCBORCanonical(recordForSigning)
741 cidBytes = generateCIDFromCBOR(cborBytes)
742
743 # Step 5: Decode signature
744 signatureBytes = base64Decode(signatureBlock.signature["$bytes"])
745
746 # Step 6: Verify signature is low-S
747 IF NOT isLowS(signatureBytes):
748 RETURN {valid: false, reason: "Signature not in low-S format"}
749
750 # Step 7: Cryptographic verification against CID bytes
751 isValid = ECDSA_Verify(publicKey, cidBytes, signatureBytes, keyType)
752
753 IF isValid:
754 RETURN {
755 valid: true,
756 type: signatureBlock["$type"],
757 key: signatureBlock.key,
758 signer: extractDIDFromKey(signatureBlock.key),
759 repository: repositoryDID
760 }
761 ELSE:
762 RETURN {valid: false, reason: "Signature verification failed"}
763END FUNCTION
764```
765
766### 5.2 Remote Attestation Verification
767
768```pseudocode
769FUNCTION verifyRemoteAttestation(proofRecord, subjectRecord, repositoryDID):
770 # Step 1: Verify the proof record exists and has CID
771 IF NOT proofRecord.cid:
772 RETURN {valid: false, reason: "Proof record missing CID"}
773
774 # Step 2: Reconstruct subject record with $sig for CID generation
775 subjectRecordForCID = copy(subjectRecord)
776 DELETE subjectRecordForCID["signatures"]
777
778 # Add $sig metadata (must include $type and repository)
779 # Note: The exact $sig content must match what was used during proof creation
780 sigMetadata = copy(proofRecord)
781 DELETE sigMetadata["cid"]
782
783 # CRITICAL: Always set repository field for replay attack prevention
784 sigMetadata["repository"] = repositoryDID
785 subjectRecordForCID["$sig"] = sigMetadata
786
787 # Step 3: Generate CID from subject record
788 cborBytes = encodeDAGCBORCanonical(subjectRecordForCID)
789 subjectCID = generateCIDFromCBOR(cborBytes)
790
791 # Step 4: Verify CID match
792 IF proofRecord.cid != subjectCID:
793 RETURN {
794 valid: false,
795 reason: "CID mismatch - possible replay attack or incorrect repository",
796 expected: proofRecord.cid,
797 actual: subjectCID
798 }
799
800 # Step 5: Extract proof metadata
801 proofURI = constructATURI(proofRecord)
802 proofDID = extractDIDFromURI(proofURI)
803
804 RETURN {
805 valid: true,
806 proofCID: proofRecord.cid,
807 proofURI: proofURI,
808 prover: proofDID,
809 subjectCID: subjectCID,
810 repository: repositoryDID
811 }
812END FUNCTION
813```
814
815### 5.3 CID Chain Verification
816
817For verifying content integrity through repository Merkle tree:
818
819```pseudocode
820FUNCTION verifyRecordInRepository(commit, recordPath):
821 # Step 1: Verify commit signature
822 IF NOT verifyCommitSignature(commit):
823 RETURN {valid: false, reason: "Invalid commit signature"}
824
825 # Step 2: Extract repository DID from commit
826 repositoryDID = commit.did
827
828 # Step 3: Fetch MST root
829 mstRoot = fetchBlock(commit.data)
830
831 # Step 4: Navigate MST to find record
832 (record, proof) = findRecordInMST(mstRoot, recordPath)
833 IF record is NULL:
834 RETURN {valid: false, reason: "Record not found in MST"}
835
836 # Step 5: Verify CID chain (with repository binding)
837 recordCID = generateRecordCID(record, getRecordSigMetadata(record), repositoryDID)
838 IF NOT verifyMSTPath(mstRoot, recordPath, recordCID):
839 RETURN {valid: false, reason: "MST path verification failed"}
840
841 # Step 6: Verify root CID matches commit
842 rootCID = generateRecordCID(mstRoot)
843 IF rootCID != commit.data:
844 RETURN {valid: false, reason: "Root CID mismatch"}
845
846 RETURN {
847 valid: true,
848 record: record,
849 recordCID: recordCID,
850 proof: proof,
851 repository: repositoryDID
852 }
853END FUNCTION
854```
855
856---
857
858## 6. Integration Patterns with ATProtocol Infrastructure
859
860### 6.1 PDS OAuth Integration
861
862**Signature Request Flow:**
863
864```
8651. Client requests OAuth scope: "identity:attestation"
8662. PDS prompts user for consent
8673. User approves specific verification method usage
8684. Client receives scoped token
8695. Client calls com.atproto.attestation.sign with repository DID
8706. PDS signs record using authorized verification method
8717. Returns signed record to client
872```
873
874**XRPC Method for Inline Signing:**
875```json
876{
877 "lexicon": 1,
878 "id": "com.atproto.attestation.sign",
879 "defs": {
880 "main": {
881 "type": "procedure",
882 "description": "Sign a CID with repository binding",
883 "input": {
884 "encoding": "application/json",
885 "schema": {
886 "type": "object",
887 "required": ["cid"],
888 "properties": {
889 "cid": {
890 "type": "string",
891 "format": "cid",
892 "description": "The CID to sign."
893 },
894 "key": {
895 "type": "string",
896 "description": "Fragment ID of verification method"
897 }
898 }
899 }
900 },
901 "output": {
902 "encoding": "application/json",
903 "schema": {
904 "type": "object",
905 "required": ["signature"],
906 "properties": {
907 "signature": {
908 "type": "bytes",
909 "description": "The signature"
910 }
911 }
912 }
913 }
914 }
915 }
916}
917```
918
919## 7. Examples and Use Cases
920
921### 7.1 Verification (Remote Attestation)
922
923**Scenario:** An organization provides profile verification records.
924
925**Proof Record:**
926```json
927{
928 "$type": "network.bsky.verification.proof",
929 "cid": "bafyreig7w5q432clkzxn5azlybqi37lnuvxvl3uucbqojgew4cujyoamzq",
930 "type": "individual"
931}
932```
933
934Note: The CID was generated with `$sig` containing:
935```json
936{
937 "$type": "network.bsky.verification.proof",
938 "repository": "did:plc:cbkjy5n7bk3ax2wplmtjofq2",
939 "type": "individual"
940}
941```
942
943**Identity's Profile Record with Reference:**
944```json
945{
946 "$type": "app.bsky.actor.profile",
947 "avatar": {
948 "$type": "blob",
949 "mimeType": "image/jpeg",
950 "ref": {
951 "$link": "bafkreie4cpq6eisks4gojzfypjqjqyetaqc53rq224vu2ic7wht3wgdtem"
952 },
953 "size": 622579
954 },
955 "banner": {
956 "$type": "blob",
957 "mimeType": "image/jpeg",
958 "ref": {
959 "$link": "bafkreigecokmzaufpshelpgtlbguq3fgcq5qxnemgjd4l3sfdkuggumsum"
960 },
961 "size": 758665
962 },
963 "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",
964 "displayName": "Nick Gerakines",
965 "signatures": [
966 {
967 "$type": "com.atproto.repo.strongRef",
968 "cid": "bafyreigk73rnjpjfjjeeii25w2cczdq7tpzwrv4xeyo7gs47m75pqshbau",
969 "uri": "at://did:plc:verify.bsky.network/network.bsky.verification.proof/3m3i7it5ya7cq"
970 }
971 ]
972}
973```
974
975### 7.2 Badge Award (Inline Attestation)
976
977**Scenario:** Issue achievement badge to user
978
979```json
980{
981 "$type": "community.lexicon.badge.award",
982 "badge": {
983 "$type": "com.atproto.repo.strongRef",
984 "cid": "bafyreibnfpriilyjmssycvlkcp46cmoscwon7okbfvhjmobggisinerj5e",
985 "uri": "at://did:plc:tgudj2fjm77pzkuawquqhsxm/community.lexicon.badge.definition/3ltwfsgx3vu2a"
986 },
987 "did": "did:plc:cbkjy5n7bk3ax2wplmtjofq2",
988 "issued": "2025-07-14T12:00:00.000Z",
989 "signatures": [
990 {
991 "$type": "community.lexicon.badge.proof",
992 "key": "did:plc:tgudj2fjm77pzkuawquqhsxm#badge",
993 "signature": {
994 "$bytes": "JjLKuf35PstZQhef36SHtGrPrlvWy6+Qt6xI2zINOBNAxh4pAAaq5X3tZdJc/3Y9wDZp1X7Sk97fMERexYMvBQ=="
995 }
996 }
997 ]
998}
999```
1000
1001Note: The signature was created from a CID generated with `$sig` containing:
1002```json
1003{
1004 "$type": "community.lexicon.badge.proof",
1005 "repository": "did:plc:cbkjy5n7bk3ax2wplmtjofq2",
1006 "key": "did:plc:tgudj2fjm77pzkuawquqhsxm#badge"
1007}
1008```
1009
1010### 7.3 Event RSVP with Capacity Control
1011
1012**Scenario:** Ticket information is back-referenced to RSVPs
1013
1014**Proof Record:**
1015```json
1016{
1017 "$type": "org.atmosphereconf.ticketProof",
1018 "cid": "bafyreid57no4elksfm3m5axi6tacs65rtjholez4qgdd3p5k5lfzeinwhm",
1019 "role": "organizer",
1020 "ticket": "full"
1021}
1022```
1023
1024Note: The CID was generated with `$sig` containing:
1025```json
1026{
1027 "$type": "org.atmosphereconf.ticketProof",
1028 "repository": "did:plc:lehcqqkwzcwvjvw66uthu5oq",
1029 "role": "organizer",
1030 "ticket": "full"
1031}
1032```
1033
1034**RSVP Record:**
1035```json
1036{
1037 "$type": "events.smokesignal.calendar.rsvp",
1038 "status": "events.smokesignal.calendar.rsvp#going",
1039 "subject": {
1040 "cid": "bafyreifwvuqm4hhkmrsaj3zxvpfqj7byjefucrb36burwbuu5uv5sk5jqa",
1041 "uri": "at://did:plc:lehcqqkwzcwvjvw66uthu5oq/events.smokesignal.calendar.event/3lcyaxnwvau2f"
1042 },
1043 "signatures": [
1044 {
1045 "$type": "com.atproto.repo.strongRef",
1046 "cid": "bafyreieo2yfcqvrkatitxhqyz54pmxvrafisnoocrpx5py5y3cawzh5slm",
1047 "uri": "at://did:plc:verify.bsky.network/org.atmosphereconf.ticketProof/3m3ia2cz7vqvo"
1048 }
1049 ]
1050}
1051```
1052
1053### 7.4 Multi-Signer Document
1054
1055**Scenario:** Multiple authors and editors sign-off on their collaboration roles.
1056
1057**Article Record:**
1058```json
1059{
1060 "$type": "publishing.awesome.article",
1061 "createdAt": "2025-10-14T09:00:00Z",
1062 "parties": [
1063 "did:plc:party1",
1064 "did:plc:party2",
1065 "did:plc:party3"
1066 ],
1067 "signatures": [
1068 {
1069 "$type": "publishing.awesome.collaborationProof",
1070 "key": "did:plc:party1#authoring",
1071 "role": "editor",
1072 "signature": {
1073 "$bytes": "jQR7uDmHE5fNbY7Z/W0Y20LK4fFDLFmJtDHjFSMhoe9EbR8Bbo6jMACMm5mERGbWyqVFFkc8rGRqpdyj51ymHA=="
1074 }
1075 },
1076 {
1077 "$type": "com.atproto.repo.strongRef",
1078 "cid": "bafyreifryor4vmbibmtauvb2dre2uobsi7nguf75cm4fpnrvdlelwodyby",
1079 "uri": "at://did:plc:party2/publishing.awesome.collaborationProof/3m3ia64sghb4g"
1080 },
1081 {
1082 "$type": "com.atproto.repo.strongRef",
1083 "cid": "bafyreifryor4vmbibmtauvb2dre2uobsi7nguf75cm4fpnrvdlelwodyby",
1084 "uri": "at://did:plc:party3/publishing.awesome.collaborationProof/3m3ia7ke7fz27"
1085 }
1086 ],
1087 "text": "This is a multi-author article"
1088}
1089```
1090
1091Note: 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`).
1092
1093**Party2 Proof Record:**
1094```json
1095{
1096 "$type": "publishing.awesome.collaborationProof",
1097 "cid": "bafyreigppx6wwhoeuf25crbkwbjyqkqj5lnk2zuyjf7okmyai6ym7obnda",
1098 "role": "co-author"
1099}
1100```
1101
1102**Party3 Proof Record:**
1103```json
1104{
1105 "$type": "publishing.awesome.collaborationProof",
1106 "cid": "bafyreigppx6wwhoeuf25crbkwbjyqkqj5lnk2zuyjf7okmyai6ym7obnda",
1107 "role": "co-author"
1108}
1109```
1110
1111## 8. Conclusion
1112
1113This specification defines a minimal yet comprehensive attestation framework for ATProtocol that:
1114
1115- **Prevents Replay Attacks**: The mandatory `repository` field in `$sig` binds attestations to specific repositories
1116- **Leverages Content Addressing**: CIDs provide immutable, verifiable references
1117- **Embraces Simplicity**: Inline attestations require only $type + signature + key; remote attestations only CID
1118- **Uses Union Types**: The `signatures` array supports multiple types with required `$type` discriminator
1119- **Unifies CID Generation**: Both patterns use the same CID generation process with mandatory `$sig.$type` and `$sig.repository`
1120- **Differentiates Usage**: Inline attestations sign CID bytes; remote attestations store CID directly
1121- **Integrates with DIDs**: Uses existing cryptographic infrastructure
1122- **Provides Security**: Repository binding, scope control via `$sig`, robust verification, and replay protection