@metalabel/dfos-protocol 0.5.0 → 0.6.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/README.md +5 -5
- package/package.json +1 -4
- package/CONTENT-MODEL.md +0 -148
- package/DID-METHOD.md +0 -325
- package/PROTOCOL.md +0 -1176
package/PROTOCOL.md
DELETED
|
@@ -1,1176 +0,0 @@
|
|
|
1
|
-
# DFOS Protocol
|
|
2
|
-
|
|
3
|
-
Verifiable identity and content chains — Ed25519 signatures, content-addressed CIDs, W3C DIDs. Cross-language verification in TypeScript, Go, Python, Rust, and Swift.
|
|
4
|
-
|
|
5
|
-
This spec is under active review. Discuss it in the [clear.txt](https://clear.dfos.com) space on DFOS.
|
|
6
|
-
|
|
7
|
-
[Source](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol) · [npm](https://www.npmjs.com/package/@metalabel/dfos-protocol) · [Gist](https://gist.github.com/bvalosek/ed4c96fd4b841302de544ffaee871648)
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## Philosophy
|
|
12
|
-
|
|
13
|
-
DFOS is a dark forest operating system. Content lives in private spaces — visible only to members, governed by the communities that create it. The cryptographic proof layer is public: signed chains of commitments that anyone can independently verify with a public key and any standard EdDSA library.
|
|
14
|
-
|
|
15
|
-
Two chain types — identity and content — use the same mechanics: Ed25519 signatures, JWS compact tokens, content-addressed CIDs. The protocol knows about keys and document hashes. It doesn't know about posts, profiles, or any application concept. Document semantics are application layer — free to evolve without protocol changes.
|
|
16
|
-
|
|
17
|
-
The protocol is not coupled to the DFOS platform. Any system implementing the same chain primitives produces interoperable, cross-verifiable proofs. An identity created on one system can sign content on another.
|
|
18
|
-
|
|
19
|
-
---
|
|
20
|
-
|
|
21
|
-
## Protocol Overview
|
|
22
|
-
|
|
23
|
-
The DFOS protocol has six components:
|
|
24
|
-
|
|
25
|
-
| Component | Concern |
|
|
26
|
-
| --------------------- | ------------------------------------------------------------------------------- |
|
|
27
|
-
| **Crypto core** | Identity chains + content chains — Ed25519 signatures, JWS tokens, CID links |
|
|
28
|
-
| **Credentials** | Auth tokens (DID-signed JWT) and VC-JWT credentials for authorization |
|
|
29
|
-
| **Beacons** | Signed merkle root announcements — periodic commitment over content sets |
|
|
30
|
-
| **Artifacts** | Standalone signed inline documents — immutable, CID-addressable structured data |
|
|
31
|
-
| **Countersignatures** | Standalone witness attestation — signed references to any CID-addressable op |
|
|
32
|
-
| **Merkle trees** | SHA-256 binary trees over content IDs — inclusion proofs for beacon roots |
|
|
33
|
-
|
|
34
|
-
The crypto core is the trust boundary — everything below it is cryptographically verified. Documents are flat content objects, content-addressed directly: `documentCID = CID(dagCborCanonicalEncode(contentObject))`. What goes inside the content object is application-defined — see the [DFOS Content Model](https://protocol.dfos.com/content-model) for the standard schema library.
|
|
35
|
-
|
|
36
|
-
### Crypto Core: Two Chain Types
|
|
37
|
-
|
|
38
|
-
| | Identity Chain | Content Chain |
|
|
39
|
-
| -------------- | -------------------------- | -------------------------------- |
|
|
40
|
-
| Commits to | Key sets (embedded) | Documents (by CID reference) |
|
|
41
|
-
| Identifier | `did:dfos:<hash>` | `<hash>` (bare) |
|
|
42
|
-
| Operations | create, update, delete | create, update, delete |
|
|
43
|
-
| JWS typ | `did:dfos:identity-op` | `did:dfos:content-op` |
|
|
44
|
-
| Self-sovereign | Yes (signs own operations) | No (signed by external identity) |
|
|
45
|
-
|
|
46
|
-
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 flat content object.
|
|
47
|
-
|
|
48
|
-
### Addressing
|
|
49
|
-
|
|
50
|
-
Three addressing modes, self-describing by format:
|
|
51
|
-
|
|
52
|
-
| Thing | Form | Example |
|
|
53
|
-
| --------------------- | ------------------------ | --------------------------------- |
|
|
54
|
-
| Operation or document | CID (dag-cbor + SHA-256) | `bafyrei...` (base32lower) |
|
|
55
|
-
| Content chain | contentId (22-char hash) | `a82z92a3hndk6c97thcrn8` |
|
|
56
|
-
| Identity chain | DID | `did:dfos:e3vvtck42d4eacdnzvtrn6` |
|
|
57
|
-
|
|
58
|
-
CIDs are specific immutable artifacts — a pointer to an exact operation or document. Content IDs are living content chain entities — the 22-char bare hash derived from the genesis CID. DIDs are living identity chain entities.
|
|
59
|
-
|
|
60
|
-
Operations and documents are CIDs — standard IPLD content addresses. Content chains and identity chains use derived identifiers — `customAlpha(SHA-256(genesis CID bytes))`. Same derivation for both. Identity chains prepend `did:dfos:` (W3C DID spec). Content identifiers are bare — just the 22-char hash, no prefix.
|
|
61
|
-
|
|
62
|
-
Application code may add prefixes for routing (e.g., `post_xxxx`) — these are strippable semantic sugar, not part of the protocol identifier.
|
|
63
|
-
|
|
64
|
-
---
|
|
65
|
-
|
|
66
|
-
## Protocol Rules
|
|
67
|
-
|
|
68
|
-
### Commitment Scheme
|
|
69
|
-
|
|
70
|
-
Both operations and documents are content-addressed via **CID** (`dagCborCanonicalEncode(payload)` → SHA-256 → CIDv1). Operations are additionally signed via **JWS**.
|
|
71
|
-
|
|
72
|
-
| Representation | Encoding | Purpose |
|
|
73
|
-
| -------------- | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
|
|
74
|
-
| CID | `dagCborCanonicalEncode(payload)` → SHA-256 → CIDv1 | Deterministic content addressing for operations and documents |
|
|
75
|
-
| JWS | `base64url(JSON.stringify(header))` + `.` + `base64url(JSON.stringify(payload))` → EdDSA signature covers both | Signature verification for operations |
|
|
76
|
-
|
|
77
|
-
CID uses [dag-cbor canonical encoding](https://ipld.io/specs/codecs/dag-cbor/spec/) for determinism — given the same logical payload, the CID MUST be identical regardless of implementation language or platform. JWS uses standard JSON for library interoperability. The dag-cbor hex test vectors in this document allow byte-level verification.
|
|
78
|
-
|
|
79
|
-
### Chain Validity
|
|
80
|
-
|
|
81
|
-
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.
|
|
82
|
-
|
|
83
|
-
**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 (e.g., longest chain, first-seen, advisory locks).
|
|
84
|
-
|
|
85
|
-
**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.
|
|
86
|
-
|
|
87
|
-
### Identity Chain Signer Validity
|
|
88
|
-
|
|
89
|
-
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.
|
|
90
|
-
|
|
91
|
-
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.
|
|
92
|
-
|
|
93
|
-
### Content Chain Signer Model
|
|
94
|
-
|
|
95
|
-
Content chain verification requires a **valid EdDSA signature** and delegates key resolution to the caller. The `kid` in each operation's JWS header is a DID URL (`did:dfos:<id>#<keyId>`). The verifier calls `resolveKey(kid)` to obtain the raw Ed25519 public key bytes for that key on that identity. How the resolver obtains and validates the identity's key state is application-defined.
|
|
96
|
-
|
|
97
|
-
**Creator sovereignty**: The DID that signs the genesis (create) operation is the **chain creator** and permanently owns the chain. The creator can sign subsequent operations directly — no credential needed. Other DIDs require a **DFOSContentWrite** VC-JWT credential in the operation's `authorization` field, issued by the creator DID. See [Credentials](#credentials) for the VC-JWT format.
|
|
98
|
-
|
|
99
|
-
**Signer-payload consistency**: The `kid` DID in the JWS header MUST match the `did` field in the content operation payload. This enables discrimination between author operations and countersignatures — if the kid DID differs from the payload `did`, it is a countersignature (witness attestation), not a chain operation.
|
|
100
|
-
|
|
101
|
-
**What the protocol enforces:**
|
|
102
|
-
|
|
103
|
-
- The EdDSA signature on each operation is valid against the key returned by `resolveKey(kid)`
|
|
104
|
-
- Chain integrity (CID links, timestamp ordering, terminal state)
|
|
105
|
-
- The `kid` DID matches the payload `did` for chain operations
|
|
106
|
-
- Creator-sovereignty authorization (when `enforceAuthorization` is enabled): non-creator signers must present a valid DFOSContentWrite VC-JWT issued by the creator
|
|
107
|
-
|
|
108
|
-
**What the protocol does NOT enforce (application concerns):**
|
|
109
|
-
|
|
110
|
-
- Which key role (auth, assert, controller) the signing key must have
|
|
111
|
-
- Ownership or attribution semantics beyond creator sovereignty
|
|
112
|
-
|
|
113
|
-
### Terminal States and Special Operations
|
|
114
|
-
|
|
115
|
-
**`delete` is the only terminal state.** No valid operations may follow a delete. An implementation MUST reject any operation after a delete. Delete prevents future operations but does NOT remove data — the complete chain remains intact for verification. Data removal is an application concern.
|
|
116
|
-
|
|
117
|
-
**Controller key requirement:** `update` operations on identity chains MUST include at least one controller key. If decommissioning is intended, `delete` is the correct terminal operation.
|
|
118
|
-
|
|
119
|
-
**Content-null:** An `update` on a content chain with `documentCID: null` means the content exists but its document is cleared. The chain continues — a subsequent update can set content again.
|
|
120
|
-
|
|
121
|
-
### `typ` Header
|
|
122
|
-
|
|
123
|
-
The JWS `typ` header uses protocol-specific values (not IANA media types):
|
|
124
|
-
|
|
125
|
-
| `typ` value | Usage |
|
|
126
|
-
| ---------------------- | --------------------------------------------- |
|
|
127
|
-
| `did:dfos:identity-op` | Identity chain operations |
|
|
128
|
-
| `did:dfos:content-op` | Content chain operations |
|
|
129
|
-
| `did:dfos:beacon` | Beacon announcements |
|
|
130
|
-
| `did:dfos:artifact` | Standalone signed inline documents |
|
|
131
|
-
| `did:dfos:countersign` | Standalone witness attestations |
|
|
132
|
-
| `JWT` | Auth tokens (DID-signed relay authentication) |
|
|
133
|
-
| `vc+jwt` | VC-JWT credentials (W3C VC Data Model v2) |
|
|
134
|
-
|
|
135
|
-
Protocol-specific `typ` values are non-standard per JOSE convention, documented intentionally. `JWT` and `vc+jwt` follow IANA conventions. The `typ` header aids routing but is not security-critical. Implementations SHOULD validate it but MUST NOT rely on it for security decisions.
|
|
136
|
-
|
|
137
|
-
### Operation Field Limits
|
|
138
|
-
|
|
139
|
-
The protocol defines maximum sizes for all operation fields as abuse-prevention ceilings. Implementations MUST reject operations that exceed these bounds. Implementations MAY impose stricter limits.
|
|
140
|
-
|
|
141
|
-
| Field | Max | Rationale |
|
|
142
|
-
| -------------------------------------------- | --------- | -------------------------------------- |
|
|
143
|
-
| `did` | 256 chars | ~8× typical `did:dfos:` (~31 chars) |
|
|
144
|
-
| `key.id` | 64 chars | ~3× typical key ID (`key_` + 22 chars) |
|
|
145
|
-
| `key.publicKeyMultibase` | 128 chars | ~2× Ed25519 multikey (~50 chars) |
|
|
146
|
-
| `authKeys` / `assertKeys` / `controllerKeys` | 16 items | Generous for key rotation |
|
|
147
|
-
| `previousOperationCID` | 256 chars | ~4× typical CIDv1 (~60 chars) |
|
|
148
|
-
| `documentCID` | 256 chars | Same as above |
|
|
149
|
-
| `note` | 256 chars | Short annotation, not prose |
|
|
150
|
-
|
|
151
|
-
These limits are enforced by the Zod schemas in `src/chain/schemas.ts`. Any implementation parsing operations MUST reject values exceeding these bounds.
|
|
152
|
-
|
|
153
|
-
The protocol does NOT limit:
|
|
154
|
-
|
|
155
|
-
- **Document content size** — the protocol commits to a CID, not the document. Document size limits are application/registry concerns.
|
|
156
|
-
- **Chain length** — no maximum operations per chain.
|
|
157
|
-
- **Number of chains per identity** — application scaling concern.
|
|
158
|
-
|
|
159
|
-
---
|
|
160
|
-
|
|
161
|
-
## Standards and Dependencies
|
|
162
|
-
|
|
163
|
-
| Component | Standard / Library |
|
|
164
|
-
| ------------------- | -------------------------------------------------------------------------- |
|
|
165
|
-
| Key generation | Ed25519 (RFC 8032) via `@noble/curves/ed25519` |
|
|
166
|
-
| Signature algorithm | EdDSA over Ed25519 (pure, no prehash — Ed25519 handles SHA-512 internally) |
|
|
167
|
-
| Key encoding | W3C Multikey (multicodec `0xed01` + base58btc multibase) |
|
|
168
|
-
| Signed envelopes | JWS Compact Serialization (RFC 7515) with `alg: "EdDSA"` |
|
|
169
|
-
| Content addressing | CIDv1 with dag-cbor codec (`0x71`) + SHA-256 multihash (`0x12`) |
|
|
170
|
-
| ID encoding | SHA-256 → custom 19-char alphabet, 22 characters |
|
|
171
|
-
|
|
172
|
-
### ID Alphabet
|
|
173
|
-
|
|
174
|
-
```
|
|
175
|
-
Alphabet: 2346789acdefhknrtvz (19 characters)
|
|
176
|
-
Length: 22 characters
|
|
177
|
-
Entropy: ~93.4 bits (19^22)
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
Process: `SHA-256(input) → for each of first 22 bytes: alphabet[byte % 19]`. The modulo introduces a ~0.3% bias (256 is not evenly divisible by 19) — not security-relevant for identifiers.
|
|
181
|
-
|
|
182
|
-
DIDs: `did:dfos:` + 22-char ID derived from `SHA-256(genesis CID raw bytes)`
|
|
183
|
-
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.
|
|
184
|
-
|
|
185
|
-
### Multikey Encoding (W3C Multikey for Ed25519)
|
|
186
|
-
|
|
187
|
-
```
|
|
188
|
-
Encode:
|
|
189
|
-
1. Take 32-byte Ed25519 public key
|
|
190
|
-
2. Prepend multicodec varint prefix [0xed, 0x01] (unsigned varint for 0xed = 237 = ed25519-pub)
|
|
191
|
-
3. Base58btc encode the 34-byte result
|
|
192
|
-
4. Prepend 'z' multibase prefix
|
|
193
|
-
→ "z6Mk..."
|
|
194
|
-
|
|
195
|
-
Decode:
|
|
196
|
-
1. Strip 'z' multibase prefix
|
|
197
|
-
2. Base58btc decode → 34 bytes
|
|
198
|
-
3. First 2 bytes must be [0xed, 0x01] (ed25519-pub multicodec varint)
|
|
199
|
-
4. Remaining 32 bytes = raw Ed25519 public key
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
**Worked example:**
|
|
203
|
-
|
|
204
|
-
```
|
|
205
|
-
Public key (hex): ba421e272fad4f941c221e47f87d9253bdc04f7d4ad2625ae667ab9f0688ce32
|
|
206
|
-
Prefix + key (hex): ed01 ba421e272fad4f941c221e47f87d9253bdc04f7d4ad2625ae667ab9f0688ce32
|
|
207
|
-
Base58btc + 'z': z6MkrzLMNwoJSV4P3YccWcbtk8vd9LtgMKnLeaDLUqLuASjb
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
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]`.
|
|
211
|
-
|
|
212
|
-
### CID Construction (dag-cbor + SHA-256)
|
|
213
|
-
|
|
214
|
-
```
|
|
215
|
-
1. JSON payload → dag-cbor canonical encoding → CBOR bytes
|
|
216
|
-
2. SHA-256(CBOR bytes) → 32-byte hash
|
|
217
|
-
3. Construct CIDv1:
|
|
218
|
-
- Version: 1 (varint: 0x01)
|
|
219
|
-
- Codec: dag-cbor (varint: 0x71)
|
|
220
|
-
- Multihash: SHA-256 (function: 0x12, length: 0x20, digest: 32 bytes)
|
|
221
|
-
4. CID binary = [0x01, 0x71, 0x12, 0x20, ...32 hash bytes]
|
|
222
|
-
5. Base32lower multibase encode → "bafyrei..."
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
dag-cbor canonical ordering: map keys sorted by encoded byte length first, then lexicographic. Strings to CBOR text strings. Null to CBOR null. Arrays to CBOR arrays. Objects to CBOR maps with sorted keys.
|
|
226
|
-
|
|
227
|
-
#### Number Encoding (Critical for CID Determinism)
|
|
228
|
-
|
|
229
|
-
JSON has a single number type (IEEE 754 double). CBOR has distinct integer and floating-point types with different byte encodings. This difference is the most common source of CID divergence across implementations.
|
|
230
|
-
|
|
231
|
-
**Rule: JSON numbers that are mathematically integers (no fractional part) MUST be encoded as CBOR integers (major type 0/1), never as CBOR floats.** This is consistent with the [IPLD data model](https://ipld.io/docs/data-model/) integer/float distinction and required by the [dag-cbor codec spec](https://ipld.io/specs/codecs/dag-cbor/spec/).
|
|
232
|
-
|
|
233
|
-
Why this matters: CBOR integer `1` encodes as a single byte `0x01`. CBOR float `1.0` encodes as three bytes `0xf9 0x3c 0x00` (half-precision). Same logical value, different bytes, different SHA-256, different CID. An implementation that encodes `version: 1` as a float will produce a valid CBOR document but a wrong CID — silent, undetectable without cross-implementation testing.
|
|
234
|
-
|
|
235
|
-
**Common trap**: Languages that decode JSON into untyped maps (Go's `map[string]any`, Python's `dict`, etc.) typically represent all JSON numbers as floating-point. When this decoded value is then CBOR-encoded, it becomes a CBOR float instead of an integer. Implementations MUST normalize number types after JSON deserialization and before CBOR encoding.
|
|
236
|
-
|
|
237
|
-
**Integer bounds**: dag-cbor integers are limited to the range `[-(2^64), 2^64 - 1]`. All integer fields in the current protocol (`version: 1`) are small positive values. Future protocol extensions SHOULD NOT introduce integer fields that exceed JSON's safe integer range (`2^53 - 1`), as JSON serialization would lose precision.
|
|
238
|
-
|
|
239
|
-
**Verification test vector** — encodes `{"version": 1, "type": "test"}`:
|
|
240
|
-
|
|
241
|
-
```
|
|
242
|
-
Integer encoding (CORRECT):
|
|
243
|
-
CBOR: a2647479706564746573746776657273696f6e01
|
|
244
|
-
CID: bafyreihp6omsp6icc6ee63ox2ovsaxm6s7ikd2a7k5eh2qz2qd5soh5bsa
|
|
245
|
-
|
|
246
|
-
Float encoding (WRONG — different bytes, different CID):
|
|
247
|
-
CBOR: a2647479706564746573746776657273696f6ef93c00
|
|
248
|
-
CID: bafyreiawbms4476m5jlrmqtyvtwe5ta3eo2bh7mdprtomfgfype7j57o4q
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
If your implementation produces the float CID, your number encoding is incorrect. The byte at offset 19 in the CBOR output is the discriminator: `0x01` = correct (CBOR integer), `0xf9` = wrong (CBOR float16 header).
|
|
252
|
-
|
|
253
|
-
**Worked example (genesis identity operation):**
|
|
254
|
-
|
|
255
|
-
```
|
|
256
|
-
CBOR bytes (441 bytes, hex):
|
|
257
|
-
a66474797065666372656174656776657273696f6e0168617574684b65797381a3626964781a6b
|
|
258
|
-
65795f72396576333466766332337a393939766561616674386474797065684d756c74696b6579
|
|
259
|
-
727075626c69634b65794d756c74696261736578307a364d6b727a4c4d4e776f4a535634503359
|
|
260
|
-
6363576362746b387664394c74674d4b6e4c6561444c55714c7541536a62696372656174656441
|
|
261
|
-
747818323032362d30332d30375430303a30303a30302e3030305a6a6173736572744b65797381
|
|
262
|
-
a3626964781a6b65795f72396576333466766332337a393939766561616674386474797065684d
|
|
263
|
-
756c74696b6579727075626c69634b65794d756c74696261736578307a364d6b727a4c4d4e776f
|
|
264
|
-
4a5356345033596363576362746b387664394c74674d4b6e4c6561444c55714c7541536a626e63
|
|
265
|
-
6f6e74726f6c6c65724b65797381a3626964781a6b65795f72396576333466766332337a393939
|
|
266
|
-
766561616674386474797065684d756c74696b6579727075626c69634b65794d756c7469626173
|
|
267
|
-
6578307a364d6b727a4c4d4e776f4a5356345033596363576362746b387664394c74674d4b6e4c
|
|
268
|
-
6561444c55714c7541536a62
|
|
269
|
-
|
|
270
|
-
CID bytes (hex): 01711220206a5e6140a5114f1e49f3ca4b339fb2cb8e70bbb34968b23156fd0e3237b486
|
|
271
|
-
CID string: bafyreibanjpgcqffcfhr4sptzjfthh5szohhbo5tjfulemkw7uhden5uqy
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
### DID Derivation (worked example)
|
|
275
|
-
|
|
276
|
-
```
|
|
277
|
-
Input: CID bytes (hex) = 01711220206a5e6140a5114f1e49f3ca4b339fb2cb8e70bbb34968b23156fd0e3237b486
|
|
278
|
-
Step 1: SHA-256(CID bytes) = 4360cfbcbbb3f1614c8e02dbfe8d55935e1195cd2129820ab8aef94bde12ea8a
|
|
279
|
-
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
|
|
280
|
-
Step 3: For each byte, alphabet[byte % 19]:
|
|
281
|
-
43=67 → 67%19=10 → 'e'
|
|
282
|
-
60=96 → 96%19=1 → '3'
|
|
283
|
-
cf=207 → 207%19=17 → 'v'
|
|
284
|
-
bc=188 → 188%19=17 → 'v'
|
|
285
|
-
...
|
|
286
|
-
Result: e3vvtck42d4eacdnzvtrn6
|
|
287
|
-
DID: did:dfos:e3vvtck42d4eacdnzvtrn6
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
---
|
|
291
|
-
|
|
292
|
-
## Operation Schemas
|
|
293
|
-
|
|
294
|
-
### Identity Operations
|
|
295
|
-
|
|
296
|
-
```typescript
|
|
297
|
-
// Genesis — starts the identity chain
|
|
298
|
-
{ version: 1, type: "create",
|
|
299
|
-
authKeys: MultikeyPublicKey[],
|
|
300
|
-
assertKeys: MultikeyPublicKey[],
|
|
301
|
-
controllerKeys: MultikeyPublicKey[], // must have at least one
|
|
302
|
-
createdAt: string } // ISO 8601, ms precision, UTC
|
|
303
|
-
|
|
304
|
-
// Key rotation / modification
|
|
305
|
-
{ version: 1, type: "update",
|
|
306
|
-
previousOperationCID: string, // CID of previous operation
|
|
307
|
-
authKeys: MultikeyPublicKey[],
|
|
308
|
-
assertKeys: MultikeyPublicKey[],
|
|
309
|
-
controllerKeys: MultikeyPublicKey[], // must have at least one
|
|
310
|
-
createdAt: string }
|
|
311
|
-
|
|
312
|
-
// Permanent destruction
|
|
313
|
-
{ version: 1, type: "delete",
|
|
314
|
-
previousOperationCID: string,
|
|
315
|
-
createdAt: string }
|
|
316
|
-
```
|
|
317
|
-
|
|
318
|
-
### Content Operations
|
|
319
|
-
|
|
320
|
-
```typescript
|
|
321
|
-
// Genesis — starts the content chain, commits initial document
|
|
322
|
-
{ version: 1, type: "create",
|
|
323
|
-
did: string, // author DID, committed to by CID
|
|
324
|
-
documentCID: string, // CID of flat content object
|
|
325
|
-
baseDocumentCID: string | null, // edit lineage — CID of prior document version
|
|
326
|
-
createdAt: string,
|
|
327
|
-
note: string | null }
|
|
328
|
-
|
|
329
|
-
// Content change (null documentCID = clear content)
|
|
330
|
-
{ version: 1, type: "update",
|
|
331
|
-
did: string, // author DID
|
|
332
|
-
previousOperationCID: string,
|
|
333
|
-
documentCID: string | null,
|
|
334
|
-
baseDocumentCID: string | null,
|
|
335
|
-
createdAt: string,
|
|
336
|
-
note: string | null,
|
|
337
|
-
authorization?: string } // VC-JWT for delegated operations
|
|
338
|
-
|
|
339
|
-
// Permanent destruction
|
|
340
|
-
{ version: 1, type: "delete",
|
|
341
|
-
did: string, // author DID
|
|
342
|
-
previousOperationCID: string,
|
|
343
|
-
createdAt: string,
|
|
344
|
-
note: string | null,
|
|
345
|
-
authorization?: string } // VC-JWT for delegated operations
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
### MultikeyPublicKey
|
|
349
|
-
|
|
350
|
-
```typescript
|
|
351
|
-
{ id: string, // e.g. "key_r9ev34fvc23z999veaaft8"
|
|
352
|
-
type: "Multikey", // literal discriminator
|
|
353
|
-
publicKeyMultibase: string } // e.g. "z6MkrzLMNwoJSV4P3YccWcbtk8vd9LtgMKnLeaDLUqLuASjb"
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
---
|
|
357
|
-
|
|
358
|
-
## JWS Envelope Format
|
|
359
|
-
|
|
360
|
-
### Signing
|
|
361
|
-
|
|
362
|
-
```
|
|
363
|
-
signingInput = base64url(JSON.stringify(header)) + "." + base64url(JSON.stringify(payload))
|
|
364
|
-
signature = ed25519.sign(UTF8_bytes(signingInput), privateKey)
|
|
365
|
-
token = signingInput + "." + base64url(signature)
|
|
366
|
-
```
|
|
367
|
-
|
|
368
|
-
### kid Rules
|
|
369
|
-
|
|
370
|
-
| Context | kid format | Example |
|
|
371
|
-
| ------------------------- | ----------- | ---------------------------- |
|
|
372
|
-
| Identity create (genesis) | Bare key ID | `key_r9ev34fvc23z999veaaft8` |
|
|
373
|
-
| Identity update/delete | DID URL | See below |
|
|
374
|
-
| All content ops | DID URL | See below |
|
|
375
|
-
|
|
376
|
-
DID URL examples:
|
|
377
|
-
|
|
378
|
-
```
|
|
379
|
-
did:dfos:e3vvtck42d4eacdnzvtrn6#key_r9ev34fvc23z999veaaft8
|
|
380
|
-
did:dfos:e3vvtck42d4eacdnzvtrn6#key_ez9a874tckr3dv933d3ckd
|
|
381
|
-
```
|
|
382
|
-
|
|
383
|
-
### `cid` Header
|
|
384
|
-
|
|
385
|
-
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.
|
|
386
|
-
|
|
387
|
-
**Signing order:**
|
|
388
|
-
|
|
389
|
-
1. Construct the operation payload
|
|
390
|
-
2. Derive the operation CID: `dagCborCanonicalEncode(payload) → CIDv1`
|
|
391
|
-
3. Build the protected header including `cid`
|
|
392
|
-
4. Sign: `ed25519.sign(UTF8(base64url(header) + "." + base64url(payload)), privateKey)`
|
|
393
|
-
|
|
394
|
-
**Verification rule:** After verifying the JWS signature and deriving the operation CID from the parsed payload, implementations MUST reject operations where:
|
|
395
|
-
|
|
396
|
-
- `header.cid` is missing
|
|
397
|
-
- `header.cid` does not match the derived CID
|
|
398
|
-
|
|
399
|
-
A CID mismatch between header and derived value immediately surfaces dag-cbor encoding disagreements across implementations.
|
|
400
|
-
|
|
401
|
-
Note: JWT auth tokens and VC-JWT credentials do NOT include a `cid` header — this field is specific to operation JWS tokens and beacons.
|
|
402
|
-
|
|
403
|
-
### CID Derivation
|
|
404
|
-
|
|
405
|
-
```
|
|
406
|
-
operation CID = dagCborCanonicalEncode(operation_payload) → SHA-256 → CIDv1 → base32lower string
|
|
407
|
-
```
|
|
408
|
-
|
|
409
|
-
The CID is derived from the JWS payload (the unsigned operation JSON), NOT from the JWS token itself.
|
|
410
|
-
|
|
411
|
-
### DID Derivation
|
|
412
|
-
|
|
413
|
-
```
|
|
414
|
-
DID = "did:dfos:" + idEncode(SHA-256(genesis_CID_raw_bytes))
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
Where `idEncode` is the 19-char alphabet encoding described above.
|
|
418
|
-
|
|
419
|
-
---
|
|
420
|
-
|
|
421
|
-
## Credentials
|
|
422
|
-
|
|
423
|
-
Two credential types handle authentication and authorization. Both are DID-signed JWTs using Ed25519 (`alg: "EdDSA"`).
|
|
424
|
-
|
|
425
|
-
### Auth Tokens (Relay Authentication)
|
|
426
|
-
|
|
427
|
-
A DID-signed JWT proving the caller controls a DID. Short-lived, scoped to a specific relay via the `aud` (audience) claim. Used for relay AuthN — establishing identity before making requests.
|
|
428
|
-
|
|
429
|
-
**JWT Header:**
|
|
430
|
-
|
|
431
|
-
```json
|
|
432
|
-
{
|
|
433
|
-
"alg": "EdDSA",
|
|
434
|
-
"typ": "JWT",
|
|
435
|
-
"kid": "did:dfos:e3vvtck42d4eacdnzvtrn6#key_r9ev34fvc23z999veaaft8"
|
|
436
|
-
}
|
|
437
|
-
```
|
|
438
|
-
|
|
439
|
-
**JWT Payload:**
|
|
440
|
-
|
|
441
|
-
```json
|
|
442
|
-
{
|
|
443
|
-
"iss": "did:dfos:e3vvtck42d4eacdnzvtrn6",
|
|
444
|
-
"sub": "did:dfos:e3vvtck42d4eacdnzvtrn6",
|
|
445
|
-
"aud": "relay.example.com",
|
|
446
|
-
"exp": 1772845200,
|
|
447
|
-
"iat": 1772841600
|
|
448
|
-
}
|
|
449
|
-
```
|
|
450
|
-
|
|
451
|
-
| Field | Type | Description |
|
|
452
|
-
| ----- | ------ | ---------------------------------------------------------- |
|
|
453
|
-
| `iss` | string | DID proving identity (the signer) |
|
|
454
|
-
| `sub` | string | Same as `iss` for auth tokens |
|
|
455
|
-
| `aud` | string | Target relay hostname (prevents cross-relay replay) |
|
|
456
|
-
| `exp` | number | Expiration — unix seconds (short-lived, typically minutes) |
|
|
457
|
-
| `iat` | number | Issued-at — unix seconds |
|
|
458
|
-
|
|
459
|
-
**Verification:** Standard JWT verification — EdDSA signature check, temporal validity (`iat` must not be in the future, `exp` must be after current time), audience match. The `kid` MUST be a DID URL (`did:dfos:xxx#key_yyy`) and the `kid` DID MUST match `iss`.
|
|
460
|
-
|
|
461
|
-
Auth tokens do NOT include a `cid` header — they are ephemeral session tokens, not content-addressed artifacts.
|
|
462
|
-
|
|
463
|
-
### VC-JWT Credentials (Authorization)
|
|
464
|
-
|
|
465
|
-
W3C Verifiable Credential Data Model v2 credentials encoded as JWT (`typ: "vc+jwt"`). Two credential types:
|
|
466
|
-
|
|
467
|
-
| Credential Type | Purpose |
|
|
468
|
-
| ------------------ | ------------------------------------------------------------ |
|
|
469
|
-
| `DFOSContentWrite` | Authorize extending a content chain (embedded in operations) |
|
|
470
|
-
| `DFOSContentRead` | Authorize reading content plane data (presented to relay) |
|
|
471
|
-
|
|
472
|
-
**VC-JWT Header:**
|
|
473
|
-
|
|
474
|
-
```json
|
|
475
|
-
{
|
|
476
|
-
"alg": "EdDSA",
|
|
477
|
-
"typ": "vc+jwt",
|
|
478
|
-
"kid": "did:dfos:e3vvtck42d4eacdnzvtrn6#key_r9ev34fvc23z999veaaft8"
|
|
479
|
-
}
|
|
480
|
-
```
|
|
481
|
-
|
|
482
|
-
**VC-JWT Payload:**
|
|
483
|
-
|
|
484
|
-
```json
|
|
485
|
-
{
|
|
486
|
-
"iss": "did:dfos:e3vvtck42d4eacdnzvtrn6",
|
|
487
|
-
"sub": "did:dfos:nzkf838efr424433rn2rzk",
|
|
488
|
-
"exp": 1798761600,
|
|
489
|
-
"iat": 1772841600,
|
|
490
|
-
"vc": {
|
|
491
|
-
"@context": ["https://www.w3.org/ns/credentials/v2"],
|
|
492
|
-
"type": ["VerifiableCredential", "DFOSContentWrite"],
|
|
493
|
-
"credentialSubject": {}
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
```
|
|
497
|
-
|
|
498
|
-
| Field | Type | Description |
|
|
499
|
-
| ---------------------- | -------- | -------------------------------------------------------- |
|
|
500
|
-
| `iss` | string | DID granting the credential (content creator/controller) |
|
|
501
|
-
| `sub` | string | DID receiving the credential (collaborator/reader) |
|
|
502
|
-
| `exp` | number | Expiration — unix seconds |
|
|
503
|
-
| `iat` | number | Issued-at — unix seconds |
|
|
504
|
-
| `vc.@context` | string[] | Must be `["https://www.w3.org/ns/credentials/v2"]` |
|
|
505
|
-
| `vc.type` | string[] | `["VerifiableCredential", "<DFOSCredentialType>"]` |
|
|
506
|
-
| `vc.credentialSubject` | object | Optional narrowing — see scope narrowing below |
|
|
507
|
-
|
|
508
|
-
**Scope narrowing:** The `credentialSubject` object may contain a `contentId` field. If absent, the credential grants broad access to all content by the issuer. If present, the credential is narrowed to the specific content chain.
|
|
509
|
-
|
|
510
|
-
```json
|
|
511
|
-
// Broad — all content by this DID
|
|
512
|
-
{ "credentialSubject": {} }
|
|
513
|
-
|
|
514
|
-
// Narrow — specific content chain only
|
|
515
|
-
{ "credentialSubject": { "contentId": "a82z92a3hndk6c97thcrn8" } }
|
|
516
|
-
```
|
|
517
|
-
|
|
518
|
-
**Verification:** EdDSA signature check, temporal validity (`iat` must not be in the future, `exp` must be after current time — using operation `createdAt` for chain-embedded VCs, wall clock for relay-presented VCs), `kid` DID URL format, `kid` DID matches `iss`, payload structure via Zod schema. Optionally verify `sub` and credential type match expectations.
|
|
519
|
-
|
|
520
|
-
### Content Chain Authorization
|
|
521
|
-
|
|
522
|
-
When `enforceAuthorization` is enabled on content chain verification:
|
|
523
|
-
|
|
524
|
-
1. **Genesis operation**: The signer is the chain creator, always authorized
|
|
525
|
-
2. **Creator signs subsequent ops**: Authorized directly — no credential needed
|
|
526
|
-
3. **Different DID signs**: Must include an `authorization` field containing a valid `DFOSContentWrite` VC-JWT where:
|
|
527
|
-
- `iss` matches the chain creator DID
|
|
528
|
-
- `sub` matches the signing DID
|
|
529
|
-
- The credential is temporally valid (`iat <= op.createdAt < exp`, not wall clock)
|
|
530
|
-
- If `contentId` is present in `credentialSubject`, it must match this chain's contentId
|
|
531
|
-
- The credential type is `DFOSContentWrite`
|
|
532
|
-
|
|
533
|
-
The `authorization` field is available on `update` and `delete` content operations. It is absent for creator-signed operations.
|
|
534
|
-
|
|
535
|
-
---
|
|
536
|
-
|
|
537
|
-
## Beacons
|
|
538
|
-
|
|
539
|
-
A beacon is a signed announcement of a merkle root — a periodic commitment over a set of content IDs. Beacons are floating signed artifacts, not chained. They provide a compact, verifiable snapshot of an identity's content set at a point in time.
|
|
540
|
-
|
|
541
|
-
### Beacon Payload
|
|
542
|
-
|
|
543
|
-
```json
|
|
544
|
-
{
|
|
545
|
-
"version": 1,
|
|
546
|
-
"type": "beacon",
|
|
547
|
-
"did": "did:dfos:e3vvtck42d4eacdnzvtrn6",
|
|
548
|
-
"merkleRoot": "7e80d4780f454e0fca0b090d8c646f572b49354f54154531606105aad2fda28e",
|
|
549
|
-
"createdAt": "2026-03-07T00:05:00.000Z"
|
|
550
|
-
}
|
|
551
|
-
```
|
|
552
|
-
|
|
553
|
-
| Field | Type | Description |
|
|
554
|
-
| ------------ | ------ | ------------------------------------------------------- |
|
|
555
|
-
| `version` | 1 | Protocol version |
|
|
556
|
-
| `type` | string | Literal `"beacon"` |
|
|
557
|
-
| `did` | string | DID of the identity publishing the beacon |
|
|
558
|
-
| `merkleRoot` | string | Hex-encoded SHA-256 root (64 chars, `/^[0-9a-f]{64}$/`) |
|
|
559
|
-
| `createdAt` | string | ISO 8601 timestamp |
|
|
560
|
-
|
|
561
|
-
### Beacon JWS Header
|
|
562
|
-
|
|
563
|
-
```json
|
|
564
|
-
{
|
|
565
|
-
"alg": "EdDSA",
|
|
566
|
-
"typ": "did:dfos:beacon",
|
|
567
|
-
"kid": "did:dfos:e3vvtck42d4eacdnzvtrn6#key_r9ev34fvc23z999veaaft8",
|
|
568
|
-
"cid": "bafyreihholuui7s7ns74iem6ahfxsb472hwogbqd32yrrp5fztc3kxa5qu"
|
|
569
|
-
}
|
|
570
|
-
```
|
|
571
|
-
|
|
572
|
-
### Worked Example: Beacon
|
|
573
|
-
|
|
574
|
-
Using the reference identity (`did:dfos:e3vvtck42d4eacdnzvtrn6`) and key 1 from the identity chain examples. The beacon commits to a merkle root over 5 content IDs (see Merkle Tree worked example below).
|
|
575
|
-
|
|
576
|
-
**Beacon CID** (dag-cbor canonical encode → CIDv1):
|
|
577
|
-
|
|
578
|
-
```
|
|
579
|
-
bafyreihholuui7s7ns74iem6ahfxsb472hwogbqd32yrrp5fztc3kxa5qu
|
|
580
|
-
```
|
|
581
|
-
|
|
582
|
-
**Controller JWS** (key 1 signs):
|
|
583
|
-
|
|
584
|
-
```
|
|
585
|
-
kid: did:dfos:e3vvtck42d4eacdnzvtrn6#key_r9ev34fvc23z999veaaft8
|
|
586
|
-
typ: did:dfos:beacon
|
|
587
|
-
cid: bafyreihholuui7s7ns74iem6ahfxsb472hwogbqd32yrrp5fztc3kxa5qu
|
|
588
|
-
```
|
|
589
|
-
|
|
590
|
-
**Witness countersignature** (a separate identity countersigns the beacon by CID):
|
|
591
|
-
|
|
592
|
-
A countersignature is a standalone operation with its own CID and `typ: did:dfos:countersign`. See the [Countersignatures](#countersignatures) section below.
|
|
593
|
-
|
|
594
|
-
Full JWS tokens are in [`examples/beacon.json`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/examples/beacon.json).
|
|
595
|
-
|
|
596
|
-
### Beacon Semantics
|
|
597
|
-
|
|
598
|
-
Beacons are not chained — there is no `previousOperationCID`. For a given DID, the latest beacon with a strictly-greater `createdAt` timestamp wins. Beacons replace, not accumulate.
|
|
599
|
-
|
|
600
|
-
**Clock skew tolerance**: Implementations MUST reject beacons with a `createdAt` more than 5 minutes in the future relative to the verifier's clock. This prevents pre-dating attacks while accommodating reasonable clock drift.
|
|
601
|
-
|
|
602
|
-
**merkleRoot**: A hex-encoded SHA-256 hash (64 characters). This is a commitment, not a CID — it uses raw SHA-256, not dag-cbor encoding. See the Merkle Tree section below for construction. An empty content set produces a `null` merkle root (no beacon needed).
|
|
603
|
-
|
|
604
|
-
---
|
|
605
|
-
|
|
606
|
-
## Merkle Trees
|
|
607
|
-
|
|
608
|
-
Beacons commit to a set of content IDs via a pure SHA-256 binary Merkle tree. The tree has no dag-cbor dependency — it uses only SHA-256 over raw bytes.
|
|
609
|
-
|
|
610
|
-
### Construction
|
|
611
|
-
|
|
612
|
-
1. **Collect** all content IDs (22-char bare hashes) in the set
|
|
613
|
-
2. **Sort** content IDs lexicographically (UTF-8 byte order)
|
|
614
|
-
3. **Hash leaves**: for each content ID, `SHA-256(UTF-8(contentId))` → 32-byte leaf hash
|
|
615
|
-
4. **Build tree**: recursively pair adjacent hashes. For each pair, `SHA-256(left || right)` → 32 bytes. If a level has an odd number of nodes, the last node is promoted to the next level unpaired.
|
|
616
|
-
5. **Root**: the final 32-byte hash, hex-encoded to a 64-character string
|
|
617
|
-
|
|
618
|
-
An empty set of content IDs produces a `null` root. A single content ID produces a root equal to `hex(SHA-256(UTF-8(contentId)))`.
|
|
619
|
-
|
|
620
|
-
### Worked Example: Merkle Tree
|
|
621
|
-
|
|
622
|
-
5 content IDs: `["alpha", "bravo", "charlie", "delta", "echo"]`
|
|
623
|
-
|
|
624
|
-
Already sorted lexicographically. Hash each leaf:
|
|
625
|
-
|
|
626
|
-
```
|
|
627
|
-
alpha → SHA-256("alpha") → 8ed3f6ad685b959ead7022518e1af76cd816f8e8ec7ccdda1ed4018e8f2223f8
|
|
628
|
-
bravo → SHA-256("bravo") → 4f4a9410ffcdf895c4adb880659e9b5c0dd1f23a30790684340b3eaacb045398
|
|
629
|
-
charlie → SHA-256("charlie") → 36ef585cd42d49706cd2827a77d86c91bfdaf87a3f22b8f0e0308bd2c16cf85f
|
|
630
|
-
delta → SHA-256("delta") → 18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4
|
|
631
|
-
echo → SHA-256("echo") → 092c79e8f80e559e404bcf660c48f3522b67aba9ff1484b0367e1a4ddef7431d
|
|
632
|
-
```
|
|
633
|
-
|
|
634
|
-
Build tree bottom-up, pairing left-to-right. Odd nodes promote unpaired:
|
|
635
|
-
|
|
636
|
-
```
|
|
637
|
-
Level 0 (leaves): [alpha] [bravo] [charlie] [delta] [echo]
|
|
638
|
-
Level 1: [alpha‖bravo] [charlie‖delta] [echo] ← promoted
|
|
639
|
-
Level 2: [L1-left‖L1-mid] [echo] ← promoted
|
|
640
|
-
Level 3 (root): [L2-left‖echo]
|
|
641
|
-
```
|
|
642
|
-
|
|
643
|
-
Interior hashes:
|
|
644
|
-
|
|
645
|
-
```
|
|
646
|
-
SHA-256(alpha‖bravo) → 90d39555bb3c223e12f5a375c3011d2462fe2e1e36b8416a0b623d5831a9b4f3
|
|
647
|
-
SHA-256(charlie‖delta) → 6b55e77bef32937d9ccce2bd4b18127b0483f0be8e5b63c30bcc2b0d09f7dd44
|
|
648
|
-
SHA-256(alpha‖bravo ‖ charlie‖delta) → 23c83cb862e3b6a86eb2dfa0ea8ba0edcf1c3f3b8f14abc5eb9d72eab2edc2f7
|
|
649
|
-
```
|
|
650
|
-
|
|
651
|
-
**Root** (level 3):
|
|
652
|
-
|
|
653
|
-
```
|
|
654
|
-
SHA-256(23c83c...edc2f7 ‖ 092c79...f7431d) → 7e80d4780f454e0fca0b090d8c646f572b49354f54154531606105aad2fda28e
|
|
655
|
-
```
|
|
656
|
-
|
|
657
|
-
### Inclusion Proofs
|
|
658
|
-
|
|
659
|
-
A Merkle inclusion proof demonstrates that a specific content ID is part of the committed set without revealing the full set. The proof consists of sibling hashes along the path from leaf to root, plus a direction (left/right) for each step.
|
|
660
|
-
|
|
661
|
-
### Worked Example: Inclusion Proof for "charlie"
|
|
662
|
-
|
|
663
|
-
Starting from the leaf hash of "charlie" (`36ef58...`), walk to the root using sibling hashes:
|
|
664
|
-
|
|
665
|
-
```
|
|
666
|
-
Step 1: charlie (index 2) paired with delta (index 3)
|
|
667
|
-
sibling: 4f4a9410...045398 (delta leaf) position: right
|
|
668
|
-
→ SHA-256(charlie ‖ delta) → 6b55e77b...f7dd44
|
|
669
|
-
|
|
670
|
-
Step 2: charlie‖delta paired with alpha‖bravo
|
|
671
|
-
sibling: 90d39555...a9b4f3 (alpha‖bravo) position: left
|
|
672
|
-
→ SHA-256(alpha‖bravo ‖ charlie‖delta) → 23c83cb8...edc2f7
|
|
673
|
-
|
|
674
|
-
Step 3: L2-left paired with echo (promoted)
|
|
675
|
-
sibling: 092c79e8...f7431d (echo leaf) position: right
|
|
676
|
-
→ SHA-256(L2-left ‖ echo) → 7e80d478...fda28e ✓ matches root
|
|
677
|
-
```
|
|
678
|
-
|
|
679
|
-
Proof path (from [`examples/merkle-tree.json`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/examples/merkle-tree.json)):
|
|
680
|
-
|
|
681
|
-
```json
|
|
682
|
-
[
|
|
683
|
-
{
|
|
684
|
-
"hash": "4f4a9410ffcdf895c4adb880659e9b5c0dd1f23a30790684340b3eaacb045398",
|
|
685
|
-
"position": "right"
|
|
686
|
-
},
|
|
687
|
-
{
|
|
688
|
-
"hash": "90d39555bb3c223e12f5a375c3011d2462fe2e1e36b8416a0b623d5831a9b4f3",
|
|
689
|
-
"position": "left"
|
|
690
|
-
},
|
|
691
|
-
{
|
|
692
|
-
"hash": "092c79e8f80e559e404bcf660c48f3522b67aba9ff1484b0367e1a4ddef7431d",
|
|
693
|
-
"position": "right"
|
|
694
|
-
}
|
|
695
|
-
]
|
|
696
|
-
```
|
|
697
|
-
|
|
698
|
-
---
|
|
699
|
-
|
|
700
|
-
## Artifacts
|
|
701
|
-
|
|
702
|
-
Artifacts are standalone signed inline documents — immutable, CID-addressable proof plane primitives. Unlike chain operations which extend a sequence, an artifact is a single signed statement with no predecessor or successor.
|
|
703
|
-
|
|
704
|
-
### Payload
|
|
705
|
-
|
|
706
|
-
```json
|
|
707
|
-
{
|
|
708
|
-
"version": 1,
|
|
709
|
-
"type": "artifact",
|
|
710
|
-
"did": "did:dfos:...",
|
|
711
|
-
"content": {
|
|
712
|
-
"$schema": "https://schemas.dfos.com/profile/v1",
|
|
713
|
-
"name": "Example"
|
|
714
|
-
},
|
|
715
|
-
"createdAt": "2026-03-25T00:00:00.000Z"
|
|
716
|
-
}
|
|
717
|
-
```
|
|
718
|
-
|
|
719
|
-
The `content` object MUST include a `$schema` string that identifies the artifact's schema. The schema acts as a discriminator — consumers use it to determine how to interpret the artifact's content. Schema names are free-form strings (no protocol-level registry).
|
|
720
|
-
|
|
721
|
-
### Constraints
|
|
722
|
-
|
|
723
|
-
- **JWS `typ` header**: `did:dfos:artifact`
|
|
724
|
-
- **Max payload size**: 16384 bytes CBOR-encoded. Protocol constant — not configurable
|
|
725
|
-
- **Immutability**: Once published, an artifact is never updated or replaced
|
|
726
|
-
- **CID-addressable**: Each artifact is addressed by the CID of its CBOR-encoded payload
|
|
727
|
-
|
|
728
|
-
### Verification
|
|
729
|
-
|
|
730
|
-
1. JWS signature verification against the signing DID's current key state
|
|
731
|
-
2. CID integrity — `header.cid` matches the CID computed from dag-cbor canonical encoding the raw payload
|
|
732
|
-
3. Payload schema validation — `version`, `type: "artifact"`, `did`, `content` with `$schema`, `createdAt`
|
|
733
|
-
4. Size limit — CBOR-encoded payload does not exceed 16384 bytes
|
|
734
|
-
|
|
735
|
-
---
|
|
736
|
-
|
|
737
|
-
## Countersignatures
|
|
738
|
-
|
|
739
|
-
A countersignature is a standalone witness attestation — a signed statement that references a target operation by CID. Each countersignature has its own `typ` header (`did:dfos:countersign`), its own payload, and its own CID distinct from the target.
|
|
740
|
-
|
|
741
|
-
### Payload
|
|
742
|
-
|
|
743
|
-
```json
|
|
744
|
-
{
|
|
745
|
-
"version": 1,
|
|
746
|
-
"type": "countersign",
|
|
747
|
-
"did": "did:dfos:witness...",
|
|
748
|
-
"targetCID": "bafy...",
|
|
749
|
-
"createdAt": "2026-03-25T00:00:00.000Z"
|
|
750
|
-
}
|
|
751
|
-
```
|
|
752
|
-
|
|
753
|
-
The `did` field is the witness identity — the DID signing the attestation. The `targetCID` references the operation being attested to.
|
|
754
|
-
|
|
755
|
-
### Properties
|
|
756
|
-
|
|
757
|
-
- **JWS `typ` header**: `did:dfos:countersign`
|
|
758
|
-
- **Own CID**: Each countersignature has its own CID derived from its own payload, distinct from the target. This avoids the ambiguity of multiple JWS tokens sharing the same CID
|
|
759
|
-
- **Stateless verification**: Signature + CID integrity + payload schema. No chain state required to verify the cryptographic validity of a countersignature
|
|
760
|
-
- **Composable**: The `targetCID` can reference any CID-addressable operation — content ops, beacons, artifacts, identity ops, even other countersignatures
|
|
761
|
-
- **Immutable**: Once published, a countersignature is permanent
|
|
762
|
-
|
|
763
|
-
### Verification
|
|
764
|
-
|
|
765
|
-
1. Decode JWS, verify `typ` is `did:dfos:countersign`
|
|
766
|
-
2. Parse and validate countersign payload (`version`, `type: "countersign"`, `did`, `targetCID`, `createdAt`)
|
|
767
|
-
3. Verify the `kid` DID matches the payload `did` (the witness must sign with their own key)
|
|
768
|
-
4. CID integrity — `header.cid` matches the CID computed from dag-cbor canonical encoding the raw payload
|
|
769
|
-
5. Verify EdDSA JWS signature against the witness's public key
|
|
770
|
-
|
|
771
|
-
Relay-level semantic checks (target exists, witness ≠ author, deduplication) are enforcement concerns, not protocol verification.
|
|
772
|
-
|
|
773
|
-
---
|
|
774
|
-
|
|
775
|
-
## Verification
|
|
776
|
-
|
|
777
|
-
### Identity Chain
|
|
778
|
-
|
|
779
|
-
1. Decode each JWS, parse payload as IdentityOperation
|
|
780
|
-
2. First op MUST be `type: "create"` — this is the genesis bootstrap:
|
|
781
|
-
- 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.
|
|
782
|
-
- 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.
|
|
783
|
-
- Derive the operation CID via dag-cbor canonical encoding. Verify `header.cid` matches the derived CID. Derive the DID from the CID.
|
|
784
|
-
3. For each subsequent op: verify `previousOperationCID` matches previous op's derived CID. Verify `createdAt` is strictly increasing (SHOULD — see Protocol Rules).
|
|
785
|
-
4. Verify the chain is not in a terminal state (deleted) before applying any operation.
|
|
786
|
-
5. Resolve `kid` — genesis uses bare key ID, non-genesis uses DID URL (extract DID, verify it matches the derived DID; extract key ID).
|
|
787
|
-
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.
|
|
788
|
-
7. Verify EdDSA JWS signature over the signing input bytes.
|
|
789
|
-
8. Apply state change: `create` initializes key state, `update` replaces key state (must have at least one controller key), `delete` marks terminal.
|
|
790
|
-
|
|
791
|
-
### Content Chain
|
|
792
|
-
|
|
793
|
-
1. Decode each JWS, parse payload as ContentOperation
|
|
794
|
-
2. First op must be `type: "create"` — the signer is the chain creator
|
|
795
|
-
3. For each subsequent op: verify `previousOperationCID` matches, verify `createdAt` increasing
|
|
796
|
-
4. Derive the operation CID via dag-cbor canonical encoding. Verify `header.cid` matches the derived CID.
|
|
797
|
-
5. Verify the `kid` DID matches the payload `did` field
|
|
798
|
-
6. Resolve `kid` via external key resolver (caller provides)
|
|
799
|
-
7. Verify EdDSA JWS signature
|
|
800
|
-
8. If `enforceAuthorization` is enabled and the signer DID differs from the chain creator: verify the `authorization` field contains a valid `DFOSContentWrite` VC-JWT issued by the creator DID, with `sub` matching the signer, not expired at `op.createdAt`, and `contentId` (if present) matching this chain
|
|
801
|
-
9. Apply state change (set document, clear, or delete)
|
|
802
|
-
|
|
803
|
-
---
|
|
804
|
-
|
|
805
|
-
## Deterministic Reference Artifacts
|
|
806
|
-
|
|
807
|
-
All artifacts below are deterministic and reproducible from fixed seeds. An independent implementer can verify every value using standard Ed25519 + dag-cbor libraries. Private keys are derived from `SHA-256(UTF8("dfos-protocol-reference-key-N"))`.
|
|
808
|
-
|
|
809
|
-
### Key 1 (Genesis Controller)
|
|
810
|
-
|
|
811
|
-
```
|
|
812
|
-
Seed: SHA-256("dfos-protocol-reference-key-1")
|
|
813
|
-
Private key: 132d4bebdb6e62359afb930fe15d756a92ad96e6b0d47619988f5a1a55272aac
|
|
814
|
-
Public key: ba421e272fad4f941c221e47f87d9253bdc04f7d4ad2625ae667ab9f0688ce32
|
|
815
|
-
Multikey: z6MkrzLMNwoJSV4P3YccWcbtk8vd9LtgMKnLeaDLUqLuASjb
|
|
816
|
-
Key ID: key_r9ev34fvc23z999veaaft8
|
|
817
|
-
```
|
|
818
|
-
|
|
819
|
-
### Key 2 (Rotated Controller)
|
|
820
|
-
|
|
821
|
-
```
|
|
822
|
-
Seed: SHA-256("dfos-protocol-reference-key-2")
|
|
823
|
-
Private key: 384f5626906db84f6a773ec46475ff2d4458e92dd4dd13fe03dbb7510f4ca2a8
|
|
824
|
-
Public key: 0f350f994f94d675f04a325bd316ebedd740ca206eaaf609bdb641b5faa0f78c
|
|
825
|
-
Multikey: z6MkfUd65JrAhfdgFuMCccU9ThQvjB2fJAMUHkuuajF992gK
|
|
826
|
-
Key ID: key_ez9a874tckr3dv933d3ckd
|
|
827
|
-
```
|
|
828
|
-
|
|
829
|
-
### Identity Chain: Create (Genesis)
|
|
830
|
-
|
|
831
|
-
Operation:
|
|
832
|
-
|
|
833
|
-
```json
|
|
834
|
-
{
|
|
835
|
-
"version": 1,
|
|
836
|
-
"type": "create",
|
|
837
|
-
"authKeys": [
|
|
838
|
-
{
|
|
839
|
-
"id": "key_r9ev34fvc23z999veaaft8",
|
|
840
|
-
"type": "Multikey",
|
|
841
|
-
"publicKeyMultibase": "z6MkrzLMNwoJSV4P3YccWcbtk8vd9LtgMKnLeaDLUqLuASjb"
|
|
842
|
-
}
|
|
843
|
-
],
|
|
844
|
-
"assertKeys": [
|
|
845
|
-
{
|
|
846
|
-
"id": "key_r9ev34fvc23z999veaaft8",
|
|
847
|
-
"type": "Multikey",
|
|
848
|
-
"publicKeyMultibase": "z6MkrzLMNwoJSV4P3YccWcbtk8vd9LtgMKnLeaDLUqLuASjb"
|
|
849
|
-
}
|
|
850
|
-
],
|
|
851
|
-
"controllerKeys": [
|
|
852
|
-
{
|
|
853
|
-
"id": "key_r9ev34fvc23z999veaaft8",
|
|
854
|
-
"type": "Multikey",
|
|
855
|
-
"publicKeyMultibase": "z6MkrzLMNwoJSV4P3YccWcbtk8vd9LtgMKnLeaDLUqLuASjb"
|
|
856
|
-
}
|
|
857
|
-
],
|
|
858
|
-
"createdAt": "2026-03-07T00:00:00.000Z"
|
|
859
|
-
}
|
|
860
|
-
```
|
|
861
|
-
|
|
862
|
-
JWS Header:
|
|
863
|
-
|
|
864
|
-
```json
|
|
865
|
-
{
|
|
866
|
-
"alg": "EdDSA",
|
|
867
|
-
"typ": "did:dfos:identity-op",
|
|
868
|
-
"kid": "key_r9ev34fvc23z999veaaft8",
|
|
869
|
-
"cid": "bafyreibanjpgcqffcfhr4sptzjfthh5szohhbo5tjfulemkw7uhden5uqy"
|
|
870
|
-
}
|
|
871
|
-
```
|
|
872
|
-
|
|
873
|
-
JWS Signature (hex):
|
|
874
|
-
|
|
875
|
-
```
|
|
876
|
-
103af20cad6ebed8b1fb5edc1ee9fdb7a31a705231dab326305d502f37c3e531654ac3af31cb9ef7ba428069f709778b545b55c60a42a21d241925e2a0a2b303
|
|
877
|
-
```
|
|
878
|
-
|
|
879
|
-
JWS Token:
|
|
880
|
-
|
|
881
|
-
```
|
|
882
|
-
eyJhbGciOiJFZERTQSIsInR5cCI6ImRpZDpkZm9zOmlkZW50aXR5LW9wIiwia2lkIjoia2V5X3I5ZXYzNGZ2YzIzejk5OXZlYWFmdDgiLCJjaWQiOiJiYWZ5cmVpYmFuanBnY3FmZmNmaHI0c3B0empmdGhoNXN6b2hoYm81dGpmdWxlbWt3N3VoZGVuNXVxeSJ9.eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiY3JlYXRlIiwiYXV0aEtleXMiOlt7ImlkIjoia2V5X3I5ZXYzNGZ2YzIzejk5OXZlYWFmdDgiLCJ0eXBlIjoiTXVsdGlrZXkiLCJwdWJsaWNLZXlNdWx0aWJhc2UiOiJ6Nk1rcnpMTU53b0pTVjRQM1ljY1djYnRrOHZkOUx0Z01LbkxlYURMVXFMdUFTamIifV0sImFzc2VydEtleXMiOlt7ImlkIjoia2V5X3I5ZXYzNGZ2YzIzejk5OXZlYWFmdDgiLCJ0eXBlIjoiTXVsdGlrZXkiLCJwdWJsaWNLZXlNdWx0aWJhc2UiOiJ6Nk1rcnpMTU53b0pTVjRQM1ljY1djYnRrOHZkOUx0Z01LbkxlYURMVXFMdUFTamIifV0sImNvbnRyb2xsZXJLZXlzIjpbeyJpZCI6ImtleV9yOWV2MzRmdmMyM3o5OTF2ZWFhZnQ4IiwidHlwZSI6Ik11bHRpa2V5IiwicHVibGljS2V5TXVsdGliYXNlIjoiejZNa3J6TE1Od29KU1Y0UDNZY2NXY2J0azh2ZDlMdGdNS25MZWFETFVxTHVBU2piIn1dLCJjcmVhdGVkQXQiOiIyMDI2LTAzLTA3VDAwOjAwOjAwLjAwMFoifQ.EDryDK1uvtix-17cHun9t6MacFIx2rMmMF1QLzfD5TFlSsOvMcue97pCgGn3CXeLVFtVxgpCoh0kGSXioKKzAw
|
|
883
|
-
```
|
|
884
|
-
|
|
885
|
-
Operation CID:
|
|
886
|
-
|
|
887
|
-
```
|
|
888
|
-
bafyreibanjpgcqffcfhr4sptzjfthh5szohhbo5tjfulemkw7uhden5uqy
|
|
889
|
-
```
|
|
890
|
-
|
|
891
|
-
**Derived DID: `did:dfos:e3vvtck42d4eacdnzvtrn6`**
|
|
892
|
-
|
|
893
|
-
### Identity Chain: Update (Key Rotation)
|
|
894
|
-
|
|
895
|
-
JWS Header:
|
|
896
|
-
|
|
897
|
-
```json
|
|
898
|
-
{
|
|
899
|
-
"alg": "EdDSA",
|
|
900
|
-
"typ": "did:dfos:identity-op",
|
|
901
|
-
"kid": "did:dfos:e3vvtck42d4eacdnzvtrn6#key_r9ev34fvc23z999veaaft8",
|
|
902
|
-
"cid": "bafyreicym4cyiednld73smbx32szaei7xdulqn4g3ste5e2w2ulajr3oqm"
|
|
903
|
-
}
|
|
904
|
-
```
|
|
905
|
-
|
|
906
|
-
Operation:
|
|
907
|
-
|
|
908
|
-
```json
|
|
909
|
-
{
|
|
910
|
-
"version": 1,
|
|
911
|
-
"type": "update",
|
|
912
|
-
"previousOperationCID": "bafyreibanjpgcqffcfhr4sptzjfthh5szohhbo5tjfulemkw7uhden5uqy",
|
|
913
|
-
"authKeys": [
|
|
914
|
-
{
|
|
915
|
-
"id": "key_ez9a874tckr3dv933d3ckd",
|
|
916
|
-
"type": "Multikey",
|
|
917
|
-
"publicKeyMultibase": "z6MkfUd65JrAhfdgFuMCccU9ThQvjB2fJAMUHkuuajF992gK"
|
|
918
|
-
}
|
|
919
|
-
],
|
|
920
|
-
"assertKeys": [
|
|
921
|
-
{
|
|
922
|
-
"id": "key_ez9a874tckr3dv933d3ckd",
|
|
923
|
-
"type": "Multikey",
|
|
924
|
-
"publicKeyMultibase": "z6MkfUd65JrAhfdgFuMCccU9ThQvjB2fJAMUHkuuajF992gK"
|
|
925
|
-
}
|
|
926
|
-
],
|
|
927
|
-
"controllerKeys": [
|
|
928
|
-
{
|
|
929
|
-
"id": "key_ez9a874tckr3dv933d3ckd",
|
|
930
|
-
"type": "Multikey",
|
|
931
|
-
"publicKeyMultibase": "z6MkfUd65JrAhfdgFuMCccU9ThQvjB2fJAMUHkuuajF992gK"
|
|
932
|
-
}
|
|
933
|
-
],
|
|
934
|
-
"createdAt": "2026-03-07T00:01:00.000Z"
|
|
935
|
-
}
|
|
936
|
-
```
|
|
937
|
-
|
|
938
|
-
JWS Signature (hex):
|
|
939
|
-
|
|
940
|
-
```
|
|
941
|
-
31272ea0196038ade3e505fdb45730d68bb4a382e0273886244b19e69bea881af549a800c80bf987ec1a8d086d83c20fedd2e533453895e5b6891adaf78e5c0e
|
|
942
|
-
```
|
|
943
|
-
|
|
944
|
-
JWS Token:
|
|
945
|
-
|
|
946
|
-
```
|
|
947
|
-
eyJhbGciOiJFZERTQSIsInR5cCI6ImRpZDpkZm9zOmlkZW50aXR5LW9wIiwia2lkIjoiZGlkOmRmb3M6ZTN2dnRjazQyZDRlYWNkbnp2dHJuNiNrZXlfcjlldjM0ZnZjMjN6OTk5dmVhYWZ0OCIsImNpZCI6ImJhZnlyZWljeW00Y3lpZWRubGQ3M3NtYngzMnN6YWVpN3hkdWxxbjRnM3N0ZTVlMncydWxhanIzb3FtIn0.eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoidXBkYXRlIiwicHJldmlvdXNPcGVyYXRpb25DSUQiOiJiYWZ5cmVpYmFuanBnY3FmZmNmaHI0c3B0empmdGhoNXN6b2hoYm81dGpmdWxlbWt3N3VoZGVuNXVxeSIsImF1dGhLZXlzIjpbeyJpZCI6ImtleV9lejlhODc0dGNrcjNkdjkzM2QzY2tkIiwidHlwZSI6Ik11bHRpa2V5IiwicHVibGljS2V5TXVsdGliYXNlIjoiejZNa2ZVZDY1SnJBaGZkZ0Z1TUNjY1U5VGhRdmpCMmZKQU1VSGt1dWFqRjk5MmdLIn1dLCJhc3NlcnRLZXlzIjpbeyJpZCI6ImtleV9lejlhODc0dGNrcjNkdjkzM2QzY2tkIiwidHlwZSI6Ik11bHRpa2V5IiwicHVibGljS2V5TXVsdGliYXNlIjoiejZNa2ZVZDY1SnJBaGZkZ0Z1TUNjY1U5VGhRdmpCMmZKQU1VSGt1dWFqRjk5MmdLIn1dLCJjb250cm9sbGVyS2V5cyI6W3siaWQiOiJrZXlfZXo5YTg3NHRja3IzZHY5MzNkM2NrZCIsInR5cGUiOiJNdWx0aWtleSIsInB1YmxpY0tleU11bHRpYmFzZSI6Ino2TWtmVWQ2NUpyQWhmZGdGdU1DY2NVOVRoUXZqQjJmSkFNVUhrdXVhakY5OTJnSyJ9XSwiY3JlYXRlZEF0IjoiMjAyNi0wMy0wN1QwMDowMTowMC4wMDBaIn0.MScuoBlgOK3j5QX9tFcw1ou0o4LgJziGJEsZ5pvqiBr1SagAyAv5h-wajQhtg8IP7dLlM0U4leW2iRra945cDg
|
|
948
|
-
```
|
|
949
|
-
|
|
950
|
-
Operation CID:
|
|
951
|
-
|
|
952
|
-
```
|
|
953
|
-
bafyreicym4cyiednld73smbx32szaei7xdulqn4g3ste5e2w2ulajr3oqm
|
|
954
|
-
```
|
|
955
|
-
|
|
956
|
-
Post-rotation: DID unchanged (`did:dfos:e3vvtck42d4eacdnzvtrn6`), controller rotated to `key_ez9a874tckr3dv933d3ckd`.
|
|
957
|
-
|
|
958
|
-
### Content Chain: Document + Create
|
|
959
|
-
|
|
960
|
-
Document (flat content object):
|
|
961
|
-
|
|
962
|
-
```json
|
|
963
|
-
{
|
|
964
|
-
"$schema": "https://schemas.dfos.com/post/v1",
|
|
965
|
-
"format": "short-post",
|
|
966
|
-
"title": "Hello World",
|
|
967
|
-
"body": "First post on the protocol.",
|
|
968
|
-
"createdByDID": "did:dfos:e3vvtck42d4eacdnzvtrn6"
|
|
969
|
-
}
|
|
970
|
-
```
|
|
971
|
-
|
|
972
|
-
Document CID:
|
|
973
|
-
|
|
974
|
-
```
|
|
975
|
-
bafyreihzwuoupfg3dxip6xmgzmxsywyii2jeoxxzbgx3zxm2in7knoi3g4
|
|
976
|
-
```
|
|
977
|
-
|
|
978
|
-
Content Create JWS Header:
|
|
979
|
-
|
|
980
|
-
```json
|
|
981
|
-
{
|
|
982
|
-
"alg": "EdDSA",
|
|
983
|
-
"typ": "did:dfos:content-op",
|
|
984
|
-
"kid": "did:dfos:e3vvtck42d4eacdnzvtrn6#key_ez9a874tckr3dv933d3ckd",
|
|
985
|
-
"cid": "bafyreiaedhjq64aajpwociahl5w37j6uoxr5mojoq5dnah6fpvxr5d4lxu"
|
|
986
|
-
}
|
|
987
|
-
```
|
|
988
|
-
|
|
989
|
-
Content Create Payload:
|
|
990
|
-
|
|
991
|
-
```json
|
|
992
|
-
{
|
|
993
|
-
"version": 1,
|
|
994
|
-
"type": "create",
|
|
995
|
-
"did": "did:dfos:e3vvtck42d4eacdnzvtrn6",
|
|
996
|
-
"documentCID": "bafyreihzwuoupfg3dxip6xmgzmxsywyii2jeoxxzbgx3zxm2in7knoi3g4",
|
|
997
|
-
"baseDocumentCID": null,
|
|
998
|
-
"createdAt": "2026-03-07T00:02:00.000Z",
|
|
999
|
-
"note": null
|
|
1000
|
-
}
|
|
1001
|
-
```
|
|
1002
|
-
|
|
1003
|
-
Content Create JWS Signature (hex):
|
|
1004
|
-
|
|
1005
|
-
```
|
|
1006
|
-
46feaf973e4c7ebc2a0d4ad25481ace197de05b91051205c5e1c7067a85fb9d4abe4cc61625d3c853a8b0ce0345b534c8cdd07b34216f635d3c0bc0fd5d30306
|
|
1007
|
-
```
|
|
1008
|
-
|
|
1009
|
-
Content Create JWS Token:
|
|
1010
|
-
|
|
1011
|
-
```
|
|
1012
|
-
eyJhbGciOiJFZERTQSIsInR5cCI6ImRpZDpkZm9zOmNvbnRlbnQtb3AiLCJraWQiOiJkaWQ6ZGZvczplM3Z2dGNrNDJkNGVhY2RuenZ0cm42I2tleV9lejlhODc0dGNrcjNkdjkzM2QzY2tkIiwiY2lkIjoiYmFmeXJlaWFlZGhqcTY0YWFqcHdvY2lhaGw1dzM3ajZ1b3hyNW1vam9xNWRuYWg2ZnB2eHI1ZDRseHUifQ.eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiY3JlYXRlIiwiZGlkIjoiZGlkOmRmb3M6ZTN2dnRjazQyZDRlYWNkbnp2dHJuNiIsImRvY3VtZW50Q0lEIjoiYmFmeXJlaWh6d3VvdXBmZzNkeGlwNnhtZ3pteHN5d3lpaTJqZW94eHpiZ3gzenhtMmluN2tub2kzZzQiLCJiYXNlRG9jdW1lbnRDSUQiOm51bGwsImNyZWF0ZWRBdCI6IjIwMjYtMDMtMDdUMDA6MDI6MDAuMDAwWiIsIm5vdGUiOm51bGx9.Rv6vlz5MfrwqDUrSVIGs4ZfeBbkQUSBcXhxwZ6hfudSr5MxhYl08hTqLDOA0W1NMjN0Hs0IW9jXTwLwP1dMDBg
|
|
1013
|
-
```
|
|
1014
|
-
|
|
1015
|
-
Content Operation CID:
|
|
1016
|
-
|
|
1017
|
-
```
|
|
1018
|
-
bafyreiaedhjq64aajpwociahl5w37j6uoxr5mojoq5dnah6fpvxr5d4lxu
|
|
1019
|
-
```
|
|
1020
|
-
|
|
1021
|
-
### Content Chain: Update
|
|
1022
|
-
|
|
1023
|
-
Content Update Payload:
|
|
1024
|
-
|
|
1025
|
-
```json
|
|
1026
|
-
{
|
|
1027
|
-
"version": 1,
|
|
1028
|
-
"type": "update",
|
|
1029
|
-
"did": "did:dfos:e3vvtck42d4eacdnzvtrn6",
|
|
1030
|
-
"previousOperationCID": "bafyreiaedhjq64aajpwociahl5w37j6uoxr5mojoq5dnah6fpvxr5d4lxu",
|
|
1031
|
-
"documentCID": "bafyreidh7e36cvwy3uw5ypitcqk7uoktbkkkj7e6hxhky4o75rxn7kxilu",
|
|
1032
|
-
"baseDocumentCID": "bafyreihzwuoupfg3dxip6xmgzmxsywyii2jeoxxzbgx3zxm2in7knoi3g4",
|
|
1033
|
-
"createdAt": "2026-03-07T00:03:00.000Z",
|
|
1034
|
-
"note": "edited title and body"
|
|
1035
|
-
}
|
|
1036
|
-
```
|
|
1037
|
-
|
|
1038
|
-
Updated document (flat content object):
|
|
1039
|
-
|
|
1040
|
-
```json
|
|
1041
|
-
{
|
|
1042
|
-
"$schema": "https://schemas.dfos.com/post/v1",
|
|
1043
|
-
"format": "short-post",
|
|
1044
|
-
"title": "Hello World (edited)",
|
|
1045
|
-
"body": "Updated content.",
|
|
1046
|
-
"createdByDID": "did:dfos:e3vvtck42d4eacdnzvtrn6"
|
|
1047
|
-
}
|
|
1048
|
-
```
|
|
1049
|
-
|
|
1050
|
-
Document CID (edited):
|
|
1051
|
-
|
|
1052
|
-
```
|
|
1053
|
-
bafyreidh7e36cvwy3uw5ypitcqk7uoktbkkkj7e6hxhky4o75rxn7kxilu
|
|
1054
|
-
```
|
|
1055
|
-
|
|
1056
|
-
Content Update CID:
|
|
1057
|
-
|
|
1058
|
-
```
|
|
1059
|
-
bafyreih6e5cbjitpozhzhgmfktmiohmxyn3ucwhqd3mjixizvwmlhv7hm4
|
|
1060
|
-
```
|
|
1061
|
-
|
|
1062
|
-
### Content Chain Verified State
|
|
1063
|
-
|
|
1064
|
-
```
|
|
1065
|
-
Content ID: a82z92a3hndk6c97thcrn8
|
|
1066
|
-
Genesis CID: bafyreiaedhjq64aajpwociahl5w37j6uoxr5mojoq5dnah6fpvxr5d4lxu
|
|
1067
|
-
Head CID: bafyreih6e5cbjitpozhzhgmfktmiohmxyn3ucwhqd3mjixizvwmlhv7hm4
|
|
1068
|
-
```
|
|
1069
|
-
|
|
1070
|
-
---
|
|
1071
|
-
|
|
1072
|
-
## Verification Checklist (For Independent Implementers)
|
|
1073
|
-
|
|
1074
|
-
Given the artifacts above, verify:
|
|
1075
|
-
|
|
1076
|
-
1. **Multikey decode**: strip `z`, base58btc decode, strip `[0xed, 0x01]` prefix → raw public key:
|
|
1077
|
-
|
|
1078
|
-
```
|
|
1079
|
-
z6MkrzLMNwoJSV4P3YccWcbtk8vd9LtgMKnLeaDLUqLuASjb
|
|
1080
|
-
→ ba421e272fad4f941c221e47f87d9253bdc04f7d4ad2625ae667ab9f0688ce32
|
|
1081
|
-
```
|
|
1082
|
-
|
|
1083
|
-
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. The header contains `cid` alongside `alg`, `typ`, and `kid`.
|
|
1084
|
-
|
|
1085
|
-
3. **Genesis CID**: base64url-decode JWS payload → parse JSON → dag-cbor canonical encode → SHA-256 → CIDv1 → should be:
|
|
1086
|
-
|
|
1087
|
-
```
|
|
1088
|
-
bafyreibanjpgcqffcfhr4sptzjfthh5szohhbo5tjfulemkw7uhden5uqy
|
|
1089
|
-
```
|
|
1090
|
-
|
|
1091
|
-
4. **CID header**: Verify each operation JWS header contains `cid` matching the derived operation CID
|
|
1092
|
-
|
|
1093
|
-
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`
|
|
1094
|
-
|
|
1095
|
-
6. **Rotation JWS**: signed by OLD controller key (key 1). Verify with key 1's public key. kid:
|
|
1096
|
-
|
|
1097
|
-
```
|
|
1098
|
-
did:dfos:e3vvtck42d4eacdnzvtrn6#key_r9ev34fvc23z999veaaft8
|
|
1099
|
-
```
|
|
1100
|
-
|
|
1101
|
-
7. **Content create JWS**: signed by NEW controller key (key 2, post-rotation). Verify with key 2's public key. kid:
|
|
1102
|
-
|
|
1103
|
-
```
|
|
1104
|
-
did:dfos:e3vvtck42d4eacdnzvtrn6#key_ez9a874tckr3dv933d3ckd
|
|
1105
|
-
```
|
|
1106
|
-
|
|
1107
|
-
8. **Document CID**: dag-cbor canonical encode the flat content object → SHA-256 → CIDv1 → should be:
|
|
1108
|
-
|
|
1109
|
-
```
|
|
1110
|
-
bafyreihzwuoupfg3dxip6xmgzmxsywyii2jeoxxzbgx3zxm2in7knoi3g4
|
|
1111
|
-
```
|
|
1112
|
-
|
|
1113
|
-
9. **Content operation `did` field**: verify the `did` field in each content operation matches the `kid` DID in the JWS header
|
|
1114
|
-
|
|
1115
|
-
10. **Content chain integrity**: update's `previousOperationCID` matches create's operation CID
|
|
1116
|
-
|
|
1117
|
-
11. **Chain completeness**: all operation CIDs, DID derivation, key rotation, and content chain linkage verified end-to-end.
|
|
1118
|
-
|
|
1119
|
-
12. **VC-JWT credential verify**: using the issuer's public key, verify a `DFOSContentWrite` or `DFOSContentRead` credential: check EdDSA signature, `typ: "vc+jwt"`, expiration, `kid` DID URL format, `kid` DID matches `iss`, `vc` claim structure matches W3C VC Data Model v2, credential type matches expected DFOS type. Test vectors in [`examples/credential-write.json`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/examples/credential-write.json) and [`examples/credential-read.json`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/examples/credential-read.json).
|
|
1120
|
-
|
|
1121
|
-
13. **Delegated content chain verify**: using [`examples/content-delegated.json`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/examples/content-delegated.json), verify a content chain where the genesis is signed by the creator and a subsequent update is signed by a delegate with an embedded `DFOSContentWrite` VC-JWT in the `authorization` field. The VC must be issued by the creator DID, with `sub` matching the delegate DID.
|
|
1122
|
-
|
|
1123
|
-
14. **Number encoding determinism**: dag-cbor encode `{"version": 1, "type": "test"}` and verify:
|
|
1124
|
-
- CBOR hex is `a2647479706564746573746776657273696f6e01` (20 bytes)
|
|
1125
|
-
- CID is `bafyreihp6omsp6icc6ee63ox2ovsaxm6s7ikd2a7k5eh2qz2qd5soh5bsa`
|
|
1126
|
-
- Byte at offset 19 is `0x01` (CBOR integer 1), NOT `0xf9` (CBOR float header)
|
|
1127
|
-
- If your implementation decodes this payload from JSON (e.g., from a JWS token) and then re-encodes to dag-cbor, the CID MUST still match. This catches the JSON `float64` → CBOR float trap.
|
|
1128
|
-
|
|
1129
|
-
---
|
|
1130
|
-
|
|
1131
|
-
## Source and Verification
|
|
1132
|
-
|
|
1133
|
-
All source lives in [`packages/dfos-protocol/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol) — self-contained, zero monorepo dependencies. 266 checks across 5 languages.
|
|
1134
|
-
|
|
1135
|
-
- [`crypto/ed25519`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/crypto/ed25519.ts) — `createNewEd25519Keypair`, `importEd25519Keypair`, `signPayloadEd25519`, `isValidEd25519Signature`
|
|
1136
|
-
- [`crypto/jws`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/crypto/jws.ts) — `createJws`, `verifyJws`, `decodeJwsUnsafe`
|
|
1137
|
-
- [`crypto/jwt`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/crypto/jwt.ts) — `createJwt`, `verifyJwt`
|
|
1138
|
-
- [`crypto/base64url`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/crypto/base64url.ts) — `base64urlEncode`, `base64urlDecode`
|
|
1139
|
-
- [`crypto/multiformats`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/crypto/multiformats.ts) — `dagCborCanonicalEncode`, `dagCborCanonicalEqual`
|
|
1140
|
-
- [`crypto/id`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/crypto/id.ts) — `generateId`, `generateIdNoPrefix`, `isValidId`
|
|
1141
|
-
- [`chain/multikey`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/multikey.ts) — `encodeEd25519Multikey`, `decodeMultikey`
|
|
1142
|
-
- [`chain/schemas`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/schemas.ts) — `IdentityOperation`, `ContentOperation`, `ArtifactPayload`, `CountersignPayload`, `MultikeyPublicKey`, `VerifiedIdentity`
|
|
1143
|
-
- [`chain/identity-chain`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/identity-chain.ts) — `signIdentityOperation`, `verifyIdentityChain`, `verifyIdentityExtensionFromTrustedState`
|
|
1144
|
-
- [`chain/content-chain`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/content-chain.ts) — `signContentOperation`, `verifyContentChain`, `verifyContentExtensionFromTrustedState`
|
|
1145
|
-
- [`chain/derivation`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/derivation.ts) — `deriveChainIdentifier`, `deriveContentId`
|
|
1146
|
-
- [`chain/beacon`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/beacon.ts) — `signBeacon`, `verifyBeacon`
|
|
1147
|
-
- [`chain/artifact`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/artifact.ts) — `signArtifact`, `verifyArtifact`
|
|
1148
|
-
- [`chain/countersign`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/countersign.ts) — `signCountersignature`, `verifyCountersignature`
|
|
1149
|
-
- [`credentials/auth-token`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/credentials/auth-token.ts) — `createAuthToken`, `verifyAuthToken`
|
|
1150
|
-
- [`credentials/credential`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/credentials/credential.ts) — `createCredential`, `verifyCredential`, `decodeCredentialUnsafe`
|
|
1151
|
-
- [`credentials/schemas`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/credentials/schemas.ts) — `AuthTokenClaims`, `CredentialClaims`, `VCClaim`, `DFOSCredentialType`
|
|
1152
|
-
- [`merkle/tree`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/merkle/tree.ts) — `buildMerkleTree`, `hashLeaf`
|
|
1153
|
-
- [`merkle/proof`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/merkle/proof.ts) — `generateMerkleProof`, `verifyMerkleProof`
|
|
1154
|
-
|
|
1155
|
-
### Related Specifications
|
|
1156
|
-
|
|
1157
|
-
- [DID Method: `did:dfos`](https://protocol.dfos.com/did-method) — W3C DID method specification for identity chains
|
|
1158
|
-
- [Content Model](https://protocol.dfos.com/content-model) — Standard content schemas (post, profile) for document content objects
|
|
1159
|
-
- [Web Relay](https://protocol.dfos.com/web-relay) — HTTP relay specification for ingestion, state, and content plane
|
|
1160
|
-
|
|
1161
|
-
### Cross-Language Verification
|
|
1162
|
-
|
|
1163
|
-
| Language | Tests | Source |
|
|
1164
|
-
| ---------- | ----- | ---------------------------------------------------------------------------------------------------- |
|
|
1165
|
-
| TypeScript | 224 | [`tests/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/tests) |
|
|
1166
|
-
| Go | 18 | [`verify/go/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/verify/go) |
|
|
1167
|
-
| Rust | 18 | [`verify/rust/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/verify/rust) |
|
|
1168
|
-
| Python | 3 | [`verify/python/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/verify/python) |
|
|
1169
|
-
| Swift | 3 | [`verify/swift/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/verify/swift) |
|
|
1170
|
-
|
|
1171
|
-
---
|
|
1172
|
-
|
|
1173
|
-
## Special Thanks
|
|
1174
|
-
|
|
1175
|
-
- **Vinny Bellavia** — [stcisgood.com](https://stcisgood.com)
|
|
1176
|
-
- **Allison Clift-Jennings** — [Jura Labs](https://juralabs.com)
|