@enbox/dids
Version:
TBD DIDs library
570 lines • 30.3 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { IonDid, IonRequest } from '@decentralized-identity/ion-sdk';
import { LocalKeyManager, computeJwkThumbprint } from '@enbox/crypto';
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';
/**
* 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 var DidIonRegisteredKeyType;
(function (DidIonRegisteredKeyType) {
/**
* Ed25519: A public-key signature system using the EdDSA (Edwards-curve Digital Signature
* Algorithm) and Curve25519.
*/
DidIonRegisteredKeyType["Ed25519"] = "Ed25519";
/**
* secp256k1: A cryptographic curve used for digital signatures in a range of decentralized
* systems.
*/
DidIonRegisteredKeyType["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.
*/
DidIonRegisteredKeyType["secp256r1"] = "secp256r1";
/**
* X25519: A Diffie-Hellman key exchange algorithm using Curve25519.
*/
DidIonRegisteredKeyType["X25519"] = "X25519";
})(DidIonRegisteredKeyType || (DidIonRegisteredKeyType = {}));
/**
* 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
};
/**
* 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 {
/**
* 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.
*/
static create() {
return __awaiter(this, arguments, void 0, function* ({ keyManager = new LocalKeyManager(), options = {} } = {}) {
// Before processing the create operation, validate DID-method-specific requirements to prevent
// keys from being generated unnecessarily.
var _a, _b, _c, _d, _e, _f, _g;
// Check 1: Validate that the algorithm for any given verification method is supported by the
// DID ION specification.
if ((_a = options.verificationMethods) === null || _a === void 0 ? void 0 : _a.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 = (_b = options.verificationMethods) === null || _b === void 0 ? void 0 : _b.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 ((_c = options.services) === null || _c === void 0 ? void 0 : _c.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 = {
algorithm: 'Ed25519',
purposes: ['authentication', 'assertionMethod', 'capabilityDelegation', 'capabilityInvocation']
};
const verificationMethodsToAdd = [];
// Generate random key material for additional verification methods, if any.
for (const vm of (_d = options.verificationMethods) !== null && _d !== void 0 ? _d : [defaultVerificationMethod]) {
// Generate a random key for the verification method.
const keyUri = yield keyManager.generateKey({ algorithm: vm.algorithm });
const publicKey = yield keyManager.getPublicKey({ keyUri });
// Add the verification method to the DID document.
verificationMethodsToAdd.push({
id: vm.id,
publicKeyJwk: publicKey,
purposes: (_e = vm.purposes) !== null && _e !== void 0 ? _e : ['authentication', 'assertionMethod', 'capabilityDelegation', 'capabilityInvocation']
});
}
// Generate a random key for the ION Recovery Key. Sidetree requires secp256k1 recovery keys.
const recoveryKeyUri = yield keyManager.generateKey({ algorithm: DidIonRegisteredKeyType.secp256k1 });
const recoveryKey = yield keyManager.getPublicKey({ keyUri: recoveryKeyUri });
// Generate a random key for the ION Update Key. Sidetree requires secp256k1 update keys.
const updateKeyUri = yield keyManager.generateKey({ algorithm: DidIonRegisteredKeyType.secp256k1 });
const updateKey = yield keyManager.getPublicKey({ keyUri: updateKeyUri });
// Compute the Long Form DID URI from the keys and services, if any.
const longFormDidUri = yield DidIonUtils.computeLongFormDidUri({
recoveryKey,
updateKey,
services: (_f = options.services) !== null && _f !== void 0 ? _f : [],
verificationMethods: verificationMethodsToAdd
});
// Expand the DID URI string to a DID document.
const { didDocument, didResolutionMetadata } = yield DidIon.resolve(longFormDidUri, { gatewayUri: options.gatewayUri });
if (didDocument === null) {
throw new Error(`Unable to resolve DID during creation: ${didResolutionMetadata === null || didResolutionMetadata === void 0 ? void 0 : 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 ((_g = options.publish) !== null && _g !== void 0 ? _g : true) {
const registrationResult = yield 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.
*/
static getSigningMethod(_a) {
return __awaiter(this, arguments, void 0, function* ({ didDocument, methodId }) {
var _b;
// 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 = (_b = didDocument.verificationMethod) === null || _b === void 0 ? void 0 : _b.find(vm => { var _a; return vm.id === (methodId !== null && methodId !== void 0 ? methodId : (_a = didDocument.assertionMethod) === null || _a === void 0 ? void 0 : _a[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.
*/
static import(_a) {
return __awaiter(this, arguments, void 0, function* ({ portableDid, keyManager = new LocalKeyManager() }) {
// Verify the DID method is supported.
const parsedDid = Did.parse(portableDid.uri);
if ((parsedDid === null || parsedDid === void 0 ? void 0 : parsedDid.method) !== DidIon.methodName) {
throw new DidError(DidErrorCode.MethodNotSupported, `Method not supported`);
}
const did = yield 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.
* ```
*/
static publish(_a) {
return __awaiter(this, arguments, void 0, function* ({ did, gatewayUri = DEFAULT_GATEWAY_URI }) {
var _b, _c, _d;
// Construct an ION verification method made up of the id, public key, and purposes from each
// verification method in the DID document.
const verificationMethods = (_c = (_b = did.document.verificationMethod) === null || _b === void 0 ? void 0 : _b.map(vm => ({
id: vm.id,
publicKeyJwk: vm.publicKeyJwk,
purposes: getVerificationRelationshipsById({ didDocument: did.document, methodId: vm.id })
}))) !== null && _c !== void 0 ? _c : [];
// Create the ION document.
const ionDocument = yield DidIonUtils.createIonDocument({
services: (_d = did.document.service) !== null && _d !== void 0 ? _d : [],
verificationMethods
});
// Construct the ION Create Operation request.
const createOperation = yield 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 = yield 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: Object.assign(Object.assign({}, did.metadata), { published: response.ok }),
didRegistrationMetadata: {}
};
}
catch (error) {
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.
*/
static resolve(didUri_1) {
return __awaiter(this, arguments, void 0, function* (didUri, options = {}) {
var _a, _b;
// Attempt to parse the DID URI.
const parsedDid = Did.parse(didUri);
// If parsing failed, the DID is invalid.
if (!parsedDid) {
return Object.assign(Object.assign({}, EMPTY_DID_RESOLUTION_RESULT), { didResolutionMetadata: { error: 'invalidDid' } });
}
// If the DID method is not "ion", return an error.
if (parsedDid.method !== DidIon.methodName) {
return Object.assign(Object.assign({}, 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 = (_a = options === null || options === void 0 ? void 0 : options.gatewayUri) !== null && _a !== void 0 ? _a : 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 = yield 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 } = yield response.json();
return Object.assign(Object.assign(Object.assign({}, EMPTY_DID_RESOLUTION_RESULT), didDocument && { didDocument }), { didDocumentMetadata: Object.assign({ published: (_b = didDocumentMetadata === null || didDocumentMetadata === void 0 ? void 0 : didDocumentMetadata.method) === null || _b === void 0 ? void 0 : _b.published }, didDocumentMetadata) });
}
catch (error) {
// 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 Object.assign(Object.assign({}, EMPTY_DID_RESOLUTION_RESULT), { didResolutionMetadata: Object.assign({ error: error.code }, error.message && { errorMessage: error.message }) });
}
});
}
}
/**
* Name of the DID method, as defined in the DID ION specification.
*/
DidIon.methodName = 'ion';
/**
* 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.
*/
static appendPathToUrl({ baseUrl, path }) {
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.
*/
static computeLongFormDidUri(_a) {
return __awaiter(this, arguments, void 0, function* ({ recoveryKey, updateKey, services, verificationMethods }) {
// Create the ION document.
const ionDocument = yield 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 = yield IonDid.createLongFormDid({
document: ionDocument,
recoveryKey: normalizedRecoveryKey,
updateKey: normalizedUpdateKey
});
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.
*/
static constructCreateRequest(_a) {
return __awaiter(this, arguments, void 0, function* ({ ionDocument, recoveryKey, updateKey }) {
// Create an ION DID create request operation.
const createRequest = yield IonRequest.createCreateRequest({
document: ionDocument,
recoveryKey: DidIonUtils.normalizeJwk(recoveryKey),
updateKey: DidIonUtils.normalizeJwk(updateKey)
});
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.
*/
static createIonDocument(_a) {
return __awaiter(this, arguments, void 0, function* ({ services, verificationMethods }) {
var _b, _c;
/**
* STEP 1: Convert verification methods to ION SDK format.
*/
const ionPublicKeys = [];
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 = (_c = (_b = vm.id) !== null && _b !== void 0 ? _b : vm.publicKeyJwk.kid) !== null && _c !== void 0 ? _c : yield computeJwkThumbprint({ jwk: vm.publicKeyJwk });
methodId = `${methodId.split('#').pop()}`; // Remove fragment prefix, if any.
// Convert public key JWK to ION format.
const publicKey = {
id: methodId,
publicKeyJwk: DidIonUtils.normalizeJwk(vm.publicKeyJwk),
purposes: vm.purposes,
type: 'JsonWebKey2020'
};
ionPublicKeys.push(publicKey);
}
/**
* STEP 2: Convert service entries, if any, to ION SDK format.
*/
const ionServices = services.map(service => (Object.assign(Object.assign({}, service), { id: `${service.id.split('#').pop()}` // Remove fragment prefix, if any.
})));
/**
* STEP 3: Format as ION document.
*/
const 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.
*/
static normalizeJwk(jwk) {
const keyType = jwk.kty;
let normalizedJwk;
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;
}
}
//# sourceMappingURL=did-ion.js.map