did:zu Method Specification

Version: v0.1.0 (October 2026)

Derived from: the did:plc Method Specification v0.3.0 (December 2025)

Maintained by: Zuzalu

DID Zu is a self-authenticating DID which is strongly-consistent, recoverable, and allows for key rotation.

An example DID is: did:zu:ewvi7nxzyoun6zhxrhs64oiz

Control over a did:zu identity rests in a set of reconfigurable rotation keys pairs. These keys can sign update operations to mutate the identity (including key rotations), with each operation referencing a prior version of the identity state by hash. Each identity starts from an initial genesis operation, and the hash of this initial object is what defines the DID itself (that is, the DID URI identifier string). A central directory server collects and validates operations, and maintains a transparent log of operations for each DID.

Relationship to did:plc

did:zu is the did:plc method, v0.3.0, with a different method name and a different directory. The operation formats, serialization, signing, hashing, recovery rules and directory HTTP API are those of did:plc v0.3.0, unchanged. The only differences are:

Names that are part of the operation data are kept as in did:plc, so that operations remain byte-for-byte identical: the operation type values are still plc_operation, plc_tombstone and create.

How it works

The metadata associated with an active did:zu identifier at any point in time is listed below. The encoding and structure differs somewhat from DID document formatting and semantics, but this information is sufficient to render a valid DID document.

Every update operation to the DID identifier, including the initial creation operation (the genesis operation), contains all of the above information, except for the did field. The DID itself is generated from a hash of the signed genesis operation (details described below), which makes the DID entirely self-certifying. Updates after initial creation contain a pointer to the most-recent previous operation (by hash).

Operations are signed and submitted to the central directory server over an un-authenticated HTTP request. The directory validates operations against any and all existing operations on the DID (including signature validation, recovery time windows, etc), and either rejects the operation or accepts and permanently stores the operation, along with a server-generated timestamp.

A special operation type is a “tombstone”, which clears all of the data fields and permanently deactivates the DID. Note that the usual recovery time window applies to tombstone operations.

Note that rotationKeys and verificationMethods (signing keys) may have public keys which are re-used across many accounts. There is not necessarily a one-to-one mapping between a DID and either rotation keys or signing keys.

Only secp256k1 (“k256”) and NIST P-256 (“p256”) keys are currently supported for rotation keys, whereas verificationMethods keys can be any syntactically-valid did:key.

Use with AT Protocol

AT Protocol accepts did:plc and did:web as account DIDs; did:zu identifiers cannot be used as atproto account DIDs. As with did:plc, the entries placed in verificationMethods, alsoKnownAs and services are defined by the applications that use the method. The examples below keep the atproto-style entries used in the did:plc specification, for comparison only.

Operation Serialization, Signing, and Validation

There are a couple of variations on the operation data object schema. The operations are also serialized both as simple JSON objects, or binary DAG-CBOR encoding for the purpose of hashing or signing.

A regular creation or update operation contains the following fields:

A tombstone operation contains:

There is also a deprecated legacy operation format, supported only for creation (“genesis”) operations:

Legacy create operations exist in the history of the did:plc directory. The format is kept here unchanged so that the same validating software can process both methods. Conversion of the legacy format to “regular” operation format is relatively straight-forward. New DIDs must not be created with the legacy format (see “DID Creation”).

The process for signing and hashing operation objects is to first encode them in the DAG-CBOR binary serialization format. DAG-CBOR is a restricted subset of the Concise Binary Object Representation (CBOR), an IETF standard (RFC 8949), with semantics and value types similar to JSON.

As an anti-abuse mechanism, operations have a maximum size when encoded as DAG-CBOR. The current limit is 7500 bytes.

For signatures, the object is first encoded as DAG-CBOR without the sig field at all (as opposed to a null value in that field). Those bytes are signed using ECDSA-SHA256, and the signature value is encoded as follows:

  1. The signature value is represented as a pair of integers (r, s), as described in RFC 4754

  2. The signature is canonicalized in “low-S” form. This means that if s is greater than or equal to half the EC group order constant, the value of s is replaced by -s (modulo the EC group order). This process is described for the secp256k1 curve as part of Bitcoin BIP-0062, but this definition can be generalized to the NIST P-256 curve also.

  3. The (r, s) tuple is encoded to bytes in the same format as specified in the SubtleCrypto web API. This format is refered to by a variety of names, including “raw”, “compact”, “IEEE P1363”. For secp256k1 and NIST P-256 curves, it is concretely: 32 big-endian bytes representing integer r, followed by 32 big-endian bytes representing integer s.

  4. Those bytes are encoded as a string using base64url encoding, without equals-padding. Trailing padding bits MUST be set to zero, as described in RFC 4648 section 3.5

