DFOS Protocol
DFOS is an open protocol for identities, signed records, and delegated permissions that any compatible application can verify for itself.
This document specifies the wire: the byte encodings, the identifier derivations, the chain rules, the possession rules, and the verification algorithms that make a DFOS operation the same object in every implementation. It is a wire specification plus the ingest and integration conventions that surround it. Authorship is verifiable without trusting any server. Which view of an identity you follow is a choice of relay.
Scope: the protocol commits to content hashes, not plaintext. It does not encrypt. Confidentiality of the underlying documents is the application layer's, enforced by whoever serves them, and a relay operator can read what it stores.
Overview
| Component | Concern |
|---|---|
| Crypto core | Identity chains and content chains: Ed25519 signatures, JWS tokens, CID links |
| Credentials | Delegated authorization; format in CREDENTIALS.md |
| Services | Identity discovery vocabulary: controller-signed relay locators and stable content anchors |
| Artifacts | Standalone signed inline documents: immutable, CID-addressable structured data |
| Countersignatures | Standalone witness attestation: signed references to any CID-addressable operation |
Documents are flat content objects, content-addressed directly:
documentCID = CID(dagCborCanonicalEncode(contentObject)). What goes inside the
content object is application-defined; the standard schema library is the
DFOS Content Model.
Two chain types
| Identity Chain | Content Chain | |
|---|---|---|
| Commits to | Key sets (embedded) | Documents (by CID reference) |
| Identifier | did:dfos:<hash> |
<hash> (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.
Addressing
Three addressing modes, self-describing by format:
| Thing | Form | Example |
|---|---|---|
| Operation or document | CID (dag-cbor + SHA-256) | bafyrei... (base32lower) |
| Content chain | contentId (31-char hash) | cv7n8vkvr64cctf3294h9k4eanhff8z |
| Identity chain | DID | did:dfos:cnnnft9f8a2rn938d6nkz38r847v2kr |
CIDs address specific immutable artifacts: an exact operation or document.
Content IDs and DIDs address living chains, derived as
customAlpha(SHA-256(genesis CID bytes)). Same derivation for both. Identity
chains prepend did:dfos: (W3C DID spec); content identifiers are bare.
Application code may add prefixes for routing (e.g. post_xxxx). These are
strippable semantic sugar, not part of the protocol identifier.
Encoding
Both operations and documents are content-addressed via CID
(dagCborCanonicalEncode(payload) to SHA-256 to 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: given the same logical payload, the CID MUST be identical regardless of implementation language or platform. JWS uses standard JSON for library interoperability.
Standards
| Component | Standard |
|---|---|
| Key generation | Ed25519 (RFC 8032) |
| 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, 31 characters |
| Timestamp encoding | ECMA-262 Date Time String Format, UTC form: a strict ISO-8601 / RFC 3339 profile, fixed millisecond precision (see Timestamp Grammar) |
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.
The operation CID is derived from the JWS payload (the unsigned operation JSON), never from the JWS token:
operation CID = dagCborCanonicalEncode(operation_payload) → SHA-256 → CIDv1 → base32lower stringNumber encoding
JSON numbers that are mathematically integers (no fractional part) MUST be
encoded as CBOR integers (major type 0/1), never as CBOR floats. This follows
the IPLD data model integer/float distinction
and the dag-cbor codec spec. CBOR
integer 1 encodes as 0x01 and CBOR float 1.0 encodes as 0xf9 0x3c 0x00:
the same logical value, a different CID.
Languages that decode JSON into untyped maps (Go's map[string]any, Python's
dict) typically represent all JSON numbers as floating-point. Implementations
MUST normalize number types after JSON deserialization and before CBOR encoding.
Number bounds (normative). A canonicalizable number MUST be an integer in the
range [-(2^53 - 1), 2^53 - 1] (JSON's safe-integer range). Implementations MUST
reject, at CID derivation and before CBOR encoding, any payload containing a
non-integer number, NaN, ±Infinity, or an integer outside that range.
Applications that need fractional or larger-magnitude values MUST encode them as
strings. This single form eliminates both the shortest-float divergence and the
integer-versus-float64 split above 2^53.
Verification test vector, encoding {"version": 1, "type": "test"}:
Integer encoding (CORRECT):
CBOR: a2647479706564746573746776657273696f6e01
CID: bafyreihp6omsp6icc6ee63ox2ovsaxm6s7ikd2a7k5eh2qz2qd5soh5bsa
Float encoding (WRONG — different bytes, different CID):
CBOR: a2647479706564746573746776657273696f6ef93c00
CID: bafyreiawbms4476m5jlrmqtyvtwe5ta3eo2bh7mdprtomfgfype7j57o4qThe byte at offset 19 in the CBOR output is the discriminator: 0x01 is a CBOR
integer, 0xf9 is a CBOR float16 header.
String encoding
String values are committed as their exact UTF-8 byte sequence. Implementations MUST NOT apply Unicode normalization (NFC, NFD, NFKC, NFKD) or any other transformation to string values before dag-cbor encoding or signing. The CID and the signature commit to the bytes as received, so two strings that are Unicode-equivalent but byte-distinct are different protocol values.
JSON payload canonicalization
The signed JWS payload is decoded as JSON, then re-encoded as dag-cbor for CID derivation. Producers MUST emit canonical JSON: object keys unique within each object, no duplicate object keys. Whitespace and key order in the source JSON do not affect the CID, because dag-cbor re-encodes from the decoded value. A payload containing duplicate keys is malformed: the signature commits to the raw payload bytes while the CID derives from the decoded value.
Decoder recursion depth
A node SHOULD apply a decoder recursion-depth guard when canonicalizing or encoding a payload. Both reference implementations cap nesting at 1024 levels. This is a local resource guard, not a chain-validity rule. Nodes MAY apply stricter local ingress limits (max bytes decoded off the wire, rate limits) provided they never accept an operation the validity rules reject, nor reject one they accept.
ID alphabet
Alphabet: 2346789acdefhknrtvz (19 characters)
Length: 31 characters
Entropy: ~131.6 bits (19^31)Process: SHA-256(input) → for each of first 31 bytes: alphabet[byte % 19]. The
modulo introduces a ~0.3% bias (256 is not evenly divisible by 19), which is not
security-relevant for identifiers.
DIDs are did:dfos: + the 31-char ID derived from SHA-256(genesis CID raw bytes):
DID = "did:dfos:" + idEncode(SHA-256(genesis_CID_raw_bytes))There is a single canonical identifier width. Verifiers MUST reject any
did:dfos: identifier that is not exactly 31 characters over this alphabet,
whether it appears in an operation's signing-key kid, in an operation payload,
or as the DID of a resolved identity state.
Key IDs are key_ + a 31-char ID. Convention: derive from the key,
key_ + customAlpha(SHA-256(publicKeyMultibase)), where the input is the
multibase string a chain carries, not the raw key bytes. This is not a
protocol requirement; key IDs can be any string.
DID derivation (worked example)
Input: CID bytes (hex) = 017112204e31ea9cb6ab4516ebdd812f7937e61601db07a16afb45723d286906f5181b69
Step 1: SHA-256(CID bytes) = c66d21f27dceea0b05534c225ad7018ac7d4dfded0609dcd18022a3739a5488c
Step 2: Take first 31 bytes: c6 6d 21 f2 7d ce ea 0b 05 53 4c 22 5a d7 01 8a c7 d4 df de d0 60 9d cd 18 02 2a 37 39 a5 48
Step 3: For each byte, alphabet[byte % 19]:
c6=198 → 198%19=8 → 'c'
6d=109 → 109%19=14 → 'n'
21=33 → 33%19=14 → 'n'
f2=242 → 242%19=14 → 'n'
...
Result: cnnnft9f8a2rn938d6nkz38r847v2kr
DID: did:dfos:cnnnft9f8a2rn938d6nkz38r847v2krMultikey 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 keyWorked example:
Public key (hex): ba421e272fad4f941c221e47f87d9253bdc04f7d4ad2625ae667ab9f0688ce32
Prefix + key (hex): ed01 ba421e272fad4f941c221e47f87d9253bdc04f7d4ad2625ae667ab9f0688ce32
Base58btc + 'z': z6MkrzLMNwoJSV4P3YccWcbtk8vd9LtgMKnLeaDLUqLuASjb[0xed, 0x01] is the unsigned varint encoding of 237 (0xed). Since
0xed > 0x7f it requires two bytes in varint format: 0xed (low 7 bits plus
continuation bit) then 0x01 (high bits). This is NOT big-endian [0x00, 0xed].
Timestamp grammar
Every operation's createdAt field MUST match exactly the grammar:
YYYY-MM-DDTHH:MM:SS.sssZThat is, in order, with no other characters and no surrounding or internal whitespace:
- a 4-digit zero-padded calendar year (
0000–9999), - a literal
-, a 2-digit zero-padded month (01–12), - a literal
-, a 2-digit zero-padded day of month, - a literal uppercase
Tdate/time separator, - a 2-digit zero-padded hour (
00–23),:, a 2-digit minute (00–59),:, a 2-digit second (00–59), - a literal
.followed by exactly three decimal digits of fractional seconds (millisecond precision, no more, no fewer), - a literal uppercase
Zdesignating UTC.
The value MUST be a real calendar instant: the month/day combination MUST be
valid (leap years are honored, so 2024-02-29 is valid and 2023-02-29 is not).
A timezone offset (e.g. +00:00, -05:00) MUST NOT appear; only the literal
Z is permitted. Leap seconds (:60) MUST be rejected. A lowercase z, a
missing or differently-sized fractional part, a space in place of T,
non-zero-padded fields, or any leading, trailing, or embedded whitespace MUST be
rejected.
A verifier MUST reject any operation whose createdAt does not match this
grammar. This applies on the read/verify path, not only at write time.
The grammar is ECMA-262 §21.4.1.32's Date Time String Format in its UTC form, a strict profile of ISO 8601 / RFC 3339 that excludes the offset forms, week dates, ordinal dates, and comma separators those admit.
The grammar is a validity rule, not a formatting suggestion: ordering and head
selection compare createdAt as raw strings, and only a fixed-width zero-padded
grammar makes byte order identical to chronological order.
Chains
Chain validity
A valid chain is a sequence of operations rooted at a genesis. Each operation
after genesis links to a predecessor via previousOperationCID. The chain
provides structural ordering independent of timestamps. The two chain kinds
differ in exactly one structural rule: whether that sequence may branch.
Identity chains are linear. Each identity operation has at most one child. An identity chain, as verified, is one sequence: the head is its last operation, and identity state is what that sequence folds to.
Content chains are DAGs. A valid content chain is a directed acyclic graph of
operations. Two content operations referencing the same previousOperationCID
constitute a fork, and both branches are accepted. The chain log stores all
branches. A deterministic head selection rule makes every implementation
holding the same set of operations compute the same head:
- Find all tips: operations with no children.
- Select the tip with the highest
createdAtstring (descending order). - If two or more tips share an identical
createdAt, break the tie by the highest CID multibase string (descending order). Distinct payloads yield distinct CIDs, so this tiebreak is total.
Semantic interpretation of a content-chain fork is application-defined. The protocol stores the DAG; clients interpret it.
Views
An identity chain is linear per view. Two identity operations that share a parent are two views of that identity, not an invalid log: a relay keeps its own log linear by admitting the first successor it sees for a position and refusing later ones, so what each relay serves is one linear chain. Choosing which view of an identity you follow is choosing a relay. On a content chain divergence needs no choice of view, because head selection resolves the same operation set to the same head everywhere.
Comparison basis
Both ordering comparisons, createdAt and the head-selection CID tiebreak, MUST
compare the raw strings byte-wise (Unicode code point, left to right). An
implementation MUST NOT parse createdAt to an epoch, a floating-point value, or
a broken-down time before comparing, and MUST NOT apply a locale-aware or
collation-aware comparison (for example ICU collation or
String.prototype.localeCompare) to either field. The createdAt grammar is
fixed-width and zero-padded and a CID is a base32-lower multibase string, so byte
order is chronological order for the first and byte order for the second.
Timestamp ordering
createdAt MUST be strictly greater than the createdAt of the parent operation
(the operation referenced by previousOperationCID). On an identity chain this
is plain successor ordering. On a content chain it is enforced per-branch: a fork
branch's timestamps are validated against its own parent, not the other branch's
operations.
createdAt is asserted by the signer. What bounds it is per-branch monotonic
ordering plus the future bound below; there is no clock anyone else vouches for.
Future timestamp bound
Relays, and any component that performs deterministic head selection, MUST reject
identity and content operations with a createdAt more than 24 hours in the
future relative to the verifier's clock. Head selection favors the highest createdAt, so an unbounded future timestamp
would otherwise dominate it permanently.
Bare linear chain verification (verifyIdentityChain / verifyContentChain)
does not select a head and does not enforce this bound; it validates only that
each operation's createdAt is strictly greater than its parent's. The reference
relays enforce the 24-hour bound at ingest.
Identity chain signer validity
An identity chain operation is valid only if the signing key was a controller key in the immediately prior declared state (Key possession defines declared versus effective state). Signer validity reads the declared set because it is a structural admission rule every relay must evaluate identically.
A genesis operation declares exactly one key, the same key in all three role
arrays, and is signed by it: the genesis introduces and simultaneously authorizes
its own key, and the signature is that key's possession proof. A genesis
declaring more than one distinct key across its role arrays is invalid.
Identities with several controllers bootstrap the ordinary way: genesis with one
key, then update operations introducing the rest under the possession rule.
The identity chain defines its own valid signers via controllerKeys and the
protocol enforces that. 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:<id>#<keyId>). The verifier calls resolveKey(kid) to obtain the raw
Ed25519 public key bytes for that key on that identity. How the resolver obtains
and validates the identity's key state is application-defined.
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, with no credential. Other DIDs require a DFOS credential
with write access in the operation's authorization field, issued by the
creator DID.
Signer-payload consistency. The kid DID in the JWS header MUST match the
did field in the content operation payload. This discriminates author
operations from countersignatures: if the kid DID differs from the payload
did, it is a countersignature, not a chain operation.
Which key role (auth, assert, controller) a signing key must hold, and any ownership or attribution semantics beyond creator sovereignty, are application concerns. What the protocol itself checks is Content chain verification.
Terminal states
Content chains: delete is the terminal state. No valid content operations
may follow a delete. This is enforced per-branch: a delete seals further linear
extension of its own branch, forks rooted at a pre-delete operation remain valid,
and head selection may make a non-deleted branch the head. Delete prevents future
operations but removes no data; the complete chain remains intact for
verification. Data removal is an application concern.
Identity chains: active ⇄ deleted, every transition an explicit signed
operation. A delete moves the identity to the deleted state and seals the
chain against every operation except one: a restore operation MAY follow
the delete as its immediate linear successor, returning the identity to the
active state. An implementation MUST reject any other operation after a delete.
Both transitions are ordinary operations in the one timeline, permanent and
auditable, never removed. As with content chains, deletion removes no data.
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.
Operation versioning
Every proof-plane operation payload (identity, content, artifact, countersign,
revocation) carries a top-level integer version field. This document specifies
version 1; verifiers MUST reject any operation whose version is not exactly
1. The operation version is distinct from content-document $schema
versioning (see CONTENT-MODEL.md),
which versions application payloads independently.
Size and cardinality limits
The protocol bounds operations with one aggregate size cap plus a small set of cardinality caps, never a per-field string-length table. The aggregate bound is measured over the exact CBOR bytes the CID commits to, so every implementation computes it identically.
| Bound | Value | Applies to |
|---|---|---|
| dag-cbor-encoded operation payload | 65536 bytes (64 KiB) | identity operations, content operations |
Verifiers MUST reject an identity or content operation whose
dagCborCanonicalEncode(payload) exceeds 65536 bytes, measured with any
embedded authorization credential excluded. Credentials are not subject to
this cap; they carry their own 262144-byte (256 KiB) ceiling, because a
maximum-depth delegation chain embeds each parent token in prf and exceeds
64 KiB. Total operation bytes are bounded by the sum (≤ 64 KiB + 256 KiB). The operation CID still commits to the complete payload
including authorization.
The keyProofs member of an identity update gets no such exclusion: its
envelopes are ordinary payload members, inside the bytes the CID commits to and
inside the 64 KiB measurement. Each envelope is separately bounded by the 4096-byte
key-proof envelope cap.
Artifacts keep their own 16384-byte cap; the services array keeps its
32768-byte cap.
Cardinality and field caps. Each row states its own unit: item counts bound structure, and the one field cap is measured in UTF-8 bytes.
| Field | Max | Note |
|---|---|---|
authKeys / assertKeys / controllerKeys |
256 items | Generous ceiling; op-size cap is the real bound |
keyProofs entries |
256 items | Bounds signature checks per operation |
services entries |
256 items | (see Services) |
countersignature relation |
64 bytes | Open-namespace tag (min 1 when present) |
Beyond the caps above, the protocol does not limit field string lengths, document content size (the protocol commits to a CID, not the document, so large binary media is referenced rather than inlined), chain length, or number of chains per identity. These are application and transport concerns.
Time basis
Every verification has a basis time, and every temporal check in this corpus resolves against it.
- For a committed artifact, an operation in a chain and anything carried
inline in it, the basis is the operation's own
createdAt. - For an ephemeral presentation, a request proof, a sign-in challenge, or a read-time credential check, the basis is now.
At the basis:
- the signing key MUST be effective in the identity's state as of the basis;
- an
expMUST be strictly greater than the basis; - no revocation effective as of the basis may cover the artifact.
Two rules stand outside the basis, and both are stated where they live. Issuer
identity deletion is retroactive: while an identity is deleted the credentials it
issued authorize nothing at any point in history, and verification of committed
operations that relied on one rejects
(CREDENTIALS, Deleted issuers).
Admitting a new operation is answered from what a relay currently knows: a relay
refuses an operation authorized by a credential it already holds a revocation
for, whatever that operation's createdAt claims
(RELAY, Revocations and standing credentials).
An identity's state as of a basis time is the state produced by the last
operation in the chain whose createdAt is less than or equal to the basis, with
key memberships read from the effective (proved) state. For an ephemeral
presentation the state is the head of the identity's chain as the relay answering
the request serves it. Admission, not the clock, is what makes an operation
current: an operation admitted with a createdAt in the future, within the
future timestamp bound, is effective from the moment
it is admitted, not from the instant its timestamp names. A basis earlier than
the chain's genesis names no state: the identity did not exist, no key of it was
effective, and an artifact with that basis does not verify.
Credentials have no validity window of their own beyond exp; iat is
informational and is not a rejection gate.
Rotation therefore revokes going forward and leaves history valid: an operation committed while a key was effective still verifies after that key is rotated out, and the same key cannot sign a new ephemeral proof once it is gone.
A credit claim runs no temporal check at all, so it has no basis time. The claimant's key is resolved against every key that claimant's identity chain has ever held. A claim binds a chain rather than a moment, so it survives the claimant's later rotations and the hosting document's later revisions (CONTENT-MODEL, Credits).
The ingest basis is derived from createdAt by converting to integer Unix
seconds:
now_s = floor(createdAt_epoch_ms / 1000)The conversion MUST truncate (floor) the millisecond remainder and MUST NOT
round. A credential's exp is integer Unix seconds (JWT NumericDate), and the
exp boundary is exclusive: a credential MUST be rejected as expired when
exp <= now_s, including the exact instant exp == now_s.
Two relays processing the same committed operation reach the same temporal
verdict regardless of when each one ingests it. A relay MUST NOT add an
ingest-time wall-clock exp check.
Key possession
Every key an identity chain lists is backed by a demonstration that its holder holds it and consented to listing it. The rule is bimodal, with no third case:
- The genesis key proves possession by signing genesis. One key, declared across all three role arrays, signing the very operation that declares it. The signature is the proof, for all three roles at once.
- Every other key proves possession by an embedded key proof. A key's
introduction to a role is accompanied by an envelope the key itself signed,
binding
{chain DID, role set, chain position}.
Introduction. An operation introduces key K to role R when K appears in
the operation's R array and K was not in the immediately prior effective R
state. For each key it introduces, an update operation carries exactly one
envelope in its keyProofs member whose publicKeyMultibase is K, whose did
is the chain's DID, whose prevCID equals the operation's own
previousOperationCID, and whose roleSet includes every role the operation
introduces K to, verified per Chain-walk verification.
An operation that merely replays a key already effective in a role carries no
proof for it: proofs live at introduction, and verification walks back to the
introducing operation. A proof is spent at the position it names, so re-adding a
removed key is a new introduction demanding a fresh envelope, and promotion into
a role the key's envelope never covered is likewise a new introduction. There is
no standing consent and no proof revocation; removal is an ordinary update.
Declared versus effective state. An identity chain yields two readings of its key arrays. The declared state is structural: the arrays as the operations wrote them. The effective state is the declared state minus every unproved membership. A key-role membership whose introduction carries no valid covering envelope is void: excluded from the effective state for that role, never resolved for signature verification by consumers, never indexed, never surfaced in recovery. Void is not invalid, the operation and the chain stand, and tooling MUST surface void memberships rather than silently dropping them.
The three doors. Possession is enforced at three surfaces:
- Writers hard-reject. Software authoring an identity operation MUST refuse to produce an unproved introduction. The void path exists for reading hostile or defective chains, not as something conformant tooling emits.
- Relays sequence regardless. A relay MUST admit and sequence every structurally valid identity operation (linearity, CIDs, timestamps, declared-state signer validity) whatever its proof status. Log membership never depends on semantic key validation. The relay's log answers what was written; proofs answer what counts.
- Verifiers compute void. Every consumer of identity key state (signature resolution for content and API surfaces, key indexes, recovery oracles) reads the effective state.
A chain whose effective controller set becomes empty is extendable in declared form but dead in effect: no key it lists can authenticate, assert, or authorize as the identity. Conformant writers cannot produce this state.
The envelope
A key proof is a compact JWS signed by the candidate key itself. Its payload is a JSON object of exactly seven members. The member set is closed: no free-form member, no content-bearing extension. A key proof proves a key and conveys no intent, content, or authority.
| Member | Description |
|---|---|
nonce |
The verifier-minted, single-use challenge string, taken verbatim from the ceremony that issued it. Opaque to the holder. A verifier SHOULD mint at least 128 bits of entropy. |
audience |
The completing authority. In a hosted ceremony: the presentation endpoint's lowercase authority, bare hostname on the default HTTPS port and host:port on any other, never a scheme and never a path. In the controller-verified leg: the target chain's DID, byte-equal to this payload's own did member. |
did |
The identity chain this key is being introduced to. |
roleSet |
The canonical role-set string naming the roles the key gains. |
prevCID |
The chain head the introduction builds on: the CID the introducing operation carries as its previousOperationCID. |
publicKeyMultibase |
The candidate key: the Multikey multibase string of the key this proof is about, which is also the key that signs it. |
timestamp |
Creation time in the timestamp grammar, floor-normalized to whole seconds (.000Z millisecond component). |
Canonical signing input. The payload is serialized as minimal UTF-8 JSON (no
insignificant whitespace) with members in exactly the order above: nonce,
audience, did, roleSet, prevCID, publicKeyMultibase, timestamp. These
bytes are the JWS payload segment. The rule binds both halves: a signer emits
these bytes, and a verifier recomputes them from the members it parsed and
refuses a payload segment that decodes to anything else.
Envelope size. An envelope over 4096 bytes is rejected before parsing.
The JWS header.
typis exactlydid:dfos:key-add, the one registered key-proof purpose.algis the signature algorithm of the candidate key's Multikey type (ed25519-pub, signed asEdDSA).kidis absent. The candidate key is in no chain, so there is no DID URL to name; the verification key rides in the signed payload. A presentkidrejects.critand embedded key members reject per the Signature verification profile.
Self-proving. The signature verifies against the payload's own
publicKeyMultibase. A valid envelope is possession demonstrated over challenge
bytes.
The roleSet grammar
roleSet names the subset of the chain's three key roles the introduction
covers, as one canonical string: members drawn from exactly auth, assert,
controller; serialized as the subset in the fixed order
auth,assert,controller; comma-joined; no whitespace; no duplicates; never
empty. auth,assert and controller are canonical spellings; assert,auth,
auth, assert, and auth,auth are schema violations.
Position binding
The did, roleSet, and prevCID members bind the proof to one introduction at
one position:
didbinds the proof to one chain. An envelope signed for one identity does not verify for any other.roleSetbinds the proof to the roles gained. A key's membership in a role is proved only by an envelope whoseroleSetincludes that role.prevCIDbinds the proof to one chain position. A removed key cannot be re-added on the strength of an old envelope: the re-adding operation has a differentpreviousOperationCID, so the old proof does not cover it.
The two legs
An introduction is verified by whoever appends it, and audience names that
party in the value domain the flow has. On the hosted ceremony leg a ceremony
operator custodying the chain mints the nonce and audience is that host's
authority, which the verifier byte-compares against its own configured authority,
never against anything request-derived. On the controller-verified leg the
chain's own controller mints the nonce and delivers {did, roleSet, prevCID, nonce} to wherever the candidate key lives, and audience MUST byte-equal the
payload's did. A host authority is never a DID, so the two value domains never
overlap and an envelope signed for one leg cannot verify in the other.
Everything between minting a nonce and adopting the returned envelope is an
integration convention rather than a chain rule, and it is specified in
INTEGRATIONS, Key ceremonies:
the short-code and URI carriage, the /.well-known/dfos-key-proof resolution
endpoint and its JSON, the holder's obligations, and the presentation check a
verifier runs before embedding the bytes. A chain walker re-runs none of it; what
a walker checks is below.
Chain-walk verification
A verifier walking an identity chain checks, for each embedded envelope:
- Header gates.
typMUST bedid:dfos:key-add;algMUST be the algorithm of the payload key's Multikey type;crit, embedded key members, and a presentkidreject. - Payload schema, over canonical bytes. Exactly the closed seven-member
schema, each member a string,
roleSetin the canonical grammar; the walker recomputes the canonical signing input from the parsed members and byte-compares it against the payload octets, and a mismatch rejects. - Key.
publicKeyMultibaseMUST equal the introduced key's multikey. - Chain.
didMUST equal the chain's own DID. - Position.
prevCIDMUST equal the carrying operation'spreviousOperationCID. - Coverage.
roleSetMUST include the role this walk is checking. The walk runs once per introduced key-role membership, so coverage is per role: an envelope covering some of the roles an operation introduces the key to proves those and leaves the rest void. - Signature. The JWS MUST verify against the payload's
publicKeyMultibase.
nonce, audience, and timestamp are presentation-time transport, byte-fixed
under the signature and inert at walk time: a walker re-checks neither freshness
nor audience nor nonce state.
A failed walk-time check makes the introduction void, excluded from the chain's effective key state. It never invalidates the operation or the chain.
Operation schemas
Identity operations
// Genesis — starts the identity chain
{ version: 1, type: "create",
authKeys: MultikeyPublicKey[], // exactly one entry —
assertKeys: MultikeyPublicKey[], // the same single key in all three
controllerKeys: MultikeyPublicKey[], // arrays, which also signs (see Key Possession)
services?: ServiceEntry[], // discovery vocabulary (optional)
createdAt: string } // timestamp grammar — 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
keyProofs?: string[], // one key-proof envelope per key this
// op introduces (see Key Possession)
services?: ServiceEntry[], // full-state — REPLACES the prior set
createdAt: string }
// Deactivation — moves the identity to the deleted state
{ version: 1, type: "delete",
previousOperationCID: string,
createdAt: string }
// Undeletion — only valid as the immediate successor of a delete
{ version: 1, type: "restore",
previousOperationCID: string, // MUST be the CID of a delete operation
createdAt: string }The optional services array is full-state discovery vocabulary projected into
verified identity state (see Services). Omitting it encodes
identically to a service-less operation (CID-neutral); an update carrying it
REPLACES the entire prior set; a delete carries the last set unchanged.
The optional keyProofs array carries the key-proof envelopes for the keys an
update introduces: compact JWS strings, embedded verbatim as
presented, one per introduced key. It is defined on update only; a create,
delete, or restore carrying it is invalid. An operation that introduces no
key carries none. Omitting it encodes identically to an empty introduction
(CID-neutral), the same rule as services.
restore validity (normative). restore is the one operation that may
follow a delete, and the successor-of-delete position is the only position in
which it is valid:
- Its
previousOperationCIDMUST reference adeleteoperation. Arestoreanywhere else in the chain is invalid. - It MUST be signed by a controller key of the deleted head state, the key
state produced by the
deleteit restores. A key rotated out before the delete is not in that state and grants nothing. - Its effect is exactly to clear the deleted state: the identity returns to
active with the keys and services as of the delete, verbatim.
restorecarries no key sets or services of its own; state changes happen via subsequent ordinaryupdateoperations. - Its
createdAtfollows the ordinary rule: strictly greater than its parent's. There is no rate limit and no once-only rule; every transition requires a current controller key, which is total authority over the identity anyway.
A protocol-level irreversible deletion that no controller key can reopen is not part of this specification.
Content operations
// 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, // committed-but-uninterpreted provenance
createdAt: string }
// Content change (null documentCID = clear content)
{ version: 1, type: "update",
did: string, // author DID
previousOperationCID: string,
documentCID: string | null,
baseDocumentCID: string | null, // committed-but-uninterpreted provenance
createdAt: string,
authorization?: string } // DFOS credential for delegated operations
// Permanent destruction
{ version: 1, type: "delete",
did: string, // author DID
previousOperationCID: string,
createdAt: string,
authorization?: string } // DFOS credential for delegated operationsbaseDocumentCID is committed-but-uninterpreted provenance, validated as
CID-or-null with no verification meaning. It lets the public proof plane express
content-version lineage without exposing the private document.
MultikeyPublicKey
{ id: string, // e.g. "key_r9ev34fvc23z999veaaft83nn29zvhe"
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_r9ev34fvc23z999veaaft83nn29zvhe |
| Identity update/delete/restore | DID URL | See below |
| All content ops | DID URL | See below |
DID URL examples:
did:dfos:cnnnft9f8a2rn938d6nkz38r847v2kr#key_r9ev34fvc23z999veaaft83nn29zvhe
did:dfos:cnnnft9f8a2rn938d6nkz38r847v2kr#key_ez9a874tckr3dv933d3ckdn7z6zrct8cid header
Every operation JWS (identity-op and content-op) includes a cid field in the
protected header: 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 the EdDSA
signature covers it.
Signing order:
- Construct the operation payload
- Derive the operation CID:
dagCborCanonicalEncode(payload) → CIDv1 - Build the protected header including
cid - 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, or header.cid does not match the derived CID.
Which envelope families carry cid is inventoried per family in the
extension registry.
typ header
The JWS typ header uses protocol-specific values, not IANA media types. Every
typ value is registered in the extension registry, which
names each value, the spec that owns it, and whether its envelope carries the
cid header. A new envelope family adds a row there, never a local name.
Registration is for typ routing and says nothing about ingestion; several
registered families are document-plane artifacts no relay ingests.
Where a family's verification requires an exact typ, that check is normative
and is stated with that family's rules: key proofs, credentials and revocations,
SIWD challenge and ask proofs, API-AUTH request and identity proofs, and
sign-requests all gate on it, and it is what keeps a JWS signed for one purpose
from being presented as another. Elsewhere typ is routing: implementations
SHOULD validate it and MUST NOT treat it as a substitute for the checks a
family's own rules require.
Signature verification profile
DFOS pins one narrow profile of the JOSE/JWS surface so that all conformant verifiers accept and reject the same signatures byte-for-byte. The rules below are normative and apply to every verification path: identity-op JWS, content-op JWS, key proofs, artifacts, countersignatures, DFOS credentials, credential revocations, and every API-AUTH and SIWD proof. A verifier MUST apply §1 to §3 to the protected header before performing any signature computation, and MUST apply §4 as part of or before the signature check. A token that violates any rule MUST be rejected regardless of whether its signature would otherwise verify.
There is no algorithm agility: the verifier never branches on alg to select a
primitive. Ed25519 (EdDSA) is the only signature algorithm.
1. Algorithm pinning (alg)
The protected header alg member MUST equal the exact string "EdDSA". Any
other value MUST be rejected before any signature check, including "none",
"HS256", "RS256", "ES256", the lowercase "eddsa", or an absent alg.
Verifiers MUST NOT use alg to choose a verification primitive; it is checked
only for exact equality.
2. crit rejection
The protected header MUST NOT contain a crit member. DFOS emits no critical
header parameters, so any token whose protected header carries crit, with any
value, MUST be rejected. Verifiers MUST observe the member's presence directly:
decoding into a fixed header shape that silently discards unknown members is not
sufficient.
3. No header key trust
The verifier MUST NOT read key material from the protected header, and MUST NOT
follow a reference that fetches it. A protected header carrying any of jwk,
jku, x5c, or x5u MUST be rejected. A URL that fetches a key is header key
trust with an extra hop.
The signing key is resolved exclusively from kid against the signer's identity
chain, in that identity's effective state as of the verification's
basis time. A key proof is the one family that carries no kid:
its key rides in the signed payload, per The envelope. A
header-supplied key is never trusted, even if it happens to match the resolved
key.
4. Canonical signature scalar (S < L)
An Ed25519 signature is R || S (64 bytes). The scalar S, the trailing 32
bytes little-endian, MUST be canonical: strictly less than the group order
L = 2^252 + 27742317777372353535851937790883648493
= 0x1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3edA signature whose S >= L MUST be rejected. A signature that does not decode to
exactly 64 bytes MUST also be rejected. An implementation on an Ed25519 library
that accepts a non-canonical S MUST add an explicit constant-time S < L
gate.
Axes outside this profile
Verifiers inherit whatever their Ed25519 library does on these axes: cofactorless
verification equation pinning (the specific [S]B == R + [k]A equation rather
than the batch or cofactored form), the full-order public key check
([L]A == identity), canonical point encoding (y < p) for R and A,
small-order public key rejection, and strict base64url tightening.
Credentials
Credentials authorize actions on resources (read, write) via attenuations, for relay access and content chain delegation. The credential format, verification rules, and revocation payloads are specified in CREDENTIALS.md.
Content chain authorization
When enforceAuthorization is enabled on content chain verification:
- Genesis operation. The signer is the chain creator, always authorized.
- Creator signs subsequent ops. Authorized directly, no credential needed.
- A different DID signs. The operation MUST include an
authorizationfield containing a valid DFOS credential where:- The delegation chain roots at the chain creator DID
- The credential's
attincludes an entry withaction: "write"covering this chain's resource - The credential is valid at the operation's basis time: the
issuer's signing key effective as of the basis,
expstrictly greater than the basis, and no revocation effective as of the basis covering it or any parent in its delegation chain
The authorization field is available on update and delete content
operations. It is absent for creator-signed operations.
Revocation
Credentials are revoked by publishing a revocation artifact, a signed
proof-plane primitive with typ: did:dfos:revocation. Only the credential's
issuer DID can revoke it, and there is no un-revoke operation. Revocation is
checked at every level of a presented credential, the leaf and each parent in
its delegation chain, on both the read path and the write path.
Revocation is forward-looking against the basis: it prevents future use of a credential and does not retroactively invalidate operations already committed to a content chain.
Services
services is an identity's discovery vocabulary: a controller-signed,
full-state array carried in identity-chain create and update operations and
projected into verified identity state. It answers "given a DID, where do I reach
this identity, and what stable content does it publish?" Services live inside
identity operations, inherit the chain's signer rules, and inherit the chain's
linearity, so one log resolves to exactly one services set.
Service entry
{ id: string, // did-core fragment, unique within the set (deref did:dfos:xxx#<id>)
type: string, // open namespace — recognized types are structurally validated
... // type-specific fields (see below)
}Every entry carries the common envelope { id, type }. The namespace is open:
two types are recognized and structurally validated, and any other type is an
opaque extension that verifiers MUST preserve verbatim and otherwise ignore. New
service types therefore never require a protocol or cross-language change. Every
registered type is indexed in the extension registry.
Recognized types:
// Transport locator — where to reach a relay serving this identity
{ id: string, type: "DfosRelay", endpoint: string } // endpoint: bare URL string
// Stable content reference under a client-defined semantic label
{ id: string, type: "ContentAnchor", label: string, anchor: string }A ContentAnchor's anchor references a stable content identifier, dispatched
by structural form:
| Anchor shape | Resolves to |
|---|---|
^[2346789acdefhknrtvz]{31}$ |
content chain (mutable, gateable) |
^bafyrei[a-z2-7]{52}$ |
artifact (immutable, public) |
These two shapes are the ONLY valid anchors and the structural dispatch above is
normative. An anchor matching neither shape MUST be rejected (AnchorInvalid);
verifiers do not accept other CID codecs or lengths. New anchor kinds arrive via
a new service type, never a new anchor shape.
The label is an opaque client-semantic key (for example "profile",
"avatar"). The protocol assigns it no meaning.
Bounds
- ≤ 256 entries per identity; entry
ids MUST be unique within the set id,type, and the recognized string fields (endpoint,label) MUST be non-empty, andanchorMUST match the contentId or CID shape. Individual field lengths are not separately capped- The CBOR-encoded
servicesarray MUST NOT exceed 32768 bytes - An entry whose recognized type is structurally malformed (for example a
DfosRelaywithout anendpoint) MUST be rejected at verification. A malformed unrecognized type is preserved and ignored
Full-state semantics
services is full-state, not a delta. A create sets the initial set; an
update REPLACES the entire set (omit the field to clear it); a delete carries
the last set unchanged into terminal state. Omitting services encodes
identically to a service-less operation (CID-neutral).
Worked example
examples/identity-services.json is a genesis publishing a relay locator and two
content anchors, one content-chain and one artifact. Signed by reference key 1:
did: did:dfos:krhcznk98f7r2r4a6ktafcv77f7k6e2
typ: did:dfos:identity-op
cid: bafyreiasjg3vqs4b3vepwy5qc4oy4f4vpkcahmwi64jtuei5cf7zqpdxjy
services: [ { id: "relay", type: "DfosRelay", endpoint: "https://relay.dfos.com" },
{ id: "profile", type: "ContentAnchor", label: "profile", anchor: "8n8fnzhrrefkrde6h72kfvff43r8c63" },
{ id: "avatar", type: "ContentAnchor", label: "avatar", anchor: "bafyreie6xfkrtwax2dq5gdw3rpsurz2glsduxycfhk7jjllewiwivkkafu" } ]The full JWS token is in examples/identity-services.json.
Standalone signed statements
An artifact and a countersignature are each one signed statement with no
predecessor and no successor: a JWS envelope over a dag-cbor payload, addressed
by the payload's CID, verified against the signing identity's state as of the
statement's own createdAt (Time basis). Neither is a chain
operation: nothing links to one by previousOperationCID, and neither changes
chain state.
Payload shape
| Field | Artifact | Countersignature |
|---|---|---|
version |
1 |
1 |
type |
"artifact" |
"countersign" |
did |
The signing identity | The witness identity |
| body | content: an object with a $schema string |
targetCID: the CID witnessed; relation: optional tag |
createdAt |
Timestamp grammar | Same |
The JWS typ is did:dfos:artifact or did:dfos:countersign, and the
envelope carries the cid header.
Artifact. content is an inline document. Its $schema is a free-form
discriminator with no protocol-level registry; consumers dispatch on it. The
CBOR-encoded payload MUST NOT exceed 16384 bytes. An artifact is immutable and
is addressed by its CID.
{
"version": 1,
"type": "artifact",
"did": "did:dfos:...",
"content": {
"$schema": "https://schemas.dfos.com/profile/v1",
"name": "Example"
},
"createdAt": "2026-03-25T00:00:00.000Z"
}Countersignature. targetCID names any CID-addressed statement: a content
or identity operation, an artifact, or another countersignature. relation is
an optional open-namespace tag of 1 to 64 UTF-8 bytes. Recognized values
(endorses, coauthors, witnessed, holds, received) inform clients;
unrecognized values MUST be preserved and ignored. When present, relation is
part of the canonical payload and so of the CID; absent, the payload encodes
identically to one that never carried it. A countersignature is permanent.
There is no withdrawal; a consumer may weight a newer statement by the same
witness over an older one. Witnessing publicly and permanently links the
witness DID to the target.
{
"version": 1,
"type": "countersign",
"did": "did:dfos:witness...",
"targetCID": "bafy...",
"relation": "endorses",
"createdAt": "2026-03-25T00:00:00.000Z"
}Verification
- Decode the JWS.
typMUST be the value registered for the payloadtype. - Validate the payload:
version,type,did,createdAt, and the body for that type (contentcarrying a$schemastring, ortargetCIDwith a well-formedrelationwhen present). - The
kidDID MUST equal the payloaddid. - Verify the EdDSA signature against the key
kidnames, effective in the signer's state as ofcreatedAt. header.cidMUST equal the CID of the dag-cbor canonical encoding of the payload.- Artifact only: the CBOR-encoded payload MUST NOT exceed 16384 bytes.
A relay's checks on a countersignature (the target exists, the witness is not the target's author, one countersignature per witness per target) are ingest rules, specified in RELAY.
Verification
Every signature check below is performed under the Signature verification profile.
Identity chain
- Decode each JWS, parse payload as IdentityOperation
- First op MUST be
type: "create", the genesis bootstrap:- Verify the single-key rule: each of the three role arrays holds exactly one entry, all three the same key with the same id. A genesis declaring more than one distinct key MUST be rejected. A genesis carrying
keyProofsMUST be rejected. - The one key declared in the genesis payload is trusted because the identity does not exist before this operation.
- The signing key (resolved from
kid) MUST be that declared key. - Derive the operation CID via dag-cbor canonical encoding. Verify
header.cidmatches the derived CID. Derive the DID from the CID.
- Verify the single-key rule: each of the three role arrays holds exactly one entry, all three the same key with the same id. A genesis declaring more than one distinct key MUST be rejected. A genesis carrying
- For each subsequent op: verify
previousOperationCIDmatches the previous op's derived CID, so the chain as verified is one linear sequence. VerifycreatedAtis strictly greater than the parent operation'screatedAt(MUST). - If the state before this operation is deleted, the only valid operation is a
restorewhosepreviousOperationCIDis the delete's CID; any other operation MUST be rejected. Arestorewhose parent is not adeleteMUST be rejected wherever it appears. AdeleteorrestorecarryingkeyProofs, and anykeyProofsmember that is not an array of strings, MUST be rejected. - Resolve
kid: genesis uses a bare key ID, non-genesis uses a DID URL (extract the DID, verify it matches the derived DID; extract the key ID). - Find the controller key matching that key ID in the current declared state, the declared state after all preceding operations. For a
restorethis is the deleted head state produced by thedelete, which carries the last key sets unchanged. Decode multikey to a raw Ed25519 public key. - Verify the EdDSA JWS signature over the signing input bytes.
- For an
update, compute possession per Key possession: for each key-role membership the operation introduces, find an envelope inkeyProofspassing chain-walk verification for this operation, this key, and that role. A covered introduction is effective; an uncovered one is void, recorded and surfaced, excluded from effective state, and not a rejection. - Apply the state change:
createinitializes declared and effective key state to its one key in all three roles;updatereplaces declared state (which must have at least one declared controller key) and folds effective state per step 8;deletemarks the state deleted (key sets and services carried unchanged);restoreclears the deleted state (keys and services as of the delete, verbatim, effective state included).
The verification result carries both readings: the declared state, the effective state, and the list of void memberships (key, role, operation CID). Consumers act on the effective state; the declared state exists for structural admission and for showing a human what a chain claims versus what it proved.
Content chain
- Decode each JWS, parse payload as ContentOperation
- First op must be
type: "create"; the signer is the chain creator - For each subsequent op: verify
previousOperationCIDmatches, verifycreatedAtis strictly greater than the parent operation'screatedAt(MUST) - Derive the operation CID via dag-cbor canonical encoding. Verify
header.cidmatches the derived CID. - Verify the
kidDID matches the payloaddidfield - Resolve
kidvia the external key resolver the caller provides - Verify the EdDSA JWS signature
- If
enforceAuthorizationis enabled and the signer DID differs from the chain creator: verify theauthorizationfield contains a valid DFOS credential withaction: "write"covering this chain, with a delegation chain rooting at the creator DID, valid at the operation's basis time - Apply the state change (set document, clear, or delete)
Extension registry
Every name the corpus registers under the core's open namespaces is indexed here:
service types under Services, and JWS typ values under the
typ header convention. Each name's semantics live in its owner
spec, which is normative wherever the two could be read to disagree. A new name
lands by adding its row here in the same PR that specifies it.
Service types
Service type |
Owner spec | Validation | Semantics |
|---|---|---|---|
DfosRelay |
PROTOCOL | core | Transport locator: where to reach a relay serving this identity. |
ContentAnchor |
PROTOCOL | core | Stable content reference: a contentId or artifact CID under a client-defined semantic label. |
DfosAuthorizationServer |
INTEGRATIONS | consumer | The canonical authorize origin able to produce this subject's signature under sign-in profile A. |
DfosOrigin |
INTEGRATIONS | consumer | The identity's claimed web domain: the chain half of the bidirectional origin binding. |
Validation. core: structurally validated by every conformant verifier, and
a malformed entry rejects at verification. consumer: opaque to the core
(preserved verbatim, ignored); structural validation is an obligation of the
owner spec's consumers. Ambiguity rules, field grammar, and display discipline
are the owner spec's.
JWS typ values
Every DFOS JWS envelope is typ-scoped. The cid column marks whether the
envelope carries the protocol's cid header.
typ value |
Owner spec | cid |
Semantics |
|---|---|---|---|
did:dfos:identity-op |
PROTOCOL | yes | Identity chain operations. |
did:dfos:content-op |
PROTOCOL | yes | Content chain operations. |
did:dfos:artifact |
PROTOCOL | yes | Standalone signed statements: inline documents. |
did:dfos:countersign |
PROTOCOL | yes | Standalone signed statements: witness attestations. |
did:dfos:key-add |
PROTOCOL | no | Key introduction proofs: the candidate key's position-bound possession-and-consent proof, self-signed, presented to ceremonies and embedded in the introducing identity operation's keyProofs. |
did:dfos:credential |
CREDENTIALS | yes | Authorization credentials; the cid is their revocation address. |
did:dfos:revocation |
CREDENTIALS | yes | Credential revocation artifacts. |
did:dfos:credit-claim |
CONTENT-MODEL | yes | Document-plane credit claims: registered for typ routing, never relay-ingested. |
did:dfos:sign-request |
RELAY | yes | Sign-request envelopes: travel the signing-mailbox courier, never POST /proof/v1/operations. |
did:dfos:siwd |
INTEGRATIONS | no | Sign In With DFOS challenge proofs: delivered by web redirect or the signing mailbox, never relay-ingested. |
did:dfos:siwd-ask |
INTEGRATIONS | no | Loopback client ask proofs: the client's key-control proof over its own authorize request. |
did:dfos:request-proof |
INTEGRATIONS | no | API request proofs: ride the Authorization header of a credential-gated API request and die with the freshness window. |
did:dfos:identity-proof |
INTEGRATIONS | no | API identity proofs: bind one exact request to a bare DID, authentication only. |
Names whose grammar is inseparable from their owner's machinery register there:
credential resource forms in
CREDENTIALS; API action tokens, sign-in
scope tokens, and app description members in
INTEGRATIONS; sign-request
payloadTyp families in RELAY; and content
schemas in CONTENT-MODEL, hosted at
schemas.dfos.com.
External registrations
The corpus also mints names in two global namespaces DFOS does not own, and RFC 8615 §3 and RFC 8552 §4.1.5 require each to be entered in its IANA registry. A spec that mints a well-known path or an underscored name adds its row here in the same PR that specifies it; registration itself is an act of the project's stewards.
| Name | Registry | Owner spec | Registered |
|---|---|---|---|
/.well-known/dfos-relay |
IANA Well-Known URIs (RFC 8615) | RELAY | not listed |
/.well-known/dfos-app.json |
IANA Well-Known URIs (RFC 8615) | INTEGRATIONS | not listed |
/.well-known/dfos-did |
IANA Well-Known URIs (RFC 8615) | INTEGRATIONS | not listed |
/.well-known/dfos-key-proof |
IANA Well-Known URIs (RFC 8615) | INTEGRATIONS | not listed |
_dfos (TXT) |
IANA Underscored DNS Node Names (RFC 8552) | INTEGRATIONS | not listed |
Reference vectors
The vectors in this section are published as
packages/protocol-verify/vectors.json,
the artifact every verification suite loads. The prose here renders that
artifact byte for byte; the TypeScript reference suite regenerates it from the
fixed seeds and fails if the two diverge.
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")).
The same values are re-derived from scratch, in five languages using only native cryptography libraries, by the suites in packages/protocol-verify/. Each suite hardcodes these constants and imports no DFOS library, so a disagreement between any two suites is an ambiguity in this document.
Key 1 (Genesis Controller)
Seed: SHA-256("dfos-protocol-reference-key-1")
Private key: 132d4bebdb6e62359afb930fe15d756a92ad96e6b0d47619988f5a1a55272aac
Public key: ba421e272fad4f941c221e47f87d9253bdc04f7d4ad2625ae667ab9f0688ce32
Multikey: z6MkrzLMNwoJSV4P3YccWcbtk8vd9LtgMKnLeaDLUqLuASjb
Key ID: key_r9ev34fvc23z999veaaft83nn29zvheKey 2 (Rotated Controller)
Seed: SHA-256("dfos-protocol-reference-key-2")
Private key: 384f5626906db84f6a773ec46475ff2d4458e92dd4dd13fe03dbb7510f4ca2a8
Public key: 0f350f994f94d675f04a325bd316ebedd740ca206eaaf609bdb641b5faa0f78c
Multikey: z6MkfUd65JrAhfdgFuMCccU9ThQvjB2fJAMUHkuuajF992gK
Key ID: key_ez9a874tckr3dv933d3ckdn7z6zrct8Identity chain: Create (Genesis)
Operation:
{
"version": 1,
"type": "create",
"authKeys": [
{
"id": "key_r9ev34fvc23z999veaaft83nn29zvhe",
"type": "Multikey",
"publicKeyMultibase": "z6MkrzLMNwoJSV4P3YccWcbtk8vd9LtgMKnLeaDLUqLuASjb"
}
],
"assertKeys": [
{
"id": "key_r9ev34fvc23z999veaaft83nn29zvhe",
"type": "Multikey",
"publicKeyMultibase": "z6MkrzLMNwoJSV4P3YccWcbtk8vd9LtgMKnLeaDLUqLuASjb"
}
],
"controllerKeys": [
{
"id": "key_r9ev34fvc23z999veaaft83nn29zvhe",
"type": "Multikey",
"publicKeyMultibase": "z6MkrzLMNwoJSV4P3YccWcbtk8vd9LtgMKnLeaDLUqLuASjb"
}
],
"createdAt": "2026-03-07T00:00:00.000Z"
}JWS Header:
{
"alg": "EdDSA",
"typ": "did:dfos:identity-op",
"kid": "key_r9ev34fvc23z999veaaft83nn29zvhe",
"cid": "bafyreicoghvjznvliuloxxmbf54tpzqwahnqpilk7ncxepjinedpkga3ne"
}JWS Signature (hex):
4dece71e7cebb4a3864ebd05ce40cbdb3fa5b8c5a701b297ae60db8be131830ff130f0a7630187391323c3e04cdbc7f44684e2ac801e0fb776d16e514ae1ae06JWS Token:
eyJhbGciOiJFZERTQSIsInR5cCI6ImRpZDpkZm9zOmlkZW50aXR5LW9wIiwia2lkIjoia2V5X3I5ZXYzNGZ2YzIzejk5OXZlYWFmdDgzbm4yOXp2aGUiLCJjaWQiOiJiYWZ5cmVpY29naHZqem52bGl1bG94eG1iZjU0dHB6cXdhaG5xcGlsazduY3hlcGppbmVkcGtnYTNuZSJ9.eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiY3JlYXRlIiwiYXV0aEtleXMiOlt7ImlkIjoia2V5X3I5ZXYzNGZ2YzIzejk5OXZlYWFmdDgzbm4yOXp2aGUiLCJ0eXBlIjoiTXVsdGlrZXkiLCJwdWJsaWNLZXlNdWx0aWJhc2UiOiJ6Nk1rcnpMTU53b0pTVjRQM1ljY1djYnRrOHZkOUx0Z01LbkxlYURMVXFMdUFTamIifV0sImFzc2VydEtleXMiOlt7ImlkIjoia2V5X3I5ZXYzNGZ2YzIzejk5OXZlYWFmdDgzbm4yOXp2aGUiLCJ0eXBlIjoiTXVsdGlrZXkiLCJwdWJsaWNLZXlNdWx0aWJhc2UiOiJ6Nk1rcnpMTU53b0pTVjRQM1ljY1djYnRrOHZkOUx0Z01LbkxlYURMVXFMdUFTamIifV0sImNvbnRyb2xsZXJLZXlzIjpbeyJpZCI6ImtleV9yOWV2MzRmdmMyM3o5OTl2ZWFhZnQ4M25uMjl6dmhlIiwidHlwZSI6Ik11bHRpa2V5IiwicHVibGljS2V5TXVsdGliYXNlIjoiejZNa3J6TE1Od29KU1Y0UDNZY2NXY2J0azh2ZDlMdGdNS25MZWFETFVxTHVBU2piIn1dLCJjcmVhdGVkQXQiOiIyMDI2LTAzLTA3VDAwOjAwOjAwLjAwMFoifQ.TeznHnzrtKOGTr0FzkDL2z-luMWnAbKXrmDbi-Exgw_xMPCnYwGHORMjw-BM28f0RoTirIAeD7d20W5RSuGuBgOperation CID:
bafyreicoghvjznvliuloxxmbf54tpzqwahnqpilk7ncxepjinedpkga3neDerived DID: did:dfos:cnnnft9f8a2rn938d6nkz38r847v2kr
Reference key proof (key 2's introduction)
The rotation below introduces key 2 to all three roles, so it carries key 2's key-proof envelope in keyProofs (Key possession). The envelope is signed by key 2, the key being introduced, while the operation carrying it is signed by key 1. Its deterministic inputs: nonce is the literal dfos-protocol-reference-nonce-1, audience is keys.dfos.com, timestamp is 2026-03-07T00:00:30.000Z. The rest is derived: did is the reference DID, roleSet is auth,assert,controller, prevCID is the genesis CID, publicKeyMultibase is key 2's multikey.
Decoded payload:
{
"nonce": "dfos-protocol-reference-nonce-1",
"audience": "keys.dfos.com",
"did": "did:dfos:cnnnft9f8a2rn938d6nkz38r847v2kr",
"roleSet": "auth,assert,controller",
"prevCID": "bafyreicoghvjznvliuloxxmbf54tpzqwahnqpilk7ncxepjinedpkga3ne",
"publicKeyMultibase": "z6MkfUd65JrAhfdgFuMCccU9ThQvjB2fJAMUHkuuajF992gK",
"timestamp": "2026-03-07T00:00:30.000Z"
}Canonical signing input (the exact payload octets):
{"nonce":"dfos-protocol-reference-nonce-1","audience":"keys.dfos.com","did":"did:dfos:cnnnft9f8a2rn938d6nkz38r847v2kr","roleSet":"auth,assert,controller","prevCID":"bafyreicoghvjznvliuloxxmbf54tpzqwahnqpilk7ncxepjinedpkga3ne","publicKeyMultibase":"z6MkfUd65JrAhfdgFuMCccU9ThQvjB2fJAMUHkuuajF992gK","timestamp":"2026-03-07T00:00:30.000Z"}JWS protected header:
{
"alg": "EdDSA",
"typ": "did:dfos:key-add"
}JWS Signature (hex):
f27835d076dbb360450ac6284983474b54f4d36d3e1d79b8683d7442b1e5a72b0ed6caa6cb553f763f72133d0225230260dc214d931b4e26a7849f021f52ed03Compact JWS token:
eyJhbGciOiJFZERTQSIsInR5cCI6ImRpZDpkZm9zOmtleS1hZGQifQ.eyJub25jZSI6ImRmb3MtcHJvdG9jb2wtcmVmZXJlbmNlLW5vbmNlLTEiLCJhdWRpZW5jZSI6ImtleXMuZGZvcy5jb20iLCJkaWQiOiJkaWQ6ZGZvczpjbm5uZnQ5ZjhhMnJuOTM4ZDZua3ozOHI4NDd2MmtyIiwicm9sZVNldCI6ImF1dGgsYXNzZXJ0LGNvbnRyb2xsZXIiLCJwcmV2Q0lEIjoiYmFmeXJlaWNvZ2h2anpudmxpdWxveHhtYmY1NHRwenF3YWhucXBpbGs3bmN4ZXBqaW5lZHBrZ2EzbmUiLCJwdWJsaWNLZXlNdWx0aWJhc2UiOiJ6Nk1rZlVkNjVKckFoZmRnRnVNQ2NjVTlUaFF2akIyZkpBTVVIa3V1YWpGOTkyZ0siLCJ0aW1lc3RhbXAiOiIyMDI2LTAzLTA3VDAwOjAwOjMwLjAwMFoifQ.8ng10Hbbs2BFCsYoSYNHS1T0020-HXm4aD10QrHlpysO1sqmy1U_dj9yEz0CJSMCYNwhTZMbTianhJ8CH1LtAwIdentity chain: Update (Key Rotation)
JWS Header:
{
"alg": "EdDSA",
"typ": "did:dfos:identity-op",
"kid": "did:dfos:cnnnft9f8a2rn938d6nkz38r847v2kr#key_r9ev34fvc23z999veaaft83nn29zvhe",
"cid": "bafyreiarc7mv6fvhaoe2mmk4ujpskgqpesv66pzd5juqlg5bzmridikkqy"
}Operation:
{
"version": 1,
"type": "update",
"previousOperationCID": "bafyreicoghvjznvliuloxxmbf54tpzqwahnqpilk7ncxepjinedpkga3ne",
"authKeys": [
{
"id": "key_ez9a874tckr3dv933d3ckdn7z6zrct8",
"type": "Multikey",
"publicKeyMultibase": "z6MkfUd65JrAhfdgFuMCccU9ThQvjB2fJAMUHkuuajF992gK"
}
],
"assertKeys": [
{
"id": "key_ez9a874tckr3dv933d3ckdn7z6zrct8",
"type": "Multikey",
"publicKeyMultibase": "z6MkfUd65JrAhfdgFuMCccU9ThQvjB2fJAMUHkuuajF992gK"
}
],
"controllerKeys": [
{
"id": "key_ez9a874tckr3dv933d3ckdn7z6zrct8",
"type": "Multikey",
"publicKeyMultibase": "z6MkfUd65JrAhfdgFuMCccU9ThQvjB2fJAMUHkuuajF992gK"
}
],
"createdAt": "2026-03-07T00:01:00.000Z",
"keyProofs": [
"eyJhbGciOiJFZERTQSIsInR5cCI6ImRpZDpkZm9zOmtleS1hZGQifQ.eyJub25jZSI6ImRmb3MtcHJvdG9jb2wtcmVmZXJlbmNlLW5vbmNlLTEiLCJhdWRpZW5jZSI6ImtleXMuZGZvcy5jb20iLCJkaWQiOiJkaWQ6ZGZvczpjbm5uZnQ5ZjhhMnJuOTM4ZDZua3ozOHI4NDd2MmtyIiwicm9sZVNldCI6ImF1dGgsYXNzZXJ0LGNvbnRyb2xsZXIiLCJwcmV2Q0lEIjoiYmFmeXJlaWNvZ2h2anpudmxpdWxveHhtYmY1NHRwenF3YWhucXBpbGs3bmN4ZXBqaW5lZHBrZ2EzbmUiLCJwdWJsaWNLZXlNdWx0aWJhc2UiOiJ6Nk1rZlVkNjVKckFoZmRnRnVNQ2NjVTlUaFF2akIyZkpBTVVIa3V1YWpGOTkyZ0siLCJ0aW1lc3RhbXAiOiIyMDI2LTAzLTA3VDAwOjAwOjMwLjAwMFoifQ.8ng10Hbbs2BFCsYoSYNHS1T0020-HXm4aD10QrHlpysO1sqmy1U_dj9yEz0CJSMCYNwhTZMbTianhJ8CH1LtAw"
]
}JWS Signature (hex):
d77a2bfd7ef30ce7b391214775604bc0f70dc88686dd794701bf661e9806f3354395dcf4c063f45fc5a25481cf2d0fb6d19b023afb21f18e816e0c5d6c8bcc04JWS Token:
eyJhbGciOiJFZERTQSIsInR5cCI6ImRpZDpkZm9zOmlkZW50aXR5LW9wIiwia2lkIjoiZGlkOmRmb3M6Y25ubmZ0OWY4YTJybjkzOGQ2bmt6MzhyODQ3djJrciNrZXlfcjlldjM0ZnZjMjN6OTk5dmVhYWZ0ODNubjI5enZoZSIsImNpZCI6ImJhZnlyZWlhcmM3bXY2ZnZoYW9lMm1tazR1anBza2dxcGVzdjY2cHpkNWp1cWxnNWJ6bXJpZGlra3F5In0.eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoidXBkYXRlIiwicHJldmlvdXNPcGVyYXRpb25DSUQiOiJiYWZ5cmVpY29naHZqem52bGl1bG94eG1iZjU0dHB6cXdhaG5xcGlsazduY3hlcGppbmVkcGtnYTNuZSIsImF1dGhLZXlzIjpbeyJpZCI6ImtleV9lejlhODc0dGNrcjNkdjkzM2QzY2tkbjd6NnpyY3Q4IiwidHlwZSI6Ik11bHRpa2V5IiwicHVibGljS2V5TXVsdGliYXNlIjoiejZNa2ZVZDY1SnJBaGZkZ0Z1TUNjY1U5VGhRdmpCMmZKQU1VSGt1dWFqRjk5MmdLIn1dLCJhc3NlcnRLZXlzIjpbeyJpZCI6ImtleV9lejlhODc0dGNrcjNkdjkzM2QzY2tkbjd6NnpyY3Q4IiwidHlwZSI6Ik11bHRpa2V5IiwicHVibGljS2V5TXVsdGliYXNlIjoiejZNa2ZVZDY1SnJBaGZkZ0Z1TUNjY1U5VGhRdmpCMmZKQU1VSGt1dWFqRjk5MmdLIn1dLCJjb250cm9sbGVyS2V5cyI6W3siaWQiOiJrZXlfZXo5YTg3NHRja3IzZHY5MzNkM2NrZG43ejZ6cmN0OCIsInR5cGUiOiJNdWx0aWtleSIsInB1YmxpY0tleU11bHRpYmFzZSI6Ino2TWtmVWQ2NUpyQWhmZGdGdU1DY2NVOVRoUXZqQjJmSkFNVUhrdXVhakY5OTJnSyJ9XSwiY3JlYXRlZEF0IjoiMjAyNi0wMy0wN1QwMDowMTowMC4wMDBaIiwia2V5UHJvb2ZzIjpbImV5SmhiR2NpT2lKRlpFUlRRU0lzSW5SNWNDSTZJbVJwWkRwa1ptOXpPbXRsZVMxaFpHUWlmUS5leUp1YjI1alpTSTZJbVJtYjNNdGNISnZkRzlqYjJ3dGNtVm1aWEpsYm1ObExXNXZibU5sTFRFaUxDSmhkV1JwWlc1alpTSTZJbXRsZVhNdVpHWnZjeTVqYjIwaUxDSmthV1FpT2lKa2FXUTZaR1p2Y3pwamJtNXVablE1WmpoaE1uSnVPVE00WkRadWEzb3pPSEk0TkRkMk1tdHlJaXdpY205c1pWTmxkQ0k2SW1GMWRHZ3NZWE56WlhKMExHTnZiblJ5YjJ4c1pYSWlMQ0p3Y21WMlEwbEVJam9pWW1GbWVYSmxhV052WjJoMmFucHVkbXhwZFd4dmVIaHRZbVkxTkhSd2VuRjNZV2h1Y1hCcGJHczNibU40WlhCcWFXNWxaSEJyWjJFemJtVWlMQ0p3ZFdKc2FXTkxaWGxOZFd4MGFXSmhjMlVpT2lKNk5rMXJabFZrTmpWS2NrRm9abVJuUm5WTlEyTmpWVGxVYUZGMmFrSXlaa3BCVFZWSWEzVjFZV3BHT1RreVowc2lMQ0owYVcxbGMzUmhiWEFpT2lJeU1ESTJMVEF6TFRBM1ZEQXdPakF3T2pNd0xqQXdNRm9pZlEuOG5nMTBIYmJzMkJGQ3NZb1NZTkhTMVQwMDIwLUhYbTRhRDEwUXJIbHB5c08xc3FteTFVX2RqOXlFejBDSlNNQ1lOd2hUWk1iVGlhbmhKOENIMUx0QXciXX0.13or_X7zDOezkSFHdWBLwPcNyIaG3XlHAb9mHpgG8zVDldz0wGP0X8WiVIHPLQ-20ZsCOvsh8Y6BbgxdbIvMBAOperation CID:
bafyreiarc7mv6fvhaoe2mmk4ujpskgqpesv66pzd5juqlg5bzmridikkqyPost-rotation: DID unchanged (did:dfos:cnnnft9f8a2rn938d6nkz38r847v2kr), controller rotated to key_ez9a874tckr3dv933d3ckdn7z6zrct8, key 2 proved for all three roles by the embedded envelope.
Identity chain: Delete + Restore
Delete Operation:
{
"version": 1,
"type": "delete",
"previousOperationCID": "bafyreiarc7mv6fvhaoe2mmk4ujpskgqpesv66pzd5juqlg5bzmridikkqy",
"createdAt": "2026-03-07T00:02:00.000Z"
}Delete JWS Header:
{
"alg": "EdDSA",
"typ": "did:dfos:identity-op",
"kid": "did:dfos:cnnnft9f8a2rn938d6nkz38r847v2kr#key_ez9a874tckr3dv933d3ckdn7z6zrct8",
"cid": "bafyreiaiy5m4fiyntdryikzfwzynojwiglkqrwiefulb4dl36eqeefbpwm"
}Delete JWS Signature (hex):
40887e1d10fe60457ce328355f72f0cfeb5724418f08baee4ecb160ba2206f98ff406d1a18f8c9eac02a144d5533c29445896616468b818655e8edbcdf560108Delete JWS Token:
eyJhbGciOiJFZERTQSIsInR5cCI6ImRpZDpkZm9zOmlkZW50aXR5LW9wIiwia2lkIjoiZGlkOmRmb3M6Y25ubmZ0OWY4YTJybjkzOGQ2bmt6MzhyODQ3djJrciNrZXlfZXo5YTg3NHRja3IzZHY5MzNkM2NrZG43ejZ6cmN0OCIsImNpZCI6ImJhZnlyZWlhaXk1bTRmaXludGRyeWlremZ3enlub2p3aWdsa3Fyd2llZnVsYjRkbDM2ZXFlZWZicHdtIn0.eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiZGVsZXRlIiwicHJldmlvdXNPcGVyYXRpb25DSUQiOiJiYWZ5cmVpYXJjN212NmZ2aGFvZTJtbWs0dWpwc2tncXBlc3Y2NnB6ZDVqdXFsZzViem1yaWRpa2txeSIsImNyZWF0ZWRBdCI6IjIwMjYtMDMtMDdUMDA6MDI6MDAuMDAwWiJ9.QIh-HRD-YEV84yg1X3Lwz-tXJEGPCLruTssWC6Igb5j_QG0aGPjJ6sAqFE1VM8KURYlmFkaLgYZV6O2831YBCADelete Operation CID:
bafyreiaiy5m4fiyntdryikzfwzynojwiglkqrwiefulb4dl36eqeefbpwmRestore Operation:
{
"version": 1,
"type": "restore",
"previousOperationCID": "bafyreiaiy5m4fiyntdryikzfwzynojwiglkqrwiefulb4dl36eqeefbpwm",
"createdAt": "2026-03-07T00:03:00.000Z"
}Restore JWS Header:
{
"alg": "EdDSA",
"typ": "did:dfos:identity-op",
"kid": "did:dfos:cnnnft9f8a2rn938d6nkz38r847v2kr#key_ez9a874tckr3dv933d3ckdn7z6zrct8",
"cid": "bafyreicfxp65m3js4tellb3optwn54ginqv7pp4ldifcnuvry7glunh7aq"
}Restore JWS Signature (hex):
2622005ca66a2199c3a5952b6dde12ec5eed1280448c478829c1a0dd645e6170052628bdeb4c2964b15fcab737c8028e36c330f614f9685462be8b3892d86e04Restore JWS Token:
eyJhbGciOiJFZERTQSIsInR5cCI6ImRpZDpkZm9zOmlkZW50aXR5LW9wIiwia2lkIjoiZGlkOmRmb3M6Y25ubmZ0OWY4YTJybjkzOGQ2bmt6MzhyODQ3djJrciNrZXlfZXo5YTg3NHRja3IzZHY5MzNkM2NrZG43ejZ6cmN0OCIsImNpZCI6ImJhZnlyZWljZnhwNjVtM2pzNHRlbGxiM29wdHduNTRnaW5xdjdwcDRsZGlmY251dnJ5N2dsdW5oN2FxIn0.eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoicmVzdG9yZSIsInByZXZpb3VzT3BlcmF0aW9uQ0lEIjoiYmFmeXJlaWFpeTVtNGZpeW50ZHJ5aWt6Znd6eW5vandpZ2xrcXJ3aWVmdWxiNGRsMzZlcWVlZmJwd20iLCJjcmVhdGVkQXQiOiIyMDI2LTAzLTA3VDAwOjAzOjAwLjAwMFoifQ.JiIAXKZqIZnDpZUrbd4S7F7tEoBEjEeIKcGg3WReYXAFJii960wpZLFfyrc3yAKONsMw9hT5aFRivos4kthuBARestore Operation CID:
bafyreicfxp65m3js4tellb3optwn54ginqv7pp4ldifcnuvry7glunh7aqContent chain: Document + Create
Document (flat content object):
{
"$schema": "https://schemas.dfos.com/post/v1",
"format": "short-post",
"publishedAt": "2026-03-07T00:02:00.000Z",
"title": "Hello World",
"body": "First post on the protocol.",
"credits": [
{
"did": "did:dfos:cnnnft9f8a2rn938d6nkz38r847v2kr",
"label": "author"
}
]
}Document CID:
bafyreie6xfkrtwax2dq5gdw3rpsurz2glsduxycfhk7jjllewiwivkkafuContent Create JWS Header:
{
"alg": "EdDSA",
"typ": "did:dfos:content-op",
"kid": "did:dfos:cnnnft9f8a2rn938d6nkz38r847v2kr#key_ez9a874tckr3dv933d3ckdn7z6zrct8",
"cid": "bafyreibs3vlvainfjfuet6x4uds3pivbmbohy7f64iegbuw3gpsuqtma6i"
}Content Create Payload:
{
"version": 1,
"type": "create",
"did": "did:dfos:cnnnft9f8a2rn938d6nkz38r847v2kr",
"documentCID": "bafyreie6xfkrtwax2dq5gdw3rpsurz2glsduxycfhk7jjllewiwivkkafu",
"baseDocumentCID": null,
"createdAt": "2026-03-07T00:02:00.000Z"
}Content Create JWS Signature (hex):
069523331dffceae6af9bc4e40dd29978b5f81ffee648b7deebedf33f76e909afefb307483bb311bd21c06ccb4451dcffc3b482b8181d7b8a4c035030c79ee03Content Create JWS Token:
eyJhbGciOiJFZERTQSIsInR5cCI6ImRpZDpkZm9zOmNvbnRlbnQtb3AiLCJraWQiOiJkaWQ6ZGZvczpjbm5uZnQ5ZjhhMnJuOTM4ZDZua3ozOHI4NDd2MmtyI2tleV9lejlhODc0dGNrcjNkdjkzM2QzY2tkbjd6NnpyY3Q4IiwiY2lkIjoiYmFmeXJlaWJzM3ZsdmFpbmZqZnVldDZ4NHVkczNwaXZibWJvaHk3ZjY0aWVnYnV3M2dwc3VxdG1hNmkifQ.eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiY3JlYXRlIiwiZGlkIjoiZGlkOmRmb3M6Y25ubmZ0OWY4YTJybjkzOGQ2bmt6MzhyODQ3djJrciIsImRvY3VtZW50Q0lEIjoiYmFmeXJlaWU2eGZrcnR3YXgyZHE1Z2R3M3Jwc3VyejJnbHNkdXh5Y2ZoazdqamxsZXdpd2l2a2thZnUiLCJiYXNlRG9jdW1lbnRDSUQiOm51bGwsImNyZWF0ZWRBdCI6IjIwMjYtMDMtMDdUMDA6MDI6MDAuMDAwWiJ9.BpUjMx3_zq5q-bxOQN0pl4tfgf_uZIt97r7fM_dukJr--zB0g7sxG9IcBsy0RR3P_DtIK4GB17ikwDUDDHnuAwContent Operation CID:
bafyreibs3vlvainfjfuet6x4uds3pivbmbohy7f64iegbuw3gpsuqtma6iContent chain: Update
Content Update Payload:
{
"version": 1,
"type": "update",
"did": "did:dfos:cnnnft9f8a2rn938d6nkz38r847v2kr",
"previousOperationCID": "bafyreibs3vlvainfjfuet6x4uds3pivbmbohy7f64iegbuw3gpsuqtma6i",
"documentCID": "bafyreiaoinzo2ai4hx56b7244zahnfqmgurcd3rppqbawhv32xzlvct5m4",
"baseDocumentCID": "bafyreie6xfkrtwax2dq5gdw3rpsurz2glsduxycfhk7jjllewiwivkkafu",
"createdAt": "2026-03-07T00:03:00.000Z"
}Updated document (flat content object):
{
"$schema": "https://schemas.dfos.com/post/v1",
"format": "short-post",
"publishedAt": "2026-03-07T00:02:00.000Z",
"title": "Hello World (edited)",
"body": "Updated content.",
"credits": [
{
"did": "did:dfos:cnnnft9f8a2rn938d6nkz38r847v2kr",
"label": "author"
}
]
}Document CID (edited):
bafyreiaoinzo2ai4hx56b7244zahnfqmgurcd3rppqbawhv32xzlvct5m4Content Update CID:
bafyreied5cjgjjt2pdz52k6pgipcjg3i4xl7txbrbdedscejvqhtgltxdiContent chain verified state
Content ID: 8n8fnzhrrefkrde6h72kfvff43r8c63
Genesis CID: bafyreibs3vlvainfjfuet6x4uds3pivbmbohy7f64iegbuw3gpsuqtma6i
Head CID: bafyreied5cjgjjt2pdz52k6pgipcjg3i4xl7txbrbdedscejvqhtgltxdiIndependent verification
The checklist is executable. The suites in
packages/protocol-verify/
load vectors.json
and re-derive every value above in five languages: multikey decoding, JWS
verification, CID and DID derivation, the rotation's key proof, dag-cbor number
encoding, and the credential and delegated-chain examples in
packages/dfos-protocol/examples/.
Source
The reference TypeScript implementation is
packages/dfos-protocol/,
self-contained with zero monorepo dependencies, published as
@metalabel/dfos-protocol.
Its Go twin is
packages/dfos-protocol-go/.
The five-language cross-verification suites are
packages/protocol-verify/.
Related specifications
- DID Method:
did:dfos: the W3C DID method registration for identity chains - Content Model: document schemas and the credit vocabulary
- Credentials: authorization credentials and revocation
- Relay: the HTTP relay: read and write contracts, ingestion, profiles, and the content plane
- Integrations: sign-in, API authentication, origin binding, and key ceremonies
- Guarantees: what holds without trusting a server, what is a chosen view, and what the operator can read