From 7c6fba576eedef968b937474276151b733c128e0 Mon Sep 17 00:00:00 2001 From: Neal Date: Thu, 2 Nov 2023 14:21:43 -0700 Subject: [PATCH 1/8] refactoring credentials api interface --- package-lock.json | 26 ++ packages/credentials/package.json | 1 + packages/credentials/src/index.ts | 6 +- .../credentials/src/presentation-exchange.ts | 161 ++++++++ packages/credentials/src/ssi.ts | 312 ---------------- packages/credentials/src/types.ts | 339 ----------------- packages/credentials/src/validators.ts | 36 +- .../credentials/src/verifiable-credential.ts | 346 ++++++++++++++++++ .../tests/credential-manifest.spec.ts | 160 -------- .../tests/presentation-exchange.spec.ts | 232 ++++-------- .../credentials/tests/ssi-validator.spec.ts | 14 +- packages/credentials/tests/ssi.spec.ts | 291 --------------- .../tests/verifiable-credential.spec.ts | 160 ++++++++ .../tests/verifiable-credentials.spec.ts | 62 ---- 14 files changed, 791 insertions(+), 1355 deletions(-) create mode 100644 packages/credentials/src/presentation-exchange.ts delete mode 100644 packages/credentials/src/ssi.ts delete mode 100644 packages/credentials/src/types.ts create mode 100644 packages/credentials/src/verifiable-credential.ts delete mode 100644 packages/credentials/tests/credential-manifest.spec.ts delete mode 100644 packages/credentials/tests/ssi.spec.ts create mode 100644 packages/credentials/tests/verifiable-credential.spec.ts delete mode 100644 packages/credentials/tests/verifiable-credentials.spec.ts diff --git a/package-lock.json b/package-lock.json index 8ebf4c522..99f684f2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,6 +53,15 @@ "node": ">=0.1.90" } }, + "node_modules/@decentralized-identity/did-common-typescript": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@decentralized-identity/did-common-typescript/-/did-common-typescript-0.1.19.tgz", + "integrity": "sha512-GB3ZLXiyBmzbdur6dwqlf26+LSqeza2i5rE3sCu0vcbZ1R5+Av/vQyTZvh3zD/s4/8bdsnH942IvGu+sQqacqw==", + "dependencies": { + "base64url": "^3.0.1", + "clone": "^2.1.2" + } + }, "node_modules/@decentralized-identity/ion-pow-sdk": { "version": "1.0.17", "license": "apache-2.0", @@ -1480,6 +1489,14 @@ "node": "^4.5.0 || >= 5.9" } }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/bech32": { "version": "2.0.0", "license": "MIT" @@ -2199,6 +2216,14 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "dev": true, @@ -8560,6 +8585,7 @@ "version": "0.3.0", "license": "Apache-2.0", "dependencies": { + "@decentralized-identity/did-common-typescript": "^0.1.19", "@sphereon/pex": "2.1.0", "did-jwt": "^7.2.6", "uuid": "^9.0.0" diff --git a/packages/credentials/package.json b/packages/credentials/package.json index bec251c1b..0d8fa8fa1 100644 --- a/packages/credentials/package.json +++ b/packages/credentials/package.json @@ -73,6 +73,7 @@ "node": ">=18.0.0" }, "dependencies": { + "@decentralized-identity/did-common-typescript": "^0.1.19", "@sphereon/pex": "2.1.0", "did-jwt": "^7.2.6", "uuid": "^9.0.0" diff --git a/packages/credentials/src/index.ts b/packages/credentials/src/index.ts index cfc577223..07b19e24c 100644 --- a/packages/credentials/src/index.ts +++ b/packages/credentials/src/index.ts @@ -1,3 +1,3 @@ -export * from './types.js'; -export * as utils from './utils.js'; -export * from './ssi.js'; \ No newline at end of file +export * from './verifiable-credential.js'; +export * from './presentation-exchange.js'; +export * as utils from './utils.js'; \ No newline at end of file diff --git a/packages/credentials/src/presentation-exchange.ts b/packages/credentials/src/presentation-exchange.ts new file mode 100644 index 000000000..67cb425ab --- /dev/null +++ b/packages/credentials/src/presentation-exchange.ts @@ -0,0 +1,161 @@ +import type { EvaluationResults, PresentationResult, SelectResults, Validated as PexValidated } from '@sphereon/pex'; +import { PEX } from '@sphereon/pex'; + +import type { PresentationDefinitionV2 as PexPresDefV2 } from '@sphereon/pex-models'; + +import type { + IPresentation, + PresentationSubmission, +} from '@sphereon/ssi-types'; + +export type Validated = PexValidated; +export type PresentationDefinitionV2 = PexPresDefV2 + +export class PresentationExchange { + /** + * The Presentation Exchange (PEX) Library implements the functionality described in the DIF Presentation Exchange specification + */ + private static pex: PEX = new PEX(); + + /** + * Selects credentials that satisfy a given presentation definition. + * + * @param vcJwts The list of Verifiable Credentials to select from. + * @param presentationDefinition The Presentation Definition to match against. + * @return A list of Verifiable Credentials that satisfy the Presentation Definition. + */ + public static selectCredentials( + vcJwts: string[], + presentationDefinition: PresentationDefinitionV2 + ): string[] { + this.resetPex(); + const selectResults: SelectResults = this.pex.selectFrom(presentationDefinition, vcJwts); + return selectResults.verifiableCredential as string[] ?? []; + } + + public static satisfiesPresentationDefinition( + vcJwts: string[], + presentationDefinition: PresentationDefinitionV2 + ): void { + this.resetPex(); + const evaluationResults: EvaluationResults = this.pex.evaluateCredentials(presentationDefinition, vcJwts); + if (evaluationResults.warnings?.length) { + console.warn('Warnings were generated during the evaluation process: ' + JSON.stringify(evaluationResults.warnings)); + } + + if (evaluationResults.areRequiredCredentialsPresent.toString() !== 'info' || evaluationResults.errors?.length) { + let errorMessage = 'Failed to create Verifiable Presentation JWT due to: '; + if(evaluationResults.areRequiredCredentialsPresent) { + errorMessage += 'Required Credentials Not Present: ' + JSON.stringify(evaluationResults.areRequiredCredentialsPresent); + } + + if (evaluationResults.errors?.length) { + errorMessage += 'Errors: ' + JSON.stringify(evaluationResults.errors); + } + + throw new Error(errorMessage); + } + } + + /** + * Creates a presentation from a list of Verifiable Credentials that satisfy a given presentation definition. + * This function initializes the Presentation Exchange (PEX) process, validates the presentation definition, + * evaluates the credentials against the definition, and finally constructs the presentation result if the + * evaluation is successful. + * + * @param {string[]} vcJwts The list of Verifiable Credentials (VCs) in JWT format to be evaluated. + * @param {PresentationDefinitionV2} presentationDefinition The Presentation Definition V2 to match the VCs against. + * @returns {PresentationResult} The result of the presentation creation process, containing a presentation submission + * that satisfies the presentation definition criteria. + * @throws {Error} If the evaluation results in warnings or errors, or if the required credentials are not present, + * an error is thrown with a descriptive message. + */ + public static createPresentationFromCredentials( + vcJwts: string[], + presentationDefinition: PresentationDefinitionV2 + ): PresentationResult { + this.resetPex(); + + const pdValidated: Validated = PEX.validateDefinition(presentationDefinition); + isValid(pdValidated); + + const evaluationResults: EvaluationResults = this.pex.evaluateCredentials(presentationDefinition, vcJwts); + + if (evaluationResults.warnings?.length) { + console.warn('Warnings were generated during the evaluation process: ' + JSON.stringify(evaluationResults.warnings)); + } + + if (evaluationResults.areRequiredCredentialsPresent.toString() !== 'info' || evaluationResults.errors?.length) { + let errorMessage = 'Failed to create Verifiable Presentation JWT due to: '; + if(evaluationResults.areRequiredCredentialsPresent) { + errorMessage += 'Required Credentials Not Present: ' + JSON.stringify(evaluationResults.areRequiredCredentialsPresent); + } + + if (evaluationResults.errors?.length) { + errorMessage += 'Errors: ' + JSON.stringify(evaluationResults.errors); + } + + throw new Error(errorMessage); + } + + const presentationResult: PresentationResult = this.pex.presentationFrom(presentationDefinition, vcJwts); + + const submissionValidated: Validated = PEX.validateSubmission(presentationResult.presentationSubmission); + isValid(submissionValidated); + + return presentationResult; + } + + /** + * 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 + */ + public static validateDefinition(presentationDefinition: PresentationDefinitionV2): 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 + */ + public static validateSubmission(presentationSubmission: PresentationSubmission): Validated { + return PEX.validateSubmission(presentationSubmission); + } + + /** + * Evaluates a presentation against a presentation definition. + * @returns {EvaluationResults} The result of the evaluation process. + */ + public static evaluatePresentation( + presentationDefinition: PresentationDefinitionV2, + presentation: IPresentation + ): EvaluationResults { + this.resetPex(); + return this.pex.evaluatePresentation(presentationDefinition, presentation); + } + + private static resetPex() { + this.pex = new PEX(); + } +} + +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); + } + } +} diff --git a/packages/credentials/src/ssi.ts b/packages/credentials/src/ssi.ts deleted file mode 100644 index 3a1106fc9..000000000 --- a/packages/credentials/src/ssi.ts +++ /dev/null @@ -1,312 +0,0 @@ -import type { Resolvable, DIDResolutionResult } from 'did-resolver'; -import { - VerifiableCredentialTypeV1, - JwtDecodedVerifiableCredential, - CredentialSubject, - VerifiablePresentationV1, - PresentationResult, - EvaluationResults, - PresentationDefinition, - JwtDecodedVerifiablePresentation, - Issuer, - CredentialSchemaType, - CredentialStatus, - validateDefinition, - validateSubmission, - Validated, - resetPex -} from './types.js'; - -import { v4 as uuidv4 } from 'uuid'; -import { evaluateCredentials, presentationFrom, VcJwt, VpJwt } from './types.js'; -import { getCurrentXmlSchema112Timestamp } from './utils.js'; -import { Convert } from '@web5/common'; -import { verifyJWT } from 'did-jwt'; -import { DidIonMethod, DidKeyMethod, DidResolver } from '@web5/dids'; -import { SsiValidator } from './validators.js'; - -export type CreateVcOptions = { - credentialSubject: CredentialSubject, - issuer: Issuer, - credentialSchema?: CredentialSchemaType, - expirationDate?: string, - credentialStatus?: CredentialStatus, -} - -export type CreateVpOptions = { - presentationDefinition: PresentationDefinition, - verifiableCredentialJwts: string[] -} - -export type SignOptions = { - kid: string; - issuerDid: string; - subjectDid: string; - signer: Signer, -} - -export type DecodedVcJwt = { - header: JwtHeaderParams - payload: JwtDecodedVerifiableCredential, - signature: string -} - -export type DecodedVpJwt = { - header: JwtHeaderParams - payload: JwtDecodedVerifiablePresentation, - signature: string -} - -type CreateJwtOpts = { - payload: any; - subject: string; - issuer: string; - kid: string; - signer: Signer; -} - -type JwtHeaderParams = { - alg: string; - typ: 'JWT' - kid: string; -}; - -type Signer = (data: Uint8Array) => Promise; - -const didResolver = new DidResolver({ didResolvers: [DidIonMethod, DidKeyMethod] }); -class TbdResolver implements Resolvable { - async resolve(didUrl: string): Promise { - return await didResolver.resolve(didUrl) as DIDResolutionResult; - } -} - -const tbdResolver = new TbdResolver(); - -export class VerifiableCredential { - /** - * Creates a Verifiable Credential (VC) JWT. - * - * @param signOptions - Options for creating the signature including the kid, issuerDid, subjectDid, and signer function. - * @param createVcOptions - Optional. Options for creating the VC including the subject, issuer and other optional parameters. - * @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?: 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'); - } - - if (!createVcOptions && !verifiableCredential) { - throw new Error('options or verifiableCredential must be provided'); - } - - let vc: VerifiableCredentialTypeV1; - - if (verifiableCredential) { - vc = verifiableCredential; - } else { - vc = { - id : uuidv4(), - '@context' : ['https://www.w3.org/2018/credentials/v1'], - credentialSubject : createVcOptions!.credentialSubject, - type : ['VerifiableCredential'], - issuer : createVcOptions!.issuer, - issuanceDate : getCurrentXmlSchema112Timestamp(), - credentialSchema : createVcOptions?.credentialSchema, - expirationDate : createVcOptions?.expirationDate, - }; - } - - this.validatePayload(vc); - const vcJwt: VcJwt = await createJwt({ payload: { vc: vc }, subject: signOptions.subjectDid, issuer: signOptions.issuerDid, kid: signOptions.kid, signer: signOptions.signer }); - return vcJwt; - } - - /** - * Validates the structure and integrity of a Verifiable Credential payload. - * - * @param vc - The Verifiable Credential object to validate. - * @throws Error if any validation check fails. - */ - public static validatePayload(vc: VerifiableCredentialTypeV1): void { - SsiValidator.validateContext(vc['@context']); - SsiValidator.validateVcType(vc.type); - SsiValidator.validateCredentialSubject(vc.credentialSubject); - if (vc.issuanceDate) SsiValidator.validateTimestamp(vc.issuanceDate); - if (vc.expirationDate) SsiValidator.validateTimestamp(vc.expirationDate); - } - - /** - * Verifies the integrity of a VC JWT. - * - * @param vcJwt - The VC JWT to verify. - * @returns A boolean or errors indicating whether the JWT is valid. - */ - public static async verify(vcJwt: VcJwt): Promise { - const verificationResponse = await verifyJWT(vcJwt, { - resolver: tbdResolver - }); - - if (!verificationResponse.verified) { - throw new Error('VC JWT could not be verified. Reason: ' + JSON.stringify(verificationResponse)); - } - - const vcDecoded = VerifiableCredential.decode(vcJwt).payload.vc; - this.validatePayload(vcDecoded); - } - - /** - * Decodes a VC JWT into its constituent parts: header, payload, and signature. - * - * @param jwt - The JWT string to decode. - * @returns An object containing the decoded header, payload, and signature. - */ - public static decode(jwt: string): DecodedVcJwt { - const [encodedHeader, encodedPayload, encodedSignature] = jwt.split('.'); - - return { - header : Convert.base64Url(encodedHeader).toObject() as JwtHeaderParams, - payload : Convert.base64Url(encodedPayload).toObject() as JwtDecodedVerifiableCredential, - signature : encodedSignature - }; - } -} - -export class VerifiablePresentation { - /** - * Creates a Verifiable Presentation (VP) JWT from a presentation definition and set of credentials. - * @param signOptions - Options for creating the VP including subjectDid, issuerDid, kid, and the sign function. - * @param createVpOptions - Options for creating the VP including presentationDefinition, verifiableCredentialJwts. - * @returns A promise that resolves to a VP JWT. - */ - public static async create(signOptions: SignOptions, createVpOptions: CreateVpOptions,): Promise { - 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)); - } - - if (evaluationResults.areRequiredCredentialsPresent.toString() !== 'info' || evaluationResults.errors?.length) { - let errorMessage = 'Failed to create Verifiable Presentation JWT due to: '; - if(evaluationResults.areRequiredCredentialsPresent) { - errorMessage += 'Required Credentials Not Present: ' + JSON.stringify(evaluationResults.areRequiredCredentialsPresent); - } - - if (evaluationResults.errors?.length) { - errorMessage += 'Errors: ' + JSON.stringify(evaluationResults.errors); - } - - throw new Error(errorMessage); - } - - 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 }); - - return vpJwt; - } - - /** - * Validates the structure and integrity of a Verifiable Presentation payload. - * - * @param vp - The Verifiable Presentation object to validate. - * @throws Error if any validation check fails. - */ - public static validatePayload(vp: VerifiablePresentationV1): void { - SsiValidator.validateContext(vp['@context']); - if (vp.type) SsiValidator.validateVpType(vp.type); - // empty credential array is allowed - if (vp.verifiableCredential && vp.verifiableCredential.length >= 1) { - for (const vc of vp.verifiableCredential) { - if (typeof vc === 'string') { - VerifiableCredential.verify(vc); - } else { - SsiValidator.validateCredentialPayload(vc); - } - } - } - if (vp.expirationDate) SsiValidator.validateTimestamp(vp.expirationDate); - } - - /** - * Verifies the integrity of a VP JWT. - * - * @param vpJwt - The VP JWT to verify. - * @returns A boolean or errors indicating whether the JWT is valid. - */ - public static async verify(vpJwt: VpJwt): Promise { - const verificationResponse = await verifyJWT(vpJwt, { - resolver: tbdResolver - }); - - if (!verificationResponse.verified) { - throw new Error('VP JWT could not be verified. Reason: ' + JSON.stringify(verificationResponse)); - } - - const vpDecoded: VerifiablePresentationV1 = VerifiablePresentation.decode(vpJwt).payload.vp; - VerifiablePresentation.validatePayload(vpDecoded); - } - - /** - * Decodes a VP JWT into its constituent parts: header, payload, and signature. - * - * @param jwt - The JWT string to decode. - * @returns An object containing the decoded header, payload, and signature. - */ - public static decode(jwt: string): DecodedVpJwt { - const [encodedHeader, encodedPayload, encodedSignature] = jwt.split('.'); - - return { - header : Convert.base64Url(encodedHeader).toObject() as JwtHeaderParams, - payload : Convert.base64Url(encodedPayload).toObject() as JwtDecodedVerifiablePresentation, - signature : encodedSignature - }; - } -} - -async function createJwt(options: CreateJwtOpts) { - const header: JwtHeaderParams = { alg: 'EdDSA', typ: 'JWT', kid: options.kid }; - const { issuer, subject, payload, signer } = options; - - const jwtPayload = { - iss : issuer, - sub : subject, - ...payload, - }; - - const encodedHeader = Convert.object(header).toBase64Url(); - const encodedPayload = Convert.object(jwtPayload).toBase64Url(); - const message = encodedHeader + '.' + encodedPayload; - const messageBytes = Convert.string(message).toUint8Array(); - - const signature = await signer(messageBytes); - - const encodedSignature = Convert.uint8Array(signature).toBase64Url(); - 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 deleted file mode 100644 index 34fb6be58..000000000 --- a/packages/credentials/src/types.ts +++ /dev/null @@ -1,339 +0,0 @@ -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, - ICredentialStatus, - ICredentialSubject, - ICredentialSchemaType, - ICredentialContextType, - AdditionalClaims, -} from '@sphereon/ssi-types'; -import type { - Descriptor, - IPresentation, - JwtDecodedVerifiableCredential as PexJwtDecodedVc, - PresentationSubmission as PexPresentationSubmission, - JwtDecodedVerifiablePresentation as PexJwtDecodedPres, -} from '@sphereon/ssi-types'; - -import { PEX } from '@sphereon/pex'; - -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 VerifiableCredentialTypeV1 = ICredential; - -/** - * A Credential Context is to convey the meaning of the data and term definitions of the data in a verifiable credential. - * - * @see {@link https://www.w3.org/TR/vc-data-model/#credentials | VC Data Model} - */ -export type CredentialContextType = ICredentialContextType; - -/** - * JWT-decoded version of a Verifiable Credential, offering a structured format for credential data. - */ -export type JwtDecodedVerifiableCredential = PexJwtDecodedVc; - -/** - * Credential Schema Types are useful when enforcing a specific structure on a given collection of - * data. - * - * @see {@link https://www.w3.org/TR/vc-data-model/#data-schemas | Data Schemas} - */ -export type CredentialSchemaType = ICredentialSchemaType; - -/** - * Issuer: The acting Entity issuing a Verifiable Credential. The value of the issuer property must - * be either a URI or an object containing an `id` property. - * - * @see {@link https://www.w3.org/TR/vc-data-model/#issuer | Issuer Data Model} - */ -export type Issuer = IIssuer; - -/** - * Credential Subject: Entity that the Verifiable Credential is about. This includes one or more - * properties related to the subject. - * - * @see {@link https://www.w3.org/TR/vc-data-model/#credential-subject | Credential Subject} - */ -export type CredentialSubject = (ICredentialSubject & AdditionalClaims) | (ICredentialSubject & AdditionalClaims)[] - -/** - * Used for the discovery of information about the current status of a verifiable credential, such - * as whether it is suspended or revoked. - * - * @see {@link https://www.w3.org/TR/vc-data-model/#status | Credential Status} - */ -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. - * - * @see {@link https://identity.foundation/presentation-exchange/#presentation-definition | Presentation Definition} - */ -export type PresentationDefinition = PresentationDefinitionV2; - -/** - * Presentation Submissions are objects embedded within target Claim negotiation formats that - * express how the inputs presented as proofs to a Verifier are provided in accordance with the - * requirements specified in a Presentation Definition. - * - * @see {@link https://identity.foundation/presentation-exchange/#presentation-submission | Presentation Submission} - */ -export type PresentationSubmission = PexPresentationSubmission; - -/** - * A Verifiable Presentation expresses data from one or more verifiable credentials, and is packaged - * n such a way that the authorship of the data is verifiable. - * - * @see {@link https://www.w3.org/TR/vc-data-model/#dfn-verifiable-presentations | Verifiable Presentation} - */ -export type VerifiablePresentationV1 = IPresentation; - -/** - * JWT-decoded version of a Verifiable Presentation, offering a structured format for presentation - * data. - */ -export type JwtDecodedVerifiablePresentation = PexJwtDecodedPres; - -/** - * Represents a Json Web Token in compact form. - */ -export type VcJwt = string; - -/** - * Represents a Json Web Token in compact form. - */ -export type VpJwt = string; - -/** - * Descriptor Map: Maps descriptors in a presentation exchange context. - */ -export type DescriptorMap = Descriptor; - -/** - * Presentation Result: The outcome of a presentation process. - */ -export type PresentationResult = PexPR; - -/** - * Evaulation Result: The outcome of a evaluation process. - */ -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. - */ -export const evaluateCredentials = ( - presentationDefinition: PresentationDefinition, - verifiableCredentials: string[] -): EvaluationResults => { - return pex.evaluateCredentials(presentationDefinition, verifiableCredentials); -}; - -/** - * Evaluates a presentation against a presentation definition. - * @returns {EvaluationResults} The result of the evaluation process. - */ -export const evaluatePresentation = ( - presentationDefinition: PresentationDefinition, - presentation: VerifiablePresentationV1 -): EvaluationResults => { - return pex.evaluatePresentation(presentationDefinition, presentation); -}; - -/** - * Constructs a presentation from a presentation definition and set of credentials. - * @returns {PresentationResult} The constructed presentation. - */ -export const presentationFrom = ( - presentationDefinition: PresentationDefinition, - verifiableCredentials: string[] -): PresentationResult => { - 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 */ - -/** - * Input Descriptors are objects used to describe the information a Verifier requires of a Holder. - * All Input Descriptors MUST be satisfied, unless otherwise specified by a Feature. - * - * See {@link https://identity.foundation/presentation-exchange/#input-descriptor-object | Input Descriptor} - */ -export type InputDescriptor = InputDescriptorV2; - -/** - * See {@link https://identity.foundation/wallet-rendering/v0.0.1/#entity-styles | Entity Styles} - */ -export type EntityStyle = { - thumbnail?: Image - hero?: Image - background?: Colorable - text?: Colorable -} - -export type Image = { - /** Valid URI string to an image resource */ - uri: string; - /** String that describes the alternate text for the image */ - alt?: string; -} - -export type Colorable = { - /** HEX string color value */ - color?: string; -} - -/** - * See {@link https://identity.foundation/credential-manifest/#output-descriptor | Output Descriptor} - */ -export type OutputDescriptor = { - /** String that does not conflict with the `id` of another OutputDescriptor in the same - * CredentialManifest */ - id: string; - /** String specifying the schema of the credential to be issued */ - schema: string; - /** Human-readable string that describes what the credential represents */ - name?: string; - /** Human-readable string that descripbes what the credential is in greater detail */ - description?: string; - /** Object or URI of the {@link https://identity.foundation/wallet-rendering/v0.0.1/#entity-styles | Entity Style} to render the OutputDescriptor */ - styles?: EntityStyle | string; - /** Object or URI of the {@link https://identity.foundation/wallet-rendering/v0.0.1/#display-mapping-object | Display Mapping} used to pull data from the target Claim */ - display?: DisplayMapping | string; -} - -/** See {@link https://identity.foundation/wallet-rendering/v0.0.1/#display-mapping-object | Display Mapping Object} */ -export type DisplayMapping = { - /** Array of JSONPath string expressions */ - path: string[]; - schema: { - /** Represents the type of data found with the `path` property */ - type: 'string' | 'boolean' | 'number' | 'integer'; - /** If the `type` property is "string", this property is used to format the string in any rendered UI */ - format?: 'date-time' | 'time' | 'date' | 'email' | 'idn-email' | 'hostname' | 'idn-hostname' | - 'ipv4' | 'ipv6' | 'uri' | 'uri-reference' | 'iri' | 'iri-reference'; - } - /** - * String to be rendered into the UI if all the `path` property's item's value is - * undefined OR incorrectly processed - */ - fallback?: string; -} - -/** See {@link https://identity.foundation/presentation-exchange/#presentation-definition | Presentation Definiton}'s `format` property */ -export type Format = { - [key: string]: any; -} - -/** See {@link https://identity.foundation/presentation-exchange/#input-descriptor-object | Input Descriptor}'s `constraints.fields.filter` property */ -export type Filter = { - [key: string]: any; -} - -/** - * Credential Manifests are a resource format that defines preconditional requirements, Issuer style - * preferences, and other facets User Agents utilize to help articulate and select the inputs - * necessary for processing and issuance of a specified credential. - * - * See {@link https://identity.foundation/credential-manifest/#credential-manifest | Credential Manifest} - */ -export type CredentialManifest = { - /** String providing a unique identifier for the desired context */ - id: string; - /** String that acts as a summarizing title for the CredentialManifest */ - name?: string; - /** - * String explaining what the CredentialManifest is generally offering for meeting - * its requirements - */ - description?: string; - spec_version?: string; - issuer: Issuer; - /** Output Descriptors are used by an Issuer to describe the credentials they are offering to a - * Holder. See Output Descriptor */ - output_descriptors: OutputDescriptor[]; - format?: Format; - /** - * Presentation Exchange is a specification codifying a Presentation Definition data format - * Verifiers can use to articulate proof requirements in a Presentation Request, and a - * Presentation Submission data format Holders can use to describe proofs submitted in accordance - * with them. - */ - presentation_definition?: PresentationDefinition; -} \ No newline at end of file diff --git a/packages/credentials/src/validators.ts b/packages/credentials/src/validators.ts index 13c10d4cc..3f17b9404 100644 --- a/packages/credentials/src/validators.ts +++ b/packages/credentials/src/validators.ts @@ -1,27 +1,26 @@ -import type { - VerifiableCredentialTypeV1, - CredentialSubject, - CredentialContextType, -} from './types.js'; - import { DEFAULT_CONTEXT, DEFAULT_VC_TYPE, - DEFAULT_VP_TYPE, -} from './types.js'; + VerifiableCredential +} from './verifiable-credential.js'; import { isValidXmlSchema112Timestamp } from './utils.js'; +import type { + ICredentialContextType, + ICredentialSubject +} from '@sphereon/ssi-types'; + export class SsiValidator { - static validateCredentialPayload(vc: VerifiableCredentialTypeV1): void { - this.validateContext(vc['@context']); + static validateCredentialPayload(vc: VerifiableCredential): void { + this.validateContext(vc.vcDataModel['@context']); this.validateVcType(vc.type); - this.validateCredentialSubject(vc.credentialSubject); - if (vc.issuanceDate) this.validateTimestamp(vc.issuanceDate); - if (vc.expirationDate) this.validateTimestamp(vc.expirationDate); + this.validateCredentialSubject(vc.vcDataModel.credentialSubject); + if (vc.vcDataModel.issuanceDate) this.validateTimestamp(vc.vcDataModel.issuanceDate); + if (vc.vcDataModel.expirationDate) this.validateTimestamp(vc.vcDataModel.expirationDate); } - static validateContext(value: CredentialContextType | CredentialContextType[]): void { + static validateContext(value: ICredentialContextType | ICredentialContextType[]): void { const input = this.asArray(value); if (input.length < 1 || input.indexOf(DEFAULT_CONTEXT) === -1) { throw new Error(`@context is missing default context "${DEFAULT_CONTEXT}"`); @@ -35,7 +34,7 @@ export class SsiValidator { } } - static validateCredentialSubject(value: CredentialSubject | CredentialSubject[]): void { + static validateCredentialSubject(value: ICredentialSubject | ICredentialSubject[]): void { if (Object.keys(value).length === 0) { throw new Error(`credentialSubject must not be empty`); } @@ -47,13 +46,6 @@ export class SsiValidator { } } - static validateVpType(value: string | string[]): void { - const input = this.asArray(value); - if (input.length < 1 || input.indexOf(DEFAULT_VP_TYPE) === -1) { - throw new TypeError(`type is missing default "${DEFAULT_VP_TYPE}"`); - } - } - static asArray(arg: any | any[]): any[] { return Array.isArray(arg) ? arg : [arg]; } diff --git a/packages/credentials/src/verifiable-credential.ts b/packages/credentials/src/verifiable-credential.ts new file mode 100644 index 000000000..486b5832b --- /dev/null +++ b/packages/credentials/src/verifiable-credential.ts @@ -0,0 +1,346 @@ +import type { Resolvable, DIDResolutionResult} from 'did-resolver'; +import {VerificationMethod} from 'did-resolver'; +import type { + ICredential, + ICredentialSubject, + JwtDecodedVerifiableCredential } from '@sphereon/ssi-types'; + +import { v4 as uuidv4 } from 'uuid'; +import { getCurrentXmlSchema112Timestamp } from './utils.js'; +import { Convert } from '@web5/common'; +import { verifyJWT } from 'did-jwt'; +import { DidIonMethod, DidKeyMethod, DidResolver } from '@web5/dids'; +import { SsiValidator } from './validators.js'; + +import { DidDocument } from '@decentralized-identity/did-common-typescript'; + +export const DEFAULT_CONTEXT = 'https://www.w3.org/2018/credentials/v1'; +export const DEFAULT_VC_TYPE = 'VerifiableCredential'; + +/** + * 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 VcDataModel = ICredential; + +export type SignOptions = { + kid: string; + issuerDid: string; + subjectDid: string; + signer: Signer, +} + +type Signer = (data: Uint8Array) => Promise; + +type CredentialSubject = ICredentialSubject; + +type JwtHeaderParams = { + alg: string; + typ: 'JWT' + kid: string; +}; + +type DecodedVcJwt = { + header: JwtHeaderParams + payload: JwtDecodedVerifiableCredential, + signature: string +} + +const didResolver = new DidResolver({ didResolvers: [DidIonMethod, DidKeyMethod] }); + +class TbdResolver implements Resolvable { + async resolve(didUrl: string): Promise { + return await didResolver.resolve(didUrl) as DIDResolutionResult; + } +} + +const tbdResolver = new TbdResolver(); + +/** + * `VerifiableCredential` represents a digitally verifiable credential according to the + * [W3C Verifiable Credentials Data Model](https://www.w3.org/TR/vc-data-model/). + * + * It provides functionalities to sign, verify, and create credentials, offering a concise API to + * work with JWT representations of verifiable credentials and ensuring that the signatures + * and claims within those JWTs can be validated. + * + * @property vcDataModel The [VcDataModel] instance representing the core data model of a verifiable credential. + */ +export class VerifiableCredential { + constructor(public vcDataModel: VcDataModel) {} + + get type(): string { + return this.vcDataModel.type[this.vcDataModel.type.length - 1]; + } + + get issuer(): string { + return this.vcDataModel.issuer.toString(); + } + + get subject(): string { + if (Array.isArray(this.vcDataModel.credentialSubject)) { + return this.vcDataModel.credentialSubject[0].id!; + } else { + return this.vcDataModel.credentialSubject.id!; + } + } + + /** + * Sign a verifiable credential using [signOptions] + * + * + * @param signOptions The sign options used to sign the credential. + * @return The JWT representing the signed verifiable credential. + * + * Example: + * ``` + * val signedVc = verifiableCredential.sign(signOptions) + * ``` + */ + // TODO: Refactor to look like: sign(did: Did, assertionMethodId?: string) + public async sign(signOptions: SignOptions): Promise { + const vcJwt: string = await createJwt({ vc: this.vcDataModel }, signOptions); + return vcJwt; + } + + /** + * Converts the current object to its JSON representation. + * + * @return The JSON representation of the object. + */ + public toString(): string { + return JSON.stringify(this.vcDataModel); + } + + /** + * Create a [VerifiableCredential] based on the provided parameters. + * + * @param type The type of the credential, as a [String]. + * @param issuer The issuer URI of the credential, as a [String]. + * @param subject The subject URI of the credential, as a [String]. + * @param data The credential data, as a generic type [T]. + * @return A [VerifiableCredential] instance. + * + * Example: + * ``` + * const vc = VerifiableCredential.create("ExampleCredential", "http://example.com/issuers/1", "http://example.com/subjects/1", myData) + * ``` + */ + public static create( + type: string, + issuer: string, + subject: string, + data: T, + expirationDate?: string + ): VerifiableCredential { + const jsonData = JSON.parse(JSON.stringify(data)); + + if (typeof jsonData !== 'object') { + throw new Error('Expected data to be parseable into a JSON object'); + } + + if(!issuer || !subject) { + throw new Error('Issuer and subject must be defined'); + } + + const credentialSubject: CredentialSubject = { + id: subject, + ...jsonData + }; + + const vcDataModel: VcDataModel = { + '@context' : [DEFAULT_CONTEXT], + type : [DEFAULT_VC_TYPE, type], + id : `urn:uuid:${uuidv4()}`, + issuer : issuer, + issuanceDate : getCurrentXmlSchema112Timestamp(), + credentialSubject : credentialSubject, + ...(expirationDate && { expirationDate }), // optional property + }; + + validatePayload(vcDataModel); + return new VerifiableCredential(vcDataModel); + } + + /** + * Verifies the integrity and authenticity of a Verifiable Credential (VC) encoded as a JSON Web Token (JWT). + * + * This function performs several crucial validation steps to ensure the trustworthiness of the provided VC: + * - Parses and validates the structure of the JWT. + * - Ensures the presence of critical header elements `alg` and `kid` in the JWT header. + * - Resolves the Decentralized Identifier (DID) and retrieves the associated DID Document. + * - Validates the DID and establishes a set of valid verification method IDs. + * - Identifies the correct Verification Method from the DID Document based on the `kid` parameter. + * - Verifies the JWT's signature using the public key associated with the Verification Method. + * + * If any of these steps fail, the function will throw a [Error] with a message indicating the nature of the failure. + * + * @param vcJwt The Verifiable Credential in JWT format as a [string]. + * @throws Error if the verification fails at any step, providing a message with failure details. + * @throws Error if critical JWT header elements are absent. + * + * ### Example: + * ``` + * try { + * VerifiableCredential.verify(signedVcJwt) + * console.log("VC Verification successful!") + * } catch (e: Error) { + * console.log("VC Verification failed: ${e.message}") + * } + * ``` + */ + public static async verify(vcJwt: string): Promise { + const jwt = decode(vcJwt); // Parse and validate JWT + + // Ensure the presence of critical header elements `alg` and `kid` + if (!jwt.header.alg || !jwt.header.kid) { + throw new Error('Signature verification failed: Expected JWS header to contain alg and kid'); + } + + const parsedDidUrl = DidDocument.getDidFromKeyId(jwt.header.kid); + const fragment = jwt.header.kid.split('#')[1]; + + if(!fragment || ! parsedDidUrl) { + throw new Error('Signature verification failed: Expected kid in JWS header to be a DID URL'); + } + + const didResolutionResult: DIDResolutionResult = await tbdResolver.resolve(parsedDidUrl); + if (didResolutionResult.didResolutionMetadata.error) { + throw new Error( + `Signature verification failed: Failed to resolve DID ${parsedDidUrl}. ` + + `Error: ${didResolutionResult.didResolutionMetadata.error}` + ); + } + + const verificationMethodIds = new Set([`${parsedDidUrl}#${fragment}`, `#${fragment}`]); + + if (!didResolutionResult.didDocument?.assertionMethod || !didResolutionResult.didDocument?.verificationMethod) { + throw new Error( + 'Signature verification failed: Expected kid in JWS header to dereference ' + + 'a DID Document Verification Method with an Assertion verification relationship' + ); + } + + const assertionMethods = didResolutionResult.didDocument?.assertionMethod; + + let assertionMethod: VerificationMethod | undefined; + + for (const element of assertionMethods) { + if (typeof element === 'string') { + if (verificationMethodIds.has(element)) { + assertionMethod = didResolutionResult.didDocument?.verificationMethod.find(vm => vm.id === element); + break; + } + } else { + if (verificationMethodIds.has(element.id)) { + assertionMethod = didResolutionResult.didDocument?.verificationMethod.find(vm => vm.id === element.id); + break; + } + } + } + + if (!assertionMethod) { + throw new Error( + 'Signature verification failed: Expected kid in JWS header to dereference ' + + 'a DID Document Verification Method with an Assertion verification relationship' + ); + } + + if (assertionMethod.type !== 'JsonWebKey2020' || !assertionMethod.publicKeyJwk) { + throw new Error( + 'Signature verification failed: Expected kid in JWS header to dereference ' + + 'a DID Document Verification Method of type JsonWebKey2020 with a publicKeyJwk' + ); + } + + // Perform the signature verification + const verificationResponse = await verifyJWT(vcJwt, { + resolver: tbdResolver + }); + + if (!verificationResponse.verified) { + throw new Error('VC JWT could not be verified. Reason: ' + JSON.stringify(verificationResponse)); + } + } + + /** + * Parses a JWT into a [VerifiableCredential] instance. + * + * @param vcJwt The verifiable credential JWT as a [String]. + * @return A [VerifiableCredential] instance derived from the JWT. + * + * Example: + * ``` + * val vc = VerifiableCredential.parseJwt(signedVcJwt) + * ``` + */ + public static parseJwt(vcJwt: string): VerifiableCredential { + const decodedVcJwt: DecodedVcJwt = decode(vcJwt); + const vcDataModel: VcDataModel = decodedVcJwt.payload.vc; + + if(!vcDataModel) { + throw Error('Jwt payload missing vc property'); + } + + return new VerifiableCredential(vcDataModel); + } +} + +/** + * Validates the structure and integrity of a Verifiable Credential payload. + * + * @param vc - The Verifiable Credential object to validate. + * @throws Error if any validation check fails. + */ +function validatePayload(vc: VcDataModel): void { + SsiValidator.validateContext(vc['@context']); + SsiValidator.validateVcType(vc.type); + SsiValidator.validateCredentialSubject(vc.credentialSubject); + if (vc.issuanceDate) SsiValidator.validateTimestamp(vc.issuanceDate); + if (vc.expirationDate) SsiValidator.validateTimestamp(vc.expirationDate); +} + +/** + * Decodes a VC JWT into its constituent parts: header, payload, and signature. + * + * @param jwt - The JWT string to decode. + * @returns An object containing the decoded header, payload, and signature. + */ +function decode(jwt: string): DecodedVcJwt { + const [encodedHeader, encodedPayload, encodedSignature] = jwt.split('.'); + + if (!encodedHeader || !encodedPayload || !encodedSignature) { + throw Error('Not a valid jwt'); + } + + return { + header : Convert.base64Url(encodedHeader).toObject() as JwtHeaderParams, + payload : Convert.base64Url(encodedPayload).toObject() as JwtDecodedVerifiableCredential, + signature : encodedSignature + }; +} + +async function createJwt(payload: any, signOptions: SignOptions) { + const { issuerDid, subjectDid, signer, kid } = signOptions; + + const header: JwtHeaderParams = { alg: 'EdDSA', typ: 'JWT', kid: kid }; + + const jwtPayload = { + iss : issuerDid, + sub : subjectDid, + ...payload, + }; + + const encodedHeader = Convert.object(header).toBase64Url(); + const encodedPayload = Convert.object(jwtPayload).toBase64Url(); + const message = encodedHeader + '.' + encodedPayload; + const messageBytes = Convert.string(message).toUint8Array(); + + const signature = await signer(messageBytes); + + const encodedSignature = Convert.uint8Array(signature).toBase64Url(); + const jwt = message + '.' + encodedSignature; + + return jwt; +} \ No newline at end of file diff --git a/packages/credentials/tests/credential-manifest.spec.ts b/packages/credentials/tests/credential-manifest.spec.ts deleted file mode 100644 index 963205c88..000000000 --- a/packages/credentials/tests/credential-manifest.spec.ts +++ /dev/null @@ -1,160 +0,0 @@ -import type { - Issuer, - InputDescriptor, - OutputDescriptor, - CredentialManifest, -} from '../src/types.js'; - -import { expect } from 'chai'; - -describe('CredentialManifest', () => { - it('creates an Output Descriptor', () => { - const outputDescriptor: OutputDescriptor = { - id : 'example-id', - schema : 'example-schema', - name : 'Output Descriptor Name', - description : 'Output Descriptor Description', - styles : { - thumbnail: { - uri : 'https://example.com/image.png', - alt : 'image' - }, - background: { - color: '#ffffff' - } - }, - display: { - path : ['$.example'], - schema : { - type : 'string', - format : 'date-time' - }, - fallback: 'N/A' - } - }; - - expect(outputDescriptor).to.have.property('id'); - expect(outputDescriptor).to.have.property('schema'); - expect(outputDescriptor).to.have.property('name'); - expect(outputDescriptor).to.have.property('description'); - expect(outputDescriptor).to.have.property('styles'); - expect(outputDescriptor.styles).to.have.property('thumbnail'); - expect(outputDescriptor.styles).to.have.property('background'); - expect(outputDescriptor).to.have.property('display'); - expect(outputDescriptor.display).to.have.property('path'); - expect(outputDescriptor.display).to.have.property('schema'); - expect(outputDescriptor.display).to.have.property('fallback'); - }); - - it('creates a credential manifest', () => { - const issuer: Issuer = { - id: 'did:example:123456' - }; - - const inputDescriptor: InputDescriptor = { - id : 'input-example', - name : 'Input Name', - purpose : 'Input Purpose', - group : ['group1'], - }; - - const credentialManifest: CredentialManifest = { - id : 'manifest-id', - name : 'Credential Manifest Name', - description : 'Credential Manifest Description', - spec_version : '1.0.0', - issuer : issuer, - output_descriptors : [{ - id : 'output-example', - schema : 'schema-example' - }], - format : { key: 'value' }, - presentation_definition : { - id : 'pd-id', - input_descriptors : [ - inputDescriptor - ] - } - }; - - expect(credentialManifest).to.have.property('id'); - expect(credentialManifest).to.have.property('name'); - expect(credentialManifest).to.have.property('description'); - expect(credentialManifest).to.have.property('spec_version'); - expect(credentialManifest).to.have.property('issuer'); - expect(credentialManifest).to.have.property('output_descriptors'); - expect(credentialManifest.output_descriptors[0]).to.have.property('id'); - expect(credentialManifest.output_descriptors[0]).to.have.property('schema'); - expect(credentialManifest).to.have.property('format'); - expect(credentialManifest.format).to.have.property('key'); - expect(credentialManifest).to.have.property('presentation_definition'); - expect(credentialManifest.presentation_definition).to.have.property('id'); - expect(credentialManifest.presentation_definition).to.have.property('input_descriptors'); - expect(credentialManifest.presentation_definition!.input_descriptors[0]).to.have.property('id'); - }); - - it('creates a driver license credential manifest', () => { - const outputDescriptor: OutputDescriptor = { - 'id' : 'driver_license_output', - 'schema' : 'https://schema.org/EducationalOccupationalCredential', - 'name' : 'Washington State Driver License', - 'description' : 'License to operate a vehicle with a gross combined weight rating (GCWR) of 26,001 or more pounds, as long as the GVWR of the vehicle(s) being towed is over 10,000 pounds.', - 'styles' : { - 'thumbnail': { - 'uri' : 'https://dol.wa.com/logo.png', - 'alt' : 'Washington State Seal' - }, - 'hero': { - 'uri' : 'https://dol.wa.com/happy-people-driving.png', - 'alt' : 'Happy people driving' - }, - 'background': { - 'color': '#ff0000' - }, - 'text': { - 'color': '#d4d400' - } - }, - 'display': { - 'path' : ['$.name', '$.vc.name'], - 'schema' : { - 'type': 'string' - }, - 'fallback': 'Washington State Driver License' - } - }; - - const credentialManifest: CredentialManifest = { - 'id' : 'WA-DL-CLASS-A', - 'spec_version' : 'https://identity.foundation/credential-manifest/spec/v1.0.0/', - 'issuer' : { - 'id' : 'did:example:123?linked-domains=3', - 'name' : 'Washington State Government', - 'styles' : { - 'thumbnail': { - 'uri' : 'https://dol.wa.com/logo.png', - 'alt' : 'Washington State Seal' - }, - 'hero': { - 'uri' : 'https://dol.wa.com/people-working.png', - 'alt' : 'People working on serious things' - }, - 'background': { - 'color': '#ff0000' - }, - 'text': { - 'color': '#d4d400' - } - } - }, - 'output_descriptors': [ - outputDescriptor - ] - }; - - expect(credentialManifest).to.have.property('id'); - expect(credentialManifest).to.have.property('spec_version'); - expect(credentialManifest).to.have.property('issuer'); - expect(credentialManifest).to.have.property('output_descriptors'); - }); -}); \ No newline at end of file diff --git a/packages/credentials/tests/presentation-exchange.spec.ts b/packages/credentials/tests/presentation-exchange.spec.ts index ff078569c..352a4fe95 100644 --- a/packages/credentials/tests/presentation-exchange.spec.ts +++ b/packages/credentials/tests/presentation-exchange.spec.ts @@ -1,142 +1,110 @@ -import type { PortableDid } from '@web5/dids'; -import type { - VerifiableCredentialTypeV1, - PresentationDefinition, - JwtDecodedVerifiablePresentation, - Validated, -} from '../src/types.js'; - -import { evaluateCredentials, evaluatePresentation, presentationFrom, validateDefinition, validateSubmission, resetPex } from '../src/types.js'; - import { expect } from 'chai'; -import { Convert } from '@web5/common'; import { DidKeyMethod } from '@web5/dids'; import { Ed25519, Jose } from '@web5/crypto'; +import { PresentationExchange, Validated, PresentationDefinitionV2 } from '../src/presentation-exchange.js'; +import { VerifiableCredential, SignOptions } from '../src/verifiable-credential.js'; +type Signer = (data: Uint8Array) => Promise; -/** - * Local types used only in this test specification. - */ -type CreateJwtOpts = { - header: JwtHeaderParams; - payload: any; - subject: string; - issuer: string; - signer: Signer; +class BitcoinCredential { + constructor( + public btcAddress: string + ) {} } -type JwtHeaderParams = { - alg: string; - typ: 'JWT' - kid: string; -}; - -type Signer = (data: Uint8Array) => Promise; - describe('PresentationExchange', () => { describe('Full Presentation Exchange', () => { - let alice: PortableDid; - let header: JwtHeaderParams; - let signer: Signer; + let signOptions: SignOptions; let btcCredentialJwt: string; - let presentationDefinition: PresentationDefinition; + let presentationDefinition: PresentationDefinitionV2; before(async () => { - alice = await DidKeyMethod.create(); - - const [ signingKeyPair ] = alice.keySet.verificationMethodKeys!; - const { keyMaterial: privateKey } = await Jose.jwkToKey({ key: signingKeyPair.privateKeyJwk! }); - signer = EdDsaSigner(privateKey); - - header = { alg: 'EdDSA', typ: 'JWT', kid: signingKeyPair.privateKeyJwk!.kid! }; - - btcCredentialJwt = await createBtcCredentialJwt(alice.did, header, signer); + const alice = await DidKeyMethod.create(); + const [signingKeyPair] = alice.keySet.verificationMethodKeys!; + const privateKey = (await Jose.jwkToKey({ key: signingKeyPair.privateKeyJwk!})).keyMaterial; + const signer = EdDsaSigner(privateKey); + signOptions = { + issuerDid : alice.did, + subjectDid : alice.did, + kid : alice.did + '#' + alice.did.split(':')[2], + signer : signer + }; + + const vc = VerifiableCredential.create( + 'StreetCred', + alice.did, + alice.did, + new BitcoinCredential('btcAddress123'), + ); + + btcCredentialJwt = await vc.sign(signOptions); presentationDefinition = createPresentationDefinition(); }); - beforeEach(() => { - resetPex(); + it('should evaluate credentials without any errors or warnings', async () => { + PresentationExchange.satisfiesPresentationDefinition([btcCredentialJwt], presentationDefinition); }); - it('should evaluate credentials without any errors or warnings', async () => { - const evaluationResults = evaluateCredentials(presentationDefinition, [btcCredentialJwt]); + it('should evaluate that the credential does not satisfy the presentation definition', async () => { + const incorrectPresentationDefinition = { + 'id' : 'test-pd-id', + 'name' : 'simple PD', + 'purpose' : 'pd for testing', + 'input_descriptors' : [ + { + 'id' : 'whatever', + 'purpose' : 'id for testing', + 'constraints' : { + 'fields': [ + { + 'path': [ + '$.credentialSubject.doesNotExist', + ] + } + ] + } + } + ] + }; - expect(evaluationResults.errors).to.deep.equal([]); - expect(evaluationResults.warnings).to.deep.equal([]); + await expectThrowsAsync(() => PresentationExchange.satisfiesPresentationDefinition([btcCredentialJwt], incorrectPresentationDefinition), 'Input candidate does not contain property'); }); it('should successfully create a presentation from the given definition and credentials', () => { - evaluateCredentials(presentationDefinition, [btcCredentialJwt]); - const presentationResult = presentationFrom(presentationDefinition, [btcCredentialJwt]); - + const presentationResult = PresentationExchange.createPresentationFromCredentials([btcCredentialJwt], presentationDefinition); 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); + const result:Validated = PresentationExchange.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); + const presentationResult = PresentationExchange.createPresentationFromCredentials([btcCredentialJwt], presentationDefinition); + const result:Validated = PresentationExchange.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, - payload : { vp: presentationResult.presentation }, - signer, - subject : alice.did, - }); - - const presentation = decodeJwt(vpJwt).payload.vp; - - const presentationEvaluationResults = evaluatePresentation(presentationDefinition, presentation ); + const presentationResult = PresentationExchange.createPresentationFromCredentials([btcCredentialJwt], presentationDefinition); + + const presentationEvaluationResults = PresentationExchange.evaluatePresentation(presentationDefinition, presentationResult.presentation ); expect(presentationEvaluationResults.errors).to.deep.equal([]); expect(presentationEvaluationResults.warnings).to.deep.equal([]); - const result:Validated = validateSubmission(presentationResult.presentationSubmission); + const result:Validated = PresentationExchange.validateSubmission(presentationResult.presentationSubmission); expect(result).to.deep.equal([{ tag: 'root', status: 'info', message: 'ok' }]); }); it('should successfully execute the complete presentation exchange flow', 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); - - const presentationResult = presentationFrom(presentationDefinition, [btcCredentialJwt]); + const presentationResult = PresentationExchange.createPresentationFromCredentials([btcCredentialJwt], presentationDefinition); expect(presentationResult).to.exist; expect(presentationResult.presentationSubmission.definition_id).to.equal(presentationDefinition.id); - const vpJwt = await createJwt({ - header, - issuer : alice.did, - payload : { vp: presentationResult.presentation }, - signer, - subject : alice.did, - }); - - const presentation = decodeJwt(vpJwt).payload.vp; - - const { warnings, errors } = evaluatePresentation(presentationDefinition, presentation ); + const { warnings, errors } = PresentationExchange.evaluatePresentation(presentationDefinition, presentationResult.presentation ); expect(errors).to.be.an('array'); expect(errors?.length).to.equal(0); @@ -147,7 +115,7 @@ describe('PresentationExchange', () => { }); }); -function createPresentationDefinition(): PresentationDefinition { +function createPresentationDefinition(): PresentationDefinitionV2 { return { 'id' : 'test-pd-id', 'name' : 'simple PD', @@ -170,67 +138,23 @@ function createPresentationDefinition(): PresentationDefinition { }; } -function createVerifiableCredential(did:string): VerifiableCredentialTypeV1 { - return { - '@context' : ['https://www.w3.org/2018/credentials/v1'], - 'id' : 'btc-credential', - 'type' : ['VerifiableCredential'], - 'issuer' : did, - 'issuanceDate' : new Date().toISOString(), - 'credentialSubject' : { - 'btcAddress': 'btcAddress123' - } - }; -} - -async function createBtcCredentialJwt(aliceDid: string, header: JwtHeaderParams, signer: Signer) { - const btcCredential: VerifiableCredentialTypeV1 = createVerifiableCredential(aliceDid); - - return await createJwt({ - header, - issuer : aliceDid, - payload : { vc: btcCredential }, - signer, - subject : aliceDid, - }); -} - -async function createJwt(options: CreateJwtOpts) { - const { header, issuer, subject, payload, signer } = options; - - const jwtPayload = { - iss : issuer, - sub : subject, - ...payload, - }; - - const encodedHeader = Convert.object(header).toBase64Url(); - const encodedPayload = Convert.object(jwtPayload).toBase64Url(); - const message = encodedHeader + '.' + encodedPayload; - const messageBytes = Convert.string(message).toUint8Array(); - - const signature = await signer(messageBytes); - - const encodedSignature = Convert.uint8Array(signature).toBase64Url(); - const jwt = message + '.' + encodedSignature; - - return jwt; -} - - -function decodeJwt(jwt: string) { - const [encodedHeader, encodedPayload, encodedSignature] = jwt.split('.'); - - return { - header : Convert.base64Url(encodedHeader).toObject() as JwtHeaderParams, - payload : Convert.base64Url(encodedPayload).toObject() as JwtDecodedVerifiablePresentation, - signature : encodedSignature - }; -} - function EdDsaSigner(privateKey: Uint8Array): Signer { return async (data: Uint8Array): Promise => { const signature = await Ed25519.sign({ data, key: privateKey}); return signature; }; -} \ No newline at end of file +} + +const expectThrowsAsync = async (method: any, errorMessage: string) => { + let error: any = null; + try { + await method(); + } + catch (err) { + error = err; + } + expect(error).to.be.an('Error'); + if (errorMessage) { + expect(error.message).to.contain(errorMessage); + } +}; \ No newline at end of file diff --git a/packages/credentials/tests/ssi-validator.spec.ts b/packages/credentials/tests/ssi-validator.spec.ts index d15023286..1815dd707 100644 --- a/packages/credentials/tests/ssi-validator.spec.ts +++ b/packages/credentials/tests/ssi-validator.spec.ts @@ -1,7 +1,8 @@ import { SsiValidator } from '../src/validators.js'; -import { DEFAULT_CONTEXT, DEFAULT_VC_TYPE, DEFAULT_VP_TYPE } from '../src/types.js'; import { expect } from 'chai'; +import { DEFAULT_CONTEXT, DEFAULT_VC_TYPE } from '../src/verifiable-credential.js'; + describe('SsiValidator', () => { describe('validateContext', () => { @@ -44,15 +45,4 @@ describe('SsiValidator', () => { expect(() => SsiValidator.validateTimestamp(validTimestamp)).not.throw(); }); }); - - describe('validateVpType', () => { - it('should throw an error if the default VP type is missing', () => { - expect(() => SsiValidator.validateVpType(['CustomType'])).throw(`type is missing default "${DEFAULT_VP_TYPE}"`); - }); - - it('should not throw an error if the default VP type is present', () => { - expect(() => SsiValidator.validateVpType([DEFAULT_VP_TYPE, 'CustomType'])).not.throw(); - }); - }); - }); diff --git a/packages/credentials/tests/ssi.spec.ts b/packages/credentials/tests/ssi.spec.ts deleted file mode 100644 index cbe42d7d5..000000000 --- a/packages/credentials/tests/ssi.spec.ts +++ /dev/null @@ -1,291 +0,0 @@ -import { expect } from 'chai'; -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'; -import { getCurrentXmlSchema112Timestamp } from '../src/utils.js'; - -type Signer = (data: Uint8Array) => Promise; - -describe('SSI Tests', () => { - let alice: any; - let signingKeyPair: any; - let privateKey: any; - let subjectIssuerDid: string; - let signer: Signer; - let signOptions: SignOptions; - - beforeEach(async () => { - alice = await DidKeyMethod.create(); - [signingKeyPair] = alice.keySet.verificationMethodKeys!; - privateKey = (await Jose.jwkToKey({ key: signingKeyPair.privateKeyJwk!})).keyMaterial; - subjectIssuerDid = alice.did; - signer = EdDsaSigner(privateKey); - signOptions = { - issuerDid : alice.did, - subjectDid : alice.did, - kid : '#' + alice.did.split(':')[2], - signer : signer - }; - }); - - describe('Verifiable Credential (VC)', () => { - it('creates a VC JWT with CreateVCOptions', async () => { - const vcCreateOptions: CreateVcOptions = { - credentialSubject : { id: subjectIssuerDid, btcAddress: 'abc123' }, - issuer : { id: subjectIssuerDid } - }; - - const vcJwt: VcJwt = await VerifiableCredential.create(signOptions, vcCreateOptions); - expect(async () => await VerifiableCredential.verify(vcJwt)).to.not.throw(); - }); - - it('creates a VC JWT with VerifiableCredentialV1 type', async () => { - const vc:VerifiableCredentialTypeV1 = { - id : 'id123', - '@context' : ['https://www.w3.org/2018/credentials/v1'], - credentialSubject : { id: subjectIssuerDid, btcAddress: 'abc123' }, - type : ['VerifiableCredential'], - issuer : { id: subjectIssuerDid }, - issuanceDate : getCurrentXmlSchema112Timestamp(), - }; - - const vcJwt: VcJwt = await VerifiableCredential.create(signOptions, undefined, vc); - expect(async () => await VerifiableCredential.verify(vcJwt)).to.not.throw(); - }); - - it('fails to create a VC JWT with CreateVCOptions and VC', async () => { - const vc:VerifiableCredentialTypeV1 = { - id : 'id123', - '@context' : ['https://www.w3.org/2018/credentials/v1'], - credentialSubject : { id: subjectIssuerDid, btcAddress: 'abc123' }, - type : ['VerifiableCredential'], - issuer : { id: subjectIssuerDid }, - issuanceDate : getCurrentXmlSchema112Timestamp(), - }; - - const vcCreateOptions: CreateVcOptions = { - credentialSubject : { id: subjectIssuerDid, btcAddress: 'abc123' }, - issuer : { id: subjectIssuerDid } - }; - - await expectThrowsAsync(() => VerifiableCredential.create(signOptions, vcCreateOptions, vc), 'options and verifiableCredentials are mutually exclusive, either include the full verifiableCredential or the options to create one'); - }); - - it('fails to create a VC JWT with no CreateVCOptions and no VC', async () => { - await expectThrowsAsync(() => VerifiableCredential.create(signOptions, undefined, undefined), 'options or verifiableCredential must be provided'); - }); - - it('creates a VC JWT with a VC', async () => { - const btcCredential: VerifiableCredentialTypeV1 = { - '@context' : ['https://www.w3.org/2018/credentials/v1'], - 'id' : 'btc-credential', - 'type' : ['VerifiableCredential'], - 'issuer' : alice.did, - 'issuanceDate' : getCurrentXmlSchema112Timestamp(), - 'credentialSubject' : { - 'btcAddress': 'btcAddress123' - } - }; - - const vcJwt: VcJwt = await VerifiableCredential.create(signOptions, undefined, btcCredential); - expect(async () => await VerifiableCredential.verify(vcJwt)).to.not.throw(); - }); - - it('fails to verify an invalid VC JWT', async () => { - await expectThrowsAsync(() => VerifiableCredential.verify('invalid-jwt'), 'Incorrect format JWT'); - }); - - it('decodes a VC JWT', async () => { - const vcCreateOptions: CreateVcOptions = { - credentialSubject : { id: subjectIssuerDid, btcAddress: 'abc123' }, - issuer : { id: subjectIssuerDid } - }; - - const vcJwt: VcJwt = await VerifiableCredential.create(signOptions, vcCreateOptions); - const vcPayload = VerifiableCredential.decode(vcJwt).payload.vc; - - expect(vcPayload).to.exist; - expect(vcPayload.issuer).to.deep.equal({ id: alice.did }); - expect(vcPayload.type).to.deep.equal(['VerifiableCredential']); - expect(vcPayload.credentialSubject).to.deep.equal({ id: alice.did, btcAddress: 'abc123' }); - expect(vcPayload['@context']).to.deep.equal(['https://www.w3.org/2018/credentials/v1']); - }); - - it('validates VC payload', async () => { - const vcCreateOptions: CreateVcOptions = { - credentialSubject : { id: subjectIssuerDid, btcAddress: 'abc123' }, - issuer : { id: subjectIssuerDid } - }; - - const vcJwt: VcJwt = await VerifiableCredential.create(signOptions, vcCreateOptions); - const vcPayload = VerifiableCredential.decode(vcJwt).payload.vc; - - expect(() => VerifiableCredential.validatePayload(vcPayload)).to.not.throw(); - }); - - it('detects invalid issuer sign options', async () => { - const vcCreateOptions: CreateVcOptions = { - credentialSubject : { id: subjectIssuerDid, btcAddress: 'abc123' }, - issuer : { id: subjectIssuerDid } - }; - - const vcSignOptions: SignOptions = { - issuerDid : 'bad:did', - subjectDid : alice.did, - kid : '#' + alice.did.split(':')[2], - signer : signer - }; - - const vcJwt: VcJwt = await VerifiableCredential.create(vcSignOptions, vcCreateOptions); - await expectThrowsAsync(() => VerifiableCredential.verify(vcJwt), 'resolver_error: Unable to resolve DID document for bad:did: invalidDid'); - }); - }); - - describe('Verifiable Presentation (VP)', () => { - let vcCreateOptions: CreateVcOptions; - let signOptions: SignOptions; - let vcJwt: VcJwt; - - beforeEach(async () => { - vcCreateOptions = {credentialSubject: {id: subjectIssuerDid, btcAddress: 'abc123'}, issuer: {id: subjectIssuerDid}}; - signOptions = {issuerDid: alice.did, subjectDid: alice.did, kid: '#' + alice.did.split(':')[2], signer: signer}; - vcJwt = await VerifiableCredential.create(signOptions, vcCreateOptions); - }); - - it('creates a VP JWT', async () => { - const vpCreateOptions: CreateVpOptions = { - presentationDefinition : getPresentationDefinition(), - verifiableCredentialJwts : [vcJwt] - }; - - const vpJwt: VpJwt = await VerifiablePresentation.create(signOptions, vpCreateOptions); - expect(vpJwt).to.exist; - - const decodedVp = VerifiablePresentation.decode(vpJwt); - expect(decodedVp).to.have.property('header'); - expect(decodedVp).to.have.property('payload'); - expect(decodedVp).to.have.property('signature'); - }); - - it('verifies a VP JWT', async () => { - const vpCreateOptions: CreateVpOptions = { - presentationDefinition : getPresentationDefinition(), - verifiableCredentialJwts : [vcJwt], - }; - - const vpJwt: VpJwt = await VerifiablePresentation.create(signOptions, vpCreateOptions); - expect(async () => await VerifiablePresentation.verify(vpJwt)).to.not.throw(); - }); - - it('evaluates an invalid VP with empty VCs', async () => { - const vpCreateOptions: CreateVpOptions = { - presentationDefinition : getPresentationDefinition(), - verifiableCredentialJwts : [] - }; - - try { - await VerifiablePresentation.create(signOptions, vpCreateOptions); - } catch (err: any) { - expect(err).instanceOf(Error); - expect(err!.message).to.equal('Failed to create Verifiable Presentation JWT due to: Required Credentials Not Present: "error"'); - } - }); - - it('evaluates an invalid VP with invalid subject', async () => { - vcCreateOptions = {credentialSubject: {id: subjectIssuerDid, badSubject: 'abc123'}, issuer: {id: subjectIssuerDid}}; - signOptions = {issuerDid: alice.did, subjectDid: alice.did, kid: '#' + alice.did.split(':')[2], signer: signer}; - vcJwt = await VerifiableCredential.create(signOptions, vcCreateOptions); - - const vpCreateOptions: CreateVpOptions = { - presentationDefinition : getPresentationDefinition(), - verifiableCredentialJwts : [vcJwt] - }; - - try { - await VerifiablePresentation.create(signOptions, vpCreateOptions); - } catch (err: any) { - expect(err).instanceOf(Error); - 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 bad presentation definition', async () => { - const presentationDefinition = getPresentationDefinition(); - presentationDefinition.input_descriptors[0].constraints!.fields![0].path = ['$.credentialSubject.badSubject']; - - 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 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"}]'); - } - }); - }); -}); - -const expectThrowsAsync = async (method: any, errorMessage: string) => { - let error: any = null; - try { - await method(); - } - catch (err) { - error = err; - } - expect(error).to.be.an('Error'); - if (errorMessage) { - expect(error.message).to.contain(errorMessage); - } -}; - -function getPresentationDefinition(): PresentationDefinition { - return { - 'id' : 'test-pd-id', - 'name' : 'simple PD', - 'purpose' : 'pd for testing', - 'input_descriptors' : [ - { - 'id' : 'whatever', - 'purpose' : 'id for testing', - 'constraints' : { - 'fields': [ - { - 'path': [ - '$.credentialSubject.btcAddress', - ] - } - ] - } - } - ] - }; -} - -function EdDsaSigner(privateKey: Uint8Array): Signer { - return async (data: Uint8Array): Promise => { - const signature = await Ed25519.sign({ data, key: privateKey}); - return signature; - }; -} \ No newline at end of file diff --git a/packages/credentials/tests/verifiable-credential.spec.ts b/packages/credentials/tests/verifiable-credential.spec.ts new file mode 100644 index 000000000..803b05a3f --- /dev/null +++ b/packages/credentials/tests/verifiable-credential.spec.ts @@ -0,0 +1,160 @@ +import { expect } from 'chai'; +// import { VcJwt, VpJwt, VerifiableCredentialTypeV1, PresentationDefinition} from '../src/types.js'; +// import {VerifiableCredential, VerifiablePresentation, CreateVcOptions, CreateVpOptions, SignOptions} from '../src/ssi.js'; +import { VerifiableCredential, SignOptions } from '../src/verifiable-credential.js'; +import { Ed25519, Jose } from '@web5/crypto'; +import { DidKeyMethod } from '@web5/dids'; +// import { getCurrentXmlSchema112Timestamp } from '../src/utils.js'; + +type Signer = (data: Uint8Array) => Promise; + +describe('Verifiable Credential Tests', () => { + let signer: Signer; + let signOptions: SignOptions; + + class StreetCredibility { + constructor( + public localRespect: string, + public legit: boolean + ) {} + } + + + beforeEach(async () => { + const alice = await DidKeyMethod.create(); + const [signingKeyPair] = alice.keySet.verificationMethodKeys!; + const privateKey = (await Jose.jwkToKey({ key: signingKeyPair.privateKeyJwk!})).keyMaterial; + signer = EdDsaSigner(privateKey); + signOptions = { + issuerDid : alice.did, + subjectDid : alice.did, + kid : alice.did + '#' + alice.did.split(':')[2], + signer : signer + }; + }); + + describe('Verifiable Credential (VC)', () => { + it('create vc works', async () => { + // const keyManager = new InMemoryKeyManager(); + const issuerDid = signOptions.issuerDid; + const subjectDid = signOptions.subjectDid; + + const vc = VerifiableCredential.create( + 'StreetCred', + issuerDid, + subjectDid, + new StreetCredibility('high', true), + ); + + expect(vc.issuer).to.equal(issuerDid); + expect(vc.subject).to.equal(subjectDid); + expect(vc.type).to.equal('StreetCred'); + expect(vc.vcDataModel.issuanceDate).to.not.be.undefined; + expect(vc.vcDataModel.credentialSubject).to.deep.equal({ id: subjectDid, localRespect: 'high', legit: true }); + + }); + + + it('signing vc works', async () => { + const issuerDid = signOptions.issuerDid; + const subjectDid = signOptions.subjectDid; + + const vc = VerifiableCredential.create( + 'StreetCred', + issuerDid, + subjectDid, + new StreetCredibility('high', true), + ); + + const vcJwt = await vc.sign(signOptions); + expect(vcJwt).to.not.be.null; + expect(vcJwt).to.be.a('string'); + + const parts = vcJwt.split('.'); + expect(parts.length).to.equal(3); + }); + + it('parseJwt throws ParseException if argument is not a valid JWT', () => { + expect(() => { + VerifiableCredential.parseJwt('hi'); + }).to.throw('Not a valid jwt'); + }); + + it('verify fails with bad issuer did', async () => { + const vc = VerifiableCredential.create( + 'StreetCred', + 'bad:did: invalidDid', + signOptions.subjectDid, + new StreetCredibility('high', true) + ); + + const badSignOptions = { + issuerDid : 'bad:did: invalidDid', + subjectDid : signOptions.subjectDid, + kid : signOptions.issuerDid + '#' + signOptions.issuerDid.split(':')[2], + signer : signer + }; + + const vcJwt = await vc.sign(badSignOptions); + + await expectThrowsAsync(() => VerifiableCredential.verify(vcJwt), 'Unable to resolve DID'); + }); + + it('parseJwt returns an instance of VerifiableCredential on success', async () => { + const vc = VerifiableCredential.create( + 'StreetCred', + signOptions.issuerDid, + signOptions.subjectDid, + new StreetCredibility('high', true) + ); + + const vcJwt = await vc.sign(signOptions); + const parsedVc = VerifiableCredential.parseJwt(vcJwt); + + expect(parsedVc).to.not.be.null; + expect(parsedVc.type).to.equal(vc.type); + expect(parsedVc.issuer).to.equal(vc.issuer); + expect(parsedVc.subject).to.equal(vc.subject); + + expect(vc.toString()).to.equal(parsedVc.toString()); + }); + + it('fails to verify an invalid VC JWT', async () => { + await expectThrowsAsync(() => VerifiableCredential.verify('invalid-jwt'), 'Not a valid jwt'); + }); + + it('verify does not throw an exception with vaild vc', async () => { + const vc = VerifiableCredential.create( + 'StreetCred', + signOptions.issuerDid, + signOptions.subjectDid, + new StreetCredibility('high', true) + ); + + const vcJwt = await vc.sign(signOptions); + + await VerifiableCredential.verify(vcJwt); + }); + }); +}); + +function EdDsaSigner(privateKey: Uint8Array): Signer { + return async (data: Uint8Array): Promise => { + const signature = await Ed25519.sign({ data, key: privateKey}); + return signature; + }; +} + +const expectThrowsAsync = async (method: any, errorMessage: string) => { + let error: any = null; + try { + await method(); + } + catch (err) { + error = err; + } + expect(error).to.be.an('Error'); + if (errorMessage) { + expect(error.message).to.contain(errorMessage); + } +}; \ No newline at end of file diff --git a/packages/credentials/tests/verifiable-credentials.spec.ts b/packages/credentials/tests/verifiable-credentials.spec.ts deleted file mode 100644 index 8a81c23e9..000000000 --- a/packages/credentials/tests/verifiable-credentials.spec.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { expect } from 'chai'; - -import type { VerifiableCredentialTypeV1, CredentialSubject, Issuer } from '../src/types.js'; - -import { getCurrentXmlSchema112Timestamp, getFutureXmlSchema112Timestamp} from '../src/utils.js'; - -describe('VerifiableCredentials', () => { - it('creates a vc', () => { - const credentialSubject: CredentialSubject = { - id: 'did:example:ebfeb1f712ebc6f1c276e12ec21', - }; - - const issuer: Issuer = { - id : 'did:example:123456', - otherProp : 'value', - }; - - // Create the credential - const credential: VerifiableCredentialTypeV1 = { - '@context' : ['https://www.w3.org/2018/credentials/v1'], - type : ['VerifiableCredential'], - issuer : issuer, - issuanceDate : getCurrentXmlSchema112Timestamp(), - credentialSubject : credentialSubject, - credentialSchema : { - id : 'http://example.com/schema/123', - type : 'SchemaType1', - }, - id : 'http://example.edu/credentials/3732', - expirationDate : getFutureXmlSchema112Timestamp(60 * 60 * 24 * 365), // expires in 1 year - name : 'Credential Name', - description : 'Credential Description', - }; - - expect(credential).to.have.property('@context'); - expect(credential).to.have.property('type'); - expect(credential).to.have.property('issuer'); - expect(credential).to.have.property('issuanceDate'); - expect(credential).to.have.property('credentialSubject'); - expect(credential).to.have.property('credentialSchema'); - expect(credential).to.have.property('id'); - expect(credential).to.have.property('expirationDate'); - expect(credential).to.have.property('name'); - expect(credential).to.have.property('description'); - }); - - it('creates a minimum viable vc', () => { - const credential: VerifiableCredentialTypeV1 = { - '@context' : ['https://www.w3.org/2018/credentials/v1'], - type : ['VerifiableCredential'], - issuer : { id: 'did:example:123456' }, - issuanceDate : getCurrentXmlSchema112Timestamp(), - credentialSubject : { id: 'did:example:ebfeb1f712ebc6f1c276e12ec21' }, - }; - - expect(credential).to.have.property('@context'); - expect(credential).to.have.property('type'); - expect(credential).to.have.property('issuer'); - expect(credential).to.have.property('issuanceDate'); - expect(credential).to.have.property('credentialSubject'); - }); -}); \ No newline at end of file From e266839be44957c48d3d0abc5435bc5365929bf0 Mon Sep 17 00:00:00 2001 From: Neal Date: Thu, 2 Nov 2023 14:26:15 -0700 Subject: [PATCH 2/8] fix comment --- packages/credentials/src/verifiable-credential.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/credentials/src/verifiable-credential.ts b/packages/credentials/src/verifiable-credential.ts index 486b5832b..ecea3971f 100644 --- a/packages/credentials/src/verifiable-credential.ts +++ b/packages/credentials/src/verifiable-credential.ts @@ -95,7 +95,7 @@ export class VerifiableCredential { * * Example: * ``` - * val signedVc = verifiableCredential.sign(signOptions) + * const signedVc = verifiableCredential.sign(signOptions) * ``` */ // TODO: Refactor to look like: sign(did: Did, assertionMethodId?: string) From 927a246068a8aa8482caf2e57ea1437f4207cd03 Mon Sep 17 00:00:00 2001 From: Neal Date: Thu, 2 Nov 2023 14:32:18 -0700 Subject: [PATCH 3/8] remove comments --- packages/credentials/tests/verifiable-credential.spec.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/credentials/tests/verifiable-credential.spec.ts b/packages/credentials/tests/verifiable-credential.spec.ts index 803b05a3f..8a4d34b94 100644 --- a/packages/credentials/tests/verifiable-credential.spec.ts +++ b/packages/credentials/tests/verifiable-credential.spec.ts @@ -1,10 +1,7 @@ import { expect } from 'chai'; -// import { VcJwt, VpJwt, VerifiableCredentialTypeV1, PresentationDefinition} from '../src/types.js'; -// import {VerifiableCredential, VerifiablePresentation, CreateVcOptions, CreateVpOptions, SignOptions} from '../src/ssi.js'; import { VerifiableCredential, SignOptions } from '../src/verifiable-credential.js'; import { Ed25519, Jose } from '@web5/crypto'; import { DidKeyMethod } from '@web5/dids'; -// import { getCurrentXmlSchema112Timestamp } from '../src/utils.js'; type Signer = (data: Uint8Array) => Promise; From ebbdd6d61e628bc8bbc63ab913a662b55806ea1e Mon Sep 17 00:00:00 2001 From: Neal Date: Thu, 2 Nov 2023 14:43:02 -0700 Subject: [PATCH 4/8] fix lint --- packages/credentials/src/presentation-exchange.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/credentials/src/presentation-exchange.ts b/packages/credentials/src/presentation-exchange.ts index 67cb425ab..0b4fd993d 100644 --- a/packages/credentials/src/presentation-exchange.ts +++ b/packages/credentials/src/presentation-exchange.ts @@ -67,7 +67,7 @@ export class PresentationExchange { * @param {PresentationDefinitionV2} presentationDefinition The Presentation Definition V2 to match the VCs against. * @returns {PresentationResult} The result of the presentation creation process, containing a presentation submission * that satisfies the presentation definition criteria. - * @throws {Error} If the evaluation results in warnings or errors, or if the required credentials are not present, + * @throws {Error} If the evaluation results in warnings or errors, or if the required credentials are not present, * an error is thrown with a descriptive message. */ public static createPresentationFromCredentials( From c20e43086ca4a7802260764fcd1eb92662fb61a8 Mon Sep 17 00:00:00 2001 From: Neal Date: Thu, 2 Nov 2023 15:00:42 -0700 Subject: [PATCH 5/8] fix vuln --- package-lock.json | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index c2ce7bed4..7b46e9080 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1862,19 +1862,23 @@ } }, "node_modules/browserify-sign": { - "version": "4.2.1", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.2.tgz", + "integrity": "sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg==", "dev": true, - "license": "ISC", "dependencies": { - "bn.js": "^5.1.1", - "browserify-rsa": "^4.0.1", + "bn.js": "^5.2.1", + "browserify-rsa": "^4.1.0", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", - "elliptic": "^6.5.3", + "elliptic": "^6.5.4", "inherits": "^2.0.4", - "parse-asn1": "^5.1.5", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" + "parse-asn1": "^5.1.6", + "readable-stream": "^3.6.2", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 4" } }, "node_modules/browserify-sign/node_modules/readable-stream": { From e4f2ee28f172344b36f356fd534043b910143b8e Mon Sep 17 00:00:00 2001 From: Neal Date: Thu, 2 Nov 2023 16:36:37 -0700 Subject: [PATCH 6/8] adding unit tests --- .../credentials/src/verifiable-credential.ts | 57 ---------------- .../tests/presentation-exchange.spec.ts | 66 ++++++++++++++++++- .../tests/verifiable-credential.spec.ts | 49 ++++++++++++++ 3 files changed, 113 insertions(+), 59 deletions(-) diff --git a/packages/credentials/src/verifiable-credential.ts b/packages/credentials/src/verifiable-credential.ts index ecea3971f..0cc48f7b2 100644 --- a/packages/credentials/src/verifiable-credential.ts +++ b/packages/credentials/src/verifiable-credential.ts @@ -198,63 +198,6 @@ export class VerifiableCredential { throw new Error('Signature verification failed: Expected JWS header to contain alg and kid'); } - const parsedDidUrl = DidDocument.getDidFromKeyId(jwt.header.kid); - const fragment = jwt.header.kid.split('#')[1]; - - if(!fragment || ! parsedDidUrl) { - throw new Error('Signature verification failed: Expected kid in JWS header to be a DID URL'); - } - - const didResolutionResult: DIDResolutionResult = await tbdResolver.resolve(parsedDidUrl); - if (didResolutionResult.didResolutionMetadata.error) { - throw new Error( - `Signature verification failed: Failed to resolve DID ${parsedDidUrl}. ` + - `Error: ${didResolutionResult.didResolutionMetadata.error}` - ); - } - - const verificationMethodIds = new Set([`${parsedDidUrl}#${fragment}`, `#${fragment}`]); - - if (!didResolutionResult.didDocument?.assertionMethod || !didResolutionResult.didDocument?.verificationMethod) { - throw new Error( - 'Signature verification failed: Expected kid in JWS header to dereference ' + - 'a DID Document Verification Method with an Assertion verification relationship' - ); - } - - const assertionMethods = didResolutionResult.didDocument?.assertionMethod; - - let assertionMethod: VerificationMethod | undefined; - - for (const element of assertionMethods) { - if (typeof element === 'string') { - if (verificationMethodIds.has(element)) { - assertionMethod = didResolutionResult.didDocument?.verificationMethod.find(vm => vm.id === element); - break; - } - } else { - if (verificationMethodIds.has(element.id)) { - assertionMethod = didResolutionResult.didDocument?.verificationMethod.find(vm => vm.id === element.id); - break; - } - } - } - - if (!assertionMethod) { - throw new Error( - 'Signature verification failed: Expected kid in JWS header to dereference ' + - 'a DID Document Verification Method with an Assertion verification relationship' - ); - } - - if (assertionMethod.type !== 'JsonWebKey2020' || !assertionMethod.publicKeyJwk) { - throw new Error( - 'Signature verification failed: Expected kid in JWS header to dereference ' + - 'a DID Document Verification Method of type JsonWebKey2020 with a publicKeyJwk' - ); - } - - // Perform the signature verification const verificationResponse = await verifyJWT(vcJwt, { resolver: tbdResolver }); diff --git a/packages/credentials/tests/presentation-exchange.spec.ts b/packages/credentials/tests/presentation-exchange.spec.ts index 352a4fe95..881614b85 100644 --- a/packages/credentials/tests/presentation-exchange.spec.ts +++ b/packages/credentials/tests/presentation-exchange.spec.ts @@ -12,6 +12,12 @@ class BitcoinCredential { ) {} } +class OtherCredential { + constructor( + public otherthing: string + ) {} +} + describe('PresentationExchange', () => { describe('Full Presentation Exchange', () => { let signOptions: SignOptions; @@ -45,8 +51,27 @@ describe('PresentationExchange', () => { PresentationExchange.satisfiesPresentationDefinition([btcCredentialJwt], presentationDefinition); }); + it('should return the selected verifiable credentials', () => { + const actualSelectedVcJwts = PresentationExchange.selectCredentials([btcCredentialJwt], presentationDefinition); + expect(actualSelectedVcJwts).to.deep.equal([btcCredentialJwt]); + }); + + it('should return the only one verifiable credential', async () => { + const vc = VerifiableCredential.create( + 'StreetCred', + signOptions.issuerDid, + signOptions.subjectDid, + new OtherCredential('otherstuff'), + ); + + const otherCredJwt = await vc.sign(signOptions); + + const actualSelectedVcJwts = PresentationExchange.selectCredentials([btcCredentialJwt, otherCredJwt], presentationDefinition); + expect(actualSelectedVcJwts).to.deep.equal([btcCredentialJwt]); + }); + it('should evaluate that the credential does not satisfy the presentation definition', async () => { - const incorrectPresentationDefinition = { + const otherPresentationDefinition = { 'id' : 'test-pd-id', 'name' : 'simple PD', 'purpose' : 'pd for testing', @@ -67,7 +92,7 @@ describe('PresentationExchange', () => { ] }; - await expectThrowsAsync(() => PresentationExchange.satisfiesPresentationDefinition([btcCredentialJwt], incorrectPresentationDefinition), 'Input candidate does not contain property'); + await expectThrowsAsync(() => PresentationExchange.satisfiesPresentationDefinition([btcCredentialJwt], otherPresentationDefinition), 'Input candidate does not contain property'); }); it('should successfully create a presentation from the given definition and credentials', () => { @@ -76,6 +101,43 @@ describe('PresentationExchange', () => { expect(presentationResult.presentationSubmission.definition_id).to.equal(presentationDefinition.id); }); + it('should throw error for invalid presentation definition', async () => { + const invalidPresentationDefinition = { + 'id' : 'test-pd-id', + 'name' : 'simple PD', + 'purpose' : 'pd for testing', + 'input_descriptors' : [ + { + 'id' : 'whatever', + 'purpose' : 'id for testing', + 'constraints' : { + 'fields': [ + { + 'path': [ + 'not a valid path', + ] + } + ] + } + } + ] + }; + + await expectThrowsAsync(() => PresentationExchange.createPresentationFromCredentials([btcCredentialJwt], invalidPresentationDefinition), 'Failed to pass validation check'); + }); + + it('should fail to create a presentation with vc that does not match presentation definition', async() => { + const vc = VerifiableCredential.create( + 'StreetCred', + signOptions.issuerDid, + signOptions.subjectDid, + new OtherCredential('otherstuff'), + ); + + const otherCredJwt = await vc.sign(signOptions); + await expectThrowsAsync(() => PresentationExchange.createPresentationFromCredentials([otherCredJwt], presentationDefinition), 'Failed to create Verifiable Presentation JWT due to: Required Credentials Not Present'); + }); + it('should successfully validate a presentation definition', () => { const result:Validated = PresentationExchange.validateDefinition(presentationDefinition); expect(result).to.deep.equal([{ tag: 'root', status: 'info', message: 'ok' }]); diff --git a/packages/credentials/tests/verifiable-credential.spec.ts b/packages/credentials/tests/verifiable-credential.spec.ts index 8a4d34b94..13a9476dc 100644 --- a/packages/credentials/tests/verifiable-credential.spec.ts +++ b/packages/credentials/tests/verifiable-credential.spec.ts @@ -48,9 +48,48 @@ describe('Verifiable Credential Tests', () => { expect(vc.type).to.equal('StreetCred'); expect(vc.vcDataModel.issuanceDate).to.not.be.undefined; expect(vc.vcDataModel.credentialSubject).to.deep.equal({ id: subjectDid, localRespect: 'high', legit: true }); + }); + + it('should throw an error if data is not parseable into a JSON object', () => { + const issuerDid = 'did:example:issuer'; + const subjectDid = 'did:example:subject'; + const invalidData = 'NotAJSONObject'; + + expect(() => { + VerifiableCredential.create( + 'InvalidDataTest', + issuerDid, + subjectDid, + invalidData + ); + }).to.throw('Expected data to be parseable into a JSON object'); }); + it('should throw an error if issuer or subject is not defined', () => { + const issuerDid = 'did:example:issuer'; + const subjectDid = 'did:example:subject'; + const validData = new StreetCredibility('high', true); + + expect(() => { + VerifiableCredential.create( + 'IssuerUndefinedTest', + '', + subjectDid, + validData + ); + }).to.throw('Issuer and subject must be defined'); + + expect(() => { + VerifiableCredential.create( + 'SubjectUndefinedTest', + issuerDid, + '', + validData + ); + }).to.throw('Issuer and subject must be defined'); + + }); it('signing vc works', async () => { const issuerDid = signOptions.issuerDid; @@ -97,6 +136,10 @@ describe('Verifiable Credential Tests', () => { await expectThrowsAsync(() => VerifiableCredential.verify(vcJwt), 'Unable to resolve DID'); }); + it('parseJwt checks if missing vc property', async () => { + await expectThrowsAsync(() => VerifiableCredential.parseJwt('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'), 'Jwt payload missing vc property'); + }); + it('parseJwt returns an instance of VerifiableCredential on success', async () => { const vc = VerifiableCredential.create( 'StreetCred', @@ -120,6 +163,12 @@ describe('Verifiable Credential Tests', () => { await expectThrowsAsync(() => VerifiableCredential.verify('invalid-jwt'), 'Not a valid jwt'); }); + it('should throw an error if JWS header does not contain alg and kid', async () => { + const invalidJwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'; + + await expectThrowsAsync(() => VerifiableCredential.verify(invalidJwt), 'Signature verification failed: Expected JWS header to contain alg and kid'); + }); + it('verify does not throw an exception with vaild vc', async () => { const vc = VerifiableCredential.create( 'StreetCred', From 7095623d2ec29049989fc83dcb32c8b9acc663d6 Mon Sep 17 00:00:00 2001 From: Neal Date: Thu, 2 Nov 2023 16:40:53 -0700 Subject: [PATCH 7/8] fix linnt --- packages/credentials/src/verifiable-credential.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/credentials/src/verifiable-credential.ts b/packages/credentials/src/verifiable-credential.ts index 0cc48f7b2..2da6086bf 100644 --- a/packages/credentials/src/verifiable-credential.ts +++ b/packages/credentials/src/verifiable-credential.ts @@ -1,5 +1,4 @@ import type { Resolvable, DIDResolutionResult} from 'did-resolver'; -import {VerificationMethod} from 'did-resolver'; import type { ICredential, ICredentialSubject, @@ -12,8 +11,6 @@ import { verifyJWT } from 'did-jwt'; import { DidIonMethod, DidKeyMethod, DidResolver } from '@web5/dids'; import { SsiValidator } from './validators.js'; -import { DidDocument } from '@decentralized-identity/did-common-typescript'; - export const DEFAULT_CONTEXT = 'https://www.w3.org/2018/credentials/v1'; export const DEFAULT_VC_TYPE = 'VerifiableCredential'; From a54f85640bc9072b58519b4e70cecc49068df163 Mon Sep 17 00:00:00 2001 From: Neal Date: Thu, 2 Nov 2023 17:02:30 -0700 Subject: [PATCH 8/8] removed unused dep --- package-lock.json | 23 ----------------------- packages/credentials/package.json | 1 - 2 files changed, 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7b46e9080..f3e6d1506 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,14 +55,6 @@ "node": ">=0.1.90" } }, - "node_modules/@decentralized-identity/did-common-typescript": { - "version": "0.1.19", - "license": "ISC", - "dependencies": { - "base64url": "^3.0.1", - "clone": "^2.1.2" - } - }, "node_modules/@decentralized-identity/ion-pow-sdk": { "version": "1.0.17", "license": "apache-2.0", @@ -1600,13 +1592,6 @@ "node": "^4.5.0 || >= 5.9" } }, - "node_modules/base64url": { - "version": "3.0.1", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/bech32": { "version": "2.0.0", "license": "MIT" @@ -2463,13 +2448,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/clone": { - "version": "2.1.2", - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, "node_modules/color-convert": { "version": "2.0.1", "dev": true, @@ -8913,7 +8891,6 @@ "version": "0.3.1", "license": "Apache-2.0", "dependencies": { - "@decentralized-identity/did-common-typescript": "^0.1.19", "@sphereon/pex": "2.1.0", "did-jwt": "^7.2.6", "uuid": "^9.0.0" diff --git a/packages/credentials/package.json b/packages/credentials/package.json index 25a803cf4..8f5e537e9 100644 --- a/packages/credentials/package.json +++ b/packages/credentials/package.json @@ -73,7 +73,6 @@ "node": ">=18.0.0" }, "dependencies": { - "@decentralized-identity/did-common-typescript": "^0.1.19", "@sphereon/pex": "2.1.0", "did-jwt": "^7.2.6", "uuid": "^9.0.0"