Skip to content

Commit

Permalink
chore: useMultipleVPs implementation for submissionFrom
Browse files Browse the repository at this point in the history
  • Loading branch information
sanderPostma committed Nov 2, 2024
1 parent 20c5011 commit de4bc83
Show file tree
Hide file tree
Showing 27 changed files with 337 additions and 345 deletions.
14 changes: 7 additions & 7 deletions lib/PEX.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,14 +319,14 @@ export class PEX {
if (!selectedCredentials) {
throw Error(`At least a verifiable credential needs to be passed in to create a presentation`);
}
const verifiableCredential = (Array.isArray(selectedCredentials) ? selectedCredentials : [selectedCredentials]) as W3CVerifiableCredential[];
if (verifiableCredential.some((c) => CredentialMapper.isSdJwtDecodedCredential(c) || CredentialMapper.isSdJwtEncoded(c))) {
const verifiableCredentials = (Array.isArray(selectedCredentials) ? selectedCredentials : [selectedCredentials]) as W3CVerifiableCredential[];
if (verifiableCredentials.some((c) => CredentialMapper.isSdJwtDecodedCredential(c) || CredentialMapper.isSdJwtEncoded(c))) {
if (!this.options?.hasher) {
throw new Error('Hasher must be provided when creating a presentation with an SD-JWT VC');
}
}

const wVCs = verifiableCredential.map((vc) => CredentialMapper.toWrappedVerifiableCredential(vc, { hasher: this.options?.hasher }));
const wVCs = verifiableCredentials.map((vc) => CredentialMapper.toWrappedVerifiableCredential(vc, { hasher: this.options?.hasher }));
const holders = Array.from(new Set(wVCs.flatMap((wvc) => getSubjectIdsAsString(wvc.credential as ICredential))));
if (holders.length !== 1 && !opts?.holderDID) {
console.log(
Expand Down Expand Up @@ -362,17 +362,17 @@ export class PEX {
}
}
const result: Array<IPresentation | PartialSdJwtDecodedVerifiableCredential> = [];
if (PEX.allowMultipleVCsPerPresentation(verifiableCredential)) {
if (PEX.allowMultipleVCsPerPresentation(verifiableCredentials)) {
result.push({
...opts?.basePresentationPayload,
'@context': context,
type,
holder,
...(!!opts?.presentationSubmission && { presentation_submission: opts.presentationSubmission }),
verifiableCredential,
verifiableCredential: verifiableCredentials,
});
} else {
verifiableCredential.forEach((vc) => {
verifiableCredentials.forEach((vc) => {
if (CredentialMapper.isSdJwtDecodedCredential(vc)) {
result.push(vc as PartialSdJwtDecodedVerifiableCredential);
} else if (CredentialMapper.isSdJwtEncoded(vc)) {
Expand All @@ -397,7 +397,7 @@ export class PEX {
/*
TODO SDK-37 refinement needed
*/
private static allowMultipleVCsPerPresentation(verifiableCredentials: Array<OriginalVerifiableCredential>): boolean {
public static allowMultipleVCsPerPresentation(verifiableCredentials: Array<OriginalVerifiableCredential>): boolean {
const jwtCredentials = verifiableCredentials.filter((c) => CredentialMapper.isJwtEncoded(c) || CredentialMapper.isJwtDecodedCredential(c));

if (jwtCredentials.length > 0) {
Expand Down
14 changes: 13 additions & 1 deletion lib/evaluation/evaluationClientWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from '@sphereon/ssi-types';

import { Checked, Status } from '../ConstraintUtils';
import { PEX } from '../PEX';
import { PresentationSubmissionLocation } from '../signing';
import {
IInternalPresentationDefinition,
Expand Down Expand Up @@ -997,7 +998,18 @@ export class EvaluationClientWrapper {

private updatePresentationSubmissionToExternal(presentationSubmission?: PresentationSubmission): PresentationSubmission {
const descriptors = presentationSubmission?.descriptor_map ?? this._client.presentationSubmission.descriptor_map;
const updatedDescriptors = descriptors.map((d) => this.updateDescriptorToExternal(d));

// Get all VCs to check if they should be in separate VPs
const vcs = this._client.wrappedVcs.map((wvc) => wvc.original);
const useMultipleVPs = !PEX.allowMultipleVCsPerPresentation(vcs) && this._client.wrappedVcs.length > 1;

const updatedDescriptors = descriptors.map((d, index) => {
// If using multiple VPs, update path to include VP index
if (useMultipleVPs) {
return this.updateDescriptorToExternal(d, { vpIndex: index, vcIndex: 0 });
}
return this.updateDescriptorToExternal(d);
});

if (presentationSubmission) {
return {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@sphereon/pex",
"version": "5.0.0-unstable.25",
"version": "5.0.0-unstable.26",
"description": "A Typescript implementation of the v1 and v2 DIF Presentation Exchange specification",
"main": "dist/main/index.js",
"module": "dist/module/index.js",
Expand Down
33 changes: 33 additions & 0 deletions test/PEX.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from '@sphereon/ssi-types';

import { IPresentationDefinition, PEX, PEXv2, Status, Validated } from '../lib';
import { EvaluationClientWrapper } from '../lib/evaluation';
import { PresentationEvaluationResults } from '../lib/evaluation/core';
import { PresentationSubmissionLocation, VerifiablePresentationResult } from '../lib/signing/types';
import { SSITypesBuilder } from '../lib/types';
Expand All @@ -27,6 +28,7 @@ import {
getProofOptionsMock,
getSingatureOptionsMock,
} from './test_data/PresentationSignUtilMock';
import { getFileAsEntity } from './utils/files';

function getFile(path: string) {
return fs.readFileSync(path, 'utf-8');
Expand Down Expand Up @@ -1579,4 +1581,35 @@ describe('evaluate', () => {
expect(result.vcIndexes?.length).toEqual(1);
expect(result.vcIndexes?.[0]).toEqual(3);
});

it('Evaluate Impierce VPs', () => {
const pdSchema = getFileAsEntity<PresentationDefinitionV2>('./test/dif_pe_examples/pdV2/impierce-example.json');
const vcArray = [getFile('./test/dif_pe_examples/vc/dienstjaar.jwt'), getFile('./test/dif_pe_examples/vc/neurolympics.jwt')];
const vpArray = [getFile('./test/dif_pe_examples/vp/dienstjaar.jwt'), getFile('./test/dif_pe_examples/vp/neurolympics.jwt')];
const pex: PEX = new PEX();
const clientWrapper: EvaluationClientWrapper = new EvaluationClientWrapper();

const internalPD = SSITypesBuilder.modelEntityInternalPresentationDefinitionV2(pdSchema);
const wvcs = SSITypesBuilder.mapExternalVerifiableCredentialsToWrappedVcs(vcArray);
const resultSelectFrom = clientWrapper.selectFrom(internalPD, wvcs, {
holderDIDs: undefined,
limitDisclosureSignatureSuites: LIMIT_DISCLOSURE_SIGNATURE_SUITES,
});
expect(resultSelectFrom.areRequiredCredentialsPresent).toEqual(Status.INFO);
expect(resultSelectFrom.verifiableCredential?.length).toEqual(2);

const presentationSubmission: PresentationSubmission = clientWrapper.submissionFrom(
internalPD,
SSITypesBuilder.mapExternalVerifiableCredentialsToWrappedVcs(resultSelectFrom.verifiableCredential as IVerifiableCredential[]),
{ presentationSubmissionLocation: PresentationSubmissionLocation.EXTERNAL },
);

const evaluationResults = pex.evaluatePresentation(pdSchema, vpArray, {
limitDisclosureSignatureSuites: LIMIT_DISCLOSURE_SIGNATURE_SUITES,
presentationSubmission,
presentationSubmissionLocation: PresentationSubmissionLocation.EXTERNAL,
});
expect(evaluationResults!.value!.descriptor_map!.length).toEqual(2);
expect(evaluationResults!.errors!.length).toEqual(0);
});
});
45 changes: 22 additions & 23 deletions test/PEXv1.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import fs from 'fs';

import { PresentationDefinitionV1 } from '@sphereon/pex-models';
import { IPresentation, IProofType, IVerifiablePresentation } from '@sphereon/ssi-types';

Expand All @@ -12,10 +10,7 @@ import {
getProofOptionsMock,
getSingatureOptionsMock,
} from './test_data/PresentationSignUtilMock';

function getFile(path: string) {
return JSON.parse(fs.readFileSync(path, 'utf-8'));
}
import { getFileAsJson } from './utils/files';

const LIMIT_DISCLOSURE_SIGNATURE_SUITES = [IProofType.BbsBlsSignatureProof2020];

Expand All @@ -27,8 +22,8 @@ describe('evaluate', () => {

it('Evaluate case with error result', () => {
const pex: PEXv1 = new PEXv1();
const pdSchema: PresentationDefinitionV1 = getFile('./test/dif_pe_examples/pdV1/pd-PermanentResidentCard.json').presentation_definition;
const vc = getFile('./test/dif_pe_examples/vc/vc-PermanentResidentCard.json');
const pdSchema: PresentationDefinitionV1 = getFileAsJson('./test/dif_pe_examples/pdV1/pd-PermanentResidentCard.json').presentation_definition;
const vc = getFileAsJson('./test/dif_pe_examples/vc/vc-PermanentResidentCard.json');
pdSchema.input_descriptors[0].schema = [{ uri: 'https://www.example.com/schema' }];
const result = pex.selectFrom(pdSchema, [vc], {
holderDIDs: ['FAsYneKJhWBP2n5E21ZzdY'],
Expand All @@ -39,8 +34,10 @@ describe('evaluate', () => {
});

it('Evaluate case without any error 1', () => {
const pdSchema: PresentationDefinitionV1 = getFile('./test/dif_pe_examples/pdV1/pd-simple-schema-age-predicate.json').presentation_definition;
const vpSimple: IVerifiablePresentation = getFile('./test/dif_pe_examples/vp/vp-simple-age-predicate.json');
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: PEXv1 = new PEXv1();
const evaluationResults = pex.evaluatePresentation(pdSchema, vpSimple, { limitDisclosureSignatureSuites: LIMIT_DISCLOSURE_SIGNATURE_SUITES });
Expand All @@ -49,8 +46,10 @@ describe('evaluate', () => {
});

it('Evaluate case without any error 2', () => {
const pdSchema: PresentationDefinitionV1 = getFile('./test/dif_pe_examples/pdV1/pd-simple-schema-age-predicate.json').presentation_definition;
const vpSimple: IVerifiablePresentation = getFile('./test/dif_pe_examples/vp/vp-simple-age-predicate.json');
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: PEXv1 = new PEXv1();
const evaluationResults = pex.evaluateCredentials(pdSchema, vpSimple.verifiableCredential!, {
Expand All @@ -62,8 +61,8 @@ describe('evaluate', () => {
});

it('Evaluate submission requirements all from group A', () => {
const pdSchema: PresentationDefinitionV1 = getFile('./test/resources/sr_rules.json').presentation_definition;
const vpSimple = getFile('./test/dif_pe_examples/vp/vp_general.json') as IVerifiablePresentation;
const pdSchema: PresentationDefinitionV1 = getFileAsJson('./test/resources/sr_rules.json').presentation_definition;
const vpSimple = getFileAsJson('./test/dif_pe_examples/vp/vp_general.json') as IVerifiablePresentation;
const HOLDER_DID = 'did:example:ebfeb1f712ebc6f1c276e12ec21';
pdSchema!.submission_requirements = [pdSchema!.submission_requirements![0]];
const pex: PEXv1 = new PEXv1();
Expand Down Expand Up @@ -93,27 +92,27 @@ describe('evaluate', () => {
});

it('Evaluate pdV1 schema of our sr_rules.json pdV1', () => {
const pdSchema: PresentationDefinitionV1 = getFile('./test/resources/sr_rules.json').presentation_definition;
const pdSchema: PresentationDefinitionV1 = getFileAsJson('./test/resources/sr_rules.json').presentation_definition;
pdSchema!.submission_requirements = [pdSchema!.submission_requirements![0]];
const result: Validated = PEX.validateDefinition(pdSchema);
expect(result).toEqual([{ message: 'ok', status: 'info', tag: 'root' }]);
});

it("Evaluate presentation submission of our vp_general's presentation_submission", () => {
const vpSimple = getFile('./test/dif_pe_examples/vp/vp_general.json');
const vpSimple = getFileAsJson('./test/dif_pe_examples/vp/vp_general.json');
const result: Validated = PEX.validateSubmission(vpSimple.presentation_submission);
expect(result).toEqual([{ message: 'ok', status: 'info', tag: 'root' }]);
});

it('Evaluate pdV1 schema of our pd_driver_license_name.json pdV1', () => {
const pdSchema = getFile('./test/dif_pe_examples/pdV1/pd_driver_license_name.json');
const pdSchema = getFileAsJson('./test/dif_pe_examples/pdV1/pd_driver_license_name.json');
const result: Validated = PEX.validateDefinition(pdSchema.presentation_definition);
expect(result).toEqual([{ message: 'ok', status: 'info', tag: 'root' }]);
});

it('should return a signed presentation', async () => {
const pdSchema = getFile('./test/dif_pe_examples/pdV1/pd-simple-schema-age-predicate.json');
const vpSimple = getFile('./test/dif_pe_examples/vp/vp-simple-age-predicate.json') as IVerifiablePresentation;
const pdSchema = getFileAsJson('./test/dif_pe_examples/pdV1/pd-simple-schema-age-predicate.json');
const vpSimple = getFileAsJson('./test/dif_pe_examples/vp/vp-simple-age-predicate.json') as IVerifiablePresentation;
const pex: PEXv1 = new PEXv1();
const vpr = await pex.verifiablePresentationFrom(pdSchema.presentation_definition, vpSimple.verifiableCredential!, assertedMockCallback, {
proofOptions: getProofOptionsMock(),
Expand All @@ -128,8 +127,8 @@ describe('evaluate', () => {
});

it("should throw error if proofOptions doesn't have a type", async () => {
const pdSchema = getFile('./test/dif_pe_examples/pdV1/pd_driver_license_name.json');
const vpSimple = getFile('./test/dif_pe_examples/vp/vp_general.json') as IVerifiablePresentation;
const pdSchema = getFileAsJson('./test/dif_pe_examples/pdV1/pd_driver_license_name.json');
const vpSimple = getFileAsJson('./test/dif_pe_examples/vp/vp_general.json') as IVerifiablePresentation;
const pex: PEXv1 = new PEXv1();
delete pdSchema.presentation_definition.input_descriptors[0].schema;
const proofOptions = getProofOptionsMock();
Expand All @@ -145,8 +144,8 @@ describe('evaluate', () => {
});

it('should throw exception if signing encounters a problem', async () => {
const pdSchema = getFile('./test/dif_pe_examples/pdV1/pd_driver_license_name.json');
const vpSimple = getFile('./test/dif_pe_examples/vp/vp_general.json') as IVerifiablePresentation;
const pdSchema = getFileAsJson('./test/dif_pe_examples/pdV1/pd_driver_license_name.json');
const vpSimple = getFileAsJson('./test/dif_pe_examples/vp/vp_general.json') as IVerifiablePresentation;
const pex: PEXv1 = new PEXv1();

await expect(
Expand Down
31 changes: 13 additions & 18 deletions test/PEXv2.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import fs from 'fs';

import { FilterV2, PresentationDefinitionV2 } from '@sphereon/pex-models';
import { IProofType, IVerifiableCredential, IVerifiablePresentation } from '@sphereon/ssi-types';

Expand All @@ -13,10 +11,7 @@ import {
getSingatureOptionsMock,
} from './test_data/PresentationSignUtilMock';
import { JwtVcs } from './test_data/jwtVcs';

function getFile(path: string) {
return JSON.parse(fs.readFileSync(path, 'utf-8'));
}
import { getFileAsJson } from './utils/files';

const LIMIT_DISCLOSURE_SIGNATURE_SUITES = [IProofType.BbsBlsSignatureProof2020];

Expand Down Expand Up @@ -262,15 +257,15 @@ describe('evaluate', () => {
});

it("Evaluate presentation submission of our vp_general's presentation_submission", () => {
const vpSimple = getFile('./test/dif_pe_examples/vp/vp_general.json');
const vpSimple = getFileAsJson('./test/dif_pe_examples/vp/vp_general.json');
const result: Validated = PEX.validateSubmission(vpSimple.presentation_submission);
expect(result).toEqual([{ message: 'ok', status: 'info', tag: 'root' }]);
});

//Credential does not contain the field
it('should return a signed presentation with PdV2', async () => {
const pdSchema = getFile('./test/dif_pe_examples/pdV1/pd-simple-schema-age-predicate.json');
const vpSimple = getFile('./test/dif_pe_examples/vp/vp-simple-age-predicate.json') as IVerifiablePresentation;
const pdSchema = getFileAsJson('./test/dif_pe_examples/pdV1/pd-simple-schema-age-predicate.json');
const vpSimple = getFileAsJson('./test/dif_pe_examples/vp/vp-simple-age-predicate.json') as IVerifiablePresentation;
const pex: PEXv2 = new PEXv2();
delete pdSchema.presentation_definition.input_descriptors[0].schema;
const vpr = await pex.verifiablePresentationFrom(pdSchema.presentation_definition, vpSimple.verifiableCredential!, assertedMockCallback, {
Expand All @@ -286,8 +281,8 @@ describe('evaluate', () => {
});

it("should throw error if proofOptions doesn't have a type", async () => {
const pdSchema = getFile('./test/dif_pe_examples/pdV1/pd_driver_license_name.json');
const vpSimple = getFile('./test/dif_pe_examples/vp/vp_general.json') as IVerifiablePresentation;
const pdSchema = getFileAsJson('./test/dif_pe_examples/pdV1/pd_driver_license_name.json');
const vpSimple = getFileAsJson('./test/dif_pe_examples/vp/vp_general.json') as IVerifiablePresentation;
const pex: PEXv2 = new PEXv2();
delete pdSchema.presentation_definition.input_descriptors[0].schema;
const proofOptions = getProofOptionsMock();
Expand All @@ -304,8 +299,8 @@ describe('evaluate', () => {

it('Evaluate selectFrom', () => {
const pex: PEXv2 = new PEXv2();
const pdSchema: PresentationDefinitionV2 = getFile('./test/dif_pe_examples/pdV2/vc_expiration(corrected).json').presentation_definition;
const vc = getFile('./test/dif_pe_examples/vc/vc-PermanentResidentCard.json');
const pdSchema: PresentationDefinitionV2 = getFileAsJson('./test/dif_pe_examples/pdV2/vc_expiration(corrected).json').presentation_definition;
const vc = getFileAsJson('./test/dif_pe_examples/vc/vc-PermanentResidentCard.json');
const result = pex.selectFrom(pdSchema, [vc], {
holderDIDs: ['FAsYneKJhWBP2n5E21ZzdY'],
limitDisclosureSignatureSuites: LIMIT_DISCLOSURE_SIGNATURE_SUITES,
Expand Down Expand Up @@ -343,8 +338,8 @@ describe('evaluate', () => {
});

it("should throw error if proofOptions doesn't have a type with v2 pd", async () => {
const pdSchema = getFile('./test/dif_pe_examples/pdV2/vc_expiration(corrected).json');
const vpSimple = getFile('./test/dif_pe_examples/vp/vp_general.json') as IVerifiablePresentation;
const pdSchema = getFileAsJson('./test/dif_pe_examples/pdV2/vc_expiration(corrected).json');
const vpSimple = getFileAsJson('./test/dif_pe_examples/vp/vp_general.json') as IVerifiablePresentation;
const pex: PEXv2 = new PEXv2();
delete pdSchema.presentation_definition.input_descriptors[0].schema;
const proofOptions = getProofOptionsMock();
Expand Down Expand Up @@ -413,7 +408,7 @@ describe('evaluate', () => {
];
delete pd.input_descriptors[0].constraints!.limit_disclosure;

const vpSimple = getFile('./test/dif_pe_examples/vp/vp_general.json') as IVerifiablePresentation;
const vpSimple = getFileAsJson('./test/dif_pe_examples/vp/vp_general.json') as IVerifiablePresentation;
const pex: PEXv2 = new PEXv2();
const result = pex.selectFrom(pd, [vpSimple.verifiableCredential![0]], {
holderDIDs: ['FAsYneKJhWBP2n5E21ZzdY'],
Expand All @@ -437,7 +432,7 @@ describe('evaluate', () => {
];
delete pd.input_descriptors[0].constraints!.limit_disclosure;

const vpSimple = getFile('./test/dif_pe_examples/vp/vp_general.json') as IVerifiablePresentation;
const vpSimple = getFileAsJson('./test/dif_pe_examples/vp/vp_general.json') as IVerifiablePresentation;
const pex: PEXv2 = new PEXv2();
const result = pex.selectFrom(pd, [vpSimple.verifiableCredential![0]], {
holderDIDs: ['FAsYneKJhWBP2n5E21ZzdY'],
Expand All @@ -461,7 +456,7 @@ describe('evaluate', () => {
];
delete pd.input_descriptors[0].constraints!.limit_disclosure;

const vpSimple = getFile('./test/dif_pe_examples/vp/vp_general.json') as IVerifiablePresentation;
const vpSimple = getFileAsJson('./test/dif_pe_examples/vp/vp_general.json') as IVerifiablePresentation;
const pex: PEXv2 = new PEXv2();
const result = pex.selectFrom(pd, [vpSimple.verifiableCredential![0]], {
holderDIDs: ['FAsYneKJhWBP2n5E21ZzdY'],
Expand Down
Loading

0 comments on commit de4bc83

Please sign in to comment.