Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# FAIR DID Library

Monorepo for most things related to DIDs: https://www.w3.org/TR/did-1.0/

Currently an internal-only repo until licensing is sorted: https://github.com/fairpm/did-manager/issues/1

313 changes: 313 additions & 0 deletions docs/did-spec.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,313 @@
<?php
/**
* Documents used, in order of precedence.
*
* * https://www.w3.org/TR/did-resolution/ (Draft 2025-08-03)
* * https://www.w3.org/TR/did-1.1/ (Draft 2025-07-31)
* * https://www.w3.org/TR/cid-1.0 (Recommendation 2025-05-15)
* * https://www.w3.org/TR/did-1.0/ (Recommendation 2022-07-19) TODO: replace with did-core
*
* The interfaces here are for sake of example, and are not intended to be the final implementation.
* Names of fields and most interfaces do try to match the specification as much as possible.
*
* All subtypes of URI, including URL, DID and, DIDURL are represented as strings in the DID data model
*
* A Set without a type comment after is assumed to contain the union of types to its left
* In other words, Foo|Bar|Set|null is Foo|Bar|Set<Foo|Bar>|null
*
* All nullable properties are assumed to be optional and have a default of null (a DTO might use Optional instead)
* Unless stated otherwise, a prop that is null is entirely absent from inputs and outputs, not set to the null value.
*
* @noinspection PhpUnused
*/

////////////////

// https://www.w3.org/TR/did-1.1/#did-syntax
interface DID extends URI
{
public string $method {get;} // [a-z0-9]+
public string $identifier {get;} // aka "method-specific-id". colon separated url-encoded segments.
public string|null $query {get;}
public string|null $fragment {get;}
}

interface DIDURL extends DID {
public string $path {get;}
}

//////////////// Documents

// The interfaces here are a minimal read-only specification, but there is no One Right Way to represent all Documents.

interface Document {
public DID $id {get;}
}

// https://www.w3.org/TR/did-1.1/#core-properties
interface CoreDocument extends Document
{
public DID|Set|null $controller {get;} // set<DID> https://www.w3.org/TR/did-1.1/#did-controller
public Set|null $alsoKnownAs {get;} // set<URL|DID> https://www.w3.org/TR/cid-1.0/#also-known-as
public Set|null $service {get;} // set<Service> https://www.w3.org/TR/cid-1.0/#services
public Set|null $verificationMethod {get;} // set<VerificationMethod> https://www.w3.org/TR/did-1.1/#verification-methods
public Set|null $authentication {get;} // set<DID|VerificationMethod> https://www.w3.org/TR/cid-1.0/#authentication
public Set|null $assertionMethod {get;} // set<DID|VerificationMethod> https://www.w3.org/TR/cid-1.0/#assertion
public Set|null $keyAgreement {get;} // set<DID|VerificationMethod> https://www.w3.org/TR/cid-1.0/#key-agreement
public Set|null $capabilityInvocation {get;} // set<DID|VerificationMethod> https://www.w3.org/TR/cid-1.0/#capability-invocation
public Set|null $capabilityDelegation {get;} // set<DID|VerificationMethod> https://www.w3.org/TR/cid-1.0/#capability-delegation
}

//////////////// Verification Methods

// https://www.w3.org/TR/did-1.1/#verification-methods
// https://www.w3.org/TR/cid-1.0/#verification-methods
interface VerificationMethod
{
public DID $id {get;} // CID spec allows any URL, DID spec requires a DID
public string $type {get;} // 'JsonWebKey' | 'Multikey' per CID spec
public DID $controller {get;} // CID spec allows any URL, DID spec requires a DID
public Date|null $expires {get;}
public Date|null $revoked {get;}
}

// https://www.w3.org/TR/cid-1.0/#Multikey
interface MultikeyVerificationMethod extends VerificationMethod
{
public string|null $publicKeyMultibase {get;}
public string|null $privateKeyMultibase {get;}
}

// https://www.w3.org/TR/cid-1.0/#JsonWebKey
interface JsonWebKeyVerificationMethod extends VerificationMethod
{
public Map|null $publicKeyJwk {get;}
public Map|null $privateKeyJwk {get;}
}

//////////////// Services

