Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DID creation / update / deactivate reponse #112

Open
frankhinek opened this issue Feb 2, 2024 · 4 comments
Open

DID creation / update / deactivate reponse #112

frankhinek opened this issue Feb 2, 2024 · 4 comments
Assignees
Labels
did related to decentralized identifiers enhancement New feature or request

Comments

@frankhinek
Copy link
Contributor

frankhinek commented Feb 2, 2024

Context

The W3C DID Core specification contains guidance for the data structure to be returned by the resolve and dereference functions:

Additionally, there draft community report that a new W3C WG is being started to finalize:

No such guidance exists for other DID registration operations such as create, update, and deactivate. The closest is a draft DIF spec that one of the W3C DID Core editors started:

Current State of SDKs

At present, the shape of result returned by create implementations vary widely by SDK:

It is expected and a positive that each SDK takes a slightly different approach to implementation due to idiomatic differences. However, it would be beneficial to have some general consistency on what is returned when a new DID is created, and for those DID methods that support it, updated or deactivated.


Proposal for Did object returned by create() or update()

/**
 * Represents a Decentralized Identifier (DID) along with its convenience functions.
 */
export interface Did {
  /**
   * The DID document associated with this DID.
   */
  didDocument: DidDocument;

  /**
   * Returns a Signer that can be used to sign messages, credentials, or arbitrary data.
   *
   * If given, the `keyUri` parameter is used to select a key from the verification methods present
   * in the DID Document. If `keyUri` is not given, each DID method implementation will select a
   * default verification method key from the DID Document.
   *
   * @param params - The parameters for the `getSigner` operation.
   * @param params.keyUri - Key URI of the key that will be used for sign and verify operations. Optional.
   * @returns An instantiated Signer that can be used to sign and verify data.
   */
  getSigner: (params?: { keyUri?: string }) => Promise<Signer>;

  /**
   * Key Management System (KMS) used to manage a DIDs keys and sign data.
   *
   * Each DID method requires at least one key be present in the provided `keyManager`.
   */
  keyManager: KeyManager;

  /**
   * Represents metadata about a DID resulting from create, update, or deactivate operations.
   */
  metadata: DidMetadata;

  /**
   * A string representation of the DID.
   *
   * A DID is a URI composed of three parts: the scheme `did:`, a method identifier, and a unique,
   * method-specific identifier specified by the DID method.
   *
   * @example
   * did:dht:h4d3ixkwt6q5a455tucw7j14jmqyghdtbr6cpiz6on5oxj5bpr3o
   */
  uri: string;
}

/**
 * Represents metadata about a DID resulting from create, update, or deactivate operations.
 */
export type DidMetadata = {
  // Additional properties of any type.
  [key: string]: any;
}

/**
 * Format to document a DID identifier, along with its associated data, which can be exported,
 * saved to a file, or imported. The intent is bundle all of the necessary metadata to enable usage
 * of the DID in different contexts.
 */
/**
 * Format that documents the key material and metadata of a Decentralized Identifier (DID) to enable
 * usage of the DID in different contexts.
 *
 * This format is useful for exporting, saving to a file, or importing a DID across process
 * boundaries or between different DID method implementations.
 *
 * @example
 * ```ts
 * // Generate a new DID.
 * const did = await DidExample.create();
 *
 * // Export the DID to a PortableDid.
 * const portableDid = await DidExample.toKeys({ did });
 *
 * // Instantiate a `Did` object from the PortableDid metadata.
 * const didFromKeys = await DidExample.fromKeys({ ...portableDid });
 * // The `didFromKeys` object should be equivalent to the original `did` object.
 * ```
 */
export interface PortableDid {
  /** {@inheritDoc DidUri#uri} */
  uri?: string;

  /**
   * An array of verification methods, including the key material and key purpose, which are
   * included in the DID document.
   *
   * @see {@link https://www.w3.org/TR/did-core/#verification-methods | DID Core Specification, § Verification Methods}
   */
  verificationMethods: PortableDidVerificationMethod[];
}

