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

fix: only apply limit disclosure for matching vcs #168

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion lib/evaluation/evaluationClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ import {
UriEvaluationHandler,
} from './handlers';

const DEFAULT_LIMIT_DISCLOSURE_TYPES = [IProofType.BbsBlsSignatureProof2020, 'DataIntegrityProof.anoncreds-2023'];
const DEFAULT_LIMIT_DISCLOSURE_TYPES = [
IProofType.BbsBlsSignatureProof2020,
'DataIntegrityProof.anoncredsvc-2023',
'DataIntegrityProof.anoncreds-2023',
];

export class EvaluationClient {
constructor() {
Expand Down
168 changes: 100 additions & 68 deletions lib/evaluation/handlers/limitDisclosureEvaluationHandler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ConstraintsV1, ConstraintsV2, FieldV2, InputDescriptorV2, Optionality } from '@sphereon/pex-models';
import { InputDescriptorV1, InputDescriptorV2, Optionality } from '@sphereon/pex-models';
import {
AdditionalClaims,
CredentialMapper,
Expand All @@ -11,12 +11,13 @@ import {
} from '@sphereon/ssi-types';

import { Status } from '../../ConstraintUtils';
import { IInternalPresentationDefinition, InternalPresentationDefinitionV2, PathComponent } from '../../types';
import { IInternalPresentationDefinition, InputDescriptorWithIndex, PathComponent } from '../../types';
import PexMessages from '../../types/Messages';
import { applySdJwtLimitDisclosure, JsonPathUtils } from '../../utils';
import { EvaluationClient } from '../evaluationClient';

import { AbstractEvaluationHandler } from './abstractEvaluationHandler';
import { elligibleInputDescriptorsForWrappedVc } from './markForSubmissionEvaluationHandler';

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"elligible" is consistently misspelled
-> eligible

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will rename it

export class LimitDisclosureEvaluationHandler extends AbstractEvaluationHandler {
constructor(client: EvaluationClient) {
Expand All @@ -28,58 +29,76 @@ export class LimitDisclosureEvaluationHandler extends AbstractEvaluationHandler
}

public handle(pd: IInternalPresentationDefinition, wrappedVcs: WrappedVerifiableCredential[]): void {
// PresentationDefinitionV2 is the common denominator
(pd as InternalPresentationDefinitionV2).input_descriptors.forEach((inDesc: InputDescriptorV2, index: number) => {
if (
inDesc.constraints?.fields &&
(inDesc.constraints?.limit_disclosure === Optionality.Required || inDesc.constraints?.limit_disclosure === Optionality.Preferred)
) {
this.evaluateLimitDisclosure(wrappedVcs, inDesc.constraints, index);
}
});
this.evaluateLimitDisclosure(pd.input_descriptors as InputDescriptorV2[], wrappedVcs);
}

private isLimitDisclosureSupported(wvc: WrappedVerifiableCredential, vcIdx: number, idIdx: number, optionality: Optionality): boolean {
private isLimitDisclosureSupported(
elligibleInputDescriptors: InputDescriptorWithIndex[],
wvc: WrappedVerifiableCredential,
vcIndex: number,
): boolean {
if (wvc.format === 'vc+sd-jwt') return true;

const limitDisclosureSignatures = this.client.limitDisclosureSignatureSuites;
const proof = (wvc.decoded as IVerifiableCredential).proof;

if (!proof || Array.isArray(proof) || !proof.type) {
const decoded = wvc.decoded as IVerifiableCredential;
const proofs = Array.isArray(decoded.proof) ? decoded.proof : decoded.proof ? [decoded.proof] : undefined;
const requiredLimitDisclosureInputDescriptorIds = elligibleInputDescriptors
.map(({ inputDescriptor: { constraints }, inputDescriptorIndex }) =>
constraints?.limit_disclosure === Optionality.Required ? inputDescriptorIndex : undefined,
)
.filter((id): id is number => id !== undefined);

if (!proofs || proofs.length === 0 || proofs.length > 1 || !proofs[0].type) {
// todo: Support/inspect array based proofs
if (requiredLimitDisclosureInputDescriptorIds.length > 0) {
this.createLimitDisclosureNotSupportedResult(
elligibleInputDescriptors.map((i) => i.inputDescriptorIndex),
vcIndex,
'Multiple proofs on verifiable credential not supported for limit disclosure',
);
}
return false;
}

const proof = proofs[0];
const signatureSuite = proof.cryptosuite ? `${proof.type}.${proof.cryptosuite}` : proof.type;
if (!limitDisclosureSignatures?.includes(signatureSuite)) {
if (optionality == Optionality.Required) {
this.createLimitDisclosureNotSupportedResult(idIdx, vcIdx);
if (requiredLimitDisclosureInputDescriptorIds.length > 0) {
this.createLimitDisclosureNotSupportedResult(
requiredLimitDisclosureInputDescriptorIds,
vcIndex,
`Signature suite '${signatureSuite}' is not present in limitDisclosureSignatureSuites [${limitDisclosureSignatures.join(',')}]`,
);
}
return false;
}

return true;
}

private evaluateLimitDisclosure(wrappedVcs: WrappedVerifiableCredential[], constraints: ConstraintsV1 | ConstraintsV2, idIdx: number): void {
const fields = constraints?.fields as FieldV2[];
const optionality = constraints.limit_disclosure;
wrappedVcs.forEach((wvc, index) => {
if (optionality && this.isLimitDisclosureSupported(wvc, index, idIdx, optionality)) {
this.enforceLimitDisclosure(wvc, fields, idIdx, index, wrappedVcs, optionality);
private evaluateLimitDisclosure(inputDescriptors: Array<InputDescriptorV2 | InputDescriptorV1>, wrappedVcs: WrappedVerifiableCredential[]): void {
wrappedVcs.forEach((wvc, vcIndex) => {
const elligibleInputDescriptors = elligibleInputDescriptorsForWrappedVc(inputDescriptors, vcIndex, this.getResults());
const includeLimitDisclosure = elligibleInputDescriptors.some(
({ inputDescriptor: { constraints } }) =>
constraints?.limit_disclosure === Optionality.Preferred || constraints?.limit_disclosure === Optionality.Required,
);

if (
elligibleInputDescriptors.length > 0 &&
includeLimitDisclosure &&
this.isLimitDisclosureSupported(elligibleInputDescriptors, wvc, vcIndex)
) {
this.enforceLimitDisclosure(wrappedVcs, elligibleInputDescriptors, vcIndex);
}
});
}

private enforceLimitDisclosure(
wvc: WrappedVerifiableCredential,
fields: FieldV2[],
idIdx: number,
index: number,
wrappedVcs: WrappedVerifiableCredential[],
limitDisclosure: Optionality,
) {
private enforceLimitDisclosure(wrappedVcs: WrappedVerifiableCredential[], elligibleInputDescriptors: InputDescriptorWithIndex[], vcIndex: number) {
const wvc = wrappedVcs[vcIndex];

if (CredentialMapper.isWrappedSdJwtVerifiableCredential(wvc)) {
const presentationFrame = this.createSdJwtPresentationFrame(wvc.credential, fields, idIdx, index);
const presentationFrame = this.createSdJwtPresentationFrame(elligibleInputDescriptors, wvc.credential, vcIndex);

// We update the SD-JWT to it's presentation format (remove disclosures, update pretty payload, etc..), except
// we don't create or include the (optional) KB-JWT yet, this is done when we create the presentation
Expand All @@ -90,63 +109,74 @@ export class LimitDisclosureEvaluationHandler extends AbstractEvaluationHandler
// But we also want to keep the format of the original credential.
wvc.original = CredentialMapper.isSdJwtDecodedCredential(wvc.original) ? wvc.credential : wvc.credential.compactSdJwtVc;

this.createSuccessResult(idIdx, `$[${index}]`, limitDisclosure);
for (const { inputDescriptorIndex, inputDescriptor } of elligibleInputDescriptors) {
this.createSuccessResult(inputDescriptorIndex, `$[${vcIndex}]`, inputDescriptor.constraints?.limit_disclosure);
}
}
} else if (CredentialMapper.isW3cCredential(wvc.credential)) {
const internalCredentialToSend = this.createVcWithRequiredFields(wvc.credential, fields, idIdx, index);
const internalCredentialToSend = this.createVcWithRequiredFields(elligibleInputDescriptors, wvc.credential, vcIndex);
/* When verifiableCredentialToSend is null/undefined an error is raised, the credential will
* remain untouched and the verifiable credential won't be submitted.
*/
if (internalCredentialToSend) {
wrappedVcs[index].credential = internalCredentialToSend;
this.createSuccessResult(idIdx, `$[${index}]`, limitDisclosure);
wvc.credential = internalCredentialToSend;
for (const { inputDescriptorIndex, inputDescriptor } of elligibleInputDescriptors) {
this.createSuccessResult(inputDescriptorIndex, `$[${vcIndex}]`, inputDescriptor.constraints?.limit_disclosure);
}
}
} else {
throw new Error(`Unsupported format for selective disclosure ${wvc.format}`);
}
}

private createSdJwtPresentationFrame(
inputDescriptors: InputDescriptorWithIndex[],
vc: SdJwtDecodedVerifiableCredential,
fields: FieldV2[],
idIdx: number,
vcIdx: number,
vcIndex: number,
): SdJwtPresentationFrame | undefined {
// Mapping of key -> true to indicate which values should be disclosed in an SD-JWT
// Can be nested array / object
const presentationFrame: SdJwtPresentationFrame = {};

for (const field of fields) {
if (field.path) {
const inputField = JsonPathUtils.extractInputField(vc.decodedPayload, field.path);

// We set the value to true at the path in the presentation frame,
if (inputField.length > 0) {
const selectedField = inputField[0];
JsonPathUtils.setValue(presentationFrame, selectedField.path, true);
} else {
this.createMandatoryFieldNotFoundResult(idIdx, vcIdx, field.path);
return undefined;
for (const { inputDescriptor, inputDescriptorIndex } of inputDescriptors) {
for (const field of inputDescriptor.constraints?.fields ?? []) {
if (field.path) {
const inputField = JsonPathUtils.extractInputField(vc.decodedPayload, field.path);

// We set the value to true at the path in the presentation frame,
if (inputField.length > 0) {
const selectedField = inputField[0];
JsonPathUtils.setValue(presentationFrame, selectedField.path, true);
} else {
this.createMandatoryFieldNotFoundResult(inputDescriptorIndex, vcIndex, field.path);
return undefined;
}
}
}
}

return presentationFrame;
}

private createVcWithRequiredFields(vc: IVerifiableCredential, fields: FieldV2[], idIdx: number, vcIdx: number): IVerifiableCredential | undefined {
private createVcWithRequiredFields(
inputDescriptors: InputDescriptorWithIndex[],
vc: IVerifiableCredential,
vcIndex: number,
): IVerifiableCredential | undefined {
let credentialToSend: IVerifiableCredential = {} as IVerifiableCredential;
credentialToSend = Object.assign(credentialToSend, vc);
credentialToSend.credentialSubject = {};

for (const field of fields) {
if (field.path) {
const inputField = JsonPathUtils.extractInputField(vc, field.path);
if (inputField.length > 0) {
credentialToSend = this.copyResultPathToDestinationCredential(inputField[0], vc, credentialToSend);
} else {
this.createMandatoryFieldNotFoundResult(idIdx, vcIdx, field.path);
return undefined;
for (const { inputDescriptor, inputDescriptorIndex } of inputDescriptors) {
for (const field of inputDescriptor.constraints?.fields ?? []) {
if (field.path) {
const inputField = JsonPathUtils.extractInputField(vc, field.path);
if (inputField.length > 0) {
credentialToSend = this.copyResultPathToDestinationCredential(inputField[0], vc, credentialToSend);
} else {
this.createMandatoryFieldNotFoundResult(inputDescriptorIndex, vcIndex, field.path);
return undefined;
}
}
}
}
Expand All @@ -172,7 +202,7 @@ export class LimitDisclosureEvaluationHandler extends AbstractEvaluationHandler
return internalCredentialToSend;
}

private createSuccessResult(idIdx: number, path: string, limitDisclosure: Optionality) {
private createSuccessResult(idIdx: number, path: string, limitDisclosure?: Optionality) {
return this.getResults().push({
input_descriptor_path: `$.input_descriptors[${idIdx}]`,
verifiable_credential_path: `${path}`,
Expand All @@ -194,13 +224,15 @@ export class LimitDisclosureEvaluationHandler extends AbstractEvaluationHandler
});
}

private createLimitDisclosureNotSupportedResult(idIdx: number, vcIdx: number) {
return this.getResults().push({
input_descriptor_path: `$.input_descriptors[${idIdx}]`,
verifiable_credential_path: `$[${vcIdx}]`,
evaluator: this.getName(),
status: Status.ERROR,
message: PexMessages.LIMIT_DISCLOSURE_NOT_SUPPORTED,
});
private createLimitDisclosureNotSupportedResult(idIdxs: number[], vcIdx: number, reason?: string) {
return this.getResults().push(
...idIdxs.map((idIdx) => ({
input_descriptor_path: `$.input_descriptors[${idIdx}]`,
verifiable_credential_path: `$[${vcIdx}]`,
evaluator: this.getName(),
status: Status.ERROR,
message: reason ? `${PexMessages.LIMIT_DISCLOSURE_NOT_SUPPORTED}. ${reason}` : PexMessages.LIMIT_DISCLOSURE_NOT_SUPPORTED,
})),
);
}
}
28 changes: 28 additions & 0 deletions lib/evaluation/handlers/markForSubmissionEvaluationHandler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { JSONPath as jp } from '@astronautlabs/jsonpath';
import { InputDescriptorV1, InputDescriptorV2 } from '@sphereon/pex-models';
import { WrappedVerifiableCredential } from '@sphereon/ssi-types';

import { Status } from '../../ConstraintUtils';
Expand All @@ -9,6 +10,33 @@ import { EvaluationClient } from '../evaluationClient';

import { AbstractEvaluationHandler } from './abstractEvaluationHandler';

export function elligibleInputDescriptorsForWrappedVc(
inputDescriptors: Array<InputDescriptorV2 | InputDescriptorV1>,
vcIndex: number,
results: HandlerCheckResult[],
) {
return inputDescriptors
.map((inputDescriptor, inputDescriptorIndex) => {
const matchingResults = results.filter(
({ verifiable_credential_path, input_descriptor_path }) =>
verifiable_credential_path === `$[${vcIndex}]` && input_descriptor_path === `$.input_descriptors[${inputDescriptorIndex}]`,
);

const hasError = matchingResults.some((result) => result.status === Status.ERROR);
const hasInfo = matchingResults.some((result) => result.status === Status.INFO);

if (hasInfo && !hasError) {
return {
inputDescriptor,
inputDescriptorIndex,
};
}

return undefined;
})
.filter((value): value is Exclude<typeof value, undefined> => value !== undefined);
}

export class MarkForSubmissionEvaluationHandler extends AbstractEvaluationHandler {
constructor(client: EvaluationClient) {
super(client);
Expand Down
5 changes: 5 additions & 0 deletions lib/types/Internal.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ import {
} from '@sphereon/pex-models';
import { IVerifiableCredential, IVerifiablePresentation } from '@sphereon/ssi-types';

export interface InputDescriptorWithIndex {
inputDescriptorIndex: number;
inputDescriptor: InputDescriptorV1 | InputDescriptorV2;
}

export type PathComponent = string | number;
export interface IInternalPresentationDefinition {
format?: Format;
Expand Down
Loading
Loading