UNPKG

@sphereon/did-auth-siop

Version:

Self Issued OpenID V2 (SIOPv2) and OpenID 4 Verifiable Presentations (OID4VP)

418 lines (386 loc) 18.7 kB
import { IPresentationDefinition, KeyEncoding, PEX, PresentationSubmissionLocation, SelectResults, Status, Validated, VerifiablePresentationFromOpts, VerifiablePresentationResult, } from '@sphereon/pex' import { PresentationEvaluationResults } from '@sphereon/pex/dist/main/lib/evaluation' import { Format, PresentationDefinitionV1, PresentationDefinitionV2, PresentationSubmission } from '@sphereon/pex-models' import { CredentialMapper, Hasher, IProofPurpose, IProofType, OriginalVerifiableCredential, OriginalVerifiablePresentation, W3CVerifiablePresentation, WrappedVerifiablePresentation, } from '@sphereon/ssi-types' import { extractDataFromPath, getWithUrl } from '../helpers' import { AuthorizationRequestPayload, SIOPErrors, SupportedVersion } from '../types' import { PresentationDefinitionLocation, PresentationDefinitionWithLocation, PresentationSignCallback, PresentationVerificationCallback, PresentationVerificationResult, } from './types' export class PresentationExchange { readonly pex: PEX readonly allVerifiableCredentials: OriginalVerifiableCredential[] readonly allDIDs constructor(opts: { allDIDs?: string[]; allVerifiableCredentials: OriginalVerifiableCredential[]; hasher?: Hasher }) { this.allDIDs = opts.allDIDs this.allVerifiableCredentials = opts.allVerifiableCredentials this.pex = new PEX({ hasher: opts.hasher }) } /** * Construct presentation submission from selected credentials * @param presentationDefinition payload object received by the OP from the RP * @param selectedCredentials * @param presentationSignCallback * @param options */ public async createVerifiablePresentation( presentationDefinition: IPresentationDefinition, selectedCredentials: OriginalVerifiableCredential[], presentationSignCallback: PresentationSignCallback, // options2?: { nonce?: string; domain?: string, proofType?: IProofType, verificationMethod?: string, signatureKeyEncoding?: KeyEncoding }, options?: VerifiablePresentationFromOpts, ): Promise<VerifiablePresentationResult> { if (!presentationDefinition) { throw new Error(SIOPErrors.REQUEST_CLAIMS_PRESENTATION_DEFINITION_NOT_VALID) } const signOptions: VerifiablePresentationFromOpts = { ...options, presentationSubmissionLocation: PresentationSubmissionLocation.EXTERNAL, proofOptions: { ...options?.proofOptions, proofPurpose: options?.proofOptions?.proofPurpose ?? IProofPurpose.authentication, type: options?.proofOptions?.type ?? IProofType.EcdsaSecp256k1Signature2019, /* challenge: options?.proofOptions?.challenge, domain: options?.proofOptions?.domain,*/ }, signatureOptions: { ...options?.signatureOptions, // verificationMethod: options?.signatureOptions?.verificationMethod, keyEncoding: options?.signatureOptions?.keyEncoding ?? KeyEncoding.Hex, }, } // When there are MDoc credentials among the selected ones, filter those out as pex does not support mdoc credentials const filteredCredentials = this.removeMDocCredentials(selectedCredentials) return await this.pex.verifiablePresentationFrom(presentationDefinition, filteredCredentials, presentationSignCallback, signOptions) } private removeMDocCredentials(selectedCredentials: OriginalVerifiableCredential[]) { return selectedCredentials.filter((vc) => !CredentialMapper.isMsoMdocDecodedCredential(vc) && !CredentialMapper.isMsoMdocOid4VPEncoded(vc)) } /** * This method will be called from the OP when we are certain that we have a * PresentationDefinition object inside our requestPayload * Finds a set of `VerifiableCredential`s from a list supplied to this class during construction, * matching presentationDefinition object found in the requestPayload * if requestPayload doesn't contain any valid presentationDefinition throws an error * if PEX library returns any error in the process, throws the error * returns the SelectResults object if successful * @param presentationDefinition object received by the OP from the RP * @param opts */ public async selectVerifiableCredentialsForSubmission( presentationDefinition: IPresentationDefinition, opts?: { holderDIDs?: string[] restrictToFormats?: Format restrictToDIDMethods?: string[] }, ): Promise<SelectResults> { if (!presentationDefinition) { throw new Error(SIOPErrors.REQUEST_CLAIMS_PRESENTATION_DEFINITION_NOT_VALID) } else if (!this.allVerifiableCredentials || this.allVerifiableCredentials.length == 0) { throw new Error(`${SIOPErrors.COULD_NOT_FIND_VCS_MATCHING_PD}, no VCs were provided`) } const selectResults: SelectResults = this.pex.selectFrom(presentationDefinition, this.allVerifiableCredentials, { ...opts, holderDIDs: opts?.holderDIDs ?? this.allDIDs, // fixme limited disclosure limitDisclosureSignatureSuites: [], }) if (selectResults.areRequiredCredentialsPresent === Status.ERROR) { throw new Error(`message: ${SIOPErrors.COULD_NOT_FIND_VCS_MATCHING_PD}, details: ${JSON.stringify(selectResults.errors)}`) } return selectResults } /** * validatePresentationAgainstDefinition function is called mainly by the RP * after receiving the VP from the OP * @param presentationDefinition object containing PD * @param verifiablePresentation * @param opts */ public static async validatePresentationAgainstDefinition( presentationDefinition: IPresentationDefinition, verifiablePresentation: OriginalVerifiablePresentation | WrappedVerifiablePresentation, opts?: { limitDisclosureSignatureSuites?: string[] restrictToFormats?: Format restrictToDIDMethods?: string[] presentationSubmission?: PresentationSubmission hasher?: Hasher }, ): Promise<PresentationEvaluationResults> { const wvp: WrappedVerifiablePresentation = typeof verifiablePresentation === 'object' && 'original' in verifiablePresentation ? (verifiablePresentation as WrappedVerifiablePresentation) : CredentialMapper.toWrappedVerifiablePresentation(verifiablePresentation as OriginalVerifiablePresentation) if (!presentationDefinition) { throw new Error(SIOPErrors.REQUEST_CLAIMS_PRESENTATION_DEFINITION_NOT_VALID) } else if ( !wvp || !wvp.presentation || (CredentialMapper.isWrappedW3CVerifiablePresentation(wvp) && (!wvp.presentation.verifiableCredential || wvp.presentation.verifiableCredential.length === 0)) ) { throw new Error(SIOPErrors.NO_VERIFIABLE_PRESENTATION_NO_CREDENTIALS) } const evaluationResults = new PEX({ hasher: opts?.hasher }).evaluatePresentation(presentationDefinition, wvp.original, opts) if (evaluationResults.errors?.length) { throw new Error(`message: ${SIOPErrors.COULD_NOT_FIND_VCS_MATCHING_PD}, details: ${JSON.stringify(evaluationResults.errors)}`) } return evaluationResults } public static assertValidPresentationSubmission(presentationSubmission: PresentationSubmission) { const validationResult: Validated = PEX.validateSubmission(presentationSubmission) if ( (Array.isArray(validationResult) && validationResult[0].message != 'ok') || (!Array.isArray(validationResult) && validationResult.message != 'ok') ) { throw new Error(`${SIOPErrors.RESPONSE_OPTS_PRESENTATIONS_SUBMISSION_IS_NOT_VALID}, details ${JSON.stringify(validationResult)}`) } } /** * Finds a valid PresentationDefinition inside the given AuthenticationRequestPayload * throws exception if the PresentationDefinition is not valid * returns null if no property named "presentation_definition" is found * returns a PresentationDefinition if a valid instance found * @param authorizationRequestPayload object that can have a presentation_definition inside * @param version */ public static async findValidPresentationDefinitions( authorizationRequestPayload: AuthorizationRequestPayload, version?: SupportedVersion, ): Promise<PresentationDefinitionWithLocation[]> { const allDefinitions: PresentationDefinitionWithLocation[] = [] async function extractDefinitionFromVPToken() { const vpTokens: PresentationDefinitionV1[] | PresentationDefinitionV2[] = extractDataFromPath( authorizationRequestPayload, '$..vp_token.presentation_definition', ).map((d) => d.value) const vpTokenRefs = extractDataFromPath(authorizationRequestPayload, '$..vp_token.presentation_definition_uri') if (vpTokens && vpTokens.length && vpTokenRefs && vpTokenRefs.length) { throw new Error(SIOPErrors.REQUEST_CLAIMS_PRESENTATION_NON_EXCLUSIVE) } if (vpTokens && vpTokens.length) { vpTokens.forEach((vpToken: PresentationDefinitionV1 | PresentationDefinitionV2) => { if (allDefinitions.find((value) => value.definition.id === vpToken.id)) { console.log( `Warning. We encountered presentation definition with id ${vpToken.id}, more then once whilst processing! Make sure your payload is valid!`, ) return } PresentationExchange.assertValidPresentationDefinition(vpToken) allDefinitions.push({ definition: vpToken, location: PresentationDefinitionLocation.CLAIMS_VP_TOKEN, version, }) }) } else if (vpTokenRefs && vpTokenRefs.length) { for (const vpTokenRef of vpTokenRefs) { const pd: PresentationDefinitionV1 | PresentationDefinitionV2 = (await getWithUrl(vpTokenRef.value)) as unknown as | PresentationDefinitionV1 | PresentationDefinitionV2 if (allDefinitions.find((value) => value.definition.id === pd.id)) { console.log( `Warning. We encountered presentation definition with id ${pd.id}, more then once whilst processing! Make sure your payload is valid!`, ) return } PresentationExchange.assertValidPresentationDefinition(pd) allDefinitions.push({ definition: pd, location: PresentationDefinitionLocation.CLAIMS_VP_TOKEN, version }) } } } function addSingleToplevelPDToPDs(definition: IPresentationDefinition, version?: SupportedVersion): void { if (allDefinitions.find((value) => value.definition.id === definition.id)) { console.log( `Warning. We encountered presentation definition with id ${definition.id}, more then once whilst processing! Make sure your payload is valid!`, ) return } PresentationExchange.assertValidPresentationDefinition(definition) allDefinitions.push({ definition, location: PresentationDefinitionLocation.TOPLEVEL_PRESENTATION_DEF, version, }) } async function extractDefinitionFromTopLevelDefinitionProperty(version?: SupportedVersion) { const definitions = extractDataFromPath(authorizationRequestPayload, '$.presentation_definition') const definitionsFromList = extractDataFromPath(authorizationRequestPayload, '$.presentation_definition[*]') const definitionRefs = extractDataFromPath(authorizationRequestPayload, '$.presentation_definition_uri') const definitionRefsFromList = extractDataFromPath(authorizationRequestPayload, '$.presentation_definition_uri[*]') const hasPD = (definitions && definitions.length > 0) || (definitionsFromList && definitionsFromList.length > 0) const hasPdRef = (definitionRefs && definitionRefs.length > 0) || (definitionRefsFromList && definitionRefsFromList.length > 0) if (hasPD && hasPdRef) { throw new Error(SIOPErrors.REQUEST_CLAIMS_PRESENTATION_NON_EXCLUSIVE) } if (definitions && definitions.length > 0) { definitions.forEach((definition) => { addSingleToplevelPDToPDs(definition.value, version) }) } else if (definitionsFromList && definitionsFromList.length > 0) { definitionsFromList.forEach((definition) => { addSingleToplevelPDToPDs(definition.value, version) }) } else if (definitionRefs && definitionRefs.length > 0) { for (const definitionRef of definitionRefs) { const pd: PresentationDefinitionV1 | PresentationDefinitionV2 = await getWithUrl(definitionRef.value) addSingleToplevelPDToPDs(pd, version) } } else if (definitionsFromList && definitionRefsFromList.length > 0) { for (const definitionRef of definitionRefsFromList) { const pd: PresentationDefinitionV1 | PresentationDefinitionV2 = await getWithUrl(definitionRef.value) addSingleToplevelPDToPDs(pd, version) } } } if (authorizationRequestPayload) { if (!version || version < SupportedVersion.SIOPv2_D11) { await extractDefinitionFromVPToken() } await extractDefinitionFromTopLevelDefinitionProperty() } return allDefinitions } public static assertValidPresentationDefinitionWithLocations(definitionsWithLocations: PresentationDefinitionWithLocation[]) { if (definitionsWithLocations && definitionsWithLocations.length > 0) { definitionsWithLocations.forEach((definitionWithLocation) => PresentationExchange.assertValidPresentationDefinition(definitionWithLocation.definition), ) } } private static assertValidPresentationDefinition(presentationDefinition: IPresentationDefinition) { const validationResult = PEX.validateDefinition(presentationDefinition) if ( (Array.isArray(validationResult) && validationResult[0].message != 'ok') || (!Array.isArray(validationResult) && validationResult.message != 'ok') ) { throw new Error(`${SIOPErrors.REQUEST_CLAIMS_PRESENTATION_DEFINITION_NOT_VALID}`) } } static async validatePresentationsAgainstDefinitions( definitions: PresentationDefinitionWithLocation[], vpPayloads: Array<WrappedVerifiablePresentation> | WrappedVerifiablePresentation, verifyPresentationCallback?: PresentationVerificationCallback | undefined, opts?: { limitDisclosureSignatureSuites?: string[] restrictToFormats?: Format restrictToDIDMethods?: string[] presentationSubmission?: PresentationSubmission hasher?: Hasher }, ) { if (!definitions || !vpPayloads || (Array.isArray(vpPayloads) && vpPayloads.length === 0) || !definitions.length) { throw new Error(SIOPErrors.COULD_NOT_FIND_VCS_MATCHING_PD) } await Promise.all( definitions.map( async (pd) => await PresentationExchange.validatePresentationsAgainstDefinition(pd.definition, vpPayloads, verifyPresentationCallback, opts), ), ) } static async validatePresentationsAgainstDefinition( definition: IPresentationDefinition, vpPayloads: Array<WrappedVerifiablePresentation> | WrappedVerifiablePresentation, verifyPresentationCallback?: PresentationVerificationCallback, opts?: { limitDisclosureSignatureSuites?: string[] restrictToFormats?: Format restrictToDIDMethods?: string[] presentationSubmission?: PresentationSubmission hasher?: Hasher }, ) { const pex = new PEX({ hasher: opts?.hasher }) const vpPayloadsArray = Array.isArray(vpPayloads) ? vpPayloads : [vpPayloads] let evaluationResults: PresentationEvaluationResults | undefined = undefined if (opts?.presentationSubmission) { evaluationResults = pex.evaluatePresentation( definition, // It's important the structure matches what we received so it can be correctly matched against the submission Array.isArray(vpPayloads) ? vpPayloads.map((wvp) => wvp.original) : vpPayloads.original, { ...opts, presentationSubmissionLocation: PresentationSubmissionLocation.EXTERNAL, }, ) } else { for (const wvp of vpPayloadsArray) { if (CredentialMapper.isWrappedW3CVerifiablePresentation(wvp) && wvp.presentation.presentation_submission) { const presentationSubmission = wvp.presentation.presentation_submission evaluationResults = pex.evaluatePresentation(definition, wvp.original, { ...opts, presentationSubmission, presentationSubmissionLocation: PresentationSubmissionLocation.PRESENTATION, }) const submission = evaluationResults.value // Found valid submission if (evaluationResults.areRequiredCredentialsPresent && submission && submission.definition_id === definition.id) break } } } if (!evaluationResults) { throw new Error(SIOPErrors.NO_PRESENTATION_SUBMISSION) } if ( evaluationResults.areRequiredCredentialsPresent === Status.ERROR || (evaluationResults.errors && evaluationResults.errors.length > 0) || !evaluationResults.value ) { throw new Error(`message: ${SIOPErrors.COULD_NOT_FIND_VCS_MATCHING_PD}, details: ${JSON.stringify(evaluationResults.errors)}`) } if (evaluationResults.value.definition_id !== definition.id) { throw new Error( `${SIOPErrors.PRESENTATION_SUBMISSION_DEFINITION_ID_DOES_NOT_MATCHING_DEFINITION_ID}. submission.definition_id: ${evaluationResults.value.definition_id}, definition.id: ${definition.id}`, ) } const presentationsToVerify = evaluationResults.presentations // The verifyPresentationCallback function is mandatory for RP only, // So the behavior here is to bypass it if not present if (verifyPresentationCallback && evaluationResults.value !== undefined) { // Verify the signature of all VPs await Promise.all( presentationsToVerify.map(async (presentation) => { let verificationResult: PresentationVerificationResult try { verificationResult = await verifyPresentationCallback(presentation as W3CVerifiablePresentation, evaluationResults.value!) } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error) throw new Error(`${SIOPErrors.VERIFIABLE_PRESENTATION_SIGNATURE_NOT_VALID}: ${errorMessage}`) } if (!verificationResult.verified) { throw new Error( SIOPErrors.VERIFIABLE_PRESENTATION_SIGNATURE_NOT_VALID + (verificationResult.reason ? `. ${verificationResult.reason}` : ''), ) } }), ) } PresentationExchange.assertValidPresentationSubmission(evaluationResults.value) return evaluationResults } }