From e994ac8e56403dcdd6866c17400320cb79545bbf Mon Sep 17 00:00:00 2001 From: Dmitri Zagidulin Date: Wed, 22 Jul 2026 10:28:50 -0400 Subject: [PATCH] Fix issuer-registries behavior on empty list. Signed-off-by: Dmitri Zagidulin --- CHANGELOG.md | 12 +++++ src/services/data-integrity-crypto.ts | 19 ++++++-- src/suites/registry/issuer-registry-check.ts | 5 +- .../services/classify-signature-error.spec.ts | 48 +++++++++++++++++++ test/suites/registry.spec.ts | 22 +++++++++ 5 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 test/services/classify-signature-error.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index d723ff5..a5180b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # @interop/verifier-core CHANGELOG +## 3.4.1 - TBD + +### Fixed + +- The issuer registry check now treats an explicitly empty `registries` list + (`registries: []`) as opting out of registry lookup and skips, instead of + failing with "issuer not found in any registry" -- e.g. for self-issued + credentials. +- `INVALID_SIGNATURE` problem details now surface the underlying sub-error + messages from an aggregate jsonld-signatures error (deduplicated and joined), + instead of the unhelpful top-level "Verification error(s)." message. + ## 3.4.0 - 2026-07-22 ### Added diff --git a/src/services/data-integrity-crypto.ts b/src/services/data-integrity-crypto.ts index 631f160..fba897b 100644 --- a/src/services/data-integrity-crypto.ts +++ b/src/services/data-integrity-crypto.ts @@ -122,7 +122,8 @@ function proofVerificationMethod( return typeof vm === 'string' ? vm : undefined; } -function classifySignatureError( +/** Exported for unit testing. */ +export function classifySignatureError( error: unknown, credential: Record | undefined ): ProblemDetail[] { @@ -211,12 +212,24 @@ function classifySignatureError( ]; } - const err = error as { message?: string } | undefined; + // Prefer the unpacked sub-error messages: an aggregate jsonld-signatures + // error carries only "Verification error(s)." on itself, with the real + // causes in its `errors[]`. + const messages = [ + ...new Set( + errors + .map(e => { + const x = e as { message?: string; error?: { message?: string } }; + return x.error?.message || x.message; + }) + .filter((m): m is string => !!m) + ) + ]; return [ { type: ProblemTypes.INVALID_SIGNATURE, title: 'Invalid Signature', - detail: err?.message || 'The signature is not valid.' + detail: messages.join('; ') || 'The signature is not valid.' } ]; } diff --git a/src/suites/registry/issuer-registry-check.ts b/src/suites/registry/issuer-registry-check.ts index c3d84ec..4cd0684 100644 --- a/src/suites/registry/issuer-registry-check.ts +++ b/src/suites/registry/issuer-registry-check.ts @@ -62,8 +62,9 @@ export const issuerRegistryCheck: VerificationCheck = { }; } - // Skip if no registries in context - if (!context.registries) { + // Skip if no registries in context (an explicitly empty list means the + // caller opted out of registry lookup, e.g. for self-issued credentials) + if (!context.registries || context.registries.length === 0) { return { status: 'skipped', reason: 'No registries configured in verification context.' diff --git a/test/services/classify-signature-error.spec.ts b/test/services/classify-signature-error.spec.ts new file mode 100644 index 0000000..ea0bc06 --- /dev/null +++ b/test/services/classify-signature-error.spec.ts @@ -0,0 +1,48 @@ +import { describe, it, expect } from 'vitest'; +import { classifySignatureError } from '../../src/services/data-integrity-crypto.js'; +import { ProblemTypes } from '../../src/problem-types.js'; + +describe('classifySignatureError', () => { + it('surfaces sub-error messages from an aggregate jsonld-signatures error', () => { + const aggregate = Object.assign(new Error('Verification error(s).'), { + errors: [ + new Error('Invalid signature.'), + { error: new Error('Public key not found.') } + ] + }); + + const problems = classifySignatureError(aggregate, undefined); + + expect(problems).toHaveLength(1); + expect(problems[0].type).toBe(ProblemTypes.INVALID_SIGNATURE); + expect(problems[0].detail).toBe( + 'Invalid signature.; Public key not found.' + ); + expect(problems[0].detail).not.toContain('Verification error(s)'); + }); + + it('deduplicates repeated sub-error messages', () => { + const aggregate = Object.assign(new Error('Verification error(s).'), { + errors: [new Error('Invalid signature.'), new Error('Invalid signature.')] + }); + + const problems = classifySignatureError(aggregate, undefined); + + expect(problems[0].detail).toBe('Invalid signature.'); + }); + + it('keeps the plain message for a non-aggregate error', () => { + const problems = classifySignatureError( + new Error('Something specific went wrong.'), + undefined + ); + + expect(problems[0].detail).toBe('Something specific went wrong.'); + }); + + it('falls back to a generic detail when no message is available', () => { + const problems = classifySignatureError(undefined, undefined); + + expect(problems[0].detail).toBe('The signature is not valid.'); + }); +}); diff --git a/test/suites/registry.spec.ts b/test/suites/registry.spec.ts index 4fcf7f3..d25f73e 100644 --- a/test/suites/registry.spec.ts +++ b/test/suites/registry.spec.ts @@ -41,6 +41,28 @@ describe('Registry Suite', () => { expect(results[0].outcome.reason).toContain('No registries configured'); } }); + + it('skips check when the registries list is empty', async () => { + const subject = createSubject( + CredentialFactory({ version: 'v2', credential: {} }) + ); + const context: VerificationContext = { + ...baseContext, + registries: [], + lookupIssuers: FakeRegistryLookup({ + found: false, + matchingRegistries: [] + }) + }; + const results = await runSuites([registrySuite], subject, context); + + expect(results).toHaveLength(1); + expect(results[0].check).toBe('registry.issuer'); + expect(results[0].outcome.status).toBe('skipped'); + if (results[0].outcome.status === 'skipped') { + expect(results[0].outcome.reason).toContain('No registries configured'); + } + }); }); describe('issuer lookup (fake)', () => {