The sig field is then populated with the encoded signature string.

When verifying signatures, the above encoding requirements must be enforced strictly, with non-canonical encodings or “High-S” values rejected as invalid. Otherwise, it would be possible for the signature encoding to be modified (thus modifying the operation’s CID) without invalidating the signature.

For prev references, the SHA-256 of the previous operation’s bytes are encoded as a “CID”, with the following parameters:

Rotation keys are serialized as strings using did:key, and only secp256k1 (“k256”) and NIST P-256 (“p256”) are currently supported.

The signing keys (verificationMethods) are also serialized using did:key in operations. When rendered in a DID document, signing keys are represented as objects, with the actual keys in multibase encoding, as required by the DID Core specification.

Although verificationMethods signing keys can be of any key type (unlike rotation keys), they must still be syntactically valid. i.e. They must have a did:key: prefix, followed by a base58btc multibase string.

The DID itself is derived from the hash of the first operation in the log, called the “genesis” operation. The signed operation is encoded in DAG-CBOR; the bytes are hashed with SHA-256; the hash bytes are base32-encoded (not hex encoded) as a string; and that string is truncated to 24 chars to yield the “identifier” segment of the DID.

In pseudo-code: did:zu:${base32Encode(sha256(createOp)).slice(0,24)}

The method name is not part of the hashed bytes, so the same genesis operation yields the same 24-character identifier under did:zu and under did:plc.

Identifier Syntax

The DID Zu method name is zu. The identifier part is 24 characters long, including only characters from the base32 encoding set. An example is did:zu:yk4dd2qkboz2yv6tpubpc6co. This means:

In ABNF (RFC 5234):

zu-did         = "did:zu:" zu-specific-id
zu-specific-id = 24base32-char
base32-char    = %x61-7A / %x32-37 ; a-z, 2-7

Key Rotation & Account Recovery

Any key specified in rotationKeys has the ability to sign operations for the DID document.

The set of rotation keys for a DID is not included in the DID document. They are an internal detail of the method, and are stored in the operation log.

Keys are listed in the rotationKeys field of operations in order of descending authority.

The directory provides a 72hr window during which a higher authority rotation key can “rewrite” history, clobbering any operations (or chain of operations) signed by a lower-authority rotation key.

To do so, that key must sign a new operation that points to the CID of the last “valid” operation - ie the fork point. The directory will accept this recovery operation as long as:

Security Considerations

Directory Trust Model

The directory has a public endpoint to receive operation objects from any client (without authentication). The directory verifies operations, orders them according to recovery rules, and makes the log of operations publicly available.

The operation log is self-certifying, and contains all the information needed to construct (or verify) the the current state of the DID document.

Some trust is required in the directory. Its attacks are limited to:

The directory cannot forge operations: every accepted operation must carry a valid signature from a rotation key in effect at that point in the log, which any client can check (see “Audit Logs”).

Integrity and Replay

Each operation commits to its predecessor by CID (prev), and the signature covers prev. An operation therefore cannot be moved to another point in the history, or applied to another DID, without invalidating its signature. Signature encodings are checked strictly (canonical “low-S” form), so the CID of an operation cannot be changed without invalidating the signature.

Operations are submitted and served over HTTPS. Because operations are self-authenticating, transport security protects the confidentiality and availability of requests rather than the integrity of the data.

Recovery Window

After the 72 hour recovery window has passed, an operation signed by a lower-authority rotation key can no longer be nullified, even if it removed higher-authority keys. Holders of higher-authority (recovery) keys need to monitor the audit log of their DID to make use of the window.

Unverified Claims

The directory does not cross-validate alsoKnownAs or service entries in operations. This means that any DID can “claim” to have any identity, or to have an active account with any service (identified by URL). This data should not be trusted without bi-directional verification, for example by checking that the claimed name or service also points back to the DID.

Namespace Separation

Because the method name is not part of the hashed bytes, a genesis operation submitted to both the did:plc directory and the did:zu directory yields the same 24-character identifier under both methods, and the two histories can diverge afterwards. did:plc:<id> and did:zu:<id> are different DIDs. Software must not treat them as equivalent, and must resolve a did:zu identifier only against the did:zu directory.

Uniqueness

The identifier is the first 120 bits (24 base32 characters) of a SHA-256 hash of the signed genesis operation. A second genesis operation with the same identifier would require a hash collision on those 120 bits.

Privacy Considerations

