Fork of github.com/did-method-plc/did-method-plc
1# DID PLC Method (did:plc)
2
3DID PLC is a self-authenticating [DID](https://www.w3.org/TR/did-core/) which is strongly-consistent, recoverable, and allows for key rotation.
4
5An example DID is: `did:plc:ewvi7nxzyoun6zhxrhs64oiz`
6
7Control over a `did:plc` 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.
8
9This git repository contains a TypeScript reference implementation of the method (`@did-plc/lib`) and a directory server `@did-plc/server`, both in the `package/` directory. The `go-didplc/`directory is intended to hold a golang implementation.
10
11## Motivation
12
13[Bluesky Social PBC](https://bsky.social/) developed DID PLC when designing the [AT Protocol](https://atproto.com) (atproto) because we were not satisfied with any of the existing DID methods. We wanted a strongly consistent, highly available, recoverable, and cryptographically secure method with fast and cheap propagation of updates.
14
15PLC stands for "Public Ledger of Credentials". We expect to evolve the system (in a backwards-compatible manner) into something less centralized - likely a permissioned DID consortium. That being said, we do intend to support `did:plc` in the current form until after any successor is deployed, with a reasonable grace period. We would also provide a migration route to allow continued use of existing `did:plc` identifiers.
16
17## How it works
18
19The core data fields associated with an active `did:plc` identifier at any point in time are 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.
20
21- `did` (string): the full DID identifier
22- `rotationKeys` (array of strings): priority-ordered list of public keys in `did:key` encoding. must include least 1 key and at most 5 keys, with no duplication. control of the DID identifier rests in these keys. not included in DID document.
23- `verificationMethods` (map with string keys and values): maps services to public keys, stored in `did:key` encoding. The service id strings should not include a `#` prefix; that will be added when rendering the DID document. used to generate `verificationMethods` of DID document. these keys do not have control over the DID document.
24- `alsoKnownAs` (array of strings): priority-ordered list of URIs which indicate other names or aliases associated with the DID identifier
25- `services` (map with string keys; values are maps with `type` and `endpoint` string fields): a set of service / URL mappings. the key strings should not include a `#` prefix; that will be added when rendering the DID document.
26
27Every 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).
28
29Operations are signed and submitted to the central PLC directory server over an un-authenticated HTTP request. The PLC server 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.
30
31A 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.
32
33Note 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.
34
35Only `secp256k1` ("k256") and NIST P-256 ("p256") keys are currently supported for rotation keys, whereas `verificationMethods` keys can be any syntactically-valid `did:key`.
36
37### Use with AT Protocol
38
39The following information should be included for use with atproto:
40
41- `verificationMethods`: an `atproto` entry with a "blessed" public key type, to be used as a signing key for authenticating updates to the account's repository. The signing key does not have any control over the DID identity unless also included in the `rotationKeys` list. Best practice is to maintain separation between rotation keys and atproto signing keys.
42- `alsoKnownAs`: should include an `at://` URI indicating a handle (hostname) for the account. Note that the handle/DID mapping needs to be validated bi-directionally (via handle resolution), and needs to be re-verified periodically
43- `services`: an `atproto_pds` entry with an `AtprotoPersonalDataServer` type and http/https URL `endpoint` indicating the account's current PDS hostname. for example, `https://pds.example.com` (no `/xrpc/` suffix needed).
44
45### Operation Serialization, Signing, and Validation
46
47There 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.
48
49A regular creation or update operation contains the following fields:
50
51- `type` (string): with fixed value `plc_operation`
52- `rotationKeys` (array of strings): as described above
53- `verificationMethods` (mapping of string keys and values): as described above
54- `alsoKnownAs` (array of strings): as described above
55- `services` (mapping of string keys and object values): as described above
56- `prev` (string, nullable): a CID hash pointer to a previous operation if an update, or `null` for a creation. If `null`, the key should actually be part of the object, with value `null`, not simply omitted. In DAG-CBOR encoding, the CID is string-encoded, not a binary IPLD "Link"
57- `sig` (string): signature of the operation in `base64url` encoding
58
59A tombstone operation contains:
60
61- `type` (string): with fixed value `plc_tombstone`
62- `prev` (string): same as above, but not nullable
63- `sig` (string): signature of the operation (same as above)
64
65There is also a deprecated legacy operation format, supported *only* for creation ("genesis") operations:
66
67- `type` (string): with fixed value `create`
68- `signingKey` (string): single `did:key` value (not an array of strings)
69- `recoveryKey` (string): single `did:key` value (not an array of strings); and note "recovery" terminology, not "rotation"
70- `handle` (string): single value, indicating atproto handle, instead of `alsoKnownAs`. bare handle, with no `at://` prefix
71- `service` (string): single value, http/https URL of atproto PDS
72- `prev` (null): always include, but always with value `null`
73- `sig` (string): signature of the operation (same as above)
74
75Legacy `create` operations are stored in the PLC registry and may be returned in responses, so validating software needs to support that format. Conversion of the legacy format to "regular" operation format is relatively straight-forward, but there exist many `did:plc` identifiers where the DID identifier itself is based on the hash of the old format, so they will unfortunately be around forever.
76
77The process for signing and hashing operation objects is to first encode them in the DAG-CBOR binary serialization format. [DAG-CBOR](https://ipld.io/specs/codecs/dag-cbor/spec/) is a restricted subset of the Concise Binary Object Representation (CBOR), an IETF standard (RFC 8949), with semantics and value types similar to JSON.
78
79As an anti-abuse mechanism, operations have a maximum size when encoded as DAG-CBOR. The current limit is 7500 bytes.
80
81For 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, and then the signature bytes are encoded as a string using `base64url` encoding. The `sig` value is then populated with the string. In strongly typed programming languages it is a best practice to have distinct "signed" and "unsigned" types.
82
83When working with signatures, note that ECDSA signatures are not necessarily *deterministic* or *unique*. That is, the same key signing the same bytes *might* generate the same signature every time, or it might generate a *different* signature every time, depending on the cryptographic library and configuration. In some cases it is also easy for a third party to take a valid signature and transform it into a new, distinct signature, which also validates. Be sure to always use the "validate signature" routine from a cryptographic library, instead of re-signing bytes and directly comparing the signature bytes.
84
85For `prev` references, the SHA-256 of the previous operation's bytes are encoded as a "[CID](https://github.com/multiformats/cid)", with the following parameters:
86
87- CIDv1
88- `base32` multibase encoding (prefix: `b`)
89- `dag-cbor` multibase type (code: 0x71)
90- `sha-256` multihash (code: 0x12)
91
92Rotation keys are serialized as strings using [did:key](https://w3c-ccg.github.io/did-key-spec/), and only `secp256k1` ("k256") and NIST P-256 ("p256") are currently supported.
93
94The 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.
95
96Although `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.
97
98The 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.
99
100In pseudo-code:
101`did:plc:${base32Encode(sha256(createOp)).slice(0,24)}`
102
103### Identifier Syntax
104
105The DID PLC method name is `plc`. The identifier part is 24 characters
106long, including only characters from the `base32` encoding set. An example is
107`did:plc:yk4dd2qkboz2yv6tpubpc6co`. This means:
108
109- the overall identifier length is 32 characters
110- the entire identifier is lower-case (and should be normalized to lower-case)
111- the entire identifier is ASCII, and includes only the characters `a-z`, `0-9`, and `:` (and does not even use digits `0189`)
112
113
114### Key Rotation & Account Recovery
115
116Any key specified in `rotationKeys` has the ability to sign operations for the DID document.
117
118The set of rotation keys for a DID is not included in the DID document. They are an internal detail of PLC, and are stored in the operation log.
119
120Keys are listed in the `rotationKeys` field of operations in order of descending authority.
121
122The PLC server 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.
123
124To do so, that key must sign a new operation that points to the CID of the last "valid" operation - ie the fork point.
125The PLC server will accept this recovery operation as long as:
126
127- it is submitted within 72hrs of the referenced operation
128- the key used for the signature has a lower index in the `rotationKeys` array than the key that signed the to-be-invalidated operation
129
130
131### Privacy and Security Concerns
132
133The 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.
134
135In the context of atproto, this includes the full history of handle updates and PDS locations (URLs) over time. To be explicit, it does not include any other account metadata such as email addresses or IP addresses. Handle history could potentially de-anonymize account holders if they switch handles between a known identity and an anonymous or pseudonymous identity.
136
137The PLC server 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-directionally verification, for example using handle resolution.
138
139The timestamp metadata encoded in the PLC 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.
140
141If "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.
142
143
144#### PLC Server Trust Model
145
146The PLC server has a public endpoint to receive operation objects from any client (without authentication). The server verifies operations, orders them according to recovery rules, and makes the log of operations publicly available.
147
148The operation log is self-certifying, and contains all the information needed to construct (or verify) the the current state of the DID document.
149
150Some trust is required in the PLC server. Its attacks are limited to:
151
152- Denial of service: rejecting valid operations, or refusing to serve some information about the DID
153- Misordering: In the event of a fork in DID document history, the server could choose to serve the "wrong" fork
154
155
156### DID Creation
157
158To summarize the process of creating a new `did:plc` identifier:
159
160- collect values for all of the core data fields, including generating new secure key pairs if necessary
161- construct an "unsigned" regular operation object. include a `prev` field with `null` value. do not use the deprecated/legacy operation format for new DID creations
162- serialize the "unsigned" operation with DAG-CBOR, and sign the resulting bytes with one of the initial `rotationKeys`. encode the signature as `base64url`, and use that to construct a "signed" operation object
163- serialize the "signed" operation with DAG-CBOR, take the SHA-256 hash of those bytes, and encode the hash bytes in `base32`. use the first 24 characters to generate DID value (`did:plc:<hashchars>`)
164- serialize the "signed" operation as simple JSON, and submit it via HTTP POST to `https://plc.directory/:did`
165- if the HTTP status code is successful, the DID has been registered
166
167When "signing" using a "`rotationKey`", what is meant is to sign using the private key associated the public key in the `rotationKey` list.
168
169### DID Update
170
171To summarize the process of updating a new `did:plc` identifier:
172
173- if the current DID state isn't known, fetch the current state from `https://plc.directory/:did/data`
174- if the most recent valid DID operation CID (hash) isn't known, fetch the audit log from `https://plc.directory/:did/log/audit`, identify the most recent valid operation, and get the `cid` value. if this is a recovery operation, the relevant "valid" operation to fork from may not be the most recent in the audit log
175- collect updated values for all of the core data fields, including generating new secure key pairs if necessary (eg, key rotation)
176- construct an "unsigned" regular operation object. include a `prev` field with the CID (hash) of the previous valid operation
177- serialize the "unsigned" operation with DAG-CBOR, and sign the resulting bytes with one of the previously-existing `rotationKeys`. encode the signature as `base64url`, and use that to construct a "signed" operation object
178- serialize the "signed" operation as simple JSON, and submit it via HTTP POST to `https://plc.directory/:did`
179- if the HTTP status code is successful, the DID has been updated
180- the DID update may be nullified by a "rotation" operation during the recovery window (currently 72hr)
181
182### DID Deactivation
183
184To summarize the process of de-activating an existing `did:plc` identifier:
185
186- if the most recent valid DID operation CID (hash) isn't known, fetch the audit log from `https://plc.directory/:did/log/audit`, identify the most recent valid operation, and get the `cid` value
187- construct an "unsigned" tombstone operation object. include a `prev` field with the CID (hash) of the previous valid operation
188- serialize the "unsigned" tombstone operation with DAG-CBOR, and sign the resulting bytes with one of the previously-existing `rotationKeys`. encode the signature as `base64url`, and use that to construct a "signed" tombstone operation object
189- serialize the "signed" tombstone operation as simple JSON, and submit it via HTTP POST to `https://plc.directory/:did`
190- if the HTTP status code is successful, the DID has been deactivated
191- the DID deactivation may be nullified by a "rotation" operation during the recovery window (currently 72hr)
192
193### DID Resolution
194
195PLC DIDs are resolved to a DID document (JSON) by making simple HTTP GET request to the PLC server. The resolution endpoint is: `https://plc.directory/:did`
196
197The PLC-specific state data (based on the most recent operation) can be fetched as a JSON object at: `https://plc.directory/:did/data`
198
199
200### Audit Logs
201
202As an additional check against abuse by the PLC server, 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.
203
204The log of currently-valid operations for a given DID, as JSON, can be found at: `https://plc.directory/:did/log/audit`
205
206The audit history of a given DID (complete with timestamps and invalidated forked histories), as JSON, can be found at: `https://plc.directory/:did/log/audit`
207
208To fully validate a DID document against the operation log:
209
210- fetch the full audit log
211- for the genesis operation, validate the DID
212 - note that the genesis operation may be in deprecated/legacy format, and should be encoded and verified in that format
213 - see the "DID Creation" section above for details
214- for each operation in the log, validate signatures:
215 - identify the set of valid `rotationKeys` at that point of time: either the initial keys for a "genesis" operation, or the keys in the `prev` operation
216 - remove any `sig` field and serialize the "unsigned" operation with DAG-CBOR, yielding bytes
217 - decode the `base64url` `sig` field to bytes
218 - for each of the `rotationKeys`, attempt to verify the signature against the "unsigned" bytes
219 - if no key matches, there has been a trust violation; the PLC server should never have accepted the operation
220- verify the correctness of "nullified" operations and the current active operation log using the rules around rotation keys and recovery windows
221
222The complete log of operations for all DIDs on the PLC server can be enumerated efficiently:
223
224- HTTP endpoint: `https://plc.directory/export`
225- output format: [JSON lines](https://jsonlines.org/)
226- `count` query parameter, as an integer, maximum 1000 lines per request
227- `after` query parameter, based on `createdAt` timestamp, for pagination
228
229
230## Example
231
232```ts
233// note: we use shorthand for keys for ease of reference, but consider them valid did:keys
234
235// Genesis operation
236const genesisOp = {
237 type: 'plc_operation',
238 verificationMethods: {
239 atproto: "did:key:zSigningKey"
240 },
241 rotationKeys: [
242 "did:key:zRecoveryKey",
243 "did:key:zRotationKey"
244 ],
245 alsoKnownAs: [
246 "at://alice.test"
247 ],
248 services: {
249 atproto_pds: {
250 type: "AtprotoPersonalDataServer",
251 endpoint: "https://example.test"
252 }
253 },
254 prev: null,
255 sig: 'sig_from_did:key:zRotationKey'
256}
257
258// Operation to update recovery key
259const updateKeys = {
260 type: 'plc_operation',
261 verificationMethods: {
262 atproto: "did:key:zSigningKey"
263 },
264 rotationKeys: [
265 "did:key:zNewRecoveryKey",
266 "did:key:zRotationKey"
267 ],
268 alsoKnownAs: [
269 "at://alice.test"
270 ],
271 services: {
272 atproto_pds: {
273 type: "AtprotoPersonalDataServer",
274 endpoint: "https://example.test"
275 }
276 },
277 prev: CID(genesisOp),
278 sig: 'sig_from_did:key:zRotationKey'
279}
280
281// Invalid operation that will be rejected
282// because did:key:zAttackerKey is not listed in rotationKeys
283const invalidUpdate = {
284 type: 'plc_operation',
285 verificationMethods: {
286 atproto: "did:key:zAttackerKey"
287 },
288 rotationKeys: [
289 "did:key:zAttackerKey"
290 ],
291 alsoKnownAs: [
292 "at://bob.test"
293 ],
294 services: {
295 atproto_pds: {
296 type: "AtprotoPersonalDataServer",
297 endpoint: "https://example.test"
298 }
299 },
300 prev: CID(updateKeys),
301 sig: 'sig_from_did:key:zAttackerKey'
302}
303
304// Valid recovery operation that "undoes" updateKeys
305const recoveryOp = {
306 type: 'plc_operation',
307 verificationMethods: {
308 atproto: "did:key:zSigningKey"
309 },
310 rotationKeys: [
311 "did:key:zRecoveryKey"
312 ],
313 alsoKnownAs: [
314 "at://alice.test"
315 ],
316 services: {
317 atproto_pds: {
318 type: "AtprotoPersonalDataServer",
319 endpoint: "https://example.test"
320 }
321 },
322 prev: CID(genesisOp),
323 sig: 'sig_from_did:key:zRecoveryKey'
324}
325```
326
327## Presentation as DID Document
328
329The following data:
330
331```ts
332{
333 did: 'did:plc:7iza6de2dwap2sbkpav7c6c6',
334 verificationMethods: {
335 atproto: 'did:key:zDnaeh9v2RmcMo13Du2d6pjUf5bZwtauYxj3n9dYjw4EZUAR7'
336 },
337 rotationKeys: [
338 'did:key:zDnaedvvAsDE6H3BDdBejpx9ve2Tz95cymyCAKF66JbyMh1Lt',
339 'did:key:zDnaeh9v2RmcMo13Du2d6pjUf5bZwtauYxj3n9dYjw4EZUAR7'
340 ],
341 alsoKnownAs: [
342 'at://alice.test'
343 ],
344 services: {
345 atproto_pds: {
346 type: "AtprotoPersonalDataServer",
347 endpoint: "https://example.test"
348 }
349 }
350}
351```
352
353Will be presented as the following DID document:
354
355```ts
356{
357 '@context': [
358 'https://www.w3.org/ns/did/v1',
359 'https://w3id.org/security/multikey/v1',
360 'https://w3id.org/security/suites/ecdsa-2019/v1'
361 ],
362 id: 'did:plc:7iza6de2dwap2sbkpav7c6c6',
363 alsoKnownAs: [ 'at://alice.test' ],
364 verificationMethod: [
365 {
366 id: '#atproto',
367 type: 'Multikey',
368 controller: 'did:plc:7iza6de2dwap2sbkpav7c6c6',
369 publicKeyMultibase: 'zDnaeh9v2RmcMo13Du2d6pjUf5bZwtauYxj3n9dYjw4EZUAR7'
370 }
371 ],
372 service: [
373 {
374 id: '#atproto_pds',
375 type: 'AtprotoPersonalDataServer',
376 serviceEndpoint: 'https://example2.com'
377 }
378 ]
379}
380```
381
382## Possible Future Changes
383
384The set of allowed ("blessed") public key cryptographic algorithms (aka, curves) may expanded over time, slowly. Likewise, support for additional blessed CID types and parameters may be expanded over time, slowly.
385
386The recovery time window may become configurable, within constraints, as part of the DID metadata itself.
387
388Support for "DID Controller Delegation" could be useful (eg, in the context of atproto PDS hosts), and may be incorporated.
389
390In the context of atproto, support for multiple handles for the same DID is being considered, with a single primary handle. But no final decision has been made yet.
391
392We welcome proposals for small additions to make `did:plc` more generic and reusable for applications other than atproto. But no promises: atproto will remain the focus for the near future.
393
394We are enthusiastic about the prospect of moving governance of the `did:plc` method, and operation of registry servers, out of the sole control of Bluesky Social PBC. Audit log snapshots, mirroring, and automated third-party auditing have all been considered as mechanisms to mitigate the centralized nature of the PLC server.
395
396The size of the `verificationMethods`, `alsoKnownAs`, and `service` mappings/arrays may be specifically constrained. And the maximum DAG-CBOR size may be constrained.
397
398As an anti-abuse mechanisms, the PLC server load balancer restricts the number of HTTP requests per time window. The limits are generous, and operating large services or scraping the operation log should not run into limits. Specific per-DID limits on operation rate may be introduced over time. For example, no more than N operations per DID per rotation key per 24 hour window.
399
400A "DID PLC history explorer" web interface would make the public nature of the DID audit log more publicly understandable.
401
402It is conceivable that longer DID PLCs, with more of the SHA-256 characters, will be supported in the future. It is also conceivable that a different hash algorithm would be allowed. Any such changes would allow existing DIDs in their existing syntax to continue being used.
403
404## License
405
406This project is dual-licensed under MIT and Apache 2.0 terms:
407
408- Apache License, Version 2.0, ([LICENSE-APACHE](https://github.com/ipfs/kubo/blob/master/LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
409- MIT license ([LICENSE-MIT](https://github.com/ipfs/kubo/blob/master/LICENSE-MIT) or http://opensource.org/licenses/MIT)
410
411Downstream projects and users may chose either license, or both, at their discretion. The motivation for this dual-licensing is the additional software patent assurance provided by Apache 2.0.
412
413## Changelog
414
415### 2025-06-05
416
417- `verificationMethods` keys may now use any syntactically-valid `did:key:`, regardless of key format (allowing e.g. `ed25519` keys). Rotation keys are not affected by this change, the original format constraints still apply.
418
419- A total limit of 10 `verificationMethods` (per DID) has been added.