// https://www.w3.org/TR/cid-1.0/#services
interface Service
{
public URI|null $id {get;}
public string|Set $type {get;} // CID spec recommends registering in https://www.w3.org/TR/vc-extensions/
public URL|Map|Set $serviceEndpoint {get;} // Map format is apparently free-form
}

//////////////// Resolution

// https://www.w3.org/TR/did-1.0/#did-resolution
interface Resolver
{
// this should be the general idea...
public function __invoke(DID $did): Document;

// This is what the spec wants according to https://www.w3.org/TR/did-core/#did-resolution

// resolve(did, resolutionOptions) →
// « didResolutionMetadata, didDocument, didDocumentMetadata »
public function resolve(DID $did, ResolutionOptions $resolutionOptions): ResolveResult;

// resolveRepresentation(did, resolutionOptions) →
// « didResolutionMetadata, didDocumentStream, didDocumentMetadata »
public function resolveRepresentation(DID $did, ResolutionOptions $resolutionOptions): ResolveRepresentationResult;

// https://www.w3.org/TR/did-1.0/#did-url-dereferencing
// dereference(didUrl, dereferenceOptions) →
// « dereferencingMetadata, contentStream, contentMetadata »
public function dereference(DIDURL $url, DereferencingOptions $dereferenceOptions): DereferencingResult;
}

interface ResolveResult
{
public DIDResolutionMetadata $didResolutionMetadata {get;}
public Document $didDocument {get;}
public array $didDocumentMetadata {get;}
}

interface ResolveRepresentationResult
{
public array $didResolutionMetadata {get;}
public ByteStream $didDocumentStream {get;}
public array $didDocumentMetadata {get;}
}

// https://www.w3.org/TR/did-resolution/#did-resolution-options
interface ResolutionOptions
{
public array $accept = [] {get;}
public bool $expandRelativeUrls = false {get;}
public string|null $versionId {get;}
public Date|null $versionTime {get;}
}

// https://www.w3.org/TR/did-1.0/#did-resolution-metadata
interface DIDResolutionMetadata
{
public string|null $contentType {get;}
public ResolutionError|null $error {get;}
}

// The mapping to URLs is probably best done externally, not as their ->value()
enum ResolutionError: string
{
// These come from did-resolution: their value is the URL for the 'type' field
// http status codes in comments come from the table at https://www.w3.org/TR/did-resolution/#bindings-https
case INVALID_DID = 'https://www.w3.org/ns/did#INVALID_DID'; // 400
case INVALID_DID_DOCUMENT = 'https://www.w3.org/ns/did#INVALID_DID_DOCUMENT'; // 500
case NOT_FOUND = 'https://www.w3.org/ns/did#NOT_FOUND'; // 404
case REPRESENTATION_NOT_SUPPORTED = 'https://www.w3.org/ns/did#REPRESENTATION_NOT_SUPPORTED'; // 406
case INVALID_DID_URL = 'https://www.w3.org/ns/did#INVALID_DID_URL'; // 400
case METHOD_NOT_SUPPORTED = 'https://www.w3.org/ns/did#METHOD_NOT_SUPPORTED'; // 501
case INVALID_OPTIONS = 'https://www.w3.org/ns/did#INVALID_OPTIONS'; // 400
case INTERNAL_ERROR = 'https://www.w3.org/ns/did#INTERNAL_ERROR'; // 500

case INVALID_PUBLIC_KEY = 'https://w3id.org/security#INVALID_PUBLIC_KEY'; // 500
case INVALID_PUBLIC_KEY_LENGTH = 'https://w3id.org/security#INVALID_PUBLIC_KEY_LENGTH'; // 500
case INVALID_PUBLIC_KEY_TYPE = 'https://w3id.org/security#INVALID_PUBLIC_KEY_TYPE'; // 500
case UNSUPPORTED_PUBLIC_KEY_TYPE = 'https://w3id.org/security#UNSUPPORTED_PUBLIC_KEY_TYPE'; // 501

// default http code for any unspecified error is 500
}

// https://www.w3.org/TR/did-1.0/#did-document-metadata
interface DocumentMetadata
{
public Date|null $created {get;}
public Date|null $updated {get;}
public Date|null $deactivated {get;}
public Date|null $nextUpdate {get;}
public string|null $versionId {get;}
public string|null $nextVersionId {get;}
public DID|null $equivalentId {get;}
public DID|null $canonicalId {get;}
}

