Skip to content
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
21 changes: 18 additions & 3 deletions lib/PEX.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export class PEX {
throw new Error('At least one presentation must be provided');
}

let originalPresentationSubmission = opts?.presentationSubmission;
const generatePresentationSubmission =
opts?.generatePresentationSubmission !== undefined ? opts.generatePresentationSubmission : opts?.presentationSubmission === undefined;
const pd: IInternalPresentationDefinition = SSITypesBuilder.toInternalPresentationDefinition(presentationDefinition);
Expand All @@ -117,11 +118,12 @@ export class PEX {
!presentationSubmission &&
presentationsArray.length === 1 &&
PexCredentialMapper.isW3cPresentation(wrappedPresentations[0].presentation) &&
!generatePresentationSubmission
!opts?.generatePresentationSubmission
) {
const decoded = wrappedPresentations[0].decoded;
if ('presentation_submission' in decoded) {
presentationSubmission = decoded.presentation_submission;
presentationSubmission = JSON.parse(JSON.stringify(decoded.presentation_submission));
originalPresentationSubmission = decoded.presentation_submission;
}
if (!presentationSubmission) {
throw Error(`Either a presentation submission as part of the VP or provided in options was expected`);
Expand All @@ -132,6 +134,16 @@ export class PEX {
`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`,
);
}

// We need to update the vp path as PEX decoded assumes it's an external submission
// So we need to update the submission paths
if (wrappedPresentations[0].format === 'jwt_vp') {
for (const descriptor of presentationSubmission.descriptor_map) {
if (!descriptor.path.startsWith('$.vp')) {
descriptor.path = descriptor.path.replace('$.', '$.vp.');
}
}
}
} else if (!presentationSubmission && !generatePresentationSubmission) {
throw new Error('Presentation submission in options was expected.');
}
Expand Down Expand Up @@ -167,7 +179,10 @@ export class PEX {
}
}

return result;
return {
...result,
value: originalPresentationSubmission ?? result.value,
};
}

