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:

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:

  1. issuer_signature.alg == "ed25519".
  2. issuer_signature.kid starts with <root_did># (issuer is a key owned by root_did).
  3. issuer_signature.sig verifies against the public key resolved for issuer_signature.kid via §4.
  4. (Resolution-time) now_s is within [0, expires_at_s].
  5. (Resolution-time) Neither issuer_signature.kid nor agent_kid is 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:

  1. Try the DID registry (/did/{did} lookup).
  2. Fall back to the test derivation if allow_test_fallback is on.
  3. 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)

9. Open questions / explicitly deferred


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:

  1. The substrate operator (or an authorized DID controller via a future delegated-control flow) calls POST /attestation/v1/did/deactivate with {"did": "<did:deos:...>", "reason": "<text>"}.
  2. Auth: bearer admin token (matches the substrate's other admin mutations).
  3. 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>"
}
  1. The deactivation event is logged in admin-events.jsonl (Sprint 2.2) and may be anchored in the receipt log per §11.
  2. 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:

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:

  1. On POST /did/register (or POST /did/derive, or POST /did/deactivate), the substrate computes the canonical bytes of the new document and BLAKE3-hashes them.
  2. 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>", ... }]
}
  1. The substrate's GET response for the DID (both the legacy and the resolve forms) carries an additional metadata field latest_did_event_id referencing 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:

  1. Local substrate registry — the current substrate's dids map.
  2. Federation manifest members — substrates listed in the local substrate's federation manifest, in declared order.
  3. did:web bridge — for DIDs of the form did:deos:X, attempt did:web:<substrate-host>:did:deos:X resolution.
  4. 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:


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:

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.