@enbox/dids
Version:
TBD DIDs library
887 lines (806 loc) • 34.6 kB
text/typescript
import type { CryptoApi, Jwk, KeyIdentifier, KeyImporterExporter, KmsExportKeyParams, KmsImportKeyParams } from '@enbox/crypto';
import type {
JwkEs256k,
IonDocumentModel,
IonPublicKeyModel,
IonPublicKeyPurpose,
} from '@decentralized-identity/ion-sdk';
import { IonDid, IonRequest } from '@decentralized-identity/ion-sdk';
import { LocalKeyManager, computeJwkThumbprint } from '@enbox/crypto';
import type { PortableDid } from '../types/portable-did.js';
import type { DidCreateOptions, DidCreateVerificationMethod, DidRegistrationResult } from '../methods/did-method.js';
import type {
DidService,
DidDocument,
DidResolutionResult,
DidResolutionOptions,
DidVerificationMethod,
DidVerificationRelationship,
} from '../types/did-core.js';
import { Did } from '../did.js';
import { BearerDid } from '../bearer-did.js';
import { DidMethod } from '../methods/did-method.js';
import { DidError, DidErrorCode } from '../did-error.js';
import { getVerificationRelationshipsById } from '../utils.js';
import { EMPTY_DID_RESOLUTION_RESULT } from '../types/did-resolution.js';
/**
* Options for creating a Decentralized Identifier (DID) using the DID ION method.
*/
export interface DidIonCreateOptions<TKms> extends DidCreateOptions<TKms> {
/**
* 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 Sidetree node. If not specified, a default
* gateway node is used.
*/
gatewayUri?: string;
/**
* Optional. Determines whether the created DID should be published to a Sidetree node.
*
* 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://identity.foundation/sidetree/spec/#create | Sidetree Protocol Specification, § Create}
*
* @example
* ```ts
* const did = await DidIon.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 DidIon.create({
* options: {
* services: [
* {
* id: 'dwn',
* type: 'DecentralizedWebNode',
* serviceEndpoint: ['https://example.com/dwn1', 'https://example/dwn2']
* }
* ]
* };
* ```
*/
services?: DidService[];
/**
* Optional. An array of verification methods to be included in the DID document.
*
* By default, a newly created DID ION document will contain a single Ed25519 verification method.
* 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 DidIon.create({
* options: {
* verificationMethods: [
* {
* algorithm: 'Ed25519',
* purposes: ['authentication', 'assertionMethod']
* },
* {
* algorithm: 'Ed25519',
* id: 'dwn-sig',
* purposes: ['authentication', 'assertionMethod']
* }
* ]
* };
* ```
*/
verificationMethods?: DidCreateVerificationMethod<TKms>[];
}
/**
* Represents the request model for managing DID documents within the ION network, according to the
* Sidetree protocol specification.
*/
export interface DidIonCreateRequest {
/** The type of operation to perform, which is always 'create' for a Create Operation. */
type: 'create';
/** Contains properties related to the initial state of the DID document. */
suffixData: {
/** A hash of the `delta` object, representing the initial changes to the DID document. */
deltaHash: string;
/** A commitment value used for future recovery operations, hashed for security. */
recoveryCommitment: string;
};
/** Details the changes to be applied to the DID document in this operation. */
delta: {
/** A commitment value used for the next update operation, hashed for security. */
updateCommitment: string;
/** An array of patch objects specifying the modifications to apply to the DID document. */
patches: {
/** The type of modification to perform (e.g., adding or removing public keys or service
* endpoints). */
action: string;
/** The document state or partial state to apply with this patch. */
document: IonDocumentModel;
}[];
}
}
/**
* Represents a {@link DidVerificationMethod | DID verification method} in the context of DID ION
* create, update, deactivate, and resolve operations.
*
* Unlike the DID Core standard {@link DidVerificationMethod} interface, this type is specific to
* the ION method operations and only includes the `id`, `publicKeyJwk`, and `purposes` properties:
* - The `id` property is optional and specifies the identifier fragment of the verification method.
* - The `publicKeyJwk` property is required and represents the public key in JWK format.
* - The `purposes` property is required and specifies the purposes for which the verification
* method can be used.
*
* @example
* ```ts
* const verificationMethod: DidIonVerificationMethod = {
* id : 'sig',
* publicKeyJwk : {
* kty : 'OKP',
* crv : 'Ed25519',
* x : 'o40shZrsco-CfEqk6mFsXfcP94ly3Az3gm84PzAUsXo',
* kid : 'BDp0xim82GswlxnPV8TPtBdUw80wkGIF8gjFbw1x5iQ',
* },
* purposes: ['authentication', 'assertionMethod']
* };
* ```
*/
export interface DidIonVerificationMethod {
/**
* Optionally specify the identifier fragment of the verification method.
*
* If not specified, the method's ID will be generated from the key's ID or thumbprint.
*
* @example
* ```ts
* const verificationMethod: DidIonVerificationMethod = {
* id: 'sig',
* ...
* };
* ```
*/
id?: string;
/**
* A public key in JWK format.
*
* A JSON Web Key (JWK) that conforms to {@link https://datatracker.ietf.org/doc/html/rfc7517 | RFC 7517}.
*
* @example
* ```ts
* const verificationMethod: DidIonVerificationMethod = {
* publicKeyJwk: {
* kty : "OKP",
* crv : "X25519",
* x : "7XdJtNmJ9pV_O_3mxWdn6YjiHJ-HhNkdYQARzVU_mwY",
* kid : "xtsuKULPh6VN9fuJMRwj66cDfQyLaxuXHkMlmAe_v6I"
* },
* ...
* };
* ```
*/
publicKeyJwk: Jwk;
/**
* 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.
*
* @example
* ```ts
* const verificationMethod: DidIonVerificationMethod = {
* purposes: ['authentication', 'assertionMethod'],
* ...
* };
* ```
*/
purposes: (DidVerificationRelationship | keyof typeof DidVerificationRelationship)[];
}
/**
* `IonPortableDid` interface extends the {@link PortableDid} interface.
*
* It represents a Decentralized Identifier (DID) that is portable and can be used across different
* domains, including the ION specific recovery and update keys.
*/
export interface IonPortableDid extends PortableDid {
/** The JSON Web Key (JWK) used for recovery purposes. */
recoveryKey: Jwk;
/** The JSON Web Key (JWK) used for updating the DID. */
updateKey: Jwk;
}
/**
* Enumerates the types of keys that can be used in a DID ION document.
*
* The DID ION method supports various cryptographic key types. These key types are essential for
* the creation and management of DIDs and their associated cryptographic operations like signing
* and encryption.
*/
export enum DidIonRegisteredKeyType {
/**
* Ed25519: A public-key signature system using the EdDSA (Edwards-curve Digital Signature
* Algorithm) and Curve25519.
*/
Ed25519 = 'Ed25519',
/**
* secp256k1: A cryptographic curve used for digital signatures in a range of decentralized
* systems.
*/
secp256k1 = 'secp256k1',
/**
* secp256r1: Also known as P-256 or prime256v1, this curve is used for cryptographic operations
* and is widely supported in various cryptographic libraries and standards.
*/
secp256r1 = 'secp256r1',
/**
* X25519: A Diffie-Hellman key exchange algorithm using Curve25519.
*/
X25519 = 'X25519'
}
/**
* Private helper that maps algorithm identifiers to their corresponding DID ION
* {@link DidIonRegisteredKeyType | registered key type}.
*/
const AlgorithmToKeyTypeMap = {
Ed25519 : DidIonRegisteredKeyType.Ed25519,
ES256K : DidIonRegisteredKeyType.secp256k1,
ES256 : DidIonRegisteredKeyType.secp256r1,
'P-256' : DidIonRegisteredKeyType.secp256r1,
secp256k1 : DidIonRegisteredKeyType.secp256k1,
secp256r1 : DidIonRegisteredKeyType.secp256r1
} as const;
/**
* The default node to use as a gateway to the Sidetree newtork when anchoring, updating, and
* resolving DID documents.
*/
const DEFAULT_GATEWAY_URI = 'https://ion.tbd.engineering';
/**
* The `DidIon` class provides an implementation of the `did:ion` DID method.
*
* Features:
* - DID Creation: Create new `did:ion` DIDs.
* - DID Key Management: Instantiate a DID object from an existing key in a Key Management System
* (KMS). If supported by the KMS, a DID's key can be exported to a portable
* DID format.
* - DID Resolution: Resolve a `did:ion` to its corresponding DID Document stored in the Sidetree
* network.
* - Signature Operations: Sign and verify messages using keys associated with a DID.
*
* @see {@link https://identity.foundation/sidetree/spec/ | Sidetree Protocol Specification}
* @see {@link https://github.com/decentralized-identity/ion/blob/master/docs/design.md | ION Design Document}
*
* @example
* ```ts
* // DID Creation
* const did = await DidIon.create();
*
* // DID Creation with a KMS
* const keyManager = new LocalKeyManager();
* const did = await DidIon.create({ keyManager });
*
* // DID Resolution
* const resolutionResult = await DidIon.resolve({ did: did.uri });
*
* // Signature Operations
* const signer = await did.getSigner();
* const signature = await signer.sign({ data: new TextEncoder().encode('Message') });
* const isValid = await signer.verify({ data: new TextEncoder().encode('Message'), signature });
*
* // Key Management
*
* // Instantiate a DID object for a published DID with existing keys in a KMS
* const did = await DidIon.fromKeyManager({
* didUri: 'did:ion:EiAzB7K-xDIKc1csXo5HX2eNBoemK9feNhL3cKwfukYOug',
* keyManager
* });
*
* // Convert a DID object to a portable format
* const portableDid = await DidIon.toKeys({ did });
* ```
*/
export class DidIon extends DidMethod {
/**
* Name of the DID method, as defined in the DID ION specification.
*/
public static methodName = 'ion';
/**
* Creates a new DID using the `did:ion` method formed from a newly generated key.
*
* Notes:
* - If no `options` are given, by default a new Ed25519 key will be generated.
*
* @example
* ```ts
* // DID Creation
* const did = await DidIon.create();
*
* // DID Creation with a KMS
* const keyManager = new LocalKeyManager();
* const did = await DidIon.create({ keyManager });
* ```
*
* @param params - The parameters for the create operation.
* @param params.keyManager - Optionally specify a Key Management System (KMS) used to generate
* keys and sign data.
* @param params.options - Optional parameters that can be specified when creating a new DID.
* @returns A Promise resolving to a {@link BearerDid} object representing the new DID.
*/
public static async create<TKms extends CryptoApi | undefined = undefined>({
keyManager = new LocalKeyManager(),
options = {}
}: {
keyManager?: TKms;
options?: DidIonCreateOptions<TKms>;
} = {}): Promise<BearerDid> {
// Before processing the create operation, validate DID-method-specific requirements to prevent
// keys from being generated unnecessarily.
// Check 1: Validate that the algorithm for any given verification method is supported by the
// DID ION specification.
if (options.verificationMethods?.some(vm => !(vm.algorithm in AlgorithmToKeyTypeMap))) {
throw new Error('One or more verification method algorithms are not supported');
}
// Check 2: Validate that the ID for any given verification method is unique.
const methodIds = options.verificationMethods?.filter(vm => 'id' in vm).map(vm => vm.id);
if (methodIds && methodIds.length !== new Set(methodIds).size) {
throw new Error('One or more verification method IDs are not unique');
}
// Check 3: Validate that the required properties for any given services are present.
if (options.services?.some(s => !s.id || !s.type || !s.serviceEndpoint)) {
throw new Error('One or more services are missing required properties');
}
// If no verification methods were specified, generate a default Ed25519 verification method.
const defaultVerificationMethod: DidCreateVerificationMethod<TKms> = {
algorithm : 'Ed25519' as any,
purposes : ['authentication', 'assertionMethod', 'capabilityDelegation', 'capabilityInvocation']
};
const verificationMethodsToAdd: DidIonVerificationMethod[] = [];
// Generate random key material for additional verification methods, if any.
for (const vm of options.verificationMethods ?? [defaultVerificationMethod]) {
// Generate a random key for the verification method.
const keyUri = await keyManager.generateKey({ algorithm: vm.algorithm });
const publicKey = await keyManager.getPublicKey({ keyUri });
// Add the verification method to the DID document.
verificationMethodsToAdd.push({
id : vm.id,
publicKeyJwk : publicKey,
purposes : vm.purposes ?? ['authentication', 'assertionMethod', 'capabilityDelegation', 'capabilityInvocation']
});
}
// Generate a random key for the ION Recovery Key. Sidetree requires secp256k1 recovery keys.
const recoveryKeyUri = await keyManager.generateKey({ algorithm: DidIonRegisteredKeyType.secp256k1 });
const recoveryKey = await keyManager.getPublicKey({ keyUri: recoveryKeyUri });
// Generate a random key for the ION Update Key. Sidetree requires secp256k1 update keys.
const updateKeyUri = await keyManager.generateKey({ algorithm: DidIonRegisteredKeyType.secp256k1 });
const updateKey = await keyManager.getPublicKey({ keyUri: updateKeyUri });
// Compute the Long Form DID URI from the keys and services, if any.
const longFormDidUri = await DidIonUtils.computeLongFormDidUri({
recoveryKey,
updateKey,
services : options.services ?? [],
verificationMethods : verificationMethodsToAdd
});
// Expand the DID URI string to a DID document.
const { didDocument, didResolutionMetadata } = await DidIon.resolve(longFormDidUri, { gatewayUri: options.gatewayUri });
if (didDocument === null) {
throw new Error(`Unable to resolve DID during creation: ${didResolutionMetadata?.error}`);
}
// Create the BearerDid object, including the "Short Form" of the DID URI, the ION update and
// recovery keys, and specifying that the DID has not yet been published.
const did = new BearerDid({
uri : longFormDidUri,
document : didDocument,
metadata : {
published : false,
canonicalId : longFormDidUri.split(':', 3).join(':'),
recoveryKey,
updateKey
},
keyManager
});
// By default, publish the DID document to a Sidetree node unless explicitly disabled.
if (options.publish ?? true) {
const registrationResult = await DidIon.publish({ did, gatewayUri: options.gatewayUri });
did.metadata = registrationResult.didDocumentMetadata;
}
return did;
}
/**
* Given the W3C DID Document of a `did:ion` DID, return the verification method that will be used
* for signing messages and credentials. If given, the `methodId` parameter is used to select the
* verification method. If not given, the first verification method in the authentication property
* in the DID Document is used.
*
* @param params - The parameters for the `getSigningMethod` operation.
* @param params.didDocument - DID Document to get the verification method from.
* @param params.methodId - ID of the verification method to use for signing.
* @returns Verification method to use for signing.
*/
public static async getSigningMethod({ didDocument, methodId }: {
didDocument: DidDocument;
methodId?: string;
}): Promise<DidVerificationMethod> {
// Verify the DID method is supported.
const parsedDid = Did.parse(didDocument.id);
if (parsedDid && parsedDid.method !== this.methodName) {
throw new DidError(DidErrorCode.MethodNotSupported, `Method not supported: ${parsedDid.method}`);
}
// Get the verification method with either the specified ID or the first assertion method.
const verificationMethod = didDocument.verificationMethod?.find(
vm => vm.id === (methodId ?? didDocument.assertionMethod?.[0])
);
if (!(verificationMethod && verificationMethod.publicKeyJwk)) {
throw new DidError(DidErrorCode.InternalError, 'A verification method intended for signing could not be determined from the DID Document');
}
return verificationMethod;
}
/**
* Instantiates a {@link BearerDid} object for the DID ION method from a given {@link PortableDid}.
*
* This method allows for the creation of a `BearerDid` object using a previously created DID's
* key material, DID document, and metadata.
*
* @example
* ```ts
* // Export an existing BearerDid to PortableDid format.
* const portableDid = await did.export();
* // Reconstruct a BearerDid object from the PortableDid.
* const did = await DidIon.import({ portableDid });
* ```
*
* @param params - The parameters for the import operation.
* @param params.portableDid - The PortableDid object to import.
* @param params.keyManager - Optionally specify an external Key Management System (KMS) used to
* generate keys and sign data. If not given, a new
* {@link LocalKeyManager} instance will be created and
* used.
* @returns A Promise resolving to a `BearerDid` object representing the DID formed from the
* provided PortableDid.
* @throws An error if the DID document does not contain any verification methods or the keys for
* any verification method are missing in the key manager.
*/
public static async import({ portableDid, keyManager = new LocalKeyManager() }: {
keyManager?: CryptoApi & KeyImporterExporter<KmsImportKeyParams, KeyIdentifier, KmsExportKeyParams>;
portableDid: PortableDid;
}): Promise<BearerDid> {
// Verify the DID method is supported.
const parsedDid = Did.parse(portableDid.uri);
if (parsedDid?.method !== DidIon.methodName) {
throw new DidError(DidErrorCode.MethodNotSupported, `Method not supported`);
}
const did = await BearerDid.import({ portableDid, keyManager });
return did;
}
/**
* Publishes a DID to a Sidetree node, making it publicly discoverable and resolvable.
*
* This method handles the publication of a DID Document associated with a `did:ion` DID to a
* Sidetree node.
*
* @remarks
* - This method is typically invoked automatically during the creation of a new DID unless the
* `publish` option is set to `false`.
* - For existing, unpublished DIDs, it can be used to publish the DID Document to a Sidetree node.
* - The method relies on the specified Sidetree node to interface with the network.
*
* @param params - The parameters for the `publish` operation.
* @param params.did - The `BearerDid` object representing the DID to be published.
* @param params.gatewayUri - Optional. The URI of a server involved in executing DID
* method operations. In the context of publishing, the
* endpoint is expected to be a Sidetree node. If not
* specified, a default node is used.
* @returns A Promise resolving to a boolean indicating whether the publication was successful.
*
* @example
* ```ts
* // Generate a new DID and keys but explicitly disable publishing.
* const did = await DidIon.create({ options: { publish: false } });
* // Publish the DID to the Sidetree network.
* const isPublished = await DidIon.publish({ did });
* // `isPublished` is true if the DID was successfully published.
* ```
*/
public static async publish({ did, gatewayUri = DEFAULT_GATEWAY_URI }: {
did: BearerDid;
gatewayUri?: string;
}): Promise<DidRegistrationResult> {
// Construct an ION verification method made up of the id, public key, and purposes from each
// verification method in the DID document.
const verificationMethods: DidIonVerificationMethod[] = did.document.verificationMethod?.map(
vm => ({
id : vm.id,
publicKeyJwk : vm.publicKeyJwk!,
purposes : getVerificationRelationshipsById({ didDocument: did.document, methodId: vm.id })
})
) ?? [];
// Create the ION document.
const ionDocument = await DidIonUtils.createIonDocument({
services: did.document.service ?? [],
verificationMethods
});
// Construct the ION Create Operation request.
const createOperation = await DidIonUtils.constructCreateRequest({
ionDocument,
recoveryKey : did.metadata.recoveryKey,
updateKey : did.metadata.updateKey
});
try {
// Construct the URL of the SideTree node's operations endpoint.
const operationsUrl = DidIonUtils.appendPathToUrl({
baseUrl : gatewayUri,
path : `/operations`
});
// Submit the Create Operation to the operations endpoint.
const response = await fetch(operationsUrl, {
method : 'POST',
mode : 'cors',
headers : { 'Content-Type': 'application/json' },
body : JSON.stringify(createOperation)
});
// Return the result of processing the Create operation, including the updated DID metadata
// with the publishing result.
return {
didDocument : did.document,
didDocumentMetadata : {
...did.metadata,
published: response.ok,
},
didRegistrationMetadata: {}
};
} catch (error: any) {
return {
didDocument : null,
didDocumentMetadata : {
published: false,
},
didRegistrationMetadata: {
error : DidErrorCode.InternalError,
errorMessage : `Failed to publish DID document for: ${did.uri}`
}
};
}
}
/**
* Resolves a `did:ion` identifier to its corresponding DID document.
*
* This method performs the resolution of a `did:ion` DID, retrieving its DID Document from the
* Sidetree-based DID overlay network. The process involves querying a Sidetree node to retrieve
* the DID Document that corresponds to the given DID identifier.
*
* @remarks
* - If a `gatewayUri` option is not specified, a default node is used to access the Sidetree
* network.
* - It decodes the DID identifier and retrieves the associated DID Document and metadata.
* - In case of resolution failure, appropriate error information is returned.
*
* @example
* ```ts
* const resolutionResult = await DidIon.resolve('did:ion:example');
* ```
*
* @param didUri - The DID to be resolved.
* @param options - Optional parameters for resolving the DID. Unused by this DID method.
* @returns A Promise resolving to a {@link DidResolutionResult} object representing the result of the resolution.
*/
public static async resolve(didUri: string, options: DidResolutionOptions = {}): Promise<DidResolutionResult> {
// Attempt to parse the DID URI.
const parsedDid = Did.parse(didUri);
// If parsing failed, the DID is invalid.
if (!parsedDid) {
return {
...EMPTY_DID_RESOLUTION_RESULT,
didResolutionMetadata: { error: 'invalidDid' }
};
}
// If the DID method is not "ion", return an error.
if (parsedDid.method !== DidIon.methodName) {
return {
...EMPTY_DID_RESOLUTION_RESULT,
didResolutionMetadata: { error: 'methodNotSupported' }
};
}
// To execute the read method operation, use the given gateway URI or a default Sidetree node.
const gatewayUri = options?.gatewayUri ?? DEFAULT_GATEWAY_URI;
try {
// Construct the URL to be used in the resolution request.
const resolutionUrl = DidIonUtils.appendPathToUrl({
baseUrl : gatewayUri,
path : `/identifiers/${didUri}`
});
// Attempt to retrieve the DID document and metadata from the Sidetree node.
const response = await fetch(resolutionUrl);
// If the DID document was not found, return an error.
if (!response.ok) {
throw new DidError(DidErrorCode.NotFound, `Unable to find DID document for: ${didUri}`);
}
// If the DID document was retrieved successfully, return it.
const { didDocument, didDocumentMetadata } = await response.json() as DidResolutionResult;
return {
...EMPTY_DID_RESOLUTION_RESULT,
...didDocument && { didDocument },
didDocumentMetadata: {
published: didDocumentMetadata?.method?.published,
...didDocumentMetadata
}
};
} catch (error: any) {
// Rethrow any unexpected errors that are not a `DidError`.
if (!(error instanceof DidError)) throw new Error(error);
// Return a DID Resolution Result with the appropriate error code.
return {
...EMPTY_DID_RESOLUTION_RESULT,
didResolutionMetadata: {
error: error.code,
...error.message && { errorMessage: error.message }
}
};
}
}
}
/**
* The `DidIonUtils` class provides utility functions to support operations in the DID ION method.
*/
export class DidIonUtils {
/**
* Appends a specified path to a base URL, ensuring proper formatting of the resulting URL.
*
* This method is useful for constructing URLs for accessing various endpoints, such as Sidetree
* nodes in the ION network. It handles the nuances of URL path concatenation, including the
* addition or removal of leading/trailing slashes, to create a well-formed URL.
*
* @param params - The parameters for URL construction.
* @param params.baseUrl - The base URL to which the path will be appended.
* @param params.path - The path to append to the base URL.
* @returns The fully constructed URL string with the path appended to the base URL.
*/
public static appendPathToUrl({ baseUrl, path }: {
baseUrl: string;
path: string;
}): string {
const url = new URL(baseUrl);
url.pathname = url.pathname.endsWith('/') ? url.pathname : url.pathname + '/';
url.pathname += path.startsWith('/') ? path.substring(1) : path;
return url.toString();
}
/**
* Computes the Long Form DID URI given an ION DID's recovery key, update key, services, and
* verification methods.
*
* @param params - The parameters for computing the Long Form DID URI.
* @param params.recoveryKey - The ION Recovery Key.
* @param params.updateKey - The ION Update Key.
* @param params.services - An array of services associated with the DID.
* @param params.verificationMethods - An array of verification methods associated with the DID.
* @returns A Promise resolving to the Long Form DID URI.
*/
public static async computeLongFormDidUri({ recoveryKey, updateKey, services, verificationMethods }: {
recoveryKey: Jwk;
updateKey: Jwk;
services: DidService[];
verificationMethods: DidIonVerificationMethod[];
}): Promise<string> {
// Create the ION document.
const ionDocument = await DidIonUtils.createIonDocument({ services, verificationMethods });
// Normalize JWK to onnly include specific members and in lexicographic order.
const normalizedRecoveryKey = DidIonUtils.normalizeJwk(recoveryKey);
const normalizedUpdateKey = DidIonUtils.normalizeJwk(updateKey);
// Compute the Long Form DID URI.
const longFormDidUri = await IonDid.createLongFormDid({
document : ionDocument,
recoveryKey : normalizedRecoveryKey as JwkEs256k,
updateKey : normalizedUpdateKey as JwkEs256k
});
return longFormDidUri;
}
/**
* Constructs a Sidetree Create Operation request for a DID document within the ION network.
*
* This method prepares the necessary payload for submitting a Create Operation to a Sidetree
* node, encapsulating the details of the DID document, recovery key, and update key.
*
* @param params - Parameters required to construct the Create Operation request.
* @param params.ionDocument - The DID document model containing public keys and service endpoints.
* @param params.recoveryKey - The recovery public key in JWK format.
* @param params.updateKey - The update public key in JWK format.
* @returns A promise resolving to the ION Create Operation request model, ready for submission to a Sidetree node.
*/
public static async constructCreateRequest({ ionDocument, recoveryKey, updateKey }: {
ionDocument: IonDocumentModel,
recoveryKey: Jwk,
updateKey: Jwk
}): Promise<DidIonCreateRequest> {
// Create an ION DID create request operation.
const createRequest = await IonRequest.createCreateRequest({
document : ionDocument,
recoveryKey : DidIonUtils.normalizeJwk(recoveryKey) as JwkEs256k,
updateKey : DidIonUtils.normalizeJwk(updateKey) as JwkEs256k
}) as DidIonCreateRequest;
return createRequest;
}
/**
* Assembles an ION document model from provided services and verification methods
*
* This model serves as the foundation for a DID document in the ION network, facilitating the
* creation and management of decentralized identities. It translates service endpoints and
* public keys into a format compatible with the Sidetree protocol, ensuring the resulting DID
* document adheres to the required specifications for ION DIDs. This method is essential for
* constructing the payload needed to register or update DIDs within the ION network.
*
* @param params - The parameters containing the services and verification methods to include in the ION document.
* @param params.services - A list of service endpoints to be included in the DID document, specifying ways to interact with the DID subject.
* @param params.verificationMethods - A list of verification methods to be included, detailing the cryptographic keys and their intended uses within the DID document.
* @returns A Promise resolving to an `IonDocumentModel`, ready for use in Sidetree operations like DID creation and updates.
*/
public static async createIonDocument({ services, verificationMethods }: {
services: DidService[];
verificationMethods: DidIonVerificationMethod[]
}): Promise<IonDocumentModel> {
/**
* STEP 1: Convert verification methods to ION SDK format.
*/
const ionPublicKeys: IonPublicKeyModel[] = [];
for (const vm of verificationMethods) {
// Use the given ID, the key's ID, or the key's thumbprint as the verification method ID.
let methodId = vm.id ?? vm.publicKeyJwk.kid ?? await computeJwkThumbprint({ jwk: vm.publicKeyJwk });
methodId = `${methodId.split('#').pop()}`; // Remove fragment prefix, if any.
// Convert public key JWK to ION format.
const publicKey: IonPublicKeyModel = {
id : methodId,
publicKeyJwk : DidIonUtils.normalizeJwk(vm.publicKeyJwk),
purposes : vm.purposes as IonPublicKeyPurpose[],
type : 'JsonWebKey2020'
};
ionPublicKeys.push(publicKey);
}
/**
* STEP 2: Convert service entries, if any, to ION SDK format.
*/
const ionServices = services.map(service => ({
...service,
id: `${service.id.split('#').pop()}` // Remove fragment prefix, if any.
}));
/**
* STEP 3: Format as ION document.
*/
const ionDocumentModel: IonDocumentModel = {
publicKeys : ionPublicKeys,
services : ionServices
};
return ionDocumentModel;
}
/**
* Normalize the given JWK to include only specific members and in lexicographic order.
*
* @param jwk - The JWK to normalize.
* @returns The normalized JWK.
*/
private static normalizeJwk(jwk: Jwk): Jwk {
const keyType = jwk.kty;
let normalizedJwk: Jwk;
if (keyType === 'EC') {
normalizedJwk = { crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y };
} else if (keyType === 'oct') {
normalizedJwk = { k: jwk.k, kty: jwk.kty };
} else if (keyType === 'OKP') {
normalizedJwk = { crv: jwk.crv, kty: jwk.kty, x: jwk.x };
} else if (keyType === 'RSA') {
normalizedJwk = { e: jwk.e, kty: jwk.kty, n: jwk.n };
} else {
throw new Error(`Unsupported key type: ${keyType}`);
}
return normalizedJwk;
}
}