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

feat: sd-jwt support #132

Merged
merged 7 commits into from
Jan 13, 2024
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: 4 additions & 2 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
PresentationSignCallBackParams,
PresentationSubmissionLocation,
ProofOptions,
SdJwtDecodedVerifiableCredentialWithKbJwtInput,
SelectResults,
SignatureOptions,
Status,
Expand All @@ -25,7 +26,7 @@ import {
ValidationPredicate,
Validator,
VerifiablePresentationFromOpts,
VerifiablePresentationResult
VerifiablePresentationResult,
} from './lib';

export {
Expand Down Expand Up @@ -55,5 +56,6 @@ export {
VerifiablePresentationResult,
PresentationResult,
PresentationFromOpts,
PresentationSubmissionLocation
PresentationSubmissionLocation,
SdJwtDecodedVerifiableCredentialWithKbJwtInput,
};
265 changes: 202 additions & 63 deletions lib/PEX.ts

Large diffs are not rendered by default.

20 changes: 14 additions & 6 deletions lib/PEXv1.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Format, PresentationDefinitionV1, PresentationSubmission } from '@sphereon/pex-models';
import { IPresentation, OriginalVerifiableCredential, OriginalVerifiablePresentation } from '@sphereon/ssi-types';
import { CredentialMapper, IPresentation, OriginalVerifiableCredential, OriginalVerifiablePresentation } from '@sphereon/ssi-types';