The full history of DID operations and updates, including timestamps, is permanently publicly accessible. This is true even after DID deactivation. It is important to recognize (and communicate to account holders) that any personally identifiable information (PII) encoded in alsoKnownAs URIs will be publicly visible even after DID deactivation, and can not be redacted or purged.

This includes the full history of alsoKnownAs entries and service endpoints (URLs) over time. It does not include any other account metadata such as email addresses or IP addresses. The history of names in alsoKnownAs could potentially de-anonymize account holders if they switch names between a known identity and an anonymous or pseudonymous identity.

The timestamp metadata encoded in the audit log could be cross-verified against network traffic or other information to de-anonymize account holders. It also makes the “identity creation date” public.

If “rotation” and “signing” keys are re-used across multiple accounts, it could reveal non-public identity details or relationships. For example, if two individuals cross-share rotation keys as a trusted backup, that information is public. If device-local recovery or signing keys are uniquely shared by two identifiers, that would indicate that those identities may actually be the same person.

DID Creation

To summarize the process of creating a new did:zu identifier:

When “signing” using a “rotationKey”, what is meant is to sign using the private key associated the public key in the rotationKey list.

DID Update

To summarize the process of updating a new did:zu identifier:

DID Deactivation

To summarize the process of de-activating an existing did:zu identifier:

DID Resolution

did:zu DIDs are resolved to a DID document (JSON) by making simple HTTP GET request to the directory. The resolution endpoint is: https://plc.zuzalu.ink/:did

The method-specific state data (based on the most recent operation) can be fetched as a JSON object at: https://plc.zuzalu.ink/:did/data

Audit Logs

As an additional check against abuse by the directory, and to promote resiliency, the set of all identifiers is enumerable, and the set of all operations for all identifiers (even “nullified” operations) can be enumerated and audited.

The log of currently-valid operations for a given DID, as JSON, can be found at: https://plc.zuzalu.ink/:did/log

The audit history of a given DID (complete with timestamps and invalidated forked histories), as JSON, can be found at: https://plc.zuzalu.ink/:did/log/audit

To fully validate a DID document against the operation log:

Bulk Export

The complete log of operations for all DIDs in the directory can be enumerated efficiently.

The HTTP endpoint https://plc.zuzalu.ink/export returns data in JSON lines format. It supports two query parameters:

When after is an integer, each JSON line has the following fields:

The seq value of the final JSON line can be used as the after cursor value to retrieve the next page of results.

For legacy compatibility reasons, when after is a timestamp string (in YYYY-MM-DDTHH:mm:ss.sssZ format), each JSON line has the following fields:

This format is referred to as the legacy export format. Its use is discouraged and it may be deprecated in the future.

Legacy response entries are sorted in createdAt order, and the createdAt value of the final JSON line can be used as the after cursor to retrieve the next page of results.

NOTE: If the after query parameter is not set, the response format defaults to the legacy format, returning the first page of results. Request after=0 to retrieve the first page of results in the sequenced_op format.

NOTE: Legacy responses order operations by createdAt, while non-legacy responses order operations by seq. These orders may be slightly different! In either case, operations for a particular DID will always be in the same order relative to each other. In other words, if you isolated the operations for a single DID, the seq order and the createdAt orders are identical.

Export Streaming WebSocket

The WebSocket endpoint at wss://plc.zuzalu.ink/export/stream returns a stream of JSON objects in the same format as the (non-legacy) /export endpoint. JSON objects are delimited by the WebSocket message framing layer (not newlines).

When a client connects with no parameters set, the WebSocket streams newly accepted operations after the time of connection.

The cursor parameter may be set to a previously-seen sequence number, to allow the client to “catch up” on earlier operations (starting with the first operation with greater seq than the passed cursor value). Once the client has caught up, the WebSocket will continue to stream new operations in real-time. The directory may place limits on how far back the cursor value can go (see close reasons below), and in such cases clients are advised to fall back to the paginated /export API. A typical catch-up limit may be on the order of 7 days.

In certain error conditions, the server will close the connection with a “reason” string (as specified in RFC-6455 Section 7.1.6). The following close reasons are specified:

Sequence Number Semantics

The directory assigns sequence numbers (seq) to all accepted operations. These sequence numbers should be seen as an annotation of the authenticated data (similarly to createdAt timestamps), and not an intrinsic property of the operations themselves. Sequence numbers are only visible in /export* API responses, and they exist to facilitate pagination/cursoring through these endpoints.

A mirror or replica service could plausibly assign different sequence numbers to the same operations, so long as the operation ordering is internally consistent.

Example

// note: we use shorthand for keys for ease of reference, but consider them valid did:keys

