UNPKG

@sphereon/ssi-sdk.presentation-exchange

Version:

199 lines (180 loc) • 7.85 kB
import { IPresentationDefinition } from '@sphereon/pex' import { Format } from '@sphereon/pex-models' import { isManagedIdentifierDidOpts, isManagedIdentifierDidResult, isManagedIdentifierX5cResult, ManagedIdentifierOptsOrResult, } from '@sphereon/ssi-sdk-ext.identifier-resolution' import { CredentialMapper, Optional, OriginalVerifiablePresentation, SdJwtDecodedVerifiableCredential, W3CVerifiablePresentation, } from '@sphereon/ssi-types' import { PresentationPayload, ProofFormat } from '@veramo/core' import { IPEXPresentationSignCallback, IRequiredContext } from './types/IPresentationExchange' export async function createPEXPresentationSignCallback( args: { idOpts: ManagedIdentifierOptsOrResult fetchRemoteContexts?: boolean skipDidResolution?: boolean format?: Format | ProofFormat domain?: string challenge?: string }, context: IRequiredContext, ): Promise<IPEXPresentationSignCallback> { function determineProofFormat(determineArgs: { format?: Format | 'jwt' | 'lds' | 'EthereumEip712Signature2021' presentationDefinition: IPresentationDefinition presentation: Optional<PresentationPayload, 'holder'> | SdJwtDecodedVerifiableCredential }): string { const { format, presentationDefinition, presentation } = determineArgs var formatOptions = format ?? presentationDefinition.format ?? args.format // TODO Refactor so it takes into account the Input Descriptors and we can lookup from there. Now we only do that if there is 1 descriptor if (!formatOptions && presentationDefinition.input_descriptors.length == 1 && 'format' in presentationDefinition.input_descriptors[0]) { formatOptions = presentationDefinition.input_descriptors[0].format } // All format arguments are optional. So if no format has been given we go for the most supported 'jwt' if (!formatOptions) { if (CredentialMapper.isSdJwtDecodedCredentialPayload(presentation.decodedPayload)) { return 'vc+sd-jwt' } else if (CredentialMapper.isMsoMdocDecodedPresentation(presentation.decodedPayload as OriginalVerifiablePresentation)) { return 'mso_mdoc' } else if (CredentialMapper.isW3cPresentation(presentation.decodedPayload)) { if (typeof presentation.signedPayload === 'string') { return 'jwt' } return 'lds' } return 'jwt' } else if (typeof formatOptions === 'string') { // if formatOptions is a singular string we can return that as the format return formatOptions } // here we transform all format options to either lds or jwt. but we also want to support sd-jwt, so we need to specifically check for this one. which is ['vc+sd-jwt'] const formats = new Set( Object.keys(formatOptions).map((form) => (form.includes('ldp') ? 'lds' : form.includes('vc+sd-jwt') ? 'vc+sd-jwt' : 'jwt')), ) // if we only have 1 format type we can return that if (formats.size === 1) { return formats.values().next().value!! } formats.keys().next() // if we can go for sd-jwt, we go for sd-jwt if (formats.has('vc+sd-jwt')) { return 'vc+sd-jwt' } // if it is not sd-jwt we would like to go for jwt else if (formats.has('jwt')) { return 'jwt' } // else we go for lds return 'lds' } return async ({ presentation, domain, presentationDefinition, format, challenge, }: { presentation: Optional<PresentationPayload, 'holder'> | SdJwtDecodedVerifiableCredential presentationDefinition: IPresentationDefinition format?: Format | ProofFormat domain?: string challenge?: string }): Promise<W3CVerifiablePresentation> => { const proofFormat = determineProofFormat({ format, presentationDefinition, presentation }) const { idOpts } = args const CLOCK_SKEW = 120 if (args.skipDidResolution && isManagedIdentifierDidOpts(idOpts)) { idOpts.offlineWhenNoDIDRegistered = true } if ('compactSdJwtVc' in presentation) { if (proofFormat !== 'vc+sd-jwt') { return Promise.reject(Error(`presentation payload does not match proof format ${proofFormat}`)) } const presentationResult = await context.agent.createSdJwtPresentation({ ...(idOpts?.method === 'oid4vci-issuer' && { holder: idOpts?.issuer as string }), presentation: presentation.compactSdJwtVc, kb: { payload: { ...presentation.kbJwt?.payload, iat: presentation.kbJwt?.payload?.iat ?? Math.floor(Date.now() / 1000 - CLOCK_SKEW), nonce: challenge ?? presentation.kbJwt?.payload?.nonce, aud: presentation.kbJwt?.payload?.aud ?? domain ?? args.domain, }, }, }) return CredentialMapper.storedPresentationToOriginalFormat(presentationResult.presentation as OriginalVerifiablePresentation) } else { const resolution = await context.agent.identifierManagedGet(idOpts) if (proofFormat === 'vc+sd-jwt') { return Promise.reject(Error(`presentation payload does not match proof format ${proofFormat}`)) } let header if (!presentation.holder) { presentation.holder = resolution.issuer } if (proofFormat === 'jwt') { header = { ...((isManagedIdentifierDidResult(resolution) || isManagedIdentifierX5cResult(resolution)) && resolution.kid && { kid: resolution.kid }), ...(isManagedIdentifierX5cResult(resolution) && { jwk: resolution.jwk }), } if (presentation.verifier || !presentation.aud) { presentation.aud = Array.isArray(presentation.verifier) ? presentation.verifier : (presentation.verifier ?? domain ?? args.domain) delete presentation.verifier } if (!presentation.nbf) { if (presentation.issuanceDate) { const converted = Date.parse(presentation.issuanceDate) if (!isNaN(converted)) { presentation.nbf = Math.floor(converted / 1000) // no skew here, as an explicit value was given } } else { presentation.nbf = Math.floor(Date.now() / 1000 - CLOCK_SKEW) } } if (!presentation.iat) { presentation.iat = presentation.nbf } if (!presentation.exp) { if (presentation.expirationDate) { const converted = Date.parse(presentation.expirationDate) if (!isNaN(converted)) { presentation.exp = Math.floor(converted / 1000) // no skew here as an explicit value w as given } } else { presentation.exp = presentation.nbf + 600 + CLOCK_SKEW } } if (!presentation.vp) { presentation.vp = {} } /*if (!presentation.sub) { presentation.sub = id.did }*/ if (!presentation.vp.holder) { presentation.vp.holder = presentation.holder } } // we ignore the alg / proof_format for now, as we already have the kid anyway at this point // todo: look for jwt_vc_json and remove types and @context const vp = await context.agent.createVerifiablePresentation({ presentation: presentation as PresentationPayload, removeOriginalFields: false, keyRef: resolution.kmsKeyRef, // domain: domain ?? args.domain, // handled above, and did-jwt-vc creates an array even for 1 entry challenge: challenge ?? args.challenge, fetchRemoteContexts: args.fetchRemoteContexts !== false, proofFormat: proofFormat as ProofFormat, header, }) // makes sure we extract an actual JWT from the internal representation in case it is a JWT return CredentialMapper.storedPresentationToOriginalFormat(vp as OriginalVerifiablePresentation) } } }