import { PEX } from './PEX';
import { EvaluationClientWrapper, EvaluationResults, SelectResults } from './evaluation';
Expand All @@ -11,10 +11,6 @@ import { PresentationDefinitionV1VB, Validated, ValidationEngine } from './valid
* This is the main interfacing class for using this library for v1 of presentation exchange
*/
export class PEXv1 extends PEX {
constructor() {
super();
}

/***
* The evaluatePresentationV1 compares what is expected from a presentation with a presentationDefinitionV1.
*
Expand Down Expand Up @@ -111,16 +107,28 @@ export class PEXv1 extends PEX {
selectedCredentials: OriginalVerifiableCredential[],
opts?: PresentationFromOpts,
): PresentationResult {
const presentationSubmissionLocation = opts?.presentationSubmissionLocation ?? PresentationSubmissionLocation.PRESENTATION;
const presentationSubmission = this._evaluationClientWrapper.submissionFrom(
SSITypesBuilder.modelEntityToInternalPresentationDefinitionV1(presentationDefinition),
SSITypesBuilder.mapExternalVerifiableCredentialsToWrappedVcs(selectedCredentials),
opts,
);

const hasSdJwtCredentials = selectedCredentials.some((c) => CredentialMapper.isSdJwtDecodedCredential(c) || CredentialMapper.isSdJwtEncoded(c));

// We could include it in the KB-JWT? Not sure if we want that
if (opts?.presentationSubmissionLocation === PresentationSubmissionLocation.PRESENTATION && hasSdJwtCredentials) {
throw new Error('Presentation submission location cannot be set to presentation when creating a presentation with an SD-JWT VC');
}

const presentationSubmissionLocation =
opts?.presentationSubmissionLocation ??
(hasSdJwtCredentials ? PresentationSubmissionLocation.EXTERNAL : PresentationSubmissionLocation.PRESENTATION);

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

return {
presentation,
presentationSubmissionLocation,
Expand Down
19 changes: 13 additions & 6 deletions lib/PEXv2.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Format, PresentationDefinitionV2, PresentationSubmission } from '@sphereon/pex-models';
import { IPresentation, OriginalVerifiableCredential, OriginalVerifiablePresentation } from '@sphereon/ssi-types';
import { CredentialMapper, IPresentation, OriginalVerifiableCredential, OriginalVerifiablePresentation } from '@sphereon/ssi-types';

import { PEX } from './PEX';
import { EvaluationClientWrapper, EvaluationResults, SelectResults } from './evaluation';
Expand All @@ -11,10 +11,6 @@ import { PresentationDefinitionV2VB, Validated, ValidationEngine } from './valid
* This is the main interfacing class to be used from outside the library to use the functionality provided by the library.
*/
export class PEXv2 extends PEX {
constructor() {
super();
}

/***
* The evaluatePresentationV2 compares what is expected from a presentation with a presentationDefinitionV2.
*
Expand Down Expand Up @@ -108,16 +104,27 @@ export class PEXv2 extends PEX {
selectedCredentials: OriginalVerifiableCredential[],
opts?: PresentationFromOpts,
): PresentationResult {
const presentationSubmissionLocation = opts?.presentationSubmissionLocation ?? PresentationSubmissionLocation.PRESENTATION;
const presentationSubmission = this._evaluationClientWrapper.submissionFrom(
SSITypesBuilder.modelEntityInternalPresentationDefinitionV2(presentationDefinition),
SSITypesBuilder.mapExternalVerifiableCredentialsToWrappedVcs(selectedCredentials),
opts,
);
const hasSdJwtCredentials = selectedCredentials.some((c) => CredentialMapper.isSdJwtDecodedCredential(c) || CredentialMapper.isSdJwtEncoded(c));

// We could include it in the KB-JWT? Not sure if we want that
if (opts?.presentationSubmissionLocation === PresentationSubmissionLocation.PRESENTATION && hasSdJwtCredentials) {
throw new Error('Presentation submission location cannot be set to presentation when creating a presentation with an SD-JWT VC');
}

const presentationSubmissionLocation =
opts?.presentationSubmissionLocation ??
(hasSdJwtCredentials ? PresentationSubmissionLocation.EXTERNAL : PresentationSubmissionLocation.PRESENTATION);

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

return {
presentation,
presentationSubmissionLocation,
Expand Down
4 changes: 2 additions & 2 deletions lib/evaluation/core/evaluationResults.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { PresentationSubmission } from '@sphereon/pex-models';
import { IVerifiableCredential } from '@sphereon/ssi-types';
import { IVerifiableCredential, SdJwtDecodedVerifiableCredential } from '@sphereon/ssi-types';

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

Expand All @@ -20,6 +20,6 @@ export interface EvaluationResults {
*/
errors?: Checked[];
value?: PresentationSubmission;
verifiableCredential: IVerifiableCredential[];
verifiableCredential: Array<IVerifiableCredential | SdJwtDecodedVerifiableCredential>;
warnings?: Checked[];
}
58 changes: 38 additions & 20 deletions lib/evaluation/evaluationClientWrapper.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { JSONPath as jp } from '@astronautlabs/jsonpath';
import { Descriptor, Format, InputDescriptorV1, InputDescriptorV2, PresentationSubmission, Rules, SubmissionRequirement } from '@sphereon/pex-models';
import { IVerifiableCredential, OriginalVerifiableCredential, WrappedVerifiableCredential } from '@sphereon/ssi-types';
import {
CredentialMapper,
IVerifiableCredential,
OriginalVerifiableCredential,
SdJwtDecodedVerifiableCredential,
WrappedVerifiableCredential,
} from '@sphereon/ssi-types';

import { Checked, Status } from '../ConstraintUtils';
import { PresentationSubmissionLocation } from '../signing';
Expand Down Expand Up @@ -311,7 +317,8 @@ export class EvaluationClientWrapper {
this._client.evaluate(pd, wvcs, opts);
const result: EvaluationResults = {
areRequiredCredentialsPresent: Status.INFO,
verifiableCredential: wvcs.map((wrapped) => wrapped.original as IVerifiableCredential),
// TODO: we should handle the string case
verifiableCredential: wvcs.map((wrapped) => wrapped.original as IVerifiableCredential | SdJwtDecodedVerifiableCredential),
};
result.warnings = this.formatNotInfo(Status.WARN);
result.errors = this.formatNotInfo(Status.ERROR);
Expand All @@ -327,7 +334,7 @@ export class EvaluationClientWrapper {
result.value = JSON.parse(JSON.stringify(this._client.presentationSubmission));
}
if (this._client.generatePresentationSubmission) {
this.updatePresentationSubmissionPathToAlias('verifiableCredential', result.value);
this.updatePresentationSubmissionPathToVpPath(result.value);
}
result.verifiableCredential = this._client.wrappedVcs.map((wrapped) => wrapped.original as IVerifiableCredential);
result.areRequiredCredentialsPresent = result.value?.descriptor_map?.length ? Status.INFO : Status.ERROR;
Expand Down Expand Up @@ -384,7 +391,7 @@ export class EvaluationClientWrapper {
const result: [number, HandlerCheckResult[]] = 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.updatePresentationSubmissionPathToAlias('verifiableCredential');
this.updatePresentationSubmissionPathToVpPath();
if (opts?.presentationSubmissionLocation === PresentationSubmissionLocation.EXTERNAL) {
this.updatePresentationSubmissionToExternal();
}
Expand All @@ -395,7 +402,8 @@ export class EvaluationClientWrapper {
);
const updatedIndexes = this.matchUserSelectedVcs(marked, vcs);
this.updatePresentationSubmission(updatedIndexes[1]);
this.updatePresentationSubmissionPathToAlias('verifiableCredential');

this.updatePresentationSubmissionPathToVpPath();
if (opts?.presentationSubmissionLocation === PresentationSubmissionLocation.EXTERNAL) {
this.updatePresentationSubmissionToExternal();
}
Expand Down Expand Up @@ -548,11 +556,10 @@ export class EvaluationClientWrapper {
public fillSelectableCredentialsToVerifiableCredentialsMapping(selectResults: SelectResults, wrappedVcs: WrappedVerifiableCredential[]) {
if (selectResults) {
selectResults.verifiableCredential?.forEach((selectableCredential) => {
const foundIndex: number = ObjectUtils.isString(selectableCredential)
? wrappedVcs.findIndex((wrappedVc) => selectableCredential === wrappedVc.original)
: wrappedVcs.findIndex(
(wrappedVc) => JSON.stringify((selectableCredential as IVerifiableCredential).proof) === JSON.stringify(wrappedVc.credential.proof),
);
const foundIndex = wrappedVcs.findIndex((wrappedVc) =>
CredentialMapper.areOriginalVerifiableCredentialsEqual(wrappedVc.original, selectableCredential),
);

if (foundIndex === -1) {
throw new Error('index is not right');
}
Expand Down Expand Up @@ -663,16 +670,27 @@ export class EvaluationClientWrapper {
}
}

private updatePresentationSubmissionPathToAlias(alias: string, presentationSubmission?: PresentationSubmission) {
if (presentationSubmission) {
presentationSubmission.descriptor_map.forEach((d) => {
this.replacePathWithAlias(d, alias);
});
} else if (this._client.generatePresentationSubmission) {
this._client.presentationSubmission.descriptor_map.forEach((d) => {
this.replacePathWithAlias(d, alias);
});
}
private updatePresentationSubmissionPathToVpPath(presentationSubmission?: PresentationSubmission) {
const descriptorMap = presentationSubmission
? presentationSubmission.descriptor_map
: this._client.generatePresentationSubmission
? this._client.presentationSubmission.descriptor_map
: undefined;

descriptorMap?.forEach((d) => {
// NOTE: currently we only support a single VP for a single PD, so that means an SD-JWT will always have the path '$'.
// If there is more consensus on whether a PD can result in one submission with multiple VPs, we could tweak this logic
// to keep supporting arrays (so it will just stay as the input format) if there's multiple SD-JWTs that are included
// in the presentation submission (we would allow the presentationFrom and verifiablePresentationFrom to just return
// an array of VPs, while still one submission is returned. This will also help with creating multiple VPs for JWT credentials)
// See https://github.com/decentralized-identity/presentation-exchange/issues/462
// Also see: https://github.com/openid/OpenID4VP/issues/69
if (d.format === 'vc+sd-jwt') {
d.path = '$';
} else {
this.replacePathWithAlias(d, 'verifiableCredential');
}
});
}

private replacePathWithAlias(descriptor: Descriptor, alias: string) {
Expand Down
22 changes: 16 additions & 6 deletions lib/evaluation/handlers/didRestrictionEvaluationHandler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { WrappedVerifiableCredential } from '@sphereon/ssi-types';
import { CredentialMapper, WrappedVerifiableCredential } from '@sphereon/ssi-types';

import { Status } from '../../ConstraintUtils';
import { IInternalPresentationDefinition, InternalPresentationDefinitionV1, InternalPresentationDefinitionV2 } from '../../types';
Expand All @@ -21,14 +21,14 @@ export class DIDRestrictionEvaluationHandler extends AbstractEvaluationHandler {
public handle(pd: IInternalPresentationDefinition, wrappedVcs: WrappedVerifiableCredential[]): void {
(pd as InternalPresentationDefinitionV1 | InternalPresentationDefinitionV2).input_descriptors.forEach((_inputDescriptor, index) => {
wrappedVcs.forEach((wvc: WrappedVerifiableCredential, vcIndex: number) => {
const issuer = typeof wvc.credential.issuer === 'object' ? wvc.credential.issuer.id : wvc.credential.issuer;
const issuerId = this.getIssuerIdFromWrappedVerifiableCredential(wvc);
if (
!this.client.hasRestrictToDIDMethods() ||
!issuer ||
isRestrictedDID(issuer, this.client.restrictToDIDMethods) ||
!issuer.toLowerCase().startsWith('did:')
!issuerId ||
isRestrictedDID(issuerId, this.client.restrictToDIDMethods) ||
!issuerId.toLowerCase().startsWith('did:')
) {
this.getResults().push(this.generateSuccessResult(index, `$[${vcIndex}]`, wvc, `${issuer} is allowed`));
this.getResults().push(this.generateSuccessResult(index, `$[${vcIndex}]`, wvc, `${issuerId} is allowed`));
} else {
this.getResults().push(this.generateErrorResult(index, `$[${vcIndex}]`, wvc));
}
Expand All @@ -38,6 +38,16 @@ export class DIDRestrictionEvaluationHandler extends AbstractEvaluationHandler {
this.updatePresentationSubmission(pd);
}

private getIssuerIdFromWrappedVerifiableCredential(wrappedVc: WrappedVerifiableCredential) {
if (CredentialMapper.isW3cCredential(wrappedVc.credential)) {
return typeof wrappedVc.credential.issuer === 'object' ? wrappedVc.credential.issuer.id : wrappedVc.credential.issuer;
} else if (CredentialMapper.isSdJwtDecodedCredential(wrappedVc.credential)) {
return wrappedVc.credential.decodedPayload.iss;
}

throw new Error('Unsupported credential type');
}

private generateErrorResult(idIdx: number, vcPath: string, wvc: WrappedVerifiableCredential): HandlerCheckResult {
return {
input_descriptor_path: `$.input_descriptors[${idIdx}]`,
Expand Down
79 changes: 68 additions & 11 deletions lib/evaluation/handlers/limitDisclosureEvaluationHandler.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import { ConstraintsV1, ConstraintsV2, FieldV2, InputDescriptorV2, Optionality } from '@sphereon/pex-models';
import { AdditionalClaims, ICredential, ICredentialSubject, IVerifiableCredential, WrappedVerifiableCredential } from '@sphereon/ssi-types';
import {
AdditionalClaims,
CredentialMapper,
ICredential,
ICredentialSubject,
IVerifiableCredential,
SdJwtDecodedVerifiableCredential,
SdJwtPresentationFrame,
WrappedVerifiableCredential,
} from '@sphereon/ssi-types';

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

import { AbstractEvaluationHandler } from './abstractEvaluationHandler';
Expand All @@ -31,6 +40,8 @@ export class LimitDisclosureEvaluationHandler extends AbstractEvaluationHandler
}

private isLimitDisclosureSupported(wvc: WrappedVerifiableCredential, vcIdx: number, idIdx: number, optionality: Optionality): 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) {
Expand All @@ -50,29 +61,75 @@ export class LimitDisclosureEvaluationHandler extends AbstractEvaluationHandler
const optionality = constraints.limit_disclosure;
wrappedVcs.forEach((wvc, index) => {
if (optionality && this.isLimitDisclosureSupported(wvc, index, idIdx, optionality)) {
this.enforceLimitDisclosure(wvc.credential, fields, idIdx, index, wrappedVcs, optionality);
this.enforceLimitDisclosure(wvc, fields, idIdx, index, wrappedVcs, optionality);
}
});
}

private enforceLimitDisclosure(
vc: IVerifiableCredential,
wvc: WrappedVerifiableCredential,
fields: FieldV2[],
idIdx: number,
index: number,
wrappedVcs: WrappedVerifiableCredential[],
limitDisclosure: Optionality,
) {
const internalCredentialToSend = this.createVcWithRequiredFields(vc, fields, idIdx, index);
/* 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);
if (CredentialMapper.isWrappedSdJwtVerifiableCredential(wvc)) {
const presentationFrame = this.createSdJwtPresentationFrame(wvc.credential, fields, idIdx, index);

// 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
if (presentationFrame) {
applySdJwtLimitDisclosure(wvc.credential, presentationFrame);
wvc.decoded = wvc.credential.decodedPayload;
// We need to overwrite the original, as that is returned in the selectFrom method
// 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);
}
} else if (CredentialMapper.isW3cCredential(wvc.credential)) {
const internalCredentialToSend = this.createVcWithRequiredFields(wvc.credential, fields, idIdx, index);
/* 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);
}
} else {
throw new Error(`Unsupported format for selective disclosure ${wvc.format}`);
}
}

private createSdJwtPresentationFrame(
vc: SdJwtDecodedVerifiableCredential,
fields: FieldV2[],
idIdx: number,
vcIdx: 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;
}
}
}

return presentationFrame;
}

private createVcWithRequiredFields(vc: IVerifiableCredential, fields: FieldV2[], idIdx: number, vcIdx: number): IVerifiableCredential | undefined {
let credentialToSend: IVerifiableCredential = {} as IVerifiableCredential;
credentialToSend = Object.assign(credentialToSend, vc);
Expand Down
Loading
Loading