UNPKG

@sphereon/pex

Version:

A Typescript implementation of the v1 and v2 DIF Presentation Exchange specification

857 lines (856 loc) 115 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.EvaluationClientWrapper = void 0; const jsonpath_1 = require("@astronautlabs/jsonpath"); const pex_models_1 = require("@sphereon/pex-models"); const ssi_types_1 = require("@sphereon/ssi-types"); const ConstraintUtils_1 = require("../ConstraintUtils"); const PEX_1 = require("../PEX"); const signing_1 = require("../signing"); const types_1 = require("../types"); const utils_1 = require("../utils"); const formatMap_1 = require("../utils/formatMap"); const core_1 = require("./core"); const evaluationClient_1 = require("./evaluationClient"); class EvaluationClientWrapper { constructor() { this._client = new evaluationClient_1.EvaluationClient(); } getEvaluationClient() { return this._client; } selectFrom(presentationDefinition, wrappedVerifiableCredentials, opts) { var _a; let selectResults; this._client.evaluate(presentationDefinition, wrappedVerifiableCredentials, opts); const warnings = [...this.formatNotInfo(ConstraintUtils_1.Status.WARN)]; const errors = [...this.formatNotInfo(ConstraintUtils_1.Status.ERROR)]; if (presentationDefinition.submission_requirements) { const info = this._client.results.filter((result) => result.evaluator === 'MarkForSubmissionEvaluation' && result.payload.group && result.status !== ConstraintUtils_1.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: ConstraintUtils_1.Status.ERROR, message: JSON.stringify(e), tag: 'matchSubmissionRequirements', }; return { errors: errors ? [...errors, matchingError] : [matchingError], warnings: warnings, areRequiredCredentialsPresent: ConstraintUtils_1.Status.ERROR, }; } const matches = this.extractMatches(matchSubmissionRequirements); const credentials = matches.map((e) => jsonpath_1.JSONPath.nodes(this._client.wrappedVcs.map((wrapped) => wrapped.original), e)[0].value); const areRequiredCredentialsPresent = this.determineAreRequiredCredentialsPresent(presentationDefinition, matchSubmissionRequirements); selectResults = { errors: areRequiredCredentialsPresent === ConstraintUtils_1.Status.INFO ? [] : errors, matches: [...matchSubmissionRequirements], areRequiredCredentialsPresent, verifiableCredential: credentials, warnings, }; } else { const marked = this._client.results.filter((result) => result.evaluator === 'MarkForSubmissionEvaluation' && result.status !== ConstraintUtils_1.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) => jsonpath_1.JSONPath.nodes(this._client.wrappedVcs.map((wrapped) => wrapped.original), e)[0].value); selectResults = { errors: [], matches: [...matchSubmissionRequirements], areRequiredCredentialsPresent: ConstraintUtils_1.Status.INFO, verifiableCredential: credentials, warnings, }; } else { return { errors: errors, matches: [], areRequiredCredentialsPresent: ConstraintUtils_1.Status.ERROR, verifiableCredential: wrappedVerifiableCredentials.map((value) => value.original), warnings: warnings, }; } } this.fillSelectableCredentialsToVerifiableCredentialsMapping(selectResults, wrappedVerifiableCredentials); selectResults.areRequiredCredentialsPresent = this.determineAreRequiredCredentialsPresent(presentationDefinition, selectResults === null || selectResults === void 0 ? void 0 : selectResults.matches); this.remapMatches(wrappedVerifiableCredentials.map((wrapped) => wrapped.original), selectResults.matches, selectResults === null || selectResults === void 0 ? void 0 : selectResults.verifiableCredential); (_a = selectResults.matches) === null || _a === void 0 ? void 0 : _a.forEach((m) => { this.updateSubmissionRequirementMatchPathToAlias(m, 'verifiableCredential'); }); if (selectResults.areRequiredCredentialsPresent === ConstraintUtils_1.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 === null || submissionRequirementMatches === void 0 ? void 0 : submissionRequirementMatches.forEach((srm) => { if (srm.from_nested) { this.remapMatches(verifiableCredentials, srm.from_nested, vcsToSend); } else { srm.vc_path.forEach((match, index, matches) => { const vc = jsonpath_1.JSONPath.query(verifiableCredentials, match)[0]; const newIndex = vcsToSend === null || vcsToSend === void 0 ? void 0 : 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: ConstraintUtils_1.Status.ERROR, payload: `Not all the InputDescriptors are addressed`, }, ]; } const inputDescriptors = pd.input_descriptors; const markedInputDescriptorPaths = utils_1.ObjectUtils.getDistinctFieldInObject(marked, 'input_descriptor_path'); if (markedInputDescriptorPaths.length !== inputDescriptors.length) { const inputDescriptorsFromLogs = markedInputDescriptorPaths.map((value) => utils_1.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: ConstraintUtils_1.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: core_1.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 = utils_1.JsonPathUtils.extractInputField(pd, [idPath]); if (inputDescriptorResults.length) { const inputDescriptor = inputDescriptorResults[0].value; submissionRequirementMatches.push({ name: inputDescriptor.name || inputDescriptor.id, rule: pex_models_1.Rules.All, vc_path: [vcPath], type: core_1.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 = jsonpath_1.JSONPath.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) { var _a, _b, _c; this._client.evaluate(pd, wvcs, opts); const result = { areRequiredCredentialsPresent: ConstraintUtils_1.Status.INFO, // TODO: we should handle the string case verifiableCredential: wvcs.map((wrapped) => wrapped.original), }; result.warnings = this.formatNotInfo(ConstraintUtils_1.Status.WARN); result.errors = this.formatNotInfo(ConstraintUtils_1.Status.ERROR); this._client.assertPresentationSubmission(); if ((_a = this._client.presentationSubmission) === null || _a === void 0 ? void 0 : _a.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 = ((_c = (_b = result.value) === null || _b === void 0 ? void 0 : _b.descriptor_map) === null || _c === void 0 ? void 0 : _c.length) ? ConstraintUtils_1.Status.INFO : ConstraintUtils_1.Status.ERROR; return result; } evaluatePresentations(pd, wvps, opts) { var _a, _b, _c; // If submission is provided as input, we match the presentations against the submission. In this case the submission MUST be valid if (opts === null || opts === void 0 ? void 0 : 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: ConstraintUtils_1.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_1.EvaluationClient(); this._client.evaluate(pd, allWvcs, opts); result.warnings = this.formatNotInfo(ConstraintUtils_1.Status.WARN); result.errors = this.formatNotInfo(ConstraintUtils_1.Status.ERROR); this._client.assertPresentationSubmission(); if ((_a = this._client.presentationSubmission) === null || _a === void 0 ? void 0 : _a.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 === null || opts === void 0 ? void 0 : opts.presentationSubmissionLocation) !== undefined ? opts.presentationSubmissionLocation === signing_1.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] = utils_1.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 = ((_c = (_b = result.value) === null || _b === void 0 ? void 0 : _b.descriptor_map) === null || _c === void 0 ? void 0 : _c.length) ? ConstraintUtils_1.Status.INFO : ConstraintUtils_1.Status.ERROR; return result; } extractWrappedVcFromWrappedVp(descriptor, descriptorIndex, wvp) { // Decoded won't work for sd-jwt or jwt?!?! const [vcResult] = utils_1.JsonPathUtils.extractInputField(wvp.decoded, [descriptor.path]); if (!vcResult) { return { error: { status: ConstraintUtils_1.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 = ssi_types_1.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: ConstraintUtils_1.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) => ssi_types_1.CredentialMapper.areOriginalVerifiableCredentialsEqual(wrappedVc.original, originalVc)); if (!wvc) { return { error: { status: ConstraintUtils_1.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) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r; const result = { areRequiredCredentialsPresent: ConstraintUtils_1.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 = ConstraintUtils_1.Status.ERROR; (_a = result.errors) === null || _a === void 0 ? void 0 : _a.push({ status: ConstraintUtils_1.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 = (_b = opts === null || opts === void 0 ? void 0 : opts.presentationSubmissionLocation) !== null && _b !== void 0 ? _b : (Array.isArray(wvps) || !ssi_types_1.CredentialMapper.isW3cPresentation(Array.isArray(wvps) ? wvps[0].presentation : wvps.presentation) ? signing_1.PresentationSubmissionLocation.EXTERNAL : signing_1.PresentationSubmissionLocation.PRESENTATION); // Iterate over each descriptor in the submission for (const [descriptorIndex, descriptor] of submission.descriptor_map.entries()) { let matchingVp; if (presentationSubmissionLocation === signing_1.PresentationSubmissionLocation.EXTERNAL) { // Extract VPs matching the descriptor path const vpResults = utils_1.JsonPathUtils.extractInputField(wvps, [descriptor.path]); if (!vpResults.length) { result.areRequiredCredentialsPresent = ConstraintUtils_1.Status.ERROR; (_c = result.errors) === null || _c === void 0 ? void 0 : _c.push({ status: ConstraintUtils_1.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 = ConstraintUtils_1.Status.ERROR; (_d = result.errors) === null || _d === void 0 ? void 0 : _d.push({ status: ConstraintUtils_1.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 = ConstraintUtils_1.Status.ERROR; (_e = result.errors) === null || _e === void 0 ? void 0 : _e.push({ status: ConstraintUtils_1.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 = ConstraintUtils_1.Status.ERROR; (_f = result.errors) === null || _f === void 0 ? void 0 : _f.push({ status: ConstraintUtils_1.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 = ConstraintUtils_1.Status.ERROR; (_g = result.errors) === null || _g === void 0 ? void 0 : _g.push({ status: ConstraintUtils_1.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 === signing_1.PresentationSubmissionLocation.EXTERNAL) { if (descriptor.path_nested) { const extractionResult = this.extractWrappedVcFromWrappedVp(descriptor.path_nested, descriptorIndex.toString(), matchingVp); if (extractionResult.error) { result.areRequiredCredentialsPresent = ConstraintUtils_1.Status.ERROR; (_h = result.errors) === null || _h === void 0 ? void 0 : _h.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 = ConstraintUtils_1.Status.ERROR; (_j = result.errors) === null || _j === void 0 ? void 0 : _j.push({ status: ConstraintUtils_1.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 = ConstraintUtils_1.Status.ERROR; (_k = result.errors) === null || _k === void 0 ? void 0 : _k.push({ status: ConstraintUtils_1.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 = ConstraintUtils_1.Status.ERROR; (_l = result.errors) === null || _l === void 0 ? void 0 : _l.push({ status: ConstraintUtils_1.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 = ConstraintUtils_1.Status.ERROR; (_m = result.errors) === null || _m === void 0 ? void 0 : _m.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 = ssi_types_1.CredentialMapper.isW3cPresentation(matchingVp.presentation) && matchingVp.presentation.holder ? [matchingVp.presentation.holder] : (opts === null || opts === void 0 ? void 0 : opts.holderDIDs) || []; if (pd.input_descriptors.findIndex((_id) => _id.id === descriptor.id) === -1) { result.areRequiredCredentialsPresent = ConstraintUtils_1.Status.ERROR; (_o = result.errors) === null || _o === void 0 ? void 0 : _o.push({ status: ConstraintUtils_1.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_1.EvaluationClient(); this._client.evaluate(pdForDescriptor, [vc], Object.assign(Object.assign({}, 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 = ConstraintUtils_1.Status.ERROR; (_p = result.errors) === null || _p === void 0 ? void 0 : _p.push(...this.formatNotInfo(ConstraintUtils_1.Status.ERROR, submissionDescriptor, vcPath)); (_q = result.warnings) === null || _q === void 0 ? void 0 : _q.push(...this.formatNotInfo(ConstraintUtils_1.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) { (_r = result.errors) === null || _r === void 0 ? void 0 : _r.push({ status: ConstraintUtils_1.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 = ConstraintUtils_1.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 === pex_models_1.Rules.Pick ? submissionRequirement.max : undefined, minRequiredMatches: submissionRequirement.rule === pex_models_1.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 === pex_models_1.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) => { var _a; return (_a = descriptor.group) === null || _a === void 0 ? void 0 : _a.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 === pex_models_1.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 types_1.InternalPresentationDefinitionV2) { return new types_1.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 types_1.InternalPresentationDefinitionV1) { return new types_1.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 !== null && vcPath !== void 0 ? vcPath : `$.verifiableCredential${x.verifiable_credential_path.substring(1)}`; const _descriptorPath = descriptorPath !== null && descriptorPath !== void 0 ? 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 !== ConstraintUtils_1.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 === null || opts === void 0 ? void 0 : opts.presentationSubmissionLocation) === signing_1.PresentationSubmissionLocation.EXTERNAL) { this.updatePresentationSubmissionToExternal(); } return this._client.presentationSubmission; } const marked = this._client.results.filter((result) => result.evaluator === 'MarkForSubmissionEvaluation' && result.status !== ConstraintUtils_1.Status.ERROR); const updatedIndexes = this.matchUserSelectedVcs(marked, vcs); this.updatePresentationSubmission(updatedIndexes[1]); this.updatePresentationSubmissionPathToVpPath(); if ((opts === null || opts === void 0 ? void 0 : opts.presentationSubmissionLocation) === signing_1.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) { var _a; const descriptors = (_a = presentationSubmission === null || presentationSubmission === void 0 ? void 0 : presentationSubmission.descriptor_map) !== null && _a !== void 0 ? _a : 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_1.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 Object.assign(Object.assign({}, 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 } = (0, formatMap_1.getVpFormatForVcFormat)(descriptor.format); const newDescriptor = Object.assign(Object.assign({}, descriptor), { format: vpFormat, path: vpIndex !== undefined ? `$[${vpIndex}]` : '$' }); if (nestedCredentialPath) { newDescriptor.path_nested = Object.assign(Object.assign({}, 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 === pex_models_1.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 === pex_models_1.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; }));