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 42 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
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
name: codecove # optional
flags: unittest
fail_ci_if_error: true # optional (default = false)
fail_ci_if_error: false # optional (default = false)
nklomp marked this conversation as resolved.
Show resolved Hide resolved
#directory: ./coverage/reports/
#files: ./coverage1.xml,./coverage2.xml
verbose: true # optional (default = false)
267 changes: 129 additions & 138 deletions lib/PEX.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export class PEX {
let presentationSubmission = opts?.presentationSubmission;
let presentationSubmissionLocation =
opts?.presentationSubmissionLocation ??
(Array.isArray(presentations) || !CredentialMapper.isW3cPresentation(wrappedPresentations[0].presentation)
((Array.isArray(presentations) && presentations.length > 1) || !CredentialMapper.isW3cPresentation(wrappedPresentations[0].presentation)
nklomp marked this conversation as resolved.
Show resolved Hide resolved
? PresentationSubmissionLocation.EXTERNAL
: PresentationSubmissionLocation.PRESENTATION);

Expand Down Expand Up @@ -290,21 +290,21 @@ export class PEX {
opts?.presentationSubmissionLocation ??
(hasSdJwtCredentials ? PresentationSubmissionLocation.EXTERNAL : PresentationSubmissionLocation.PRESENTATION);

const presentation = PEX.constructPresentation(selectedCredentials, {
const presentations = this.constructPresentations(selectedCredentials, {
...opts,
// We only pass in the submission in case it needs to be included in the presentation
presentationSubmission: presentationSubmissionLocation === PresentationSubmissionLocation.PRESENTATION ? presentationSubmission : undefined,
hasher: this.options?.hasher,
});

this.updateSdJwtCredentials(presentations);
return {
presentation,
presentations,
presentationSubmissionLocation,
presentationSubmission,
};
}

public static constructPresentation(
public constructPresentations(
selectedCredentials: OriginalVerifiableCredential | OriginalVerifiableCredential[],
opts?: {
presentationSubmission?: PresentationSubmission;
Expand All @@ -315,108 +315,89 @@ export class PEX {
*/
hasher?: Hasher;
},
): IPresentation | PartialSdJwtDecodedVerifiableCredential {
const credentials = Array.isArray(selectedCredentials) ? selectedCredentials : [selectedCredentials];

// for SD-JWT we want to return the SD-JWT with only the needed disclosures (so filter disclosures array, and update the compactSdJwt)
// in addition we want to create the KB-JWT payload as well.
// FIXME: include the KB-JWT payload?
if (credentials.some((c) => CredentialMapper.isSdJwtDecodedCredential(c) || CredentialMapper.isSdJwtEncoded(c))) {
if (credentials.length > 1) {
// Until there's some consensus around the following issue, we'll only support a single
// SD-JWT credential in a presentation
// https://github.com/decentralized-identity/presentation-exchange/issues/462
throw new Error('Only a single credential is supported when creating a presentation with an SD-JWT VC');
}

if (opts?.presentationSubmission) {
throw new Error('Presentation submission cannot be included in the presentation when creating a presentation with an SD-JWT VC');
}

if (opts?.basePresentationPayload) {
throw new Error('Base presentation payload cannot be when creating a presentation from an SD-JWT VC');
}

// NOTE: we assume the credential already has selective disclosure applied, even if it is encoded. Is
// that a valid assumption? It seems to be this way for BBS SD as well
const decoded = (
CredentialMapper.isSdJwtEncoded(credentials[0]) ? CredentialMapper.decodeVerifiableCredential(credentials[0], opts?.hasher) : credentials[0]
) as SdJwtDecodedVerifiableCredential;

if (!opts?.hasher) {
): Array<IPresentation | PartialSdJwtDecodedVerifiableCredential> {
if (!selectedCredentials) {
throw Error(`At least a verifiable credential needs to be passed in to create a presentation`);
}
const verifiableCredential = (Array.isArray(selectedCredentials) ? selectedCredentials : [selectedCredentials]) as W3CVerifiableCredential[];
if (verifiableCredential.some((c) => CredentialMapper.isSdJwtDecodedCredential(c) || CredentialMapper.isSdJwtEncoded(c))) {
if (!this.options?.hasher) {
throw new Error('Hasher must be provided when creating a presentation with an SD-JWT VC');
}
}

// extract sd_alg or default to sha-256
const hashAlg = decoded.signedPayload._sd_alg ?? 'sha-256';
const sdHash = calculateSdHash(decoded.compactSdJwtVc, hashAlg, opts.hasher);

const kbJwt = {
// alg MUST be set by the signer
header: {
typ: 'kb+jwt',
// aud MUST be set by the signer or provided by e.g. SIOP/OpenID4VP lib
},
payload: {
iat: new Date().getTime(),
sd_hash: sdHash,
},
} satisfies PartialSdJwtKbJwt;
const wVCs = verifiableCredential.map((vc) => CredentialMapper.toWrappedVerifiableCredential(vc, { hasher: this.options?.hasher }));
const holders = Array.from(new Set(wVCs.flatMap((wvc) => getSubjectIdsAsString(wvc.credential as ICredential))));
if (holders.length !== 1 && !opts?.holderDID) {
console.log(
`We deduced ${holders.length} subject from ${wVCs.length} Verifiable Credentials, and no holder property was given. This might lead to undesired results`,
);
}
const holder = opts?.holderDID ?? (holders.length === 1 ? holders[0] : undefined);

const type = opts?.basePresentationPayload?.type
? Array.isArray(opts.basePresentationPayload.type)
? opts.basePresentationPayload.type
: [opts.basePresentationPayload.type]
: [];
if (!type.includes('VerifiablePresentation')) {
type.push('VerifiablePresentation');
}

const sdJwtDecodedPresentation: PartialSdJwtDecodedVerifiableCredential = {
...decoded,
kbJwt,
};
return sdJwtDecodedPresentation;
} else {
if (!selectedCredentials) {
throw Error(`At least a verifiable credential needs to be passed in to create a presentation`);
}
const verifiableCredential = (Array.isArray(selectedCredentials) ? selectedCredentials : [selectedCredentials]) as W3CVerifiableCredential[];
const wVCs = verifiableCredential.map((vc) => CredentialMapper.toWrappedVerifiableCredential(vc));
const holders = Array.from(new Set(wVCs.flatMap((wvc) => getSubjectIdsAsString(wvc.credential as ICredential))));
if (holders.length !== 1 && !opts?.holderDID) {
console.log(
`We deduced ${holders.length} subject from ${wVCs.length} Verifiable Credentials, and no holder property was given. This might lead to undesired results`,
);
}
const holder = opts?.holderDID ?? (holders.length === 1 ? holders[0] : undefined);

const type = opts?.basePresentationPayload?.type
? Array.isArray(opts.basePresentationPayload.type)
? opts.basePresentationPayload.type
: [opts.basePresentationPayload.type]
: [];
if (!type.includes('VerifiablePresentation')) {
type.push('VerifiablePresentation');
}
const context = opts?.basePresentationPayload?.['@context']
? Array.isArray(opts.basePresentationPayload['@context'])
? opts.basePresentationPayload['@context']
: [opts.basePresentationPayload['@context']]
: [];
if (!context.includes('https://www.w3.org/2018/credentials/v1')) {
context.push('https://www.w3.org/2018/credentials/v1');
}

const context = opts?.basePresentationPayload?.['@context']
? Array.isArray(opts.basePresentationPayload['@context'])
? opts.basePresentationPayload['@context']
: [opts.basePresentationPayload['@context']]
: [];
if (!context.includes('https://www.w3.org/2018/credentials/v1')) {
context.push('https://www.w3.org/2018/credentials/v1');
if (opts?.presentationSubmission) {
if (!type.includes('PresentationSubmission')) {
type.push('PresentationSubmission');
}

if (opts?.presentationSubmission) {
if (!type.includes('PresentationSubmission')) {
type.push('PresentationSubmission');
}
if (!context.includes('https://identity.foundation/presentation-exchange/submission/v1')) {
context.push('https://identity.foundation/presentation-exchange/submission/v1');
}
if (!context.includes('https://identity.foundation/presentation-exchange/submission/v1')) {
context.push('https://identity.foundation/presentation-exchange/submission/v1');
}
return {
}
const result: Array<IPresentation | PartialSdJwtDecodedVerifiableCredential> = [];
if (PEX.allowMultipleVCsPerPresentation(verifiableCredential)) {
result.push({
...opts?.basePresentationPayload,
'@context': context,
type,
holder,
...(!!opts?.presentationSubmission && { presentation_submission: opts.presentationSubmission }),
verifiableCredential,
};
});
} else {
verifiableCredential.forEach((vc) => {
if (CredentialMapper.isSdJwtDecodedCredential(vc)) {
result.push(vc as PartialSdJwtDecodedVerifiableCredential);
} else if (CredentialMapper.isSdJwtEncoded(vc)) {
const decoded = CredentialMapper.decodeVerifiableCredential(vc, opts?.hasher);
result.push(decoded as PartialSdJwtDecodedVerifiableCredential);
} else {
// This should be plain jwt only
nklomp marked this conversation as resolved.
Show resolved Hide resolved
result.push({
...opts?.basePresentationPayload,
'@context': context,
type,
holder,
...(!!opts?.presentationSubmission && { presentation_submission: opts.presentationSubmission }),
verifiableCredential: [vc],
});
}
});
}
return result;
}

private static allowMultipleVCsPerPresentation(verifiableCredential: Array<OriginalVerifiableCredential>) {
return !verifiableCredential.some(
(c) => CredentialMapper.isJwtEncoded(c) || CredentialMapper.isSdJwtEncoded(c) || CredentialMapper.isSdJwtDecodedCredential(c),
nklomp marked this conversation as resolved.
Show resolved Hide resolved
);
}

/**
Expand Down Expand Up @@ -504,7 +485,8 @@ export class PEX {
});

const presentationResult = this.presentationFrom(presentationDefinition, evaluationResult.verifiableCredential, opts);
const evaluationResults = this.evaluatePresentation(presentationDefinition, presentationResult.presentation, {
const presentations = presentationResult.presentations;
const evaluationResults = this.evaluatePresentation(presentationDefinition, presentations, {
limitDisclosureSignatureSuites,
...(presentationResult.presentationSubmissionLocation === PresentationSubmissionLocation.EXTERNAL && {
presentationSubmission: presentationResult.presentationSubmission,
Expand All @@ -529,59 +511,68 @@ export class PEX {
domain: proofOptions?.domain,
};

let presentation = presentationResult.presentation;
this.updateSdJwtCredentials(presentations, proofOptions?.nonce);

// Select type without kbJwt as isSdJwtDecodedCredential and won't accept the partial sdvc type
if (CredentialMapper.isSdJwtDecodedCredential(presentationResult.presentation as SdJwtDecodedVerifiableCredential)) {
const sdJwtPresentation = presentation as SdJwtDecodedVerifiableCredential;
if (!this.options?.hasher) {
throw new Error('Hasher must be provided when creating a presentation with an SD-JWT VC');
}

// extract sd_alg or default to sha-256
const hashAlg = presentationResult.presentation.signedPayload._sd_alg ?? 'sha-256';
const sdHash = calculateSdHash(presentationResult.presentation.compactSdJwtVc, hashAlg, this.options.hasher);

const kbJwt = {
// alg MUST be set by the signer
header: {
typ: 'kb+jwt',
},
// aud MUST be set by the signer or provided by e.g. SIOP/OpenID4VP lib
payload: {
iat: new Date().getTime(),
nonce: proofOptions?.nonce,
sd_hash: sdHash,
const verifiablePresentations: Array<W3CVerifiablePresentation | CompactSdJwtVc> = [];
for (const presentation of presentations) {
const callBackParams: PresentationSignCallBackParams = {
options: {
...opts,
presentationSubmissionLocation: presentationResult.presentationSubmissionLocation,
},
} satisfies PartialSdJwtKbJwt;

presentation = {
...sdJwtPresentation,
kbJwt,
} satisfies PartialSdJwtDecodedVerifiableCredential;
presentation,
presentationDefinition,
selectedCredentials,
proof,
presentationSubmission: evaluationResults.value,
evaluationResults,
};
verifiablePresentations.push(await signingCallBack(callBackParams));
}

const callBackParams: PresentationSignCallBackParams = {
options: {
...opts,
presentationSubmissionLocation: presentationResult.presentationSubmissionLocation,
},
presentation,
presentationDefinition,
selectedCredentials,
proof,
presentationSubmission: evaluationResults.value,
evaluationResults,
};
const verifiablePresentation = await signingCallBack(callBackParams);

return {
verifiablePresentation,
verifiablePresentations,
presentationSubmissionLocation: presentationResult.presentationSubmissionLocation,
presentationSubmission: evaluationResults.value,
};
}

private updateSdJwtCredentials(
presentations: Array<IPresentation | SdJwtDecodedVerifiableCredential | PartialSdJwtDecodedVerifiableCredential>,
nonce?: string,
) {
presentations.forEach((presentation, index) => {
// Select type without kbJwt as isSdJwtDecodedCredential and won't accept the partial sdvc type
if (CredentialMapper.isSdJwtDecodedCredential(presentation as SdJwtDecodedVerifiableCredential)) {
const sdJwtCredential = presentation as SdJwtDecodedVerifiableCredential;
if (!this.options?.hasher) {
throw new Error('Hasher must be provided when creating a presentation with an SD-JWT VC');
}

// extract sd_alg or default to sha-256
const hashAlg = sdJwtCredential.signedPayload._sd_alg ?? 'sha-256';
const sdHash = calculateSdHash(sdJwtCredential.compactSdJwtVc, hashAlg, this.options.hasher);

const kbJwt = {
// alg MUST be set by the signer
header: {
typ: 'kb+jwt',
},
// aud MUST be set by the signer or provided by e.g. SIOP/OpenID4VP lib
payload: {
iat: Math.floor(new Date().getTime() / 1000),
nonce: nonce,
sd_hash: sdHash,
},
} satisfies PartialSdJwtKbJwt;

presentations[index] = {
...sdJwtCredential,
kbJwt,
} satisfies PartialSdJwtDecodedVerifiableCredential;
}
});
}

public static definitionVersionDiscovery(presentationDefinition: IPresentationDefinition): DiscoveredVersion {
return definitionVersionDiscovery(presentationDefinition);
}
Expand Down
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 presentation = this.constructPresentations(selectedCredentials, {
...opts,
presentationSubmission: presentationSubmissionLocation === PresentationSubmissionLocation.PRESENTATION ? presentationSubmission : undefined,
});

return {
presentation,
presentations: presentation,
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 presentation = this.constructPresentations(selectedCredentials, {
nklomp marked this conversation as resolved.
Show resolved Hide resolved
...opts,
presentationSubmission: presentationSubmissionLocation === PresentationSubmissionLocation.PRESENTATION ? presentationSubmission : undefined,
});

return {
presentation,
presentations: presentation,
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
Loading