diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b683ead..a1c15dd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,8 +16,6 @@ jobs: with: fetch-depth: 0 - uses: pnpm/action-setup@v4 - with: - version: 9 - name: Use Node.js uses: actions/setup-node@v4 with: diff --git a/lib/evaluation/evaluationClientWrapper.ts b/lib/evaluation/evaluationClientWrapper.ts index b2bc874..214aa2a 100644 --- a/lib/evaluation/evaluationClientWrapper.ts +++ b/lib/evaluation/evaluationClientWrapper.ts @@ -571,12 +571,12 @@ export class EvaluationClientWrapper { // Iterate over each descriptor in the submission for (const [descriptorIndex, descriptor] of submission.descriptor_map.entries()) { - let matchingVps: WrappedVerifiablePresentation[] = []; + let matchingVp: WrappedVerifiablePresentation; if (presentationSubmissionLocation === PresentationSubmissionLocation.EXTERNAL) { // Extract VPs matching the descriptor path const vpResults = JsonPathUtils.extractInputField(wvps, [descriptor.path]) as Array<{ - value: WrappedVerifiablePresentation[]; + value: WrappedVerifiablePresentation; }>; if (!vpResults.length) { @@ -587,45 +587,56 @@ export class EvaluationClientWrapper { message: `Unable to extract path ${descriptor.path} for submission.descriptor_map[${descriptorIndex}] from presentation(s)`, }); continue; + } else if (vpResults.length > 1) { + result.areRequiredCredentialsPresent = Status.ERROR; + result.errors?.push({ + status: Status.ERROR, + tag: 'SubmissionPathMultipleEntries', + message: `Extraction of path ${descriptor.path} for submission.descriptor_map[${descriptorIndex}] resulted in multiple values being returned.`, + }); + continue; } - // Flatten the array of VPs - const allVps = vpResults.flatMap((vpResult) => vpResult.value); - - // Filter VPs that match the required format - matchingVps = allVps.filter((vp) => vp.format === descriptor.format); - - if (!matchingVps.length) { + matchingVp = vpResults[0].value; + if (Array.isArray(matchingVp)) { result.areRequiredCredentialsPresent = Status.ERROR; result.errors?.push({ status: Status.ERROR, - tag: 'SubmissionFormatNoMatch', - message: `No VP at path ${descriptor.path} matches the required format ${descriptor.format}`, + tag: 'SubmissionPathMultipleEntries', + message: `Extraction of path ${descriptor.path} for submission.descriptor_map[${descriptorIndex}] returned multiple entires. This is probably because the submission uses '$' to reference the presentation, while an array was used (thus all presentations are selected). Make sure the submission uses the correct path.`, + }); + continue; + } + if (!matchingVp) { + result.areRequiredCredentialsPresent = Status.ERROR; + result.errors?.push({ + status: Status.ERROR, + tag: 'SubmissionPathNotFound', + message: `Extraction of path ${descriptor.path} for submission.descriptor_map[${descriptorIndex}] succeeded, but the value was undefined.`, }); continue; } - // Log a warning if multiple VPs match the descriptor - if (matchingVps.length > 1) { - result.warnings?.push({ - status: Status.WARN, - tag: 'MultipleVpsMatched', - message: `Multiple VPs matched for descriptor_path[${descriptorIndex}]. Using the first matching VP.`, + if (matchingVp.format !== descriptor.format) { + result.areRequiredCredentialsPresent = Status.ERROR; + result.errors?.push({ + status: Status.ERROR, + tag: 'SubmissionFormatNoMatch', + message: `The VP at path ${descriptor.path} does not match the required format ${descriptor.format}`, }); + continue; } } else { // When submission location is PRESENTATION, assume a single VP - matchingVps = Array.isArray(wvps) ? [wvps[0]] : [wvps]; + matchingVp = Array.isArray(wvps) ? wvps[0] : wvps; } - // Process the first matching VP - const vp = matchingVps[0]; let vc: WrappedVerifiableCredential; let vcPath: string = `presentation ${descriptor.path}`; if (presentationSubmissionLocation === PresentationSubmissionLocation.EXTERNAL) { if (descriptor.path_nested) { - const extractionResult = this.extractWrappedVcFromWrappedVp(descriptor.path_nested, descriptorIndex.toString(), vp); + const extractionResult = this.extractWrappedVcFromWrappedVp(descriptor.path_nested, descriptorIndex.toString(), matchingVp); if (extractionResult.error) { result.areRequiredCredentialsPresent = Status.ERROR; result.errors?.push(extractionResult.error); @@ -635,7 +646,7 @@ export class EvaluationClientWrapper { vc = extractionResult.wvc; vcPath += ` with nested credential ${descriptor.path_nested.path}`; } else if (descriptor.format === 'vc+sd-jwt' || descriptor.format === 'mso_mdoc') { - if (!vp.vcs || !vp.vcs.length) { + if (!matchingVp.vcs || !matchingVp.vcs.length) { result.areRequiredCredentialsPresent = Status.ERROR; result.errors?.push({ status: Status.ERROR, @@ -644,18 +655,18 @@ export class EvaluationClientWrapper { }); continue; } - vc = vp.vcs[0]; + vc = matchingVp.vcs[0]; } else { result.areRequiredCredentialsPresent = Status.ERROR; result.errors?.push({ status: Status.ERROR, tag: 'UnsupportedFormat', - message: `VP format ${vp.format} is not supported`, + message: `VP format ${matchingVp.format} is not supported`, }); continue; } } else { - const extractionResult = this.extractWrappedVcFromWrappedVp(descriptor, descriptorIndex.toString(), vp); + const extractionResult = this.extractWrappedVcFromWrappedVp(descriptor, descriptorIndex.toString(), matchingVp); if (extractionResult.error) { result.areRequiredCredentialsPresent = Status.ERROR; result.errors?.push(extractionResult.error); @@ -671,7 +682,9 @@ export class EvaluationClientWrapper { // Determine holder DIDs const holderDIDs = - CredentialMapper.isW3cPresentation(vp.presentation) && vp.presentation.holder ? [vp.presentation.holder] : opts?.holderDIDs || []; + CredentialMapper.isW3cPresentation(matchingVp.presentation) && matchingVp.presentation.holder + ? [matchingVp.presentation.holder] + : opts?.holderDIDs || []; if (pd.input_descriptors.findIndex((_id) => _id.id === descriptor.id) === -1) { result.areRequiredCredentialsPresent = Status.ERROR; diff --git a/test/PEX.spec.ts b/test/PEX.spec.ts index 8feeada..dd5737a 100644 --- a/test/PEX.spec.ts +++ b/test/PEX.spec.ts @@ -1088,6 +1088,59 @@ describe('evaluate', () => { expect(result.error).toEqual('This is not a valid PresentationDefinition'); }); + it('evaluatePresentation with submission using $[0] while not passing an array will result in an error', function () { + const pdSchema: PresentationDefinitionV2 = { + id: '49768857', + input_descriptors: [ + { + id: 'prc_type', + name: 'Name', + purpose: 'We can only support a familyName in a Permanent Resident Card', + constraints: { + fields: [ + { + path: ['$.credentialSubject.familyName'], + filter: { + type: 'string', + const: 'Pasteur', + }, + }, + ], + }, + }, + ], + }; + const pex: PEX = new PEX(); + const jwtEncodedVp = getFile('./test/dif_pe_examples/vp/vp_permanentResidentCard.jwt'); + const evalResult: PresentationEvaluationResults = pex.evaluatePresentation(pdSchema, [jwtEncodedVp], { + presentationSubmissionLocation: PresentationSubmissionLocation.EXTERNAL, + presentationSubmission: { + definition_id: pdSchema.id, + id: 'random', + descriptor_map: [ + { + id: 'prc_type', + format: 'ldp_vp', + path: '$', + path_nested: { + id: 'prc_type', + format: 'ldp_vc', + path: '$.verifiableCredential[0]', + }, + }, + ], + }, + }); + expect(evalResult.errors).toEqual([ + { + message: + "Extraction of path $ for submission.descriptor_map[0] returned multiple entires. This is probably because the submission uses '$' to reference the presentation, while an array was used (thus all presentations are selected). Make sure the submission uses the correct path.", + status: 'error', + tag: 'SubmissionPathMultipleEntries', + }, + ]); + }); + it('should pass with jwt vp with submission data', function () { const pdSchema: PresentationDefinitionV2 = { id: '49768857',