UNPKG

@dwn-protocol/id-sdk

Version:

SDK for accessing the features and capabilities

791 lines (698 loc) 28 kB
import type { PrivateKeyJwk, PublicKeyJwk, IDCrypto } from '../crypto/index.js'; import { universalTypeOf } from '../common/index.js'; // import { keyToMultibaseId, multibaseIdToKey } from '../crypto/utils.js'; import { Jose, Ed25519, Secp256k1, EcdsaAlgorithm, EdDsaAlgorithm, utils as cryptoUtils, } from '../crypto/index.js'; import type { DidMethod, DidDocument, PortableDid, VerificationMethod, DidResolutionResult, DidResolutionOptions, DidKeySetVerificationMethodKey } from './types.js'; import { getVerificationMethodTypes, parseDid } from './utils.js'; const SupportedCryptoAlgorithms = [ 'Ed25519', 'secp256k1' ] as const; const SupportedPublicKeyFormats = [ 'Ed25519VerificationKey2020', 'JsonWebKey2020', 'X25519KeyAgreementKey2020' ]; const VERIFICATION_METHOD_TYPES: Record<string, string> = { 'Ed25519VerificationKey2020' : 'https://w3id.org/security/suites/ed25519-2020/v1', 'JsonWebKey2020' : 'https://w3id.org/security/suites/jws-2020/v1', 'X25519KeyAgreementKey2020' : 'https://w3id.org/security/suites/x25519-2020/v1', } as const; export type DidVerificationMethodType = keyof typeof VERIFICATION_METHOD_TYPES; const MULTICODEC_PUBLIC_KEY_LENGTH: Record<number, number> = { // secp256k1-pub - Secp256k1 public key (compressed) - 33 bytes 0xe7: 33, // x25519-pub - Curve25519 public key - 32 bytes 0xec: 32, // ed25519-pub - Ed25519 public key - 32 bytes 0xed: 32 }; export type DidKeyCreateOptions = { enableEncryptionKeyDerivation?: boolean; keyAlgorithm?: typeof SupportedCryptoAlgorithms[number]; keySet?: DidKeyKeySet; publicKeyFormat?: DidVerificationMethodType; } export type DidKeyCreateDocumentOptions = { defaultContext?: string; did: string; enableEncryptionKeyDerivation?: boolean; enableExperimentalPublicKeyTypes?: boolean; publicKeyFormat?: DidVerificationMethodType; } export type DidKeyDeriveEncryptionKeyResult = { key: Uint8Array; multicodecCode: number; } export type DidKeyIdentifier = { fragment: string; method: string; multibaseValue: string; scheme: string; version: string; } export type DidKeyKeySet = { verificationMethodKeys?: DidKeySetVerificationMethodKey[]; } export class DidKeyMethod implements DidMethod { /** * Name of the DID method */ public static methodName = 'key'; public static async create(options?: DidKeyCreateOptions): Promise<PortableDid> { let { enableEncryptionKeyDerivation = false, keyAlgorithm, keySet, publicKeyFormat = 'JsonWebKey2020' } = options ?? { }; // If keySet not given, generate a default key set. if (keySet === undefined) { keySet = await DidKeyMethod.generateKeySet({ keyAlgorithm }); } const portableDid: Partial<PortableDid> = {}; let multibaseId = ''; if (keySet.verificationMethodKeys?.[0]?.publicKeyJwk) { // Compute the multibase identifier based on the JSON Web Key. const publicKeyJwk = keySet.verificationMethodKeys[0].publicKeyJwk; multibaseId = await Jose.jwkToMultibaseId({ key: publicKeyJwk }); } if (!multibaseId) { throw new Error('DidKeyMethod: Failed to create DID with given input.'); } // Concatenate the DID identifier. portableDid.did = `did:key:${multibaseId}`; // Expand the DID identifier to a DID document. portableDid.document = await DidKeyMethod.createDocument({ did: portableDid.did, publicKeyFormat, enableEncryptionKeyDerivation }); // Return the given or generated key set. portableDid.keySet = keySet; return portableDid as PortableDid; } /** * Expands a did:key identifier to a DID Document. * * Reference: https://w3c-ccg.github.io/did-method-key/#document-creation-algorithm * * @param options * @returns - A DID dodcument. */ public static async createDocument(options: DidKeyCreateDocumentOptions): Promise<DidDocument> { const { defaultContext = 'https://www.w3.org/ns/did/v1', did, enableEncryptionKeyDerivation = false, enableExperimentalPublicKeyTypes = false, publicKeyFormat = 'JsonWebKey2020' } = options; /** * 1. Initialize document to an empty object. */ const document: Partial<DidDocument> = {}; /** * 2. Using a colon (:) as the delimiter, split the identifier into its * components: a scheme, a method, a version, and a multibaseValue. * If there are only three components set the version to the string * value 1 and use the last value as the multibaseValue. * * Note: The W3C DID specification makes no mention of a version value * being part of the DID syntax. Additionally, there does not * appear to be any real-world usage of the version number. * Consequently, this implementation will ignore the version * related guidance in the did:key specification. */ let multibaseValue: string; try { ({ id: multibaseValue } = parseDid({ didUrl: did })); } catch (error: any) { throw new Error(`invalidDid: Unknown format: ${did}`); } /** * 3. Check the validity of the input identifier. * The scheme MUST be the value did. The method MUST be the value key. * The version MUST be convertible to a positive integer value. The * multibaseValue MUST be a string and begin with the letter z. If any * of these requirements fail, an invalidDid error MUST be raised. */ if (!DidKeyMethod.validateIdentifier({ did })) { throw new Error(`invalidDid: Invalid identifier format: ${did}`); } /** * 4. Initialize the signatureVerificationMethod to the result of passing * identifier, multibaseValue, and options to a * {@link https://w3c-ccg.github.io/did-method-key/#signature-method-creation-algorithm | Signature Method Creation Algorithm}. */ const signatureVerificationMethod = await DidKeyMethod.createSignatureMethod({ did, enableExperimentalPublicKeyTypes, multibaseValue, publicKeyFormat }); /** * 5. Set document.id to identifier. If document.id is not a valid DID, * an invalidDid error MUST be raised. * * Note: Identifier was already confirmed to be valid in Step 3, so * skipping the redundant validation. */ document.id = did; /** * 6. Initialize the verificationMethod property in document to an array * where the first value is the signatureVerificationMethod. */ document.verificationMethod = [signatureVerificationMethod]; /** * 7. Initialize the authentication, assertionMethod, capabilityInvocation, * and the capabilityDelegation properties in document to an array where * the first item is the value of the id property in * signatureVerificationMethod. */ document.authentication = [signatureVerificationMethod.id]; document.assertionMethod = [signatureVerificationMethod.id]; document.capabilityInvocation = [signatureVerificationMethod.id]; document.capabilityDelegation = [signatureVerificationMethod.id]; /** * 8. If options.enableEncryptionKeyDerivation is set to true: * Add the encryptionVerificationMethod value to the verificationMethod * array. Initialize the keyAgreement property in document to an array * where the first item is the value of the id property in * encryptionVerificationMethod. */ if (enableEncryptionKeyDerivation === true) { /** * Although not covered by the did:key method specification, a sensible * default will be taken to use the 'X25519KeyAgreementKey2020' * verification method type if the given publicKeyFormat is * 'Ed25519VerificationKey2020' and 'JsonWebKey2020' otherwise. */ const encryptionPublicKeyFormat = (publicKeyFormat === 'Ed25519VerificationKey2020') ? 'X25519KeyAgreementKey2020' : 'JsonWebKey2020'; /** * 8.1 Initialize the encryptionVerificationMethod to the result of * passing identifier, multibaseValue, and options to an * {@link https://w3c-ccg.github.io/did-method-key/#encryption-method-creation-algorithm | Encryption Method Creation Algorithm}. */ const encryptionVerificationMethod = await this.createEncryptionMethod({ did, enableExperimentalPublicKeyTypes, multibaseValue, publicKeyFormat: encryptionPublicKeyFormat }); /** * 8.2 Add the encryptionVerificationMethod value to the * verificationMethod array. */ document.verificationMethod.push(encryptionVerificationMethod); /** * 8.3. Initialize the keyAgreement property in document to an array * where the first item is the value of the id property in * encryptionVerificationMethod. */ document.keyAgreement = [encryptionVerificationMethod.id]; } /** * 9. Initialize the @context property in document to the result of passing * document and options to the Context Creation algorithm. */ // Set contextArray to an array that is initialized to // options.defaultContext. const contextArray = [defaultContext]; // For every object in every verification relationship listed in document, // add a string value to the contextArray based on the object type value, // if it doesn't already exist, according to the following table: // {@link https://w3c-ccg.github.io/did-method-key/#context-creation-algorithm | Context Type URL} const verificationMethodTypes = getVerificationMethodTypes({ didDocument: document }); verificationMethodTypes.forEach((typeName: string) => { const typeUrl = VERIFICATION_METHOD_TYPES[typeName]; contextArray.push(typeUrl); }); document['@context'] = contextArray; /** * 10. Return document. */ return document as DidDocument; } /** * Decoding a multibase-encoded multicodec value into a verification method * that is suitable for verifying that encrypted information will be * received by the intended recipient. */ public static async createEncryptionMethod(options: { did: string, enableExperimentalPublicKeyTypes: boolean, multibaseValue: string, publicKeyFormat: DidVerificationMethodType }): Promise<VerificationMethod> { const { did, enableExperimentalPublicKeyTypes, multibaseValue, publicKeyFormat } = options; /** * 1. Initialize verificationMethod to an empty object. */ const verificationMethod: Partial<VerificationMethod> = {}; /** * 2. Set multicodecValue and rawPublicKeyBytes to the result of passing * multibaseValue and options to a Derive Encryption Key algorithm. */ const { key: rawPublicKeyBytes, multicodecCode: multicodecValue, } = await DidKeyMethod.deriveEncryptionKey({ multibaseValue }); /** * 3. Ensure the proper key length of rawPublicKeyBytes based on the * multicodecValue table provided below: * * Multicodec hexadecimal value: 0xec * * If the byte length of rawPublicKeyBytes * does not match the expected public key length for the associated * multicodecValue, an invalidPublicKeyLength error MUST be raised. */ const actualLength = rawPublicKeyBytes.byteLength; const expectedLength = MULTICODEC_PUBLIC_KEY_LENGTH[multicodecValue]; if (actualLength !== expectedLength) { throw new Error(`invalidPublicKeyLength: Expected ${actualLength} bytes. Actual ${expectedLength} bytes.`); } /** * 4. Create the multibaseValue by concatenating the letter 'z' and the * base58-btc encoding of the concatenation of the multicodecValue and * the rawPublicKeyBytes. */ const kemMultibaseValue = cryptoUtils.keyToMultibaseId({ key : rawPublicKeyBytes, multicodecCode : multicodecValue }); /** * 5. Set the verificationMethod.id value by concatenating identifier, * a hash character (#), and the multibaseValue. If verificationMethod.id * is not a valid DID URL, an invalidDidUrl error MUST be raised. */ verificationMethod.id = `${did}#${kemMultibaseValue}`; try { new URL(verificationMethod.id); } catch (error: any) { throw new Error('invalidDidUrl: Verification Method ID is not a valid DID URL.'); } /** * 6. Set the publicKeyFormat value to the options.publicKeyFormat value. * 7. If publicKeyFormat is not known to the implementation, an * unsupportedPublicKeyType error MUST be raised. */ if (!(SupportedPublicKeyFormats.includes(publicKeyFormat))) { throw new Error(`unsupportedPublicKeyType: Unsupported format: ${publicKeyFormat}`); } /** * 8. If options.enableExperimentalPublicKeyTypes is set to false and * publicKeyFormat is not Multikey, JsonWebKey2020, or * X25519KeyAgreementKey2020, an invalidPublicKeyType error MUST be * raised. */ const StandardPublicKeyTypes = ['Multikey', 'JsonWebKey2020', 'X25519KeyAgreementKey2020']; if (enableExperimentalPublicKeyTypes === false && !(StandardPublicKeyTypes.includes(publicKeyFormat))) { throw new Error(`invalidPublicKeyType: Specified '${publicKeyFormat}' without setting enableExperimentalPublicKeyTypes to true.`); } /** * 9. Set verificationMethod.type to the publicKeyFormat value. */ verificationMethod.type = publicKeyFormat; /** * 10. Set verificationMethod.controller to the identifier value. * If verificationMethod.controller is not a valid DID, an invalidDid * error MUST be raised. */ verificationMethod.controller = did; if (!DidKeyMethod.validateIdentifier({ did })) { throw new Error(`invalidDid: Invalid identifier format: ${did}`); } /** * 11. If publicKeyFormat is Multikey or X25519KeyAgreementKey2020, * set the verificationMethod.publicKeyMultibase value to multibaseValue. * * Note: This implementation does not currently support the Multikey * format. */ if (publicKeyFormat === 'X25519KeyAgreementKey2020') { verificationMethod.publicKeyMultibase = kemMultibaseValue; } /** * 12. If publicKeyFormat is JsonWebKey2020, set the * verificationMethod.publicKeyJwk value to the result of passing * multicodecValue and rawPublicKeyBytes to a JWK encoding algorithm. */ if (publicKeyFormat === 'JsonWebKey2020') { const jwkParams = await Jose.multicodecToJose({ code: multicodecValue }); const jsonWebKey = await Jose.keyToJwk({ keyMaterial : rawPublicKeyBytes, keyType : 'public', ...jwkParams }); // Ensure that "d" is NOT present. if ('x' in jsonWebKey && !('d' in jsonWebKey)) { verificationMethod.publicKeyJwk = jsonWebKey; } } /** * 13. Return verificationMethod. */ return verificationMethod as VerificationMethod; } /** * Transform a multibase-encoded multicodec value to public encryption key * components that are suitable for encrypting messages to a receiver. A * mathematical proof elaborating on the safety of performing this operation * is available in: * {@link https://eprint.iacr.org/2021/509.pdf | On using the same key pair for Ed25519 and an X25519 based KEM} */ public static async deriveEncryptionKey(options: { multibaseValue: string }): Promise<DidKeyDeriveEncryptionKeyResult> { const { multibaseValue } = options; /** * 1. Set publicEncryptionKey to an empty object. */ let publicEncryptionKey: Partial<DidKeyDeriveEncryptionKeyResult> = {}; /** * 2. Decode multibaseValue using the base58-btc multibase alphabet and * set multicodecValue to the multicodec header for the decoded value. * Implementers are cautioned to ensure that the multicodecValue is set * to the result after performing varint decoding. * * 3. Set the rawPublicKeyBytes to the bytes remaining after the multicodec * header. */ const { key: rawPublicKeyBytes, multicodecCode: multicodecValue } = cryptoUtils.multibaseIdToKey({ multibaseKeyId: multibaseValue }); /** * 4. If the multicodecValue is 0xed, derive a public X25519 encryption key * by using the rawPublicKeyBytes and the algorithm defined in * {@link https://datatracker.ietf.org/doc/html/draft-ietf-core-oscore-groupcomm | Group OSCORE - Secure Group Communication for CoAP} * for Curve25519 in Section 2.4.2: ECDH with Montgomery Coordinates and * set generatedPublicEncryptionKeyBytes to the result. */ if (multicodecValue === 0xed) { const generatedPublicEncryptionKeyBytes = await Ed25519.convertPublicKeyToX25519({ publicKey: rawPublicKeyBytes }); /** * 5. Set multicodecValue in publicEncryptionKey to 0xec. * * 6. Set rawPublicKeyBytes in publicEncryptionKey to * generatedPublicEncryptionKeyBytes. */ publicEncryptionKey = { key : generatedPublicEncryptionKeyBytes, multicodecCode : 0xec }; } /** * 7. Return publicEncryptionKey. */ return publicEncryptionKey as DidKeyDeriveEncryptionKeyResult; } /** * Decodes a multibase-encoded multicodec value into a verification method * that is suitable for verifying digital signatures. * @param options - Signature method creation algorithm inputs. * @returns - A verification method. */ public static async createSignatureMethod(options: { did: string, enableExperimentalPublicKeyTypes: boolean, multibaseValue: string, publicKeyFormat: DidVerificationMethodType }): Promise<VerificationMethod> { const { did, enableExperimentalPublicKeyTypes, multibaseValue, publicKeyFormat } = options; /** * 1. Initialize verificationMethod to an empty object. */ const verificationMethod: Partial<VerificationMethod> = {}; /** * 2. Set multicodecValue and rawPublicKeyBytes to the result of passing * multibaseValue and options to a Decode Public Key algorithm. */ const { key: rawPublicKeyBytes, multicodecCode: multicodecValue, multicodecName } = cryptoUtils.multibaseIdToKey({ multibaseKeyId: multibaseValue }); /** * 3. Ensure the proper key length of rawPublicKeyBytes based on the * multicodecValue {@link https://w3c-ccg.github.io/did-method-key/#signature-method-creation-algorithm | table provided}. * If the byte length of rawPublicKeyBytes does not match the expected * public key length for the associated multicodecValue, an * invalidPublicKeyLength error MUST be raised. */ const actualLength = rawPublicKeyBytes.byteLength; const expectedLength = MULTICODEC_PUBLIC_KEY_LENGTH[multicodecValue]; if (actualLength !== expectedLength) { throw new Error(`invalidPublicKeyLength: Expected ${actualLength} bytes. Actual ${expectedLength} bytes.`); } /** * 4. Ensure the rawPublicKeyBytes are a proper encoding of the public * key type as specified by the multicodecValue. This validation is often * done by a cryptographic library when importing the public key by, * for example, ensuring that an Elliptic Curve public key is a specific * coordinate that exists on the elliptic curve. If an invalid public key * value is detected, an invalidPublicKey error MUST be raised. */ let isValid = false; switch (multicodecName) { case 'secp256k1-pub': isValid = await Secp256k1.validatePublicKey({ key: rawPublicKeyBytes }); break; case 'ed25519-pub': isValid = await Ed25519.validatePublicKey({ key: rawPublicKeyBytes }); break; case 'x25519-pub': // TODO: validate key once/if X25519.validatePublicKey() is implemented. // isValid = X25519.validatePublicKey({ key: rawPublicKeyBytes}) isValid = true; break; } if (!isValid) { throw new Error('invalidPublicKey: Invalid public key detected.'); } /** * 5. Set the verificationMethod.id value by concatenating identifier, * a hash character (#), and the multibaseValue. If verificationMethod.id * is not a valid DID URL, an invalidDidUrl error MUST be raised. */ verificationMethod.id = `${did}#${multibaseValue}`; try { new URL(verificationMethod.id); } catch (error: any) { throw new Error('invalidDidUrl: Verification Method ID is not a valid DID URL.'); } /** * 6. Set the publicKeyFormat value to the options.publicKeyFormat value. * 7. If publicKeyFormat is not known to the implementation, an * unsupportedPublicKeyType error MUST be raised. */ if (!(SupportedPublicKeyFormats.includes(publicKeyFormat))) { throw new Error(`unsupportedPublicKeyType: Unsupported format: ${publicKeyFormat}`); } /** * 8. If options.enableExperimentalPublicKeyTypes is set to false and * publicKeyFormat is not Multikey, JsonWebKey2020, or * Ed25519VerificationKey2020, an invalidPublicKeyType error MUST be * raised. */ const StandardPublicKeyTypes = ['Multikey', 'JsonWebKey2020', 'Ed25519VerificationKey2020']; if (enableExperimentalPublicKeyTypes === false && !(StandardPublicKeyTypes.includes(publicKeyFormat))) { throw new Error(`invalidPublicKeyType: Specified '${publicKeyFormat}' without setting enableExperimentalPublicKeyTypes to true.`); } /** * 9. Set verificationMethod.type to the publicKeyFormat value. */ verificationMethod.type = publicKeyFormat; /** * 10. Set verificationMethod.controller to the identifier value. * If verificationMethod.controller is not a valid DID, an invalidDid * error MUST be raised. */ verificationMethod.controller = did; if (!DidKeyMethod.validateIdentifier({ did })) { throw new Error(`invalidDid: Invalid identifier format: ${did}`); } /** * 11. If publicKeyFormat is Multikey or Ed25519VerificationKey2020, * set the verificationMethod.publicKeyMultibase value to multibaseValue. * * Note: This implementation does not currently support the Multikey * format. */ if (publicKeyFormat === 'Ed25519VerificationKey2020') { verificationMethod.publicKeyMultibase = multibaseValue; } /** * 12. If publicKeyFormat is JsonWebKey2020, set the * verificationMethod.publicKeyJwk value to the result of passing * multicodecValue and rawPublicKeyBytes to a JWK encoding algorithm. */ if (publicKeyFormat === 'JsonWebKey2020') { const jwkParams = await Jose.multicodecToJose({ code: multicodecValue }); const jsonWebKey = await Jose.keyToJwk({ keyMaterial : rawPublicKeyBytes, keyType : 'public', ...jwkParams }); // Ensure that "d" is NOT present. if ('x' in jsonWebKey && !('d' in jsonWebKey)) { verificationMethod.publicKeyJwk = jsonWebKey; } } /** * 13. Return verificationMethod. */ return verificationMethod as VerificationMethod; } public static async generateKeySet(options?: { keyAlgorithm?: typeof SupportedCryptoAlgorithms[number] }): Promise<DidKeyKeySet> { // Generate Ed25519 keys, by default. const { keyAlgorithm = 'Ed25519' } = options ?? {}; let keyPair: IDCrypto.CryptoKeyPair; switch (keyAlgorithm) { case 'Ed25519': { keyPair = await new EdDsaAlgorithm().generateKey({ algorithm : { name: 'EdDSA', namedCurve: 'Ed25519' }, extractable : true, keyUsages : ['sign', 'verify'] }); break; } case 'secp256k1': { keyPair = await new EcdsaAlgorithm().generateKey({ algorithm : { name: 'ECDSA', namedCurve: 'secp256k1' }, extractable : true, keyUsages : ['sign', 'verify'] }); break; } default: { throw new Error(`Unsupported crypto algorithm: '${keyAlgorithm}'`); } } const publicKeyJwk = await Jose.cryptoKeyToJwk({ key: keyPair.publicKey }) as PublicKeyJwk; const privateKeyJwk = await Jose.cryptoKeyToJwk({ key: keyPair.privateKey }) as PrivateKeyJwk; const keySet: DidKeyKeySet = { verificationMethodKeys: [{ publicKeyJwk, privateKeyJwk, relationships: ['authentication'] }] }; return keySet; } /** * Given the W3C DID Document of a `did:key` DID, return the identifier of * the verification method key that will be used for signing messages and * credentials, by default. * * @param document = DID Document to get the default signing key from. * @returns Verification method identifier for the default signing key. */ public static async getDefaultSigningKey(options: { didDocument: DidDocument }): Promise<string | undefined> { const { didDocument } = options; if (didDocument.authentication && Array.isArray(didDocument.authentication) && didDocument.authentication.length > 0 && typeof didDocument.authentication[0] === 'string') { const [verificationMethodId] = didDocument.authentication; const signingKeyId = verificationMethodId; return signingKeyId; } } public static async resolve(options: { didUrl: string, resolutionOptions?: DidResolutionOptions }): Promise<DidResolutionResult> { const { didUrl, resolutionOptions: _ } = options; // TODO: add resolutionOptions as defined in https://www.w3.org/TR/did-core/#did-resolution const parsedDid = parseDid({ didUrl }); if (!parsedDid) { return { '@context' : 'https://w3id.org/did-resolution/v1', didDocument : undefined, didDocumentMetadata : {}, didResolutionMetadata : { contentType : 'application/did+json', error : 'invalidDid', errorMessage : `Cannot parse DID: ${didUrl}` } }; } if (parsedDid.method !== 'key') { return { '@context' : 'https://w3id.org/did-resolution/v1', didDocument : undefined, didDocumentMetadata : {}, didResolutionMetadata : { contentType : 'application/did+json', error : 'methodNotSupported', errorMessage : `Method not supported: ${parsedDid.method}` } }; } const didDocument = await DidKeyMethod.createDocument({ did: parsedDid.did }); return { '@context' : 'https://w3id.org/did-resolution/v1', didDocument, didDocumentMetadata : {}, didResolutionMetadata : { contentType : 'application/did+json', did : { didString : parsedDid.did, methodSpecificId : parsedDid.id, method : parsedDid.method } } }; } public static validateIdentifier(options: { did: string }): boolean { const { did } = options; const { method, id: multibaseValue } = parseDid({ didUrl: did }); const [scheme] = did.split(':', 1); /** * Note: The W3C DID specification makes no mention of a version value * being part of the DID syntax. Additionally, there does not * appear to be any real-world usage of the version number. * Consequently, this implementation will ignore the version * related guidance in the did:key specification. */ const version = '1'; return ( scheme !== 'did' || method !== 'key' || parseInt(version) > 0 || universalTypeOf(multibaseValue) !== 'String' || !multibaseValue.startsWith('z') ); } }