From c5fc07042af584f8bd88fdbbb334eb26f3128c4c Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Tue, 20 Aug 2024 15:39:59 +0200 Subject: [PATCH] fix: handle non-exteral submission edge cases Signed-off-by: Timo Glastra --- lib/PEX.ts | 23 ++- lib/evaluation/evaluationClientWrapper.ts | 147 ++++++++++++------ test/PEX.spec.ts | 52 ++++++- .../pdV1/pd-simple-schema-jwt-degree.json | 25 +++ 4 files changed, 193 insertions(+), 54 deletions(-) create mode 100644 test/dif_pe_examples/pdV1/pd-simple-schema-jwt-degree.json diff --git a/lib/PEX.ts b/lib/PEX.ts index 24328075..2f823ce2 100644 --- a/lib/PEX.ts +++ b/lib/PEX.ts @@ -80,8 +80,8 @@ export class PEX { presentationSubmission?: PresentationSubmission; /** * The location of the presentation submission. By default {@link PresentationSubmissionLocation.PRESENTATION} - * is used when one presentation is passed (not as array), while {@link PresentationSubmissionLocation.EXTERNAL} is - * used when an array is passed + * is used when one W3C presentation is passed (not as array) , while {@link PresentationSubmissionLocation.EXTERNAL} is + * used when an array is passed or the presentation is not a W3C presentation */ presentationSubmissionLocation?: PresentationSubmissionLocation; generatePresentationSubmission?: boolean; @@ -104,13 +104,29 @@ export class PEX { ); let presentationSubmission = opts?.presentationSubmission; + let presentationSubmissionLocation = + opts?.presentationSubmissionLocation ?? + (Array.isArray(presentations) || !CredentialMapper.isW3cPresentation(wrappedPresentations[0].presentation) + ? PresentationSubmissionLocation.EXTERNAL + : PresentationSubmissionLocation.PRESENTATION); // When only one presentation, we also allow it to be present in the VP - if (!presentationSubmission && presentationsArray.length === 1 && !generatePresentationSubmission) { + if ( + !presentationSubmission && + presentationsArray.length === 1 && + CredentialMapper.isW3cPresentation(wrappedPresentations[0].presentation) && + !generatePresentationSubmission + ) { presentationSubmission = wrappedPresentations[0].decoded.presentation_submission; if (!presentationSubmission) { throw Error(`Either a presentation submission as part of the VP or provided in options was expected`); } + presentationSubmissionLocation = PresentationSubmissionLocation.PRESENTATION; + if (opts?.presentationSubmissionLocation && opts.presentationSubmissionLocation !== PresentationSubmissionLocation.PRESENTATION) { + throw new Error( + `unexpected presentationSubmissionLocation ${opts.presentationSubmissionLocation} was provided. Expected ${PresentationSubmissionLocation.PRESENTATION} when no presentationSubmission passed and first verifiable presentation contains a presentation_submission and generatePresentationSubmission is false`, + ); + } } else if (!presentationSubmission && !generatePresentationSubmission) { throw new Error('Presentation submission in options was expected.'); } @@ -125,6 +141,7 @@ export class PEX { ...opts, holderDIDs, presentationSubmission, + presentationSubmissionLocation, generatePresentationSubmission, }; diff --git a/lib/evaluation/evaluationClientWrapper.ts b/lib/evaluation/evaluationClientWrapper.ts index dd3f0ef4..4a35fa74 100644 --- a/lib/evaluation/evaluationClientWrapper.ts +++ b/lib/evaluation/evaluationClientWrapper.ts @@ -381,8 +381,8 @@ export class EvaluationClientWrapper { generatePresentationSubmission?: boolean; /** * The location of the presentation submission. By default {@link PresentationSubmissionLocation.PRESENTATION} - * is used when one presentation is passed (not as array), while {@link PresentationSubmissionLocation.EXTERNAL} is - * used when an array is passed + * is used when one W3C presentation is passed (not as array) , while {@link PresentationSubmissionLocation.EXTERNAL} is + * used when an array is passed or the presentation is not a W3C presentation */ presentationSubmissionLocation?: PresentationSubmissionLocation; }, @@ -444,6 +444,48 @@ export class EvaluationClientWrapper { return result; } + private extractWrappedVcFromWrappedVp( + descriptor: Descriptor, + descriptorIndex: string, + wvp: WrappedVerifiablePresentation, + ): { error: Checked; wvc: undefined } | { wvc: WrappedVerifiableCredential; error: undefined } { + // Decoded won't work for sd-jwt or jwt?!?! + const [vcResult] = JsonPathUtils.extractInputField(wvp.decoded, [descriptor.path]) as Array<{ + value: string | IVerifiableCredential; + }>; + + if (!vcResult) { + return { + error: { + status: Status.ERROR, + tag: 'SubmissionPathNotFound', + message: `Unable to extract path ${descriptor.path} for submission.descriptor_path[${descriptorIndex}] from verifiable presentation`, + }, + wvc: undefined, + }; + } + + // Find the wrapped VC based on the original VC + const originalVc = vcResult.value; + const wvc = wvp.vcs.find((wvc) => CredentialMapper.areOriginalVerifiableCredentialsEqual(wvc.original, originalVc)); + + if (!wvc) { + return { + error: { + status: Status.ERROR, + tag: 'SubmissionPathNotFound', + message: `Unable to find wrapped vc`, + }, + wvc: undefined, + }; + } + + return { + wvc, + error: undefined, + }; + } + private evaluatePresentationsAgainstSubmission( pd: IInternalPresentationDefinition, wvps: OrArray, @@ -452,6 +494,7 @@ export class EvaluationClientWrapper { holderDIDs?: string[]; limitDisclosureSignatureSuites?: string[]; restrictToFormats?: Format; + presentationSubmissionLocation?: PresentationSubmissionLocation; }, ): PresentationEvaluationResults { const result: PresentationEvaluationResults = { @@ -462,76 +505,80 @@ export class EvaluationClientWrapper { value: submission, }; + // If only a single VP is passed that is not w3c and no presentationSubmissionLocation, we set the default location to presentation. Otherwise we assume it's external + const presentationSubmissionLocation = + opts?.presentationSubmissionLocation ?? + (Array.isArray(wvps) || !CredentialMapper.isW3cPresentation(wvps.presentation) + ? PresentationSubmissionLocation.EXTERNAL + : PresentationSubmissionLocation.PRESENTATION); + // We loop over all the descriptors in the submission for (const descriptorIndex in submission.descriptor_map) { const descriptor = submission.descriptor_map[descriptorIndex]; - // Extract the VP from the wrapped VPs - const [vpResult] = JsonPathUtils.extractInputField(wvps, [descriptor.path]) as Array<{ value: WrappedVerifiablePresentation }>; - if (!vpResult) { - result.areRequiredCredentialsPresent = Status.ERROR; - result.errors?.push({ - status: Status.ERROR, - tag: 'SubmissionPathNotFound', - message: `Unable to extract path ${descriptor.path} for submission.descriptor_path[${descriptorIndex}] from presentation(s)`, - }); - continue; - } - const vp = vpResult.value; - let vcPath = `presentation ${descriptor.path}`; - - if (vp.format !== descriptor.format) { - result.areRequiredCredentialsPresent = Status.ERROR; - result.errors?.push({ - status: Status.ERROR, - tag: 'SubmissionFormatNoMatch', - message: `VP at path ${descriptor.path} has format ${vp.format}, while submission.descriptor_path[${descriptorIndex}] has format ${descriptor.format}`, - }); - continue; - } - + let vp: WrappedVerifiablePresentation; let vc: WrappedVerifiableCredential; - if (descriptor.path_nested) { - const [vcResult] = JsonPathUtils.extractInputField(vp.decoded, [descriptor.path_nested.path]) as Array<{ - value: string | IVerifiableCredential; - }>; + let vcPath: string; - if (!vcResult) { + if (presentationSubmissionLocation === PresentationSubmissionLocation.EXTERNAL) { + // Extract the VP from the wrapped VPs + const [vpResult] = JsonPathUtils.extractInputField(wvps, [descriptor.path]) as Array<{ value: WrappedVerifiablePresentation }>; + if (!vpResult) { result.areRequiredCredentialsPresent = Status.ERROR; result.errors?.push({ status: Status.ERROR, tag: 'SubmissionPathNotFound', - message: `Unable to extract path_nested.path ${descriptor.path_nested.path} for submission.descriptor_path[${descriptorIndex}] from verifiable presentation`, + message: `Unable to extract path ${descriptor.path} for submission.descriptor_path[${descriptorIndex}] from presentation(s)`, }); continue; } + vp = vpResult.value; + vcPath = `presentation ${descriptor.path}`; - // Find the wrapped VC based on the orignial VC - const originalVc = vcResult.value; - const wvc = vp.vcs.find((wvc) => CredentialMapper.areOriginalVerifiableCredentialsEqual(wvc.original, originalVc)); - - if (!wvc) { + if (vp.format !== descriptor.format) { result.areRequiredCredentialsPresent = Status.ERROR; result.errors?.push({ status: Status.ERROR, - tag: 'SubmissionPathNotFound', - message: `Unable to find wrapped vc`, + tag: 'SubmissionFormatNoMatch', + message: `VP at path ${descriptor.path} has format ${vp.format}, while submission.descriptor_path[${descriptorIndex}] has format ${descriptor.format}`, }); continue; } - vc = wvc; - vcPath += ` with nested credential ${descriptor.path_nested.path}`; - } else if (descriptor.format === 'vc+sd-jwt') { - vc = vp.vcs[0]; + if (descriptor.path_nested) { + const extractionResult = this.extractWrappedVcFromWrappedVp(descriptor.path_nested, descriptorIndex, vp); + if (extractionResult.error) { + result.areRequiredCredentialsPresent = Status.ERROR; + result.errors?.push(extractionResult.error); + continue; + } + + vc = extractionResult.wvc; + vcPath += ` with nested credential ${descriptor.path_nested.path}`; + } else if (descriptor.format === 'vc+sd-jwt') { + vc = vp.vcs[0]; + } else { + result.areRequiredCredentialsPresent = Status.ERROR; + result.errors?.push({ + status: Status.ERROR, + tag: 'UnsupportedFormat', + message: `VP format ${vp.format} is not supported`, + }); + continue; + } } else { - result.areRequiredCredentialsPresent = Status.ERROR; - result.errors?.push({ - status: Status.ERROR, - tag: 'UnsupportedFormat', - message: `VP format ${vp.format} is not supported`, - }); - continue; + // TODO: check that not longer than 0 + vp = Array.isArray(wvps) ? wvps[0] : wvps; + vcPath = `credential ${descriptor.path}`; + + const extractionResult = this.extractWrappedVcFromWrappedVp(descriptor, descriptorIndex, vp); + if (extractionResult.error) { + result.areRequiredCredentialsPresent = Status.ERROR; + result.errors?.push(extractionResult.error); + continue; + } + + vc = extractionResult.wvc; } // TODO: we should probably add support for holder dids in the kb-jwt of an SD-JWT. We can extract this from the diff --git a/test/PEX.spec.ts b/test/PEX.spec.ts index 52337f1b..75d73f33 100644 --- a/test/PEX.spec.ts +++ b/test/PEX.spec.ts @@ -124,7 +124,57 @@ describe('evaluate', () => { const vpSimple: IVerifiablePresentation = getFileAsJson('./test/dif_pe_examples/vp/vp-simple-age-predicate.json'); pdSchema.input_descriptors[0].schema.push({ uri: 'https://www.w3.org/TR/vc-data-model/#types1' }); const pex: PEX = new PEX(); - const evaluationResults = pex.evaluatePresentation(pdSchema, vpSimple, { limitDisclosureSignatureSuites: LIMIT_DISCLOSURE_SIGNATURE_SUITES }); + const evaluationResults = pex.evaluatePresentation(pdSchema, vpSimple, { + limitDisclosureSignatureSuites: LIMIT_DISCLOSURE_SIGNATURE_SUITES, + }); + expect(evaluationResults!.value!.descriptor_map!.length).toEqual(1); + expect(evaluationResults!.errors!.length).toEqual(0); + }); + + it('Evaluate case without any error passing submission and presentation submission location', () => { + const pdSchema: PresentationDefinitionV1 = getFileAsJson( + './test/dif_pe_examples/pdV1/pd-simple-schema-age-predicate.json', + ).presentation_definition; + const vpSimple: IVerifiablePresentation = getFileAsJson('./test/dif_pe_examples/vp/vp-simple-age-predicate.json'); + pdSchema.input_descriptors[0].schema.push({ uri: 'https://www.w3.org/TR/vc-data-model/#types1' }); + const pex: PEX = new PEX(); + const evaluationResults = pex.evaluatePresentation(pdSchema, vpSimple, { + limitDisclosureSignatureSuites: LIMIT_DISCLOSURE_SIGNATURE_SUITES, + presentationSubmission: { + id: 'accd5adf-1dbf-4ed9-9ba2-d687476126cb', + definition_id: '31e2f0f1-6b70-411d-b239-56aed5321884', + descriptor_map: [ + { + id: '867bfe7a-5b91-46b2-9ba4-70028b8d9cc8', + format: 'ldp_vp', + path: '$.verifiableCredential[0]', + }, + ], + }, + presentationSubmissionLocation: PresentationSubmissionLocation.PRESENTATION, + }); + expect(evaluationResults!.value!.descriptor_map!.length).toEqual(1); + expect(evaluationResults!.errors!.length).toEqual(0); + }); + + it('Evaluate case without any error passing submission and presentation submission location presentation W3C JWT vc', () => { + const pdSchema: PresentationDefinitionV1 = getFileAsJson('./test/dif_pe_examples/pdV1/pd-simple-schema-jwt-degree.json').presentation_definition; + const pex: PEX = new PEX(); + const evaluationResults = pex.evaluatePresentation(pdSchema, getFile('./test/dif_pe_examples/vp/vp_universityDegree.jwt'), { + limitDisclosureSignatureSuites: LIMIT_DISCLOSURE_SIGNATURE_SUITES, + presentationSubmission: { + id: 'accd5adf-1dbf-4ed9-9ba2-d687476126cb', + definition_id: '31e2f0f1-6b70-411d-b239-56aed5321884', + descriptor_map: [ + { + id: '867bfe7a-5b91-46b2-9ba4-70028b8d9cc8', + format: 'jwt_vc', + path: '$.vp.verifiableCredential[0]', + }, + ], + }, + presentationSubmissionLocation: PresentationSubmissionLocation.PRESENTATION, + }); expect(evaluationResults!.value!.descriptor_map!.length).toEqual(1); expect(evaluationResults!.errors!.length).toEqual(0); }); diff --git a/test/dif_pe_examples/pdV1/pd-simple-schema-jwt-degree.json b/test/dif_pe_examples/pdV1/pd-simple-schema-jwt-degree.json new file mode 100644 index 00000000..299b583e --- /dev/null +++ b/test/dif_pe_examples/pdV1/pd-simple-schema-jwt-degree.json @@ -0,0 +1,25 @@ +{ + "comment": "Note: VP, OIDC, DIDComm, or CHAPI outer wrapper would be here.", + "presentation_definition": { + "id": "31e2f0f1-6b70-411d-b239-56aed5321884", + "purpose": "To sell you a drink we need to know that you are an adult.", + "input_descriptors": [ + { + "id": "867bfe7a-5b91-46b2-9ba4-70028b8d9cc8", + "purpose": "Degree", + "schema": [ + { + "uri": "https://www.w3.org/2018/credentials/v1" + } + ], + "constraints": { + "fields": [ + { + "path": ["$.vc.credentialSubject.degree.type"] + } + ] + } + } + ] + } +}