From 1545645cca2d7ea1554cca568ed1c65cdf1f0edc Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Tue, 7 Jan 2025 13:14:35 -0500 Subject: [PATCH 01/72] wip --- .github/workflows/main.yml | 2 +- .knownDidRegistries.ts | 19 +++ CHANGELOG.md | 2 +- LICENSE.md | 2 +- README.md | 20 +-- package.json | 13 +- src/Example.ts | 8 -- src/Verify.ts | 84 ++++++++++++ src/credentialStatus.ts | 39 ++++++ src/declarations.d.ts | 12 +- src/index.ts | 3 +- src/issuerRegistries.ts | 46 +++++++ src/test-fixtures/vc.ts | 253 +++++++++++++++++++++++++++++++++++++ test/Example.spec.ts | 9 -- test/Verify.spec.ts | 16 +++ 15 files changed, 494 insertions(+), 34 deletions(-) create mode 100644 .knownDidRegistries.ts delete mode 100644 src/Example.ts create mode 100644 src/Verify.ts create mode 100644 src/credentialStatus.ts create mode 100644 src/issuerRegistries.ts create mode 100644 src/test-fixtures/vc.ts delete mode 100644 test/Example.spec.ts create mode 100644 test/Verify.spec.ts diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 06c3297..4aa9aa3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,7 +8,7 @@ jobs: timeout-minutes: 10 strategy: matrix: - node-version: [16.x] + node-version: [18.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} diff --git a/.knownDidRegistries.ts b/.knownDidRegistries.ts new file mode 100644 index 0000000..ed44cb4 --- /dev/null +++ b/.knownDidRegistries.ts @@ -0,0 +1,19 @@ +export const knownDidRegistries = [ + { + name: 'DCC Pilot Registry', + url: 'https://digitalcredentials.github.io/issuer-registry/registry.json' + }, + { + name: 'DCC Sandbox Registry', + url: 'https://digitalcredentials.github.io/sandbox-registry/registry.json' + }, + { + name: 'DCC Community Registry', + url: 'https://digitalcredentials.github.io/community-registry/registry.json' + }, + { + name: 'DCC Registry', + url: 'https://digitalcredentials.github.io/dcc-registry/registry.json' + } +] + diff --git a/CHANGELOG.md b/CHANGELOG.md index cdf18c2..2312d23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # isomorphic-lib-template Changelog -## 1.0.0 - TBD +## 0.0.1 - TBD ### Added diff --git a/LICENSE.md b/LICENSE.md index 6d9b390..10ea4ba 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 Digital Credentials Consortium +Copyright (c) 2025 Digital Credentials Consortium Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 51429f3..c813e8b 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -# Example Isomorphic TS/JS Lib Template _(@digitalcredentials/isomorphic-lib-template)_ +# verifier-core _(@digitalcredentials/verifier-core)_ -[![Build status](https://img.shields.io/github/actions/workflow/status/digitalcredentials/isomorphic-lib-template/main.yml?branch=main)](https://github.com/digitalcredentials/isomorphic-lib-template/actions?query=workflow%3A%22Node.js+CI%22) -[![NPM Version](https://img.shields.io/npm/v/@digitalcredentials/isomorphic-lib-template.svg)](https://npm.im/@digitalcredentials/isomorphic-lib-template) +[![Build status](https://img.shields.io/github/actions/workflow/status/digitalcredentials/verifier-core/main.yml?branch=main)](https://github.com/digitalcredentials/verifier-core/actions?query=workflow%3A%22Node.js+CI%22) +[![NPM Version](https://img.shields.io/npm/v/@digitalcredentials/verifier-core.svg)](https://npm.im/@digitalcredentials/verifier-core) -> A Typescript/Javascript isomorphic library template, for use in the browser, Node.js, and React Native. +> For verifying Verifiable Credentials in the browser, Node.js, and React Native. ## Table of Contents @@ -16,7 +16,7 @@ ## Background -TBD +The Digital Credentials Consortium has a few applications that verify credentials in essentially the same way, with consequent code duplication. This package extracts that common functionality to a single shared package to make ongoing maintenance easier. ## Security @@ -24,14 +24,14 @@ TBD ## Install -- Node.js 16+ is recommended. +- Node.js 18+ is recommended. ### NPM To install via NPM: ``` -npm install @digitalcredentials/isomorphic-lib-template +npm install @digitalcredentials/verifier-core ``` ### Development @@ -39,8 +39,8 @@ npm install @digitalcredentials/isomorphic-lib-template To install locally (for development): ``` -git clone https://github.com/digitalcredentials/isomorphic-lib-template.git -cd isomorphic-lib-template +git clone https://github.com/digitalcredentials/verifier-core.git +cd verifier-core npm install ``` @@ -57,4 +57,4 @@ If editing the Readme, please conform to the ## License -[MIT License](LICENSE.md) © 2022 Digital Credentials Consortium. +[MIT License](LICENSE.md) © 2025 Digital Credentials Consortium. diff --git a/package.json b/package.json index 6dab6b5..23b8de3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "@digitalcredentials/isomorphic-lib-template", - "description": "A Typescript/Javascript isomorphic library template, for use in the browser, Node.js, and React Native.", + "name": "@digitalcredentials/verifier-core", + "description": "For verifying Verifiable Credentials in the browser, Node.js, and React Native.", "version": "0.0.1", "scripts": { "build": "npm run clear && tsc -d && tsc -p tsconfig.esm.json", @@ -23,6 +23,15 @@ "module": "dist/esm/index.js", "types": "dist/index.d.ts", "dependencies": { + "@digitalbazaar/data-integrity": "^2.5.0", + "@digitalbazaar/eddsa-rdfc-2022-cryptosuite": "^1.2.0", + "@digitalcredentials/ed25519-signature-2020": "^6.0.0", + "@digitalcredentials/issuer-registry-client": "^3.0.0", + "@digitalcredentials/jsonld-signatures": "^12.0.1", + "@digitalcredentials/security-document-loader": "^6.0.1", + "@digitalcredentials/vc": "^9.0.1", + "@digitalcredentials/vc-bitstring-status-list": "^1.0.0", + "@digitalcredentials/vc-status-list": "^9.0.0" }, "devDependencies": { "@types/chai": "^4.3.4", diff --git a/src/Example.ts b/src/Example.ts deleted file mode 100644 index 4e4a5f1..0000000 --- a/src/Example.ts +++ /dev/null @@ -1,8 +0,0 @@ -/*! - * Copyright (c) 2022 Digital Credentials Consortium. All rights reserved. - */ -export class Example { - public hello(): string { - return 'world' - } -} diff --git a/src/Verify.ts b/src/Verify.ts new file mode 100644 index 0000000..3f1dddb --- /dev/null +++ b/src/Verify.ts @@ -0,0 +1,84 @@ +// import '@digitalcredentials/data-integrity-rn'; +import { Ed25519Signature2020 } from '@digitalcredentials/ed25519-signature-2020'; +import * as vc from '@digitalcredentials/vc'; +import { securityLoader } from '@digitalcredentials/security-document-loader'; +import { getCredentialStatusChecker } from './credentialStatus'; +import { addTrustedIssuersToVerificationResponse } from './issuerRegistries'; + +import { Credential } from './types/credential'; + +/* +// the new eddsa-rdfc-2022-cryptosuite +import {DataIntegrityProof} from '@digitalbazaar/data-integrity'; +import {cryptosuite as eddsaRdfc2022CryptoSuite} from '@digitalbazaar/eddsa-rdfc-2022-cryptosuite'; +const suite = new DataIntegrityProof({ + cryptosuite: eddsaRdfc2022CryptoSuite +}); +*/ + +const documentLoader = securityLoader({ fetchRemoteContexts: true }).build(); +const suite = new Ed25519Signature2020(); + +export interface VerificationError { + "message": string, + "isFatal": boolean +} + +export interface Step { + "id": string, + "valid": boolean, + "foundInRegistries"?: string[], +} + +export interface VerificationResponse { + "verified": boolean, + "isFatal": boolean, + "credential": object, + "errors"?: VerificationError[], + "log"?: Step[] +} + +export async function verifyCredential({credential, reloadIssuerRegistry = true}:{credential: Credential, reloadIssuerRegistry: boolean}): Promise { + + const fatalErrorMessage = checkForFatalErrors(credential) + + if (fatalErrorMessage) return {credential, isFatal: true, verified: false, errors: [{message: fatalErrorMessage, isFatal: true}]} + + const verificationResponse = await vc.verifyCredential({ + credential, + suite, + documentLoader, + checkStatus: getCredentialStatusChecker(credential) + }); + + delete verificationResponse.results + delete verificationResponse.statusResult + + const { issuer } = credential + await addTrustedIssuersToVerificationResponse({verificationResponse, reloadIssuerRegistry, issuer}) + verificationResponse.isFatal = false + + return verificationResponse; +} + +function checkForFatalErrors(credential: Credential) : string | null { + + try { + // eslint-disable-next-line no-new + new URL(credential.id as string); + } catch (e) { + return "The credential's id uses an invalid format. It may have been issued as part of an early pilot. Please contact the issuer to get a replacement." + } + + if (!credential.proof) { + return 'This is not a Verifiable Credential - it does not have a digital signature.' + } + + return null +} + + +// import { purposes } from '@digitalcredentials/jsonld-signatures'; +// import { VerifiablePresentation, PresentationError } from './types/presentation'; +// const presentationPurpose = new purposes.AssertionProofPurpose(); +// import { extractCredentialsFrom } from './verifiableObject'; diff --git a/src/credentialStatus.ts b/src/credentialStatus.ts new file mode 100644 index 0000000..793aa4a --- /dev/null +++ b/src/credentialStatus.ts @@ -0,0 +1,39 @@ +import { checkStatus } from '@digitalcredentials/vc-bitstring-status-list'; +import { checkStatus as checkStatusLegacy } from '@digitalcredentials/vc-status-list'; +import { Credential } from './types/credential'; + +export enum StatusPurpose { + Revocation = 'revocation', + Suspension = 'suspension' +} + +export function getCredentialStatusChecker(credential: Credential) : (() => boolean) | null { + if (!credential.credentialStatus) { + return null; + } + const credentialStatuses = Array.isArray(credential.credentialStatus) ? + credential.credentialStatus : + [credential.credentialStatus]; + const [credentialStatus] = credentialStatuses; + switch (credentialStatus.type) { + case 'BitstringStatusListEntry': + return checkStatus; + case 'StatusList2021Entry': + return checkStatusLegacy; + default: + return null; + } +} + +export function hasStatusPurpose( + credential: Credential, + statusPurpose: StatusPurpose +) : boolean { + if (!credential.credentialStatus) { + return false; + } + const credentialStatuses = Array.isArray(credential.credentialStatus) ? + credential.credentialStatus : + [credential.credentialStatus]; + return credentialStatuses.some(s => s.statusPurpose === statusPurpose); +} diff --git a/src/declarations.d.ts b/src/declarations.d.ts index d4dea51..e276bec 100644 --- a/src/declarations.d.ts +++ b/src/declarations.d.ts @@ -1 +1,11 @@ -// declare module 'jsonld' +declare module '@digitalcredentials/did-io'; +declare module '@digitalcredentials/did-method-key'; +declare module '@digitalcredentials/vc'; +declare module '@digitalcredentials/vc-bitstring-status-list'; +declare module '@digitalcredentials/vc-status-list'; +declare module '@digitalcredentials/vpqr'; +declare module '@digitalcredentials/jsonld-signatures'; +declare module '@digitalbazaar/data-integrity'; +declare module '@digitalbazaar/eddsa-rdfc-2022-cryptosuite'; +declare module '@digitalcredentials/ed25519-signature-2020'; +declare module '@digitalcredentials/ed25519-verification-key-2020'; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 86d30cb..9d833f2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ /*! * Copyright (c) 2022 Digital Credentials Consortium. All rights reserved. */ -export { Example } from './Example' +export { verifyCredential, // verifyPresentation + } from './Verify' diff --git a/src/issuerRegistries.ts b/src/issuerRegistries.ts new file mode 100644 index 0000000..ed9a795 --- /dev/null +++ b/src/issuerRegistries.ts @@ -0,0 +1,46 @@ +import {RegistryClient} from '@digitalcredentials/issuer-registry-client'; +import {knownDidRegistries} from '../.knownDidRegistries' +import { VerificationResponse } from './Verify'; +const registries = new RegistryClient() +const registryNotYetLoaded = true; +/** + * Checks to see if a VC's issuer appears in any of the known DID registries. + * + * @returns A list of the names of the DID registries in which the issuer appears. + */ + +export async function getTrustedRegistryListForIssuer({ issuer, reloadIssuerRegistry = false }: { + issuer: string | any, + reloadIssuerRegistry: boolean | null +}): Promise { + + if (reloadIssuerRegistry ?? registryNotYetLoaded) { + await registries.load({ config: knownDidRegistries }) + } + const issuerDid = typeof issuer === 'string' ? issuer : issuer.id; + const issuerInfo = registries.didEntry(issuerDid); + // See if the issuer DID appears in any of the known registries + // If yes, assemble a list of registries in which it appears + return issuerInfo?.inRegistries + ? Array.from(issuerInfo.inRegistries).map(r => r.name) + : null; +} + +export async function addTrustedIssuersToVerificationResponse( {issuer, reloadIssuerRegistry = false, verificationResponse} :{ + issuer: string | any, + reloadIssuerRegistry: boolean | null + verificationResponse: VerificationResponse +}) : Promise + { + const foundInRegistries = await getTrustedRegistryListForIssuer( {issuer, reloadIssuerRegistry}); + + const registryStep = { + "id": "registered_issuer", + "valid": !!foundInRegistries, + ...(foundInRegistries && { foundInRegistries }) + }; + + (verificationResponse.log ??= []).push(registryStep) + +} + diff --git a/src/test-fixtures/vc.ts b/src/test-fixtures/vc.ts new file mode 100644 index 0000000..7a6bfed --- /dev/null +++ b/src/test-fixtures/vc.ts @@ -0,0 +1,253 @@ +const signedVC1Unrevoked = { + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "name": "Teamwork Badge", + "issuer": { + "type": [ + "Profile" + ], + "name": "Example Corp", + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q" + }, + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "validFrom": "2010-01-01T00:00:00Z", + "credentialSubject": { + "type": [ + "AchievementSubject" + ], + "name": "Taylor Tuna", + "achievement": { + "id": "https://example.com/achievements/21st-century-skills/teamwork", + "type": [ + "Achievement" + ], + "name": "Masters", + "criteria": { + "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." + }, + "description": "This badge recognizes the development of the capacity to collaborate within a group environment." + }, + "id": "did:key:z6Mktp8yHRrcEXePJGFhUDsL7X32pwfuuV4TrpaP7dZupdwg" + }, + "id": "urn:uuid:6740bee6b9c3df2a256e144e", + "credentialStatus": { + "id": "https://testing.dcconsortium.org/status/e5WK8CbZ1GjycuPombrj#4", + "type": "BitstringStatusListEntry", + "statusPurpose": "revocation", + "statusListCredential": "https://testing.dcconsortium.org/status/e5WK8CbZ1GjycuPombrj", + "statusListIndex": "4" + }, + "proof": { + "type": "Ed25519Signature2020", + "created": "2024-11-22T17:28:35Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "z38x1N8hFFXEQgfomjv1MvP32qqtqzx4sGQAyqqfDGXqLBcw39jKBQvcwWeiVJrqtxZJmu8RZ5DPUrrAc36ejoPyE" + } +} + +const signedVC1 = { + '@context': [ + 'https://www.w3.org/2018/credentials/v1', + 'https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.2.json', + 'https://w3id.org/security/suites/ed25519-2020/v1' + ], + id: 'urn:uuid:2fe53dc9-b2ec-4939-9b2c-0d00f6663b6c', + type: ['VerifiableCredential', 'OpenBadgeCredential'], + name: 'DCC Test Credential', + issuer: { + type: ['Profile'], + id: 'did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q', + name: 'Digital Credentials Consortium Test Issuer', + url: 'https://dcconsortium.org', + image: + 'https://user-images.githubusercontent.com/752326/230469660-8f80d264-eccf-4edd-8e50-ea634d407778.png' + }, + issuanceDate: '2023-08-02T17:43:32.903Z', + credentialSubject: { + type: ['AchievementSubject'], + achievement: { + id: 'urn:uuid:bd6d9316-f7ae-4073-a1e5-2f7f5bd22922', + type: ['Achievement'], + achievementType: 'Diploma', + name: 'Badge', + description: + 'This is a sample credential issued by the Digital Credentials Consortium to demonstrate the functionality of Verifiable Credentials for wallets and verifiers.', + criteria: { + type: 'Criteria', + narrative: + 'This credential was issued to a student that demonstrated proficiency in the Python programming language that occurred from **February 17, 2023** to **June 12, 2023**.' + }, + image: { + id: 'https://user-images.githubusercontent.com/752326/214947713-15826a3a-b5ac-4fba-8d4a-884b60cb7157.png', + type: 'Image' + } + }, + name: 'Jane Doe' + }, + proof: { + type: 'Ed25519Signature2020', + created: '2023-10-05T11:17:41Z', + verificationMethod: + 'did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q', + proofPurpose: 'assertionMethod', + proofValue: + 'z5fk6gq9upyZvcFvJdRdeL5KmvHr69jxEkyDEd2HyQdyhk9VnDEonNSmrfLAcLEDT9j4gGdCG24WHhojVHPbRsNER' + } + } + +const usignedVCv2 = { + '@context': [ + 'https://www.w3.org/ns/credentials/v2', + 'https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json', + 'https://w3id.org/security/suites/ed25519-2020/v1' + ], + id: 'urn:uuid:2fe53dc9-b2ec-4939-9b2c-0d00f6663b6c', + type: ['VerifiableCredential', 'OpenBadgeCredential'], + name: 'DCC Test Credential', + issuer: { + type: ['Profile'], + id: 'did:key:z6MkhVTX9BF3NGYX6cc7jWpbNnR7cAjH8LUffabZP8Qu4ysC', + name: 'Digital Credentials Consortium Test Issuer', + url: 'https://dcconsortium.org', + image: + 'https://user-images.githubusercontent.com/752326/230469660-8f80d264-eccf-4edd-8e50-ea634d407778.png' + }, + validFrom: '2023-08-02T17:43:32.903Z', + credentialSubject: { + type: ['AchievementSubject'], + achievement: { + id: 'urn:uuid:bd6d9316-f7ae-4073-a1e5-2f7f5bd22922', + type: ['Achievement'], + achievementType: 'Diploma', + name: 'Badge', + description: + 'This is a sample credential issued by the Digital Credentials Consortium to demonstrate the functionality of Verifiable Credentials for wallets and verifiers.', + criteria: { + type: 'Criteria', + narrative: + 'This credential was issued to a student that demonstrated proficiency in the Python programming language that occurred from **February 17, 2023** to **June 12, 2023**.' + }, + image: { + id: 'https://user-images.githubusercontent.com/752326/214947713-15826a3a-b5ac-4fba-8d4a-884b60cb7157.png', + type: 'Image' + } + }, + name: 'Jane Doe' + } +} +const unsignedVC = { + '@context': [ + 'https://www.w3.org/2018/credentials/v1', + 'https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json', + 'https://w3id.org/vc/status-list/2021/v1', + 'https://w3id.org/security/suites/ed25519-2020/v1' + ], + id: 'urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1', + type: ['VerifiableCredential', 'OpenBadgeCredential'], + issuer: { + id: 'did:key:z6MkhVTX9BF3NGYX6cc7jWpbNnR7cAjH8LUffabZP8Qu4ysC', + type: 'Profile', + name: 'Izzy the Issuer', + description: 'Issue Issue Issue', + url: 'https://izzy.iz/', + image: { + id: 'https://upload.wikimedia.org/wikipedia/commons/a/ad/Blank_2018.png', + type: 'Image' + } + }, + issuanceDate: '2020-01-01T00:00:00Z', + name: 'Introduction to Digital Credentialing', + credentialSubject: { + type: 'AchievementSubject', + identifier: { + type: 'IdentityObject', + identityHash: 'jc.chartrand@gmail.com', + hashed: 'false' + }, + achievement: { + id: 'http://izzy.iz', + type: 'Achievement', + criteria: { + narrative: 'Completion of a credential.' + }, + description: 'Well done you!', + name: 'Introduction to Digital Credentialing' + } + } +} + +// "credentialStatus": +const credentialStatus = { + id: 'https://digitalcredentials.github.io/credential-status-jc-test/XA5AAK1PV4#16', + type: 'StatusList2021Entry', + statusPurpose: 'revocation', + statusListIndex: 16, + statusListCredential: + 'https://digitalcredentials.github.io/credential-status-jc-test/XA5AAK1PV4' +} + +const credentialStatusBitString = { + id: 'https://digitalcredentials.github.io/credential-status-jc-test/XA5AAK1PV4#16', + type: 'BitstringStatusListEntry', + statusPurpose: 'revocation', + statusListIndex: 16, + statusListCredential: + 'https://digitalcredentials.github.io/credential-status-jc-test/XA5AAK1PV4' +} + +const getUnsignedVC = (): any => JSON.parse(JSON.stringify(unsignedVC)) + +const getUnsignedVCv2 = (): any => JSON.parse(JSON.stringify(usignedVCv2)) + +const getUnsignedVCWithoutSuiteContext = (): any => { + const vcCopy = JSON.parse(JSON.stringify(unsignedVC)) + const index = vcCopy['@context'].indexOf(ed25519SuiteContext) + if (index > -1) { + vcCopy['@context'].splice(index, 1) + } + return vcCopy +} +const getCredentialStatus = (): any => JSON.parse(JSON.stringify(credentialStatus)) +const getCredentialStatusBitString = (): any => + JSON.parse(JSON.stringify(credentialStatusBitString)) + +const getUnsignedVCWithStatus = (): any => { + const unsignedVCWithStatus = getUnsignedVC() + unsignedVCWithStatus.credentialStatus = getCredentialStatus() + return unsignedVCWithStatus +} + +const getUnsignedVC2WithStatus = (): any => { + const unsignedVC2WithStatus = getUnsignedVCv2() + unsignedVC2WithStatus.credentialStatus = getCredentialStatusBitString() + return unsignedVC2WithStatus +} + +const ed25519SuiteContext = + 'https://w3id.org/security/suites/ed25519-2020/v1' + +const getSignedVC = (): any => { + return signedVC1 +} + +const getSignedUnrevokedVC2 = (): any => { + return signedVC1Unrevoked +} +export { + getSignedUnrevokedVC2, + getSignedVC, + getUnsignedVC, + getUnsignedVCWithoutSuiteContext, + getCredentialStatus, + getCredentialStatusBitString, + getUnsignedVCWithStatus, + getUnsignedVC2WithStatus, + ed25519SuiteContext +} diff --git a/test/Example.spec.ts b/test/Example.spec.ts deleted file mode 100644 index 3bf5e63..0000000 --- a/test/Example.spec.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { expect } from 'chai' -import { Example } from '../src' - -describe('Example', () => { - it('calls function', async () => { - const ex = new Example() - expect(ex.hello()).to.equal('world') - }) -}) diff --git a/test/Verify.spec.ts b/test/Verify.spec.ts new file mode 100644 index 0000000..542765e --- /dev/null +++ b/test/Verify.spec.ts @@ -0,0 +1,16 @@ +//import { expect } from 'chai' +import { strict as assert } from 'assert'; +import { verifyCredential } from '../src/Verify' +import { getSignedUnrevokedVC2 } from '../src/test-fixtures/vc' + +describe('Verify', () => { + it('calls function', async () => { + //const signedVC : any = getSignedVC() + const signedVC2Unrevoked : any = getSignedUnrevokedVC2() + const result = await verifyCredential({credential: signedVC2Unrevoked, reloadIssuerRegistry: true}) + console.log("result returned from verifyCredential call:") + console.log(JSON.stringify(result,null,2)) + assert.ok(result.verified); + //expect(result.verified).to.be.true + }) +}) From dfc8c27ead7393817aa1b5628c3adf9f4f946bbc Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Tue, 7 Jan 2025 17:59:41 -0500 Subject: [PATCH 02/72] add error handling --- .github/workflows/main.yml | 4 +- src/Verify.ts | 32 +++++++++++-- src/test-fixtures/expiredV2.ts | 46 +++++++++++++++++++ src/test-fixtures/vc.ts | 84 +++++++++++++--------------------- test/Verify.spec.ts | 23 +++++++++- 5 files changed, 128 insertions(+), 61 deletions(-) create mode 100644 src/test-fixtures/expiredV2.ts diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4aa9aa3..20fcb80 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -26,7 +26,7 @@ jobs: timeout-minutes: 10 strategy: matrix: - node-version: [16.x] + node-version: [18.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} @@ -40,7 +40,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [16.x] + node-version: [18.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} diff --git a/src/Verify.ts b/src/Verify.ts index 3f1dddb..afef0d4 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -21,7 +21,9 @@ const suite = new Ed25519Signature2020(); export interface VerificationError { "message": string, - "isFatal": boolean + "isFatal": boolean, + "name"?: string, + stackTrace?: string } export interface Step { @@ -38,11 +40,15 @@ export interface VerificationResponse { "log"?: Step[] } + export async function verifyCredential({credential, reloadIssuerRegistry = true}:{credential: Credential, reloadIssuerRegistry: boolean}): Promise { + const fatalErrorMessage = checkForFatalErrors(credential) - if (fatalErrorMessage) return {credential, isFatal: true, verified: false, errors: [{message: fatalErrorMessage, isFatal: true}]} + if (fatalErrorMessage) { + return buildFatalErrorObject(fatalErrorMessage, "fatalError", credential, null) + } const verificationResponse = await vc.verifyCredential({ credential, @@ -51,18 +57,36 @@ export async function verifyCredential({credential, reloadIssuerRegistry = true} checkStatus: getCredentialStatusChecker(credential) }); + verificationResponse.isFatal = false + + if (verificationResponse.error) { + if (verificationResponse.error.log) { + verificationResponse.log = verificationResponse.error.log + delete verificationResponse.error + } else if (verificationResponse?.error?.name === 'VerificationError') { + const fatalErrorMessage = 'The signature is not valid.' + const stackTrace = verificationResponse?.error?.errors?.stack + return buildFatalErrorObject(fatalErrorMessage, "invalidSignature", credential, stackTrace) + } + } + console.log('results from the verify call:') + console.log(JSON.stringify(verificationResponse)) + delete verificationResponse.results delete verificationResponse.statusResult const { issuer } = credential await addTrustedIssuersToVerificationResponse({verificationResponse, reloadIssuerRegistry, issuer}) - verificationResponse.isFatal = false + return verificationResponse; } -function checkForFatalErrors(credential: Credential) : string | null { +function buildFatalErrorObject(fatalErrorMessage: string, name: string, credential: Credential, stackTrace: string | null) : VerificationResponse { + return {credential, isFatal: true, verified: false, errors: [{name, message: fatalErrorMessage, isFatal: true, ...stackTrace?{stackTrace}:null}]} +} +function checkForFatalErrors(credential: Credential) : string | null { try { // eslint-disable-next-line no-new new URL(credential.id as string); diff --git a/src/test-fixtures/expiredV2.ts b/src/test-fixtures/expiredV2.ts new file mode 100644 index 0000000..5e01afa --- /dev/null +++ b/src/test-fixtures/expiredV2.ts @@ -0,0 +1,46 @@ +export const expiredV2 = { + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "id": "http://example.com/credentials/3527", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "issuer": { + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "type": [ + "Profile" + ], + "name": "Example Corp" + }, + "validFrom": "2010-01-01T00:00:00Z", + "validUntil": "2011-01-01T00:00:00Z", + "name": "Teamwork Badge", + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "type": [ + "AchievementSubject" + ], + "achievement": { + "id": "https://example.com/achievements/21st-century-skills/teamwork", + "type": [ + "Achievement" + ], + "criteria": { + "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." + }, + "description": "This badge recognizes the development of the capacity to collaborate within a group environment.", + "name": "Teamwork" + } + }, + "proof": { + "type": "Ed25519Signature2020", + "created": "2025-01-07T22:14:25Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "z2B4R2hwrcgR8ag39SZEgm2hAJSPnoWae7zdRr8RUTkcbMPNHu7tedk1x3D29J3CmiU5Wb1e7zew82nQAKYCQuRBo" + } +} \ No newline at end of file diff --git a/src/test-fixtures/vc.ts b/src/test-fixtures/vc.ts index 7a6bfed..9a2e138 100644 --- a/src/test-fixtures/vc.ts +++ b/src/test-fixtures/vc.ts @@ -1,3 +1,4 @@ +import { expiredV2 } from "./expiredV2" const signedVC1Unrevoked = { "type": [ "VerifiableCredential", @@ -142,7 +143,7 @@ const usignedVCv2 = { name: 'Jane Doe' } } -const unsignedVC = { +const unsignedVCv1 = { '@context': [ 'https://www.w3.org/2018/credentials/v1', 'https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json', @@ -183,71 +184,48 @@ const unsignedVC = { } } -// "credentialStatus": -const credentialStatus = { - id: 'https://digitalcredentials.github.io/credential-status-jc-test/XA5AAK1PV4#16', - type: 'StatusList2021Entry', - statusPurpose: 'revocation', - statusListIndex: 16, - statusListCredential: - 'https://digitalcredentials.github.io/credential-status-jc-test/XA5AAK1PV4' -} -const credentialStatusBitString = { - id: 'https://digitalcredentials.github.io/credential-status-jc-test/XA5AAK1PV4#16', - type: 'BitstringStatusListEntry', - statusPurpose: 'revocation', - statusListIndex: 16, - statusListCredential: - 'https://digitalcredentials.github.io/credential-status-jc-test/XA5AAK1PV4' -} -const getUnsignedVC = (): any => JSON.parse(JSON.stringify(unsignedVC)) + +const getUnsignedVC1 = (): any => JSON.parse(JSON.stringify(unsignedVCv1)) const getUnsignedVCv2 = (): any => JSON.parse(JSON.stringify(usignedVCv2)) -const getUnsignedVCWithoutSuiteContext = (): any => { - const vcCopy = JSON.parse(JSON.stringify(unsignedVC)) - const index = vcCopy['@context'].indexOf(ed25519SuiteContext) - if (index > -1) { - vcCopy['@context'].splice(index, 1) - } - return vcCopy -} -const getCredentialStatus = (): any => JSON.parse(JSON.stringify(credentialStatus)) -const getCredentialStatusBitString = (): any => - JSON.parse(JSON.stringify(credentialStatusBitString)) - -const getUnsignedVCWithStatus = (): any => { - const unsignedVCWithStatus = getUnsignedVC() - unsignedVCWithStatus.credentialStatus = getCredentialStatus() - return unsignedVCWithStatus -} -const getUnsignedVC2WithStatus = (): any => { - const unsignedVC2WithStatus = getUnsignedVCv2() - unsignedVC2WithStatus.credentialStatus = getCredentialStatusBitString() - return unsignedVC2WithStatus +const getSignedVC1 = (): any => { + return JSON.parse(JSON.stringify(signedVC1)) } -const ed25519SuiteContext = - 'https://w3id.org/security/suites/ed25519-2020/v1' -const getSignedVC = (): any => { - return signedVC1 -} const getSignedUnrevokedVC2 = (): any => { return signedVC1Unrevoked } + +const getTamperedVC1 = (): any => { + const signedVC1 = getSignedVC1() + signedVC1.name = 'Introduction to Tampering' + return signedVC1 +} + +const getExpiredVC1 = (): any => { + return null +} +const getExpiredVC2 = (): any => { + return JSON.parse(JSON.stringify(expiredV2)) +} + +const getExpiredAndTamperedVC2 = (): any => { + const cred = getExpiredVC2() + cred.name = 'tampered!' + return cred +} + export { + getExpiredVC2, + getExpiredAndTamperedVC2, + getTamperedVC1, getSignedUnrevokedVC2, - getSignedVC, - getUnsignedVC, - getUnsignedVCWithoutSuiteContext, - getCredentialStatus, - getCredentialStatusBitString, - getUnsignedVCWithStatus, - getUnsignedVC2WithStatus, - ed25519SuiteContext + getSignedVC1, + getUnsignedVC1 } diff --git a/test/Verify.spec.ts b/test/Verify.spec.ts index 542765e..1b8eff4 100644 --- a/test/Verify.spec.ts +++ b/test/Verify.spec.ts @@ -1,10 +1,10 @@ //import { expect } from 'chai' import { strict as assert } from 'assert'; import { verifyCredential } from '../src/Verify' -import { getSignedUnrevokedVC2 } from '../src/test-fixtures/vc' +import { getSignedUnrevokedVC2, getTamperedVC1, getExpiredVC2 } from '../src/test-fixtures/vc' describe('Verify', () => { - it('calls function', async () => { + it('verifies with valid status', async () => { //const signedVC : any = getSignedVC() const signedVC2Unrevoked : any = getSignedUnrevokedVC2() const result = await verifyCredential({credential: signedVC2Unrevoked, reloadIssuerRegistry: true}) @@ -13,4 +13,23 @@ describe('Verify', () => { assert.ok(result.verified); //expect(result.verified).to.be.true }) + + it.only('returns fatal error when tampered', async () => { + const tamperedVC1 : any = getTamperedVC1() + const result = await verifyCredential({credential: tamperedVC1, reloadIssuerRegistry: true}) + console.log("result returned from verifyCredential call:") + console.log(JSON.stringify(result,null,2)) + assert.ok(result.verified === false); + //expect(result.verified).to.be.true + }) + + it('returns fatal error when tampered', async () => { + const expiredVC2 : any = getExpiredVC2() + const result = await verifyCredential({credential: expiredVC2, reloadIssuerRegistry: true}) + console.log("result returned from verifyCredential call:") + console.log(JSON.stringify(result,null,2)) + assert.ok(result.verified === false); + //expect(result.verified).to.be.true + }) + }) From 584f9a95014c2e29fd872d44f3bed0514ed71b22 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Wed, 8 Jan 2025 14:32:39 -0500 Subject: [PATCH 03/72] update tests and readme --- README.md | 235 +++++++++++++++++++++++++++++++++++++--- src/Verify.ts | 20 +--- src/issuerRegistries.ts | 2 +- test/Verify.spec.ts | 7 +- 4 files changed, 228 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index c813e8b..8a18d78 100644 --- a/README.md +++ b/README.md @@ -3,24 +3,237 @@ [![Build status](https://img.shields.io/github/actions/workflow/status/digitalcredentials/verifier-core/main.yml?branch=main)](https://github.com/digitalcredentials/verifier-core/actions?query=workflow%3A%22Node.js+CI%22) [![NPM Version](https://img.shields.io/npm/v/@digitalcredentials/verifier-core.svg)](https://npm.im/@digitalcredentials/verifier-core) -> For verifying Verifiable Credentials in the browser, Node.js, and React Native. +> Verifies W3C Verifiable Credentials in the browser, Node.js, and React Native. ## Table of Contents - -- [Background](#background) -- [Security](#security) +- [Overview](#overview) +- [API](#api) - [Install](#install) -- [Usage](#usage) - [Contribute](#contribute) - [License](#license) -## Background +## Overview + +Verifies the following versions of W3C Verifiable Credentials: + +* [1.0](https://www.w3.org/TR/2019/REC-vc-data-model-20191119/) +* [1.1](https://www.w3.org/TR/2022/REC-vc-data-model-20220303/) +* [2.0](https://www.w3.org/TR/vc-data-model-2.0/) + +The verification checks that the credential: + +* has a valid signature (i.e, that the credential hasn't been tampered with) +* hasn't expired +* hasn't been revoked +* was signed by a trusted issuer + +As of January 2025 issuers are trusted if they are listed in one of the Digital Credentials Issuer Registries: + +``` +{ + name: 'DCC Pilot Registry', + url: 'https://digitalcredentials.github.io/issuer-registry/registry.json' + }, + { + name: 'DCC Sandbox Registry', + url: 'https://digitalcredentials.github.io/sandbox-registry/registry.json' + }, + { + name: 'DCC Community Registry', + url: 'https://digitalcredentials.github.io/community-registry/registry.json' + }, + { + name: 'DCC Registry', + url: 'https://digitalcredentials.github.io/dcc-registry/registry.json' + } + ``` + + The DCC is actively working on a new trust registry model that will likely extend the registry scope. + +## API + +This package exports two methods: + +* verifyCredential +* verifyPresentation + +### verifyCredential -The Digital Credentials Consortium has a few applications that verify credentials in essentially the same way, with consequent code duplication. This package extracts that common functionality to a single shared package to make ongoing maintenance easier. +```verifyCredential({credential, reloadIssuerRegistry = true})``` -## Security +#### arguments -TBD +* credential - The W3C Verifiable Credential to be verified. +* reloadIssuerRegistry - A boolean (true/false) indication whether or not to refresh the cached copy of the registry. + +#### result + +The typescript definitions for the result can be found [here](./src/types/result.ts) + +##### successful verification + +A verification is successful if the signature is valid (the credential hasn't been tampered with), hasn't expired, hasn't been revoked, and was signed by a trusted issuer. + +A successful verification might look like this example: + +``` +{ + "verified": true, + "isFatal": false, + "credential": {the supplied vc - left out here for brevity/clarity}, + "log": [ + { + "id": "valid_signature", + "valid": true + }, + { + "id": "issuer_did_resolves", + "valid": true + }, + { + "id": "expiration", + "valid": true + }, + { + "id": "revocation_status", + "valid": true + }, + { + "id": "registered_issuer", + "valid": true, + "foundInRegistries": [ + "DCC Sandbox Registry" + ] + } + ] +} +``` + +##### unsucessful verification + +An unsuccessful verification means that one of the steps (other than the 'valid_signature' step) returned false, so the credential has expired, and/or been revoked, and/or can't be confirmed to be signed by a known issuer. Note that an invalid signature is considered fatal because it means that the revocation status, expiry data, or issuer id may have been changed so we can't say anything conclusive about any of them. + +An unsuccessful verification might look like this example: + +``` +{ + "verified": false, + "isFatal": false, + "credential": {the supplied vc - left out here for brevity/clarity}, + "log": [ + { + "id": "valid_signature", + "valid": true + }, + { + "id": "issuer_did_resolves", + "valid": true + }, + { + "id": "expiration", + "valid": false + }, + "id": "revocation_status", + "valid": true + }, + { + "id": "registered_issuer", + "valid": true, + "foundInRegistries": [ + "DCC Sandbox Registry" + ] + } + ] +} +``` + +##### partially successful verification + +A verification might partly succeed if it can verify the signature and the expiry date, but can't retrieve any of the revocation status, the issuer registry, or the issuer's DID document from the network to verify the revocation status and issuer identity. + +For those steps that we couldn't verify conclusively one way or the other (true or false) we return an 'error' propery rather than a 'valid' property. + +A partially successful verification might look like this example: + +``` +{ + "verified": false, + "isFatal": false, + "credential": {the supplied vc - left out here for brevity/clarity}, + "log": [ + { + "id": "valid_signature", + "valid": true + }, + { + "id": "expiration", + "valid": true + }, + { + "id": "revocation_status", + "error": { + "name": "network-error", + "message": "Could not retrieve the revocation status list." + } + }, + { + "id": "registered_issuer", + "error": { + "name": "network-error", + "message": "Could not retrieve the issuer registry." + } + }, + { + "id": "issuer_did_resolves", + "error": { + "name": "network-error", + "message": "Could not retrieve the issuer DID." + } + } + ] +} +``` + +##### fatal error + +Fatal errors are errors that prevent us from saying anything conclusive about the credential, and so we don't list the results of each step (the 'log') because we can't say decisively one way or the other if any are true or false. Reverting to saying they are all false would be misleading, because that could be interepreted to mean that the credential was, for example, revoked when really we just don't know one way or the other. + +``` +{ + "credential": {the vc goes here}, + "isFatal": true, + "verified": false, + "errors": [ + { + "name": "invalidSignature", + "message": "The signature is not valid." + } + ] +} +``` +Examples of fatal errors: + +* invalid signature +Fatal because if the signature is invalid it means any part of the credential could have been tampered with, including the revocation status, expiration, and issuer identity + +* software problem +A software error might prevent verification + +* malformed credential +The supplied credential may not conform to the VerifiableCredential or LinkedData specifications(possibly because it follows some older convention, or maybe hasn't yet been signed) and might not even be a Verifiable Credential at all. + + + +### verifyPresentation + +```verifyPresentation({presentation, reloadIssuerRegistry = true})``` + +#### arguments + +* presentation - The W3C Verifiable Presentation to be verified. +* reloadIssuerRegistry - Whether or not to refresh the cached copy of the registry. + +#### result ## Install @@ -44,10 +257,6 @@ cd verifier-core npm install ``` -## Usage - -TBD - ## Contribute PRs accepted. diff --git a/src/Verify.ts b/src/Verify.ts index afef0d4..4c2b791 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -6,6 +6,7 @@ import { getCredentialStatusChecker } from './credentialStatus'; import { addTrustedIssuersToVerificationResponse } from './issuerRegistries'; import { Credential } from './types/credential'; +import { VerificationResponse } from './types/result'; /* // the new eddsa-rdfc-2022-cryptosuite @@ -19,26 +20,7 @@ const suite = new DataIntegrityProof({ const documentLoader = securityLoader({ fetchRemoteContexts: true }).build(); const suite = new Ed25519Signature2020(); -export interface VerificationError { - "message": string, - "isFatal": boolean, - "name"?: string, - stackTrace?: string -} - -export interface Step { - "id": string, - "valid": boolean, - "foundInRegistries"?: string[], -} -export interface VerificationResponse { - "verified": boolean, - "isFatal": boolean, - "credential": object, - "errors"?: VerificationError[], - "log"?: Step[] -} export async function verifyCredential({credential, reloadIssuerRegistry = true}:{credential: Credential, reloadIssuerRegistry: boolean}): Promise { diff --git a/src/issuerRegistries.ts b/src/issuerRegistries.ts index ed9a795..953c86c 100644 --- a/src/issuerRegistries.ts +++ b/src/issuerRegistries.ts @@ -1,6 +1,6 @@ import {RegistryClient} from '@digitalcredentials/issuer-registry-client'; import {knownDidRegistries} from '../.knownDidRegistries' -import { VerificationResponse } from './Verify'; +import { VerificationResponse } from './types/result'; const registries = new RegistryClient() const registryNotYetLoaded = true; /** diff --git a/test/Verify.spec.ts b/test/Verify.spec.ts index 1b8eff4..91d908b 100644 --- a/test/Verify.spec.ts +++ b/test/Verify.spec.ts @@ -4,7 +4,7 @@ import { verifyCredential } from '../src/Verify' import { getSignedUnrevokedVC2, getTamperedVC1, getExpiredVC2 } from '../src/test-fixtures/vc' describe('Verify', () => { - it('verifies with valid status', async () => { + it.only('verifies with valid status', async () => { //const signedVC : any = getSignedVC() const signedVC2Unrevoked : any = getSignedUnrevokedVC2() const result = await verifyCredential({credential: signedVC2Unrevoked, reloadIssuerRegistry: true}) @@ -14,7 +14,7 @@ describe('Verify', () => { //expect(result.verified).to.be.true }) - it.only('returns fatal error when tampered', async () => { + it('returns fatal error when tampered', async () => { const tamperedVC1 : any = getTamperedVC1() const result = await verifyCredential({credential: tamperedVC1, reloadIssuerRegistry: true}) console.log("result returned from verifyCredential call:") @@ -23,12 +23,13 @@ describe('Verify', () => { //expect(result.verified).to.be.true }) - it('returns fatal error when tampered', async () => { + it('returns unverified when expired', async () => { const expiredVC2 : any = getExpiredVC2() const result = await verifyCredential({credential: expiredVC2, reloadIssuerRegistry: true}) console.log("result returned from verifyCredential call:") console.log(JSON.stringify(result,null,2)) assert.ok(result.verified === false); + assert.ok(result.log); //expect(result.verified).to.be.true }) From c9b45b7be5f9ac6f92802229bb4c307b69fc3d78 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Wed, 8 Jan 2025 14:39:07 -0500 Subject: [PATCH 04/72] Update README.md fix formatting --- README.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8a18d78..f71de8b 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,9 @@ This package exports two methods: The typescript definitions for the result can be found [here](./src/types/result.ts) -##### successful verification +There are four general flavours of result that might be returned: + +1. successful verification A verification is successful if the signature is valid (the credential hasn't been tampered with), hasn't expired, hasn't been revoked, and was signed by a trusted issuer. @@ -109,11 +111,11 @@ A successful verification might look like this example: } ``` -##### unsucessful verification +2. unsucessful verification An unsuccessful verification means that one of the steps (other than the 'valid_signature' step) returned false, so the credential has expired, and/or been revoked, and/or can't be confirmed to be signed by a known issuer. Note that an invalid signature is considered fatal because it means that the revocation status, expiry data, or issuer id may have been changed so we can't say anything conclusive about any of them. -An unsuccessful verification might look like this example: +An unsuccessful verification (in this case because the credential has expired) might look like this example: ``` { @@ -147,7 +149,7 @@ An unsuccessful verification might look like this example: } ``` -##### partially successful verification +3. partially successful verification A verification might partly succeed if it can verify the signature and the expiry date, but can't retrieve any of the revocation status, the issuer registry, or the issuer's DID document from the network to verify the revocation status and issuer identity. @@ -194,7 +196,7 @@ A partially successful verification might look like this example: } ``` -##### fatal error +4. fatal error Fatal errors are errors that prevent us from saying anything conclusive about the credential, and so we don't list the results of each step (the 'log') because we can't say decisively one way or the other if any are true or false. Reverting to saying they are all false would be misleading, because that could be interepreted to mean that the credential was, for example, revoked when really we just don't know one way or the other. @@ -214,12 +216,15 @@ Fatal errors are errors that prevent us from saying anything conclusive about th Examples of fatal errors: * invalid signature + Fatal because if the signature is invalid it means any part of the credential could have been tampered with, including the revocation status, expiration, and issuer identity * software problem + A software error might prevent verification * malformed credential + The supplied credential may not conform to the VerifiableCredential or LinkedData specifications(possibly because it follows some older convention, or maybe hasn't yet been signed) and might not even be a Verifiable Credential at all. From 9798dfbf3a889f69bea1a2bb1a88b1a42e53ff91 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Wed, 8 Jan 2025 18:15:26 -0500 Subject: [PATCH 05/72] wip: update readme and organize test fixtures --- README.md | 16 ++-- src/test-fixtures/signedUnrevokedVC1.ts | 54 ++++++++++++ src/test-fixtures/signedVC1.ts | 49 +++++++++++ src/test-fixtures/vc.ts | 110 +----------------------- 4 files changed, 118 insertions(+), 111 deletions(-) create mode 100644 src/test-fixtures/signedUnrevokedVC1.ts create mode 100644 src/test-fixtures/signedVC1.ts diff --git a/README.md b/README.md index f71de8b..cdda6da 100644 --- a/README.md +++ b/README.md @@ -215,24 +215,28 @@ Fatal errors are errors that prevent us from saying anything conclusive about th ``` Examples of fatal errors: -* invalid signature +invalid signature Fatal because if the signature is invalid it means any part of the credential could have been tampered with, including the revocation status, expiration, and issuer identity -* software problem +software problem A software error might prevent verification -* malformed credential +malformed credential The supplied credential may not conform to the VerifiableCredential or LinkedData specifications(possibly because it follows some older convention, or maybe hasn't yet been signed) and might not even be a Verifiable Credential at all. - - ### verifyPresentation ```verifyPresentation({presentation, reloadIssuerRegistry = true})``` +A Verifiable Presentation (VP) is a wrapper around zero or more Verifiable Credentials. A VP is also cryptographically signed, like a VC, but whereas a VC is signed by the issuer of the credentials, the VP is signed by the holder of the credentials, typically to demonstrate 'control' of the contained credentials. The VP is signed with a DID that the holder owns, and ofthen that DID is recorded inside the Verifiable Credentials as the 'owner' or 'holder' of the credential. So by signing the VP with the private key corresponding to the DID we can prove we 'own' the credentials. + +A VP is also sometimes used without any containted VC simply to prove that we control a given DID, say for authentication, or often for the case where when an issuer is issuing a credential to a DID, the issuer wants to know that the recipient in fact does control that DID. + +Verifying a VP amounts to verifying the signature on the VP and that the VP hasn't expired, and also verifying all of the contained VCs, one by one. + #### arguments * presentation - The W3C Verifiable Presentation to be verified. @@ -240,6 +244,8 @@ The supplied credential may not conform to the VerifiableCredential or LinkedDat #### result +With a VP we have a result for the vp as well as for all the contained VCs. + ## Install - Node.js 18+ is recommended. diff --git a/src/test-fixtures/signedUnrevokedVC1.ts b/src/test-fixtures/signedUnrevokedVC1.ts new file mode 100644 index 0000000..1782433 --- /dev/null +++ b/src/test-fixtures/signedUnrevokedVC1.ts @@ -0,0 +1,54 @@ +export const signedUnrevokedVC1 = { + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "name": "Teamwork Badge", + "issuer": { + "type": [ + "Profile" + ], + "name": "Example Corp", + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q" + }, + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "validFrom": "2010-01-01T00:00:00Z", + "credentialSubject": { + "type": [ + "AchievementSubject" + ], + "name": "Taylor Tuna", + "achievement": { + "id": "https://example.com/achievements/21st-century-skills/teamwork", + "type": [ + "Achievement" + ], + "name": "Masters", + "criteria": { + "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." + }, + "description": "This badge recognizes the development of the capacity to collaborate within a group environment." + }, + "id": "did:key:z6Mktp8yHRrcEXePJGFhUDsL7X32pwfuuV4TrpaP7dZupdwg" + }, + "id": "urn:uuid:6740bee6b9c3df2a256e144e", + "credentialStatus": { + "id": "https://testing.dcconsortium.org/status/e5WK8CbZ1GjycuPombrj#4", + "type": "BitstringStatusListEntry", + "statusPurpose": "revocation", + "statusListCredential": "https://testing.dcconsortium.org/status/e5WK8CbZ1GjycuPombrj", + "statusListIndex": "4" + }, + "proof": { + "type": "Ed25519Signature2020", + "created": "2024-11-22T17:28:35Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "z38x1N8hFFXEQgfomjv1MvP32qqtqzx4sGQAyqqfDGXqLBcw39jKBQvcwWeiVJrqtxZJmu8RZ5DPUrrAc36ejoPyE" + } + } + \ No newline at end of file diff --git a/src/test-fixtures/signedVC1.ts b/src/test-fixtures/signedVC1.ts new file mode 100644 index 0000000..9ed8d09 --- /dev/null +++ b/src/test-fixtures/signedVC1.ts @@ -0,0 +1,49 @@ +export const signedVC1 = { + '@context': [ + 'https://www.w3.org/2018/credentials/v1', + 'https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.2.json', + 'https://w3id.org/security/suites/ed25519-2020/v1' + ], + id: 'urn:uuid:2fe53dc9-b2ec-4939-9b2c-0d00f6663b6c', + type: ['VerifiableCredential', 'OpenBadgeCredential'], + name: 'DCC Test Credential', + issuer: { + type: ['Profile'], + id: 'did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q', + name: 'Digital Credentials Consortium Test Issuer', + url: 'https://dcconsortium.org', + image: + 'https://user-images.githubusercontent.com/752326/230469660-8f80d264-eccf-4edd-8e50-ea634d407778.png' + }, + issuanceDate: '2023-08-02T17:43:32.903Z', + credentialSubject: { + type: ['AchievementSubject'], + achievement: { + id: 'urn:uuid:bd6d9316-f7ae-4073-a1e5-2f7f5bd22922', + type: ['Achievement'], + achievementType: 'Diploma', + name: 'Badge', + description: + 'This is a sample credential issued by the Digital Credentials Consortium to demonstrate the functionality of Verifiable Credentials for wallets and verifiers.', + criteria: { + type: 'Criteria', + narrative: + 'This credential was issued to a student that demonstrated proficiency in the Python programming language that occurred from **February 17, 2023** to **June 12, 2023**.' + }, + image: { + id: 'https://user-images.githubusercontent.com/752326/214947713-15826a3a-b5ac-4fba-8d4a-884b60cb7157.png', + type: 'Image' + } + }, + name: 'Jane Doe' + }, + proof: { + type: 'Ed25519Signature2020', + created: '2023-10-05T11:17:41Z', + verificationMethod: + 'did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q', + proofPurpose: 'assertionMethod', + proofValue: + 'z5fk6gq9upyZvcFvJdRdeL5KmvHr69jxEkyDEd2HyQdyhk9VnDEonNSmrfLAcLEDT9j4gGdCG24WHhojVHPbRsNER' + } + } diff --git a/src/test-fixtures/vc.ts b/src/test-fixtures/vc.ts index 9a2e138..0bb460a 100644 --- a/src/test-fixtures/vc.ts +++ b/src/test-fixtures/vc.ts @@ -1,107 +1,6 @@ -import { expiredV2 } from "./expiredV2" -const signedVC1Unrevoked = { - "type": [ - "VerifiableCredential", - "OpenBadgeCredential" - ], - "name": "Teamwork Badge", - "issuer": { - "type": [ - "Profile" - ], - "name": "Example Corp", - "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q" - }, - "@context": [ - "https://www.w3.org/ns/credentials/v2", - "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", - "https://w3id.org/security/suites/ed25519-2020/v1" - ], - "validFrom": "2010-01-01T00:00:00Z", - "credentialSubject": { - "type": [ - "AchievementSubject" - ], - "name": "Taylor Tuna", - "achievement": { - "id": "https://example.com/achievements/21st-century-skills/teamwork", - "type": [ - "Achievement" - ], - "name": "Masters", - "criteria": { - "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." - }, - "description": "This badge recognizes the development of the capacity to collaborate within a group environment." - }, - "id": "did:key:z6Mktp8yHRrcEXePJGFhUDsL7X32pwfuuV4TrpaP7dZupdwg" - }, - "id": "urn:uuid:6740bee6b9c3df2a256e144e", - "credentialStatus": { - "id": "https://testing.dcconsortium.org/status/e5WK8CbZ1GjycuPombrj#4", - "type": "BitstringStatusListEntry", - "statusPurpose": "revocation", - "statusListCredential": "https://testing.dcconsortium.org/status/e5WK8CbZ1GjycuPombrj", - "statusListIndex": "4" - }, - "proof": { - "type": "Ed25519Signature2020", - "created": "2024-11-22T17:28:35Z", - "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", - "proofPurpose": "assertionMethod", - "proofValue": "z38x1N8hFFXEQgfomjv1MvP32qqtqzx4sGQAyqqfDGXqLBcw39jKBQvcwWeiVJrqtxZJmu8RZ5DPUrrAc36ejoPyE" - } -} - -const signedVC1 = { - '@context': [ - 'https://www.w3.org/2018/credentials/v1', - 'https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.2.json', - 'https://w3id.org/security/suites/ed25519-2020/v1' - ], - id: 'urn:uuid:2fe53dc9-b2ec-4939-9b2c-0d00f6663b6c', - type: ['VerifiableCredential', 'OpenBadgeCredential'], - name: 'DCC Test Credential', - issuer: { - type: ['Profile'], - id: 'did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q', - name: 'Digital Credentials Consortium Test Issuer', - url: 'https://dcconsortium.org', - image: - 'https://user-images.githubusercontent.com/752326/230469660-8f80d264-eccf-4edd-8e50-ea634d407778.png' - }, - issuanceDate: '2023-08-02T17:43:32.903Z', - credentialSubject: { - type: ['AchievementSubject'], - achievement: { - id: 'urn:uuid:bd6d9316-f7ae-4073-a1e5-2f7f5bd22922', - type: ['Achievement'], - achievementType: 'Diploma', - name: 'Badge', - description: - 'This is a sample credential issued by the Digital Credentials Consortium to demonstrate the functionality of Verifiable Credentials for wallets and verifiers.', - criteria: { - type: 'Criteria', - narrative: - 'This credential was issued to a student that demonstrated proficiency in the Python programming language that occurred from **February 17, 2023** to **June 12, 2023**.' - }, - image: { - id: 'https://user-images.githubusercontent.com/752326/214947713-15826a3a-b5ac-4fba-8d4a-884b60cb7157.png', - type: 'Image' - } - }, - name: 'Jane Doe' - }, - proof: { - type: 'Ed25519Signature2020', - created: '2023-10-05T11:17:41Z', - verificationMethod: - 'did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q', - proofPurpose: 'assertionMethod', - proofValue: - 'z5fk6gq9upyZvcFvJdRdeL5KmvHr69jxEkyDEd2HyQdyhk9VnDEonNSmrfLAcLEDT9j4gGdCG24WHhojVHPbRsNER' - } - } +import { expiredV2 } from "./expiredV2" +import { signedUnrevokedVC1 } from "./signedUnrevokedVC1" +import { signedVC1 } from "./signedVC1" const usignedVCv2 = { '@context': [ @@ -197,9 +96,8 @@ const getSignedVC1 = (): any => { } - const getSignedUnrevokedVC2 = (): any => { - return signedVC1Unrevoked + return signedUnrevokedVC1 } const getTamperedVC1 = (): any => { From e5bafe7d7f5497afc10a42ceebde9107bf77af6a Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Thu, 9 Jan 2025 07:48:20 -0500 Subject: [PATCH 06/72] reorganize test fixtures --- src/test-fixtures/vc.ts | 130 ++++-------------- .../v1/v1NoStatus.ts} | 2 +- .../v1/v1WithValidStatus.ts} | 4 +- .../v2/v2Expired.ts} | 0 4 files changed, 30 insertions(+), 106 deletions(-) rename src/test-fixtures/{signedVC1.ts => verifiableCredentials/v1/v1NoStatus.ts} (98%) rename src/test-fixtures/{signedUnrevokedVC1.ts => verifiableCredentials/v1/v1WithValidStatus.ts} (97%) rename src/test-fixtures/{expiredV2.ts => verifiableCredentials/v2/v2Expired.ts} (100%) diff --git a/src/test-fixtures/vc.ts b/src/test-fixtures/vc.ts index 0bb460a..3ef730f 100644 --- a/src/test-fixtures/vc.ts +++ b/src/test-fixtures/vc.ts @@ -1,106 +1,29 @@ -import { expiredV2 } from "./expiredV2" -import { signedUnrevokedVC1 } from "./signedUnrevokedVC1" -import { signedVC1 } from "./signedVC1" +import { v2Expired } from "./verifiableCredentials/v2/v2Expired" +import { v1WithValidStatus } from "./verifiableCredentials/v1/v1WithValidStatus" +import { v1NoStatus } from "./verifiableCredentials/v1/v1NoStatus" -const usignedVCv2 = { - '@context': [ - 'https://www.w3.org/ns/credentials/v2', - 'https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json', - 'https://w3id.org/security/suites/ed25519-2020/v1' - ], - id: 'urn:uuid:2fe53dc9-b2ec-4939-9b2c-0d00f6663b6c', - type: ['VerifiableCredential', 'OpenBadgeCredential'], - name: 'DCC Test Credential', - issuer: { - type: ['Profile'], - id: 'did:key:z6MkhVTX9BF3NGYX6cc7jWpbNnR7cAjH8LUffabZP8Qu4ysC', - name: 'Digital Credentials Consortium Test Issuer', - url: 'https://dcconsortium.org', - image: - 'https://user-images.githubusercontent.com/752326/230469660-8f80d264-eccf-4edd-8e50-ea634d407778.png' - }, - validFrom: '2023-08-02T17:43:32.903Z', - credentialSubject: { - type: ['AchievementSubject'], - achievement: { - id: 'urn:uuid:bd6d9316-f7ae-4073-a1e5-2f7f5bd22922', - type: ['Achievement'], - achievementType: 'Diploma', - name: 'Badge', - description: - 'This is a sample credential issued by the Digital Credentials Consortium to demonstrate the functionality of Verifiable Credentials for wallets and verifiers.', - criteria: { - type: 'Criteria', - narrative: - 'This credential was issued to a student that demonstrated proficiency in the Python programming language that occurred from **February 17, 2023** to **June 12, 2023**.' - }, - image: { - id: 'https://user-images.githubusercontent.com/752326/214947713-15826a3a-b5ac-4fba-8d4a-884b60cb7157.png', - type: 'Image' - } - }, - name: 'Jane Doe' - } -} -const unsignedVCv1 = { - '@context': [ - 'https://www.w3.org/2018/credentials/v1', - 'https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json', - 'https://w3id.org/vc/status-list/2021/v1', - 'https://w3id.org/security/suites/ed25519-2020/v1' - ], - id: 'urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1', - type: ['VerifiableCredential', 'OpenBadgeCredential'], - issuer: { - id: 'did:key:z6MkhVTX9BF3NGYX6cc7jWpbNnR7cAjH8LUffabZP8Qu4ysC', - type: 'Profile', - name: 'Izzy the Issuer', - description: 'Issue Issue Issue', - url: 'https://izzy.iz/', - image: { - id: 'https://upload.wikimedia.org/wikipedia/commons/a/ad/Blank_2018.png', - type: 'Image' - } - }, - issuanceDate: '2020-01-01T00:00:00Z', - name: 'Introduction to Digital Credentialing', - credentialSubject: { - type: 'AchievementSubject', - identifier: { - type: 'IdentityObject', - identityHash: 'jc.chartrand@gmail.com', - hashed: 'false' - }, - achievement: { - id: 'http://izzy.iz', - type: 'Achievement', - criteria: { - narrative: 'Completion of a credential.' - }, - description: 'Well done you!', - name: 'Introduction to Digital Credentialing' - } - } +const getVCv1 = (): any => { + return JSON.parse(JSON.stringify(v1NoStatus)) } +const getVCv2NoProof = (): any => { + // TODO + } - - - -const getUnsignedVC1 = (): any => JSON.parse(JSON.stringify(unsignedVCv1)) -const getUnsignedVCv2 = (): any => JSON.parse(JSON.stringify(usignedVCv2)) - +const getVCv1NoProof = (): any => { + const v1 = getVCv1() + delete v1.proof + return v1} const getSignedVC1 = (): any => { - return JSON.parse(JSON.stringify(signedVC1)) + return JSON.parse(JSON.stringify(v1NoStatus)) } - -const getSignedUnrevokedVC2 = (): any => { - return signedUnrevokedVC1 +const getVCv1ValidStatus = (): any => { + return v1WithValidStatus } -const getTamperedVC1 = (): any => { +const getVCv1Tampered = (): any => { const signedVC1 = getSignedVC1() signedVC1.name = 'Introduction to Tampering' return signedVC1 @@ -109,21 +32,22 @@ const getTamperedVC1 = (): any => { const getExpiredVC1 = (): any => { return null } -const getExpiredVC2 = (): any => { - return JSON.parse(JSON.stringify(expiredV2)) +const getVCv2Expired = (): any => { + return JSON.parse(JSON.stringify(v2Expired)) } -const getExpiredAndTamperedVC2 = (): any => { - const cred = getExpiredVC2() +const getVCv2ExpiredAndTampered = (): any => { + const cred = getVCv2Expired() cred.name = 'tampered!' return cred } export { - getExpiredVC2, - getExpiredAndTamperedVC2, - getTamperedVC1, - getSignedUnrevokedVC2, - getSignedVC1, - getUnsignedVC1 + getVCv2Expired, + getVCv2ExpiredAndTampered, + getVCv1Tampered, + getVCv1ValidStatus, + getVCv1, + getVCv1NoProof, + getVCv2NoProof } diff --git a/src/test-fixtures/signedVC1.ts b/src/test-fixtures/verifiableCredentials/v1/v1NoStatus.ts similarity index 98% rename from src/test-fixtures/signedVC1.ts rename to src/test-fixtures/verifiableCredentials/v1/v1NoStatus.ts index 9ed8d09..28044b5 100644 --- a/src/test-fixtures/signedVC1.ts +++ b/src/test-fixtures/verifiableCredentials/v1/v1NoStatus.ts @@ -1,4 +1,4 @@ -export const signedVC1 = { +export const v1NoStatus = { '@context': [ 'https://www.w3.org/2018/credentials/v1', 'https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.2.json', diff --git a/src/test-fixtures/signedUnrevokedVC1.ts b/src/test-fixtures/verifiableCredentials/v1/v1WithValidStatus.ts similarity index 97% rename from src/test-fixtures/signedUnrevokedVC1.ts rename to src/test-fixtures/verifiableCredentials/v1/v1WithValidStatus.ts index 1782433..7fe0769 100644 --- a/src/test-fixtures/signedUnrevokedVC1.ts +++ b/src/test-fixtures/verifiableCredentials/v1/v1WithValidStatus.ts @@ -1,4 +1,4 @@ -export const signedUnrevokedVC1 = { +export const v1WithValidStatus = { "type": [ "VerifiableCredential", "OpenBadgeCredential" @@ -50,5 +50,5 @@ export const signedUnrevokedVC1 = { "proofPurpose": "assertionMethod", "proofValue": "z38x1N8hFFXEQgfomjv1MvP32qqtqzx4sGQAyqqfDGXqLBcw39jKBQvcwWeiVJrqtxZJmu8RZ5DPUrrAc36ejoPyE" } - } +} \ No newline at end of file diff --git a/src/test-fixtures/expiredV2.ts b/src/test-fixtures/verifiableCredentials/v2/v2Expired.ts similarity index 100% rename from src/test-fixtures/expiredV2.ts rename to src/test-fixtures/verifiableCredentials/v2/v2Expired.ts From 15010021a0479258782b3c0524d0b60a4000999d Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Thu, 9 Jan 2025 13:03:36 -0500 Subject: [PATCH 07/72] add test vc combinations of revoked/expired --- src/test-fixtures/status/e5WK8CbZ1GjycuPombrj | 26 ++++++ src/test-fixtures/vc.ts | 11 ++- .../verifiableCredentials/v1/v1Expired.ts | 54 ++++++++++++ .../v1/v1ExpiredWithValidStatus.ts | 60 +++++++++++++ .../verifiableCredentials/v1/v1Revoked.ts | 59 +++++++++++++ .../v1/v1RevokedAndExpired.ts | 60 +++++++++++++ .../v1/v1WithValidStatus.ts | 85 ++++++++++--------- .../v2/v2ExpiredWithValidStatus.ts | 53 ++++++++++++ .../verifiableCredentials/v2/v2NoStatus.ts | 45 ++++++++++ .../verifiableCredentials/v2/v2Revoked.ts | 52 ++++++++++++ .../v2/v2RevokedAndExpired.ts | 53 ++++++++++++ .../v2/v2WithValidStatus.ts | 52 ++++++++++++ 12 files changed, 569 insertions(+), 41 deletions(-) create mode 100644 src/test-fixtures/status/e5WK8CbZ1GjycuPombrj create mode 100644 src/test-fixtures/verifiableCredentials/v1/v1Expired.ts create mode 100644 src/test-fixtures/verifiableCredentials/v1/v1ExpiredWithValidStatus.ts create mode 100644 src/test-fixtures/verifiableCredentials/v1/v1Revoked.ts create mode 100644 src/test-fixtures/verifiableCredentials/v1/v1RevokedAndExpired.ts create mode 100644 src/test-fixtures/verifiableCredentials/v2/v2ExpiredWithValidStatus.ts create mode 100644 src/test-fixtures/verifiableCredentials/v2/v2NoStatus.ts create mode 100644 src/test-fixtures/verifiableCredentials/v2/v2Revoked.ts create mode 100644 src/test-fixtures/verifiableCredentials/v2/v2RevokedAndExpired.ts create mode 100644 src/test-fixtures/verifiableCredentials/v2/v2WithValidStatus.ts diff --git a/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj b/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj new file mode 100644 index 0000000..d4758e5 --- /dev/null +++ b/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj @@ -0,0 +1,26 @@ +{ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "id": "https://testing.dcconsortium.org/status/e5WK8CbZ1GjycuPombrj", + "type": [ + "VerifiableCredential", + "BitstringStatusListCredential" + ], + "credentialSubject": { + "id": "https://testing.dcconsortium.org/status/e5WK8CbZ1GjycuPombrj#list", + "type": "BitstringStatusList", + "encodedList": "uH4sIAAAAAAAAA-3BMQEAAAwCoGUx6aLbwgvIHwAAAAAAAAAAAAAAwFwBZnztF9QwAAA", + "statusPurpose": "revocation" + }, + "issuer": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "validFrom": "2025-01-09T15:20:02.183Z", + "proof": { + "type": "Ed25519Signature2020", + "created": "2025-01-09T15:20:02Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "z4WFodWdHXGieqNtWYK2448A7qZdhMkxyqjVuMqifdanFYXXAqPT8xatjncxjDsXT6fskz8pC8TLBmEhnd7BC7Tqb" + } +} \ No newline at end of file diff --git a/src/test-fixtures/vc.ts b/src/test-fixtures/vc.ts index 3ef730f..a6407e8 100644 --- a/src/test-fixtures/vc.ts +++ b/src/test-fixtures/vc.ts @@ -1,6 +1,8 @@ import { v2Expired } from "./verifiableCredentials/v2/v2Expired" import { v1WithValidStatus } from "./verifiableCredentials/v1/v1WithValidStatus" import { v1NoStatus } from "./verifiableCredentials/v1/v1NoStatus" +import { v2Revoked } from "./verifiableCredentials/v2/v2Revoked" +import { v2WithValidStatus } from "./verifiableCredentials/v2/v2WithValidStatus" const getVCv1 = (): any => { return JSON.parse(JSON.stringify(v1NoStatus)) @@ -22,6 +24,9 @@ const getSignedVC1 = (): any => { const getVCv1ValidStatus = (): any => { return v1WithValidStatus } +const getVCv2ValidStatus = (): any => { + return v2WithValidStatus +} const getVCv1Tampered = (): any => { const signedVC1 = getSignedVC1() @@ -35,7 +40,9 @@ const getExpiredVC1 = (): any => { const getVCv2Expired = (): any => { return JSON.parse(JSON.stringify(v2Expired)) } - +const getVCv2Revoked = (): any => { + return JSON.parse(JSON.stringify(v2Revoked)) +} const getVCv2ExpiredAndTampered = (): any => { const cred = getVCv2Expired() cred.name = 'tampered!' @@ -44,9 +51,11 @@ const getVCv2ExpiredAndTampered = (): any => { export { getVCv2Expired, + getVCv2Revoked, getVCv2ExpiredAndTampered, getVCv1Tampered, getVCv1ValidStatus, + getVCv2ValidStatus, getVCv1, getVCv1NoProof, getVCv2NoProof diff --git a/src/test-fixtures/verifiableCredentials/v1/v1Expired.ts b/src/test-fixtures/verifiableCredentials/v1/v1Expired.ts new file mode 100644 index 0000000..0ada725 --- /dev/null +++ b/src/test-fixtures/verifiableCredentials/v1/v1Expired.ts @@ -0,0 +1,54 @@ +export const v1Expired = { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.2.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "id": "urn:uuid:2fe53dc9-b2ec-4939-9b2c-0d00f6663b6c", + "issuanceDate": "2025-01-09T15:06:31Z", + "expirationDate": "2025-01-09T16:23:24Z", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "name": "DCC Test Credential", + "issuer": { + "type": [ + "Profile" + ], + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "name": "Digital Credentials Consortium Test Issuer", + "url": "https://dcconsortium.org", + "image": "https://user-images.githubusercontent.com/752326/230469660-8f80d264-eccf-4edd-8e50-ea634d407778.png" + }, + "credentialSubject": { + "type": [ + "AchievementSubject" + ], + "achievement": { + "id": "urn:uuid:bd6d9316-f7ae-4073-a1e5-2f7f5bd22922", + "type": [ + "Achievement" + ], + "achievementType": "Diploma", + "name": "Badge", + "description": "This is a sample credential issued by the Digital Credentials Consortium to demonstrate the functionality of Verifiable Credentials for wallets and verifiers.", + "criteria": { + "type": "Criteria", + "narrative": "This credential was issued to a student that demonstrated proficiency in the Python programming language that occurred from **February 17, 2023** to **June 12, 2023**." + }, + "image": { + "id": "https://user-images.githubusercontent.com/752326/214947713-15826a3a-b5ac-4fba-8d4a-884b60cb7157.png", + "type": "Image" + } + }, + "name": "Jane Doe" + }, + "proof": { + "type": "Ed25519Signature2020", + "created": "2025-01-09T17:56:21Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "z44bwcZ2bQftiyUGKY6L7Gmg7iYfi9k6Va15osdm3KaKVnSW2DscpAMJVSs4UBf9riYReQ8VbZRf2qCY8W1rq2k3z" + } +} \ No newline at end of file diff --git a/src/test-fixtures/verifiableCredentials/v1/v1ExpiredWithValidStatus.ts b/src/test-fixtures/verifiableCredentials/v1/v1ExpiredWithValidStatus.ts new file mode 100644 index 0000000..073a8f6 --- /dev/null +++ b/src/test-fixtures/verifiableCredentials/v1/v1ExpiredWithValidStatus.ts @@ -0,0 +1,60 @@ +export const v1ExpiredWithValidStatus = { + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "name": "Taylor Tuna - Mock Bachelor of Science Degree in Biology", + "issuer": { + "url": "https://web.mit.edu/", + "type": "Profile", + "name": "Massachusetts Institute of Technology", + "image": { + "id": "https://github.com/digitalcredentials/test-files/assets/206059/01eca9f5-a508-40ac-9dd5-c12d11308894", + "type": "Image" + }, + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q" + }, + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.1.json", + "https://www.w3.org/ns/credentials/status/v1", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "credentialSubject": { + "type": [ + "AchievementSubject" + ], + "name": "Taylor Tuna", + "achievement": { + "id": "urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1", + "type": [ + "Achievement" + ], + "name": "Mock Bachelor of Science in Biology", + "criteria": { + "type": "Criteria", + "narrative": "In Recognition of proficiency in the general and the special studies and exercises prescribed by said institution for such mock degree given this day under the seal of the Institute at Cambridge in the Commonwealth of Massachusetts" + }, + "description": "Massachusetts Institute of Technology Mock Bachelor of Science in Computer Science", + "fieldOfStudy": "Biology", + "achievementType": "BachelorDegree" + } + }, + "id": "urn:uuid:677fe54fcacf98774d482bcc", + "credentialStatus": { + "id": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj#7", + "type": "BitstringStatusListEntry", + "statusPurpose": "revocation", + "statusListCredential": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj", + "statusListIndex": "7" + }, + "issuanceDate": "2025-01-09T15:06:31Z", + "expirationDate": "2025-01-09T16:23:24Z", + "proof": { + "type": "Ed25519Signature2020", + "created": "2025-01-09T18:00:03Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "z3mo89Ne4jeqGtDE86kAFBoQ2f4YzKMTvZTxi1knvYJQYxEe1Jhrk1PTXYdS4KXtdGF7ASESmmaoX4HTEdtJmrmYC" + } +} \ No newline at end of file diff --git a/src/test-fixtures/verifiableCredentials/v1/v1Revoked.ts b/src/test-fixtures/verifiableCredentials/v1/v1Revoked.ts new file mode 100644 index 0000000..38cfd12 --- /dev/null +++ b/src/test-fixtures/verifiableCredentials/v1/v1Revoked.ts @@ -0,0 +1,59 @@ +export const v1Revoked = { + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "name": "Sam Salmon - Mock Bachelor of Science Degree in Computer Science", + "issuer": { + "url": "https://web.mit.edu/", + "type": "Profile", + "name": "Massachusetts Institute of Technology", + "image": { + "id": "https://github.com/digitalcredentials/test-files/assets/206059/01eca9f5-a508-40ac-9dd5-c12d11308894", + "type": "Image" + }, + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q" + }, + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.1.json", + "https://www.w3.org/ns/credentials/status/v1", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "credentialSubject": { + "type": [ + "AchievementSubject" + ], + "name": "Sam Salmon", + "achievement": { + "id": "urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1", + "type": [ + "Achievement" + ], + "name": "Mock Bachelor of Science in Computer Science", + "criteria": { + "type": "Criteria", + "narrative": "In Recognition of proficiency in the general and the special studies and exercises prescribed by said institution for such mock degree given this day under the seal of the Institute at Cambridge in the Commonwealth of Massachusetts" + }, + "description": "Massachusetts Institute of Technology Mock Bachelor of Science in Computer Science", + "fieldOfStudy": "Computer Science", + "achievementType": "BachelorDegree" + } + }, + "id": "urn:uuid:677fe54fcacf98774d482bcb", + "credentialStatus": { + "id": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj#6", + "type": "BitstringStatusListEntry", + "statusPurpose": "revocation", + "statusListCredential": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj", + "statusListIndex": "6" + }, + "issuanceDate": "2025-01-09T15:06:31Z", + "proof": { + "type": "Ed25519Signature2020", + "created": "2025-01-09T17:47:52Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "zbc8N9nkno2w6FimK67KnN2apUPeNgEWdyCgwEtEY8HvvhxJqCs7EetgxuWadAeGKrnD6wnetMAvxWTRniGFSzPd" + } +} \ No newline at end of file diff --git a/src/test-fixtures/verifiableCredentials/v1/v1RevokedAndExpired.ts b/src/test-fixtures/verifiableCredentials/v1/v1RevokedAndExpired.ts new file mode 100644 index 0000000..98742c7 --- /dev/null +++ b/src/test-fixtures/verifiableCredentials/v1/v1RevokedAndExpired.ts @@ -0,0 +1,60 @@ +export const v1RevokedAndExpired = { + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "name": "Sam Salmon - Mock Bachelor of Science Degree in Computer Science", + "issuer": { + "url": "https://web.mit.edu/", + "type": "Profile", + "name": "Massachusetts Institute of Technology", + "image": { + "id": "https://github.com/digitalcredentials/test-files/assets/206059/01eca9f5-a508-40ac-9dd5-c12d11308894", + "type": "Image" + }, + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q" + }, + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.1.json", + "https://www.w3.org/ns/credentials/status/v1", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "credentialSubject": { + "type": [ + "AchievementSubject" + ], + "name": "Sam Salmon", + "achievement": { + "id": "urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1", + "type": [ + "Achievement" + ], + "name": "Mock Bachelor of Science in Computer Science", + "criteria": { + "type": "Criteria", + "narrative": "In Recognition of proficiency in the general and the special studies and exercises prescribed by said institution for such mock degree given this day under the seal of the Institute at Cambridge in the Commonwealth of Massachusetts" + }, + "description": "Massachusetts Institute of Technology Mock Bachelor of Science in Computer Science", + "fieldOfStudy": "Computer Science", + "achievementType": "BachelorDegree" + } + }, + "id": "urn:uuid:677fe54fcacf98774d482bcb", + "credentialStatus": { + "id": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj#6", + "type": "BitstringStatusListEntry", + "statusPurpose": "revocation", + "statusListCredential": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj", + "statusListIndex": "6" + }, + "issuanceDate": "2025-01-09T15:06:31Z", + "expirationDate": "2025-01-09T16:23:24Z", + "proof": { + "type": "Ed25519Signature2020", + "created": "2025-01-09T17:53:13Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "zCFwdjxMQpWfVtdz85WC2i44qBjHZfJ4GzZ4weHtk48VN7E292P7X1NL7EXwtuEfax8wFPjh3cdhCVAedHNb2uZM" + } +} \ No newline at end of file diff --git a/src/test-fixtures/verifiableCredentials/v1/v1WithValidStatus.ts b/src/test-fixtures/verifiableCredentials/v1/v1WithValidStatus.ts index 7fe0769..bc9ef79 100644 --- a/src/test-fixtures/verifiableCredentials/v1/v1WithValidStatus.ts +++ b/src/test-fixtures/verifiableCredentials/v1/v1WithValidStatus.ts @@ -1,54 +1,59 @@ export const v1WithValidStatus = { - "type": [ + "type": [ "VerifiableCredential", "OpenBadgeCredential" - ], - "name": "Teamwork Badge", - "issuer": { - "type": [ - "Profile" - ], - "name": "Example Corp", + ], + "name": "Taylor Tuna - Mock Bachelor of Science Degree in Biology", + "issuer": { + "url": "https://web.mit.edu/", + "type": "Profile", + "name": "Massachusetts Institute of Technology", + "image": { + "id": "https://github.com/digitalcredentials/test-files/assets/206059/01eca9f5-a508-40ac-9dd5-c12d11308894", + "type": "Image" + }, "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q" - }, - "@context": [ - "https://www.w3.org/ns/credentials/v2", - "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", + }, + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.1.json", + "https://www.w3.org/ns/credentials/status/v1", "https://w3id.org/security/suites/ed25519-2020/v1" - ], - "validFrom": "2010-01-01T00:00:00Z", - "credentialSubject": { + ], + "credentialSubject": { "type": [ - "AchievementSubject" + "AchievementSubject" ], "name": "Taylor Tuna", "achievement": { - "id": "https://example.com/achievements/21st-century-skills/teamwork", - "type": [ - "Achievement" - ], - "name": "Masters", - "criteria": { - "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." - }, - "description": "This badge recognizes the development of the capacity to collaborate within a group environment." - }, - "id": "did:key:z6Mktp8yHRrcEXePJGFhUDsL7X32pwfuuV4TrpaP7dZupdwg" - }, - "id": "urn:uuid:6740bee6b9c3df2a256e144e", - "credentialStatus": { - "id": "https://testing.dcconsortium.org/status/e5WK8CbZ1GjycuPombrj#4", + "id": "urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1", + "type": [ + "Achievement" + ], + "name": "Mock Bachelor of Science in Biology", + "criteria": { + "type": "Criteria", + "narrative": "In Recognition of proficiency in the general and the special studies and exercises prescribed by said institution for such mock degree given this day under the seal of the Institute at Cambridge in the Commonwealth of Massachusetts" + }, + "description": "Massachusetts Institute of Technology Mock Bachelor of Science in Computer Science", + "fieldOfStudy": "Biology", + "achievementType": "BachelorDegree" + } + }, + "id": "urn:uuid:677fe54fcacf98774d482bcc", + "credentialStatus": { + "id": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj#7", "type": "BitstringStatusListEntry", "statusPurpose": "revocation", - "statusListCredential": "https://testing.dcconsortium.org/status/e5WK8CbZ1GjycuPombrj", - "statusListIndex": "4" - }, - "proof": { + "statusListCredential": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj", + "statusListIndex": "7" + }, + "issuanceDate": "2025-01-09T15:15:26Z", + "proof": { "type": "Ed25519Signature2020", - "created": "2024-11-22T17:28:35Z", + "created": "2025-01-09T17:45:28Z", "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", "proofPurpose": "assertionMethod", - "proofValue": "z38x1N8hFFXEQgfomjv1MvP32qqtqzx4sGQAyqqfDGXqLBcw39jKBQvcwWeiVJrqtxZJmu8RZ5DPUrrAc36ejoPyE" - } -} - \ No newline at end of file + "proofValue": "zNq3fAUVhqHYJz1dJnw3kfMXjQK6xUTc4j2Zg8NjtVcCE5sXMiynVpCpPTK9jhUaVjZVNsc4XkDgcgsKMEUWTjU3" + } +} \ No newline at end of file diff --git a/src/test-fixtures/verifiableCredentials/v2/v2ExpiredWithValidStatus.ts b/src/test-fixtures/verifiableCredentials/v2/v2ExpiredWithValidStatus.ts new file mode 100644 index 0000000..3a43da2 --- /dev/null +++ b/src/test-fixtures/verifiableCredentials/v2/v2ExpiredWithValidStatus.ts @@ -0,0 +1,53 @@ +export const v2ExpiredWithValidStatus = { + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "name": "Teamwork Badge", + "issuer": { + "type": [ + "Profile" + ], + "name": "Example Corp", + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q" + }, + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "validFrom": "2010-01-01T00:00:00Z", + "validUntil": "2012-01-01T00:00:00Z", + "credentialSubject": { + "type": [ + "AchievementSubject" + ], + "name": "Taylor Tuna", + "achievement": { + "id": "https://example.com/achievements/21st-century-skills/teamwork", + "type": [ + "Achievement" + ], + "name": "Masters - v2 - unrevoked", + "criteria": { + "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." + }, + "description": "This badge recognizes the development of the capacity to collaborate within a group environment." + } + }, + "id": "urn:uuid:677fe8a6cacf98774d482d07", + "credentialStatus": { + "id": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj#9", + "type": "BitstringStatusListEntry", + "statusPurpose": "revocation", + "statusListCredential": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj", + "statusListIndex": "9" + }, + "proof": { + "type": "Ed25519Signature2020", + "created": "2025-01-09T18:02:02Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "z4GcediyH9xPL1nhauNuUpwoBcHb5u8y2MAMpAPG5DxbPXetT85fR4D6XJTpwSUEUmdwrhPWaDszCqTEdzw3Ke2HY" + } +} \ No newline at end of file diff --git a/src/test-fixtures/verifiableCredentials/v2/v2NoStatus.ts b/src/test-fixtures/verifiableCredentials/v2/v2NoStatus.ts new file mode 100644 index 0000000..54a9cf1 --- /dev/null +++ b/src/test-fixtures/verifiableCredentials/v2/v2NoStatus.ts @@ -0,0 +1,45 @@ +export const v2NoStatus = { + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "id": "http://example.com/credentials/3527", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "issuer": { + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "type": [ + "Profile" + ], + "name": "Example Corp" + }, + "validFrom": "2010-01-01T00:00:00Z", + "name": "Teamwork Badge", + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "type": [ + "AchievementSubject" + ], + "achievement": { + "id": "https://example.com/achievements/21st-century-skills/teamwork", + "type": [ + "Achievement" + ], + "criteria": { + "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." + }, + "description": "This badge recognizes the development of the capacity to collaborate within a group environment.", + "name": "Teamwork" + } + }, + "proof": { + "type": "Ed25519Signature2020", + "created": "2025-01-09T17:58:33Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "z62t6TYCERpTKuWCRhHc2fV7JoMhiFuEcCXGkX9iit8atQPhviN5cZeZfXRnvJWa3Bm6DjagKyrauaSJfp9C9i7q3" + } +} \ No newline at end of file diff --git a/src/test-fixtures/verifiableCredentials/v2/v2Revoked.ts b/src/test-fixtures/verifiableCredentials/v2/v2Revoked.ts new file mode 100644 index 0000000..655126f --- /dev/null +++ b/src/test-fixtures/verifiableCredentials/v2/v2Revoked.ts @@ -0,0 +1,52 @@ +export const v2Revoked = { + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "name": "Teamwork Badge", + "issuer": { + "type": [ + "Profile" + ], + "name": "Example Corp", + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q" + }, + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "validFrom": "2010-01-01T00:00:00Z", + "credentialSubject": { + "type": [ + "AchievementSubject" + ], + "name": "Sam Salmon", + "achievement": { + "id": "https://example.com/achievements/21st-century-skills/teamwork", + "type": [ + "Achievement" + ], + "name": "Bachelors - v2 - revoked", + "criteria": { + "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." + }, + "description": "This badge recognizes the development of the capacity to collaborate within a group environment." + } + }, + "id": "urn:uuid:677fe8a6cacf98774d482d06", + "credentialStatus": { + "id": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj#8", + "type": "BitstringStatusListEntry", + "statusPurpose": "revocation", + "statusListCredential": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj", + "statusListIndex": "8" + }, + "proof": { + "type": "Ed25519Signature2020", + "created": "2025-01-09T17:40:46Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "z5nD6P3KrB9mTs6wSJuP8iJ3T2G9mWZuZnJ6wkYZoBWnZ2KD3ZzFTzvvpPD4HtoKhP8Fs9uewxMwc3yfdfzCV85sr" + } +} \ No newline at end of file diff --git a/src/test-fixtures/verifiableCredentials/v2/v2RevokedAndExpired.ts b/src/test-fixtures/verifiableCredentials/v2/v2RevokedAndExpired.ts new file mode 100644 index 0000000..6a623ef --- /dev/null +++ b/src/test-fixtures/verifiableCredentials/v2/v2RevokedAndExpired.ts @@ -0,0 +1,53 @@ +export const v2RevokedAndExpired = { + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "name": "Teamwork Badge", + "issuer": { + "type": [ + "Profile" + ], + "name": "Example Corp", + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q" + }, + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "validFrom": "2010-01-01T00:00:00Z", + "validUntil": "2011-01-01T00:00:00Z", + "credentialSubject": { + "type": [ + "AchievementSubject" + ], + "name": "Sam Salmon", + "achievement": { + "id": "https://example.com/achievements/21st-century-skills/teamwork", + "type": [ + "Achievement" + ], + "name": "Bachelors - v2 - revoked", + "criteria": { + "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." + }, + "description": "This badge recognizes the development of the capacity to collaborate within a group environment." + } + }, + "id": "urn:uuid:677fe8a6cacf98774d482d06", + "credentialStatus": { + "id": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj#8", + "type": "BitstringStatusListEntry", + "statusPurpose": "revocation", + "statusListCredential": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj", + "statusListIndex": "8" + }, + "proof": { + "type": "Ed25519Signature2020", + "created": "2025-01-09T17:49:31Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "z4a51wePqfcFXExrPeQNUZ1d7xN4iH8N4gwuv4wcfdTM6EYjRgu5obWyA1W86pyhF7L3sZAXQ3QRSAHTCvUTwU1qL" + } +} \ No newline at end of file diff --git a/src/test-fixtures/verifiableCredentials/v2/v2WithValidStatus.ts b/src/test-fixtures/verifiableCredentials/v2/v2WithValidStatus.ts new file mode 100644 index 0000000..b53fdb1 --- /dev/null +++ b/src/test-fixtures/verifiableCredentials/v2/v2WithValidStatus.ts @@ -0,0 +1,52 @@ +export const v2WithValidStatus = { + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "name": "Teamwork Badge", + "issuer": { + "type": [ + "Profile" + ], + "name": "Example Corp", + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q" + }, + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "validFrom": "2010-01-01T00:00:00Z", + "credentialSubject": { + "type": [ + "AchievementSubject" + ], + "name": "Taylor Tuna", + "achievement": { + "id": "https://example.com/achievements/21st-century-skills/teamwork", + "type": [ + "Achievement" + ], + "name": "Masters - v2 - unrevoked", + "criteria": { + "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." + }, + "description": "This badge recognizes the development of the capacity to collaborate within a group environment." + } + }, + "id": "urn:uuid:677fe8a6cacf98774d482d07", + "credentialStatus": { + "id": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj#9", + "type": "BitstringStatusListEntry", + "statusPurpose": "revocation", + "statusListCredential": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj", + "statusListIndex": "9" + }, + "proof": { + "type": "Ed25519Signature2020", + "created": "2025-01-09T17:39:28Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "z5ZzyLDUsPtvXM4o7atD5n14vg8BFh4RRUppDvbWeg3e1cGWqRvchu7npkjQK4mLsxTt81Bqqqv4necWcse734Y1S" + } +} \ No newline at end of file From a59c4fab2b69f5f95009e8bb15bf769bbe4e50b9 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Fri, 10 Jan 2025 08:53:31 -0500 Subject: [PATCH 08/72] fix texts --- src/test-fixtures/vc.ts | 6 +++-- .../verifiableCredentials/v2/v2Expired.ts | 2 +- test/Verify.spec.ts | 22 +++++++++---------- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/test-fixtures/vc.ts b/src/test-fixtures/vc.ts index a6407e8..910eb4d 100644 --- a/src/test-fixtures/vc.ts +++ b/src/test-fixtures/vc.ts @@ -1,8 +1,10 @@ import { v2Expired } from "./verifiableCredentials/v2/v2Expired" -import { v1WithValidStatus } from "./verifiableCredentials/v1/v1WithValidStatus" -import { v1NoStatus } from "./verifiableCredentials/v1/v1NoStatus" import { v2Revoked } from "./verifiableCredentials/v2/v2Revoked" import { v2WithValidStatus } from "./verifiableCredentials/v2/v2WithValidStatus" +import { v2ExpiredWithValidStatus } from "./verifiableCredentials/v2/v2ExpiredWithValidStatus" + +import { v1WithValidStatus } from "./verifiableCredentials/v1/v1WithValidStatus" +import { v1NoStatus } from "./verifiableCredentials/v1/v1NoStatus" const getVCv1 = (): any => { return JSON.parse(JSON.stringify(v1NoStatus)) diff --git a/src/test-fixtures/verifiableCredentials/v2/v2Expired.ts b/src/test-fixtures/verifiableCredentials/v2/v2Expired.ts index 5e01afa..e69709e 100644 --- a/src/test-fixtures/verifiableCredentials/v2/v2Expired.ts +++ b/src/test-fixtures/verifiableCredentials/v2/v2Expired.ts @@ -1,4 +1,4 @@ -export const expiredV2 = { +export const v2Expired = { "@context": [ "https://www.w3.org/ns/credentials/v2", "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", diff --git a/test/Verify.spec.ts b/test/Verify.spec.ts index 91d908b..9a36b88 100644 --- a/test/Verify.spec.ts +++ b/test/Verify.spec.ts @@ -1,33 +1,33 @@ //import { expect } from 'chai' import { strict as assert } from 'assert'; import { verifyCredential } from '../src/Verify' -import { getSignedUnrevokedVC2, getTamperedVC1, getExpiredVC2 } from '../src/test-fixtures/vc' +import { getVCv2Expired, getVCv1Tampered, getVCv2ValidStatus } from '../src/test-fixtures/vc' describe('Verify', () => { - it.only('verifies with valid status', async () => { + it('verifies with valid status', async () => { //const signedVC : any = getSignedVC() - const signedVC2Unrevoked : any = getSignedUnrevokedVC2() + const signedVC2Unrevoked : any = getVCv2ValidStatus() const result = await verifyCredential({credential: signedVC2Unrevoked, reloadIssuerRegistry: true}) - console.log("result returned from verifyCredential call:") - console.log(JSON.stringify(result,null,2)) + // console.log("result returned from verifyCredential call:") + //console.log(JSON.stringify(result,null,2)) assert.ok(result.verified); //expect(result.verified).to.be.true }) it('returns fatal error when tampered', async () => { - const tamperedVC1 : any = getTamperedVC1() + const tamperedVC1 : any = getVCv1Tampered() const result = await verifyCredential({credential: tamperedVC1, reloadIssuerRegistry: true}) - console.log("result returned from verifyCredential call:") - console.log(JSON.stringify(result,null,2)) + // console.log("result returned from verifyCredential call:") + // console.log(JSON.stringify(result,null,2)) assert.ok(result.verified === false); //expect(result.verified).to.be.true }) it('returns unverified when expired', async () => { - const expiredVC2 : any = getExpiredVC2() + const expiredVC2 : any = getVCv2Expired() const result = await verifyCredential({credential: expiredVC2, reloadIssuerRegistry: true}) - console.log("result returned from verifyCredential call:") - console.log(JSON.stringify(result,null,2)) + //console.log("result returned from verifyCredential call:") + // console.log(JSON.stringify(result,null,2)) assert.ok(result.verified === false); assert.ok(result.log); //expect(result.verified).to.be.true From 711d7b12dc11b79dd7e285ecca9d2488a35d85d6 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Fri, 10 Jan 2025 09:09:14 -0500 Subject: [PATCH 09/72] make knownRegistryList an argument of verifyCredential --- .knownDidRegistries.ts | 2 +- src/Verify.ts | 4 ++-- src/issuerRegistries.ts | 13 +++++++------ test/Verify.spec.ts | 15 ++++++++------- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/.knownDidRegistries.ts b/.knownDidRegistries.ts index ed44cb4..6840b02 100644 --- a/.knownDidRegistries.ts +++ b/.knownDidRegistries.ts @@ -1,4 +1,4 @@ -export const knownDidRegistries = [ +export const knownDIDRegistries = [ { name: 'DCC Pilot Registry', url: 'https://digitalcredentials.github.io/issuer-registry/registry.json' diff --git a/src/Verify.ts b/src/Verify.ts index 4c2b791..7f003c1 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -23,7 +23,7 @@ const suite = new Ed25519Signature2020(); -export async function verifyCredential({credential, reloadIssuerRegistry = true}:{credential: Credential, reloadIssuerRegistry: boolean}): Promise { +export async function verifyCredential({credential, knownDIDRegistries, reloadIssuerRegistry = true}:{credential: Credential, knownDIDRegistries: object, reloadIssuerRegistry: boolean}): Promise { const fatalErrorMessage = checkForFatalErrors(credential) @@ -58,7 +58,7 @@ export async function verifyCredential({credential, reloadIssuerRegistry = true} delete verificationResponse.statusResult const { issuer } = credential - await addTrustedIssuersToVerificationResponse({verificationResponse, reloadIssuerRegistry, issuer}) + await addTrustedIssuersToVerificationResponse({verificationResponse, knownDIDRegistries,reloadIssuerRegistry, issuer}) return verificationResponse; diff --git a/src/issuerRegistries.ts b/src/issuerRegistries.ts index 953c86c..c3f2463 100644 --- a/src/issuerRegistries.ts +++ b/src/issuerRegistries.ts @@ -1,5 +1,4 @@ import {RegistryClient} from '@digitalcredentials/issuer-registry-client'; -import {knownDidRegistries} from '../.knownDidRegistries' import { VerificationResponse } from './types/result'; const registries = new RegistryClient() const registryNotYetLoaded = true; @@ -9,13 +8,14 @@ const registryNotYetLoaded = true; * @returns A list of the names of the DID registries in which the issuer appears. */ -export async function getTrustedRegistryListForIssuer({ issuer, reloadIssuerRegistry = false }: { +export async function getTrustedRegistryListForIssuer({ issuer, knownDIDRegistries, reloadIssuerRegistry = false }: { issuer: string | any, + knownDIDRegistries: object, reloadIssuerRegistry: boolean | null }): Promise { if (reloadIssuerRegistry ?? registryNotYetLoaded) { - await registries.load({ config: knownDidRegistries }) + await registries.load({ config: knownDIDRegistries }) } const issuerDid = typeof issuer === 'string' ? issuer : issuer.id; const issuerInfo = registries.didEntry(issuerDid); @@ -26,13 +26,14 @@ export async function getTrustedRegistryListForIssuer({ issuer, reloadIssuerRegi : null; } -export async function addTrustedIssuersToVerificationResponse( {issuer, reloadIssuerRegistry = false, verificationResponse} :{ +export async function addTrustedIssuersToVerificationResponse( {issuer, knownDIDRegistries, reloadIssuerRegistry = false, verificationResponse} :{ issuer: string | any, - reloadIssuerRegistry: boolean | null + reloadIssuerRegistry: boolean | null, + knownDIDRegistries: object, verificationResponse: VerificationResponse }) : Promise { - const foundInRegistries = await getTrustedRegistryListForIssuer( {issuer, reloadIssuerRegistry}); + const foundInRegistries = await getTrustedRegistryListForIssuer( {issuer, knownDIDRegistries, reloadIssuerRegistry}); const registryStep = { "id": "registered_issuer", diff --git a/test/Verify.spec.ts b/test/Verify.spec.ts index 9a36b88..5b5634a 100644 --- a/test/Verify.spec.ts +++ b/test/Verify.spec.ts @@ -1,22 +1,23 @@ -//import { expect } from 'chai' +import { expect } from 'chai' import { strict as assert } from 'assert'; import { verifyCredential } from '../src/Verify' import { getVCv2Expired, getVCv1Tampered, getVCv2ValidStatus } from '../src/test-fixtures/vc' +import { knownDIDRegistries } from '../.knownDidRegistries'; describe('Verify', () => { it('verifies with valid status', async () => { //const signedVC : any = getSignedVC() - const signedVC2Unrevoked : any = getVCv2ValidStatus() - const result = await verifyCredential({credential: signedVC2Unrevoked, reloadIssuerRegistry: true}) + const credential : any = getVCv2ValidStatus() + const result = await verifyCredential({credential, reloadIssuerRegistry: true, knownDIDRegistries}) // console.log("result returned from verifyCredential call:") //console.log(JSON.stringify(result,null,2)) - assert.ok(result.verified); - //expect(result.verified).to.be.true + //assert.ok(result.verified); + expect(result.verified).to.be.true }) it('returns fatal error when tampered', async () => { const tamperedVC1 : any = getVCv1Tampered() - const result = await verifyCredential({credential: tamperedVC1, reloadIssuerRegistry: true}) + const result = await verifyCredential({credential: tamperedVC1, reloadIssuerRegistry: true, knownDIDRegistries}) // console.log("result returned from verifyCredential call:") // console.log(JSON.stringify(result,null,2)) assert.ok(result.verified === false); @@ -25,7 +26,7 @@ describe('Verify', () => { it('returns unverified when expired', async () => { const expiredVC2 : any = getVCv2Expired() - const result = await verifyCredential({credential: expiredVC2, reloadIssuerRegistry: true}) + const result = await verifyCredential({credential: expiredVC2, reloadIssuerRegistry: true, knownDIDRegistries}) //console.log("result returned from verifyCredential call:") // console.log(JSON.stringify(result,null,2)) assert.ok(result.verified === false); From 1f4c5f76e15f6ee29340ea6cf747a9ecd60ee55a Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Fri, 10 Jan 2025 10:11:52 -0500 Subject: [PATCH 10/72] add credential to result --- src/Verify.ts | 1 + test/Verify.spec.ts | 7 +++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Verify.ts b/src/Verify.ts index 7f003c1..f0ec33b 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -40,6 +40,7 @@ export async function verifyCredential({credential, knownDIDRegistries, reloadIs }); verificationResponse.isFatal = false + verificationResponse.credential = credential if (verificationResponse.error) { if (verificationResponse.error.log) { diff --git a/test/Verify.spec.ts b/test/Verify.spec.ts index 5b5634a..0664139 100644 --- a/test/Verify.spec.ts +++ b/test/Verify.spec.ts @@ -6,13 +6,12 @@ import { knownDIDRegistries } from '../.knownDidRegistries'; describe('Verify', () => { it('verifies with valid status', async () => { - //const signedVC : any = getSignedVC() const credential : any = getVCv2ValidStatus() const result = await verifyCredential({credential, reloadIssuerRegistry: true, knownDIDRegistries}) - // console.log("result returned from verifyCredential call:") - //console.log(JSON.stringify(result,null,2)) - //assert.ok(result.verified); + console.log("result returned from verifyCredential call:") + console.log(JSON.stringify(result,null,2)) expect(result.verified).to.be.true + expect(result.credential).to.eql(credential) }) it('returns fatal error when tampered', async () => { From 0b47854b0f36b9a3d69936aaff7e935d384a76ec Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Fri, 10 Jan 2025 10:41:58 -0500 Subject: [PATCH 11/72] add deep equal test on positive result --- package.json | 2 ++ test/Verify.spec.ts | 41 +++++++++++++++++++++++++++++++++++++++-- tsconfig.spec.json | 1 + 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 23b8de3..0f13533 100644 --- a/package.json +++ b/package.json @@ -35,12 +35,14 @@ }, "devDependencies": { "@types/chai": "^4.3.4", + "@types/deep-equal-in-any-order": "^1.0.4", "@types/mocha": "^10.0.1", "@types/node": "^18.11.17", "@typescript-eslint/eslint-plugin": "^5.46.1", "@typescript-eslint/parser": "^5.46.1", "chai": "^4.3.7", "cross-env": "^7.0.3", + "deep-equal-in-any-order": "^2.0.6", "eslint": "^8.30.0", "eslint-config-prettier": "^8.5.0", "eslint-config-standard-with-typescript": "^24.0.0", diff --git a/test/Verify.spec.ts b/test/Verify.spec.ts index 0664139..d6ad68c 100644 --- a/test/Verify.spec.ts +++ b/test/Verify.spec.ts @@ -1,17 +1,54 @@ -import { expect } from 'chai' +import chai from 'chai' +import deepEqualInAnyOrder from 'deep-equal-in-any-order' import { strict as assert } from 'assert'; import { verifyCredential } from '../src/Verify' import { getVCv2Expired, getVCv1Tampered, getVCv2ValidStatus } from '../src/test-fixtures/vc' import { knownDIDRegistries } from '../.knownDidRegistries'; +chai.use(deepEqualInAnyOrder); +const {expect} = chai; + +const expectedResult = { + "verified": true, + "log": [ + { + "id": "valid_signature", + "valid": true + }, + { + "id": "issuer_did_resolves", + "valid": true + }, + { + "id": "expiration", + "valid": true + }, + { + "id": "revocation_status", + "valid": true + }, + { + "id": "registered_issuer", + "valid": true, + "foundInRegistries": [ + "DCC Sandbox Registry" + ] + } + ], + credential: null, + "isFatal": false +} + describe('Verify', () => { - it('verifies with valid status', async () => { + it.only('verifies with valid status', async () => { const credential : any = getVCv2ValidStatus() + expectedResult.credential = credential; const result = await verifyCredential({credential, reloadIssuerRegistry: true, knownDIDRegistries}) console.log("result returned from verifyCredential call:") console.log(JSON.stringify(result,null,2)) expect(result.verified).to.be.true expect(result.credential).to.eql(credential) + expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define }) it('returns fatal error when tampered', async () => { diff --git a/tsconfig.spec.json b/tsconfig.spec.json index e947de9..dfd8525 100644 --- a/tsconfig.spec.json +++ b/tsconfig.spec.json @@ -5,6 +5,7 @@ "lib": ["es2022", "dom"], "module": "commonjs", "moduleResolution": "node", + "esModuleInterop": true, "outDir": "dist/esm", "noImplicitAny": true, "removeComments": false, From 1ca9abf3c0180736efddbe1be28bbe45b480eda2 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Fri, 10 Jan 2025 11:09:38 -0500 Subject: [PATCH 12/72] add and reorganize tests --- src/Verify.ts | 4 ++-- src/test-fixtures/vc.ts | 24 +++++++++++++++----- test/Verify.spec.ts | 50 +++++++++++++++++++++++++++-------------- 3 files changed, 53 insertions(+), 25 deletions(-) diff --git a/src/Verify.ts b/src/Verify.ts index f0ec33b..8f54330 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -52,8 +52,8 @@ export async function verifyCredential({credential, knownDIDRegistries, reloadIs return buildFatalErrorObject(fatalErrorMessage, "invalidSignature", credential, stackTrace) } } - console.log('results from the verify call:') - console.log(JSON.stringify(verificationResponse)) + // console.log('results from the verify call:') + // console.log(JSON.stringify(verificationResponse)) delete verificationResponse.results delete verificationResponse.statusResult diff --git a/src/test-fixtures/vc.ts b/src/test-fixtures/vc.ts index 910eb4d..045339e 100644 --- a/src/test-fixtures/vc.ts +++ b/src/test-fixtures/vc.ts @@ -5,11 +5,16 @@ import { v2ExpiredWithValidStatus } from "./verifiableCredentials/v2/v2ExpiredWi import { v1WithValidStatus } from "./verifiableCredentials/v1/v1WithValidStatus" import { v1NoStatus } from "./verifiableCredentials/v1/v1NoStatus" +import { v2NoStatus } from "./verifiableCredentials/v2/v2NoStatus" const getVCv1 = (): any => { return JSON.parse(JSON.stringify(v1NoStatus)) } +const getVCv2 = (): any => { + return JSON.parse(JSON.stringify(v2NoStatus)) +} + const getVCv2NoProof = (): any => { // TODO } @@ -19,9 +24,7 @@ const getVCv1NoProof = (): any => { delete v1.proof return v1} -const getSignedVC1 = (): any => { - return JSON.parse(JSON.stringify(v1NoStatus)) -} + const getVCv1ValidStatus = (): any => { return v1WithValidStatus @@ -31,7 +34,7 @@ const getVCv2ValidStatus = (): any => { } const getVCv1Tampered = (): any => { - const signedVC1 = getSignedVC1() + const signedVC1 = getVCv1() signedVC1.name = 'Introduction to Tampering' return signedVC1 } @@ -51,14 +54,23 @@ const getVCv2ExpiredAndTampered = (): any => { return cred } +const getVCv2Tampered = (): any => { + const cred = getVCv2() + cred.name = 'tampered!' + return cred +} + export { getVCv2Expired, getVCv2Revoked, + getVCv2Tampered, getVCv2ExpiredAndTampered, + getVCv2NoProof, + getVCv2, + getVCv1Tampered, getVCv1ValidStatus, getVCv2ValidStatus, getVCv1, - getVCv1NoProof, - getVCv2NoProof + getVCv1NoProof } diff --git a/test/Verify.spec.ts b/test/Verify.spec.ts index d6ad68c..5bb564f 100644 --- a/test/Verify.spec.ts +++ b/test/Verify.spec.ts @@ -2,7 +2,7 @@ import chai from 'chai' import deepEqualInAnyOrder from 'deep-equal-in-any-order' import { strict as assert } from 'assert'; import { verifyCredential } from '../src/Verify' -import { getVCv2Expired, getVCv1Tampered, getVCv2ValidStatus } from '../src/test-fixtures/vc' +import { getVCv2Expired, getVCv1Tampered, getVCv2ValidStatus, getVCv2Tampered } from '../src/test-fixtures/vc' import { knownDIDRegistries } from '../.knownDidRegistries'; chai.use(deepEqualInAnyOrder); @@ -10,6 +10,8 @@ const {expect} = chai; const expectedResult = { "verified": true, + "credential": null, + "isFatal": false, "log": [ { "id": "valid_signature", @@ -34,32 +36,45 @@ const expectedResult = { "DCC Sandbox Registry" ] } - ], - credential: null, - "isFatal": false + ] } describe('Verify', () => { - it.only('verifies with valid status', async () => { + describe('.verifyCredential', () => { + describe('with VC version 1', () => { + describe('returns fatal error', () => { + it('when tampered with', async () => { + const tamperedVC1 : any = getVCv1Tampered() + const result = await verifyCredential({credential: tamperedVC1, reloadIssuerRegistry: true, knownDIDRegistries}) + assert.ok(result.verified === false); + }) + }) + }) + describe('with VC version 2', () => { + + describe('returns fatal error', () => { + it('when tampered with', async () => { + const tamperedVC2 : any = getVCv2Tampered() + const result = await verifyCredential({credential: tamperedVC2, reloadIssuerRegistry: true, knownDIDRegistries}) + // console.log("result returned from verifyCredential call:") + // console.log(JSON.stringify(result,null,2)) + assert.ok(result.verified === false); + //expect(result.verified).to.be.true + }) + }) + + + it('verifies with valid status', async () => { const credential : any = getVCv2ValidStatus() expectedResult.credential = credential; const result = await verifyCredential({credential, reloadIssuerRegistry: true, knownDIDRegistries}) - console.log("result returned from verifyCredential call:") - console.log(JSON.stringify(result,null,2)) + //console.log("result returned from verifyCredential call:") + //console.log(JSON.stringify(result,null,2)) expect(result.verified).to.be.true expect(result.credential).to.eql(credential) expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define }) - it('returns fatal error when tampered', async () => { - const tamperedVC1 : any = getVCv1Tampered() - const result = await verifyCredential({credential: tamperedVC1, reloadIssuerRegistry: true, knownDIDRegistries}) - // console.log("result returned from verifyCredential call:") - // console.log(JSON.stringify(result,null,2)) - assert.ok(result.verified === false); - //expect(result.verified).to.be.true - }) - it('returns unverified when expired', async () => { const expiredVC2 : any = getVCv2Expired() const result = await verifyCredential({credential: expiredVC2, reloadIssuerRegistry: true, knownDIDRegistries}) @@ -69,5 +84,6 @@ describe('Verify', () => { assert.ok(result.log); //expect(result.verified).to.be.true }) - + }) +}) }) From 9b3777eddf1292c46657e9cc6592503c4b59b367 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Sun, 12 Jan 2025 13:40:55 -0500 Subject: [PATCH 13/72] switch testing to run after tsc compile --- package.json | 17 ++++----- src/Verify.ts | 14 +++---- src/issuerRegistries.ts | 2 +- src/test-fixtures/vc.ts | 22 ++++++----- test/Verify.spec.ts | 82 +++++++++++++++++++++++++---------------- tsconfig.spec.json | 6 +-- 6 files changed, 80 insertions(+), 63 deletions(-) diff --git a/package.json b/package.json index 0f13533..2bd014a 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "version": "0.0.1", "scripts": { "build": "npm run clear && tsc -d && tsc -p tsconfig.esm.json", + "build-test": "npm run clear && tsc -p tsconfig.spec.json", "clear": "rimraf dist/*", "lint": "eslint .", "lint:fix": "eslint --fix .", @@ -12,7 +13,8 @@ "rebuild": "npm run clear && npm run build", "test": "npm run lint && npm run test-node", "test-karma": "karma start karma.conf.js", - "test-node": "cross-env NODE_ENV=test TS_NODE_PROJECT=tsconfig.spec.json TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\" }' mocha -r ts-node/register --project tsconfig.spec.json 'test/*.ts'" + "test-node-old": "cross-env NODE_ENV=test TS_NODE_PROJECT=tsconfig.spec.json TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\" }' mocha -r ts-node/register --project tsconfig.spec.json 'test/*.ts'", + "test-node": "npm run build-test && mocha dist/esm/test/*.spec.js && rm -rf dist/esm/test || true" }, "files": [ "dist", @@ -61,23 +63,17 @@ "prettier": "^2.8.1", "rimraf": "^3.0.2", "ts-node": "^10.9.1", + "tsx": "^4.19.2", "typescript": "^4.9.4" }, "publishConfig": { "access": "public" }, - "mocha": { - "require": "ts-node/register", - "extension": [ - "ts" - ], - "spec": "test/**/*.ts" - }, "keywords": [ "dcc" ], "engines": { - "node": ">=16.0" + "node": ">=18.0" }, "author": { "name": "Digital Credentials Consortium", @@ -89,5 +85,6 @@ "url": "https://github.com/digitalcredentials/isomorphic-lib-template" }, "homepage": "https://github.com/digitalcredentials/isomorphic-lib-template", - "bugs": "https://github.com/digitalcredentials/isomorphic-lib-template/issues" + "bugs": "https://github.com/digitalcredentials/isomorphic-lib-template/issues", + "type": "module" } diff --git a/src/Verify.ts b/src/Verify.ts index 8f54330..b713557 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -2,20 +2,20 @@ import { Ed25519Signature2020 } from '@digitalcredentials/ed25519-signature-2020'; import * as vc from '@digitalcredentials/vc'; import { securityLoader } from '@digitalcredentials/security-document-loader'; -import { getCredentialStatusChecker } from './credentialStatus'; -import { addTrustedIssuersToVerificationResponse } from './issuerRegistries'; +import { getCredentialStatusChecker } from './credentialStatus.js'; +import { addTrustedIssuersToVerificationResponse } from './issuerRegistries.js'; + +import { Credential } from './types/credential.js'; +import { VerificationResponse } from './types/result.js'; -import { Credential } from './types/credential'; -import { VerificationResponse } from './types/result'; -/* // the new eddsa-rdfc-2022-cryptosuite import {DataIntegrityProof} from '@digitalbazaar/data-integrity'; import {cryptosuite as eddsaRdfc2022CryptoSuite} from '@digitalbazaar/eddsa-rdfc-2022-cryptosuite'; -const suite = new DataIntegrityProof({ +const eddsaSuite = new DataIntegrityProof({ cryptosuite: eddsaRdfc2022CryptoSuite }); -*/ + const documentLoader = securityLoader({ fetchRemoteContexts: true }).build(); const suite = new Ed25519Signature2020(); diff --git a/src/issuerRegistries.ts b/src/issuerRegistries.ts index c3f2463..4ee1999 100644 --- a/src/issuerRegistries.ts +++ b/src/issuerRegistries.ts @@ -1,5 +1,5 @@ import {RegistryClient} from '@digitalcredentials/issuer-registry-client'; -import { VerificationResponse } from './types/result'; +import { VerificationResponse } from './types/result.js'; const registries = new RegistryClient() const registryNotYetLoaded = true; /** diff --git a/src/test-fixtures/vc.ts b/src/test-fixtures/vc.ts index 045339e..72b5ca2 100644 --- a/src/test-fixtures/vc.ts +++ b/src/test-fixtures/vc.ts @@ -1,11 +1,12 @@ -import { v2Expired } from "./verifiableCredentials/v2/v2Expired" -import { v2Revoked } from "./verifiableCredentials/v2/v2Revoked" -import { v2WithValidStatus } from "./verifiableCredentials/v2/v2WithValidStatus" -import { v2ExpiredWithValidStatus } from "./verifiableCredentials/v2/v2ExpiredWithValidStatus" +import { v2NoStatus } from "./verifiableCredentials/v2/v2NoStatus.js" +import { v2Expired } from "./verifiableCredentials/v2/v2Expired.js" +import { v2Revoked } from "./verifiableCredentials/v2/v2Revoked.js" +import { v2WithValidStatus } from "./verifiableCredentials/v2/v2WithValidStatus.js" +import { v2ExpiredWithValidStatus } from "./verifiableCredentials/v2/v2ExpiredWithValidStatus.js" -import { v1WithValidStatus } from "./verifiableCredentials/v1/v1WithValidStatus" -import { v1NoStatus } from "./verifiableCredentials/v1/v1NoStatus" -import { v2NoStatus } from "./verifiableCredentials/v2/v2NoStatus" +import { v1WithValidStatus } from "./verifiableCredentials/v1/v1WithValidStatus.js" +import { v1NoStatus } from "./verifiableCredentials/v1/v1NoStatus.js" +import { v1Expired } from "./verifiableCredentials/v1/v1Expired.js" const getVCv1 = (): any => { return JSON.parse(JSON.stringify(v1NoStatus)) @@ -39,8 +40,8 @@ const getVCv1Tampered = (): any => { return signedVC1 } -const getExpiredVC1 = (): any => { - return null +const getVCv1Expired = (): any => { + return JSON.parse(JSON.stringify(v1Expired)) } const getVCv2Expired = (): any => { return JSON.parse(JSON.stringify(v2Expired)) @@ -64,13 +65,14 @@ export { getVCv2Expired, getVCv2Revoked, getVCv2Tampered, + getVCv2ValidStatus, getVCv2ExpiredAndTampered, getVCv2NoProof, getVCv2, + getVCv1Expired, getVCv1Tampered, getVCv1ValidStatus, - getVCv2ValidStatus, getVCv1, getVCv1NoProof } diff --git a/test/Verify.spec.ts b/test/Verify.spec.ts index 5bb564f..393790b 100644 --- a/test/Verify.spec.ts +++ b/test/Verify.spec.ts @@ -1,9 +1,9 @@ import chai from 'chai' import deepEqualInAnyOrder from 'deep-equal-in-any-order' import { strict as assert } from 'assert'; -import { verifyCredential } from '../src/Verify' -import { getVCv2Expired, getVCv1Tampered, getVCv2ValidStatus, getVCv2Tampered } from '../src/test-fixtures/vc' -import { knownDIDRegistries } from '../.knownDidRegistries'; +import { verifyCredential } from '../src/Verify.js' +import { getVCv2Expired, getVCv1Tampered, getVCv1Expired, getVCv1ValidStatus, getVCv2ValidStatus, getVCv2Tampered } from '../src/test-fixtures/vc.js' +import { knownDIDRegistries } from '../.knownDidRegistries.js'; chai.use(deepEqualInAnyOrder); const {expect} = chai; @@ -41,49 +41,67 @@ const expectedResult = { describe('Verify', () => { describe('.verifyCredential', () => { + describe('with VC version 1', () => { + describe('returns fatal error', () => { - it('when tampered with', async () => { - const tamperedVC1 : any = getVCv1Tampered() - const result = await verifyCredential({credential: tamperedVC1, reloadIssuerRegistry: true, knownDIDRegistries}) - assert.ok(result.verified === false); - }) + + it('when tampered with', async () => { + const tamperedVC1 : any = getVCv1Tampered() + const result = await verifyCredential({credential: tamperedVC1, reloadIssuerRegistry: true, knownDIDRegistries}) + assert.ok(result.verified === false); + }) + }) + + describe('returns as verified', () => { + it('when status is valid', async () => { + const credential : any = getVCv1ValidStatus() + expectedResult.credential = credential; + const result = await verifyCredential({credential, reloadIssuerRegistry: true, knownDIDRegistries}) + expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define + }) + }) + + describe('returns as unverified', () => { + it('when expired', async () => { + const credential : any = getVCv1Expired() + const result = await verifyCredential({credential, reloadIssuerRegistry: true, knownDIDRegistries}) + assert.ok(result.verified === false); + assert.ok(result.log); + }) + }) + + }) + describe('with VC version 2', () => { describe('returns fatal error', () => { it('when tampered with', async () => { const tamperedVC2 : any = getVCv2Tampered() const result = await verifyCredential({credential: tamperedVC2, reloadIssuerRegistry: true, knownDIDRegistries}) - // console.log("result returned from verifyCredential call:") - // console.log(JSON.stringify(result,null,2)) assert.ok(result.verified === false); - //expect(result.verified).to.be.true }) - }) - + }) - it('verifies with valid status', async () => { - const credential : any = getVCv2ValidStatus() - expectedResult.credential = credential; - const result = await verifyCredential({credential, reloadIssuerRegistry: true, knownDIDRegistries}) - //console.log("result returned from verifyCredential call:") - //console.log(JSON.stringify(result,null,2)) - expect(result.verified).to.be.true - expect(result.credential).to.eql(credential) - expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define - }) + describe('returns as verified', () => { + it('when status is valid', async () => { + const credential : any = getVCv2ValidStatus() + expectedResult.credential = credential; + const result = await verifyCredential({credential, reloadIssuerRegistry: true, knownDIDRegistries}) + expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define + }) + }) - it('returns unverified when expired', async () => { - const expiredVC2 : any = getVCv2Expired() - const result = await verifyCredential({credential: expiredVC2, reloadIssuerRegistry: true, knownDIDRegistries}) - //console.log("result returned from verifyCredential call:") - // console.log(JSON.stringify(result,null,2)) - assert.ok(result.verified === false); - assert.ok(result.log); - //expect(result.verified).to.be.true - }) + describe('returns as unverified', () => { + it('when expired', async () => { + const credential : any = getVCv2Expired() + const result = await verifyCredential({credential, reloadIssuerRegistry: true, knownDIDRegistries}) + assert.ok(result.verified === false); + assert.ok(result.log); + }) + }) }) }) }) diff --git a/tsconfig.spec.json b/tsconfig.spec.json index dfd8525..389b0c1 100644 --- a/tsconfig.spec.json +++ b/tsconfig.spec.json @@ -3,9 +3,8 @@ "strict": true, "target": "es2022", "lib": ["es2022", "dom"], - "module": "commonjs", + "module": "es2022", "moduleResolution": "node", - "esModuleInterop": true, "outDir": "dist/esm", "noImplicitAny": true, "removeComments": false, @@ -20,8 +19,9 @@ }, "include": [ "src/**/*", + "test/**/*", ".eslintrc.js", "karma.conf.js" ], - "exclude": ["node_modules", "dist", "test"] + "exclude": ["node_modules", "dist"] } From 9aff7723fae6dfa37d0ef06f6997c948b7e86ac7 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Sun, 12 Jan 2025 17:38:48 -0500 Subject: [PATCH 14/72] fix tests --- src/Verify.ts | 4 +-- src/test-fixtures/expectedResults.ts | 52 +++++++++++++++++++++++++++ src/test-fixtures/vc.ts | 6 ++++ test/Verify.spec.ts | 53 +++++++--------------------- 4 files changed, 73 insertions(+), 42 deletions(-) create mode 100644 src/test-fixtures/expectedResults.ts diff --git a/src/Verify.ts b/src/Verify.ts index b713557..10f3464 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -21,8 +21,6 @@ const documentLoader = securityLoader({ fetchRemoteContexts: true }).build(); const suite = new Ed25519Signature2020(); - - export async function verifyCredential({credential, knownDIDRegistries, reloadIssuerRegistry = true}:{credential: Credential, knownDIDRegistries: object, reloadIssuerRegistry: boolean}): Promise { @@ -39,6 +37,7 @@ export async function verifyCredential({credential, knownDIDRegistries, reloadIs checkStatus: getCredentialStatusChecker(credential) }); + verificationResponse.isFatal = false verificationResponse.credential = credential @@ -57,6 +56,7 @@ export async function verifyCredential({credential, knownDIDRegistries, reloadIs delete verificationResponse.results delete verificationResponse.statusResult + delete verificationResponse.verified const { issuer } = credential await addTrustedIssuersToVerificationResponse({verificationResponse, knownDIDRegistries,reloadIssuerRegistry, issuer}) diff --git a/src/test-fixtures/expectedResults.ts b/src/test-fixtures/expectedResults.ts new file mode 100644 index 0000000..6d84e9d --- /dev/null +++ b/src/test-fixtures/expectedResults.ts @@ -0,0 +1,52 @@ +import { VerificationResponse, VerificationStep } from "src/types/result"; + +const expectedResult = { + "credential": {}, + "isFatal": false, + "log": [ + { + "id": "valid_signature", + "valid": true + }, + { + "id": "issuer_did_resolves", + "valid": true + }, + { + "id": "expiration", + "valid": true + }, + { + "id": "registered_issuer", + "valid": true, + "foundInRegistries": [ + "DCC Sandbox Registry" + ] + } + ] + } + + const getCopyOfExpectedResult = (credential:object, withStatus: boolean) : VerificationResponse => { + const expectedResultCopy = JSON.parse(JSON.stringify(expectedResult)) + if (withStatus) { + expectedResultCopy.log?.push( + { + "id": "revocation_status", + "valid": true + } + ) + } + expectedResultCopy.credential = credential; + return expectedResultCopy; + } + + export const getExpectedVerifiedResult = ({credential, withStatus }: {credential:object, withStatus:boolean}) : VerificationResponse => { + return getCopyOfExpectedResult(credential, withStatus); + } + + export const getExpectedUnverifiedResult = ( {credential, unVerifiedStep, withStatus }: {credential:object, unVerifiedStep:string, withStatus:boolean}) : VerificationResponse => { + const expectedResult = getCopyOfExpectedResult(credential, withStatus); + const step = expectedResult.log?.find((entry:VerificationStep)=>entry.id === unVerifiedStep) + if (step) step.valid = false; + return expectedResult; + } \ No newline at end of file diff --git a/src/test-fixtures/vc.ts b/src/test-fixtures/vc.ts index 72b5ca2..3afab68 100644 --- a/src/test-fixtures/vc.ts +++ b/src/test-fixtures/vc.ts @@ -6,6 +6,7 @@ import { v2ExpiredWithValidStatus } from "./verifiableCredentials/v2/v2ExpiredWi import { v1WithValidStatus } from "./verifiableCredentials/v1/v1WithValidStatus.js" import { v1NoStatus } from "./verifiableCredentials/v1/v1NoStatus.js" +import { v1Revoked } from "./verifiableCredentials/v1/v1Revoked.js" import { v1Expired } from "./verifiableCredentials/v1/v1Expired.js" const getVCv1 = (): any => { @@ -49,6 +50,10 @@ const getVCv2Expired = (): any => { const getVCv2Revoked = (): any => { return JSON.parse(JSON.stringify(v2Revoked)) } +const getVCv1Revoked = (): any => { + return JSON.parse(JSON.stringify(v1Revoked)) +} + const getVCv2ExpiredAndTampered = (): any => { const cred = getVCv2Expired() cred.name = 'tampered!' @@ -71,6 +76,7 @@ export { getVCv2, getVCv1Expired, + getVCv1Revoked, getVCv1Tampered, getVCv1ValidStatus, getVCv1, diff --git a/test/Verify.spec.ts b/test/Verify.spec.ts index 393790b..637b260 100644 --- a/test/Verify.spec.ts +++ b/test/Verify.spec.ts @@ -2,62 +2,31 @@ import chai from 'chai' import deepEqualInAnyOrder from 'deep-equal-in-any-order' import { strict as assert } from 'assert'; import { verifyCredential } from '../src/Verify.js' -import { getVCv2Expired, getVCv1Tampered, getVCv1Expired, getVCv1ValidStatus, getVCv2ValidStatus, getVCv2Tampered } from '../src/test-fixtures/vc.js' +import { getVCv2Expired, getVCv1Tampered, getVCv1Expired, getVCv1Revoked, getVCv1ValidStatus, getVCv2ValidStatus, getVCv2Tampered } from '../src/test-fixtures/vc.js' import { knownDIDRegistries } from '../.knownDidRegistries.js'; +import { getExpectedVerifiedResult, getExpectedUnverifiedResult } from '../src/test-fixtures/expectedResults.js'; chai.use(deepEqualInAnyOrder); const {expect} = chai; -const expectedResult = { - "verified": true, - "credential": null, - "isFatal": false, - "log": [ - { - "id": "valid_signature", - "valid": true - }, - { - "id": "issuer_did_resolves", - "valid": true - }, - { - "id": "expiration", - "valid": true - }, - { - "id": "revocation_status", - "valid": true - }, - { - "id": "registered_issuer", - "valid": true, - "foundInRegistries": [ - "DCC Sandbox Registry" - ] - } - ] -} - describe('Verify', () => { + describe('.verifyCredential', () => { describe('with VC version 1', () => { describe('returns fatal error', () => { - it('when tampered with', async () => { const tamperedVC1 : any = getVCv1Tampered() const result = await verifyCredential({credential: tamperedVC1, reloadIssuerRegistry: true, knownDIDRegistries}) assert.ok(result.verified === false); }) - }) describe('returns as verified', () => { it('when status is valid', async () => { const credential : any = getVCv1ValidStatus() - expectedResult.credential = credential; + const expectedResult = getExpectedVerifiedResult({credential, withStatus: true}) const result = await verifyCredential({credential, reloadIssuerRegistry: true, knownDIDRegistries}) expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define }) @@ -67,7 +36,11 @@ describe('Verify', () => { it('when expired', async () => { const credential : any = getVCv1Expired() const result = await verifyCredential({credential, reloadIssuerRegistry: true, knownDIDRegistries}) - assert.ok(result.verified === false); + assert.ok(result.log); + }) + it('when revoked', async () => { + const credential : any = getVCv1Revoked() + const result = await verifyCredential({credential, reloadIssuerRegistry: true, knownDIDRegistries}) assert.ok(result.log); }) }) @@ -81,14 +54,13 @@ describe('Verify', () => { it('when tampered with', async () => { const tamperedVC2 : any = getVCv2Tampered() const result = await verifyCredential({credential: tamperedVC2, reloadIssuerRegistry: true, knownDIDRegistries}) - assert.ok(result.verified === false); }) }) describe('returns as verified', () => { it('when status is valid', async () => { const credential : any = getVCv2ValidStatus() - expectedResult.credential = credential; + const expectedResult = getExpectedVerifiedResult({credential, withStatus: true}) const result = await verifyCredential({credential, reloadIssuerRegistry: true, knownDIDRegistries}) expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define }) @@ -97,11 +69,12 @@ describe('Verify', () => { describe('returns as unverified', () => { it('when expired', async () => { const credential : any = getVCv2Expired() + const expectedResult = getExpectedUnverifiedResult({credential, unVerifiedStep: 'expiration', withStatus:false}) const result = await verifyCredential({credential, reloadIssuerRegistry: true, knownDIDRegistries}) - assert.ok(result.verified === false); - assert.ok(result.log); + expect(result).to.deep.equalInAnyOrder(expectedResult) }) }) }) }) }) + From 0bb989accd917592d0a5e61b38af96278856af3e Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Mon, 13 Jan 2025 09:20:29 -0500 Subject: [PATCH 15/72] replace nullish coalescing with logical or for registry check --- src/issuerRegistries.ts | 2 +- test/Verify.spec.ts | 21 +++++++++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/issuerRegistries.ts b/src/issuerRegistries.ts index 4ee1999..6704917 100644 --- a/src/issuerRegistries.ts +++ b/src/issuerRegistries.ts @@ -14,7 +14,7 @@ export async function getTrustedRegistryListForIssuer({ issuer, knownDIDRegistri reloadIssuerRegistry: boolean | null }): Promise { - if (reloadIssuerRegistry ?? registryNotYetLoaded) { + if (reloadIssuerRegistry || registryNotYetLoaded) { await registries.load({ config: knownDIDRegistries }) } const issuerDid = typeof issuer === 'string' ? issuer : issuer.id; diff --git a/test/Verify.spec.ts b/test/Verify.spec.ts index 637b260..8b8d7cc 100644 --- a/test/Verify.spec.ts +++ b/test/Verify.spec.ts @@ -2,7 +2,7 @@ import chai from 'chai' import deepEqualInAnyOrder from 'deep-equal-in-any-order' import { strict as assert } from 'assert'; import { verifyCredential } from '../src/Verify.js' -import { getVCv2Expired, getVCv1Tampered, getVCv1Expired, getVCv1Revoked, getVCv1ValidStatus, getVCv2ValidStatus, getVCv2Tampered } from '../src/test-fixtures/vc.js' +import { getVCv2Expired, getVCv1Tampered, getVCv1Expired, getVCv1Revoked, getVCv2Revoked, getVCv1ValidStatus, getVCv2ValidStatus, getVCv2Tampered } from '../src/test-fixtures/vc.js' import { knownDIDRegistries } from '../.knownDidRegistries.js'; import { getExpectedVerifiedResult, getExpectedUnverifiedResult } from '../src/test-fixtures/expectedResults.js'; @@ -18,7 +18,7 @@ describe('Verify', () => { describe('returns fatal error', () => { it('when tampered with', async () => { const tamperedVC1 : any = getVCv1Tampered() - const result = await verifyCredential({credential: tamperedVC1, reloadIssuerRegistry: true, knownDIDRegistries}) + const result = await verifyCredential({credential: tamperedVC1, reloadIssuerRegistry: false, knownDIDRegistries}) assert.ok(result.verified === false); }) }) @@ -27,7 +27,7 @@ describe('Verify', () => { it('when status is valid', async () => { const credential : any = getVCv1ValidStatus() const expectedResult = getExpectedVerifiedResult({credential, withStatus: true}) - const result = await verifyCredential({credential, reloadIssuerRegistry: true, knownDIDRegistries}) + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define }) }) @@ -35,12 +35,12 @@ describe('Verify', () => { describe('returns as unverified', () => { it('when expired', async () => { const credential : any = getVCv1Expired() - const result = await verifyCredential({credential, reloadIssuerRegistry: true, knownDIDRegistries}) + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) assert.ok(result.log); }) it('when revoked', async () => { const credential : any = getVCv1Revoked() - const result = await verifyCredential({credential, reloadIssuerRegistry: true, knownDIDRegistries}) + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) assert.ok(result.log); }) }) @@ -53,7 +53,7 @@ describe('Verify', () => { describe('returns fatal error', () => { it('when tampered with', async () => { const tamperedVC2 : any = getVCv2Tampered() - const result = await verifyCredential({credential: tamperedVC2, reloadIssuerRegistry: true, knownDIDRegistries}) + const result = await verifyCredential({credential: tamperedVC2, reloadIssuerRegistry: false, knownDIDRegistries}) }) }) @@ -61,7 +61,7 @@ describe('Verify', () => { it('when status is valid', async () => { const credential : any = getVCv2ValidStatus() const expectedResult = getExpectedVerifiedResult({credential, withStatus: true}) - const result = await verifyCredential({credential, reloadIssuerRegistry: true, knownDIDRegistries}) + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define }) }) @@ -70,9 +70,14 @@ describe('Verify', () => { it('when expired', async () => { const credential : any = getVCv2Expired() const expectedResult = getExpectedUnverifiedResult({credential, unVerifiedStep: 'expiration', withStatus:false}) - const result = await verifyCredential({credential, reloadIssuerRegistry: true, knownDIDRegistries}) + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) expect(result).to.deep.equalInAnyOrder(expectedResult) }) + it('when revoked', async () => { + const credential : any = getVCv2Revoked() + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + assert.ok(result.log); + }) }) }) }) From d53e7f144b7374141920e5f493194408afb25c6d Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Mon, 13 Jan 2025 10:17:11 -0500 Subject: [PATCH 16/72] test for no proof --- src/Verify.ts | 44 ++++++++++++++++------------ src/test-fixtures/expectedResults.ts | 36 +++++++++++++++++++++-- src/test-fixtures/vc.ts | 19 ++++++++---- test/Verify.spec.ts | 36 +++++++++++++++++++---- 4 files changed, 103 insertions(+), 32 deletions(-) diff --git a/src/Verify.ts b/src/Verify.ts index 10f3464..24158f8 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -8,7 +8,6 @@ import { addTrustedIssuersToVerificationResponse } from './issuerRegistries.js'; import { Credential } from './types/credential.js'; import { VerificationResponse } from './types/result.js'; - // the new eddsa-rdfc-2022-cryptosuite import {DataIntegrityProof} from '@digitalbazaar/data-integrity'; import {cryptosuite as eddsaRdfc2022CryptoSuite} from '@digitalbazaar/eddsa-rdfc-2022-cryptosuite'; @@ -16,18 +15,16 @@ const eddsaSuite = new DataIntegrityProof({ cryptosuite: eddsaRdfc2022CryptoSuite }); - const documentLoader = securityLoader({ fetchRemoteContexts: true }).build(); const suite = new Ed25519Signature2020(); - export async function verifyCredential({credential, knownDIDRegistries, reloadIssuerRegistry = true}:{credential: Credential, knownDIDRegistries: object, reloadIssuerRegistry: boolean}): Promise { - - const fatalErrorMessage = checkForFatalErrors(credential) - if (fatalErrorMessage) { - return buildFatalErrorObject(fatalErrorMessage, "fatalError", credential, null) +const fatalError = checkForFatalErrors(credential) + + if (fatalError) { + return fatalError } const verificationResponse = await vc.verifyCredential({ @@ -37,7 +34,11 @@ export async function verifyCredential({credential, knownDIDRegistries, reloadIs checkStatus: getCredentialStatusChecker(credential) }); - + // remove things we don't need in the result or that are duplicated elsewhere + delete verificationResponse.results + delete verificationResponse.statusResult + delete verificationResponse.verified + verificationResponse.isFatal = false verificationResponse.credential = credential @@ -51,36 +52,41 @@ export async function verifyCredential({credential, knownDIDRegistries, reloadIs return buildFatalErrorObject(fatalErrorMessage, "invalidSignature", credential, stackTrace) } } - // console.log('results from the verify call:') - // console.log(JSON.stringify(verificationResponse)) - delete verificationResponse.results - delete verificationResponse.statusResult - delete verificationResponse.verified - const { issuer } = credential await addTrustedIssuersToVerificationResponse({verificationResponse, knownDIDRegistries,reloadIssuerRegistry, issuer}) - return verificationResponse; } function buildFatalErrorObject(fatalErrorMessage: string, name: string, credential: Credential, stackTrace: string | null) : VerificationResponse { - return {credential, isFatal: true, verified: false, errors: [{name, message: fatalErrorMessage, isFatal: true, ...stackTrace?{stackTrace}:null}]} + return {credential, isFatal: true, errors: [{name, message: fatalErrorMessage, isFatal: true, ...stackTrace?{stackTrace}:null}]} } -function checkForFatalErrors(credential: Credential) : string | null { +function checkForFatalErrors(credential: Credential) : VerificationResponse | null { + + /* if (!credential.doesn't have context with vc) { + } + */ + try { // eslint-disable-next-line no-new new URL(credential.id as string); } catch (e) { - return "The credential's id uses an invalid format. It may have been issued as part of an early pilot. Please contact the issuer to get a replacement." + const fatalErrorMessage = "The credential's id uses an invalid format. It may have been issued as part of an early pilot. Please contact the issuer to get a replacement." + const name = 'invalid_issuer_id' + return buildFatalErrorObject(fatalErrorMessage, name, credential, null) } if (!credential.proof) { - return 'This is not a Verifiable Credential - it does not have a digital signature.' + const fatalErrorMessage = 'This is not a Verifiable Credential - it does not have a digital signature.' + const name = 'no_proof' + return buildFatalErrorObject(fatalErrorMessage, name, credential,null) } + + + return null } diff --git a/src/test-fixtures/expectedResults.ts b/src/test-fixtures/expectedResults.ts index 6d84e9d..d46b96a 100644 --- a/src/test-fixtures/expectedResults.ts +++ b/src/test-fixtures/expectedResults.ts @@ -1,3 +1,4 @@ +import { error } from "console"; import { VerificationResponse, VerificationStep } from "src/types/result"; const expectedResult = { @@ -26,6 +27,26 @@ const expectedResult = { ] } + const fatalResult = { + credential: {}, + isFatal: true, + errors: [ + { + name: 'error name goes here, e.g., no_proof', + message: 'error message goes here', + isFatal: true + } + ] + } + + const getCopyOfFatalResult = (credential:object, errorName:string, errorMessage:string) : VerificationResponse => { + const expectedResultCopy = JSON.parse(JSON.stringify(fatalResult)) + expectedResultCopy.credential = credential; + expectedResultCopy.errors[0].name = errorName; + expectedResultCopy.errors[0].message = errorMessage + return expectedResultCopy; + } + const getCopyOfExpectedResult = (credential:object, withStatus: boolean) : VerificationResponse => { const expectedResultCopy = JSON.parse(JSON.stringify(expectedResult)) if (withStatus) { @@ -40,13 +61,24 @@ const expectedResult = { return expectedResultCopy; } - export const getExpectedVerifiedResult = ({credential, withStatus }: {credential:object, withStatus:boolean}) : VerificationResponse => { + const getExpectedVerifiedResult = ({credential, withStatus }: {credential:object, withStatus:boolean}) : VerificationResponse => { return getCopyOfExpectedResult(credential, withStatus); } - export const getExpectedUnverifiedResult = ( {credential, unVerifiedStep, withStatus }: {credential:object, unVerifiedStep:string, withStatus:boolean}) : VerificationResponse => { + const getExpectedUnverifiedResult = ( {credential, unVerifiedStep, withStatus }: {credential:object, unVerifiedStep:string, withStatus:boolean}) : VerificationResponse => { const expectedResult = getCopyOfExpectedResult(credential, withStatus); const step = expectedResult.log?.find((entry:VerificationStep)=>entry.id === unVerifiedStep) if (step) step.valid = false; return expectedResult; + } + + const getExpectedFatalResult = ( {credential, errorName, errorMessage }: {credential:object, errorName:string, errorMessage:string}) : VerificationResponse => { + const expectedResult = getCopyOfFatalResult(credential, errorName, errorMessage); + return expectedResult; + } + + export { + getExpectedVerifiedResult, + getExpectedUnverifiedResult, + getExpectedFatalResult } \ No newline at end of file diff --git a/src/test-fixtures/vc.ts b/src/test-fixtures/vc.ts index 3afab68..00d0e9a 100644 --- a/src/test-fixtures/vc.ts +++ b/src/test-fixtures/vc.ts @@ -18,13 +18,16 @@ const getVCv2 = (): any => { } const getVCv2NoProof = (): any => { - // TODO + const v2 = getVCv2() + delete v2.proof + return v2 } const getVCv1NoProof = (): any => { const v1 = getVCv1() delete v1.proof - return v1} + return v1 +} @@ -59,7 +62,11 @@ const getVCv2ExpiredAndTampered = (): any => { cred.name = 'tampered!' return cred } - +const getVCv1ExpiredAndTampered = (): any => { + const cred = getVCv1Expired() + cred.name = 'tampered!' + return cred +} const getVCv2Tampered = (): any => { const cred = getVCv2() cred.name = 'tampered!' @@ -67,18 +74,20 @@ const getVCv2Tampered = (): any => { } export { + getVCv2, getVCv2Expired, getVCv2Revoked, getVCv2Tampered, getVCv2ValidStatus, getVCv2ExpiredAndTampered, getVCv2NoProof, - getVCv2, + + getVCv1, getVCv1Expired, getVCv1Revoked, getVCv1Tampered, getVCv1ValidStatus, - getVCv1, + getVCv1ExpiredAndTampered, getVCv1NoProof } diff --git a/test/Verify.spec.ts b/test/Verify.spec.ts index 8b8d7cc..ea28aa2 100644 --- a/test/Verify.spec.ts +++ b/test/Verify.spec.ts @@ -2,9 +2,9 @@ import chai from 'chai' import deepEqualInAnyOrder from 'deep-equal-in-any-order' import { strict as assert } from 'assert'; import { verifyCredential } from '../src/Verify.js' -import { getVCv2Expired, getVCv1Tampered, getVCv1Expired, getVCv1Revoked, getVCv2Revoked, getVCv1ValidStatus, getVCv2ValidStatus, getVCv2Tampered } from '../src/test-fixtures/vc.js' +import { getVCv2Expired, getVCv1Tampered, getVCv1Expired, getVCv1Revoked, getVCv2Revoked, getVCv1ValidStatus, getVCv2ValidStatus, getVCv2Tampered, getVCv1NoProof, getVCv2NoProof } from '../src/test-fixtures/vc.js' import { knownDIDRegistries } from '../.knownDidRegistries.js'; -import { getExpectedVerifiedResult, getExpectedUnverifiedResult } from '../src/test-fixtures/expectedResults.js'; +import { getExpectedVerifiedResult, getExpectedUnverifiedResult, getExpectedFatalResult } from '../src/test-fixtures/expectedResults.js'; chai.use(deepEqualInAnyOrder); const {expect} = chai; @@ -17,9 +17,20 @@ describe('Verify', () => { describe('returns fatal error', () => { it('when tampered with', async () => { - const tamperedVC1 : any = getVCv1Tampered() - const result = await verifyCredential({credential: tamperedVC1, reloadIssuerRegistry: false, knownDIDRegistries}) - assert.ok(result.verified === false); + const credential : any = getVCv1Tampered() + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + assert.ok(result.isFatal === true); + }) + it.only('when no proof', async () => { + const credential : any = getVCv1NoProof() + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + + const expectedResult = getExpectedFatalResult({ + credential, + errorMessage: 'This is not a Verifiable Credential - it does not have a digital signature.', + errorName: 'no_proof' + }) + expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define }) }) @@ -54,7 +65,20 @@ describe('Verify', () => { it('when tampered with', async () => { const tamperedVC2 : any = getVCv2Tampered() const result = await verifyCredential({credential: tamperedVC2, reloadIssuerRegistry: false, knownDIDRegistries}) - }) + // TODO add check here + }) + + it.only('when no proof', async () => { + const credential : any = getVCv2NoProof() + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + + const expectedResult = getExpectedFatalResult({ + credential, + errorMessage: 'This is not a Verifiable Credential - it does not have a digital signature.', + errorName: 'no_proof' + }) + expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define + }) }) describe('returns as verified', () => { From f3135f9aa41f4b31684320554c910f51284866da Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Mon, 13 Jan 2025 10:38:22 -0500 Subject: [PATCH 17/72] fix invalid signature tests --- src/Verify.ts | 2 +- test/Verify.spec.ts | 22 ++++++++++++++++------ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/Verify.ts b/src/Verify.ts index 24158f8..004349f 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -49,7 +49,7 @@ const fatalError = checkForFatalErrors(credential) } else if (verificationResponse?.error?.name === 'VerificationError') { const fatalErrorMessage = 'The signature is not valid.' const stackTrace = verificationResponse?.error?.errors?.stack - return buildFatalErrorObject(fatalErrorMessage, "invalidSignature", credential, stackTrace) + return buildFatalErrorObject(fatalErrorMessage, "invalid_signature", credential, stackTrace) } } diff --git a/test/Verify.spec.ts b/test/Verify.spec.ts index ea28aa2..f7b44ba 100644 --- a/test/Verify.spec.ts +++ b/test/Verify.spec.ts @@ -19,9 +19,14 @@ describe('Verify', () => { it('when tampered with', async () => { const credential : any = getVCv1Tampered() const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) - assert.ok(result.isFatal === true); + const expectedResult = getExpectedFatalResult({ + credential, + errorMessage: 'The signature is not valid.', + errorName: 'invalid_signature' + }) + expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define }) - it.only('when no proof', async () => { + it('when no proof', async () => { const credential : any = getVCv1NoProof() const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) @@ -63,12 +68,17 @@ describe('Verify', () => { describe('returns fatal error', () => { it('when tampered with', async () => { - const tamperedVC2 : any = getVCv2Tampered() - const result = await verifyCredential({credential: tamperedVC2, reloadIssuerRegistry: false, knownDIDRegistries}) - // TODO add check here + const credential : any = getVCv2Tampered() + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + const expectedResult = getExpectedFatalResult({ + credential, + errorMessage: 'The signature is not valid.', + errorName: 'invalid_signature' + }) + expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define }) - it.only('when no proof', async () => { + it('when no proof', async () => { const credential : any = getVCv2NoProof() const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) From bf671877e5077c91ad1156e52bfc957d43cff86d Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Mon, 13 Jan 2025 14:40:11 -0500 Subject: [PATCH 18/72] add test for jsonld context --- src/Verify.ts | 19 ++++++++++++++++--- src/test-fixtures/vc.ts | 8 ++++++++ test/Verify.spec.ts | 22 +++++++++++++++++++--- 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/Verify.ts b/src/Verify.ts index 004349f..b297637 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -64,11 +64,24 @@ function buildFatalErrorObject(fatalErrorMessage: string, name: string, credenti } function checkForFatalErrors(credential: Credential) : VerificationResponse | null { + const validVCContexts = [ + 'https://www.w3.org/2018/credentials/v1', + 'https://www.w3.org/ns/credentials/v2' + ] + const suppliedContexts = credential['@context'] + + if (!suppliedContexts) { + const fatalErrorMessage = "The credential does not appear to be a valid jsonld document - there is no context." + const name = 'invalid_jsonld' + return buildFatalErrorObject(fatalErrorMessage, name, credential, null) + } - /* if (!credential.doesn't have context with vc) { + if (! validVCContexts.some(contextURI => suppliedContexts.includes(contextURI))) { + const fatalErrorMessage = "The credential doesn't have a verifiable credential context." + const name = 'no_vc_context' + return buildFatalErrorObject(fatalErrorMessage, name, credential, null) } - */ - + try { // eslint-disable-next-line no-new new URL(credential.id as string); diff --git a/src/test-fixtures/vc.ts b/src/test-fixtures/vc.ts index 00d0e9a..d23f62b 100644 --- a/src/test-fixtures/vc.ts +++ b/src/test-fixtures/vc.ts @@ -73,7 +73,15 @@ const getVCv2Tampered = (): any => { return cred } +const getCredentialWithoutContext = (): any => { + const cred = getVCv2() + delete cred['@context'] + return cred +} + export { + + getCredentialWithoutContext, getVCv2, getVCv2Expired, getVCv2Revoked, diff --git a/test/Verify.spec.ts b/test/Verify.spec.ts index f7b44ba..e668d4d 100644 --- a/test/Verify.spec.ts +++ b/test/Verify.spec.ts @@ -2,7 +2,7 @@ import chai from 'chai' import deepEqualInAnyOrder from 'deep-equal-in-any-order' import { strict as assert } from 'assert'; import { verifyCredential } from '../src/Verify.js' -import { getVCv2Expired, getVCv1Tampered, getVCv1Expired, getVCv1Revoked, getVCv2Revoked, getVCv1ValidStatus, getVCv2ValidStatus, getVCv2Tampered, getVCv1NoProof, getVCv2NoProof } from '../src/test-fixtures/vc.js' +import { getVCv2Expired, getVCv1Tampered, getVCv1Expired, getVCv1Revoked, getVCv2Revoked, getVCv1ValidStatus, getVCv2ValidStatus, getVCv2Tampered, getVCv1NoProof, getVCv2NoProof, getCredentialWithoutContext } from '../src/test-fixtures/vc.js' import { knownDIDRegistries } from '../.knownDidRegistries.js'; import { getExpectedVerifiedResult, getExpectedUnverifiedResult, getExpectedFatalResult } from '../src/test-fixtures/expectedResults.js'; @@ -13,6 +13,20 @@ describe('Verify', () => { describe('.verifyCredential', () => { + describe('general fatal errors', () => { + it('when not jsonld', async () => { + const credential : any = getCredentialWithoutContext() + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + + const expectedResult = getExpectedFatalResult({ + credential, + errorMessage: 'The credential does not appear to be a valid jsonld document - there is no context.', + errorName: 'invalid_jsonld' + }) + expect(result).to.deep.equalInAnyOrder(expectedResult) + }) + }) + describe('with VC version 1', () => { describe('returns fatal error', () => { @@ -75,7 +89,7 @@ describe('Verify', () => { errorMessage: 'The signature is not valid.', errorName: 'invalid_signature' }) - expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define + expect(result).to.deep.equalInAnyOrder(expectedResult) }) it('when no proof', async () => { @@ -87,8 +101,10 @@ describe('Verify', () => { errorMessage: 'This is not a Verifiable Credential - it does not have a digital signature.', errorName: 'no_proof' }) - expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define + expect(result).to.deep.equalInAnyOrder(expectedResult) }) + + }) describe('returns as verified', () => { From 588adbece15ae0b65beb18daa0ad4a77658dbb2f Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Mon, 13 Jan 2025 14:56:43 -0500 Subject: [PATCH 19/72] add test for missing vc context --- src/test-fixtures/vc.ts | 8 ++++++++ test/Verify.spec.ts | 17 +++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/test-fixtures/vc.ts b/src/test-fixtures/vc.ts index d23f62b..ad60c7b 100644 --- a/src/test-fixtures/vc.ts +++ b/src/test-fixtures/vc.ts @@ -79,9 +79,17 @@ const getCredentialWithoutContext = (): any => { return cred } +const getCredentialWithoutVCContext = (): any => { + const cred = getVCv2() + cred['@context'] = cred['@context'].filter((context : string) => context !== 'https://www.w3.org/ns/credentials/v2') // remove the vc context + return cred +} + export { getCredentialWithoutContext, + getCredentialWithoutVCContext, + getVCv2, getVCv2Expired, getVCv2Revoked, diff --git a/test/Verify.spec.ts b/test/Verify.spec.ts index e668d4d..80e05ca 100644 --- a/test/Verify.spec.ts +++ b/test/Verify.spec.ts @@ -2,7 +2,7 @@ import chai from 'chai' import deepEqualInAnyOrder from 'deep-equal-in-any-order' import { strict as assert } from 'assert'; import { verifyCredential } from '../src/Verify.js' -import { getVCv2Expired, getVCv1Tampered, getVCv1Expired, getVCv1Revoked, getVCv2Revoked, getVCv1ValidStatus, getVCv2ValidStatus, getVCv2Tampered, getVCv1NoProof, getVCv2NoProof, getCredentialWithoutContext } from '../src/test-fixtures/vc.js' +import { getVCv2Expired, getVCv1Tampered, getVCv1Expired, getVCv1Revoked, getVCv2Revoked, getVCv1ValidStatus, getVCv2ValidStatus, getVCv2Tampered, getVCv1NoProof, getVCv2NoProof, getCredentialWithoutContext, getCredentialWithoutVCContext } from '../src/test-fixtures/vc.js' import { knownDIDRegistries } from '../.knownDidRegistries.js'; import { getExpectedVerifiedResult, getExpectedUnverifiedResult, getExpectedFatalResult } from '../src/test-fixtures/expectedResults.js'; @@ -13,7 +13,7 @@ describe('Verify', () => { describe('.verifyCredential', () => { - describe('general fatal errors', () => { + describe('returns general fatal errors', () => { it('when not jsonld', async () => { const credential : any = getCredentialWithoutContext() const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) @@ -25,6 +25,19 @@ describe('Verify', () => { }) expect(result).to.deep.equalInAnyOrder(expectedResult) }) + + it('when no vc context', async () => { + const credential : any = getCredentialWithoutVCContext() + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + + const expectedResult = getExpectedFatalResult({ + credential, + errorMessage: "The credential doesn't have a verifiable credential context.", + errorName: 'no_vc_context' + }) + expect(result).to.deep.equalInAnyOrder(expectedResult) + }) + }) describe('with VC version 1', () => { From 3ab368976771a34dd6cc80bf17c813527215829a Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Mon, 13 Jan 2025 17:12:37 -0500 Subject: [PATCH 20/72] add bad id test --- src/Verify.ts | 2 +- src/test-fixtures/vc.ts | 7 +++++++ test/Verify.spec.ts | 15 ++++++++++++++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/Verify.ts b/src/Verify.ts index b297637..fe2edb8 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -87,7 +87,7 @@ function checkForFatalErrors(credential: Credential) : VerificationResponse | nu new URL(credential.id as string); } catch (e) { const fatalErrorMessage = "The credential's id uses an invalid format. It may have been issued as part of an early pilot. Please contact the issuer to get a replacement." - const name = 'invalid_issuer_id' + const name = 'invalid_credential_id' return buildFatalErrorObject(fatalErrorMessage, name, credential, null) } diff --git a/src/test-fixtures/vc.ts b/src/test-fixtures/vc.ts index ad60c7b..fb9ff85 100644 --- a/src/test-fixtures/vc.ts +++ b/src/test-fixtures/vc.ts @@ -85,10 +85,17 @@ const getCredentialWithoutVCContext = (): any => { return cred } +const getCredentialWithNonURIId = (): any => { + const cred = getVCv2() + cred.id = "0923lksjf" + return cred +} + export { getCredentialWithoutContext, getCredentialWithoutVCContext, + getCredentialWithNonURIId, getVCv2, getVCv2Expired, diff --git a/test/Verify.spec.ts b/test/Verify.spec.ts index 80e05ca..95a6150 100644 --- a/test/Verify.spec.ts +++ b/test/Verify.spec.ts @@ -2,7 +2,7 @@ import chai from 'chai' import deepEqualInAnyOrder from 'deep-equal-in-any-order' import { strict as assert } from 'assert'; import { verifyCredential } from '../src/Verify.js' -import { getVCv2Expired, getVCv1Tampered, getVCv1Expired, getVCv1Revoked, getVCv2Revoked, getVCv1ValidStatus, getVCv2ValidStatus, getVCv2Tampered, getVCv1NoProof, getVCv2NoProof, getCredentialWithoutContext, getCredentialWithoutVCContext } from '../src/test-fixtures/vc.js' +import { getVCv2Expired, getVCv1Tampered, getVCv1Expired, getVCv1Revoked, getVCv2Revoked, getVCv1ValidStatus, getVCv2ValidStatus, getVCv2Tampered, getVCv1NoProof, getVCv2NoProof, getCredentialWithoutContext, getCredentialWithoutVCContext, getCredentialWithNonURIId } from '../src/test-fixtures/vc.js' import { knownDIDRegistries } from '../.knownDidRegistries.js'; import { getExpectedVerifiedResult, getExpectedUnverifiedResult, getExpectedFatalResult } from '../src/test-fixtures/expectedResults.js'; @@ -38,6 +38,18 @@ describe('Verify', () => { expect(result).to.deep.equalInAnyOrder(expectedResult) }) + it('when credential id is not a uri', async () => { + const credential : any = getCredentialWithNonURIId() + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + + const expectedResult = getExpectedFatalResult({ + credential, + errorMessage: "The credential's id uses an invalid format. It may have been issued as part of an early pilot. Please contact the issuer to get a replacement.", + errorName: 'invalid_credential_id' + }) + expect(result).to.deep.equalInAnyOrder(expectedResult) + }) + }) describe('with VC version 1', () => { @@ -64,6 +76,7 @@ describe('Verify', () => { }) expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define }) + }) describe('returns as verified', () => { From a8d48665a5f9dcaf4c13aaafad85e7b91a5b4208 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Mon, 13 Jan 2025 19:14:14 -0500 Subject: [PATCH 21/72] add test for simultaneous expired and tampered --- src/test-fixtures/vc.ts | 25 ++++++++-- test/Verify.spec.ts | 100 +++++++++++++++++++++++++++++++++------- 2 files changed, 105 insertions(+), 20 deletions(-) diff --git a/src/test-fixtures/vc.ts b/src/test-fixtures/vc.ts index fb9ff85..119c5fc 100644 --- a/src/test-fixtures/vc.ts +++ b/src/test-fixtures/vc.ts @@ -8,6 +8,7 @@ import { v1WithValidStatus } from "./verifiableCredentials/v1/v1WithValidStatus. import { v1NoStatus } from "./verifiableCredentials/v1/v1NoStatus.js" import { v1Revoked } from "./verifiableCredentials/v1/v1Revoked.js" import { v1Expired } from "./verifiableCredentials/v1/v1Expired.js" +import { v1ExpiredWithValidStatus } from "./verifiableCredentials/v1/v1ExpiredWithValidStatus.js" const getVCv1 = (): any => { return JSON.parse(JSON.stringify(v1NoStatus)) @@ -85,17 +86,30 @@ const getCredentialWithoutVCContext = (): any => { return cred } -const getCredentialWithNonURIId = (): any => { +const getVCv2NonURIId = (): any => { const cred = getVCv2() cred.id = "0923lksjf" return cred } +const getVCv1NonURIId = (): any => { + const cred = getVCv1() + cred.id = "0923lksjf" + return cred +} + +const getVCv1ExpiredWithValidStatus = (): any => { + return JSON.parse(JSON.stringify(v1ExpiredWithValidStatus)) +} + +const getVCv2ExpiredWithValidStatus = (): any => { + return JSON.parse(JSON.stringify(v2ExpiredWithValidStatus)) +} + export { getCredentialWithoutContext, getCredentialWithoutVCContext, - getCredentialWithNonURIId, getVCv2, getVCv2Expired, @@ -103,8 +117,9 @@ export { getVCv2Tampered, getVCv2ValidStatus, getVCv2ExpiredAndTampered, + getVCv2ExpiredWithValidStatus, getVCv2NoProof, - + getVCv2NonURIId, getVCv1, getVCv1Expired, @@ -112,5 +127,7 @@ export { getVCv1Tampered, getVCv1ValidStatus, getVCv1ExpiredAndTampered, - getVCv1NoProof + getVCv1ExpiredWithValidStatus, + getVCv1NoProof, + getVCv1NonURIId } diff --git a/test/Verify.spec.ts b/test/Verify.spec.ts index 95a6150..7687fef 100644 --- a/test/Verify.spec.ts +++ b/test/Verify.spec.ts @@ -2,18 +2,52 @@ import chai from 'chai' import deepEqualInAnyOrder from 'deep-equal-in-any-order' import { strict as assert } from 'assert'; import { verifyCredential } from '../src/Verify.js' -import { getVCv2Expired, getVCv1Tampered, getVCv1Expired, getVCv1Revoked, getVCv2Revoked, getVCv1ValidStatus, getVCv2ValidStatus, getVCv2Tampered, getVCv1NoProof, getVCv2NoProof, getCredentialWithoutContext, getCredentialWithoutVCContext, getCredentialWithNonURIId } from '../src/test-fixtures/vc.js' +import { + getVCv2Expired, + getVCv1Tampered, + getVCv1Expired, + getVCv1Revoked, + getVCv2Revoked, + getVCv1ValidStatus, + getVCv2ValidStatus, + getVCv2Tampered, + getVCv1NoProof, + getVCv2NoProof, + getCredentialWithoutContext, + getCredentialWithoutVCContext, + getVCv1NonURIId, + getVCv2NonURIId, + getVCv1ExpiredAndTampered, + getVCv2ExpiredAndTampered, + getVCv1ExpiredWithValidStatus, + getVCv2ExpiredWithValidStatus +} from '../src/test-fixtures/vc.js' import { knownDIDRegistries } from '../.knownDidRegistries.js'; import { getExpectedVerifiedResult, getExpectedUnverifiedResult, getExpectedFatalResult } from '../src/test-fixtures/expectedResults.js'; chai.use(deepEqualInAnyOrder); const {expect} = chai; +/* +tests to add: + +- simulatenouly expired and revoked for v1 and v2 +- expired but valid status for v1 and v2 +- simultaneosly expired and tampered for v1 and v2 +- returns registry entries that are retunred by nock call +- returns no registry entry for nock with no result +- returns unverified for issuer DID that doesn't resolve +- returns verified when no status property + +*/ + + describe('Verify', () => { describe('.verifyCredential', () => { describe('returns general fatal errors', () => { + it('when not jsonld', async () => { const credential : any = getCredentialWithoutContext() const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) @@ -36,19 +70,7 @@ describe('Verify', () => { errorName: 'no_vc_context' }) expect(result).to.deep.equalInAnyOrder(expectedResult) - }) - - it('when credential id is not a uri', async () => { - const credential : any = getCredentialWithNonURIId() - const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) - - const expectedResult = getExpectedFatalResult({ - credential, - errorMessage: "The credential's id uses an invalid format. It may have been issued as part of an early pilot. Please contact the issuer to get a replacement.", - errorName: 'invalid_credential_id' - }) - expect(result).to.deep.equalInAnyOrder(expectedResult) - }) + }) }) @@ -65,6 +87,20 @@ describe('Verify', () => { }) expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define }) + + describe('returns fatal error', () => { + + it.only('when expired and tampered with', async () => { + const credential : any = getVCv1ExpiredAndTampered() + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + const expectedResult = getExpectedFatalResult({ + credential, + errorMessage: 'The signature is not valid.', + errorName: 'invalid_signature' + }) + expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define + }) + it('when no proof', async () => { const credential : any = getVCv1NoProof() const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) @@ -76,6 +112,17 @@ describe('Verify', () => { }) expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define }) + it('when credential id is not a uri', async () => { + const credential : any = getVCv1NonURIId() + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + + const expectedResult = getExpectedFatalResult({ + credential, + errorMessage: "The credential's id uses an invalid format. It may have been issued as part of an early pilot. Please contact the issuer to get a replacement.", + errorName: 'invalid_credential_id' + }) + expect(result).to.deep.equalInAnyOrder(expectedResult) + }) }) @@ -118,6 +165,17 @@ describe('Verify', () => { expect(result).to.deep.equalInAnyOrder(expectedResult) }) + it.only('when expired and tampered with', async () => { + const credential : any = getVCv2ExpiredAndTampered() + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + const expectedResult = getExpectedFatalResult({ + credential, + errorMessage: 'The signature is not valid.', + errorName: 'invalid_signature' + }) + expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define + }) + it('when no proof', async () => { const credential : any = getVCv2NoProof() const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) @@ -129,7 +187,17 @@ describe('Verify', () => { }) expect(result).to.deep.equalInAnyOrder(expectedResult) }) - + it('when credential id is not a uri', async () => { + const credential : any = getVCv2NonURIId() + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + + const expectedResult = getExpectedFatalResult({ + credential, + errorMessage: "The credential's id uses an invalid format. It may have been issued as part of an early pilot. Please contact the issuer to get a replacement.", + errorName: 'invalid_credential_id' + }) + expect(result).to.deep.equalInAnyOrder(expectedResult) + }) }) @@ -158,4 +226,4 @@ describe('Verify', () => { }) }) }) - +}) From 46cc4b5d6e5fd1ffec5119c5d37163c986b54dd4 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Mon, 13 Jan 2025 19:55:00 -0500 Subject: [PATCH 22/72] add tests for revoked with valid status --- test/Verify.spec.ts | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/test/Verify.spec.ts b/test/Verify.spec.ts index 7687fef..312b3ee 100644 --- a/test/Verify.spec.ts +++ b/test/Verify.spec.ts @@ -23,7 +23,11 @@ import { getVCv2ExpiredWithValidStatus } from '../src/test-fixtures/vc.js' import { knownDIDRegistries } from '../.knownDidRegistries.js'; -import { getExpectedVerifiedResult, getExpectedUnverifiedResult, getExpectedFatalResult } from '../src/test-fixtures/expectedResults.js'; +import { + getExpectedVerifiedResult, + getExpectedUnverifiedResult, + getExpectedFatalResult + } from '../src/test-fixtures/expectedResults.js'; chai.use(deepEqualInAnyOrder); const {expect} = chai; @@ -88,9 +92,7 @@ describe('Verify', () => { expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define }) - describe('returns fatal error', () => { - - it.only('when expired and tampered with', async () => { + it('when expired and tampered with', async () => { const credential : any = getVCv1ExpiredAndTampered() const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) const expectedResult = getExpectedFatalResult({ @@ -135,17 +137,26 @@ describe('Verify', () => { }) }) - describe('returns as unverified', () => { + describe('returns unverified', () => { it('when expired', async () => { const credential : any = getVCv1Expired() + const expectedResult = getExpectedUnverifiedResult({credential, unVerifiedStep: 'expiration', withStatus:false}) const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) - assert.ok(result.log); + expect(result).to.deep.equalInAnyOrder(expectedResult) }) it('when revoked', async () => { const credential : any = getVCv1Revoked() const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) assert.ok(result.log); }) + it('when expired with valid status', async () => { + const credential : any = getVCv1ExpiredWithValidStatus() + const expectedResult = getExpectedUnverifiedResult({credential, unVerifiedStep: 'expiration', withStatus:true}) + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + // NOTE: this will continue to fail until we fix https://github.com/digitalcredentials/vc/issues/28 + expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define + }) + }) @@ -165,7 +176,7 @@ describe('Verify', () => { expect(result).to.deep.equalInAnyOrder(expectedResult) }) - it.only('when expired and tampered with', async () => { + it('when expired and tampered with', async () => { const credential : any = getVCv2ExpiredAndTampered() const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) const expectedResult = getExpectedFatalResult({ @@ -222,8 +233,16 @@ describe('Verify', () => { const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) assert.ok(result.log); }) + it('when expired with valid status', async () => { + const credential : any = getVCv2ExpiredWithValidStatus() + const expectedResult = getExpectedUnverifiedResult({credential, unVerifiedStep: 'expiration', withStatus:true}) + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + // NOTE: this will continue to fail until we fix https://github.com/digitalcredentials/vc/issues/28 + expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define + }) + }) }) }) }) -}) + From 664451b1b33b5b4a5579dae62bf73c8649945695 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Tue, 14 Jan 2025 07:48:20 -0500 Subject: [PATCH 23/72] fix linting --- .eslintrc.js => .eslintrc.cjs | 1 + package.json | 2 +- src/Verify.ts | 4 ++-- src/issuerRegistries.ts | 2 ++ src/test-fixtures/expectedResults.ts | 1 - tsconfig.json | 6 +++--- tsconfig.spec.json | 2 +- 7 files changed, 10 insertions(+), 8 deletions(-) rename .eslintrc.js => .eslintrc.cjs (91%) diff --git a/.eslintrc.js b/.eslintrc.cjs similarity index 91% rename from .eslintrc.js rename to .eslintrc.cjs index ec4f13b..b371d40 100644 --- a/.eslintrc.js +++ b/.eslintrc.cjs @@ -14,6 +14,7 @@ module.exports = { '@typescript-eslint/prefer-ts-expect-error': 'off', '@typescript-eslint/return-await': 'off', '@typescript-eslint/strict-boolean-expressions': 'off', + '@typescript-eslint/prefer-nullish-coalescing': 'off', 'no-empty-pattern': 'off' } } diff --git a/package.json b/package.json index 2bd014a..a7932e5 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "test": "npm run lint && npm run test-node", "test-karma": "karma start karma.conf.js", "test-node-old": "cross-env NODE_ENV=test TS_NODE_PROJECT=tsconfig.spec.json TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\" }' mocha -r ts-node/register --project tsconfig.spec.json 'test/*.ts'", - "test-node": "npm run build-test && mocha dist/esm/test/*.spec.js && rm -rf dist/esm/test || true" + "test-node": "npm run build-test && mocha dist/test/*.spec.js && rm -rf dist/esm/test || true" }, "files": [ "dist", diff --git a/src/Verify.ts b/src/Verify.ts index fe2edb8..362c76e 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -9,11 +9,11 @@ import { Credential } from './types/credential.js'; import { VerificationResponse } from './types/result.js'; // the new eddsa-rdfc-2022-cryptosuite -import {DataIntegrityProof} from '@digitalbazaar/data-integrity'; +/* import {DataIntegrityProof} from '@digitalbazaar/data-integrity'; import {cryptosuite as eddsaRdfc2022CryptoSuite} from '@digitalbazaar/eddsa-rdfc-2022-cryptosuite'; const eddsaSuite = new DataIntegrityProof({ cryptosuite: eddsaRdfc2022CryptoSuite -}); +}); */ const documentLoader = securityLoader({ fetchRemoteContexts: true }).build(); const suite = new Ed25519Signature2020(); diff --git a/src/issuerRegistries.ts b/src/issuerRegistries.ts index 6704917..1d5f93c 100644 --- a/src/issuerRegistries.ts +++ b/src/issuerRegistries.ts @@ -14,6 +14,8 @@ export async function getTrustedRegistryListForIssuer({ issuer, knownDIDRegistri reloadIssuerRegistry: boolean | null }): Promise { + + // eslint-disable-next-line no-use-before-define if (reloadIssuerRegistry || registryNotYetLoaded) { await registries.load({ config: knownDIDRegistries }) } diff --git a/src/test-fixtures/expectedResults.ts b/src/test-fixtures/expectedResults.ts index d46b96a..629ebbf 100644 --- a/src/test-fixtures/expectedResults.ts +++ b/src/test-fixtures/expectedResults.ts @@ -1,4 +1,3 @@ -import { error } from "console"; import { VerificationResponse, VerificationStep } from "src/types/result"; const expectedResult = { diff --git a/tsconfig.json b/tsconfig.json index e947de9..71fd4f1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,9 +3,9 @@ "strict": true, "target": "es2022", "lib": ["es2022", "dom"], - "module": "commonjs", + "module": "es6", "moduleResolution": "node", - "outDir": "dist/esm", + "outDir": "dist", "noImplicitAny": true, "removeComments": false, "preserveConstEnums": true, @@ -19,7 +19,7 @@ }, "include": [ "src/**/*", - ".eslintrc.js", + ".eslintrc.cjs", "karma.conf.js" ], "exclude": ["node_modules", "dist", "test"] diff --git a/tsconfig.spec.json b/tsconfig.spec.json index 389b0c1..e1b17c6 100644 --- a/tsconfig.spec.json +++ b/tsconfig.spec.json @@ -5,7 +5,7 @@ "lib": ["es2022", "dom"], "module": "es2022", "moduleResolution": "node", - "outDir": "dist/esm", + "outDir": "dist", "noImplicitAny": true, "removeComments": false, "preserveConstEnums": true, From de56d0b305af11894fc267724841d8e9d6edcdf8 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Tue, 14 Jan 2025 08:21:01 -0500 Subject: [PATCH 24/72] remove types from .gitignore --- .gitignore | 2 +- src/types/credential.ts | 153 ++++++++++++++++++++++++++++++++++++++ src/types/presentation.ts | 14 ++++ src/types/result.ts | 19 +++++ 4 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 src/types/credential.ts create mode 100644 src/types/presentation.ts create mode 100644 src/types/result.ts diff --git a/.gitignore b/.gitignore index 2ae83b1..aaacd06 100644 --- a/.gitignore +++ b/.gitignore @@ -46,7 +46,7 @@ node_modules/ jspm_packages/ # TypeScript v1 declaration files -types/ +#types/ # TypeScript cache *.tsbuildinfo diff --git a/src/types/credential.ts b/src/types/credential.ts new file mode 100644 index 0000000..44bff38 --- /dev/null +++ b/src/types/credential.ts @@ -0,0 +1,153 @@ + + + +export type IssuerURI = string; + +export interface ImageObject { + readonly id: string; + readonly type: string; +} + +export interface IssuerObject { + readonly id: IssuerURI; + readonly type?: string; + readonly name?: string; + readonly url?: string; + readonly image?: string | ImageObject; +} +export type Issuer = IssuerURI | IssuerObject; + +export interface CreditValue { + value?: string; +} + +export interface CompletionDocument { + readonly type?: string; + readonly identifier?: string; + readonly name?: string; + readonly description?: string; + readonly numberOfCredits?: CreditValue; + readonly startDate?: string; + readonly endDate?: string; +} + +export interface EducationalOperationalCredentialExtensions { + readonly type?: string[]; + readonly awardedOnCompletionOf?: CompletionDocument; + readonly criteria?: { + type: string; + narrative: string; + }; + readonly image?: ImageObject; +} + +// https://schema.org/EducationalOccupationalCredential (this doesn't really conform) +export type EducationalOperationalCredential = EducationalOperationalCredentialExtensions & { + readonly id?: string; + readonly name?: string; + readonly description?: string; + readonly competencyRequired?: string; + readonly credentialCategory?: string; + readonly achievementType?: string; +} + +export interface DegreeCompletion { + readonly type: string; + readonly name: string; +} + +export interface StudentId { + readonly id: string; + readonly image: string; +} + +interface SubjectExtensions { + readonly type?: string; + readonly name?: string; + readonly hasCredential?: EducationalOperationalCredential; // https://schema.org/hasCredential + readonly degree?: DegreeCompletion; + readonly studentId?: StudentId; + // Open Badges v3 + readonly achievement?: EducationalOperationalCredential | EducationalOperationalCredential[]; + readonly identifier?: OBV3IdentifierObject | OBV3IdentifierObject[]; +} + +export interface OBV3IdentifierObject { + readonly identityType?: string; + readonly identityHash?: string; +} + +export type Subject = SubjectExtensions & { + readonly id?: string; +} + +export interface Proof { + type: string; + created: string; + verificationMethod: string; + proofPurpose: string; + proofValue: string; + challenge?: string; + jws?: string; +} + +export interface RenderMethod { + id?: string; + type: string; + name?: string; + css3MediaQuery?: string; +} + +// https://www.w3.org/TR/vc-data-model/ +export interface CredentialV1 { + readonly '@context': string[]; // https://www.w3.org/TR/vc-data-model/#contexts + readonly id?: string; // https://www.w3.org/TR/vc-data-model/#identifiers + readonly type: string[]; // https://www.w3.org/TR/vc-data-model/#types + readonly issuer: Issuer; // https://www.w3.org/TR/vc-data-model/#issuer + readonly issuanceDate: string; // https://www.w3.org/TR/vc-data-model/#issuance-date + readonly expirationDate?: string; // https://www.w3.org/TR/vc-data-model/#expiration + readonly credentialSubject: Subject; // https://www.w3.org/TR/vc-data-model/#credential-subject + readonly credentialStatus?: CredentialStatus | CredentialStatus[]; // https://www.w3.org/TR/vc-data-model/#status + readonly proof?: Proof; // https://www.w3.org/TR/vc-data-model/#proofs-signatures + readonly name?: string; + readonly renderMethod?: RenderMethod[]; +} + +// https://www.w3.org/TR/vc-data-model-2.0/ +// (At this time, this should be in sync with https://w3c.github.io/vc-data-model/) +export interface CredentialV2 { + readonly '@context': string[]; // https://www.w3.org/TR/vc-data-model-2.0/#contexts + readonly id?: string; // https://www.w3.org/TR/vc-data-model-2.0/#identifiers + readonly type: string[]; // https://www.w3.org/TR/vc-data-model-2.0/#types + readonly issuer: Issuer; // https://www.w3.org/TR/vc-data-model-2.0/#issuer + readonly validFrom?: string; // https://www.w3.org/TR/vc-data-model-2.0/#validity-period + readonly validUntil?: string; // https://www.w3.org/TR/vc-data-model-2.0/#validity-period + readonly credentialSubject: Subject; // https://www.w3.org/TR/vc-data-model-2.0/#credential-subject + readonly credentialStatus?: CredentialStatus | CredentialStatus[]; // https://www.w3.org/TR/vc-data-model-2.0/#status + readonly proof?: Proof; // https://w3c.github.io/vc-data-model/#proofs-signatures + readonly name?: string; + readonly renderMethod?: RenderMethod[]; // https://www.w3.org/TR/vc-data-model-2.0/#reserved-extension-points +} + +export type Credential = CredentialV1 | CredentialV2; + +// https://www.w3.org/TR/vc-bitstring-status-list +export interface CredentialStatus { + readonly id: string; + readonly type: string | string[]; + readonly statusPurpose: string; + readonly statusListIndex: string | number; + readonly statusListCredential: string; +} + +export enum CredentialError { + IsNotVerified = 'Credential is not verified.', + CouldNotBeVerified = 'Credential could not be checked for verification and may be malformed.', + DidNotInRegistry = 'Could not find issuer in registry with given DID.', +} + +export interface CredentialImportReport { + success: string[]; + duplicate: string[]; + failed: string[]; +} diff --git a/src/types/presentation.ts b/src/types/presentation.ts new file mode 100644 index 0000000..e252ad8 --- /dev/null +++ b/src/types/presentation.ts @@ -0,0 +1,14 @@ +import type { Credential, Proof, Issuer } from './credential'; + +export interface VerifiablePresentation { + readonly '@context': string[]; + readonly issuer: Issuer; + readonly type: string; + readonly verifiableCredential: Credential | Credential[]; + readonly proof: Proof; +} + +export enum PresentationError { + IsNotVerified = 'Presentation is not verified.', + CouldNotBeVerified = 'Presentation encoded could not be checked for verification and may be malformed.', +} diff --git a/src/types/result.ts b/src/types/result.ts new file mode 100644 index 0000000..440eaa6 --- /dev/null +++ b/src/types/result.ts @@ -0,0 +1,19 @@ +export interface VerificationError { + "message": string, + "isFatal": boolean, + "name"?: string, + stackTrace?: string + } + + export interface VerificationStep { + "id": string, + "valid": boolean, + "foundInRegistries"?: string[], + } + + export interface VerificationResponse { + "isFatal": boolean, + "credential": object, + "errors"?: VerificationError[], + "log"?: VerificationStep[] + } \ No newline at end of file From 52720803ac1bbf975a05e9cf99fa1de8a3913f65 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Tue, 14 Jan 2025 08:30:58 -0500 Subject: [PATCH 25/72] disable karma --- .github/workflows/main.yml | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 20fcb80..e1b4349 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,22 +20,22 @@ jobs: run: npm run test-node env: CI: true - test-karma: - runs-on: ubuntu-latest -# needs: [lint] - timeout-minutes: 10 - strategy: - matrix: - node-version: [18.x] - steps: - - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - - run: npm install - - name: Run karma tests - run: npm run test-karma +# test-karma: + # runs-on: ubuntu-latest +## needs: [lint] +# timeout-minutes: 10 + # strategy: + # matrix: + # node-version: [18.x] + # steps: + # - uses: actions/checkout@v2 +# - name: Use Node.js ${{ matrix.node-version }} + # uses: actions/setup-node@v1 + # with: + # node-version: ${{ matrix.node-version }} + # - run: npm install + # - name: Run karma tests + # run: npm run test-karma lint: runs-on: ubuntu-latest strategy: From a0404fecc2bbc61efcfc6f52378c836e83315e47 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Wed, 15 Jan 2025 15:17:31 -0500 Subject: [PATCH 26/72] add eddsa verification --- src/Verify.ts | 57 +++++++++-------- src/test-fixtures/vc.ts | 9 +++ .../eddsa/v2/v2EddsaWithValidStatus.ts | 53 ++++++++++++++++ .../v2/v2DoubleSigWithValidStatus.ts | 62 +++++++++++++++++++ src/types/credential.ts | 1 + test/Verify.spec.ts | 22 +++++-- 6 files changed, 173 insertions(+), 31 deletions(-) create mode 100644 src/test-fixtures/verifiableCredentials/eddsa/v2/v2EddsaWithValidStatus.ts create mode 100644 src/test-fixtures/verifiableCredentials/eddsaAndEd25519/v2/v2DoubleSigWithValidStatus.ts diff --git a/src/Verify.ts b/src/Verify.ts index 362c76e..30c1543 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -8,25 +8,27 @@ import { addTrustedIssuersToVerificationResponse } from './issuerRegistries.js'; import { Credential } from './types/credential.js'; import { VerificationResponse } from './types/result.js'; -// the new eddsa-rdfc-2022-cryptosuite -/* import {DataIntegrityProof} from '@digitalbazaar/data-integrity'; -import {cryptosuite as eddsaRdfc2022CryptoSuite} from '@digitalbazaar/eddsa-rdfc-2022-cryptosuite'; -const eddsaSuite = new DataIntegrityProof({ - cryptosuite: eddsaRdfc2022CryptoSuite -}); */ - const documentLoader = securityLoader({ fetchRemoteContexts: true }).build(); -const suite = new Ed25519Signature2020(); -export async function verifyCredential({credential, knownDIDRegistries, reloadIssuerRegistry = true}:{credential: Credential, knownDIDRegistries: object, reloadIssuerRegistry: boolean}): Promise { - +// for verifying eddsa-2022 signatures +import { DataIntegrityProof } from '@digitalbazaar/data-integrity'; +import { cryptosuite as eddsaRdfc2022CryptoSuite } from '@digitalbazaar/eddsa-rdfc-2022-cryptosuite'; +const eddsaSuite = new DataIntegrityProof({ cryptosuite: eddsaRdfc2022CryptoSuite }); + +// for verifying ed25519-2020 signatures +const ed25519Suite = new Ed25519Signature2020(); + +export async function verifyCredential({ credential, knownDIDRegistries, reloadIssuerRegistry = true }: { credential: Credential, knownDIDRegistries: object, reloadIssuerRegistry: boolean }): Promise { -const fatalError = checkForFatalErrors(credential) + const fatalError = checkForFatalErrors(credential) if (fatalError) { return fatalError } + const suite = (credential?.proof?.cryptosuite === 'eddsa-rdfc-2022') ? + eddsaSuite : ed25519Suite + const verificationResponse = await vc.verifyCredential({ credential, suite, @@ -34,7 +36,7 @@ const fatalError = checkForFatalErrors(credential) checkStatus: getCredentialStatusChecker(credential) }); - // remove things we don't need in the result or that are duplicated elsewhere + // remove things we don't need from the result or that are duplicated elsewhere delete verificationResponse.results delete verificationResponse.statusResult delete verificationResponse.verified @@ -44,9 +46,17 @@ const fatalError = checkForFatalErrors(credential) if (verificationResponse.error) { if (verificationResponse.error.log) { + // move the log out of the error to the response, since it + // isn't part of the error, but rather the true/false values + // for each step in verification verificationResponse.log = verificationResponse.error.log + // delete the error, because again, this wasn't an error, just + // a false value on one of the steps delete verificationResponse.error } else if (verificationResponse?.error?.name === 'VerificationError') { + // this is in fact an error so return a fatal error. + // this means something happened (likely a bad signature) that prevents us from + // saying anything conclusive about the various steps in verification const fatalErrorMessage = 'The signature is not valid.' const stackTrace = verificationResponse?.error?.errors?.stack return buildFatalErrorObject(fatalErrorMessage, "invalid_signature", credential, stackTrace) @@ -54,16 +64,16 @@ const fatalError = checkForFatalErrors(credential) } const { issuer } = credential - await addTrustedIssuersToVerificationResponse({verificationResponse, knownDIDRegistries,reloadIssuerRegistry, issuer}) - + await addTrustedIssuersToVerificationResponse({ verificationResponse, knownDIDRegistries, reloadIssuerRegistry, issuer }) + return verificationResponse; } -function buildFatalErrorObject(fatalErrorMessage: string, name: string, credential: Credential, stackTrace: string | null) : VerificationResponse { - return {credential, isFatal: true, errors: [{name, message: fatalErrorMessage, isFatal: true, ...stackTrace?{stackTrace}:null}]} +function buildFatalErrorObject(fatalErrorMessage: string, name: string, credential: Credential, stackTrace: string | null): VerificationResponse { + return { credential, isFatal: true, errors: [{ name, message: fatalErrorMessage, isFatal: true, ...stackTrace ? { stackTrace } : null }] } } -function checkForFatalErrors(credential: Credential) : VerificationResponse | null { +function checkForFatalErrors(credential: Credential): VerificationResponse | null { const validVCContexts = [ 'https://www.w3.org/2018/credentials/v1', 'https://www.w3.org/ns/credentials/v2' @@ -75,13 +85,13 @@ function checkForFatalErrors(credential: Credential) : VerificationResponse | nu const name = 'invalid_jsonld' return buildFatalErrorObject(fatalErrorMessage, name, credential, null) } - - if (! validVCContexts.some(contextURI => suppliedContexts.includes(contextURI))) { + + if (!validVCContexts.some(contextURI => suppliedContexts.includes(contextURI))) { const fatalErrorMessage = "The credential doesn't have a verifiable credential context." const name = 'no_vc_context' return buildFatalErrorObject(fatalErrorMessage, name, credential, null) } - + try { // eslint-disable-next-line no-new new URL(credential.id as string); @@ -90,16 +100,13 @@ function checkForFatalErrors(credential: Credential) : VerificationResponse | nu const name = 'invalid_credential_id' return buildFatalErrorObject(fatalErrorMessage, name, credential, null) } - + if (!credential.proof) { const fatalErrorMessage = 'This is not a Verifiable Credential - it does not have a digital signature.' const name = 'no_proof' - return buildFatalErrorObject(fatalErrorMessage, name, credential,null) + return buildFatalErrorObject(fatalErrorMessage, name, credential, null) } - - - return null } diff --git a/src/test-fixtures/vc.ts b/src/test-fixtures/vc.ts index 119c5fc..f4fbfec 100644 --- a/src/test-fixtures/vc.ts +++ b/src/test-fixtures/vc.ts @@ -10,6 +10,8 @@ import { v1Revoked } from "./verifiableCredentials/v1/v1Revoked.js" import { v1Expired } from "./verifiableCredentials/v1/v1Expired.js" import { v1ExpiredWithValidStatus } from "./verifiableCredentials/v1/v1ExpiredWithValidStatus.js" +import { v2EddsaWithValidStatus } from "./verifiableCredentials/eddsa/v2/v2EddsaWithValidStatus.js" + const getVCv1 = (): any => { return JSON.parse(JSON.stringify(v1NoStatus)) } @@ -106,11 +108,18 @@ const getVCv2ExpiredWithValidStatus = (): any => { return JSON.parse(JSON.stringify(v2ExpiredWithValidStatus)) } +const getVCv2EddsaWithValidStatus = (): any => { + return JSON.parse(JSON.stringify(v2EddsaWithValidStatus)) +} + + export { getCredentialWithoutContext, getCredentialWithoutVCContext, + getVCv2EddsaWithValidStatus, + getVCv2, getVCv2Expired, getVCv2Revoked, diff --git a/src/test-fixtures/verifiableCredentials/eddsa/v2/v2EddsaWithValidStatus.ts b/src/test-fixtures/verifiableCredentials/eddsa/v2/v2EddsaWithValidStatus.ts new file mode 100644 index 0000000..665d926 --- /dev/null +++ b/src/test-fixtures/verifiableCredentials/eddsa/v2/v2EddsaWithValidStatus.ts @@ -0,0 +1,53 @@ +export const v2EddsaWithValidStatus = { + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "name": "Teamwork Badge", + "issuer": { + "type": [ + "Profile" + ], + "name": "Example Corp", + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q" + }, + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "validFrom": "2010-01-01T00:00:00Z", + "credentialSubject": { + "type": [ + "AchievementSubject" + ], + "name": "Taylor Tuna", + "achievement": { + "id": "https://example.com/achievements/21st-century-skills/teamwork", + "type": [ + "Achievement" + ], + "name": "Masters - v2 - unrevoked", + "criteria": { + "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." + }, + "description": "This badge recognizes the development of the capacity to collaborate within a group environment." + } + }, + "id": "urn:uuid:677fe8a6cacf98774d482d07", + "credentialStatus": { + "id": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj#9", + "type": "BitstringStatusListEntry", + "statusPurpose": "revocation", + "statusListCredential": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj", + "statusListIndex": "9" + }, + "proof": { + "type": "DataIntegrityProof", + "created": "2025-01-15T17:08:56Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "cryptosuite": "eddsa-rdfc-2022", + "proofPurpose": "assertionMethod", + "proofValue": "z2gSJFxCToKUpGAf4DfjvVPjW6F9ktz6UgCAg7siW5usS3GCbiWSuMaiExapnDox2YLvEtcPyx7ZHMPmfDexJYXKm" + } +} \ No newline at end of file diff --git a/src/test-fixtures/verifiableCredentials/eddsaAndEd25519/v2/v2DoubleSigWithValidStatus.ts b/src/test-fixtures/verifiableCredentials/eddsaAndEd25519/v2/v2DoubleSigWithValidStatus.ts new file mode 100644 index 0000000..79752c9 --- /dev/null +++ b/src/test-fixtures/verifiableCredentials/eddsaAndEd25519/v2/v2DoubleSigWithValidStatus.ts @@ -0,0 +1,62 @@ +export const v2DoubleSigWithValidStatus = { + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "name": "Teamwork Badge", + "issuer": { + "type": [ + "Profile" + ], + "name": "Example Corp", + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q" + }, + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "validFrom": "2010-01-01T00:00:00Z", + "credentialSubject": { + "type": [ + "AchievementSubject" + ], + "name": "Taylor Tuna", + "achievement": { + "id": "https://example.com/achievements/21st-century-skills/teamwork", + "type": [ + "Achievement" + ], + "name": "Masters - v2 - unrevoked", + "criteria": { + "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." + }, + "description": "This badge recognizes the development of the capacity to collaborate within a group environment." + } + }, + "id": "urn:uuid:677fe8a6cacf98774d482d07", + "credentialStatus": { + "id": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj#9", + "type": "BitstringStatusListEntry", + "statusPurpose": "revocation", + "statusListCredential": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj", + "statusListIndex": "9" + }, + "proof": [ + { + "type": "DataIntegrityProof", + "created": "2025-01-15T16:29:50Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "cryptosuite": "eddsa-rdfc-2022", + "proofPurpose": "assertionMethod", + "proofValue": "z5Hk3nDNYGeuXfSqMTjiFsa2FLWbQn6KfXSioDc6PufxkngVgEQayTWZ5RpbBy9K2FU6tZacsWrQEqB2wgmsVKoUh" + }, + { + "type": "Ed25519Signature2020", + "created": "2025-01-15T16:34:28Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "zCXYPtKh6M5TEe4YTAk7FNPvLHfP7wrroxkfwECF7GK4GJjS9aRkMYw4Ns9E5PZ8GCZ53MV1bMzhsVuSWTLq885h" + } + ] +} \ No newline at end of file diff --git a/src/types/credential.ts b/src/types/credential.ts index 44bff38..5dcc835 100644 --- a/src/types/credential.ts +++ b/src/types/credential.ts @@ -87,6 +87,7 @@ export interface Proof { verificationMethod: string; proofPurpose: string; proofValue: string; + cryptosuite?: string; challenge?: string; jws?: string; } diff --git a/test/Verify.spec.ts b/test/Verify.spec.ts index 312b3ee..7a7af46 100644 --- a/test/Verify.spec.ts +++ b/test/Verify.spec.ts @@ -20,7 +20,8 @@ import { getVCv1ExpiredAndTampered, getVCv2ExpiredAndTampered, getVCv1ExpiredWithValidStatus, - getVCv2ExpiredWithValidStatus + getVCv2ExpiredWithValidStatus, + getVCv2EddsaWithValidStatus } from '../src/test-fixtures/vc.js' import { knownDIDRegistries } from '../.knownDidRegistries.js'; import { @@ -149,11 +150,12 @@ describe('Verify', () => { const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) assert.ok(result.log); }) - it('when expired with valid status', async () => { + + it.skip('when expired with valid status', async () => { + // NOTE: this will continue to fail until we fix https://github.com/digitalcredentials/vc/issues/28 const credential : any = getVCv1ExpiredWithValidStatus() const expectedResult = getExpectedUnverifiedResult({credential, unVerifiedStep: 'expiration', withStatus:true}) const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) - // NOTE: this will continue to fail until we fix https://github.com/digitalcredentials/vc/issues/28 expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define }) @@ -219,6 +221,14 @@ describe('Verify', () => { const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define }) + it('when status is valid for eddsa', async () => { + const credential : any = getVCv2EddsaWithValidStatus() + // const expectedResult = getExpectedVerifiedResult({credential, withStatus: true}) + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + console.log("result from the eddsa verification:") + console.log(result) + // expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define + }) }) describe('returns as unverified', () => { @@ -233,12 +243,12 @@ describe('Verify', () => { const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) assert.ok(result.log); }) - it('when expired with valid status', async () => { + it.skip('when expired with valid status', async () => { + // NOTE: TODO - this will continue to fail until we fix https://github.com/digitalcredentials/vc/issues/28 const credential : any = getVCv2ExpiredWithValidStatus() const expectedResult = getExpectedUnverifiedResult({credential, unVerifiedStep: 'expiration', withStatus:true}) const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) - // NOTE: this will continue to fail until we fix https://github.com/digitalcredentials/vc/issues/28 - expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define + expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define }) }) From 02f7b59cc3e095f19a3b6f414c4e9659e435e296 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Wed, 15 Jan 2025 15:24:11 -0500 Subject: [PATCH 27/72] fix lint errors --- src/Verify.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Verify.ts b/src/Verify.ts index 30c1543..2ee351a 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -1,5 +1,7 @@ // import '@digitalcredentials/data-integrity-rn'; import { Ed25519Signature2020 } from '@digitalcredentials/ed25519-signature-2020'; +import { DataIntegrityProof } from '@digitalbazaar/data-integrity'; +import { cryptosuite as eddsaRdfc2022CryptoSuite } from '@digitalbazaar/eddsa-rdfc-2022-cryptosuite'; import * as vc from '@digitalcredentials/vc'; import { securityLoader } from '@digitalcredentials/security-document-loader'; import { getCredentialStatusChecker } from './credentialStatus.js'; @@ -11,8 +13,6 @@ import { VerificationResponse } from './types/result.js'; const documentLoader = securityLoader({ fetchRemoteContexts: true }).build(); // for verifying eddsa-2022 signatures -import { DataIntegrityProof } from '@digitalbazaar/data-integrity'; -import { cryptosuite as eddsaRdfc2022CryptoSuite } from '@digitalbazaar/eddsa-rdfc-2022-cryptosuite'; const eddsaSuite = new DataIntegrityProof({ cryptosuite: eddsaRdfc2022CryptoSuite }); // for verifying ed25519-2020 signatures From de74064c19ec7def2d921ecf5b27b012413de64e Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Thu, 16 Jan 2025 08:38:12 -0500 Subject: [PATCH 28/72] update to beta vc package --- package.json | 2 +- test/Verify.spec.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index a7932e5..9917c08 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "@digitalcredentials/issuer-registry-client": "^3.0.0", "@digitalcredentials/jsonld-signatures": "^12.0.1", "@digitalcredentials/security-document-loader": "^6.0.1", - "@digitalcredentials/vc": "^9.0.1", + "@digitalcredentials/vc": "^9.0.1-beta.1", "@digitalcredentials/vc-bitstring-status-list": "^1.0.0", "@digitalcredentials/vc-status-list": "^9.0.0" }, diff --git a/test/Verify.spec.ts b/test/Verify.spec.ts index 7a7af46..6d736e0 100644 --- a/test/Verify.spec.ts +++ b/test/Verify.spec.ts @@ -151,7 +151,7 @@ describe('Verify', () => { assert.ok(result.log); }) - it.skip('when expired with valid status', async () => { + it('when expired with valid status', async () => { // NOTE: this will continue to fail until we fix https://github.com/digitalcredentials/vc/issues/28 const credential : any = getVCv1ExpiredWithValidStatus() const expectedResult = getExpectedUnverifiedResult({credential, unVerifiedStep: 'expiration', withStatus:true}) @@ -243,7 +243,7 @@ describe('Verify', () => { const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) assert.ok(result.log); }) - it.skip('when expired with valid status', async () => { + it('when expired with valid status', async () => { // NOTE: TODO - this will continue to fail until we fix https://github.com/digitalcredentials/vc/issues/28 const credential : any = getVCv2ExpiredWithValidStatus() const expectedResult = getExpectedUnverifiedResult({credential, unVerifiedStep: 'expiration', withStatus:true}) From c6ea1228f7a376f7641adeb0215e188e3bfa21d0 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Thu, 16 Jan 2025 17:41:57 -0500 Subject: [PATCH 29/72] update result description and eddsa handling in README --- README.md | 45 +++++++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index cdda6da..171e8d1 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ Verifies the following versions of W3C Verifiable Credentials: * [1.1](https://www.w3.org/TR/2022/REC-vc-data-model-20220303/) * [2.0](https://www.w3.org/TR/vc-data-model-2.0/) +And verifies signatures from both [eddsa-rdfc-2022 Data Integrity Proof](https://github.com/digitalbazaar/eddsa-rdfc-2022-cryptosuite) and [ed25519-signature-2020 Linked Data Proof](https://github.com/digitalbazaar/ed25519-signature-2020) cryptosuites. + The verification checks that the credential: * has a valid signature (i.e, that the credential hasn't been tampered with) @@ -48,7 +50,7 @@ As of January 2025 issuers are trusted if they are listed in one of the Digital } ``` - The DCC is actively working on a new trust registry model that will likely extend the registry scope. + The DCC is actively working on a new trust registry model that will extend the registry scope. ## API @@ -70,17 +72,18 @@ This package exports two methods: The typescript definitions for the result can be found [here](./src/types/result.ts) -There are four general flavours of result that might be returned: +Note that the verification result doesn't make any conclusion about the overall validity of a credential. It only checks the validity of each of the four steps, leaving it up to the consumer of the result to decide on the overall validity. The consumer might not, for example, consider a credential that had expired or had been revoked to be 'invalid'. The credential might still in fact be useful as a record of history, i.e, I had a driver's licence that expired two years ago, but did have it during the period 2018 to 2023, and that information might be useful. + +There are three general flavours of result that might be returned: -1. successful verification +1. all checks were conclusive -A verification is successful if the signature is valid (the credential hasn't been tampered with), hasn't expired, hasn't been revoked, and was signed by a trusted issuer. +All of the checks were run *conclusively*, meaning that we determined wether each of the four steps in verification (signature, expiry, revocation, known issuer) was true or false. -A successful verification might look like this example: +A conclusive verification might look like this example where all steps returned valid=true: ``` { - "verified": true, "isFatal": false, "credential": {the supplied vc - left out here for brevity/clarity}, "log": [ @@ -111,15 +114,12 @@ A successful verification might look like this example: } ``` -2. unsucessful verification +Note that an invalid signature is considered fatal because it means that the revocation status, expiry data, or issuer id may have been changed so we can't say anything conclusive about any of them. -An unsuccessful verification means that one of the steps (other than the 'valid_signature' step) returned false, so the credential has expired, and/or been revoked, and/or can't be confirmed to be signed by a known issuer. Note that an invalid signature is considered fatal because it means that the revocation status, expiry data, or issuer id may have been changed so we can't say anything conclusive about any of them. - -An unsuccessful verification (in this case because the credential has expired) might look like this example: +Here is what the verification result for an expired credential might look like, where we have still made conclusive determinations about each step, and all are true except for the expiry: ``` { - "verified": false, "isFatal": false, "credential": {the supplied vc - left out here for brevity/clarity}, "log": [ @@ -149,17 +149,27 @@ An unsuccessful verification (in this case because the credential has expired) m } ``` -3. partially successful verification +2. partially successful verification + +A verification might partly succeed if it can verify: + +* the signature +* the expiry date + +But can't retrieve (from the network) any one of the: + +* revocation status +* the issuer registry +* the issuer's DID document -A verification might partly succeed if it can verify the signature and the expiry date, but can't retrieve any of the revocation status, the issuer registry, or the issuer's DID document from the network to verify the revocation status and issuer identity. +to verify the revocation status and issuer identity. -For those steps that we couldn't verify conclusively one way or the other (true or false) we return an 'error' propery rather than a 'valid' property. +For steps that we can't conclusively verify one way or the other (true or false) we return an 'error' propery rather than a 'valid' property. A partially successful verification might look like this example: ``` { - "verified": false, "isFatal": false, "credential": {the supplied vc - left out here for brevity/clarity}, "log": [ @@ -196,15 +206,14 @@ A partially successful verification might look like this example: } ``` -4. fatal error +3. fatal error -Fatal errors are errors that prevent us from saying anything conclusive about the credential, and so we don't list the results of each step (the 'log') because we can't say decisively one way or the other if any are true or false. Reverting to saying they are all false would be misleading, because that could be interepreted to mean that the credential was, for example, revoked when really we just don't know one way or the other. +Fatal errors are errors that prevent us from saying anything conclusive about the credential, and so we don't list the results of each step (the 'log') because we can't decisively say if any are true or false. Reverting to saying they are all false would be misleading, because that could be interepreted to mean that the credential was, for example, revoked when really we just don't know one way or the other. ``` { "credential": {the vc goes here}, "isFatal": true, - "verified": false, "errors": [ { "name": "invalidSignature", From 10aced7c82f50def34a5f558947b833a0588cc42 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Fri, 17 Jan 2025 15:30:08 -0500 Subject: [PATCH 30/72] add both signature suites to verify call --- src/Verify.ts | 13 +++++++++---- src/credentialStatus.ts | 1 + src/issuerRegistries.ts | 2 +- test/Verify.spec.ts | 25 +++++++++++++++---------- 4 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/Verify.ts b/src/Verify.ts index 2ee351a..c320491 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -26,14 +26,17 @@ export async function verifyCredential({ credential, knownDIDRegistries, reloadI return fatalError } - const suite = (credential?.proof?.cryptosuite === 'eddsa-rdfc-2022') ? - eddsaSuite : ed25519Suite +const suite = [ed25519Suite, eddsaSuite] + /* const suite = (credential?.proof?.cryptosuite === 'eddsa-rdfc-2022') ? + eddsaSuite : ed25519Suite */ - const verificationResponse = await vc.verifyCredential({ + const checkStatus = getCredentialStatusChecker(credential) + + const verificationResponse = await vc.verifyCredential({ credential, suite, documentLoader, - checkStatus: getCredentialStatusChecker(credential) + checkStatus }); // remove things we don't need from the result or that are duplicated elsewhere @@ -64,7 +67,9 @@ export async function verifyCredential({ credential, knownDIDRegistries, reloadI } const { issuer } = credential + await addTrustedIssuersToVerificationResponse({ verificationResponse, knownDIDRegistries, reloadIssuerRegistry, issuer }) + return verificationResponse; } diff --git a/src/credentialStatus.ts b/src/credentialStatus.ts index 793aa4a..e50c53d 100644 --- a/src/credentialStatus.ts +++ b/src/credentialStatus.ts @@ -11,6 +11,7 @@ export function getCredentialStatusChecker(credential: Credential) : (() => bool if (!credential.credentialStatus) { return null; } + const credentialStatuses = Array.isArray(credential.credentialStatus) ? credential.credentialStatus : [credential.credentialStatus]; diff --git a/src/issuerRegistries.ts b/src/issuerRegistries.ts index 1d5f93c..3ca1799 100644 --- a/src/issuerRegistries.ts +++ b/src/issuerRegistries.ts @@ -17,7 +17,7 @@ export async function getTrustedRegistryListForIssuer({ issuer, knownDIDRegistri // eslint-disable-next-line no-use-before-define if (reloadIssuerRegistry || registryNotYetLoaded) { - await registries.load({ config: knownDIDRegistries }) + const result = await registries.load({ config: knownDIDRegistries }) } const issuerDid = typeof issuer === 'string' ? issuer : issuer.id; const issuerInfo = registries.didEntry(issuerDid); diff --git a/test/Verify.spec.ts b/test/Verify.spec.ts index 6d736e0..34a2d33 100644 --- a/test/Verify.spec.ts +++ b/test/Verify.spec.ts @@ -51,6 +51,19 @@ describe('Verify', () => { describe('.verifyCredential', () => { + describe('with eddsa signature and', () => { + describe('with VC version 1', () => { + describe('it returns as verified', () => { + it('when status is valid', async () => { + const credential : any = getVCv2EddsaWithValidStatus() + const expectedResult = getExpectedVerifiedResult({credential, withStatus: true}) + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + expect(result).to.deep.equalInAnyOrder(expectedResult) + }) + }) + }) + + }) describe('returns general fatal errors', () => { it('when not jsonld', async () => { @@ -152,8 +165,7 @@ describe('Verify', () => { }) it('when expired with valid status', async () => { - // NOTE: this will continue to fail until we fix https://github.com/digitalcredentials/vc/issues/28 - const credential : any = getVCv1ExpiredWithValidStatus() + const credential : any = getVCv1ExpiredWithValidStatus() const expectedResult = getExpectedUnverifiedResult({credential, unVerifiedStep: 'expiration', withStatus:true}) const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define @@ -221,14 +233,7 @@ describe('Verify', () => { const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define }) - it('when status is valid for eddsa', async () => { - const credential : any = getVCv2EddsaWithValidStatus() - // const expectedResult = getExpectedVerifiedResult({credential, withStatus: true}) - const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) - console.log("result from the eddsa verification:") - console.log(result) - // expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define - }) + }) describe('returns as unverified', () => { From ac99e17b8b0f8d575f2853013113b01900939e03 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Fri, 17 Jan 2025 20:08:27 -0500 Subject: [PATCH 31/72] add list of unloaded registries to result --- package.json | 2 +- src/Verify.ts | 1 - src/issuerRegistries.ts | 29 +++++++++++++++++----------- src/test-fixtures/expectedResults.ts | 3 ++- src/types/result.ts | 17 ++++++++++++---- 5 files changed, 34 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 9917c08..9900a98 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "@digitalbazaar/data-integrity": "^2.5.0", "@digitalbazaar/eddsa-rdfc-2022-cryptosuite": "^1.2.0", "@digitalcredentials/ed25519-signature-2020": "^6.0.0", - "@digitalcredentials/issuer-registry-client": "^3.0.0", + "@digitalcredentials/issuer-registry-client": "file:../issuer-registry-client", "@digitalcredentials/jsonld-signatures": "^12.0.1", "@digitalcredentials/security-document-loader": "^6.0.1", "@digitalcredentials/vc": "^9.0.1-beta.1", diff --git a/src/Verify.ts b/src/Verify.ts index c320491..a87a3a9 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -70,7 +70,6 @@ const suite = [ed25519Suite, eddsaSuite] await addTrustedIssuersToVerificationResponse({ verificationResponse, knownDIDRegistries, reloadIssuerRegistry, issuer }) - return verificationResponse; } diff --git a/src/issuerRegistries.ts b/src/issuerRegistries.ts index 3ca1799..b9afc23 100644 --- a/src/issuerRegistries.ts +++ b/src/issuerRegistries.ts @@ -1,31 +1,37 @@ -import {RegistryClient} from '@digitalcredentials/issuer-registry-client'; -import { VerificationResponse } from './types/result.js'; +import {RegistryClient, LoadResult} from '@digitalcredentials/issuer-registry-client'; +import { VerificationResponse, RegistriesNotLoaded, RegistryListResult } from './types/result.js'; const registries = new RegistryClient() const registryNotYetLoaded = true; + /** * Checks to see if a VC's issuer appears in any of the known DID registries. * - * @returns A list of the names of the DID registries in which the issuer appears. + * @returns An object containing a list of the names of the DID registries in + * which the issuer appears and a list of registries that couldn't be loaded */ export async function getTrustedRegistryListForIssuer({ issuer, knownDIDRegistries, reloadIssuerRegistry = false }: { issuer: string | any, knownDIDRegistries: object, reloadIssuerRegistry: boolean | null -}): Promise { - +}): Promise { + let registryLoadResult:LoadResult[] = [] // eslint-disable-next-line no-use-before-define if (reloadIssuerRegistry || registryNotYetLoaded) { - const result = await registries.load({ config: knownDIDRegistries }) + registryLoadResult = await registries.load({ config: knownDIDRegistries }) } + const registriesNotLoaded : {name: string, url: string}[] = registryLoadResult.filter((registry:LoadResult)=>registry.loaded===false).map(entry=>{return {name:entry.name, url:entry.url}}) const issuerDid = typeof issuer === 'string' ? issuer : issuer.id; const issuerInfo = registries.didEntry(issuerDid); // See if the issuer DID appears in any of the known registries // If yes, assemble a list of registries in which it appears - return issuerInfo?.inRegistries + const foundInRegistries = issuerInfo?.inRegistries ? Array.from(issuerInfo.inRegistries).map(r => r.name) - : null; + : [] + + return {foundInRegistries, registriesNotLoaded} + } export async function addTrustedIssuersToVerificationResponse( {issuer, knownDIDRegistries, reloadIssuerRegistry = false, verificationResponse} :{ @@ -35,12 +41,13 @@ export async function addTrustedIssuersToVerificationResponse( {issuer, knownDID verificationResponse: VerificationResponse }) : Promise { - const foundInRegistries = await getTrustedRegistryListForIssuer( {issuer, knownDIDRegistries, reloadIssuerRegistry}); + const {foundInRegistries,registriesNotLoaded} = await getTrustedRegistryListForIssuer( {issuer, knownDIDRegistries, reloadIssuerRegistry}); const registryStep = { "id": "registered_issuer", - "valid": !!foundInRegistries, - ...(foundInRegistries && { foundInRegistries }) + "valid": !!foundInRegistries.length, + foundInRegistries, + registriesNotLoaded }; (verificationResponse.log ??= []).push(registryStep) diff --git a/src/test-fixtures/expectedResults.ts b/src/test-fixtures/expectedResults.ts index 629ebbf..7b03d13 100644 --- a/src/test-fixtures/expectedResults.ts +++ b/src/test-fixtures/expectedResults.ts @@ -21,7 +21,8 @@ const expectedResult = { "valid": true, "foundInRegistries": [ "DCC Sandbox Registry" - ] + ], + "registriesNotLoaded": [] } ] } diff --git a/src/types/result.ts b/src/types/result.ts index 440eaa6..64fe7b3 100644 --- a/src/types/result.ts +++ b/src/types/result.ts @@ -1,19 +1,28 @@ -export interface VerificationError { + +export type VerificationError = { "message": string, "isFatal": boolean, "name"?: string, stackTrace?: string } - export interface VerificationStep { + export type VerificationStep = { "id": string, "valid": boolean, "foundInRegistries"?: string[], + "registriesNotLoaded"?: RegistriesNotLoaded[] } - export interface VerificationResponse { + export type VerificationResponse = { "isFatal": boolean, "credential": object, "errors"?: VerificationError[], "log"?: VerificationStep[] - } \ No newline at end of file + } + + export type RegistryListResult = { + foundInRegistries: string[] + registriesNotLoaded: RegistriesNotLoaded[] + } + + export type RegistriesNotLoaded = {name: string, url: string} \ No newline at end of file From d9cceede8bca552a334fc6e3626329d588223cd7 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Fri, 17 Jan 2025 20:14:07 -0500 Subject: [PATCH 32/72] fix lint errors --- src/issuerRegistries.ts | 4 ++-- src/types/result.ts | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/issuerRegistries.ts b/src/issuerRegistries.ts index b9afc23..850468b 100644 --- a/src/issuerRegistries.ts +++ b/src/issuerRegistries.ts @@ -1,5 +1,5 @@ import {RegistryClient, LoadResult} from '@digitalcredentials/issuer-registry-client'; -import { VerificationResponse, RegistriesNotLoaded, RegistryListResult } from './types/result.js'; +import { VerificationResponse, RegistryListResult } from './types/result.js'; const registries = new RegistryClient() const registryNotYetLoaded = true; @@ -21,7 +21,7 @@ export async function getTrustedRegistryListForIssuer({ issuer, knownDIDRegistri if (reloadIssuerRegistry || registryNotYetLoaded) { registryLoadResult = await registries.load({ config: knownDIDRegistries }) } - const registriesNotLoaded : {name: string, url: string}[] = registryLoadResult.filter((registry:LoadResult)=>registry.loaded===false).map(entry=>{return {name:entry.name, url:entry.url}}) + const registriesNotLoaded : Array<{name: string, url: string}> = registryLoadResult.filter((registry:LoadResult)=>!registry.loaded).map(entry=>{return {name:entry.name, url:entry.url}}) const issuerDid = typeof issuer === 'string' ? issuer : issuer.id; const issuerInfo = registries.didEntry(issuerDid); // See if the issuer DID appears in any of the known registries diff --git a/src/types/result.ts b/src/types/result.ts index 64fe7b3..34f2464 100644 --- a/src/types/result.ts +++ b/src/types/result.ts @@ -1,28 +1,28 @@ -export type VerificationError = { +export interface VerificationError { "message": string, "isFatal": boolean, "name"?: string, stackTrace?: string } - export type VerificationStep = { + export interface VerificationStep { "id": string, "valid": boolean, "foundInRegistries"?: string[], "registriesNotLoaded"?: RegistriesNotLoaded[] } - export type VerificationResponse = { + export interface VerificationResponse { "isFatal": boolean, "credential": object, "errors"?: VerificationError[], "log"?: VerificationStep[] } - export type RegistryListResult = { + export interface RegistryListResult { foundInRegistries: string[] registriesNotLoaded: RegistriesNotLoaded[] } - export type RegistriesNotLoaded = {name: string, url: string} \ No newline at end of file + export interface RegistriesNotLoaded {name: string, url: string} \ No newline at end of file From 5a965b48086f72d77716cbed9f5e6d58dd2b2be9 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Sat, 18 Jan 2025 17:01:19 -0500 Subject: [PATCH 33/72] update registry client to new beta version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9900a98..a6b93b6 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "@digitalbazaar/data-integrity": "^2.5.0", "@digitalbazaar/eddsa-rdfc-2022-cryptosuite": "^1.2.0", "@digitalcredentials/ed25519-signature-2020": "^6.0.0", - "@digitalcredentials/issuer-registry-client": "file:../issuer-registry-client", + "@digitalcredentials/issuer-registry-client": "^3.0.1-beta.1", "@digitalcredentials/jsonld-signatures": "^12.0.1", "@digitalcredentials/security-document-loader": "^6.0.1", "@digitalcredentials/vc": "^9.0.1-beta.1", From dd9f0d54346fb5fcd0a73597a1f9773bc5aafc8f Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Sat, 18 Jan 2025 19:15:08 -0500 Subject: [PATCH 34/72] add tests for bad registries and missing matches --- test/Verify.spec.ts | 47 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/test/Verify.spec.ts b/test/Verify.spec.ts index 34a2d33..7daf374 100644 --- a/test/Verify.spec.ts +++ b/test/Verify.spec.ts @@ -171,6 +171,53 @@ describe('Verify', () => { expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define }) + it.only('when no matching registry', async () => { + const credential : any = getVCv1ValidStatus() + const noMatchingRegistryList = JSON.parse(JSON.stringify(knownDIDRegistries)) + // set the one matching registry to a url that won't load + noMatchingRegistryList[1].url = 'https://onlynoyrt.com/registry.json' + const expectedResult : any = getExpectedVerifiedResult({credential, withStatus: true}) + const expectedRsultRegistryLogEntry = expectedResult.log.find((entry:any)=>entry.id==='registered_issuer') + expectedRsultRegistryLogEntry.registriesNotLoaded = [ + { + "name": "DCC Pilot Registry", + "url": "https://onlynoyrt.com/registry.json" + } + ] + expectedRsultRegistryLogEntry.valid = false; + + const result = await verifyCredential({credential, reloadIssuerRegistry: true, knownDIDRegistries: noMatchingRegistryList}) + console.log(JSON.parse(JSON.stringify(result))) + assert.ok(result.log); + }) + + + }) + + describe('returns accurate registry list', () => { + + it('for non-existant registry url', async () => { + const credential : any = getVCv1ValidStatus() + const badRegistryList = JSON.parse(JSON.stringify(knownDIDRegistries)) + badRegistryList[0].url = 'https://onlynoyrt.com/registry.json' + const expectedResult : any = getExpectedVerifiedResult({credential, withStatus: true}) + expectedResult.log.find((entry:any)=>entry.id==='registered_issuer').registriesNotLoaded = [ + { + "name": "DCC Pilot Registry", + "url": "https://onlynoyrt.com/registry.json" + } + ] + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries: badRegistryList}) + expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define + }) + + it('when all registries exist', async () => { + const credential : any = getVCv1ValidStatus() + const expectedResult = getExpectedVerifiedResult({credential, withStatus: true}) + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define + }) + }) From ee4f5ff73f92dd2637c592f5edc455419aa80fd6 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Sat, 18 Jan 2025 19:15:54 -0500 Subject: [PATCH 35/72] enable all tests --- test/Verify.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Verify.spec.ts b/test/Verify.spec.ts index 7daf374..7d9c7b6 100644 --- a/test/Verify.spec.ts +++ b/test/Verify.spec.ts @@ -171,7 +171,7 @@ describe('Verify', () => { expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define }) - it.only('when no matching registry', async () => { + it('when no matching registry', async () => { const credential : any = getVCv1ValidStatus() const noMatchingRegistryList = JSON.parse(JSON.stringify(knownDIDRegistries)) // set the one matching registry to a url that won't load From 8fde1f04bc65a8da590c589d90610700b10cecef Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Sun, 19 Jan 2025 13:10:03 -0500 Subject: [PATCH 36/72] update tests --- test/Verify.spec.ts | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/test/Verify.spec.ts b/test/Verify.spec.ts index 7d9c7b6..c668027 100644 --- a/test/Verify.spec.ts +++ b/test/Verify.spec.ts @@ -187,7 +187,6 @@ describe('Verify', () => { expectedRsultRegistryLogEntry.valid = false; const result = await verifyCredential({credential, reloadIssuerRegistry: true, knownDIDRegistries: noMatchingRegistryList}) - console.log(JSON.parse(JSON.stringify(result))) assert.ok(result.log); }) @@ -196,7 +195,7 @@ describe('Verify', () => { describe('returns accurate registry list', () => { - it('for non-existant registry url', async () => { + it('when one registry url does not exist', async () => { const credential : any = getVCv1ValidStatus() const badRegistryList = JSON.parse(JSON.stringify(knownDIDRegistries)) badRegistryList[0].url = 'https://onlynoyrt.com/registry.json' @@ -211,6 +210,25 @@ describe('Verify', () => { expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define }) + it('when two registry urls do not exist', async () => { + const credential : any = getVCv1ValidStatus() + const badRegistryList = JSON.parse(JSON.stringify(knownDIDRegistries)) + badRegistryList[0].url = 'https://onlynoyrt.com/registry.json' + badRegistryList[2].url = 'https://onlynoyrrrt.com/registry.json' + const expectedResult : any = getExpectedVerifiedResult({credential, withStatus: true}) + expectedResult.log.find((entry:any)=>entry.id==='registered_issuer').registriesNotLoaded = [ + { + "name": "DCC Community Registry", + "url": "https://onlynoyrrrt.com/registry.json" + }, + { + "name": "DCC Pilot Registry", + "url": "https://onlynoyrt.com/registry.json" + } + ] + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries: badRegistryList}) + expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define + }) it('when all registries exist', async () => { const credential : any = getVCv1ValidStatus() const expectedResult = getExpectedVerifiedResult({credential, withStatus: true}) From 37e4fbb1077d6e9d1145fb993bdc3f8c44d8c6a2 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Sun, 19 Jan 2025 14:12:06 -0500 Subject: [PATCH 37/72] add handling for notFound status list --- src/Verify.ts | 24 +++++++++++++++++++++--- src/test-fixtures/expectedResults.ts | 3 +-- src/types/result.ts | 8 ++++---- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/Verify.ts b/src/Verify.ts index a87a3a9..783f14d 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -39,14 +39,16 @@ const suite = [ed25519Suite, eddsaSuite] checkStatus }); + processAnyStatusError({verificationResponse, statusResult: verificationResponse.statusResult}); + // remove things we don't need from the result or that are duplicated elsewhere delete verificationResponse.results delete verificationResponse.statusResult delete verificationResponse.verified - - verificationResponse.isFatal = false verificationResponse.credential = credential + verificationResponse.isFatal = false + if (verificationResponse.error) { if (verificationResponse.error.log) { // move the log out of the error to the response, since it @@ -74,7 +76,7 @@ const suite = [ed25519Suite, eddsaSuite] } function buildFatalErrorObject(fatalErrorMessage: string, name: string, credential: Credential, stackTrace: string | null): VerificationResponse { - return { credential, isFatal: true, errors: [{ name, message: fatalErrorMessage, isFatal: true, ...stackTrace ? { stackTrace } : null }] } + return { credential, isFatal: true, errors: [{ name, message: fatalErrorMessage, ...stackTrace ? { stackTrace } : null }] } } function checkForFatalErrors(credential: Credential): VerificationResponse | null { @@ -114,6 +116,22 @@ function checkForFatalErrors(credential: Credential): VerificationResponse | nul return null } +function processAnyStatusError( {verificationResponse, statusResult} :{ + verificationResponse: VerificationResponse, + statusResult: any +}) : void + { + if (statusResult?.error?.cause?.message?.startsWith('NotFoundError')) { + const statusStep = { + "id": "revocation_status", + "error": { + name: 'status_list_not_found', + message: statusResult.error.cause.message + } + }; + (verificationResponse.log ??= []).push(statusStep) + } +} // import { purposes } from '@digitalcredentials/jsonld-signatures'; // import { VerifiablePresentation, PresentationError } from './types/presentation'; diff --git a/src/test-fixtures/expectedResults.ts b/src/test-fixtures/expectedResults.ts index 7b03d13..7284a5e 100644 --- a/src/test-fixtures/expectedResults.ts +++ b/src/test-fixtures/expectedResults.ts @@ -33,8 +33,7 @@ const expectedResult = { errors: [ { name: 'error name goes here, e.g., no_proof', - message: 'error message goes here', - isFatal: true + message: 'error message goes here' } ] } diff --git a/src/types/result.ts b/src/types/result.ts index 34f2464..6da9be7 100644 --- a/src/types/result.ts +++ b/src/types/result.ts @@ -1,16 +1,16 @@ export interface VerificationError { "message": string, - "isFatal": boolean, "name"?: string, - stackTrace?: string + "stackTrace"?: string } export interface VerificationStep { "id": string, - "valid": boolean, + "valid"?: boolean, "foundInRegistries"?: string[], - "registriesNotLoaded"?: RegistriesNotLoaded[] + "registriesNotLoaded"?: RegistriesNotLoaded[], + "error"?: VerificationError } export interface VerificationResponse { From 39bfd3e9c6282a1773439a908a6f955d6c3178e6 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Sun, 19 Jan 2025 15:24:46 -0500 Subject: [PATCH 38/72] update unit tests --- src/test-fixtures/vc.ts | 6 ++ .../v2/v2DoubleSigWithBadStatusUrl.ts | 62 +++++++++++++++++++ test/Verify.spec.ts | 23 ++++++- 3 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 src/test-fixtures/verifiableCredentials/eddsaAndEd25519/v2/v2DoubleSigWithBadStatusUrl.ts diff --git a/src/test-fixtures/vc.ts b/src/test-fixtures/vc.ts index f4fbfec..71e8d63 100644 --- a/src/test-fixtures/vc.ts +++ b/src/test-fixtures/vc.ts @@ -11,6 +11,7 @@ import { v1Expired } from "./verifiableCredentials/v1/v1Expired.js" import { v1ExpiredWithValidStatus } from "./verifiableCredentials/v1/v1ExpiredWithValidStatus.js" import { v2EddsaWithValidStatus } from "./verifiableCredentials/eddsa/v2/v2EddsaWithValidStatus.js" +import { v2DoubleSigWithBadStatusUrl } from "./verifiableCredentials/eddsaAndEd25519/v2/v2DoubleSigWithBadStatusUrl.js" const getVCv1 = (): any => { return JSON.parse(JSON.stringify(v1NoStatus)) @@ -112,6 +113,10 @@ const getVCv2EddsaWithValidStatus = (): any => { return JSON.parse(JSON.stringify(v2EddsaWithValidStatus)) } +const getVCv2DoubleSigWithBadStatusUrl = (): any => { + return JSON.parse(JSON.stringify(v2DoubleSigWithBadStatusUrl)) +} + export { @@ -119,6 +124,7 @@ export { getCredentialWithoutVCContext, getVCv2EddsaWithValidStatus, + getVCv2DoubleSigWithBadStatusUrl, getVCv2, getVCv2Expired, diff --git a/src/test-fixtures/verifiableCredentials/eddsaAndEd25519/v2/v2DoubleSigWithBadStatusUrl.ts b/src/test-fixtures/verifiableCredentials/eddsaAndEd25519/v2/v2DoubleSigWithBadStatusUrl.ts new file mode 100644 index 0000000..f5325b5 --- /dev/null +++ b/src/test-fixtures/verifiableCredentials/eddsaAndEd25519/v2/v2DoubleSigWithBadStatusUrl.ts @@ -0,0 +1,62 @@ +export const v2DoubleSigWithBadStatusUrl = { + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "name": "Teamwork Badge", + "issuer": { + "type": [ + "Profile" + ], + "name": "Example Corp", + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q" + }, + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "validFrom": "2010-01-01T00:00:00Z", + "credentialSubject": { + "type": [ + "AchievementSubject" + ], + "name": "Taylor Tuna", + "achievement": { + "id": "https://example.com/achievements/21st-century-skills/teamwork", + "type": [ + "Achievement" + ], + "name": "Masters - v2 - unrevoked", + "criteria": { + "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." + }, + "description": "This badge recognizes the development of the capacity to collaborate within a group environment." + } + }, + "id": "urn:uuid:677fe8a6cacf98774d482d07", + "credentialStatus": { + "id": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5VK8CbZ1GjycuPombrj#9", + "type": "BitstringStatusListEntry", + "statusPurpose": "revocation", + "statusListCredential": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5VK8CbZ1GjycuPombrj", + "statusListIndex": "9" + }, + "proof": [ + { + "type": "Ed25519Signature2020", + "created": "2025-01-19T20:13:44Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "z5fqVd2F99EP5yDuUXmGpDvQS7tkz981ZL4qYkHv6ienYmeQy5za5a2qjgZfQWu65DPD9vSxyjgqACt1sHSmEyq9b" + }, + { + "type": "DataIntegrityProof", + "created": "2025-01-19T20:13:44Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "cryptosuite": "eddsa-rdfc-2022", + "proofPurpose": "assertionMethod", + "proofValue": "z4ybSpzWGcrs8HrZ8KdiH2CHAetsq6e926bi8KmEfWPMsWPGbfpZDxgKSaGcCmCwmD92qEpRrpr9bM476BhfbhkDg" + } + ] +} \ No newline at end of file diff --git a/test/Verify.spec.ts b/test/Verify.spec.ts index c668027..b9c0aa9 100644 --- a/test/Verify.spec.ts +++ b/test/Verify.spec.ts @@ -21,7 +21,8 @@ import { getVCv2ExpiredAndTampered, getVCv1ExpiredWithValidStatus, getVCv2ExpiredWithValidStatus, - getVCv2EddsaWithValidStatus + getVCv2EddsaWithValidStatus, + getVCv2DoubleSigWithBadStatusUrl } from '../src/test-fixtures/vc.js' import { knownDIDRegistries } from '../.knownDidRegistries.js'; import { @@ -51,6 +52,26 @@ describe('Verify', () => { describe('.verifyCredential', () => { + describe('ed25519 and eddsa signature', () => { + describe('with VC version 2', () => { + describe('returns notfound error', () => { + it('when statuslist url is unreachable', async () => { + const credential : any = getVCv2DoubleSigWithBadStatusUrl() + const expectedResult = getExpectedVerifiedResult({credential, withStatus: false}) + expectedResult.log?.push( + { + "id": "revocation_status", + "error": { + "name": "status_list_not_found", + "message": "NotFoundError loading \"https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5VK8CbZ1GjycuPombrj\": Request failed with status code 404 Not Found: GET https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5VK8CbZ1GjycuPombrj" + } + }) + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + expect(result).to.deep.equalInAnyOrder(expectedResult) + }) + }) + }) +}) describe('with eddsa signature and', () => { describe('with VC version 1', () => { describe('it returns as verified', () => { From 763b3426912318488fefe6e854d3a3b39ea0be98 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Mon, 20 Jan 2025 09:21:40 -0500 Subject: [PATCH 39/72] add did:web fixture for testing --- src/test-fixtures/did/.well-known/did.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/test-fixtures/did/.well-known/did.json diff --git a/src/test-fixtures/did/.well-known/did.json b/src/test-fixtures/did/.well-known/did.json new file mode 100644 index 0000000..6483cd5 --- /dev/null +++ b/src/test-fixtures/did/.well-known/did.json @@ -0,0 +1,16 @@ +{ + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/ed25519-2020/v1", + "https://w3id.org/security/suites/x25519-2020/v1" + ], + "id": "did:web:raw.githubusercontent.com:digitalcredentials:verifier-core:refs:heads:main:src:text-fixtures:did", + "assertionMethod": [ + { + "id": "did:web:raw.githubusercontent.com:digitalcredentials:verifier-core:refs:heads:main:src:text-fixtures:did#z6MkfGZKFTyxiH9HgFUHbPQigEWh8PtFaRkESt9oQLiTvhVq", + "type": "Ed25519VerificationKey2020", + "controller": "did:web:raw.githubusercontent.com:jchartrand:didWebTest:main", + "publicKeyMultibase": "z6MkfGZKFTyxiH9HgFUHbPQigEWh8PtFaRkESt9oQLiTvhVq" + } + ] +} \ No newline at end of file From 2331e3aefdd289825f80705ddf57b931a76ec169 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Mon, 20 Jan 2025 20:07:42 -0500 Subject: [PATCH 40/72] add did:web tests --- src/Verify.ts | 3 +- src/test-fixtures/did/.well-known/did.json | 16 ------ src/test-fixtures/vc.ts | 7 +++ .../v2/didWeb/v2didWebWithValidStatus.ts | 52 +++++++++++++++++++ test/Verify.spec.ts | 39 +++++++++++++- 5 files changed, 99 insertions(+), 18 deletions(-) delete mode 100644 src/test-fixtures/did/.well-known/did.json create mode 100644 src/test-fixtures/verifiableCredentials/v2/didWeb/v2didWebWithValidStatus.ts diff --git a/src/Verify.ts b/src/Verify.ts index 783f14d..1c46874 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -36,7 +36,8 @@ const suite = [ed25519Suite, eddsaSuite] credential, suite, documentLoader, - checkStatus + checkStatus, + verifyMatchingIssuers: false }); processAnyStatusError({verificationResponse, statusResult: verificationResponse.statusResult}); diff --git a/src/test-fixtures/did/.well-known/did.json b/src/test-fixtures/did/.well-known/did.json deleted file mode 100644 index 6483cd5..0000000 --- a/src/test-fixtures/did/.well-known/did.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "@context": [ - "https://www.w3.org/ns/did/v1", - "https://w3id.org/security/suites/ed25519-2020/v1", - "https://w3id.org/security/suites/x25519-2020/v1" - ], - "id": "did:web:raw.githubusercontent.com:digitalcredentials:verifier-core:refs:heads:main:src:text-fixtures:did", - "assertionMethod": [ - { - "id": "did:web:raw.githubusercontent.com:digitalcredentials:verifier-core:refs:heads:main:src:text-fixtures:did#z6MkfGZKFTyxiH9HgFUHbPQigEWh8PtFaRkESt9oQLiTvhVq", - "type": "Ed25519VerificationKey2020", - "controller": "did:web:raw.githubusercontent.com:jchartrand:didWebTest:main", - "publicKeyMultibase": "z6MkfGZKFTyxiH9HgFUHbPQigEWh8PtFaRkESt9oQLiTvhVq" - } - ] -} \ No newline at end of file diff --git a/src/test-fixtures/vc.ts b/src/test-fixtures/vc.ts index 71e8d63..a03606e 100644 --- a/src/test-fixtures/vc.ts +++ b/src/test-fixtures/vc.ts @@ -13,6 +13,8 @@ import { v1ExpiredWithValidStatus } from "./verifiableCredentials/v1/v1ExpiredWi import { v2EddsaWithValidStatus } from "./verifiableCredentials/eddsa/v2/v2EddsaWithValidStatus.js" import { v2DoubleSigWithBadStatusUrl } from "./verifiableCredentials/eddsaAndEd25519/v2/v2DoubleSigWithBadStatusUrl.js" +import { v2didWebWithValidStatus } from "./verifiableCredentials/v2/didWeb/v2didWebWithValidStatus.js" + const getVCv1 = (): any => { return JSON.parse(JSON.stringify(v1NoStatus)) } @@ -117,6 +119,10 @@ const getVCv2DoubleSigWithBadStatusUrl = (): any => { return JSON.parse(JSON.stringify(v2DoubleSigWithBadStatusUrl)) } +const getVCv2DidWebWithValidStatus = (): any => { + return JSON.parse(JSON.stringify(v2didWebWithValidStatus)) +} + export { @@ -135,6 +141,7 @@ export { getVCv2ExpiredWithValidStatus, getVCv2NoProof, getVCv2NonURIId, + getVCv2DidWebWithValidStatus, getVCv1, getVCv1Expired, diff --git a/src/test-fixtures/verifiableCredentials/v2/didWeb/v2didWebWithValidStatus.ts b/src/test-fixtures/verifiableCredentials/v2/didWeb/v2didWebWithValidStatus.ts new file mode 100644 index 0000000..1463725 --- /dev/null +++ b/src/test-fixtures/verifiableCredentials/v2/didWeb/v2didWebWithValidStatus.ts @@ -0,0 +1,52 @@ +export const v2didWebWithValidStatus = { + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "name": "Teamwork Badge", + "issuer": { + "type": [ + "Profile" + ], + "name": "Example Corp", + "id": "did:web:digitalcredentials.github.io:dcc-did-web" + }, + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "validFrom": "2010-01-01T00:00:00Z", + "credentialSubject": { + "type": [ + "AchievementSubject" + ], + "name": "Taylor Tuna", + "achievement": { + "id": "https://example.com/achievements/21st-century-skills/teamwork", + "type": [ + "Achievement" + ], + "name": "Masters - v2 - unrevoked", + "criteria": { + "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." + }, + "description": "This badge recognizes the development of the capacity to collaborate within a group environment." + } + }, + "id": "urn:uuid:677fe8a6cacf98774d482d07", + "credentialStatus": { + "id": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj#9", + "type": "BitstringStatusListEntry", + "statusPurpose": "revocation", + "statusListCredential": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj", + "statusListIndex": "9" + }, + "proof": { + "type": "Ed25519Signature2020", + "created": "2025-01-20T21:40:14Z", + "verificationMethod": "did:web:digitalcredentials.github.io:dcc-did-web#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "z5ty6xS56YfvmD4wpCEBVnHv8a26Y6nmN1nYapS2mijK6WGCDXpjcFWKj15sYarJvbzvYkvp3Wwnpq54PT4VTdS5a" + } +} \ No newline at end of file diff --git a/test/Verify.spec.ts b/test/Verify.spec.ts index b9c0aa9..52cecc3 100644 --- a/test/Verify.spec.ts +++ b/test/Verify.spec.ts @@ -22,7 +22,8 @@ import { getVCv1ExpiredWithValidStatus, getVCv2ExpiredWithValidStatus, getVCv2EddsaWithValidStatus, - getVCv2DoubleSigWithBadStatusUrl + getVCv2DoubleSigWithBadStatusUrl, + getVCv2DidWebWithValidStatus } from '../src/test-fixtures/vc.js' import { knownDIDRegistries } from '../.knownDidRegistries.js'; import { @@ -48,8 +49,27 @@ tests to add: */ + describe('Verify', () => { + const originalLogFunction = console.log; + let output:string; + + beforeEach(function(done) { + output = ''; + console.log = (msg) => { + output += msg + '\n'; + }; + done() + }); + + afterEach(function() { + console.log = originalLogFunction; // undo dummy log function + if (this?.currentTest?.state === 'failed') { + console.log(output); + } + }); + describe('.verifyCredential', () => { describe('ed25519 and eddsa signature', () => { @@ -319,6 +339,23 @@ describe('Verify', () => { const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define }) + describe('with did:web issuer', () => { + + it('when status is valid', async () => { + const credential : any = getVCv2DidWebWithValidStatus() + const expectedResult = getExpectedVerifiedResult({credential, withStatus: true}) + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define + }) + + it('with different issuer for vc and statusList ', async () => { + const credential : any = getVCv2DidWebWithValidStatus() + const expectedResult = getExpectedVerifiedResult({credential, withStatus: true}) + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define + }) + + }) }) From 243a8e7d5a3fcea455bbc85cfca1d7b56fb1e10d Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Tue, 21 Jan 2025 18:51:55 -0500 Subject: [PATCH 41/72] add check for unresolved did:web --- src/Verify.ts | 118 ++++++++++++------ src/test-fixtures/vc.ts | 6 + .../v2/didWeb/v2WithBadDidWeb.ts | 45 +++++++ test/Verify.spec.ts | 43 ++++--- 4 files changed, 157 insertions(+), 55 deletions(-) create mode 100644 src/test-fixtures/verifiableCredentials/v2/didWeb/v2WithBadDidWeb.ts diff --git a/src/Verify.ts b/src/Verify.ts index 1c46874..bd8c520 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -14,65 +14,51 @@ const documentLoader = securityLoader({ fetchRemoteContexts: true }).build(); // for verifying eddsa-2022 signatures const eddsaSuite = new DataIntegrityProof({ cryptosuite: eddsaRdfc2022CryptoSuite }); - // for verifying ed25519-2020 signatures const ed25519Suite = new Ed25519Signature2020(); export async function verifyCredential({ credential, knownDIDRegistries, reloadIssuerRegistry = true }: { credential: Credential, knownDIDRegistries: object, reloadIssuerRegistry: boolean }): Promise { - const fatalError = checkForFatalErrors(credential) + const fatalCredentialError = checkForFatalCredentialErrors(credential) - if (fatalError) { - return fatalError + if (fatalCredentialError) { + return fatalCredentialError } -const suite = [ed25519Suite, eddsaSuite] - /* const suite = (credential?.proof?.cryptosuite === 'eddsa-rdfc-2022') ? - eddsaSuite : ed25519Suite */ + // add both suites - the vc lib will use whichever is appropriate + const suite = [ed25519Suite, eddsaSuite] - const checkStatus = getCredentialStatusChecker(credential) + // a statusCheck is returned only if the credential has a status + // that needs checking, otherwise null + const statusChecker = getCredentialStatusChecker(credential) - const verificationResponse = await vc.verifyCredential({ + const verificationResponse = await vc.verifyCredential({ credential, suite, documentLoader, - checkStatus, + checkStatus: statusChecker, verifyMatchingIssuers: false }); - processAnyStatusError({verificationResponse, statusResult: verificationResponse.statusResult}); - + + processAnyStatusError({ verificationResponse, statusResult: verificationResponse.statusResult }); + // remove things we don't need from the result or that are duplicated elsewhere delete verificationResponse.results delete verificationResponse.statusResult delete verificationResponse.verified + // add things we always want in the response verificationResponse.credential = credential - verificationResponse.isFatal = false - - if (verificationResponse.error) { - if (verificationResponse.error.log) { - // move the log out of the error to the response, since it - // isn't part of the error, but rather the true/false values - // for each step in verification - verificationResponse.log = verificationResponse.error.log - // delete the error, because again, this wasn't an error, just - // a false value on one of the steps - delete verificationResponse.error - } else if (verificationResponse?.error?.name === 'VerificationError') { - // this is in fact an error so return a fatal error. - // this means something happened (likely a bad signature) that prevents us from - // saying anything conclusive about the various steps in verification - const fatalErrorMessage = 'The signature is not valid.' - const stackTrace = verificationResponse?.error?.errors?.stack - return buildFatalErrorObject(fatalErrorMessage, "invalid_signature", credential, stackTrace) - } + + const fatalSignatureError = processAnySignatureError({ verificationResponse, credential }) + if (fatalSignatureError) { + return fatalSignatureError } const { issuer } = credential - await addTrustedIssuersToVerificationResponse({ verificationResponse, knownDIDRegistries, reloadIssuerRegistry, issuer }) - + return verificationResponse; } @@ -80,7 +66,7 @@ function buildFatalErrorObject(fatalErrorMessage: string, name: string, credenti return { credential, isFatal: true, errors: [{ name, message: fatalErrorMessage, ...stackTrace ? { stackTrace } : null }] } } -function checkForFatalErrors(credential: Credential): VerificationResponse | null { +function checkForFatalCredentialErrors(credential: Credential): VerificationResponse | null { const validVCContexts = [ 'https://www.w3.org/2018/credentials/v1', 'https://www.w3.org/ns/credentials/v2' @@ -117,23 +103,77 @@ function checkForFatalErrors(credential: Credential): VerificationResponse | nul return null } -function processAnyStatusError( {verificationResponse, statusResult} :{ +function processAnyStatusError({ verificationResponse, statusResult }: { verificationResponse: VerificationResponse, statusResult: any -}) : void - { +}): void { if (statusResult?.error?.cause?.message?.startsWith('NotFoundError')) { const statusStep = { "id": "revocation_status", "error": { name: 'status_list_not_found', - message: statusResult.error.cause.message + message: statusResult.error.cause.message } - }; + }; (verificationResponse.log ??= []).push(statusStep) } } +function processAnySignatureError({ verificationResponse, credential }: { verificationResponse: any, credential: Credential }) { + if (verificationResponse.error) { + + if (verificationResponse?.error?.name === 'VerificationError') { + // Can't validate the signature. + // Either a bad signature or maybe a did:web that can't + // be resolved. Because we can't validate the signature, we + // can't therefore say anything conclusive about the various + // steps in verification. + // So, return a fatal error and no log (because we can't say + // anything meaningful about the steps in the log) + let fatalErrorMessage = "" + let errorName = "" + // check to see if the error is http related + const httpError = verificationResponse.error.errors.find((error: any) => error.name === 'HTTPError') + if (httpError) { + // was it caused by a did:web that couldn't be resolved??? + const issuerDID: string = (((credential.issuer) as any).id) || credential.issuer + if (issuerDID.toLowerCase().startsWith('did:web')) { + // change did to a url: + const didUrl = issuerDID.slice(8).replaceAll(':', '/').toLowerCase() + if (httpError.requestUrl.toLowerCase().includes(didUrl)) { + fatalErrorMessage = `The signature could not be checked because the public signing key could not be retrieved from ${httpError.requestUrl}` + errorName = 'did_web_unresolved' + } else { + // some other kind of http error + fatalErrorMessage = 'An http error prevented the signature check.' + errorName = 'http_error_with_signature_check' + } + } + } else { + // not an http error, so likely bad signature + fatalErrorMessage = 'The signature is not valid.' + errorName = 'invalid_signature' + } + const stackTrace = verificationResponse?.error?.errors?.stack + return buildFatalErrorObject(fatalErrorMessage, errorName, credential, stackTrace) + + + } else if (verificationResponse.error.log) { + // There wasn't actually an error, it is just that one of the + // steps returned false. + // So move the log out of the error to the response, since it + // isn't part of the error + verificationResponse.log = verificationResponse.error.log + // delete the error, because again, this wasn't an error, just + // a false value on one of the steps + delete verificationResponse.error + } + } + } + + + + // import { purposes } from '@digitalcredentials/jsonld-signatures'; // import { VerifiablePresentation, PresentationError } from './types/presentation'; // const presentationPurpose = new purposes.AssertionProofPurpose(); diff --git a/src/test-fixtures/vc.ts b/src/test-fixtures/vc.ts index a03606e..6df4ed8 100644 --- a/src/test-fixtures/vc.ts +++ b/src/test-fixtures/vc.ts @@ -14,6 +14,7 @@ import { v2EddsaWithValidStatus } from "./verifiableCredentials/eddsa/v2/v2Eddsa import { v2DoubleSigWithBadStatusUrl } from "./verifiableCredentials/eddsaAndEd25519/v2/v2DoubleSigWithBadStatusUrl.js" import { v2didWebWithValidStatus } from "./verifiableCredentials/v2/didWeb/v2didWebWithValidStatus.js" +import { v2WithBadDidWeb } from "./verifiableCredentials/v2/didWeb/v2WithBadDidWeb.js" const getVCv1 = (): any => { return JSON.parse(JSON.stringify(v1NoStatus)) @@ -123,6 +124,10 @@ const getVCv2DidWebWithValidStatus = (): any => { return JSON.parse(JSON.stringify(v2didWebWithValidStatus)) } +const getVCv2WithBadDidWebUrl = (): any => { + return JSON.parse(JSON.stringify(v2WithBadDidWeb)) +} + export { @@ -142,6 +147,7 @@ export { getVCv2NoProof, getVCv2NonURIId, getVCv2DidWebWithValidStatus, + getVCv2WithBadDidWebUrl, getVCv1, getVCv1Expired, diff --git a/src/test-fixtures/verifiableCredentials/v2/didWeb/v2WithBadDidWeb.ts b/src/test-fixtures/verifiableCredentials/v2/didWeb/v2WithBadDidWeb.ts new file mode 100644 index 0000000..26d4f6b --- /dev/null +++ b/src/test-fixtures/verifiableCredentials/v2/didWeb/v2WithBadDidWeb.ts @@ -0,0 +1,45 @@ +export const v2WithBadDidWeb = { + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "name": "Teamwork Badge", + "issuer": { + "type": [ + "Profile" + ], + "name": "Example Corp", + "id": "did:web:digitalcredentials.github.io:dcc-did-web-bad" + }, + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "validFrom": "2010-01-01T00:00:00Z", + "credentialSubject": { + "type": [ + "AchievementSubject" + ], + "name": "Taylor Tuna", + "achievement": { + "id": "https://example.com/achievements/21st-century-skills/teamwork", + "type": [ + "Achievement" + ], + "name": "Masters - v2 - unrevoked", + "criteria": { + "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." + }, + "description": "This badge recognizes the development of the capacity to collaborate within a group environment." + } + }, + "id": "urn:uuid:677fe8a6cacf98774d482d07", + "proof": { + "type": "Ed25519Signature2020", + "created": "2025-01-21T17:20:42Z", + "verificationMethod": "did:web:digitalcredentials.github.io:dcc-did-web-bad#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "z3exwX8VPoA2qME3WQdZ5RvCY9ahbUDDAXkm8Fb4zuCE5dnMWUVZrEN1q5hVmgroQq65WUwNiGHVgr2av5r7wRtb7" + } +} \ No newline at end of file diff --git a/test/Verify.spec.ts b/test/Verify.spec.ts index 52cecc3..d5bedaa 100644 --- a/test/Verify.spec.ts +++ b/test/Verify.spec.ts @@ -23,7 +23,8 @@ import { getVCv2ExpiredWithValidStatus, getVCv2EddsaWithValidStatus, getVCv2DoubleSigWithBadStatusUrl, - getVCv2DidWebWithValidStatus + getVCv2DidWebWithValidStatus, + getVCv2WithBadDidWebUrl } from '../src/test-fixtures/vc.js' import { knownDIDRegistries } from '../.knownDidRegistries.js'; import { @@ -74,21 +75,22 @@ describe('Verify', () => { describe('ed25519 and eddsa signature', () => { describe('with VC version 2', () => { - describe('returns notfound error', () => { - it('when statuslist url is unreachable', async () => { - const credential : any = getVCv2DoubleSigWithBadStatusUrl() - const expectedResult = getExpectedVerifiedResult({credential, withStatus: false}) - expectedResult.log?.push( - { - "id": "revocation_status", - "error": { - "name": "status_list_not_found", - "message": "NotFoundError loading \"https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5VK8CbZ1GjycuPombrj\": Request failed with status code 404 Not Found: GET https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5VK8CbZ1GjycuPombrj" - } + describe('returns log error', () => { + it('when statuslist url is unreachable', async () => { + const credential : any = getVCv2DoubleSigWithBadStatusUrl() + const expectedResult = getExpectedVerifiedResult({credential, withStatus: false}) + expectedResult.log?.push( + { + "id": "revocation_status", + "error": { + "name": "status_list_not_found", + "message": "NotFoundError loading \"https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5VK8CbZ1GjycuPombrj\": Request failed with status code 404 Not Found: GET https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5VK8CbZ1GjycuPombrj" + } + }) + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + expect(result).to.deep.equalInAnyOrder(expectedResult) }) - const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) - expect(result).to.deep.equalInAnyOrder(expectedResult) - }) + }) }) }) @@ -105,7 +107,7 @@ describe('Verify', () => { }) }) - describe('returns general fatal errors', () => { + describe('returns fatal errors', () => { it('when not jsonld', async () => { const credential : any = getCredentialWithoutContext() @@ -330,6 +332,15 @@ describe('Verify', () => { expect(result).to.deep.equalInAnyOrder(expectedResult) }) + it('when did:web url is unreachable', async () => { + const credential : any = getVCv2WithBadDidWebUrl() + const errorName = "did_web_unresolved" + const errorMessage = "The signature could not be checked because the public signing key could not be retrieved from https://digitalcredentials.github.io/dcc-did-web-bad/did.json" + const expectedResult = getExpectedFatalResult({credential, errorName, errorMessage}) + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + expect(result).to.deep.equalInAnyOrder(expectedResult) + }) + }) describe('returns as verified', () => { From 1bbbd322020fc32f94b209e8b4ada5246fdbf99b Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Tue, 21 Jan 2025 20:06:51 -0500 Subject: [PATCH 42/72] fix linting --- src/Verify.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Verify.ts b/src/Verify.ts index bd8c520..de961c2 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -119,7 +119,7 @@ function processAnyStatusError({ verificationResponse, statusResult }: { } } -function processAnySignatureError({ verificationResponse, credential }: { verificationResponse: any, credential: Credential }) { +function processAnySignatureError({ verificationResponse, credential }: { verificationResponse: any, credential: Credential }) : null | VerificationResponse { if (verificationResponse.error) { if (verificationResponse?.error?.name === 'VerificationError') { @@ -141,7 +141,7 @@ function processAnySignatureError({ verificationResponse, credential }: { verifi // change did to a url: const didUrl = issuerDID.slice(8).replaceAll(':', '/').toLowerCase() if (httpError.requestUrl.toLowerCase().includes(didUrl)) { - fatalErrorMessage = `The signature could not be checked because the public signing key could not be retrieved from ${httpError.requestUrl}` + fatalErrorMessage = `The signature could not be checked because the public signing key could not be retrieved from ${httpError.requestUrl as string}` errorName = 'did_web_unresolved' } else { // some other kind of http error @@ -169,6 +169,7 @@ function processAnySignatureError({ verificationResponse, credential }: { verifi delete verificationResponse.error } } + return null } From 05def427258434c4a2668d66456d5b2685295524 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Wed, 22 Jan 2025 11:12:53 -0500 Subject: [PATCH 43/72] remove unnecessary did log entry; update README --- README.md | 85 +++++++++++++++++----------- src/Verify.ts | 32 ++++++----- src/test-fixtures/expectedResults.ts | 4 -- test/Verify.spec.ts | 52 ++++++++++------- 4 files changed, 99 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index 171e8d1..fcb3695 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ As of January 2025 issuers are trusted if they are listed in one of the Digital } ``` - The DCC is actively working on a new trust registry model that will extend the registry scope. + The DCC is working on a new trust registry model that will extend the registry scope. ## API @@ -66,7 +66,8 @@ This package exports two methods: #### arguments * credential - The W3C Verifiable Credential to be verified. -* reloadIssuerRegistry - A boolean (true/false) indication whether or not to refresh the cached copy of the registry. +* knownDidRegistries - a list of trusted registries. +* reloadIssuerRegistry - A boolean (true/false) indication whether or not to refresh the cached copy of the registries. #### result @@ -78,7 +79,7 @@ There are three general flavours of result that might be returned: 1. all checks were conclusive -All of the checks were run *conclusively*, meaning that we determined wether each of the four steps in verification (signature, expiry, revocation, known issuer) was true or false. +All of the checks were run *conclusively*, meaning that we determined whether each of the four steps in verification (signature, expiry, revocation, known issuer) was true or false. A conclusive verification might look like this example where all steps returned valid=true: @@ -91,10 +92,6 @@ A conclusive verification might look like this example where all steps returned "id": "valid_signature", "valid": true }, - { - "id": "issuer_did_resolves", - "valid": true - }, { "id": "expiration", "valid": true @@ -108,15 +105,16 @@ A conclusive verification might look like this example where all steps returned "valid": true, "foundInRegistries": [ "DCC Sandbox Registry" - ] + ], + "registriesNotLoaded":[] } ] } ``` -Note that an invalid signature is considered fatal because it means that the revocation status, expiry data, or issuer id may have been changed so we can't say anything conclusive about any of them. +Note that an invalid signature is considered fatal because it means that the revocation status, expiry data, or issuer id may have been tampered with, and so we can't say anything conclusive about any of them. -Here is what the verification result for an expired credential might look like, where we have still made conclusive determinations about each step, and all are true except for the expiry: +Here is what the verification result for an expired credential might look like, where we have made conclusive determinations about each step, and all are true except for the expiry: ``` { @@ -127,10 +125,6 @@ Here is what the verification result for an expired credential might look like, "id": "valid_signature", "valid": true }, - { - "id": "issuer_did_resolves", - "valid": true - }, { "id": "expiration", "valid": false @@ -143,7 +137,8 @@ Here is what the verification result for an expired credential might look like, "valid": true, "foundInRegistries": [ "DCC Sandbox Registry" - ] + ], + "registriesNotLoaded":[] } ] } @@ -162,7 +157,7 @@ But can't retrieve (from the network) any one of the: * the issuer registry * the issuer's DID document -to verify the revocation status and issuer identity. +which are needed to verify the revocation status and issuer identity. For steps that we can't conclusively verify one way or the other (true or false) we return an 'error' propery rather than a 'valid' property. @@ -190,17 +185,14 @@ A partially successful verification might look like this example: }, { "id": "registered_issuer", - "error": { - "name": "network-error", - "message": "Could not retrieve the issuer registry." - } - }, - { - "id": "issuer_did_resolves", - "error": { - "name": "network-error", - "message": "Could not retrieve the issuer DID." - } + "valid": false, + "foundInRegistries": [], + "registriesNotLoaded": [ + { + "name": "DCC Sandbox Registry", + "url": "https://onlynoyrt.com/registry.json" + } + ] } ] } @@ -210,9 +202,15 @@ A partially successful verification might look like this example: Fatal errors are errors that prevent us from saying anything conclusive about the credential, and so we don't list the results of each step (the 'log') because we can't decisively say if any are true or false. Reverting to saying they are all false would be misleading, because that could be interepreted to mean that the credential was, for example, revoked when really we just don't know one way or the other. +Examples of fatal errors: + +invalid signature + +Fatal because if the signature is invalid it means any part of the credential could have been tampered with, including the revocation status, expiration, and issuer identity. + ``` { - "credential": {the vc goes here}, + "credential": {vc removed for brevity/clarity}, "isFatal": true, "errors": [ { @@ -222,19 +220,38 @@ Fatal errors are errors that prevent us from saying anything conclusive about th ] } ``` -Examples of fatal errors: -invalid signature + +unresolvable did + +Fatal because we couldn't retrieve the DID document containing the public signing key with which to check the signature. This error is most likely to happen with a did:web if the url for the did:web document is wrong or +has been taken down, or there is a network error. + +``` +{ + "credential": {vc removed for brevity/clarity}, + "isFatal": true, + "errors": [ + { + "name": "did_web_unresolved", + "message": "The signature could not be checked because the public signing key could not be retrieved from https://digitalcredentials.github.io/dcc-did-web-bad/did.json" + } + ] +} +``` + +malformed credential -Fatal because if the signature is invalid it means any part of the credential could have been tampered with, including the revocation status, expiration, and issuer identity +The supplied credential may not conform to the VerifiableCredential or LinkedData specifications(possibly because it follows some older convention, or maybe hasn't yet been signed) and might not even be a Verifiable Credential at all. + +``` + +``` software problem A software error might prevent verification -malformed credential - -The supplied credential may not conform to the VerifiableCredential or LinkedData specifications(possibly because it follows some older convention, or maybe hasn't yet been signed) and might not even be a Verifiable Credential at all. ### verifyPresentation diff --git a/src/Verify.ts b/src/Verify.ts index de961c2..2551927 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -8,7 +8,7 @@ import { getCredentialStatusChecker } from './credentialStatus.js'; import { addTrustedIssuersToVerificationResponse } from './issuerRegistries.js'; import { Credential } from './types/credential.js'; -import { VerificationResponse } from './types/result.js'; +import { VerificationResponse, VerificationStep } from './types/result.js'; const documentLoader = securityLoader({ fetchRemoteContexts: true }).build(); @@ -19,7 +19,7 @@ const ed25519Suite = new Ed25519Signature2020(); export async function verifyCredential({ credential, knownDIDRegistries, reloadIssuerRegistry = true }: { credential: Credential, knownDIDRegistries: object, reloadIssuerRegistry: boolean }): Promise { - const fatalCredentialError = checkForFatalCredentialErrors(credential) + const fatalCredentialError = handleAnyFatalCredentialErrors(credential) if (fatalCredentialError) { return fatalCredentialError @@ -40,25 +40,29 @@ export async function verifyCredential({ credential, knownDIDRegistries, reloadI verifyMatchingIssuers: false }); + //console.log("the verification response:") + //console.log(JSON.stringify(verificationResponse, null, 2)) - processAnyStatusError({ verificationResponse, statusResult: verificationResponse.statusResult }); + handleAnyStatusError({ verificationResponse, statusResult: verificationResponse.statusResult }); + + const fatalSignatureError = handleAnySignatureError({ verificationResponse, credential }) + if (fatalSignatureError) { + return fatalSignatureError + } + + const { issuer } = credential + await addTrustedIssuersToVerificationResponse({ verificationResponse, knownDIDRegistries, reloadIssuerRegistry, issuer }) // remove things we don't need from the result or that are duplicated elsewhere delete verificationResponse.results delete verificationResponse.statusResult delete verificationResponse.verified + verificationResponse.log = verificationResponse.log.filter((entry:VerificationStep)=>entry.id !== 'issuer_did_resolves') + // add things we always want in the response verificationResponse.credential = credential verificationResponse.isFatal = false - const fatalSignatureError = processAnySignatureError({ verificationResponse, credential }) - if (fatalSignatureError) { - return fatalSignatureError - } - - const { issuer } = credential - await addTrustedIssuersToVerificationResponse({ verificationResponse, knownDIDRegistries, reloadIssuerRegistry, issuer }) - return verificationResponse; } @@ -66,7 +70,7 @@ function buildFatalErrorObject(fatalErrorMessage: string, name: string, credenti return { credential, isFatal: true, errors: [{ name, message: fatalErrorMessage, ...stackTrace ? { stackTrace } : null }] } } -function checkForFatalCredentialErrors(credential: Credential): VerificationResponse | null { +function handleAnyFatalCredentialErrors(credential: Credential): VerificationResponse | null { const validVCContexts = [ 'https://www.w3.org/2018/credentials/v1', 'https://www.w3.org/ns/credentials/v2' @@ -103,7 +107,7 @@ function checkForFatalCredentialErrors(credential: Credential): VerificationResp return null } -function processAnyStatusError({ verificationResponse, statusResult }: { +function handleAnyStatusError({ verificationResponse, statusResult }: { verificationResponse: VerificationResponse, statusResult: any }): void { @@ -119,7 +123,7 @@ function processAnyStatusError({ verificationResponse, statusResult }: { } } -function processAnySignatureError({ verificationResponse, credential }: { verificationResponse: any, credential: Credential }) : null | VerificationResponse { +function handleAnySignatureError({ verificationResponse, credential }: { verificationResponse: any, credential: Credential }) : null | VerificationResponse { if (verificationResponse.error) { if (verificationResponse?.error?.name === 'VerificationError') { diff --git a/src/test-fixtures/expectedResults.ts b/src/test-fixtures/expectedResults.ts index 7284a5e..a72e4fd 100644 --- a/src/test-fixtures/expectedResults.ts +++ b/src/test-fixtures/expectedResults.ts @@ -8,10 +8,6 @@ const expectedResult = { "id": "valid_signature", "valid": true }, - { - "id": "issuer_did_resolves", - "valid": true - }, { "id": "expiration", "valid": true diff --git a/test/Verify.spec.ts b/test/Verify.spec.ts index d5bedaa..906aa56 100644 --- a/test/Verify.spec.ts +++ b/test/Verify.spec.ts @@ -36,6 +36,7 @@ import { chai.use(deepEqualInAnyOrder); const {expect} = chai; +const DISABLE_CONSOLE_WHEN_NO_ERRORS = false /* tests to add: @@ -57,17 +58,21 @@ describe('Verify', () => { let output:string; beforeEach(function(done) { - output = ''; - console.log = (msg) => { - output += msg + '\n'; - }; + if (DISABLE_CONSOLE_WHEN_NO_ERRORS) { + output = ''; + console.log = (msg) => { + output += msg + '\n'; + }; + } done() }); afterEach(function() { - console.log = originalLogFunction; // undo dummy log function - if (this?.currentTest?.state === 'failed') { - console.log(output); + if (DISABLE_CONSOLE_WHEN_NO_ERRORS) { + console.log = originalLogFunction; // undo dummy log function + if (this?.currentTest?.state === 'failed') { + console.log(output); + } } }); @@ -218,22 +223,23 @@ describe('Verify', () => { const credential : any = getVCv1ValidStatus() const noMatchingRegistryList = JSON.parse(JSON.stringify(knownDIDRegistries)) // set the one matching registry to a url that won't load - noMatchingRegistryList[1].url = 'https://onlynoyrt.com/registry.json' + noMatchingRegistryList[1].url = 'https://onldynoyrt.com/registry.json' const expectedResult : any = getExpectedVerifiedResult({credential, withStatus: true}) - const expectedRsultRegistryLogEntry = expectedResult.log.find((entry:any)=>entry.id==='registered_issuer') - expectedRsultRegistryLogEntry.registriesNotLoaded = [ + const expectedResultRegistryLogEntry = expectedResult.log.find((entry:any)=>entry.id==='registered_issuer') + expectedResultRegistryLogEntry.registriesNotLoaded = [ { - "name": "DCC Pilot Registry", - "url": "https://onlynoyrt.com/registry.json" + "name": "DCC Sandbox Registry", + "url": "https://onldynoyrt.com/registry.json" } ] - expectedRsultRegistryLogEntry.valid = false; + expectedResultRegistryLogEntry.valid = false; + expectedResultRegistryLogEntry.foundInRegistries = [] const result = await verifyCredential({credential, reloadIssuerRegistry: true, knownDIDRegistries: noMatchingRegistryList}) - assert.ok(result.log); + + //console.log(JSON.stringify(result, null, 2)) + expect(result).to.deep.equalInAnyOrder(expectedResult) }) - - }) describe('returns accurate registry list', () => { @@ -241,12 +247,12 @@ describe('Verify', () => { it('when one registry url does not exist', async () => { const credential : any = getVCv1ValidStatus() const badRegistryList = JSON.parse(JSON.stringify(knownDIDRegistries)) - badRegistryList[0].url = 'https://onlynoyrt.com/registry.json' + badRegistryList[0].url = 'https://onldynoyrt.com/registry.json' const expectedResult : any = getExpectedVerifiedResult({credential, withStatus: true}) expectedResult.log.find((entry:any)=>entry.id==='registered_issuer').registriesNotLoaded = [ { "name": "DCC Pilot Registry", - "url": "https://onlynoyrt.com/registry.json" + "url": "https://onldynoyrt.com/registry.json" } ] const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries: badRegistryList}) @@ -256,17 +262,17 @@ describe('Verify', () => { it('when two registry urls do not exist', async () => { const credential : any = getVCv1ValidStatus() const badRegistryList = JSON.parse(JSON.stringify(knownDIDRegistries)) - badRegistryList[0].url = 'https://onlynoyrt.com/registry.json' - badRegistryList[2].url = 'https://onlynoyrrrt.com/registry.json' + badRegistryList[0].url = 'https://onldynoyrt.com/registry.json' + badRegistryList[2].url = 'https://onldynoyrrrt.com/registry.json' const expectedResult : any = getExpectedVerifiedResult({credential, withStatus: true}) expectedResult.log.find((entry:any)=>entry.id==='registered_issuer').registriesNotLoaded = [ { "name": "DCC Community Registry", - "url": "https://onlynoyrrrt.com/registry.json" + "url": "https://onldynoyrrrt.com/registry.json" }, { "name": "DCC Pilot Registry", - "url": "https://onlynoyrt.com/registry.json" + "url": "https://onldynoyrt.com/registry.json" } ] const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries: badRegistryList}) @@ -286,6 +292,8 @@ describe('Verify', () => { describe('with VC version 2', () => { + + //console.log(JSON.stringify(result, null, 2)) describe('returns fatal error', () => { it('when tampered with', async () => { const credential : any = getVCv2Tampered() From 8a2de136bd085d38779b54d0428372d6f46711d3 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Wed, 22 Jan 2025 11:18:34 -0500 Subject: [PATCH 44/72] fix lint errors --- src/Verify.ts | 3 --- test/Verify.spec.ts | 12 ++++++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Verify.ts b/src/Verify.ts index 2551927..f2bcfe2 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -40,9 +40,6 @@ export async function verifyCredential({ credential, knownDIDRegistries, reloadI verifyMatchingIssuers: false }); - //console.log("the verification response:") - //console.log(JSON.stringify(verificationResponse, null, 2)) - handleAnyStatusError({ verificationResponse, statusResult: verificationResponse.statusResult }); const fatalSignatureError = handleAnySignatureError({ verificationResponse, credential }) diff --git a/test/Verify.spec.ts b/test/Verify.spec.ts index 906aa56..6aef1c2 100644 --- a/test/Verify.spec.ts +++ b/test/Verify.spec.ts @@ -295,6 +295,18 @@ describe('Verify', () => { //console.log(JSON.stringify(result, null, 2)) describe('returns fatal error', () => { + + it('when no context', async () => { + const credential : any = getVCv2NoContext() + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + const expectedResult = getExpectedFatalResult({ + credential, + errorMessage: 'The signature is not valid.', + errorName: 'invalid_signature' + }) + expect(result).to.deep.equalInAnyOrder(expectedResult) + }) + it('when tampered with', async () => { const credential : any = getVCv2Tampered() const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) From 52a061b024f07389069442e3101f58d62a5151c3 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Wed, 22 Jan 2025 11:58:22 -0500 Subject: [PATCH 45/72] update README --- README.md | 251 +++++++++++++++++++++++++++++++++++++++++++- test/Verify.spec.ts | 19 +--- 2 files changed, 251 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index fcb3695..0e82283 100644 --- a/README.md +++ b/README.md @@ -210,18 +210,17 @@ Fatal because if the signature is invalid it means any part of the credential co ``` { - "credential": {vc removed for brevity/clarity}, + "credential": {vc removed for brevity/clarity in this example}, "isFatal": true, "errors": [ { - "name": "invalidSignature", + "name": "invalid_signature", "message": "The signature is not valid." } ] } ``` - unresolvable did Fatal because we couldn't retrieve the DID document containing the public signing key with which to check the signature. This error is most likely to happen with a did:web if the url for the did:web document is wrong or @@ -244,10 +243,256 @@ has been taken down, or there is a network error. The supplied credential may not conform to the VerifiableCredential or LinkedData specifications(possibly because it follows some older convention, or maybe hasn't yet been signed) and might not even be a Verifiable Credential at all. +Some specific examples: + +invalid_jsonld + +There is no @context property at the top level of the credential: + +``` +{ + "credential": { + "id": "http://example.com/credentials/3527", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "issuer": { + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "type": [ + "Profile" + ], + "name": "Example Corp" + }, + "validFrom": "2010-01-01T00:00:00Z", + "name": "Teamwork Badge", + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "type": [ + "AchievementSubject" + ], + "achievement": { + "id": "https://example.com/achievements/21st-century-skills/teamwork", + "type": [ + "Achievement" + ], + "criteria": { + "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." + }, + "description": "This badge recognizes the development of the capacity to collaborate within a group environment.", + "name": "Teamwork" + } + }, + "proof": { + "type": "Ed25519Signature2020", + "created": "2025-01-09T17:58:33Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "z62t6TYCERpTKuWCRhHc2fV7JoMhiFuEcCXGkX9iit8atQPhviN5cZeZfXRnvJWa3Bm6DjagKyrauaSJfp9C9i7q3" + } + }, + "isFatal": true, + "errors": [ + { + "name": "invalid_jsonld", + "message": "The credential does not appear to be a valid jsonld document - there is no context." + } + ] +} ``` +no_vc_context + +Although this is a linked data document, with an @context property, the Verifiable Credential context (i.e, "https://www.w3.org/2018/credentials/v1") is missing: + +``` +{ + "credential": { + "@context": [ + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "id": "http://example.com/credentials/3527", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "issuer": { + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "type": [ + "Profile" + ], + "name": "Example Corp" + }, + "validFrom": "2010-01-01T00:00:00Z", + "name": "Teamwork Badge", + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "type": [ + "AchievementSubject" + ], + "achievement": { + "id": "https://example.com/achievements/21st-century-skills/teamwork", + "type": [ + "Achievement" + ], + "criteria": { + "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." + }, + "description": "This badge recognizes the development of the capacity to collaborate within a group environment.", + "name": "Teamwork" + } + }, + "proof": { + "type": "Ed25519Signature2020", + "created": "2025-01-09T17:58:33Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "z62t6TYCERpTKuWCRhHc2fV7JoMhiFuEcCXGkX9iit8atQPhviN5cZeZfXRnvJWa3Bm6DjagKyrauaSJfp9C9i7q3" + } + }, + "isFatal": true, + "errors": [ + { + "name": "no_vc_context", + "message": "The credential doesn't have a verifiable credential context." + } + ] +} ``` +invalid_credential_id + +In this example, the top level id property on the credential is not a uri, but should be: + +``` +{ + "credential": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.2.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "id": "0923lksjf", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "name": "DCC Test Credential", + "issuer": { + "type": [ + "Profile" + ], + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "name": "Digital Credentials Consortium Test Issuer", + "url": "https://dcconsortium.org", + "image": "https://user-images.githubusercontent.com/752326/230469660-8f80d264-eccf-4edd-8e50-ea634d407778.png" + }, + "issuanceDate": "2023-08-02T17:43:32.903Z", + "credentialSubject": { + "type": [ + "AchievementSubject" + ], + "achievement": { + "id": "urn:uuid:bd6d9316-f7ae-4073-a1e5-2f7f5bd22922", + "type": [ + "Achievement" + ], + "achievementType": "Diploma", + "name": "Badge", + "description": "This is a sample credential issued by the Digital Credentials Consortium to demonstrate the functionality of Verifiable Credentials for wallets and verifiers.", + "criteria": { + "type": "Criteria", + "narrative": "This credential was issued to a student that demonstrated proficiency in the Python programming language that occurred from **February 17, 2023** to **June 12, 2023**." + }, + "image": { + "id": "https://user-images.githubusercontent.com/752326/214947713-15826a3a-b5ac-4fba-8d4a-884b60cb7157.png", + "type": "Image" + } + }, + "name": "Jane Doe" + }, + "proof": { + "type": "Ed25519Signature2020", + "created": "2023-10-05T11:17:41Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "z5fk6gq9upyZvcFvJdRdeL5KmvHr69jxEkyDEd2HyQdyhk9VnDEonNSmrfLAcLEDT9j4gGdCG24WHhojVHPbRsNER" + } + }, + "isFatal": true, + "errors": [ + { + "name": "invalid_credential_id", + "message": "The credential's id uses an invalid format. It may have been issued as part of an early pilot. Please contact the issuer to get a replacement." + } + ] +} +``` + +no_proof + +The proof property is missing, likely because the credential hasn't been signed: + +``` +{ + "credential": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.2.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "id": "urn:uuid:2fe53dc9-b2ec-4939-9b2c-0d00f6663b6c", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "name": "DCC Test Credential", + "issuer": { + "type": [ + "Profile" + ], + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "name": "Digital Credentials Consortium Test Issuer", + "url": "https://dcconsortium.org", + "image": "https://user-images.githubusercontent.com/752326/230469660-8f80d264-eccf-4edd-8e50-ea634d407778.png" + }, + "issuanceDate": "2023-08-02T17:43:32.903Z", + "credentialSubject": { + "type": [ + "AchievementSubject" + ], + "achievement": { + "id": "urn:uuid:bd6d9316-f7ae-4073-a1e5-2f7f5bd22922", + "type": [ + "Achievement" + ], + "achievementType": "Diploma", + "name": "Badge", + "description": "This is a sample credential issued by the Digital Credentials Consortium to demonstrate the functionality of Verifiable Credentials for wallets and verifiers.", + "criteria": { + "type": "Criteria", + "narrative": "This credential was issued to a student that demonstrated proficiency in the Python programming language that occurred from **February 17, 2023** to **June 12, 2023**." + }, + "image": { + "id": "https://user-images.githubusercontent.com/752326/214947713-15826a3a-b5ac-4fba-8d4a-884b60cb7157.png", + "type": "Image" + } + }, + "name": "Jane Doe" + } + }, + "isFatal": true, + "errors": [ + { + "name": "no_proof", + "message": "This is not a Verifiable Credential - it does not have a digital signature." + } + ] +} +``` + + software problem A software error might prevent verification diff --git a/test/Verify.spec.ts b/test/Verify.spec.ts index 6aef1c2..eff364c 100644 --- a/test/Verify.spec.ts +++ b/test/Verify.spec.ts @@ -25,6 +25,7 @@ import { getVCv2DoubleSigWithBadStatusUrl, getVCv2DidWebWithValidStatus, getVCv2WithBadDidWebUrl + } from '../src/test-fixtures/vc.js' import { knownDIDRegistries } from '../.knownDidRegistries.js'; import { @@ -36,7 +37,7 @@ import { chai.use(deepEqualInAnyOrder); const {expect} = chai; -const DISABLE_CONSOLE_WHEN_NO_ERRORS = false +const DISABLE_CONSOLE_WHEN_NO_ERRORS = true /* tests to add: @@ -117,7 +118,6 @@ describe('Verify', () => { it('when not jsonld', async () => { const credential : any = getCredentialWithoutContext() const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) - const expectedResult = getExpectedFatalResult({ credential, errorMessage: 'The credential does not appear to be a valid jsonld document - there is no context.', @@ -168,7 +168,6 @@ describe('Verify', () => { it('when no proof', async () => { const credential : any = getVCv1NoProof() const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) - const expectedResult = getExpectedFatalResult({ credential, errorMessage: 'This is not a Verifiable Credential - it does not have a digital signature.', @@ -179,7 +178,6 @@ describe('Verify', () => { it('when credential id is not a uri', async () => { const credential : any = getVCv1NonURIId() const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) - const expectedResult = getExpectedFatalResult({ credential, errorMessage: "The credential's id uses an invalid format. It may have been issued as part of an early pilot. Please contact the issuer to get a replacement.", @@ -293,20 +291,9 @@ describe('Verify', () => { describe('with VC version 2', () => { - //console.log(JSON.stringify(result, null, 2)) + describe('returns fatal error', () => { - it('when no context', async () => { - const credential : any = getVCv2NoContext() - const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) - const expectedResult = getExpectedFatalResult({ - credential, - errorMessage: 'The signature is not valid.', - errorName: 'invalid_signature' - }) - expect(result).to.deep.equalInAnyOrder(expectedResult) - }) - it('when tampered with', async () => { const credential : any = getVCv2Tampered() const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) From f72814f2cbc9e592998ce66e04ce9201459a4f0b Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Wed, 22 Jan 2025 16:59:45 -0500 Subject: [PATCH 46/72] add presentation verification --- src/Verify.ts | 71 ++++++++++++++++++++++++++++---- src/extractCredentialsFrom.ts | 33 +++++++++++++++ test/Verify.presentation.spec.ts | 69 +++++++++++++++++++++++++++++++ test/didAuth.ts | 38 +++++++++++++++++ 4 files changed, 204 insertions(+), 7 deletions(-) create mode 100644 src/extractCredentialsFrom.ts create mode 100644 test/Verify.presentation.spec.ts create mode 100644 test/didAuth.ts diff --git a/src/Verify.ts b/src/Verify.ts index f2bcfe2..9545d7b 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -9,6 +9,13 @@ import { addTrustedIssuersToVerificationResponse } from './issuerRegistries.js'; import { Credential } from './types/credential.js'; import { VerificationResponse, VerificationStep } from './types/result.js'; +import { VerifiablePresentation, PresentationError } from './types/presentation.js'; + +import { extractCredentialsFrom} from './extractCredentialsFrom.js'; + +// import { purposes } from '@digitalcredentials/jsonld-signatures'; +// const presentationPurpose = new purposes.AssertionProofPurpose(); +// import { extractCredentialsFrom } from './verifiableObject'; const documentLoader = securityLoader({ fetchRemoteContexts: true }).build(); @@ -16,6 +23,63 @@ const documentLoader = securityLoader({ fetchRemoteContexts: true }).build(); const eddsaSuite = new DataIntegrityProof({ cryptosuite: eddsaRdfc2022CryptoSuite }); // for verifying ed25519-2020 signatures const ed25519Suite = new Ed25519Signature2020(); + // add both suites - the vc lib will use whichever is appropriate +const suite = [ed25519Suite, eddsaSuite] + + +export async function verifyPresentation( + presentation: VerifiablePresentation, + challenge: string, + unsignedPresentation = false, +): Promise { + try { + const credential = extractCredentialsFrom(presentation)?.find( + vc => vc.credentialStatus); + const checkStatus = credential ? getCredentialStatusChecker(credential) : undefined; + const result = await vc.verify({ + presentation, + // presentationPurpose, + suite, + documentLoader, + unsignedPresentation, + checkStatus, + challenge, + verifyMatchingIssuers: false + }); + + if (!result.verified) { + console.warn('VP not verified:', JSON.stringify(result, null, 2)); + } + return result; + } catch (err) { + console.warn(err); + + throw new Error(PresentationError.CouldNotBeVerified); + } +} +/* + +from Verifier Plus: + +export async function verifyPresentation( + presentation: VerifiablePresentation, + unsignedPresentation = true, +): Promise { + try { + const result = await vc.verify({ + presentation, + presentationPurpose, + suite, + documentLoader, + unsignedPresentation, + }); + + return result; + } catch (err) { + console.warn(err); + throw new Error(PresentationError.CouldNotBeVerified); + } +} */ export async function verifyCredential({ credential, knownDIDRegistries, reloadIssuerRegistry = true }: { credential: Credential, knownDIDRegistries: object, reloadIssuerRegistry: boolean }): Promise { @@ -25,9 +89,6 @@ export async function verifyCredential({ credential, knownDIDRegistries, reloadI return fatalCredentialError } - // add both suites - the vc lib will use whichever is appropriate - const suite = [ed25519Suite, eddsaSuite] - // a statusCheck is returned only if the credential has a status // that needs checking, otherwise null const statusChecker = getCredentialStatusChecker(credential) @@ -176,7 +237,3 @@ function handleAnySignatureError({ verificationResponse, credential }: { verific -// import { purposes } from '@digitalcredentials/jsonld-signatures'; -// import { VerifiablePresentation, PresentationError } from './types/presentation'; -// const presentationPurpose = new purposes.AssertionProofPurpose(); -// import { extractCredentialsFrom } from './verifiableObject'; diff --git a/src/extractCredentialsFrom.ts b/src/extractCredentialsFrom.ts new file mode 100644 index 0000000..fc6c920 --- /dev/null +++ b/src/extractCredentialsFrom.ts @@ -0,0 +1,33 @@ +import { Credential } from './types/credential.js'; +import { VerifiablePresentation } from './types/presentation'; + +/** + * This type is used to identify a request response that could be a + * Verifiable Credential or Verifiable Presentation. + */ +export type VerifiableObject = Credential | VerifiablePresentation; + +export function isVerifiableCredential(obj: VerifiableObject): obj is Credential { + return obj.type?.includes('VerifiableCredential'); +} + +export function isVerifiablePresentation(obj: VerifiableObject): obj is VerifiablePresentation { + return obj.type?.includes('VerifiablePresentation'); +} + +export function extractCredentialsFrom(obj: VerifiableObject): Credential[] | null { + if (isVerifiableCredential(obj)) { + return [obj]; + } + + if (isVerifiablePresentation(obj)) { + const { verifiableCredential } = obj; + + if (verifiableCredential instanceof Array) { + return verifiableCredential; + } + return [verifiableCredential]; + } + + return null; +} diff --git a/test/Verify.presentation.spec.ts b/test/Verify.presentation.spec.ts new file mode 100644 index 0000000..a143a00 --- /dev/null +++ b/test/Verify.presentation.spec.ts @@ -0,0 +1,69 @@ +import chai from 'chai' +import deepEqualInAnyOrder from 'deep-equal-in-any-order' +import { verifyCredential, verifyPresentation } from '../src/Verify.js' +import { + getVCv2DidWebWithValidStatus, + getVCv2EddsaWithValidStatus, + getVCv2ValidStatus, +} from '../src/test-fixtures/vc.js' +import { knownDIDRegistries } from '../.knownDidRegistries.js'; +import { + getExpectedVerifiedResult + } from '../src/test-fixtures/expectedResults.js'; + + import { getSignedDIDAuth, verifyDIDAuth } from './didAuth.js'; + +chai.use(deepEqualInAnyOrder); +const {expect} = chai; + +const DISABLE_CONSOLE_WHEN_NO_ERRORS = false + + +describe('Verify', () => { + + const originalLogFunction = console.log; + let output:string; + + beforeEach(function(done) { + if (DISABLE_CONSOLE_WHEN_NO_ERRORS) { + output = ''; + console.log = (msg) => { + output += msg + '\n'; + }; + } + done() + }); + + afterEach(function() { + if (DISABLE_CONSOLE_WHEN_NO_ERRORS) { + console.log = originalLogFunction; // undo dummy log function + if (this?.currentTest?.state === 'failed') { + console.log(output); + } + } + }); + + describe('.verifyPresentation', () => { + + describe('it returns as verified', () => { + + it.only('when presentation is valid', async () => { + const challenge = '2223q23' + const firstVC : any = getVCv2DidWebWithValidStatus() + const secondVC : any = getVCv2ValidStatus() + const verifiableCredential = [firstVC, secondVC] + // const expectedResult = getExpectedVerifiedResult({credential: verifiableCredential, withStatus: true}) + const presentation = await getSignedDIDAuth({challenge, verifiableCredential, holder: 'did:ex:12345'}) + console.log(JSON.stringify(presentation, null, 2)) + const result = await verifyPresentation(presentation, challenge) + //await verifyDIDAuth({presentation, challenge}) + console.log("====================== verification result") + console.log(JSON.stringify(result,null,2)) + expect(true) + }) + }) + }) + + }) + + diff --git a/test/didAuth.ts b/test/didAuth.ts new file mode 100644 index 0000000..8332934 --- /dev/null +++ b/test/didAuth.ts @@ -0,0 +1,38 @@ +import {verify,signPresentation, createPresentation} from '@digitalcredentials/vc'; + +import {Ed25519Signature2020} from '@digitalcredentials/ed25519-signature-2020'; +import { securityLoader } from '@digitalcredentials/security-document-loader'; +import {Ed25519VerificationKey2020} from '@digitalcredentials/ed25519-verification-key-2020'; + +const documentLoader = securityLoader().build() + +const key = await Ed25519VerificationKey2020.generate( + { + seed: new Uint8Array ([ + 217, 87, 166, 30, 75, 106, 132, 55, + 32, 120, 171, 23, 116, 73, 254, 74, + 230, 16, 127, 91, 2, 252, 224, 96, + 184, 172, 245, 157, 58, 217, 91, 240 + ]), + controller: "did:key:z6MkvL5yVCgPhYvQwSoSRQou6k6ZGfD5mNM57HKxufEXwfnP" + } +) + + +const signingSuite = new Ed25519Signature2020({key}); + +export const getSignedDIDAuth = async ({holder, challenge, verifiableCredential}:{holder:string,challenge:string,verifiableCredential:any}):Promise => { + const presentation = createPresentation({holder, verifiableCredential}); + return await signPresentation({ + presentation, suite:signingSuite, challenge, documentLoader + }); +} + + +const verificationSuite = new Ed25519Signature2020(); + +export const verifyDIDAuth = async ({presentation, challenge}:{presentation:any,challenge:string}):Promise => { + const result = await verify({presentation, challenge, suite: verificationSuite, documentLoader}); + return result +} + From 0ad410e4d02ab1678cc9f9a087d391090e72c13e Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Fri, 24 Jan 2025 09:56:07 -0500 Subject: [PATCH 47/72] fix assertionMethod and challenge on verifyPresentation --- src/Verify.ts | 36 +++++++------------------------- test/Verify.presentation.spec.ts | 10 ++++----- test/didAuth.ts | 11 +++++++--- 3 files changed, 20 insertions(+), 37 deletions(-) diff --git a/src/Verify.ts b/src/Verify.ts index 9545d7b..4c32f7e 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -13,9 +13,9 @@ import { VerifiablePresentation, PresentationError } from './types/presentation. import { extractCredentialsFrom} from './extractCredentialsFrom.js'; -// import { purposes } from '@digitalcredentials/jsonld-signatures'; -// const presentationPurpose = new purposes.AssertionProofPurpose(); -// import { extractCredentialsFrom } from './verifiableObject'; +import pkg from '@digitalcredentials/jsonld-signatures'; +const { purposes } = pkg; +const presentationPurpose = new purposes.AssertionProofPurpose(); const documentLoader = securityLoader({ fetchRemoteContexts: true }).build(); @@ -29,7 +29,7 @@ const suite = [ed25519Suite, eddsaSuite] export async function verifyPresentation( presentation: VerifiablePresentation, - challenge: string, + challenge: string = 'canbeanything', unsignedPresentation = false, ): Promise { try { @@ -38,7 +38,7 @@ export async function verifyPresentation( const checkStatus = credential ? getCredentialStatusChecker(credential) : undefined; const result = await vc.verify({ presentation, - // presentationPurpose, + presentationPurpose, suite, documentLoader, unsignedPresentation, @@ -57,37 +57,15 @@ export async function verifyPresentation( throw new Error(PresentationError.CouldNotBeVerified); } } -/* -from Verifier Plus: - -export async function verifyPresentation( - presentation: VerifiablePresentation, - unsignedPresentation = true, -): Promise { - try { - const result = await vc.verify({ - presentation, - presentationPurpose, - suite, - documentLoader, - unsignedPresentation, - }); - - return result; - } catch (err) { - console.warn(err); - throw new Error(PresentationError.CouldNotBeVerified); - } -} */ export async function verifyCredential({ credential, knownDIDRegistries, reloadIssuerRegistry = true }: { credential: Credential, knownDIDRegistries: object, reloadIssuerRegistry: boolean }): Promise { - const fatalCredentialError = handleAnyFatalCredentialErrors(credential) + const fatalCredentialError = handleAnyFatalCredentialErrors(credential) if (fatalCredentialError) { return fatalCredentialError - } + } // a statusCheck is returned only if the credential has a status // that needs checking, otherwise null diff --git a/test/Verify.presentation.spec.ts b/test/Verify.presentation.spec.ts index a143a00..7fe1a41 100644 --- a/test/Verify.presentation.spec.ts +++ b/test/Verify.presentation.spec.ts @@ -53,12 +53,12 @@ describe('Verify', () => { const secondVC : any = getVCv2ValidStatus() const verifiableCredential = [firstVC, secondVC] // const expectedResult = getExpectedVerifiedResult({credential: verifiableCredential, withStatus: true}) - const presentation = await getSignedDIDAuth({challenge, verifiableCredential, holder: 'did:ex:12345'}) - console.log(JSON.stringify(presentation, null, 2)) - const result = await verifyPresentation(presentation, challenge) + const presentation = await getSignedDIDAuth({verifiableCredential, holder: 'did:ex:12345'}) + // console.log(JSON.stringify(presentation, null, 2)) + const result = await verifyPresentation(presentation) //await verifyDIDAuth({presentation, challenge}) - console.log("====================== verification result") - console.log(JSON.stringify(result,null,2)) + // console.log("====================== verification result") + //console.log(JSON.stringify(result,null,2)) expect(true) }) }) diff --git a/test/didAuth.ts b/test/didAuth.ts index 8332934..a9882ac 100644 --- a/test/didAuth.ts +++ b/test/didAuth.ts @@ -1,4 +1,4 @@ -import {verify,signPresentation, createPresentation} from '@digitalcredentials/vc'; +import {verify,signPresentation,createPresentation} from '@digitalcredentials/vc'; import {Ed25519Signature2020} from '@digitalcredentials/ed25519-signature-2020'; import { securityLoader } from '@digitalcredentials/security-document-loader'; @@ -6,6 +6,10 @@ import {Ed25519VerificationKey2020} from '@digitalcredentials/ed25519-verificati const documentLoader = securityLoader().build() +import pkg from '@digitalcredentials/jsonld-signatures'; +const { purposes } = pkg; +const presentationPurpose = new purposes.AssertionProofPurpose(); + const key = await Ed25519VerificationKey2020.generate( { seed: new Uint8Array ([ @@ -21,10 +25,11 @@ const key = await Ed25519VerificationKey2020.generate( const signingSuite = new Ed25519Signature2020({key}); -export const getSignedDIDAuth = async ({holder, challenge, verifiableCredential}:{holder:string,challenge:string,verifiableCredential:any}):Promise => { +export const getSignedDIDAuth = async ({holder, verifiableCredential}:{holder:string,verifiableCredential:any}):Promise => { const presentation = createPresentation({holder, verifiableCredential}); + const challenge = 'canbeanything33' return await signPresentation({ - presentation, suite:signingSuite, challenge, documentLoader + presentation, suite:signingSuite, documentLoader, challenge, purpose: presentationPurpose }); } From ff5d62cff1d2f3af689c12e79df084c5d3e561ae Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Fri, 24 Jan 2025 11:37:28 -0500 Subject: [PATCH 48/72] extract verification result transformation --- src/Verify.ts | 28 +++++++++++++++++----------- test/Verify.presentation.spec.ts | 2 +- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/Verify.ts b/src/Verify.ts index 4c32f7e..7e70efb 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -61,14 +61,7 @@ export async function verifyPresentation( export async function verifyCredential({ credential, knownDIDRegistries, reloadIssuerRegistry = true }: { credential: Credential, knownDIDRegistries: object, reloadIssuerRegistry: boolean }): Promise { - const fatalCredentialError = handleAnyFatalCredentialErrors(credential) - - if (fatalCredentialError) { - return fatalCredentialError - } - - // a statusCheck is returned only if the credential has a status - // that needs checking, otherwise null + // null unless credential has a status const statusChecker = getCredentialStatusChecker(credential) const verificationResponse = await vc.verifyCredential({ @@ -79,6 +72,18 @@ export async function verifyCredential({ credential, knownDIDRegistries, reloadI verifyMatchingIssuers: false }); + const adjustedResponse = transformResponse(verificationResponse, credential, knownDIDRegistries, reloadIssuerRegistry) + return adjustedResponse; +} + +async function transformResponse(verificationResponse:any, credential:Credential, knownDIDRegistries: object, reloadIssuerRegistry: boolean ) : Promise { + + const fatalCredentialError = handleAnyFatalCredentialErrors(credential) + + if (fatalCredentialError) { + return fatalCredentialError + } + handleAnyStatusError({ verificationResponse, statusResult: verificationResponse.statusResult }); const fatalSignatureError = handleAnySignatureError({ verificationResponse, credential }) @@ -99,7 +104,7 @@ export async function verifyCredential({ credential, knownDIDRegistries, reloadI verificationResponse.credential = credential verificationResponse.isFatal = false - return verificationResponse; + return verificationResponse as VerificationResponse; } function buildFatalErrorObject(fatalErrorMessage: string, name: string, credential: Credential, stackTrace: string | null): VerificationResponse { @@ -143,10 +148,11 @@ function handleAnyFatalCredentialErrors(credential: Credential): VerificationRes return null } -function handleAnyStatusError({ verificationResponse, statusResult }: { - verificationResponse: VerificationResponse, +function handleAnyStatusError({ verificationResponse }: { + verificationResponse: any, statusResult: any }): void { + const statusResult = verificationResponse.statusResult if (statusResult?.error?.cause?.message?.startsWith('NotFoundError')) { const statusStep = { "id": "revocation_status", diff --git a/test/Verify.presentation.spec.ts b/test/Verify.presentation.spec.ts index 7fe1a41..9bb7fb5 100644 --- a/test/Verify.presentation.spec.ts +++ b/test/Verify.presentation.spec.ts @@ -47,7 +47,7 @@ describe('Verify', () => { describe('it returns as verified', () => { - it.only('when presentation is valid', async () => { + it('when presentation is valid', async () => { const challenge = '2223q23' const firstVC : any = getVCv2DidWebWithValidStatus() const secondVC : any = getVCv2ValidStatus() From a99fc099f46a126066c0dc6cf565b8334b5d20c4 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Mon, 27 Jan 2025 11:19:16 -0500 Subject: [PATCH 49/72] transform vc verification responses in presentation --- package.json | 2 +- src/Verify.ts | 20 ++++++++++++-------- test/Verify.presentation.spec.ts | 16 ++++++++++------ 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index a6b93b6..528d35e 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "@digitalcredentials/issuer-registry-client": "^3.0.1-beta.1", "@digitalcredentials/jsonld-signatures": "^12.0.1", "@digitalcredentials/security-document-loader": "^6.0.1", - "@digitalcredentials/vc": "^9.0.1-beta.1", + "@digitalcredentials/vc": "^9.0.1-beta.2", "@digitalcredentials/vc-bitstring-status-list": "^1.0.0", "@digitalcredentials/vc-status-list": "^9.0.0" }, diff --git a/src/Verify.ts b/src/Verify.ts index 7e70efb..5b23d2b 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -26,11 +26,12 @@ const ed25519Suite = new Ed25519Signature2020(); // add both suites - the vc lib will use whichever is appropriate const suite = [ed25519Suite, eddsaSuite] - -export async function verifyPresentation( - presentation: VerifiablePresentation, - challenge: string = 'canbeanything', - unsignedPresentation = false, +export async function verifyPresentation({presentation, challenge = 'blah', unsignedPresentation = false, knownDIDRegistries, reloadIssuerRegistry=true}: + {presentation: VerifiablePresentation, + challenge?: string | null, + unsignedPresentation? : boolean, + knownDIDRegistries: object, + reloadIssuerRegistry?: boolean} ): Promise { try { const credential = extractCredentialsFrom(presentation)?.find( @@ -47,9 +48,12 @@ export async function verifyPresentation( verifyMatchingIssuers: false }); - if (!result.verified) { - console.warn('VP not verified:', JSON.stringify(result, null, 2)); - } + const transformedVCResults = await Promise.all(result.credentialResults.map(async (credentialResult:any) => { + return transformResponse(credentialResult, credentialResult.credential, knownDIDRegistries, reloadIssuerRegistry) + })); + + result.credentialResults = transformedVCResults + return result; } catch (err) { console.warn(err); diff --git a/test/Verify.presentation.spec.ts b/test/Verify.presentation.spec.ts index 9bb7fb5..3cd7402 100644 --- a/test/Verify.presentation.spec.ts +++ b/test/Verify.presentation.spec.ts @@ -5,6 +5,8 @@ import { getVCv2DidWebWithValidStatus, getVCv2EddsaWithValidStatus, getVCv2ValidStatus, + getVCv1NoProof, + getVCv2NonURIId } from '../src/test-fixtures/vc.js' import { knownDIDRegistries } from '../.knownDidRegistries.js'; import { @@ -12,6 +14,7 @@ import { } from '../src/test-fixtures/expectedResults.js'; import { getSignedDIDAuth, verifyDIDAuth } from './didAuth.js'; +import { VerifiablePresentation } from '../src/types/presentation.js'; chai.use(deepEqualInAnyOrder); const {expect} = chai; @@ -48,17 +51,18 @@ describe('Verify', () => { describe('it returns as verified', () => { it('when presentation is valid', async () => { - const challenge = '2223q23' const firstVC : any = getVCv2DidWebWithValidStatus() const secondVC : any = getVCv2ValidStatus() - const verifiableCredential = [firstVC, secondVC] + const noProofVC : any = getVCv1NoProof() + const badIdVC : any = getVCv2NonURIId() + const verifiableCredential = [firstVC, secondVC, firstVC] // const expectedResult = getExpectedVerifiedResult({credential: verifiableCredential, withStatus: true}) - const presentation = await getSignedDIDAuth({verifiableCredential, holder: 'did:ex:12345'}) + const presentation = await getSignedDIDAuth({verifiableCredential, holder: 'did:ex:12345'}) as VerifiablePresentation // console.log(JSON.stringify(presentation, null, 2)) - const result = await verifyPresentation(presentation) + const result = await verifyPresentation({presentation, knownDIDRegistries}) //await verifyDIDAuth({presentation, challenge}) - // console.log("====================== verification result") - //console.log(JSON.stringify(result,null,2)) + console.log("====================== verification result") + console.log(JSON.stringify(result,null,2)) expect(true) }) }) From 69e30c593102952e2a453c7596694498c4e16cb1 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Mon, 27 Jan 2025 12:33:11 -0500 Subject: [PATCH 50/72] reorganize tests --- test/Verify.general.spec.ts | 71 ++++++++ test/{Verify.spec.ts => Verify.v1.spec.ts} | 190 +------------------ test/Verify.v2.spec.ts | 202 +++++++++++++++++++++ 3 files changed, 274 insertions(+), 189 deletions(-) create mode 100644 test/Verify.general.spec.ts rename test/{Verify.spec.ts => Verify.v1.spec.ts} (51%) create mode 100644 test/Verify.v2.spec.ts diff --git a/test/Verify.general.spec.ts b/test/Verify.general.spec.ts new file mode 100644 index 0000000..fc878e4 --- /dev/null +++ b/test/Verify.general.spec.ts @@ -0,0 +1,71 @@ +import chai from 'chai' +import deepEqualInAnyOrder from 'deep-equal-in-any-order' +import { verifyCredential } from '../src/Verify.js' +import { + getCredentialWithoutContext, + getCredentialWithoutVCContext, +} from '../src/test-fixtures/vc.js' +import { knownDIDRegistries } from '../.knownDidRegistries.js'; +import { + getExpectedFatalResult + } from '../src/test-fixtures/expectedResults.js'; + +chai.use(deepEqualInAnyOrder); +const {expect} = chai; + +const DISABLE_CONSOLE_WHEN_NO_ERRORS = true + +describe('Verify', () => { + + const originalLogFunction = console.log; + let output:string; + + beforeEach(function(done) { + if (DISABLE_CONSOLE_WHEN_NO_ERRORS) { + output = ''; + console.log = (msg) => { + output += msg + '\n'; + }; + } + done() + }); + + afterEach(function() { + if (DISABLE_CONSOLE_WHEN_NO_ERRORS) { + console.log = originalLogFunction; // undo dummy log function + if (this?.currentTest?.state === 'failed') { + console.log(output); + } + } + }); + + describe('.verifyCredential', () => { + + describe('returns fatal errors', () => { + + it('when not jsonld', async () => { + const credential : any = getCredentialWithoutContext() + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + const expectedResult = getExpectedFatalResult({ + credential, + errorMessage: 'The credential does not appear to be a valid jsonld document - there is no context.', + errorName: 'invalid_jsonld' + }) + expect(result).to.deep.equalInAnyOrder(expectedResult) + }) + + it('when no vc context', async () => { + const credential : any = getCredentialWithoutVCContext() + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + + const expectedResult = getExpectedFatalResult({ + credential, + errorMessage: "The credential doesn't have a verifiable credential context.", + errorName: 'no_vc_context' + }) + expect(result).to.deep.equalInAnyOrder(expectedResult) + }) + + }) +}) +}) \ No newline at end of file diff --git a/test/Verify.spec.ts b/test/Verify.v1.spec.ts similarity index 51% rename from test/Verify.spec.ts rename to test/Verify.v1.spec.ts index eff364c..332da84 100644 --- a/test/Verify.spec.ts +++ b/test/Verify.v1.spec.ts @@ -3,28 +3,16 @@ import deepEqualInAnyOrder from 'deep-equal-in-any-order' import { strict as assert } from 'assert'; import { verifyCredential } from '../src/Verify.js' import { - getVCv2Expired, getVCv1Tampered, getVCv1Expired, getVCv1Revoked, - getVCv2Revoked, getVCv1ValidStatus, - getVCv2ValidStatus, - getVCv2Tampered, getVCv1NoProof, - getVCv2NoProof, getCredentialWithoutContext, getCredentialWithoutVCContext, getVCv1NonURIId, - getVCv2NonURIId, getVCv1ExpiredAndTampered, - getVCv2ExpiredAndTampered, - getVCv1ExpiredWithValidStatus, - getVCv2ExpiredWithValidStatus, - getVCv2EddsaWithValidStatus, - getVCv2DoubleSigWithBadStatusUrl, - getVCv2DidWebWithValidStatus, - getVCv2WithBadDidWebUrl + getVCv1ExpiredWithValidStatus } from '../src/test-fixtures/vc.js' import { knownDIDRegistries } from '../.knownDidRegistries.js'; @@ -79,67 +67,6 @@ describe('Verify', () => { describe('.verifyCredential', () => { - describe('ed25519 and eddsa signature', () => { - describe('with VC version 2', () => { - describe('returns log error', () => { - it('when statuslist url is unreachable', async () => { - const credential : any = getVCv2DoubleSigWithBadStatusUrl() - const expectedResult = getExpectedVerifiedResult({credential, withStatus: false}) - expectedResult.log?.push( - { - "id": "revocation_status", - "error": { - "name": "status_list_not_found", - "message": "NotFoundError loading \"https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5VK8CbZ1GjycuPombrj\": Request failed with status code 404 Not Found: GET https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5VK8CbZ1GjycuPombrj" - } - }) - const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) - expect(result).to.deep.equalInAnyOrder(expectedResult) - }) - - }) - }) -}) - describe('with eddsa signature and', () => { - describe('with VC version 1', () => { - describe('it returns as verified', () => { - it('when status is valid', async () => { - const credential : any = getVCv2EddsaWithValidStatus() - const expectedResult = getExpectedVerifiedResult({credential, withStatus: true}) - const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) - expect(result).to.deep.equalInAnyOrder(expectedResult) - }) - }) - }) - - }) - describe('returns fatal errors', () => { - - it('when not jsonld', async () => { - const credential : any = getCredentialWithoutContext() - const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) - const expectedResult = getExpectedFatalResult({ - credential, - errorMessage: 'The credential does not appear to be a valid jsonld document - there is no context.', - errorName: 'invalid_jsonld' - }) - expect(result).to.deep.equalInAnyOrder(expectedResult) - }) - - it('when no vc context', async () => { - const credential : any = getCredentialWithoutVCContext() - const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) - - const expectedResult = getExpectedFatalResult({ - credential, - errorMessage: "The credential doesn't have a verifiable credential context.", - errorName: 'no_vc_context' - }) - expect(result).to.deep.equalInAnyOrder(expectedResult) - }) - - }) - describe('with VC version 1', () => { describe('returns fatal error', () => { @@ -282,123 +209,8 @@ describe('Verify', () => { const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define }) - }) - - }) - - describe('with VC version 2', () => { - - - - describe('returns fatal error', () => { - - it('when tampered with', async () => { - const credential : any = getVCv2Tampered() - const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) - const expectedResult = getExpectedFatalResult({ - credential, - errorMessage: 'The signature is not valid.', - errorName: 'invalid_signature' - }) - expect(result).to.deep.equalInAnyOrder(expectedResult) - }) - - it('when expired and tampered with', async () => { - const credential : any = getVCv2ExpiredAndTampered() - const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) - const expectedResult = getExpectedFatalResult({ - credential, - errorMessage: 'The signature is not valid.', - errorName: 'invalid_signature' - }) - expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define - }) - - it('when no proof', async () => { - const credential : any = getVCv2NoProof() - const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) - - const expectedResult = getExpectedFatalResult({ - credential, - errorMessage: 'This is not a Verifiable Credential - it does not have a digital signature.', - errorName: 'no_proof' - }) - expect(result).to.deep.equalInAnyOrder(expectedResult) - }) - it('when credential id is not a uri', async () => { - const credential : any = getVCv2NonURIId() - const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) - - const expectedResult = getExpectedFatalResult({ - credential, - errorMessage: "The credential's id uses an invalid format. It may have been issued as part of an early pilot. Please contact the issuer to get a replacement.", - errorName: 'invalid_credential_id' - }) - expect(result).to.deep.equalInAnyOrder(expectedResult) - }) - - it('when did:web url is unreachable', async () => { - const credential : any = getVCv2WithBadDidWebUrl() - const errorName = "did_web_unresolved" - const errorMessage = "The signature could not be checked because the public signing key could not be retrieved from https://digitalcredentials.github.io/dcc-did-web-bad/did.json" - const expectedResult = getExpectedFatalResult({credential, errorName, errorMessage}) - const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) - expect(result).to.deep.equalInAnyOrder(expectedResult) - }) - - }) - - describe('returns as verified', () => { - it('when status is valid', async () => { - const credential : any = getVCv2ValidStatus() - const expectedResult = getExpectedVerifiedResult({credential, withStatus: true}) - const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) - expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define - }) - describe('with did:web issuer', () => { - - it('when status is valid', async () => { - const credential : any = getVCv2DidWebWithValidStatus() - const expectedResult = getExpectedVerifiedResult({credential, withStatus: true}) - const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) - expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define - }) - - it('with different issuer for vc and statusList ', async () => { - const credential : any = getVCv2DidWebWithValidStatus() - const expectedResult = getExpectedVerifiedResult({credential, withStatus: true}) - const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) - expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define - }) - - }) - - }) - - describe('returns as unverified', () => { - it('when expired', async () => { - const credential : any = getVCv2Expired() - const expectedResult = getExpectedUnverifiedResult({credential, unVerifiedStep: 'expiration', withStatus:false}) - const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) - expect(result).to.deep.equalInAnyOrder(expectedResult) - }) - it('when revoked', async () => { - const credential : any = getVCv2Revoked() - const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) - assert.ok(result.log); - }) - it('when expired with valid status', async () => { - // NOTE: TODO - this will continue to fail until we fix https://github.com/digitalcredentials/vc/issues/28 - const credential : any = getVCv2ExpiredWithValidStatus() - const expectedResult = getExpectedUnverifiedResult({credential, unVerifiedStep: 'expiration', withStatus:true}) - const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) - expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define - }) - - }) - }) }) }) diff --git a/test/Verify.v2.spec.ts b/test/Verify.v2.spec.ts new file mode 100644 index 0000000..90fcfa1 --- /dev/null +++ b/test/Verify.v2.spec.ts @@ -0,0 +1,202 @@ +import chai from 'chai' +import deepEqualInAnyOrder from 'deep-equal-in-any-order' +import { strict as assert } from 'assert'; +import { verifyCredential } from '../src/Verify.js' +import { + getVCv2Expired, + getVCv2Revoked, + getVCv2ValidStatus, + getVCv2Tampered, + getVCv2NoProof, + getCredentialWithoutContext, + getCredentialWithoutVCContext, + getVCv2NonURIId, + getVCv2ExpiredAndTampered, + getVCv2ExpiredWithValidStatus, + getVCv2EddsaWithValidStatus, + getVCv2DoubleSigWithBadStatusUrl, + getVCv2DidWebWithValidStatus, + getVCv2WithBadDidWebUrl + +} from '../src/test-fixtures/vc.js' +import { knownDIDRegistries } from '../.knownDidRegistries.js'; +import { + getExpectedVerifiedResult, + getExpectedUnverifiedResult, + getExpectedFatalResult + } from '../src/test-fixtures/expectedResults.js'; + +chai.use(deepEqualInAnyOrder); +const {expect} = chai; + +const DISABLE_CONSOLE_WHEN_NO_ERRORS = true + +describe('Verify', () => { + + const originalLogFunction = console.log; + let output:string; + + beforeEach(function(done) { + if (DISABLE_CONSOLE_WHEN_NO_ERRORS) { + output = ''; + console.log = (msg) => { + output += msg + '\n'; + }; + } + done() + }); + + afterEach(function() { + if (DISABLE_CONSOLE_WHEN_NO_ERRORS) { + console.log = originalLogFunction; // undo dummy log function + if (this?.currentTest?.state === 'failed') { + console.log(output); + } + } + }); + + describe('.verifyCredential', () => { + + describe('with VC version 2', () => { + + describe('and ed25519 and eddsa signature', () => { + describe('returns log error', () => { + it('when statuslist url is unreachable', async () => { + const credential : any = getVCv2DoubleSigWithBadStatusUrl() + const expectedResult = getExpectedVerifiedResult({credential, withStatus: false}) + expectedResult.log?.push( + { + "id": "revocation_status", + "error": { + "name": "status_list_not_found", + "message": "NotFoundError loading \"https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5VK8CbZ1GjycuPombrj\": Request failed with status code 404 Not Found: GET https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5VK8CbZ1GjycuPombrj" + } + }) + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + expect(result).to.deep.equalInAnyOrder(expectedResult) + }) + + }) + }) + describe('with eddsa signature', () => { + describe('it returns as verified', () => { + it('when status is valid', async () => { + const credential : any = getVCv2EddsaWithValidStatus() + const expectedResult = getExpectedVerifiedResult({credential, withStatus: true}) + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + expect(result).to.deep.equalInAnyOrder(expectedResult) + }) + + }) + + + describe('returns fatal error', () => { + + it('when tampered with', async () => { + const credential : any = getVCv2Tampered() + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + const expectedResult = getExpectedFatalResult({ + credential, + errorMessage: 'The signature is not valid.', + errorName: 'invalid_signature' + }) + expect(result).to.deep.equalInAnyOrder(expectedResult) + }) + + it('when expired and tampered with', async () => { + const credential : any = getVCv2ExpiredAndTampered() + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + const expectedResult = getExpectedFatalResult({ + credential, + errorMessage: 'The signature is not valid.', + errorName: 'invalid_signature' + }) + expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define + }) + + it('when no proof', async () => { + const credential : any = getVCv2NoProof() + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + + const expectedResult = getExpectedFatalResult({ + credential, + errorMessage: 'This is not a Verifiable Credential - it does not have a digital signature.', + errorName: 'no_proof' + }) + expect(result).to.deep.equalInAnyOrder(expectedResult) + }) + it('when credential id is not a uri', async () => { + const credential : any = getVCv2NonURIId() + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + + const expectedResult = getExpectedFatalResult({ + credential, + errorMessage: "The credential's id uses an invalid format. It may have been issued as part of an early pilot. Please contact the issuer to get a replacement.", + errorName: 'invalid_credential_id' + }) + expect(result).to.deep.equalInAnyOrder(expectedResult) + }) + + it('when did:web url is unreachable', async () => { + const credential : any = getVCv2WithBadDidWebUrl() + const errorName = "did_web_unresolved" + const errorMessage = "The signature could not be checked because the public signing key could not be retrieved from https://digitalcredentials.github.io/dcc-did-web-bad/did.json" + const expectedResult = getExpectedFatalResult({credential, errorName, errorMessage}) + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + expect(result).to.deep.equalInAnyOrder(expectedResult) + }) + + }) + + describe('returns as verified', () => { + it('when status is valid', async () => { + const credential : any = getVCv2ValidStatus() + const expectedResult = getExpectedVerifiedResult({credential, withStatus: true}) + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define + }) + describe('with did:web issuer', () => { + + it('when status is valid', async () => { + const credential : any = getVCv2DidWebWithValidStatus() + const expectedResult = getExpectedVerifiedResult({credential, withStatus: true}) + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define + }) + + it('with different issuer for vc and statusList ', async () => { + const credential : any = getVCv2DidWebWithValidStatus() + const expectedResult = getExpectedVerifiedResult({credential, withStatus: true}) + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define + }) + + }) + + }) + + describe('returns as unverified', () => { + it('when expired', async () => { + const credential : any = getVCv2Expired() + const expectedResult = getExpectedUnverifiedResult({credential, unVerifiedStep: 'expiration', withStatus:false}) + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + expect(result).to.deep.equalInAnyOrder(expectedResult) + }) + it('when revoked', async () => { + const credential : any = getVCv2Revoked() + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + assert.ok(result.log); + }) + it('when expired with valid status', async () => { + // NOTE: TODO - this will continue to fail until we fix https://github.com/digitalcredentials/vc/issues/28 + const credential : any = getVCv2ExpiredWithValidStatus() + const expectedResult = getExpectedUnverifiedResult({credential, unVerifiedStep: 'expiration', withStatus:true}) + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define + }) + + }) + }) +}) +}) +}) \ No newline at end of file From 05d5ac285c415bf2e06947968816f653d05f2777 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Mon, 27 Jan 2025 20:14:14 -0500 Subject: [PATCH 51/72] transform verification response --- src/Verify.ts | 28 +++++++---- src/test-fixtures/expectedResults.ts | 22 ++++++++- src/types/result.ts | 18 ++++++- test/Verify.presentation.spec.ts | 70 +++++++++++++++++++--------- test/didAuth.ts | 2 +- 5 files changed, 104 insertions(+), 36 deletions(-) diff --git a/src/Verify.ts b/src/Verify.ts index 5b23d2b..50d5d35 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -8,7 +8,7 @@ import { getCredentialStatusChecker } from './credentialStatus.js'; import { addTrustedIssuersToVerificationResponse } from './issuerRegistries.js'; import { Credential } from './types/credential.js'; -import { VerificationResponse, VerificationStep } from './types/result.js'; +import { VerificationResponse, VerificationStep, PresentationVerificationResponse, PresentationSignatureResult } from './types/result.js'; import { VerifiablePresentation, PresentationError } from './types/presentation.js'; import { extractCredentialsFrom} from './extractCredentialsFrom.js'; @@ -32,7 +32,7 @@ export async function verifyPresentation({presentation, challenge = 'blah', unsi unsignedPresentation? : boolean, knownDIDRegistries: object, reloadIssuerRegistry?: boolean} -): Promise { +): Promise { try { const credential = extractCredentialsFrom(presentation)?.find( vc => vc.credentialStatus); @@ -48,19 +48,26 @@ export async function verifyPresentation({presentation, challenge = 'blah', unsi verifyMatchingIssuers: false }); - const transformedVCResults = await Promise.all(result.credentialResults.map(async (credentialResult:any) => { + const transformedCredentialResults = await Promise.all(result.credentialResults.map(async (credentialResult:any) => { return transformResponse(credentialResult, credentialResult.credential, knownDIDRegistries, reloadIssuerRegistry) })); + + // take what we need from the presentation part of the result + let signature : PresentationSignatureResult; + if (unsignedPresentation) { + signature = 'unsigned' + } else { + signature = result.presentationResult.verified ? 'valid' : 'invalid' + } + const errors = result.error ? [{message: result.error, name: 'presentation_error'}] : null + const presentationResult = {signature, ...(errors && {errors} ) } - result.credentialResults = transformedVCResults - - return result; - } catch (err) { - console.warn(err); - - throw new Error(PresentationError.CouldNotBeVerified); + return {presentationResult, credentialResults: transformedCredentialResults, isFatal: false}; + } catch (error) { + return {isFatal: true, errors: [{message: 'Could not verify presentation.', name: 'presentation_error', stackTrace: error}], } } +} export async function verifyCredential({ credential, knownDIDRegistries, reloadIssuerRegistry = true }: { credential: Credential, knownDIDRegistries: object, reloadIssuerRegistry: boolean }): Promise { @@ -102,6 +109,7 @@ async function transformResponse(verificationResponse:any, credential:Credential delete verificationResponse.results delete verificationResponse.statusResult delete verificationResponse.verified + delete verificationResponse.credentialId verificationResponse.log = verificationResponse.log.filter((entry:VerificationStep)=>entry.id !== 'issuer_did_resolves') // add things we always want in the response diff --git a/src/test-fixtures/expectedResults.ts b/src/test-fixtures/expectedResults.ts index a72e4fd..b92287d 100644 --- a/src/test-fixtures/expectedResults.ts +++ b/src/test-fixtures/expectedResults.ts @@ -1,5 +1,11 @@ -import { VerificationResponse, VerificationStep } from "src/types/result"; +import { VerificationResponse, VerificationStep, PresentationVerificationResponse } from "src/types/result"; +const expectedPresentationResult = { + "isFatal": false, + "presentationResult": { + "signature": 'valid', + } +} const expectedResult = { "credential": {}, "isFatal": false, @@ -56,6 +62,11 @@ const expectedResult = { return expectedResultCopy; } + const getCopyOfExpectedVPResult = () : PresentationVerificationResponse => { + return JSON.parse(JSON.stringify(expectedPresentationResult)) + } + + const getExpectedVerifiedResult = ({credential, withStatus }: {credential:object, withStatus:boolean}) : VerificationResponse => { return getCopyOfExpectedResult(credential, withStatus); } @@ -72,8 +83,15 @@ const expectedResult = { return expectedResult; } + const getExpectedVerifiedPresentationResult = ({credentialResults}: {credentialResults:VerificationResponse[]}) : PresentationVerificationResponse => { + const expectedResult = getCopyOfExpectedVPResult(); + expectedResult.credentialResults = credentialResults + return expectedResult; + } + export { getExpectedVerifiedResult, getExpectedUnverifiedResult, - getExpectedFatalResult + getExpectedFatalResult, + getExpectedVerifiedPresentationResult } \ No newline at end of file diff --git a/src/types/result.ts b/src/types/result.ts index 6da9be7..617df2c 100644 --- a/src/types/result.ts +++ b/src/types/result.ts @@ -2,7 +2,7 @@ export interface VerificationError { "message": string, "name"?: string, - "stackTrace"?: string + "stackTrace"?: any } export interface VerificationStep { @@ -20,6 +20,22 @@ export interface VerificationError { "log"?: VerificationStep[] } + + const signatureOptions = ['valid', 'invalid', 'unsigned'] as const; + export type PresentationSignatureResult = typeof signatureOptions[number]; //'valid', 'invalid', 'unsigned' + + export interface PresentationResult { + "signature":PresentationSignatureResult, + "error"?: any + } + + export interface PresentationVerificationResponse { + "isFatal": boolean, + "credentialResults"?: VerificationResponse[], + "presentationResult"?: PresentationResult, + "errors"?: VerificationError[] + } + export interface RegistryListResult { foundInRegistries: string[] registriesNotLoaded: RegistriesNotLoaded[] diff --git a/test/Verify.presentation.spec.ts b/test/Verify.presentation.spec.ts index 3cd7402..666c3ea 100644 --- a/test/Verify.presentation.spec.ts +++ b/test/Verify.presentation.spec.ts @@ -10,7 +10,8 @@ import { } from '../src/test-fixtures/vc.js' import { knownDIDRegistries } from '../.knownDidRegistries.js'; import { - getExpectedVerifiedResult + getExpectedVerifiedResult, + getExpectedVerifiedPresentationResult } from '../src/test-fixtures/expectedResults.js'; import { getSignedDIDAuth, verifyDIDAuth } from './didAuth.js'; @@ -22,7 +23,9 @@ const {expect} = chai; const DISABLE_CONSOLE_WHEN_NO_ERRORS = false -describe('Verify', () => { +describe('Verify.verifyPresentation', () => { + + const holder = 'did:ex:12345'; const originalLogFunction = console.log; let output:string; @@ -46,28 +49,51 @@ describe('Verify', () => { } }); - describe('.verifyPresentation', () => { - - describe('it returns as verified', () => { - - it('when presentation is valid', async () => { - const firstVC : any = getVCv2DidWebWithValidStatus() - const secondVC : any = getVCv2ValidStatus() - const noProofVC : any = getVCv1NoProof() - const badIdVC : any = getVCv2NonURIId() - const verifiableCredential = [firstVC, secondVC, firstVC] - // const expectedResult = getExpectedVerifiedResult({credential: verifiableCredential, withStatus: true}) - const presentation = await getSignedDIDAuth({verifiableCredential, holder: 'did:ex:12345'}) as VerifiablePresentation - // console.log(JSON.stringify(presentation, null, 2)) - const result = await verifyPresentation({presentation, knownDIDRegistries}) - //await verifyDIDAuth({presentation, challenge}) - console.log("====================== verification result") - console.log(JSON.stringify(result,null,2)) - expect(true) - }) + /* + - vp signed + - vp unsigned + - passing in good challenge + - passing in bad challenge + - vp with bad vc + - vp with no vcs + */ + + + describe('it returns as verified', () => { + + it.only('when signed presentation has one vc', async () => { + const singleVC : any = getVCv2ValidStatus() + const verifiableCredential= [singleVC] + const presentation = await getSignedDIDAuth({holder, verifiableCredential}) as VerifiablePresentation + const expectedVCResult = getExpectedVerifiedResult({credential:singleVC, withStatus: true}) + const expectedPresentationResult = getExpectedVerifiedPresentationResult({credentialResults:[expectedVCResult]}) + + const result = await verifyPresentation({presentation, knownDIDRegistries}) + console.log("====================== verification result") + console.log(JSON.stringify(result,null,2)) + expect(result).to.deep.equalInAnyOrder(expectedPresentationResult) }) - }) + it('when presentation is valid', async () => { + const firstVC : any = getVCv2DidWebWithValidStatus() + const secondVC : any = getVCv2ValidStatus() + const noProofVC : any = getVCv1NoProof() + const badIdVC : any = getVCv2NonURIId() + const verifiableCredential = [firstVC, secondVC, firstVC] + // const expectedResult = getExpectedVerifiedResult({credential: verifiableCredential, withStatus: true}) + const presentation = await getSignedDIDAuth({verifiableCredential, holder: 'did:ex:12345'}) as VerifiablePresentation + // console.log(JSON.stringify(presentation, null, 2)) + const result = await verifyPresentation({presentation, knownDIDRegistries}) + //await verifyDIDAuth({presentation, challenge}) + //console.log("====================== verification result") + //console.log(JSON.stringify(result,null,2)) + expect(true) }) + + + }) +}) + + diff --git a/test/didAuth.ts b/test/didAuth.ts index a9882ac..42c9ede 100644 --- a/test/didAuth.ts +++ b/test/didAuth.ts @@ -25,7 +25,7 @@ const key = await Ed25519VerificationKey2020.generate( const signingSuite = new Ed25519Signature2020({key}); -export const getSignedDIDAuth = async ({holder, verifiableCredential}:{holder:string,verifiableCredential:any}):Promise => { +export const getSignedDIDAuth = async ({holder, verifiableCredential}:{holder:string,verifiableCredential?:any}):Promise => { const presentation = createPresentation({holder, verifiableCredential}); const challenge = 'canbeanything33' return await signPresentation({ From dba2d31f276382adb165f486a17f8e7c6b244dbe Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Tue, 28 Jan 2025 07:13:36 -0500 Subject: [PATCH 52/72] fix lint errors --- src/Verify.ts | 2 +- src/types/result.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Verify.ts b/src/Verify.ts index 50d5d35..9bbb11b 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -9,7 +9,7 @@ import { addTrustedIssuersToVerificationResponse } from './issuerRegistries.js'; import { Credential } from './types/credential.js'; import { VerificationResponse, VerificationStep, PresentationVerificationResponse, PresentationSignatureResult } from './types/result.js'; -import { VerifiablePresentation, PresentationError } from './types/presentation.js'; +import { VerifiablePresentation } from './types/presentation.js'; import { extractCredentialsFrom} from './extractCredentialsFrom.js'; diff --git a/src/types/result.ts b/src/types/result.ts index 617df2c..7368218 100644 --- a/src/types/result.ts +++ b/src/types/result.ts @@ -22,7 +22,7 @@ export interface VerificationError { const signatureOptions = ['valid', 'invalid', 'unsigned'] as const; - export type PresentationSignatureResult = typeof signatureOptions[number]; //'valid', 'invalid', 'unsigned' + export type PresentationSignatureResult = typeof signatureOptions[number]; // i.e., 'valid', 'invalid', 'unsigned' export interface PresentationResult { "signature":PresentationSignatureResult, From ee809a86b5a5e85206789e7cf6a0b61b08eafb7c Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Tue, 28 Jan 2025 09:37:27 -0500 Subject: [PATCH 53/72] add mixed VC presentation test --- test/Verify.presentation.spec.ts | 89 ++++++++++++++++++++++---------- test/Verify.v1.spec.ts | 2 - 2 files changed, 62 insertions(+), 29 deletions(-) diff --git a/test/Verify.presentation.spec.ts b/test/Verify.presentation.spec.ts index 666c3ea..d5bc6cc 100644 --- a/test/Verify.presentation.spec.ts +++ b/test/Verify.presentation.spec.ts @@ -1,22 +1,67 @@ import chai from 'chai' import deepEqualInAnyOrder from 'deep-equal-in-any-order' -import { verifyCredential, verifyPresentation } from '../src/Verify.js' +import { verifyPresentation } from '../src/Verify.js' import { - getVCv2DidWebWithValidStatus, + getVCv2Expired, + getVCv2Revoked, + getVCv2ValidStatus, + getVCv2Tampered, + getVCv2NoProof, + getCredentialWithoutContext, + getCredentialWithoutVCContext, + getVCv2NonURIId, + getVCv2ExpiredAndTampered, + getVCv2ExpiredWithValidStatus, getVCv2EddsaWithValidStatus, - getVCv2ValidStatus, - getVCv1NoProof, - getVCv2NonURIId + getVCv2DoubleSigWithBadStatusUrl, + getVCv2DidWebWithValidStatus, + getVCv2WithBadDidWebUrl + } from '../src/test-fixtures/vc.js' + import { knownDIDRegistries } from '../.knownDidRegistries.js'; import { getExpectedVerifiedResult, + getExpectedUnverifiedResult, + getExpectedFatalResult, getExpectedVerifiedPresentationResult } from '../src/test-fixtures/expectedResults.js'; - import { getSignedDIDAuth, verifyDIDAuth } from './didAuth.js'; + import { + getVCv1Tampered, + getVCv1Expired, + getVCv1Revoked, + getVCv1ValidStatus, + getVCv1NoProof, + getVCv1NonURIId, + getVCv1ExpiredAndTampered, + getVCv1ExpiredWithValidStatus +} from '../src/test-fixtures/vc.js' + +import { getSignedDIDAuth } from './didAuth.js'; import { VerifiablePresentation } from '../src/types/presentation.js'; + + const noProofVC : any = getVCv1NoProof() + const expectedNoProofResult = getExpectedFatalResult({ + credential: noProofVC, + errorMessage: 'This is not a Verifiable Credential - it does not have a digital signature.', + errorName: 'no_proof' + }) + + + const badIdVC : any = getVCv2NonURIId() + + const didWebVC : any = getVCv2DidWebWithValidStatus() + const expectedDidWebResult = getExpectedVerifiedResult({credential:didWebVC, withStatus: true}) + + const v2WithStatus : any = getVCv2ValidStatus() + const expectedV2WithStatusResult = getExpectedVerifiedResult({credential:v2WithStatus, withStatus: true}) + + const v2Eddsa : any = getVCv2EddsaWithValidStatus() + const expectedv2EddsaResult = getExpectedVerifiedResult({credential: v2Eddsa, withStatus: true}) + + chai.use(deepEqualInAnyOrder); const {expect} = chai; @@ -60,34 +105,24 @@ describe('Verify.verifyPresentation', () => { describe('it returns as verified', () => { - - it.only('when signed presentation has one vc', async () => { - const singleVC : any = getVCv2ValidStatus() - const verifiableCredential= [singleVC] + + it('when signed presentation has one vc', async () => { + + const verifiableCredential= [v2WithStatus] const presentation = await getSignedDIDAuth({holder, verifiableCredential}) as VerifiablePresentation - const expectedVCResult = getExpectedVerifiedResult({credential:singleVC, withStatus: true}) - const expectedPresentationResult = getExpectedVerifiedPresentationResult({credentialResults:[expectedVCResult]}) - + const credentialResults = [expectedV2WithStatusResult] + const expectedPresentationResult = getExpectedVerifiedPresentationResult({credentialResults}) const result = await verifyPresentation({presentation, knownDIDRegistries}) - console.log("====================== verification result") - console.log(JSON.stringify(result,null,2)) expect(result).to.deep.equalInAnyOrder(expectedPresentationResult) }) - it('when presentation is valid', async () => { - const firstVC : any = getVCv2DidWebWithValidStatus() - const secondVC : any = getVCv2ValidStatus() - const noProofVC : any = getVCv1NoProof() - const badIdVC : any = getVCv2NonURIId() - const verifiableCredential = [firstVC, secondVC, firstVC] - // const expectedResult = getExpectedVerifiedResult({credential: verifiableCredential, withStatus: true}) + it('when signed presentation has mix of VCs', async () => { + const verifiableCredential = [v2WithStatus, v2Eddsa, didWebVC] const presentation = await getSignedDIDAuth({verifiableCredential, holder: 'did:ex:12345'}) as VerifiablePresentation - // console.log(JSON.stringify(presentation, null, 2)) + const credentialResults = [expectedV2WithStatusResult, expectedv2EddsaResult, expectedDidWebResult] + const expectedPresentationResult = getExpectedVerifiedPresentationResult({credentialResults}) const result = await verifyPresentation({presentation, knownDIDRegistries}) - //await verifyDIDAuth({presentation, challenge}) - //console.log("====================== verification result") - //console.log(JSON.stringify(result,null,2)) - expect(true) + expect(result).to.deep.equalInAnyOrder(expectedPresentationResult) }) diff --git a/test/Verify.v1.spec.ts b/test/Verify.v1.spec.ts index 332da84..54cfeaa 100644 --- a/test/Verify.v1.spec.ts +++ b/test/Verify.v1.spec.ts @@ -8,8 +8,6 @@ import { getVCv1Revoked, getVCv1ValidStatus, getVCv1NoProof, - getCredentialWithoutContext, - getCredentialWithoutVCContext, getVCv1NonURIId, getVCv1ExpiredAndTampered, getVCv1ExpiredWithValidStatus From 8bce92c7b322e78f8d0703b784726aec9badf44d Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Tue, 28 Jan 2025 10:32:09 -0500 Subject: [PATCH 54/72] add unsigned presentation tests --- test/Verify.presentation.spec.ts | 69 ++++++++++++++++++++++++++++++-- test/didAuth.ts | 8 +++- 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/test/Verify.presentation.spec.ts b/test/Verify.presentation.spec.ts index d5bc6cc..0767393 100644 --- a/test/Verify.presentation.spec.ts +++ b/test/Verify.presentation.spec.ts @@ -38,7 +38,7 @@ import { getVCv1ExpiredWithValidStatus } from '../src/test-fixtures/vc.js' -import { getSignedDIDAuth } from './didAuth.js'; +import { getSignedVP, getUnSignedVP } from './didAuth.js'; import { VerifiablePresentation } from '../src/types/presentation.js'; @@ -51,7 +51,13 @@ import { VerifiablePresentation } from '../src/types/presentation.js'; const badIdVC : any = getVCv2NonURIId() - + const expectedBadIdResult = getExpectedFatalResult({ + credential: badIdVC, + errorMessage: "The credential's id uses an invalid format. It may have been issued as part of an early pilot. Please contact the issuer to get a replacement.", + errorName: 'invalid_credential_id' + }) + + const didWebVC : any = getVCv2DidWebWithValidStatus() const expectedDidWebResult = getExpectedVerifiedResult({credential:didWebVC, withStatus: true}) @@ -109,7 +115,7 @@ describe('Verify.verifyPresentation', () => { it('when signed presentation has one vc', async () => { const verifiableCredential= [v2WithStatus] - const presentation = await getSignedDIDAuth({holder, verifiableCredential}) as VerifiablePresentation + const presentation = await getSignedVP({holder, verifiableCredential}) as VerifiablePresentation const credentialResults = [expectedV2WithStatusResult] const expectedPresentationResult = getExpectedVerifiedPresentationResult({credentialResults}) const result = await verifyPresentation({presentation, knownDIDRegistries}) @@ -118,15 +124,70 @@ describe('Verify.verifyPresentation', () => { it('when signed presentation has mix of VCs', async () => { const verifiableCredential = [v2WithStatus, v2Eddsa, didWebVC] - const presentation = await getSignedDIDAuth({verifiableCredential, holder: 'did:ex:12345'}) as VerifiablePresentation + const presentation = await getSignedVP({verifiableCredential, holder: 'did:ex:12345'}) as VerifiablePresentation const credentialResults = [expectedV2WithStatusResult, expectedv2EddsaResult, expectedDidWebResult] const expectedPresentationResult = getExpectedVerifiedPresentationResult({credentialResults}) const result = await verifyPresentation({presentation, knownDIDRegistries}) expect(result).to.deep.equalInAnyOrder(expectedPresentationResult) }) + }) + + describe('it returns as unverified', () => { + + it.skip('when signed presentation has bad vc', async () => { + /// hmmmmm, this returns an error because of that check on jsonLD.getValue. + // Think I need to catch the error and return a fatal error of some sort. + const verifiableCredential= [badIdVC] + const presentation = await getSignedVP({holder, verifiableCredential}) as VerifiablePresentation + const credentialResults = [expectedBadIdResult] + const expectedPresentationResult = getExpectedVerifiedPresentationResult({credentialResults}) + const result = await verifyPresentation({presentation, knownDIDRegistries}) + expect(result).to.deep.equalInAnyOrder(expectedPresentationResult) + }) + + it('when signed presentation has no proof vc', async () => { + /// hmmmmm, this returns an error because of that check on jsonLD.getValue. + // Think I need to catch the error and return a fatal error of some sort. + const verifiableCredential= [noProofVC] + const presentation = await getSignedVP({holder, verifiableCredential}) as VerifiablePresentation + const credentialResults = [expectedNoProofResult] + const expectedPresentationResult = getExpectedVerifiedPresentationResult({credentialResults}) + const result = await verifyPresentation({presentation, knownDIDRegistries}) + expect(result).to.deep.equalInAnyOrder(expectedPresentationResult) + }) + + it('when unsigned presentation', async () => { + /// hmmmmm, this returns an error because of that check on jsonLD.getValue. + // Think I need to catch the error and return a fatal error of some sort. + const verifiableCredential= [noProofVC] + const presentation = getUnSignedVP({verifiableCredential}) as VerifiablePresentation + const credentialResults = [expectedNoProofResult] + // this should return isFatal=true on the whole thing, but isn't + const expectedPresentationResult = getExpectedVerifiedPresentationResult({credentialResults}) + + if (expectedPresentationResult?.presentationResult) { + expectedPresentationResult.presentationResult.signature = 'unsigned' + } + + const result = await verifyPresentation({presentation, knownDIDRegistries, unsignedPresentation:true}) + //console.log(result) + expect(result).to.deep.equalInAnyOrder(expectedPresentationResult) + }) + + it.skip('when unsigned presentation not properly specified', async () => { + /// hmmmmm, NEED TO HAVE AN ERROR FOR EXPECTED RESULT + const verifiableCredential= [noProofVC] + const presentation = await getUnSignedVP({verifiableCredential}) as VerifiablePresentation + const credentialResults = [expectedNoProofResult] + const expectedPresentationResult = getExpectedVerifiedPresentationResult({credentialResults}) + const result = await verifyPresentation({presentation, knownDIDRegistries}) + console.log(result) + expect(result).to.deep.equalInAnyOrder(expectedPresentationResult) + }) }) + }) diff --git a/test/didAuth.ts b/test/didAuth.ts index 42c9ede..3ca382a 100644 --- a/test/didAuth.ts +++ b/test/didAuth.ts @@ -7,6 +7,8 @@ import {Ed25519VerificationKey2020} from '@digitalcredentials/ed25519-verificati const documentLoader = securityLoader().build() import pkg from '@digitalcredentials/jsonld-signatures'; +import { verifyCredential } from '../src/Verify'; +import { VerifiablePresentation } from '../src/types/presentation'; const { purposes } = pkg; const presentationPurpose = new purposes.AssertionProofPurpose(); @@ -25,7 +27,7 @@ const key = await Ed25519VerificationKey2020.generate( const signingSuite = new Ed25519Signature2020({key}); -export const getSignedDIDAuth = async ({holder, verifiableCredential}:{holder:string,verifiableCredential?:any}):Promise => { +export const getSignedVP = async ({holder, verifiableCredential}:{holder:string,verifiableCredential?:any}):Promise => { const presentation = createPresentation({holder, verifiableCredential}); const challenge = 'canbeanything33' return await signPresentation({ @@ -33,6 +35,10 @@ export const getSignedDIDAuth = async ({holder, verifiableCredential}:{holder:st }); } +export const getUnSignedVP = ({verifiableCredential}:{verifiableCredential?:any}):VerifiablePresentation => { + return createPresentation({verifiableCredential}); +} + const verificationSuite = new Ed25519Signature2020(); From dd1116cab9388d0c0f75c2a7af061d5173741a34 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Tue, 28 Jan 2025 12:56:43 -0500 Subject: [PATCH 55/72] remove isFatal flag from results --- src/Verify.ts | 7 +++---- src/test-fixtures/expectedResults.ts | 3 --- src/types/result.ts | 2 -- test/Verify.presentation.spec.ts | 2 +- 4 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/Verify.ts b/src/Verify.ts index 9bbb11b..f27137a 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -62,9 +62,9 @@ export async function verifyPresentation({presentation, challenge = 'blah', unsi const errors = result.error ? [{message: result.error, name: 'presentation_error'}] : null const presentationResult = {signature, ...(errors && {errors} ) } - return {presentationResult, credentialResults: transformedCredentialResults, isFatal: false}; + return {presentationResult, credentialResults: transformedCredentialResults}; } catch (error) { - return {isFatal: true, errors: [{message: 'Could not verify presentation.', name: 'presentation_error', stackTrace: error}], + return {errors: [{message: 'Could not verify presentation.', name: 'presentation_error', stackTrace: error}], } } } @@ -114,13 +114,12 @@ async function transformResponse(verificationResponse:any, credential:Credential // add things we always want in the response verificationResponse.credential = credential - verificationResponse.isFatal = false return verificationResponse as VerificationResponse; } function buildFatalErrorObject(fatalErrorMessage: string, name: string, credential: Credential, stackTrace: string | null): VerificationResponse { - return { credential, isFatal: true, errors: [{ name, message: fatalErrorMessage, ...stackTrace ? { stackTrace } : null }] } + return { credential, errors: [{ name, message: fatalErrorMessage, ...stackTrace ? { stackTrace } : null }] } } function handleAnyFatalCredentialErrors(credential: Credential): VerificationResponse | null { diff --git a/src/test-fixtures/expectedResults.ts b/src/test-fixtures/expectedResults.ts index b92287d..4523bdc 100644 --- a/src/test-fixtures/expectedResults.ts +++ b/src/test-fixtures/expectedResults.ts @@ -1,14 +1,12 @@ import { VerificationResponse, VerificationStep, PresentationVerificationResponse } from "src/types/result"; const expectedPresentationResult = { - "isFatal": false, "presentationResult": { "signature": 'valid', } } const expectedResult = { "credential": {}, - "isFatal": false, "log": [ { "id": "valid_signature", @@ -31,7 +29,6 @@ const expectedResult = { const fatalResult = { credential: {}, - isFatal: true, errors: [ { name: 'error name goes here, e.g., no_proof', diff --git a/src/types/result.ts b/src/types/result.ts index 7368218..114b1cf 100644 --- a/src/types/result.ts +++ b/src/types/result.ts @@ -14,7 +14,6 @@ export interface VerificationError { } export interface VerificationResponse { - "isFatal": boolean, "credential": object, "errors"?: VerificationError[], "log"?: VerificationStep[] @@ -30,7 +29,6 @@ export interface VerificationError { } export interface PresentationVerificationResponse { - "isFatal": boolean, "credentialResults"?: VerificationResponse[], "presentationResult"?: PresentationResult, "errors"?: VerificationError[] diff --git a/test/Verify.presentation.spec.ts b/test/Verify.presentation.spec.ts index 0767393..3e226ec 100644 --- a/test/Verify.presentation.spec.ts +++ b/test/Verify.presentation.spec.ts @@ -171,7 +171,7 @@ describe('Verify.verifyPresentation', () => { } const result = await verifyPresentation({presentation, knownDIDRegistries, unsignedPresentation:true}) - //console.log(result) + console.log(result) expect(result).to.deep.equalInAnyOrder(expectedPresentationResult) }) From d8435a11189fa9314169e5da3ce4868a84f8303f Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Tue, 28 Jan 2025 14:07:44 -0500 Subject: [PATCH 56/72] update tests --- src/test-fixtures/expectedResults.ts | 5 ++++- test/Verify.presentation.spec.ts | 29 ++++++++++------------------ test/{didAuth.ts => vpUtils.ts} | 0 3 files changed, 14 insertions(+), 20 deletions(-) rename test/{didAuth.ts => vpUtils.ts} (100%) diff --git a/src/test-fixtures/expectedResults.ts b/src/test-fixtures/expectedResults.ts index 4523bdc..8621073 100644 --- a/src/test-fixtures/expectedResults.ts +++ b/src/test-fixtures/expectedResults.ts @@ -80,9 +80,12 @@ const expectedResult = { return expectedResult; } - const getExpectedVerifiedPresentationResult = ({credentialResults}: {credentialResults:VerificationResponse[]}) : PresentationVerificationResponse => { + const getExpectedVerifiedPresentationResult = ({credentialResults, unsigned = false}: {credentialResults:VerificationResponse[], unsigned?:boolean}) : PresentationVerificationResponse => { const expectedResult = getCopyOfExpectedVPResult(); expectedResult.credentialResults = credentialResults + if (unsigned && expectedResult.presentationResult) { + expectedResult.presentationResult.signature = 'unsigned' + } return expectedResult; } diff --git a/test/Verify.presentation.spec.ts b/test/Verify.presentation.spec.ts index 3e226ec..87238a9 100644 --- a/test/Verify.presentation.spec.ts +++ b/test/Verify.presentation.spec.ts @@ -38,7 +38,7 @@ import { getVCv1ExpiredWithValidStatus } from '../src/test-fixtures/vc.js' -import { getSignedVP, getUnSignedVP } from './didAuth.js'; +import { getSignedVP, getUnSignedVP } from './vpUtils.js'; import { VerifiablePresentation } from '../src/types/presentation.js'; @@ -135,20 +135,19 @@ describe('Verify.verifyPresentation', () => { describe('it returns as unverified', () => { - it.skip('when signed presentation has bad vc', async () => { - /// hmmmmm, this returns an error because of that check on jsonLD.getValue. - // Think I need to catch the error and return a fatal error of some sort. + it('when unsigned presentation has bad vc', async () => { + /// NOTE that this is an unsigned vp because the vc libs signing + // method doesn't allow signing a VP with a 'bad' VC, so + // we can't easily get a test vp const verifiableCredential= [badIdVC] - const presentation = await getSignedVP({holder, verifiableCredential}) as VerifiablePresentation + const presentation = await getUnSignedVP({verifiableCredential}) as VerifiablePresentation const credentialResults = [expectedBadIdResult] - const expectedPresentationResult = getExpectedVerifiedPresentationResult({credentialResults}) - const result = await verifyPresentation({presentation, knownDIDRegistries}) + const expectedPresentationResult = getExpectedVerifiedPresentationResult({credentialResults, unsigned: true}) + const result = await verifyPresentation({presentation, knownDIDRegistries, unsignedPresentation: true}) expect(result).to.deep.equalInAnyOrder(expectedPresentationResult) }) it('when signed presentation has no proof vc', async () => { - /// hmmmmm, this returns an error because of that check on jsonLD.getValue. - // Think I need to catch the error and return a fatal error of some sort. const verifiableCredential= [noProofVC] const presentation = await getSignedVP({holder, verifiableCredential}) as VerifiablePresentation const credentialResults = [expectedNoProofResult] @@ -158,32 +157,24 @@ describe('Verify.verifyPresentation', () => { }) it('when unsigned presentation', async () => { - /// hmmmmm, this returns an error because of that check on jsonLD.getValue. - // Think I need to catch the error and return a fatal error of some sort. const verifiableCredential= [noProofVC] const presentation = getUnSignedVP({verifiableCredential}) as VerifiablePresentation const credentialResults = [expectedNoProofResult] - // this should return isFatal=true on the whole thing, but isn't const expectedPresentationResult = getExpectedVerifiedPresentationResult({credentialResults}) - if (expectedPresentationResult?.presentationResult) { expectedPresentationResult.presentationResult.signature = 'unsigned' } - const result = await verifyPresentation({presentation, knownDIDRegistries, unsignedPresentation:true}) - console.log(result) expect(result).to.deep.equalInAnyOrder(expectedPresentationResult) }) - it.skip('when unsigned presentation not properly specified', async () => { + it('when unsigned presentation not properly specified', async () => { /// hmmmmm, NEED TO HAVE AN ERROR FOR EXPECTED RESULT const verifiableCredential= [noProofVC] const presentation = await getUnSignedVP({verifiableCredential}) as VerifiablePresentation const credentialResults = [expectedNoProofResult] - const expectedPresentationResult = getExpectedVerifiedPresentationResult({credentialResults}) const result = await verifyPresentation({presentation, knownDIDRegistries}) - console.log(result) - expect(result).to.deep.equalInAnyOrder(expectedPresentationResult) + expect(result?.presentationResult?.signature).to.equal('invalid') }) }) diff --git a/test/didAuth.ts b/test/vpUtils.ts similarity index 100% rename from test/didAuth.ts rename to test/vpUtils.ts From e70be8d34fb56b5a62b39fbe08185ad806a2467f Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Tue, 28 Jan 2025 14:12:37 -0500 Subject: [PATCH 57/72] add test for bad challenge and presentation purpose --- test/Verify.presentation.spec.ts | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/test/Verify.presentation.spec.ts b/test/Verify.presentation.spec.ts index 87238a9..93177d5 100644 --- a/test/Verify.presentation.spec.ts +++ b/test/Verify.presentation.spec.ts @@ -100,20 +100,9 @@ describe('Verify.verifyPresentation', () => { } }); - /* - - vp signed - - vp unsigned - - passing in good challenge - - passing in bad challenge - - vp with bad vc - - vp with no vcs - */ - - describe('it returns as verified', () => { it('when signed presentation has one vc', async () => { - const verifiableCredential= [v2WithStatus] const presentation = await getSignedVP({holder, verifiableCredential}) as VerifiablePresentation const credentialResults = [expectedV2WithStatusResult] @@ -131,6 +120,15 @@ describe('Verify.verifyPresentation', () => { expect(result).to.deep.equalInAnyOrder(expectedPresentationResult) }) + it('when wrong challenge and presentation purpose', async () => { + const verifiableCredential= [v2WithStatus] + const presentation = await getSignedVP({holder, verifiableCredential}) as VerifiablePresentation + const credentialResults = [expectedV2WithStatusResult] + const expectedPresentationResult = getExpectedVerifiedPresentationResult({credentialResults}) + const result = await verifyPresentation({presentation, knownDIDRegistries, challenge: 'blahblahblue'}) + expect(result).to.deep.equalInAnyOrder(expectedPresentationResult) + }) + }) describe('it returns as unverified', () => { From 10a625f970294e3278fca5b545889420e0e8c790 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Tue, 28 Jan 2025 14:54:34 -0500 Subject: [PATCH 58/72] add coverage --- .github/workflows/main.yml | 14 +++++++++----- README.md | 1 + package.json | 4 +++- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e1b4349..342814e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,16 +8,20 @@ jobs: timeout-minutes: 10 strategy: matrix: - node-version: [18.x] + node-version: [20.x] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm install - name: Run test with Node.js ${{ matrix.node-version }} - run: npm run test-node + run: npm run coveralls + - name: Coveralls GitHub Action + uses: coverallsapp/github-action@v2 + with: + github-token: ${{ github.token }} env: CI: true # test-karma: @@ -40,7 +44,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [18.x] + node-version: [20.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} diff --git a/README.md b/README.md index 0e82283..62e1072 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![Build status](https://img.shields.io/github/actions/workflow/status/digitalcredentials/verifier-core/main.yml?branch=main)](https://github.com/digitalcredentials/verifier-core/actions?query=workflow%3A%22Node.js+CI%22) [![NPM Version](https://img.shields.io/npm/v/@digitalcredentials/verifier-core.svg)](https://npm.im/@digitalcredentials/verifier-core) +[![Coverage Status](https://coveralls.io/repos/github/digitalcredentials/verifier-core/badge.svg?branch=main)](https://coveralls.io/github/digitalcredentials/verifier-core?branch=main) > Verifies W3C Verifiable Credentials in the browser, Node.js, and React Native. diff --git a/package.json b/package.json index 528d35e..15fcb28 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "test": "npm run lint && npm run test-node", "test-karma": "karma start karma.conf.js", "test-node-old": "cross-env NODE_ENV=test TS_NODE_PROJECT=tsconfig.spec.json TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\" }' mocha -r ts-node/register --project tsconfig.spec.json 'test/*.ts'", - "test-node": "npm run build-test && mocha dist/test/*.spec.js && rm -rf dist/esm/test || true" + "test-node": "npm run build-test && npx c8 mocha dist/test/*.spec.js && rm -rf dist/esm/test || true", + "coveralls": "npm run test; npx c8 report --reporter=text-lcov > ./coverage/lcov.info" }, "files": [ "dist", @@ -43,6 +44,7 @@ "@typescript-eslint/eslint-plugin": "^5.46.1", "@typescript-eslint/parser": "^5.46.1", "chai": "^4.3.7", + "coveralls": "^3.1.1", "cross-env": "^7.0.3", "deep-equal-in-any-order": "^2.0.6", "eslint": "^8.30.0", From 30e46b6250f060b1d34e3c50c6c66c7b0d2d5713 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Tue, 28 Jan 2025 16:19:40 -0500 Subject: [PATCH 59/72] enable coverage badge --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 62e1072..0983ced 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # verifier-core _(@digitalcredentials/verifier-core)_ -[![Build status](https://img.shields.io/github/actions/workflow/status/digitalcredentials/verifier-core/main.yml?branch=main)](https://github.com/digitalcredentials/verifier-core/actions?query=workflow%3A%22Node.js+CI%22) +[![Build status](https://img.shields.io/github/actions/workflow/status/digitalcredentials/verifier-core/main.yml?branch=jc-implement)](https://github.com/digitalcredentials/verifier-core/actions?query=workflow%3A%22Node.js+CI%22) [![NPM Version](https://img.shields.io/npm/v/@digitalcredentials/verifier-core.svg)](https://npm.im/@digitalcredentials/verifier-core) -[![Coverage Status](https://coveralls.io/repos/github/digitalcredentials/verifier-core/badge.svg?branch=main)](https://coveralls.io/github/digitalcredentials/verifier-core?branch=main) +[![Coverage Status](https://coveralls.io/repos/github/digitalcredentials/verifier-core/badge.svg?branch=jc-implement)](https://coveralls.io/github/digitalcredentials/verifier-core?branch=jc-implement) > Verifies W3C Verifiable Credentials in the browser, Node.js, and React Native. From 6754a6de114f2f6dc10a336ed727db96cd390c01 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Tue, 28 Jan 2025 17:59:17 -0500 Subject: [PATCH 60/72] fix test coverage --- old.c8rc | 12 ++++++++++++ package.json | 4 ++-- src/Verify.ts | 12 +++++------- src/credentialStatus.ts | 12 ------------ src/extractCredentialsFrom.ts | 27 ++------------------------- test/Verify.presentation.spec.ts | 15 +++++++++++++-- 6 files changed, 34 insertions(+), 48 deletions(-) create mode 100644 old.c8rc diff --git a/old.c8rc b/old.c8rc new file mode 100644 index 0000000..805c9d3 --- /dev/null +++ b/old.c8rc @@ -0,0 +1,12 @@ +{ + "check-coverage": true, + "reports-dir": "./build/", + "branches": 80, + "functions": 80, + "lines": 80, + "statements": 80, + "exclude": [ + "build/dev/unit/**", + "build/dev/mock/**" + ] +} \ No newline at end of file diff --git a/package.json b/package.json index 15fcb28..4287b51 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,8 @@ "test": "npm run lint && npm run test-node", "test-karma": "karma start karma.conf.js", "test-node-old": "cross-env NODE_ENV=test TS_NODE_PROJECT=tsconfig.spec.json TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\" }' mocha -r ts-node/register --project tsconfig.spec.json 'test/*.ts'", - "test-node": "npm run build-test && npx c8 mocha dist/test/*.spec.js && rm -rf dist/esm/test || true", - "coveralls": "npm run test; npx c8 report --reporter=text-lcov > ./coverage/lcov.info" + "test-node": "npm run build-test && npx c8 --exclude 'dist/test/**' mocha dist/test/*.spec.js && rm -rf dist/esm/test || true", + "coveralls": "npm run test; npx c8 --exclude 'dist/test/**' report --reporter=text-lcov > ./coverage/lcov.info" }, "files": [ "dist", diff --git a/src/Verify.ts b/src/Verify.ts index f27137a..9622729 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -64,7 +64,7 @@ export async function verifyPresentation({presentation, challenge = 'blah', unsi return {presentationResult, credentialResults: transformedCredentialResults}; } catch (error) { - return {errors: [{message: 'Could not verify presentation.', name: 'presentation_error', stackTrace: error}], + return {errors: [{message: 'Could not verify presentation.', name: 'presentation_error', stackTrace: error}] } } } @@ -119,7 +119,7 @@ async function transformResponse(verificationResponse:any, credential:Credential } function buildFatalErrorObject(fatalErrorMessage: string, name: string, credential: Credential, stackTrace: string | null): VerificationResponse { - return { credential, errors: [{ name, message: fatalErrorMessage, ...stackTrace ? { stackTrace } : null }] } + return { credential, errors: [{ name, message: fatalErrorMessage, ...(stackTrace ? { stackTrace } : null) }] }; } function handleAnyFatalCredentialErrors(credential: Credential): VerificationResponse | null { @@ -192,6 +192,8 @@ function handleAnySignatureError({ verificationResponse, credential }: { verific // check to see if the error is http related const httpError = verificationResponse.error.errors.find((error: any) => error.name === 'HTTPError') if (httpError) { + fatalErrorMessage = 'An http error prevented the signature check.' + errorName = 'http_error_with_signature_check' // was it caused by a did:web that couldn't be resolved??? const issuerDID: string = (((credential.issuer) as any).id) || credential.issuer if (issuerDID.toLowerCase().startsWith('did:web')) { @@ -200,11 +202,7 @@ function handleAnySignatureError({ verificationResponse, credential }: { verific if (httpError.requestUrl.toLowerCase().includes(didUrl)) { fatalErrorMessage = `The signature could not be checked because the public signing key could not be retrieved from ${httpError.requestUrl as string}` errorName = 'did_web_unresolved' - } else { - // some other kind of http error - fatalErrorMessage = 'An http error prevented the signature check.' - errorName = 'http_error_with_signature_check' - } + } } } else { // not an http error, so likely bad signature diff --git a/src/credentialStatus.ts b/src/credentialStatus.ts index e50c53d..6bd4a33 100644 --- a/src/credentialStatus.ts +++ b/src/credentialStatus.ts @@ -26,15 +26,3 @@ export function getCredentialStatusChecker(credential: Credential) : (() => bool } } -export function hasStatusPurpose( - credential: Credential, - statusPurpose: StatusPurpose -) : boolean { - if (!credential.credentialStatus) { - return false; - } - const credentialStatuses = Array.isArray(credential.credentialStatus) ? - credential.credentialStatus : - [credential.credentialStatus]; - return credentialStatuses.some(s => s.statusPurpose === statusPurpose); -} diff --git a/src/extractCredentialsFrom.ts b/src/extractCredentialsFrom.ts index fc6c920..ef5322e 100644 --- a/src/extractCredentialsFrom.ts +++ b/src/extractCredentialsFrom.ts @@ -1,33 +1,10 @@ import { Credential } from './types/credential.js'; import { VerifiablePresentation } from './types/presentation'; -/** - * This type is used to identify a request response that could be a - * Verifiable Credential or Verifiable Presentation. - */ -export type VerifiableObject = Credential | VerifiablePresentation; - -export function isVerifiableCredential(obj: VerifiableObject): obj is Credential { - return obj.type?.includes('VerifiableCredential'); -} - -export function isVerifiablePresentation(obj: VerifiableObject): obj is VerifiablePresentation { - return obj.type?.includes('VerifiablePresentation'); -} - -export function extractCredentialsFrom(obj: VerifiableObject): Credential[] | null { - if (isVerifiableCredential(obj)) { - return [obj]; - } - - if (isVerifiablePresentation(obj)) { - const { verifiableCredential } = obj; - +export function extractCredentialsFrom(vp: VerifiablePresentation): Credential[] | null { + const { verifiableCredential } = vp; if (verifiableCredential instanceof Array) { return verifiableCredential; } return [verifiableCredential]; - } - - return null; } diff --git a/test/Verify.presentation.spec.ts b/test/Verify.presentation.spec.ts index 93177d5..91b2691 100644 --- a/test/Verify.presentation.spec.ts +++ b/test/Verify.presentation.spec.ts @@ -167,14 +167,25 @@ describe('Verify.verifyPresentation', () => { }) it('when unsigned presentation not properly specified', async () => { - /// hmmmmm, NEED TO HAVE AN ERROR FOR EXPECTED RESULT const verifiableCredential= [noProofVC] const presentation = await getUnSignedVP({verifiableCredential}) as VerifiablePresentation - const credentialResults = [expectedNoProofResult] const result = await verifyPresentation({presentation, knownDIDRegistries}) expect(result?.presentationResult?.signature).to.equal('invalid') }) + it('when bad presentation', async () => { + const verifiableCredential= [noProofVC] + const presentation = await getUnSignedVP({verifiableCredential}) as any + delete presentation['@context'] + const result = await verifyPresentation({presentation, knownDIDRegistries}) + if (result?.errors) { + expect(result.errors[0].name).to.equal('presentation_error') + } else { + expect(false).to.equal(true) + } + + }) + }) }) From 4f0f9835b065d28d75aaa12e6f3e0cd77125a599 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Tue, 28 Jan 2025 19:16:37 -0500 Subject: [PATCH 61/72] update tests and README --- README.md | 73 ++++++++++++++++++++------------ src/Verify.ts | 8 ++-- src/types/result.ts | 2 +- test/Verify.presentation.spec.ts | 12 +++++- test/vpUtils.ts | 1 - 5 files changed, 62 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 0983ced..a28a1a1 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,15 @@ And verifies signatures from both [eddsa-rdfc-2022 Data Integrity Proof](https:/ The verification checks that the credential: -* has a valid signature (i.e, that the credential hasn't been tampered with) +* has a valid signature, so: + * credential hasn't been tampered with + * the signing key was retrieved from the did document * hasn't expired * hasn't been revoked * was signed by a trusted issuer +The verification will also tell us if any of the registries listed in the trusted registry list couldn't be loaded, which is important because those missing registries might be the very registries that affirm the trustworthiness of the issuer of a given credential. + As of January 2025 issuers are trusted if they are listed in one of the Digital Credentials Issuer Registries: ``` @@ -74,7 +78,7 @@ This package exports two methods: The typescript definitions for the result can be found [here](./src/types/result.ts) -Note that the verification result doesn't make any conclusion about the overall validity of a credential. It only checks the validity of each of the four steps, leaving it up to the consumer of the result to decide on the overall validity. The consumer might not, for example, consider a credential that had expired or had been revoked to be 'invalid'. The credential might still in fact be useful as a record of history, i.e, I had a driver's licence that expired two years ago, but did have it during the period 2018 to 2023, and that information might be useful. +Note that the verification result doesn't make any conclusion about the overall validity of a credential. It only checks the validity of each of the four steps, leaving it up to the consumer of the result to decide on the overall validity. The consumer might not, for example, consider a credential that had expired or had been revoked to be 'invalid'. The credential might still in fact be useful as a record of history, i.e, I had a driver's licence that expired two years ago, but it was valid during the period 2018 to 2023, and that information might be useful. There are three general flavours of result that might be returned: @@ -86,7 +90,6 @@ A conclusive verification might look like this example where all steps returned ``` { - "isFatal": false, "credential": {the supplied vc - left out here for brevity/clarity}, "log": [ { @@ -115,11 +118,10 @@ A conclusive verification might look like this example where all steps returned Note that an invalid signature is considered fatal because it means that the revocation status, expiry data, or issuer id may have been tampered with, and so we can't say anything conclusive about any of them. -Here is what the verification result for an expired credential might look like, where we have made conclusive determinations about each step, and all are true except for the expiry: +And here is a slightly different verification result where we have still made conclusive determinations about each step, and all are true except for the expiry: ``` { - "isFatal": false, "credential": {the supplied vc - left out here for brevity/clarity}, "log": [ { @@ -152,21 +154,22 @@ A verification might partly succeed if it can verify: * the signature * the expiry date -But can't retrieve (from the network) any one of the: +But can't retrieve (from the network) any one of: -* revocation status -* the issuer registry +* the revocation status +* an issuer registry from our list of trusted issuers * the issuer's DID document which are needed to verify the revocation status and issuer identity. -For steps that we can't conclusively verify one way or the other (true or false) we return an 'error' propery rather than a 'valid' property. +For the valid_signature and revocation_status steps, if we can't conclusively verify one way or the other (true or false) we return an 'error' propery rather than a 'valid' property. + +For the registered_issuer step we always return false if the issuer isn't found in a loaded registry, but with the caveat that if the 'registriesNotLoaded' property does contain one or more registries, then the credential *might* have been in one of those registries. It is up to the consumer of the result to decide how to deal with that. -A partially successful verification might look like this example: +A partially successful verification might look like this example, where we couldn't retrieve the status list or one of the registries: ``` { - "isFatal": false, "credential": {the supplied vc - left out here for brevity/clarity}, "log": [ { @@ -207,12 +210,11 @@ Examples of fatal errors: invalid signature -Fatal because if the signature is invalid it means any part of the credential could have been tampered with, including the revocation status, expiration, and issuer identity. +Fatal because if the signature is invalid it means any part of the credential could have been tampered with, including the revocation status, expiration, and issuer identity. In these cases we don't return a 'valid' property, but instead an 'errors' property ``` { "credential": {vc removed for brevity/clarity in this example}, - "isFatal": true, "errors": [ { "name": "invalid_signature", @@ -230,7 +232,6 @@ has been taken down, or there is a network error. ``` { "credential": {vc removed for brevity/clarity}, - "isFatal": true, "errors": [ { "name": "did_web_unresolved", @@ -292,7 +293,6 @@ There is no @context property at the top level of the credential: "proofValue": "z62t6TYCERpTKuWCRhHc2fV7JoMhiFuEcCXGkX9iit8atQPhviN5cZeZfXRnvJWa3Bm6DjagKyrauaSJfp9C9i7q3" } }, - "isFatal": true, "errors": [ { "name": "invalid_jsonld", @@ -352,7 +352,6 @@ Although this is a linked data document, with an @context property, the Verifiab "proofValue": "z62t6TYCERpTKuWCRhHc2fV7JoMhiFuEcCXGkX9iit8atQPhviN5cZeZfXRnvJWa3Bm6DjagKyrauaSJfp9C9i7q3" } }, - "isFatal": true, "errors": [ { "name": "no_vc_context", @@ -421,7 +420,6 @@ In this example, the top level id property on the credential is not a uri, but s "proofValue": "z5fk6gq9upyZvcFvJdRdeL5KmvHr69jxEkyDEd2HyQdyhk9VnDEonNSmrfLAcLEDT9j4gGdCG24WHhojVHPbRsNER" } }, - "isFatal": true, "errors": [ { "name": "invalid_credential_id", @@ -483,7 +481,6 @@ The proof property is missing, likely because the credential hasn't been signed: "name": "Jane Doe" } }, - "isFatal": true, "errors": [ { "name": "no_proof", @@ -494,29 +491,52 @@ The proof property is missing, likely because the credential hasn't been signed: ``` -software problem +other problem -A software error might prevent verification +Some other error might also prevent verification, and a stack trace might be returned: + +``` +{ + "errors": [ + { + "name": "unknown_error", + "message": "Some kind of error - this message will depend on the error", + "stackTrace": "some kind of stack trace" + } + ] +} +``` ### verifyPresentation -```verifyPresentation({presentation, reloadIssuerRegistry = true})``` +```verifyPresentation({presentation, reloadIssuerRegistry = true, unsignedPresentation = false})``` + +A Verifiable Presentation (VP) is a wrapper around zero or more Verifiable Credentials. A VP can also be cryptographically signed, like a VC, but whereas a VC is signed by the issuer of the credentials, the VP is signed by the holder of the credentials, typically to demonstrate 'control' of the contained credentials. The VP is signed with a DID that the holder owns, and often that DID is recorded inside the Verifiable Credentials as the 'owner' or 'holder' of the credential. So by signing the VP with the private key corresponding to that DID we can prove we 'own' the credentials. -A Verifiable Presentation (VP) is a wrapper around zero or more Verifiable Credentials. A VP is also cryptographically signed, like a VC, but whereas a VC is signed by the issuer of the credentials, the VP is signed by the holder of the credentials, typically to demonstrate 'control' of the contained credentials. The VP is signed with a DID that the holder owns, and ofthen that DID is recorded inside the Verifiable Credentials as the 'owner' or 'holder' of the credential. So by signing the VP with the private key corresponding to the DID we can prove we 'own' the credentials. +A VP needn't be signed. It could simply be used as to 'package' together a set of VCs. -A VP is also sometimes used without any containted VC simply to prove that we control a given DID, say for authentication, or often for the case where when an issuer is issuing a credential to a DID, the issuer wants to know that the recipient in fact does control that DID. +A VP is also sometimes used without any containted VC simply to prove that we control a given DID, say for authentication, or often for the case where when an issuer is issuing a credential to a DID, the issuer wants to know that the recipient in fact does control that DID. In these cases the VP is used as a `did-auth`. This verifier-core library does not, however, provide verification for `did-auth`, only to verify a presentation containing VCs. -Verifying a VP amounts to verifying the signature on the VP and that the VP hasn't expired, and also verifying all of the contained VCs, one by one. +Verifying a VP amounts to verifying the signature on the VP (if the signature exists) and also verifying all of the contained VCs, one by one. #### arguments * presentation - The W3C Verifiable Presentation to be verified. * reloadIssuerRegistry - Whether or not to refresh the cached copy of the registry. +* unsignedPresentation - wether the submitted vp has been signed or not #### result -With a VP we have a result for the vp as well as for all the contained VCs. +With a VP we have a result for the vp as well as for all the contained VCs. Each of the VC results follows exactly the format described above for the results of verifying an individual VCs. We may also have an error. + +A successful VP result might look like so: + +A VP that itself verfies (i.e, it's signature), but has one VC that doesn't might look like so: + +A VP with a bad signature might look like so: + + ## Install @@ -544,9 +564,6 @@ npm install PRs accepted. -If editing the Readme, please conform to the -[standard-readme](https://github.com/RichardLitt/standard-readme) specification. - ## License [MIT License](LICENSE.md) © 2025 Digital Credentials Consortium. diff --git a/src/Verify.ts b/src/Verify.ts index 9622729..c4be393 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -64,14 +64,13 @@ export async function verifyPresentation({presentation, challenge = 'blah', unsi return {presentationResult, credentialResults: transformedCredentialResults}; } catch (error) { - return {errors: [{message: 'Could not verify presentation.', name: 'presentation_error', stackTrace: error}] - } + return {errors: [{message: 'Could not verify presentation.', name: 'presentation_error', stackTrace: error}]} } } export async function verifyCredential({ credential, knownDIDRegistries, reloadIssuerRegistry = true }: { credential: Credential, knownDIDRegistries: object, reloadIssuerRegistry: boolean }): Promise { - +try { // null unless credential has a status const statusChecker = getCredentialStatusChecker(credential) @@ -85,6 +84,9 @@ export async function verifyCredential({ credential, knownDIDRegistries, reloadI const adjustedResponse = transformResponse(verificationResponse, credential, knownDIDRegistries, reloadIssuerRegistry) return adjustedResponse; +} catch (error) { + return {errors: [{message: 'Could not verify credential.', name: 'unknown_error', stackTrace: error}]} +} } async function transformResponse(verificationResponse:any, credential:Credential, knownDIDRegistries: object, reloadIssuerRegistry: boolean ) : Promise { diff --git a/src/types/result.ts b/src/types/result.ts index 114b1cf..335a05d 100644 --- a/src/types/result.ts +++ b/src/types/result.ts @@ -14,7 +14,7 @@ export interface VerificationError { } export interface VerificationResponse { - "credential": object, + "credential"?: object, "errors"?: VerificationError[], "log"?: VerificationStep[] } diff --git a/test/Verify.presentation.spec.ts b/test/Verify.presentation.spec.ts index 91b2691..d72f5ff 100644 --- a/test/Verify.presentation.spec.ts +++ b/test/Verify.presentation.spec.ts @@ -102,7 +102,7 @@ describe('Verify.verifyPresentation', () => { describe('it returns as verified', () => { - it('when signed presentation has one vc', async () => { + it('when signed presentation has one vc in an array', async () => { const verifiableCredential= [v2WithStatus] const presentation = await getSignedVP({holder, verifiableCredential}) as VerifiablePresentation const credentialResults = [expectedV2WithStatusResult] @@ -111,6 +111,16 @@ describe('Verify.verifyPresentation', () => { expect(result).to.deep.equalInAnyOrder(expectedPresentationResult) }) + it('when unsigned presentation has one vc not in an array', async () => { + const verifiableCredential= v2WithStatus + const presentation = await getUnSignedVP({verifiableCredential}) as any + presentation.verifiableCredential = presentation.verifiableCredential[0] + const credentialResults = [expectedV2WithStatusResult] + const expectedPresentationResult = getExpectedVerifiedPresentationResult({credentialResults, unsigned:true}) + const result = await verifyPresentation({presentation, knownDIDRegistries, unsignedPresentation: true}) + expect(result).to.deep.equalInAnyOrder(expectedPresentationResult) + }) + it('when signed presentation has mix of VCs', async () => { const verifiableCredential = [v2WithStatus, v2Eddsa, didWebVC] const presentation = await getSignedVP({verifiableCredential, holder: 'did:ex:12345'}) as VerifiablePresentation diff --git a/test/vpUtils.ts b/test/vpUtils.ts index 3ca382a..060f14a 100644 --- a/test/vpUtils.ts +++ b/test/vpUtils.ts @@ -7,7 +7,6 @@ import {Ed25519VerificationKey2020} from '@digitalcredentials/ed25519-verificati const documentLoader = securityLoader().build() import pkg from '@digitalcredentials/jsonld-signatures'; -import { verifyCredential } from '../src/Verify'; import { VerifiablePresentation } from '../src/types/presentation'; const { purposes } = pkg; const presentationPurpose = new purposes.AssertionProofPurpose(); From e455b38c8538ce50c6cd760050b716b631ba3c1f Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Tue, 28 Jan 2025 19:19:46 -0500 Subject: [PATCH 62/72] update method signature in README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a28a1a1..659c7ac 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,8 @@ And verifies signatures from both [eddsa-rdfc-2022 Data Integrity Proof](https:/ The verification checks that the credential: -* has a valid signature, so: - * credential hasn't been tampered with +* has a valid signature, and so: + * the credential hasn't been tampered with * the signing key was retrieved from the did document * hasn't expired * hasn't been revoked @@ -66,7 +66,7 @@ This package exports two methods: ### verifyCredential -```verifyCredential({credential, reloadIssuerRegistry = true})``` +```verifyCredential({credential, knownDidRegistries, reloadIssuerRegistry = true})``` #### arguments From c9122e210d7c4cc57fbdef416478e95db842848c Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Wed, 29 Jan 2025 10:37:46 -0500 Subject: [PATCH 63/72] update README examples and tests --- README.md | 308 ++++++++++++++++++++++++++++++- old.c8rc | 12 -- package.json | 2 +- test/Verify.presentation.spec.ts | 29 ++- 4 files changed, 330 insertions(+), 21 deletions(-) delete mode 100644 old.c8rc diff --git a/README.md b/README.md index 659c7ac..4a2cf85 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ And verifies signatures from both [eddsa-rdfc-2022 Data Integrity Proof](https:/ The verification checks that the credential: -* has a valid signature, and so: +* has a valid signature, and so therefore: * the credential hasn't been tampered with * the signing key was retrieved from the did document * hasn't expired @@ -493,7 +493,7 @@ The proof property is missing, likely because the credential hasn't been signed: other problem -Some other error might also prevent verification, and a stack trace might be returned: +Some other error might also prevent verification, and an error, possibly with a stack trace, might be returned: ``` { @@ -512,11 +512,11 @@ Some other error might also prevent verification, and a stack trace might be ret ```verifyPresentation({presentation, reloadIssuerRegistry = true, unsignedPresentation = false})``` -A Verifiable Presentation (VP) is a wrapper around zero or more Verifiable Credentials. A VP can also be cryptographically signed, like a VC, but whereas a VC is signed by the issuer of the credentials, the VP is signed by the holder of the credentials, typically to demonstrate 'control' of the contained credentials. The VP is signed with a DID that the holder owns, and often that DID is recorded inside the Verifiable Credentials as the 'owner' or 'holder' of the credential. So by signing the VP with the private key corresponding to that DID we can prove we 'own' the credentials. +A Verifiable Presentation (VP) is a wrapper around zero or more Verifiable Credentials. A VP can be cryptographically signed, like a VC, but whereas a VC is signed by the issuer of the credentials, the VP is signed by the holder of the credentials contained in the VP, typically to demonstrate 'control' of the contained credentials. The VP is signed with a DID that the holder owns, and usually that DID was recorded inside the Verifiable Credentials - at the time of issuance - as the 'owner' or 'holder' of the credential. So by signing the VP with the private key corresponding to that DID we can prove we 'own' the credentials, or in other words, that the credentials were issued to us (to our DID.) A VP needn't be signed. It could simply be used as to 'package' together a set of VCs. -A VP is also sometimes used without any containted VC simply to prove that we control a given DID, say for authentication, or often for the case where when an issuer is issuing a credential to a DID, the issuer wants to know that the recipient in fact does control that DID. In these cases the VP is used as a `did-auth`. This verifier-core library does not, however, provide verification for `did-auth`, only to verify a presentation containing VCs. +A signed VP is also sometimes used for authentication, without any contained VC. Say for the case where when an issuer is issuing a credential to a DID, and the issuer wants to know that the recipient in fact does control that DID. In these cases the VP is typically the response to a request for [DIDAuthentication (DidAuth)](https://w3c-ccg.github.io/vp-request-spec/#did-authentication). This verifier-core library does not, however, provide verification for DidAuthentication, only to verify a presentation containing VCs. Verifying a VP amounts to verifying the signature on the VP (if the signature exists) and also verifying all of the contained VCs, one by one. @@ -530,13 +530,307 @@ Verifying a VP amounts to verifying the signature on the VP (if the signature ex With a VP we have a result for the vp as well as for all the contained VCs. Each of the VC results follows exactly the format described above for the results of verifying an individual VCs. We may also have an error. -A successful VP result might look like so: +A successful signed VP result with two packaged VCs might look like so: -A VP that itself verfies (i.e, it's signature), but has one VC that doesn't might look like so: +``` +{ + "presentationResult": { + "signature": "valid" + }, + "credentialResults": [ + { + "log": [ + { + "id": "valid_signature", + "valid": true + }, + { + "id": "revocation_status", + "valid": true + }, + { + "id": "expiration", + "valid": true + }, + { + "id": "registered_issuer", + "valid": true, + "foundInRegistries": [ + "DCC Sandbox Registry" + ], + "registriesNotLoaded": [] + } + ], + "credential": {vc omitted for brevity/clarity} + }, + { + "log": [ + { + "id": "valid_signature", + "valid": true + }, + { + "id": "revocation_status", + "valid": true + }, + { + "id": "expiration", + "valid": true + }, + { + "id": "registered_issuer", + "valid": true, + "foundInRegistries": [ + "DCC Sandbox Registry" + ], + "registriesNotLoaded": [] + } + ], + "credential": {vc omitted for brevity/clarity} + } + ] +} +``` + +A VP that itself verfies (i.e, it's signature), but has one VC that doesn't, might look like so: + +``` +{ + "presentationResult": { + "signature": "signed" + }, + "credentialResults": [ + { + "credential": { + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "id": "0923lksjf", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "issuer": { + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "type": [ + "Profile" + ], + "name": "Example Corp" + }, + "validFrom": "2010-01-01T00:00:00Z", + "name": "Teamwork Badge", + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "type": [ + "AchievementSubject" + ], + "achievement": { + "id": "https://example.com/achievements/21st-century-skills/teamwork", + "type": [ + "Achievement" + ], + "criteria": { + "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." + }, + "description": "This badge recognizes the development of the capacity to collaborate within a group environment.", + "name": "Teamwork" + } + }, + "proof": { + "type": "Ed25519Signature2020", + "created": "2025-01-09T17:58:33Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "z62t6TYCERpTKuWCRhHc2fV7JoMhiFuEcCXGkX9iit8atQPhviN5cZeZfXRnvJWa3Bm6DjagKyrauaSJfp9C9i7q3" + } + }, + "errors": [ + { + "name": "invalid_credential_id", + "message": "The credential's id uses an invalid format. It may have been issued as part of an early pilot. Please contact the issuer to get a replacement." + } + ] + } + ] +} +``` + +It is important to note in the above example that the validity of the signature of the presentation is different from the validity of each of the contained VCs. A valid presentation signature simply means that nothing in the VP was tampered with. + +An unsigned VP containing a single verified credential: + +``` +{ + "presentationResult": { + "signature": "unsigned" + }, + "credentialResults": [ + { + "log": [ + { + "id": "valid_signature", + "valid": true + }, + { + "id": "revocation_status", + "valid": true + }, + { + "id": "expiration", + "valid": true + }, + { + "id": "registered_issuer", + "valid": true, + "foundInRegistries": [ + "DCC Sandbox Registry" + ], + "registriesNotLoaded": [] + } + ], + "credential": {vc omitted for brevity/clarity} + } + ] +} +``` -A VP with a bad signature might look like so: +A VP where we've tampered with one of the packaged credentials (by changing the credential name). Note here that both the VP and the VC don't verify because changing the VC affected the VC signature bit also the VP signature which contains the VC. +``` +{ + "presentationResult": { + "signature": "invalid", + "errors": [ + { + "message": { + "name": "VerificationError", + "errors": [ + { + "name": "Error", + "message": "Invalid signature.", + "stack": "Error: Invalid signature.\n at Ed25519Signature2020.verifyProof (/Users/jameschartrand/Documents/github/dcc/verifier-core/node_modules/@digitalcredentials/jsonld-signatures/lib/suites/LinkedDataSignature.js:189:15)\n at async /Users/jameschartrand/Documents/github/dcc/verifier-core/node_modules/@digitalcredentials/jsonld-signatures/lib/ProofSet.js:273:53\n at async Promise.all (index 0)\n at async _verify (/Users/jameschartrand/Documents/github/dcc/verifier-core/node_modules/@digitalcredentials/jsonld-signatures/lib/ProofSet.js:261:3)\n at async ProofSet.verify (/Users/jameschartrand/Documents/github/dcc/verifier-core/node_modules/@digitalcredentials/jsonld-signatures/lib/ProofSet.js:195:23)\n at async Object.verify (/Users/jameschartrand/Documents/github/dcc/verifier-core/node_modules/@digitalcredentials/jsonld-signatures/lib/jsonld-signatures.js:160:18)\n at async _verifyPresentation (/Users/jameschartrand/Documents/github/dcc/verifier-core/node_modules/@digitalcredentials/vc/dist/index.js:578:30)\n at async verifyPresentation (file:///Users/jameschartrand/Documents/github/dcc/verifier-core/dist/src/Verify.js:24:24)\n at async Context. (file:///Users/jameschartrand/Documents/github/dcc/verifier-core/dist/test/Verify.presentation.spec.js:97:28)" + } + ] + }, + "name": "presentation_error" + } + ] + }, + "credentialResults": [ + { + "credential": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.2.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "id": "urn:uuid:2fe53dc9-b2ec-4939-9b2c-0d00f6663b6c", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "name": "Tampered Name", + "issuer": { + "type": [ + "Profile" + ], + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "name": "Digital Credentials Consortium Test Issuer", + "url": "https://dcconsortium.org", + "image": "https://user-images.githubusercontent.com/752326/230469660-8f80d264-eccf-4edd-8e50-ea634d407778.png" + }, + "issuanceDate": "2023-08-02T17:43:32.903Z", + "credentialSubject": { + "type": [ + "AchievementSubject" + ], + "achievement": { + "id": "urn:uuid:bd6d9316-f7ae-4073-a1e5-2f7f5bd22922", + "type": [ + "Achievement" + ], + "achievementType": "Diploma", + "name": "Badge", + "description": "This is a sample credential issued by the Digital Credentials Consortium to demonstrate the functionality of Verifiable Credentials for wallets and verifiers.", + "criteria": { + "type": "Criteria", + "narrative": "This credential was issued to a student that demonstrated proficiency in the Python programming language that occurred from **February 17, 2023** to **June 12, 2023**." + }, + "image": { + "id": "https://user-images.githubusercontent.com/752326/214947713-15826a3a-b5ac-4fba-8d4a-884b60cb7157.png", + "type": "Image" + } + }, + "name": "Jane Doe" + }, + "proof": { + "type": "Ed25519Signature2020", + "created": "2023-10-05T11:17:41Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "z5fk6gq9upyZvcFvJdRdeL5KmvHr69jxEkyDEd2HyQdyhk9VnDEonNSmrfLAcLEDT9j4gGdCG24WHhojVHPbRsNER" + } + }, + "errors": [ + { + "name": "invalid_signature", + "message": "The signature is not valid." + } + ] + } + ] +} +``` + +And here is a VP where just the VP has been tampered with, and not the embedded VC, and so the VC returns as valid, but not the presentation signature: +``` +{ + "presentationResult": { + "signature": "invalid", + "errors": [ + { + "message": { + "name": "VerificationError", + "errors": [ + { + "name": "Error", + "message": "Invalid signature.", + "stack": "Error: Invalid signature.\n at Ed25519Signature2020.verifyProof (/Users/jameschartrand/Documents/github/dcc/verifier-core/node_modules/@digitalcredentials/jsonld-signatures/lib/suites/LinkedDataSignature.js:189:15)\n at async /Users/jameschartrand/Documents/github/dcc/verifier-core/node_modules/@digitalcredentials/jsonld-signatures/lib/ProofSet.js:273:53\n at async Promise.all (index 0)\n at async _verify (/Users/jameschartrand/Documents/github/dcc/verifier-core/node_modules/@digitalcredentials/jsonld-signatures/lib/ProofSet.js:261:3)\n at async ProofSet.verify (/Users/jameschartrand/Documents/github/dcc/verifier-core/node_modules/@digitalcredentials/jsonld-signatures/lib/ProofSet.js:195:23)\n at async Object.verify (/Users/jameschartrand/Documents/github/dcc/verifier-core/node_modules/@digitalcredentials/jsonld-signatures/lib/jsonld-signatures.js:160:18)\n at async _verifyPresentation (/Users/jameschartrand/Documents/github/dcc/verifier-core/node_modules/@digitalcredentials/vc/dist/index.js:578:30)\n at async verifyPresentation (file:///Users/jameschartrand/Documents/github/dcc/verifier-core/dist/src/Verify.js:24:24)\n at async Context. (file:///Users/jameschartrand/Documents/github/dcc/verifier-core/dist/test/Verify.presentation.spec.js:101:28)" + } + ] + }, + "name": "presentation_error" + } + ] + }, + "credentialResults": [ + { + "log": [ + { + "id": "valid_signature", + "valid": true + }, + { + "id": "expiration", + "valid": true + }, + { + "id": "registered_issuer", + "valid": true, + "foundInRegistries": [ + "DCC Sandbox Registry" + ], + "registriesNotLoaded": [] + } + ], + "credential": {vc ommitted for clarity/brevity} + } + ] +} +``` ## Install diff --git a/old.c8rc b/old.c8rc deleted file mode 100644 index 805c9d3..0000000 --- a/old.c8rc +++ /dev/null @@ -1,12 +0,0 @@ -{ - "check-coverage": true, - "reports-dir": "./build/", - "branches": 80, - "functions": 80, - "lines": 80, - "statements": 80, - "exclude": [ - "build/dev/unit/**", - "build/dev/mock/**" - ] -} \ No newline at end of file diff --git a/package.json b/package.json index 4287b51..774ee41 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "rebuild": "npm run clear && npm run build", "test": "npm run lint && npm run test-node", "test-karma": "karma start karma.conf.js", - "test-node-old": "cross-env NODE_ENV=test TS_NODE_PROJECT=tsconfig.spec.json TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\" }' mocha -r ts-node/register --project tsconfig.spec.json 'test/*.ts'", + "test-node-no-cov": "npm run build-test && mocha dist/test/*.spec.js && rm -rf dist/esm/test || true", "test-node": "npm run build-test && npx c8 --exclude 'dist/test/**' mocha dist/test/*.spec.js && rm -rf dist/esm/test || true", "coveralls": "npm run test; npx c8 --exclude 'dist/test/**' report --reporter=text-lcov > ./coverage/lcov.info" }, diff --git a/test/Verify.presentation.spec.ts b/test/Verify.presentation.spec.ts index d72f5ff..5ac54dc 100644 --- a/test/Verify.presentation.spec.ts +++ b/test/Verify.presentation.spec.ts @@ -27,7 +27,8 @@ import { getExpectedVerifiedPresentationResult } from '../src/test-fixtures/expectedResults.js'; - import { + import { + getVCv1, getVCv1Tampered, getVCv1Expired, getVCv1Revoked, @@ -64,6 +65,9 @@ import { VerifiablePresentation } from '../src/types/presentation.js'; const v2WithStatus : any = getVCv2ValidStatus() const expectedV2WithStatusResult = getExpectedVerifiedResult({credential:v2WithStatus, withStatus: true}) + const v1NoStatus : any = getVCv1() + const expectedV1Result = getExpectedVerifiedResult({credential:v1NoStatus, withStatus: false}) + const v2Eddsa : any = getVCv2EddsaWithValidStatus() const expectedv2EddsaResult = getExpectedVerifiedResult({credential: v2Eddsa, withStatus: true}) @@ -143,6 +147,28 @@ describe('Verify.verifyPresentation', () => { describe('it returns as unverified', () => { + + + it('when vc in signed presentation has been tampered with', async () => { + const v1 : any = getVCv1() + const verifiableCredential= [v1] + const presentation = await getSignedVP({holder, verifiableCredential}) as any + presentation.verifiableCredential[0].name = 'Tampered Name' + const result = await verifyPresentation({presentation, knownDIDRegistries}) as any + expect(result.presentationResult.signature).to.equal('invalid') + expect(result.credentialResults[0].errors[0].name).to.equal('invalid_signature') + }) + + it('when signed presentation has been tampered with', async () => { + const verifiableCredential= [v1NoStatus] + const presentation = await getSignedVP({holder, verifiableCredential}) as any + presentation.holder = 'did:ex:tampered' + const result = await verifyPresentation({presentation, knownDIDRegistries}) as any + const expectedCredentialResults = [expectedV1Result] + expect(result.credentialResults).to.deep.equalInAnyOrder(expectedCredentialResults) + expect(result.presentationResult.signature).to.equal('invalid') + }) + it('when unsigned presentation has bad vc', async () => { /// NOTE that this is an unsigned vp because the vc libs signing // method doesn't allow signing a VP with a 'bad' VC, so @@ -153,6 +179,7 @@ describe('Verify.verifyPresentation', () => { const expectedPresentationResult = getExpectedVerifiedPresentationResult({credentialResults, unsigned: true}) const result = await verifyPresentation({presentation, knownDIDRegistries, unsignedPresentation: true}) expect(result).to.deep.equalInAnyOrder(expectedPresentationResult) + }) it('when signed presentation has no proof vc', async () => { From fa842ef943c280613b016d7cb09bcedd1f6c9b14 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Wed, 29 Jan 2025 11:43:08 -0500 Subject: [PATCH 64/72] test for string valued issuer id --- src/test-fixtures/vc.ts | 12 ++++- .../v1/v1SimpleIssuerId.ts | 45 +++++++++++++++++++ .../v2/v2SimpleIssuerId.ts | 39 ++++++++++++++++ test/Verify.presentation.spec.ts | 19 +++++++- 4 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 src/test-fixtures/verifiableCredentials/v1/v1SimpleIssuerId.ts create mode 100644 src/test-fixtures/verifiableCredentials/v2/v2SimpleIssuerId.ts diff --git a/src/test-fixtures/vc.ts b/src/test-fixtures/vc.ts index 6df4ed8..3dab2b7 100644 --- a/src/test-fixtures/vc.ts +++ b/src/test-fixtures/vc.ts @@ -9,12 +9,13 @@ import { v1NoStatus } from "./verifiableCredentials/v1/v1NoStatus.js" import { v1Revoked } from "./verifiableCredentials/v1/v1Revoked.js" import { v1Expired } from "./verifiableCredentials/v1/v1Expired.js" import { v1ExpiredWithValidStatus } from "./verifiableCredentials/v1/v1ExpiredWithValidStatus.js" - +import { v2SimpleIssuerId } from "./verifiableCredentials/v2/v2SimpleIssuerId.js" import { v2EddsaWithValidStatus } from "./verifiableCredentials/eddsa/v2/v2EddsaWithValidStatus.js" import { v2DoubleSigWithBadStatusUrl } from "./verifiableCredentials/eddsaAndEd25519/v2/v2DoubleSigWithBadStatusUrl.js" import { v2didWebWithValidStatus } from "./verifiableCredentials/v2/didWeb/v2didWebWithValidStatus.js" import { v2WithBadDidWeb } from "./verifiableCredentials/v2/didWeb/v2WithBadDidWeb.js" +import { v1SimpleIssuerId } from "./verifiableCredentials/v1/v1SimpleIssuerId.js" const getVCv1 = (): any => { return JSON.parse(JSON.stringify(v1NoStatus)) @@ -128,6 +129,13 @@ const getVCv2WithBadDidWebUrl = (): any => { return JSON.parse(JSON.stringify(v2WithBadDidWeb)) } +const getVCv1SimpleIssuerId = (): any => { + return JSON.parse(JSON.stringify(v1SimpleIssuerId)) +} + +const getVCv2SimpleIssuerId = (): any => { + return JSON.parse(JSON.stringify(v2SimpleIssuerId)) +} export { @@ -138,6 +146,7 @@ export { getVCv2DoubleSigWithBadStatusUrl, getVCv2, + getVCv2SimpleIssuerId, getVCv2Expired, getVCv2Revoked, getVCv2Tampered, @@ -150,6 +159,7 @@ export { getVCv2WithBadDidWebUrl, getVCv1, + getVCv1SimpleIssuerId, getVCv1Expired, getVCv1Revoked, getVCv1Tampered, diff --git a/src/test-fixtures/verifiableCredentials/v1/v1SimpleIssuerId.ts b/src/test-fixtures/verifiableCredentials/v1/v1SimpleIssuerId.ts new file mode 100644 index 0000000..f65fb7d --- /dev/null +++ b/src/test-fixtures/verifiableCredentials/v1/v1SimpleIssuerId.ts @@ -0,0 +1,45 @@ +export const v1SimpleIssuerId = { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.2.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "id": "urn:uuid:2fe53dc9-b2ec-4939-9b2c-0d00f6663b6c", + "issuanceDate": "2025-01-09T15:06:31Z", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "name": "DCC Test Credential", + "issuer": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "credentialSubject": { + "type": [ + "AchievementSubject" + ], + "achievement": { + "id": "urn:uuid:bd6d9316-f7ae-4073-a1e5-2f7f5bd22922", + "type": [ + "Achievement" + ], + "achievementType": "Diploma", + "name": "Badge", + "description": "This is a sample credential issued by the Digital Credentials Consortium to demonstrate the functionality of Verifiable Credentials for wallets and verifiers.", + "criteria": { + "type": "Criteria", + "narrative": "This credential was issued to a student that demonstrated proficiency in the Python programming language that occurred from **February 17, 2023** to **June 12, 2023**." + }, + "image": { + "id": "https://user-images.githubusercontent.com/752326/214947713-15826a3a-b5ac-4fba-8d4a-884b60cb7157.png", + "type": "Image" + } + }, + "name": "Jane Doe" + }, + "proof": { + "type": "Ed25519Signature2020", + "created": "2025-01-29T16:27:25Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "z5FQSyBv3j1mxtVn5UjiXhBFo6tTx5mCHUk3bfwJQZHHmz2J18aeMRMzVieWoxNHEBUswV1ECFjSLnzDb44DcFB3C" + } +} \ No newline at end of file diff --git a/src/test-fixtures/verifiableCredentials/v2/v2SimpleIssuerId.ts b/src/test-fixtures/verifiableCredentials/v2/v2SimpleIssuerId.ts new file mode 100644 index 0000000..52cdfe8 --- /dev/null +++ b/src/test-fixtures/verifiableCredentials/v2/v2SimpleIssuerId.ts @@ -0,0 +1,39 @@ +export const v2SimpleIssuerId = { + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "id": "http://example.com/credentials/3527", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "issuer": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "validFrom": "2010-01-01T00:00:00Z", + "name": "Teamwork Badge", + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "type": [ + "AchievementSubject" + ], + "achievement": { + "id": "https://example.com/achievements/21st-century-skills/teamwork", + "type": [ + "Achievement" + ], + "criteria": { + "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." + }, + "description": "This badge recognizes the development of the capacity to collaborate within a group environment.", + "name": "Teamwork" + } + }, + "proof": { + "type": "Ed25519Signature2020", + "created": "2025-01-29T16:29:21Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "z2QqZBSqcJjydA9qaMrc9ddk5Tb65wmN1qZCXgmXrsghfzcXRgsVZ9ZyLQZLbgpox65GBRxh78J7nDznVksbU5EH1" + } +} \ No newline at end of file diff --git a/test/Verify.presentation.spec.ts b/test/Verify.presentation.spec.ts index 5ac54dc..ecb917d 100644 --- a/test/Verify.presentation.spec.ts +++ b/test/Verify.presentation.spec.ts @@ -2,6 +2,8 @@ import chai from 'chai' import deepEqualInAnyOrder from 'deep-equal-in-any-order' import { verifyPresentation } from '../src/Verify.js' import { + getVCv1SimpleIssuerId, + getVCv2SimpleIssuerId, getVCv2Expired, getVCv2Revoked, getVCv2ValidStatus, @@ -68,14 +70,20 @@ import { VerifiablePresentation } from '../src/types/presentation.js'; const v1NoStatus : any = getVCv1() const expectedV1Result = getExpectedVerifiedResult({credential:v1NoStatus, withStatus: false}) + const v2Eddsa : any = getVCv2EddsaWithValidStatus() const expectedv2EddsaResult = getExpectedVerifiedResult({credential: v2Eddsa, withStatus: true}) + const v1SimpleIssuerId : any = getVCv1SimpleIssuerId() + const expectedV1SimpleIssuerResult = getExpectedVerifiedResult({credential:v1SimpleIssuerId, withStatus: false}) + + const v2SimpleIssuerId : any = getVCv2SimpleIssuerId() + const expectedV2SimpleIssuerResult = getExpectedVerifiedResult({credential:v2SimpleIssuerId, withStatus: false}) chai.use(deepEqualInAnyOrder); const {expect} = chai; -const DISABLE_CONSOLE_WHEN_NO_ERRORS = false +const DISABLE_CONSOLE_WHEN_NO_ERRORS = true describe('Verify.verifyPresentation', () => { @@ -106,6 +114,15 @@ describe('Verify.verifyPresentation', () => { describe('it returns as verified', () => { + it('with v1 and v2 vcs with simple issuer ids', async () => { + const verifiableCredential= [v1SimpleIssuerId, v2SimpleIssuerId] + const presentation = await getSignedVP({holder, verifiableCredential}) as VerifiablePresentation + const credentialResults = [expectedV1SimpleIssuerResult,expectedV2SimpleIssuerResult] + const expectedPresentationResult = getExpectedVerifiedPresentationResult({credentialResults}) + const result = await verifyPresentation({presentation, knownDIDRegistries}) + expect(result).to.deep.equalInAnyOrder(expectedPresentationResult) + }) + it('when signed presentation has one vc in an array', async () => { const verifiableCredential= [v2WithStatus] const presentation = await getSignedVP({holder, verifiableCredential}) as VerifiablePresentation From 1111a36235dd6fc911a28a6da54d73ae11d7de12 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Wed, 29 Jan 2025 15:31:01 -0500 Subject: [PATCH 65/72] extract constants to files --- src/Verify.ts | 148 +++++++++++++++-------------- src/constants/errors.ts | 10 ++ src/constants/external.ts | 4 + src/constants/verificationSteps.ts | 9 ++ src/issuerRegistries.ts | 3 +- test/Verify.general.spec.ts | 5 +- test/Verify.presentation.spec.ts | 18 ++-- test/Verify.v1.spec.ts | 21 ++-- test/Verify.v2.spec.ts | 20 ++-- 9 files changed, 138 insertions(+), 100 deletions(-) create mode 100644 src/constants/errors.ts create mode 100644 src/constants/external.ts create mode 100644 src/constants/verificationSteps.ts diff --git a/src/Verify.ts b/src/Verify.ts index c4be393..0f61053 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -6,14 +6,20 @@ import * as vc from '@digitalcredentials/vc'; import { securityLoader } from '@digitalcredentials/security-document-loader'; import { getCredentialStatusChecker } from './credentialStatus.js'; import { addTrustedIssuersToVerificationResponse } from './issuerRegistries.js'; - +import { + PRESENTATION_ERROR, UNKNOWN_ERROR, INVALID_JSONLD, NO_VC_CONTEXT, + INVALID_CREDENTIAL_ID, NO_PROOF, STATUS_LIST_NOT_FOUND, + HTTP_ERROR_WITH_SIGNATURE_CHECK, DID_WEB_UNRESOLVED, + INVALID_SIGNATURE } from './constants/errors.js'; +import { SIGNATURE_INVALID, SIGNATURE_VALID, SIGNATURE_UNSIGNED, REVOCATION_STATUS_STEP_ID } from './constants/verificationSteps.js'; import { Credential } from './types/credential.js'; import { VerificationResponse, VerificationStep, PresentationVerificationResponse, PresentationSignatureResult } from './types/result.js'; import { VerifiablePresentation } from './types/presentation.js'; -import { extractCredentialsFrom} from './extractCredentialsFrom.js'; +import { extractCredentialsFrom } from './extractCredentialsFrom.js'; import pkg from '@digitalcredentials/jsonld-signatures'; +import { ISSUER_DID_RESOLVES, NOT_FOUND_ERROR, VERIFICATION_ERROR } from './constants/external.js'; const { purposes } = pkg; const presentationPurpose = new purposes.AssertionProofPurpose(); @@ -23,15 +29,17 @@ const documentLoader = securityLoader({ fetchRemoteContexts: true }).build(); const eddsaSuite = new DataIntegrityProof({ cryptosuite: eddsaRdfc2022CryptoSuite }); // for verifying ed25519-2020 signatures const ed25519Suite = new Ed25519Signature2020(); - // add both suites - the vc lib will use whichever is appropriate +// add both suites - the vc lib will use whichever is appropriate const suite = [ed25519Suite, eddsaSuite] -export async function verifyPresentation({presentation, challenge = 'blah', unsignedPresentation = false, knownDIDRegistries, reloadIssuerRegistry=true}: - {presentation: VerifiablePresentation, - challenge?: string | null, - unsignedPresentation? : boolean, - knownDIDRegistries: object, - reloadIssuerRegistry?: boolean} +export async function verifyPresentation({ presentation, challenge = 'meaningless', unsignedPresentation = false, knownDIDRegistries, reloadIssuerRegistry = true }: + { + presentation: VerifiablePresentation, + challenge?: string | null, + unsignedPresentation?: boolean, + knownDIDRegistries: object, + reloadIssuerRegistry?: boolean + } ): Promise { try { const credential = extractCredentialsFrom(presentation)?.find( @@ -48,54 +56,54 @@ export async function verifyPresentation({presentation, challenge = 'blah', unsi verifyMatchingIssuers: false }); - const transformedCredentialResults = await Promise.all(result.credentialResults.map(async (credentialResult:any) => { + const transformedCredentialResults = await Promise.all(result.credentialResults.map(async (credentialResult: any) => { return transformResponse(credentialResult, credentialResult.credential, knownDIDRegistries, reloadIssuerRegistry) })); - + // take what we need from the presentation part of the result - let signature : PresentationSignatureResult; + let signature: PresentationSignatureResult; if (unsignedPresentation) { - signature = 'unsigned' + signature = SIGNATURE_UNSIGNED } else { - signature = result.presentationResult.verified ? 'valid' : 'invalid' + signature = result.presentationResult.verified ? SIGNATURE_VALID : SIGNATURE_INVALID } - const errors = result.error ? [{message: result.error, name: 'presentation_error'}] : null - const presentationResult = {signature, ...(errors && {errors} ) } + const errors = result.error ? [{ message: result.error, name: PRESENTATION_ERROR }] : null + const presentationResult = { signature, ...(errors && { errors }) } - return {presentationResult, credentialResults: transformedCredentialResults}; + return { presentationResult, credentialResults: transformedCredentialResults }; } catch (error) { - return {errors: [{message: 'Could not verify presentation.', name: 'presentation_error', stackTrace: error}]} -} + return { errors: [{ message: 'Could not verify presentation.', name: PRESENTATION_ERROR, stackTrace: error }] } + } } export async function verifyCredential({ credential, knownDIDRegistries, reloadIssuerRegistry = true }: { credential: Credential, knownDIDRegistries: object, reloadIssuerRegistry: boolean }): Promise { -try { - // null unless credential has a status - const statusChecker = getCredentialStatusChecker(credential) - - const verificationResponse = await vc.verifyCredential({ - credential, - suite, - documentLoader, - checkStatus: statusChecker, - verifyMatchingIssuers: false - }); - - const adjustedResponse = transformResponse(verificationResponse, credential, knownDIDRegistries, reloadIssuerRegistry) - return adjustedResponse; -} catch (error) { - return {errors: [{message: 'Could not verify credential.', name: 'unknown_error', stackTrace: error}]} -} + try { + // null unless credential has a status + const statusChecker = getCredentialStatusChecker(credential) + + const verificationResponse = await vc.verifyCredential({ + credential, + suite, + documentLoader, + checkStatus: statusChecker, + verifyMatchingIssuers: false + }); + + const adjustedResponse = transformResponse(verificationResponse, credential, knownDIDRegistries, reloadIssuerRegistry) + return adjustedResponse; + } catch (error) { + return { errors: [{ message: 'Could not verify credential.', name: UNKNOWN_ERROR, stackTrace: error }] } + } } -async function transformResponse(verificationResponse:any, credential:Credential, knownDIDRegistries: object, reloadIssuerRegistry: boolean ) : Promise { - +async function transformResponse(verificationResponse: any, credential: Credential, knownDIDRegistries: object, reloadIssuerRegistry: boolean): Promise { + const fatalCredentialError = handleAnyFatalCredentialErrors(credential) if (fatalCredentialError) { return fatalCredentialError - } + } handleAnyStatusError({ verificationResponse, statusResult: verificationResponse.statusResult }); @@ -112,7 +120,7 @@ async function transformResponse(verificationResponse:any, credential:Credential delete verificationResponse.statusResult delete verificationResponse.verified delete verificationResponse.credentialId - verificationResponse.log = verificationResponse.log.filter((entry:VerificationStep)=>entry.id !== 'issuer_did_resolves') + verificationResponse.log = verificationResponse.log.filter((entry: VerificationStep) => entry.id !== ISSUER_DID_RESOLVES) // add things we always want in the response verificationResponse.credential = credential @@ -133,13 +141,13 @@ function handleAnyFatalCredentialErrors(credential: Credential): VerificationRes if (!suppliedContexts) { const fatalErrorMessage = "The credential does not appear to be a valid jsonld document - there is no context." - const name = 'invalid_jsonld' + const name = INVALID_JSONLD return buildFatalErrorObject(fatalErrorMessage, name, credential, null) } if (!validVCContexts.some(contextURI => suppliedContexts.includes(contextURI))) { const fatalErrorMessage = "The credential doesn't have a verifiable credential context." - const name = 'no_vc_context' + const name = NO_VC_CONTEXT return buildFatalErrorObject(fatalErrorMessage, name, credential, null) } @@ -148,13 +156,13 @@ function handleAnyFatalCredentialErrors(credential: Credential): VerificationRes new URL(credential.id as string); } catch (e) { const fatalErrorMessage = "The credential's id uses an invalid format. It may have been issued as part of an early pilot. Please contact the issuer to get a replacement." - const name = 'invalid_credential_id' + const name = INVALID_CREDENTIAL_ID return buildFatalErrorObject(fatalErrorMessage, name, credential, null) } if (!credential.proof) { const fatalErrorMessage = 'This is not a Verifiable Credential - it does not have a digital signature.' - const name = 'no_proof' + const name = NO_PROOF return buildFatalErrorObject(fatalErrorMessage, name, credential, null) } @@ -166,11 +174,11 @@ function handleAnyStatusError({ verificationResponse }: { statusResult: any }): void { const statusResult = verificationResponse.statusResult - if (statusResult?.error?.cause?.message?.startsWith('NotFoundError')) { + if (statusResult?.error?.cause?.message?.startsWith(NOT_FOUND_ERROR)) { const statusStep = { - "id": "revocation_status", + "id": REVOCATION_STATUS_STEP_ID, "error": { - name: 'status_list_not_found', + name: STATUS_LIST_NOT_FOUND, message: statusResult.error.cause.message } }; @@ -178,10 +186,10 @@ function handleAnyStatusError({ verificationResponse }: { } } -function handleAnySignatureError({ verificationResponse, credential }: { verificationResponse: any, credential: Credential }) : null | VerificationResponse { +function handleAnySignatureError({ verificationResponse, credential }: { verificationResponse: any, credential: Credential }): null | VerificationResponse { if (verificationResponse.error) { - if (verificationResponse?.error?.name === 'VerificationError') { + if (verificationResponse?.error?.name === VERIFICATION_ERROR) { // Can't validate the signature. // Either a bad signature or maybe a did:web that can't // be resolved. Because we can't validate the signature, we @@ -194,8 +202,8 @@ function handleAnySignatureError({ verificationResponse, credential }: { verific // check to see if the error is http related const httpError = verificationResponse.error.errors.find((error: any) => error.name === 'HTTPError') if (httpError) { - fatalErrorMessage = 'An http error prevented the signature check.' - errorName = 'http_error_with_signature_check' + fatalErrorMessage = 'An http error prevented the signature check.' + errorName = HTTP_ERROR_WITH_SIGNATURE_CHECK // was it caused by a did:web that couldn't be resolved??? const issuerDID: string = (((credential.issuer) as any).id) || credential.issuer if (issuerDID.toLowerCase().startsWith('did:web')) { @@ -203,32 +211,32 @@ function handleAnySignatureError({ verificationResponse, credential }: { verific const didUrl = issuerDID.slice(8).replaceAll(':', '/').toLowerCase() if (httpError.requestUrl.toLowerCase().includes(didUrl)) { fatalErrorMessage = `The signature could not be checked because the public signing key could not be retrieved from ${httpError.requestUrl as string}` - errorName = 'did_web_unresolved' - } + errorName = DID_WEB_UNRESOLVED + } } } else { - // not an http error, so likely bad signature - fatalErrorMessage = 'The signature is not valid.' - errorName = 'invalid_signature' + // not an http error, so likely bad signature + fatalErrorMessage = 'The signature is not valid.' + errorName = INVALID_SIGNATURE } const stackTrace = verificationResponse?.error?.errors?.stack return buildFatalErrorObject(fatalErrorMessage, errorName, credential, stackTrace) - - - } else if (verificationResponse.error.log) { - // There wasn't actually an error, it is just that one of the - // steps returned false. - // So move the log out of the error to the response, since it - // isn't part of the error - verificationResponse.log = verificationResponse.error.log - // delete the error, because again, this wasn't an error, just - // a false value on one of the steps - delete verificationResponse.error - } + + + } else if (verificationResponse.error.log) { + // There wasn't actually an error, it is just that one of the + // steps returned false. + // So move the log out of the error to the response, since it + // isn't part of the error + verificationResponse.log = verificationResponse.error.log + // delete the error, because again, this wasn't an error, just + // a false value on one of the steps + delete verificationResponse.error } - return null } + return null +} + - diff --git a/src/constants/errors.ts b/src/constants/errors.ts new file mode 100644 index 0000000..615f289 --- /dev/null +++ b/src/constants/errors.ts @@ -0,0 +1,10 @@ +export const PRESENTATION_ERROR = 'presentation_error' +export const UNKNOWN_ERROR = 'unknown_error' +export const INVALID_JSONLD = 'invalid_jsonld' +export const NO_VC_CONTEXT = 'no_vc_context' +export const INVALID_CREDENTIAL_ID = 'invalid_credential_id' +export const NO_PROOF = 'no_proof' +export const STATUS_LIST_NOT_FOUND = 'status_list_not_found' +export const HTTP_ERROR_WITH_SIGNATURE_CHECK = 'http_error_with_signature_check' +export const DID_WEB_UNRESOLVED = 'did_web_unresolved' +export const INVALID_SIGNATURE = 'invalid_signature' diff --git a/src/constants/external.ts b/src/constants/external.ts new file mode 100644 index 0000000..7476b46 --- /dev/null +++ b/src/constants/external.ts @@ -0,0 +1,4 @@ +// constants used by @digitalcredentials.vc library +export const ISSUER_DID_RESOLVES = 'issuer_did_resolves' +export const NOT_FOUND_ERROR = 'NotFoundError' +export const VERIFICATION_ERROR = 'VerificationError' \ No newline at end of file diff --git a/src/constants/verificationSteps.ts b/src/constants/verificationSteps.ts new file mode 100644 index 0000000..6f868a0 --- /dev/null +++ b/src/constants/verificationSteps.ts @@ -0,0 +1,9 @@ +// VERIFICATION STEP iDS +export const REVOCATION_STATUS_STEP_ID = 'revocation_status' +export const REGISTERED_ISSUER_STEP_ID = 'registered_issuer' +export const EXPIRATION_STEP_ID = 'expiration' +export const VALID_SIGNATURE_STEP_ID = 'valid_signature' +// VERIFICATION VALUES +export const SIGNATURE_VALID = 'valid' +export const SIGNATURE_INVALID = 'invalid' +export const SIGNATURE_UNSIGNED = 'unsigned' diff --git a/src/issuerRegistries.ts b/src/issuerRegistries.ts index 850468b..4259d92 100644 --- a/src/issuerRegistries.ts +++ b/src/issuerRegistries.ts @@ -1,5 +1,6 @@ import {RegistryClient, LoadResult} from '@digitalcredentials/issuer-registry-client'; import { VerificationResponse, RegistryListResult } from './types/result.js'; +import { REGISTERED_ISSUER_STEP_ID } from './constants/verificationSteps.js'; const registries = new RegistryClient() const registryNotYetLoaded = true; @@ -44,7 +45,7 @@ export async function addTrustedIssuersToVerificationResponse( {issuer, knownDID const {foundInRegistries,registriesNotLoaded} = await getTrustedRegistryListForIssuer( {issuer, knownDIDRegistries, reloadIssuerRegistry}); const registryStep = { - "id": "registered_issuer", + "id": REGISTERED_ISSUER_STEP_ID, "valid": !!foundInRegistries.length, foundInRegistries, registriesNotLoaded diff --git a/test/Verify.general.spec.ts b/test/Verify.general.spec.ts index fc878e4..7a5ba35 100644 --- a/test/Verify.general.spec.ts +++ b/test/Verify.general.spec.ts @@ -9,6 +9,7 @@ import { knownDIDRegistries } from '../.knownDidRegistries.js'; import { getExpectedFatalResult } from '../src/test-fixtures/expectedResults.js'; +import { INVALID_JSONLD, NO_VC_CONTEXT } from '../src/constants/errors.js'; chai.use(deepEqualInAnyOrder); const {expect} = chai; @@ -49,7 +50,7 @@ describe('Verify', () => { const expectedResult = getExpectedFatalResult({ credential, errorMessage: 'The credential does not appear to be a valid jsonld document - there is no context.', - errorName: 'invalid_jsonld' + errorName: INVALID_JSONLD }) expect(result).to.deep.equalInAnyOrder(expectedResult) }) @@ -61,7 +62,7 @@ describe('Verify', () => { const expectedResult = getExpectedFatalResult({ credential, errorMessage: "The credential doesn't have a verifiable credential context.", - errorName: 'no_vc_context' + errorName: NO_VC_CONTEXT }) expect(result).to.deep.equalInAnyOrder(expectedResult) }) diff --git a/test/Verify.presentation.spec.ts b/test/Verify.presentation.spec.ts index ecb917d..63709f1 100644 --- a/test/Verify.presentation.spec.ts +++ b/test/Verify.presentation.spec.ts @@ -43,13 +43,15 @@ import { import { getSignedVP, getUnSignedVP } from './vpUtils.js'; import { VerifiablePresentation } from '../src/types/presentation.js'; +import { INVALID_CREDENTIAL_ID, INVALID_SIGNATURE, NO_PROOF, PRESENTATION_ERROR } from '../src/constants/errors.js'; +import { SIGNATURE_INVALID, SIGNATURE_UNSIGNED } from '../src/constants/verificationSteps.js'; const noProofVC : any = getVCv1NoProof() const expectedNoProofResult = getExpectedFatalResult({ credential: noProofVC, errorMessage: 'This is not a Verifiable Credential - it does not have a digital signature.', - errorName: 'no_proof' + errorName: NO_PROOF }) @@ -57,7 +59,7 @@ import { VerifiablePresentation } from '../src/types/presentation.js'; const expectedBadIdResult = getExpectedFatalResult({ credential: badIdVC, errorMessage: "The credential's id uses an invalid format. It may have been issued as part of an early pilot. Please contact the issuer to get a replacement.", - errorName: 'invalid_credential_id' + errorName: INVALID_CREDENTIAL_ID }) @@ -172,8 +174,8 @@ describe('Verify.verifyPresentation', () => { const presentation = await getSignedVP({holder, verifiableCredential}) as any presentation.verifiableCredential[0].name = 'Tampered Name' const result = await verifyPresentation({presentation, knownDIDRegistries}) as any - expect(result.presentationResult.signature).to.equal('invalid') - expect(result.credentialResults[0].errors[0].name).to.equal('invalid_signature') + expect(result.presentationResult.signature).to.equal(SIGNATURE_INVALID) + expect(result.credentialResults[0].errors[0].name).to.equal(INVALID_SIGNATURE) }) it('when signed presentation has been tampered with', async () => { @@ -183,7 +185,7 @@ describe('Verify.verifyPresentation', () => { const result = await verifyPresentation({presentation, knownDIDRegistries}) as any const expectedCredentialResults = [expectedV1Result] expect(result.credentialResults).to.deep.equalInAnyOrder(expectedCredentialResults) - expect(result.presentationResult.signature).to.equal('invalid') + expect(result.presentationResult.signature).to.equal(SIGNATURE_INVALID) }) it('when unsigned presentation has bad vc', async () => { @@ -214,7 +216,7 @@ describe('Verify.verifyPresentation', () => { const credentialResults = [expectedNoProofResult] const expectedPresentationResult = getExpectedVerifiedPresentationResult({credentialResults}) if (expectedPresentationResult?.presentationResult) { - expectedPresentationResult.presentationResult.signature = 'unsigned' + expectedPresentationResult.presentationResult.signature = SIGNATURE_UNSIGNED } const result = await verifyPresentation({presentation, knownDIDRegistries, unsignedPresentation:true}) expect(result).to.deep.equalInAnyOrder(expectedPresentationResult) @@ -224,7 +226,7 @@ describe('Verify.verifyPresentation', () => { const verifiableCredential= [noProofVC] const presentation = await getUnSignedVP({verifiableCredential}) as VerifiablePresentation const result = await verifyPresentation({presentation, knownDIDRegistries}) - expect(result?.presentationResult?.signature).to.equal('invalid') + expect(result?.presentationResult?.signature).to.equal(SIGNATURE_INVALID) }) it('when bad presentation', async () => { @@ -233,7 +235,7 @@ describe('Verify.verifyPresentation', () => { delete presentation['@context'] const result = await verifyPresentation({presentation, knownDIDRegistries}) if (result?.errors) { - expect(result.errors[0].name).to.equal('presentation_error') + expect(result.errors[0].name).to.equal(PRESENTATION_ERROR) } else { expect(false).to.equal(true) } diff --git a/test/Verify.v1.spec.ts b/test/Verify.v1.spec.ts index 54cfeaa..dcf5355 100644 --- a/test/Verify.v1.spec.ts +++ b/test/Verify.v1.spec.ts @@ -19,6 +19,8 @@ import { getExpectedUnverifiedResult, getExpectedFatalResult } from '../src/test-fixtures/expectedResults.js'; +import { INVALID_CREDENTIAL_ID, INVALID_SIGNATURE, NO_PROOF } from '../src/constants/errors.js'; +import { EXPIRATION_STEP_ID, REGISTERED_ISSUER_STEP_ID } from '../src/constants/verificationSteps.js'; chai.use(deepEqualInAnyOrder); const {expect} = chai; @@ -74,7 +76,7 @@ describe('Verify', () => { const expectedResult = getExpectedFatalResult({ credential, errorMessage: 'The signature is not valid.', - errorName: 'invalid_signature' + errorName: INVALID_SIGNATURE }) expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define }) @@ -85,7 +87,7 @@ describe('Verify', () => { const expectedResult = getExpectedFatalResult({ credential, errorMessage: 'The signature is not valid.', - errorName: 'invalid_signature' + errorName: INVALID_SIGNATURE }) expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define }) @@ -96,7 +98,7 @@ describe('Verify', () => { const expectedResult = getExpectedFatalResult({ credential, errorMessage: 'This is not a Verifiable Credential - it does not have a digital signature.', - errorName: 'no_proof' + errorName: NO_PROOF }) expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define }) @@ -106,11 +108,10 @@ describe('Verify', () => { const expectedResult = getExpectedFatalResult({ credential, errorMessage: "The credential's id uses an invalid format. It may have been issued as part of an early pilot. Please contact the issuer to get a replacement.", - errorName: 'invalid_credential_id' + errorName: INVALID_CREDENTIAL_ID }) expect(result).to.deep.equalInAnyOrder(expectedResult) }) - }) describe('returns as verified', () => { @@ -125,7 +126,7 @@ describe('Verify', () => { describe('returns unverified', () => { it('when expired', async () => { const credential : any = getVCv1Expired() - const expectedResult = getExpectedUnverifiedResult({credential, unVerifiedStep: 'expiration', withStatus:false}) + const expectedResult = getExpectedUnverifiedResult({credential, unVerifiedStep: EXPIRATION_STEP_ID, withStatus:false}) const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) expect(result).to.deep.equalInAnyOrder(expectedResult) }) @@ -137,7 +138,7 @@ describe('Verify', () => { it('when expired with valid status', async () => { const credential : any = getVCv1ExpiredWithValidStatus() - const expectedResult = getExpectedUnverifiedResult({credential, unVerifiedStep: 'expiration', withStatus:true}) + const expectedResult = getExpectedUnverifiedResult({credential, unVerifiedStep: EXPIRATION_STEP_ID, withStatus:true}) const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define }) @@ -148,7 +149,7 @@ describe('Verify', () => { // set the one matching registry to a url that won't load noMatchingRegistryList[1].url = 'https://onldynoyrt.com/registry.json' const expectedResult : any = getExpectedVerifiedResult({credential, withStatus: true}) - const expectedResultRegistryLogEntry = expectedResult.log.find((entry:any)=>entry.id==='registered_issuer') + const expectedResultRegistryLogEntry = expectedResult.log.find((entry:any)=>entry.id===REGISTERED_ISSUER_STEP_ID) expectedResultRegistryLogEntry.registriesNotLoaded = [ { "name": "DCC Sandbox Registry", @@ -172,7 +173,7 @@ describe('Verify', () => { const badRegistryList = JSON.parse(JSON.stringify(knownDIDRegistries)) badRegistryList[0].url = 'https://onldynoyrt.com/registry.json' const expectedResult : any = getExpectedVerifiedResult({credential, withStatus: true}) - expectedResult.log.find((entry:any)=>entry.id==='registered_issuer').registriesNotLoaded = [ + expectedResult.log.find((entry:any)=>entry.id===REGISTERED_ISSUER_STEP_ID).registriesNotLoaded = [ { "name": "DCC Pilot Registry", "url": "https://onldynoyrt.com/registry.json" @@ -188,7 +189,7 @@ describe('Verify', () => { badRegistryList[0].url = 'https://onldynoyrt.com/registry.json' badRegistryList[2].url = 'https://onldynoyrrrt.com/registry.json' const expectedResult : any = getExpectedVerifiedResult({credential, withStatus: true}) - expectedResult.log.find((entry:any)=>entry.id==='registered_issuer').registriesNotLoaded = [ + expectedResult.log.find((entry:any)=>entry.id===REGISTERED_ISSUER_STEP_ID).registriesNotLoaded = [ { "name": "DCC Community Registry", "url": "https://onldynoyrrrt.com/registry.json" diff --git a/test/Verify.v2.spec.ts b/test/Verify.v2.spec.ts index 90fcfa1..2dc8e07 100644 --- a/test/Verify.v2.spec.ts +++ b/test/Verify.v2.spec.ts @@ -25,6 +25,8 @@ import { getExpectedUnverifiedResult, getExpectedFatalResult } from '../src/test-fixtures/expectedResults.js'; +import { EXPIRATION_STEP_ID, REVOCATION_STATUS_STEP_ID } from '../src/constants/verificationSteps.js'; +import { DID_WEB_UNRESOLVED, INVALID_CREDENTIAL_ID, INVALID_SIGNATURE, NO_PROOF, STATUS_LIST_NOT_FOUND } from '../src/constants/errors.js'; chai.use(deepEqualInAnyOrder); const {expect} = chai; @@ -66,9 +68,9 @@ describe('Verify', () => { const expectedResult = getExpectedVerifiedResult({credential, withStatus: false}) expectedResult.log?.push( { - "id": "revocation_status", + "id": REVOCATION_STATUS_STEP_ID, "error": { - "name": "status_list_not_found", + "name": STATUS_LIST_NOT_FOUND, "message": "NotFoundError loading \"https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5VK8CbZ1GjycuPombrj\": Request failed with status code 404 Not Found: GET https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5VK8CbZ1GjycuPombrj" } }) @@ -98,7 +100,7 @@ describe('Verify', () => { const expectedResult = getExpectedFatalResult({ credential, errorMessage: 'The signature is not valid.', - errorName: 'invalid_signature' + errorName: INVALID_SIGNATURE }) expect(result).to.deep.equalInAnyOrder(expectedResult) }) @@ -109,7 +111,7 @@ describe('Verify', () => { const expectedResult = getExpectedFatalResult({ credential, errorMessage: 'The signature is not valid.', - errorName: 'invalid_signature' + errorName: INVALID_SIGNATURE }) expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define }) @@ -121,7 +123,7 @@ describe('Verify', () => { const expectedResult = getExpectedFatalResult({ credential, errorMessage: 'This is not a Verifiable Credential - it does not have a digital signature.', - errorName: 'no_proof' + errorName: NO_PROOF }) expect(result).to.deep.equalInAnyOrder(expectedResult) }) @@ -132,14 +134,14 @@ describe('Verify', () => { const expectedResult = getExpectedFatalResult({ credential, errorMessage: "The credential's id uses an invalid format. It may have been issued as part of an early pilot. Please contact the issuer to get a replacement.", - errorName: 'invalid_credential_id' + errorName: INVALID_CREDENTIAL_ID }) expect(result).to.deep.equalInAnyOrder(expectedResult) }) it('when did:web url is unreachable', async () => { const credential : any = getVCv2WithBadDidWebUrl() - const errorName = "did_web_unresolved" + const errorName = DID_WEB_UNRESOLVED const errorMessage = "The signature could not be checked because the public signing key could not be retrieved from https://digitalcredentials.github.io/dcc-did-web-bad/did.json" const expectedResult = getExpectedFatalResult({credential, errorName, errorMessage}) const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) @@ -178,7 +180,7 @@ describe('Verify', () => { describe('returns as unverified', () => { it('when expired', async () => { const credential : any = getVCv2Expired() - const expectedResult = getExpectedUnverifiedResult({credential, unVerifiedStep: 'expiration', withStatus:false}) + const expectedResult = getExpectedUnverifiedResult({credential, unVerifiedStep: EXPIRATION_STEP_ID, withStatus:false}) const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) expect(result).to.deep.equalInAnyOrder(expectedResult) }) @@ -190,7 +192,7 @@ describe('Verify', () => { it('when expired with valid status', async () => { // NOTE: TODO - this will continue to fail until we fix https://github.com/digitalcredentials/vc/issues/28 const credential : any = getVCv2ExpiredWithValidStatus() - const expectedResult = getExpectedUnverifiedResult({credential, unVerifiedStep: 'expiration', withStatus:true}) + const expectedResult = getExpectedUnverifiedResult({credential, unVerifiedStep: EXPIRATION_STEP_ID, withStatus:true}) const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define }) From 1e2360e34e35746947dc872c4eb46e8be19c6aa8 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Wed, 29 Jan 2025 15:38:39 -0500 Subject: [PATCH 66/72] remove old status list check --- package.json | 3 +-- src/credentialStatus.ts | 14 +++++--------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 774ee41..d3db3f1 100644 --- a/package.json +++ b/package.json @@ -33,8 +33,7 @@ "@digitalcredentials/jsonld-signatures": "^12.0.1", "@digitalcredentials/security-document-loader": "^6.0.1", "@digitalcredentials/vc": "^9.0.1-beta.2", - "@digitalcredentials/vc-bitstring-status-list": "^1.0.0", - "@digitalcredentials/vc-status-list": "^9.0.0" + "@digitalcredentials/vc-bitstring-status-list": "^1.0.0" }, "devDependencies": { "@types/chai": "^4.3.4", diff --git a/src/credentialStatus.ts b/src/credentialStatus.ts index 6bd4a33..760f6f1 100644 --- a/src/credentialStatus.ts +++ b/src/credentialStatus.ts @@ -1,5 +1,4 @@ import { checkStatus } from '@digitalcredentials/vc-bitstring-status-list'; -import { checkStatus as checkStatusLegacy } from '@digitalcredentials/vc-status-list'; import { Credential } from './types/credential'; export enum StatusPurpose { @@ -8,21 +7,18 @@ export enum StatusPurpose { } export function getCredentialStatusChecker(credential: Credential) : (() => boolean) | null { + let statusChecker = null; if (!credential.credentialStatus) { return null; } - const credentialStatuses = Array.isArray(credential.credentialStatus) ? credential.credentialStatus : [credential.credentialStatus]; const [credentialStatus] = credentialStatuses; - switch (credentialStatus.type) { - case 'BitstringStatusListEntry': - return checkStatus; - case 'StatusList2021Entry': - return checkStatusLegacy; - default: - return null; + if (credentialStatus.type === 'BitstringStatusListEntry') { + statusChecker = checkStatus; } + return statusChecker; + } From 197845f907218214e3bf4dde718db3f13710cdef Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Wed, 29 Jan 2025 16:37:32 -0500 Subject: [PATCH 67/72] update error explanation in README --- README.md | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 59 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 4a2cf85..2554229 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,41 @@ The typescript definitions for the result can be found [here](./src/types/result Note that the verification result doesn't make any conclusion about the overall validity of a credential. It only checks the validity of each of the four steps, leaving it up to the consumer of the result to decide on the overall validity. The consumer might not, for example, consider a credential that had expired or had been revoked to be 'invalid'. The credential might still in fact be useful as a record of history, i.e, I had a driver's licence that expired two years ago, but it was valid during the period 2018 to 2023, and that information might be useful. +Four steps are checked, returning a result per step in a log like so: + + +``` +{ + "credential": {the VC that was submitted is returned here}, + "log": [ + { + "id": "valid_signature", + "valid": true (if it is false then an error is returned instead of the log) + }, + { + "id": "expiration", + "valid": true/false + }, + { + "id": "revocation_status", + "valid": true/false + }, + { + "id": "registered_issuer", + "valid": true/false, + "foundInRegistries": [ + "DCC Sandbox Registry" + ], + "registriesNotLoaded":[ + "DCC Issuer Registry" + ] + } + ] +} +``` + +Variations and errors are covered next... + There are three general flavours of result that might be returned: 1. all checks were conclusive @@ -183,7 +218,7 @@ A partially successful verification might look like this example, where we could { "id": "revocation_status", "error": { - "name": "network-error", + "name": "'status_list_not_found'", "message": "Could not retrieve the revocation status list." } }, @@ -241,11 +276,29 @@ has been taken down, or there is a network error. } ``` +unknown http error + +A catchall error for unknown http errors when verifying the signature. + +``` +{ + "credential": {vc removed for brevity/clarity}, + "errors": [ + { + "name": "http_error_with_signature_check", + "message": "An http error prevented the signature check." + } + ] +} +``` + + + malformed credential The supplied credential may not conform to the VerifiableCredential or LinkedData specifications(possibly because it follows some older convention, or maybe hasn't yet been signed) and might not even be a Verifiable Credential at all. -Some specific examples: +Specific cases: invalid_jsonld @@ -696,7 +749,7 @@ An unsigned VP containing a single verified credential: } ``` -A VP where we've tampered with one of the packaged credentials (by changing the credential name). Note here that both the VP and the VC don't verify because changing the VC affected the VC signature bit also the VP signature which contains the VC. +A VP where we've tampered with one of the packaged credentials (by changing the credential name). Note here that both the VP and the VC don't verify because changing the VC affected the VC's signature and also the VP signature which contains the VC. ``` { @@ -792,6 +845,7 @@ And here is a VP where just the VP has been tampered with, and not the embedded "signature": "invalid", "errors": [ { + "name": "presentation_error", "message": { "name": "VerificationError", "errors": [ @@ -801,8 +855,7 @@ And here is a VP where just the VP has been tampered with, and not the embedded "stack": "Error: Invalid signature.\n at Ed25519Signature2020.verifyProof (/Users/jameschartrand/Documents/github/dcc/verifier-core/node_modules/@digitalcredentials/jsonld-signatures/lib/suites/LinkedDataSignature.js:189:15)\n at async /Users/jameschartrand/Documents/github/dcc/verifier-core/node_modules/@digitalcredentials/jsonld-signatures/lib/ProofSet.js:273:53\n at async Promise.all (index 0)\n at async _verify (/Users/jameschartrand/Documents/github/dcc/verifier-core/node_modules/@digitalcredentials/jsonld-signatures/lib/ProofSet.js:261:3)\n at async ProofSet.verify (/Users/jameschartrand/Documents/github/dcc/verifier-core/node_modules/@digitalcredentials/jsonld-signatures/lib/ProofSet.js:195:23)\n at async Object.verify (/Users/jameschartrand/Documents/github/dcc/verifier-core/node_modules/@digitalcredentials/jsonld-signatures/lib/jsonld-signatures.js:160:18)\n at async _verifyPresentation (/Users/jameschartrand/Documents/github/dcc/verifier-core/node_modules/@digitalcredentials/vc/dist/index.js:578:30)\n at async verifyPresentation (file:///Users/jameschartrand/Documents/github/dcc/verifier-core/dist/src/Verify.js:24:24)\n at async Context. (file:///Users/jameschartrand/Documents/github/dcc/verifier-core/dist/test/Verify.presentation.spec.js:101:28)" } ] - }, - "name": "presentation_error" + } } ] }, @@ -834,7 +887,7 @@ And here is a VP where just the VP has been tampered with, and not the embedded ## Install -- Node.js 18+ is recommended. +- Node.js 20+ is recommended. ### NPM From e7ee7b78ffc05d06fa594aefcb7eb10ec3337b00 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Mon, 3 Feb 2025 19:44:09 -0500 Subject: [PATCH 68/72] add js extension to fix published package --- package.json | 21 ++++++++++----------- src/index.ts | 2 +- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index d3db3f1..5dca88f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@digitalcredentials/verifier-core", "description": "For verifying Verifiable Credentials in the browser, Node.js, and React Native.", - "version": "0.0.1", + "version": "0.0.1-beta.2", "scripts": { "build": "npm run clear && tsc -d && tsc -p tsconfig.esm.json", "build-test": "npm run clear && tsc -p tsconfig.spec.json", @@ -17,14 +17,14 @@ "test-node": "npm run build-test && npx c8 --exclude 'dist/test/**' mocha dist/test/*.spec.js && rm -rf dist/esm/test || true", "coveralls": "npm run test; npx c8 --exclude 'dist/test/**' report --reporter=text-lcov > ./coverage/lcov.info" }, + "type": "module", + "exports": { + ".": {"import": "./dist/index.js"}, + "./package.json": "./package.json" + }, "files": [ - "dist", - "README.md", - "LICENSE.md" + "dist/**/*.js" ], - "main": "dist/index.js", - "module": "dist/esm/index.js", - "types": "dist/index.d.ts", "dependencies": { "@digitalbazaar/data-integrity": "^2.5.0", "@digitalbazaar/eddsa-rdfc-2022-cryptosuite": "^1.2.0", @@ -83,9 +83,8 @@ "license": "MIT", "repository": { "type": "git", - "url": "https://github.com/digitalcredentials/isomorphic-lib-template" + "url": "https://github.com/digitalcredentials/verifier-core" }, - "homepage": "https://github.com/digitalcredentials/isomorphic-lib-template", - "bugs": "https://github.com/digitalcredentials/isomorphic-lib-template/issues", - "type": "module" + "homepage": "https://github.com/digitalcredentials/verifier-core", + "bugs": "https://github.com/digitalcredentials/verifier-core" } diff --git a/src/index.ts b/src/index.ts index 9d833f2..1bd0438 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,4 +2,4 @@ * Copyright (c) 2022 Digital Credentials Consortium. All rights reserved. */ export { verifyCredential, // verifyPresentation - } from './Verify' + } from './Verify.js' From e8dba6ece29905a67084fac90d159db31c66b357 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Wed, 5 Feb 2025 19:05:56 -0500 Subject: [PATCH 69/72] update dependencies and version for publish --- package.json | 10 ++++++---- src/Verify.ts | 4 ++-- src/declarations.d.ts | 4 ++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 5dca88f..ba5b261 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@digitalcredentials/verifier-core", "description": "For verifying Verifiable Credentials in the browser, Node.js, and React Native.", - "version": "0.0.1-beta.2", + "version": "0.0.1-beta.3", "scripts": { "build": "npm run clear && tsc -d && tsc -p tsconfig.esm.json", "build-test": "npm run clear && tsc -p tsconfig.spec.json", @@ -19,16 +19,18 @@ }, "type": "module", "exports": { - ".": {"import": "./dist/index.js"}, + ".": { + "import": "./dist/index.js" + }, "./package.json": "./package.json" }, "files": [ "dist/**/*.js" ], "dependencies": { - "@digitalbazaar/data-integrity": "^2.5.0", - "@digitalbazaar/eddsa-rdfc-2022-cryptosuite": "^1.2.0", + "@digitalcredentials/data-integrity": "^2.5.1-beta.1", "@digitalcredentials/ed25519-signature-2020": "^6.0.0", + "@digitalcredentials/eddsa-rdfc-2022-cryptosuite": "^1.2.1-beta.1", "@digitalcredentials/issuer-registry-client": "^3.0.1-beta.1", "@digitalcredentials/jsonld-signatures": "^12.0.1", "@digitalcredentials/security-document-loader": "^6.0.1", diff --git a/src/Verify.ts b/src/Verify.ts index 0f61053..a084289 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -1,7 +1,7 @@ // import '@digitalcredentials/data-integrity-rn'; import { Ed25519Signature2020 } from '@digitalcredentials/ed25519-signature-2020'; -import { DataIntegrityProof } from '@digitalbazaar/data-integrity'; -import { cryptosuite as eddsaRdfc2022CryptoSuite } from '@digitalbazaar/eddsa-rdfc-2022-cryptosuite'; +import { DataIntegrityProof } from '@digitalcredentials/data-integrity'; +import { cryptosuite as eddsaRdfc2022CryptoSuite } from '@digitalcredentials/eddsa-rdfc-2022-cryptosuite'; import * as vc from '@digitalcredentials/vc'; import { securityLoader } from '@digitalcredentials/security-document-loader'; import { getCredentialStatusChecker } from './credentialStatus.js'; diff --git a/src/declarations.d.ts b/src/declarations.d.ts index e276bec..9509cb8 100644 --- a/src/declarations.d.ts +++ b/src/declarations.d.ts @@ -5,7 +5,7 @@ declare module '@digitalcredentials/vc-bitstring-status-list'; declare module '@digitalcredentials/vc-status-list'; declare module '@digitalcredentials/vpqr'; declare module '@digitalcredentials/jsonld-signatures'; -declare module '@digitalbazaar/data-integrity'; -declare module '@digitalbazaar/eddsa-rdfc-2022-cryptosuite'; +declare module '@digitalcredentials/data-integrity'; +declare module '@digitalcredentials/eddsa-rdfc-2022-cryptosuite'; declare module '@digitalcredentials/ed25519-signature-2020'; declare module '@digitalcredentials/ed25519-verification-key-2020'; \ No newline at end of file From a2116a7c39e88e2e1ed893f078ec639167a8d685 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Thu, 6 Feb 2025 07:37:15 -0500 Subject: [PATCH 70/72] add await to result transform call --- src/Verify.ts | 2 +- test/Verify.presentation.spec.ts | 2 +- test/Verify.v2.spec.ts | 5 ++++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Verify.ts b/src/Verify.ts index a084289..56b71b0 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -90,7 +90,7 @@ export async function verifyCredential({ credential, knownDIDRegistries, reloadI verifyMatchingIssuers: false }); - const adjustedResponse = transformResponse(verificationResponse, credential, knownDIDRegistries, reloadIssuerRegistry) + const adjustedResponse = await transformResponse(verificationResponse, credential, knownDIDRegistries, reloadIssuerRegistry) return adjustedResponse; } catch (error) { return { errors: [{ message: 'Could not verify credential.', name: UNKNOWN_ERROR, stackTrace: error }] } diff --git a/test/Verify.presentation.spec.ts b/test/Verify.presentation.spec.ts index 63709f1..731eaa5 100644 --- a/test/Verify.presentation.spec.ts +++ b/test/Verify.presentation.spec.ts @@ -144,7 +144,7 @@ describe('Verify.verifyPresentation', () => { expect(result).to.deep.equalInAnyOrder(expectedPresentationResult) }) - it('when signed presentation has mix of VCs', async () => { + it.skip('when signed presentation has mix of VCs', async () => { const verifiableCredential = [v2WithStatus, v2Eddsa, didWebVC] const presentation = await getSignedVP({verifiableCredential, holder: 'did:ex:12345'}) as VerifiablePresentation const credentialResults = [expectedV2WithStatusResult, expectedv2EddsaResult, expectedDidWebResult] diff --git a/test/Verify.v2.spec.ts b/test/Verify.v2.spec.ts index 2dc8e07..d7217c7 100644 --- a/test/Verify.v2.spec.ts +++ b/test/Verify.v2.spec.ts @@ -82,10 +82,13 @@ describe('Verify', () => { }) describe('with eddsa signature', () => { describe('it returns as verified', () => { - it('when status is valid', async () => { + it.skip('when status is valid', async () => { const credential : any = getVCv2EddsaWithValidStatus() const expectedResult = getExpectedVerifiedResult({credential, withStatus: true}) const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + + // console.log(JSON.stringify(result,null,2)) + expect(result).to.deep.equalInAnyOrder(expectedResult) }) From b875b05e86db213eac76e6d98f6b90c93f3b853b Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Thu, 6 Feb 2025 12:48:04 -0500 Subject: [PATCH 71/72] update cryptosuite dependency and version for publish --- package.json | 4 ++-- test/Verify.presentation.spec.ts | 2 +- test/Verify.v2.spec.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index ba5b261..9f6b16b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@digitalcredentials/verifier-core", "description": "For verifying Verifiable Credentials in the browser, Node.js, and React Native.", - "version": "0.0.1-beta.3", + "version": "0.0.1-beta.4", "scripts": { "build": "npm run clear && tsc -d && tsc -p tsconfig.esm.json", "build-test": "npm run clear && tsc -p tsconfig.spec.json", @@ -30,7 +30,7 @@ "dependencies": { "@digitalcredentials/data-integrity": "^2.5.1-beta.1", "@digitalcredentials/ed25519-signature-2020": "^6.0.0", - "@digitalcredentials/eddsa-rdfc-2022-cryptosuite": "^1.2.1-beta.1", + "@digitalcredentials/eddsa-rdfc-2022-cryptosuite": "^1.2.1-beta.2", "@digitalcredentials/issuer-registry-client": "^3.0.1-beta.1", "@digitalcredentials/jsonld-signatures": "^12.0.1", "@digitalcredentials/security-document-loader": "^6.0.1", diff --git a/test/Verify.presentation.spec.ts b/test/Verify.presentation.spec.ts index 731eaa5..63709f1 100644 --- a/test/Verify.presentation.spec.ts +++ b/test/Verify.presentation.spec.ts @@ -144,7 +144,7 @@ describe('Verify.verifyPresentation', () => { expect(result).to.deep.equalInAnyOrder(expectedPresentationResult) }) - it.skip('when signed presentation has mix of VCs', async () => { + it('when signed presentation has mix of VCs', async () => { const verifiableCredential = [v2WithStatus, v2Eddsa, didWebVC] const presentation = await getSignedVP({verifiableCredential, holder: 'did:ex:12345'}) as VerifiablePresentation const credentialResults = [expectedV2WithStatusResult, expectedv2EddsaResult, expectedDidWebResult] diff --git a/test/Verify.v2.spec.ts b/test/Verify.v2.spec.ts index d7217c7..75517cc 100644 --- a/test/Verify.v2.spec.ts +++ b/test/Verify.v2.spec.ts @@ -82,7 +82,7 @@ describe('Verify', () => { }) describe('with eddsa signature', () => { describe('it returns as verified', () => { - it.skip('when status is valid', async () => { + it('when status is valid', async () => { const credential : any = getVCv2EddsaWithValidStatus() const expectedResult = getExpectedVerifiedResult({credential, withStatus: true}) const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) From ea15abb3f3523b3589d0b85b1f4516f9f485c288 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Mon, 10 Feb 2025 09:17:00 -0500 Subject: [PATCH 72/72] cleanup --- .knownDidRegistries.ts | 19 ------------------- README.md | 2 +- karma.conf.js => karma.conf.cjs | 0 package.json | 6 +++--- src/Verify.ts | 10 ++++++---- src/test-fixtures/knownDidRegistries.ts | 18 ++++++++++++++++++ test/Verify.general.spec.ts | 2 +- test/Verify.presentation.spec.ts | 2 +- test/Verify.v1.spec.ts | 2 +- test/Verify.v2.spec.ts | 2 +- tsconfig.json | 4 ++-- 11 files changed, 34 insertions(+), 33 deletions(-) delete mode 100644 .knownDidRegistries.ts rename karma.conf.js => karma.conf.cjs (100%) create mode 100644 src/test-fixtures/knownDidRegistries.ts diff --git a/.knownDidRegistries.ts b/.knownDidRegistries.ts deleted file mode 100644 index 6840b02..0000000 --- a/.knownDidRegistries.ts +++ /dev/null @@ -1,19 +0,0 @@ -export const knownDIDRegistries = [ - { - name: 'DCC Pilot Registry', - url: 'https://digitalcredentials.github.io/issuer-registry/registry.json' - }, - { - name: 'DCC Sandbox Registry', - url: 'https://digitalcredentials.github.io/sandbox-registry/registry.json' - }, - { - name: 'DCC Community Registry', - url: 'https://digitalcredentials.github.io/community-registry/registry.json' - }, - { - name: 'DCC Registry', - url: 'https://digitalcredentials.github.io/dcc-registry/registry.json' - } -] - diff --git a/README.md b/README.md index 2554229..c81d616 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ The verification checks that the credential: * has a valid signature, and so therefore: * the credential hasn't been tampered with - * the signing key was retrieved from the did document + * the public key was successfully retrieved from the did document * hasn't expired * hasn't been revoked * was signed by a trusted issuer diff --git a/karma.conf.js b/karma.conf.cjs similarity index 100% rename from karma.conf.js rename to karma.conf.cjs diff --git a/package.json b/package.json index 9f6b16b..a1a6d22 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@digitalcredentials/verifier-core", "description": "For verifying Verifiable Credentials in the browser, Node.js, and React Native.", - "version": "0.0.1-beta.4", + "version": "1.0.0", "scripts": { "build": "npm run clear && tsc -d && tsc -p tsconfig.esm.json", "build-test": "npm run clear && tsc -p tsconfig.spec.json", @@ -12,7 +12,7 @@ "prettier": "prettier src --write", "rebuild": "npm run clear && npm run build", "test": "npm run lint && npm run test-node", - "test-karma": "karma start karma.conf.js", + "test-karma": "karma start karma.conf.cjs", "test-node-no-cov": "npm run build-test && mocha dist/test/*.spec.js && rm -rf dist/esm/test || true", "test-node": "npm run build-test && npx c8 --exclude 'dist/test/**' mocha dist/test/*.spec.js && rm -rf dist/esm/test || true", "coveralls": "npm run test; npx c8 --exclude 'dist/test/**' report --reporter=text-lcov > ./coverage/lcov.info" @@ -30,7 +30,7 @@ "dependencies": { "@digitalcredentials/data-integrity": "^2.5.1-beta.1", "@digitalcredentials/ed25519-signature-2020": "^6.0.0", - "@digitalcredentials/eddsa-rdfc-2022-cryptosuite": "^1.2.1-beta.2", + "@digitalcredentials/eddsa-rdfc-2022-cryptosuite": "^1.2.1-beta.6", "@digitalcredentials/issuer-registry-client": "^3.0.1-beta.1", "@digitalcredentials/jsonld-signatures": "^12.0.1", "@digitalcredentials/security-document-loader": "^6.0.1", diff --git a/src/Verify.ts b/src/Verify.ts index 56b71b0..ad52736 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -4,22 +4,24 @@ import { DataIntegrityProof } from '@digitalcredentials/data-integrity'; import { cryptosuite as eddsaRdfc2022CryptoSuite } from '@digitalcredentials/eddsa-rdfc-2022-cryptosuite'; import * as vc from '@digitalcredentials/vc'; import { securityLoader } from '@digitalcredentials/security-document-loader'; +import pkg from '@digitalcredentials/jsonld-signatures'; + import { getCredentialStatusChecker } from './credentialStatus.js'; import { addTrustedIssuersToVerificationResponse } from './issuerRegistries.js'; +import { extractCredentialsFrom } from './extractCredentialsFrom.js'; + import { PRESENTATION_ERROR, UNKNOWN_ERROR, INVALID_JSONLD, NO_VC_CONTEXT, INVALID_CREDENTIAL_ID, NO_PROOF, STATUS_LIST_NOT_FOUND, HTTP_ERROR_WITH_SIGNATURE_CHECK, DID_WEB_UNRESOLVED, INVALID_SIGNATURE } from './constants/errors.js'; import { SIGNATURE_INVALID, SIGNATURE_VALID, SIGNATURE_UNSIGNED, REVOCATION_STATUS_STEP_ID } from './constants/verificationSteps.js'; +import { ISSUER_DID_RESOLVES, NOT_FOUND_ERROR, VERIFICATION_ERROR } from './constants/external.js'; + import { Credential } from './types/credential.js'; import { VerificationResponse, VerificationStep, PresentationVerificationResponse, PresentationSignatureResult } from './types/result.js'; import { VerifiablePresentation } from './types/presentation.js'; -import { extractCredentialsFrom } from './extractCredentialsFrom.js'; - -import pkg from '@digitalcredentials/jsonld-signatures'; -import { ISSUER_DID_RESOLVES, NOT_FOUND_ERROR, VERIFICATION_ERROR } from './constants/external.js'; const { purposes } = pkg; const presentationPurpose = new purposes.AssertionProofPurpose(); diff --git a/src/test-fixtures/knownDidRegistries.ts b/src/test-fixtures/knownDidRegistries.ts new file mode 100644 index 0000000..515f591 --- /dev/null +++ b/src/test-fixtures/knownDidRegistries.ts @@ -0,0 +1,18 @@ +export const knownDIDRegistries : object = [ + { + name: 'DCC Pilot Registry', + url: 'https://digitalcredentials.github.io/issuer-registry/registry.json' + }, + { + name: 'DCC Sandbox Registry', + url: 'https://digitalcredentials.github.io/sandbox-registry/registry.json' + }, + { + name: 'DCC Community Registry', + url: 'https://digitalcredentials.github.io/community-registry/registry.json' + }, + { + name: 'DCC Registry', + url: 'https://digitalcredentials.github.io/dcc-registry/registry.json' + } + ] \ No newline at end of file diff --git a/test/Verify.general.spec.ts b/test/Verify.general.spec.ts index 7a5ba35..639d82f 100644 --- a/test/Verify.general.spec.ts +++ b/test/Verify.general.spec.ts @@ -5,7 +5,7 @@ import { getCredentialWithoutContext, getCredentialWithoutVCContext, } from '../src/test-fixtures/vc.js' -import { knownDIDRegistries } from '../.knownDidRegistries.js'; +import { knownDIDRegistries } from '../src/test-fixtures/knownDidRegistries.js'; import { getExpectedFatalResult } from '../src/test-fixtures/expectedResults.js'; diff --git a/test/Verify.presentation.spec.ts b/test/Verify.presentation.spec.ts index 63709f1..59207dd 100644 --- a/test/Verify.presentation.spec.ts +++ b/test/Verify.presentation.spec.ts @@ -21,7 +21,7 @@ import { } from '../src/test-fixtures/vc.js' -import { knownDIDRegistries } from '../.knownDidRegistries.js'; +import { knownDIDRegistries } from '../src/test-fixtures/knownDidRegistries.js'; import { getExpectedVerifiedResult, getExpectedUnverifiedResult, diff --git a/test/Verify.v1.spec.ts b/test/Verify.v1.spec.ts index dcf5355..fa1b8f4 100644 --- a/test/Verify.v1.spec.ts +++ b/test/Verify.v1.spec.ts @@ -13,7 +13,7 @@ import { getVCv1ExpiredWithValidStatus } from '../src/test-fixtures/vc.js' -import { knownDIDRegistries } from '../.knownDidRegistries.js'; +import { knownDIDRegistries } from '../src/test-fixtures/knownDidRegistries.js'; import { getExpectedVerifiedResult, getExpectedUnverifiedResult, diff --git a/test/Verify.v2.spec.ts b/test/Verify.v2.spec.ts index 75517cc..ec6c16c 100644 --- a/test/Verify.v2.spec.ts +++ b/test/Verify.v2.spec.ts @@ -19,7 +19,7 @@ import { getVCv2WithBadDidWebUrl } from '../src/test-fixtures/vc.js' -import { knownDIDRegistries } from '../.knownDidRegistries.js'; +import { knownDIDRegistries } from '../src/test-fixtures/knownDidRegistries.js'; import { getExpectedVerifiedResult, getExpectedUnverifiedResult, diff --git a/tsconfig.json b/tsconfig.json index 71fd4f1..3000add 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,7 +20,7 @@ "include": [ "src/**/*", ".eslintrc.cjs", - "karma.conf.js" - ], + "karma.conf.cjs" +, "src/test-fixtures/knownDidRegistries.ts" ], "exclude": ["node_modules", "dist", "test"] }