Skip to content

Commit c96a9a0

Browse files
authored
Fix/228 (#362)
Signed-off-by: Mirko Mollik <mirko.mollik@eudi.sprind.org>
1 parent 576f172 commit c96a9a0

File tree

8 files changed

+674
-1
lines changed

8 files changed

+674
-1
lines changed

packages/core/README.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,68 @@ If you want to use the pure sd-jwt class or implement your own sd-jwt credential
4141
- @sd-jwt/present
4242
- @sd-jwt/types
4343
- @sd-jwt/utils
44+
45+
### Verification
46+
47+
The library provides two verification approaches:
48+
49+
#### Standard Verification (Fail-Fast)
50+
51+
The `verify()` method throws an error immediately when the first validation failure is encountered:
52+
53+
```typescript
54+
try {
55+
const result = await sdjwt.verify(credential);
56+
console.log('Verified payload:', result.payload);
57+
} catch (error) {
58+
console.error('Verification failed:', error.message);
59+
}
60+
```
61+
62+
#### Safe Verification (Collect All Errors)
63+
64+
The `safeVerify()` method collects all validation errors instead of failing on the first one. This is useful when you want to show users all issues with a credential at once:
65+
66+
```typescript
67+
import type { SafeVerifyResult, VerificationError } from '@sd-jwt/types';
68+
69+
const result = await sdjwt.safeVerify(credential);
70+
71+
if (result.success) {
72+
// Verification succeeded
73+
console.log('Verified payload:', result.data.payload);
74+
console.log('Header:', result.data.header);
75+
if (result.data.kb) {
76+
console.log('Key binding:', result.data.kb);
77+
}
78+
} else {
79+
// Verification failed - inspect all errors
80+
for (const error of result.errors) {
81+
console.error(`[${error.code}] ${error.message}`);
82+
if (error.details) {
83+
console.error('Details:', error.details);
84+
}
85+
}
86+
}
87+
```
88+
89+
##### Error Codes
90+
91+
The `safeVerify()` method returns errors with the following codes:
92+
93+
| Code | Description |
94+
|------|-------------|
95+
| `HASHER_NOT_FOUND` | Hasher function not configured |
96+
| `VERIFIER_NOT_FOUND` | Verifier function not configured |
97+
| `INVALID_SD_JWT` | SD-JWT structure is invalid or cannot be decoded |
98+
| `INVALID_JWT_FORMAT` | JWT format is malformed |
99+
| `JWT_NOT_YET_VALID` | JWT `iat` or `nbf` claim is in the future |
100+
| `JWT_EXPIRED` | JWT `exp` claim is in the past |
101+
| `INVALID_JWT_SIGNATURE` | Signature verification failed |
102+
| `MISSING_REQUIRED_CLAIMS` | Required claim keys are not present |
103+
| `KEY_BINDING_JWT_MISSING` | Key binding JWT required but not present |
104+
| `KEY_BINDING_VERIFIER_NOT_FOUND` | Key binding verifier not configured |
105+
| `KEY_BINDING_SIGNATURE_INVALID` | Key binding signature verification failed |
106+
| `KEY_BINDING_SD_HASH_INVALID` | Key binding `sd_hash` does not match |
107+
| `UNKNOWN_ERROR` | An unexpected error occurred |
108+

packages/core/src/index.ts

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@ import {
77
KB_JWT_TYP,
88
type KBOptions,
99
type PresentationFrame,
10+
type SafeVerifyResult,
1011
type SDJWTCompact,
1112
type SDJWTConfig,
1213
type Signer,
14+
type VerificationError,
15+
type VerificationErrorCode,
1316
} from '@sd-jwt/types';
1417
import {
1518
base64urlDecode,
@@ -259,6 +262,202 @@ export class SDJwtInstance<ExtendedPayload extends SdJwtPayload, T = unknown> {
259262
return { payload, header, kb };
260263
}
261264

265+
/**
266+
* Safe verification that collects all errors instead of failing fast.
267+
* Returns a result object with either the verified data or an array of all errors.
268+
*
269+
* @param encodedSDJwt - The encoded SD-JWT to verify
270+
* @param options - Verification options
271+
* @returns A SafeVerifyResult containing either success data or collected errors
272+
*/
273+
public async safeVerify(
274+
encodedSDJwt: string,
275+
options?: T & VerifierOptions,
276+
): Promise<
277+
SafeVerifyResult<{
278+
payload: ExtendedPayload;
279+
header: Record<string, unknown> | undefined;
280+
kb?: {
281+
payload: Record<string, unknown>;
282+
header: Record<string, unknown>;
283+
};
284+
}>
285+
> {
286+
const errors: VerificationError[] = [];
287+
288+
// Helper to add errors
289+
const addError = (
290+
code: VerificationErrorCode,
291+
message: string,
292+
details?: unknown,
293+
) => {
294+
errors.push({ code, message, details });
295+
};
296+
297+
// Helper to convert exception to error code
298+
const exceptionToCode = (error: Error): VerificationErrorCode => {
299+
const message = error.message.toLowerCase();
300+
if (message.includes('hasher not found')) return 'HASHER_NOT_FOUND';
301+
if (message.includes('verifier not found')) return 'VERIFIER_NOT_FOUND';
302+
if (message.includes('invalid sd jwt') || message.includes('invalid jwt'))
303+
return 'INVALID_SD_JWT';
304+
if (message.includes('not yet valid')) return 'JWT_NOT_YET_VALID';
305+
if (message.includes('expired')) return 'JWT_EXPIRED';
306+
if (message.includes('signature')) return 'INVALID_JWT_SIGNATURE';
307+
if (message.includes('missing required claim'))
308+
return 'MISSING_REQUIRED_CLAIMS';
309+
if (message.includes('key binding jwt not exist'))
310+
return 'KEY_BINDING_JWT_MISSING';
311+
if (message.includes('key binding verifier not found'))
312+
return 'KEY_BINDING_VERIFIER_NOT_FOUND';
313+
if (message.includes('sd_hash')) return 'KEY_BINDING_SD_HASH_INVALID';
314+
return 'UNKNOWN_ERROR';
315+
};
316+
317+
// Check basic configuration first
318+
if (!this.userConfig.hasher) {
319+
addError('HASHER_NOT_FOUND', 'Hasher not found');
320+
}
321+
if (!this.userConfig.verifier) {
322+
addError('VERIFIER_NOT_FOUND', 'Verifier not found');
323+
}
324+
325+
// If basic config is missing, return early
326+
if (errors.length > 0) {
327+
return { success: false, errors };
328+
}
329+
330+
const hasher = this.userConfig.hasher as Hasher;
331+
332+
// Try to decode and validate the SD-JWT
333+
let sdjwt: SDJwt | undefined;
334+
let payload: ExtendedPayload | undefined;
335+
let header: Record<string, unknown> | undefined;
336+
337+
try {
338+
sdjwt = await SDJwt.fromEncode(encodedSDJwt, hasher);
339+
if (!sdjwt.jwt || !sdjwt.jwt.payload) {
340+
addError('INVALID_SD_JWT', 'Invalid SD JWT: missing JWT or payload');
341+
}
342+
} catch (error) {
343+
addError(
344+
'INVALID_SD_JWT',
345+
`Failed to decode SD-JWT: ${(error as Error).message}`,
346+
error,
347+
);
348+
}
349+
350+
// Validate signature and claims
351+
if (sdjwt?.jwt) {
352+
try {
353+
const result = await this.VerifyJwt(sdjwt.jwt, options);
354+
header = result.header;
355+
const claims = await sdjwt.getClaims(hasher);
356+
payload = claims as ExtendedPayload;
357+
} catch (error) {
358+
const code = exceptionToCode(error as Error);
359+
addError(code, (error as Error).message, error);
360+
}
361+
}
362+
363+
// Check required claim keys
364+
if (sdjwt && options?.requiredClaimKeys) {
365+
try {
366+
const keys = await sdjwt.keys(hasher);
367+
const missingKeys = options.requiredClaimKeys.filter(
368+
(k) => !keys.includes(k),
369+
);
370+
if (missingKeys.length > 0) {
371+
addError(
372+
'MISSING_REQUIRED_CLAIMS',
373+
`Missing required claim keys: ${missingKeys.join(', ')}`,
374+
{ missingKeys },
375+
);
376+
}
377+
} catch (error) {
378+
addError(
379+
'UNKNOWN_ERROR',
380+
`Failed to check required claims: ${(error as Error).message}`,
381+
error,
382+
);
383+
}
384+
}
385+
386+
// Verify key binding if requested
387+
let kb:
388+
| { payload: Record<string, unknown>; header: Record<string, unknown> }
389+
| undefined;
390+
if (options?.keyBindingNonce && sdjwt) {
391+
if (!sdjwt.kbJwt) {
392+
addError('KEY_BINDING_JWT_MISSING', 'Key Binding JWT not exist');
393+
} else if (!this.userConfig.kbVerifier) {
394+
addError(
395+
'KEY_BINDING_VERIFIER_NOT_FOUND',
396+
'Key Binding Verifier not found',
397+
);
398+
} else if (payload) {
399+
try {
400+
const kbResult = await sdjwt.kbJwt.verifyKB({
401+
verifier: this.userConfig.kbVerifier,
402+
payload: payload as JwtPayload,
403+
nonce: options.keyBindingNonce,
404+
});
405+
if (!kbResult) {
406+
addError(
407+
'KEY_BINDING_SIGNATURE_INVALID',
408+
'Key binding signature is not valid',
409+
);
410+
} else {
411+
kb = kbResult;
412+
413+
// Verify sd_hash
414+
const sdjwtWithoutKb = new SDJwt({
415+
jwt: sdjwt.jwt,
416+
disclosures: sdjwt.disclosures,
417+
});
418+
const presentSdJwtWithoutKb = sdjwtWithoutKb.encodeSDJwt();
419+
const sdHashStr = await this.calculateSDHash(
420+
presentSdJwtWithoutKb,
421+
sdjwt,
422+
hasher,
423+
);
424+
425+
if (sdHashStr !== kbResult.payload.sd_hash) {
426+
addError(
427+
'KEY_BINDING_SD_HASH_INVALID',
428+
'Invalid sd_hash in Key Binding JWT',
429+
{
430+
expected: sdHashStr,
431+
received: kbResult.payload.sd_hash,
432+
},
433+
);
434+
}
435+
}
436+
} catch (error) {
437+
addError(
438+
'KEY_BINDING_SIGNATURE_INVALID',
439+
`Key binding verification failed: ${(error as Error).message}`,
440+
error,
441+
);
442+
}
443+
}
444+
}
445+
446+
// Return result
447+
if (errors.length > 0) {
448+
return { success: false, errors };
449+
}
450+
451+
return {
452+
success: true,
453+
data: {
454+
payload: payload as ExtendedPayload,
455+
header,
456+
kb,
457+
},
458+
};
459+
}
460+
262461
private async calculateSDHash(
263462
presentSdJwtWithoutKb: string,
264463
sdjwt: SDJwt,

0 commit comments

Comments
 (0)