UNPKG

@sphereon/ssi-sdk-ext.identifier-resolution

Version:

406 lines (386 loc) 16.1 kB
import { getFirstKeyWithRelation } from '@sphereon/ssi-sdk-ext.did-utils' import { calculateJwkThumbprint, coseKeyToJwk, globalCrypto, toJwk } from '@sphereon/ssi-sdk-ext.key-utils' import { pemOrDerToX509Certificate } from '@sphereon/ssi-sdk-ext.x509-utils' import { contextHasDidManager, contextHasKeyManager } from '@sphereon/ssi-sdk.agent-config' import type { ICoseKeyJson, JWK } from '@sphereon/ssi-types' import type { IAgentContext, IIdentifier, IKey, IKeyManager } from '@veramo/core' import { CryptoEngine, setEngine } from 'pkijs' import { webcrypto } from 'node:crypto' import type { IIdentifierResolution, ManagedIdentifierCoseKeyOpts, ManagedIdentifierCoseKeyResult, ManagedIdentifierDidOpts, ManagedIdentifierDidResult, ManagedIdentifierOID4VCIssuerOpts, ManagedIdentifierOID4VCIssuerResult, ManagedIdentifierJwkOpts, ManagedIdentifierJwkResult, ManagedIdentifierKeyOpts, ManagedIdentifierKeyResult, ManagedIdentifierKidOpts, ManagedIdentifierKidResult, ManagedIdentifierOptsOrResult, ManagedIdentifierResult, ManagedIdentifierX5cOpts, ManagedIdentifierX5cResult, } from '../types' import { isManagedIdentifierCoseKeyOpts, isManagedIdentifierDidOpts, isManagedIdentifierDidResult, isManagedIdentifierOID4VCIssuerOpts, isManagedIdentifierJwkOpts, isManagedIdentifierJwkResult, isManagedIdentifierKeyOpts, isManagedIdentifierKeyResult, isManagedIdentifierKidOpts, isManagedIdentifierX5cOpts, } from '../types' export async function getManagedKidIdentifier( opts: ManagedIdentifierKidOpts, context: IAgentContext<IKeyManager> ): Promise<ManagedIdentifierKidResult> { const method = 'kid' let key: IKey | undefined = undefined let issuer: string | undefined = undefined let kid: string | undefined = undefined if (!contextHasKeyManager(context)) { return Promise.reject(Error(`Cannot get Key/JWK identifier if KeyManager plugin is not enabled!`)) } else if (opts.identifier.startsWith('did:')) { const did = opts.identifier.split('#')[0] const didIdentifier = await getManagedDidIdentifier({ ...opts, method: 'did', identifier: did }, context) key = didIdentifier.key issuer = didIdentifier.issuer kid = opts?.kid ?? (key.meta?.verificationMethod?.id as string) ?? didIdentifier.kid } if (!key) { key = await context.agent.keyManagerGet({ kid: opts.kmsKeyRef ?? opts.identifier }) } const jwk = toJwk(key.publicKeyHex, key.type, { key }) const jwkThumbprint = (key.meta?.jwkThumbprint as string) ?? calculateJwkThumbprint({ jwk }) if (!kid) { kid = opts.kid ?? (key.meta?.verificationMethod?.id as string) ?? key.kid ?? jwkThumbprint } if (!issuer) { issuer = opts.issuer ?? kid // The different identifiers should set the value. Defaults to the kid } return { method, key, identifier: opts.identifier, jwk, jwkThumbprint, kid, clientId: opts.clientId, clientIdScheme: opts.clientIdScheme, issuer, kmsKeyRef: key.kid, opts, } satisfies ManagedIdentifierKidResult } export function isManagedIdentifierResult( identifier: ManagedIdentifierOptsOrResult & { crypto?: webcrypto.Crypto } ): identifier is ManagedIdentifierResult { return 'key' in identifier && 'kmsKeyRef' in identifier && 'method' in identifier && 'opts' in identifier && 'jwkThumbprint' in identifier } /** * Allows to get a managed identifier result in case identifier options are passed in, but returns the identifier directly in case results are passed in. This means resolution can have happened before, or happens in this method * @param identifier * @param context */ export async function ensureManagedIdentifierResult( identifier: ManagedIdentifierOptsOrResult & { crypto?: webcrypto.Crypto }, context: IAgentContext<IKeyManager> ): Promise<ManagedIdentifierResult> { const { lazyDisabled = false } = identifier return !lazyDisabled && isManagedIdentifierResult(identifier) ? identifier : await getManagedIdentifier(identifier, context) } /** * This function is just a convenience function to get a common result. The user already apparently had a key, so could have called the kid version as well * @param opts * @param _context */ export async function getManagedKeyIdentifier(opts: ManagedIdentifierKeyOpts, _context?: IAgentContext<any>): Promise<ManagedIdentifierKeyResult> { const method = 'key' const key: IKey = opts.identifier if (opts.kmsKeyRef && opts.kmsKeyRef !== key.kid) { return Promise.reject(Error(`Cannot get a managed key object by providing a key and a kmsKeyRef that are different.}`)) } const jwk = toJwk(key.publicKeyHex, key.type, { key }) const jwkThumbprint = (key.meta?.jwkThumbprint as string) ?? calculateJwkThumbprint({ jwk }) const kid = opts.kid ?? (key.meta?.verificationMethod?.id as string) ?? jwkThumbprint const issuer = opts.issuer ?? kid // The different identifiers should set the value. Defaults to the kid return { method, key, identifier: key, jwk, jwkThumbprint, kid, issuer, kmsKeyRef: key.kid, clientId: opts.clientId, clientIdScheme: opts.clientIdScheme, opts, } satisfies ManagedIdentifierKeyResult } /** * This function is just a convenience function to get a common result. The user already apparently had a key, so could have called the kid version as well * @param opts * @param context */ export async function getManagedCoseKeyIdentifier( opts: ManagedIdentifierCoseKeyOpts, context: IAgentContext<any> ): Promise<ManagedIdentifierCoseKeyResult> { const method = 'cose_key' const coseKey: ICoseKeyJson = opts.identifier if (!contextHasKeyManager(context)) { return Promise.reject(Error(`Cannot get Cose Key identifier if KeyManager plugin is not enabled!`)) } const jwk = coseKeyToJwk(coseKey) const jwkThumbprint = calculateJwkThumbprint({ jwk }) const key = await context.agent.keyManagerGet({ kid: opts.kmsKeyRef ?? jwkThumbprint }) const kid = opts.kid ?? coseKey.kid ?? jwkThumbprint const issuer = opts.issuer return { method, key, identifier: opts.identifier, jwk, jwkThumbprint, kid, issuer, kmsKeyRef: key.kid, clientId: opts.clientId, clientIdScheme: opts.clientIdScheme, opts, } satisfies ManagedIdentifierCoseKeyResult } export async function getManagedDidIdentifier(opts: ManagedIdentifierDidOpts, context: IAgentContext<any>): Promise<ManagedIdentifierDidResult> { const method = 'did' if (!contextHasDidManager(context)) { return Promise.reject(Error(`Cannot get DID identifier if DID Manager plugin is not enabled!`)) } let identifier: IIdentifier if (typeof opts.identifier === 'string') { identifier = await context.agent.didManagerGet({ did: opts.identifier.split('#')[0] }) } else { identifier = opts.identifier } const did = identifier.did const keys = identifier?.keys // fixme: We really want to return the vmRelationship keys here actually const extendedKey = await getFirstKeyWithRelation( { ...opts, // Make sure we use offline mode if no pref was supplied. We are looking for managed DIDs after all. Could be it is not published yet offlineWhenNoDIDRegistered: opts.offlineWhenNoDIDRegistered ?? true, identifier, vmRelationship: opts.vmRelationship ?? 'verificationMethod', }, context ) const key = extendedKey const controllerKeyId = identifier.controllerKeyId const jwk = toJwk(key.publicKeyHex, key.type, { key }) const jwkThumbprint = key.meta?.jwkThumbprint ?? calculateJwkThumbprint({ jwk }) let kid = opts.kid ?? extendedKey.meta?.verificationMethod?.id if (!kid.startsWith(did)) { // Make sure we create a fully qualified kid const hash = kid.startsWith('#') ? '' : '#' kid = `${did}${hash}${kid}` } const issuer = opts.issuer ?? did return { method, key, did, kmsKeyRef: key.kid, jwk, jwkThumbprint, controllerKeyId, kid, keys, issuer, identifier, clientId: opts.clientId, clientIdScheme: opts.clientIdScheme, opts, } } export async function getManagedJwkIdentifier( opts: ManagedIdentifierJwkOpts, context: IAgentContext<IKeyManager> ): Promise<ManagedIdentifierJwkResult> { const method = 'jwk' const { kid, issuer } = opts if (!contextHasKeyManager(context)) { return Promise.reject(Error(`Cannot get Key/JWK identifier if KeyManager plugin is not enabled!`)) } const key = await context.agent.keyManagerGet({ kid: opts.kmsKeyRef ?? calculateJwkThumbprint({ jwk: opts.identifier }) }) const jwk = opts.identifier ?? toJwk(key.publicKeyHex, key.type, { key }) const jwkThumbprint = (key.meta?.jwkThumbprint as string) ?? calculateJwkThumbprint({ jwk }) // we explicitly do not set the kid and issuer, meaning it can remain null. Normally you do not provide a kid and issuer with Jwks. return { method, key, kmsKeyRef: key.kid, identifier: jwk, jwk, jwkThumbprint, kid, issuer, clientId: opts.clientId, clientIdScheme: opts.clientIdScheme, opts, } satisfies ManagedIdentifierJwkResult } export async function getManagedX5cIdentifier( opts: ManagedIdentifierX5cOpts & { crypto?: webcrypto.Crypto }, context: IAgentContext<IKeyManager> ): Promise<ManagedIdentifierX5cResult> { const { kid, issuer } = opts const method = 'x5c' const x5c = opts.identifier if (x5c.length === 0) { return Promise.reject(`Cannot resolve x5c when an empty x5c is passed in`) } else if (!contextHasKeyManager(context)) { return Promise.reject(Error(`Cannot get X5c identifier if KeyManager plugin is not enabled!`)) } const cryptoImpl = globalCrypto(false, opts.crypto) const certificate = pemOrDerToX509Certificate(x5c[0]) const cryptoEngine = new CryptoEngine({ name: 'identifier_resolver_managed', crypto: cryptoImpl }) setEngine(cryptoEngine.name, cryptoEngine) const pk = await certificate.getPublicKey(undefined, cryptoEngine) const jwk = (await cryptoEngine.subtle.exportKey('jwk', pk)) as JWK const jwkThumbprint = calculateJwkThumbprint({ jwk }) const key = await context.agent.keyManagerGet({ kid: opts.kmsKeyRef ?? jwkThumbprint }) // we explicitly do not set the kid and issuer, meaning it can remain null. Normally you do not provide a kid and issuer with x5c. return { method, x5c, identifier: x5c, certificate, jwk, jwkThumbprint, key, kmsKeyRef: key.kid, kid, issuer, clientId: opts.clientId, clientIdScheme: opts.clientIdScheme, opts, } satisfies ManagedIdentifierX5cResult } export async function getManagedOID4VCIssuerIdentifier( opts: ManagedIdentifierOID4VCIssuerOpts, context: IAgentContext<IKeyManager> ): Promise<ManagedIdentifierOID4VCIssuerResult> { const { identifier } = opts const method = 'oid4vci-issuer' // FIXME: We need to eventually determine the JWK based on the issuer. Using a dummy JWK for now const jwk = { kty: 'RSA', kid: 'dummy-jwk-for-vci-issuer-signing', use: 'sig', n: 'pjdss8ZaDfEH6K6U7GeW2nxDqR4IP049fk1fK0lndimbMMVBdPv_hSpm8T8EtBDxrUdi1OHZfMhUixGaut-3nQ4GG9nM249oxhCtxqqNvEXrmQRGqczyLxuh-fKn9Fg--hS9UpazHpfVAFnB5aCfXoNhPuI8oByyFKMKaOVgHNqP5NBEqabiLftZD3W_lsFCPGuzr4Vp0YS7zS2hDYScC2oOMu4rGU1LcMZf39p3153Cq7bS2Xh6Y-vw5pwzFYZdjQxDn8x8BG3fJ6j8TGLXQsbKH1218_HcUJRvMwdpbUQG5nvA2GXVqLqdwp054Lzk9_B_f1lVrmOKuHjTNHq48w', e: 'AQAB', d: 'ksDmucdMJXkFGZxiomNHnroOZxe8AmDLDGO1vhs-POa5PZM7mtUPonxwjVmthmpbZzla-kg55OFfO7YcXhg-Hm2OWTKwm73_rLh3JavaHjvBqsVKuorX3V3RYkSro6HyYIzFJ1Ek7sLxbjDRcDOj4ievSX0oN9l-JZhaDYlPlci5uJsoqro_YrE0PRRWVhtGynd-_aWgQv1YzkfZuMD-hJtDi1Im2humOWxA4eZrFs9eG-whXcOvaSwO4sSGbS99ecQZHM2TcdXeAs1PvjVgQ_dKnZlGN3lTWoWfQP55Z7Tgt8Nf1q4ZAKd-NlMe-7iqCFfsnFwXjSiaOa2CRGZn-Q', p: '4A5nU4ahEww7B65yuzmGeCUUi8ikWzv1C81pSyUKvKzu8CX41hp9J6oRaLGesKImYiuVQK47FhZ--wwfpRwHvSxtNU9qXb8ewo-BvadyO1eVrIk4tNV543QlSe7pQAoJGkxCia5rfznAE3InKF4JvIlchyqs0RQ8wx7lULqwnn0', q: 'ven83GM6SfrmO-TBHbjTk6JhP_3CMsIvmSdo4KrbQNvp4vHO3w1_0zJ3URkmkYGhz2tgPlfd7v1l2I6QkIh4Bumdj6FyFZEBpxjE4MpfdNVcNINvVj87cLyTRmIcaGxmfylY7QErP8GFA-k4UoH_eQmGKGK44TRzYj5hZYGWIC8', dp: 'lmmU_AG5SGxBhJqb8wxfNXDPJjf__i92BgJT2Vp4pskBbr5PGoyV0HbfUQVMnw977RONEurkR6O6gxZUeCclGt4kQlGZ-m0_XSWx13v9t9DIbheAtgVJ2mQyVDvK4m7aRYlEceFh0PsX8vYDS5o1txgPwb3oXkPTtrmbAGMUBpE', dq: 'mxRTU3QDyR2EnCv0Nl0TCF90oliJGAHR9HJmBe__EjuCBbwHfcT8OG3hWOv8vpzokQPRl5cQt3NckzX3fs6xlJN4Ai2Hh2zduKFVQ2p-AF2p6Yfahscjtq-GY9cB85NxLy2IXCC0PF--Sq9LOrTE9QV988SJy_yUrAjcZ5MmECk', qi: 'ldHXIrEmMZVaNwGzDF9WG8sHj2mOZmQpw9yrjLK9hAsmsNr5LTyqWAqJIYZSwPTYWhY4nu2O0EY9G9uYiqewXfCKw_UngrJt8Xwfq1Zruz0YY869zPN4GiE9-9rzdZB33RBw8kIOquY3MK74FMwCihYx_LiU2YTHkaoJ3ncvtvg', } as JWK const jwkThumbprint = calculateJwkThumbprint({ jwk }) const key = { kid: 'dummy-key-for-vci-issuer-signing', kms: 'local', type: 'RSA', publicKeyHex: '9a3f75b2e4d8b91128fc6e9a8f6782e5a4f1cba3718e58b5d0a789d6e5f52b3a', } as IKey return { method, identifier, jwk, jwkThumbprint, key, // FIXME: We need construct a key as soon as we have the external VCI Issuer resolution kmsKeyRef: identifier, // FIXME: We need use kmsKeyRef as soon as we have the external VCI Issuer resolution issuer: identifier.replace('/.well-known/openid-credential-issuer', ''), clientId: opts.clientId, clientIdScheme: opts.clientIdScheme, opts, } satisfies ManagedIdentifierOID4VCIssuerResult } export async function getManagedIdentifier( opts: ManagedIdentifierOptsOrResult & { crypto?: webcrypto.Crypto }, context: IAgentContext<IKeyManager> ): Promise<ManagedIdentifierResult> { let resolutionResult: ManagedIdentifierResult if (isManagedIdentifierResult(opts)) { opts } if (isManagedIdentifierKidOpts(opts)) { resolutionResult = await getManagedKidIdentifier(opts, context) } else if (isManagedIdentifierDidOpts(opts)) { resolutionResult = await getManagedDidIdentifier(opts, context) } else if (isManagedIdentifierJwkOpts(opts)) { resolutionResult = await getManagedJwkIdentifier(opts, context) } else if (isManagedIdentifierX5cOpts(opts)) { resolutionResult = await getManagedX5cIdentifier(opts, context) } else if (isManagedIdentifierKeyOpts(opts)) { resolutionResult = await getManagedKeyIdentifier(opts, context) } else if (isManagedIdentifierCoseKeyOpts(opts)) { resolutionResult = await getManagedCoseKeyIdentifier(opts, context) } else if (isManagedIdentifierOID4VCIssuerOpts(opts)) { resolutionResult = await getManagedOID4VCIssuerIdentifier(opts, context) } else { return Promise.reject(Error(`Could not determine identifier method. Please provide explicitly`)) } const { key } = resolutionResult if ( (!key && !isManagedIdentifierOID4VCIssuerOpts(opts)) || (isManagedIdentifierDidOpts(opts) && isManagedIdentifierDidResult(resolutionResult) && !resolutionResult.identifier) ) { console.log(`Cannot find identifier`, opts.identifier) return Promise.reject(`Cannot find identifier ${opts.identifier}`) } return resolutionResult } export async function managedIdentifierToKeyResult( identifier: ManagedIdentifierOptsOrResult, context: IAgentContext<IIdentifierResolution & IKeyManager> ): Promise<ManagedIdentifierKeyResult> { const resolved = await ensureManagedIdentifierResult(identifier, context) if (isManagedIdentifierKeyResult(resolved)) { return resolved } return { ...resolved, method: 'key', identifier: resolved.key, } satisfies ManagedIdentifierKeyResult } export async function managedIdentifierToJwk( identifier: ManagedIdentifierOptsOrResult, context: IAgentContext<IIdentifierResolution & IKeyManager> ): Promise<ManagedIdentifierJwkResult> { const resolved = await ensureManagedIdentifierResult(identifier, context) if (isManagedIdentifierJwkResult(resolved)) { return resolved } return { ...resolved, method: 'jwk', identifier: resolved.jwk, } satisfies ManagedIdentifierJwkResult }