// Note the use of "Dereferencing" for a prefix instead of just "Dereference", to be consistent with the spec.

// https://www.w3.org/TR/did-1.0/#did-url-dereferencing-options
// https://www.w3.org/TR/did-resolution/#did-url-dereferencing-options
interface DereferencingOptions
{
public string|null $accept {get;} // from base spec
public string|null $verificationRelationship {get;} // from did-resolution
}

interface DereferencingResult
{
public DereferencingMetadata $dereferencingMetadata {get;}
public ByteStream $contentStream {get;}
public ContentMetadata $contentMetadata {get;}
}

// https://www.w3.org/TR/did-1.0/#did-url-dereferencing-metadata
interface DereferencingMetadata
{
public string|null $contentType {get;}
public ProblemDetails|null $error {get;}
}

// https://www.w3.org/TR/did-resolution/#did-url-content-metadata
interface ContentMetadata {} // defines no properties

//////////////// Types not specific to DIDs

// https://www.rfc-editor.org/rfc/rfc9457

// Serve with Content-type: application/problem+json
// null fields should be absent, not literal nulls.
interface ProblemDetails {
public URI|null $type {get;} // assumed to be 'about:blank' if not present
public int|null $status {get;} // http status code
public string|null $title {get;}
public string|null $detail {get;}
public URI|null $instance {get;}
}

// type about:blank is registered with the following:
// Type URI: about:blank
// Title: See HTTP Status Code
// Recommended HTTP status code: N/A
// Reference: RFC 9457

//////////////// Opaque Types

interface Date {} // https://www.w3.org/TR/xmlschema11-2/#dateTime, e.g. an ISO8601 string
interface ByteStream {} // could be a string containing the resource, or some stream object
interface URI {} // likely to be a string in real life
interface URL {} // also a string
interface Set {} // likely to be an array in real life. no duplicates, but assumed to be ordered.
interface Map {} // object with arbitrary keys/values, probably an array in PHP

//////////////// DID Resolution Algorithm
// https://www.w3.org/TR/did-resolution/#resolving-algorithm

// resolve(did, resolutionOptions) →
// « didResolutionMetadata, didDocument, didDocumentMetadata »

// constants like INVALID_DID are assumed to be instances of didResolutionMeta with 'error' set
// {...} appears in the spec as «[...]», indicating some valid but unspecified object

// function resolve(did, resolutionOptions): [didResolutionMeta, didDocument, didDocumentMetadata]
// if not valid(did): return [INVALID_DID, null, {}]
//
// method = did.method
// if not supported(method): return [METHOD_NOT_SUPPORTED, null, {}]
//
// registry = getVerifiableDataRegistry(method)
// [found, doc, docmeta] = registry.Read(did, resolutionOptions) // type of Read() is not fully specified
// if not found: return [NOT_FOUND, null, {}]
// if docmeta.deactivated: return [{...}, null, {deactivated: true, ...}] // http status 410
//
// if resolutionOptions.expandRelativeUrls:
// foreach section in [doc.services, doc.verificationMethods, doc.verificationRelationships]:
// foreach item in section:
// resolveRelativeURLsToAbsolute(item) // per https://www.w3.org/TR/did-1.0/#relative-did-urls
//
// return [{...}, doc, {contentType: docMeta.contentType, ...}]



//////////////// TODO: DID URL Dereferencing Algorithm
// https://www.w3.org/TR/did-resolution/#dereferencing
// https://www.w3.org/TR/did-resolution/#dereferencing-algorithm

// dereference(didUrl, dereferenceOptions) →
// « dereferencingMetadata, contentStream, contentMetadata »



//////////////// Miscellaneous notes

// * alsoKnownAs
// - arbitrary URI or list of them, they need not be DIDs
// - not just for providing known aliases for the same document
// - may resolve to different documents with a different data model


////////////////////////////////////////////////////////////////

// References:

// * [Controlled Identifiers 1.0](https://www.w3.org/TR/cid-1.0/)
// * [Decentralized Identifiers (DIDs) v1.0](https://www.w3.org/TR/did-core/)
// * [JSON-LD 1.1](https://www.w3.org/TR/json-ld11/)
// * [WHATWG Infra Standard](https://infra.spec.whatwg.org/)
// * [WHATWG URL Standard](https://url.spec.whatwg.org/)
// * [XML Schema Definition Language (XSD) 1.1 Part 2: Datatypes](https://www.w3.org/TR/xmlschema11-2/)

