DID Guardian Method Specification

Version 0.1, draft. Modeled closely on did:plc's own method specification -- a real, proven, 40-million-identifier-scale design for a self-authenticating, directory-resolved DID with key rotation -- adopted wherever it already solves something this method also needs, extended specifically where it doesn't: group guardianship as a first-class, cryptographically-enforced relationship, and domain-qualified identifiers in place of a single, central directory.

1. Motivation

Every identity needs to be controllable by its own keys. Some identities also need a second, distinct kind of control: an external party capable of recovering, revoking, or transferring the identity if its own keys are lost, compromised, or simply because responsibility for it is changing hands. Family law has a name for this: guardianship -- a relationship that starts for a reason, is mutable, is transferable by mutual consent, can be shared among several guardians at once, and is meant to end. Nothing in did:plc, did:web, or the W3C DID Core spec itself models this relationship; each leaves "who controls this identity" as a single, flat, undifferentiated set of keys.

This method treats the governed identity as a ward and the overseeing party or parties as its guardian(s) -- terms chosen specifically because they carry no assumption about what kind of thing is on either side. A person can be ward to another person's guardianship (a board overseeing an executive's company-issued identity), a person's identity can guard an autonomous agent's, or an organization can guard an account it issues and may need to recover. The method doesn't need to know or care which.

2. Identifier syntax

did-guardian-identifier = "did:guardian:" name 1*( ":" domain-label )
name        = 3*32(ALPHA / DIGIT / "-")
              ; lowercase only; must start and end with ALPHA/DIGIT, not "-";
              ; no consecutive "--"
domain-label = 1*63(ALPHA / DIGIT / "-")
              ; an ordinary DNS label

Examples: did:guardian:alice:acme:example, did:guardian:fred:example:org, did:guardian:ops-bot:accounts:example:com (a subdomain registrar).

The identifier is name-first: the ward's own chosen name comes immediately after the method name, followed by the registering domain's labels in ordinary left-to-right reading order -- alice, then acme, then example, reading the same way alice@acme.example does. This is a deliberate choice over the reverse-DNS convention did:web itself popularized (did:web:example:acme is not how did:web actually works, but the general reverse-label convention is common elsewhere, e.g. Java package names): reverse-label ordering earns its keep when one shared, central directory needs to prefix-group entries from many different registrars together for efficient indexing. Resolution in this method is already partitioned by registrar (§5 below) -- each registrar's own server only ever serves its own namespace, by construction, independent of how the identifier string reads -- so that benefit doesn't apply here, leaving plain readability as the deciding factor.

