@metalabel/dfos-protocol 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/PROTOCOL.md ADDED
@@ -0,0 +1,873 @@
1
+ # DFOS Protocol: Complete Reference
2
+
3
+ - **Date**: 2026-03-10
4
+ - **Status**: Implemented and tested — TypeScript + Go + Python + Rust + Swift cross-language verification. This spec is currently under review. Discuss the DFOS protocol in the [clear.txt](https://clear.dfos.com) space.
5
+ - **Source**: `packages/dfos-protocol` (self-contained, zero monorepo dependencies, OSS-ready)
6
+ - **Gist**: https://gist.github.com/bvalosek/ed4c96fd4b841302de544ffaee871648 (synced from this file)
7
+
8
+ ---
9
+
10
+ ## Philosophy
11
+
12
+ DFOS is a dark forest operating system. Content lives in private spaces — visible only to members, governed by the communities that create it. The forest floor is dark by default.
13
+
14
+ But the cryptographic proof layer is public and verifiable. Every piece of content, every identity, every edit has a signed chain of commitments that anyone can independently verify. You don't need to trust the platform. You don't need access to the database. You need a public key and a chain of JWS tokens.
15
+
16
+ If you have content — from the official app, from an API export, from a screenshot someone sent you, from a pirate mirror, from anywhere — you can verify it's authentic. Hash the content, check the CID, walk the chain, verify the signature. The content is dark; the proof is light.
17
+
18
+ The protocol makes this verification radically simple. Two chain types — identity and content — using the same mechanics: Ed25519 signatures, JWS compact tokens, content-addressed CIDs. The protocol is deliberately minimal. It knows about keys and document hashes. It doesn't know about posts, profiles, or any application concept. Document semantics are entirely application layer — free to evolve without protocol changes.
19
+
20
+ This means the protocol is not coupled to DFOS. Any system could implement the same identity and content chain primitives — a fork, an alternative client, a completely independent platform — and produce interoperable, cross-verifiable proofs. An identity created on one system can sign content on another. A proof chain started here can be extended there. The protocol is a shared substrate, not a product feature. DFOS is one application built on it. There could be others.
21
+
22
+ The result: a signed content ledger that any standard EdDSA library can verify, in any language, without DFOS-specific dependencies. The dark forest has public roots.
23
+
24
+ ---
25
+
26
+ This document is a complete, self-contained reference for the DFOS protocol. All artifacts are deterministic and reproducible from fixed seeds. An independent implementer can verify every value using standard Ed25519 + dag-cbor libraries.
27
+
28
+ **To regenerate**: `pnpm --filter @metalabel/dfos-protocol exec vitest run tests/protocol-reference.spec.ts`
29
+
30
+ ---
31
+
32
+ ## Protocol Overview
33
+
34
+ The DFOS protocol has three layers:
35
+
36
+ | Layer | Concern |
37
+ | --------------------- | ---------------------------------------------------------------------------- |
38
+ | **Crypto core** | Identity chains + content chains — Ed25519 signatures, JWS tokens, CID links |
39
+ | **Document envelope** | Standard wrapper: `content` + `baseDocumentCID` + `createdByDID` + timestamp |
40
+ | **Content schemas** | JSON Schema definitions for what goes inside `content` (post, profile, etc.) |
41
+
42
+ The crypto core is the trust boundary — everything below it is cryptographically verified. The document envelope provides structural metadata (attribution, edit lineage, timestamps). Content schemas define the application-level semantics.
43
+
44
+ ### Crypto Core: Two Chain Types
45
+
46
+ | | Identity Chain | Content Chain |
47
+ | -------------- | -------------------------- | -------------------------------- |
48
+ | Commits to | Key sets (embedded) | Documents (by CID reference) |
49
+ | Identifier | `did:dfos:<hash>` | `<hash>` (bare) |
50
+ | Operations | create, update, delete | create, update, delete |
51
+ | JWS typ | `did:dfos:identity-op` | `did:dfos:content-op` |
52
+ | Self-sovereign | Yes (signs own operations) | No (signed by external identity) |
53
+
54
+ Both chains are signed linked lists of state commitments. Identity chains embed their state (key sets). Content chains reference their state via `documentCID` — a content-addressed pointer to a document envelope.
55
+
56
+ ### Document Envelope
57
+
58
+ Every document committed to by a content chain uses a standard envelope, defined by JSON Schema at [`schemas/document-envelope.v1.json`](schemas/document-envelope.v1.json) (`https://schemas.dfos.com/document-envelope/v1`):
59
+
60
+ ```json
61
+ {
62
+ "content": { "$schema": "https://schemas.dfos.com/post/v1", ... },
63
+ "baseDocumentCID": "bafyrei..." | null,
64
+ "createdByDID": "did:dfos:...",
65
+ "createdAt": "2026-03-07T00:02:00.000Z"
66
+ }
67
+ ```
68
+
69
+ | Field | Type | Description |
70
+ | ----------------- | ------------ | --------------------------------------------------------------------------- |
71
+ | `content` | object | Application-defined content — must include `$schema` URI, opaque to chains |
72
+ | `baseDocumentCID` | string\|null | CID of the previous document version (edit lineage). Null for first version |
73
+ | `createdByDID` | string | DID of the identity that created this document version |
74
+ | `createdAt` | ISO 8601 | When this document version was created |
75
+
76
+ The `documentCID` in a content chain operation is `CID(dagCborEncode(envelope))`. The envelope provides attribution and edit history at the protocol level. The `content` field is where application-defined JSON Schema types live. The `content` object must include a `$schema` property identifying its content type — this makes every document self-describing and its schema cryptographically committed via the CID.
77
+
78
+ ### Content Schemas
79
+
80
+ The `content` field inside the document envelope is validated by JSON Schema. The protocol ships a standard library of schemas (post, profile) — see [Standard Document Schemas](#standard-document-schemas). These are conventions, not requirements. Any implementation can define custom schemas.
81
+
82
+ ### Addressing
83
+
84
+ Three canonical representations:
85
+
86
+ | Thing | Form | Example |
87
+ | ---------------------- | -------------------------- | ------------------------------------------------------------- |
88
+ | Operation or document | CID (dag-cbor + SHA-256) | `bafyreibanjpgcqffcfhr4sptzjfthh5szohhbo5tjfulemkw7uhden5uqy` |
89
+ | Entity (content chain) | `<hash>` (bare, no prefix) | `67t27rzc83v7c22n9t6z7c` |
90
+ | Identity (key chain) | `did:dfos:<hash>` | `did:dfos:e3vvtck42d4eacdnzvtrn6` |
91
+
92
+ Operations and documents are CIDs — standard IPLD content addresses. Entities and identities are derived identifiers — `customAlpha(SHA-256(genesis CID bytes))`. Same derivation for both. Identity chains prepend `did:dfos:` (W3C DID spec). Entity identifiers are bare — just the 22-char hash, no prefix.
93
+
94
+ Application code may add prefixes for routing (e.g., `post_xxxx`) — these are strippable semantic sugar, not part of the protocol identifier.
95
+
96
+ ---
97
+
98
+ ## Protocol Rules
99
+
100
+ ### Commitment Scheme
101
+
102
+ The protocol requires a **deterministic payload commitment**: given the same logical operation, the commitment (CID) MUST be identical regardless of implementation language or platform. The commitment scheme is **dag-cbor canonical encoding + SHA-256 + CIDv1**. This is not a recommendation — it is the protocol.
103
+
104
+ Implementations MUST use dag-cbor canonical encoding as defined by the [IPLD dag-cbor codec specification](https://ipld.io/specs/codecs/dag-cbor/spec/). Raw JSON serialization, pretty-printed JSON, or any non-canonical encoding MUST NOT be used for CID derivation. The dag-cbor hex test vectors in this document allow byte-level verification of any implementation's canonical encoding.
105
+
106
+ **JWS signing vs CID derivation are intentionally different representations of the same payload.** JWS signs `base64url(JSON.stringify(payload))` — the UTF-8 bytes of the JSON serialization. CID commits to `dagCborCanonicalEncode(payload)` — the dag-cbor canonical encoding of the parsed object. These produce different bytes from the same logical data. This is by design: JWS uses standard JSON for maximum interoperability with existing JWS libraries, while CID uses dag-cbor for deterministic content addressing.
107
+
108
+ ### Chain Validity
109
+
110
+ A valid chain is a **linear sequence** of operations. Each operation (after genesis) links to its predecessor via `previousOperationCID`. The chain provides structural ordering independent of timestamps.
111
+
112
+ **Forks are invalid at the protocol level.** Two operations referencing the same `previousOperationCID` constitute a fork. The protocol does not define fork resolution — this is application-defined. In DFOS's custodial model, forks are prevented by database-level advisory locks. A non-custodial implementation would need its own fork resolution strategy (e.g., longest chain, first-seen, application-specified preference).
113
+
114
+ **Timestamp ordering**: `createdAt` SHOULD be strictly increasing within a chain. Implementations SHOULD reject operations with non-increasing timestamps as a sanity check against replayed or mis-ordered operations. However, the chain link (CID reference) is the authoritative ordering mechanism, not the timestamp. Implementations MAY relax timestamp ordering in constrained environments where clock synchronization is impractical.
115
+
116
+ ### Identity Chain Signer Validity
117
+
118
+ An identity chain operation is valid only if the signing key was a **controller key in the immediately prior state**. For genesis operations, the signing key MUST be one of the controller keys declared in that same operation — this is the bootstrap: the genesis operation introduces and simultaneously authorizes its own keys.
119
+
120
+ This is a self-sovereign invariant: the identity chain defines its own valid signers via `controllerKeys`, and the protocol enforces this. No external authority is consulted.
121
+
122
+ ### Content Chain Signer Model
123
+
124
+ Content chain verification requires a **valid EdDSA signature** — nothing more. The protocol does not define which identities may sign operations on a content chain, does not track or enforce key roles, and does not restrict a chain to a single signer.
125
+
126
+ The signing key is resolved via the `kid` (DID URL), which references a key on an external identity. The content chain verifier delegates key resolution to the caller via a `resolveKey` callback — the protocol does not prescribe how to look up an identity's current key state.
127
+
128
+ This is a deliberate asymmetry with identity chains. Identity chains are self-sovereign — they define their own valid signers internally. Content chains are externally signed — the signing authority model is entirely an application concern, delegated through `resolveKey`. A content chain with operations signed by multiple different identities is valid at the protocol level, as long as each operation's signature verifies against the resolved key.
129
+
130
+ **What the protocol enforces:**
131
+
132
+ - The EdDSA signature on each operation is valid against the key returned by `resolveKey(kid)`
133
+ - Chain integrity (CID links, timestamp ordering, terminal state)
134
+
135
+ **What the protocol does NOT enforce (application concerns):**
136
+
137
+ - Which identities are authorized to sign operations on a given chain
138
+ - Which key role (auth, assert, controller) the signing key must have
139
+ - Whether a chain must have a single signer or may have multiple signers
140
+ - Ownership or attribution semantics between signers and entities
141
+
142
+ ### Terminal States
143
+
144
+ **`delete` is the only terminal state.** No valid operations may follow a delete in either chain type. An implementation MUST reject any operation that appears after a delete.
145
+
146
+ `delete` is a terminal marker that prevents future operations on the chain but does NOT remove data. The complete chain — including all prior operations and their signatures — MUST remain intact for verification. Any party holding the chain can still walk it, verify every signature, and confirm the history up to and including the delete. Data removal (e.g., purging content from storage) is an application-layer concern, not a protocol operation.
147
+
148
+ ### Controller Key Requirement
149
+
150
+ `update` operations on identity chains MUST include at least one controller key. Validation MUST reject any `update` with an empty `controllerKeys` array. This ensures that an identity always has a path forward — if decommissioning is intended, `delete` is the correct terminal operation.
151
+
152
+ ### Content-Null Semantics
153
+
154
+ An `update` operation on a content chain with `documentCID: null` means **the entity exists but its current content is cleared**. This is not a delete — the chain continues, and a subsequent update can set content again. Think of it as "unpublish" rather than "destroy."
155
+
156
+ ### `typ` Header
157
+
158
+ The JWS `typ` header (`did:dfos:identity-op`, `did:dfos:content-op`) is advisory — it aids routing and dispatch but is not a security-critical field. Verification checks the signature and chain integrity, not the `typ` value. Implementations SHOULD validate `typ` for correctness but MUST NOT rely on it for security decisions.
159
+
160
+ ### JWT `kid` vs Operation `kid`
161
+
162
+ JWT tokens (for device auth, MCP sessions, etc.) use `kid` as a simple key identifier for lookup — e.g., `key_ez9a874tckr3dv933d3ckd`. This does NOT follow the same DID URL convention used in operation JWS headers. Operation `kid` uses bare key ID for identity genesis and DID URL (`did:dfos:xxx#key_id`) for everything else. JWT `kid` is always a bare key ID — the JWT's `sub` claim carries the DID separately.
163
+
164
+ ### ID Modulo Bias
165
+
166
+ The ID encoding uses `byte % 19` where each byte ranges 0-255. Since 256 is not evenly divisible by 19, values 0-8 (alphabet positions) appear with probability ~5.26% while values 9-18 appear with probability ~5.22%. This is a ~0.3% bias — not security-relevant for identifiers but acknowledged here for completeness. A rejection-sampling approach (retry if `byte >= 247`) would eliminate the bias entirely.
167
+
168
+ ### Operation Field Limits
169
+
170
+ The protocol defines maximum sizes for all operation fields. These are abuse-prevention ceilings — deliberately loose, not tight validation. Implementations MUST reject operations that exceed these bounds. Implementations MAY impose stricter limits.
171
+
172
+ | Field | Max | Rationale |
173
+ | -------------------------------------------- | --------- | -------------------------------------- |
174
+ | `key.id` | 64 chars | ~3× typical key ID (`key_` + 22 chars) |
175
+ | `key.publicKeyMultibase` | 128 chars | ~2× Ed25519 multikey (~50 chars) |
176
+ | `authKeys` / `assertKeys` / `controllerKeys` | 16 items | Generous for key rotation |
177
+ | `previousOperationCID` | 256 chars | ~4× typical CIDv1 (~60 chars) |
178
+ | `documentCID` | 256 chars | Same as above |
179
+ | `note` | 256 chars | Short annotation, not prose |
180
+
181
+ These limits are enforced by the Zod schemas in `src/chain/schemas.ts`. Any implementation parsing operations MUST reject values exceeding these bounds.
182
+
183
+ The protocol does NOT limit:
184
+
185
+ - **Document content size** — the protocol commits to a CID, not the document. Document size limits are application/registry concerns.
186
+ - **Chain length** — no maximum operations per chain.
187
+ - **Number of chains per identity** — application scaling concern.
188
+
189
+ ---
190
+
191
+ ## Standard Document Schemas
192
+
193
+ The crypto core commits to `documentCID` values without inspecting their contents. The document envelope provides structural metadata. The **content** inside the envelope is where JSON Schema validation applies.
194
+
195
+ The protocol ships a standard library of content schemas as JSON Schema (draft 2020-12) definitions. These are not required — any implementation can define its own content types. They are provided as a starting point for content built on the DFOS protocol, and they are what DFOS uses internally.
196
+
197
+ ### Schema Convention
198
+
199
+ Documents declare their type via a `$schema` field pointing to a schema URI:
200
+
201
+ ```json
202
+ {
203
+ "$schema": "https://schemas.dfos.com/post/v1",
204
+ "format": "short-post",
205
+ "body": "Hello world."
206
+ }
207
+ ```
208
+
209
+ Because the `$schema` field is part of the document, it is behind the `documentCID` — cryptographically committed in the content chain. Any verifier can resolve the document, read `$schema`, and validate against the schema.
210
+
211
+ ### Schema Evolution
212
+
213
+ Schemas are versioned via the URI path (`/post/v1`, `/post/v2`). Evolution rules:
214
+
215
+ - **Strictly additive within a version** — new optional fields can be added to an existing version at any time without breaking existing documents
216
+ - **Breaking changes require a new version** — removing fields, changing types, or adding new required fields means a new version URI
217
+ - **Implementations declare which versions they understand** — a registry or application can accept `post/v1` and `post/v2` simultaneously, or only `post/v1`
218
+
219
+ ### Standard Schemas
220
+
221
+ Schema files live in `schemas/` in the protocol package. Each is a standalone JSON Schema (draft 2020-12).
222
+
223
+ #### Post (`https://schemas.dfos.com/post/v1`)
224
+
225
+ The primary content type. Covers short posts, long-form posts, comments, and replies via the `format` discriminator.
226
+
227
+ | Field | Type | Required | Description |
228
+ | ------------- | -------- | -------- | ---------------------------------------------------------------------------------- |
229
+ | `$schema` | string | yes | `"https://schemas.dfos.com/post/v1"` |
230
+ | `format` | enum | yes | `"short-post"`, `"long-post"`, `"comment"`, `"reply"` — immutable, set at creation |
231
+ | `title` | string | no | Post title (typically for long-post format) |
232
+ | `body` | string | no | Post body content |
233
+ | `cover` | media | no | Cover image |
234
+ | `attachments` | media[] | no | Attached media objects |
235
+ | `topics` | string[] | no | Topic names (stored as names for portability) |
236
+
237
+ #### Profile (`https://schemas.dfos.com/profile/v1`)
238
+
239
+ The displayable identity for any agent, person, group, or space.
240
+
241
+ | Field | Type | Required | Description |
242
+ | ------------- | ------ | -------- | --------------------------------------- |
243
+ | `$schema` | string | yes | `"https://schemas.dfos.com/profile/v1"` |
244
+ | `name` | string | no | Display name |
245
+ | `description` | string | no | Short bio or description |
246
+ | `avatar` | media | no | Avatar image |
247
+ | `banner` | media | no | Banner image |
248
+ | `background` | media | no | Background image |
249
+
250
+ ### Media Object
251
+
252
+ Several schemas reference media objects. The standard representation:
253
+
254
+ ```json
255
+ {
256
+ "id": "media_abc123",
257
+ "uri": "https://cdn.example.com/media/abc123.jpg"
258
+ }
259
+ ```
260
+
261
+ `id` is required (opaque identifier). `uri` is optional.
262
+
263
+ ### Custom Schemas
264
+
265
+ Any implementation can define custom document schemas following the same pattern — a JSON Schema with a `$schema` const field pointing to a unique URI. The protocol will commit to the document via CID regardless of what's inside. The standard schemas are conventions, not constraints.
266
+
267
+ ---
268
+
269
+ ## Standards and Dependencies
270
+
271
+ | Component | Standard / Library |
272
+ | ------------------- | -------------------------------------------------------------------------- |
273
+ | Key generation | Ed25519 (RFC 8032) via `@noble/curves/ed25519` |
274
+ | Signature algorithm | EdDSA over Ed25519 (pure, no prehash — Ed25519 handles SHA-512 internally) |
275
+ | Key encoding | W3C Multikey (multicodec `0xed01` + base58btc multibase) |
276
+ | Signed envelopes | JWS Compact Serialization (RFC 7515) with `alg: "EdDSA"` |
277
+ | Content addressing | CIDv1 with dag-cbor codec (`0x71`) + SHA-256 multihash (`0x12`) |
278
+ | Auth tokens | JWT (RFC 7519) with `alg: "EdDSA"` |
279
+ | ID encoding | SHA-256 → custom 19-char alphabet, 22 characters |
280
+
281
+ ### ID Alphabet
282
+
283
+ ```
284
+ Alphabet: 2346789acdefhknrtvz (19 characters)
285
+ Length: 22 characters
286
+ Entropy: ~93.4 bits (19^22)
287
+ ```
288
+
289
+ Process: `SHA-256(input) → for each of first 22 bytes: alphabet[byte % 19]`
290
+
291
+ DIDs: `did:dfos:` + 22-char ID derived from `SHA-256(genesis CID raw bytes)`
292
+ Key IDs: `key_` + 22-char ID. Convention: derive from public key hash (`key_` + `customAlpha(SHA-256(publicKey))`), making key IDs deterministic and verifiable. Not a protocol requirement — key IDs can be any string.
293
+
294
+ ### Multikey Encoding (W3C Multikey for Ed25519)
295
+
296
+ ```
297
+ Encode:
298
+ 1. Take 32-byte Ed25519 public key
299
+ 2. Prepend multicodec varint prefix [0xed, 0x01] (unsigned varint for 0xed = 237 = ed25519-pub)
300
+ 3. Base58btc encode the 34-byte result
301
+ 4. Prepend 'z' multibase prefix
302
+ → "z6Mk..."
303
+
304
+ Decode:
305
+ 1. Strip 'z' multibase prefix
306
+ 2. Base58btc decode → 34 bytes
307
+ 3. First 2 bytes must be [0xed, 0x01] (ed25519-pub multicodec varint)
308
+ 4. Remaining 32 bytes = raw Ed25519 public key
309
+ ```
310
+
311
+ **Worked example:**
312
+
313
+ ```
314
+ Public key (hex): ba421e272fad4f941c221e47f87d9253bdc04f7d4ad2625ae667ab9f0688ce32
315
+ Prefix + key (hex): ed01 ba421e272fad4f941c221e47f87d9253bdc04f7d4ad2625ae667ab9f0688ce32
316
+ Base58btc + 'z': z6MkrzLMNwoJSV4P3YccWcbtk8vd9LtgMKnLeaDLUqLuASjb
317
+ ```
318
+
319
+ Note: `[0xed, 0x01]` is the unsigned varint encoding of 237 (`0xed`). Since `0xed > 0x7f`, it requires two bytes in varint format: `0xed` (low 7 bits + continuation bit) then `0x01` (high bits). This is NOT big-endian `[0x00, 0xed]`.
320
+
321
+ ### CID Construction (dag-cbor + SHA-256)
322
+
323
+ ```
324
+ 1. JSON payload → dag-cbor canonical encoding → CBOR bytes
325
+ 2. SHA-256(CBOR bytes) → 32-byte hash
326
+ 3. Construct CIDv1:
327
+ - Version: 1 (varint: 0x01)
328
+ - Codec: dag-cbor (varint: 0x71)
329
+ - Multihash: SHA-256 (function: 0x12, length: 0x20, digest: 32 bytes)
330
+ 4. CID binary = [0x01, 0x71, 0x12, 0x20, ...32 hash bytes]
331
+ 5. Base32lower multibase encode → "bafyrei..."
332
+ ```
333
+
334
+ dag-cbor canonical ordering: map keys sorted by encoded byte length first, then lexicographic. JSON numbers map to CBOR integers. Strings to CBOR text strings. Null to CBOR null. Arrays to CBOR arrays. Objects to CBOR maps with sorted keys.
335
+
336
+ **Worked example (genesis identity operation):**
337
+
338
+ ```
339
+ CBOR bytes (441 bytes, hex):
340
+ a66474797065666372656174656776657273696f6e0168617574684b65797381a3626964781a6b
341
+ 65795f72396576333466766332337a393939766561616674386474797065684d756c74696b6579
342
+ 727075626c69634b65794d756c74696261736578307a364d6b727a4c4d4e776f4a535634503359
343
+ 6363576362746b387664394c74674d4b6e4c6561444c55714c7541536a62696372656174656441
344
+ 747818323032362d30332d30375430303a30303a30302e3030305a6a6173736572744b65797381
345
+ a3626964781a6b65795f72396576333466766332337a393939766561616674386474797065684d
346
+ 756c74696b6579727075626c69634b65794d756c74696261736578307a364d6b727a4c4d4e776f
347
+ 4a5356345033596363576362746b387664394c74674d4b6e4c6561444c55714c7541536a626e63
348
+ 6f6e74726f6c6c65724b65797381a3626964781a6b65795f72396576333466766332337a393939
349
+ 766561616674386474797065684d756c74696b6579727075626c69634b65794d756c7469626173
350
+ 6578307a364d6b727a4c4d4e776f4a5356345033596363576362746b387664394c74674d4b6e4c
351
+ 6561444c55714c7541536a62
352
+
353
+ CID bytes (hex): 01711220206a5e6140a5114f1e49f3ca4b339fb2cb8e70bbb34968b23156fd0e3237b486
354
+ CID string: bafyreibanjpgcqffcfhr4sptzjfthh5szohhbo5tjfulemkw7uhden5uqy
355
+ ```
356
+
357
+ ### DID Derivation (worked example)
358
+
359
+ ```
360
+ Input: CID bytes (hex) = 01711220206a5e6140a5114f1e49f3ca4b339fb2cb8e70bbb34968b23156fd0e3237b486
361
+ Step 1: SHA-256(CID bytes) = 4360cfbcbbb3f1614c8e02dbfe8d55935e1195cd2129820ab8aef94bde12ea8a
362
+ Step 2: Take first 22 bytes: 43 60 cf bc bb b3 f1 61 4c 8e 02 db fe 8d 55 93 5e 11 95 cd 21 29
363
+ Step 3: For each byte, alphabet[byte % 19]:
364
+ 43=67 → 67%19=10 → 'e'
365
+ 60=96 → 96%19=1 → '3'
366
+ cf=207 → 207%19=17 → 'v'
367
+ bc=188 → 188%19=17 → 'v'
368
+ ...
369
+ Result: e3vvtck42d4eacdnzvtrn6
370
+ DID: did:dfos:e3vvtck42d4eacdnzvtrn6
371
+ ```
372
+
373
+ ---
374
+
375
+ ## Operation Schemas
376
+
377
+ ### Identity Operations
378
+
379
+ ```typescript
380
+ // Genesis — starts the identity chain
381
+ { version: 1, type: "create",
382
+ authKeys: MultikeyPublicKey[],
383
+ assertKeys: MultikeyPublicKey[],
384
+ controllerKeys: MultikeyPublicKey[], // must have at least one
385
+ createdAt: string } // ISO 8601, ms precision, UTC
386
+
387
+ // Key rotation / modification
388
+ { version: 1, type: "update",
389
+ previousOperationCID: string, // CID of previous operation
390
+ authKeys: MultikeyPublicKey[],
391
+ assertKeys: MultikeyPublicKey[],
392
+ controllerKeys: MultikeyPublicKey[], // must have at least one
393
+ createdAt: string }
394
+
395
+ // Permanent destruction
396
+ { version: 1, type: "delete",
397
+ previousOperationCID: string,
398
+ createdAt: string }
399
+ ```
400
+
401
+ ### Content Operations
402
+
403
+ ```typescript
404
+ // Genesis — starts the content chain, commits initial document
405
+ { version: 1, type: "create",
406
+ documentCID: string, // CID of document content
407
+ createdAt: string,
408
+ note: string | null }
409
+
410
+ // Content change (null documentCID = clear content)
411
+ { version: 1, type: "update",
412
+ previousOperationCID: string,
413
+ documentCID: string | null,
414
+ createdAt: string,
415
+ note: string | null }
416
+
417
+ // Permanent entity destruction
418
+ { version: 1, type: "delete",
419
+ previousOperationCID: string,
420
+ createdAt: string,
421
+ note: string | null }
422
+ ```
423
+
424
+ ### MultikeyPublicKey
425
+
426
+ ```typescript
427
+ { id: string, // e.g. "key_r9ev34fvc23z999veaaft8"
428
+ type: "Multikey", // literal discriminator
429
+ publicKeyMultibase: string } // e.g. "z6MkrzLMNwoJSV4P3YccWcbtk8vd9LtgMKnLeaDLUqLuASjb"
430
+ ```
431
+
432
+ ---
433
+
434
+ ## JWS Envelope Format
435
+
436
+ ### Signing
437
+
438
+ ```
439
+ signingInput = base64url(JSON.stringify(header)) + "." + base64url(JSON.stringify(payload))
440
+ signature = ed25519.sign(UTF8_bytes(signingInput), privateKey)
441
+ token = signingInput + "." + base64url(signature)
442
+ ```
443
+
444
+ ### kid Rules
445
+
446
+ | Context | kid format | Example |
447
+ | ------------------------- | ----------- | ------------------------------------------------------------ |
448
+ | Identity create (genesis) | Bare key ID | `key_r9ev34fvc23z999veaaft8` |
449
+ | Identity update/delete | DID URL | `did:dfos:e3vvtck42d4eacdnzvtrn6#key_r9ev34fvc23z999veaaft8` |
450
+ | All content ops | DID URL | `did:dfos:e3vvtck42d4eacdnzvtrn6#key_ez9a874tckr3dv933d3ckd` |
451
+
452
+ ### `cid` Header
453
+
454
+ Every operation JWS (identity-op and content-op) includes a `cid` field in the protected header. This is the CIDv1 string of the operation payload, derived from `dagCborCanonicalEncode(payload) → SHA-256 → CIDv1 → base32lower`. The `cid` is computed before signing and embedded in the protected header, so it is covered by the EdDSA signature.
455
+
456
+ **Signing order:**
457
+
458
+ 1. Construct the operation payload
459
+ 2. Derive the operation CID: `dagCborCanonicalEncode(payload) → CIDv1`
460
+ 3. Build the protected header including `cid`
461
+ 4. Sign: `ed25519.sign(UTF8(base64url(header) + "." + base64url(payload)), privateKey)`
462
+
463
+ **Verification rule:** After verifying the JWS signature and deriving the operation CID from the parsed payload, implementations MUST reject operations where:
464
+
465
+ - `header.cid` is missing
466
+ - `header.cid` does not match the derived CID
467
+
468
+ This provides three benefits:
469
+
470
+ - **Pre-verification routing**: The operation CID can be read from the header without parsing the payload or running dag-cbor encoding
471
+ - **Cross-implementation consistency**: A CID mismatch between header and derived value immediately surfaces dag-cbor encoding disagreements across implementations
472
+ - **Self-documenting tokens**: Each JWS token declares its content-addressed identity
473
+
474
+ Note: JWT tokens (device auth) do NOT include a `cid` header — this field is specific to operation JWS tokens.
475
+
476
+ ### CID Derivation
477
+
478
+ ```
479
+ operation CID = dagCborCanonicalEncode(operation_payload) → SHA-256 → CIDv1 → base32lower string
480
+ ```
481
+
482
+ The CID is derived from the JWS payload (the unsigned operation JSON), NOT from the JWS token itself.
483
+
484
+ ### DID Derivation
485
+
486
+ ```
487
+ DID = "did:dfos:" + idEncode(SHA-256(genesis_CID_raw_bytes))
488
+ ```
489
+
490
+ Where `idEncode` is the 19-char alphabet encoding described above.
491
+
492
+ ---
493
+
494
+ ## Verification
495
+
496
+ ### Identity Chain
497
+
498
+ 1. Decode each JWS, parse payload as IdentityOperation
499
+ 2. First op MUST be `type: "create"` — this is the genesis bootstrap:
500
+ - The controller keys declared in the genesis payload are trusted because the identity does not exist before this operation. There is no prior state to verify against.
501
+ - The signing key (resolved from `kid`) MUST be one of the controller keys declared in this same operation. The genesis simultaneously introduces and authorizes its own keys.
502
+ - Derive the operation CID via dag-cbor canonical encoding. Verify `header.cid` matches the derived CID. Derive the DID from the CID.
503
+ 3. For each subsequent op: verify `previousOperationCID` matches previous op's derived CID. Verify `createdAt` is strictly increasing (SHOULD — see Protocol Rules).
504
+ 4. Verify the chain is not in a terminal state (deleted) before applying any operation.
505
+ 5. Resolve `kid` — genesis uses bare key ID, non-genesis uses DID URL (extract DID, verify it matches the derived DID; extract key ID).
506
+ 6. Find controller key matching key ID **in the current state** (i.e., the state after all preceding operations). Decode multikey → raw Ed25519 public key.
507
+ 7. Verify EdDSA JWS signature over the signing input bytes.
508
+ 8. Apply state change: `create` initializes key state, `update` replaces key state (must have at least one controller key), `delete` marks terminal.
509
+
510
+ ### Content Chain
511
+
512
+ 1. Decode each JWS, parse payload as ContentOperation
513
+ 2. First op must be `type: "create"`
514
+ 3. For each subsequent op: verify `previousOperationCID` matches, verify `createdAt` increasing
515
+ 4. Derive the operation CID via dag-cbor canonical encoding. Verify `header.cid` matches the derived CID.
516
+ 5. Resolve `kid` via external key resolver (caller provides)
517
+ 6. Verify EdDSA JWS signature
518
+ 7. Apply state change (set document, clear, or delete)
519
+
520
+ ---
521
+
522
+ ## Deterministic Reference Artifacts
523
+
524
+ All values below are deterministic and reproducible. Private keys are derived from `SHA-256(UTF8("dfos-protocol-reference-key-N"))`.
525
+
526
+ ### Key 1 (Genesis Controller)
527
+
528
+ ```
529
+ Seed: SHA-256("dfos-protocol-reference-key-1")
530
+ Private key: 132d4bebdb6e62359afb930fe15d756a92ad96e6b0d47619988f5a1a55272aac
531
+ Public key: ba421e272fad4f941c221e47f87d9253bdc04f7d4ad2625ae667ab9f0688ce32
532
+ Multikey: z6MkrzLMNwoJSV4P3YccWcbtk8vd9LtgMKnLeaDLUqLuASjb
533
+ Key ID: key_r9ev34fvc23z999veaaft8
534
+ ```
535
+
536
+ ### Key 2 (Rotated Controller)
537
+
538
+ ```
539
+ Seed: SHA-256("dfos-protocol-reference-key-2")
540
+ Private key: 384f5626906db84f6a773ec46475ff2d4458e92dd4dd13fe03dbb7510f4ca2a8
541
+ Public key: 0f350f994f94d675f04a325bd316ebedd740ca206eaaf609bdb641b5faa0f78c
542
+ Multikey: z6MkfUd65JrAhfdgFuMCccU9ThQvjB2fJAMUHkuuajF992gK
543
+ Key ID: key_ez9a874tckr3dv933d3ckd
544
+ ```
545
+
546
+ ### Identity Chain: Create (Genesis)
547
+
548
+ Operation:
549
+
550
+ ```json
551
+ {
552
+ "version": 1,
553
+ "type": "create",
554
+ "authKeys": [
555
+ {
556
+ "id": "key_r9ev34fvc23z999veaaft8",
557
+ "type": "Multikey",
558
+ "publicKeyMultibase": "z6MkrzLMNwoJSV4P3YccWcbtk8vd9LtgMKnLeaDLUqLuASjb"
559
+ }
560
+ ],
561
+ "assertKeys": [
562
+ {
563
+ "id": "key_r9ev34fvc23z999veaaft8",
564
+ "type": "Multikey",
565
+ "publicKeyMultibase": "z6MkrzLMNwoJSV4P3YccWcbtk8vd9LtgMKnLeaDLUqLuASjb"
566
+ }
567
+ ],
568
+ "controllerKeys": [
569
+ {
570
+ "id": "key_r9ev34fvc23z999veaaft8",
571
+ "type": "Multikey",
572
+ "publicKeyMultibase": "z6MkrzLMNwoJSV4P3YccWcbtk8vd9LtgMKnLeaDLUqLuASjb"
573
+ }
574
+ ],
575
+ "createdAt": "2026-03-07T00:00:00.000Z"
576
+ }
577
+ ```
578
+
579
+ JWS Header:
580
+
581
+ ```json
582
+ {
583
+ "alg": "EdDSA",
584
+ "typ": "did:dfos:identity-op",
585
+ "kid": "key_r9ev34fvc23z999veaaft8",
586
+ "cid": "bafyreibanjpgcqffcfhr4sptzjfthh5szohhbo5tjfulemkw7uhden5uqy"
587
+ }
588
+ ```
589
+
590
+ JWS Signature (hex):
591
+
592
+ ```
593
+ 103af20cad6ebed8b1fb5edc1ee9fdb7a31a705231dab326305d502f37c3e531654ac3af31cb9ef7ba428069f709778b545b55c60a42a21d241925e2a0a2b303
594
+ ```
595
+
596
+ JWS Token:
597
+
598
+ ```
599
+ eyJhbGciOiJFZERTQSIsInR5cCI6ImRpZDpkZm9zOmlkZW50aXR5LW9wIiwia2lkIjoia2V5X3I5ZXYzNGZ2YzIzejk5OXZlYWFmdDgiLCJjaWQiOiJiYWZ5cmVpYmFuanBnY3FmZmNmaHI0c3B0empmdGhoNXN6b2hoYm81dGpmdWxlbWt3N3VoZGVuNXVxeSJ9.eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiY3JlYXRlIiwiYXV0aEtleXMiOlt7ImlkIjoia2V5X3I5ZXYzNGZ2YzIzejk5OXZlYWFmdDgiLCJ0eXBlIjoiTXVsdGlrZXkiLCJwdWJsaWNLZXlNdWx0aWJhc2UiOiJ6Nk1rcnpMTU53b0pTVjRQM1ljY1djYnRrOHZkOUx0Z01LbkxlYURMVXFMdUFTamIifV0sImFzc2VydEtleXMiOlt7ImlkIjoia2V5X3I5ZXYzNGZ2YzIzejk5OXZlYWFmdDgiLCJ0eXBlIjoiTXVsdGlrZXkiLCJwdWJsaWNLZXlNdWx0aWJhc2UiOiJ6Nk1rcnpMTU53b0pTVjRQM1ljY1djYnRrOHZkOUx0Z01LbkxlYURMVXFMdUFTamIifV0sImNvbnRyb2xsZXJLZXlzIjpbeyJpZCI6ImtleV9yOWV2MzRmdmMyM3o5OTl2ZWFhZnQ4IiwidHlwZSI6Ik11bHRpa2V5IiwicHVibGljS2V5TXVsdGliYXNlIjoiejZNa3J6TE1Od29KU1Y0UDNZY2NXY2J0azh2ZDlMdGdNS25MZWFETFVxTHVBU2piIn1dLCJjcmVhdGVkQXQiOiIyMDI2LTAzLTA3VDAwOjAwOjAwLjAwMFoifQ.EDryDK1uvtix-17cHun9t6MacFIx2rMmMF1QLzfD5TFlSsOvMcue97pCgGn3CXeLVFtVxgpCoh0kGSXioKKzAw
600
+ ```
601
+
602
+ Operation CID: `bafyreibanjpgcqffcfhr4sptzjfthh5szohhbo5tjfulemkw7uhden5uqy`
603
+
604
+ **Derived DID: `did:dfos:e3vvtck42d4eacdnzvtrn6`**
605
+
606
+ ### Identity Chain: Update (Key Rotation)
607
+
608
+ JWS Header:
609
+
610
+ ```json
611
+ {
612
+ "alg": "EdDSA",
613
+ "typ": "did:dfos:identity-op",
614
+ "kid": "did:dfos:e3vvtck42d4eacdnzvtrn6#key_r9ev34fvc23z999veaaft8",
615
+ "cid": "bafyreicym4cyiednld73smbx32szaei7xdulqn4g3ste5e2w2ulajr3oqm"
616
+ }
617
+ ```
618
+
619
+ Operation:
620
+
621
+ ```json
622
+ {
623
+ "version": 1,
624
+ "type": "update",
625
+ "previousOperationCID": "bafyreibanjpgcqffcfhr4sptzjfthh5szohhbo5tjfulemkw7uhden5uqy",
626
+ "authKeys": [
627
+ {
628
+ "id": "key_ez9a874tckr3dv933d3ckd",
629
+ "type": "Multikey",
630
+ "publicKeyMultibase": "z6MkfUd65JrAhfdgFuMCccU9ThQvjB2fJAMUHkuuajF992gK"
631
+ }
632
+ ],
633
+ "assertKeys": [
634
+ {
635
+ "id": "key_ez9a874tckr3dv933d3ckd",
636
+ "type": "Multikey",
637
+ "publicKeyMultibase": "z6MkfUd65JrAhfdgFuMCccU9ThQvjB2fJAMUHkuuajF992gK"
638
+ }
639
+ ],
640
+ "controllerKeys": [
641
+ {
642
+ "id": "key_ez9a874tckr3dv933d3ckd",
643
+ "type": "Multikey",
644
+ "publicKeyMultibase": "z6MkfUd65JrAhfdgFuMCccU9ThQvjB2fJAMUHkuuajF992gK"
645
+ }
646
+ ],
647
+ "createdAt": "2026-03-07T00:01:00.000Z"
648
+ }
649
+ ```
650
+
651
+ JWS Signature (hex):
652
+
653
+ ```
654
+ 31272ea0196038ade3e505fdb45730d68bb4a382e0273886244b19e69bea881af549a800c80bf987ec1a8d086d83c20fedd2e533453895e5b6891adaf78e5c0e
655
+ ```
656
+
657
+ JWS Token:
658
+
659
+ ```
660
+ eyJhbGciOiJFZERTQSIsInR5cCI6ImRpZDpkZm9zOmlkZW50aXR5LW9wIiwia2lkIjoiZGlkOmRmb3M6ZTN2dnRjazQyZDRlYWNkbnp2dHJuNiNrZXlfcjlldjM0ZnZjMjN6OTk5dmVhYWZ0OCIsImNpZCI6ImJhZnlyZWljeW00Y3lpZWRubGQ3M3NtYngzMnN6YWVpN3hkdWxxbjRnM3N0ZTVlMncydWxhanIzb3FtIn0.eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoidXBkYXRlIiwicHJldmlvdXNPcGVyYXRpb25DSUQiOiJiYWZ5cmVpYmFuanBnY3FmZmNmaHI0c3B0empmdGhoNXN6b2hoYm81dGpmdWxlbWt3N3VoZGVuNXVxeSIsImF1dGhLZXlzIjpbeyJpZCI6ImtleV9lejlhODc0dGNrcjNkdjkzM2QzY2tkIiwidHlwZSI6Ik11bHRpa2V5IiwicHVibGljS2V5TXVsdGliYXNlIjoiejZNa2ZVZDY1SnJBaGZkZ0Z1TUNjY1U5VGhRdmpCMmZKQU1VSGt1dWFqRjk5MmdLIn1dLCJhc3NlcnRLZXlzIjpbeyJpZCI6ImtleV9lejlhODc0dGNrcjNkdjkzM2QzY2tkIiwidHlwZSI6Ik11bHRpa2V5IiwicHVibGljS2V5TXVsdGliYXNlIjoiejZNa2ZVZDY1SnJBaGZkZ0Z1TUNjY1U5VGhRdmpCMmZKQU1VSGt1dWFqRjk5MmdLIn1dLCJjb250cm9sbGVyS2V5cyI6W3siaWQiOiJrZXlfZXo5YTg3NHRja3IzZHY5MzNkM2NrZCIsInR5cGUiOiJNdWx0aWtleSIsInB1YmxpY0tleU11bHRpYmFzZSI6Ino2TWtmVWQ2NUpyQWhmZGdGdU1DY2NVOVRoUXZqQjJmSkFNVUhrdXVhakY5OTJnSyJ9XSwiY3JlYXRlZEF0IjoiMjAyNi0wMy0wN1QwMDowMTowMC4wMDBaIn0.MScuoBlgOK3j5QX9tFcw1ou0o4LgJziGJEsZ5pvqiBr1SagAyAv5h-wajQhtg8IP7dLlM0U4leW2iRra945cDg
661
+ ```
662
+
663
+ Operation CID: `bafyreicym4cyiednld73smbx32szaei7xdulqn4g3ste5e2w2ulajr3oqm`
664
+
665
+ Post-rotation: DID unchanged (`did:dfos:e3vvtck42d4eacdnzvtrn6`), controller rotated to `key_ez9a874tckr3dv933d3ckd`.
666
+
667
+ ### Content Chain: Document + Create
668
+
669
+ Document (application layer):
670
+
671
+ ```json
672
+ {
673
+ "content": {
674
+ "$schema": "https://schemas.dfos.com/post/v1",
675
+ "format": "short-post",
676
+ "title": "Hello World",
677
+ "body": "First post on the protocol."
678
+ },
679
+ "baseDocumentCID": null,
680
+ "createdByDID": "did:dfos:e3vvtck42d4eacdnzvtrn6",
681
+ "createdAt": "2026-03-07T00:02:00.000Z"
682
+ }
683
+ ```
684
+
685
+ Document CID: `bafyreifpvwuarml62sfogdpi2vlltvg2ev6o4xtw74zfud7cpkg7426zne`
686
+
687
+ Content Create JWS Header:
688
+
689
+ ```json
690
+ {
691
+ "alg": "EdDSA",
692
+ "typ": "did:dfos:content-op",
693
+ "kid": "did:dfos:e3vvtck42d4eacdnzvtrn6#key_ez9a874tckr3dv933d3ckd",
694
+ "cid": "bafyreia5z7zxknae5ds72euihuf2rg3ixl6t4fbzjefhcogg3nqppyogqu"
695
+ }
696
+ ```
697
+
698
+ Content Create Payload:
699
+
700
+ ```json
701
+ {
702
+ "version": 1,
703
+ "type": "create",
704
+ "documentCID": "bafyreifpvwuarml62sfogdpi2vlltvg2ev6o4xtw74zfud7cpkg7426zne",
705
+ "createdAt": "2026-03-07T00:02:00.000Z",
706
+ "note": null
707
+ }
708
+ ```
709
+
710
+ Content Create JWS Signature (hex):
711
+
712
+ ```
713
+ b7f0c3909fd398d7a42065053b6d86f96efc4281385d383d2ca4388330101da2b707ae3dd538abf5bfb0b69fa173098436ed87aa789eaafe404a2a9f16b11b0f
714
+ ```
715
+
716
+ Content Create JWS Token:
717
+
718
+ ```
719
+ eyJhbGciOiJFZERTQSIsInR5cCI6ImRpZDpkZm9zOmNvbnRlbnQtb3AiLCJraWQiOiJkaWQ6ZGZvczplM3Z2dGNrNDJkNGVhY2RuenZ0cm42I2tleV9lejlhODc0dGNrcjNkdjkzM2QzY2tkIiwiY2lkIjoiYmFmeXJlaWE1ejd6eGtuYWU1ZHM3MmV1aWh1ZjJyZzNpeGw2dDRmYnpqZWZoY29nZzNucXBweW9ncXUifQ.eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiY3JlYXRlIiwiZG9jdW1lbnRDSUQiOiJiYWZ5cmVpZnB2d3Vhcm1sNjJzZm9nZHBpMnZsbHR2ZzJldjZvNHh0dzc0emZ1ZDdjcGtnNzQyNnpuZSIsImNyZWF0ZWRBdCI6IjIwMjYtMDMtMDdUMDA6MDI6MDAuMDAwWiIsIm5vdGUiOm51bGx9.t_DDkJ_TmNekIGUFO22G-W78QoE4XTg9LKQ4gzAQHaK3B6491Tir9b-wtp-hcwmENu2Hqnieqv5ASiqfFrEbDw
720
+ ```
721
+
722
+ Content Operation CID: `bafyreia5z7zxknae5ds72euihuf2rg3ixl6t4fbzjefhcogg3nqppyogqu`
723
+
724
+ ### Content Chain: Update
725
+
726
+ Content Update Payload:
727
+
728
+ ```json
729
+ {
730
+ "version": 1,
731
+ "type": "update",
732
+ "previousOperationCID": "bafyreia5z7zxknae5ds72euihuf2rg3ixl6t4fbzjefhcogg3nqppyogqu",
733
+ "documentCID": "bafyreieuo26zfmjxwpmw5jk6bqzqhvivxcbckgxtyeuc7ypf3p4sihgq4q",
734
+ "createdAt": "2026-03-07T00:03:00.000Z",
735
+ "note": "edited title and body"
736
+ }
737
+ ```
738
+
739
+ Updated document:
740
+
741
+ ```json
742
+ {
743
+ "content": {
744
+ "$schema": "https://schemas.dfos.com/post/v1",
745
+ "format": "short-post",
746
+ "title": "Hello World (edited)",
747
+ "body": "Updated content."
748
+ },
749
+ "baseDocumentCID": "bafyreifpvwuarml62sfogdpi2vlltvg2ev6o4xtw74zfud7cpkg7426zne",
750
+ "createdByDID": "did:dfos:e3vvtck42d4eacdnzvtrn6",
751
+ "createdAt": "2026-03-07T00:03:00.000Z"
752
+ }
753
+ ```
754
+
755
+ Document CID (edited): `bafyreieuo26zfmjxwpmw5jk6bqzqhvivxcbckgxtyeuc7ypf3p4sihgq4q`
756
+ Content Update CID: `bafyreibb4lsvqmz4j76rsvhkqw3v2b4vp23t7dimm6vl5g5wlninvkemxq`
757
+
758
+ ### EdDSA JWT
759
+
760
+ Header:
761
+
762
+ ```json
763
+ { "alg": "EdDSA", "typ": "JWT", "kid": "key_ez9a874tckr3dv933d3ckd" }
764
+ ```
765
+
766
+ Payload:
767
+
768
+ ```json
769
+ {
770
+ "iss": "dfos",
771
+ "sub": "did:dfos:e3vvtck42d4eacdnzvtrn6",
772
+ "aud": "dfos-api",
773
+ "exp": 1772902800,
774
+ "iat": 1772899200,
775
+ "jti": "session_ref_example_01"
776
+ }
777
+ ```
778
+
779
+ JWT Token:
780
+
781
+ ```
782
+ eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImtpZCI6ImtleV9lejlhODc0dGNrcjNkdjkzM2QzY2tkIn0.eyJpc3MiOiJkZm9zIiwic3ViIjoiZGlkOmRmb3M6ZTN2dnRjazQyZDRlYWNkbnp2dHJuNiIsImF1ZCI6ImRmb3MtYXBpIiwiZXhwIjoxNzcyOTAyODAwLCJpYXQiOjE3NzI4OTkyMDAsImp0aSI6InNlc3Npb25fcmVmX2V4YW1wbGVfMDEifQ.zhKeXJHHF7a1-MwF4QoUTRptCplAwh20-rLnuWGDFT6uJheN4E_SA5NhqvMNflLHxd7h97gdaVnMZGE67SXEBA
783
+ ```
784
+
785
+ ---
786
+
787
+ ## Verification Checklist (For Independent Implementers)
788
+
789
+ Given the artifacts above, verify:
790
+
791
+ 1. **Multikey decode**: `z6MkrzLMNwoJSV4P3YccWcbtk8vd9LtgMKnLeaDLUqLuASjb` → strip `z`, base58btc decode, strip `[0xed, 0x01]` → public key `ba421e272fad4f941c221e47f87d9253bdc04f7d4ad2625ae667ab9f0688ce32`
792
+
793
+ 2. **Genesis JWS verify**: split token on `.`, take first two segments as signing input (UTF-8 bytes), base64url-decode third segment as 64-byte signature, `ed25519.verify(signature, signingInputBytes, publicKey)` → true. Note the header now contains `cid` alongside `alg`, `typ`, and `kid`.
794
+
795
+ 3. **Genesis CID**: base64url-decode JWS payload → parse JSON → dag-cbor canonical encode → SHA-256 → CIDv1 → should be `bafyreibanjpgcqffcfhr4sptzjfthh5szohhbo5tjfulemkw7uhden5uqy`
796
+
797
+ 4. **CID header**: Verify each operation JWS header contains `cid` matching the derived operation CID
798
+
799
+ 5. **DID derivation**: take raw CID bytes of genesis CID → SHA-256 → first 22 bytes → `byte % 19` → alphabet lookup → should be `e3vvtck42d4eacdnzvtrn6` → DID = `did:dfos:e3vvtck42d4eacdnzvtrn6`
800
+
801
+ 6. **Rotation JWS**: kid = `did:dfos:e3vvtck42d4eacdnzvtrn6#key_r9ev34fvc23z999veaaft8` — signed by OLD controller key (key 1). Verify with key 1's public key.
802
+
803
+ 7. **Content create JWS**: kid = `did:dfos:e3vvtck42d4eacdnzvtrn6#key_ez9a874tckr3dv933d3ckd` — signed by NEW controller key (key 2, post-rotation). Verify with key 2's public key.
804
+
805
+ 8. **Document CID**: dag-cbor canonical encode the document JSON → SHA-256 → CIDv1 → should be `bafyreifpvwuarml62sfogdpi2vlltvg2ev6o4xtw74zfud7cpkg7426zne`
806
+
807
+ 9. **Content chain integrity**: update's `previousOperationCID` matches create's operation CID
808
+
809
+ 10. **JWT verify**: same signing mechanics as JWS — `ed25519.verify(signature, UTF8(header.payload), key2_publicKey)` → true. Check `exp > currentTime`, `iss == "dfos"`, `aud == "dfos-api"`.
810
+
811
+ ---
812
+
813
+ ## Source Code Reference
814
+
815
+ All source lives in `packages/dfos-protocol/` — self-contained, zero monorepo dependencies.
816
+
817
+ | File | Contents |
818
+ | ----------------------------------- | -------------------------------------------------------------------------------------------------- |
819
+ | `src/crypto/ed25519.ts` | `createNewEd25519Keypair`, `importEd25519Keypair`, `signPayloadEd25519`, `isValidEd25519Signature` |
820
+ | `src/crypto/jws.ts` | `createJws`, `verifyJws`, `decodeJwsUnsafe`, `JwsVerificationError` |
821
+ | `src/crypto/jwt.ts` | `createJwt`, `verifyJwt`, `decodeJwtUnsafe` (EdDSA only) |
822
+ | `src/crypto/base64url.ts` | `base64urlEncode`, `base64urlDecode` |
823
+ | `src/crypto/multiformats.ts` | `dagCborCanonicalEncode`, `dagCborCanonicalEqual` |
824
+ | `src/crypto/id.ts` | `generateId`, `generateIdNoPrefix`, `isValidId` |
825
+ | `src/chain/multikey.ts` | `encodeEd25519Multikey`, `decodeMultikey` |
826
+ | `src/chain/schemas.ts` | `IdentityOperation`, `ContentOperation`, `MultikeyPublicKey`, `VerifiedIdentity` |
827
+ | `src/chain/identity-chain.ts` | `signIdentityOperation`, `verifyIdentityChain` |
828
+ | `src/chain/content-chain.ts` | `signContentOperation`, `verifyContentChain` |
829
+ | `src/chain/derivation.ts` | `deriveChainIdentifier` |
830
+ | `src/registry/schemas.ts` | Registry API Zod types (wire contract) |
831
+ | `src/registry/server.ts` | Reference Hono registry server |
832
+ | `src/registry/store.ts` | In-memory chain store with linear enforcement |
833
+ | `openapi.yaml` | OpenAPI 3.1 spec for registry API |
834
+ | `schemas/document-envelope.v1.json` | JSON Schema for the document envelope wrapper |
835
+ | `schemas/post.v1.json` | JSON Schema for post documents |
836
+ | `schemas/profile.v1.json` | JSON Schema for profile documents |
837
+ | `tests/protocol-reference.spec.ts` | Deterministic artifact generator (this doc's source) |
838
+ | `verify/go/` | Go cross-language verification (9 tests) |
839
+ | `verify/python/` | Python cross-language verification (32 checks) |
840
+ | `verify/rust/` | Rust cross-language verification (9 tests) |
841
+ | `verify/swift/` | Swift cross-language verification (8 tests) |
842
+
843
+ ---
844
+
845
+ ## Test Coverage (160 checks across 5 languages)
846
+
847
+ ### TypeScript — dfos-protocol (99 tests)
848
+
849
+ - `tests/crypto.spec.ts` (13): ed25519 keypair/sign/verify, JWS round-trip/wrong key/tampered/decode/malformed
850
+ - `tests/chain.spec.ts` (39): multikey encoding, identity chain (genesis, DID, rotation, delete, cid header, errors), content chain (lifecycle, clear, delete, cid header, errors)
851
+ - `tests/registry.spec.ts` (18): HTTP contract — submission, resubmission, extension, fork rejection, pagination, cross-chain key resolution, 404s
852
+ - `tests/schemas.spec.ts` (28): JSON Schema compilation + validation for document envelope, post, profile — conforming documents, missing fields, invalid values, additional properties
853
+ - `tests/protocol-reference.spec.ts` (1): deterministic artifact generator
854
+
855
+ ### Python — verify/ (35 checks)
856
+
857
+ - Key derivation, multikey, dag-cbor bytes, CID/DID derivation, JWS/JWT signatures, CID header verification, document CID
858
+ - Dependencies: `pynacl`, `dag-cbor`, `base58`
859
+
860
+ ### Go — verify/ (9 tests)
861
+
862
+ - Key derivation, multikey, dag-cbor, CID/DID derivation, JWS/JWT signatures, CID header verification
863
+ - Dependencies: `fxamacker/cbor/v2`, `mr-tron/base58`
864
+
865
+ ### Rust — verify/ (9 tests)
866
+
867
+ - Key derivation, multikey, dag-cbor, CID/DID derivation, JWS/JWT signatures, CID header verification
868
+ - Dependencies: `ed25519-dalek`, `ciborium`, `sha2`, `bs58`, `base64`, `data-encoding`
869
+
870
+ ### Swift — verify/ (8 tests)
871
+
872
+ - Key derivation, multikey, CID/DID derivation, JWS/JWT signatures, CID header verification
873
+ - Dependencies: Apple `Crypto` (swift-crypto)