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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
19 changes: 16 additions & 3 deletions src/services/data-integrity-crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> | undefined
): ProblemDetail[] {
Expand Down Expand Up @@ -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.'
}
];
}
Expand Down
5 changes: 3 additions & 2 deletions src/suites/registry/issuer-registry-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.'
Expand Down
48 changes: 48 additions & 0 deletions test/services/classify-signature-error.spec.ts
Original file line number Diff line number Diff line change
@@ -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.');
});
});
22 changes: 22 additions & 0 deletions test/suites/registry.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)', () => {
Expand Down