Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
40 changes: 35 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -650,6 +648,38 @@ The proof property is missing, likely because the credential hasn't been signed:
}
```

<b><i>jsonld.ValidationError</i></b>

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 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 of a relative @type reference (showing just the just the errors section of the verification result):

```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."
}]}
```

<b>other problem</b>

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
31 changes: 19 additions & 12 deletions src/Verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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 = [
Expand Down Expand Up @@ -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
Expand All @@ -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
}
Expand All @@ -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) }] };
}
9 changes: 8 additions & 1 deletion src/test-fixtures/vc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -142,6 +144,9 @@ const getVCv2SimpleIssuerId = (): any => {
return JSON.parse(JSON.stringify(v2SimpleIssuerId))
}

const getSafeModeBreaker = (): any => {
return JSON.parse(JSON.stringify(safeModeBreaker))
}
export {

getCredentialWithoutContext,
Expand Down Expand Up @@ -174,5 +179,7 @@ export {
getVCv1ExpiredAndTampered,
getVCv1ExpiredWithValidStatus,
getVCv1NoProof,
getVCv1NonURIId
getVCv1NonURIId,

getSafeModeBreaker
}
123 changes: 123 additions & 0 deletions src/test-fixtures/verifiableCredentials/eddsa/v2/safeModeBreaker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
export const safeModeBreaker = {
"@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": []
}
3 changes: 2 additions & 1 deletion src/types/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
export interface VerificationError {
"message": string,
"name"?: string,
"details"?: object,
"stackTrace"?: any
}

Expand All @@ -28,7 +29,7 @@ export interface VerificationError {
"additionalInformation"?: AdditionalInformationEntry[];
"credential"?: object,
"errors"?: VerificationError[],
"log"?: VerificationStep[]
"log"?: VerificationStep[],
}


Expand Down
16 changes: 13 additions & 3 deletions test/Verify.v2.spec.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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';
Expand All @@ -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', () => {

Expand Down Expand Up @@ -98,6 +99,15 @@ describe('Verify', () => {

describe('returns fatal error', () => {

it('for safe mode violation', async () => {
const credential : any = getSafeModeBreaker()
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 () => {
const credential: any = getVCv2Tampered()
const result = await verifyCredential({ credential, knownDIDRegistries })
Expand Down