From 7ee131b1c646393459d5198e5e8805922e2d1f09 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Tue, 9 Dec 2025 14:44:15 -0500 Subject: [PATCH 1/8] wip --- test/Verify.experimentation.spec.ts | 10 +-- test/Verify.status.spec.ts | 127 ++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 5 deletions(-) create mode 100644 test/Verify.status.spec.ts diff --git a/test/Verify.experimentation.spec.ts b/test/Verify.experimentation.spec.ts index 19f162a..db853fd 100644 --- a/test/Verify.experimentation.spec.ts +++ b/test/Verify.experimentation.spec.ts @@ -176,17 +176,17 @@ const didKeyCredential = { describe('any test we like', () => { - it.skip('tests', async () => { + it.only('tests', async () => { // change this however you like to test things // const originalVC = await fetchVC('https://digitalcredentials.github.io/vc-test-fixtures/verifiableCredentials/v2/dataIntegrityProof/didKey/legacyRegistry-noStatus-notExpired-withSchema.json') const vc = testVC as any; // const vc = JSON.parse(JSON.stringify(originalVC)) // vc.proof = [vc.proof] - const result = await checkSchemas(vc) - // const result = await verifyCredential({ credential: didKeyCredential, knownDIDRegistries }) + // const result = await checkSchemas(vc) + const result = await verifyCredential({ credential: didKeyCredential, knownDIDRegistries }) console.log(JSON.stringify(result, null, 2)) - expect(result.results[0].result.errors).to.exist - expect(result.results[0].result.valid).to.be.false + // expect(result.results[0].result.errors).to.exist + // expect(result.results[0].result.valid).to.be.false }) }) diff --git a/test/Verify.status.spec.ts b/test/Verify.status.spec.ts new file mode 100644 index 0000000..bb052c4 --- /dev/null +++ b/test/Verify.status.spec.ts @@ -0,0 +1,127 @@ +import chai from 'chai' +import deepEqualInAnyOrder from 'deep-equal-in-any-order' +import { verifyCredential } from '../src/Verify.js' +import { Credential } from '../src/types/credential.js'; +import { knownDIDRegistries } from '../src/test-fixtures/knownDidRegistries.js'; +import { checkSchemas } from '../src/schemaCheck.js'; +import { SCHEMA_ENTRY_ID } from '../src/constants/verificationSteps.js'; +chai.use(deepEqualInAnyOrder); +const { expect } = chai; + +/* +Tests credential *schema* validation. +*/ + +describe('schema checks', () => { + + describe('schemaCheck.checkSchemas', () => { + it('validates as expected', async () => { + const vc = await fetchVC('https://digitalcredentials.github.io/vc-test-fixtures/verifiableCredentials/v2/dataIntegrityProof/didKey/legacyRegistry-noStatus-notExpired-withSchema.json') + const result = await checkSchemas(vc) + expect(result.results[0].result.errors).to.not.exist + expect(result.results[0].result.valid).to.be.true + }) + + it('fails for missing achievement id', async () => { + const originalVC = await fetchVC('https://digitalcredentials.github.io/vc-test-fixtures/verifiableCredentials/v2/dataIntegrityProof/didKey/legacyRegistry-noStatus-notExpired-withSchema.json') + const vc = JSON.parse(JSON.stringify(originalVC)); + delete vc.credentialSubject.achievement.id + const result = await checkSchemas(vc) + expect(result.results[0].result.errors).to.exist + expect(result.results[0].result.valid).to.be.false + }) + + it('returns NO_SCHEMA when no credentialSchema property or context', async () => { + const vc = await fetchVC('https://digitalcredentials.github.io/vc-test-fixtures/verifiableCredentials/v2/dataIntegrityProof/didKey/legacyRegistry-noStatus-noExpiry-minimal.json') + const result = await checkSchemas(vc) + expect(result).to.deep.equalInAnyOrder({ results: 'NO_SCHEMA' }) + }) + + it('returns NO_SCHEMA when ob context but no OB type', async () => { + const vc = await fetchVC('https://digitalcredentials.github.io/vc-test-fixtures/verifiableCredentials/v2/bothSignatureTypes/didkey/legacy-noStatus-noExpiry-nonOBWithOBContext.json') + const result = await checkSchemas(vc) + expect(result).to.deep.equalInAnyOrder({ results: 'NO_SCHEMA' }) + }) + + it('fails for obv3 guessed by context with missing achievement id', async () => { + const vc = await fetchVC('https://digitalcredentials.github.io/vc-test-fixtures/verifiableCredentials/v2/ed25519/didKey/legacy-noStatus-noExpiry.json') + // @ts-ignore: Property does not exist + delete vc.credentialSubject.achievement!.id + const result = await checkSchemas(vc) + expect(result.results[0].result.errors).to.exist + expect(result.results[0].result.valid).to.be.false + }) + + it('returns valid for obv3 v2 guessed by context', async () => { + const vc = await fetchVC('https://digitalcredentials.github.io/vc-test-fixtures/verifiableCredentials/v2/ed25519/didKey/legacy-noStatus-noExpiry.json') + const result = await checkSchemas(vc) + expect(result.results[0].result.errors).to.not.exist + expect(result.results[0].result.valid).to.be.true + }) + + it('returns valid for obv3 v1 guessed by context', async () => { + const vc = await fetchVC('https://digitalcredentials.github.io/vc-test-fixtures/verifiableCredentials/v1/bothSignatureTypes/didKey/legacy-noStatus-noExpiry-basicOBv3.json') + const result = await checkSchemas(vc) + expect(result.results[0].result.errors).to.not.exist + expect(result.results[0].result.valid).to.be.true + }) + + it('returns error if bad schema url', async () => { + const vc = await fetchVC('https://digitalcredentials.github.io/vc-test-fixtures/verifiableCredentials/v2/dataIntegrityProof/didKey/legacyRegistry-noStatus-notExpired-withSchema.json') + // change the good schema url to a non-existant url + // @ts-ignore: Object is possibly 'null'. + vc.credentialSchema[0].id = 'https://purl.imsglobal.org/spec/ob/v3p0/schema/json/non-existant_schema.json' + const result = await checkSchemas(vc) + expect(result).to.deep.equalInAnyOrder({ results: 'INVALID_SCHEMA - possibly not a valid url' }) + }) + + it.skip('returns valid for obv3 v2 endorsement guessed by context', async () => { + const vc = await fetchVC('https://digitalcredentials.github.io/vc-test-fixtures/verifiableCredentials/v2/bothSignatureTypes/didkey/legacy-noStatus-notExpired-endorsement.json') + const result = await checkSchemas(vc) + expect(result.results[0].result.errors).to.not.exist + expect(result.results[0].result.valid).to.be.true + }) + + it.skip('returns valid for obv3 v2 endorsement with credentialSchema property', async () => { + const vc = await fetchVC('https://digitalcredentials.github.io/vc-test-fixtures/verifiableCredentials/v2/bothSignatureTypes/didkey/legacy-noStatus-notExpired-endorsement-withSchema.json') + const result = await checkSchemas(vc) + expect(result.results[0].result.errors).to.not.exist + expect(result.results[0].result.valid).to.be.true + }) + + }) + + describe('schema results for verification call', () => { + it('returns positive result', async () => { + const credential = await fetchVC('https://digitalcredentials.github.io/vc-test-fixtures/verifiableCredentials/v2/ed25519/didKey/legacy-noStatus-noExpiry.json') + const result = await verifyCredential({ credential, knownDIDRegistries }) + expect(result.additionalInformation![0].id).to.equal(SCHEMA_ENTRY_ID); + expect(result.additionalInformation![0].results![0].result.valid).to.be.true + expect(result.additionalInformation![0].results![0].source).to.equal("Assumed based on vc.type: 'OpenBadgeCredential' and vc version: 'version 2'") + }) + + it('returns error for extra term', async () => { + const credential = await fetchVC("https://digitalcredentials.github.io/vc-test-fixtures/verifiableCredentials/v2/dataIntegrityProof/didKey/twoOIDF-revoked-notExpired-badSchema.json"); + const result = await verifyCredential({ credential, knownDIDRegistries }) + expect(result.additionalInformation![0].id).to.equal(SCHEMA_ENTRY_ID) + expect(result.additionalInformation![0].results![0].result.valid).to.be.false + expect(result.additionalInformation![0].results![0].source).to.equal("Assumed based on vc.type: 'OpenBadgeCredential' and vc version: 'version 2'") + expect(result.additionalInformation![0].results![0].result.errors![0]).to.deep.equalInAnyOrder({ + "instancePath": "", + "schemaPath": "#/required", + "keyword": "required", + "params": { + "missingProperty": "validFrom" + }, + "message": "must have required property 'validFrom'" + }) + }) + }) + +}) + +async function fetchVC(url: string): Promise { + const response = await fetch(url); + const data = await response.json(); + return data as Credential +} \ No newline at end of file From 0823dd5c69109c4decb53ae5c3f6e2e9120ed6b3 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Tue, 9 Dec 2025 16:54:50 -0500 Subject: [PATCH 2/8] set up test --- test/Verify.experimentation.spec.ts | 2 +- test/Verify.status.spec.ts | 135 +++++++--------------------- 2 files changed, 35 insertions(+), 102 deletions(-) diff --git a/test/Verify.experimentation.spec.ts b/test/Verify.experimentation.spec.ts index db853fd..57eedef 100644 --- a/test/Verify.experimentation.spec.ts +++ b/test/Verify.experimentation.spec.ts @@ -176,7 +176,7 @@ const didKeyCredential = { describe('any test we like', () => { - it.only('tests', async () => { + it.skip('tests', async () => { // change this however you like to test things // const originalVC = await fetchVC('https://digitalcredentials.github.io/vc-test-fixtures/verifiableCredentials/v2/dataIntegrityProof/didKey/legacyRegistry-noStatus-notExpired-withSchema.json') const vc = testVC as any; diff --git a/test/Verify.status.spec.ts b/test/Verify.status.spec.ts index bb052c4..769c269 100644 --- a/test/Verify.status.spec.ts +++ b/test/Verify.status.spec.ts @@ -3,119 +3,52 @@ import deepEqualInAnyOrder from 'deep-equal-in-any-order' import { verifyCredential } from '../src/Verify.js' import { Credential } from '../src/types/credential.js'; import { knownDIDRegistries } from '../src/test-fixtures/knownDidRegistries.js'; -import { checkSchemas } from '../src/schemaCheck.js'; import { SCHEMA_ENTRY_ID } from '../src/constants/verificationSteps.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'; +import { getVCv2DoubleSigWithBadStatusUrl } from '../src/test-fixtures/vc.js'; +import { getExpectedVerifiedResult } from '../src/test-fixtures/expectedResults.js'; + chai.use(deepEqualInAnyOrder); const { expect } = chai; /* -Tests credential *schema* validation. -*/ - -describe('schema checks', () => { - - describe('schemaCheck.checkSchemas', () => { - it('validates as expected', async () => { - const vc = await fetchVC('https://digitalcredentials.github.io/vc-test-fixtures/verifiableCredentials/v2/dataIntegrityProof/didKey/legacyRegistry-noStatus-notExpired-withSchema.json') - const result = await checkSchemas(vc) - expect(result.results[0].result.errors).to.not.exist - expect(result.results[0].result.valid).to.be.true - }) - - it('fails for missing achievement id', async () => { - const originalVC = await fetchVC('https://digitalcredentials.github.io/vc-test-fixtures/verifiableCredentials/v2/dataIntegrityProof/didKey/legacyRegistry-noStatus-notExpired-withSchema.json') - const vc = JSON.parse(JSON.stringify(originalVC)); - delete vc.credentialSubject.achievement.id - const result = await checkSchemas(vc) - expect(result.results[0].result.errors).to.exist - expect(result.results[0].result.valid).to.be.false - }) - - it('returns NO_SCHEMA when no credentialSchema property or context', async () => { - const vc = await fetchVC('https://digitalcredentials.github.io/vc-test-fixtures/verifiableCredentials/v2/dataIntegrityProof/didKey/legacyRegistry-noStatus-noExpiry-minimal.json') - const result = await checkSchemas(vc) - expect(result).to.deep.equalInAnyOrder({ results: 'NO_SCHEMA' }) - }) - it('returns NO_SCHEMA when ob context but no OB type', async () => { - const vc = await fetchVC('https://digitalcredentials.github.io/vc-test-fixtures/verifiableCredentials/v2/bothSignatureTypes/didkey/legacy-noStatus-noExpiry-nonOBWithOBContext.json') - const result = await checkSchemas(vc) - expect(result).to.deep.equalInAnyOrder({ results: 'NO_SCHEMA' }) - }) +Tests credential status validation. - it('fails for obv3 guessed by context with missing achievement id', async () => { - const vc = await fetchVC('https://digitalcredentials.github.io/vc-test-fixtures/verifiableCredentials/v2/ed25519/didKey/legacy-noStatus-noExpiry.json') - // @ts-ignore: Property does not exist - delete vc.credentialSubject.achievement!.id - const result = await checkSchemas(vc) - expect(result.results[0].result.errors).to.exist - expect(result.results[0].result.valid).to.be.false - }) - - it('returns valid for obv3 v2 guessed by context', async () => { - const vc = await fetchVC('https://digitalcredentials.github.io/vc-test-fixtures/verifiableCredentials/v2/ed25519/didKey/legacy-noStatus-noExpiry.json') - const result = await checkSchemas(vc) - expect(result.results[0].result.errors).to.not.exist - expect(result.results[0].result.valid).to.be.true - }) - - it('returns valid for obv3 v1 guessed by context', async () => { - const vc = await fetchVC('https://digitalcredentials.github.io/vc-test-fixtures/verifiableCredentials/v1/bothSignatureTypes/didKey/legacy-noStatus-noExpiry-basicOBv3.json') - const result = await checkSchemas(vc) - expect(result.results[0].result.errors).to.not.exist - expect(result.results[0].result.valid).to.be.true - }) - - it('returns error if bad schema url', async () => { - const vc = await fetchVC('https://digitalcredentials.github.io/vc-test-fixtures/verifiableCredentials/v2/dataIntegrityProof/didKey/legacyRegistry-noStatus-notExpired-withSchema.json') - // change the good schema url to a non-existant url - // @ts-ignore: Object is possibly 'null'. - vc.credentialSchema[0].id = 'https://purl.imsglobal.org/spec/ob/v3p0/schema/json/non-existant_schema.json' - const result = await checkSchemas(vc) - expect(result).to.deep.equalInAnyOrder({ results: 'INVALID_SCHEMA - possibly not a valid url' }) - }) - - it.skip('returns valid for obv3 v2 endorsement guessed by context', async () => { - const vc = await fetchVC('https://digitalcredentials.github.io/vc-test-fixtures/verifiableCredentials/v2/bothSignatureTypes/didkey/legacy-noStatus-notExpired-endorsement.json') - const result = await checkSchemas(vc) - expect(result.results[0].result.errors).to.not.exist - expect(result.results[0].result.valid).to.be.true - }) - - it.skip('returns valid for obv3 v2 endorsement with credentialSchema property', async () => { - const vc = await fetchVC('https://digitalcredentials.github.io/vc-test-fixtures/verifiableCredentials/v2/bothSignatureTypes/didkey/legacy-noStatus-notExpired-endorsement-withSchema.json') - const result = await checkSchemas(vc) - expect(result.results[0].result.errors).to.not.exist - expect(result.results[0].result.valid).to.be.true - }) - - }) +*/ - describe('schema results for verification call', () => { - it('returns positive result', async () => { - const credential = await fetchVC('https://digitalcredentials.github.io/vc-test-fixtures/verifiableCredentials/v2/ed25519/didKey/legacy-noStatus-noExpiry.json') +describe('status checks', () => { + + 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_STEP_ID, + "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, knownDIDRegistries }) - expect(result.additionalInformation![0].id).to.equal(SCHEMA_ENTRY_ID); - expect(result.additionalInformation![0].results![0].result.valid).to.be.true - expect(result.additionalInformation![0].results![0].source).to.equal("Assumed based on vc.type: 'OpenBadgeCredential' and vc version: 'version 2'") + expect(result).to.have.property('log').that.deep.equalInAnyOrder(expectedResult.log); + expect(result).to.have.property("credential").that.equals(credential) + }) + }) + - it('returns error for extra term', async () => { - const credential = await fetchVC("https://digitalcredentials.github.io/vc-test-fixtures/verifiableCredentials/v2/dataIntegrityProof/didKey/twoOIDF-revoked-notExpired-badSchema.json"); + describe('verification with', () => { + it.only('expired status list', async () => { + const credential = await fetchVC('https://digitalcredentials.github.io/vc-test-fixtures/verifiableCredentials/v2/ed25519/didKey/legacy-expiredStatus-noExpiry.json') const result = await verifyCredential({ credential, knownDIDRegistries }) - expect(result.additionalInformation![0].id).to.equal(SCHEMA_ENTRY_ID) - expect(result.additionalInformation![0].results![0].result.valid).to.be.false - expect(result.additionalInformation![0].results![0].source).to.equal("Assumed based on vc.type: 'OpenBadgeCredential' and vc version: 'version 2'") - expect(result.additionalInformation![0].results![0].result.errors![0]).to.deep.equalInAnyOrder({ - "instancePath": "", - "schemaPath": "#/required", - "keyword": "required", - "params": { - "missingProperty": "validFrom" - }, - "message": "must have required property 'validFrom'" - }) - }) + console.log(result) + // THIS SHOULD BE RETURNING AN ERROR FOR THE REVOCATION STEP!!!!!! + // expect(result).to.have.property('log').that.deep.equalInAnyOrder(expectedResult.log); + expect(result).to.have.property("credential").that.equals(credential) }) }) }) From 1edef816748211f42a229c589afe7cdcc25576b2 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Wed, 10 Dec 2025 15:19:08 -0500 Subject: [PATCH 3/8] add handling for expired status list --- src/Verify.ts | 61 +++++++++++++++++++---------- src/constants/errors.ts | 4 +- src/constants/external.ts | 1 + test/Verify.experimentation.spec.ts | 7 ++-- test/Verify.status.spec.ts | 21 ++++++++-- 5 files changed, 65 insertions(+), 29 deletions(-) diff --git a/src/Verify.ts b/src/Verify.ts index 172e907..b17cbd9 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -11,13 +11,16 @@ import { addTrustedIssuersToVerificationResponse } from './issuerRegistries.js'; import { addSchemaCheckToVerificationResponse } from './schemaCheck.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 { + 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, + STATUS_LIST_EXPIRED, + UNKNOWN_STATUS_LIST_ERROR +} 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 { EXPIRED_ERROR, 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'; @@ -80,7 +83,7 @@ export async function verifyPresentation({ presentation, challenge = 'meaningles } -export async function verifyCredential({ credential, knownDIDRegistries}: { credential: Credential, knownDIDRegistries: object}): Promise { +export async function verifyCredential({ credential, knownDIDRegistries }: { credential: Credential, knownDIDRegistries: object }): Promise { try { // null unless credential has a status const statusChecker = getCredentialStatusChecker(credential) @@ -92,6 +95,7 @@ export async function verifyCredential({ credential, knownDIDRegistries}: { cred checkStatus: statusChecker, verifyMatchingIssuers: false }); + // console.log(JSON.stringify(verificationResponse,null,2)) const adjustedResponse = await transformResponse(verificationResponse, credential, knownDIDRegistries) return adjustedResponse; } catch (error) { @@ -107,7 +111,7 @@ async function transformResponse(verificationResponse: any, credential: Credenti return fatalCredentialError } - handleAnyStatusError({ verificationResponse, statusResult: verificationResponse.statusResult }); + handleAnyStatusError({ verificationResponse }); const fatalSignatureError = handleAnySignatureError({ verificationResponse, credential }) if (fatalSignatureError) { @@ -117,7 +121,7 @@ async function transformResponse(verificationResponse: any, credential: Credenti const { issuer } = credential await addTrustedIssuersToVerificationResponse({ verificationResponse, knownDIDRegistries, issuer }) - await addSchemaCheckToVerificationResponse({verificationResponse, credential}) + await addSchemaCheckToVerificationResponse({ verificationResponse, credential }) // remove things we don't need from the result or that are duplicated elsewhere delete verificationResponse.results @@ -171,21 +175,36 @@ function handleAnyFatalCredentialErrors(credential: Credential): VerificationRes return null } -function handleAnyStatusError({ verificationResponse }: { - verificationResponse: any, - statusResult: any -}): void { +function handleAnyStatusError({ verificationResponse }: { verificationResponse: any}): void { const statusResult = verificationResponse.statusResult - if (statusResult?.error?.cause?.message?.startsWith(NOT_FOUND_ERROR)) { + // console.log("STATUS RESULT:") + // console.log(statusResult?.error?.cause?.message) + if (statusResult?.error) { + + let error + if (statusResult?.error?.cause?.message?.startsWith(NOT_FOUND_ERROR)) { + error = { + name: STATUS_LIST_NOT_FOUND, + message: statusResult.error.cause.message + } + } else if (statusResult?.error?.cause?.message?.includes(EXPIRED_ERROR)) { + error = { + name: STATUS_LIST_EXPIRED, + message: "The status list verifiable credential has expired." + } + } else { + error = { + name: UNKNOWN_STATUS_LIST_ERROR, + message: statusResult.error.cause.message ?? "The status list couldnt' be verified." + } + } const statusStep = { "id": REVOCATION_STATUS_STEP_ID, - "error": { - name: STATUS_LIST_NOT_FOUND, - message: statusResult.error.cause.message - } + error }; (verificationResponse.log ??= []).push(statusStep) } + } function handleAnySignatureError({ verificationResponse, credential }: { verificationResponse: any, credential: Credential }): null | VerificationResponse { @@ -202,7 +221,7 @@ function handleAnySignatureError({ verificationResponse, credential }: { verific const httpError = verificationResponse.error.errors.find((error: any) => error.name === 'HTTPError') // or a json-ld parsing error const jsonLdError = verificationResponse.error.errors.find((error: any) => error.name === 'jsonld.ValidationError') - + if (httpError) { fatalErrorMessage = 'An http error prevented the signature check.' errorName = HTTP_ERROR_WITH_SIGNATURE_CHECK @@ -217,13 +236,13 @@ function handleAnySignatureError({ verificationResponse, credential }: { verific } } } else if (jsonLdError) { - const errors = verificationResponse.error.errors.map((error:any)=>{ + const errors = verificationResponse.error.errors.map((error: any) => { // need to rename the stack property to stackTrace to fit with old error structure error.stackTrace = error.stack; delete error.stack; return error }) - return {credential, errors} + return { credential, errors } } else { // not an http or json-ld error, so likely bad signature fatalErrorMessage = 'The signature is not valid.' diff --git a/src/constants/errors.ts b/src/constants/errors.ts index 615f289..f1e2ac5 100644 --- a/src/constants/errors.ts +++ b/src/constants/errors.ts @@ -4,7 +4,9 @@ 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' +export const STATUS_LIST_NOT_FOUND = 'status_list_not_found' +export const STATUS_LIST_EXPIRED = 'status_list_expired' +export const UNKNOWN_STATUS_LIST_ERROR = 'status_list_error' diff --git a/src/constants/external.ts b/src/constants/external.ts index 7476b46..87d77af 100644 --- a/src/constants/external.ts +++ b/src/constants/external.ts @@ -1,4 +1,5 @@ // constants used by @digitalcredentials.vc library export const ISSUER_DID_RESOLVES = 'issuer_did_resolves' export const NOT_FOUND_ERROR = 'NotFoundError' +export const EXPIRED_ERROR = 'is after "validUntil"' export const VERIFICATION_ERROR = 'VerificationError' \ No newline at end of file diff --git a/test/Verify.experimentation.spec.ts b/test/Verify.experimentation.spec.ts index 57eedef..64c0e6e 100644 --- a/test/Verify.experimentation.spec.ts +++ b/test/Verify.experimentation.spec.ts @@ -179,12 +179,13 @@ describe('any test we like', () => { it.skip('tests', async () => { // change this however you like to test things // const originalVC = await fetchVC('https://digitalcredentials.github.io/vc-test-fixtures/verifiableCredentials/v2/dataIntegrityProof/didKey/legacyRegistry-noStatus-notExpired-withSchema.json') - const vc = testVC as any; + const notYetValidVC = await fetchVC('https://digitalcredentials.github.io/vc-test-fixtures/verifiableCredentials/v2/dataIntegrityProof/didKey/oidf-noStatus-notExpired-notYetValid.json') + // const vc = testVC as any; // const vc = JSON.parse(JSON.stringify(originalVC)) // vc.proof = [vc.proof] // const result = await checkSchemas(vc) - const result = await verifyCredential({ credential: didKeyCredential, knownDIDRegistries }) - console.log(JSON.stringify(result, null, 2)) + const result = await verifyCredential({ credential: notYetValidVC, knownDIDRegistries }) + // console.log(JSON.stringify(result, null, 2)) // expect(result.results[0].result.errors).to.exist // expect(result.results[0].result.valid).to.be.false }) diff --git a/test/Verify.status.spec.ts b/test/Verify.status.spec.ts index 769c269..e69261e 100644 --- a/test/Verify.status.spec.ts +++ b/test/Verify.status.spec.ts @@ -6,7 +6,7 @@ import { knownDIDRegistries } from '../src/test-fixtures/knownDidRegistries.js'; import { SCHEMA_ENTRY_ID } from '../src/constants/verificationSteps.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'; +import { DID_WEB_UNRESOLVED, INVALID_CREDENTIAL_ID, INVALID_SIGNATURE, NO_PROOF, STATUS_LIST_NOT_FOUND, STATUS_LIST_EXPIRED } from '../src/constants/errors.js'; import { getVCv2DoubleSigWithBadStatusUrl } from '../src/test-fixtures/vc.js'; import { getExpectedVerifiedResult } from '../src/test-fixtures/expectedResults.js'; @@ -42,13 +42,26 @@ describe('status checks', () => { describe('verification with', () => { - it.only('expired status list', async () => { + it('expired status list', async () => { const credential = await fetchVC('https://digitalcredentials.github.io/vc-test-fixtures/verifiableCredentials/v2/ed25519/didKey/legacy-expiredStatus-noExpiry.json') + const expectedResult = getExpectedVerifiedResult({ credential, withStatus: false }) + expectedResult.log?.push( + { + "id": REVOCATION_STATUS_STEP_ID, + "error": { + "name": STATUS_LIST_EXPIRED, + "message": "The status list verifiable credential has expired." + } + }) const result = await verifyCredential({ credential, knownDIDRegistries }) + expect(result).to.have.property('log').that.deep.equalInAnyOrder(expectedResult.log); + expect(result).to.have.property("credential").that.equals(credential) + + console.log(result) // THIS SHOULD BE RETURNING AN ERROR FOR THE REVOCATION STEP!!!!!! - // expect(result).to.have.property('log').that.deep.equalInAnyOrder(expectedResult.log); - expect(result).to.have.property("credential").that.equals(credential) }) + // expect(result).to.have.property('log').that.deep.equalInAnyOrder(expectedResult.log); + }) }) }) From d57fd897e471328ab8fed62457c2a93374b7a3fb Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Thu, 11 Dec 2025 07:03:21 -0500 Subject: [PATCH 4/8] add check for bad signature on status list --- src/Verify.ts | 47 +++++++++++++++++++++----------------- src/constants/errors.ts | 2 ++ src/constants/external.ts | 3 ++- test/Verify.status.spec.ts | 30 ++++++++++++++---------- 4 files changed, 48 insertions(+), 34 deletions(-) diff --git a/src/Verify.ts b/src/Verify.ts index b17cbd9..12d0728 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -17,10 +17,11 @@ import { HTTP_ERROR_WITH_SIGNATURE_CHECK, DID_WEB_UNRESOLVED, INVALID_SIGNATURE, STATUS_LIST_EXPIRED, - UNKNOWN_STATUS_LIST_ERROR + UNKNOWN_STATUS_LIST_ERROR, + STATUS_LIST_SIGNATURE_ERROR } from './constants/errors.js'; import { SIGNATURE_INVALID, SIGNATURE_VALID, SIGNATURE_UNSIGNED, REVOCATION_STATUS_STEP_ID } from './constants/verificationSteps.js'; -import { EXPIRED_ERROR, ISSUER_DID_RESOLVES, NOT_FOUND_ERROR, VERIFICATION_ERROR } from './constants/external.js'; +import { EXPIRED_ERROR, ISSUER_DID_RESOLVES, NOT_FOUND_ERROR, STATUS_SIGNATURE_ERROR, VERIFICATION_ERROR } from './constants/external.js'; import { Credential } from './types/credential.js'; import { VerificationResponse, VerificationStep, PresentationVerificationResponse, PresentationSignatureResult } from './types/result.js'; @@ -175,29 +176,33 @@ function handleAnyFatalCredentialErrors(credential: Credential): VerificationRes return null } -function handleAnyStatusError({ verificationResponse }: { verificationResponse: any}): void { +function handleAnyStatusError({ verificationResponse }: { verificationResponse: any }): void { const statusResult = verificationResponse.statusResult - // console.log("STATUS RESULT:") - // console.log(statusResult?.error?.cause?.message) + // console.log("STATUS RESULT:") + // console.log(statusResult?.error?.cause?.message) if (statusResult?.error) { - let error - if (statusResult?.error?.cause?.message?.startsWith(NOT_FOUND_ERROR)) { - error = { - name: STATUS_LIST_NOT_FOUND, - message: statusResult.error.cause.message - } + if (statusResult?.error?.cause?.message?.startsWith(NOT_FOUND_ERROR)) { + error = { + name: STATUS_LIST_NOT_FOUND, + message: statusResult.error.cause.message + } } else if (statusResult?.error?.cause?.message?.includes(EXPIRED_ERROR)) { - error = { - name: STATUS_LIST_EXPIRED, - message: "The status list verifiable credential has expired." - } - } else { - error = { - name: UNKNOWN_STATUS_LIST_ERROR, - message: statusResult.error.cause.message ?? "The status list couldnt' be verified." - } - } + error = { + name: STATUS_LIST_EXPIRED, + message: "The status list verifiable credential has expired." + } + } else if (statusResult?.error?.cause?.message?.startsWith(STATUS_SIGNATURE_ERROR)) { + error = { + name: STATUS_LIST_SIGNATURE_ERROR, + message: "The signature on the status list is invalid." + } + } else { + error = { + name: UNKNOWN_STATUS_LIST_ERROR, + message: statusResult.error.cause.message ?? "The status list couldn't be verified." + } + } const statusStep = { "id": REVOCATION_STATUS_STEP_ID, error diff --git a/src/constants/errors.ts b/src/constants/errors.ts index f1e2ac5..e5af5aa 100644 --- a/src/constants/errors.ts +++ b/src/constants/errors.ts @@ -7,6 +7,8 @@ export const NO_PROOF = 'no_proof' 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' + export const STATUS_LIST_NOT_FOUND = 'status_list_not_found' export const STATUS_LIST_EXPIRED = 'status_list_expired' export const UNKNOWN_STATUS_LIST_ERROR = 'status_list_error' +export const STATUS_LIST_SIGNATURE_ERROR = 'status_list_signature_error' diff --git a/src/constants/external.ts b/src/constants/external.ts index 87d77af..ecd6585 100644 --- a/src/constants/external.ts +++ b/src/constants/external.ts @@ -2,4 +2,5 @@ export const ISSUER_DID_RESOLVES = 'issuer_did_resolves' export const NOT_FOUND_ERROR = 'NotFoundError' export const EXPIRED_ERROR = 'is after "validUntil"' -export const VERIFICATION_ERROR = 'VerificationError' \ No newline at end of file +export const VERIFICATION_ERROR = 'VerificationError' +export const STATUS_SIGNATURE_ERROR = 'Verification error' \ No newline at end of file diff --git a/test/Verify.status.spec.ts b/test/Verify.status.spec.ts index e69261e..2cf1fc1 100644 --- a/test/Verify.status.spec.ts +++ b/test/Verify.status.spec.ts @@ -6,7 +6,7 @@ import { knownDIDRegistries } from '../src/test-fixtures/knownDidRegistries.js'; import { SCHEMA_ENTRY_ID } from '../src/constants/verificationSteps.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, STATUS_LIST_EXPIRED } from '../src/constants/errors.js'; +import { DID_WEB_UNRESOLVED, INVALID_CREDENTIAL_ID, INVALID_SIGNATURE, NO_PROOF, STATUS_LIST_NOT_FOUND, STATUS_LIST_EXPIRED, STATUS_LIST_SIGNATURE_ERROR } from '../src/constants/errors.js'; import { getVCv2DoubleSigWithBadStatusUrl } from '../src/test-fixtures/vc.js'; import { getExpectedVerifiedResult } from '../src/test-fixtures/expectedResults.js'; @@ -37,12 +37,9 @@ describe('status checks', () => { expect(result).to.have.property('log').that.deep.equalInAnyOrder(expectedResult.log); expect(result).to.have.property("credential").that.equals(credential) }) - }) - - describe('verification with', () => { - it('expired status list', async () => { + it('when status list has expired', async () => { const credential = await fetchVC('https://digitalcredentials.github.io/vc-test-fixtures/verifiableCredentials/v2/ed25519/didKey/legacy-expiredStatus-noExpiry.json') const expectedResult = getExpectedVerifiedResult({ credential, withStatus: false }) expectedResult.log?.push( @@ -56,14 +53,23 @@ describe('status checks', () => { const result = await verifyCredential({ credential, knownDIDRegistries }) expect(result).to.have.property('log').that.deep.equalInAnyOrder(expectedResult.log); expect(result).to.have.property("credential").that.equals(credential) - - - console.log(result) - // THIS SHOULD BE RETURNING AN ERROR FOR THE REVOCATION STEP!!!!!! - // expect(result).to.have.property('log').that.deep.equalInAnyOrder(expectedResult.log); - }) - }) + }) + it('when status list has been tampered with', async () => { + const credential = await fetchVC('https://digitalcredentials.github.io/vc-test-fixtures/verifiableCredentials/v2/ed25519/didKey/legacy-tamperedStatus-noExpiry.json') + const expectedResult = getExpectedVerifiedResult({ credential, withStatus: false }) + expectedResult.log?.push( + { + "id": REVOCATION_STATUS_STEP_ID, + "error": { + "name": STATUS_LIST_SIGNATURE_ERROR, + "message": "The signature on the status list is invalid." + } + }) + const result = await verifyCredential({ credential, knownDIDRegistries }) + expect(result).to.have.property('log').that.deep.equalInAnyOrder(expectedResult.log); + expect(result).to.have.property("credential").that.equals(credential) + }) }) async function fetchVC(url: string): Promise { From 8222e043097dd9db6d0a0a3de33e54ad9e70e763 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Thu, 11 Dec 2025 09:02:09 -0500 Subject: [PATCH 5/8] add handling for status type error --- src/Verify.ts | 13 +++++++++---- src/constants/errors.ts | 2 ++ src/constants/external.ts | 4 +++- test/Verify.status.spec.ts | 26 +++++++++++++++++++++----- 4 files changed, 35 insertions(+), 10 deletions(-) diff --git a/src/Verify.ts b/src/Verify.ts index 12d0728..c20521e 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -18,10 +18,12 @@ import { INVALID_SIGNATURE, STATUS_LIST_EXPIRED, UNKNOWN_STATUS_LIST_ERROR, - STATUS_LIST_SIGNATURE_ERROR + STATUS_LIST_SIGNATURE_ERROR, + STATUS_LIST_NOT_YET_VALID_ERROR, + STATUS_LIST_TYPE_ERROR } from './constants/errors.js'; import { SIGNATURE_INVALID, SIGNATURE_VALID, SIGNATURE_UNSIGNED, REVOCATION_STATUS_STEP_ID } from './constants/verificationSteps.js'; -import { EXPIRED_ERROR, ISSUER_DID_RESOLVES, NOT_FOUND_ERROR, STATUS_SIGNATURE_ERROR, VERIFICATION_ERROR } from './constants/external.js'; +import { EXPIRED_ERROR, ISSUER_DID_RESOLVES, NOT_FOUND_ERROR, STATUS_NOT_YET_VALID_ERROR, STATUS_SIGNATURE_ERROR, STATUS_TYPE_ERROR, VERIFICATION_ERROR } from './constants/external.js'; import { Credential } from './types/credential.js'; import { VerificationResponse, VerificationStep, PresentationVerificationResponse, PresentationSignatureResult } from './types/result.js'; @@ -178,8 +180,6 @@ function handleAnyFatalCredentialErrors(credential: Credential): VerificationRes function handleAnyStatusError({ verificationResponse }: { verificationResponse: any }): void { const statusResult = verificationResponse.statusResult - // console.log("STATUS RESULT:") - // console.log(statusResult?.error?.cause?.message) if (statusResult?.error) { let error if (statusResult?.error?.cause?.message?.startsWith(NOT_FOUND_ERROR)) { @@ -197,6 +197,11 @@ function handleAnyStatusError({ verificationResponse }: { verificationResponse: name: STATUS_LIST_SIGNATURE_ERROR, message: "The signature on the status list is invalid." } + } else if (statusResult?.error?.message?.startsWith(STATUS_TYPE_ERROR)) { + error = { + name: STATUS_LIST_TYPE_ERROR, + message: 'Status list credential type must include "BitstringStatusListCredential".' + } } else { error = { name: UNKNOWN_STATUS_LIST_ERROR, diff --git a/src/constants/errors.ts b/src/constants/errors.ts index e5af5aa..4e85d16 100644 --- a/src/constants/errors.ts +++ b/src/constants/errors.ts @@ -12,3 +12,5 @@ export const STATUS_LIST_NOT_FOUND = 'status_list_not_found' export const STATUS_LIST_EXPIRED = 'status_list_expired' export const UNKNOWN_STATUS_LIST_ERROR = 'status_list_error' export const STATUS_LIST_SIGNATURE_ERROR = 'status_list_signature_error' +export const STATUS_LIST_TYPE_ERROR = 'status_list_type_error' +export const STATUS_LIST_NOT_YET_VALID_ERROR = 'status_list_not_yet_valid' \ No newline at end of file diff --git a/src/constants/external.ts b/src/constants/external.ts index ecd6585..8e56324 100644 --- a/src/constants/external.ts +++ b/src/constants/external.ts @@ -3,4 +3,6 @@ export const ISSUER_DID_RESOLVES = 'issuer_did_resolves' export const NOT_FOUND_ERROR = 'NotFoundError' export const EXPIRED_ERROR = 'is after "validUntil"' export const VERIFICATION_ERROR = 'VerificationError' -export const STATUS_SIGNATURE_ERROR = 'Verification error' \ No newline at end of file +export const STATUS_SIGNATURE_ERROR = 'Verification error' +export const STATUS_TYPE_ERROR = 'Status list credential type must include "BitstringStatusListCredential".' +export const STATUS_NOT_YET_VALID_ERROR = '' \ No newline at end of file diff --git a/test/Verify.status.spec.ts b/test/Verify.status.spec.ts index 2cf1fc1..81565c6 100644 --- a/test/Verify.status.spec.ts +++ b/test/Verify.status.spec.ts @@ -6,7 +6,7 @@ import { knownDIDRegistries } from '../src/test-fixtures/knownDidRegistries.js'; import { SCHEMA_ENTRY_ID } from '../src/constants/verificationSteps.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, STATUS_LIST_EXPIRED, STATUS_LIST_SIGNATURE_ERROR } from '../src/constants/errors.js'; +import { DID_WEB_UNRESOLVED, INVALID_CREDENTIAL_ID, INVALID_SIGNATURE, NO_PROOF, STATUS_LIST_NOT_FOUND, STATUS_LIST_EXPIRED, STATUS_LIST_SIGNATURE_ERROR, STATUS_LIST_TYPE_ERROR } from '../src/constants/errors.js'; import { getVCv2DoubleSigWithBadStatusUrl } from '../src/test-fixtures/vc.js'; import { getExpectedVerifiedResult } from '../src/test-fixtures/expectedResults.js'; @@ -21,8 +21,8 @@ Tests credential status validation. describe('status checks', () => { - describe('returns log error', () => { - it('when statuslist url is unreachable', async () => { + describe('returns log error when status list', () => { + it('url is unreachable', async () => { const credential: any = getVCv2DoubleSigWithBadStatusUrl() const expectedResult = getExpectedVerifiedResult({ credential, withStatus: false }) expectedResult.log?.push( @@ -39,7 +39,7 @@ describe('status checks', () => { }) }) - it('when status list has expired', async () => { + it('has expired', async () => { const credential = await fetchVC('https://digitalcredentials.github.io/vc-test-fixtures/verifiableCredentials/v2/ed25519/didKey/legacy-expiredStatus-noExpiry.json') const expectedResult = getExpectedVerifiedResult({ credential, withStatus: false }) expectedResult.log?.push( @@ -55,7 +55,7 @@ describe('status checks', () => { expect(result).to.have.property("credential").that.equals(credential) }) - it('when status list has been tampered with', async () => { + it('has been tampered with', async () => { const credential = await fetchVC('https://digitalcredentials.github.io/vc-test-fixtures/verifiableCredentials/v2/ed25519/didKey/legacy-tamperedStatus-noExpiry.json') const expectedResult = getExpectedVerifiedResult({ credential, withStatus: false }) expectedResult.log?.push( @@ -70,6 +70,22 @@ describe('status checks', () => { expect(result).to.have.property('log').that.deep.equalInAnyOrder(expectedResult.log); expect(result).to.have.property("credential").that.equals(credential) }) + + it.only('is missing BitstringStatusListCredential type', async () => { + const credential = await fetchVC('https://digitalcredentials.github.io/vc-test-fixtures/verifiableCredentials/v2/ed25519/didKey/legacy-missingTypeStatus-noExpiry.json') + const expectedResult = getExpectedVerifiedResult({ credential, withStatus: false }) + expectedResult.log?.push( + { + "id": REVOCATION_STATUS_STEP_ID, + "error": { + "name": STATUS_LIST_TYPE_ERROR, + "message": 'Status list credential type must include "BitstringStatusListCredential".' + } + }) + const result = await verifyCredential({ credential, knownDIDRegistries }) + expect(result).to.have.property('log').that.deep.equalInAnyOrder(expectedResult.log); + expect(result).to.have.property("credential").that.equals(credential) + }) }) async function fetchVC(url: string): Promise { From 624f979ba3be9f4da79f751453de8474315747dd Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Thu, 11 Dec 2025 09:23:33 -0500 Subject: [PATCH 6/8] add handling for not yet valid status list --- src/Verify.ts | 7 ++++++- src/constants/external.ts | 2 +- test/Verify.status.spec.ts | 21 +++++++++++++++++++-- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/Verify.ts b/src/Verify.ts index c20521e..bb52e56 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -202,10 +202,15 @@ function handleAnyStatusError({ verificationResponse }: { verificationResponse: name: STATUS_LIST_TYPE_ERROR, message: 'Status list credential type must include "BitstringStatusListCredential".' } + } else if (statusResult?.error?.cause?.message?.includes(STATUS_NOT_YET_VALID_ERROR)) { + error = { + name: STATUS_LIST_NOT_YET_VALID_ERROR, + message: 'The validFrom date on the status list credential is in the future.' + } } else { error = { name: UNKNOWN_STATUS_LIST_ERROR, - message: statusResult.error.cause.message ?? "The status list couldn't be verified." + message: statusResult.error.cause?.message ?? "The status list couldn't be verified." } } const statusStep = { diff --git a/src/constants/external.ts b/src/constants/external.ts index 8e56324..9b27f8a 100644 --- a/src/constants/external.ts +++ b/src/constants/external.ts @@ -5,4 +5,4 @@ export const EXPIRED_ERROR = 'is after "validUntil"' export const VERIFICATION_ERROR = 'VerificationError' export const STATUS_SIGNATURE_ERROR = 'Verification error' export const STATUS_TYPE_ERROR = 'Status list credential type must include "BitstringStatusListCredential".' -export const STATUS_NOT_YET_VALID_ERROR = '' \ No newline at end of file +export const STATUS_NOT_YET_VALID_ERROR = 'is before "validFrom"' \ No newline at end of file diff --git a/test/Verify.status.spec.ts b/test/Verify.status.spec.ts index 81565c6..c31cd28 100644 --- a/test/Verify.status.spec.ts +++ b/test/Verify.status.spec.ts @@ -6,7 +6,7 @@ import { knownDIDRegistries } from '../src/test-fixtures/knownDidRegistries.js'; import { SCHEMA_ENTRY_ID } from '../src/constants/verificationSteps.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, STATUS_LIST_EXPIRED, STATUS_LIST_SIGNATURE_ERROR, STATUS_LIST_TYPE_ERROR } from '../src/constants/errors.js'; +import { DID_WEB_UNRESOLVED, INVALID_CREDENTIAL_ID, INVALID_SIGNATURE, NO_PROOF, STATUS_LIST_NOT_FOUND, STATUS_LIST_EXPIRED, STATUS_LIST_SIGNATURE_ERROR, STATUS_LIST_TYPE_ERROR, STATUS_LIST_NOT_YET_VALID_ERROR } from '../src/constants/errors.js'; import { getVCv2DoubleSigWithBadStatusUrl } from '../src/test-fixtures/vc.js'; import { getExpectedVerifiedResult } from '../src/test-fixtures/expectedResults.js'; @@ -71,7 +71,7 @@ describe('status checks', () => { expect(result).to.have.property("credential").that.equals(credential) }) - it.only('is missing BitstringStatusListCredential type', async () => { + it('is missing BitstringStatusListCredential type', async () => { const credential = await fetchVC('https://digitalcredentials.github.io/vc-test-fixtures/verifiableCredentials/v2/ed25519/didKey/legacy-missingTypeStatus-noExpiry.json') const expectedResult = getExpectedVerifiedResult({ credential, withStatus: false }) expectedResult.log?.push( @@ -86,6 +86,23 @@ describe('status checks', () => { expect(result).to.have.property('log').that.deep.equalInAnyOrder(expectedResult.log); expect(result).to.have.property("credential").that.equals(credential) }) + + it('is not yet valid', async () => { + const credential = await fetchVC('https://digitalcredentials.github.io/vc-test-fixtures/verifiableCredentials/v2/ed25519/didKey/legacy-notYetValidStatus-noExpiry.json') + const expectedResult = getExpectedVerifiedResult({ credential, withStatus: false }) + expectedResult.log?.push( + { + "id": REVOCATION_STATUS_STEP_ID, + "error": { + "name": STATUS_LIST_NOT_YET_VALID_ERROR, + "message": 'The validFrom date on the status list credential is in the future.' + } + }) + const result = await verifyCredential({ credential, knownDIDRegistries }) + expect(result).to.have.property('log').that.deep.equalInAnyOrder(expectedResult.log); + expect(result).to.have.property("credential").that.equals(credential) + }) + }) async function fetchVC(url: string): Promise { From 872d3a4fb5f808081be35a96bcfa18426e53aca2 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Thu, 11 Dec 2025 16:09:54 -0500 Subject: [PATCH 7/8] update README with status list errors --- README.md | 117 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 100 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 1458c54..e753616 100644 --- a/README.md +++ b/README.md @@ -175,6 +175,10 @@ Variations and errors are covered next... There are three general flavours of result that might be returned: +- all checks were conclusive +- verification was partially successful +- verification was fatal + 1. all checks were conclusive 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. @@ -222,7 +226,7 @@ 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. +An invalid signature is considered fatal, rather than conclusive (even though in a sense it conclusively rejects the entire credential) because an invalid signature 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 those steps, and can't even check them because they could be fraudulent. 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: @@ -268,23 +272,21 @@ And here is a slightly different verification result where we have still made co 2. partially successful verification -A verification might partly succeed if it can verify: - -* the signature -* the expiry date +A verification might partly succeed if it can conclusively determine some of the steps - most +importantly that the credential hasn't been tampered with - but can't conclusively verify (as true or false) some other steps. -But can't retrieve (from the network) any one of: +A good example is if there are network problems and the verifier can't retrieve things an issuer registry, and so can't say whether the did used to sign the VC is listed in the registry. It might be, but it might not be. -* the revocation status -* an issuer registry from our list of trusted issuers +Another example is the revocation status - if we can't retrieve the status list from the network then we again +can't see one way or the other if the credential has been revoked. It might have been, it might not have been. -which are needed to verify the revocation status and issuer identity. +The revocation status is an especially interesting example because the status list is itself a Verifiable Credential, which could have expired, been revoked, or been tampered with. And if so, then we again can't say anything about the status of the VC we are trying to to verify because the status list is not valid. 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, where we couldn't retrieve the status list or one of the registries: +A partially successful verification might look like this example, where we couldn't retrieve one of the registries: ``` { @@ -298,13 +300,6 @@ A partially successful verification might look like this example, where we could "id": "expiration", "valid": true }, - { - "id": "revocation_status", - "error": { - "name": "'status_list_not_found'", - "message": "Could not retrieve the revocation status list." - } - }, { "id": "registered_issuer", "valid": false, @@ -341,6 +336,94 @@ A partially successful verification might look like this example, where we could } ``` +Or for a status list that couldn't be retrieved: + +``` +{ + "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": "'status_list_not_found'", + "message": "Could not retrieve the revocation status list." + } + }, + { + "id": "registered_issuer", + "valid": true, + "matchingIssuers": [ + { + "issuer": { + "federation_entity": { + "organization_name": "DCC did:web test", + "homepage_uri": "https://digitalcredentials.mit.edu", + "location": "Cambridge, MA, USA" + } + }, + "registry": { + "name": "DCC Sandbox Registry", + "type": "dcc-legacy", + "url": "https://digitalcredentials.github.io/sandbox-registry/registry.json" + } + } + ] + } + ] +} +``` + +The status list errors that we return include: +``` + "error": { + "name": "status_list_not_found", + "message": "Could not retrieve the revocation status list." + } +``` + +``` + "error": { + "name": "status_list_expired", + "message": "The status list verifiable credential has expired." + } +``` + +``` + "error": { + "name": "status_list_signature_error", + "message": "The signature on the status list is invalid." + } + ``` + +``` + "error": { + "name": "status_list_type_error", + "message": "Status list credential type must include \"BitstringStatusListCredential\"." + } +``` + +``` + "error": { + "name": "status_list_not_yet_valid", + "message": "The validFrom date on the status list credential is in the future." + } +``` +And a fallback for any unknown error: +``` + "error": { + "name": "status_list_error", + "message": "The status list couldn't be verified." + } +``` + 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 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. From 98478591683d5aaa9ad1bfdf8cde39b8c00166e6 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Fri, 12 Dec 2025 08:39:25 -0500 Subject: [PATCH 8/8] move error messages to constants file --- src/Verify.ts | 13 +++++++------ src/constants/messages.ts | 5 +++++ test/Verify.status.spec.ts | 18 ++++++++---------- 3 files changed, 20 insertions(+), 16 deletions(-) create mode 100644 src/constants/messages.ts diff --git a/src/Verify.ts b/src/Verify.ts index bb52e56..e6f3a46 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -28,6 +28,7 @@ import { EXPIRED_ERROR, ISSUER_DID_RESOLVES, NOT_FOUND_ERROR, STATUS_NOT_YET_VAL import { Credential } from './types/credential.js'; import { VerificationResponse, VerificationStep, PresentationVerificationResponse, PresentationSignatureResult } from './types/result.js'; import { VerifiablePresentation } from './types/presentation.js'; +import { GENERAL_STATUS_LIST_ERROR_MSG, STATUS_LIST_EXPIRED_MSG, STATUS_LIST_NOT_YET_VALID_MSG, STATUS_LIST_SIGNATURE_ERROR_MSG, STATUS_LIST_TYPE_ERROR_MSG } from './constants/messages.js'; const { purposes } = pkg; const presentationPurpose = new purposes.AssertionProofPurpose(); @@ -98,7 +99,7 @@ export async function verifyCredential({ credential, knownDIDRegistries }: { cre checkStatus: statusChecker, verifyMatchingIssuers: false }); - // console.log(JSON.stringify(verificationResponse,null,2)) + const adjustedResponse = await transformResponse(verificationResponse, credential, knownDIDRegistries) return adjustedResponse; } catch (error) { @@ -190,27 +191,27 @@ function handleAnyStatusError({ verificationResponse }: { verificationResponse: } else if (statusResult?.error?.cause?.message?.includes(EXPIRED_ERROR)) { error = { name: STATUS_LIST_EXPIRED, - message: "The status list verifiable credential has expired." + message: STATUS_LIST_EXPIRED_MSG } } else if (statusResult?.error?.cause?.message?.startsWith(STATUS_SIGNATURE_ERROR)) { error = { name: STATUS_LIST_SIGNATURE_ERROR, - message: "The signature on the status list is invalid." + message: STATUS_LIST_SIGNATURE_ERROR_MSG } } else if (statusResult?.error?.message?.startsWith(STATUS_TYPE_ERROR)) { error = { name: STATUS_LIST_TYPE_ERROR, - message: 'Status list credential type must include "BitstringStatusListCredential".' + message: STATUS_LIST_TYPE_ERROR_MSG } } else if (statusResult?.error?.cause?.message?.includes(STATUS_NOT_YET_VALID_ERROR)) { error = { name: STATUS_LIST_NOT_YET_VALID_ERROR, - message: 'The validFrom date on the status list credential is in the future.' + message: STATUS_LIST_NOT_YET_VALID_MSG } } else { error = { name: UNKNOWN_STATUS_LIST_ERROR, - message: statusResult.error.cause?.message ?? "The status list couldn't be verified." + message: statusResult.error.cause?.message ?? GENERAL_STATUS_LIST_ERROR_MSG } } const statusStep = { diff --git a/src/constants/messages.ts b/src/constants/messages.ts new file mode 100644 index 0000000..8ac83f2 --- /dev/null +++ b/src/constants/messages.ts @@ -0,0 +1,5 @@ +export const STATUS_LIST_EXPIRED_MSG = "The status list verifiable credential has expired." +export const STATUS_LIST_SIGNATURE_ERROR_MSG = "The signature on the status list is invalid." +export const STATUS_LIST_TYPE_ERROR_MSG = 'Status list credential type must include "BitstringStatusListCredential".' +export const STATUS_LIST_NOT_YET_VALID_MSG = 'The validFrom date on the status list credential is in the future.' +export const GENERAL_STATUS_LIST_ERROR_MSG = "The status list couldn't be verified." \ No newline at end of file diff --git a/test/Verify.status.spec.ts b/test/Verify.status.spec.ts index c31cd28..c136778 100644 --- a/test/Verify.status.spec.ts +++ b/test/Verify.status.spec.ts @@ -3,20 +3,18 @@ import deepEqualInAnyOrder from 'deep-equal-in-any-order' import { verifyCredential } from '../src/Verify.js' import { Credential } from '../src/types/credential.js'; import { knownDIDRegistries } from '../src/test-fixtures/knownDidRegistries.js'; -import { SCHEMA_ENTRY_ID } from '../src/constants/verificationSteps.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, STATUS_LIST_EXPIRED, STATUS_LIST_SIGNATURE_ERROR, STATUS_LIST_TYPE_ERROR, STATUS_LIST_NOT_YET_VALID_ERROR } from '../src/constants/errors.js'; +import { REVOCATION_STATUS_STEP_ID } from '../src/constants/verificationSteps.js'; +import { STATUS_LIST_NOT_FOUND, STATUS_LIST_EXPIRED, STATUS_LIST_SIGNATURE_ERROR, STATUS_LIST_TYPE_ERROR, STATUS_LIST_NOT_YET_VALID_ERROR } from '../src/constants/errors.js'; import { getVCv2DoubleSigWithBadStatusUrl } from '../src/test-fixtures/vc.js'; import { getExpectedVerifiedResult } from '../src/test-fixtures/expectedResults.js'; +import { STATUS_LIST_EXPIRED_MSG, STATUS_LIST_NOT_YET_VALID_MSG, STATUS_LIST_SIGNATURE_ERROR_MSG, STATUS_LIST_TYPE_ERROR_MSG } from '../src/constants/messages.js'; chai.use(deepEqualInAnyOrder); const { expect } = chai; /* - -Tests credential status validation. - +Tests credential status list validation. */ describe('status checks', () => { @@ -47,7 +45,7 @@ describe('status checks', () => { "id": REVOCATION_STATUS_STEP_ID, "error": { "name": STATUS_LIST_EXPIRED, - "message": "The status list verifiable credential has expired." + "message": STATUS_LIST_EXPIRED_MSG } }) const result = await verifyCredential({ credential, knownDIDRegistries }) @@ -63,7 +61,7 @@ describe('status checks', () => { "id": REVOCATION_STATUS_STEP_ID, "error": { "name": STATUS_LIST_SIGNATURE_ERROR, - "message": "The signature on the status list is invalid." + "message": STATUS_LIST_SIGNATURE_ERROR_MSG } }) const result = await verifyCredential({ credential, knownDIDRegistries }) @@ -79,7 +77,7 @@ describe('status checks', () => { "id": REVOCATION_STATUS_STEP_ID, "error": { "name": STATUS_LIST_TYPE_ERROR, - "message": 'Status list credential type must include "BitstringStatusListCredential".' + "message": STATUS_LIST_TYPE_ERROR_MSG } }) const result = await verifyCredential({ credential, knownDIDRegistries }) @@ -95,7 +93,7 @@ describe('status checks', () => { "id": REVOCATION_STATUS_STEP_ID, "error": { "name": STATUS_LIST_NOT_YET_VALID_ERROR, - "message": 'The validFrom date on the status list credential is in the future.' + "message": STATUS_LIST_NOT_YET_VALID_MSG } }) const result = await verifyCredential({ credential, knownDIDRegistries })