Skip to content

Commit

Permalink
Merge pull request #160 from TimoGlastra/fix/handle-non-external-subm…
Browse files Browse the repository at this point in the history
…ission-edge-cases

fix: handle non-exteral submission edge cases
  • Loading branch information
nklomp committed Aug 29, 2024
2 parents c599ee9 + c5fc070 commit 270ca38
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 54 deletions.
23 changes: 20 additions & 3 deletions lib/PEX.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ export class PEX {
presentationSubmission?: PresentationSubmission;
/**
* The location of the presentation submission. By default {@link PresentationSubmissionLocation.PRESENTATION}
* is used when one presentation is passed (not as array), while {@link PresentationSubmissionLocation.EXTERNAL} is
* used when an array is passed
* is used when one W3C presentation is passed (not as array) , while {@link PresentationSubmissionLocation.EXTERNAL} is
* used when an array is passed or the presentation is not a W3C presentation
*/
presentationSubmissionLocation?: PresentationSubmissionLocation;
generatePresentationSubmission?: boolean;
Expand All @@ -104,13 +104,29 @@ export class PEX {
);

let presentationSubmission = opts?.presentationSubmission;
let presentationSubmissionLocation =
opts?.presentationSubmissionLocation ??
(Array.isArray(presentations) || !CredentialMapper.isW3cPresentation(wrappedPresentations[0].presentation)
? PresentationSubmissionLocation.EXTERNAL
: PresentationSubmissionLocation.PRESENTATION);

// When only one presentation, we also allow it to be present in the VP
if (!presentationSubmission && presentationsArray.length === 1 && !generatePresentationSubmission) {
if (
!presentationSubmission &&
presentationsArray.length === 1 &&
CredentialMapper.isW3cPresentation(wrappedPresentations[0].presentation) &&
!generatePresentationSubmission
) {
presentationSubmission = wrappedPresentations[0].decoded.presentation_submission;
if (!presentationSubmission) {
throw Error(`Either a presentation submission as part of the VP or provided in options was expected`);
}
presentationSubmissionLocation = PresentationSubmissionLocation.PRESENTATION;
if (opts?.presentationSubmissionLocation && opts.presentationSubmissionLocation !== PresentationSubmissionLocation.PRESENTATION) {
throw new Error(
`unexpected presentationSubmissionLocation ${opts.presentationSubmissionLocation} was provided. Expected ${PresentationSubmissionLocation.PRESENTATION} when no presentationSubmission passed and first verifiable presentation contains a presentation_submission and generatePresentationSubmission is false`,
);
}
} else if (!presentationSubmission && !generatePresentationSubmission) {
throw new Error('Presentation submission in options was expected.');
}
Expand All @@ -125,6 +141,7 @@ export class PEX {
...opts,
holderDIDs,
presentationSubmission,
presentationSubmissionLocation,
generatePresentationSubmission,
};

Expand Down
147 changes: 97 additions & 50 deletions lib/evaluation/evaluationClientWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,8 +381,8 @@ export class EvaluationClientWrapper {
generatePresentationSubmission?: boolean;
/**
* The location of the presentation submission. By default {@link PresentationSubmissionLocation.PRESENTATION}
* is used when one presentation is passed (not as array), while {@link PresentationSubmissionLocation.EXTERNAL} is
* used when an array is passed
* is used when one W3C presentation is passed (not as array) , while {@link PresentationSubmissionLocation.EXTERNAL} is
* used when an array is passed or the presentation is not a W3C presentation
*/
presentationSubmissionLocation?: PresentationSubmissionLocation;
},
Expand Down Expand Up @@ -444,6 +444,48 @@ export class EvaluationClientWrapper {
return result;
}

private extractWrappedVcFromWrappedVp(
descriptor: Descriptor,
descriptorIndex: string,
wvp: WrappedVerifiablePresentation,
): { error: Checked; wvc: undefined } | { wvc: WrappedVerifiableCredential; error: undefined } {
// Decoded won't work for sd-jwt or jwt?!?!
const [vcResult] = JsonPathUtils.extractInputField(wvp.decoded, [descriptor.path]) as Array<{
value: string | IVerifiableCredential;
}>;

if (!vcResult) {
return {
error: {
status: Status.ERROR,
tag: 'SubmissionPathNotFound',
message: `Unable to extract path ${descriptor.path} for submission.descriptor_path[${descriptorIndex}] from verifiable presentation`,
},
wvc: undefined,
};
}

// Find the wrapped VC based on the original VC
const originalVc = vcResult.value;
const wvc = wvp.vcs.find((wvc) => CredentialMapper.areOriginalVerifiableCredentialsEqual(wvc.original, originalVc));

if (!wvc) {
return {
error: {
status: Status.ERROR,
tag: 'SubmissionPathNotFound',
message: `Unable to find wrapped vc`,
},
wvc: undefined,
};
}

return {
wvc,
error: undefined,
};
}

private evaluatePresentationsAgainstSubmission(
pd: IInternalPresentationDefinition,
wvps: OrArray<WrappedVerifiablePresentation>,
Expand All @@ -452,6 +494,7 @@ export class EvaluationClientWrapper {
holderDIDs?: string[];
limitDisclosureSignatureSuites?: string[];
restrictToFormats?: Format;
presentationSubmissionLocation?: PresentationSubmissionLocation;
},
): PresentationEvaluationResults {
const result: PresentationEvaluationResults = {
Expand All @@ -462,76 +505,80 @@ export class EvaluationClientWrapper {
value: submission,
};

// If only a single VP is passed that is not w3c and no presentationSubmissionLocation, we set the default location to presentation. Otherwise we assume it's external
const presentationSubmissionLocation =
opts?.presentationSubmissionLocation ??
(Array.isArray(wvps) || !CredentialMapper.isW3cPresentation(wvps.presentation)
? PresentationSubmissionLocation.EXTERNAL
: PresentationSubmissionLocation.PRESENTATION);

// We loop over all the descriptors in the submission
for (const descriptorIndex in submission.descriptor_map) {
const descriptor = submission.descriptor_map[descriptorIndex];

// Extract the VP from the wrapped VPs
const [vpResult] = JsonPathUtils.extractInputField(wvps, [descriptor.path]) as Array<{ value: WrappedVerifiablePresentation }>;
if (!vpResult) {
result.areRequiredCredentialsPresent = Status.ERROR;
result.errors?.push({
status: Status.ERROR,
tag: 'SubmissionPathNotFound',
message: `Unable to extract path ${descriptor.path} for submission.descriptor_path[${descriptorIndex}] from presentation(s)`,
});
continue;
}
const vp = vpResult.value;
let vcPath = `presentation ${descriptor.path}`;

if (vp.format !== descriptor.format) {
result.areRequiredCredentialsPresent = Status.ERROR;
result.errors?.push({
status: Status.ERROR,
tag: 'SubmissionFormatNoMatch',
message: `VP at path ${descriptor.path} has format ${vp.format}, while submission.descriptor_path[${descriptorIndex}] has format ${descriptor.format}`,
});
continue;
}

let vp: WrappedVerifiablePresentation;
let vc: WrappedVerifiableCredential;
if (descriptor.path_nested) {
const [vcResult] = JsonPathUtils.extractInputField(vp.decoded, [descriptor.path_nested.path]) as Array<{
value: string | IVerifiableCredential;
}>;
let vcPath: string;

if (!vcResult) {
if (presentationSubmissionLocation === PresentationSubmissionLocation.EXTERNAL) {
// Extract the VP from the wrapped VPs
const [vpResult] = JsonPathUtils.extractInputField(wvps, [descriptor.path]) as Array<{ value: WrappedVerifiablePresentation }>;
if (!vpResult) {
result.areRequiredCredentialsPresent = Status.ERROR;
result.errors?.push({
status: Status.ERROR,
tag: 'SubmissionPathNotFound',
message: `Unable to extract path_nested.path ${descriptor.path_nested.path} for submission.descriptor_path[${descriptorIndex}] from verifiable presentation`,
message: `Unable to extract path ${descriptor.path} for submission.descriptor_path[${descriptorIndex}] from presentation(s)`,
});
continue;
}
vp = vpResult.value;
vcPath = `presentation ${descriptor.path}`;

// Find the wrapped VC based on the orignial VC
const originalVc = vcResult.value;
const wvc = vp.vcs.find((wvc) => CredentialMapper.areOriginalVerifiableCredentialsEqual(wvc.original, originalVc));

if (!wvc) {
if (vp.format !== descriptor.format) {
result.areRequiredCredentialsPresent = Status.ERROR;
result.errors?.push({
status: Status.ERROR,
tag: 'SubmissionPathNotFound',
message: `Unable to find wrapped vc`,
tag: 'SubmissionFormatNoMatch',
message: `VP at path ${descriptor.path} has format ${vp.format}, while submission.descriptor_path[${descriptorIndex}] has format ${descriptor.format}`,
});
continue;
}

vc = wvc;
vcPath += ` with nested credential ${descriptor.path_nested.path}`;
} else if (descriptor.format === 'vc+sd-jwt') {
vc = vp.vcs[0];
if (descriptor.path_nested) {
const extractionResult = this.extractWrappedVcFromWrappedVp(descriptor.path_nested, descriptorIndex, vp);
if (extractionResult.error) {
result.areRequiredCredentialsPresent = Status.ERROR;
result.errors?.push(extractionResult.error);
continue;
}

vc = extractionResult.wvc;
vcPath += ` with nested credential ${descriptor.path_nested.path}`;
} else if (descriptor.format === 'vc+sd-jwt') {
vc = vp.vcs[0];
} else {
result.areRequiredCredentialsPresent = Status.ERROR;
result.errors?.push({
status: Status.ERROR,
tag: 'UnsupportedFormat',
message: `VP format ${vp.format} is not supported`,
});
continue;
}
} else {
result.areRequiredCredentialsPresent = Status.ERROR;
result.errors?.push({
status: Status.ERROR,
tag: 'UnsupportedFormat',
message: `VP format ${vp.format} is not supported`,
});
continue;
// TODO: check that not longer than 0
vp = Array.isArray(wvps) ? wvps[0] : wvps;
vcPath = `credential ${descriptor.path}`;

const extractionResult = this.extractWrappedVcFromWrappedVp(descriptor, descriptorIndex, vp);
if (extractionResult.error) {
result.areRequiredCredentialsPresent = Status.ERROR;
result.errors?.push(extractionResult.error);
continue;
}

vc = extractionResult.wvc;
}

// TODO: we should probably add support for holder dids in the kb-jwt of an SD-JWT. We can extract this from the
Expand Down
52 changes: 51 additions & 1 deletion test/PEX.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,57 @@ describe('evaluate', () => {
const vpSimple: IVerifiablePresentation = getFileAsJson('./test/dif_pe_examples/vp/vp-simple-age-predicate.json');
pdSchema.input_descriptors[0].schema.push({ uri: 'https://www.w3.org/TR/vc-data-model/#types1' });
const pex: PEX = new PEX();
const evaluationResults = pex.evaluatePresentation(pdSchema, vpSimple, { limitDisclosureSignatureSuites: LIMIT_DISCLOSURE_SIGNATURE_SUITES });
const evaluationResults = pex.evaluatePresentation(pdSchema, vpSimple, {
limitDisclosureSignatureSuites: LIMIT_DISCLOSURE_SIGNATURE_SUITES,
});
expect(evaluationResults!.value!.descriptor_map!.length).toEqual(1);
expect(evaluationResults!.errors!.length).toEqual(0);
});

it('Evaluate case without any error passing submission and presentation submission location', () => {
const pdSchema: PresentationDefinitionV1 = getFileAsJson(
'./test/dif_pe_examples/pdV1/pd-simple-schema-age-predicate.json',
).presentation_definition;
const vpSimple: IVerifiablePresentation = getFileAsJson('./test/dif_pe_examples/vp/vp-simple-age-predicate.json');
pdSchema.input_descriptors[0].schema.push({ uri: 'https://www.w3.org/TR/vc-data-model/#types1' });
const pex: PEX = new PEX();
const evaluationResults = pex.evaluatePresentation(pdSchema, vpSimple, {
limitDisclosureSignatureSuites: LIMIT_DISCLOSURE_SIGNATURE_SUITES,
presentationSubmission: {
id: 'accd5adf-1dbf-4ed9-9ba2-d687476126cb',
definition_id: '31e2f0f1-6b70-411d-b239-56aed5321884',
descriptor_map: [
{
id: '867bfe7a-5b91-46b2-9ba4-70028b8d9cc8',
format: 'ldp_vp',
path: '$.verifiableCredential[0]',
},
],
},
presentationSubmissionLocation: PresentationSubmissionLocation.PRESENTATION,
});
expect(evaluationResults!.value!.descriptor_map!.length).toEqual(1);
expect(evaluationResults!.errors!.length).toEqual(0);
});

it('Evaluate case without any error passing submission and presentation submission location presentation W3C JWT vc', () => {
const pdSchema: PresentationDefinitionV1 = getFileAsJson('./test/dif_pe_examples/pdV1/pd-simple-schema-jwt-degree.json').presentation_definition;
const pex: PEX = new PEX();
const evaluationResults = pex.evaluatePresentation(pdSchema, getFile('./test/dif_pe_examples/vp/vp_universityDegree.jwt'), {
limitDisclosureSignatureSuites: LIMIT_DISCLOSURE_SIGNATURE_SUITES,
presentationSubmission: {
id: 'accd5adf-1dbf-4ed9-9ba2-d687476126cb',
definition_id: '31e2f0f1-6b70-411d-b239-56aed5321884',
descriptor_map: [
{
id: '867bfe7a-5b91-46b2-9ba4-70028b8d9cc8',
format: 'jwt_vc',
path: '$.vp.verifiableCredential[0]',
},
],
},
presentationSubmissionLocation: PresentationSubmissionLocation.PRESENTATION,
});
expect(evaluationResults!.value!.descriptor_map!.length).toEqual(1);
expect(evaluationResults!.errors!.length).toEqual(0);
});
Expand Down
25 changes: 25 additions & 0 deletions test/dif_pe_examples/pdV1/pd-simple-schema-jwt-degree.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"comment": "Note: VP, OIDC, DIDComm, or CHAPI outer wrapper would be here.",
"presentation_definition": {
"id": "31e2f0f1-6b70-411d-b239-56aed5321884",
"purpose": "To sell you a drink we need to know that you are an adult.",
"input_descriptors": [
{
"id": "867bfe7a-5b91-46b2-9ba4-70028b8d9cc8",
"purpose": "Degree",
"schema": [
{
"uri": "https://www.w3.org/2018/credentials/v1"
}
],
"constraints": {
"fields": [
{
"path": ["$.vc.credentialSubject.degree.type"]
}
]
}
}
]
}
}

0 comments on commit 270ca38

Please sign in to comment.