From 8b4e21a37f9fd7279e8d066738c0c6b3f82b3e98 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Mon, 22 Apr 2024 22:41:08 +0200 Subject: [PATCH] feat: evaluate multi-presentations Signed-off-by: Timo Glastra --- lib/PEX.ts | 69 +- lib/PEXv1.ts | 4 +- lib/PEXv2.ts | 4 +- lib/evaluation/core/evaluationResults.ts | 7 +- lib/evaluation/evaluationClient.ts | 7 +- lib/evaluation/evaluationClientWrapper.ts | 478 +++++++++++-- .../subjectIsIssuerEvaluationHandler.ts | 7 +- lib/evaluation/index.ts | 4 +- lib/signing/types.ts | 4 +- lib/types/Internal.types.ts | 3 + lib/utils/VCUtils.ts | 15 +- lib/utils/formatMap.ts | 27 + pnpm-lock.yaml | 40 +- test/PEX.spec.ts | 645 +++++++++++++++++- test/SdJwt.spec.ts | 4 +- ...mats-multi-vp-submission-requirements.json | 86 +++ .../pdV2/pd-multi-formats-multi-vp.json | 64 ++ test/dif_pe_examples/vc/vc-driverLicense.json | 22 +- .../vp/vp_state-business-license.sd-jwt | 1 + test/evaluation/selectFrom.spec.ts | 2 +- test/thirdParty/JGiter.spec.ts | 3 +- 21 files changed, 1356 insertions(+), 140 deletions(-) create mode 100644 lib/utils/formatMap.ts create mode 100644 test/dif_pe_examples/pdV2/pd-multi-formats-multi-vp-submission-requirements.json create mode 100644 test/dif_pe_examples/pdV2/pd-multi-formats-multi-vp.json create mode 100644 test/dif_pe_examples/vp/vp_state-business-license.sd-jwt diff --git a/lib/PEX.ts b/lib/PEX.ts index 7157b227..53cd9f0e 100644 --- a/lib/PEX.ts +++ b/lib/PEX.ts @@ -18,6 +18,7 @@ import { import { Status } from './ConstraintUtils'; import { EvaluationClientWrapper, EvaluationResults, SelectResults } from './evaluation'; +import { PresentationEvaluationResults } from './evaluation/core'; import { PresentationFromOpts, PresentationResult, @@ -28,7 +29,7 @@ import { VerifiablePresentationFromOpts, VerifiablePresentationResult, } from './signing'; -import { DiscoveredVersion, IInternalPresentationDefinition, IPresentationDefinition, PEVersion, SSITypesBuilder } from './types'; +import { DiscoveredVersion, IInternalPresentationDefinition, IPresentationDefinition, OrArray, PEVersion, SSITypesBuilder } from './types'; import { calculateSdHash, definitionVersionDiscovery, getSubjectIdsAsString } from './utils'; import { PresentationDefinitionV1VB, PresentationDefinitionV2VB, PresentationSubmissionVB, Validated, ValidationEngine } from './validation'; @@ -59,46 +60,67 @@ export class PEX { } /*** - * The evaluatePresentation compares what is expected from a presentation with a presentationDefinition. + * The evaluatePresentation compares what is expected from one or more presentations with a presentationDefinition. * presentationDefinition: It can be either v1 or v2 of presentationDefinition * * @param presentationDefinition the definition of what is expected in the presentation. - * @param presentation the presentation which has to be evaluated in comparison of the definition. + * @param presentations the presentation(s) which have to be evaluated in comparison of the definition. * @param opts - limitDisclosureSignatureSuites the credential signature suites that support limit disclosure * * @return the evaluation results specify what was expected and was fulfilled and also specifies which requirements described in the input descriptors - * were not fulfilled by the presentation. + * were not fulfilled by the presentation(s). */ public evaluatePresentation( presentationDefinition: IPresentationDefinition, - presentation: OriginalVerifiablePresentation | IPresentation, + presentations: OrArray, opts?: { limitDisclosureSignatureSuites?: string[]; restrictToFormats?: Format; restrictToDIDMethods?: string[]; 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 + */ + presentationSubmissionLocation?: PresentationSubmissionLocation; generatePresentationSubmission?: boolean; }, - ): EvaluationResults { + ): PresentationEvaluationResults { + // We map it to an array for now to make processing on the presentations easier, but before checking against the submission + // we will transform it to the original structure (array vs single) so the references in the submission stay correct + const presentationsArray = Array.isArray(presentations) ? presentations : [presentations]; + const generatePresentationSubmission = opts?.generatePresentationSubmission !== undefined ? opts.generatePresentationSubmission : opts?.presentationSubmission === undefined; const pd: IInternalPresentationDefinition = SSITypesBuilder.toInternalPresentationDefinition(presentationDefinition); - const presentationCopy: OriginalVerifiablePresentation = JSON.parse(JSON.stringify(presentation)); - const wrappedPresentation: WrappedVerifiablePresentation = SSITypesBuilder.mapExternalVerifiablePresentationToWrappedVP( - presentationCopy, - this.options?.hasher, + const presentationsCopy: OriginalVerifiablePresentation[] = JSON.parse(JSON.stringify(presentationsArray)); + + if (presentationsArray.length === 0) { + throw new Error('At least one presentation must be provided'); + } + + const wrappedPresentations: WrappedVerifiablePresentation[] = presentationsCopy.map((p) => + SSITypesBuilder.mapExternalVerifiablePresentationToWrappedVP(p, this.options?.hasher), ); - const presentationSubmission = opts?.presentationSubmission ?? wrappedPresentation.decoded.presentation_submission; - if (!presentationSubmission && !generatePresentationSubmission) { - throw Error(`Either a presentation submission as part of the VP or provided separately was expected`); + + let presentationSubmission = opts?.presentationSubmission; + + // When only one presentation, we also allow it to be present in the VP + if (!presentationSubmission && presentationsArray.length === 1 && !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`); + } + } else if (!presentationSubmission && !generatePresentationSubmission) { + throw new Error('Presentation submission in options was expected.'); } // TODO: we should probably add support for holder dids in the kb-jwt of an SD-JWT. We can extract this from the // `wrappedPresentation.original.compactKbJwt`, but as HAIP doesn't use dids, we'll leave it for now. - const holderDIDs = - CredentialMapper.isW3cPresentation(wrappedPresentation.presentation) && wrappedPresentation.presentation.holder - ? [wrappedPresentation.presentation.holder] - : []; + const holderDIDs = wrappedPresentations + .map((p) => (CredentialMapper.isW3cPresentation(p.presentation) && p.presentation.holder ? p.presentation.holder : undefined)) + .filter((d): d is string => d !== undefined); const updatedOpts = { ...opts, @@ -107,14 +129,21 @@ export class PEX { generatePresentationSubmission, }; - const result: EvaluationResults = this._evaluationClientWrapper.evaluate(pd, wrappedPresentation.vcs, updatedOpts); - if (result.value?.descriptor_map.length) { + const allWvcs = wrappedPresentations.reduce((all, wvp) => [...all, ...wvp.vcs], [] as WrappedVerifiableCredential[]); + const result = this._evaluationClientWrapper.evaluatePresentations( + pd, + Array.isArray(presentations) ? wrappedPresentations : wrappedPresentations[0], + updatedOpts, + ); + + if (result.areRequiredCredentialsPresent !== Status.ERROR) { const selectFromClientWrapper = new EvaluationClientWrapper(); - const selectResults: SelectResults = selectFromClientWrapper.selectFrom(pd, wrappedPresentation.vcs, updatedOpts); + const selectResults: SelectResults = selectFromClientWrapper.selectFrom(pd, allWvcs, updatedOpts); if (selectResults.areRequiredCredentialsPresent !== Status.ERROR) { result.errors = []; } } + return result; } diff --git a/lib/PEXv1.ts b/lib/PEXv1.ts index ffdd76bc..6853eb8c 100644 --- a/lib/PEXv1.ts +++ b/lib/PEXv1.ts @@ -2,7 +2,7 @@ import { Format, PresentationDefinitionV1, PresentationSubmission } from '@spher import { CredentialMapper, IPresentation, OriginalVerifiableCredential, OriginalVerifiablePresentation } from '@sphereon/ssi-types'; import { PEX } from './PEX'; -import { EvaluationClientWrapper, EvaluationResults, SelectResults } from './evaluation'; +import { EvaluationClientWrapper, EvaluationResults, PresentationEvaluationResults, SelectResults } from './evaluation'; import { PresentationFromOpts, PresentationResult, PresentationSubmissionLocation } from './signing'; import { SSITypesBuilder } from './types'; import { PresentationDefinitionV1VB, Validated, ValidationEngine } from './validation'; @@ -29,7 +29,7 @@ export class PEXv1 extends PEX { restrictToFormats?: Format; restrictToDIDMethods?: string[]; }, - ): EvaluationResults { + ): PresentationEvaluationResults { SSITypesBuilder.modelEntityToInternalPresentationDefinitionV1(presentationDefinition); // only doing validation return super.evaluatePresentation(presentationDefinition, presentation, opts); } diff --git a/lib/PEXv2.ts b/lib/PEXv2.ts index d7609199..03e9d45b 100644 --- a/lib/PEXv2.ts +++ b/lib/PEXv2.ts @@ -2,7 +2,7 @@ import { Format, PresentationDefinitionV2, PresentationSubmission } from '@spher import { CredentialMapper, IPresentation, OriginalVerifiableCredential, OriginalVerifiablePresentation } from '@sphereon/ssi-types'; import { PEX } from './PEX'; -import { EvaluationClientWrapper, EvaluationResults, SelectResults } from './evaluation'; +import { EvaluationClientWrapper, EvaluationResults, PresentationEvaluationResults, SelectResults } from './evaluation'; import { PresentationFromOpts, PresentationResult, PresentationSubmissionLocation } from './signing'; import { SSITypesBuilder } from './types'; import { PresentationDefinitionV2VB, Validated, ValidationEngine } from './validation'; @@ -29,7 +29,7 @@ export class PEXv2 extends PEX { restrictToFormats?: Format; restrictToDIDMethods?: string[]; }, - ): EvaluationResults { + ): PresentationEvaluationResults { SSITypesBuilder.modelEntityInternalPresentationDefinitionV2(presentationDefinition); // only doing validation return super.evaluatePresentation(presentationDefinition, presentation, opts); } diff --git a/lib/evaluation/core/evaluationResults.ts b/lib/evaluation/core/evaluationResults.ts index 550c53c9..43b84a6d 100644 --- a/lib/evaluation/core/evaluationResults.ts +++ b/lib/evaluation/core/evaluationResults.ts @@ -1,7 +1,12 @@ import { PresentationSubmission } from '@sphereon/pex-models'; -import { IVerifiableCredential, SdJwtDecodedVerifiableCredential } from '@sphereon/ssi-types'; +import { IPresentation, IVerifiableCredential, OriginalVerifiablePresentation, SdJwtDecodedVerifiableCredential } from '@sphereon/ssi-types'; import { Checked, Status } from '../../ConstraintUtils'; +import { OrArray } from '../../types'; + +export interface PresentationEvaluationResults extends Omit { + presentation: OrArray; +} export interface EvaluationResults { /** diff --git a/lib/evaluation/evaluationClient.ts b/lib/evaluation/evaluationClient.ts index 789dd9ad..9dfa35bc 100644 --- a/lib/evaluation/evaluationClient.ts +++ b/lib/evaluation/evaluationClient.ts @@ -71,7 +71,6 @@ export class EvaluationClient { this._generatePresentationSubmission = opts?.generatePresentationSubmission !== undefined ? opts.generatePresentationSubmission : true; if (opts?.presentationSubmission) { this._presentationSubmission = opts.presentationSubmission; - // this._requirePresentationSubmission = true; } let currentHandler: EvaluationHandler | undefined = this.initEvaluationHandlers(); currentHandler?.handle(pd, wvcs); @@ -85,6 +84,12 @@ export class EvaluationClient { throw this.failed_catched; } } + + // filter the presentation submission + this.presentationSubmission = { + ...this.presentationSubmission, + descriptor_map: this.presentationSubmission.descriptor_map.filter((d) => d), + }; } public get results(): HandlerCheckResult[] { diff --git a/lib/evaluation/evaluationClientWrapper.ts b/lib/evaluation/evaluationClientWrapper.ts index 8070fa17..b5d498dd 100644 --- a/lib/evaluation/evaluationClientWrapper.ts +++ b/lib/evaluation/evaluationClientWrapper.ts @@ -1,21 +1,54 @@ import { JSONPath as jp } from '@astronautlabs/jsonpath'; -import { Descriptor, Format, InputDescriptorV1, InputDescriptorV2, PresentationSubmission, Rules, SubmissionRequirement } from '@sphereon/pex-models'; +import { Descriptor, Format, InputDescriptorV1, InputDescriptorV2, PresentationSubmission, Rules } from '@sphereon/pex-models'; +import type { SubmissionRequirement } from '@sphereon/pex-models'; import { CredentialMapper, IVerifiableCredential, OriginalVerifiableCredential, SdJwtDecodedVerifiableCredential, WrappedVerifiableCredential, + WrappedVerifiablePresentation, } from '@sphereon/ssi-types'; import { Checked, Status } from '../ConstraintUtils'; import { PresentationSubmissionLocation } from '../signing'; -import { IInternalPresentationDefinition, InternalPresentationDefinitionV2, IPresentationDefinition } from '../types'; +import { + IInternalPresentationDefinition, + InternalPresentationDefinitionV1, + InternalPresentationDefinitionV2, + IPresentationDefinition, + OrArray, +} from '../types'; import { JsonPathUtils, ObjectUtils } from '../utils'; +import { getVpFormatForVcFormat } from '../utils/formatMap'; -import { EvaluationResults, HandlerCheckResult, SelectResults, SubmissionRequirementMatch } from './core'; +import { EvaluationResults, HandlerCheckResult, PresentationEvaluationResults, SelectResults, SubmissionRequirementMatch } from './core'; import { EvaluationClient } from './evaluationClient'; +interface SubmissionSatisfiesSubmissionRequirementResult { + isSubmissionRequirementSatisfied: boolean; + totalMatches: number; + minRequiredMatches?: number; + totalRequiredMatches?: number; + maxRequiredMatches?: number; + + errors: string[]; + + nested?: SubmissionSatisfiesSubmissionRequirementResult[]; +} + +interface SubmissionSatisfiesDefinitionResult { + doesSubmissionSatisfyDefinition: boolean; + error?: string; + totalMatches: number; + totalRequiredMatches: number; + + /** + * Only populated if submission requirements are present + */ + submisisonRequirementResults?: SubmissionSatisfiesSubmissionRequirementResult[]; +} + export class EvaluationClientWrapper { private _client: EvaluationClient; @@ -325,14 +358,10 @@ export class EvaluationClientWrapper { this._client.assertPresentationSubmission(); if (this._client.presentationSubmission?.descriptor_map.length) { - const len = this._client.presentationSubmission?.descriptor_map.length; - for (let i = 0; i < len; i++) { - this._client.presentationSubmission.descriptor_map[i] && - this._client.presentationSubmission.descriptor_map.push(this._client.presentationSubmission.descriptor_map[i]); - } - this._client.presentationSubmission.descriptor_map.splice(0, len); // cut the array and leave only the non-empty values + this._client.presentationSubmission.descriptor_map = this._client.presentationSubmission.descriptor_map.filter((v) => v !== undefined); result.value = JSON.parse(JSON.stringify(this._client.presentationSubmission)); } + if (this._client.generatePresentationSubmission) { this.updatePresentationSubmissionPathToVpPath(result.value); } @@ -341,15 +370,366 @@ export class EvaluationClientWrapper { return result; } - private formatNotInfo(status: Status): Checked[] { + public evaluatePresentations( + pd: IInternalPresentationDefinition, + wvps: OrArray, + opts?: { + holderDIDs?: string[]; + limitDisclosureSignatureSuites?: string[]; + restrictToFormats?: Format; + presentationSubmission?: PresentationSubmission; + 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 + */ + presentationSubmissionLocation?: PresentationSubmissionLocation; + }, + ): PresentationEvaluationResults { + // If submission is provided as input, we match the presentations against the submission. In this case the submission MUST be valid + if (opts?.presentationSubmission) { + return this.evaluatePresentationsAgainstSubmission(pd, wvps, opts.presentationSubmission, opts); + } + + const wrappedPresentations = Array.isArray(wvps) ? wvps : [wvps]; + const allWvcs = wrappedPresentations.reduce((all, wvp) => [...all, ...wvp.vcs], [] as WrappedVerifiableCredential[]); + + const result: PresentationEvaluationResults = { + areRequiredCredentialsPresent: Status.INFO, + presentation: Array.isArray(wvps) ? wvps.map((wvp) => wvp.original) : wvps.original, + errors: [], + warnings: [], + }; + + this._client.evaluate(pd, allWvcs, opts); + result.warnings = this.formatNotInfo(Status.WARN); + result.errors = this.formatNotInfo(Status.ERROR); + + this._client.assertPresentationSubmission(); + if (this._client.presentationSubmission?.descriptor_map.length) { + this._client.presentationSubmission.descriptor_map = this._client.presentationSubmission.descriptor_map.filter((v) => v !== undefined); + result.value = JSON.parse(JSON.stringify(this._client.presentationSubmission)); + } + + const useExternalSubmission = + opts?.presentationSubmissionLocation !== undefined + ? opts.presentationSubmissionLocation === PresentationSubmissionLocation.EXTERNAL + : Array.isArray(wvps); + + if (this._client.generatePresentationSubmission && result.value && useExternalSubmission) { + // we map the descriptors of the generated submisison to take into account the nexted values + result.value.descriptor_map = result.value.descriptor_map.map((descriptor) => { + const [wvcResult] = JsonPathUtils.extractInputField(allWvcs, [descriptor.path]) as Array<{ value: WrappedVerifiableCredential }>; + if (!wvcResult) { + throw new Error(`Could not find descriptor path ${descriptor.path} in wrapped verifiable credentials`); + } + const matchingWvc = wvcResult.value; + const matchingVpIndex = wrappedPresentations.findIndex((wvp) => (wvp.vcs as WrappedVerifiableCredential[]).includes(matchingWvc)); + const matchingVp = wrappedPresentations[matchingVpIndex]; + const matcingWvcIndexInVp = matchingVp.vcs.findIndex((wvc) => wvc === matchingWvc); + + return this.updateDescriptorToExternal(descriptor, { + // We don't want to add vp index if the input to evaluate was a single presentation + vpIndex: Array.isArray(wvps) ? matchingVpIndex : undefined, + vcIndex: matcingWvcIndexInVp, + }); + }); + } else if (this._client.generatePresentationSubmission && result.value) { + this.updatePresentationSubmissionPathToVpPath(result.value); + } + + result.areRequiredCredentialsPresent = result.value?.descriptor_map?.length ? Status.INFO : Status.ERROR; + + return result; + } + + private evaluatePresentationsAgainstSubmission( + pd: IInternalPresentationDefinition, + wvps: OrArray, + submission: PresentationSubmission, + opts?: { + holderDIDs?: string[]; + limitDisclosureSignatureSuites?: string[]; + restrictToFormats?: Format; + }, + ): PresentationEvaluationResults { + const result: PresentationEvaluationResults = { + areRequiredCredentialsPresent: Status.INFO, + presentation: Array.isArray(wvps) ? wvps.map((wvp) => wvp.original) : wvps.original, + errors: [], + warnings: [], + value: submission, + }; + + // 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 vc: WrappedVerifiableCredential; + if (descriptor.path_nested) { + const [vcResult] = JsonPathUtils.extractInputField(vp.decoded, [descriptor.path_nested.path]) as Array<{ + value: string | IVerifiableCredential; + }>; + + if (!vcResult) { + 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`, + }); + continue; + } + + // 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) { + result.areRequiredCredentialsPresent = Status.ERROR; + result.errors?.push({ + status: Status.ERROR, + tag: 'SubmissionPathNotFound', + message: `Unable to find wrapped vc`, + }); + continue; + } + + vc = 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; + } + + // TODO: we should probably add support for holder dids in the kb-jwt of an SD-JWT. We can extract this from the + // `wrappedPresentation.original.compactKbJwt`, but as HAIP doesn't use dids, we'll leave it for now. + const holderDIDs = CredentialMapper.isW3cPresentation(vp.presentation) && vp.presentation.holder ? [vp.presentation.holder] : []; + + // Get the presentation definition only for this descriptor, so we can evaluate it separately + const pdForDescriptor = this.internalPresentationDefinitionForDescriptor(pd, descriptor.id); + + // Reset the client on each iteration. + this._client = new EvaluationClient(); + this._client.evaluate(pdForDescriptor, [vc], { + ...opts, + holderDIDs, + presentationSubmission: undefined, + generatePresentationSubmission: undefined, + }); + + if (this._client.presentationSubmission.descriptor_map.length !== 1) { + const submissionDescriptor = `submission.descriptor_map[${descriptorIndex}]`; + result.areRequiredCredentialsPresent = Status.ERROR; + result.errors?.push(...this.formatNotInfo(Status.ERROR, submissionDescriptor, vcPath)); + result.warnings?.push(...this.formatNotInfo(Status.WARN, submissionDescriptor, vcPath)); + } + } + + // Output submission is same as input presentation submission, it's just that if it doesn't match, we return Error. + const submissionAgainstDefinitionResult = this.validateIfSubmissionSatisfiesDefinition(pd, submission); + if (!submissionAgainstDefinitionResult.doesSubmissionSatisfyDefinition) { + result.errors?.push({ + status: Status.ERROR, + tag: 'SubmissionDoesNotSatisfyDefinition', + // TODO: it would be nice to add the nested errors here for beter understanding WHY the submission + // does not satisfy the definition, as we have that info, but we can only include one message here + message: submissionAgainstDefinitionResult.error, + }); + result.areRequiredCredentialsPresent = Status.ERROR; + } + + return result; + } + + private checkIfSubmissionSatisfiesSubmissionRequirement( + pd: IInternalPresentationDefinition, + submission: PresentationSubmission, + submissionRequirement: SubmissionRequirement, + submissionRequirementName: string, + ): SubmissionSatisfiesSubmissionRequirementResult { + if ((submissionRequirement.from && submissionRequirement.from_nested) || (!submissionRequirement.from && !submissionRequirement.from_nested)) { + return { + isSubmissionRequirementSatisfied: false, + totalMatches: 0, + errors: [ + `Either 'from' OR 'from_nested' MUST be present on submission requirement ${submissionRequirementName}, but not neither and not both`, + ], + }; + } + + const result: SubmissionSatisfiesSubmissionRequirementResult = { + isSubmissionRequirementSatisfied: false, + totalMatches: 0, + maxRequiredMatches: submissionRequirement.rule === Rules.Pick ? submissionRequirement.max : undefined, + minRequiredMatches: submissionRequirement.rule === Rules.Pick ? submissionRequirement.min : undefined, + errors: [], + }; + + // Populate from_nested requirements + if (submissionRequirement.from_nested) { + const nestedResults = submissionRequirement.from_nested.map((nestedSubmissionRequirement, index) => + this.checkIfSubmissionSatisfiesSubmissionRequirement( + pd, + submission, + nestedSubmissionRequirement, + `${submissionRequirementName}.from_nested[${index}]`, + ), + ); + + result.totalRequiredMatches = submissionRequirement.rule === Rules.All ? submissionRequirement.from_nested.length : submissionRequirement.count; + result.totalMatches = nestedResults.filter((n) => n.isSubmissionRequirementSatisfied).length; + result.nested = nestedResults; + } + + // Populate from requirements + if (submissionRequirement.from) { + const inputDescriptorsForGroup = pd.input_descriptors.filter((descriptor) => descriptor.group?.includes(submissionRequirement.from as string)); + const descriptorIdsInSubmission = submission.descriptor_map.map((descriptor) => descriptor.id); + const inputDescriptorsInSubmission = inputDescriptorsForGroup.filter((inputDescriptor) => + descriptorIdsInSubmission.includes(inputDescriptor.id), + ); + + result.totalMatches = inputDescriptorsInSubmission.length; + result.totalRequiredMatches = submissionRequirement.rule === Rules.All ? inputDescriptorsForGroup.length : submissionRequirement.count; + } + + // Validate if the min/max/count requirements are satisfied + if (result.totalRequiredMatches !== undefined && result.totalMatches !== result.totalRequiredMatches) { + result.errors.push( + `Expected ${result.totalRequiredMatches} requirements to be satisfied for submission requirement ${submissionRequirementName}, but found ${result.totalMatches}`, + ); + } + + if (result.minRequiredMatches !== undefined && result.totalMatches < result.minRequiredMatches) { + result.errors.push( + `Expected at least ${result.minRequiredMatches} requirements to be satisfied from submission requirement ${submissionRequirementName}, but found ${result.totalMatches}`, + ); + } + + if (result.maxRequiredMatches !== undefined && result.totalMatches > result.maxRequiredMatches) { + result.errors.push( + `Expected at most ${result.maxRequiredMatches} requirements to be satisfied from submission requirement ${submissionRequirementName}, but found ${result.totalMatches}`, + ); + } + + result.isSubmissionRequirementSatisfied = result.errors.length === 0; + return result; + } + + /** + * Checks whether a submission satisfies the requirements of a presentation definition + */ + private validateIfSubmissionSatisfiesDefinition( + pd: IInternalPresentationDefinition, + submission: PresentationSubmission, + ): SubmissionSatisfiesDefinitionResult { + const submissionDescriptorIds = submission.descriptor_map.map((descriptor) => descriptor.id); + + const result: SubmissionSatisfiesDefinitionResult = { + doesSubmissionSatisfyDefinition: false, + totalMatches: 0, + totalRequiredMatches: 0, + }; + + // All MUST match + if (pd.submission_requirements) { + const submissionRequirementResults = pd.submission_requirements.map((submissionRequirement, index) => + this.checkIfSubmissionSatisfiesSubmissionRequirement(pd, submission, submissionRequirement, `$.submission_requirements[${index}]`), + ); + + result.totalRequiredMatches = pd.submission_requirements.length; + result.totalMatches = submissionRequirementResults.filter((r) => r.isSubmissionRequirementSatisfied).length; + result.submisisonRequirementResults = submissionRequirementResults; + + if (result.totalMatches !== result.totalRequiredMatches) { + result.error = `Expected all submission requirements (${result.totalRequiredMatches}) to be satisfifed in submission, but found ${result.totalMatches}.`; + } + } else { + result.totalRequiredMatches = pd.input_descriptors.length; + result.totalMatches = submissionDescriptorIds.length; + const notInSubmission = pd.input_descriptors.filter((inputDescriptor) => !submissionDescriptorIds.includes(inputDescriptor.id)); + + if (notInSubmission.length > 0) { + result.error = `Expected all input descriptors (${pd.input_descriptors.length}) to be satisfifed in submission, but found ${submissionDescriptorIds.length}. Missing ${notInSubmission.map((d) => d.id).join(', ')}`; + } + } + + result.doesSubmissionSatisfyDefinition = result.error === undefined; + return result; + } + + private internalPresentationDefinitionForDescriptor(pd: IInternalPresentationDefinition, descriptorId: string): IInternalPresentationDefinition { + if (pd instanceof InternalPresentationDefinitionV2) { + const inputDescriptorIndex = pd.input_descriptors.findIndex((i) => i.id === descriptorId); + return new InternalPresentationDefinitionV2( + pd.id, + [pd.input_descriptors[inputDescriptorIndex]], + pd.format, + pd.frame, + pd.name, + pd.purpose, + // we ignore submission requirements as we're verifying a single input descriptor here + undefined, + ); + } else if (pd instanceof InternalPresentationDefinitionV1) { + const inputDescriptorIndex = pd.input_descriptors.findIndex((i) => i.id === descriptorId); + return new InternalPresentationDefinitionV1( + pd.id, + [pd.input_descriptors[inputDescriptorIndex]], + pd.format, + pd.name, + pd.purpose, + // we ignore submission requirements as we're verifying a single input descriptor here + undefined, + ); + } + + throw new Error('Unrecognized presentation definition instance'); + } + + private formatNotInfo(status: Status, descriptorPath?: string, vcPath?: string): Checked[] { return this._client.results .filter((result) => result.status === status) .map((x) => { - const vcPath = x.verifiable_credential_path.substring(1); + const _vcPath = vcPath ?? `$.verifiableCredential${x.verifiable_credential_path.substring(1)}`; + const _descriptorPath = descriptorPath ?? x.input_descriptor_path; return { tag: x.evaluator, status: x.status, - message: `${x.message}: ${x.input_descriptor_path}: $.verifiableCredential${vcPath}`, + message: `${x.message}: ${_descriptorPath}: ${_vcPath}`, }; }); } @@ -425,35 +805,53 @@ export class EvaluationClientWrapper { }); } - private updatePresentationSubmissionToExternal() { - const descriptors = this._client.presentationSubmission.descriptor_map; - this._client.presentationSubmission.descriptor_map = descriptors.map((descriptor) => { - if (descriptor.path_nested) { - return descriptor; - } - // sd-jwt doesn't use path_nested and format is the same for vc or vp - else if (descriptor.format === 'vc+sd-jwt') { - return descriptor; - } + private updatePresentationSubmissionToExternal(presentationSubmission?: PresentationSubmission): PresentationSubmission { + const descriptors = presentationSubmission?.descriptor_map ?? this._client.presentationSubmission.descriptor_map; + const updatedDescriptors = descriptors.map((d) => this.updateDescriptorToExternal(d)); - const format = descriptor.format; - const nestedDescriptor = { ...descriptor }; - nestedDescriptor.path_nested = { ...descriptor }; - nestedDescriptor.path = '$'; - // todo: We really should also look at the context of the VP, to determine whether it is jwt_vp vs jwt_vp_json instead of relying on the VC type - if (format.startsWith('ldp_')) { - nestedDescriptor.format = 'ldp_vp'; - } else if (format.startsWith('di_')) { - nestedDescriptor.format = 'di_vp'; - } else if (format === 'jwt_vc') { - nestedDescriptor.format = 'jwt_vp'; - nestedDescriptor.path_nested.path = nestedDescriptor.path_nested.path.replace('$.verifiableCredential[', '$.vp.verifiableCredential['); - } else if (format === 'jwt_vc_json') { - nestedDescriptor.format = 'jwt_vp_json'; - nestedDescriptor.path_nested.path = nestedDescriptor.path_nested.path.replace('$.verifiableCredential[', '$.vp.verifiableCredential['); - } - return nestedDescriptor; - }); + if (presentationSubmission) { + return { + ...presentationSubmission, + descriptor_map: updatedDescriptors, + }; + } + + this._client.presentationSubmission.descriptor_map = updatedDescriptors; + return this._client.presentationSubmission; + } + + private updateDescriptorToExternal( + descriptor: Descriptor, + { + vpIndex, + vcIndex, + }: { + /* index of the vp. if not provided $ will be used */ + vpIndex?: number; + /* index of the vc in the vp. if not provided, the current index on the descriptor will be used */ + vcIndex?: number; + } = {}, + ) { + if (descriptor.path_nested) { + return descriptor; + } + + const { nestedCredentialPath, vpFormat } = getVpFormatForVcFormat(descriptor.format); + + const newDescriptor = { + ...descriptor, + format: vpFormat, + path: vpIndex !== undefined ? `$[${vpIndex}]` : '$', + }; + + if (nestedCredentialPath) { + newDescriptor.path_nested = { + ...descriptor, + path: vcIndex !== undefined ? `${nestedCredentialPath}[${vcIndex}]` : descriptor.path.replace('$.', nestedCredentialPath), + }; + } + + return newDescriptor; } private matchUserSelectedVcs(marked: HandlerCheckResult[], vcs: WrappedVerifiableCredential[]): [HandlerCheckResult[], [string, string][]] { diff --git a/lib/evaluation/handlers/subjectIsIssuerEvaluationHandler.ts b/lib/evaluation/handlers/subjectIsIssuerEvaluationHandler.ts index ef1d17e7..e7448df2 100644 --- a/lib/evaluation/handlers/subjectIsIssuerEvaluationHandler.ts +++ b/lib/evaluation/handlers/subjectIsIssuerEvaluationHandler.ts @@ -1,5 +1,5 @@ import { ConstraintsV1, ConstraintsV2, Optionality } from '@sphereon/pex-models'; -import { CredentialMapper, IVerifiableCredential, WrappedVerifiableCredential } from '@sphereon/ssi-types'; +import { CredentialMapper, IVerifiableCredential, SdJwtDecodedVerifiableCredential, WrappedVerifiableCredential } from '@sphereon/ssi-types'; import { Status } from '../../ConstraintUtils'; import { IInternalPresentationDefinition, InternalPresentationDefinitionV2, PathComponent } from '../../types'; @@ -37,10 +37,11 @@ export class SubjectIsIssuerEvaluationHandler extends AbstractEvaluationHandler private checkSubjectIsIssuer(inputDescriptorId: string, wrappedVcs: WrappedVerifiableCredential[], idIdx: number): void { this.client.presentationSubmission.descriptor_map.forEach((currentDescriptor) => { if (currentDescriptor.id === inputDescriptorId) { - const mappings: { path: PathComponent[]; value: IVerifiableCredential }[] = JsonPathUtils.extractInputField( + const mappings = JsonPathUtils.extractInputField( wrappedVcs.map((wvc) => wvc.credential), [currentDescriptor.path], - ) as { path: PathComponent[]; value: IVerifiableCredential }[]; + ) as { path: PathComponent[]; value: IVerifiableCredential | SdJwtDecodedVerifiableCredential }[]; + for (const mapping of mappings) { const issuer = getIssuerString(mapping.value); if (mapping && mapping.value && getSubjectIdsAsString(mapping.value).every((item) => item === issuer)) { diff --git a/lib/evaluation/index.ts b/lib/evaluation/index.ts index 038e43aa..58e5fa15 100644 --- a/lib/evaluation/index.ts +++ b/lib/evaluation/index.ts @@ -1,9 +1,9 @@ -import { EvaluationResults, HandlerCheckResult, SelectResults, SubmissionRequirementMatch } from './core'; +import { EvaluationResults, HandlerCheckResult, PresentationEvaluationResults, SelectResults, SubmissionRequirementMatch } from './core'; import { EvaluationClient } from './evaluationClient'; import { EvaluationHandler } from './handlers'; export { EvaluationClient }; export { SubmissionRequirementMatch, SelectResults }; export { EvaluationHandler }; export { EvaluationClientWrapper } from './evaluationClientWrapper'; -export { EvaluationResults }; +export { EvaluationResults, PresentationEvaluationResults }; export { HandlerCheckResult }; diff --git a/lib/signing/types.ts b/lib/signing/types.ts index 426c5ac7..b2f84c12 100644 --- a/lib/signing/types.ts +++ b/lib/signing/types.ts @@ -10,7 +10,7 @@ import { W3CVerifiablePresentation, } from '@sphereon/ssi-types'; -import { EvaluationResults } from '../evaluation'; +import { PresentationEvaluationResults } from '../evaluation'; export interface ProofOptions { /** @@ -213,7 +213,7 @@ export interface PresentationSignCallBackParams { /** * The evaluation results, which the callback function could use to create a VP using the proof(s) using the supplied credentials */ - evaluationResults: EvaluationResults; + evaluationResults: PresentationEvaluationResults; } export enum KeyEncoding { diff --git a/lib/types/Internal.types.ts b/lib/types/Internal.types.ts index 2818d261..77514796 100644 --- a/lib/types/Internal.types.ts +++ b/lib/types/Internal.types.ts @@ -15,6 +15,7 @@ export interface IInternalPresentationDefinition { name?: string; purpose?: string; submission_requirements?: Array; + input_descriptors: Array<{ id: string; group?: string[] }>; getVersion(): PEVersion; } @@ -103,3 +104,5 @@ export enum PEVersion { v1 = 'v1', v2 = 'v2', } + +export type OrArray = T | Array; diff --git a/lib/utils/VCUtils.ts b/lib/utils/VCUtils.ts index 3a47a755..0e29cf15 100644 --- a/lib/utils/VCUtils.ts +++ b/lib/utils/VCUtils.ts @@ -1,4 +1,4 @@ -import { AdditionalClaims, ICredential, ICredentialSubject, IIssuer } from '@sphereon/ssi-types'; +import { AdditionalClaims, CredentialMapper, ICredential, ICredentialSubject, IIssuer, SdJwtDecodedVerifiableCredential } from '@sphereon/ssi-types'; import { DiscoveredVersion, IPresentationDefinition, PEVersion } from '../types'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -11,12 +11,21 @@ import validatePDv2 from '../validation/validatePDv2.js'; import { ObjectUtils } from './ObjectUtils'; import { JsonPathUtils } from './jsonPathUtils'; -export function getSubjectIdsAsString(vc: ICredential): string[] { +export function getSubjectIdsAsString(vc: ICredential | SdJwtDecodedVerifiableCredential): string[] { + if (CredentialMapper.isSdJwtDecodedCredential(vc)) { + // TODO: should we also handle `cnf` claim? + return vc.signedPayload.sub ? [vc.signedPayload.sub] : []; + } + const subjects: (ICredentialSubject & AdditionalClaims)[] = Array.isArray(vc.credentialSubject) ? vc.credentialSubject : [vc.credentialSubject]; return subjects.filter((s) => !!s.id).map((value) => value.id) as string[]; } -export function getIssuerString(vc: ICredential): string { +export function getIssuerString(vc: ICredential | SdJwtDecodedVerifiableCredential): string { + if (CredentialMapper.isSdJwtDecodedCredential(vc)) { + return vc.signedPayload.iss; + } + return ObjectUtils.isString(vc.issuer) ? (vc.issuer as string) : (vc.issuer as IIssuer).id; } diff --git a/lib/utils/formatMap.ts b/lib/utils/formatMap.ts new file mode 100644 index 00000000..de7e3824 --- /dev/null +++ b/lib/utils/formatMap.ts @@ -0,0 +1,27 @@ +export function getVpFormatForVcFormat(vcFormat: string) { + if (vcFormat in vcVpFormatMap) { + const vpFormat = vcVpFormatMap[vcFormat as keyof typeof vcVpFormatMap]; + + let nestedCredentialPath: string | undefined = undefined; + if (vpFormat === 'di_vp' || vpFormat === 'ldp_vp') { + nestedCredentialPath = '$.verifiableCredential'; + } else if (vpFormat === 'jwt_vp' || vpFormat === 'jwt_vp_json') { + nestedCredentialPath = '$.vp.verifiableCredential'; + } + + return { + vpFormat, + nestedCredentialPath, + }; + } + + throw new Error(`Unrecognized vc format ${vcFormat}`); +} + +const vcVpFormatMap = { + di_vc: 'di_vp', + jwt_vc_json: 'jwt_vp_json', + ldp_vc: 'ldp_vp', + jwt_vc: 'jwt_vp', + 'vc+sd-jwt': 'vc+sd-jwt', +} as const; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 58536e03..9a9e502f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,8 +21,8 @@ dependencies: specifier: ^2.2.2 version: 2.2.2 '@sphereon/ssi-types': - specifier: 0.19.0 - version: 0.19.0 + specifier: 0.22.0 + version: 0.22.0 ajv: specifier: ^8.12.0 version: 8.12.0 @@ -823,13 +823,6 @@ packages: dev: true optional: true - /@sd-jwt/decode@0.2.1: - resolution: {integrity: sha512-rs55WB3llrMObxN8jeMl06km/h0WivO9jSWNubO9JUIdlfrVhssU38xoXakvQeSDjAJkUUhfZcvmC2vNo1X6Wg==} - dependencies: - '@sd-jwt/types': 0.2.1 - '@sd-jwt/utils': 0.2.1 - dev: false - /@sd-jwt/decode@0.6.1: resolution: {integrity: sha512-QgTIoYd5zyKKLgXB4xEYJTrvumVwtsj5Dog0v0L9UH9ZvHekDaeexS247X7A4iSdzTvmZzUpGskgABOa4D8NmQ==} engines: {node: '>=16'} @@ -847,22 +840,11 @@ packages: '@sd-jwt/utils': 0.6.1 dev: false - /@sd-jwt/types@0.2.1: - resolution: {integrity: sha512-nbNik/cq6UIMsN144FcgPZQzaqIsjEEj307j3ZSFORkQBR4Tsmcj54aswTuNh0Z0z/4aSbfw14vOKBZvRWyVLQ==} - dev: false - /@sd-jwt/types@0.6.1: resolution: {integrity: sha512-LKpABZJGT77jNhOLvAHIkNNmGqXzyfwBT+6r+DN9zNzMx1CzuNR0qXk1GMUbast9iCfPkGbnEpUv/jHTBvlIvg==} engines: {node: '>=16'} dev: false - /@sd-jwt/utils@0.2.1: - resolution: {integrity: sha512-9eRrge44dhE3fenawR/RZGxP5iuW9DtgdOVANu/JK5PEl80r0fDsMwm/gDjuv8OgLDCmQ6uSaVte1lYaTG71bQ==} - dependencies: - '@sd-jwt/types': 0.2.1 - buffer: 6.0.3 - dev: false - /@sd-jwt/utils@0.6.1: resolution: {integrity: sha512-1NHZ//+GecGQJb+gSdDicnrHG0DvACUk9jTnXA5yLZhlRjgkjyfJLNsCZesYeCyVp/SiyvIC9B+JwoY4kI0TwQ==} engines: {node: '>=16'} @@ -891,10 +873,10 @@ packages: resolution: {integrity: sha512-CZIsBoaV5rMZEWYBsmH+RxsdoxpXf5FSDwDz0GB0qOf5WFk1BGUnzpZzi5yJ+2L151mhPk97dlRc9Wb01Awr4Q==} dev: false - /@sphereon/ssi-types@0.19.0: - resolution: {integrity: sha512-C4NW4a9rhnEApkQvMYQx3GFboyZDwS0C0Ec6vVRuhFp7AZU4EBMBZsfP3wXUjoBHBdTF4ru/SdriCs7XvN5wIg==} + /@sphereon/ssi-types@0.22.0: + resolution: {integrity: sha512-YPJAZlKmzNALXK8ohP3ETxj1oVzL4+M9ljj3fD5xrbacvYax1JPCVKc8BWSubGcQckKHPbgbpcS7LYEeghyT9Q==} dependencies: - '@sd-jwt/decode': 0.2.1 + '@sd-jwt/decode': 0.6.1 jwt-decode: 3.1.2 dev: false @@ -1443,10 +1425,6 @@ packages: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: true - /base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - dev: false - /brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} dependencies: @@ -1495,13 +1473,6 @@ packages: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} dev: true - /buffer@6.0.3: - resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - dev: false - /bundle-name@4.1.0: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} @@ -2593,6 +2564,7 @@ packages: /ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + dev: true /ignore-walk@3.0.4: resolution: {integrity: sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ==} diff --git a/test/PEX.spec.ts b/test/PEX.spec.ts index a9709c95..5fab4578 100644 --- a/test/PEX.spec.ts +++ b/test/PEX.spec.ts @@ -1,6 +1,6 @@ import fs from 'fs'; -import { PresentationDefinitionV1, PresentationDefinitionV2 } from '@sphereon/pex-models'; +import { PresentationDefinitionV1, PresentationDefinitionV2, PresentationSubmission } from '@sphereon/pex-models'; import { ICredential, ICredentialSubject, @@ -12,10 +12,12 @@ import { WrappedW3CVerifiableCredential, } from '@sphereon/ssi-types'; -import { EvaluationResults, PEX, Validated } from '../lib'; -import { VerifiablePresentationResult } from '../lib/signing/types'; +import { PEX, Status, Validated } from '../lib'; +import { PresentationEvaluationResults } from '../lib/evaluation/core'; +import { PresentationSubmissionLocation, VerifiablePresentationResult } from '../lib/signing/types'; import { SSITypesBuilder } from '../lib/types'; +import { hasher } from './SdJwt.spec'; import { assertedMockCallback, assertedMockCallbackWithoutProofType, @@ -127,6 +129,524 @@ describe('evaluate', () => { expect(evaluationResults!.errors!.length).toEqual(0); }); + it('Evaluate case multiple presentations multiple formats submission generated without any error 1', () => { + const vps = [ + { + '@context': ['https://www.w3.org/2018/credentials/v1'], + type: ['VerifiablePresentation'], + verifiableCredential: [getFileAsJson('./test/dif_pe_examples/vc/vc-PermanentResidentCard.json')], + }, + { + '@context': ['https://www.w3.org/2018/credentials/v1'], + type: ['VerifiablePresentation'], + verifiableCredential: [getFileAsJson('./test/dif_pe_examples/vc/vc-driverLicense.json')], + }, + getFile('./test/dif_pe_examples/vp/vp_universityDegree.jwt'), + getFile('./test/dif_pe_examples/vp/vp_state-business-license.sd-jwt'), + ]; + + const presentationDefinition: PresentationDefinitionV2 = getFileAsJson( + './test/dif_pe_examples/pdV2/pd-multi-formats-multi-vp.json', + ).presentation_definition; + + const pex: PEX = new PEX({ hasher }); + const evaluationResults = pex.evaluatePresentation(presentationDefinition, vps, { + limitDisclosureSignatureSuites: LIMIT_DISCLOSURE_SIGNATURE_SUITES, + }); + + // Should correctly generate the presentation submission with nested values + expect(evaluationResults).toEqual({ + areRequiredCredentialsPresent: Status.INFO, + presentation: vps, + errors: [], + warnings: [], + value: { + id: expect.any(String), + definition_id: '31e2f0f1-6b70-411d-b239-56aed5321884', + descriptor_map: [ + { + id: '867bfe7a-5b91-46b2-9ba4-70028b8d9cc8', + format: 'ldp_vp', + path: '$[0]', + path_nested: { + id: '867bfe7a-5b91-46b2-9ba4-70028b8d9cc8', + format: 'ldp_vc', + path: '$.verifiableCredential[0]', + }, + }, + { + id: 'f09f7000-6bf2-4239-8e8d-13014e681eba', + format: 'ldp_vp', + path: '$[1]', + path_nested: { + id: 'f09f7000-6bf2-4239-8e8d-13014e681eba', + format: 'ldp_vc', + path: '$.verifiableCredential[0]', + }, + }, + { + id: 'ddc4a62f-73d4-4410-a3d7-b20720a113ed', + format: 'vc+sd-jwt', + path: '$[3]', + }, + { + id: 'e0556bbf-d1c0-48d8-8564-09f6e07bac9b', + format: 'jwt_vp', + path: '$[2]', + path_nested: { + id: 'e0556bbf-d1c0-48d8-8564-09f6e07bac9b', + format: 'jwt_vc', + path: '$.vp.verifiableCredential[0]', + }, + }, + ], + }, + }); + }); + + it('Evaluate case multiple presentations multiple formats submission provided without any error 1', () => { + const vps = [ + { + '@context': ['https://www.w3.org/2018/credentials/v1'], + type: ['VerifiablePresentation'], + verifiableCredential: [getFileAsJson('./test/dif_pe_examples/vc/vc-PermanentResidentCard.json')], + }, + { + '@context': ['https://www.w3.org/2018/credentials/v1'], + type: ['VerifiablePresentation'], + verifiableCredential: [getFileAsJson('./test/dif_pe_examples/vc/vc-driverLicense.json')], + }, + getFile('./test/dif_pe_examples/vp/vp_universityDegree.jwt'), + getFile('./test/dif_pe_examples/vp/vp_state-business-license.sd-jwt'), + ]; + + const presentationDefinition: PresentationDefinitionV2 = getFileAsJson( + './test/dif_pe_examples/pdV2/pd-multi-formats-multi-vp.json', + ).presentation_definition; + + const submission: PresentationSubmission = { + id: 'fbc551d2-9ca7-4e87-a553-790e93eb13fb', + definition_id: '31e2f0f1-6b70-411d-b239-56aed5321884', + descriptor_map: [ + { + id: '867bfe7a-5b91-46b2-9ba4-70028b8d9cc8', + format: 'ldp_vp', + path: '$[0]', + path_nested: { + id: '867bfe7a-5b91-46b2-9ba4-70028b8d9cc8', + format: 'ldp_vc', + path: '$.verifiableCredential[0]', + }, + }, + { + id: 'f09f7000-6bf2-4239-8e8d-13014e681eba', + format: 'ldp_vp', + path: '$[1]', + path_nested: { + id: 'f09f7000-6bf2-4239-8e8d-13014e681eba', + format: 'ldp_vc', + path: '$.verifiableCredential[0]', + }, + }, + { + id: 'ddc4a62f-73d4-4410-a3d7-b20720a113ed', + format: 'vc+sd-jwt', + path: '$[3]', + }, + { + id: 'e0556bbf-d1c0-48d8-8564-09f6e07bac9b', + format: 'jwt_vp', + path: '$[2]', + path_nested: { + id: 'e0556bbf-d1c0-48d8-8564-09f6e07bac9b', + format: 'jwt_vc', + path: '$.vp.verifiableCredential[0]', + }, + }, + ], + }; + + const pex: PEX = new PEX({ hasher }); + const evaluationResults = pex.evaluatePresentation(presentationDefinition, vps, { + limitDisclosureSignatureSuites: LIMIT_DISCLOSURE_SIGNATURE_SUITES, + presentationSubmission: JSON.parse(JSON.stringify(submission)), + }); + + expect(evaluationResults).toEqual({ + areRequiredCredentialsPresent: Status.INFO, + presentation: vps, + errors: [], + warnings: [], + // Should return the same presentation submission if provided + value: submission, + }); + }); + + it('Evaluate case multiple presentations with matching input descriptor multiple formats submission generated without any error 1', () => { + const vps = [ + { + '@context': ['https://www.w3.org/2018/credentials/v1'], + type: ['VerifiablePresentation'], + verifiableCredential: [ + getFileAsJson('./test/dif_pe_examples/vc/vc-driverLicense.json'), + getFileAsJson('./test/dif_pe_examples/vc/vc-PermanentResidentCard.json'), + ], + }, + { + '@context': ['https://www.w3.org/2018/credentials/v1'], + type: ['VerifiablePresentation'], + verifiableCredential: [ + getFileAsJson('./test/dif_pe_examples/vc/vc-PermanentResidentCard.json'), + getFileAsJson('./test/dif_pe_examples/vc/vc-driverLicense.json'), + ], + }, + getFile('./test/dif_pe_examples/vp/vp_universityDegree.jwt'), + getFile('./test/dif_pe_examples/vp/vp_state-business-license.sd-jwt'), + ]; + + const presentationDefinition: PresentationDefinitionV2 = getFileAsJson( + './test/dif_pe_examples/pdV2/pd-multi-formats-multi-vp.json', + ).presentation_definition; + + const pex: PEX = new PEX({ hasher }); + const evaluationResults = pex.evaluatePresentation(presentationDefinition, vps, { + limitDisclosureSignatureSuites: LIMIT_DISCLOSURE_SIGNATURE_SUITES, + }); + + expect(evaluationResults).toEqual({ + areRequiredCredentialsPresent: Status.INFO, + presentation: vps, + errors: [], + warnings: [], + // Should return the same presentation submission if provided + value: { + id: expect.any(String), + definition_id: '31e2f0f1-6b70-411d-b239-56aed5321884', + descriptor_map: [ + { + format: 'ldp_vp', + id: '867bfe7a-5b91-46b2-9ba4-70028b8d9cc8', + path: '$[0]', + path_nested: { + format: 'ldp_vc', + id: '867bfe7a-5b91-46b2-9ba4-70028b8d9cc8', + path: '$.verifiableCredential[1]', + }, + }, + { + format: 'ldp_vp', + id: '867bfe7a-5b91-46b2-9ba4-70028b8d9cc8', + path: '$[1]', + path_nested: { + format: 'ldp_vc', + id: '867bfe7a-5b91-46b2-9ba4-70028b8d9cc8', + path: '$.verifiableCredential[0]', + }, + }, + { + format: 'ldp_vp', + id: 'f09f7000-6bf2-4239-8e8d-13014e681eba', + path: '$[0]', + path_nested: { + format: 'ldp_vc', + id: 'f09f7000-6bf2-4239-8e8d-13014e681eba', + path: '$.verifiableCredential[0]', + }, + }, + { + format: 'ldp_vp', + id: 'f09f7000-6bf2-4239-8e8d-13014e681eba', + path: '$[1]', + path_nested: { + format: 'ldp_vc', + id: 'f09f7000-6bf2-4239-8e8d-13014e681eba', + path: '$.verifiableCredential[1]', + }, + }, + { + format: 'vc+sd-jwt', + id: 'ddc4a62f-73d4-4410-a3d7-b20720a113ed', + path: '$[3]', + }, + { + format: 'jwt_vp', + id: 'e0556bbf-d1c0-48d8-8564-09f6e07bac9b', + path: '$[2]', + path_nested: { + format: 'jwt_vc', + id: 'e0556bbf-d1c0-48d8-8564-09f6e07bac9b', + path: '$.vp.verifiableCredential[0]', + }, + }, + ], + }, + }); + }); + + it('Evaluate case multiple presentations with matching input descriptor multiple formats invalid submission provided with error 1', () => { + const vps = [ + { + '@context': ['https://www.w3.org/2018/credentials/v1'], + type: ['VerifiablePresentation'], + verifiableCredential: [ + getFileAsJson('./test/dif_pe_examples/vc/vc-driverLicense.json'), + getFileAsJson('./test/dif_pe_examples/vc/vc-PermanentResidentCard.json'), + ], + }, + { + '@context': ['https://www.w3.org/2018/credentials/v1'], + type: ['VerifiablePresentation'], + verifiableCredential: [ + getFileAsJson('./test/dif_pe_examples/vc/vc-PermanentResidentCard.json'), + getFileAsJson('./test/dif_pe_examples/vc/vc-driverLicense.json'), + ], + }, + getFile('./test/dif_pe_examples/vp/vp_universityDegree.jwt'), + getFile('./test/dif_pe_examples/vp/vp_state-business-license.sd-jwt'), + ]; + + const presentationDefinition: PresentationDefinitionV2 = getFileAsJson( + './test/dif_pe_examples/pdV2/pd-multi-formats-multi-vp.json', + ).presentation_definition; + + const submission: PresentationSubmission = { + id: '984129ed-32a9-4e5e-ae7a-f83a75a49c5b', + definition_id: '31e2f0f1-6b70-411d-b239-56aed5321884', + descriptor_map: [ + { + id: '867bfe7a-5b91-46b2-9ba4-70028b8d9cc8', + format: 'ldp_vp', + path: '$[0]', + path_nested: { + id: '867bfe7a-5b91-46b2-9ba4-70028b8d9cc8', + format: 'ldp_vc', + path: '$.verifiableCredential[0]', + }, + }, + { + id: 'f09f7000-6bf2-4239-8e8d-13014e681eba', + format: 'ldp_vp', + path: '$[1]', + path_nested: { + id: 'f09f7000-6bf2-4239-8e8d-13014e681eba', + format: 'ldp_vc', + path: '$.verifiableCredential[0]', + }, + }, + { + id: 'ddc4a62f-73d4-4410-a3d7-b20720a113ed', + format: 'vc+sd-jwt', + path: '$[3]', + }, + { + id: 'e0556bbf-d1c0-48d8-8564-09f6e07bac9b', + format: 'jwt_vp', + path: '$[2]', + path_nested: { + id: 'e0556bbf-d1c0-48d8-8564-09f6e07bac9b', + format: 'jwt_vc', + path: '$.vp.verifiableCredential[0]', + }, + }, + ], + }; + + const pex: PEX = new PEX({ hasher }); + const evaluationResults = pex.evaluatePresentation(presentationDefinition, vps, { + limitDisclosureSignatureSuites: LIMIT_DISCLOSURE_SIGNATURE_SUITES, + presentationSubmission: submission, + }); + + expect(evaluationResults).toEqual({ + areRequiredCredentialsPresent: Status.ERROR, + presentation: vps, + errors: [ + { + message: + 'Input candidate does not contain property: submission.descriptor_map[0]: presentation $[0] with nested credential $.verifiableCredential[0]', + status: 'error', + tag: 'FilterEvaluation', + }, + { + message: + 'The input candidate is not eligible for submission: submission.descriptor_map[0]: presentation $[0] with nested credential $.verifiableCredential[0]', + status: 'error', + tag: 'MarkForSubmissionEvaluation', + }, + { + message: + 'Input candidate does not contain property: submission.descriptor_map[1]: presentation $[1] with nested credential $.verifiableCredential[0]', + status: 'error', + tag: 'FilterEvaluation', + }, + { + message: + 'The input candidate is not eligible for submission: submission.descriptor_map[1]: presentation $[1] with nested credential $.verifiableCredential[0]', + status: 'error', + tag: 'MarkForSubmissionEvaluation', + }, + ], + warnings: [], + value: submission, + }); + }); + + it('Evaluate case multiple presentations multiple formats submission provided not satisfying definition no submission_requirements with error 1', () => { + const vps = [ + { + '@context': ['https://www.w3.org/2018/credentials/v1'], + type: ['VerifiablePresentation'], + verifiableCredential: [getFileAsJson('./test/dif_pe_examples/vc/vc-PermanentResidentCard.json')], + }, + { + '@context': ['https://www.w3.org/2018/credentials/v1'], + type: ['VerifiablePresentation'], + verifiableCredential: [getFileAsJson('./test/dif_pe_examples/vc/vc-driverLicense.json')], + }, + getFile('./test/dif_pe_examples/vp/vp_universityDegree.jwt'), + ]; + + const presentationDefinition: PresentationDefinitionV2 = getFileAsJson( + './test/dif_pe_examples/pdV2/pd-multi-formats-multi-vp.json', + ).presentation_definition; + + const submission: PresentationSubmission = { + id: 'fbc551d2-9ca7-4e87-a553-790e93eb13fb', + definition_id: '31e2f0f1-6b70-411d-b239-56aed5321884', + descriptor_map: [ + { + id: '867bfe7a-5b91-46b2-9ba4-70028b8d9cc8', + format: 'ldp_vp', + path: '$[0]', + path_nested: { + id: '867bfe7a-5b91-46b2-9ba4-70028b8d9cc8', + format: 'ldp_vc', + path: '$.verifiableCredential[0]', + }, + }, + { + id: 'f09f7000-6bf2-4239-8e8d-13014e681eba', + format: 'ldp_vp', + path: '$[1]', + path_nested: { + id: 'f09f7000-6bf2-4239-8e8d-13014e681eba', + format: 'ldp_vc', + path: '$.verifiableCredential[0]', + }, + }, + { + id: 'e0556bbf-d1c0-48d8-8564-09f6e07bac9b', + format: 'jwt_vp', + path: '$[2]', + path_nested: { + id: 'e0556bbf-d1c0-48d8-8564-09f6e07bac9b', + format: 'jwt_vc', + path: '$.vp.verifiableCredential[0]', + }, + }, + ], + }; + + const pex: PEX = new PEX({ hasher }); + const evaluationResults = pex.evaluatePresentation(presentationDefinition, vps, { + limitDisclosureSignatureSuites: LIMIT_DISCLOSURE_SIGNATURE_SUITES, + presentationSubmission: JSON.parse(JSON.stringify(submission)), + }); + + expect(evaluationResults).toEqual({ + areRequiredCredentialsPresent: Status.ERROR, + presentation: vps, + errors: [ + { + message: 'Expected all input descriptors (4) to be satisfifed in submission, but found 3. Missing ddc4a62f-73d4-4410-a3d7-b20720a113ed', + status: 'error', + tag: 'SubmissionDoesNotSatisfyDefinition', + }, + ], + warnings: [], + // Should return the same presentation submission if provided + value: submission, + }); + }); + + it('Evaluate case multiple presentations multiple formats submission provided not satisfying definition with submission_requirements with error 1', () => { + const vps = [ + { + '@context': ['https://www.w3.org/2018/credentials/v1'], + type: ['VerifiablePresentation'], + verifiableCredential: [getFileAsJson('./test/dif_pe_examples/vc/vc-PermanentResidentCard.json')], + }, + { + '@context': ['https://www.w3.org/2018/credentials/v1'], + type: ['VerifiablePresentation'], + verifiableCredential: [getFileAsJson('./test/dif_pe_examples/vc/vc-driverLicense.json')], + }, + getFile('./test/dif_pe_examples/vp/vp_universityDegree.jwt'), + ]; + + const presentationDefinition: PresentationDefinitionV2 = getFileAsJson( + './test/dif_pe_examples/pdV2/pd-multi-formats-multi-vp-submission-requirements.json', + ).presentation_definition; + + const submission: PresentationSubmission = { + id: 'fbc551d2-9ca7-4e87-a553-790e93eb13fb', + definition_id: '31e2f0f1-6b70-411d-b239-56aed5321884', + descriptor_map: [ + { + id: '867bfe7a-5b91-46b2-9ba4-70028b8d9cc8', + format: 'ldp_vp', + path: '$[0]', + path_nested: { + id: '867bfe7a-5b91-46b2-9ba4-70028b8d9cc8', + format: 'ldp_vc', + path: '$.verifiableCredential[0]', + }, + }, + { + id: 'f09f7000-6bf2-4239-8e8d-13014e681eba', + format: 'ldp_vp', + path: '$[1]', + path_nested: { + id: 'f09f7000-6bf2-4239-8e8d-13014e681eba', + format: 'ldp_vc', + path: '$.verifiableCredential[0]', + }, + }, + { + id: 'e0556bbf-d1c0-48d8-8564-09f6e07bac9b', + format: 'jwt_vp', + path: '$[2]', + path_nested: { + id: 'e0556bbf-d1c0-48d8-8564-09f6e07bac9b', + format: 'jwt_vc', + path: '$.vp.verifiableCredential[0]', + }, + }, + ], + }; + + const pex: PEX = new PEX({ hasher }); + const evaluationResults = pex.evaluatePresentation(presentationDefinition, vps, { + limitDisclosureSignatureSuites: LIMIT_DISCLOSURE_SIGNATURE_SUITES, + presentationSubmission: JSON.parse(JSON.stringify(submission)), + }); + + expect(evaluationResults).toEqual({ + areRequiredCredentialsPresent: Status.ERROR, + presentation: vps, + errors: [ + { + message: 'Expected all submission requirements (1) to be satisfifed in submission, but found 0.', + status: 'error', + tag: 'SubmissionDoesNotSatisfyDefinition', + }, + ], + warnings: [], + // Should return the same presentation submission if provided + value: submission, + }); + }); + it('Evaluate case with error. No submission data', () => { const pdSchema: PresentationDefinitionV1 = getFileAsJson( './test/dif_pe_examples/pdV1/pd-simple-schema-age-predicate.json', @@ -142,7 +662,7 @@ describe('evaluate', () => { limitDisclosureSignatureSuites: LIMIT_DISCLOSURE_SIGNATURE_SUITES, generatePresentationSubmission: false, }), - ).toThrowError('Either a presentation submission as part of the VP or provided separately was expected'); + ).toThrow('Either a presentation submission as part of the VP or provided in options was expected'); }); it('Evaluate case without any error 2', () => { @@ -459,15 +979,126 @@ describe('evaluate', () => { }; const pex: PEX = new PEX(); const jwtEncodedVp = getFile('./test/dif_pe_examples/vp/vp_permanentResidentCard.jwt'); - const evalResult: EvaluationResults = pex.evaluatePresentation(pdSchema, jwtEncodedVp); + const evalResult: PresentationEvaluationResults = pex.evaluatePresentation(pdSchema, jwtEncodedVp); + expect(evalResult.errors).toEqual([]); + expect(evalResult.value?.descriptor_map[0]).toEqual({ + id: 'prc_type', + format: 'ldp_vc', + path: '$.verifiableCredential[0]', + }); + }); + + it('when array of presentations is passed, submission is always constructed as external', 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]); + expect(evalResult.errors).toEqual([]); + expect(evalResult.value?.descriptor_map[0]).toEqual({ + id: 'prc_type', + format: 'ldp_vp', + path: '$[0]', + path_nested: { + id: 'prc_type', + format: 'ldp_vc', + path: '$.verifiableCredential[0]', + }, + }); + }); + + it('when single presentation is passed, it defaults to non-external submission', 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); expect(evalResult.errors).toEqual([]); expect(evalResult.value?.descriptor_map[0]).toEqual({ - id: '1', + id: 'prc_type', format: 'ldp_vc', path: '$.verifiableCredential[0]', }); }); + it('when single presentation is passed with presentationSubmissionLocation.EXTERNAL, it generates the submission as external', 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, + }); + expect(evalResult.errors).toEqual([]); + expect(evalResult.value?.descriptor_map[0]).toEqual({ + id: 'prc_type', + format: 'ldp_vp', + path: '$', + path_nested: { + id: 'prc_type', + format: 'ldp_vc', + path: '$.verifiableCredential[0]', + }, + }); + }); + it('should not pass with jwt vp without submission data', function () { const pdSchema: PresentationDefinitionV2 = { id: '49768857-aec0-4e9d-8392-0e2e01d20120', @@ -492,7 +1123,7 @@ describe('evaluate', () => { }; const pex: PEX = new PEX(); const jwtEncodedVp = getFile('./test/dif_pe_examples/vp/vp_universityDegree.jwt'); - const evalResult: EvaluationResults = pex.evaluatePresentation(pdSchema, jwtEncodedVp, { generatePresentationSubmission: true }); + const evalResult: PresentationEvaluationResults = pex.evaluatePresentation(pdSchema, jwtEncodedVp, { generatePresentationSubmission: true }); expect(evalResult.errors).toEqual([]); expect(evalResult.value?.descriptor_map[0]).toEqual({ id: 'universityDegree_type', diff --git a/test/SdJwt.spec.ts b/test/SdJwt.spec.ts index 2b297b56..64ff7af2 100644 --- a/test/SdJwt.spec.ts +++ b/test/SdJwt.spec.ts @@ -6,7 +6,7 @@ import { SdJwtDecodedVerifiableCredential } from '@sphereon/ssi-types'; import { PEX, PresentationSubmissionLocation, SdJwtDecodedVerifiableCredentialWithKbJwtInput, Status, Validated } from '../lib'; import { calculateSdHash } from '../lib/utils'; -const hasher = (data: string) => createHash('sha256').update(data).digest(); +export const hasher = (data: string) => createHash('sha256').update(data).digest(); const decodedSdJwtVc = { compactSdJwtVc: @@ -284,7 +284,7 @@ describe('evaluate', () => { expect(evaluateResults).toEqual({ // Do we want to return the compact variant here? Or the decoded/pretty variant? - verifiableCredential: [decodedSdJwtVcWithDisclosuresRemoved.compactSdJwtVc + kbJwt], + presentation: decodedSdJwtVcWithDisclosuresRemoved.compactSdJwtVc + kbJwt, areRequiredCredentialsPresent: Status.INFO, warnings: [], errors: [], diff --git a/test/dif_pe_examples/pdV2/pd-multi-formats-multi-vp-submission-requirements.json b/test/dif_pe_examples/pdV2/pd-multi-formats-multi-vp-submission-requirements.json new file mode 100644 index 00000000..8fbc3cf9 --- /dev/null +++ b/test/dif_pe_examples/pdV2/pd-multi-formats-multi-vp-submission-requirements.json @@ -0,0 +1,86 @@ +{ + "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", + "group": ["A"], + "constraints": { + "fields": [ + { + "path": ["$.credentialSubject.lprCategory"], + "filter": { + "type": "string", + "const": "C09" + } + } + ] + } + }, + { + "id": "f09f7000-6bf2-4239-8e8d-13014e681eba", + "group": ["A"], + "constraints": { + "fields": [ + { + "path": ["$.credentialSubject.driversLicenseType"], + "filter": { + "type": "string", + "const": "B" + } + } + ] + } + }, + { + "id": "ddc4a62f-73d4-4410-a3d7-b20720a113ed", + "group": ["A"], + "constraints": { + "fields": [ + { + "path": ["$.vct"], + "filter": { + "type": "string", + "const": "https://high-assurance.com/StateBusinessLicense" + } + } + ] + } + }, + { + "id": "e0556bbf-d1c0-48d8-8564-09f6e07bac9b", + "group": ["A"], + "constraints": { + "fields": [ + { + "path": ["$.vc.credentialSubject.degree.type"], + "filter": { + "type": "string", + "const": "BachelorDegree" + } + } + ] + } + } + ], + "submission_requirements": [ + { + "rule": "all", + "from_nested": [ + { + "rule": "all", + "from": "A" + }, + { + "rule": "pick", + "from": "A", + "min": 4, + "max": 4, + "count": 4 + } + ] + } + ] + } +} diff --git a/test/dif_pe_examples/pdV2/pd-multi-formats-multi-vp.json b/test/dif_pe_examples/pdV2/pd-multi-formats-multi-vp.json new file mode 100644 index 00000000..e15c754f --- /dev/null +++ b/test/dif_pe_examples/pdV2/pd-multi-formats-multi-vp.json @@ -0,0 +1,64 @@ +{ + "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", + "constraints": { + "fields": [ + { + "path": ["$.credentialSubject.lprCategory"], + "filter": { + "type": "string", + "const": "C09" + } + } + ] + } + }, + { + "id": "f09f7000-6bf2-4239-8e8d-13014e681eba", + "constraints": { + "fields": [ + { + "path": ["$.credentialSubject.driversLicenseType"], + "filter": { + "type": "string", + "const": "B" + } + } + ] + } + }, + { + "id": "ddc4a62f-73d4-4410-a3d7-b20720a113ed", + "constraints": { + "fields": [ + { + "path": ["$.vct"], + "filter": { + "type": "string", + "const": "https://high-assurance.com/StateBusinessLicense" + } + } + ] + } + }, + { + "id": "e0556bbf-d1c0-48d8-8564-09f6e07bac9b", + "constraints": { + "fields": [ + { + "path": ["$.vc.credentialSubject.degree.type"], + "filter": { + "type": "string", + "const": "BachelorDegree" + } + } + ] + } + } + ] + } +} diff --git a/test/dif_pe_examples/vc/vc-driverLicense.json b/test/dif_pe_examples/vc/vc-driverLicense.json index 66c3078c..ffd68287 100644 --- a/test/dif_pe_examples/vc/vc-driverLicense.json +++ b/test/dif_pe_examples/vc/vc-driverLicense.json @@ -1,30 +1,14 @@ { - "description": "Government of Example Permanent Resident Card.", "expirationDate": "2029-12-03T12:19:52Z", "issuanceDate": "2019-12-03T12:19:52Z", - "id": "https://issuer.oidp.uscis.gov/credentials/83627465", - "name": "Permanent Resident Card", + "name": "DriversLicense", "identifier": "83627465", "credentialSubject": { - "birthDate": "1958-07-17", - "lprCategory": "C09", - "lprNumber": "999-999-999", - "image": "data:image/png;base64,iVBORw0KGgokJggg==", - "type": [ - "PermanentResident", - "Person" - ], - "commuterClassification": "C1", - "familyName": "SMITH", - "id": "did:example:b34ca6cd37bbf23", - "givenName": "JANE", - "gender": "Female", - "residentSince": "2015-01-01", - "birthCountry": "Bahamas" + "driversLicenseType": "B" }, "type": [ "VerifiableCredential", - "PermanentResidentCard" + "DriversLicnese" ], "@context": [ "https://www.w3.org/2018/credentials/v1", diff --git a/test/dif_pe_examples/vp/vp_state-business-license.sd-jwt b/test/dif_pe_examples/vp/vp_state-business-license.sd-jwt new file mode 100644 index 00000000..7ca7657e --- /dev/null +++ b/test/dif_pe_examples/vp/vp_state-business-license.sd-jwt @@ -0,0 +1 @@ +eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCJ9.eyJpYXQiOjE3MDA0NjQ3MzYwNzYsImlzcyI6ImRpZDprZXk6c29tZS1yYW5kb20tZGlkLWtleSIsIm5iZiI6MTcwMDQ2NDczNjE3NiwidmN0IjoiaHR0cHM6Ly9oaWdoLWFzc3VyYW5jZS5jb20vU3RhdGVCdXNpbmVzc0xpY2Vuc2UiLCJ1c2VyIjp7Il9zZCI6WyI5QmhOVDVsSG5QVmpqQUp3TnR0NDIzM216MFVVMUd3RmFmLWVNWkFQV0JNIiwiSVl5d1FQZl8tNE9hY2Z2S2l1cjRlSnFMa1ZleWRxcnQ1Y2UwMGJReWNNZyIsIlNoZWM2TUNLakIxeHlCVl91QUtvLURlS3ZvQllYbUdBd2VGTWFsd05xbUEiLCJXTXpiR3BZYmhZMkdoNU9pWTRHc2hRU1dQREtSeGVPZndaNEhaQW5YS1RZIiwiajZ6ZFg1OUJYZHlTNFFaTGJITWJ0MzJpenRzWXdkZzRjNkpzWUxNc3ZaMCIsInhKR3Radm41cFM4VEhqVFlJZ3MwS1N5VC1uR3BSR3hDVnp6c1ZEbmMyWkUiXX0sImxpY2Vuc2UiOnsibnVtYmVyIjoxMH0sImNuZiI6eyJqd2siOnsia3R5IjoiRUMiLCJjcnYiOiJQLTI1NiIsIngiOiJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VHZW1jIiwieSI6Ilp4amlXV2JaTVFHSFZXS1ZRNGhiU0lpcnNWZnVlY0NFNnQ0alQ5RjJIWlEifX0sIl9zZF9hbGciOiJzaGEtMjU2IiwiX3NkIjpbIl90YnpMeHBaeDBQVHVzV2hPOHRUZlVYU2ZzQjVlLUtrbzl3dmZaaFJrYVkiLCJ1WmNQaHdUTmN4LXpNQU1zemlYMkFfOXlJTGpQSEhobDhEd2pvVXJLVVdZIl19.HAcudVInhNpXkTPQGNosjKTFRJWgKj90NpfloRaDQchGd4zxc1ChWTCCPXzUXTBypASKrzgjZCiXlTr0bzmLAg~WyJ1LUt3cmJvMkZfTExQekdSZE1XLUtBIiwibmFtZSIsIkpvaG4iXQ~eyJ0eXAiOiJrYitqd3QiLCJhbGciOiJFZERTQSJ9.eyJpYXQiOjE3MTM0NTU1MDI4MDUsIm5vbmNlIjoibm9uY2UtZnJvbS1yZXF1ZXN0IiwiX3NkX2hhc2giOiJQd2FJWHhJUUFmVU9RS2RONmpxS19IRW9ZOU5ISXVzV0R1Vy1MYy1iYXlBIiwiYXVkIjoiZGlkOndlYjpzb21ldGhpbmcifQ.signature \ No newline at end of file diff --git a/test/evaluation/selectFrom.spec.ts b/test/evaluation/selectFrom.spec.ts index da02cb1e..26bf1fc9 100644 --- a/test/evaluation/selectFrom.spec.ts +++ b/test/evaluation/selectFrom.spec.ts @@ -925,7 +925,7 @@ describe('selectFrom tests', () => { const pdSchema: InternalPresentationDefinitionV1 = getFile('./test/dif_pe_examples/pdV1/pd_driver_license_name.json') .presentation_definition as InternalPresentationDefinitionV1; const pd = SSITypesBuilder.modelEntityToInternalPresentationDefinitionV1(pdSchema); - const verifiableCredential: IVerifiableCredential = getFile('./test/dif_pe_examples/vc/vc-driverLicense.json') as IVerifiableCredential; + const verifiableCredential: IVerifiableCredential = getFile('./test/dif_pe_examples/vc/vc-PermanentResidentCard.json') as IVerifiableCredential; const wvcs: WrappedVerifiableCredential[] = SSITypesBuilder.mapExternalVerifiableCredentialsToWrappedVcs([verifiableCredential]); const evaluationClientWrapper: EvaluationClientWrapper = new EvaluationClientWrapper(); const result = evaluationClientWrapper.selectFrom(pd, wvcs, { diff --git a/test/thirdParty/JGiter.spec.ts b/test/thirdParty/JGiter.spec.ts index 6e39d136..d8685e52 100644 --- a/test/thirdParty/JGiter.spec.ts +++ b/test/thirdParty/JGiter.spec.ts @@ -2,6 +2,7 @@ import { PresentationDefinitionV2, Rules } from '@sphereon/pex-models'; import { IPresentation, IProofType, IVerifiableCredential } from '@sphereon/ssi-types'; import { EvaluationResults, PEX, Status } from '../../lib'; +import { PresentationEvaluationResults } from '../../lib/evaluation'; const LIMIT_DISCLOSURE_SIGNATURE_SUITES = [IProofType.BbsBlsSignatureProof2020]; @@ -520,7 +521,7 @@ describe('evaluate JGiter tests', () => { path: '$.verifiableCredential[1]', }, ]); - const evalResult: EvaluationResults = pex.evaluatePresentation(pdSchema, presentation); + const evalResult: PresentationEvaluationResults = pex.evaluatePresentation(pdSchema, presentation); expect(evalResult.errors?.length).toEqual(0); });