# DFOS Protocol — Full Content Dump > All protocol site content as plain text. Specifications, overview, and FAQ. > Source: https://protocol.dfos.com --- # Overview The DFOS Protocol specifies how [Ed25519 signed chains](https://protocol.dfos.com/spec) establish identity, commit content, and produce proofs that anyone can verify — with a public key and any standard signature library, offline, in any language. It is transport-agnostic: a proof obtained from an API, a USB drive, or a peer-to-peer exchange verifies the same way. ## Why This Exists Identity on the internet is platform-granted. Your account, your content history, your social graph — all exist at the discretion of the service you're using. If the platform changes its rules, locks your account, or shuts down, your identity goes with it. This is structural, not a policy failure. The architecture of platform identity means someone else always holds the keys. ## Chain Topology The protocol inverts this by deriving identity from cryptographic keys you control. An identity is a directed acyclic graph (DAG) of signed operations — key rotations, content commitments, recoveries, deletions — rooted at a genesis. Each operation links to its predecessor via content-addressed CID ([`did:dfos`](https://protocol.dfos.com/did-method) derives from the genesis hash, making it self-certifying). Forks are valid. Two operations referencing the same predecessor both get accepted. All implementations converge to the same head via a deterministic rule: highest `createdAt` timestamp among tips, with lexicographic CID as tiebreaker. Given the same set of operations, any relay computes the same head regardless of ingestion order. Convergence without consensus. Content chains use the same mechanics — signed commitments to content-addressed documents. The protocol sees document hashes, never documents. It doesn't know what a "post" or "profile" is. Application semantics live in a [separate content layer](https://protocol.dfos.com/content-model), free to evolve without protocol changes. ## Proof and Content The internet is a dark forest — the most meaningful creative and social activity happens in private groups, closed communities, invite-only spaces. The protocol is designed for this topology. Two surfaces: the proof surface is public — signed chains that anyone can verify. The content surface is private — documents live in member-governed spaces, visible only to participants. The protocol defines the proof surface. You can prove you authored something without revealing what it is. ## Relay Network [Web relays](https://protocol.dfos.com/web-relay) are verifying HTTP endpoints that store and serve chains. Every relay independently verifies every operation on ingestion — relays don't trust each other or any central authority. Three peering behaviors compose to form the network: - **Gossip** — push new operations to peers when ingested (fire-and-forget, only on first ingestion to prevent storms) - **Read-through** — fetch chains from peers on local cache miss - **Sync** — periodically pull operations from peers via cursor-based polling There are no relay roles, tiers, or hierarchy. Topology is emergent from per-peer configuration. A relay with only gossip enabled is a write-only edge node. One with only read-through is a read cache. Full peering creates a convergent mesh. ## Verification Verification is a pure function. Given a chain and a public key, any Ed25519 implementation returns valid or invalid. The chain carries everything needed — public keys, signatures, content-addressed hashes. There is no registry to query, no blockchain to sync. The reference implementation is in [TypeScript](https://www.npmjs.com/package/@metalabel/dfos-protocol). Cross-language verification exists in Go, Python, Rust, and Swift — all running against the same [deterministic test vectors](https://protocol.dfos.com/spec#deterministic-reference-artifacts) from the specification. ## Design Principles - **Self-certifying.** Identity derives from cryptographic operations. The DID is a deterministic hash of the genesis operation. - **DAG-native.** Chains are directed acyclic graphs. Forks are valid. Convergence is deterministic without consensus. - **Transport-agnostic.** No privileged registry, blockchain, or API. Chains verify from any source. - **Offline-first.** Verification requires no network. A chain exported today is verifiable by code that doesn't exist yet. - **Protocol-only.** Signed chains, CID derivation, [DID resolution](https://protocol.dfos.com/did-method), merkle trees, beacons. Application semantics are a [separate concern](https://protocol.dfos.com/content-model). - **Platform-independent.** Not coupled to the [DFOS platform](https://dfos.com). Any system implementing the same primitives produces interoperable proofs. ## Status The specification is under active development. It is open source under the MIT license. The [CLI](https://protocol.dfos.com/cli) ships pre-built binaries for Linux, macOS, and Windows — installable via Homebrew, Docker, or a single curl command. Discussion happens in the [clear.txt](https://clear.dfos.com) space on DFOS. Install the [CLI](https://protocol.dfos.com/cli) to create identities, sign content, and run relays locally. Read the [full specification](https://protocol.dfos.com/spec), explore the [FAQ](https://protocol.dfos.com/faq), or browse the [source on GitHub](https://github.com/metalabel/dfos). --- # DFOS Protocol Verifiable identity and content chains — Ed25519 signatures, content-addressed CIDs, W3C DIDs. Cross-language verification in TypeScript, Go, Python, Rust, and Swift. This spec is under active review. Discuss it in the [clear.txt](https://clear.dfos.com) space on DFOS. [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) --- ## Philosophy 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. 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. 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. --- ## Protocol Overview The DFOS protocol has six components: | Component | Concern | | --------------------- | ------------------------------------------------------------------------------- | | **Crypto core** | Identity chains + content chains — Ed25519 signatures, JWS tokens, CID links | | **Credentials** | Auth tokens (DID-signed JWT) and VC-JWT credentials for authorization | | **Beacons** | Signed merkle root announcements — periodic commitment over content sets | | **Artifacts** | Standalone signed inline documents — immutable, CID-addressable structured data | | **Countersignatures** | Standalone witness attestation — signed references to any CID-addressable op | | **Merkle trees** | SHA-256 binary trees over content IDs — inclusion proofs for beacon roots | 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. ### Crypto Core: Two Chain Types | | Identity Chain | Content Chain | | -------------- | -------------------------- | -------------------------------- | | Commits to | Key sets (embedded) | Documents (by CID reference) | | Identifier | `did:dfos:` | `` (bare) | | Operations | create, update, delete | create, update, delete | | JWS typ | `did:dfos:identity-op` | `did:dfos:content-op` | | Self-sovereign | Yes (signs own operations) | No (signed by external identity) | 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. ### Addressing Three addressing modes, self-describing by format: | Thing | Form | Example | | --------------------- | ------------------------ | --------------------------------- | | Operation or document | CID (dag-cbor + SHA-256) | `bafyrei...` (base32lower) | | Content chain | contentId (22-char hash) | `a82z92a3hndk6c97thcrn8` | | Identity chain | DID | `did:dfos:e3vvtck42d4eacdnzvtrn6` | 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. 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. Application code may add prefixes for routing (e.g., `post_xxxx`) — these are strippable semantic sugar, not part of the protocol identifier. --- ## Protocol Rules ### Commitment Scheme Both operations and documents are content-addressed via **CID** (`dagCborCanonicalEncode(payload)` → SHA-256 → CIDv1). Operations are additionally signed via **JWS**. | Representation | Encoding | Purpose | | -------------- | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | | CID | `dagCborCanonicalEncode(payload)` → SHA-256 → CIDv1 | Deterministic content addressing for operations and documents | | JWS | `base64url(JSON.stringify(header))` + `.` + `base64url(JSON.stringify(payload))` → EdDSA signature covers both | Signature verification for operations | 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. ### Chain Validity A valid chain is a **directed acyclic graph (DAG)** of operations rooted at a genesis. Each operation (after genesis) links to a predecessor via `previousOperationCID`. The chain provides structural ordering independent of timestamps. **Forks are valid.** Two operations referencing the same `previousOperationCID` constitute a fork — both branches are accepted. The chain log stores all branches. A **deterministic head selection** rule ensures convergence across implementations given the same set of operations: 1. Find all **tips** — operations with no children 2. Select the tip with the **highest `createdAt`** timestamp 3. **Lexicographic highest CID** as tiebreaker This is deterministic: any implementation with the same operations computes the same head, regardless of ingestion order. Semantic interpretation of forks (concurrency glitch, intentional recovery, etc.) is application-defined — the protocol stores the DAG, clients interpret it. **Timestamp ordering**: `createdAt` MUST be strictly greater than the `createdAt` of the parent operation (the operation referenced by `previousOperationCID`). This is enforced per-branch, not globally — a fork branch's timestamps are validated against its own parent, not the other branch's operations. **Future timestamp bound**: Implementations MUST reject identity and content operations with a `createdAt` more than 24 hours in the future relative to the verifier's clock. Since deterministic head selection favors the highest `createdAt`, a far-future timestamp would permanently dominate head selection — this guard prevents temporal denial-of-service. ### Identity Chain Signer Validity 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. 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. ### Content Chain Signer Model 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:#`). 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. **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. **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. **What the protocol enforces:** - The EdDSA signature on each operation is valid against the key returned by `resolveKey(kid)` - Chain integrity (CID links, timestamp ordering, terminal state) - The `kid` DID matches the payload `did` for chain operations - Creator-sovereignty authorization (when `enforceAuthorization` is enabled): non-creator signers must present a valid DFOSContentWrite VC-JWT issued by the creator **What the protocol does NOT enforce (application concerns):** - Which key role (auth, assert, controller) the signing key must have - Ownership or attribution semantics beyond creator sovereignty ### Terminal States and Special Operations **`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. **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. **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. ### `typ` Header The JWS `typ` header uses protocol-specific values (not IANA media types): | `typ` value | Usage | | ---------------------- | --------------------------------------------- | | `did:dfos:identity-op` | Identity chain operations | | `did:dfos:content-op` | Content chain operations | | `did:dfos:beacon` | Beacon announcements | | `did:dfos:artifact` | Standalone signed inline documents | | `did:dfos:countersign` | Standalone witness attestations | | `JWT` | Auth tokens (DID-signed relay authentication) | | `vc+jwt` | VC-JWT credentials (W3C VC Data Model v2) | 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. ### Operation Field Limits 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. | Field | Max | Rationale | | -------------------------------------------- | --------- | -------------------------------------- | | `did` | 256 chars | ~8× typical `did:dfos:` (~31 chars) | | `key.id` | 64 chars | ~3× typical key ID (`key_` + 22 chars) | | `key.publicKeyMultibase` | 128 chars | ~2× Ed25519 multikey (~50 chars) | | `authKeys` / `assertKeys` / `controllerKeys` | 16 items | Generous for key rotation | | `previousOperationCID` | 256 chars | ~4× typical CIDv1 (~60 chars) | | `documentCID` | 256 chars | Same as above | | `note` | 256 chars | Short annotation, not prose | These limits are enforced by the Zod schemas in `src/chain/schemas.ts`. Any implementation parsing operations MUST reject values exceeding these bounds. The protocol does NOT limit: - **Document content size** — the protocol commits to a CID, not the document. Document size limits are application/registry concerns. - **Chain length** — no maximum operations per chain. - **Number of chains per identity** — application scaling concern. --- ## Standards and Dependencies | Component | Standard / Library | | ------------------- | -------------------------------------------------------------------------- | | Key generation | Ed25519 (RFC 8032) via `@noble/curves/ed25519` | | Signature algorithm | EdDSA over Ed25519 (pure, no prehash — Ed25519 handles SHA-512 internally) | | Key encoding | W3C Multikey (multicodec `0xed01` + base58btc multibase) | | Signed envelopes | JWS Compact Serialization (RFC 7515) with `alg: "EdDSA"` | | Content addressing | CIDv1 with dag-cbor codec (`0x71`) + SHA-256 multihash (`0x12`) | | ID encoding | SHA-256 → custom 19-char alphabet, 22 characters | ### ID Alphabet ``` Alphabet: 2346789acdefhknrtvz (19 characters) Length: 22 characters Entropy: ~93.4 bits (19^22) ``` 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. DIDs: `did:dfos:` + 22-char ID derived from `SHA-256(genesis CID raw bytes)` 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. ### Multikey Encoding (W3C Multikey for Ed25519) ``` Encode: 1. Take 32-byte Ed25519 public key 2. Prepend multicodec varint prefix [0xed, 0x01] (unsigned varint for 0xed = 237 = ed25519-pub) 3. Base58btc encode the 34-byte result 4. Prepend 'z' multibase prefix → "z6Mk..." Decode: 1. Strip 'z' multibase prefix 2. Base58btc decode → 34 bytes 3. First 2 bytes must be [0xed, 0x01] (ed25519-pub multicodec varint) 4. Remaining 32 bytes = raw Ed25519 public key ``` **Worked example:** ``` Public key (hex): ba421e272fad4f941c221e47f87d9253bdc04f7d4ad2625ae667ab9f0688ce32 Prefix + key (hex): ed01 ba421e272fad4f941c221e47f87d9253bdc04f7d4ad2625ae667ab9f0688ce32 Base58btc + 'z': z6MkrzLMNwoJSV4P3YccWcbtk8vd9LtgMKnLeaDLUqLuASjb ``` 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]`. ### CID Construction (dag-cbor + SHA-256) ``` 1. JSON payload → dag-cbor canonical encoding → CBOR bytes 2. SHA-256(CBOR bytes) → 32-byte hash 3. Construct CIDv1: - Version: 1 (varint: 0x01) - Codec: dag-cbor (varint: 0x71) - Multihash: SHA-256 (function: 0x12, length: 0x20, digest: 32 bytes) 4. CID binary = [0x01, 0x71, 0x12, 0x20, ...32 hash bytes] 5. Base32lower multibase encode → "bafyrei..." ``` 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. #### Number Encoding (Critical for CID Determinism) 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. **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/). 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. **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. **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. **Verification test vector** — encodes `{"version": 1, "type": "test"}`: ``` Integer encoding (CORRECT): CBOR: a2647479706564746573746776657273696f6e01 CID: bafyreihp6omsp6icc6ee63ox2ovsaxm6s7ikd2a7k5eh2qz2qd5soh5bsa Float encoding (WRONG — different bytes, different CID): CBOR: a2647479706564746573746776657273696f6ef93c00 CID: bafyreiawbms4476m5jlrmqtyvtwe5ta3eo2bh7mdprtomfgfype7j57o4q ``` 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). **Worked example (genesis identity operation):** ``` CBOR bytes (441 bytes, hex): a66474797065666372656174656776657273696f6e0168617574684b65797381a3626964781a6b 65795f72396576333466766332337a393939766561616674386474797065684d756c74696b6579 727075626c69634b65794d756c74696261736578307a364d6b727a4c4d4e776f4a535634503359 6363576362746b387664394c74674d4b6e4c6561444c55714c7541536a62696372656174656441 747818323032362d30332d30375430303a30303a30302e3030305a6a6173736572744b65797381 a3626964781a6b65795f72396576333466766332337a393939766561616674386474797065684d 756c74696b6579727075626c69634b65794d756c74696261736578307a364d6b727a4c4d4e776f 4a5356345033596363576362746b387664394c74674d4b6e4c6561444c55714c7541536a626e63 6f6e74726f6c6c65724b65797381a3626964781a6b65795f72396576333466766332337a393939 766561616674386474797065684d756c74696b6579727075626c69634b65794d756c7469626173 6578307a364d6b727a4c4d4e776f4a5356345033596363576362746b387664394c74674d4b6e4c 6561444c55714c7541536a62 CID bytes (hex): 01711220206a5e6140a5114f1e49f3ca4b339fb2cb8e70bbb34968b23156fd0e3237b486 CID string: bafyreibanjpgcqffcfhr4sptzjfthh5szohhbo5tjfulemkw7uhden5uqy ``` ### DID Derivation (worked example) ``` Input: CID bytes (hex) = 01711220206a5e6140a5114f1e49f3ca4b339fb2cb8e70bbb34968b23156fd0e3237b486 Step 1: SHA-256(CID bytes) = 4360cfbcbbb3f1614c8e02dbfe8d55935e1195cd2129820ab8aef94bde12ea8a 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 Step 3: For each byte, alphabet[byte % 19]: 43=67 → 67%19=10 → 'e' 60=96 → 96%19=1 → '3' cf=207 → 207%19=17 → 'v' bc=188 → 188%19=17 → 'v' ... Result: e3vvtck42d4eacdnzvtrn6 DID: did:dfos:e3vvtck42d4eacdnzvtrn6 ``` --- ## Operation Schemas ### Identity Operations ```typescript // Genesis — starts the identity chain { version: 1, type: "create", authKeys: MultikeyPublicKey[], assertKeys: MultikeyPublicKey[], controllerKeys: MultikeyPublicKey[], // must have at least one createdAt: string } // ISO 8601, ms precision, UTC // Key rotation / modification { version: 1, type: "update", previousOperationCID: string, // CID of previous operation authKeys: MultikeyPublicKey[], assertKeys: MultikeyPublicKey[], controllerKeys: MultikeyPublicKey[], // must have at least one createdAt: string } // Permanent destruction { version: 1, type: "delete", previousOperationCID: string, createdAt: string } ``` ### Content Operations ```typescript // Genesis — starts the content chain, commits initial document { version: 1, type: "create", did: string, // author DID, committed to by CID documentCID: string, // CID of flat content object baseDocumentCID: string | null, // edit lineage — CID of prior document version createdAt: string, note: string | null } // Content change (null documentCID = clear content) { version: 1, type: "update", did: string, // author DID previousOperationCID: string, documentCID: string | null, baseDocumentCID: string | null, createdAt: string, note: string | null, authorization?: string } // VC-JWT for delegated operations // Permanent destruction { version: 1, type: "delete", did: string, // author DID previousOperationCID: string, createdAt: string, note: string | null, authorization?: string } // VC-JWT for delegated operations ``` ### MultikeyPublicKey ```typescript { id: string, // e.g. "key_r9ev34fvc23z999veaaft8" type: "Multikey", // literal discriminator publicKeyMultibase: string } // e.g. "z6MkrzLMNwoJSV4P3YccWcbtk8vd9LtgMKnLeaDLUqLuASjb" ``` --- ## JWS Envelope Format ### Signing ``` signingInput = base64url(JSON.stringify(header)) + "." + base64url(JSON.stringify(payload)) signature = ed25519.sign(UTF8_bytes(signingInput), privateKey) token = signingInput + "." + base64url(signature) ``` ### kid Rules | Context | kid format | Example | | ------------------------- | ----------- | ---------------------------- | | Identity create (genesis) | Bare key ID | `key_r9ev34fvc23z999veaaft8` | | Identity update/delete | DID URL | See below | | All content ops | DID URL | See below | DID URL examples: ``` did:dfos:e3vvtck42d4eacdnzvtrn6#key_r9ev34fvc23z999veaaft8 did:dfos:e3vvtck42d4eacdnzvtrn6#key_ez9a874tckr3dv933d3ckd ``` ### `cid` Header 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. **Signing order:** 1. Construct the operation payload 2. Derive the operation CID: `dagCborCanonicalEncode(payload) → CIDv1` 3. Build the protected header including `cid` 4. Sign: `ed25519.sign(UTF8(base64url(header) + "." + base64url(payload)), privateKey)` **Verification rule:** After verifying the JWS signature and deriving the operation CID from the parsed payload, implementations MUST reject operations where: - `header.cid` is missing - `header.cid` does not match the derived CID A CID mismatch between header and derived value immediately surfaces dag-cbor encoding disagreements across implementations. Note: JWT auth tokens and VC-JWT credentials do NOT include a `cid` header — this field is specific to operation JWS tokens and beacons. ### CID Derivation ``` operation CID = dagCborCanonicalEncode(operation_payload) → SHA-256 → CIDv1 → base32lower string ``` The CID is derived from the JWS payload (the unsigned operation JSON), NOT from the JWS token itself. ### DID Derivation ``` DID = "did:dfos:" + idEncode(SHA-256(genesis_CID_raw_bytes)) ``` Where `idEncode` is the 19-char alphabet encoding described above. --- ## Credentials Two credential types handle authentication and authorization. Both are DID-signed JWTs using Ed25519 (`alg: "EdDSA"`). ### Auth Tokens (Relay Authentication) 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. **JWT Header:** ```json { "alg": "EdDSA", "typ": "JWT", "kid": "did:dfos:e3vvtck42d4eacdnzvtrn6#key_r9ev34fvc23z999veaaft8" } ``` **JWT Payload:** ```json { "iss": "did:dfos:e3vvtck42d4eacdnzvtrn6", "sub": "did:dfos:e3vvtck42d4eacdnzvtrn6", "aud": "relay.example.com", "exp": 1772845200, "iat": 1772841600 } ``` | Field | Type | Description | | ----- | ------ | ---------------------------------------------------------- | | `iss` | string | DID proving identity (the signer) | | `sub` | string | Same as `iss` for auth tokens | | `aud` | string | Target relay hostname (prevents cross-relay replay) | | `exp` | number | Expiration — unix seconds (short-lived, typically minutes) | | `iat` | number | Issued-at — unix seconds | **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`. Auth tokens do NOT include a `cid` header — they are ephemeral session tokens, not content-addressed artifacts. ### VC-JWT Credentials (Authorization) W3C Verifiable Credential Data Model v2 credentials encoded as JWT (`typ: "vc+jwt"`). Two credential types: | Credential Type | Purpose | | ------------------ | ------------------------------------------------------------ | | `DFOSContentWrite` | Authorize extending a content chain (embedded in operations) | | `DFOSContentRead` | Authorize reading content plane data (presented to relay) | **VC-JWT Header:** ```json { "alg": "EdDSA", "typ": "vc+jwt", "kid": "did:dfos:e3vvtck42d4eacdnzvtrn6#key_r9ev34fvc23z999veaaft8" } ``` **VC-JWT Payload:** ```json { "iss": "did:dfos:e3vvtck42d4eacdnzvtrn6", "sub": "did:dfos:nzkf838efr424433rn2rzk", "exp": 1798761600, "iat": 1772841600, "vc": { "@context": ["https://www.w3.org/ns/credentials/v2"], "type": ["VerifiableCredential", "DFOSContentWrite"], "credentialSubject": {} } } ``` | Field | Type | Description | | ---------------------- | -------- | -------------------------------------------------------- | | `iss` | string | DID granting the credential (content creator/controller) | | `sub` | string | DID receiving the credential (collaborator/reader) | | `exp` | number | Expiration — unix seconds | | `iat` | number | Issued-at — unix seconds | | `vc.@context` | string[] | Must be `["https://www.w3.org/ns/credentials/v2"]` | | `vc.type` | string[] | `["VerifiableCredential", ""]` | | `vc.credentialSubject` | object | Optional narrowing — see scope narrowing below | **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. ```json // Broad — all content by this DID { "credentialSubject": {} } // Narrow — specific content chain only { "credentialSubject": { "contentId": "a82z92a3hndk6c97thcrn8" } } ``` **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. ### Content Chain Authorization When `enforceAuthorization` is enabled on content chain verification: 1. **Genesis operation**: The signer is the chain creator, always authorized 2. **Creator signs subsequent ops**: Authorized directly — no credential needed 3. **Different DID signs**: Must include an `authorization` field containing a valid `DFOSContentWrite` VC-JWT where: - `iss` matches the chain creator DID - `sub` matches the signing DID - The credential is temporally valid (`iat <= op.createdAt < exp`, not wall clock) - If `contentId` is present in `credentialSubject`, it must match this chain's contentId - The credential type is `DFOSContentWrite` The `authorization` field is available on `update` and `delete` content operations. It is absent for creator-signed operations. --- ## Beacons 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. ### Beacon Payload ```json { "version": 1, "type": "beacon", "did": "did:dfos:e3vvtck42d4eacdnzvtrn6", "merkleRoot": "7e80d4780f454e0fca0b090d8c646f572b49354f54154531606105aad2fda28e", "createdAt": "2026-03-07T00:05:00.000Z" } ``` | Field | Type | Description | | ------------ | ------ | ------------------------------------------------------- | | `version` | 1 | Protocol version | | `type` | string | Literal `"beacon"` | | `did` | string | DID of the identity publishing the beacon | | `merkleRoot` | string | Hex-encoded SHA-256 root (64 chars, `/^[0-9a-f]{64}$/`) | | `createdAt` | string | ISO 8601 timestamp | ### Beacon JWS Header ```json { "alg": "EdDSA", "typ": "did:dfos:beacon", "kid": "did:dfos:e3vvtck42d4eacdnzvtrn6#key_r9ev34fvc23z999veaaft8", "cid": "bafyreihholuui7s7ns74iem6ahfxsb472hwogbqd32yrrp5fztc3kxa5qu" } ``` ### Worked Example: Beacon 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). **Beacon CID** (dag-cbor canonical encode → CIDv1): ``` bafyreihholuui7s7ns74iem6ahfxsb472hwogbqd32yrrp5fztc3kxa5qu ``` **Controller JWS** (key 1 signs): ``` kid: did:dfos:e3vvtck42d4eacdnzvtrn6#key_r9ev34fvc23z999veaaft8 typ: did:dfos:beacon cid: bafyreihholuui7s7ns74iem6ahfxsb472hwogbqd32yrrp5fztc3kxa5qu ``` **Witness countersignature** (a separate identity countersigns the beacon by CID): A countersignature is a standalone operation with its own CID and `typ: did:dfos:countersign`. See the [Countersignatures](#countersignatures) section below. Full JWS tokens are in [`examples/beacon.json`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/examples/beacon.json). ### Beacon Semantics 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. **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. **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). --- ## Merkle Trees 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. ### Construction 1. **Collect** all content IDs (22-char bare hashes) in the set 2. **Sort** content IDs lexicographically (UTF-8 byte order) 3. **Hash leaves**: for each content ID, `SHA-256(UTF-8(contentId))` → 32-byte leaf hash 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. 5. **Root**: the final 32-byte hash, hex-encoded to a 64-character string 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)))`. ### Worked Example: Merkle Tree 5 content IDs: `["alpha", "bravo", "charlie", "delta", "echo"]` Already sorted lexicographically. Hash each leaf: ``` alpha → SHA-256("alpha") → 8ed3f6ad685b959ead7022518e1af76cd816f8e8ec7ccdda1ed4018e8f2223f8 bravo → SHA-256("bravo") → 4f4a9410ffcdf895c4adb880659e9b5c0dd1f23a30790684340b3eaacb045398 charlie → SHA-256("charlie") → 36ef585cd42d49706cd2827a77d86c91bfdaf87a3f22b8f0e0308bd2c16cf85f delta → SHA-256("delta") → 18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4 echo → SHA-256("echo") → 092c79e8f80e559e404bcf660c48f3522b67aba9ff1484b0367e1a4ddef7431d ``` Build tree bottom-up, pairing left-to-right. Odd nodes promote unpaired: ``` Level 0 (leaves): [alpha] [bravo] [charlie] [delta] [echo] Level 1: [alpha‖bravo] [charlie‖delta] [echo] ← promoted Level 2: [L1-left‖L1-mid] [echo] ← promoted Level 3 (root): [L2-left‖echo] ``` Interior hashes: ``` SHA-256(alpha‖bravo) → 90d39555bb3c223e12f5a375c3011d2462fe2e1e36b8416a0b623d5831a9b4f3 SHA-256(charlie‖delta) → 6b55e77bef32937d9ccce2bd4b18127b0483f0be8e5b63c30bcc2b0d09f7dd44 SHA-256(alpha‖bravo ‖ charlie‖delta) → 23c83cb862e3b6a86eb2dfa0ea8ba0edcf1c3f3b8f14abc5eb9d72eab2edc2f7 ``` **Root** (level 3): ``` SHA-256(23c83c...edc2f7 ‖ 092c79...f7431d) → 7e80d4780f454e0fca0b090d8c646f572b49354f54154531606105aad2fda28e ``` ### Inclusion Proofs 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. ### Worked Example: Inclusion Proof for "charlie" Starting from the leaf hash of "charlie" (`36ef58...`), walk to the root using sibling hashes: ``` Step 1: charlie (index 2) paired with delta (index 3) sibling: 4f4a9410...045398 (delta leaf) position: right → SHA-256(charlie ‖ delta) → 6b55e77b...f7dd44 Step 2: charlie‖delta paired with alpha‖bravo sibling: 90d39555...a9b4f3 (alpha‖bravo) position: left → SHA-256(alpha‖bravo ‖ charlie‖delta) → 23c83cb8...edc2f7 Step 3: L2-left paired with echo (promoted) sibling: 092c79e8...f7431d (echo leaf) position: right → SHA-256(L2-left ‖ echo) → 7e80d478...fda28e ✓ matches root ``` Proof path (from [`examples/merkle-tree.json`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/examples/merkle-tree.json)): ```json [ { "hash": "4f4a9410ffcdf895c4adb880659e9b5c0dd1f23a30790684340b3eaacb045398", "position": "right" }, { "hash": "90d39555bb3c223e12f5a375c3011d2462fe2e1e36b8416a0b623d5831a9b4f3", "position": "left" }, { "hash": "092c79e8f80e559e404bcf660c48f3522b67aba9ff1484b0367e1a4ddef7431d", "position": "right" } ] ``` --- ## Artifacts 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. ### Payload ```json { "version": 1, "type": "artifact", "did": "did:dfos:...", "content": { "$schema": "https://schemas.dfos.com/profile/v1", "name": "Example" }, "createdAt": "2026-03-25T00:00:00.000Z" } ``` 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). ### Constraints - **JWS `typ` header**: `did:dfos:artifact` - **Max payload size**: 16384 bytes CBOR-encoded. Protocol constant — not configurable - **Immutability**: Once published, an artifact is never updated or replaced - **CID-addressable**: Each artifact is addressed by the CID of its CBOR-encoded payload ### Verification 1. JWS signature verification against the signing DID's current key state 2. CID integrity — `header.cid` matches the CID computed from dag-cbor canonical encoding the raw payload 3. Payload schema validation — `version`, `type: "artifact"`, `did`, `content` with `$schema`, `createdAt` 4. Size limit — CBOR-encoded payload does not exceed 16384 bytes --- ## Countersignatures 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. ### Payload ```json { "version": 1, "type": "countersign", "did": "did:dfos:witness...", "targetCID": "bafy...", "createdAt": "2026-03-25T00:00:00.000Z" } ``` The `did` field is the witness identity — the DID signing the attestation. The `targetCID` references the operation being attested to. ### Properties - **JWS `typ` header**: `did:dfos:countersign` - **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 - **Stateless verification**: Signature + CID integrity + payload schema. No chain state required to verify the cryptographic validity of a countersignature - **Composable**: The `targetCID` can reference any CID-addressable operation — content ops, beacons, artifacts, identity ops, even other countersignatures - **Immutable**: Once published, a countersignature is permanent ### Verification 1. Decode JWS, verify `typ` is `did:dfos:countersign` 2. Parse and validate countersign payload (`version`, `type: "countersign"`, `did`, `targetCID`, `createdAt`) 3. Verify the `kid` DID matches the payload `did` (the witness must sign with their own key) 4. CID integrity — `header.cid` matches the CID computed from dag-cbor canonical encoding the raw payload 5. Verify EdDSA JWS signature against the witness's public key Relay-level semantic checks (target exists, witness ≠ author, deduplication) are enforcement concerns, not protocol verification. --- ## Verification ### Identity Chain 1. Decode each JWS, parse payload as IdentityOperation 2. First op MUST be `type: "create"` — this is the genesis bootstrap: - 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. - 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. - Derive the operation CID via dag-cbor canonical encoding. Verify `header.cid` matches the derived CID. Derive the DID from the CID. 3. For each subsequent op: verify `previousOperationCID` matches previous op's derived CID. Verify `createdAt` is strictly increasing (SHOULD — see Protocol Rules). 4. Verify the chain is not in a terminal state (deleted) before applying any operation. 5. Resolve `kid` — genesis uses bare key ID, non-genesis uses DID URL (extract DID, verify it matches the derived DID; extract key ID). 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. 7. Verify EdDSA JWS signature over the signing input bytes. 8. Apply state change: `create` initializes key state, `update` replaces key state (must have at least one controller key), `delete` marks terminal. ### Content Chain 1. Decode each JWS, parse payload as ContentOperation 2. First op must be `type: "create"` — the signer is the chain creator 3. For each subsequent op: verify `previousOperationCID` matches, verify `createdAt` increasing 4. Derive the operation CID via dag-cbor canonical encoding. Verify `header.cid` matches the derived CID. 5. Verify the `kid` DID matches the payload `did` field 6. Resolve `kid` via external key resolver (caller provides) 7. Verify EdDSA JWS signature 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 9. Apply state change (set document, clear, or delete) --- ## Deterministic Reference Artifacts 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"))`. ### Key 1 (Genesis Controller) ``` Seed: SHA-256("dfos-protocol-reference-key-1") Private key: 132d4bebdb6e62359afb930fe15d756a92ad96e6b0d47619988f5a1a55272aac Public key: ba421e272fad4f941c221e47f87d9253bdc04f7d4ad2625ae667ab9f0688ce32 Multikey: z6MkrzLMNwoJSV4P3YccWcbtk8vd9LtgMKnLeaDLUqLuASjb Key ID: key_r9ev34fvc23z999veaaft8 ``` ### Key 2 (Rotated Controller) ``` Seed: SHA-256("dfos-protocol-reference-key-2") Private key: 384f5626906db84f6a773ec46475ff2d4458e92dd4dd13fe03dbb7510f4ca2a8 Public key: 0f350f994f94d675f04a325bd316ebedd740ca206eaaf609bdb641b5faa0f78c Multikey: z6MkfUd65JrAhfdgFuMCccU9ThQvjB2fJAMUHkuuajF992gK Key ID: key_ez9a874tckr3dv933d3ckd ``` ### Identity Chain: Create (Genesis) Operation: ```json { "version": 1, "type": "create", "authKeys": [ { "id": "key_r9ev34fvc23z999veaaft8", "type": "Multikey", "publicKeyMultibase": "z6MkrzLMNwoJSV4P3YccWcbtk8vd9LtgMKnLeaDLUqLuASjb" } ], "assertKeys": [ { "id": "key_r9ev34fvc23z999veaaft8", "type": "Multikey", "publicKeyMultibase": "z6MkrzLMNwoJSV4P3YccWcbtk8vd9LtgMKnLeaDLUqLuASjb" } ], "controllerKeys": [ { "id": "key_r9ev34fvc23z999veaaft8", "type": "Multikey", "publicKeyMultibase": "z6MkrzLMNwoJSV4P3YccWcbtk8vd9LtgMKnLeaDLUqLuASjb" } ], "createdAt": "2026-03-07T00:00:00.000Z" } ``` JWS Header: ```json { "alg": "EdDSA", "typ": "did:dfos:identity-op", "kid": "key_r9ev34fvc23z999veaaft8", "cid": "bafyreibanjpgcqffcfhr4sptzjfthh5szohhbo5tjfulemkw7uhden5uqy" } ``` JWS Signature (hex): ``` 103af20cad6ebed8b1fb5edc1ee9fdb7a31a705231dab326305d502f37c3e531654ac3af31cb9ef7ba428069f709778b545b55c60a42a21d241925e2a0a2b303 ``` JWS Token: ``` eyJhbGciOiJFZERTQSIsInR5cCI6ImRpZDpkZm9zOmlkZW50aXR5LW9wIiwia2lkIjoia2V5X3I5ZXYzNGZ2YzIzejk5OXZlYWFmdDgiLCJjaWQiOiJiYWZ5cmVpYmFuanBnY3FmZmNmaHI0c3B0empmdGhoNXN6b2hoYm81dGpmdWxlbWt3N3VoZGVuNXVxeSJ9.eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiY3JlYXRlIiwiYXV0aEtleXMiOlt7ImlkIjoia2V5X3I5ZXYzNGZ2YzIzejk5OXZlYWFmdDgiLCJ0eXBlIjoiTXVsdGlrZXkiLCJwdWJsaWNLZXlNdWx0aWJhc2UiOiJ6Nk1rcnpMTU53b0pTVjRQM1ljY1djYnRrOHZkOUx0Z01LbkxlYURMVXFMdUFTamIifV0sImFzc2VydEtleXMiOlt7ImlkIjoia2V5X3I5ZXYzNGZ2YzIzejk5OXZlYWFmdDgiLCJ0eXBlIjoiTXVsdGlrZXkiLCJwdWJsaWNLZXlNdWx0aWJhc2UiOiJ6Nk1rcnpMTU53b0pTVjRQM1ljY1djYnRrOHZkOUx0Z01LbkxlYURMVXFMdUFTamIifV0sImNvbnRyb2xsZXJLZXlzIjpbeyJpZCI6ImtleV9yOWV2MzRmdmMyM3o5OTF2ZWFhZnQ4IiwidHlwZSI6Ik11bHRpa2V5IiwicHVibGljS2V5TXVsdGliYXNlIjoiejZNa3J6TE1Od29KU1Y0UDNZY2NXY2J0azh2ZDlMdGdNS25MZWFETFVxTHVBU2piIn1dLCJjcmVhdGVkQXQiOiIyMDI2LTAzLTA3VDAwOjAwOjAwLjAwMFoifQ.EDryDK1uvtix-17cHun9t6MacFIx2rMmMF1QLzfD5TFlSsOvMcue97pCgGn3CXeLVFtVxgpCoh0kGSXioKKzAw ``` Operation CID: ``` bafyreibanjpgcqffcfhr4sptzjfthh5szohhbo5tjfulemkw7uhden5uqy ``` **Derived DID: `did:dfos:e3vvtck42d4eacdnzvtrn6`** ### Identity Chain: Update (Key Rotation) JWS Header: ```json { "alg": "EdDSA", "typ": "did:dfos:identity-op", "kid": "did:dfos:e3vvtck42d4eacdnzvtrn6#key_r9ev34fvc23z999veaaft8", "cid": "bafyreicym4cyiednld73smbx32szaei7xdulqn4g3ste5e2w2ulajr3oqm" } ``` Operation: ```json { "version": 1, "type": "update", "previousOperationCID": "bafyreibanjpgcqffcfhr4sptzjfthh5szohhbo5tjfulemkw7uhden5uqy", "authKeys": [ { "id": "key_ez9a874tckr3dv933d3ckd", "type": "Multikey", "publicKeyMultibase": "z6MkfUd65JrAhfdgFuMCccU9ThQvjB2fJAMUHkuuajF992gK" } ], "assertKeys": [ { "id": "key_ez9a874tckr3dv933d3ckd", "type": "Multikey", "publicKeyMultibase": "z6MkfUd65JrAhfdgFuMCccU9ThQvjB2fJAMUHkuuajF992gK" } ], "controllerKeys": [ { "id": "key_ez9a874tckr3dv933d3ckd", "type": "Multikey", "publicKeyMultibase": "z6MkfUd65JrAhfdgFuMCccU9ThQvjB2fJAMUHkuuajF992gK" } ], "createdAt": "2026-03-07T00:01:00.000Z" } ``` JWS Signature (hex): ``` 31272ea0196038ade3e505fdb45730d68bb4a382e0273886244b19e69bea881af549a800c80bf987ec1a8d086d83c20fedd2e533453895e5b6891adaf78e5c0e ``` JWS Token: ``` eyJhbGciOiJFZERTQSIsInR5cCI6ImRpZDpkZm9zOmlkZW50aXR5LW9wIiwia2lkIjoiZGlkOmRmb3M6ZTN2dnRjazQyZDRlYWNkbnp2dHJuNiNrZXlfcjlldjM0ZnZjMjN6OTk5dmVhYWZ0OCIsImNpZCI6ImJhZnlyZWljeW00Y3lpZWRubGQ3M3NtYngzMnN6YWVpN3hkdWxxbjRnM3N0ZTVlMncydWxhanIzb3FtIn0.eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoidXBkYXRlIiwicHJldmlvdXNPcGVyYXRpb25DSUQiOiJiYWZ5cmVpYmFuanBnY3FmZmNmaHI0c3B0empmdGhoNXN6b2hoYm81dGpmdWxlbWt3N3VoZGVuNXVxeSIsImF1dGhLZXlzIjpbeyJpZCI6ImtleV9lejlhODc0dGNrcjNkdjkzM2QzY2tkIiwidHlwZSI6Ik11bHRpa2V5IiwicHVibGljS2V5TXVsdGliYXNlIjoiejZNa2ZVZDY1SnJBaGZkZ0Z1TUNjY1U5VGhRdmpCMmZKQU1VSGt1dWFqRjk5MmdLIn1dLCJhc3NlcnRLZXlzIjpbeyJpZCI6ImtleV9lejlhODc0dGNrcjNkdjkzM2QzY2tkIiwidHlwZSI6Ik11bHRpa2V5IiwicHVibGljS2V5TXVsdGliYXNlIjoiejZNa2ZVZDY1SnJBaGZkZ0Z1TUNjY1U5VGhRdmpCMmZKQU1VSGt1dWFqRjk5MmdLIn1dLCJjb250cm9sbGVyS2V5cyI6W3siaWQiOiJrZXlfZXo5YTg3NHRja3IzZHY5MzNkM2NrZCIsInR5cGUiOiJNdWx0aWtleSIsInB1YmxpY0tleU11bHRpYmFzZSI6Ino2TWtmVWQ2NUpyQWhmZGdGdU1DY2NVOVRoUXZqQjJmSkFNVUhrdXVhakY5OTJnSyJ9XSwiY3JlYXRlZEF0IjoiMjAyNi0wMy0wN1QwMDowMTowMC4wMDBaIn0.MScuoBlgOK3j5QX9tFcw1ou0o4LgJziGJEsZ5pvqiBr1SagAyAv5h-wajQhtg8IP7dLlM0U4leW2iRra945cDg ``` Operation CID: ``` bafyreicym4cyiednld73smbx32szaei7xdulqn4g3ste5e2w2ulajr3oqm ``` Post-rotation: DID unchanged (`did:dfos:e3vvtck42d4eacdnzvtrn6`), controller rotated to `key_ez9a874tckr3dv933d3ckd`. ### Content Chain: Document + Create Document (flat content object): ```json { "$schema": "https://schemas.dfos.com/post/v1", "format": "short-post", "title": "Hello World", "body": "First post on the protocol.", "createdByDID": "did:dfos:e3vvtck42d4eacdnzvtrn6" } ``` Document CID: ``` bafyreihzwuoupfg3dxip6xmgzmxsywyii2jeoxxzbgx3zxm2in7knoi3g4 ``` Content Create JWS Header: ```json { "alg": "EdDSA", "typ": "did:dfos:content-op", "kid": "did:dfos:e3vvtck42d4eacdnzvtrn6#key_ez9a874tckr3dv933d3ckd", "cid": "bafyreiaedhjq64aajpwociahl5w37j6uoxr5mojoq5dnah6fpvxr5d4lxu" } ``` Content Create Payload: ```json { "version": 1, "type": "create", "did": "did:dfos:e3vvtck42d4eacdnzvtrn6", "documentCID": "bafyreihzwuoupfg3dxip6xmgzmxsywyii2jeoxxzbgx3zxm2in7knoi3g4", "baseDocumentCID": null, "createdAt": "2026-03-07T00:02:00.000Z", "note": null } ``` Content Create JWS Signature (hex): ``` 46feaf973e4c7ebc2a0d4ad25481ace197de05b91051205c5e1c7067a85fb9d4abe4cc61625d3c853a8b0ce0345b534c8cdd07b34216f635d3c0bc0fd5d30306 ``` Content Create JWS Token: ``` eyJhbGciOiJFZERTQSIsInR5cCI6ImRpZDpkZm9zOmNvbnRlbnQtb3AiLCJraWQiOiJkaWQ6ZGZvczplM3Z2dGNrNDJkNGVhY2RuenZ0cm42I2tleV9lejlhODc0dGNrcjNkdjkzM2QzY2tkIiwiY2lkIjoiYmFmeXJlaWFlZGhqcTY0YWFqcHdvY2lhaGw1dzM3ajZ1b3hyNW1vam9xNWRuYWg2ZnB2eHI1ZDRseHUifQ.eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiY3JlYXRlIiwiZGlkIjoiZGlkOmRmb3M6ZTN2dnRjazQyZDRlYWNkbnp2dHJuNiIsImRvY3VtZW50Q0lEIjoiYmFmeXJlaWh6d3VvdXBmZzNkeGlwNnhtZ3pteHN5d3lpaTJqZW94eHpiZ3gzenhtMmluN2tub2kzZzQiLCJiYXNlRG9jdW1lbnRDSUQiOm51bGwsImNyZWF0ZWRBdCI6IjIwMjYtMDMtMDdUMDA6MDI6MDAuMDAwWiIsIm5vdGUiOm51bGx9.Rv6vlz5MfrwqDUrSVIGs4ZfeBbkQUSBcXhxwZ6hfudSr5MxhYl08hTqLDOA0W1NMjN0Hs0IW9jXTwLwP1dMDBg ``` Content Operation CID: ``` bafyreiaedhjq64aajpwociahl5w37j6uoxr5mojoq5dnah6fpvxr5d4lxu ``` ### Content Chain: Update Content Update Payload: ```json { "version": 1, "type": "update", "did": "did:dfos:e3vvtck42d4eacdnzvtrn6", "previousOperationCID": "bafyreiaedhjq64aajpwociahl5w37j6uoxr5mojoq5dnah6fpvxr5d4lxu", "documentCID": "bafyreidh7e36cvwy3uw5ypitcqk7uoktbkkkj7e6hxhky4o75rxn7kxilu", "baseDocumentCID": "bafyreihzwuoupfg3dxip6xmgzmxsywyii2jeoxxzbgx3zxm2in7knoi3g4", "createdAt": "2026-03-07T00:03:00.000Z", "note": "edited title and body" } ``` Updated document (flat content object): ```json { "$schema": "https://schemas.dfos.com/post/v1", "format": "short-post", "title": "Hello World (edited)", "body": "Updated content.", "createdByDID": "did:dfos:e3vvtck42d4eacdnzvtrn6" } ``` Document CID (edited): ``` bafyreidh7e36cvwy3uw5ypitcqk7uoktbkkkj7e6hxhky4o75rxn7kxilu ``` Content Update CID: ``` bafyreih6e5cbjitpozhzhgmfktmiohmxyn3ucwhqd3mjixizvwmlhv7hm4 ``` ### Content Chain Verified State ``` Content ID: a82z92a3hndk6c97thcrn8 Genesis CID: bafyreiaedhjq64aajpwociahl5w37j6uoxr5mojoq5dnah6fpvxr5d4lxu Head CID: bafyreih6e5cbjitpozhzhgmfktmiohmxyn3ucwhqd3mjixizvwmlhv7hm4 ``` --- ## Verification Checklist (For Independent Implementers) Given the artifacts above, verify: 1. **Multikey decode**: strip `z`, base58btc decode, strip `[0xed, 0x01]` prefix → raw public key: ``` z6MkrzLMNwoJSV4P3YccWcbtk8vd9LtgMKnLeaDLUqLuASjb → ba421e272fad4f941c221e47f87d9253bdc04f7d4ad2625ae667ab9f0688ce32 ``` 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`. 3. **Genesis CID**: base64url-decode JWS payload → parse JSON → dag-cbor canonical encode → SHA-256 → CIDv1 → should be: ``` bafyreibanjpgcqffcfhr4sptzjfthh5szohhbo5tjfulemkw7uhden5uqy ``` 4. **CID header**: Verify each operation JWS header contains `cid` matching the derived operation CID 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` 6. **Rotation JWS**: signed by OLD controller key (key 1). Verify with key 1's public key. kid: ``` did:dfos:e3vvtck42d4eacdnzvtrn6#key_r9ev34fvc23z999veaaft8 ``` 7. **Content create JWS**: signed by NEW controller key (key 2, post-rotation). Verify with key 2's public key. kid: ``` did:dfos:e3vvtck42d4eacdnzvtrn6#key_ez9a874tckr3dv933d3ckd ``` 8. **Document CID**: dag-cbor canonical encode the flat content object → SHA-256 → CIDv1 → should be: ``` bafyreihzwuoupfg3dxip6xmgzmxsywyii2jeoxxzbgx3zxm2in7knoi3g4 ``` 9. **Content operation `did` field**: verify the `did` field in each content operation matches the `kid` DID in the JWS header 10. **Content chain integrity**: update's `previousOperationCID` matches create's operation CID 11. **Chain completeness**: all operation CIDs, DID derivation, key rotation, and content chain linkage verified end-to-end. 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). 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. 14. **Number encoding determinism**: dag-cbor encode `{"version": 1, "type": "test"}` and verify: - CBOR hex is `a2647479706564746573746776657273696f6e01` (20 bytes) - CID is `bafyreihp6omsp6icc6ee63ox2ovsaxm6s7ikd2a7k5eh2qz2qd5soh5bsa` - Byte at offset 19 is `0x01` (CBOR integer 1), NOT `0xf9` (CBOR float header) - 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. --- ## Source and Verification 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. - [`crypto/ed25519`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/crypto/ed25519.ts) — `createNewEd25519Keypair`, `importEd25519Keypair`, `signPayloadEd25519`, `isValidEd25519Signature` - [`crypto/jws`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/crypto/jws.ts) — `createJws`, `verifyJws`, `decodeJwsUnsafe` - [`crypto/jwt`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/crypto/jwt.ts) — `createJwt`, `verifyJwt` - [`crypto/base64url`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/crypto/base64url.ts) — `base64urlEncode`, `base64urlDecode` - [`crypto/multiformats`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/crypto/multiformats.ts) — `dagCborCanonicalEncode`, `dagCborCanonicalEqual` - [`crypto/id`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/crypto/id.ts) — `generateId`, `generateIdNoPrefix`, `isValidId` - [`chain/multikey`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/multikey.ts) — `encodeEd25519Multikey`, `decodeMultikey` - [`chain/schemas`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/schemas.ts) — `IdentityOperation`, `ContentOperation`, `ArtifactPayload`, `CountersignPayload`, `MultikeyPublicKey`, `VerifiedIdentity` - [`chain/identity-chain`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/identity-chain.ts) — `signIdentityOperation`, `verifyIdentityChain`, `verifyIdentityExtensionFromTrustedState` - [`chain/content-chain`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/content-chain.ts) — `signContentOperation`, `verifyContentChain`, `verifyContentExtensionFromTrustedState` - [`chain/derivation`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/derivation.ts) — `deriveChainIdentifier`, `deriveContentId` - [`chain/beacon`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/beacon.ts) — `signBeacon`, `verifyBeacon` - [`chain/artifact`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/artifact.ts) — `signArtifact`, `verifyArtifact` - [`chain/countersign`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/countersign.ts) — `signCountersignature`, `verifyCountersignature` - [`credentials/auth-token`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/credentials/auth-token.ts) — `createAuthToken`, `verifyAuthToken` - [`credentials/credential`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/credentials/credential.ts) — `createCredential`, `verifyCredential`, `decodeCredentialUnsafe` - [`credentials/schemas`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/credentials/schemas.ts) — `AuthTokenClaims`, `CredentialClaims`, `VCClaim`, `DFOSCredentialType` - [`merkle/tree`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/merkle/tree.ts) — `buildMerkleTree`, `hashLeaf` - [`merkle/proof`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/merkle/proof.ts) — `generateMerkleProof`, `verifyMerkleProof` ### Related Specifications - [DID Method: `did:dfos`](https://protocol.dfos.com/did-method) — W3C DID method specification for identity chains - [Content Model](https://protocol.dfos.com/content-model) — Standard content schemas (post, profile) for document content objects - [Web Relay](https://protocol.dfos.com/web-relay) — HTTP relay specification for ingestion, state, and content plane ### Cross-Language Verification | Language | Tests | Source | | ---------- | ----- | ---------------------------------------------------------------------------------------------------- | | TypeScript | 224 | [`tests/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/tests) | | Go | 18 | [`verify/go/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/verify/go) | | Rust | 18 | [`verify/rust/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/verify/rust) | | Python | 3 | [`verify/python/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/verify/python) | | Swift | 3 | [`verify/swift/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/verify/swift) | --- ## Special Thanks - **Vinny Bellavia** — [stcisgood.com](https://stcisgood.com) - **Allison Clift-Jennings** — [Jura Labs](https://juralabs.com) --- # DID Method: `did:dfos` W3C DID Method specification for DFOS identity chains. Self-certifying, transport-agnostic, Ed25519-based decentralized identifiers. This spec is under active review. Discuss it in the [clear.txt](https://clear.dfos.com) space on DFOS. [Source](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol) · [Protocol Specification](https://protocol.dfos.com/spec) · [npm](https://www.npmjs.com/package/@metalabel/dfos-protocol) --- ## Abstract This document defines the `did:dfos` DID method in conformance with the [W3C Decentralized Identifiers (DIDs) v1.0](https://www.w3.org/TR/did-core/) specification. A `did:dfos` identifier is derived deterministically from the genesis operation of a cryptographically signed identity chain. Resolution is verification-first and transport-agnostic — the identifier itself is the trust anchor, not any particular registry or consensus layer. --- ## Status This specification is under active development. It has not been submitted to any formal standards body. --- ## Conformance The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119). --- ## 1. Introduction DFOS is a protocol for verifiable identity and content chains using Ed25519 signatures and content-addressed CIDs. Every identity in DFOS is an append-only chain of signed operations — a self-sovereign log of key management events. The DID for an identity is derived deterministically from the hash of the chain's genesis operation, making `did:dfos` identifiers **self-certifying**: given the chain, anyone can independently verify the DID without trusting the source. This property makes `did:dfos` fundamentally transport-agnostic. There is no privileged registry, blockchain, or consensus layer. The chain can be obtained from any source — an HTTP API, a peer-to-peer exchange, a local file, a USB drive — and the verifier can independently confirm the chain belongs to the claimed DID. For full protocol details including cryptographic primitives, chain mechanics, and test vectors, see the [DFOS Protocol Specification](https://protocol.dfos.com/spec). ### 1.1 Design Goals - **Self-certifying** — The DID is a deterministic derivation of the genesis content. No external authority is needed to verify the binding between identifier and chain. - **Transport-agnostic** — Resolution requires obtaining and verifying a chain, not querying a specific endpoint. Any system that stores and serves identity chains is a valid source. - **Key rotation** — Identity chains support full key rotation via signed update operations. Keys can be added, removed, and replaced without changing the DID. - **Deactivation** — Identities can be permanently deactivated via a signed delete operation. - **Minimal** — The method defines identifiers and verification. It deliberately does not define discovery, gossip, or consensus mechanisms. --- ## 2. DID Method Name The method name is `dfos`. A DID using this method MUST begin with the prefix `did:dfos:`. --- ## 3. Method-Specific Identifier The method-specific identifier is a 22-character string derived from the genesis operation CID of an identity chain. ### 3.1 ABNF ```abnf dfos-did = "did:dfos:" dfos-id dfos-id = 22dfos-char dfos-char = "2" / "3" / "4" / "6" / "7" / "8" / "9" / "a" / "c" / "d" / "e" / "f" / "h" / "k" / "n" / "r" / "t" / "v" / "z" ``` The alphabet is 19 characters: `2346789acdefhknrtvz`. The identifier is exactly 22 characters, providing ~93.4 bits of entropy. ### 3.2 Derivation The method-specific identifier is derived deterministically from the genesis identity operation: ``` 1. Construct the genesis identity operation payload (type: "create") 2. Canonical-encode the payload as dag-cbor → CBOR bytes 3. Hash: SHA-256(CBOR bytes) → 32-byte digest 4. Construct CIDv1: [0x01, 0x71, 0x12, 0x20, ...32 digest bytes] → CID bytes 5. Hash the CID: SHA-256(CID bytes) → 32-byte digest 6. Encode: for each of the first 22 bytes → alphabet[byte % 19] ``` The resulting 22-character string is the method-specific identifier. The full DID is `did:dfos:` prepended to this string. ### 3.3 Example ``` Genesis CID bytes (hex): 01711220206a5e6140a5114f1e49f3ca4b339fb2cb8e70bbb34968b23156fd0e3237b486 SHA-256 of CID bytes: 4360cfbcbbb3f1614c8e02dbfe8d55935e1195cd2129820ab8aef94bde12ea8a First 22 bytes encoded: e3vvtck42d4eacdnzvtrn6 DID: did:dfos:e3vvtck42d4eacdnzvtrn6 ``` See the [DFOS Protocol Specification](https://protocol.dfos.com/spec) for the complete worked example with key material, CBOR encoding, and CID construction. --- ## 4. DID Document A resolved `did:dfos` DID Document is constructed from the current state of the identity chain — specifically, the key sets declared in the most recent non-terminal operation. ### 4.1 DID Document Structure ```json { "@context": ["https://www.w3.org/ns/did/v1", "https://w3id.org/security/multikey/v1"], "id": "did:dfos:e3vvtck42d4eacdnzvtrn6", "controller": "did:dfos:e3vvtck42d4eacdnzvtrn6", "verificationMethod": [ { "id": "did:dfos:e3vvtck42d4eacdnzvtrn6#key_r9ev34fvc23z999veaaft8", "type": "Multikey", "controller": "did:dfos:e3vvtck42d4eacdnzvtrn6", "publicKeyMultibase": "z6MkrzLMNwoJSV4P3YccWcbtk8vd9LtgMKnLeaDLUqLuASjb" } ], "authentication": ["did:dfos:e3vvtck42d4eacdnzvtrn6#key_r9ev34fvc23z999veaaft8"], "assertionMethod": ["did:dfos:e3vvtck42d4eacdnzvtrn6#key_r9ev34fvc23z999veaaft8"], "capabilityInvocation": ["did:dfos:e3vvtck42d4eacdnzvtrn6#key_r9ev34fvc23z999veaaft8"] } ``` ### 4.2 Verification Method Mapping Identity chain operations declare three key sets. These map to W3C verification relationships as follows: | Identity Chain Key Set | W3C Verification Relationship | Purpose | | ---------------------- | ----------------------------- | -------------------------------------------------------------------- | | `authKeys` | `authentication` | Prove control of the DID (e.g., login, session establishment) | | `assertKeys` | `assertionMethod` | Issue verifiable assertions (e.g., sign content chain operations) | | `controllerKeys` | `capabilityInvocation` | Manage the DID itself (sign identity chain update/delete operations) | Each key in the identity chain state becomes a `verificationMethod` entry. The `id` is constructed as a DID URL: `did:dfos:#`. The `type` is `Multikey`. The `publicKeyMultibase` is the W3C Multikey encoding (multicodec `0xed01` prefix + base58btc + `z` multibase prefix). ### 4.3 Controller `did:dfos` identities are self-sovereign. The `controller` property of the DID Document is always the DID itself. Only keys within the identity chain's `controllerKeys` set can sign operations that modify the chain. ### 4.4 Key Rotation When an identity chain includes `update` operations that change the key sets, the DID Document reflects the **current state** — the key sets from the most recent operation. Previous keys are not included in the resolved DID Document. Historical key states can be recovered by walking the chain. ### 4.5 Services The core `did:dfos` method does not define any `service` entries in the DID Document. Applications MAY extend the DID Document with service endpoints through application-layer conventions. --- ## 5. Operations ### 5.1 Create Creating a `did:dfos` identifier means constructing and signing a genesis identity chain operation. 1. Generate one or more Ed25519 key pairs. 2. Construct the genesis operation payload with `type: "create"`, populating `authKeys`, `assertKeys`, and `controllerKeys`. At least one `controllerKeys` entry is REQUIRED. 3. Canonical-encode the payload as dag-cbor, derive the CID, and include it in the JWS protected header as `cid`. 4. Sign the operation as a JWS Compact Serialization token using one of the controller keys. The `kid` in the protected header is the bare key ID (not a DID URL, since the DID does not yet exist). 5. The DID is derived from the genesis CID as described in [Section 3.2](#32-derivation). The identity chain now exists as a single-operation chain. It can be stored in any system that serves identity chains. ### 5.2 Read (Resolve) Resolving a `did:dfos` DID means obtaining the identity chain and constructing a DID Document from its current state. #### 5.2.1 Resolution Algorithm Given a DID `did:dfos:`: 1. **Obtain** the identity chain from any available source. The method does not prescribe how chains are discovered or transported. 2. **Verify** the chain: a. Decode each JWS token and parse the operation payload. b. The first operation MUST be `type: "create"`. c. Derive the genesis operation CID via dag-cbor canonical encoding. d. Verify that `SHA-256(genesis CID bytes)` encoded with the ID alphabet produces ``. If it does not match, the chain does not belong to this DID — reject it. e. For each operation, verify the JWS EdDSA signature against the appropriate key (controller key from current chain state). f. Verify `previousOperationCID` linkage, `createdAt` ordering, and `header.cid` consistency. g. See the [DFOS Protocol Specification](https://protocol.dfos.com/spec) for complete verification rules. 3. **Construct** the DID Document from the terminal chain state using the mapping in [Section 4.2](#42-verification-method-mapping). #### 5.2.2 Resolution Metadata | Property | Value | | ---------------- | ------------------------------------------------------------ | | `contentType` | `application/did+ld+json` | | `created` | `createdAt` from the genesis operation | | `updated` | `createdAt` from the most recent operation | | `deactivated` | `true` if the chain's terminal operation is `type: "delete"` | | `operationCount` | Number of operations in the chain | #### 5.2.3 Self-Certification The critical property of `did:dfos` resolution: **the DID is verified against the chain, not the source.** Step 2d above is the self-certification check — it proves the chain belongs to the claimed DID using only the chain content and a hash function. This means: - A resolver does not need to trust the registry, server, or peer that provided the chain. - The same chain can be served by multiple independent sources with identical results. - Chains can be cached, replicated, and redistributed without loss of verifiability. - Offline resolution is possible if the chain is available locally. #### 5.2.4 Transport Bindings (Non-Normative) The `did:dfos` method is transport-agnostic. Any system that can deliver an ordered sequence of JWS tokens (the identity chain) is a valid transport. Examples include: - **HTTP API** — Any HTTP service that stores and retrieves ordered JWS logs can serve as a transport binding. - **Peer-to-peer exchange** — Chains can be exchanged directly between parties. - **Local storage** — Chains can be stored in local files, databases, or key-value stores. - **Bundle export** — Applications can export chains as portable bundles (e.g., JSON arrays of JWS tokens). ### 5.3 Update Updating a `did:dfos` DID means appending a signed `update` operation to the identity chain. 1. Construct an update operation payload with `type: "update"`, the new key sets, and `previousOperationCID` set to the CID of the current chain tip. 2. Sign the operation using a key from the **current** `controllerKeys` set. The `kid` is a DID URL: `did:dfos:#`. 3. Append the signed JWS token to the chain. The DID does not change. The resolved DID Document now reflects the new key sets. ### 5.4 Deactivate (Delete) Deactivating a `did:dfos` DID means appending a signed `delete` operation to the identity chain. 1. Construct a delete operation payload with `type: "delete"` and `previousOperationCID` set to the CID of the current chain tip. 2. Sign the operation using a key from the current `controllerKeys` set. The `kid` is a DID URL. 3. Append the signed JWS token to the chain. After deactivation: - The chain is in a **terminal state**. No further operations can be appended. - Resolution MUST return a DID Document with `deactivated: true` in the resolution metadata. - The DID Document SHOULD contain an empty set of verification methods, as the identity no longer has active keys. Deactivation is **permanent and irreversible**. The DID cannot be reactivated. --- ## 6. Security Considerations ### 6.1 Self-Certifying Identifiers `did:dfos` identifiers are derived from a cryptographic hash of the genesis operation content. This binding is verified during resolution (Section 5.2.1, step 2d). An attacker cannot present a forged chain for a given DID — the genesis content would hash to a different identifier. ### 6.2 Key Compromise If a controller key is compromised, the legitimate holder should immediately sign a key rotation (`update`) operation removing the compromised key. The protocol does not support key pre-rotation — there is no mechanism to pre-commit to a future key. The window of vulnerability exists between compromise and rotation. ### 6.3 Equivocation Because `did:dfos` has no global consensus layer, an identity holder could theoretically sign two different operations at the same chain position (same `previousOperationCID`, different payloads). This creates a **fork** — two valid chain branches. Equivocation is **detectable**: a verifier who encounters two valid operations sharing a `previousOperationCID` can identify the conflict. Resolution policy for equivocation (reject both branches, prefer one, flag for human review) is an application-level concern and is deliberately outside the scope of this method specification. In practice, equivocation requires the identity holder to act against themselves — no external party can extend an identity chain, since all operations must be signed by a current controller key. ### 6.4 Transport Security The `did:dfos` method does not mandate any specific transport security. Because resolution is verification-first (the chain is validated against the DID, not the source), transport-layer attacks (MITM, DNS hijacking) cannot produce a valid chain for a targeted DID. An attacker who intercepts a chain request can: - **Withhold** the chain (denial of service) — the resolver gets no result - **Serve a stale chain** — the resolver gets a valid but outdated DID Document - **Serve a completely different chain** — the self-certification check fails, the resolver rejects it An attacker **cannot** serve a modified or forged chain that passes the self-certification check. ### 6.5 Denial of Service A resolver that depends on a single source for chain retrieval is vulnerable to denial of service. Applications SHOULD support multiple chain sources and MAY cache verified chains locally to mitigate this. ### 6.6 Cryptographic Agility The current specification uses Ed25519 exclusively. The protocol does not currently support multiple signature algorithms. Future versions MAY introduce additional algorithms via new multicodec identifiers and verification method types. Implementations MUST reject operations signed with unrecognized algorithms. --- ## 7. Privacy Considerations ### 7.1 Correlation `did:dfos` identifiers are persistent and globally unique. Any content chain signed by a DID can be correlated to the same identity. Users who require unlinkability across contexts should use distinct identities (distinct identity chains and DIDs) for each context. ### 7.2 Key Material Identity chains contain only public keys. Private key material is never included in the chain and MUST NOT be transmitted during resolution. ### 7.3 Chain History The full identity chain is available to any resolver. This reveals the history of key rotations, including timestamps (`createdAt`). Applications that consider key rotation history sensitive should be aware that this metadata is inherently public as part of the chain. ### 7.4 Herd Privacy Because `did:dfos` resolution can happen through any transport (including local storage), a resolver does not necessarily reveal which DIDs it is interested in. However, when using a shared registry API, the registry operator can observe resolution patterns. Applications with strong privacy requirements SHOULD resolve chains through privacy-preserving transports or maintain local chain caches. --- ## 8. Reference Implementation A complete reference implementation is available as the `@metalabel/dfos-protocol` npm package: - **npm**: [@metalabel/dfos-protocol](https://www.npmjs.com/package/@metalabel/dfos-protocol) - **Source**: [github.com/metalabel/dfos](https://github.com/metalabel/dfos) - **Cross-language verification**: Go, Python, Rust, and Swift implementations verify the same deterministic test vectors --- ## 9. References ### 9.1 Normative References | Reference | URI | | --------------------------- | --------------------------------------------------- | | W3C DID Core 1.0 | https://www.w3.org/TR/did-core/ | | W3C Multikey | https://www.w3.org/TR/controller-document/#multikey | | RFC 2119 (Key Words) | https://www.rfc-editor.org/rfc/rfc2119 | | RFC 7515 (JWS) | https://www.rfc-editor.org/rfc/rfc7515 | | RFC 8032 (Ed25519) | https://www.rfc-editor.org/rfc/rfc8032 | | DFOS Protocol Specification | https://protocol.dfos.com/spec | ### 9.2 Informative References | Reference | URI | | ----------------------- | ------------------------------------------- | | W3C DID Spec Registries | https://w3c.github.io/did-spec-registries/ | | Multicodec Table | https://github.com/multiformats/multicodec | | CIDv1 Specification | https://github.com/multiformats/cid | | dag-cbor Codec | https://ipld.io/specs/codecs/dag-cbor/spec/ | --- # DFOS Content Model Standard content schemas for documents committed to DFOS content chains. JSON Schema (draft 2020-12) definitions for content objects committed directly by CID. These schemas are conventions, not protocol requirements. The DFOS Protocol commits to content objects by CID without inspecting their contents — any valid JSON object with a `$schema` field can be committed. The content model defines the vocabulary that DFOS uses internally, provided as a starting point for applications built on the protocol. [Protocol Specification](https://protocol.dfos.com/spec) · [schemas.dfos.com](https://schemas.dfos.com) · [Source](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/schemas) --- ## Schema Convention Content objects are committed directly to a content chain by CID. The CID is derived from the canonical dag-cbor encoding of the content object itself: ``` documentCID = CID(dagCborCanonicalEncode(contentObject)) ``` The protocol requires one thing of the content object: it must include a `$schema` property identifying its content type. ```json { "$schema": "https://schemas.dfos.com/post/v1", "format": "short-post", "body": "Hello world." } ``` Because `$schema` is part of the content object, it is behind the `documentCID` — cryptographically committed in the content chain. Any verifier can resolve the document, read `$schema`, and validate against the schema. Documents are self-describing. --- ## Schema Evolution Schemas are versioned via the URI path (`/post/v1`, `/post/v2`). Evolution rules: - **Strictly additive within a version** — new optional fields can be added to an existing version at any time without breaking existing documents - **Breaking changes require a new version** — removing fields, changing types, or adding new required fields means a new version URI - **Implementations declare which versions they understand** — a registry or application can accept `post/v1` and `post/v2` simultaneously, or only `post/v1` --- ## Standard Schemas Schema files live in [`schemas/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/schemas) in the protocol package. Each is a standalone JSON Schema (draft 2020-12) definition, served at `https://schemas.dfos.com`. ### Post (`https://schemas.dfos.com/post/v1`) The primary content type. Covers short posts, long-form posts, comments, and replies via the `format` discriminator. | Field | Type | Required | Description | | -------------- | -------- | -------- | ---------------------------------------------------------------------------------- | | `$schema` | string | yes | `"https://schemas.dfos.com/post/v1"` | | `format` | enum | yes | `"short-post"`, `"long-post"`, `"comment"`, `"reply"` — immutable, set at creation | | `title` | string | no | Post title (typically for long-post format) | | `body` | string | no | Post body content | | `cover` | media | no | Cover image | | `attachments` | media[] | no | Attached media objects | | `topics` | string[] | no | Topic names (stored as names for portability) | | `createdByDID` | string | no | DID of the content author — distinct from the chain operation signer | `createdByDID` answers "who authored this content", which may differ from the signer of the chain operation (the `kid` DID). For example, an agent acting on behalf of a user commits the operation, but `createdByDID` records the human author. ### Profile (`https://schemas.dfos.com/profile/v1`) The displayable identity for any agent, person, group, or space. | Field | Type | Required | Description | | -------------- | ------ | -------- | --------------------------------------- | | `$schema` | string | yes | `"https://schemas.dfos.com/profile/v1"` | | `name` | string | no | Display name | | `description` | string | no | Short bio or description | | `avatar` | media | no | Avatar image | | `banner` | media | no | Banner image | | `background` | media | no | Background image | | `createdByDID` | string | no | DID of the identity subject | ### Manifest (`https://schemas.dfos.com/manifest/v1`) A semantic index mapping path-like labels to protocol object references. The navigation layer for a DID's content. | Field | Type | Required | Description | | --------- | ------ | -------- | --------------------------------------------------- | | `$schema` | string | yes | `"https://schemas.dfos.com/manifest/v1"` | | `entries` | object | yes | Map of path-like keys to protocol object references | Entry keys: lowercase alphanumeric with dots, underscores, hyphens, forward slashes. 2–128 chars. Must start and end with alphanumeric. Examples: `profile`, `posts`, `drafts/post-1`, `v1.0/release-notes`. Entry values are protocol object references, self-describing by format: - **contentId** (22-char bare hash) — references a living content chain - **DID** (`did:dfos:...`) — references an identity - **CID** (`bafyrei...`) — references a specific immutable document snapshot ```json { "$schema": "https://schemas.dfos.com/manifest/v1", "entries": { "profile": "67t27rzc83v7c22n9t6z7c", "posts": "a4b8c2d3e5f6g7h8i9j0k1", "dark-publisher": "did:dfos:e3vvtck42d4eacdnzvtrn6", "pinned-charter": "bafyreibanjpgcqffcfhr4sptzjfthh5szohhbo5tjfulemkw7uhden5uqy" } } ``` Manifests are content chains — same signing, same verification, same CIDs. A manifest's contentId appears in the DID's content set like any other chain. The semantic index (the document) is dark forest content — requires authorization to read. The operation chain (proof substrate) is public. ### Media Object Several schemas reference media objects. The standard representation: ```json { "id": "media_abc123", "uri": "https://cdn.example.com/media/abc123.jpg" } ``` `id` is required (opaque identifier). `uri` is optional. --- ## Chain Interpretation A content chain is a signed append-only log. The protocol enforces ordering, authorship, and integrity. It does not prescribe what the chain _means_. How an application interprets a content chain depends on the content types committed to it. ### Living Document The chain represents a single evolving thing — a profile, a post, a policy document. Each operation is a **revision**. The resolved state is the latest `documentCID`. History is audit trail. The content _is_ the current version. This is the default interpretation for the standard schemas. Edit lineage is tracked via `baseDocumentCID` on the content operation payload — each new operation can reference the document CID it replaced. ### Stream The chain represents a sequence — a feed, a journal, a log. Each operation is a discrete emission, not a revision. There is no single "current state" — the chain _is_ the content. Previous documents aren't superseded, they're siblings in a series. ### Other Patterns The protocol cannot distinguish these patterns — the operation schema is identical in both cases. The difference is a reading convention, signaled by the `$schema` of the documents. Future content types could define event-sourcing patterns, append-only collections, or interpretations not yet imagined. --- ## Custom Schemas 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. Custom schema URIs should use a namespace you control (e.g., `https://schemas.example.com/my-type/v1`) to avoid collisions with the standard library. --- # DFOS Web Relay An HTTP relay for the DFOS protocol — receives, verifies, stores, and serves identity chains, content chains, artifacts, beacons, countersignatures, and content blobs. This spec is under active review. Discuss it in the [clear.txt](https://clear.dfos.com) space on DFOS. [Source](https://github.com/metalabel/dfos/tree/main/packages/dfos-web-relay) · [npm](https://www.npmjs.com/package/@metalabel/dfos-web-relay) · [Protocol](https://protocol.dfos.com) --- ## Philosophy The DFOS protocol defines signed chain primitives — identity and content chains, beacons, credentials — but says nothing about transport. A web relay is the HTTP layer that carries these primitives between participants. Relays are not authorities. They verify what they receive and serve what they've verified, but they don't issue identity, grant permissions, or define content semantics. Any relay implementing the same verification rules and given the same operations produces the same deterministic head state. Clients can replicate their data across multiple relays without coordination. A relay is a library, not a service. `createRelay()` returns a portable Hono application that any runtime can host — Node.js, Cloudflare Workers, Deno, Bun, a Docker container, a Raspberry Pi. The consumer provides a storage backend, peer configuration, and optional capabilities. The relay handles verification, peering, and HTTP semantics. --- ## Two Planes The relay serves two distinct planes of data with different access models: ### Proof Plane (public) Signed chain operations, artifacts, beacons, and countersignatures. These are cryptographic proofs — anyone can verify them with a public key. The proof plane gossips freely: relays push operations to peers, peers verify and store independently. All proof plane routes are unauthenticated. The operations themselves carry their own authentication (Ed25519 signatures). ### Content Plane (private) Raw content blobs — the actual documents that content chains commit to via `documentCID`. The content plane never gossips. Blobs are stored by the relay that received them and served only to authorized readers. Content plane access requires two credentials: - **Auth token**: A DID-signed JWT proving the caller controls an identity (AuthN) - **Read credential** (for non-creators): A `DFOSContentRead` VC-JWT issued by the content creator, granting the caller read access (AuthZ) The content creator (the DID that signed the genesis content operation) can always read their own blobs with just an auth token. Content plane support is optional per relay. When disabled (`content: false` in the well-known response), all content plane routes return **501 Not Implemented** — not 404 (resource doesn't exist), but 501 (capability not supported). --- ## Operation Ingestion All proof plane operations enter through a single endpoint: `POST /operations`. The request body is an array of JWS tokens — identity operations, content operations, artifacts, beacons, and countersignatures can be mixed freely in the same batch. ### Classification Each token is classified by its JWS `typ` header: | `typ` header | Classification | | ---------------------- | ------------------------ | | `did:dfos:identity-op` | Identity chain operation | | `did:dfos:content-op` | Content chain operation | | `did:dfos:beacon` | Beacon announcement | | `did:dfos:artifact` | Artifact | | `did:dfos:countersign` | Countersignature | Each operation type has its own `typ` header. Classification is unambiguous — no DID comparison needed. ### Dependency Sort Within a batch, operations are sorted by dependency priority before processing: 1. **Identity operations** — must be processed first so their keys are available 2. **Beacons and artifacts** — reference identity keys for signature verification 3. **Content operations** — reference identity keys, may have chain dependencies 4. **Countersignatures** — reference identity keys and existing operations (target must exist) Within each priority level, genesis operations (no `previousOperationCID`) are processed before extensions. This ensures that a single batch can bootstrap an entire identity-and-content lifecycle — including chained create + update operations — without multiple round trips. ### Verification Each operation is verified against the relay's stored state: - **Identity operations**: Extension operations are verified against the relay's current trusted state using O(1) extension verification — the trusted head state plus the new operation is sufficient. Genesis operations verify the single-operation chain. The relay uses `verifyIdentityChain()` / `verifyIdentityExtensionFromTrustedState()` from the protocol library - **Content operations**: Extension operations are verified against trusted state with `enforceAuthorization: true`. Non-creator signers must include a `DFOSContentWrite` VC-JWT. The relay uses `verifyContentChain()` / `verifyContentExtensionFromTrustedState()` from the protocol library - **Artifacts**: Signature is verified against the signing DID's current identity state. CID integrity is checked. Payload must conform to the declared `$schema`. CBOR-encoded payload must not exceed 16384 bytes - **Beacons**: Signature, CID integrity, and clock skew are verified. Replace-on-newer: only the most recent beacon per DID is retained - **Countersignatures**: Two-phase verification. Protocol-level (stateless): signature, CID integrity, payload schema. Relay-level (stateful): target CID must exist in the relay, witness DID must differ from the target's author DID, one countersign per witness per target ### Chain Resolution The relay must route each incoming operation to its chain. Resolution differs by type: - **Identity genesis**: No prior chain — the relay verifies the single-operation chain and creates a new `StoredIdentityChain` keyed by the new DID - **Identity extension**: The `kid` in the JWS header is a DID URL (`did:dfos:#`). The relay extracts the DID prefix (before `#`) and looks up the existing chain. A `kid` without `#` on a non-genesis operation is rejected — it cannot be routed - **Content genesis**: No prior chain — creates a new `StoredContentChain` keyed by the content ID derived from verification - **Content extension**: The `previousOperationCID` payload field is used to look up a `StoredOperation`, which carries the `chainId`. The relay then fetches the content chain by that `chainId`. If the previous operation doesn't exist or isn't a content operation, the extension is rejected - **Countersignatures**: The `targetCID` payload field is used to look up the target operation. The target's author DID is resolved from the stored operation to enforce the witness ≠ author rule This is relay-level machinery — the protocol library verifies chain integrity, but the relay decides how to locate the chain a given operation belongs to. ### Fork Acceptance Forks are accepted. If an incoming operation's `previousOperationCID` references any operation in the chain (not just the current head), the relay verifies the extension against the chain state at that fork point and accepts it. The chain log accumulates all branches. **Deterministic head selection**: after accepting a fork, the relay recomputes the head — highest `createdAt` among tips, lexicographic highest CID as tiebreaker. This is deterministic across relays given the same set of operations, regardless of ingestion order. As forks propagate via peering, all relays converge to the same head. **State at fork point**: to verify a fork extension, the relay computes chain state at the parent CID. The Store interface abstracts this via `getIdentityStateAtCID` / `getContentStateAtCID` — implementations choose the strategy (full replay, snapshot-backed, etc.). **Undeletion**: falls naturally from the fork model. An identity holder can fork from before a delete with a higher `createdAt`. The fork becomes the head. The delete remains visible in the log (auditable, gossiped) but is on a non-head branch. **Future timestamp guard**: Identity and content operations with a `createdAt` more than 24 hours in the future are rejected. Since head selection favors the highest timestamp, a far-future `createdAt` would permanently dominate head selection — a temporal denial-of-service. The 24-hour window accommodates clock drift while preventing abuse. Beacons enforce a stricter 5-minute bound at the protocol level. ### Ingestion Statuses Three distinct outcomes from ingestion: | Status | Meaning | | ----------- | ------------------------------------------------ | | `new` | First time seen, verified, stored, state changed | | `duplicate` | Already had it, no state change (see note below) | | `rejected` | Verification failed | For chain operations, `duplicate` means the exact same CID and JWS token was already stored — a true idempotent resubmission. Submissions with the same CID but a different JWS token are rejected — since Ed25519 is deterministic, a different token for the same payload means a different signing key. For beacons, `duplicate` means "no state change resulted" — a beacon with `createdAt` less than or equal to the stored beacon's `createdAt` returns `duplicate` even if the CID differs. This is replace-on-newer semantics: only a strictly newer beacon updates the stored state. Duplicate countersignatures (same witness DID, same target CID) MUST be deduplicated — one countersign per witness per target. The relay MUST NOT store multiple attestations from the same witness for the same target. Resubmission SHOULD return `duplicate` (idempotent). ### Deletion Semantics Deletion means the identity stops being an active participant. Historical operations remain verifiable — keys persist in state for signature verification — but no new acts flow from a deleted identity. Specifically: - **Identity operations after deletion**: Rejected. A deleted identity chain is sealed. - **Content operations after deletion**: Rejected. Both paths are checked: (a) the signer's identity is deleted — no operations from that DID are accepted, and (b) the content chain's creator identity is deleted — the chain is sealed regardless of who signs. - **Beacons from deleted identities**: Rejected. A deleted identity MUST NOT publish new beacons. - **Artifacts from deleted identities**: Rejected. A deleted identity MUST NOT publish new artifacts. - **Credentials from deleted issuers**: Rejected. Identity deletion revokes all authority, including outstanding `DFOSContentRead` and `DFOSContentWrite` credentials issued by the deleted identity. Credentials that were valid at time of issuance cease to be honored once the issuer is deleted. - **Countersignatures from deleted witnesses**: Rejected. A deleted identity MUST NOT publish new countersignatures. Countersignatures on operations by deleted authors are still accepted — deletion of the target's author does not prevent other identities from attesting. Self-countersignatures — where the witness DID matches the target's author DID — are rejected at the relay level. A countersignature's semantic is "a distinct witness attests." The protocol-level verifier is stateless and does not enforce this; the relay resolves the target's author and rejects self-attestation. ### Result Ordering Ingestion results are returned in the same order as the input `operations` array, regardless of internal processing order. `results[i]` corresponds to `operations[i]`. --- ## Artifacts 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. ### Payload ```json { "version": 1, "type": "artifact", "did": "did:dfos:...", "content": { "$schema": "https://schemas.dfos.com/profile/v1", "name": "My Relay", "description": "A relay for the dark forest" }, "createdAt": "2026-03-25T00:00:00.000Z" } ``` 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). Communities may establish conventions for well-known schemas. ### Constraints - **JWS `typ` header**: `did:dfos:artifact` - **Max payload size**: 16384 bytes CBOR-encoded. This is a protocol constant — not configurable per relay - **Immutability**: Once ingested, an artifact is never updated or replaced. To "update" an artifact's content, publish a new artifact - **CID-addressable**: Each artifact is addressed by the CID of its CBOR-encoded payload ### Verification 1. JWS signature verification against the signing DID's current key state 2. CID integrity — the payload CID matches the computed CID from the raw payload bytes 3. Payload schema validation — the payload conforms to the artifact structure (`version`, `type`, `did`, `content` with `$schema`, `createdAt`) 4. Size limit — CBOR-encoded payload does not exceed 16384 bytes --- ## Countersignatures A countersignature is a standalone witness attestation — a signed statement that references a target operation by CID. Unlike the original operation primitives (which carry the data itself), a countersign is pure attestation: "I, witness W, attest to operation X." ### Payload ```json { "version": 1, "type": "countersign", "did": "did:dfos:witness...", "targetCID": "bafy...", "createdAt": "2026-03-25T00:00:00.000Z" } ``` ### Properties - **JWS `typ` header**: `did:dfos:countersign` - **Own CID**: Each countersign has its own CID, distinct from the target. This avoids the ambiguity of multiple JWS tokens sharing the same CID - **Stateless verification**: Signature + CID integrity + payload schema. No relay state required to verify the cryptographic validity of a countersign - **Composable**: The `targetCID` can reference any CID-addressable operation — content ops, beacons, artifacts, identity ops, even other countersigns - **Immutable**: Once published, a countersign is permanent ### Relay-Level Checks The relay enforces semantic rules beyond cryptographic validity: 1. **Target exists**: The `targetCID` must reference an operation already stored in the relay 2. **Witness ≠ author**: The countersign's `did` (witness) must differ from the target operation's author DID 3. **Deduplication**: One countersign per witness per target. If the same witness submits a second countersign for the same target, the relay accepts idempotently 4. **Deleted witness rejection**: Countersigns from deleted identities are rejected ### Endpoints Two routes serve countersignature data: - **`GET /countersignatures/:cid`** — Primary lookup. Returns all countersignatures for the given CID. Works for any CID-addressable target (operations, beacons, artifacts). Returns `{ cid, countersignatures: string[] }` where each entry is a compact JWS token. Returns 404 if no countersignatures exist for the CID. - **`GET /operations/:cid/countersignatures`** — Operation-scoped lookup. Returns countersignatures only if `:cid` is a known operation. Returns `{ operationCID, countersignatures: string[] }`. Returns 404 if the operation doesn't exist. --- ## Relay Identity Every relay has a DID that resolves on its own proof plane. The relay DID serves as: - **Auth token audience**: Auth tokens are scoped to a specific relay via the JWT `aud` claim, preventing cross-relay token replay - **Peer identity**: When relays gossip proof plane data to each other, the relay DID identifies the peer - **Self-proof anchor**: The relay's identity chain lives in its own store, verifiable by anyone querying the relay ### Relay Profile The relay MUST publish a profile artifact signed by its own DID using the HEAD key state. The profile artifact uses the `https://schemas.dfos.com/profile/v1` schema: ```json { "$schema": "https://schemas.dfos.com/profile/v1", "name": "edge.relay.dfos.com", "description": "Cloudflare edge relay for the DFOS network", "image": { "id": "relay-avatar", "uri": "https://cdn.example.com/avatar.png" }, "operator": "Metalabel", "motd": "Welcome to the dark forest" } ``` All fields are optional except `name`, which SHOULD be present. The `image.uri` field is any valid URI (operator choice — CDN, content-plane reference, or any resolvable URL). The profile JWS token is inlined in the well-known response — self-proving, no extra fetch needed. ### Well-Known Endpoint (`GET /.well-known/dfos-relay`) Returns relay metadata. All fields are required — `profile` is the relay's proof of DID controllership (an artifact JWS signed by the relay DID's controller key). ```json { "did": "did:dfos:edgerelay0000000000000", "protocol": "dfos-web-relay", "version": "0.1.0", "proof": true, "content": false, "log": true, "profile": "eyJhbGciOiJFZERTQSIs..." } ``` | Field | Type | Description | | ---------- | ------- | -------------------------------------------------------------------------- | | `did` | string | The relay's DID, resolvable on this relay's proof plane | | `protocol` | string | Protocol identifier, always `"dfos-web-relay"` | | `version` | string | Relay protocol version (semver) | | `proof` | boolean | MUST be `true`. A relay without proof plane capability is not a relay | | `content` | boolean | Whether the relay supports content plane (blob upload/download) | | `log` | boolean | Whether the global operation log is available (`GET /log`) | | `profile` | string | The relay's profile artifact as a compact JWS token — self-proving payload | `proof: false` is not a valid value. A compliant relay always serves the proof plane. When `log: false`, `GET /log` returns **501 Not Implemented**. Per-chain logs are always available regardless of this setting. --- ## Operation Log The relay maintains a global append-only operation log. Every successfully ingested operation (identity ops, content ops, artifacts, beacons, countersignatures) is appended to the log in ingestion order. ### Global Log (`GET /log?after={cid}&limit=N`) Returns log entries starting after the given CID cursor. ```json { "entries": [ { "cid": "bafy...", "jwsToken": "eyJhbGciOiJFZERTQSIs...", "kind": "identity-op", "chainId": "did:dfos:..." }, { "cid": "bafy...", "jwsToken": "eyJhbGciOiJFZERTQSIs...", "kind": "artifact", "chainId": "did:dfos:..." } ], "cursor": "bafy..." } ``` | Field | Type | Description | | -------------------- | ------------ | -------------------------------------------------------------------------------- | | `entries[].cid` | string | Operation CID | | `entries[].jwsToken` | string | The full compact JWS token — makes the log self-contained for sync | | `entries[].kind` | string | Operation kind: `identity-op`, `content-op`, `beacon`, `artifact`, `countersign` | | `entries[].chainId` | string | DID (identity/beacon/artifact), contentId (content), or targetCID (countersign) | | `cursor` | string\|null | CID to pass as `after` for the next page. `null` means caught up | Parameters: - **`after`** (optional): CID cursor. Omit to start from the beginning of the log - **`limit`** (optional): Max entries to return. Default: 100. Max: 1000 Pagination is forward-only. The log is ordered by ingestion time. JWS tokens are included in every entry because proof-plane JWS payloads are bounded (chain operations and artifacts have finite size), keeping the log self-contained — a syncing peer can replay the log without separate fetches. ### Per-Chain Logs Identity and content chains expose their own log views with the same cursor-based pagination: - `GET /identities/:did/log?after={cid}&limit=N` - `GET /content/:contentId/log?after={cid}&limit=N` Same cursor-based pagination parameters as the global log. Per-chain log entries include `{ cid, jwsToken }` — the chain-specific subset of the global log entry shape. Returns operations belonging to that chain in chain order. --- ## Identity and Content State State endpoints return projected state — the computed result of replaying the chain — without embedding the full operation log. ### Identity State (`GET /identities/:did`) ```json { "did": "did:dfos:abc123...", "headCID": "bafy...", "state": { "did": "did:dfos:abc123...", "isDeleted": false, "authKeys": [...], "assertKeys": [...], "controllerKeys": [...] } } ``` ### Content State (`GET /content/:contentId`) ```json { "contentId": "abc123...", "genesisCID": "bafy...", "headCID": "bafy...", "state": { "contentId": "abc123...", "genesisCID": "bafy...", "headCID": "bafy...", "isDeleted": false, "currentDocumentCID": "bafy...", "length": 1, "creatorDID": "did:dfos:..." } } ``` Chain history is available via the per-chain log routes described above. --- ## Content Plane Access ### Blob Upload (`PUT /content/:contentId/blob/:operationCID`) The upload path mirrors the download path — the operation CID identifies which operation's document is being uploaded. Requirements: - Valid auth token (Bearer header) - The operation CID must reference an operation in this content chain that has a `documentCID` - The authenticated DID must be either the chain creator OR the signer of the referenced operation (enabling delegated uploads) - The uploaded bytes must hash to the operation's `documentCID` (dag-cbor + sha-256 verification) Blobs are stored by `(creatorDID, documentCID)` — always keyed to the chain creator regardless of who uploads. If multiple content chains by the same creator reference the same document, the blob is shared (deduplication). ### Blob Download (`GET /content/:contentId/blob[/:ref]`) Requirements: - Valid auth token (Bearer header) - If the caller is the chain creator: no further credentials needed - If the caller is not the creator: must present a `DFOSContentRead` VC-JWT in the `X-Credential` header, issued by the creator to the caller The optional `:ref` parameter selects which operation's document to return: - `head` (default): the current document at chain head - An operation CID: the document committed by that specific operation --- ## Key Resolution The relay uses two key resolution strategies: - **Historical resolver** (for chain re-verification): searches all keys that have ever appeared in an identity chain's log, including rotated-out keys. This is necessary because re-verifying a full content chain from genesis must resolve keys from operations signed before a key rotation. - **Current-state resolver** (for live authentication): only resolves keys in the identity's current state. After a key rotation, the old key immediately stops working for auth tokens and credentials. This prevents a compromised rotated-out key from being used to authenticate new requests. --- ## Storage Interface The relay delegates persistence to a `RelayStore` interface. Implementations handle how data is stored — the relay handles what to store and when. ```typescript interface RelayStore { getOperation(cid: string): Promise; putOperation(op: StoredOperation): Promise; getIdentityChain(did: string): Promise; putIdentityChain(chain: StoredIdentityChain): Promise; getContentChain(contentId: string): Promise; putContentChain(chain: StoredContentChain): Promise; getBeacon(did: string): Promise; putBeacon(beacon: StoredBeacon): Promise; getBlob(key: BlobKey): Promise; putBlob(key: BlobKey, data: Uint8Array): Promise; getCountersignatures(operationCID: string): Promise; addCountersignature(operationCID: string, jwsToken: string): Promise; appendToLog(entry: LogEntry): Promise; readLog(params: { after?: string; limit: number; }): Promise<{ entries: LogEntry[]; cursor: string | null }>; // chain state at arbitrary CID (fork verification) getIdentityStateAtCID( did: string, cid: string, ): Promise<{ state: VerifiedIdentity; lastCreatedAt: string } | null>; getContentStateAtCID( contentId: string, cid: string, ): Promise<{ state: VerifiedContentChain; lastCreatedAt: string } | null>; // peer sync cursors getPeerCursor(peerUrl: string): Promise; setPeerCursor(peerUrl: string, cursor: string): Promise; } ``` The `getIdentityStateAtCID` / `getContentStateAtCID` methods compute materialized chain state at an arbitrary operation CID. Used by fork verification — the ingestion pipeline needs state at the fork point to verify signer authority and timestamp ordering. Implementations decide how: `MemoryStore` replays from genesis, `SQLiteStore` can use snapshot tables. The package includes `MemoryRelayStore` for development and testing. Production deployments would implement the interface over Postgres, SQLite, D1, or any durable store. --- ## Quick Start ```typescript import { createHttpPeerClient, createRelay, MemoryRelayStore } from '@metalabel/dfos-web-relay'; // JIT mode — generates relay identity + profile artifact at startup const relay = await createRelay({ store: new MemoryRelayStore(), }); // With peering const relay = await createRelay({ store: new MemoryRelayStore(), peers: [{ url: 'https://peer.relay.example.com' }], peerClient: createHttpPeerClient(), }); // Mount on any Hono-compatible runtime export default relay.app; // Schedule sync polling setInterval(() => relay.syncFromPeers(), 30_000); ``` The returned `CreatedRelay` includes `app` (Hono), `did` (string), and `syncFromPeers` (async function). The Hono app exposes: | Method | Path | Plane | Auth | | ------ | ------------------------------------ | ------- | ----------------------- | | `GET` | `/.well-known/dfos-relay` | meta | none | | `POST` | `/operations` | proof | none | | `GET` | `/operations/:cid` | proof | none | | `GET` | `/operations/:cid/countersignatures` | proof | none | | `GET` | `/countersignatures/:cid` | proof | none | | `GET` | `/identities/:did` | proof | none | | `GET` | `/identities/:did/log` | proof | none | | `GET` | `/content/:contentId` | proof | none | | `GET` | `/content/:contentId/log` | proof | none | | `GET` | `/beacons/:did` | proof | none | | `GET` | `/log` | proof | none | | `PUT` | `/content/:contentId/blob/:opCID` | content | auth token | | `GET` | `/content/:contentId/blob[/:ref]` | content | auth token + credential | --- ## Peering Relay-to-relay peering enables data replication across the network. The relay expresses peering intent through a `PeerClient` interface (injected like `Store`) and per-peer configuration flags. ### Three Behaviors | Behavior | Trigger | Mechanism | | ---------------- | ---------------- | ------------------------------------------ | | **Gossip-out** | New op ingested | Push to peers with `gossip: true` | | **Read-through** | Local 404 on GET | Fetch from peers with `readThrough: true` | | **Sync-in** | Scheduled poll | Pull from peers with `sync: true` via /log | Gossip fires on `new` status only — `duplicate` results are not re-gossiped, preventing gossip storms. Read-through applies to **identity chains** and **content chains** only — beacons, operations, and countersignatures are not read-through targets. When triggered, the relay fetches the full chain log from a peer and ingests locally (full verification, no trust). Sync-in uses cursor-based pagination against the peer's global log. ### Peer Configuration ```typescript interface PeerConfig { url: string; gossip?: boolean; // default: true readThrough?: boolean; // default: true sync?: boolean; // default: true } ``` No relay roles or types. Topology is emergent from configuration. A relay with `gossip: true, readThrough: false, sync: false` is a write-only edge node. A relay with `gossip: false, readThrough: true, sync: false` is a read-only cache. ### PeerClient Interface The `PeerClient` is injected like `Store` — semantic per-resource methods, not raw HTTP. The default implementation uses HTTP. Tests inject mocks that route directly to another relay's API in-process. ```typescript interface PeerClient { getIdentityLog( peerUrl: string, did: string, params?: { after?: string; limit?: number }, ): Promise<{ entries: PeerLogEntry[]; cursor: string | null } | null>; getContentLog( peerUrl: string, contentId: string, params?: { after?: string; limit?: number }, ): Promise<{ entries: PeerLogEntry[]; cursor: string | null } | null>; getOperationLog( peerUrl: string, params?: { after?: string; limit?: number }, ): Promise<{ entries: PeerLogEntry[]; cursor: string | null } | null>; submitOperations(peerUrl: string, operations: string[]): Promise; } ``` Each method corresponds to a peering behavior: `getIdentityLog` / `getContentLog` support read-through, `getOperationLog` supports sync-in, and `submitOperations` supports gossip-out. A `PeerLogEntry` is `{ cid: string; jwsToken: string }`. --- ## Convergence The protocol guarantees: given the same set of operations, any relay computes the same deterministic head state. Peering (gossip, read-through, sync) replicates operations across relays. But operations may arrive before their causal dependencies — a content extension before its identity chain, a fork before the branch it forks from. A relay MUST eventually process any structurally valid operation whose causal dependencies have been processed. This is the convergence contract. ### Causal Dependencies An operation's causal dependencies are the minimum state required for verification: | Operation type | Dependencies | | ------------------ | ------------------------------------------------------- | | Identity genesis | None | | Identity extension | Previous identity operation (by `previousOperationCID`) | | Content genesis | Creator's identity chain (for key resolution) | | Content extension | Previous content operation + creator's identity chain | | Beacon | Signer's identity chain | | Artifact | Signer's identity chain | | Countersignature | Signer's identity chain + target operation | If all causal dependencies are present, the operation MUST be verifiable. If any dependency is missing, the operation cannot be verified yet — but it is not invalid. The relay MUST retain it and re-attempt verification when the missing dependency arrives. ### Store-Then-Verify A relay MUST NOT discard a structurally well-formed operation because its dependencies are temporarily unavailable. The implementation strategy is store-then-verify: 1. **Store**: on receipt (via `POST /operations`, gossip, sync, or read-through), store the raw JWS token in a content-addressed buffer keyed by operation CID. This is idempotent — duplicate CIDs are ignored. 2. **Verify**: attempt full verification against current state. Three outcomes: - **Sequenced** — verification succeeded, operation committed to chain state and global log - **Dependency failure** — a causal dependency is missing, operation remains in the buffer - **Permanent rejection** — structurally invalid, bad signature, deleted identity, etc. — will never succeed regardless of what state arrives 3. **Sequence loop**: after each ingestion batch, re-attempt all buffered operations in dependency order until no further progress is made (fixed-point). This ensures cross-batch dependencies resolve immediately — when batch B provides the identity that batch A's content operation was waiting for, the sequencer resolves it within B's response cycle. ### Dependency Failures A rejection is a dependency failure if and only if it is caused by missing state that may arrive later via peering. The set is small and stable: - Previous operation not yet in store (`previousOperationCID` unknown) - Identity chain not yet available (key resolution fails) - Content chain not yet created (genesis not arrived) - Fork state cannot be computed (ancestor in branch path not yet available) All other rejections are permanent. Permanent rejections MUST NOT be retried. ### Serialization All chain-state mutations (ingestion + sequencing) MUST be serialized. Concurrent ingestion of operations for the same chain produces a read-modify-write race: two goroutines read the chain log, both append their operation, and the second write clobbers the first. Raw operation storage (`putRawOp`) does not require serialization — it is idempotent and append-only. ### Convergence Bound Given a fully connected peer mesh where every relay syncs from every other relay: - After one sync cycle, every relay has every operation that any peer accepted (stored as raw) - After one sequencer pass, every operation whose full dependency chain exists locally is sequenced - Deterministic head selection ensures all relays agree on the canonical head In practice, the dependency depth for fork operations is 1 (each op depends on its immediate predecessor). Convergence is typically achieved in a single sync + sequence cycle. ### Storage Interface (Convergence) The `RelayStore` interface extends with methods for raw operation buffering: ```typescript // raw ops — content-addressed store for all received operations putRawOp(cid: string, jwsToken: string): Promise; getUnsequencedOps(limit: number): Promise; markOpsSequenced(cids: string[]): Promise; markOpRejected(cid: string, reason: string): Promise; countUnsequenced(): Promise; resetSequencer(): Promise; ``` --- ## What's Deferred - **Peer discovery**: Static configuration only — no dynamic discovery - **SSE/realtime push**: Polling `GET /log` for now, SSE in the future - **Fork visibility API**: Dedicated endpoint to list tips/branches - **Branch termination op**: Protocol-level operation to explicitly kill fork branches - **Rate limiting / anti-spam**: Operational concern, not protocol concern - **Blob size limits**: No enforcement yet — production deployments should add limits at the middleware layer - **Artifact `$schema` registry**: Schema names are free-form strings for now — no formal registry or validation beyond structural checks --- # DFOS CLI A command-line interface for the DFOS protocol — manages identities, signs operations, interacts with relays, and stores chains locally. The sovereign actor in the DFOS architecture. This spec is under active review. Discuss it in the [clear.txt](https://clear.dfos.com) space on DFOS. [Source](https://github.com/metalabel/dfos/tree/main/packages/dfos-cli) · [Protocol](https://protocol.dfos.com) --- ## Install ### One-liner (Linux / macOS) ```bash curl -sSL https://protocol.dfos.com/install.sh | sh ``` ### Homebrew (macOS) ```bash brew install metalabel/tap/dfos ``` ### Container ```bash docker pull ghcr.io/metalabel/dfos:latest ``` ### Windows Download the latest release from [GitHub Releases](https://github.com/metalabel/dfos/releases/latest). Extract the zip and add `dfos.exe` to your PATH. ### From source ```bash cd packages/dfos-cli && make build ``` --- ## Quickstart ```bash # create your identity dfos identity create --name myname # publish your first post echo '{"body":"gm"}' | dfos content create - # see it dfos content list # run a relay dfos serve ``` --- ## Philosophy The DFOS protocol defines signed chain primitives — identity and content chains, beacons, credentials — but says nothing about how a user manages keys or communicates with relays. The CLI is the user-side agent that bridges this gap. Relays are dumb pipes that verify and store. The CLI is the sovereign actor: it generates keys, signs operations, decides what to publish and when, and independently verifies what relays serve back. Private key material never leaves the local machine. The CLI is designed for both human operators and AI agents. Every command that produces output supports `--json` for structured machine-readable responses. Every interactive prompt has a flag equivalent. Stdin is accepted wherever a file is expected. --- ## Architecture ``` ┌──────────────────────────┐ │ OS Keychain │ Ed25519 private key seeds │ (never on disk) │ macOS Keychain / Linux secret-service / Windows Credential Manager └──────────┬───────────────┘ │ ┌──────────▼───────────────┐ │ ~/.dfos/ │ Configuration + local relay │ ├── config.toml │ Relays, identities, contexts, defaults │ └── relay.db │ SQLite — chains, operations, blobs └──────────┬───────────────┘ │ ┌──────────▼───────────────┐ │ Relays (HTTP) │ Verify, store, serve │ relay.dfos.com │ Relays are peers, not authorities │ localhost:4444 │ └──────────────────────────┘ ``` The CLI embeds a full relay locally — the same SQLite-backed relay that runs as a network service via `dfos serve`. Every CLI command reads and writes to this local relay. Running `dfos serve` exposes it over HTTP with peer sync, gossip, and read-through. The CLI has three layers of state: - **OS Keychain**: private key material only. One entry per Ed25519 key, keyed by `dfos` service + `did:dfos:xxx#key_yyy` account. Hex-encoded 32-byte seed. Never written to disk. - **Local relay** (`~/.dfos/relay.db`): SQLite database storing identity chains, content chains, operations, beacons, countersignatures, and blobs. Both chains you own (have private keys for) and chains you've fetched from relays. - **Config** (`~/.dfos/config.toml`): relay URLs, identity names, active context, defaults. --- ## Context Model A **context** is a (named-identity, named-relay) pair. Contexts determine which identity signs operations and which relay receives them. ### Configuration ```toml active_context = "alice@local" [relays.local] url = "http://localhost:4444" [relays.prod] url = "https://relay.dfos.com" [identities.alice] did = "did:dfos:f2r3vt89fnh9ntkk7neffe" [identities.bob] did = "did:dfos:a8c4rr6d29t2zehn2tc9hv" [defaults] auth_token_ttl = "5m" credential_ttl = "24h" ``` Contexts are implicit: `alice@local` resolves to identity "alice" + relay "local" without needing an explicit `[contexts]` section. Named contexts can be defined for non-obvious names. ### Resolution Precedence Every command resolves its active (identity, relay) pair via: ``` --ctx flag → DFOS_CONTEXT env → active_context in config → error --identity → DFOS_IDENTITY env → from resolved context --relay → DFOS_RELAY env → from resolved context → (optional for local-only ops) ``` The `@` syntax is shorthand: `alice@local` = identity "alice" + relay "local". If both the identity and relay names exist in config, the context resolves without pre-registration. --- ## Key Management ### OS Keychain Integration One keychain entry per Ed25519 key: | Field | Value | | ------- | -------------------------------- | | Service | `dfos` | | Account | `did:dfos:xxx#key_yyy` | | Secret | hex-encoded 32-byte Ed25519 seed | During identity genesis (before the DID is known), keys are stored under a temporary account (`pending:`) and renamed after the DID is derived from the genesis CID. The CLI discovers which keys belong to which identity by querying the identity's chain state (from local store or relay) and checking which keys have private material in the keychain. ### In-Memory Mode `DFOS_NO_KEYCHAIN=1` switches to in-memory key storage. Keys exist only in the current process and are lost on exit. Designed for CI, testing, and environments without an OS keychain daemon. ### Security Properties - Private keys exist in memory only during signing operations - No key material is ever written to the filesystem - `identity keys` shows keychain presence/absence, never key material - After key rotation, old keys remain in the keychain (needed for historical chain re-verification) but are no longer used for new operations --- ## Local-First Workflow The default mode is local. Operations are signed and stored in `~/.dfos/store/` without network access. Publishing to relays is explicit. ### Create-Then-Publish ```bash # create identity (local only) dfos identity create --name alice # → keys stored in keychain, genesis stored in ~/.dfos/relay.db # create content (local only) dfos content create post.json # → blob and chain stored in ~/.dfos/relay.db # publish when ready dfos identity publish alice --relay local dfos content publish --relay local ``` ### Direct-to-Relay If `--relay` is present on create commands, the CLI creates and publishes in one step: ```bash dfos identity create --name alice --relay local dfos content create post.json --relay local ``` ### Smart Dependency Resolution If you create content with `--relay` but the identity hasn't been published to that relay, the CLI detects the dependency and either prompts or auto-publishes (with `--yes`). --- ## Local Relay The CLI stores all chain data in a SQLite database at `~/.dfos/relay.db`. This is the same relay implementation that powers network relays via `dfos serve` — the CLI just runs it embedded, without HTTP. Identity chains, content chains, operations, beacons, countersignatures, and blobs all live in this single database. Local metadata (identity names, publish state) is tracked in `config.toml`. ### Fetching Remote Chains The CLI can download and store any chain from any relay, without owning the private keys: ```bash dfos identity fetch did:dfos:xxx --relay prod --name carol dfos content fetch abc123 --relay prod ``` Fetched identities appear in `identity list` with `KEYS 0/N` — visible public keys but no private material in the keychain. This enables local verification, credential checking, and countersigning against remote identities. --- ## Content Create Content creation accepts any JSON document. The CLI enforces one convention: documents should have a `$schema` field pointing to a content model schema. ```bash # from file dfos content create post.json # from stdin echo '{"$schema":"...","body":"hello"}' | dfos content create - # from heredoc dfos content create - <<'EOF' {"$schema":"https://schemas.dfos.com/post/v1","format":"short-post","body":"hello"} EOF # with operation note dfos content create post.json --note "initial draft" ``` If the document has no `$schema` field, the CLI warns but proceeds. The relay is document-agnostic — schema enforcement is a client-side convention, not a protocol rule. --- ## Credentials The CLI issues VC-JWT credentials for content access control: ```bash # grant read access dfos content grant --read # grant write access (allows extending the content chain) dfos content grant --write # with custom TTL dfos content grant --read --ttl 1h ``` Credentials are printed to stdout (or as JSON with `--json`). The recipient passes them to relay endpoints via the `X-Credential` header, or to the CLI via `--credential`: ```bash dfos content download --credential --relay local ``` Credential transport is out-of-band — the CLI mints and consumes them, but doesn't transmit them between parties. --- ## Verification `content verify` re-verifies a chain's integrity locally — re-derives all CIDs, re-checks all Ed25519 signatures, and optionally verifies blob integrity. Zero trust in the relay. ```bash dfos content verify ``` This catches relay corruption, data tampering, and implementation bugs (including the CBOR number encoding trap — see PROTOCOL.md § Number Encoding). --- ## Raw API Access `dfos api` is the escape hatch for agents and power users — raw HTTP to the relay with automatic auth token injection: ```bash # unauthenticated dfos api GET /.well-known/dfos-relay dfos api GET /identities/did:dfos:xxx # with auto auth (mints a fresh JWT, injects Authorization header) dfos api GET /content/abc123/blob --auth # POST with body dfos api POST /operations --body '{"operations":["eyJ..."]}' # custom headers dfos api PUT /content/abc123/blob --auth -H "X-Document-CID: bafyrei..." --body-file doc.bin # response headers dfos api GET /identities/did:dfos:xxx -i ``` The `--auth` flag resolves the active identity, loads the auth key from the keychain, fetches the relay's DID from well-known, mints a short-lived JWT, and injects it. One flag replaces the entire auth token lifecycle. --- ## Environment Variables | Variable | Purpose | | ---------------------- | ------------------------------------------------- | | `DFOS_CONTEXT` | Override active context (`identity@relay`) | | `DFOS_IDENTITY` | Override active identity name | | `DFOS_RELAY` | Override active relay name | | `DFOS_CONFIG` | Config file path (default: `~/.dfos/config.toml`) | | `DFOS_NO_KEYCHAIN` | In-memory keys only (CI/testing) | | `DFOS_NO_UPDATE_CHECK` | Disable automatic version update checks | | `DFOS_DEBUG` | Debug logging (HTTP traffic, key resolution) | --- ## Commands | Method | Command | Description | | ------ | -------------------------------- | ------------------------------------------- | | `GET` | `identity list` | List all known identities (owned + fetched) | | `GET` | `identity show [name\|did]` | Show identity state | | `GET` | `identity keys [name\|did]` | Show key state + keychain availability | | `POST` | `identity create --name` | Generate keys + sign genesis | | `POST` | `identity update` | Rotate keys (sign update operation) | | `POST` | `identity delete` | Permanently delete identity | | `POST` | `identity publish [name]` | Submit identity chain to a relay | | `GET` | `identity fetch ` | Download identity chain from relay | | `GET` | `content show ` | Show content chain state | | `GET` | `content log ` | Show operation history | | `GET` | `content download ` | Download blob (stdout or file) | | `POST` | `content create ` | Create content chain | | `POST` | `content update ` | Update content chain (supports delegation) | | `POST` | `content delete ` | Permanently delete content chain | | `POST` | `content publish ` | Submit content chain + blob to a relay | | `GET` | `content fetch ` | Download content chain from relay | | `POST` | `content grant ` | Issue read/write credential | | `GET` | `content verify ` | Re-verify chain integrity locally | | `GET` | `beacon show [did\|name]` | Show latest beacon | | `POST` | `beacon announce ` | Build merkle root, sign, submit | | `POST` | `beacon countersign ` | Countersign someone's beacon | | `POST` | `witness ` | Countersign an operation | | `GET` | `countersigs ` | Show countersignatures for operation/beacon | | `GET` | `auth token` | Mint short-lived auth token (stdout) | | `GET` | `auth status` | Show current auth state | | `*` | `api ` | Raw HTTP to relay with optional `--auth` | | `GET` | `relay list` | List configured relays | | `GET` | `relay info [name]` | Show relay metadata | | `POST` | `relay add ` | Register a named relay | | `DEL` | `relay remove ` | Unregister a relay | | `SET` | `use ` | Set active context | | `GET` | `config list` | Show full configuration | | `GET` | `status` | At-a-glance overview | --- ## What's Deferred - **Schema validation**: validate documents against bundled JSON schemas (currently warns on missing `$schema` only) - **Key backup/recovery**: mnemonic seed phrases or encrypted export - **Shell completion**: cobra generates these, needs testing and docs - **Batch refresh** (`identity fetch --all`): re-fetch all tracked remote identities --- # Frequently Asked Questions ## What is the DFOS Protocol? A specification for cryptographic identity and content proof. It defines how Ed25519 signed chains, content-addressed CIDs, and W3C DIDs work together to create verifiable identity and content — independent of any particular platform, infrastructure, or trust assumption. Chains are directed acyclic graphs (DAGs) that converge deterministically across implementations without consensus. ## What problem does it solve? Platform identity is platform-controlled. If a service shuts down or locks your account, your identity and content history disappear with it. The DFOS Protocol makes identity and content provenance self-sovereign — derived from cryptographic keys you control, verifiable by anyone with your public key and any standard EdDSA library. ## What does "dark forest" mean in this context? The internet is a dark forest — most meaningful creative and social activity happens in private spaces, not on the public web. DFOS is designed for this reality. Content lives in private, member-governed spaces. The cryptographic proof layer is the only public surface: signed commitments that anyone can verify, without revealing the content itself. The proof is public. The content is private. ## How do chains handle forks and conflicts? Chains are DAGs, not linear sequences. Forks are valid — two operations referencing the same predecessor both get accepted. All implementations converge to the same head via a deterministic rule: highest createdAt timestamp among tips, with lexicographic CID as tiebreaker. Given the same set of operations, any relay computes the same head regardless of ingestion order. This is convergence without consensus — no coordination protocol, no leader election, no global ordering. ## How does the relay network work? Web relays are verifying HTTP endpoints that store and serve chains. Every relay independently verifies every operation on ingestion — there is no trust relationship between relays. Three peering behaviors compose to form the network: gossip (push new operations to peers), read-through (fetch from peers on cache miss), and sync (periodic pull via cursor-based polling). There are no relay roles or hierarchy. Topology is emergent from per-peer configuration. ## Do I need to run a server or connect to a network? No. Verification is offline and self-contained. A signed chain carries everything needed to verify it — public keys, signatures, content-addressed hashes. There is no registry to query, no blockchain to sync, no API to call. Given a chain and a public key, any standard Ed25519 library in any language can verify it. Relays are useful for storage and distribution, but verification never depends on them. ## What languages are supported? The reference implementation is in TypeScript (available as @metalabel/dfos-protocol on npm). Cross-language verification implementations exist in Go, Python, Rust, and Swift — all verifying the same deterministic test vectors from the protocol specification. The CLI is written in Go with pre-built binaries for Linux, macOS, and Windows — installable via Homebrew, curl, or Docker. ## How is this different from blockchain-based identity? Blockchain identity systems anchor trust in a shared ledger — you need to sync with or query the chain to verify identity. The DFOS Protocol anchors trust in cryptographic signatures alone. There is no consensus layer, no gas fees, no chain state to maintain. Verification is a pure function: public key + signed chain = valid or invalid. Forks converge deterministically without coordination. This makes it simpler, faster, and fully transport-agnostic. ## How does this compare to AT Protocol (Bluesky)? AT Protocol and DFOS Protocol share foundations — self-sovereign identity, signed data, content-addressed storage, DIDs — but differ in topology. AT Protocol is public-by-default: your data repository is a public document, posts are visible to the network, and federation relays ingest content openly. The DFOS Protocol inverts this. Content is private — it lives in member-governed spaces, visible only to participants. The cryptographic proof layer is the only public surface. This is an architectural choice, not a privacy setting. AT Protocol is also a full social networking protocol (federation, data repositories, application schemas). The DFOS Protocol is narrower by design — cryptographic primitives only, agnostic to transport, federation, and application semantics. ## How do identity chains relate to DIDs? Every identity chain is also a DID. The DID (did:dfos:) is derived deterministically from the hash of the chain's genesis operation — making it self-certifying. Given the chain, anyone can verify that it belongs to the claimed DID without trusting the source. The DID method specification defines how did:dfos identifiers conform to the W3C DID standard. ## Is the protocol coupled to the DFOS platform? No. The protocol is independent. DFOS (the platform) is one implementation, but any system that implements the same chain primitives produces interoperable, cross-verifiable proofs. An identity created on one system can sign content on another. The protocol is MIT-licensed open source. ## Is this production-ready? The protocol specification is under active review and development. The TypeScript reference implementation is published and tested, with deterministic test vectors verified across five languages. The CLI ships pre-built binaries for 6 platforms via Homebrew, Docker, and direct download. The DFOS platform runs on this protocol in production. The specification has not been submitted to any formal standards body. ## Where can I discuss the protocol? The specification is open source on GitHub (metalabel/dfos). Protocol discussion happens in the clear.txt space on DFOS.