diff --git a/packages/did-utils/src/did-functions.ts b/packages/did-utils/src/did-functions.ts index 6d9c450b..83fc07cd 100644 --- a/packages/did-utils/src/did-functions.ts +++ b/packages/did-utils/src/did-functions.ts @@ -1,197 +1,207 @@ -import { computeAddress } from '@ethersproject/transactions' -import { UniResolver } from '@sphereon/did-uni-client' +import {computeAddress} from '@ethersproject/transactions' +import {UniResolver} from '@sphereon/did-uni-client' import { - ENC_KEY_ALGS, - getKms, - JwkKeyUse, - keyTypeFromCryptographicSuite, - signatureAlgorithmFromKey, - TKeyType, - toJwk, + ENC_KEY_ALGS, + getKms, + JwkKeyUse, + keyTypeFromCryptographicSuite, + sanitizedJwk, + signatureAlgorithmFromKey, + TKeyType, + toJwk, } from '@sphereon/ssi-sdk-ext.key-utils' -import { base64ToHex, hexKeyFromPEMBasedJwk } from '@sphereon/ssi-sdk-ext.x509-utils' -import { base58ToBytes, base64ToBytes, bytesToHex, hexToBytes, multibaseKeyToBytes } from '@sphereon/ssi-sdk.core' -import { JWK } from '@sphereon/ssi-types' -import { convertPublicKeyToX25519 } from '@stablelib/ed25519' -import { DIDDocument, DIDDocumentSection, DIDResolutionResult, IAgentContext, IDIDManager, IIdentifier, IKey, IResolver } from '@veramo/core' +import {base64ToHex, hexKeyFromPEMBasedJwk} from '@sphereon/ssi-sdk-ext.x509-utils' +import {base58ToBytes, base64ToBytes, bytesToHex, hexToBytes, multibaseKeyToBytes} from '@sphereon/ssi-sdk.core' +import {JWK} from '@sphereon/ssi-types' +import {convertPublicKeyToX25519} from '@stablelib/ed25519' import { - _ExtendedIKey, - _ExtendedVerificationMethod, - _NormalizedVerificationMethod, - compressIdentifierSecp256k1Keys, - convertIdentifierEncryptionKeys, - getEthereumAddress, - isDefined, - mapIdentifierKeysToDoc, + DIDDocument, + DIDDocumentSection, + DIDResolutionResult, + IAgentContext, + IDIDManager, + IIdentifier, + IKey, + IResolver +} from '@veramo/core' +import { + _ExtendedIKey, + _ExtendedVerificationMethod, + _NormalizedVerificationMethod, + compressIdentifierSecp256k1Keys, + convertIdentifierEncryptionKeys, + getEthereumAddress, + isDefined, + mapIdentifierKeysToDoc, } from '@veramo/utils' -import { createJWT, Signer } from 'did-jwt' -import { DIDResolutionOptions, JsonWebKey, Resolvable, VerificationMethod } from 'did-resolver' +import {createJWT, Signer} from 'did-jwt' +import {DIDResolutionOptions, JsonWebKey, Resolvable, VerificationMethod} from 'did-resolver' // @ts-ignore import elliptic from 'elliptic' import * as u8a from 'uint8arrays' import { - CreateIdentifierOpts, - CreateOrGetIdentifierOpts, - DID_PREFIX, - GetOrCreateResult, - GetSignerArgs, - IdentifierAliasEnum, - IdentifierProviderOpts, - IDIDOptions, - SignJwtArgs, - SupportedDidMethodEnum, + CreateIdentifierOpts, + CreateOrGetIdentifierOpts, + DID_PREFIX, + GetOrCreateResult, + GetSignerArgs, + IdentifierAliasEnum, + IdentifierProviderOpts, + IDIDOptions, + SignJwtArgs, + SupportedDidMethodEnum, } from './types' export const getAuthenticationKey = async ( - { - identifier, - offlineWhenNoDIDRegistered, - noVerificationMethodFallback, - keyType, - controllerKey, - }: { - identifier: IIdentifier - keyType?: TKeyType - offlineWhenNoDIDRegistered?: boolean - noVerificationMethodFallback?: boolean - controllerKey?: boolean - }, - context: IAgentContext -): Promise<_ExtendedIKey> => { - return await getFirstKeyWithRelation( { - identifier, - offlineWhenNoDIDRegistered, - noVerificationMethodFallback, - keyType, - controllerKey, - vmRelationship: 'authentication', + identifier, + offlineWhenNoDIDRegistered, + noVerificationMethodFallback, + keyType, + controllerKey, + }: { + identifier: IIdentifier + keyType?: TKeyType + offlineWhenNoDIDRegistered?: boolean + noVerificationMethodFallback?: boolean + controllerKey?: boolean }, - context - ) -} -export const getFirstKeyWithRelation = async ( - { - identifier, - offlineWhenNoDIDRegistered, - noVerificationMethodFallback, - keyType, - controllerKey, - vmRelationship, - }: { - identifier: IIdentifier - keyType?: TKeyType - offlineWhenNoDIDRegistered?: boolean - noVerificationMethodFallback?: boolean - controllerKey?: boolean - vmRelationship: DIDDocumentSection - }, - context: IAgentContext + context: IAgentContext ): Promise<_ExtendedIKey> => { - let key: _ExtendedIKey | undefined = undefined - try { - key = - (await getFirstKeyWithRelationFromDIDDoc( + return await getFirstKeyWithRelation( { - identifier, - vmRelationship, - errorOnNotFound: false, - keyType, - controllerKey, + identifier, + offlineWhenNoDIDRegistered, + noVerificationMethodFallback, + keyType, + controllerKey, + vmRelationship: 'authentication', }, context - )) ?? - (noVerificationMethodFallback || vmRelationship === 'verificationMethod' // let's not fallback to the same value again - ? undefined - : await getFirstKeyWithRelationFromDIDDoc( - { - identifier, - vmRelationship: 'verificationMethod', - errorOnNotFound: false, - keyType, - controllerKey, - }, - context - )) - } catch (e) { - if (e instanceof Error) { - if (!e.message.includes('404') || !offlineWhenNoDIDRegistered) { - throw e - } - } else { - throw e - } - } - if (!key && offlineWhenNoDIDRegistered) { - const offlineDID = toDidDocument(identifier) - key = - (await getFirstKeyWithRelationFromDIDDoc( - { - identifier, - vmRelationship, - errorOnNotFound: false, - didDocument: offlineDID, - keyType, - controllerKey, - }, - context - )) ?? - (noVerificationMethodFallback || vmRelationship === 'verificationMethod' // let's not fallback to the same value again - ? undefined - : await getFirstKeyWithRelationFromDIDDoc( - { - identifier, - vmRelationship: 'verificationMethod', - errorOnNotFound: false, - didDocument: offlineDID, - keyType, - controllerKey, - }, - context - )) + ) +} +export const getFirstKeyWithRelation = async ( + { + identifier, + offlineWhenNoDIDRegistered, + noVerificationMethodFallback, + keyType, + controllerKey, + vmRelationship, + }: { + identifier: IIdentifier + keyType?: TKeyType + offlineWhenNoDIDRegistered?: boolean + noVerificationMethodFallback?: boolean + controllerKey?: boolean + vmRelationship: DIDDocumentSection + }, + context: IAgentContext +): Promise<_ExtendedIKey> => { + let key: _ExtendedIKey | undefined = undefined + try { + key = + (await getFirstKeyWithRelationFromDIDDoc( + { + identifier, + vmRelationship, + errorOnNotFound: false, + keyType, + controllerKey, + }, + context + )) ?? + (noVerificationMethodFallback || vmRelationship === 'verificationMethod' // let's not fallback to the same value again + ? undefined + : await getFirstKeyWithRelationFromDIDDoc( + { + identifier, + vmRelationship: 'verificationMethod', + errorOnNotFound: false, + keyType, + controllerKey, + }, + context + )) + } catch (e) { + if (e instanceof Error) { + if (!e.message.includes('404') || !offlineWhenNoDIDRegistered) { + throw e + } + } else { + throw e + } + } + if (!key && offlineWhenNoDIDRegistered) { + const offlineDID = toDidDocument(identifier) + key = + (await getFirstKeyWithRelationFromDIDDoc( + { + identifier, + vmRelationship, + errorOnNotFound: false, + didDocument: offlineDID, + keyType, + controllerKey, + }, + context + )) ?? + (noVerificationMethodFallback || vmRelationship === 'verificationMethod' // let's not fallback to the same value again + ? undefined + : await getFirstKeyWithRelationFromDIDDoc( + { + identifier, + vmRelationship: 'verificationMethod', + errorOnNotFound: false, + didDocument: offlineDID, + keyType, + controllerKey, + }, + context + )) + if (!key) { + key = identifier.keys + .map((key) => key as _ExtendedIKey) + .filter((key) => keyType === undefined || key.type === keyType || (controllerKey && key.kid === identifier.controllerKeyId)) + .find((key) => key.meta.verificationMethod?.type.includes('authentication') || key.meta.purposes?.includes('authentication')) + } + } if (!key) { - key = identifier.keys - .map((key) => key as _ExtendedIKey) - .filter((key) => keyType === undefined || key.type === keyType || (controllerKey && key.kid === identifier.controllerKeyId)) - .find((key) => key.meta.verificationMethod?.type.includes('authentication') || key.meta.purposes?.includes('authentication')) + throw Error(`Could not find authentication key for DID ${identifier.did}`) } - } - if (!key) { - throw Error(`Could not find authentication key for DID ${identifier.did}`) - } - return key + return key } export const getOrCreatePrimaryIdentifier = async ( - context: IAgentContext, - opts?: CreateOrGetIdentifierOpts + context: IAgentContext, + opts?: CreateOrGetIdentifierOpts ): Promise> => { - const primaryIdentifier = await getPrimaryIdentifier(context, { ...opts?.createOpts?.options, ...(opts?.method && { method: opts.method }) }) - if (primaryIdentifier !== undefined) { - return { - created: false, - result: primaryIdentifier, + const primaryIdentifier = await getPrimaryIdentifier(context, {...opts?.createOpts?.options, ...(opts?.method && {method: opts.method})}) + if (primaryIdentifier !== undefined) { + return { + created: false, + result: primaryIdentifier, + } } - } - if (opts?.method === SupportedDidMethodEnum.DID_KEY) { - const createOpts = opts?.createOpts ?? {} - createOpts.options = { codecName: 'EBSI', type: 'Secp256r1', ...createOpts } - opts.createOpts = createOpts - } - const createdIdentifier = await createIdentifier(context, opts) - return { - created: true, - result: createdIdentifier, - } + if (opts?.method === SupportedDidMethodEnum.DID_KEY) { + const createOpts = opts?.createOpts ?? {} + createOpts.options = {codecName: 'EBSI', type: 'Secp256r1', ...createOpts} + opts.createOpts = createOpts + } + const createdIdentifier = await createIdentifier(context, opts) + return { + created: true, + result: createdIdentifier, + } } export const getPrimaryIdentifier = async (context: IAgentContext, opts?: IdentifierProviderOpts): Promise => { - const identifiers = (await context.agent.didManagerFind(opts?.method ? { provider: `${DID_PREFIX}${opts?.method}` } : {})).filter( - (identifier: IIdentifier) => opts?.type === undefined || identifier.keys.some((key: IKey) => key.type === opts?.type) - ) + const identifiers = (await context.agent.didManagerFind(opts?.method ? {provider: `${DID_PREFIX}${opts?.method}`} : {})).filter( + (identifier: IIdentifier) => opts?.type === undefined || identifier.keys.some((key: IKey) => key.type === opts?.type) + ) - return identifiers && identifiers.length > 0 ? identifiers[0] : undefined + return identifiers && identifiers.length > 0 ? identifiers[0] : undefined } export const createIdentifier = async (context: IAgentContext, opts?: CreateIdentifierOpts): Promise => { @@ -204,80 +214,80 @@ export const createIdentifier = async (context: IAgentContext, opts } export const getFirstKeyWithRelationFromDIDDoc = async ( - { - identifier, - vmRelationship = 'verificationMethod', - keyType, - errorOnNotFound = false, - didDocument, - controllerKey, - }: { - identifier: IIdentifier - controllerKey?: boolean - vmRelationship?: DIDDocumentSection - keyType?: TKeyType - errorOnNotFound?: boolean - didDocument?: DIDDocument - }, - context: IAgentContext + { + identifier, + vmRelationship = 'verificationMethod', + keyType, + errorOnNotFound = false, + didDocument, + controllerKey, + }: { + identifier: IIdentifier + controllerKey?: boolean + vmRelationship?: DIDDocumentSection + keyType?: TKeyType + errorOnNotFound?: boolean + didDocument?: DIDDocument + }, + context: IAgentContext ): Promise<_ExtendedIKey | undefined> => { - const matchedKeys = await mapIdentifierKeysToDocWithJwkSupport({ identifier, vmRelationship, didDocument }, context) - if (Array.isArray(matchedKeys) && matchedKeys.length > 0) { - const result = matchedKeys.find( - (key) => keyType === undefined || key.type === keyType || (controllerKey && key.kid === identifier.controllerKeyId) - ) - if (result) { - return result + const matchedKeys = await mapIdentifierKeysToDocWithJwkSupport({identifier, vmRelationship, didDocument}, context) + if (Array.isArray(matchedKeys) && matchedKeys.length > 0) { + const result = matchedKeys.find( + (key) => keyType === undefined || key.type === keyType || (controllerKey && key.kid === identifier.controllerKeyId) + ) + if (result) { + return result + } } - } - if (errorOnNotFound) { - throw new Error( - `Could not find key with relationship ${vmRelationship} in DID document for ${identifier.did}${keyType ? ' and key type: ' + keyType : ''}` - ) - } - return undefined + if (errorOnNotFound) { + throw new Error( + `Could not find key with relationship ${vmRelationship} in DID document for ${identifier.did}${keyType ? ' and key type: ' + keyType : ''}` + ) + } + return undefined } -export const getEthereumAddressFromKey = ({ key }: { key: IKey }) => { - if (key.type !== 'Secp256k1') { - throw Error(`Can only get ethereum address from a Secp256k1 key. Type is ${key.type} for keyRef: ${key.kid}`) - } - const ethereumAddress = key.meta?.ethereumAddress ?? key.meta?.account?.toLowerCase() ?? computeAddress(`0x${key.publicKeyHex}`).toLowerCase() - if (!ethereumAddress) { - throw Error(`Could not get or generate ethereum address from key with keyRef ${key.kid}`) - } - return ethereumAddress +export const getEthereumAddressFromKey = ({key}: { key: IKey }) => { + if (key.type !== 'Secp256k1') { + throw Error(`Can only get ethereum address from a Secp256k1 key. Type is ${key.type} for keyRef: ${key.kid}`) + } + const ethereumAddress = key.meta?.ethereumAddress ?? key.meta?.account?.toLowerCase() ?? computeAddress(`0x${key.publicKeyHex}`).toLowerCase() + if (!ethereumAddress) { + throw Error(`Could not get or generate ethereum address from key with keyRef ${key.kid}`) + } + return ethereumAddress } -export const getControllerKey = ({ identifier }: { identifier: IIdentifier }) => { - const key = identifier.keys.find((key) => key.kid === identifier.controllerKeyId) - if (!key) { - throw Error(`Could not get controller key for identifier ${identifier}`) - } - return key +export const getControllerKey = ({identifier}: { identifier: IIdentifier }) => { + const key = identifier.keys.find((key) => key.kid === identifier.controllerKeyId) + if (!key) { + throw Error(`Could not get controller key for identifier ${identifier}`) + } + return key } export const getKeys = ({ - jwkThumbprint, - kms, - identifier, - kmsKeyRef, - keyType, - controllerKey, -}: { - identifier: IIdentifier - kmsKeyRef?: string - keyType?: TKeyType - kms?: string - jwkThumbprint?: string - controllerKey?: boolean + jwkThumbprint, + kms, + identifier, + kmsKeyRef, + keyType, + controllerKey, + }: { + identifier: IIdentifier + kmsKeyRef?: string + keyType?: TKeyType + kms?: string + jwkThumbprint?: string + controllerKey?: boolean }) => { - return identifier.keys - .filter((key) => !keyType || key.type === keyType) - .filter((key) => !kms || key.kms === kms) - .filter((key) => !kmsKeyRef || key.kid === kmsKeyRef) - .filter((key) => !jwkThumbprint || key.meta?.jwkThumbprint === jwkThumbprint) - .filter((key) => !controllerKey || identifier.controllerKeyId === key.kid) + return identifier.keys + .filter((key) => !keyType || key.type === keyType) + .filter((key) => !kms || key.kms === kms) + .filter((key) => !kmsKeyRef || key.kid === kmsKeyRef) + .filter((key) => !jwkThumbprint || key.meta?.jwkThumbprint === jwkThumbprint) + .filter((key) => !controllerKey || identifier.controllerKeyId === key.kid) } //TODO: Move to ssi-sdk/core and create PR upstream @@ -292,52 +302,52 @@ export const getKeys = ({ * @beta This API may change without a BREAKING CHANGE notice. */ export async function dereferenceDidKeysWithJwkSupport( - didDocument: DIDDocument, - section: DIDDocumentSection = 'keyAgreement', - context: IAgentContext + didDocument: DIDDocument, + section: DIDDocumentSection = 'keyAgreement', + context: IAgentContext ): Promise<_NormalizedVerificationMethod[]> { - const convert = section === 'keyAgreement' - if (section === 'service') { - return [] - } - return ( - await Promise.all( - (didDocument[section] || []).map(async (key: string | VerificationMethod) => { - if (typeof key === 'string') { - try { - return (await context.agent.getDIDComponentById({ - didDocument, - didUrl: key, - section, - })) as _ExtendedVerificationMethod - } catch (e) { - return null - } - } else { - return key as _ExtendedVerificationMethod - } - }) + const convert = section === 'keyAgreement' + if (section === 'service') { + return [] + } + return ( + await Promise.all( + (didDocument[section] || []).map(async (key: string | VerificationMethod) => { + if (typeof key === 'string') { + try { + return (await context.agent.getDIDComponentById({ + didDocument, + didUrl: key, + section, + })) as _ExtendedVerificationMethod + } catch (e) { + return null + } + } else { + return key as _ExtendedVerificationMethod + } + }) + ) ) - ) - .filter(isDefined) - .map((key) => { - const hexKey = extractPublicKeyHexWithJwkSupport(key, convert) - const { publicKeyHex, publicKeyBase58, publicKeyBase64, publicKeyJwk, ...keyProps } = key - const newKey = { ...keyProps, publicKeyHex: hexKey } - if (convert && 'Ed25519VerificationKey2018' === newKey.type) { - newKey.type = 'X25519KeyAgreementKey2019' - } - return newKey - }) + .filter(isDefined) + .map((key) => { + const hexKey = extractPublicKeyHexWithJwkSupport(key, convert) + const {publicKeyHex, publicKeyBase58, publicKeyBase64, publicKeyJwk, ...keyProps} = key + const newKey = {...keyProps, publicKeyHex: hexKey} + if (convert && 'Ed25519VerificationKey2018' === newKey.type) { + newKey.type = 'X25519KeyAgreementKey2019' + } + return newKey + }) } export function jwkTtoPublicKeyHex(jwk: JWK): string { - // todo: Hacky way to convert this to a VM. Should extract the logic from the below methods - // @ts-ignore - const vm: _ExtendedVerificationMethod = { - publicKeyJwk: jwk, - } - return extractPublicKeyHexWithJwkSupport(vm) + // todo: Hacky way to convert this to a VM. Should extract the logic from the below methods + // @ts-ignore + const vm: _ExtendedVerificationMethod = { + publicKeyJwk: sanitizedJwk(jwk), + } + return extractPublicKeyHexWithJwkSupport(vm) } /** @@ -350,38 +360,42 @@ export function jwkTtoPublicKeyHex(jwk: JWK): string { * @beta This API may change without a BREAKING CHANGE notice. */ export function extractPublicKeyHexWithJwkSupport(pk: _ExtendedVerificationMethod, convert = false): string { - if (pk.publicKeyJwk) { - if (pk.publicKeyJwk.kty === 'EC') { - const curve = pk.publicKeyJwk.crv ? toEcLibCurve(pk.publicKeyJwk.crv) : 'p256' - const ec = new elliptic.ec(curve) - - const xHex = base64ToHex(pk.publicKeyJwk.x!, 'base64url') - const yHex = base64ToHex(pk.publicKeyJwk.y!, 'base64url') - const prefix = '04' // isEven(yHex) ? '02' : '03' - // Uncompressed Hex format: 04 - // Compressed Hex format: 02 (for even y) or 03 (for uneven y) - const hex = `${prefix}${xHex}${yHex}` - // We return directly as we don't want to convert the result back into Uint8Array and then convert again to hex as the elliptic lib already returns hex strings - const publicKeyHex = ec.keyFromPublic(hex, 'hex').getPublic(true, 'hex') - // This returns a short form (x) with 02 or 03 prefix - return publicKeyHex - } else if (pk.publicKeyJwk.crv === 'Ed25519') { - return u8a.toString(u8a.fromString(pk.publicKeyJwk.x!, 'base64url'), 'base16') - } else if (pk.publicKeyJwk.kty === 'RSA') { - return hexKeyFromPEMBasedJwk(pk.publicKeyJwk, 'public') - } - } - // delegate the other types to the original Veramo function - return extractPublicKeyHex(pk, convert) + if (pk.publicKeyJwk) { + const jwk = sanitizedJwk(pk.publicKeyJwk) + if (jwk.kty === 'EC') { + const curve = jwk.crv ? toEcLibCurve(jwk.crv) : 'p256' + const xHex = base64ToHex(jwk.x!, 'base64url') + const yHex = base64ToHex(jwk.y!, 'base64url') + const prefix = '04' // isEven(yHex) ? '02' : '03' + // Uncompressed Hex format: 04 + // Compressed Hex format: 02 (for even y) or 03 (for uneven y) + const hex = `${prefix}${xHex}${yHex}` + try { + const ec = new elliptic.ec(curve) + // We return directly as we don't want to convert the result back into Uint8Array and then convert again to hex as the elliptic lib already returns hex strings + const publicKeyHex = ec.keyFromPublic(hex, 'hex').getPublic(true, 'hex') + // This returns a short form (x) with 02 or 03 prefix + return publicKeyHex + } catch (error: any) { + console.error(`Error converting EC with elliptic lib curve ${curve} from JWK to hex. x: ${jwk.x}, y: ${jwk.y}, error: ${error}`, error) + } + } else if (jwk.crv === 'Ed25519') { + return u8a.toString(u8a.fromString(jwk.x!, 'base64url'), 'base16') + } else if (jwk.kty === 'RSA') { + return hexKeyFromPEMBasedJwk(jwk, 'public') + } + } + // delegate the other types to the original Veramo function + return extractPublicKeyHex(pk, convert) } export function isEvenHexString(hex: string) { - const lastChar = hex[hex.length - 1].toLowerCase() - return ['0', '2', '4', '6', '8', 'a', 'c', 'e'].includes(lastChar) + const lastChar = hex[hex.length - 1].toLowerCase() + return ['0', '2', '4', '6', '8', 'a', 'c', 'e'].includes(lastChar) } interface LegacyVerificationMethod extends VerificationMethod { - publicKeyBase64: string + publicKeyBase64: string } /** @@ -394,91 +408,86 @@ interface LegacyVerificationMethod extends VerificationMethod { * @beta This API may change without a BREAKING CHANGE notice. */ export function extractPublicKeyHex(pk: _ExtendedVerificationMethod, convert: boolean = false): string { - let keyBytes = extractPublicKeyBytes(pk) - if (convert) { - if ( - ['Ed25519', 'Ed25519VerificationKey2018', 'Ed25519VerificationKey2020'].includes(pk.type) || - (pk.type === 'JsonWebKey2020' && pk.publicKeyJwk?.crv === 'Ed25519') - ) { - keyBytes = convertPublicKeyToX25519(keyBytes) - } else if ( - !['X25519', 'X25519KeyAgreementKey2019', 'X25519KeyAgreementKey2020'].includes(pk.type) && - !(pk.type === 'JsonWebKey2020' && pk.publicKeyJwk?.crv === 'X25519') - ) { - return '' + let keyBytes = extractPublicKeyBytes(pk) + const jwk = pk.publicKeyJwk ? sanitizedJwk(pk.publicKeyJwk) : undefined + if (convert) { + if ( + ['Ed25519', 'Ed25519VerificationKey2018', 'Ed25519VerificationKey2020'].includes(pk.type) || + (pk.type === 'JsonWebKey2020' && jwk?.crv === 'Ed25519') + ) { + keyBytes = convertPublicKeyToX25519(keyBytes) + } else if ( + !['X25519', 'X25519KeyAgreementKey2019', 'X25519KeyAgreementKey2020'].includes(pk.type) && + !(pk.type === 'JsonWebKey2020' && jwk?.crv === 'X25519') + ) { + return '' + } } - } - return bytesToHex(keyBytes) + return bytesToHex(keyBytes) } function toEcLibCurve(input: string) { - return input.toLowerCase().replace('-', '').replace('_', '') + return input.toLowerCase().replace('-', '').replace('_', '') } function extractPublicKeyBytes(pk: VerificationMethod): Uint8Array { - if (pk.publicKeyBase58) { - return base58ToBytes(pk.publicKeyBase58) - } else if (pk.publicKeyMultibase) { - return multibaseKeyToBytes(pk.publicKeyMultibase) - } else if ((pk).publicKeyBase64) { - return base64ToBytes((pk).publicKeyBase64) - } else if (pk.publicKeyHex) { - return hexToBytes(pk.publicKeyHex) - } else if (pk.publicKeyJwk?.crv && pk.publicKeyJwk.x && pk.publicKeyJwk.y) { - const secp = new elliptic.ec(toEcLibCurve(pk.publicKeyJwk.crv)) - return hexToBytes( - secp - .keyFromPublic({ - x: base64ToHex(pk.publicKeyJwk.x, 'base64url'), - y: base64ToHex(pk.publicKeyJwk.y, 'base64url'), - }) - .getPublic('hex') - ) - } else if (pk.publicKeyJwk && (pk.publicKeyJwk.crv === 'Ed25519' || pk.publicKeyJwk.crv === 'X25519') && pk.publicKeyJwk.x) { - return base64ToBytes(pk.publicKeyJwk.x) - } - return new Uint8Array() + if (pk.publicKeyBase58) { + return base58ToBytes(pk.publicKeyBase58) + } else if (pk.publicKeyMultibase) { + return multibaseKeyToBytes(pk.publicKeyMultibase) + } else if ((pk).publicKeyBase64) { + return base64ToBytes((pk).publicKeyBase64) + } else if (pk.publicKeyHex) { + return hexToBytes(pk.publicKeyHex) + } else if (pk.publicKeyJwk?.crv && pk.publicKeyJwk.x && pk.publicKeyJwk.y) { + return hexToBytes( + extractPublicKeyHexWithJwkSupport(pk) + ) + } else if (pk.publicKeyJwk && (pk.publicKeyJwk.crv === 'Ed25519' || pk.publicKeyJwk.crv === 'X25519') && pk.publicKeyJwk.x) { + return base64ToBytes(pk.publicKeyJwk.x) + } + return new Uint8Array() } export function verificationMethodToJwk(vm: VerificationMethod): JWK { - let jwk: JWK | undefined = vm.publicKeyJwk as JWK - if (!jwk) { - let publicKeyHex = vm.publicKeyHex ?? u8a.toString(extractPublicKeyBytes(vm), 'hex') - jwk = toJwk(publicKeyHex, keyTypeFromCryptographicSuite({ crv: vm.type })) - } - if (!jwk) { - throw Error(`Could not convert verification method to jwk`) - } - jwk.kid = vm.id - return jwk + let jwk: JWK | undefined = vm.publicKeyJwk as JWK + if (!jwk) { + let publicKeyHex = vm.publicKeyHex ?? u8a.toString(extractPublicKeyBytes(vm), 'hex') + jwk = toJwk(publicKeyHex, keyTypeFromCryptographicSuite({crv: vm.type})) + } + if (!jwk) { + throw Error(`Could not convert verification method to jwk`) + } + jwk.kid = vm.id + return sanitizedJwk(jwk) } function didDocumentSectionToJwks( - didDocumentSection: DIDDocumentSection, - searchForVerificationMethods?: (VerificationMethod | string)[], - verificationMethods?: VerificationMethod[] + didDocumentSection: DIDDocumentSection, + searchForVerificationMethods?: (VerificationMethod | string)[], + verificationMethods?: VerificationMethod[] ) { - const jwks = (searchForVerificationMethods ?? []) - .map((vmOrId) => (typeof vmOrId === 'object' ? vmOrId : verificationMethods?.find((vm) => vm.id === vmOrId))) - .filter(isDefined) - .map((vm) => verificationMethodToJwk(vm)) - return { didDocumentSection, jwks: jwks } + const jwks = new Set((searchForVerificationMethods ?? []) + .map((vmOrId) => (typeof vmOrId === 'object' ? vmOrId : verificationMethods?.find((vm) => vm.id === vmOrId))) + .filter(isDefined) + .map((vm) => verificationMethodToJwk(vm))) + return {didDocumentSection, jwks: Array.from(jwks)} } export type DidDocumentJwks = Record, Array> export function didDocumentToJwks(didDocument: DIDDocument): DidDocumentJwks { - return { - verificationMethod: [ - ...didDocumentSectionToJwks('publicKey', didDocument.publicKey, didDocument.verificationMethod).jwks, // legacy support - ...didDocumentSectionToJwks('verificationMethod', didDocument.verificationMethod, didDocument.verificationMethod).jwks, - ], - assertionMethod: didDocumentSectionToJwks('assertionMethod', didDocument.assertionMethod, didDocument.verificationMethod).jwks, - authentication: didDocumentSectionToJwks('authentication', didDocument.authentication, didDocument.verificationMethod).jwks, - keyAgreement: didDocumentSectionToJwks('keyAgreement', didDocument.keyAgreement, didDocument.verificationMethod).jwks, - capabilityInvocation: didDocumentSectionToJwks('capabilityInvocation', didDocument.capabilityInvocation, didDocument.verificationMethod).jwks, - capabilityDelegation: didDocumentSectionToJwks('capabilityDelegation', didDocument.capabilityDelegation, didDocument.verificationMethod).jwks, - } + return { + verificationMethod: [ + ...didDocumentSectionToJwks('publicKey', didDocument.publicKey, didDocument.verificationMethod).jwks, // legacy support + ...didDocumentSectionToJwks('verificationMethod', didDocument.verificationMethod, didDocument.verificationMethod).jwks, + ], + assertionMethod: didDocumentSectionToJwks('assertionMethod', didDocument.assertionMethod, didDocument.verificationMethod).jwks, + authentication: didDocumentSectionToJwks('authentication', didDocument.authentication, didDocument.verificationMethod).jwks, + keyAgreement: didDocumentSectionToJwks('keyAgreement', didDocument.keyAgreement, didDocument.verificationMethod).jwks, + capabilityInvocation: didDocumentSectionToJwks('capabilityInvocation', didDocument.capabilityInvocation, didDocument.verificationMethod).jwks, + capabilityDelegation: didDocumentSectionToJwks('capabilityDelegation', didDocument.capabilityDelegation, didDocument.verificationMethod).jwks, + } } /** @@ -499,58 +508,58 @@ export function didDocumentToJwks(didDocument: DIDDocument): DidDocumentJwks { * @beta This API may change without a BREAKING CHANGE notice. */ export async function mapIdentifierKeysToDocWithJwkSupport( - { - identifier, - vmRelationship = 'verificationMethod', - didDocument, - }: { - identifier: IIdentifier - vmRelationship?: DIDDocumentSection - didDocument?: DIDDocument - }, - context: IAgentContext + { + identifier, + vmRelationship = 'verificationMethod', + didDocument, + }: { + identifier: IIdentifier + vmRelationship?: DIDDocumentSection + didDocument?: DIDDocument + }, + context: IAgentContext ): Promise<_ExtendedIKey[]> { - const didDoc = - didDocument ?? - (await getAgentResolver(context) - .resolve(identifier.did) - .then((result) => result.didDocument)) - if (!didDoc) { - throw Error(`Could not resolve DID ${identifier.did}`) - } - - // const rsaDidWeb = identifier.keys && identifier.keys.length > 0 && identifier.keys.find((key) => key.type === 'RSA') && didDocument - - // We skip mapping in case the identifier is RSA and a did document is supplied. - const keys = didDoc ? [] : await mapIdentifierKeysToDoc(identifier, vmRelationship, context) - - // dereference all key agreement keys from DID document and normalize - const documentKeys: VerificationMethod[] = await dereferenceDidKeysWithJwkSupport(didDoc, vmRelationship, context) - - const localKeys = vmRelationship === 'keyAgreement' ? convertIdentifierEncryptionKeys(identifier) : compressIdentifierSecp256k1Keys(identifier) - - // finally map the didDocument keys to the identifier keys by comparing `publicKeyHex` - const extendedKeys: _ExtendedIKey[] = documentKeys - .map((verificationMethod) => { - /*if (verificationMethod.type !== 'JsonWebKey2020') { - return null - }*/ - const localKey = localKeys.find( - (localKey) => - localKey.publicKeyHex === verificationMethod.publicKeyHex || - verificationMethod.publicKeyHex?.startsWith(localKey.publicKeyHex) || - compareBlockchainAccountId(localKey, verificationMethod) - ) - if (localKey) { - const { meta, ...localProps } = localKey - return { ...localProps, meta: { ...meta, verificationMethod } } - } else { - return null - } - }) - .filter(isDefined) - - return keys.concat(extendedKeys) + const didDoc = + didDocument ?? + (await getAgentResolver(context) + .resolve(identifier.did) + .then((result) => result.didDocument)) + if (!didDoc) { + throw Error(`Could not resolve DID ${identifier.did}`) + } + + // const rsaDidWeb = identifier.keys && identifier.keys.length > 0 && identifier.keys.find((key) => key.type === 'RSA') && didDocument + + // We skip mapping in case the identifier is RSA and a did document is supplied. + const keys = didDoc ? [] : await mapIdentifierKeysToDoc(identifier, vmRelationship, context) + + // dereference all key agreement keys from DID document and normalize + const documentKeys: VerificationMethod[] = await dereferenceDidKeysWithJwkSupport(didDoc, vmRelationship, context) + + const localKeys = vmRelationship === 'keyAgreement' ? convertIdentifierEncryptionKeys(identifier) : compressIdentifierSecp256k1Keys(identifier) + + // finally map the didDocument keys to the identifier keys by comparing `publicKeyHex` + const extendedKeys: _ExtendedIKey[] = documentKeys + .map((verificationMethod) => { + /*if (verificationMethod.type !== 'JsonWebKey2020') { + return null + }*/ + const localKey = localKeys.find( + (localKey) => + localKey.publicKeyHex === verificationMethod.publicKeyHex || + verificationMethod.publicKeyHex?.startsWith(localKey.publicKeyHex) || + compareBlockchainAccountId(localKey, verificationMethod) + ) + if (localKey) { + const {meta, ...localProps} = localKey + return {...localProps, meta: {...meta, verificationMethod}} + } else { + return null + } + }) + .filter(isDefined) + + return keys.concat(extendedKeys) } /** @@ -566,96 +575,96 @@ export async function mapIdentifierKeysToDocWithJwkSupport( * @beta This API may change without a BREAKING CHANGE notice. */ function compareBlockchainAccountId(localKey: IKey, verificationMethod: VerificationMethod): boolean { - if ( - (verificationMethod.type !== 'EcdsaSecp256k1RecoveryMethod2020' && verificationMethod.type !== 'EcdsaSecp256k1VerificationKey2019') || - localKey.type !== 'Secp256k1' - ) { - return false - } - let vmEthAddr = getEthereumAddress(verificationMethod) - if (localKey.meta?.account) { - return vmEthAddr === localKey.meta?.account.toLowerCase() - } - const computedAddr = computeAddress('0x' + localKey.publicKeyHex).toLowerCase() - return computedAddr === vmEthAddr + if ( + (verificationMethod.type !== 'EcdsaSecp256k1RecoveryMethod2020' && verificationMethod.type !== 'EcdsaSecp256k1VerificationKey2019') || + localKey.type !== 'Secp256k1' + ) { + return false + } + let vmEthAddr = getEthereumAddress(verificationMethod) + if (localKey.meta?.account) { + return vmEthAddr === localKey.meta?.account.toLowerCase() + } + const computedAddr = computeAddress('0x' + localKey.publicKeyHex).toLowerCase() + return computedAddr === vmEthAddr } export async function getAgentDIDMethods(context: IAgentContext) { - return (await context.agent.didManagerGetProviders()).map((provider) => provider.toLowerCase().replace('did:', '')) + return (await context.agent.didManagerGetProviders()).map((provider) => provider.toLowerCase().replace('did:', '')) } export function getDID(idOpts: { identifier: IIdentifier | string }): string { - if (typeof idOpts.identifier === 'string') { - return idOpts.identifier - } else if (typeof idOpts.identifier === 'object') { - return idOpts.identifier.did - } - throw Error(`Cannot get DID from identifier value`) + if (typeof idOpts.identifier === 'string') { + return idOpts.identifier + } else if (typeof idOpts.identifier === 'object') { + return idOpts.identifier.did + } + throw Error(`Cannot get DID from identifier value`) } export function toDID(identifier: string | IIdentifier | Partial): string { - if (typeof identifier === 'string') { - return identifier - } - if (identifier.did) { - return identifier.did - } - throw Error(`No DID value present in identifier`) + if (typeof identifier === 'string') { + return identifier + } + if (identifier.did) { + return identifier.did + } + throw Error(`No DID value present in identifier`) } export function toDIDs(identifiers?: (string | IIdentifier | Partial)[]): string[] { - if (!identifiers) { - return [] - } - return identifiers.map(toDID) + if (!identifiers) { + return [] + } + return identifiers.map(toDID) } export async function getKey( - { - identifier, - vmRelationship = 'authentication', - kmsKeyRef, - }: { - identifier: IIdentifier - vmRelationship?: DIDDocumentSection - kmsKeyRef?: string - }, - context: IAgentContext + { + identifier, + vmRelationship = 'authentication', + kmsKeyRef, + }: { + identifier: IIdentifier + vmRelationship?: DIDDocumentSection + kmsKeyRef?: string + }, + context: IAgentContext ): Promise { - if (!identifier) { - return Promise.reject(new Error(`No identifier provided to getKey method!`)) - } - // normalize to kid, in case keyId was passed in as did#vm or #vm - const kmsKeyRefParts = kmsKeyRef?.split(`#`) - const kid = kmsKeyRefParts ? (kmsKeyRefParts?.length === 2 ? kmsKeyRefParts[1] : kmsKeyRefParts[0]) : undefined - // todo: We really should do a keyRef and external kid here - let identifierKey = kmsKeyRef ? identifier.keys.find((key: IKey) => key.kid === kid || key?.meta?.jwkThumbprint === kid) : undefined - if (!identifierKey) { - const keys = await mapIdentifierKeysToDocWithJwkSupport({ identifier, vmRelationship: vmRelationship }, context) - if (!keys || keys.length === 0) { - throw new Error(`No keys found for verificationMethodSection: ${vmRelationship} and did ${identifier.did}`) - } - if (kmsKeyRef) { - identifierKey = keys.find( - (key: _ExtendedIKey) => key.meta.verificationMethod?.id === kmsKeyRef || (kid && key.meta.verificationMethod?.id?.includes(kid)) - ) + if (!identifier) { + return Promise.reject(new Error(`No identifier provided to getKey method!`)) } + // normalize to kid, in case keyId was passed in as did#vm or #vm + const kmsKeyRefParts = kmsKeyRef?.split(`#`) + const kid = kmsKeyRefParts ? (kmsKeyRefParts?.length === 2 ? kmsKeyRefParts[1] : kmsKeyRefParts[0]) : undefined + // todo: We really should do a keyRef and external kid here + let identifierKey = kmsKeyRef ? identifier.keys.find((key: IKey) => key.kid === kid || key?.meta?.jwkThumbprint === kid) : undefined if (!identifierKey) { - identifierKey = keys.find( - (key: _ExtendedIKey) => key.meta.verificationMethod?.type === vmRelationship || key.meta.purposes?.includes(vmRelationship) - ) + const keys = await mapIdentifierKeysToDocWithJwkSupport({identifier, vmRelationship: vmRelationship}, context) + if (!keys || keys.length === 0) { + throw new Error(`No keys found for verificationMethodSection: ${vmRelationship} and did ${identifier.did}`) + } + if (kmsKeyRef) { + identifierKey = keys.find( + (key: _ExtendedIKey) => key.meta.verificationMethod?.id === kmsKeyRef || (kid && key.meta.verificationMethod?.id?.includes(kid)) + ) + } + if (!identifierKey) { + identifierKey = keys.find( + (key: _ExtendedIKey) => key.meta.verificationMethod?.type === vmRelationship || key.meta.purposes?.includes(vmRelationship) + ) + } + if (!identifierKey) { + identifierKey = keys[0] + } } if (!identifierKey) { - identifierKey = keys[0] + throw new Error( + `No matching verificationMethodSection key found for keyId: ${kmsKeyRef} and vmSection: ${vmRelationship} for id ${identifier.did}` + ) } - } - if (!identifierKey) { - throw new Error( - `No matching verificationMethodSection key found for keyId: ${kmsKeyRef} and vmSection: ${vmRelationship} for id ${identifier.did}` - ) - } - return identifierKey + return identifierKey } /** @@ -666,17 +675,17 @@ export async function getKey( * @deprecated Replaced by the identfier resolution plugin */ async function legacyGetIdentifier( - { - identifier, - }: { - identifier: string | IIdentifier - }, - context: IAgentContext + { + identifier, + }: { + identifier: string | IIdentifier + }, + context: IAgentContext ): Promise { - if (typeof identifier === 'string') { - return await context.agent.didManagerGet({ did: identifier }) - } - return identifier + if (typeof identifier === 'string') { + return await context.agent.didManagerGet({did: identifier}) + } + return identifier } /** @@ -686,132 +695,132 @@ async function legacyGetIdentifier( * @param context */ export async function determineKid( - { - key, - idOpts, - }: { - key: IKey - idOpts: { identifier: IIdentifier | string; kmsKeyRef?: string } - }, - context: IAgentContext -): Promise { - if (key.meta?.verificationMethod?.id) { - return key.meta?.verificationMethod?.id - } - const identifier = await legacyGetIdentifier(idOpts, context) - const mappedKeys = await mapIdentifierKeysToDocWithJwkSupport( { - identifier, - vmRelationship: 'verificationMethod', + key, + idOpts, + }: { + key: IKey + idOpts: { identifier: IIdentifier | string; kmsKeyRef?: string } }, - context - ) - const vmKey = mappedKeys.find((extendedKey) => extendedKey.kid === key.kid) - if (vmKey) { - return vmKey.meta?.verificationMethod?.id ?? vmKey.meta?.jwkThumbprint ?? idOpts.kmsKeyRef ?? vmKey.kid - } + context: IAgentContext +): Promise { + if (key.meta?.verificationMethod?.id) { + return key.meta?.verificationMethod?.id + } + const identifier = await legacyGetIdentifier(idOpts, context) + const mappedKeys = await mapIdentifierKeysToDocWithJwkSupport( + { + identifier, + vmRelationship: 'verificationMethod', + }, + context + ) + const vmKey = mappedKeys.find((extendedKey) => extendedKey.kid === key.kid) + if (vmKey) { + return vmKey.meta?.verificationMethod?.id ?? vmKey.meta?.jwkThumbprint ?? idOpts.kmsKeyRef ?? vmKey.kid + } - return key.meta?.jwkThumbprint ?? idOpts.kmsKeyRef ?? key.kid + return key.meta?.jwkThumbprint ?? idOpts.kmsKeyRef ?? key.kid } export async function getSupportedDIDMethods(didOpts: IDIDOptions, context: IAgentContext) { - return didOpts.supportedDIDMethods ?? (await getAgentDIDMethods(context)) + return didOpts.supportedDIDMethods ?? (await getAgentDIDMethods(context)) } export function getAgentResolver( - context: IAgentContext, - opts?: { - localResolution?: boolean // Resolve identifiers hosted by the agent - uniresolverResolution?: boolean // Resolve identifiers using universal resolver - resolverResolution?: boolean // Use registered drivers - } + context: IAgentContext, + opts?: { + localResolution?: boolean // Resolve identifiers hosted by the agent + uniresolverResolution?: boolean // Resolve identifiers using universal resolver + resolverResolution?: boolean // Use registered drivers + } ): Resolvable { - return new AgentDIDResolver(context, opts) + return new AgentDIDResolver(context, opts) } export class AgentDIDResolver implements Resolvable { - private readonly context: IAgentContext - private readonly resolverResolution: boolean - private readonly uniresolverResolution: boolean - private readonly localResolution: boolean + private readonly context: IAgentContext + private readonly resolverResolution: boolean + private readonly uniresolverResolution: boolean + private readonly localResolution: boolean + + constructor( + context: IAgentContext, + opts?: { uniresolverResolution?: boolean; localResolution?: boolean; resolverResolution?: boolean } + ) { + this.context = context + this.resolverResolution = opts?.resolverResolution !== false + this.uniresolverResolution = opts?.uniresolverResolution !== false + this.localResolution = opts?.localResolution !== false + } - constructor( - context: IAgentContext, - opts?: { uniresolverResolution?: boolean; localResolution?: boolean; resolverResolution?: boolean } - ) { - this.context = context - this.resolverResolution = opts?.resolverResolution !== false - this.uniresolverResolution = opts?.uniresolverResolution !== false - this.localResolution = opts?.localResolution !== false - } - - async resolve(didUrl: string, options?: DIDResolutionOptions): Promise { - let resolutionResult: DIDResolutionResult | undefined - let origResolutionResult: DIDResolutionResult | undefined - let err: any - if (!this.resolverResolution && !this.localResolution && !this.uniresolverResolution) { - throw Error(`No agent hosted DID resolution, regular agent resolution nor universal resolver resolution is enabled. Cannot resolve DIDs.`) - } - if (this.resolverResolution) { - try { - resolutionResult = await this.context.agent.resolveDid({ didUrl, options }) - } catch (error: unknown) { - err = error - } - } - if (resolutionResult) { - origResolutionResult = resolutionResult - if (resolutionResult.didDocument === null) { - resolutionResult = undefined - } - } else { - console.log(`Agent resolver resolution is disabled. This typically isn't desirable!`) - } - if (!resolutionResult && this.localResolution) { - console.log(`Using local DID resolution, looking at DIDs hosted by the agent.`) - try { - const did = didUrl.split('#')[0] - const iIdentifier = await this.context.agent.didManagerGet({ did }) - resolutionResult = toDidResolutionResult(iIdentifier, { did }) - if (resolutionResult.didDocument) { - err = undefined + async resolve(didUrl: string, options?: DIDResolutionOptions): Promise { + let resolutionResult: DIDResolutionResult | undefined + let origResolutionResult: DIDResolutionResult | undefined + let err: any + if (!this.resolverResolution && !this.localResolution && !this.uniresolverResolution) { + throw Error(`No agent hosted DID resolution, regular agent resolution nor universal resolver resolution is enabled. Cannot resolve DIDs.`) + } + if (this.resolverResolution) { + try { + resolutionResult = await this.context.agent.resolveDid({didUrl, options}) + } catch (error: unknown) { + err = error + } + } + if (resolutionResult) { + origResolutionResult = resolutionResult + if (resolutionResult.didDocument === null) { + resolutionResult = undefined + } } else { - console.log(`Local resolution resulted in a DID Document for ${did}`) + console.log(`Agent resolver resolution is disabled. This typically isn't desirable!`) } - } catch (error: unknown) { - if (!err) { - err = error + if (!resolutionResult && this.localResolution) { + console.log(`Using local DID resolution, looking at DIDs hosted by the agent.`) + try { + const did = didUrl.split('#')[0] + const iIdentifier = await this.context.agent.didManagerGet({did}) + resolutionResult = toDidResolutionResult(iIdentifier, {did}) + if (resolutionResult.didDocument) { + err = undefined + } else { + console.log(`Local resolution resulted in a DID Document for ${did}`) + } + } catch (error: unknown) { + if (!err) { + err = error + } + } + } + if (resolutionResult) { + if (!origResolutionResult) { + origResolutionResult = resolutionResult + } + if (!resolutionResult.didDocument) { + resolutionResult = undefined + } + } + if (!resolutionResult && this.uniresolverResolution) { + console.log(`Using universal resolver resolution for did ${didUrl} `) + resolutionResult = await new UniResolver().resolve(didUrl, options) + if (!origResolutionResult) { + origResolutionResult = resolutionResult + } + if (resolutionResult.didDocument) { + err = undefined + } } - } - } - if (resolutionResult) { - if (!origResolutionResult) { - origResolutionResult = resolutionResult - } - if (!resolutionResult.didDocument) { - resolutionResult = undefined - } - } - if (!resolutionResult && this.uniresolverResolution) { - console.log(`Using universal resolver resolution for did ${didUrl} `) - resolutionResult = await new UniResolver().resolve(didUrl, options) - if (!origResolutionResult) { - origResolutionResult = resolutionResult - } - if (resolutionResult.didDocument) { - err = undefined - } - } - if (err) { - // throw original error - throw err - } - if (!resolutionResult && !origResolutionResult) { - throw `Could not resolve ${didUrl}. Resolutions tried: online: ${this.resolverResolution}, local: ${this.localResolution}, uni resolver: ${this.uniresolverResolution}` + if (err) { + // throw original error + throw err + } + if (!resolutionResult && !origResolutionResult) { + throw `Could not resolve ${didUrl}. Resolutions tried: online: ${this.resolverResolution}, local: ${this.localResolution}, uni resolver: ${this.uniresolverResolution}` + } + return resolutionResult ?? origResolutionResult! } - return resolutionResult ?? origResolutionResult! - } } /** @@ -825,190 +834,190 @@ export class AgentDIDResolver implements Resolvable { * @param opts */ export function toDidDocument( - identifier?: IIdentifier, - opts?: { - did?: string - use?: JwkKeyUse[] - } + identifier?: IIdentifier, + opts?: { + did?: string + use?: JwkKeyUse[] + } ): DIDDocument | undefined { - let didDocument: DIDDocument | undefined = undefined - // TODO: Introduce jwk thumbprints here - if (identifier) { - const did = identifier.did ?? opts?.did - didDocument = { - '@context': 'https://www.w3.org/ns/did/v1', - id: did, - verificationMethod: identifier.keys.map((key) => { - const vm: VerificationMethod = { - controller: did, - id: key.kid.startsWith(did) && key.kid.includes('#') ? key.kid : `${did}#${key.kid}`, - publicKeyJwk: toJwk(key.publicKeyHex, key.type, { - use: ENC_KEY_ALGS.includes(key.type) ? JwkKeyUse.Encryption : JwkKeyUse.Signature, - key, - }) as JsonWebKey, - type: 'JsonWebKey2020', - } - return vm - }), - ...((opts?.use === undefined || opts?.use?.includes(JwkKeyUse.Signature)) && - identifier.keys && { - assertionMethod: identifier.keys - .filter( - (key) => - key?.meta?.purpose === undefined || key?.meta?.purpose === 'assertionMethod' || key?.meta?.purposes?.includes('assertionMethod') - ) - .map((key) => { - if (key.kid.startsWith(did) && key.kid.includes('#')) { - return key.kid - } - return `${did}#${key.kid}` - }), - }), - ...((opts?.use === undefined || opts?.use?.includes(JwkKeyUse.Signature)) && - identifier.keys && { - authentication: identifier.keys - .filter( - (key) => key?.meta?.purpose === undefined || key?.meta?.purpose === 'authentication' || key?.meta?.purposes?.includes('authentication') - ) - .map((key) => { - if (key.kid.startsWith(did) && key.kid.includes('#')) { - return key.kid - } - return `${did}#${key.kid}` + let didDocument: DIDDocument | undefined = undefined + // TODO: Introduce jwk thumbprints here + if (identifier) { + const did = identifier.did ?? opts?.did + didDocument = { + '@context': 'https://www.w3.org/ns/did/v1', + id: did, + verificationMethod: identifier.keys.map((key) => { + const vm: VerificationMethod = { + controller: did, + id: key.kid.startsWith(did) && key.kid.includes('#') ? key.kid : `${did}#${key.kid}`, + publicKeyJwk: toJwk(key.publicKeyHex, key.type, { + use: ENC_KEY_ALGS.includes(key.type) ? JwkKeyUse.Encryption : JwkKeyUse.Signature, + key, + }) as JsonWebKey, + type: 'JsonWebKey2020', + } + return vm }), - }), - ...((opts?.use === undefined || opts?.use?.includes(JwkKeyUse.Encryption)) && - identifier.keys && { - keyAgreement: identifier.keys - .filter((key) => key.type === 'X25519' || key?.meta?.purpose === 'keyAgreement' || key?.meta?.purposes?.includes('keyAgreement')) - .map((key) => { - if (key.kid.startsWith(did) && key.kid.includes('#')) { - return key.kid - } - return `${did}#${key.kid}` - }), - }), - ...((opts?.use === undefined || opts?.use?.includes(JwkKeyUse.Encryption)) && - identifier.keys && { - capabilityInvocation: identifier.keys - .filter( - (key) => key.type === 'X25519' || key?.meta?.purpose === 'capabilityInvocation' || key?.meta?.purposes?.includes('capabilityInvocation') - ) - .map((key) => { - if (key.kid.startsWith(did) && key.kid.includes('#')) { - return key.kid - } - return `${did}#${key.kid}` - }), - }), - ...((opts?.use === undefined || opts?.use?.includes(JwkKeyUse.Encryption)) && - identifier.keys && { - capabilityDelegation: identifier.keys - .filter( - (key) => key.type === 'X25519' || key?.meta?.purpose === 'capabilityDelegation' || key?.meta?.purposes?.includes('capabilityDelegation') - ) - .map((key) => { - if (key.kid.startsWith(did) && key.kid.includes('#')) { - return key.kid - } - return `${did}#${key.kid}` - }), - }), - ...(identifier.services && identifier.services.length > 0 && { service: identifier.services }), + ...((opts?.use === undefined || opts?.use?.includes(JwkKeyUse.Signature)) && + identifier.keys && { + assertionMethod: identifier.keys + .filter( + (key) => + key?.meta?.purpose === undefined || key?.meta?.purpose === 'assertionMethod' || key?.meta?.purposes?.includes('assertionMethod') + ) + .map((key) => { + if (key.kid.startsWith(did) && key.kid.includes('#')) { + return key.kid + } + return `${did}#${key.kid}` + }), + }), + ...((opts?.use === undefined || opts?.use?.includes(JwkKeyUse.Signature)) && + identifier.keys && { + authentication: identifier.keys + .filter( + (key) => key?.meta?.purpose === undefined || key?.meta?.purpose === 'authentication' || key?.meta?.purposes?.includes('authentication') + ) + .map((key) => { + if (key.kid.startsWith(did) && key.kid.includes('#')) { + return key.kid + } + return `${did}#${key.kid}` + }), + }), + ...((opts?.use === undefined || opts?.use?.includes(JwkKeyUse.Encryption)) && + identifier.keys && { + keyAgreement: identifier.keys + .filter((key) => key.type === 'X25519' || key?.meta?.purpose === 'keyAgreement' || key?.meta?.purposes?.includes('keyAgreement')) + .map((key) => { + if (key.kid.startsWith(did) && key.kid.includes('#')) { + return key.kid + } + return `${did}#${key.kid}` + }), + }), + ...((opts?.use === undefined || opts?.use?.includes(JwkKeyUse.Encryption)) && + identifier.keys && { + capabilityInvocation: identifier.keys + .filter( + (key) => key.type === 'X25519' || key?.meta?.purpose === 'capabilityInvocation' || key?.meta?.purposes?.includes('capabilityInvocation') + ) + .map((key) => { + if (key.kid.startsWith(did) && key.kid.includes('#')) { + return key.kid + } + return `${did}#${key.kid}` + }), + }), + ...((opts?.use === undefined || opts?.use?.includes(JwkKeyUse.Encryption)) && + identifier.keys && { + capabilityDelegation: identifier.keys + .filter( + (key) => key.type === 'X25519' || key?.meta?.purpose === 'capabilityDelegation' || key?.meta?.purposes?.includes('capabilityDelegation') + ) + .map((key) => { + if (key.kid.startsWith(did) && key.kid.includes('#')) { + return key.kid + } + return `${did}#${key.kid}` + }), + }), + ...(identifier.services && identifier.services.length > 0 && {service: identifier.services}), + } } - } - return didDocument + return didDocument } export function toDidResolutionResult( - identifier?: IIdentifier, - opts?: { - did?: string - supportedMethods?: string[] - } + identifier?: IIdentifier, + opts?: { + did?: string + supportedMethods?: string[] + } ): DIDResolutionResult { - const didDocument = toDidDocument(identifier, opts) ?? null // null is used in case of errors and required by the did resolution spec - - const resolutionResult: DIDResolutionResult = { - '@context': 'https://w3id.org/did-resolution/v1', - didDocument, - didResolutionMetadata: { - ...(!didDocument && { error: 'notFound' }), - ...(Array.isArray(opts?.supportedMethods) && - identifier && - !opts?.supportedMethods.includes(identifier.provider.replace('did:', '')) && { error: 'unsupportedDidMethod' }), - }, - didDocumentMetadata: { - ...(identifier?.alias && { equivalentId: identifier?.alias }), - }, - } - return resolutionResult + const didDocument = toDidDocument(identifier, opts) ?? null // null is used in case of errors and required by the did resolution spec + + const resolutionResult: DIDResolutionResult = { + '@context': 'https://w3id.org/did-resolution/v1', + didDocument, + didResolutionMetadata: { + ...(!didDocument && {error: 'notFound'}), + ...(Array.isArray(opts?.supportedMethods) && + identifier && + !opts?.supportedMethods.includes(identifier.provider.replace('did:', '')) && {error: 'unsupportedDidMethod'}), + }, + didDocumentMetadata: { + ...(identifier?.alias && {equivalentId: identifier?.alias}), + }, + } + return resolutionResult } export async function asDidWeb(hostnameOrDID: string): Promise { - let did = hostnameOrDID - if (!did) { - throw Error('Domain or DID expected, but received nothing.') - } - if (did.startsWith('did:web:')) { - return did - } - return `did:web:${did.replace(/https?:\/\/([^/?#]+).*/i, '$1').toLowerCase()}` + let did = hostnameOrDID + if (!did) { + throw Error('Domain or DID expected, but received nothing.') + } + if (did.startsWith('did:web:')) { + return did + } + return `did:web:${did.replace(/https?:\/\/([^/?#]+).*/i, '$1').toLowerCase()}` } /** * @deprecated Replaced by the new signer service */ export const signDidJWT = async (args: SignJwtArgs): Promise => { - const { idOpts, header, payload, context, options } = args - const jwtOptions = { - ...options, - signer: await getDidSigner({ idOpts, context }), - } + const {idOpts, header, payload, context, options} = args + const jwtOptions = { + ...options, + signer: await getDidSigner({idOpts, context}), + } - return createJWT(payload, jwtOptions, header) + return createJWT(payload, jwtOptions, header) } /** * @deprecated Replaced by the new signer service */ export const getDidSigner = async ( - args: GetSignerArgs & { - idOpts: { - /** - * @deprecated - */ - identifier: IIdentifier | string - /** - * @deprecated - */ - verificationMethodSection?: DIDDocumentSection - /** - * @deprecated - */ - kmsKeyRef?: string - } - } + args: GetSignerArgs & { + idOpts: { + /** + * @deprecated + */ + identifier: IIdentifier | string + /** + * @deprecated + */ + verificationMethodSection?: DIDDocumentSection + /** + * @deprecated + */ + kmsKeyRef?: string + } + } ): Promise => { - const { idOpts, context } = args + const {idOpts, context} = args - const identifier = await legacyGetIdentifier(idOpts, context) - const key = await getKey( - { - identifier, - vmRelationship: idOpts.verificationMethodSection, - kmsKeyRef: idOpts.kmsKeyRef, - }, - context - ) - const algorithm = await signatureAlgorithmFromKey({ key }) - - return async (data: string | Uint8Array): Promise => { - const input = data instanceof Object.getPrototypeOf(Uint8Array) ? new TextDecoder().decode(data as Uint8Array) : (data as string) - return await context.agent.keyManagerSign({ - keyRef: key.kid, - algorithm, - data: input, - }) - } + const identifier = await legacyGetIdentifier(idOpts, context) + const key = await getKey( + { + identifier, + vmRelationship: idOpts.verificationMethodSection, + kmsKeyRef: idOpts.kmsKeyRef, + }, + context + ) + const algorithm = await signatureAlgorithmFromKey({key}) + + return async (data: string | Uint8Array): Promise => { + const input = data instanceof Object.getPrototypeOf(Uint8Array) ? new TextDecoder().decode(data as Uint8Array) : (data as string) + return await context.agent.keyManagerSign({ + keyRef: key.kid, + algorithm, + data: input, + }) + } } diff --git a/packages/identifier-resolution/__tests__/shared/identifierResolution.ts b/packages/identifier-resolution/__tests__/shared/identifierResolution.ts index d51eb405..1622c883 100644 --- a/packages/identifier-resolution/__tests__/shared/identifierResolution.ts +++ b/packages/identifier-resolution/__tests__/shared/identifierResolution.ts @@ -57,8 +57,17 @@ export default (testContext: { getAgent: () => ConfiguredAgent; setup: () => Pro const did = identifier.did // These all contain a did or are an internal did identifier - await expect(agent.identifierExternalResolve({ identifier: did })).resolves.toMatchObject(resolvedDidMatcher) - await expect(agent.identifierExternalResolveByDid({ identifier: did })).resolves.toMatchObject(resolvedDidMatcher) + const didResult = await agent.identifierExternalResolve({ identifier: did }) + console.log('==========================') + console.log(JSON.stringify(didResult, null, 2)) + expect(didResult).toMatchObject(resolvedDidMatcher) + + const did2Result = await agent.identifierExternalResolveByDid({ identifier: did }) + console.log('==========================') + console.log(JSON.stringify(did2Result, null, 2)) + console.log('==========================') + + expect(did2Result).toMatchObject(resolvedDidMatcher) }) it('should resolve x5c identifier by x5c array', async () => { @@ -72,11 +81,14 @@ export default (testContext: { getAgent: () => ConfiguredAgent; setup: () => Pro expect(certResult.method).toEqual('x5c') expect(certResult.jwks.length).toEqual(2) + + const certResult2 = await agent.identifierExternalResolveByX5c({ identifier: [sphereonTest, sphereonCA], trustAnchors: [sphereonCA], verificationTime, }) + expect(certResult2).toEqual(certResult) expect(certResult2.verificationResult).toBeDefined() expect(certResult2.verificationResult?.error).toBe(false) @@ -167,6 +179,8 @@ const resolvedDidMatcher = { kid: expect.stringContaining('did:jwk:'), kty: 'EC', use: 'sig', + x: expect.anything(), + y: expect.anything() }, type: 'JsonWebKey2020', }, @@ -246,9 +260,12 @@ const resolvedDidMatcher = { kid: expect.stringContaining('did:jwk:'), kty: 'EC', use: 'sig', + x: expect.anything(), + y: expect.anything() }, jwkThumbprint: expect.anything(), kid: expect.stringContaining('did:jwk:'), + publicKeyHex: expect.anything() }, ], method: 'did', diff --git a/packages/identifier-resolution/src/functions/externalIdentifierFunctions.ts b/packages/identifier-resolution/src/functions/externalIdentifierFunctions.ts index b27ff62f..f15eb2be 100644 --- a/packages/identifier-resolution/src/functions/externalIdentifierFunctions.ts +++ b/packages/identifier-resolution/src/functions/externalIdentifierFunctions.ts @@ -245,20 +245,18 @@ export async function resolveExternalDidIdentifier( const didDocument = didResolutionResult.didDocument ?? undefined const didJwks = didDocument ? didDocumentToJwks(didDocument) : undefined const jwks = didJwks - ? Array.from( - new Set( + ? Array.from(new Set(Array.from( Object.values(didJwks) .filter((jwks) => isDefined(jwks) && jwks.length > 0) .flatMap((jwks) => jwks) - ) - ).map((jwk) => { + ).flatMap((jwk) => { return { jwk, jwkThumbprint: calculateJwkThumbprint({ jwk }), kid: jwk.kid, publicKeyHex: jwkTtoPublicKeyHex(jwk), } - }) + }).map(jwk => JSON.stringify(jwk)))).map((jwks) => JSON.parse(jwks)) : [] if (didResolutionResult?.didDocument) { diff --git a/packages/jwt-service/src/agent/JwtService.ts b/packages/jwt-service/src/agent/JwtService.ts index 42bc8e38..c61c7083 100644 --- a/packages/jwt-service/src/agent/JwtService.ts +++ b/packages/jwt-service/src/agent/JwtService.ts @@ -1,5 +1,8 @@ -import { globalCrypto } from '@sphereon/ssi-sdk-ext.key-utils' import { IAgentPlugin } from '@veramo/core' +import debug from 'debug' +import { importJWK } from 'jose' + +import * as u8a from 'uint8arrays' import { createJwsCompact, CreateJwsCompactArgs, @@ -26,8 +29,6 @@ import { } from '..' import { CompactJwtEncrypter } from '../functions/JWE' -import * as u8a from 'uint8arrays' - /** * @public */ @@ -67,51 +68,47 @@ export class JwtService implements IAgentPlugin { private async jwtEncryptJweCompactJwt(args: EncryptJweCompactJwtArgs, context: IRequiredContext): Promise { const { payload, protectedHeader = { alg: args.alg, enc: args.enc }, recipientKey, issuer, expirationTime, audience } = args - console.log(JSON.stringify(args, null, 2)) + try { + debug(`JWE Encrypt: ${JSON.stringify(args, null, 2)}`) - const alg = jweAlg(args.alg) ?? jweAlg(protectedHeader.alg) ?? 'ECDH-ES' - const enc = jweEnc(args.enc) ?? jweEnc(protectedHeader.enc) ?? 'A256GCM' - const encJwks = - recipientKey.jwks.length === 1 - ? [recipientKey.jwks[0]] - : recipientKey.jwks.filter((jwk) => (jwk.kid && (jwk.kid === jwk.jwk.kid || jwk.kid === jwk.jwkThumbprint)) || jwk.jwk.use === 'enc') - if (encJwks.length === 0) { - return Promise.reject(Error(`No public JWK found that can be used to encrypt against`)) - } - const jwkInfo = encJwks[0] - if (encJwks.length > 0) { - JwtLogger.warning(`More than one JWK with 'enc' usage found. Selected the first one as no 'kid' was provided`, encJwks) - } - if (jwkInfo.jwk.kty?.startsWith('EC') !== true || !alg.startsWith('ECDH')) { - return Promise.reject(Error(`Currently only ECDH-ES is supported for encryption. JWK alg ${jwkInfo.jwk.kty}, header alg ${alg}`)) // TODO: Probably we support way more already - } - const apuVal = protectedHeader.apu ?? args.apu - const apu = apuVal ? u8a.fromString(apuVal, 'base64url') : undefined - const apvVal = protectedHeader.apv ?? args.apv - const apv = apvVal ? u8a.fromString(apvVal, 'base64url') : undefined + const alg = jweAlg(args.alg) ?? jweAlg(protectedHeader.alg) ?? 'ECDH-ES' + const enc = jweEnc(args.enc) ?? jweEnc(protectedHeader.enc) ?? 'A256GCM' + const encJwks = + recipientKey.jwks.length === 1 + ? [recipientKey.jwks[0]] + : recipientKey.jwks.filter((jwk) => (jwk.kid && (jwk.kid === jwk.jwk.kid || jwk.kid === jwk.jwkThumbprint)) || jwk.jwk.use === 'enc') + if (encJwks.length === 0) { + return Promise.reject(Error(`No public JWK found that can be used to encrypt against`)) + } + const jwkInfo = encJwks[0] + if (encJwks.length > 0) { + JwtLogger.warning(`More than one JWK with 'enc' usage found. Selected the first one as no 'kid' was provided`, encJwks) + } + if (jwkInfo.jwk.kty?.startsWith('EC') !== true || !alg.startsWith('ECDH')) { + return Promise.reject(Error(`Currently only ECDH-ES is supported for encryption. JWK alg ${jwkInfo.jwk.kty}, header alg ${alg}`)) // TODO: Probably we support way more already + } + const apuVal = protectedHeader.apu ?? args.apu + const apu = apuVal ? u8a.fromString(apuVal, 'base64url') : undefined + const apvVal = protectedHeader.apv ?? args.apv + const apv = apvVal ? u8a.fromString(apvVal, 'base64url') : undefined - const pubKey = await globalCrypto(false).subtle.importKey( - 'jwk', - jwkInfo.jwk, - { - name: 'ECDH', - namedCurve: 'P-256', - }, - true, - [] - ) - const encrypter = new CompactJwtEncrypter({ - enc, - alg, - keyManagementParams: { apu, apv }, - key: pubKey, - issuer, - expirationTime, - audience, - }) + const pubKey = await importJWK(jwkInfo.jwk) + const encrypter = new CompactJwtEncrypter({ + enc, + alg, + keyManagementParams: { apu, apv }, + key: pubKey, + issuer, + expirationTime, + audience, + }) - const jwe = await encrypter.encryptCompactJWT(payload, {}) - return { jwt: jwe } + const jwe = await encrypter.encryptCompactJWT(payload, {}) + return { jwt: jwe } + } catch (error: any) { + console.error(`Error encrypting JWE: ${error.message}`, error) + throw error + } } private async jwtDecryptJweCompactJwt(args: DecryptJweCompactJwtArgs, context: IRequiredContext): Promise { diff --git a/packages/jwt-service/src/functions/index.ts b/packages/jwt-service/src/functions/index.ts index 758bcb04..34b109fc 100644 --- a/packages/jwt-service/src/functions/index.ts +++ b/packages/jwt-service/src/functions/index.ts @@ -1,4 +1,3 @@ -import { jwkTtoPublicKeyHex } from '@sphereon/ssi-sdk-ext.did-utils' import { ensureManagedIdentifierResult, ExternalIdentifierDidOpts, @@ -10,12 +9,10 @@ import { ManagedIdentifierResult, resolveExternalJwkIdentifier, } from '@sphereon/ssi-sdk-ext.identifier-resolution' -import { keyTypeFromCryptographicSuite, verifyRawSignature } from '@sphereon/ssi-sdk-ext.key-utils' -import { contextHasPlugin } from '@sphereon/ssi-sdk.agent-config' +import { verifyRawSignature } from '@sphereon/ssi-sdk-ext.key-utils' import { JWK } from '@sphereon/ssi-types' import { IAgentContext } from '@veramo/core' -import { bytesToBase64url, decodeJoseBlob, encodeJoseBlob } from '@veramo/utils' -import { base64ToBytes } from '@veramo/utils' +import { base64ToBytes, bytesToBase64url, decodeJoseBlob, encodeJoseBlob } from '@veramo/utils' import * as u8a from 'uint8arrays' import { CreateJwsCompactArgs, @@ -26,19 +23,19 @@ import { isJwsCompact, isJwsJsonFlattened, isJwsJsonGeneral, + JweHeader, Jws, JwsCompact, + JwsHeader, JwsIdentifierMode, JwsJsonFlattened, JwsJsonGeneral, JwsJsonGeneralWithIdentifiers, JwsJsonSignature, JwsJsonSignatureWithIdentifier, - JwsHeader, JwsPayload, PreparedJwsObject, VerifyJwsArgs, - JweHeader, } from '../types/IJwtService' const payloadToBytes = (payload: string | JwsPayload | Uint8Array): Uint8Array => { @@ -317,7 +314,7 @@ export const verifyJws = async (args: VerifyJwsArgs, context: IAgentContext): Promise => { diff --git a/packages/key-utils/__tests__/functions.test.ts b/packages/key-utils/__tests__/functions.test.ts index 632a46e8..d87d0809 100644 --- a/packages/key-utils/__tests__/functions.test.ts +++ b/packages/key-utils/__tests__/functions.test.ts @@ -1,6 +1,6 @@ -import { JoseSignatureAlgorithm } from '@sphereon/ssi-types' +import { JoseSignatureAlgorithm, JWK } from '@sphereon/ssi-types' import * as u8a from 'uint8arrays' -import { generatePrivateKeyHex, Key, padLeft, toJwk, verifyRawSignature } from '../src' +import { generatePrivateKeyHex, jwkToRawHexKey, Key, padLeft, toJwk, verifyRawSignature } from '../src' describe('functions: key generator', () => { it('Secp256k1 should generate random keys', async () => { @@ -66,6 +66,21 @@ describe('functions: Leftpad', () => { }) describe('functions: verifySignature', () => { + it('should convert jwk to hex', async () => { + const publicKeyHex = + '04c92ac29c7e06ba171a5ed3730f8a3243645a679827352963e2c7d7127537e6108ddd439d9d34f827f39cf3dc96471433c14f0022b55cba66d18c76687bdf94a7' + const jwk: JWK = { + alg: 'ES256', + kid: 'https://oidf-dev.vault.azure.net/keys/test-key-39ca8c0e-1a7e-4356-8a61-f7edc80f3bbe/da7e0883d3f04a06a48ba40c0eaaa690', + kty: 'EC', + x: 'ySrCnH4GuhcaXtNzD4oyQ2RaZ5gnNSlj4sfXEnU35hA', + y: 'jd1DnZ00+CfznPPclkcUM8FPACK1XLpm0Yx2aHvflKc', + } + + const hex = await jwkToRawHexKey(jwk) + expect(hex).toEqual(publicKeyHex) + }) + it('should verify signature with secp256k1', async () => { const publicKeyHex = '04782c8ed17e3b2a783b5464f33b09652a71c678e05ec51e84e2bcfc663a3de963af9acb4280b8c7f7c42f4ef9aba6245ec1ec1712fd38a0fa96418d8cd6aa6152' diff --git a/packages/key-utils/src/functions.ts b/packages/key-utils/src/functions.ts index b46d59af..a02b9e92 100644 --- a/packages/key-utils/src/functions.ts +++ b/packages/key-utils/src/functions.ts @@ -12,6 +12,7 @@ import { generateRSAKeyAsPEM, hexToBase64, hexToPEM, PEMToJwk, privateKeyHexFrom import { JoseCurve, JoseSignatureAlgorithm, JWK, JwkKeyType, Loggers } from '@sphereon/ssi-types' import { generateKeyPair as generateSigningKeyPair } from '@stablelib/ed25519' import { IAgentContext, IKey, IKeyManager, ManagedKeyInfo, MinimalImportableKey } from '@veramo/core' +import debug from 'debug' import { JsonWebKey } from 'did-resolver' import elliptic from 'elliptic' @@ -179,7 +180,8 @@ export const toBase64url = (input: string): string => u8a.toString(u8a.fromStrin * @param args */ export const calculateJwkThumbprint = (args: { jwk: JWK; digestAlgorithm?: 'sha256' | 'sha512' }): string => { - const { jwk, digestAlgorithm = 'sha256' } = args + const { digestAlgorithm = 'sha256' } = args + const jwk = sanitizedJwk(args.jwk) let components switch (jwk.kty) { case 'EC': @@ -262,7 +264,7 @@ export const toJwk = ( if (!jwk.kid && !noKidThumbprint) { jwk['kid'] = calculateJwkThumbprint({ jwk }) } - return jwk + return sanitizedJwk(jwk) } /** @@ -273,10 +275,11 @@ export const toJwk = ( */ export const jwkToRawHexKey = async (jwk: JWK): Promise => { // TODO: Probably makes sense to have an option to do the same for private keys + jwk = sanitizedJwk(jwk) if (jwk.kty === 'RSA') { return rsaJwkToRawHexKey(jwk) } else if (jwk.kty === 'EC') { - return '04' + ecJwkToRawHexKey(jwk) + return ecJwkToRawHexKey(jwk) } else if (jwk.kty === 'OKP') { return okpJwkToRawHexKey(jwk) } else if (jwk.kty === 'oct') { @@ -292,12 +295,14 @@ export const jwkToRawHexKey = async (jwk: JWK): Promise => { * @returns A string representing the RSA key in raw hexadecimal format. */ function rsaJwkToRawHexKey(jwk: JsonWebKey): string { + jwk = sanitizedJwk(jwk) if (!jwk.n || !jwk.e) { throw new Error("RSA JWK must contain 'n' and 'e' properties.") } - const modulus = u8a.fromString(jwk.n, 'base64url') // 'n' is the modulus - const exponent = u8a.fromString(jwk.e, 'base64url') // 'e' is the exponent + // We are converting from base64 to base64url to be sure. The spec uses base64url, but in the wild we sometimes encounter a base64 string + const modulus = u8a.fromString(jwk.n.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''), 'base64url') // 'n' is the modulus + const exponent = u8a.fromString(jwk.e.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''), 'base64url') // 'e' is the exponent return u8a.toString(modulus, 'hex') + u8a.toString(exponent, 'hex') } @@ -308,14 +313,16 @@ function rsaJwkToRawHexKey(jwk: JsonWebKey): string { * @returns A string representing the EC key in raw hexadecimal format. */ function ecJwkToRawHexKey(jwk: JsonWebKey): string { + jwk = sanitizedJwk(jwk) if (!jwk.x || !jwk.y) { throw new Error("EC JWK must contain 'x' and 'y' properties.") } - const x = u8a.fromString(jwk.x, 'base64url') - const y = u8a.fromString(jwk.y, 'base64url') + // We are converting from base64 to base64url to be sure. The spec uses base64url, but in the wild we sometimes encounter a base64 string + const x = u8a.fromString(jwk.x.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''), 'base64url') + const y = u8a.fromString(jwk.y.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''), 'base64url') - return u8a.toString(x, 'hex') + u8a.toString(y, 'hex') + return '04' + u8a.toString(x, 'hex') + u8a.toString(y, 'hex') } /** @@ -324,11 +331,13 @@ function ecJwkToRawHexKey(jwk: JsonWebKey): string { * @returns A string representing the EC key in raw hexadecimal format. */ function okpJwkToRawHexKey(jwk: JsonWebKey): string { + jwk = sanitizedJwk(jwk) if (!jwk.x) { throw new Error("OKP JWK must contain 'x' property.") } - const x = u8a.fromString(jwk.x, 'base64url') + // We are converting from base64 to base64url to be sure. The spec uses base64url, but in the wild we sometimes encounter a base64 string + const x = u8a.fromString(jwk.x.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''), 'base64url') return u8a.toString(x, 'hex') } @@ -339,11 +348,13 @@ function okpJwkToRawHexKey(jwk: JsonWebKey): string { * @returns A string representing the octet key in raw hexadecimal format. */ function octJwkToRawHexKey(jwk: JsonWebKey): string { + jwk = sanitizedJwk(jwk) if (!jwk.k) { throw new Error("Octet JWK must contain 'k' property.") } - const key = u8a.fromString(jwk.k, 'base64url') + // We are converting from base64 to base64url to be sure. The spec uses base64url, but in the wild we sometimes encounter a base64 string + const key = u8a.fromString(jwk.k.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''), 'base64url') return u8a.toString(key, 'hex') } @@ -404,7 +415,7 @@ const toSecp256k1Jwk = (keyHex: string, opts?: { use?: JwkKeyUse; isPrivateKey?: const keyPair = opts?.isPrivateKey ? secp256k1.keyFromPrivate(keyBytes) : secp256k1.keyFromPublic(keyBytes) const pubPoint = keyPair.getPublic() - return { + return sanitizedJwk({ alg: JoseSignatureAlgorithm.ES256K, ...(use !== undefined && { use }), kty: JwkKeyType.EC, @@ -412,7 +423,7 @@ const toSecp256k1Jwk = (keyHex: string, opts?: { use?: JwkKeyUse; isPrivateKey?: x: hexToBase64(pubPoint.getX().toString('hex'), 'base64url'), y: hexToBase64(pubPoint.getY().toString('hex'), 'base64url'), ...(opts?.isPrivateKey && { d: hexToBase64(keyPair.getPrivate('hex'), 'base64url') }), - } + }) } /** @@ -435,7 +446,7 @@ const toSecp256r1Jwk = (keyHex: string, opts?: { use?: JwkKeyUse; isPrivateKey?: logger.debug(`keyBytes length: ${keyBytes}`) const keyPair = opts?.isPrivateKey ? secp256r1.keyFromPrivate(keyBytes) : secp256r1.keyFromPublic(keyBytes) const pubPoint = keyPair.getPublic() - return { + return sanitizedJwk({ alg: JoseSignatureAlgorithm.ES256, ...(use !== undefined && { use }), kty: JwkKeyType.EC, @@ -443,7 +454,7 @@ const toSecp256r1Jwk = (keyHex: string, opts?: { use?: JwkKeyUse; isPrivateKey?: x: hexToBase64(pubPoint.getX().toString('hex'), 'base64url'), y: hexToBase64(pubPoint.getY().toString('hex'), 'base64url'), ...(opts?.isPrivateKey && { d: hexToBase64(keyPair.getPrivate('hex'), 'base64url') }), - } + }) } /** @@ -461,13 +472,13 @@ const toEd25519OrX25519Jwk = ( ): JWK => { assertProperKeyLength(publicKeyHex, 64) const { use } = opts ?? {} - return { + return sanitizedJwk({ alg: JoseSignatureAlgorithm.EdDSA, ...(use !== undefined && { use }), kty: JwkKeyType.OKP, crv: opts?.crv ?? JoseCurve.Ed25519, x: hexToBase64(publicKeyHex, 'base64url'), - } + }) } const toRSAJwk = (publicKeyHex: string, opts?: { use?: JwkKeyUse; key?: IKey | MinimalImportableKey }): JWK => { @@ -488,12 +499,12 @@ const toRSAJwk = (publicKeyHex: string, opts?: { use?: JwkKeyUse; key?: IKey | M // const modulusBitLength = (modulus.length / 2) * 8 // const alg = modulusBitLength === 2048 ? JoseSignatureAlgorithm.RS256 : modulusBitLength === 3072 ? JoseSignatureAlgorithm.RS384 : modulusBitLength === 4096 ? JoseSignatureAlgorithm.RS512 : undefined - return { + return sanitizedJwk({ kty: 'RSA', n: hexToBase64(modulus, 'base64url'), e: hexToBase64(exponent, 'base64url'), // ...(alg && { alg }), - } + }) } export const padLeft = (args: { data: string; size?: number; padString?: string }): string => { @@ -718,6 +729,26 @@ export const globalCrypto = (setGlobal: boolean, suppliedCrypto?: Crypto): Crypt return webcrypto } +export const sanitizedJwk = (input: JWK | JsonWebKey): JWK => { + const inputJwk = typeof input['toJsonDTO'] === 'function' ? input['toJsonDTO']() : {...input} as JWK // KMP code can expose this. It converts a KMP JWK with mangled names into a clean JWK + + const jwk = { + ...inputJwk, + ...(inputJwk.x && { x: base64ToBase64Url(inputJwk.x as string) }), + ...(inputJwk.y && { y: base64ToBase64Url(inputJwk.y as string) }), + ...(inputJwk.d && { d: base64ToBase64Url(inputJwk.d as string) }), + ...(inputJwk.n && { n: base64ToBase64Url(inputJwk.n as string) }), + ...(inputJwk.e && { e: base64ToBase64Url(inputJwk.e as string) }), + ...(inputJwk.k && { k: base64ToBase64Url(inputJwk.k as string) }), + } as JWK + + return removeNulls(jwk) +} + +const base64ToBase64Url = (input: string): string => { + return input.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') +} + /** * */ @@ -748,75 +779,82 @@ export async function verifyRawSignature({ return BigInt(`0x${hex}`) } - const key = removeNulls(inputKey) - validateJwk(key, { crvOptional: true }) - const keyType = keyTypeFromCryptographicSuite({ crv: key.crv, kty: key.kty, alg: key.alg }) - const publicKeyHex = await jwkToRawHexKey(key) - - // TODO: We really should look at the signature alg first if provided! From key type should be the last resort - switch (keyType) { - case 'Secp256k1': - return secp256k1.verify(signature, data, publicKeyHex, { format: 'compact', prehash: true }) - case 'Secp256r1': - return p256.verify(signature, data, publicKeyHex, { format: 'compact', prehash: true }) - case 'Secp384r1': - return p384.verify(signature, data, publicKeyHex, { format: 'compact', prehash: true }) - case 'Secp521r1': - return p521.verify(signature, data, publicKeyHex, { format: 'compact', prehash: true }) - case 'Ed25519': - return ed25519.verify(signature, data, u8a.fromString(publicKeyHex, 'hex')) - case 'Bls12381G1': - case 'Bls12381G2': - return bls12_381.verify(signature, data, u8a.fromString(publicKeyHex, 'hex')) - case 'RSA': { - const signatureAlgorithm = opts?.signatureAlg ?? JoseSignatureAlgorithm.PS256 - const hashAlg = - signatureAlgorithm === (JoseSignatureAlgorithm.RS512 || JoseSignatureAlgorithm.PS512) - ? sha512 - : signatureAlgorithm === (JoseSignatureAlgorithm.RS384 || JoseSignatureAlgorithm.PS384) - ? sha384 - : sha256 - switch (signatureAlgorithm) { - case JoseSignatureAlgorithm.RS256: - return rsa.PKCS1_SHA256.verify( - { - n: jwkPropertyToBigInt(key.n), - e: jwkPropertyToBigInt(key.e), - }, - data, - signature - ) - case JoseSignatureAlgorithm.RS384: - return rsa.PKCS1_SHA384.verify( - { - n: jwkPropertyToBigInt(key.n), - e: jwkPropertyToBigInt(key.e), - }, - data, - signature - ) - case JoseSignatureAlgorithm.RS512: - return rsa.PKCS1_SHA512.verify( - { - n: jwkPropertyToBigInt(key.n), - e: jwkPropertyToBigInt(key.e), - }, - data, - signature - ) - case JoseSignatureAlgorithm.PS256: - case JoseSignatureAlgorithm.PS384: - case JoseSignatureAlgorithm.PS512: - return rsa.PSS(hashAlg, rsa.mgf1(hashAlg)).verify( - { - n: jwkPropertyToBigInt(key.n), - e: jwkPropertyToBigInt(key.e), - }, - data, - signature - ) + try { + debug(`verifyRawSignature for: ${inputKey}`) + const jwk = sanitizedJwk(inputKey) + validateJwk(jwk, { crvOptional: true }) + const keyType = keyTypeFromCryptographicSuite({ crv: jwk.crv, kty: jwk.kty, alg: jwk.alg }) + const publicKeyHex = await jwkToRawHexKey(jwk) + + // TODO: We really should look at the signature alg first if provided! From key type should be the last resort + switch (keyType) { + case 'Secp256k1': + return secp256k1.verify(signature, data, publicKeyHex, { format: 'compact', prehash: true }) + case 'Secp256r1': + return p256.verify(signature, data, publicKeyHex, { format: 'compact', prehash: true }) + case 'Secp384r1': + return p384.verify(signature, data, publicKeyHex, { format: 'compact', prehash: true }) + case 'Secp521r1': + return p521.verify(signature, data, publicKeyHex, { format: 'compact', prehash: true }) + case 'Ed25519': + return ed25519.verify(signature, data, u8a.fromString(publicKeyHex, 'hex')) + case 'Bls12381G1': + case 'Bls12381G2': + return bls12_381.verify(signature, data, u8a.fromString(publicKeyHex, 'hex')) + case 'RSA': { + const signatureAlgorithm = opts?.signatureAlg ?? JoseSignatureAlgorithm.PS256 + const hashAlg = + signatureAlgorithm === (JoseSignatureAlgorithm.RS512 || JoseSignatureAlgorithm.PS512) + ? sha512 + : signatureAlgorithm === (JoseSignatureAlgorithm.RS384 || JoseSignatureAlgorithm.PS384) + ? sha384 + : sha256 + switch (signatureAlgorithm) { + case JoseSignatureAlgorithm.RS256: + return rsa.PKCS1_SHA256.verify( + { + n: jwkPropertyToBigInt(jwk.n!), + e: jwkPropertyToBigInt(jwk.e!), + }, + data, + signature + ) + case JoseSignatureAlgorithm.RS384: + return rsa.PKCS1_SHA384.verify( + { + n: jwkPropertyToBigInt(jwk.n!), + e: jwkPropertyToBigInt(jwk.e!), + }, + data, + signature + ) + case JoseSignatureAlgorithm.RS512: + return rsa.PKCS1_SHA512.verify( + { + n: jwkPropertyToBigInt(jwk.n!), + e: jwkPropertyToBigInt(jwk.e!), + }, + data, + signature + ) + case JoseSignatureAlgorithm.PS256: + case JoseSignatureAlgorithm.PS384: + case JoseSignatureAlgorithm.PS512: + return rsa.PSS(hashAlg, rsa.mgf1(hashAlg)).verify( + { + n: jwkPropertyToBigInt(jwk.n!), + e: jwkPropertyToBigInt(jwk.e!), + }, + data, + signature + ) + } } } + + throw Error(`Unsupported key type for signature validation: ${keyType}`) + } catch (error: any) { + logger.error(`Error: ${error}`) + throw error } - throw Error(`Unsupported key type for signature validation: ${keyType}`) }