@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/LICENSE +21 -0
- package/PROTOCOL.md +873 -0
- package/README.md +50 -0
- package/dist/chain/index.d.ts +196 -0
- package/dist/chain/index.js +33 -0
- package/dist/chunk-3PB644X2.js +330 -0
- package/dist/chunk-LWC4PWGZ.js +385 -0
- package/dist/chunk-ZXXP5W5N.js +251 -0
- package/dist/crypto/index.d.ts +205 -0
- package/dist/crypto/index.js +46 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +108 -0
- package/dist/registry/index.d.ts +151 -0
- package/dist/registry/index.js +36 -0
- package/examples/content-delete.json +28 -0
- package/examples/content-lifecycle.json +39 -0
- package/examples/identity-delete.json +19 -0
- package/examples/identity-genesis.json +18 -0
- package/examples/identity-rotation.json +19 -0
- package/openapi.yaml +408 -0
- package/package.json +75 -0
- package/schemas/document-envelope.v1.json +37 -0
- package/schemas/post.v1.json +59 -0
- package/schemas/profile.v1.json +52 -0
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)
|