UNPKG

@enbox/dids

Version:
287 lines (251 loc) 12.1 kB
import type { Jwk, Signer, CryptoApi, KeyIdentifier, EnclosedSignParams, KmsExportKeyParams, KmsImportKeyParams, KeyImporterExporter, EnclosedVerifyParams, } from '@enbox/crypto'; import { LocalKeyManager, CryptoUtils } from '@enbox/crypto'; import type { DidDocument } from './types/did-core.js'; import type { DidMetadata, PortableDid } from './types/portable-did.js'; import { DidError, DidErrorCode } from './did-error.js'; import { extractDidFragment, getVerificationMethods } from './utils.js'; /** * A `BearerDidSigner` extends the {@link Signer} interface to include specific properties for * signing with a Decentralized Identifier (DID). It encapsulates the algorithm and key identifier, * which are often needed when signing JWTs, JWSs, JWEs, and other data structures. * * Typically, the algorithm and key identifier are used to populate the `alg` and `kid` fields of a * JWT or JWS header. */ export interface BearerDidSigner extends Signer { /** * The cryptographic algorithm identifier used for signing operations. * * Typically, this value is used to populate the `alg` field of a JWT or JWS header. The * registered algorithm names are defined in the * {@link https://www.iana.org/assignments/jose/jose.xhtml#web-signature-encryption-algorithms | IANA JSON Web Signature and Encryption Algorithms registry}. * * @example * "ES256" // ECDSA using P-256 and SHA-256 */ algorithm: string; /** * The unique identifier of the key within the DID document that is used for signing and * verification operations. * * This identifier must be a DID URI with a fragment (e.g., did:method:123#key-0) that references * a specific verification method in the DID document. It allows users of a `BearerDidSigner` to * determine the DID and key that will be used for signing and verification operations. * * @example * "did:dht:123#key-1" // A fragment identifier referring to a key in the DID document */ keyId: string; } /** * Represents a Decentralized Identifier (DID) along with its DID document, key manager, metadata, * and convenience functions. */ export class BearerDid { /** {@inheritDoc Did#uri} */ uri: string; /** * The DID document associated with this DID. * * @see {@link https://www.w3.org/TR/did-core/#dfn-diddocument | DID Core Specification, § DID Document} */ document: DidDocument; /** {@inheritDoc DidMetadata} */ metadata: DidMetadata; /** * Key Management System (KMS) used to manage the DIDs keys and sign data. * * Each DID method requires at least one key be present in the provided `keyManager`. */ keyManager: CryptoApi; constructor({ uri, document, metadata, keyManager }: { uri: string, document: DidDocument, metadata: DidMetadata, keyManager: CryptoApi }) { this.uri = uri; this.document = document; this.metadata = metadata; this.keyManager = keyManager; } /** * Converts a `BearerDid` object to a portable format containing the URI and verification methods * associated with the DID. * * This method is useful when you need to represent the key material and metadata associated with * a DID in format that can be used independently of the specific DID method implementation. It * extracts both public and private keys from the DID's key manager and organizes them into a * `PortableDid` structure. * * @remarks * If the DID's key manager does not allow private keys to be exported, the `PortableDid` returned * will not contain a `privateKeys` property. This enables the importing and exporting DIDs that * use the same underlying KMS even if the KMS does not support exporting private keys. Examples * include hardware security modules (HSMs) and cloud-based KMS services like AWS KMS. * * If the DID's key manager does support exporting private keys, the resulting `PortableDid` will * include a `privateKeys` property which contains the same number of entries as there are * verification methods as the DID document, each with its associated private key and the * purpose(s) for which the key can be used (e.g., `authentication`, `assertionMethod`, etc.). * * @example * ```ts * // Assuming `did` is an instance of BearerDid * const portableDid = await did.export(); * // portableDid now contains the DID URI, document, metadata, and optionally, private keys. * ``` * * @returns A `PortableDid` containing the URI, DID document, metadata, and optionally private * keys associated with the `BearerDid`. * @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 async export(): Promise<PortableDid> { // Verify the DID document contains at least one verification method. if (!(Array.isArray(this.document.verificationMethod) && this.document.verificationMethod.length > 0)) { throw new Error(`DID document for '${this.uri}' is missing verification methods`); } // Create a new `PortableDid` copy object to store the exported data. let portableDid: PortableDid = JSON.parse(JSON.stringify({ uri : this.uri, document : this.document, metadata : this.metadata })); // If the BearerDid's key manager supports exporting private keys, add them to the portable DID. if ('exportKey' in this.keyManager && typeof this.keyManager.exportKey === 'function') { const privateKeys: Jwk[] = []; for (let vm of this.document.verificationMethod) { if (!vm.publicKeyJwk) { throw new Error(`Verification method '${vm.id}' does not contain a public key in JWK format`); } // Compute the key URI of the verification method's public key. const keyUri = await this.keyManager.getKeyUri({ key: vm.publicKeyJwk }); // Retrieve the private key from the key manager. const privateKey = await this.keyManager.exportKey({ keyUri }) as Jwk; // Add the verification method to the key set. privateKeys.push({ ...privateKey }); } portableDid.privateKeys = privateKeys; } return portableDid; } /** * Return a {@link Signer} that can be used to sign messages, credentials, or arbitrary data. * * If given, the `methodId` parameter is used to select a key from the verification methods * present in the DID Document. * * If `methodID` is not given, the first verification method intended for signing claims is used. * * @param params - The parameters for the `getSigner` operation. * @param params.methodId - ID of the verification method key that will be used for sign and * verify operations. Optional. * @returns An instantiated {@link Signer} that can be used to sign and verify data. */ public async getSigner(params?: { methodId: string }): Promise<BearerDidSigner> { // Attempt to find a verification method that matches the given method ID, or if not given, // find the first verification method intended for signing claims. const verificationMethod = this.document.verificationMethod?.find( vm => extractDidFragment(vm.id) === (extractDidFragment(params?.methodId) ?? extractDidFragment(this.document.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'); } // Compute the expected key URI of the signing key. const keyUri = await this.keyManager.getKeyUri({ key: verificationMethod.publicKeyJwk }); // Get the public key to be used for verify operations, which also verifies that the key is // present in the key manager's store. const publicKey = await this.keyManager.getPublicKey({ keyUri }); // Bind the DID's key manager to the signer. const keyManager = this.keyManager; // Determine the signing algorithm. const algorithm = CryptoUtils.getJoseSignatureAlgorithmFromPublicKey(publicKey); return { algorithm : algorithm, keyId : verificationMethod.id, async sign({ data }: EnclosedSignParams): Promise<Uint8Array> { const signature = await keyManager.sign({ data, keyUri: keyUri! }); // `keyUri` is guaranteed to be defined at this point. return signature; }, async verify({ data, signature }: EnclosedVerifyParams): Promise<boolean> { const isValid = await keyManager.verify({ data, key: publicKey!, signature }); // `publicKey` is guaranteed to be defined at this point. return isValid; } }; } /** * Instantiates a {@link BearerDid} object 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 BearerDid.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 PortableDid 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> { // Get all verification methods from the given DID document, including embedded methods. const verificationMethods = getVerificationMethods({ didDocument: portableDid.document }); // Validate that the DID document contains at least one verification method. if (verificationMethods.length === 0) { throw new DidError(DidErrorCode.InvalidDidDocument, `At least one verification method is required but 0 were given`); } // If given, import the private key material into the key manager. for (let key of portableDid.privateKeys ?? []) { // confirm th key does not already exist before importing it to avoid failures from the key manager const keyUri = await keyManager.getKeyUri({ key }); const keyExists = await keyManager.getPublicKey({ keyUri }).then(() => true).catch(() => false); if (!keyExists) { await keyManager.importKey({ key }); } } // Validate that the key material for every verification method in the DID document is present // in the key manager. for (let vm of verificationMethods) { if (!vm.publicKeyJwk) { throw new Error(`Verification method '${vm.id}' does not contain a public key in JWK format`); } // Compute the key URI of the verification method's public key. const keyUri = await keyManager.getKeyUri({ key: vm.publicKeyJwk }); // Verify that the key is present in the key manager. If not, an error is thrown. await keyManager.getPublicKey({ keyUri }); } // Use the given PortableDid to construct the BearerDid object. const did = new BearerDid({ uri : portableDid.uri, document : portableDid.document, metadata : portableDid.metadata, keyManager }); return did; } }