Names are scoped per registrar, not globally flat -- and that's not just a syntax detail, it's what makes a real, common situation expressible at all. Take did:guardian:alice:acme:example: Alice is Acme's CEO, and a public company's own executive identity is exactly the case this method's group guardianship was built for -- Acme's board of directors is the guardian set, holding real recovery power (if Alice's key is lost or compromised, or her role simply ends) under whatever quorum Acme's own GuardianPolicy requires, the same governance relationship a board already has over the role, now expressed over the identity. Alice also keeps did:guardian:alice:inkwell:example -- a personal account on a hobby art-sharing site, under a completely different registrar, with no guardian at all. These are two entirely separate, unrelated DIDs: different keys, different guardians (or none), different independent history. That separation is the point, not a limitation -- Alice's corporate identity carries Acme's own governance and accountability; her personal one carries none of it and nobody at Acme can see it, let alone guard it, which is exactly why it's a separate identity rather than a second "account" hanging off the first. Unifying the two later, if Alice ever wanted to, needs its own explicit, consent-based mechanism; see §8's "cross-registrar identity unification" item -- it is deliberately not possible by default.

3. DID Document shape

Resolving did:guardian:alice:acme:example returns an ordinary W3C DID Core document:

 1{
 2  "@context": ["https://www.w3.org/ns/did/v1"],
 3  "id": "did:guardian:alice:acme:example",
 4  "verificationMethod": [
 5    { "id": "did:guardian:alice:acme:example#key-1", "type": "Multikey", "controller": "did:guardian:alice:acme:example", "publicKeyMultibase": "z..." }
 6  ],
 7  "authentication": ["did:guardian:alice:acme:example#key-1"],
 8  "assertionMethod": ["did:guardian:alice:acme:example#key-1"],
 9  "service": [
10    { "id": "did:guardian:alice:acme:example#registrar", "type": "GuardianRegistrarService", "serviceEndpoint": "https://acme.example" }
11  ],
12  "alsoKnownAs": []
13}

Only the operational tier of rotation keys (§4) is reflected in verificationMethod/authentication. The guardian tier, GuardianPolicy, and the full operation log are method-internal state needed to produce this document, never part of the document itself -- the same way did:plc's own rotationKeys are omitted from its presented document. Nothing in this document's own shape distinguishes any particular kind of ward from any other.

4. The operation log

Every did:guardian identity is defined by an append-only, hash-chained, signed log of operations -- the actual source of truth; the DID Document is always derived by replaying it, never stored directly. Directly adopted from did:plc: each operation (except genesis) names the CID of the immediately preceding operation (prev), forming a verifiable chain; the DID itself is derived from the genesis operation's own hash, not assigned by the registrar, so the registrar cannot forge history even though it's the party trusted to order operations and resist denial-of-service against the log. Serialization, hashing, and signing follow did:plc directly and unchanged: DAG-CBOR encoding, ECDSA signatures (low-S form, base64url-encoded), prev references as CIDv1 (base32 multibase, dag-cbor codec, sha-256 multihash).

4.1 custodial_create (genesis)

 1{
 2  "type": "custodial_create",
 3  "name": "alice",
 4  "operationalKeys": ["did:key:z6Mk..."],
 5  "verificationMethods": { "key-1": "did:key:z6Mk..." },
 6  "services": { "registrar": { "type": "GuardianRegistrarService", "endpoint": "https://acme.example" } },
 7  "registrar": { "did": "did:guardian-registrar:acme:example", "sig": "..." },
 8  "outgoing": [],
 9  "incoming": ["did:guardian:acme-board:acme:example"],
10  "prev": null,
11  "sigs": { "did:guardian:acme-board:acme:example": "..." }
12}

did:guardian:<name>:<domain> becomes valid once this operation is accepted by the registrar (§5) -- the registrar independently checks the name is free within its own namespace (no cross-registrar negotiation needed, since names are scoped per registrar, not global).

4.2 custodial_rotate (operational key rotation)

Identical in shape and purpose to did:plc's own regular operation: a signed operation naming the full new state of operationalKeys/verificationMethods/services, with prev pointing at the latest valid operation CID. Signed by any current operational-tier key -- did:plc's own any-single-key authority, unchanged. Never touches guardian-tier content; that has its own dedicated operations (§4.3, §4.4) specifically so an ordinary rotation can never, even by a bug, accidentally reach into guardianship.

4.3 guardian_transfer

 1{
 2  "type": "guardian_transfer",
 3  "outgoing": ["did:guardian:troy:example:com"],
 4  "incoming": ["did:guardian:caleb:example:com"],
 5  "prev": "bafyrei...",
 6  "sigs": {
 7    "did:guardian:troy:example:com": "...",
 8    "did:guardian:caleb:example:com": "..."
 9  }
10}

outgoing must be a non-empty subset of the current guardian set (empty for genesis only -- §4.1); incoming becomes the new guardian set wholesale (not an add/remove delta -- a three-guardian set transferring to two-of-the-original-plus-one-new is expressed as one operation naming the complete new set).

Signature requirement, stated precisely: sigs carries one signature per affected party, keyed by their own DID, each checked against that DID's own independently-resolved keys -- never against the ward's own keys. The set of required signers is outgoing ∪ (incoming \ current), where current is the guardian set immediately before this operation: every guardian being removed, plus every guardian who is genuinely new -- not already a guardian beforehand. A guardian who continues unaffected (already in current, still in incoming, not named in outgoing) doesn't need to re-sign. Telling "continuing" apart from "newly joining" genuinely requires knowing current, not just outgoing/incoming -- both look identical ("absent from outgoing, present in incoming") without it. Each signature covers the full operation content (outgoing, incoming, prev together), binding it to this exact transfer so it can't be lifted and replayed to vouch for a different one.

This closes two forgery directions by construction, not by convention: a ward cannot be made to falsely name an unwilling party as its guardian (that party's own signature is structurally required), and one co-guardian cannot unilaterally strip a different co-guardian's standing (the co-guardian being removed is named in outgoing and must sign too).

4.4 guardian_relinquish

1{
2  "type": "guardian_relinquish",
3  "prev": "bafyrei...",
4  "sigs": { "did:guardian:troy:example:com": "..." }
5}

Sets the guardian set to empty -- full, unattenuated standing for the ward. sigs must meet the current GuardianPolicy.removal_threshold (§4.6) among current guardians; by default this is majority, deliberately stricter than the any default for ordinary recovery actions, since this ends the relationship entirely rather than exercising it. Whether the ward's own signature can ever substitute for or supplement the guardians' is left explicitly open (§8) -- it decides who actually gets to call a ward ready for full autonomy, and this version doesn't resolve it. Once the guardian set is empty, only custodial_rotate (self-signed by the ward's own current operational key) and a future guardian_transfer re-establishing a guardian remain available; there is no operation in this version for re-entering guardianship from zero, itself an open question (§8), not a settled permanent rule.

4.5 custodial_tombstone (deactivate)

{ "type": "custodial_tombstone", "prev": ..., "sigs": {...} } -- meeting GuardianPolicy.tombstone_threshold (default any, i.e. any single current rotation-tier key, operational or guardian). Deactivates the DID; resolution still returns the full historical log (§7), but no new operations are accepted afterward except a recovery nullification within the recovery window (§4.7).

4.6 GuardianPolicy and consensus thresholds

Guardian actions are gated by a small, explicit, per-ward policy, not one hardcoded rule for everything:

1{
2  "recovery_threshold": "any",
3  "tombstone_threshold": "any",
4  "removal_threshold": "majority",
5  "policy_threshold": "majority"
6}

Each threshold is one of:

Defaults are deliberately loose, not strict. recovery_threshold and tombstone_threshold default to any so that small guardian sets stay low-friction for exactly the moments speed matters most (an operational key may already be actively compromised). removal_threshold -- forcibly removing a guardian without their own consent signature, the deadlock-escape mechanism for an unresponsive or uncooperative co-guardian -- defaults to majority instead, since bypassing someone's own consent is a meaningfully bigger power than ordinary mutual transfer and shouldn't inherit a loose default by accident.

The policy governs changes to itself. Updating GuardianPolicy requires meeting whichever threshold policy_threshold currently names -- defaulting to majority -- so weakening your own bar (e.g. dropping removal_threshold to any right before exploiting a single compromised guardian key) is gated exactly like any other guardian action, never a free unilateral move. This mirrors the same self-referential pattern used elsewhere for policy documents that govern their own editing: a policy is never editable more loosely than its own current rule allows.

Verification is a simple count-and-check, not an enumeration. For any signed operation, take the signatures actually submitted, check each is valid and from a DID currently eligible under the relevant set, count them, and compare to the threshold. O(N), never a search over possible cohorts -- the quorum-intersection property above is a structural guarantee that makes reasoning about other cohorts unnecessary at verification time.

4.7 Key rotation authority and recovery

Directly adopted from did:plc, applied to a tiered rotationKeys structure rather than one flat list:

Guardianship never grants the power to impersonate the ward. A guardian's recovery action is always signed by the guardian's own, distinct key -- never the ward's -- so it remains a separately attributable, auditable operation in the replayed log, never indistinguishable from the ward acting on its own. This holds as a structural guarantee as long as guardian and ward key material is genuinely kept separate (a custody/deployment concern no protocol can enforce, same as any PKI -- the method guarantees attributability of whichever key actually signed, not what a deployer chooses to do with key material outside the protocol).

Changing who is in the guardian tier is not ordinary rotation authority -- it's gated by guardian_transfer's own per-party consent rule (§4.3), deliberately never reachable via a single guardian's ordinary recovery power, specifically so one guardian can't exploit recovery authority to quietly redefine the guardian set itself.

4.8 Guardian status is a live reference, not a frozen key

A ward's guardian entries are DID references, resolved fresh at the time of each new signature -- never a snapshot of a specific key taken when guardianship was established. A guardian rotating their own keys (an ordinary custodial_rotate on their own identity, unrelated to any ward) never locks them out of any ward relationship: their next signature on a ward's operation is simply checked against whatever their current keys now are. The one real failure mode this doesn't cover is full deactivation of the guardian's own identity (no current key left to resolve at all) -- a real, already-acknowledged risk for a sole-guardian ward, not a new one.

One subtler, correct-but-worth-stating consequence: if a guardian's own key rotation is later nullified within their own recovery window (§4.7, applied recursively to their own identity), any ward-level action they signed in the interim using the now-reverted key should be treated as only provisionally valid until their own recovery window closes -- the same "not fully final until past the window" property did:plc already has for a single identity, propagating one hop across a guardianship relationship.

5. Resolution

Registrars operate independently -- there is no single, central directory for this method, unlike did:plc. Resolving did:guardian:alice:acme:example:

  1. Reconstruct the domain from the identifier's trailing labels, in order: acme.example.
  2. GET https://acme.example/.well-known/guardian/alice -- returns the current, derived DID Document (§3).
  3. GET https://acme.example/.well-known/guardian/alice/log -- returns the full, timestamped operation log.
  4. POST https://acme.example/.well-known/guardian/alice/operations -- submits one new signed operation.

Full validation requires replaying the entire log from genesis: checking the genesis hash matches the DID, verifying every operation's signatures against whichever keys/guardians had authority at that point in the log, and applying nullification rules -- the registrar's own say-so is never the root of trust, the self-certifying log is, exactly as did:plc already establishes for its own single directory.

6. Deactivation and data permanence

The full operation log remains permanently, publicly resolvable even after custodial_tombstone -- deactivation stops a ward from acting, it does not erase its history. A name is never reclaimed into the available pool within its registrar after deactivation, for the same reason a real person's old handle shouldn't become available for someone else to impersonate them with later.

7. Security and privacy considerations

Adopted directly from did:plc, unchanged in substance:

New to this method:

8. What this version leaves open