@sphereon/pex
Version:
A Typescript implementation of the v1 and v2 DIF Presentation Exchange specification
939 lines (938 loc) • 111 kB
JavaScript
import { JSONPath as jp } from '@astronautlabs/jsonpath';
import { Rules } from '@sphereon/pex-models';
import { CredentialMapper, } from '@sphereon/ssi-types';
import { Status } from '../ConstraintUtils';
import { PEX } from '../PEX';
import { PresentationSubmissionLocation } from '../signing';
import { InternalPresentationDefinitionV1, InternalPresentationDefinitionV2, } from '../types';
import { JsonPathUtils, ObjectUtils } from '../utils';
import { getVpFormatForVcFormat } from '../utils/formatMap';
import { SubmissionRequirementMatchType, } from './core';
import { EvaluationClient } from './evaluationClient';
export class EvaluationClientWrapper {
_client;
constructor() {
this._client = new EvaluationClient();
}
getEvaluationClient() {
return this._client;
}
selectFrom(presentationDefinition, wrappedVerifiableCredentials, opts) {
let selectResults;
this._client.evaluate(presentationDefinition, wrappedVerifiableCredentials, opts);
const warnings = [...this.formatNotInfo(Status.WARN)];
const errors = [...this.formatNotInfo(Status.ERROR)];
if (presentationDefinition.submission_requirements) {
const info = this._client.results.filter((result) => result.evaluator === 'MarkForSubmissionEvaluation' && result.payload.group && result.status !== Status.ERROR);
const marked = Array.from(new Set(info));
let matchSubmissionRequirements;
try {
matchSubmissionRequirements = this.matchSubmissionRequirements(presentationDefinition, presentationDefinition.submission_requirements, marked);
}
catch (e) {
const matchingError = {
status: Status.ERROR,
message: JSON.stringify(e),
tag: 'matchSubmissionRequirements',
};
return {
errors: errors ? [...errors, matchingError] : [matchingError],
warnings: warnings,
areRequiredCredentialsPresent: Status.ERROR,
};
}
const matches = this.extractMatches(matchSubmissionRequirements);
const credentials = matches.map((e) => jp.nodes(this._client.wrappedVcs.map((wrapped) => wrapped.original), e)[0].value);
const areRequiredCredentialsPresent = this.determineAreRequiredCredentialsPresent(presentationDefinition, matchSubmissionRequirements);
selectResults = {
errors: areRequiredCredentialsPresent === Status.INFO ? [] : errors,
matches: [...matchSubmissionRequirements],
areRequiredCredentialsPresent,
verifiableCredential: credentials,
warnings,
};
}
else {
const marked = this._client.results.filter((result) => result.evaluator === 'MarkForSubmissionEvaluation' && result.status !== Status.ERROR);
const checkWithoutSRResults = this.checkWithoutSubmissionRequirements(marked, presentationDefinition);
if (!checkWithoutSRResults.length) {
const matchSubmissionRequirements = this.matchWithoutSubmissionRequirements(marked, presentationDefinition);
const matches = this.extractMatches(matchSubmissionRequirements);
const credentials = matches.map((e) => jp.nodes(this._client.wrappedVcs.map((wrapped) => wrapped.original), e)[0].value);
selectResults = {
errors: [],
matches: [...matchSubmissionRequirements],
areRequiredCredentialsPresent: Status.INFO,
verifiableCredential: credentials,
warnings,
};
}
else {
return {
errors: errors,
matches: [],
areRequiredCredentialsPresent: Status.ERROR,
verifiableCredential: wrappedVerifiableCredentials.map((value) => value.original),
warnings: warnings,
};
}
}
this.fillSelectableCredentialsToVerifiableCredentialsMapping(selectResults, wrappedVerifiableCredentials);
selectResults.areRequiredCredentialsPresent = this.determineAreRequiredCredentialsPresent(presentationDefinition, selectResults?.matches);
this.remapMatches(wrappedVerifiableCredentials.map((wrapped) => wrapped.original), selectResults.matches, selectResults?.verifiableCredential);
selectResults.matches?.forEach((m) => {
this.updateSubmissionRequirementMatchPathToAlias(m, 'verifiableCredential');
});
if (selectResults.areRequiredCredentialsPresent === Status.INFO) {
selectResults.errors = [];
}
else {
selectResults.errors = errors;
selectResults.warnings = warnings;
selectResults.verifiableCredential = wrappedVerifiableCredentials.map((value) => value.original);
}
return selectResults;
}
remapMatches(verifiableCredentials, submissionRequirementMatches, vcsToSend) {
submissionRequirementMatches?.forEach((srm) => {
if (srm.from_nested) {
this.remapMatches(verifiableCredentials, srm.from_nested, vcsToSend);
}
else {
srm.vc_path.forEach((match, index, matches) => {
const vc = jp.query(verifiableCredentials, match)[0];
const newIndex = vcsToSend?.findIndex((svc) => JSON.stringify(svc) === JSON.stringify(vc));
if (newIndex === -1) {
throw new Error(`The index of the VerifiableCredential in your current call can't be found in your previously submitted credentials. Are you trying to send a new Credential?\nverifiableCredential: ${vc}`);
}
matches[index] = `$[${newIndex}]`;
});
srm.name;
}
});
}
extractMatches(matchSubmissionRequirements) {
const matches = [];
matchSubmissionRequirements.forEach((e) => {
matches.push(...e.vc_path);
if (e.from_nested) {
matches.push(...this.extractMatches(e.from_nested));
}
});
return Array.from(new Set(matches));
}
/**
* Since this is without SubmissionRequirements object, each InputDescriptor has to have at least one corresponding VerifiableCredential
* @param marked: info logs for `MarkForSubmissionEvaluation` handler
* @param pd
* @private
*/
checkWithoutSubmissionRequirements(marked, pd) {
const checkResult = [];
if (!pd.input_descriptors) {
return [];
}
if (!marked.length) {
return [
{
input_descriptor_path: '',
evaluator: 'checkWithoutSubmissionRequirement',
verifiable_credential_path: '',
status: Status.ERROR,
payload: `Not all the InputDescriptors are addressed`,
},
];
}
const inputDescriptors = pd.input_descriptors;
const markedInputDescriptorPaths = ObjectUtils.getDistinctFieldInObject(marked, 'input_descriptor_path');
if (markedInputDescriptorPaths.length !== inputDescriptors.length) {
const inputDescriptorsFromLogs = markedInputDescriptorPaths.map((value) => JsonPathUtils.extractInputField(pd, [value])[0].value).map((value) => value.id);
for (let i = 0; i < pd.input_descriptors.length; i++) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (inputDescriptorsFromLogs.indexOf(pd.input_descriptors[i].id) == -1) {
checkResult.push({
input_descriptor_path: `$.input_descriptors[${i}]`,
evaluator: 'checkWithoutSubmissionRequirement',
verifiable_credential_path: '',
status: Status.ERROR,
payload: `Not all the InputDescriptors are addressed`,
});
}
}
}
return checkResult;
}
matchSubmissionRequirements(pd, submissionRequirements, marked) {
const submissionRequirementMatches = [];
for (const [srIndex, sr] of Object.entries(submissionRequirements)) {
// Create a default SubmissionRequirementMatch object
const srm = {
rule: sr.rule,
vc_path: [],
name: sr.name,
type: SubmissionRequirementMatchType.SubmissionRequirement,
id: Number(srIndex),
};
if (sr.from) {
srm.from = sr.from;
}
// Assign min, max, and count regardless of 'from' or 'from_nested'
sr.min ? (srm.min = sr.min) : undefined;
sr.max ? (srm.max = sr.max) : undefined;
sr.count ? (srm.count = sr.count) : undefined;
if (sr.from) {
const matchingVcPaths = this.getMatchingVcPathsForSubmissionRequirement(pd, sr, marked);
srm.vc_path.push(...matchingVcPaths);
submissionRequirementMatches.push(srm);
}
else if (sr.from_nested) {
// Recursive call to matchSubmissionRequirements for nested requirements
try {
srm.from_nested = this.matchSubmissionRequirements(pd, sr.from_nested, marked);
submissionRequirementMatches.push(srm);
}
catch (err) {
throw new Error(`Error in handling value of from_nested: ${sr.from_nested}: err: ${err}`);
}
}
else {
// Throw an error if neither 'from' nor 'from_nested' is found
throw new Error("Invalid SubmissionRequirement object: Must contain either 'from' or 'from_nested'");
}
}
return submissionRequirementMatches;
}
matchWithoutSubmissionRequirements(marked, pd) {
const submissionRequirementMatches = [];
const partitionedIdToVcMap = this.createIdToVcMap(marked);
for (const [idPath, sameIdVcs] of partitionedIdToVcMap.entries()) {
if (!sameIdVcs || !sameIdVcs.length) {
continue;
}
for (const vcPath of sameIdVcs) {
const inputDescriptorResults = JsonPathUtils.extractInputField(pd, [idPath]);
if (inputDescriptorResults.length) {
const inputDescriptor = inputDescriptorResults[0].value;
submissionRequirementMatches.push({
name: inputDescriptor.name || inputDescriptor.id,
rule: Rules.All,
vc_path: [vcPath],
type: SubmissionRequirementMatchType.InputDescriptor,
id: inputDescriptor.id,
});
}
}
}
return this.removeDuplicateSubmissionRequirementMatches(submissionRequirementMatches);
}
getMatchingVcPathsForSubmissionRequirement(pd, sr, marked) {
const vcPaths = new Set();
if (!sr.from)
return Array.from(vcPaths);
for (const m of marked) {
const inputDescriptor = jp.query(pd, m.input_descriptor_path)[0];
if (inputDescriptor.group && inputDescriptor.group.indexOf(sr.from) === -1) {
continue;
}
if (m.payload.group.includes(sr.from)) {
vcPaths.add(m.verifiable_credential_path);
}
}
return Array.from(vcPaths);
}
evaluate(pd, wvcs, opts) {
this._client.evaluate(pd, wvcs, opts);
const result = {
areRequiredCredentialsPresent: Status.INFO,
// TODO: we should handle the string case
verifiableCredential: wvcs.map((wrapped) => wrapped.original),
};
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));
}
if (this._client.generatePresentationSubmission) {
this.updatePresentationSubmissionPathToVpPath(result.value);
}
result.verifiableCredential = this._client.wrappedVcs.map((wrapped) => wrapped.original);
result.areRequiredCredentialsPresent = result.value?.descriptor_map?.length ? Status.INFO : Status.ERROR;
return result;
}
evaluatePresentations(pd, wvps, opts) {
// 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], []);
const result = {
areRequiredCredentialsPresent: Status.INFO,
presentations: Array.isArray(wvps) ? wvps.map((wvp) => wvp.original) : [wvps.original],
errors: [],
warnings: [],
};
// Reset and configure the evaluation client on each iteration
this._client = new EvaluationClient();
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 submission to take into account the nexted values
result.value.descriptor_map = result.value.descriptor_map.map((descriptor) => {
const [wvcResult] = JsonPathUtils.extractInputField(allWvcs, [descriptor.path]);
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.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;
}
extractWrappedVcFromWrappedVp(descriptor, descriptorIndex, wvp) {
// Decoded won't work for sd-jwt or jwt?!?!
const [vcResult] = JsonPathUtils.extractInputField(wvp.decoded, [descriptor.path]);
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,
};
}
// FIXME figure out possible types, can't see that in debug mode...
const isCredential = CredentialMapper.isCredential(vcResult.value);
if (!vcResult.value ||
(typeof vcResult.value === 'string' && !isCredential) ||
(typeof vcResult.value !== 'string' && !isCredential && !('verifiableCredential' in vcResult.value || 'vp' in vcResult.value))) {
return {
error: {
status: Status.ERROR,
tag: 'NoVerifiableCredentials',
message: `No verifiable credentials found at path "${descriptor.path}" for submission.descriptor_path[${descriptorIndex}]`,
},
wvc: undefined,
};
}
// When result is an array, extract the first Verifiable Credential from the array FIXME figure out proper types, can't see that in debug mode...
let originalVc;
if (isCredential) {
originalVc = vcResult.value;
}
else if (typeof vcResult.value !== 'string') {
if ('verifiableCredential' in vcResult.value) {
originalVc = Array.isArray(vcResult.value.verifiableCredential)
? vcResult.value.verifiableCredential[0]
: vcResult.value.verifiableCredential;
}
else {
throw Error('Could not deduce original VC from evaluation result');
}
}
else {
throw Error('Could not deduce original VC from evaluation result');
}
// Find the corresponding Wrapped Verifiable Credential (wvc) based on the original VC
const wvc = wvp.vcs.find((wrappedVc) => CredentialMapper.areOriginalVerifiableCredentialsEqual(wrappedVc.original, originalVc));
if (!wvc) {
return {
error: {
status: Status.ERROR,
tag: 'SubmissionPathNotFound',
message: `Unable to find wrapped VC for the extracted credential at path "${descriptor.path}" in descriptor_path[${descriptorIndex}]`,
},
wvc: undefined,
};
}
return {
wvc,
error: undefined,
};
}
evaluatePresentationsAgainstSubmission(pd, wvps, submission, opts) {
const result = {
areRequiredCredentialsPresent: Status.INFO,
presentations: Array.isArray(wvps) ? wvps.map((wvp) => wvp.original) : [wvps.original],
errors: [],
warnings: [],
value: submission,
};
if (submission.definition_id !== pd.id) {
result.areRequiredCredentialsPresent = Status.ERROR;
result.errors?.push({
status: Status.ERROR,
tag: 'SubmissionDefinitionIdNotFound',
message: `Presentation submission defines definition_id '${submission.definition_id}', but the provided definition has id '${pd.id}'`,
});
}
// 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(Array.isArray(wvps) ? wvps[0].presentation : wvps.presentation)
? PresentationSubmissionLocation.EXTERNAL
: PresentationSubmissionLocation.PRESENTATION);
// Iterate over each descriptor in the submission
for (const [descriptorIndex, descriptor] of submission.descriptor_map.entries()) {
let matchingVp;
if (presentationSubmissionLocation === PresentationSubmissionLocation.EXTERNAL) {
// Extract VPs matching the descriptor path
const vpResults = JsonPathUtils.extractInputField(wvps, [descriptor.path]);
if (!vpResults.length) {
result.areRequiredCredentialsPresent = Status.ERROR;
result.errors?.push({
status: Status.ERROR,
tag: 'SubmissionPathNotFound',
message: `Unable to extract path ${descriptor.path} for submission.descriptor_map[${descriptorIndex}] from presentation(s)`,
});
continue;
}
else if (vpResults.length > 1) {
result.areRequiredCredentialsPresent = Status.ERROR;
result.errors?.push({
status: Status.ERROR,
tag: 'SubmissionPathMultipleEntries',
message: `Extraction of path ${descriptor.path} for submission.descriptor_map[${descriptorIndex}] resulted in multiple values being returned.`,
});
continue;
}
matchingVp = vpResults[0].value;
if (Array.isArray(matchingVp)) {
result.areRequiredCredentialsPresent = Status.ERROR;
result.errors?.push({
status: Status.ERROR,
tag: 'SubmissionPathMultipleEntries',
message: `Extraction of path ${descriptor.path} for submission.descriptor_map[${descriptorIndex}] returned multiple entires. This is probably because the submission uses '$' to reference the presentation, while an array was used (thus all presentations are selected). Make sure the submission uses the correct path.`,
});
continue;
}
if (!matchingVp) {
result.areRequiredCredentialsPresent = Status.ERROR;
result.errors?.push({
status: Status.ERROR,
tag: 'SubmissionPathNotFound',
message: `Extraction of path ${descriptor.path} for submission.descriptor_map[${descriptorIndex}] succeeded, but the value was undefined.`,
});
continue;
}
if (matchingVp.format !== descriptor.format) {
result.areRequiredCredentialsPresent = Status.ERROR;
result.errors?.push({
status: Status.ERROR,
tag: 'SubmissionFormatNoMatch',
message: `The VP at path ${descriptor.path} does not match the required format ${descriptor.format}`,
});
continue;
}
}
else {
// When submission location is PRESENTATION, assume a single VP
matchingVp = Array.isArray(wvps) ? wvps[0] : wvps;
}
let vc;
let vcPath = `presentation ${descriptor.path}`;
if (presentationSubmissionLocation === PresentationSubmissionLocation.EXTERNAL) {
if (descriptor.path_nested) {
const extractionResult = this.extractWrappedVcFromWrappedVp(descriptor.path_nested, descriptorIndex.toString(), matchingVp);
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') {
if (!matchingVp.vcs || !matchingVp.vcs.length) {
result.areRequiredCredentialsPresent = Status.ERROR;
result.errors?.push({
status: Status.ERROR,
tag: 'NoCredentialsFound',
message: `No credentials found in VP at path ${descriptor.path}`,
});
continue;
}
vc = matchingVp.vcs[0];
}
else if (descriptor.format === 'mso_mdoc') {
// We already know the format is mso_mdoc so this cast is safe
const vcs = matchingVp.vcs;
vcPath += ` with nested mdoc with doctype ${descriptor.id}`;
const matchingVc = vcs.find((vc) => descriptor.id === vc.credential.docType.asStr);
if (!matchingVc) {
const allDoctypes = vcs.map((vc) => `'${vc.credential.docType.asStr}'`).join(', ');
result.areRequiredCredentialsPresent = Status.ERROR;
result.errors?.push({
status: Status.ERROR,
tag: 'NoCredentialsFound',
message: `No mdoc credential with doctype '${descriptor.id}' found in mdoc vp. Available documents are ${allDoctypes}`,
});
continue;
}
vc = matchingVc;
}
else {
result.areRequiredCredentialsPresent = Status.ERROR;
result.errors?.push({
status: Status.ERROR,
tag: 'UnsupportedFormat',
message: `VP format ${matchingVp.format} is not supported`,
});
continue;
}
}
else {
const extractionResult = this.extractWrappedVcFromWrappedVp(descriptor, descriptorIndex.toString(), matchingVp);
if (extractionResult.error) {
result.areRequiredCredentialsPresent = Status.ERROR;
result.errors?.push(extractionResult.error);
continue;
}
vc = extractionResult.wvc;
vcPath = `credential ${descriptor.path}`;
}
// 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.
// Determine holder DIDs
const holderDIDs = CredentialMapper.isW3cPresentation(matchingVp.presentation) && matchingVp.presentation.holder
? [matchingVp.presentation.holder]
: opts?.holderDIDs || [];
if (pd.input_descriptors.findIndex((_id) => _id.id === descriptor.id) === -1) {
result.areRequiredCredentialsPresent = Status.ERROR;
result.errors?.push({
status: Status.ERROR,
tag: 'SubmissionInputDescriptorIdNotFound',
message: `Submission references descriptor id '${descriptor.id}' but presentation definition with id '${pd.id}' does not have an input descriptor with this id. Available input descriptors are ${pd.input_descriptors.map((i) => `'${i.id}'`).join(', ')}`,
});
}
else {
// Get the presentation definition specific to the current descriptor
const pdForDescriptor = this.internalPresentationDefinitionForDescriptor(pd, descriptor.id);
// Reset and configure the evaluation client on each iteration
this._client = new EvaluationClient();
this._client.evaluate(pdForDescriptor, [vc], {
...opts,
holderDIDs,
presentationSubmission: undefined,
generatePresentationSubmission: undefined,
});
// Check if the evaluation resulted in exactly one descriptor map entry
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;
}
checkIfSubmissionSatisfiesSubmissionRequirement(pd, submission, submissionRequirement, submissionRequirementName) {
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 = {
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));
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
*/
validateIfSubmissionSatisfiesDefinition(pd, submission) {
const submissionDescriptorIds = submission.descriptor_map.map((descriptor) => descriptor.id);
const result = {
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.submissionRequirementResults = submissionRequirementResults;
if (result.totalMatches !== result.totalRequiredMatches) {
result.error = `Expected all submission requirements (${result.totalRequiredMatches}) to be satisfied 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.map((i) => `'${i.id}'`).join(', ')}) to be satisfied in submission, but found ${submissionDescriptorIds.map((i) => `'${i}'`).join(',')}. Missing ${notInSubmission.map((d) => `'${d.id}'`).join(', ')}`;
}
}
result.doesSubmissionSatisfyDefinition = result.error === undefined;
return result;
}
internalPresentationDefinitionForDescriptor(pd, descriptorId) {
const inputDescriptorIndex = pd.input_descriptors.findIndex((i) => i.id === descriptorId);
// If we receive a submission with input descriptors that do not exist
if (inputDescriptorIndex === -1) {
throw new Error(`Input descriptor with id '${descriptorId}' not found in presentation definition with id '${pd.id}'. Available input descriptors are ${pd.input_descriptors.map((i) => `'${i.id}'`).join(', ')}`);
}
if (pd instanceof InternalPresentationDefinitionV2) {
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) {
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');
}
formatNotInfo(status, descriptorPath, vcPath) {
return this._client.results
.filter((result) => result.status === status)
.map((x) => {
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}: ${_descriptorPath}: ${_vcPath}`,
};
});
}
submissionFrom(pd, vcs, opts) {
if (!this._client.results || this._client.results.length === 0) {
if (vcs.length === 0) {
throw Error('The WrappedVerifiableCredentials input array is empty');
}
throw Error('You need to call evaluate() before pex.presentationFrom()');
}
if (!this._client.generatePresentationSubmission) {
return this._client.presentationSubmission;
}
if (pd.submission_requirements) {
const marked = this._client.results.filter((result) => result.evaluator === 'MarkForSubmissionEvaluation' && result.payload.group && result.status !== Status.ERROR);
const [updatedMarked, upIdx] = this.matchUserSelectedVcs(marked, vcs);
const groupCount = new Map();
//TODO instanceof fails in some cases, need to check how to fix it
if ('input_descriptors' in pd) {
pd.input_descriptors.forEach((e) => {
if (e.group) {
e.group.forEach((key) => {
if (groupCount.has(key)) {
groupCount.set(key, groupCount.get(key) + 1);
}
else {
groupCount.set(key, 1);
}
});
}
});
}
const result = this.evaluateRequirements(pd.submission_requirements, updatedMarked, groupCount, 0);
const finalIdx = upIdx.filter((ui) => result[1].find((r) => r.verifiable_credential_path === ui[1]));
this.updatePresentationSubmission(finalIdx);
this.updatePresentationSubmissionPathToVpPath();
if (opts?.presentationSubmissionLocation === PresentationSubmissionLocation.EXTERNAL) {
this.updatePresentationSubmissionToExternal();
}
return this._client.presentationSubmission;
}
const marked = this._client.results.filter((result) => result.evaluator === 'MarkForSubmissionEvaluation' && result.status !== Status.ERROR);
const updatedIndexes = this.matchUserSelectedVcs(marked, vcs);
this.updatePresentationSubmission(updatedIndexes[1]);
this.updatePresentationSubmissionPathToVpPath();
if (opts?.presentationSubmissionLocation === PresentationSubmissionLocation.EXTERNAL) {
this.updatePresentationSubmissionToExternal();
}
return this._client.presentationSubmission;
}
updatePresentationSubmission(updatedIndexes) {
if (!this._client.generatePresentationSubmission) {
return; // never update a supplied submission
}
this._client.presentationSubmission.descriptor_map = this._client.presentationSubmission.descriptor_map
.filter((descriptor) => updatedIndexes.find((ui) => ui[0] === descriptor.path))
.map((descriptor) => {
const result = updatedIndexes.find((ui) => ui[0] === descriptor.path);
if (result) {
descriptor.path = result[1];
}
return descriptor;
});
}
updatePresentationSubmissionToExternal(presentationSubmission) {
const descriptors = presentationSubmission?.descriptor_map ?? this._client.presentationSubmission.descriptor_map;
// 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 {
...presentationSubmission,
descriptor_map: updatedDescriptors,
};
}
this._client.presentationSubmission.descriptor_map = updatedDescriptors;
return this._client.presentationSubmission;
}
updateDescriptorToExternal(descriptor, { vpIndex, vcIndex, } = {}) {
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('$.verifiableCredential', nestedCredentialPath).replace('$[', `${nestedCredentialPath}[`),
};
}
return newDescriptor;
}
matchUserSelectedVcs(marked, vcs) {
const userSelected = vcs.map((vc, index) => [index, JSON.stringify(vc.original)]);
const allCredentials = this._client.wrappedVcs.map((vc, index) => [index, JSON.stringify(vc.original)]);
const updatedIndexes = [];
userSelected.forEach((us, i) => {
allCredentials.forEach((ac, j) => {
if (ac[1] === us[1]) {
updatedIndexes.push([`$[${j}]`, `$[${i}]`]);
}
});
});
marked = marked
.filter((m) => updatedIndexes.find((ui) => ui[0] === m.verifiable_credential_path))
.map((m) => {
const index = updatedIndexes.find((ui) => ui[0] === m.verifiable_credential_path);
if (index) {
m.verifiable_credential_path = index[1];
}
return m;
});
return [marked, updatedIndexes];
}
evaluateRequirements(submissionRequirement, marked, groupCount, level) {
let total = 0;
const result = [];
for (const sr of submissionRequirement) {
if (sr.from) {
if (sr.rule === Rules.All) {
const [count, matched] = this.countMatchingInputDescriptors(sr, marked);
if (count !== (groupCount.get(sr.from) || 0)) {
throw Error(`Not all input descriptors are members of group ${sr.from}`);
}
total++;
result.push(...matched);
}
else if (sr.rule === Rules.Pick) {
const [count, matched] = this.countMatchingInputDescriptors(sr, marked);
try {
this.handleCount(sr, count, level);
total++;
}
catch (error) {
if (level === 0)
throw error;
}
result.push(...matched);
}
}
else if (sr.from_nested) {
const [count, matched] = this.evaluateRequirements(sr.from_nested, marked, groupCount, ++level);
total += count;
result.push(...matched);
this.handleCount(sr, count, level);
}
}
return [total, result];
}
countMatchingInputDescriptors(submissionRequirement, marked) {
let count = 0;
const matched = [];
for (const m of marked) {
if (m.payload.group.includes(submissionRequirement.from)) {
matched.push(m);
count++;
}
}
return [count, matched];
}
handleCount(submissionRequirement, count, level) {
if (submissionRequirement.count) {
if (count !== submissionRequirement.count) {
throw Error(`Count: expected: ${submissionRequirement.count} actual: ${count} at level: ${level}`);
}
}
if (submissionRequirement.min) {
if (count < submissionRequirement.min) {
throw Error(`Min: expected: ${submissionRequirement.min} actual: ${count} at level: ${level}`);
}
}
if (submissionRequirement.max) {
if (count > submissionRequirement.max) {
throw Error(`Max: expected: ${submissionRequirement.max} actual: ${count} at level: ${level}`);
}
}
}
removeDuplicateSubmissionRequirementMatches(matches) {
return matches.filter((match, index) => {
const _match = JSON.stringify(match);
return (index ===
matches.findIndex((obj) => {
return JSON.stringify(obj) === _match;
}));
});
}
fillSelectableCredentialsToVerifiableCredentialsMapping(selectResults, wrappedVcs) {
if (selectResults) {
selectResults.verifiableCredential?.forEach((selectableCredential) => {
const foundIndex = wrappedVcs.findIndex((wrappedVc) => CredentialMapper.areOriginalVerifiableCredentialsEqual(wrappedVc.original, selectableCredential));
if (foundIndex === -1) {
throw new Error('index is not right');
}
selectResults.vcIndexes
? !selectResults.vcIndexes.includes(foundIndex) && selectResults.vcIndexes.push(foundIndex)
: (selectResults.vcIndexes = [foundIndex]);
});
}
}
determineAreRequiredCredentialsPresent(presentationDefinition, matchSubmissionRequirements, parentMsr) {
if (!matchSubmissionRequirements || !matchSubmissionRequirements.length) {
return Status.ERROR;
}
// collect child statuses
const childStatuses = matchSubmissionRequirements.map((m) => this.determineSubmissionRequirementStatus(presentationDefinition, m));
// decide status based on child statuses and parent's rule
if (!parentMsr) {
if (childStatuses.includes(Status.ERROR)) {
return Status.ERROR;
}
else if (childStatuses.includes(Status.WARN)) {
return Status.WARN;
}
else {
return Status.INFO;
}
}
else {
if (parentMsr.rule === Rules.All && childStatuses.includes(Status.ERROR)) {
return Status.ERROR;
}
const nonErrStatCount = childStatuses.filter((status) => status !== Status.ERROR).length;
if (parentMsr.count) {
return parentMsr.count > nonErrStatCount ? Status.ERROR : parentMsr.count < nonErrStatCount ? Status.WARN : Status.INFO;
}
else {
if (parentMsr.min && parentMsr.min > nonErrStatCount) {
return Status.ERROR;
}
else if (parentMsr.max && parentMsr.max < nonErrStatCount) {
return Status.WARN;
}
}
}
return Status.INFO;
}
determineSubmissionRequirementStatus(pd, m) {
if (m.from && m.from_nested) {
throw new Error('Invalid submission_requirement object: MUST contain either a from or from_nested property.');
}
if (!m.from && !m.from_nested && m.vc_path.length !== 1) {
return Status.ERROR;
}
if (m.from) {
const groupCount = this.countGroupIDs(pd.input_descriptors, m.from);
switch (m.rule) {
case Rules.All:
// Ensure that all descriptors associated with `m.from` are satisfied.
return m.vc_path.length === groupCount ? Status.INFO : Status.WARN;
case Rules.Pick:
return this.getPickRuleStatus(m);
default:
return Status.ERROR;
}
}
else if (m.from_nested) {
return this.determineAreRequiredCredentialsPresent(pd, m.from_nested, m);