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

feature/SDK-34_multi-vp-fixes #172

Merged
merged 53 commits into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
4936ccf
chore: removed /src import
sanderPostma Sep 18, 2024
5e3432e
Merge branch 'develop' of https://github.com/Sphereon-Opensource/PEX …
sanderPostma Sep 18, 2024
197bc1b
Merge branch 'develop' of https://github.com/Sphereon-Opensource/PEX …
sanderPostma Sep 18, 2024
fd50b2e
chore: elligible -> eligible
sanderPostma Sep 18, 2024
f15f0af
chore: Added Northern Block pd/vcs
sanderPostma Sep 23, 2024
9165816
chore: version up
sanderPostma Sep 23, 2024
9fbec91
chore: fixed VP array and VC possibilities in vcResult.value
sanderPostma Sep 23, 2024
edfac4d
chore: version up
sanderPostma Sep 23, 2024
afebb9b
chore: handled vcResult.value.vp.* case
sanderPostma Sep 23, 2024
8378479
chore: handled CompactJWT case
sanderPostma Sep 23, 2024
ac1f4fe
chore: handled CompactJWT case
sanderPostma Sep 23, 2024
97c59fd
chore: comment
sanderPostma Sep 24, 2024
01b81ac
fix: fixed iat in kn-jwt
sanderPostma Sep 24, 2024
54f115a
chore: version up
sanderPostma Sep 24, 2024
644d9d6
Merge branch 'develop-20240918' into feature/multivp-integration
sanderPostma Sep 30, 2024
4a1bc26
chore: fix to get multi-vp working
sanderPostma Sep 30, 2024
6a0f80a
Merge branch 'feature/multivp-integration' into feature/nb-pd
sanderPostma Sep 30, 2024
09d4a98
chore: prettier & lint
sanderPostma Sep 30, 2024
39289ec
chore: paxSegments array
sanderPostma Oct 2, 2024
2ee8261
chore: paxSegments array
sanderPostma Oct 2, 2024
8f142fa
chore: fixed the replacing of the new lines to be platform-agnostic
sksadjad Oct 2, 2024
39e952f
chore: changed the presentation definition for iata to query the corr…
sksadjad Oct 2, 2024
9bd14f6
chore: changed the expect for iata test
sksadjad Oct 2, 2024
fdbcdb5
chore: updated the dependencies
sksadjad Oct 2, 2024
7ecc88c
chore: lockfile
sanderPostma Oct 2, 2024
fafb725
Merge branch 'feature/nb-pd' of https://github.com/Sphereon-Opensourc…
sanderPostma Oct 2, 2024
57deb6b
chore: ssi-types main release
sanderPostma Oct 2, 2024
21e61f3
chore: do jot fail CI on codecov
sanderPostma Oct 2, 2024
88eb3f9
chore: release unstable.11
sanderPostma Oct 2, 2024
057899f
chore: multi-vp fixes
sanderPostma Oct 2, 2024
f634b51
chore: multi-vp fixes
sanderPostma Oct 2, 2024
7a5f71c
chore: multi-vp fixes
sanderPostma Oct 2, 2024
546e01c
chore: multi-vp fixes
sanderPostma Oct 2, 2024
f0c86f4
chore: multi-vp fixes
sanderPostma Oct 3, 2024
6fafdb7
chore: iata test fixes
sanderPostma Oct 7, 2024
fe717b8
chore: reverted to 'fixes'
sanderPostma Oct 7, 2024
5d548f5
chore: prettier
sanderPostma Oct 7, 2024
5a98769
chore: version up
sanderPostma Oct 7, 2024
f31ed17
chore: version up
sanderPostma Oct 7, 2024
8d6b394
fix: Remove matchAll polyfil as it has issues when running RN in debu…
nklomp Oct 8, 2024
ac3cff5
Merge remote-tracking branch 'origin/feature/multi-vp-fixes' into fea…
sanderPostma Oct 8, 2024
bfff5d4
chore: typo
sanderPostma Oct 9, 2024
1e37b8a
chore: PR feedback
sanderPostma Oct 9, 2024
6b61f3a
chore: prettier & lint
sanderPostma Oct 9, 2024
9d37a75
chore: codecov fix
sanderPostma Oct 9, 2024
def0fdc
chore: added TODO
sanderPostma Oct 9, 2024
3fb8883
chore: removed test:cov script
sanderPostma Oct 9, 2024
7c9c934
chore: restart workflow
sanderPostma Oct 9, 2024
4065f49
chore: reverted codecov name
sanderPostma Oct 9, 2024
0474cfe
chore: extra error state The WrappedVerifiableCredentials input array…
sanderPostma Oct 9, 2024
7a71092
chore: up version
nklomp Oct 9, 2024
5ac9a66
chore: codecov fixes
nklomp Oct 9, 2024
dd98f31
fix: codecov fixes
nklomp Oct 9, 2024
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
8 changes: 4 additions & 4 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ jobs:
name: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pnpm/action-setup@v2
- uses: pnpm/action-setup@v4
with:
version: 8
- name: Use Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: '18.x'
cache: 'pnpm'
Expand All @@ -31,7 +31,7 @@ jobs:
- name: pnpm test
run: pnpm test
- name: codecov
uses: codecov/codecov-action@v2
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
name: codecove # optional
Expand Down
305 changes: 165 additions & 140 deletions lib/PEX.ts

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions lib/PEXv1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,13 +124,13 @@ export class PEXv1 extends PEX {
opts?.presentationSubmissionLocation ??
(hasSdJwtCredentials ? PresentationSubmissionLocation.EXTERNAL : PresentationSubmissionLocation.PRESENTATION);

const presentation = PEX.constructPresentation(selectedCredentials, {
const presentations = this.constructPresentations(selectedCredentials, {
...opts,
presentationSubmission: presentationSubmissionLocation === PresentationSubmissionLocation.PRESENTATION ? presentationSubmission : undefined,
});

return {
presentation,
presentations,
presentationSubmissionLocation,
presentationSubmission,
};
Expand Down
6 changes: 3 additions & 3 deletions lib/PEXv2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export class PEXv2 extends PEX {
* The evaluatePresentationV2 compares what is expected from a presentation with a presentationDefinitionV2.
*
* @param presentationDefinition the definition of what is expected in the presentation.
* @param presentation the presentation which has to be evaluated in comparison of the definition.
* @param presentations the presentation which has to be evaluated in comparison of the definition.
* @param opts - limitDisclosureSignatureSuites the credential signature suites that support limit disclosure
*
* @return the evaluation results specify what was expected and was fulfilled and also specifies which requirements described in the input descriptors
Expand Down Expand Up @@ -120,13 +120,13 @@ export class PEXv2 extends PEX {
opts?.presentationSubmissionLocation ??
(hasSdJwtCredentials ? PresentationSubmissionLocation.EXTERNAL : PresentationSubmissionLocation.PRESENTATION);

const presentation = PEX.constructPresentation(selectedCredentials, {
const presentations = this.constructPresentations(selectedCredentials, {
...opts,
presentationSubmission: presentationSubmissionLocation === PresentationSubmissionLocation.PRESENTATION ? presentationSubmission : undefined,
});

return {
presentation,
presentations,
presentationSubmissionLocation,
presentationSubmission,
};
Expand Down
3 changes: 1 addition & 2 deletions lib/evaluation/core/evaluationResults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ import { PresentationSubmission } from '@sphereon/pex-models';
import { IPresentation, IVerifiableCredential, OriginalVerifiablePresentation, SdJwtDecodedVerifiableCredential } from '@sphereon/ssi-types';

import { Checked, Status } from '../../ConstraintUtils';
import { OrArray } from '../../types';

export interface PresentationEvaluationResults extends Omit<EvaluationResults, 'verifiableCredential'> {
presentation: OrArray<OriginalVerifiablePresentation | IPresentation>;
presentations: Array<OriginalVerifiablePresentation | IPresentation>;
}

export interface EvaluationResults {
Expand Down
145 changes: 110 additions & 35 deletions lib/evaluation/evaluationClientWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { SubmissionRequirement } from '@sphereon/pex-models';
import {
CredentialMapper,
IVerifiableCredential,
IVerifiablePresentation,
OriginalVerifiableCredential,
SdJwtDecodedVerifiableCredential,
WrappedVerifiableCredential,
Expand Down Expand Up @@ -96,7 +97,11 @@ export class EvaluationClientWrapper {
marked,
);
} catch (e) {
const matchingError: Checked = { status: Status.ERROR, message: JSON.stringify(e), tag: 'matchSubmissionRequirements' };
const matchingError: Checked = {
status: Status.ERROR,
message: JSON.stringify(e),
tag: 'matchSubmissionRequirements',
};
return {
errors: errors ? [...errors, matchingError] : [matchingError],
warnings: warnings,
Expand Down Expand Up @@ -406,7 +411,7 @@ export class EvaluationClientWrapper {

const result: PresentationEvaluationResults = {
areRequiredCredentialsPresent: Status.INFO,
presentation: Array.isArray(wvps) ? wvps.map((wvp) => wvp.original) : wvps.original,
presentations: Array.isArray(wvps) ? wvps.map((wvp) => wvp.original) : [wvps.original],
errors: [],
warnings: [],
};
Expand All @@ -429,7 +434,9 @@ export class EvaluationClientWrapper {
if (this._client.generatePresentationSubmission && result.value && useExternalSubmission) {
// we map the descriptors of the generated submisison to take into account the nexted values
result.value.descriptor_map = result.value.descriptor_map.map((descriptor) => {
const [wvcResult] = JsonPathUtils.extractInputField(allWvcs, [descriptor.path]) as Array<{ value: WrappedVerifiableCredential }>;
const [wvcResult] = JsonPathUtils.extractInputField(allWvcs, [descriptor.path]) as Array<{
value: WrappedVerifiableCredential;
}>;
if (!wvcResult) {
throw new Error(`Could not find descriptor path ${descriptor.path} in wrapped verifiable credentials`);
}
Expand Down Expand Up @@ -460,7 +467,7 @@ export class EvaluationClientWrapper {
): { error: Checked; wvc: undefined } | { wvc: WrappedVerifiableCredential; error: undefined } {
// Decoded won't work for sd-jwt or jwt?!?!
const [vcResult] = JsonPathUtils.extractInputField(wvp.decoded, [descriptor.path]) as Array<{
value: string | IVerifiableCredential;
value: string | IVerifiablePresentation[] | IVerifiableCredential;
}>;

if (!vcResult) {
Expand All @@ -474,16 +481,48 @@ export class EvaluationClientWrapper {
};
}

// Find the wrapped VC based on the original VC
const originalVc = vcResult.value;
const wvc = wvp.vcs.find((wvc) => CredentialMapper.areOriginalVerifiableCredentialsEqual(wvc.original, originalVc));
// FIXME figure out possible types, can't see that in debug mode...
const isCredential = CredentialMapper.isCredential(vcResult.value as OriginalVerifiableCredential);
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`,
message: `Unable to find wrapped VC for the extracted credential at path "${descriptor.path}" in descriptor_path[${descriptorIndex}]`,
},
wvc: undefined,
};
Expand All @@ -508,7 +547,7 @@ export class EvaluationClientWrapper {
): PresentationEvaluationResults {
const result: PresentationEvaluationResults = {
areRequiredCredentialsPresent: Status.INFO,
presentation: Array.isArray(wvps) ? wvps.map((wvp) => wvp.original) : wvps.original,
presentations: Array.isArray(wvps) ? wvps.map((wvp) => wvp.original) : [wvps.original],
errors: [],
warnings: [],
value: submission,
Expand All @@ -517,45 +556,67 @@ export class EvaluationClientWrapper {
// 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)
(Array.isArray(wvps) || !CredentialMapper.isW3cPresentation(Array.isArray(wvps) ? wvps[0].presentation : 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];

let vp: WrappedVerifiablePresentation;
let vc: WrappedVerifiableCredential;
let vcPath: string;
// Iterate over each descriptor in the submission
for (const [descriptorIndex, descriptor] of submission.descriptor_map.entries()) {
let matchingVps: WrappedVerifiablePresentation[] = [];

if (presentationSubmissionLocation === PresentationSubmissionLocation.EXTERNAL) {
// Extract the VP from the wrapped VPs
const [vpResult] = JsonPathUtils.extractInputField(wvps, [descriptor.path]) as Array<{ value: WrappedVerifiablePresentation }>;
if (!vpResult) {
// Extract VPs matching the descriptor path
const vpResults = JsonPathUtils.extractInputField(wvps, [descriptor.path]) as Array<{
value: WrappedVerifiablePresentation[];
}>;

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_path[${descriptorIndex}] from presentation(s)`,
message: `Unable to extract path ${descriptor.path} for submission.descriptor_map[${descriptorIndex}] from presentation(s)`,
});
continue;
}
vp = vpResult.value;
vcPath = `presentation ${descriptor.path}`;

if (vp.format !== descriptor.format) {
// Flatten the array of VPs
const allVps = vpResults.flatMap((vpResult) => vpResult.value);

// Filter VPs that match the required format
matchingVps = allVps.filter((vp) => vp.format === descriptor.format);

if (!matchingVps.length) {
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}`,
message: `No VP at path ${descriptor.path} matches the required format ${descriptor.format}`,
});
continue;
}

// Log a warning if multiple VPs match the descriptor
if (matchingVps.length > 1) {
result.warnings?.push({
status: Status.WARN,
tag: 'MultipleVpsMatched',
message: `Multiple VPs matched for descriptor_path[${descriptorIndex}]. Using the first matching VP.`,
});
}
} else {
// When submission location is PRESENTATION, assume a single VP
matchingVps = Array.isArray(wvps) ? [wvps[0]] : [wvps];
}

// Process the first matching VP
const vp = matchingVps[0];
let vc: WrappedVerifiableCredential;
let vcPath: string = `presentation ${descriptor.path}`;

if (presentationSubmissionLocation === PresentationSubmissionLocation.EXTERNAL) {
if (descriptor.path_nested) {
const extractionResult = this.extractWrappedVcFromWrappedVp(descriptor.path_nested, descriptorIndex, vp);
const extractionResult = this.extractWrappedVcFromWrappedVp(descriptor.path_nested, descriptorIndex.toString(), vp);
if (extractionResult.error) {
result.areRequiredCredentialsPresent = Status.ERROR;
result.errors?.push(extractionResult.error);
Expand All @@ -565,6 +626,15 @@ export class EvaluationClientWrapper {
vc = extractionResult.wvc;
vcPath += ` with nested credential ${descriptor.path_nested.path}`;
} else if (descriptor.format === 'vc+sd-jwt') {
if (!vp.vcs || !vp.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 = vp.vcs[0];
} else {
result.areRequiredCredentialsPresent = Status.ERROR;
Expand All @@ -576,28 +646,28 @@ export class EvaluationClientWrapper {
continue;
}
} else {
// TODO: check that not longer than 0
vp = Array.isArray(wvps) ? wvps[0] : wvps;
vcPath = `credential ${descriptor.path}`;

const extractionResult = this.extractWrappedVcFromWrappedVp(descriptor, descriptorIndex, vp);
const extractionResult = this.extractWrappedVcFromWrappedVp(descriptor, descriptorIndex.toString(), vp);
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.
const holderDIDs = CredentialMapper.isW3cPresentation(vp.presentation) && vp.presentation.holder ? [vp.presentation.holder] : [];

// Get the presentation definition only for this descriptor, so we can evaluate it separately
// Determine holder DIDs
const holderDIDs =
CredentialMapper.isW3cPresentation(vp.presentation) && vp.presentation.holder ? [vp.presentation.holder] : opts?.holderDIDs || [];

// Get the presentation definition specific to the current descriptor
const pdForDescriptor = this.internalPresentationDefinitionForDescriptor(pd, descriptor.id);

// Reset the client on each iteration.
// Reset and configure the evaluation client on each iteration
this._client = new EvaluationClient();
this._client.evaluate(pdForDescriptor, [vc], {
...opts,
Expand All @@ -606,6 +676,7 @@ export class EvaluationClientWrapper {
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;
Expand All @@ -622,6 +693,7 @@ export class EvaluationClientWrapper {
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;
Expand Down Expand Up @@ -797,7 +869,10 @@ export class EvaluationClientWrapper {
presentationSubmissionLocation?: PresentationSubmissionLocation;
},
): PresentationSubmission {
if (!this._client.results) {
if (!this._client.results || this._client.results.length === 0) {
nklomp marked this conversation as resolved.
Show resolved Hide resolved
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) {
Expand Down
Loading
Loading