// * [RFC3986 - Uniform Resource Identifier (URI): Generic Syntax](https://datatracker.ietf.org/doc/rfc3986/)
// * [RFC5234 - Augmented BNF for Syntax Specifications](https://datatracker.ietf.org/doc/rfc5234/)
// * [RFC6838 - Media Type Specifications and Registration Procedures](https://datatracker.ietf.org/doc/rfc6838/)
// * [RFC8259 - The JavaScript Object Notation (JSON) Data Interchange Format](https://datatracker.ietf.org/doc/rfc8259/)
// * [RFC9457 - Problem Details for HTTP APIs](https://datatracker.ietf.org/doc/rfc8259/)

//////////////// Other Links to move up to the above

// * https://www.w3.org/TR/did-resolution/
// * https://w3c.github.io/did-rubric/
// * https://www.w3.org/TR/did-extensions/
// * https://www.w3.org/TR/did-use-cases/
// * https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml

72 changes: 72 additions & 0 deletions docs/plc-spec.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

require(__DIR__ . '/did-spec.php');

// See:
// * https://github.com/did-method-plc/did-method-plc
// * https://github.com/multiformats/cid - Content Identifier specification

// plc.directory endpoints (see https://web.plc.directory/api/redoc)
//
// * https://plc.directory/{did}
// * https://plc.directory/{did}/log
// * https://plc.directory/{did}/log/audit
// * https://plc.directory/{did}/log/last
// * https://plc.directory/{did}/data
// * https://plc.directory/export?count=1000&after=2025-01-01
// * https://web.plc.directory/did/{did}
//
// sample DIDs:
// * did:plc:ewvi7nxzyoun6zhxrhs64oiz - at://atproto.com
// * did:plc:afjf7gsjzsqmgc7dlhb553mv - Git Updater FAIR package

// Note how the fields _almost_ line up with CoreDocument fields, but not quite.
// Instead of 'id' we have 'did', 'services' (note the plural) is a Map, not a Set, and so on.

// It's also quite unlikely we'll use an object to pass operation parameters, these are just for documentation
// https://github.com/did-method-plc/did-method-plc#how-it-works
interface PLCOperationParameters {
public string|null $did {get;} // not passed in the genesis operation (it doesn't exist yet)
public Set|null $rotationKeys {get;} // Set<DID>, min 1, max 5 (must be k256 or p256 keys)
public Map|null $verificationMethods {get;} // Map<string, DID> (must be did:key, limit 10 entries)
public Set|null $alsoKnownAs {get;} // Set<URI>
public Map|null $services {get;} // Map<string, {string type, string endpoint}>
}

// The spec doesn't specify a 'did' parameter for update ops, possibly that's what $prev is for?

interface CreateOrUpdateOperation {
public string $type {get;} // always 'plc_operation'
public Set|null $rotationKeys {get;}
public Map|null $verificationMethods {get;}
public Set|null $alsoKnownAs {get;}
public Map|null $services {get;}
public string|null $prev {get;} // CID hash pointer to previous op, or EXPLICIT null (not omitted) on create
public string $sig {get;} // base64url encoded signature
}

interface TombstoneOperation {
public string $type {get;} // always 'plc_tombstone'
public string $prev {get;} // not nullable
public string $sig {get;}
}

// FAIR doesn't need to support legacy operations, but they exist in the directory, so this is the format
interface LegacyCreateOperation {
public string $type {get;} // always 'create'
public DID $signingKey {get;} // a single did:key, maps to verificationMethods
public DID $recoveryKey {get;} // a single did:key, maps to rotationKeys
public string $handle {get;} // atproto handle without at:// prefix, maps to alsoKnownAs
public URL $service {get;} // https url to atproto PDS
public null $prev {get;} // always null, but always included
public string $sig {get;}
}


//////////////// Operational Notes

// * Operations are encoded with DAG-CBOR with a max length of 7500 bytes
// * prev references are CIDv1, with base32 multibase encoding, dag-cbor multibase type, and sha-256 multihash
// * rotation keys are in order of authority, highest first
// * higher-authority keys can clobber ops from a lower authority by using the op's parent in 'prev' in a 72h window

Loading