Proposal for DidMethod.create()

  • Only allows creating a new DID object and generating new keys
  • If called with no parameters (e.g., DidMethod.create() a local key manager that stores keys in-memory will be used for key generation using a default algorithm (e.g., Ed25519)
  • The method optionally takes a keyManager which will be an instance of a KMS (could be local key manager with memory/disk persistence, AWS KMS key manager, or a custom implementation)
  • The method optionally takes verificationMethods that specify additional verification methods to add to the DID document.
  • The method optionally takes services which will be an array of DidService objects.
  • Returns a Did object or instance (depending on the SDK) that has uri, didDocument, keyManager, and metadata properties along with a convenience function getSigner() that returns a signer that can sign() and verify() arbitrary data.

Pseudo type definition for simple methods like DID JWK:

export interface DidCreateOptions {
  /**
   * Optional. An array of verification methods to be included in the DID document.
   */
  verificationMethods?: DidCreateVerificationMethod[];
}

/**
 * Options for additional verification methods added to the DID Document during the creation of a
 * new Decentralized Identifier (DID).
 */
export interface DidCreateVerificationMethod {
  /**
   * The name of the cryptographic algorithm to be used for key generation.
   *
   * Examples might include `Ed25519` and `ES256K` but will vary depending on the DID method
   * specification and the key management system in use.
   */
  algorithm: string;

  /**
   * Optionally specify the purposes for which a verification method is intended to be used in a DID
   * document.
   *
   * The `purposes` property defines the specific
   * {@link DidVerificationRelationship | verification relationships} between the DID subject and
   * the verification method. This enables the verification method to be utilized for distinct
   * actions such as authentication, assertion, key agreement, capability delegation, and others. It
   * is important for verifiers to recognize that a verification method must be associated with the
   * relevant purpose in the DID document to be valid for that specific use case.
   */
  purposes?: (DidVerificationRelationship | keyof typeof DidVerificationRelationship)[];
}

Type definition for a more complex method like DID DHT:

export interface DidDhtCreateOptions extends DidCreateOptions {
  /**
   * Optionally specify that the DID Subject is also identified by one or more other DIDs or URIs.
   *
   * A DID subject can have multiple identifiers for different purposes, or at different times.
   * The assertion that two or more DIDs (or other types of URI) refer to the same DID subject can
   * be made using the `alsoKnownAs` property.
   *
   * @see {@link https://www.w3.org/TR/did-core/#also-known-as | DID Core Specification, § Also Known As}
   *
   * @example
   * ```ts
   * const did = await DidDht.create({
   *  options: {
   *   alsoKnownAs: 'did:example:123'
   * };
   * ```
   */
  alsoKnownAs?: string[];

  /**
   * Optionally specify which DID (or DIDs) is authorized to make changes to the DID document.
   *
   * A DID controller is an entity that is authorized to make changes to a DID document. Typically,
   * only the DID Subject (i.e., the value of `id` property in the DID document) is authoritative.
   * However, another DID (or DIDs) can be specified as the DID controller, and when doing so, any
   * verification methods contained in the DID document for the other DID should be accepted as
   * authoritative. In other words, proofs created by the controller DID should be considered
   * equivalent to proofs created by the DID Subject.
   *
   * @see {@link https://www.w3.org/TR/did-core/#did-controller | DID Core Specification, § DID Controller}
   *
   * @example
   * ```ts
   * const did = await DidDht.create({
   *  options: {
   *   controller: 'did:example:123'
   * };
   * ```
   */
  controllers?: string | string[];

  /**
   * Optional. The URI of a server involved in executing DID method operations. In the context of
   * DID creation, the endpoint is expected to be a Pkarr relay. If not specified, a default gateway
   * node is used.
   */
  gatewayUri?: string;

  /**
   * Optional. Determines whether the created DID should be published to the DHT network.
   *
   * If set to `true` or omitted, the DID is publicly discoverable. If `false`, the DID is not
   * published and cannot be resolved by others. By default, newly created DIDs are published.
   *
   * @see {@link https://did-dht.com | DID DHT Method Specification}
   *
   * @example
   * ```ts
   * const did = await DidDht.create({
   *  options: {
   *   publish: false
   * };
   * ```
   */
  publish?: boolean;

  /**
   * Optional. An array of service endpoints associated with the DID.
   *
   * Services are used in DID documents to express ways of communicating with the DID subject or
   * associated entities. A service can be any type of service the DID subject wants to advertise,
   * including decentralized identity management services for further discovery, authentication,
   * authorization, or interaction.
   *
   * @see {@link https://www.w3.org/TR/did-core/#services | DID Core Specification, § Services}
   *
   * @example
   * ```ts
   * const did = await DidDht.create({
   *  options: {
   *   services: [
   *     {
   *       id: 'did:dht:i9xkp8ddcbcg8jwq54ox699wuzxyifsqx4jru45zodqu453ksz6y#dwn',
   *       type: 'DecentralizedWebNode',
   *       serviceEndpoint: ['https://example.com/dwn1', 'https://example/dwn2']
   *     }
   *   ]
   * };
   * ```
   */
  services?: DidService[];

  /**
   * Optionally specify one or more registered DID DHT types to make the DID discovereable.
   *
   * Type indexing is an OPTIONAL feature that enables DIDs to become discoverable. DIDs that wish
   * to be discoverable and resolveable by type can include one or more types when publishing their
   * DID document to a DID DHT Gateway.
   *
   * The registered DID types are published in the {@link https://did-dht.com/registry/index.html#indexed-types | DID DHT Registry}.
   */
  types?: (DidDhtRegisteredDidType | keyof typeof DidDhtRegisteredDidType)[];

  /**
   * Optional. An array of verification methods to be included in the DID document.
   *
   * By default, a newly created DID DHT document will contain a single Ed25519 verification method,
   * also known as the {@link https://did-dht.com/#term:identity-key | Identity Key}. Additional
   * verification methods can be added to the DID document using the `verificationMethods` property.
   *
   * @see {@link https://www.w3.org/TR/did-core/#verification-methods | DID Core Specification, § Verification Methods}
   *
   * @example
   * ```ts
   * const did = await DidDht.create({
   *  options: {
   *   verificationMethods: [
   *     {
   *       algorithm: 'Ed25519',
   *       purposes: ['authentication', 'assertionMethod']
   *     },
   *     {
   *       algorithm: 'Ed25519',
   *       id: 'dwn-sig',
   *       purposes: ['authentication', 'assertionMethod']
   *     }
   *   ]
   * };
   * ```
   */
  verificationMethods?: DidCreateVerificationMethod<TKms>[];
}

DidMethod.fromKeys()

  • Only allow instantiating a DID using previously generated keys
  • Keys are provided as a key set which contains one or more keys
  • returns the same Did object as create().

DidMethod.fromKeyManager()

  • Only allows instantiating a previously created DID using existing keys that are stored in a key manager instance.
  • The only input is the DID URI and the keyManager.

DidMethod.toKeys()

After either create() or fromKeys() returns a Did object/instance, a developer can transform (or "export") the DID back to a PortableDid.

@frankhinek frankhinek added the did related to decentralized identifiers label Feb 2, 2024
@frankhinek
Copy link
Contributor Author

frankhinek commented Feb 2, 2024

Output from the call today:

BearerDid

  • A DID w/ key manager and convenience functions is called a BearerDid
  • BearerDid includes a function getSigner that returns an object/instance with sign() and verify() methods that can be used to sign messages, credentials, or arbitrary data. The object/instance returns also has algorithm and keyId properties that are needed for header properties when creating JWS and JWE. By default, if getSigner is called with no arguments it uses the first verification method found in the DID Document's assertionMethod property. It can optionally be called with a methodId argument to select a specific verification method from the DID document.
{
    uri: "did:method:123",
    document: {}, // DID Document
    metadata: {
        published: boolean
        // method specific properties
    },

    // Instance of `KeyManager` that holds keys for the DID
    keyManager,
    
    // Returns a Signer with `sign()` and `verify()` methods and `algorithm` and `keyId` properties
    getSigner: (params?: { methodId?: string }) => Signer;

    // Returns a `PortableDid` with the properties described above.
    export: () => PortableDid;
}

PortableDid

{
    uri: "did:method:123",
    document: {}, // DID Document
    metadata: {
        published: boolean
        // method specific properties
    }

    // Optional and would be empty if `AwsKeyManager` since keys can't be exported
    privateKeys: [{...jwk}]
}

Additional Changes

  • Delete / eliminate the DID method fromKeyManager(). It was introduced to solve an issue for a PFI partner that is now better solved by import().
  • Delete / eliminate the DID method specific functions toKeys or toPortableDid. It has been replaced by the export() function on the BearerDid instance.
  • Rename load() / fromKeys / fromPortableDid to DidMethod.import()
  • import() must optionally accept a key manager (e.g., AwsKeyManager or LocalKeyManager)
  • just Did = the string and has Did.parse()

@mistermoe
Copy link
Contributor

Any thoughts on calling fromPortableDid and toPortableDid importBearerDid and export respectively?

Thought it might make sense given that it is a port able did! No biggie if not. Just staring at did.BearerDIDFromPortableDID and/or did<method>.BearerDIDFromPortableDID and thinking "damn.. that's a long function name"

@frankhinek
Copy link
Contributor Author

frankhinek commented Feb 5, 2024

While they are verbose, I do appreciate the symmetry of fromPortableDid() and toPortableDid() to make it clear that these methods are involved in converting back and forth between PortableDid.

Something about importBearerDid and export makes it less clear that the two complement each other. It also exposes the developer to having to understand what a BearerDid is right away, which is also true of toPortableDid()/fromPortableDid().

IMHO importBearerDid reads as "importing a BearerDid" -- but then a developer must provide a PortableDid as input, which might be slightly confusing for some.

If you do want to change, what about:

  • Did<method>.import(): Function that is DID-method specific and transforms a PortableDid to a BearerDid:

    • validating that the input is valid for the given DID method
    • confirming all of the keys are present in the provided KeyManager OR if no KeyManager is given, instantiating a new LocalKeyManager and importing the private keys
  • did.export(): A BearerDid instance/object function that transforms a BearerDid to a PortableDid:

    • exports the DID material and metadata (key material, DID document, DID metadata) to a portable format
    • The exported PortableDid object can be imported by the DID method's import() function either using the same SDK or an SDK implemented in a different language

These two methods enable the round-trip conversion of DID types, ensuring that DID material & metadata can be transformed between runtime and portable formats, thereby enabling interoperability between the various Web5 SDKs.

If the method is DidJwk.import() and did.export() it ought to be pretty obvious what's being exported and imported. If we wanted to be a bit more verbose we could make that DidJwk.importDid() and did.exportDid() but not sure if thats necessary.

All that being said...

It seems regardless of the language that it should be did<method>.fromPortableDID() or did<method>.import() rather than trying to attach BearerDIDFromPortableDID() to a generic BearerDID or DID.

@decentralgabe
Copy link
Member

@frankhinek do you think the guidance in the spec today is sufficient?

if not, what should we add?

also, I added some actions for test coverage around this. please add more detail / actions as you see fit #167

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
did related to decentralized identifiers enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants