DEOS DID Method (did:deos) — v1 Specification
Status: v1.0 — published 2026-05-02 (v1.1 normative additions appended in §10–§14, drafted 2026-05-05) Reference impl: Rust resolver in DEOS Computing's working repo (access-controlled during v1 alpha). Audit traceability: v1.1 additions close DID1.1-1, DID1.1-6, DID1.1-9, DID1.1-10, DID1.1-11, DID1.1-12 from the v1.1 backlog.
A did:deos:* DID resolves against a DEOS trust substrate that holds the published DID document. The document carries one or more Ed25519 verification methods. Agent DIDs additionally carry an embedded derivation chain that proves the agent's authority back to a root DID via signed delegation.
Reference implementations through M9 used a deterministic BLAKE3-derived test key for every did:deos:* kid. M10 replaced that with real DID resolution; the test derivation remains as a backward-compatible fallback so prior demos and test vectors keep working.
This document is normative for v1.
1. Method syntax
did:deos:<identifier>
Where <identifier> is a free-form ASCII string (printable, no /, no
#). Examples:
did:deos:user-mayadid:deos:org-acmedid:deos:agent-maya-2026-04did:deos:substrate-alpha-1
The substrate is the registry for did:deos — resolution targets a specific substrate. v1.0 expects callers to know which substrate is authoritative; v1.1 adds a federation manifest so a DID can resolve across substrates.
2. DID document
W3C-aligned with a small deos_derivation extension for agent DIDs.
2.1 Root DID document
{
"@context": ["https://www.w3.org/ns/did/v1"],
"id": "did:deos:user-maya",
"verificationMethod": [
{
"id": "did:deos:user-maya#sig-1",
"type": "Ed25519VerificationKey2020",
"controller": "did:deos:user-maya",
"publicKeyBase64Url": "<base64url-nopad of 32 bytes>"
}
],
"authentication": ["did:deos:user-maya#sig-1"],
"assertionMethod": ["did:deos:user-maya#sig-1"]
}
2.2 Agent DID document
{
"@context": ["https://www.w3.org/ns/did/v1"],
"id": "did:deos:agent-maya-2026",
"verificationMethod": [
{
"id": "did:deos:agent-maya-2026#sig-1",
"type": "Ed25519VerificationKey2020",
"controller": "did:deos:agent-maya-2026",
"publicKeyBase64Url": "<base64url-nopad of agent's 32-byte pubkey>"
}
],
"authentication": ["did:deos:agent-maya-2026#sig-1"],
"assertionMethod": ["did:deos:agent-maya-2026#sig-1"],
"deos_derivation": {
"root_did": "did:deos:user-maya",
"chain": [
{
"schema": "deos_agent_derivation_v1",
"root_did": "did:deos:user-maya",
"agent_did": "did:deos:agent-maya-2026",
"agent_kid": "did:deos:agent-maya-2026#sig-1",
"agent_public_key_b64u": "...",
"issued_at_ns": 1730000000000000000,
"expires_at_s": 2500000000,
"issuer_signature": {
"kid": "did:deos:user-maya#sig-1",
"alg": "ed25519",
"sig": "<base64url-nopad of 64 bytes>"
}
}
]
}
}
3. Derivation entry
A derivation entry is a substrate-published, root-signed delegation asserting that an agent's pubkey is authorized by a root DID.
| Field | Required | Notes |
|---|---|---|
schema |
yes | MUST equal "deos_agent_derivation_v1". |
root_did |
yes | The root DID. |
agent_did |
yes | The DID being authorized. |
agent_kid |
yes | The <agent_did>#fragment that this entry binds. |
agent_public_key_b64u |
yes | base64url-nopad of 32-byte Ed25519 pubkey. |
issued_at_ns |
yes | unix nanoseconds. |
expires_at_s |
yes | unix seconds — derivation is invalid past this time. |
issuer_signature |
yes | Inline signature object. |
3.1 Signature payload
sig_payload = "deos-agent-derivation-v1:" || JCS(entry minus { issuer_signature })
sig = base64url-nopad(Ed25519_sign(root_secret, sig_payload))
3.2 Validity rules
A derivation entry is valid iff:
issuer_signature.alg == "ed25519".issuer_signature.kidstarts with<root_did>#(issuer is a key owned byroot_did).issuer_signature.sigverifies against the public key resolved forissuer_signature.kidvia §4.- (Resolution-time)
now_sis within[0, expires_at_s]. - (Resolution-time) Neither
issuer_signature.kidnoragent_kidis in the substrate's revocation set.
4. Resolution
GET /attestation/v1/did/{did}
Returns the DID document if registered, 404 otherwise.
4.1 Resolving a verifying key for a kid
resolve(kid):
1. did = kid.split('#')[0]
2. fetch /attestation/v1/did/{did}
3. if 200: walk verificationMethod, find entry with id == kid,
decode publicKeyBase64Url
4. if not found in registry AND test-fallback is enabled:
return BLAKE3("deos-test-key-v1:" || did) as Ed25519 secret seed
5. else: return None (verifier rejects)
The test-fallback path is reference-impl-only. Production substrates
disable it by setting allow_test_fallback: false on their HybridDidResolver.
5. Server endpoints
5.1 POST /attestation/v1/did/register
Request:
{ "did": "did:deos:user-maya", "public_key_b64u": "<...>" }
The substrate creates and persists a root DID document per §2.1 and returns it. Returns 409 if the DID is already registered.
In v1 this endpoint is unauthenticated. Production deployments MUST gate it (substrate-operator approval, or proof-of-key-control via challenge-response).
5.2 POST /attestation/v1/did/derive
Request:
{ "derivation_entry": { /* signed derivation entry per §3 */ } }
The substrate verifies the entry's signature using its current resolver (so the root key must be registered, OR test-fallback is on). On success, the substrate creates and persists the agent DID document per §2.2 and returns it.
5.3 GET /attestation/v1/did/{did}
Returns the DID document if registered, else 404.
6. Receipt-time integration
When the substrate validates a UCAN-shaped capability chain on
POST /attestation/v1/append,
it uses the same HybridDidResolver for every chain token's s.kid:
- Try the DID registry (
/did/{did}lookup). - Fall back to the test derivation if
allow_test_fallbackis on. - If the kid is in the revocation set (§7), reject the chain.
The external verifier mirrors this — for each kid in the chain, it issues a
HTTP GET /attestation/v1/did/{did} and falls back to the test derivation
only if the request 404s.
7. Key revocation
POST /attestation/v1/revoke with token_hash = <kid> revokes a key.
The resolver's revoked_kids set is rebuilt from the revocation log on
every chain validation, so a revoked kid resolves to None and any
receipt whose chain involves the revoked kid is rejected at append /
verify time.
Root-key revocation cascades to derived agent keys: the agent's verification depends on the agent's kid being resolvable AND the derivation entry's signature being verifiable. Revoking the root kid breaks the derivation entry's signature, which breaks the agent's authority.
8. Acceptance criteria (M10, this milestone)
- Spec doc published.
- Resolution:
GET /attestation/v1/did/{did}returns the DID document withverificationMethod,authentication,assertionMethod, and (for agent DIDs)deos_derivation.chain. - Derivation: a chain of two entries (root → agent) round-trips
through
POST /did/deriveand the derivation signature verifies. - Receipt format unchanged —
subjectandprincipalare DIDs that now resolve through the DID registry rather than only test derivation. - Revoking a root key invalidates derived chains: post-revoke
POST /appendreturns 400 withunknown kid. (Shown in step 10 of the demo.) - 7 unit tests in
did.rscovering: round-trip, derivation sign + verify, tamper detection, kid-not-issuer-owned, hybrid resolver preferring registered docs, hybrid resolver fallback, revoked-kid rejection.
9. Open questions / explicitly deferred
- Cross-substrate federation. v1 expects the caller to know which substrate is authoritative for a DID. v1.1 adds a federation manifest (a list of substrates a verifier consults in order).
- Multi-step derivation chains. v1's
deos_derivation.chainadmits arbitrary depth in the JSON; v1's verifier only checks the leaf entry. v1.1 walks the full chain validating each entry against its parent. - Key rotation as KERI-style events. v1 has an implicit rotation via "register a new DID with the new key + revoke the old kid." v1.1 adds an explicit rotation event log inside the DID document for forensic traceability.
- Service endpoints in DID documents. v1 documents only carry keys.
v1.1 adds a
servicearray (e.g. messaging endpoints, attestation endpoints, agent dispatch URLs). - Authenticated registration. v1 leaves
/did/registeropen; v1.1 requires either substrate-operator approval or proof-of-secret-key-control via a challenge-response flow. - DID document anchoring. v1.1 anchors DID documents to the receipt log so changes are forensically recoverable.
- Cooperative resolution between SDKs. Both Rust and TS SDKs in
v1.1 will expose a public
resolveDid(kid)helper; today it's internal to the verification flow.
10. v1.1 normative additions: W3C DID-Core compliance
This section is normative for v1.1 substrates. It brings did:deos
into strict alignment with W3C DID Core 1.0
and DID Resolution 1.0.
10.1 Document-level controller (DID1.1-10)
A DID document MUST carry a top-level controller field naming the DID(s)
authorized to update the document. For self-controlled DIDs (the common
case) this equals the document's id. For agent DIDs derived from a
root, controller MUST equal root_did so that root-key compromise
detection covers the agent.
{
"@context": ["https://www.w3.org/ns/did/v1"],
"id": "did:deos:agent-maya-2026",
"controller": "did:deos:user-maya",
"verificationMethod": [...],
...
}
The verificationMethod's per-method controller field stays unchanged.
The new top-level controller is additive.
10.2 Deactivation (DID1.1-9)
A DID may be deactivated. Per W3C DID Core §7, a deactivated DID's
document MUST resolve as {deactivated: true} in didDocumentMetadata,
and the document body itself MAY be replaced by a tombstone form
(only id + deactivated: true). Verifiers MUST refuse to validate
signatures using a key from a deactivated DID for any operation
post-deactivation.
Deactivation procedure:
- The substrate operator (or an authorized DID controller via a future
delegated-control flow) calls
POST /attestation/v1/did/deactivatewith{"did": "<did:deos:...>", "reason": "<text>"}. - Auth: bearer admin token (matches the substrate's other admin mutations).
- The substrate replaces the DID document on disk with a deactivation tombstone:
{
"@context": ["https://www.w3.org/ns/did/v1"],
"id": "<did>",
"deactivated": true,
"deactivated_at_ns": <unix nanoseconds>,
"reason": "<text>"
}
- The deactivation event is logged in
admin-events.jsonl(Sprint 2.2) and may be anchored in the receipt log per §11. - Future resolutions return the tombstone with
didDocumentMetadata.deactivated: true.
Reactivation is NOT allowed in v1.1. A DID, once deactivated, stays so. A controller wishing to reissue MUST register a fresh DID.
10.3 DID Resolution v1.0 wrapper (DID1.1-11)
GET /attestation/v1/did/resolve/{did} (NEW endpoint) returns the
W3C DID Resolution v1.0 result triple:
{
"didResolutionMetadata": {
"contentType": "application/did+json"
},
"didDocument": { /* per §10.1 */ },
"didDocumentMetadata": {
"created": "<ISO 8601 timestamp, optional>",
"updated": "<ISO 8601 timestamp, optional>",
"deactivated": <bool>,
"deactivated_at_ns": <unix ns, only if deactivated>
}
}
For backward compatibility, the legacy endpoint
GET /attestation/v1/did/{did} is preserved and returns the bare
DID document (no triple). v1.2 may deprecate the legacy form.
If the DID is unknown, the wrapper returns:
{
"didResolutionMetadata": {
"error": "notFound",
"errorMessage": "did not registered with this substrate"
},
"didDocument": null,
"didDocumentMetadata": null
}
with HTTP status 404.
10.4 did:web bridge (DID1.1-12)
A v1.1 substrate publishes each registered DID as a did:web-resolvable
document at:
https://<substrate-host>/.well-known/did/<did:deos identifier>.json
The bridge URL is announced in the DID document's service array:
"service": [{
"id": "<did>#did-web-bridge",
"type": "DIDWebBridge",
"serviceEndpoint": "https://substrate.example/.well-known/did/<id>.json"
}]
A did:web:substrate.example:did:deos:user-maya-style alias MAY be constructed by a verifier and resolves to the same document, giving did:deos a fallback path through did:web for verifiers that don't yet implement did:deos.
The bridge document is identical to what /attestation/v1/did/resolve/{did}
returns (just the didDocument portion, not the wrapper). Cache headers
on the well-known endpoint SHOULD allow short caching (1 hour) since DID
documents change rarely.
10.5 Identifier syntax constraints (DID1.1-13)
The <identifier> portion of a did:deos: DID is constrained:
- Length: 1 to 256 characters.
- Charset: ASCII printable, no whitespace, no
/, no#, no?. - Normalization: implementations MUST treat identifiers as case-sensitive and MUST NOT apply Unicode normalization (the identifier is already ASCII).
Out-of-range identifiers MUST be rejected at registration time.
11. v1.1 normative additions: DID document anchoring (DID1.1-6)
DID documents are MUTABLE state — the substrate's
/attestation/v1/did/register endpoint can overwrite an existing
document (subject to authentication, DID1.1-5). Without anchoring,
a substrate operator could rewrite a DID document and erase the
prior content from any verifier that doesn't have a cached copy.
A v1.1 substrate anchors every DID document update in the receipt log:
- On
POST /did/register(orPOST /did/derive, orPOST /did/deactivate), the substrate computes the canonical bytes of the new document and BLAKE3-hashes them. - A signed "did_event" leaf is appended to the receipt MMR with
schema
deos_did_event_v1:
{
"v": 1,
"schema": "deos_did_event_v1",
"issuer_did": "did:deos:substrate-alpha-1",
"tree_id": "alpha-log",
"did": "<the affected DID>",
"event_type": "register" | "derive" | "deactivate" | "update",
"document_hash_b3": "<lowercase hex>",
"issued_at_ns": <unix ns>,
"signatures": [{ "kid": "<substrate kid>", ... }]
}
- The substrate's GET response for the DID (both the legacy and
the resolve forms) carries an additional metadata field
latest_did_event_idreferencing the most recent did_event leaf.
A verifier auditing a DID's history walks the receipt MMR for all
leaves with schema == "deos_did_event_v1" and did == <target>,
producing a tamper-evident timeline of every change.
This makes silent DID document rewrites detectable: a verifier holding the prior latest_did_event_id checks the MMR for the expected leaf, and any rewrite the operator omits from the log shows up as a hash mismatch.
12. v1.1 normative additions: Federation manifest (DID1.1-1)
A did:deos:user-maya is registered with a specific substrate, but
verifiers in v1.1 frequently need to resolve DIDs across substrates
without knowing in advance which one is authoritative.
A v1.1 substrate MAY publish a federation manifest at:
GET /attestation/v1/federation
Returning:
{
"federation_id": "<URL-safe slug>",
"members": [
{
"issuer_did": "did:deos:substrate-alpha-1",
"url": "https://alpha.substrate.example",
"region": "us-east-1",
"added_at_ns": 1746201600000000000
},
{
"issuer_did": "did:deos:substrate-beta-1",
"url": "https://beta.substrate.example",
"region": "eu-west-1",
"added_at_ns": 1746288000000000000
}
],
"signatures": [
{ "kid": "<federation operator kid>", "alg": "ed25519", "sig": "..." }
]
}
A verifier resolving an unknown DID iterates members in order,
calling each substrate's /attestation/v1/did/resolve/{did} until
one returns a didDocument. The federation manifest itself is
signed by a federation-operator key; verifiers SHOULD pin the
federation_id and operator kid out-of-band.
Federation membership changes are append-only: members can be added
but not silently removed. Removal requires publishing a signed
federation_removal_v1 event that gets anchored in each member
substrate's receipt log.
v1.1 stops here; v1.2 adds witness-based federation health checks (members cross-attest each other's tree heads via the log-gossip protocol, MMR1.1-4).
13. v1.1 normative additions: Resolver caveats
13.1 HybridDidResolver hierarchy
The reference resolver consults sources in this order:
- Local substrate registry — the current substrate's
didsmap. - Federation manifest members — substrates listed in the local substrate's federation manifest, in declared order.
- did:web bridge — for DIDs of the form
did:deos:X, attemptdid:web:<substrate-host>:did:deos:Xresolution. - Test fallback (for DEOS demos / fixtures only): a deterministic
BLAKE3-derived test key per DID. Production substrates MUST set
allow_test_fallback: false(CLI flag in v1.1).
A deactivated DID returned by any step MUST be honored — verifiers MUST NOT fall through to lower-priority resolvers looking for an active document.
13.2 Caching and TTL
Verifiers SHOULD cache resolved DID documents subject to a TTL of at most 1 hour. Cache invalidation events:
- The local substrate publishes a new
latest_did_event_idfor the cached DID. - The federation manifest changes (add or removal events).
- An explicit cache-clear admin action (out of substrate scope; client-side concern).
14. v1.1 normative additions: Security considerations
(Supplements the v1.0 security considerations elsewhere in this document.)
14.1 Deactivation authority
The deactivation endpoint requires a substrate admin token. In v1.1
this means the substrate operator can unilaterally deactivate any DID
they registered. v1.2 will add controller-driven deactivation: the
DID's controller key signs a deactivation request, which the substrate
verifies before processing. Until then, operator deactivation is
recorded in admin-events.jsonl for audit.
14.2 Federation manifest tampering
A malicious substrate operator could publish a federation manifest with attacker-controlled members. Mitigations:
- The federation_id and operator kid are pinned out-of-band by verifiers (e.g. configuration of the verifier's trust anchors).
- Federation members cross-publish each other's manifest entries, detectable by comparison.
- v1.2 log gossip extends to federation manifest events.
14.3 did:web bridge spoofing
The bridge URL is operator-controlled. A compromised bridge could
return a different document than the substrate's authoritative
record. Verifiers SHOULD prefer /did/resolve/{did} over the bridge
when they know which substrate to ask; the bridge is a fallback path.
14.4 DID document anchoring race
Between writing the new document on disk and appending the did_event leaf, the substrate is in an inconsistent state. The implementation MUST persist the leaf BEFORE the document update, in the same way key rotation persists rotation events before swapping key.json. If the substrate crashes mid-update, the audit trail is intact but the document may be the old version; manual recovery procedure is documented in the operator runbook.