From a873c27edcd0f201267c24a5ac882492693b6dd3 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Wed, 22 Oct 2025 09:46:12 -0400 Subject: [PATCH 1/9] add safe mode breaking test vc --- .../eddsa/v2/safeModeBreaker.json | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 src/test-fixtures/verifiableCredentials/eddsa/v2/safeModeBreaker.json diff --git a/src/test-fixtures/verifiableCredentials/eddsa/v2/safeModeBreaker.json b/src/test-fixtures/verifiableCredentials/eddsa/v2/safeModeBreaker.json new file mode 100644 index 0000000..72f59a3 --- /dev/null +++ b/src/test-fixtures/verifiableCredentials/eddsa/v2/safeModeBreaker.json @@ -0,0 +1,123 @@ +{ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json" + ], + "credentialSubject": { + "id": "urn:uuid:d1f916d3-3446-420f-afd4-6acd440cb6c5", + "type": [ + "AchievementSubject" + ], + "achievement": { + "id": "https://badging-build.vercel.app/issuers/67115d0efcca67051ffe3b0c/credentials/6813f23a79473f4ec0f5cbfb", + "type": [ + "Achievement" + ], + "alignment": [], + "achievementType": "Certification", + "creator": { + "id": "urn:uuid:700cd93c-8286-4ac8-a6ba-c5118c1adc54", + "type": [ + "Profile" + ], + "name": "KQED & PBS", + "description": "KQED and PBS have partnered to offer a series of media literacy micro-credentials that validate media literacy skills and classroom implementation practices. These micro-credentials also provide the pathway to earning certification as a PBS Media Literacy Educator.\n\nKQED is a San Francisco based NPR and PBS affiliate serving Northern California and beyond with a public-supported alternative to commercial TV, radio and digital media. KQED provides the educational community with professional development and standards-aligned resources on civic media literacy for use in all grades and subjects.\n\nPBS, with nearly 350 member stations, offers all Americans the opportunity to explore new ideas and new worlds through television and digital content. PBS is committed to supporting educators through multi-platform media and professional learning opportunities, helping spark curiosity and a lifelong love of learning.", + "endorsementJwt": [], + "image": { + "id": "https://d2umcf2gmasgod.cloudfront.net/67115d06fcca67051ffe3a9c.png", + "type": "Image" + }, + "email": "mcsupport@digitalpromise.org", + "otherIdentifier": [], + "endorsement": [] + }, + "criteria": { + "id": "https://badging-build.vercel.app/issuers/67115d0efcca67051ffe3b0c/credentials/6813f23a79473f4ec0f5cbfb#Criteria", + "narrative": "To earn the PBS Media Literacy Educator Certification by KQED, the educator must demonstrate mastery of the eight core competencies of media literacy instruction. The educator is required to demonstrate their ability to analyze, evaluate, create and share media in a variety of formats, and to instruct their students to do the same.\n\n\n\nThe educator has earned the following micro-credentials:\n\n* Assessing Student Media\n* Creating a Code of Conduct\n* Critically Analyzing Media\n* Evaluating Online Information\n* Evaluating Online Tools for Classroom Use \n* Implementing Media Projects in Early Childhood \n* Making Media for Classroom Use: Audio & Video\n* Making Media for Classroom Use: Images, Graphics & Interactives" + }, + "description": "Educators who receive eight PBS media literacy micro-credentials are awarded the PBS Media Literacy Educator Certification by KQED.", + "endorsement": [], + "endorsementJwt": [], + "image": { + "id": "https://d2umcf2gmasgod.cloudfront.net/6813f23979473f4ec0f5cbfa.png", + "type": "Image" + }, + "name": "PBS Media Literacy Educator Certification by KQED", + "otherIdentifier": [], + "related": [], + "resultDescription": [], + "tag": [ + "Media Literacy" + ] + }, + "identifier": [ + { + "type": "IdentityObject", + "hashed": false, + "identityHash": "kfranklin@digitalpromise.org", + "identityType": "emailAddress" + }, + { + "type": "IdentityObject", + "hashed": false, + "identityHash": "Kristen Test Franklin", + "identityType": "name" + } + ], + "result": [], + "source": { + "id": "urn:uuid:700cd93c-8286-4ac8-a6ba-c5118c1adc54", + "type": [ + "Profile" + ], + "name": "KQED & PBS", + "description": "KQED and PBS have partnered to offer a series of media literacy micro-credentials that validate media literacy skills and classroom implementation practices. These micro-credentials also provide the pathway to earning certification as a PBS Media Literacy Educator.\n\nKQED is a San Francisco based NPR and PBS affiliate serving Northern California and beyond with a public-supported alternative to commercial TV, radio and digital media. KQED provides the educational community with professional development and standards-aligned resources on civic media literacy for use in all grades and subjects.\n\nPBS, with nearly 350 member stations, offers all Americans the opportunity to explore new ideas and new worlds through television and digital content. PBS is committed to supporting educators through multi-platform media and professional learning opportunities, helping spark curiosity and a lifelong love of learning.", + "endorsementJwt": [], + "image": { + "id": "https://d2umcf2gmasgod.cloudfront.net/67115d06fcca67051ffe3a9c.png", + "type": "Image" + }, + "email": "mcsupport@digitalpromise.org", + "otherIdentifier": [], + "endorsement": [] + } + }, + "issuer": { + "id": "urn:uuid:700cd93c-8286-4ac8-a6ba-c5118c1adc54", + "type": [ + "Profile" + ], + "name": "KQED & PBS", + "description": "KQED and PBS have partnered to offer a series of media literacy micro-credentials that validate media literacy skills and classroom implementation practices. These micro-credentials also provide the pathway to earning certification as a PBS Media Literacy Educator.\n\nKQED is a San Francisco based NPR and PBS affiliate serving Northern California and beyond with a public-supported alternative to commercial TV, radio and digital media. KQED provides the educational community with professional development and standards-aligned resources on civic media literacy for use in all grades and subjects.\n\nPBS, with nearly 350 member stations, offers all Americans the opportunity to explore new ideas and new worlds through television and digital content. PBS is committed to supporting educators through multi-platform media and professional learning opportunities, helping spark curiosity and a lifelong love of learning.", + "endorsementJwt": [], + "email": "mcsupport@digitalpromise.org" + }, + "validFrom": "2025-07-14T20:59:07.322Z", + "proof": [ + { + "type": "DataIntegrityProof", + "cryptosuite": "eddsa-rdfc-2022", + "created": "2025-07-14T20:59:10.588Z", + "verificationMethod": "did:key:z6MkkAAkEonJCyLYi1fLy2g59JqJqWCUdo5FMiNExgewngCT", + "proofPurpose": "assertionMethod", + "proofValue": "zR6sVKjUr6LG6pA6hNTSsan8BQGfAycGZatM2d5FMjVFXSph9ymCwJnbvLe3vFuHz9wZEafnECVk2ZrRc6q5DUQg" + } + ], + "credentialSchema": [], + "credentialStatus": { + "id": "https://digital-promise.github.io/credential-status-list/5R6EGMYJCL#848", + "type": "StatusList2021Entry" + }, + "termsOfUse": [], + "type": [ + "VerifiableCredential", + "AchievementCredential" + ], + "id": "https://badging-build.vercel.app/awards/68756f9bec16711d138c650d", + "name": "PBS Media Literacy Educator Certification by KQED", + "description": "Educators who receive eight PBS media literacy micro-credentials are awarded the PBS Media Literacy Educator Certification by KQED.", + "awardedDate": "2025-07-14T20:59:07.322Z", + "endorsement": [], + "endorsementJwt": [], + "evidence": [] +} \ No newline at end of file From 0807776b2670f42754ea1d04b212bfe24e1668be Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Wed, 22 Oct 2025 10:01:38 -0400 Subject: [PATCH 2/9] rework export of safe mode test vc --- src/test-fixtures/vc.ts | 9 ++++++++- .../v2/{safeModeBreaker.json => safeModeBreaker.ts} | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) rename src/test-fixtures/verifiableCredentials/eddsa/v2/{safeModeBreaker.json => safeModeBreaker.ts} (99%) diff --git a/src/test-fixtures/vc.ts b/src/test-fixtures/vc.ts index c4a34a7..7282aef 100644 --- a/src/test-fixtures/vc.ts +++ b/src/test-fixtures/vc.ts @@ -4,6 +4,8 @@ import { v2Revoked } from "./verifiableCredentials/v2/v2Revoked.js" import { v2WithValidStatus } from "./verifiableCredentials/v2/v2WithValidStatus.js" import { v2ExpiredWithValidStatus } from "./verifiableCredentials/v2/v2ExpiredWithValidStatus.js" +import {safeModeBreaker} from "./verifiableCredentials/eddsa/v2/safeModeBreaker.js" + import { v1WithValidStatus } from "./verifiableCredentials/v1/v1WithValidStatus.js" import { v1NoStatus } from "./verifiableCredentials/v1/v1NoStatus.js" import { v1Revoked } from "./verifiableCredentials/v1/v1Revoked.js" @@ -142,6 +144,9 @@ const getVCv2SimpleIssuerId = (): any => { return JSON.parse(JSON.stringify(v2SimpleIssuerId)) } +const getSafeModeBreaker = (): any => { + return JSON.parse(JSON.stringify(safeModeBreaker)) +} export { getCredentialWithoutContext, @@ -174,5 +179,7 @@ export { getVCv1ExpiredAndTampered, getVCv1ExpiredWithValidStatus, getVCv1NoProof, - getVCv1NonURIId + getVCv1NonURIId, + + getSafeModeBreaker } diff --git a/src/test-fixtures/verifiableCredentials/eddsa/v2/safeModeBreaker.json b/src/test-fixtures/verifiableCredentials/eddsa/v2/safeModeBreaker.ts similarity index 99% rename from src/test-fixtures/verifiableCredentials/eddsa/v2/safeModeBreaker.json rename to src/test-fixtures/verifiableCredentials/eddsa/v2/safeModeBreaker.ts index 72f59a3..01957b9 100644 --- a/src/test-fixtures/verifiableCredentials/eddsa/v2/safeModeBreaker.json +++ b/src/test-fixtures/verifiableCredentials/eddsa/v2/safeModeBreaker.ts @@ -1,4 +1,4 @@ -{ +export const safeModeBreaker = { "@context": [ "https://www.w3.org/ns/credentials/v2", "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json" From 9019f0b64465b9d0f594e4ab451db3b231673bb9 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Wed, 22 Oct 2025 10:02:04 -0400 Subject: [PATCH 3/9] add safe mode test --- test/Verify.v2.spec.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/test/Verify.v2.spec.ts b/test/Verify.v2.spec.ts index 4bf1518..bde3520 100644 --- a/test/Verify.v2.spec.ts +++ b/test/Verify.v2.spec.ts @@ -1,6 +1,6 @@ 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, @@ -17,7 +17,8 @@ import { getVCv2DoubleSigWithBadStatusUrl, getVCv2DidWebWithValidStatus, getVCv2WithBadDidWebUrl, - getVCv2DidWebMultikeyWithValidStatus + getVCv2DidWebMultikeyWithValidStatus, + getSafeModeBreaker } from '../src/test-fixtures/vc.js' import { knownDIDRegistries } from '../src/test-fixtures/knownDidRegistries.js'; @@ -33,7 +34,7 @@ chai.use(deepEqualInAnyOrder); const { expect } = chai; const REGISTERED_ISSUER_STEP_ID = 'registered_issuer' -const DISABLE_CONSOLE_WHEN_NO_ERRORS = true +const DISABLE_CONSOLE_WHEN_NO_ERRORS = false describe('Verify', () => { @@ -98,6 +99,18 @@ describe('Verify', () => { describe('returns fatal error', () => { + it.only('for safe mode violation', async () => { + const credential : any = getSafeModeBreaker() + const result = await verifyCredential({ credential, knownDIDRegistries }) + console.log(JSON.stringify(result)) + 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, knownDIDRegistries }) From eef8189cd21af5e8a94c77cb94524c15e4b26faf Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Wed, 22 Oct 2025 15:15:09 -0400 Subject: [PATCH 4/9] fix safe mode test --- test/Verify.v2.spec.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/test/Verify.v2.spec.ts b/test/Verify.v2.spec.ts index bde3520..0238e87 100644 --- a/test/Verify.v2.spec.ts +++ b/test/Verify.v2.spec.ts @@ -99,16 +99,13 @@ describe('Verify', () => { describe('returns fatal error', () => { - it.only('for safe mode violation', async () => { + it('for safe mode violation', async () => { const credential : any = getSafeModeBreaker() - const result = await verifyCredential({ credential, knownDIDRegistries }) - console.log(JSON.stringify(result)) - const expectedResult = getExpectedFatalResult({ - credential, - errorMessage: 'The signature is not valid.', - errorName: INVALID_SIGNATURE - }) - expect(result).to.deep.equalInAnyOrder(expectedResult) + const result = await verifyCredential({ credential, knownDIDRegistries }) + expect(result.errors).to.exist + expect(result.errors![0].name).to.equal('jsonld.ValidationError') + // @ts-ignore + expect(result.errors[0].details.event.message).to.equal('Relative @type reference found.') }) it('when tampered with', async () => { From de36cd45c2b8d3ab998e0ee37ba41ee62059c4cc Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Wed, 22 Oct 2025 15:15:28 -0400 Subject: [PATCH 5/9] return details for jsonld errors --- src/Verify.ts | 31 +++++++++++++++++++------------ src/types/result.ts | 3 ++- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/Verify.ts b/src/Verify.ts index 5922f61..172e907 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -92,7 +92,6 @@ export async function verifyCredential({ credential, knownDIDRegistries}: { cred checkStatus: statusChecker, verifyMatchingIssuers: false }); - const adjustedResponse = await transformResponse(verificationResponse, credential, knownDIDRegistries) return adjustedResponse; } catch (error) { @@ -133,9 +132,7 @@ async function transformResponse(verificationResponse: any, credential: Credenti return verificationResponse as VerificationResponse; } -function buildFatalErrorObject(fatalErrorMessage: string, name: string, credential: Credential, stackTrace: string | null): VerificationResponse { - return { credential, errors: [{ name, message: fatalErrorMessage, ...(stackTrace ? { stackTrace } : null) }] }; -} + function handleAnyFatalCredentialErrors(credential: Credential): VerificationResponse | null { const validVCContexts = [ @@ -195,17 +192,17 @@ function handleAnySignatureError({ verificationResponse, credential }: { verific if (verificationResponse.error) { 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 + // Can't verify the signature. Maybe a bad signature or a did:web that can't + // be resolved or a json-ld error. 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) + // steps in verification, so return a fatal error and no 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') + // 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 @@ -219,8 +216,16 @@ function handleAnySignatureError({ verificationResponse, credential }: { verific errorName = DID_WEB_UNRESOLVED } } + } else if (jsonLdError) { + 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} } else { - // not an http error, so likely bad signature + // not an http or json-ld error, so likely bad signature fatalErrorMessage = 'The signature is not valid.' errorName = INVALID_SIGNATURE } @@ -244,4 +249,6 @@ function handleAnySignatureError({ verificationResponse, credential }: { verific - +function buildFatalErrorObject(fatalErrorMessage: string, name: string, credential: Credential, stackTrace: string | null): VerificationResponse { + return { credential, errors: [{ name, message: fatalErrorMessage, ...(stackTrace ? { stackTrace } : null) }] }; +} diff --git a/src/types/result.ts b/src/types/result.ts index 549eaed..f852051 100644 --- a/src/types/result.ts +++ b/src/types/result.ts @@ -2,6 +2,7 @@ export interface VerificationError { "message": string, "name"?: string, + "details"?: object, "stackTrace"?: any } @@ -28,7 +29,7 @@ export interface VerificationError { "additionalInformation"?: AdditionalInformationEntry[]; "credential"?: object, "errors"?: VerificationError[], - "log"?: VerificationStep[] + "log"?: VerificationStep[], } From 5fd6e6480ffc3ddca1bb92787331c7759b73d9d5 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Thu, 23 Oct 2025 13:43:02 -0400 Subject: [PATCH 6/9] update README with json-ld validation error section --- README.md | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2991e09..21c7e04 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,6 @@ This package exports two methods: * credential - The W3C Verifiable Credential to be verified. * knownDidRegistries - a list of issuer DIDs in which to lookup signing DIDs - #### result The typescript definitions for the result can be found [here](./src/types/result.ts) @@ -279,7 +278,6 @@ But can't retrieve (from the network) any one of: * 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. @@ -650,6 +648,36 @@ The proof property is missing, likely because the credential hasn't been signed: } ``` +jsonld.ValidationError + +An error was returnd by the json-ld parser. This is often a safe-mode error, and in particular +is often that a property has been included for which there is no definition in the context. + +Another common error here is an @type property that contains a value that is 'relative', meaning +that it cannot be resolved to an absolute IRI (which it must be according to the spec). + +A example (just the errors section) of a relative @type reference. + +```json +{ "errors": [{ + "name": "jsonld.ValidationError", + "details": { + "event": { + "type": [ + "JsonLdEvent" + ], + "code": "relative @type reference", + "level": "warning", + "message": "Relative @type reference found.", + "details": { + "type": "StatusList2021Entry" + } + } + }, + "message": "Safe mode validation error.", + "stack": "jsonld.ValidationError: Safe mode validation error....etc. removed for brevity." +}]} +``` other problem From 514ac38d9ae59e1e14b562dfff0d5d1b71d2a5af Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Fri, 24 Oct 2025 09:08:31 -0400 Subject: [PATCH 7/9] update safe mode section in readme --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 21c7e04..d0e3487 100644 --- a/README.md +++ b/README.md @@ -651,12 +651,14 @@ The proof property is missing, likely because the credential hasn't been signed: jsonld.ValidationError An error was returnd by the json-ld parser. This is often a safe-mode error, and in particular -is often that a property has been included for which there is no definition in the context. +is often that a property has been included in the VC, but for which there is no definition for the property in the context. + +A common example is including either or both of the 'issuanceDate' or the 'expirationDate' properties in a Verifiable Credential that uses version 2 of the Verifiable Credential Data Model. Those two properties are used in version 1 only, and have been replaced by validFrom and validUntil in version 2. So including the old properties in a Verifiable Credential for which only the version 2 context has been specified precipitates a safe-mode error. Another common error here is an @type property that contains a value that is 'relative', meaning that it cannot be resolved to an absolute IRI (which it must be according to the spec). -A example (just the errors section) of a relative @type reference. +A example of a relative @type reference (showing just the just the errors section of the verification result): ```json { "errors": [{ From 6fbcf1c165b1202a6589537db7085262fe65be5c Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Fri, 24 Oct 2025 09:08:52 -0400 Subject: [PATCH 8/9] update version number and changelog --- CHANGELOG.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0fdd72..d665b68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # @digitalcredentials/verifier-core CHANGELOG +## 1.0.0-beta.10 - October 24 2025 + +### Added + +- Returns more informative results for json-ld safe-mode errors. See the README for details. + ## 1.0.0-beta.9 - October 2 2025 ### Added diff --git a/package.json b/package.json index e39fe26..ec68820 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": "1.0.0-beta.9", + "version": "1.0.0-beta.10", "scripts": { "build-esm": "tsc -p tsconfig.esm.json", "build-types": "tsc -p tsconfig.types.json", From 472e56767a38169422e811505dfb77bf85c5e0b9 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Fri, 24 Oct 2025 10:00:27 -0400 Subject: [PATCH 9/9] update build and coverage status badges --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d0e3487..246fd2a 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=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=jc-implement)](https://coveralls.io/github/digitalcredentials/verifier-core?branch=jc-implement) +[![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/package/@digitalcredentials/verifier-core/v/1.0.0-beta.10) +[![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.