Skip to content

Commit

Permalink
fix pex state issues
Browse files Browse the repository at this point in the history
  • Loading branch information
nitro-neal committed Sep 6, 2023
1 parent 14cad2c commit 82558a7
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 79 deletions.
70 changes: 36 additions & 34 deletions packages/credentials/src/ssi.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { JwsHeaderParams } from '@web5/crypto';
import type { Resolvable, DIDResolutionResult } from 'did-resolver';
import type {
VerifiableCredentialV1,
import {
VerifiableCredentialTypeV1,
JwtDecodedVerifiableCredential,
CredentialSubject,
VerifiablePresentationV1,
Expand All @@ -11,11 +11,15 @@ import type {
JwtDecodedVerifiablePresentation,
Issuer,
CredentialSchemaType,
CredentialStatus
CredentialStatus,
validateDefinition,
validateSubmission,
Validated,
resetPex
} from './types.js';

import { v4 as uuidv4 } from 'uuid';
import { evaluateCredentials, evaluatePresentation, presentationFrom, VcJwt, VpJwt } from './types.js';
import { evaluateCredentials, presentationFrom, VcJwt, VpJwt } from './types.js';
import { getCurrentXmlSchema112Timestamp } from './utils.js';
import { Convert } from '@web5/common';
import { verifyJWT } from 'did-jwt';
Expand Down Expand Up @@ -87,7 +91,7 @@ export class VerifiableCredential {
* @param verifiableCredential - Optional. Actual VC object to be signed.
* @returns A promise that resolves to a VC JWT.
*/
public static async create(signOptions: SignOptions, createVcOptions?: CreateVcOptions, verifiableCredential?: VerifiableCredentialV1): Promise<VcJwt> {
public static async create(signOptions: SignOptions, createVcOptions?: CreateVcOptions, verifiableCredential?: VerifiableCredentialTypeV1): Promise<VcJwt> {
if (createVcOptions && verifiableCredential) {
throw new Error('options and verifiableCredentials are mutually exclusive, either include the full verifiableCredential or the options to create one');
}
Expand All @@ -96,7 +100,7 @@ export class VerifiableCredential {
throw new Error('options or verifiableCredential must be provided');
}

let vc: VerifiableCredentialV1;
let vc: VerifiableCredentialTypeV1;

if (verifiableCredential) {
vc = verifiableCredential;
Expand Down Expand Up @@ -124,7 +128,7 @@ export class VerifiableCredential {
* @param vc - The Verifiable Credential object to validate.
* @throws Error if any validation check fails.
*/
public static validatePayload(vc: VerifiableCredentialV1): void {
public static validatePayload(vc: VerifiableCredentialTypeV1): void {
SsiValidator.validateContext(vc['@context']);
SsiValidator.validateVcType(vc.type);
SsiValidator.validateCredentialSubject(vc.credentialSubject);
Expand Down Expand Up @@ -166,19 +170,6 @@ export class VerifiableCredential {
signature : encodedSignature
};
}

/**
* Evaluates a set of verifiable credentials against a specified presentation definition.
*
* This method checks if the provided credentials meet the criteria defined in the presentation definition.
*
* @param presentationDefinition - The definition that specifies the criteria for the credentials.
* @param verifiableCredentialJwts - An array of JWT strings representing the verifiable credentials to be evaluated.
* @returns {EvaluationResults} The result of the evaluation process, indicating whether each credential meets the criteria.
*/
public static evaluateCredentials(presentationDefinition: PresentationDefinition, verifiableCredentialJwts: string[]): EvaluationResults {
return evaluateCredentials(presentationDefinition, verifiableCredentialJwts);
}
}

export class VerifiablePresentation {
Expand All @@ -189,7 +180,12 @@ export class VerifiablePresentation {
* @returns A promise that resolves to a VP JWT.
*/
public static async create(signOptions: SignOptions, createVpOptions: CreateVpOptions,): Promise<VpJwt> {
const evaluationResults: EvaluationResults = VerifiableCredential.evaluateCredentials(createVpOptions.presentationDefinition, createVpOptions.verifiableCredentialJwts);
resetPex();

const pdValidated: Validated = validateDefinition(createVpOptions.presentationDefinition);
isValid(pdValidated);

const evaluationResults: EvaluationResults = evaluateCredentials(createVpOptions.presentationDefinition, createVpOptions.verifiableCredentialJwts);

if (evaluationResults.warnings?.length) {
console.warn('Warnings were generated during the evaluation process: ' + JSON.stringify(evaluationResults.warnings));
Expand All @@ -209,6 +205,10 @@ export class VerifiablePresentation {
}

const presentationResult: PresentationResult = presentationFrom(createVpOptions.presentationDefinition, createVpOptions.verifiableCredentialJwts);

const submissionValidated: Validated = validateSubmission(presentationResult.presentationSubmission);
isValid(submissionValidated);

const verifiablePresentation: VerifiablePresentationV1 = presentationResult.presentation;
const vpJwt: VpJwt = await createJwt({ payload: { vp: verifiablePresentation }, subject: signOptions.subjectDid, issuer: signOptions.issuerDid, kid: signOptions.kid, signer: signOptions.signer });

Expand Down Expand Up @@ -271,19 +271,6 @@ export class VerifiablePresentation {
signature : encodedSignature
};
}

/**
* Evaluates a given Verifiable Presentation against a specified presentation definition.
*
* This method checks if the presentation meets the criteria defined in the presentation definition.
*
* @param presentationDefinition - The definition that specifies the criteria for the presentation.
* @param presentation - The Verifiable Presentation to evaluate.
* @returns {EvaluationResults} The result of the evaluation process, indicating whether the presentation meets the criteria.
*/
public static evaluatePresentation(presentationDefinition: PresentationDefinition, presentation: VerifiablePresentationV1): EvaluationResults {
return evaluatePresentation(presentationDefinition, presentation);
}
}

async function createJwt(options: CreateJwtOpts) {
Expand All @@ -307,4 +294,19 @@ async function createJwt(options: CreateJwtOpts) {
const jwt = message + '.' + encodedSignature;

return jwt;
}

function isValid(validated: Validated) {
let errorMessage = 'Failed to pass validation check due to: ';
if (Array.isArray(validated)) {
if (!validated.every(item => item.status === 'info')) {
errorMessage += 'Validation Errors: ' + JSON.stringify(validated);
throw new Error(errorMessage);
}
} else {
if (validated.status !== 'info') {
errorMessage += 'Validation Errors: ' + JSON.stringify(validated);
throw new Error(errorMessage);
}
}
}
70 changes: 65 additions & 5 deletions packages/credentials/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { PresentationDefinitionV2, InputDescriptorV2 } from '@sphereon/pex-models';
import type { EvaluationResults as ER, PresentationResult as PexPR } from '@sphereon/pex';
import type { PresentationDefinitionV2, PresentationDefinitionV1 as PexPresDefV1, InputDescriptorV2 } from '@sphereon/pex-models';
import type { EvaluationResults as ER, PresentationResult as PexPR, SelectResults as PexSR, Validated as PexValidated } from '@sphereon/pex';
import type {
IIssuer,
ICredential,
Expand All @@ -17,22 +17,30 @@ import type {
JwtDecodedVerifiablePresentation as PexJwtDecodedPres,
} from '@sphereon/ssi-types';

import { PEXv2 } from '@sphereon/pex';
import { PEX } from '@sphereon/pex';

const pex = new PEXv2();
let pex = new PEX();

export const DEFAULT_CONTEXT = 'https://www.w3.org/2018/credentials/v1';
export const DEFAULT_VC_TYPE = 'VerifiableCredential';
export const DEFAULT_VP_TYPE = 'VerifiablePresentation';

/**
* Resets the state for PEX lib, needed for every fresh Presentation Exchange flow.
*/
export const resetPex = () => {
pex = new PEX();
};


/** Presentation Exchange */

/**
* A Verifiable Credential is a set of one or more claims made by the same entity.
*
* @see {@link https://www.w3.org/TR/vc-data-model/#credentials | VC Data Model}
*/
export type VerifiableCredentialV1 = ICredential;
export type VerifiableCredentialTypeV1 = ICredential;

/**
* A Credential Context is to convey the meaning of the data and term definitions of the data in a verifiable credential.
Expand Down Expand Up @@ -78,6 +86,13 @@ export type CredentialSubject = (ICredentialSubject & AdditionalClaims) | (ICred
*/
export type CredentialStatus = ICredentialStatus;

/**
* Presentation Definition: Outlines the requirements Verifiers have for Proofs.
*
* @see {@link https://identity.foundation/presentation-exchange/#presentation-definition | Presentation Definition}
*/
export type PresentationDefinitionType = PexPresDefV1;

/**
* Presentation Definition: Outlines the requirements Verifiers have for Proofs.
*
Expand Down Expand Up @@ -133,6 +148,17 @@ export type PresentationResult = PexPR;
*/
export type EvaluationResults = ER;


/**
* Search Result: The outcome of the select results process.
*/
export type SelectResults = PexSR;

/**
* Validated: The outcome of the validation process.
*/
export type Validated = PexValidated;

/**
* Evaluates given credentials against a presentation definition.
* @returns {EvaluationResults} The result of the evaluation process.
Expand Down Expand Up @@ -166,6 +192,40 @@ export const presentationFrom = (
return pex.presentationFrom(presentationDefinition, verifiableCredentials);
};

/**
* The selectFrom method is a helper function that helps filter out the verifiable credentials which can not be selected and returns
* the selectable credentials.
*
* @param presentationDefinition the definition of what is expected in the presentation.
* @param verifiableCredentials verifiable credentials are the credentials from wallet provided to the library to find selectable credentials.
*
* @return the selectable credentials.
*/
export const selectFrom = (presentationDefinition: PresentationDefinition, verifiableCredentials: VcJwt[]): SelectResults => {
return pex.selectFrom(presentationDefinition, verifiableCredentials);
};

/**
* This method validates whether an object is usable as a presentation definition or not.
*
* @param presentationDefinition: presentationDefinition to be validated.
*
* @return the validation results to reveal what is acceptable/unacceptable about the passed object to be considered a valid presentation definition
*/
export const validateDefinition = (presentationDefinition: PresentationDefinition): Validated => {
return PEX.validateDefinition(presentationDefinition);
};

/**
* This method validates whether an object is usable as a presentation submission or not.
*
* @param presentationSubmission the object to be validated.
*
* @return the validation results to reveal what is acceptable/unacceptable about the passed object to be considered a valid presentation submission
*/
export const validateSubmission = (presentationSubmission: PresentationSubmission): Validated => {
return PEX.validateSubmission(presentationSubmission);
};

/** Credential Manifest */

Expand Down
4 changes: 2 additions & 2 deletions packages/credentials/src/validators.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type {
VerifiableCredentialV1,
VerifiableCredentialTypeV1,
CredentialSubject,
CredentialContextType,
} from './types.js';
Expand All @@ -13,7 +13,7 @@ import {
import { isValidXmlSchema112Timestamp } from './utils.js';

export class SsiValidator {
static validateCredentialPayload(vc: VerifiableCredentialV1): void {
static validateCredentialPayload(vc: VerifiableCredentialTypeV1): void {
this.validateContext(vc['@context']);
this.validateVcType(vc.type);
this.validateCredentialSubject(vc.credentialSubject);
Expand Down
56 changes: 39 additions & 17 deletions packages/credentials/tests/presentation-exchange.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import type { PortableDid } from '@web5/dids';
import type { JwsHeaderParams } from '@web5/crypto';
import type {
PresentationResult,
VerifiableCredentialV1,
VerifiableCredentialTypeV1,
PresentationDefinition,
JwtDecodedVerifiablePresentation,
Validated,
} from '../src/types.js';

import { evaluateCredentials, evaluatePresentation, presentationFrom } from '../src/types.js';
import { evaluateCredentials, evaluatePresentation, presentationFrom, validateDefinition, validateSubmission, resetPex } from '../src/types.js';

import { expect } from 'chai';
import { Convert } from '@web5/common';
Expand Down Expand Up @@ -40,7 +40,6 @@ describe('PresentationExchange', () => {
let signer: Signer;
let btcCredentialJwt: string;
let presentationDefinition: PresentationDefinition;
let presentationResult: PresentationResult;

before(async () => {
alice = await DidKeyMethod.create();
Expand All @@ -55,23 +54,47 @@ describe('PresentationExchange', () => {
presentationDefinition = createPresentationDefinition();
});

beforeEach(() => {
resetPex();
});

it('should evaluate credentials without any errors or warnings', async () => {
const evaluationResults = evaluateCredentials(presentationDefinition, [btcCredentialJwt]);

expect(evaluationResults.errors).to.be.an('array');
expect(evaluationResults.errors?.length).to.equal(0);
expect(evaluationResults.warnings).to.be.an('array');
expect(evaluationResults.warnings?.length).to.equal(0);
expect(evaluationResults.errors).to.deep.equal([]);
expect(evaluationResults.warnings).to.deep.equal([]);
});

it('should successfully create a presentation from the given definition and credentials', () => {
presentationResult = presentationFrom(presentationDefinition, [btcCredentialJwt]);
evaluateCredentials(presentationDefinition, [btcCredentialJwt]);
const presentationResult = presentationFrom(presentationDefinition, [btcCredentialJwt]);

expect(presentationResult).to.exist;
expect(presentationResult.presentationSubmission.definition_id).to.equal(presentationDefinition.id);
});

it('should successfully validate a presentation definition', () => {
const result:Validated = validateDefinition(presentationDefinition);
expect(result).to.deep.equal([{ tag: 'root', status: 'info', message: 'ok' }]);
});

it('should successfully validate a submission', () => {
const evaluationResults = evaluateCredentials(presentationDefinition, [btcCredentialJwt]);
expect(evaluationResults.errors).to.deep.equal([]);
expect(evaluationResults.warnings).to.deep.equal([]);

const presentationResult = presentationFrom(presentationDefinition, [btcCredentialJwt]);

const result:Validated = validateSubmission(presentationResult.presentationSubmission);
expect(result).to.deep.equal([{ tag: 'root', status: 'info', message: 'ok' }]);
});

it('should evaluate the presentation without any errors or warnings', async () => {
const credEvaluationResults = evaluateCredentials(presentationDefinition, [btcCredentialJwt]);
expect(credEvaluationResults.errors).to.deep.equal([]);
expect(credEvaluationResults.warnings).to.deep.equal([]);

const presentationResult = presentationFrom(presentationDefinition, [btcCredentialJwt]);
const vpJwt = await createJwt({
header,
issuer : alice.did,
Expand All @@ -82,13 +105,12 @@ describe('PresentationExchange', () => {

const presentation = decodeJwt(vpJwt).payload.vp;

const { warnings, errors } = evaluatePresentation(presentationDefinition, presentation );

expect(errors).to.be.an('array');
expect(errors?.length).to.equal(0);
const presentationEvaluationResults = evaluatePresentation(presentationDefinition, presentation );
expect(presentationEvaluationResults.errors).to.deep.equal([]);
expect(presentationEvaluationResults.warnings).to.deep.equal([]);

expect(warnings).to.be.an('array');
expect(warnings?.length).to.equal(0);
const result:Validated = validateSubmission(presentationResult.presentationSubmission);
expect(result).to.deep.equal([{ tag: 'root', status: 'info', message: 'ok' }]);
});

it('should successfully execute the complete presentation exchange flow', async () => {
Expand All @@ -99,7 +121,7 @@ describe('PresentationExchange', () => {
expect(evaluationResults.warnings).to.be.an('array');
expect(evaluationResults.warnings?.length).to.equal(0);

presentationResult = presentationFrom(presentationDefinition, [btcCredentialJwt]);
const presentationResult = presentationFrom(presentationDefinition, [btcCredentialJwt]);

expect(presentationResult).to.exist;
expect(presentationResult.presentationSubmission.definition_id).to.equal(presentationDefinition.id);
Expand All @@ -126,7 +148,7 @@ describe('PresentationExchange', () => {
});

async function createBtcCredentialJwt(aliceDid: string, header: JwtHeaderParams, signer: Signer) {
const btcCredential: VerifiableCredentialV1 = {
const btcCredential: VerifiableCredentialTypeV1 = {
'@context' : ['https://www.w3.org/2018/credentials/v1'],
'id' : 'btc-credential',
'type' : ['VerifiableCredential'],
Expand Down
Loading

0 comments on commit 82558a7

Please sign in to comment.