@@ -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' ;
1417import {
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