// Genesis operation
const genesisOp = {
  type: 'plc_operation',
  verificationMethods: {
    atproto: 'did:key:zSigningKey',
  },
  rotationKeys: ['did:key:zRecoveryKey', 'did:key:zRotationKey'],
  alsoKnownAs: ['at://alice.test'],
  services: {
    atproto_pds: {
      type: 'AtprotoPersonalDataServer',
      endpoint: 'https://example.test',
    },
  },
  prev: null,
  sig: 'sig_from_did:key:zRotationKey',
}

// Operation to update recovery key
const updateKeys = {
  type: 'plc_operation',
  verificationMethods: {
    atproto: 'did:key:zSigningKey',
  },
  rotationKeys: ['did:key:zNewRecoveryKey', 'did:key:zRotationKey'],
  alsoKnownAs: ['at://alice.test'],
  services: {
    atproto_pds: {
      type: 'AtprotoPersonalDataServer',
      endpoint: 'https://example.test',
    },
  },
  prev: CID(genesisOp),
  sig: 'sig_from_did:key:zRotationKey',
}

// Invalid operation that will be rejected
// because did:key:zAttackerKey is not listed in rotationKeys
const invalidUpdate = {
  type: 'plc_operation',
  verificationMethods: {
    atproto: 'did:key:zAttackerKey',
  },
  rotationKeys: ['did:key:zAttackerKey'],
  alsoKnownAs: ['at://bob.test'],
  services: {
    atproto_pds: {
      type: 'AtprotoPersonalDataServer',
      endpoint: 'https://example.test',
    },
  },
  prev: CID(updateKeys),
  sig: 'sig_from_did:key:zAttackerKey',
}

// Valid recovery operation that "undoes" updateKeys
const recoveryOp = {
  type: 'plc_operation',
  verificationMethods: {
    atproto: 'did:key:zSigningKey',
  },
  rotationKeys: ['did:key:zRecoveryKey'],
  alsoKnownAs: ['at://alice.test'],
  services: {
    atproto_pds: {
      type: 'AtprotoPersonalDataServer',
      endpoint: 'https://example.test',
    },
  },
  prev: CID(genesisOp),
  sig: 'sig_from_did:key:zRecoveryKey',
}

Presentation as DID Document

The following data:

{
  did: 'did:zu:7iza6de2dwap2sbkpav7c6c6',
  verificationMethods: {
    atproto: 'did:key:zDnaeh9v2RmcMo13Du2d6pjUf5bZwtauYxj3n9dYjw4EZUAR7'
  },
  rotationKeys: [
    'did:key:zDnaedvvAsDE6H3BDdBejpx9ve2Tz95cymyCAKF66JbyMh1Lt',
    'did:key:zDnaeh9v2RmcMo13Du2d6pjUf5bZwtauYxj3n9dYjw4EZUAR7'
  ],
  alsoKnownAs: [
    'at://alice.test'
  ],
  services: {
    atproto_pds: {
      type: "AtprotoPersonalDataServer",
      endpoint: "https://example.test"
    }
  }
}

Will be presented as the following DID document:

{
  '@context': [
    'https://www.w3.org/ns/did/v1',
    'https://w3id.org/security/multikey/v1',
    'https://w3id.org/security/suites/ecdsa-2019/v1'
  ],
  id: 'did:zu:7iza6de2dwap2sbkpav7c6c6',
  alsoKnownAs: [ 'at://alice.test' ],
  verificationMethod: [
    {
      id: '#atproto',
      type: 'Multikey',
      controller: 'did:zu:7iza6de2dwap2sbkpav7c6c6',
      publicKeyMultibase: 'zDnaeh9v2RmcMo13Du2d6pjUf5bZwtauYxj3n9dYjw4EZUAR7'
    }
  ],
  service: [
    {
      id: '#atproto_pds',
      type: 'AtprotoPersonalDataServer',
      serviceEndpoint: 'https://example.test'
    }
  ]
}

Changes to did:plc

Changes to the did:plc specification after v0.3.0 are not automatically part of this method. Any change that is adopted is recorded in the Changelog below.

Changelog

2026-10-29 (v0.1.0)

First version, derived from did:plc v0.3.0:

did:plc history

The following entries are from the did:plc specification, up to the version this method is derived from.

2025-12-04 (v0.3.0)

2025-10-22 (v0.2.1)

This update resolves ambiguities in the spec, aligning it with the existing behaviour of the reference implementation.

2025-06-05 (v0.2)

License

This specification is derived from the did:plc Method Specification, Copyright © 2022-2026 Bluesky Social PBC, and is available under the same terms: the MIT license or the Apache License, Version 2.0, at your option.