/***
Expand Down
44 changes: 27 additions & 17 deletions lib/evaluation/handlers/formatRestrictionEvaluationHandler.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { FieldV2 } from '@sphereon/pex-models';

import { Status } from '../../ConstraintUtils';
import { IInternalPresentationDefinition, InternalPresentationDefinitionV1, InternalPresentationDefinitionV2 } from '../../types';
import PexMessages from '../../types/Messages';
Expand Down Expand Up @@ -30,8 +32,29 @@ export class FormatRestrictionEvaluationHandler extends AbstractEvaluationHandle

if (allowedFormats.includes(wvc.format)) {
// According to 18013-7 the docType MUST match the input descriptor ID
if (wvc.format === 'mso_mdoc' && wvc.credential.docType !== _inputDescriptor.id) {
this.getResults().push(this.generateInputDescriptorIdDoctypeErrorResult(index, `$[${vcIndex}]`, wvc));
if (wvc.format === 'mso_mdoc') {
if (wvc.credential.docType !== _inputDescriptor.id) {
this.getResults().push(
this.generateErrorResult(index, `$[${vcIndex}]`, wvc, PexMessages.INPUT_DESCRIPTOR_ID_MATCHES_MDOC_DOCTYPE_DIDNT_PASS),
);
}

if (_inputDescriptor.constraints?.fields?.some((field) => field.filter !== undefined)) {
this.getResults().push(
this.generateErrorResult(index, `$[${vcIndex}]`, wvc, "Fields cannot have a 'filter' defined for mdoc credentials (ISO 18013-7)."),
);
}

if (_inputDescriptor.constraints?.fields?.some((field: FieldV2) => field.intent_to_retain === undefined)) {
this.getResults().push(
this.generateErrorResult(
index,
`$[${vcIndex}]`,
wvc,
"Fields must have 'intent_to_retain' defined for mdoc credentials (ISO 18013-7).",
),
);
}
}

this.getResults().push(
Expand All @@ -46,25 +69,12 @@ export class FormatRestrictionEvaluationHandler extends AbstractEvaluationHandle
this.updatePresentationSubmission(pd);
}

private generateInputDescriptorIdDoctypeErrorResult(idIdx: number, vcPath: string, wvc: WrappedVerifiableCredential): HandlerCheckResult {
return {
input_descriptor_path: `$.input_descriptors[${idIdx}]`,
evaluator: this.getName(),
status: Status.ERROR,
message: PexMessages.INPUT_DESCRIPTOR_ID_MATCHES_MDOC_DOCTYPE_DIDNT_PASS,
verifiable_credential_path: vcPath,
payload: {
format: wvc.format,
},
};
}

private generateErrorResult(idIdx: number, vcPath: string, wvc: WrappedVerifiableCredential): HandlerCheckResult {
private generateErrorResult(idIdx: number, vcPath: string, wvc: WrappedVerifiableCredential, message?: string): HandlerCheckResult {
return {
input_descriptor_path: `$.input_descriptors[${idIdx}]`,
evaluator: this.getName(),
status: Status.ERROR,
message: PexMessages.FORMAT_RESTRICTION_DIDNT_PASS,
message: message ?? PexMessages.FORMAT_RESTRICTION_DIDNT_PASS,
verifiable_credential_path: vcPath,
payload: {
format: wvc.format,
Expand Down
27 changes: 25 additions & 2 deletions lib/evaluation/handlers/inputDescriptorFilterEvaluationHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,13 @@ export class InputDescriptorFilterEvaluationHandler extends AbstractEvaluationHa
wrappedVcs.forEach((wvc: WrappedVerifiableCredential, vcIndex: number) => {
this.createNoFieldResults(pd, vcIndex, wvc);
fields.forEach((field) => {
const isPredicate = field.value.predicate !== undefined;

let inputField: { path: PathComponent[]; value: unknown }[] = [];
if (field.value.path) {
inputField = JsonPathUtils.extractInputField(wvc.decoded, field.value.path);
}

let resultFound = false;
for (const inputFieldKey of inputField) {
if (this.evaluateFilter(inputFieldKey, field.value)) {
Expand All @@ -54,6 +57,26 @@ export class InputDescriptorFilterEvaluationHandler extends AbstractEvaluationHa
this.getResults().push({
...this.createResultObject(jp.stringify(field.path.slice(0, 3)), vcIndex, payload),
});
} else if (
isPredicate &&
this.evaluateFilter(inputFieldKey, {
...field.value,
filter: {
type: 'boolean',
const: true,
},
})
) {
resultFound = true;
const payload = { result: { ...inputField[0] }, valid: true, format: wvc.format, predicate: true };
this.getResults().push({
...this.createResultObject(
jp.stringify(field.path.slice(0, 3)),
vcIndex,
payload,
PexMessages.INPUT_CANDIDATE_PREDICATE_VALUE_IS_ELIGIBLE_FOR_PRESENTATION_SUBMISSION,
),
});
}
}

Expand Down Expand Up @@ -109,13 +132,13 @@ export class InputDescriptorFilterEvaluationHandler extends AbstractEvaluationHa
});
}

private createResultObject(path: string, vcIndex: number, payload: unknown): HandlerCheckResult {
private createResultObject(path: string, vcIndex: number, payload: unknown, message?: string): HandlerCheckResult {
return {
input_descriptor_path: path,
verifiable_credential_path: `$[${vcIndex}]`,
evaluator: this.getName(),
status: Status.INFO,
message: PexMessages.INPUT_CANDIDATE_IS_ELIGIBLE_FOR_PRESENTATION_SUBMISSION,
message: message ?? PexMessages.INPUT_CANDIDATE_IS_ELIGIBLE_FOR_PRESENTATION_SUBMISSION,
payload,
};
}
Expand Down
69 changes: 64 additions & 5 deletions lib/evaluation/handlers/predicateRelatedFieldEvaluationHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export class PredicateRelatedFieldEvaluationHandler extends AbstractEvaluationHa
this.examinePredicateRelatedField(index, inDesc.constraints);
}
});
// this.updatePresentationSubmission(pdV1);
this.updatePresentationSubmission(pd);
}

private examinePredicateRelatedField(input_descriptor_idx: number, constraints: ConstraintsV1 | ConstraintsV2): void {
Expand Down Expand Up @@ -58,12 +58,37 @@ export class PredicateRelatedFieldEvaluationHandler extends AbstractEvaluationHa
constraints.fields[fieldIdx].path &&
constraints.fields[fieldIdx].path?.includes(this.concatenatePath(results[resultIdx].payload.result.path))
) {
const field = constraints.fields[fieldIdx];
const evaluationResult = { ...results[resultIdx].payload.result };
const resultObject = this.createResultObject(input_descriptor_idx, resultIdx, evaluationResult, results);
if (constraints.fields[fieldIdx].predicate === Optionality.Required) {

// We only support number with minimum/maximum for predicate type
if (
(field.filter?.type !== 'number' && field.filter?.type !== 'integer') ||
(!field.filter.minimum && !field.filter.exclusiveMinimum && !field.filter.maximum && !field.filter.exclusiveMaximum)
) {
results.push(
this.createErrorResultObject(
input_descriptor_idx,
resultIdx,
evaluationResult,
results,
"Only 'number' and 'integer' predicate with 'minimum', 'exclusiveMinimum', 'maximum', or 'exclusiveMaximum' supported.",
),
);
return;
}

if (evaluationResult.value === true) {
const resultObject = this.createResultObject(input_descriptor_idx, resultIdx, evaluationResult, results);
results.push(resultObject);
} else {
resultObject.payload['value'] = true;
} else if (field.predicate === Optionality.Required) {
const resultObject = this.createWarnResultObject(
input_descriptor_idx,
resultIdx,
evaluationResult,
results,
'Predicate is required but not applied',
);
results.push(resultObject);
}
}
Expand Down Expand Up @@ -106,4 +131,38 @@ export class PredicateRelatedFieldEvaluationHandler extends AbstractEvaluationHa
payload: evaluationResult,
};
}

private createWarnResultObject(
input_descriptor_idx: number,
resultIdx: number,
evaluationResult: unknown,
results: HandlerCheckResult[],
message: string,
): HandlerCheckResult {
return {
input_descriptor_path: `$.input_descriptors[${input_descriptor_idx}]`,
verifiable_credential_path: results[resultIdx].verifiable_credential_path,
evaluator: this.getName(),
status: Status.WARN,
message,
payload: evaluationResult,
};
}

private createErrorResultObject(
input_descriptor_idx: number,
resultIdx: number,
evaluationResult: unknown,
results: HandlerCheckResult[],
message: string,
): HandlerCheckResult {
return {
input_descriptor_path: `$.input_descriptors[${input_descriptor_idx}]`,
verifiable_credential_path: results[resultIdx].verifiable_credential_path,
evaluator: this.getName(),
status: Status.ERROR,
message,
payload: evaluationResult,
};
}
}
1 change: 1 addition & 0 deletions lib/types/Messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ enum PexMessages {
INPUT_CANDIDATE_DOESNT_CONTAIN_PROPERTY = 'Input candidate does not contain property',
INPUT_CANDIDATE_FAILED_FILTER_EVALUATION = 'Input candidate failed filter evaluation',
INPUT_CANDIDATE_IS_ELIGIBLE_FOR_PRESENTATION_SUBMISSION = 'The input candidate is eligible for submission',
INPUT_CANDIDATE_PREDICATE_VALUE_IS_ELIGIBLE_FOR_PRESENTATION_SUBMISSION = 'The input candidate is eligible for submission based on predicate value result',
INPUT_CANDIDATE_IS_NOT_ELIGIBLE_FOR_PRESENTATION_SUBMISSION = 'The input candidate is not eligible for submission',
INPUT_DESCRIPTOR_CONTEXT_CONTAINS_HASHLINK_VERIFICATION_NOT_SUPPORTED = "Input descriptor contains hashlink. This version doesn't support hashlink verification.",
LIMIT_DISCLOSURE_APPLIED = 'added variable in the limit_disclosure to the verifiableCredential',
Expand Down
12 changes: 6 additions & 6 deletions test/PEX.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1181,7 +1181,7 @@ describe('evaluate', () => {

it('should pass with jwt vp with submission data', function () {
const pdSchema: PresentationDefinitionV2 = {
id: '49768857',
id: '00000000-0000-0000-0000-000000000000',
input_descriptors: [
{
id: 'prc_type',
Expand Down Expand Up @@ -1250,12 +1250,12 @@ describe('evaluate', () => {
});
});

it('when single presentation is passed, it defaults to non-external submission', function () {
it.only('when single presentation is passed, it defaults to non-external submission', function () {
const pdSchema: PresentationDefinitionV2 = {
id: '49768857',
id: '00000000-0000-0000-0000-000000000000',
input_descriptors: [
{
id: 'prc_type',
id: '1',
name: 'Name',
purpose: 'We can only support a familyName in a Permanent Resident Card',
constraints: {
Expand All @@ -1277,15 +1277,15 @@ describe('evaluate', () => {
const evalResult: PresentationEvaluationResults = pex.evaluatePresentation(pdSchema, jwtEncodedVp);
expect(evalResult.errors).toEqual([]);
expect(evalResult.value?.descriptor_map[0]).toEqual({
id: 'prc_type',
id: '1',
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',
id: '00000000-0000-0000-0000-000000000000',
input_descriptors: [
{
id: 'prc_type',
Expand Down
58 changes: 58 additions & 0 deletions test/dif_pe_examples/pdV1/pd-schema-invalid-predicates.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"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": "Your age should be greater or equal to 18.",
"schema": [
{
"uri": "https://www.w3.org/TR/vc-data-model/#types"
}
],
"constraints": {
"limit_disclosure": "required",
"fields": [
{
"path": [
"$.credentialSubject.age",
"$.credentialSubject.details.age"
],
"filter": {
"type": "integer",
"minimum": 18
},
"predicate": "required"
}, {
"path": [
"$.credentialSubject.citizenship[*]",
"$.credentialSubject.details.citizenship[*]"
],
"filter": {
"type": "string",
"enum": [
"eu",
"us",
"uk"
]
},
"predicate": "required"
}, {
"path": [
"$.credentialSubject.country[*].abbr",
"$.credentialSubject.details.country[*].abbr"
],
"filter": {
"type": "string",
"pattern": "NLD"
},
"predicate": "required"
}
]
}
}
]
}
}
Loading