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/README.md b/README.md index 2991e09..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. @@ -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,38 @@ 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 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." +}]} +``` other problem 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", 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/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.ts b/src/test-fixtures/verifiableCredentials/eddsa/v2/safeModeBreaker.ts new file mode 100644 index 0000000..01957b9 --- /dev/null +++ b/src/test-fixtures/verifiableCredentials/eddsa/v2/safeModeBreaker.ts @@ -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": [] +} \ No newline at end of file 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[], } diff --git a/test/Verify.v2.spec.ts b/test/Verify.v2.spec.ts index 4bf1518..0238e87 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,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 })