Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: handle non-exteral submission edge cases #160

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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?!?!
nklomp marked this conversation as resolved.
Show resolved Hide resolved
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest to add it immediately. It is easy to implement and would prevent a implementer from making mistakes that are hard to track down

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes sorry that is sloppy of me, meant to do that before opening the PR. will fix

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 @@ -124,7 +124,57 @@ describe('evaluate', () => {
const vpSimple: IVerifiablePresentation = getFileAsJson('./test/dif_pe_examples/vp/vp-simple-age-predicate.json');
pdSchema.input_descriptors[0].schema.push({ uri: 'https://www.w3.org/TR/vc-data-model/#types1' });
const pex: PEX = new PEX();
const evaluationResults = pex.evaluatePresentation(pdSchema, vpSimple, { limitDisclosureSignatureSuites: LIMIT_DISCLOSURE_SIGNATURE_SUITES });
const evaluationResults = pex.evaluatePresentation(pdSchema, vpSimple, {
limitDisclosureSignatureSuites: LIMIT_DISCLOSURE_SIGNATURE_SUITES,
});
expect(evaluationResults!.value!.descriptor_map!.length).toEqual(1);
expect(evaluationResults!.errors!.length).toEqual(0);
});

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

it('Evaluate case without any error passing submission and presentation submission location presentation W3C JWT vc', () => {
const pdSchema: PresentationDefinitionV1 = getFileAsJson('./test/dif_pe_examples/pdV1/pd-simple-schema-jwt-degree.json').presentation_definition;
const pex: PEX = new PEX();
const evaluationResults = pex.evaluatePresentation(pdSchema, getFile('./test/dif_pe_examples/vp/vp_universityDegree.jwt'), {
limitDisclosureSignatureSuites: LIMIT_DISCLOSURE_SIGNATURE_SUITES,
presentationSubmission: {
id: 'accd5adf-1dbf-4ed9-9ba2-d687476126cb',
definition_id: '31e2f0f1-6b70-411d-b239-56aed5321884',
descriptor_map: [
{
id: '867bfe7a-5b91-46b2-9ba4-70028b8d9cc8',
format: 'jwt_vc',
path: '$.vp.verifiableCredential[0]',
},
],
},
presentationSubmissionLocation: PresentationSubmissionLocation.PRESENTATION,
});
expect(evaluationResults!.value!.descriptor_map!.length).toEqual(1);
expect(evaluationResults!.errors!.length).toEqual(0);
});
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"]
}
]
}
}
]
}
}
Loading