diff --git a/packages/credentials/src/ssi.ts b/packages/credentials/src/ssi.ts index dba56dbbe..2720678d4 100644 --- a/packages/credentials/src/ssi.ts +++ b/packages/credentials/src/ssi.ts @@ -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, @@ -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'; @@ -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 { + public static async create(signOptions: SignOptions, createVcOptions?: CreateVcOptions, verifiableCredential?: VerifiableCredentialTypeV1): Promise { if (createVcOptions && verifiableCredential) { throw new Error('options and verifiableCredentials are mutually exclusive, either include the full verifiableCredential or the options to create one'); } @@ -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; @@ -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); @@ -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 { @@ -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 { - 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)); @@ -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 }); @@ -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) { @@ -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); + } + } } \ No newline at end of file diff --git a/packages/credentials/src/types.ts b/packages/credentials/src/types.ts index 0b8d086be..34fb6be58 100644 --- a/packages/credentials/src/types.ts +++ b/packages/credentials/src/types.ts @@ -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, @@ -17,14 +17,22 @@ 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 */ /** @@ -32,7 +40,7 @@ export const DEFAULT_VP_TYPE = 'VerifiablePresentation'; * * @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. @@ -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. * @@ -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. @@ -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 */ diff --git a/packages/credentials/src/validators.ts b/packages/credentials/src/validators.ts index 8af65fe87..13c10d4cc 100644 --- a/packages/credentials/src/validators.ts +++ b/packages/credentials/src/validators.ts @@ -1,5 +1,5 @@ import type { - VerifiableCredentialV1, + VerifiableCredentialTypeV1, CredentialSubject, CredentialContextType, } from './types.js'; @@ -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); diff --git a/packages/credentials/tests/presentation-exchange.spec.ts b/packages/credentials/tests/presentation-exchange.spec.ts index 77b6dcc51..d8a7a7832 100644 --- a/packages/credentials/tests/presentation-exchange.spec.ts +++ b/packages/credentials/tests/presentation-exchange.spec.ts @@ -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'; @@ -40,7 +40,6 @@ describe('PresentationExchange', () => { let signer: Signer; let btcCredentialJwt: string; let presentationDefinition: PresentationDefinition; - let presentationResult: PresentationResult; before(async () => { alice = await DidKeyMethod.create(); @@ -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, @@ -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 () => { @@ -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); @@ -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'], diff --git a/packages/credentials/tests/ssi.spec.ts b/packages/credentials/tests/ssi.spec.ts index 8fb4ba854..cdb0404b5 100644 --- a/packages/credentials/tests/ssi.spec.ts +++ b/packages/credentials/tests/ssi.spec.ts @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { VcJwt, VpJwt, EvaluationResults, VerifiableCredentialV1, PresentationDefinition} from '../src/types.js'; +import { VcJwt, VpJwt, VerifiableCredentialTypeV1, PresentationDefinition} from '../src/types.js'; import {VerifiableCredential, VerifiablePresentation, CreateVcOptions, CreateVpOptions, SignOptions} from '../src/ssi.js'; import { Ed25519, Jose } from '@web5/crypto'; import { DidKeyMethod } from '@web5/dids'; @@ -43,7 +43,7 @@ describe('SSI Tests', () => { }); it('creates a VC JWT with VerifiableCredentialV1 type', async () => { - const vc:VerifiableCredentialV1 = { + const vc:VerifiableCredentialTypeV1 = { id : 'id123', '@context' : ['https://www.w3.org/2018/credentials/v1'], credentialSubject : { id: subjectIssuerDid, btcAddress: 'abc123' }, @@ -57,7 +57,7 @@ describe('SSI Tests', () => { }); it('creates a VC JWT with a VC', async () => { - const btcCredential: VerifiableCredentialV1 = { + const btcCredential: VerifiableCredentialTypeV1 = { '@context' : ['https://www.w3.org/2018/credentials/v1'], 'id' : 'btc-credential', 'type' : ['VerifiableCredential'], @@ -154,21 +154,6 @@ describe('SSI Tests', () => { expect(async () => await VerifiablePresentation.verify(vpJwt)).to.not.throw(); }); - it('evaluates a VP', async () => { - const vpCreateOptions: CreateVpOptions = { - presentationDefinition : getPresentationDefinition(), - verifiableCredentialJwts : [vcJwt] - }; - - const vpJwt: VpJwt = await VerifiablePresentation.create(signOptions, vpCreateOptions, ); - const result: EvaluationResults = VerifiablePresentation.evaluatePresentation(getPresentationDefinition(), VerifiablePresentation.decode(vpJwt).payload.vp); - - expect(result.warnings).to.be.an('array'); - expect(result.warnings!.length).to.equal(0); - expect(result.errors).to.be.an('array'); - expect(result.errors!.length).to.equal(0); - }); - it('evaluates an invalid VP with empty VCs', async () => { const vpCreateOptions: CreateVpOptions = { presentationDefinition : getPresentationDefinition(), @@ -217,6 +202,23 @@ describe('SSI Tests', () => { expect(err!.message).to.equal('Failed to create Verifiable Presentation JWT due to: Required Credentials Not Present: "error"Errors: [{"tag":"FilterEvaluation","status":"error","message":"Input candidate does not contain property: $.input_descriptors[0]: $.verifiableCredential[0]"},{"tag":"MarkForSubmissionEvaluation","status":"error","message":"The input candidate is not eligible for submission: $.input_descriptors[0]: $.verifiableCredential[0]"}]'); } }); + + it('evaluates an invalid VP with an invalid presentation definition', async () => { + const presentationDefinition = getPresentationDefinition(); + presentationDefinition.frame = { '@id': 'this is not valid' }; + + const vpCreateOptions: CreateVpOptions = { + presentationDefinition : presentationDefinition, + verifiableCredentialJwts : [vcJwt] + }; + + try { + await VerifiablePresentation.create(signOptions, vpCreateOptions); + } catch (err: any) { + expect(err).instanceOf(Error); + expect(err!.message).to.equal('Failed to pass validation check due to: Validation Errors: [{"tag":"presentation_definition.frame","status":"error","message":"frame value is not valid"}]'); + } + }); }); }); diff --git a/packages/credentials/tests/verifiable-credentials.spec.ts b/packages/credentials/tests/verifiable-credentials.spec.ts index 748117652..8a81c23e9 100644 --- a/packages/credentials/tests/verifiable-credentials.spec.ts +++ b/packages/credentials/tests/verifiable-credentials.spec.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; -import type { VerifiableCredentialV1, CredentialSubject, Issuer } from '../src/types.js'; +import type { VerifiableCredentialTypeV1, CredentialSubject, Issuer } from '../src/types.js'; import { getCurrentXmlSchema112Timestamp, getFutureXmlSchema112Timestamp} from '../src/utils.js'; @@ -16,7 +16,7 @@ describe('VerifiableCredentials', () => { }; // Create the credential - const credential: VerifiableCredentialV1 = { + const credential: VerifiableCredentialTypeV1 = { '@context' : ['https://www.w3.org/2018/credentials/v1'], type : ['VerifiableCredential'], issuer : issuer, @@ -45,7 +45,7 @@ describe('VerifiableCredentials', () => { }); it('creates a minimum viable vc', () => { - const credential: VerifiableCredentialV1 = { + const credential: VerifiableCredentialTypeV1 = { '@context' : ['https://www.w3.org/2018/credentials/v1'], type : ['VerifiableCredential'], issuer : { id: 'did:example:123456' },