@sd-jwt/core 0.19.1-next.0 → 0.19.1-next.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -41,3 +41,68 @@ If you want to use the pure sd-jwt class or implement your own sd-jwt credential
41
41
  - @sd-jwt/present
42
42
  - @sd-jwt/types
43
43
  - @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
+
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _sd_jwt_types from '@sd-jwt/types';
2
- import { Signer, Base64urlString, Verifier, kbHeader, kbPayload, KbVerifier, JwtPayload, SDJWTCompact, Hasher, PresentationFrame, DisclosureFrame, HasherAndAlg, SaltGenerator, SDJWTConfig, KBOptions } from '@sd-jwt/types';
2
+ import { Signer, Base64urlString, Verifier, kbHeader, kbPayload, KbVerifier, JwtPayload, SDJWTCompact, Hasher, PresentationFrame, DisclosureFrame, HasherAndAlg, SaltGenerator, SDJWTConfig, KBOptions, SafeVerifyResult } from '@sd-jwt/types';
3
3
  import { Disclosure } from '@sd-jwt/utils';
4
4
 
5
5
  type FlattenJSONData = {
@@ -230,6 +230,22 @@ declare class SDJwtInstance<ExtendedPayload extends SdJwtPayload, T = unknown> {
230
230
  header: _sd_jwt_types.kbHeader;
231
231
  };
232
232
  }>;
233
+ /**
234
+ * Safe verification that collects all errors instead of failing fast.
235
+ * Returns a result object with either the verified data or an array of all errors.
236
+ *
237
+ * @param encodedSDJwt - The encoded SD-JWT to verify
238
+ * @param options - Verification options
239
+ * @returns A SafeVerifyResult containing either success data or collected errors
240
+ */
241
+ safeVerify(encodedSDJwt: string, options?: T & VerifierOptions): Promise<SafeVerifyResult<{
242
+ payload: ExtendedPayload;
243
+ header: Record<string, unknown> | undefined;
244
+ kb?: {
245
+ payload: Record<string, unknown>;
246
+ header: Record<string, unknown>;
247
+ };
248
+ }>>;
233
249
  private calculateSDHash;
234
250
  /**
235
251
  * This function is for validating the SD JWT
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _sd_jwt_types from '@sd-jwt/types';
2
- import { Signer, Base64urlString, Verifier, kbHeader, kbPayload, KbVerifier, JwtPayload, SDJWTCompact, Hasher, PresentationFrame, DisclosureFrame, HasherAndAlg, SaltGenerator, SDJWTConfig, KBOptions } from '@sd-jwt/types';
2
+ import { Signer, Base64urlString, Verifier, kbHeader, kbPayload, KbVerifier, JwtPayload, SDJWTCompact, Hasher, PresentationFrame, DisclosureFrame, HasherAndAlg, SaltGenerator, SDJWTConfig, KBOptions, SafeVerifyResult } from '@sd-jwt/types';
3
3
  import { Disclosure } from '@sd-jwt/utils';
4
4
 
5
5
  type FlattenJSONData = {
@@ -230,6 +230,22 @@ declare class SDJwtInstance<ExtendedPayload extends SdJwtPayload, T = unknown> {
230
230
  header: _sd_jwt_types.kbHeader;
231
231
  };
232
232
  }>;
233
+ /**
234
+ * Safe verification that collects all errors instead of failing fast.
235
+ * Returns a result object with either the verified data or an array of all errors.
236
+ *
237
+ * @param encodedSDJwt - The encoded SD-JWT to verify
238
+ * @param options - Verification options
239
+ * @returns A SafeVerifyResult containing either success data or collected errors
240
+ */
241
+ safeVerify(encodedSDJwt: string, options?: T & VerifierOptions): Promise<SafeVerifyResult<{
242
+ payload: ExtendedPayload;
243
+ header: Record<string, unknown> | undefined;
244
+ kb?: {
245
+ payload: Record<string, unknown>;
246
+ header: Record<string, unknown>;
247
+ };
248
+ }>>;
233
249
  private calculateSDHash;
234
250
  /**
235
251
  * This function is for validating the SD JWT
package/dist/index.js CHANGED
@@ -818,6 +818,161 @@ var _SDJwtInstance = class _SDJwtInstance {
818
818
  return { payload, header, kb };
819
819
  });
820
820
  }
821
+ /**
822
+ * Safe verification that collects all errors instead of failing fast.
823
+ * Returns a result object with either the verified data or an array of all errors.
824
+ *
825
+ * @param encodedSDJwt - The encoded SD-JWT to verify
826
+ * @param options - Verification options
827
+ * @returns A SafeVerifyResult containing either success data or collected errors
828
+ */
829
+ safeVerify(encodedSDJwt, options) {
830
+ return __async(this, null, function* () {
831
+ const errors = [];
832
+ const addError = (code, message, details) => {
833
+ errors.push({ code, message, details });
834
+ };
835
+ const exceptionToCode = (error) => {
836
+ const message = error.message.toLowerCase();
837
+ if (message.includes("hasher not found")) return "HASHER_NOT_FOUND";
838
+ if (message.includes("verifier not found")) return "VERIFIER_NOT_FOUND";
839
+ if (message.includes("invalid sd jwt") || message.includes("invalid jwt"))
840
+ return "INVALID_SD_JWT";
841
+ if (message.includes("not yet valid")) return "JWT_NOT_YET_VALID";
842
+ if (message.includes("expired")) return "JWT_EXPIRED";
843
+ if (message.includes("signature")) return "INVALID_JWT_SIGNATURE";
844
+ if (message.includes("missing required claim"))
845
+ return "MISSING_REQUIRED_CLAIMS";
846
+ if (message.includes("key binding jwt not exist"))
847
+ return "KEY_BINDING_JWT_MISSING";
848
+ if (message.includes("key binding verifier not found"))
849
+ return "KEY_BINDING_VERIFIER_NOT_FOUND";
850
+ if (message.includes("sd_hash")) return "KEY_BINDING_SD_HASH_INVALID";
851
+ return "UNKNOWN_ERROR";
852
+ };
853
+ if (!this.userConfig.hasher) {
854
+ addError("HASHER_NOT_FOUND", "Hasher not found");
855
+ }
856
+ if (!this.userConfig.verifier) {
857
+ addError("VERIFIER_NOT_FOUND", "Verifier not found");
858
+ }
859
+ if (errors.length > 0) {
860
+ return { success: false, errors };
861
+ }
862
+ const hasher = this.userConfig.hasher;
863
+ let sdjwt;
864
+ let payload;
865
+ let header;
866
+ try {
867
+ sdjwt = yield SDJwt.fromEncode(encodedSDJwt, hasher);
868
+ if (!sdjwt.jwt || !sdjwt.jwt.payload) {
869
+ addError("INVALID_SD_JWT", "Invalid SD JWT: missing JWT or payload");
870
+ }
871
+ } catch (error) {
872
+ addError(
873
+ "INVALID_SD_JWT",
874
+ `Failed to decode SD-JWT: ${error.message}`,
875
+ error
876
+ );
877
+ }
878
+ if (sdjwt == null ? void 0 : sdjwt.jwt) {
879
+ try {
880
+ const result = yield this.VerifyJwt(sdjwt.jwt, options);
881
+ header = result.header;
882
+ const claims = yield sdjwt.getClaims(hasher);
883
+ payload = claims;
884
+ } catch (error) {
885
+ const code = exceptionToCode(error);
886
+ addError(code, error.message, error);
887
+ }
888
+ }
889
+ if (sdjwt && (options == null ? void 0 : options.requiredClaimKeys)) {
890
+ try {
891
+ const keys = yield sdjwt.keys(hasher);
892
+ const missingKeys = options.requiredClaimKeys.filter(
893
+ (k) => !keys.includes(k)
894
+ );
895
+ if (missingKeys.length > 0) {
896
+ addError(
897
+ "MISSING_REQUIRED_CLAIMS",
898
+ `Missing required claim keys: ${missingKeys.join(", ")}`,
899
+ { missingKeys }
900
+ );
901
+ }
902
+ } catch (error) {
903
+ addError(
904
+ "UNKNOWN_ERROR",
905
+ `Failed to check required claims: ${error.message}`,
906
+ error
907
+ );
908
+ }
909
+ }
910
+ let kb;
911
+ if ((options == null ? void 0 : options.keyBindingNonce) && sdjwt) {
912
+ if (!sdjwt.kbJwt) {
913
+ addError("KEY_BINDING_JWT_MISSING", "Key Binding JWT not exist");
914
+ } else if (!this.userConfig.kbVerifier) {
915
+ addError(
916
+ "KEY_BINDING_VERIFIER_NOT_FOUND",
917
+ "Key Binding Verifier not found"
918
+ );
919
+ } else if (payload) {
920
+ try {
921
+ const kbResult = yield sdjwt.kbJwt.verifyKB({
922
+ verifier: this.userConfig.kbVerifier,
923
+ payload,
924
+ nonce: options.keyBindingNonce
925
+ });
926
+ if (!kbResult) {
927
+ addError(
928
+ "KEY_BINDING_SIGNATURE_INVALID",
929
+ "Key binding signature is not valid"
930
+ );
931
+ } else {
932
+ kb = kbResult;
933
+ const sdjwtWithoutKb = new SDJwt({
934
+ jwt: sdjwt.jwt,
935
+ disclosures: sdjwt.disclosures
936
+ });
937
+ const presentSdJwtWithoutKb = sdjwtWithoutKb.encodeSDJwt();
938
+ const sdHashStr = yield this.calculateSDHash(
939
+ presentSdJwtWithoutKb,
940
+ sdjwt,
941
+ hasher
942
+ );
943
+ if (sdHashStr !== kbResult.payload.sd_hash) {
944
+ addError(
945
+ "KEY_BINDING_SD_HASH_INVALID",
946
+ "Invalid sd_hash in Key Binding JWT",
947
+ {
948
+ expected: sdHashStr,
949
+ received: kbResult.payload.sd_hash
950
+ }
951
+ );
952
+ }
953
+ }
954
+ } catch (error) {
955
+ addError(
956
+ "KEY_BINDING_SIGNATURE_INVALID",
957
+ `Key binding verification failed: ${error.message}`,
958
+ error
959
+ );
960
+ }
961
+ }
962
+ }
963
+ if (errors.length > 0) {
964
+ return { success: false, errors };
965
+ }
966
+ return {
967
+ success: true,
968
+ data: {
969
+ payload,
970
+ header,
971
+ kb
972
+ }
973
+ };
974
+ });
975
+ }
821
976
  calculateSDHash(presentSdJwtWithoutKb, sdjwt, hasher) {
822
977
  return __async(this, null, function* () {
823
978
  if (!sdjwt.jwt || !sdjwt.jwt.payload) {
package/dist/index.mjs CHANGED
@@ -803,6 +803,161 @@ var _SDJwtInstance = class _SDJwtInstance {
803
803
  return { payload, header, kb };
804
804
  });
805
805
  }
806
+ /**
807
+ * Safe verification that collects all errors instead of failing fast.
808
+ * Returns a result object with either the verified data or an array of all errors.
809
+ *
810
+ * @param encodedSDJwt - The encoded SD-JWT to verify
811
+ * @param options - Verification options
812
+ * @returns A SafeVerifyResult containing either success data or collected errors
813
+ */
814
+ safeVerify(encodedSDJwt, options) {
815
+ return __async(this, null, function* () {
816
+ const errors = [];
817
+ const addError = (code, message, details) => {
818
+ errors.push({ code, message, details });
819
+ };
820
+ const exceptionToCode = (error) => {
821
+ const message = error.message.toLowerCase();
822
+ if (message.includes("hasher not found")) return "HASHER_NOT_FOUND";
823
+ if (message.includes("verifier not found")) return "VERIFIER_NOT_FOUND";
824
+ if (message.includes("invalid sd jwt") || message.includes("invalid jwt"))
825
+ return "INVALID_SD_JWT";
826
+ if (message.includes("not yet valid")) return "JWT_NOT_YET_VALID";
827
+ if (message.includes("expired")) return "JWT_EXPIRED";
828
+ if (message.includes("signature")) return "INVALID_JWT_SIGNATURE";
829
+ if (message.includes("missing required claim"))
830
+ return "MISSING_REQUIRED_CLAIMS";
831
+ if (message.includes("key binding jwt not exist"))
832
+ return "KEY_BINDING_JWT_MISSING";
833
+ if (message.includes("key binding verifier not found"))
834
+ return "KEY_BINDING_VERIFIER_NOT_FOUND";
835
+ if (message.includes("sd_hash")) return "KEY_BINDING_SD_HASH_INVALID";
836
+ return "UNKNOWN_ERROR";
837
+ };
838
+ if (!this.userConfig.hasher) {
839
+ addError("HASHER_NOT_FOUND", "Hasher not found");
840
+ }
841
+ if (!this.userConfig.verifier) {
842
+ addError("VERIFIER_NOT_FOUND", "Verifier not found");
843
+ }
844
+ if (errors.length > 0) {
845
+ return { success: false, errors };
846
+ }
847
+ const hasher = this.userConfig.hasher;
848
+ let sdjwt;
849
+ let payload;
850
+ let header;
851
+ try {
852
+ sdjwt = yield SDJwt.fromEncode(encodedSDJwt, hasher);
853
+ if (!sdjwt.jwt || !sdjwt.jwt.payload) {
854
+ addError("INVALID_SD_JWT", "Invalid SD JWT: missing JWT or payload");
855
+ }
856
+ } catch (error) {
857
+ addError(
858
+ "INVALID_SD_JWT",
859
+ `Failed to decode SD-JWT: ${error.message}`,
860
+ error
861
+ );
862
+ }
863
+ if (sdjwt == null ? void 0 : sdjwt.jwt) {
864
+ try {
865
+ const result = yield this.VerifyJwt(sdjwt.jwt, options);
866
+ header = result.header;
867
+ const claims = yield sdjwt.getClaims(hasher);
868
+ payload = claims;
869
+ } catch (error) {
870
+ const code = exceptionToCode(error);
871
+ addError(code, error.message, error);
872
+ }
873
+ }
874
+ if (sdjwt && (options == null ? void 0 : options.requiredClaimKeys)) {
875
+ try {
876
+ const keys = yield sdjwt.keys(hasher);
877
+ const missingKeys = options.requiredClaimKeys.filter(
878
+ (k) => !keys.includes(k)
879
+ );
880
+ if (missingKeys.length > 0) {
881
+ addError(
882
+ "MISSING_REQUIRED_CLAIMS",
883
+ `Missing required claim keys: ${missingKeys.join(", ")}`,
884
+ { missingKeys }
885
+ );
886
+ }
887
+ } catch (error) {
888
+ addError(
889
+ "UNKNOWN_ERROR",
890
+ `Failed to check required claims: ${error.message}`,
891
+ error
892
+ );
893
+ }
894
+ }
895
+ let kb;
896
+ if ((options == null ? void 0 : options.keyBindingNonce) && sdjwt) {
897
+ if (!sdjwt.kbJwt) {
898
+ addError("KEY_BINDING_JWT_MISSING", "Key Binding JWT not exist");
899
+ } else if (!this.userConfig.kbVerifier) {
900
+ addError(
901
+ "KEY_BINDING_VERIFIER_NOT_FOUND",
902
+ "Key Binding Verifier not found"
903
+ );
904
+ } else if (payload) {
905
+ try {
906
+ const kbResult = yield sdjwt.kbJwt.verifyKB({
907
+ verifier: this.userConfig.kbVerifier,
908
+ payload,
909
+ nonce: options.keyBindingNonce
910
+ });
911
+ if (!kbResult) {
912
+ addError(
913
+ "KEY_BINDING_SIGNATURE_INVALID",
914
+ "Key binding signature is not valid"
915
+ );
916
+ } else {
917
+ kb = kbResult;
918
+ const sdjwtWithoutKb = new SDJwt({
919
+ jwt: sdjwt.jwt,
920
+ disclosures: sdjwt.disclosures
921
+ });
922
+ const presentSdJwtWithoutKb = sdjwtWithoutKb.encodeSDJwt();
923
+ const sdHashStr = yield this.calculateSDHash(
924
+ presentSdJwtWithoutKb,
925
+ sdjwt,
926
+ hasher
927
+ );
928
+ if (sdHashStr !== kbResult.payload.sd_hash) {
929
+ addError(
930
+ "KEY_BINDING_SD_HASH_INVALID",
931
+ "Invalid sd_hash in Key Binding JWT",
932
+ {
933
+ expected: sdHashStr,
934
+ received: kbResult.payload.sd_hash
935
+ }
936
+ );
937
+ }
938
+ }
939
+ } catch (error) {
940
+ addError(
941
+ "KEY_BINDING_SIGNATURE_INVALID",
942
+ `Key binding verification failed: ${error.message}`,
943
+ error
944
+ );
945
+ }
946
+ }
947
+ }
948
+ if (errors.length > 0) {
949
+ return { success: false, errors };
950
+ }
951
+ return {
952
+ success: true,
953
+ data: {
954
+ payload,
955
+ header,
956
+ kb
957
+ }
958
+ };
959
+ });
960
+ }
806
961
  calculateSDHash(presentSdJwtWithoutKb, sdjwt, hasher) {
807
962
  return __async(this, null, function* () {
808
963
  if (!sdjwt.jwt || !sdjwt.jwt.payload) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sd-jwt/core",
3
- "version": "0.19.1-next.0+15c4776",
3
+ "version": "0.19.1-next.2+c96a9a0",
4
4
  "description": "sd-jwt draft 7 implementation in typescript",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -37,13 +37,13 @@
37
37
  },
38
38
  "license": "Apache-2.0",
39
39
  "devDependencies": {
40
- "@sd-jwt/crypto-nodejs": "0.19.1-next.0+15c4776"
40
+ "@sd-jwt/crypto-nodejs": "0.19.1-next.2+c96a9a0"
41
41
  },
42
42
  "dependencies": {
43
- "@sd-jwt/decode": "0.19.1-next.0+15c4776",
44
- "@sd-jwt/present": "0.19.1-next.0+15c4776",
45
- "@sd-jwt/types": "0.19.1-next.0+15c4776",
46
- "@sd-jwt/utils": "0.19.1-next.0+15c4776"
43
+ "@sd-jwt/decode": "0.19.1-next.2+c96a9a0",
44
+ "@sd-jwt/present": "0.19.1-next.2+c96a9a0",
45
+ "@sd-jwt/types": "0.19.1-next.2+c96a9a0",
46
+ "@sd-jwt/utils": "0.19.1-next.2+c96a9a0"
47
47
  },
48
48
  "publishConfig": {
49
49
  "access": "public"
@@ -61,5 +61,5 @@
61
61
  "esm"
62
62
  ]
63
63
  },
64
- "gitHead": "15c477615ababfd649c7a542629868d1d08a3048"
64
+ "gitHead": "c96a9a0e73d09d2978db5004a3f7ca307bb926f3"
65
65
  }
package/src/index.ts CHANGED
@@ -7,9 +7,12 @@ import {
7
7
  KB_JWT_TYP,
8
8
  type KBOptions,
9
9
  type PresentationFrame,
10
+ type SafeVerifyResult,
10
11
  type SDJWTCompact,
11
12
  type SDJWTConfig,
12
13
  type Signer,
14
+ type VerificationError,
15
+ type VerificationErrorCode,
13
16
  } from '@sd-jwt/types';
14
17
  import {
15
18
  base64urlDecode,
@@ -259,6 +262,202 @@ export class SDJwtInstance<ExtendedPayload extends SdJwtPayload, T = unknown> {
259
262
  return { payload, header, kb };
260
263
  }
261
264
 
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
+
262
461
  private async calculateSDHash(
263
462
  presentSdJwtWithoutKb: string,
264
463
  sdjwt: SDJwt,
@@ -627,4 +627,156 @@ describe('index', () => {
627
627
  expect(decode).toBeDefined();
628
628
  },
629
629
  );
630
+
631
+ test('safeVerify - success case', async () => {
632
+ const { signer, verifier } = createSignerVerifier();
633
+ const sdjwt = new SDJwtInstance<SdJwtPayload>({
634
+ signer,
635
+ signAlg: 'EdDSA',
636
+ verifier,
637
+ hasher: digest,
638
+ saltGenerator: generateSalt,
639
+ });
640
+
641
+ const credential = await sdjwt.issue(
642
+ {
643
+ foo: 'bar',
644
+ iss: 'Issuer',
645
+ iat: Math.floor(Date.now() / 1000),
646
+ },
647
+ {
648
+ _sd: ['foo'],
649
+ },
650
+ );
651
+
652
+ const result = await sdjwt.safeVerify(credential);
653
+
654
+ expect(result.success).toBe(true);
655
+ if (result.success) {
656
+ expect(result.data.payload).toBeDefined();
657
+ expect(result.data.payload.foo).toBe('bar');
658
+ }
659
+ });
660
+
661
+ test('safeVerify - collect multiple errors', async () => {
662
+ const sdjwt = new SDJwtInstance<SdJwtPayload>({});
663
+
664
+ const result = await sdjwt.safeVerify('invalid.jwt.token');
665
+
666
+ expect(result.success).toBe(false);
667
+ if (!result.success) {
668
+ // Should have multiple errors: hasher not found, verifier not found
669
+ expect(result.errors.length).toBeGreaterThanOrEqual(2);
670
+ const errorCodes = result.errors.map((e) => e.code);
671
+ expect(errorCodes).toContain('HASHER_NOT_FOUND');
672
+ expect(errorCodes).toContain('VERIFIER_NOT_FOUND');
673
+ }
674
+ });
675
+
676
+ test('safeVerify - invalid signature error', async () => {
677
+ const { signer } = createSignerVerifier();
678
+ const { verifier: wrongVerifier } = createSignerVerifier(); // Different key pair
679
+
680
+ const issuer = new SDJwtInstance<SdJwtPayload>({
681
+ signer,
682
+ signAlg: 'EdDSA',
683
+ hasher: digest,
684
+ saltGenerator: generateSalt,
685
+ });
686
+
687
+ const verifierInstance = new SDJwtInstance<SdJwtPayload>({
688
+ verifier: wrongVerifier,
689
+ hasher: digest,
690
+ });
691
+
692
+ const credential = await issuer.issue(
693
+ {
694
+ foo: 'bar',
695
+ iss: 'Issuer',
696
+ iat: Math.floor(Date.now() / 1000),
697
+ },
698
+ {
699
+ _sd: ['foo'],
700
+ },
701
+ );
702
+
703
+ const result = await verifierInstance.safeVerify(credential);
704
+
705
+ expect(result.success).toBe(false);
706
+ if (!result.success) {
707
+ // The error message from the verifier contains 'signature' which maps to INVALID_JWT_SIGNATURE
708
+ const hasSignatureError = result.errors.some(
709
+ (e) =>
710
+ e.code === 'INVALID_JWT_SIGNATURE' ||
711
+ e.message.toLowerCase().includes('signature'),
712
+ );
713
+ expect(hasSignatureError).toBe(true);
714
+ }
715
+ });
716
+
717
+ test('safeVerify - expired JWT error', async () => {
718
+ const { signer, verifier } = createSignerVerifier();
719
+ const sdjwt = new SDJwtInstance<SdJwtPayload>({
720
+ signer,
721
+ signAlg: 'EdDSA',
722
+ verifier,
723
+ hasher: digest,
724
+ saltGenerator: generateSalt,
725
+ });
726
+
727
+ const credential = await sdjwt.issue(
728
+ {
729
+ foo: 'bar',
730
+ iss: 'Issuer',
731
+ iat: Math.floor(Date.now() / 1000) - 3600, // 1 hour ago
732
+ exp: Math.floor(Date.now() / 1000) - 1800, // Expired 30 min ago
733
+ },
734
+ {
735
+ _sd: ['foo'],
736
+ },
737
+ );
738
+
739
+ const result = await sdjwt.safeVerify(credential);
740
+
741
+ expect(result.success).toBe(false);
742
+ if (!result.success) {
743
+ expect(result.errors.some((e) => e.code === 'JWT_EXPIRED')).toBe(true);
744
+ }
745
+ });
746
+
747
+ test('safeVerify - missing required claims error', async () => {
748
+ const { signer, verifier } = createSignerVerifier();
749
+ const sdjwt = new SDJwtInstance<SdJwtPayload>({
750
+ signer,
751
+ signAlg: 'EdDSA',
752
+ verifier,
753
+ hasher: digest,
754
+ saltGenerator: generateSalt,
755
+ });
756
+
757
+ const credential = await sdjwt.issue(
758
+ {
759
+ foo: 'bar',
760
+ iss: 'Issuer',
761
+ iat: Math.floor(Date.now() / 1000),
762
+ },
763
+ {
764
+ _sd: ['foo'],
765
+ },
766
+ );
767
+
768
+ // Present without disclosing 'foo'
769
+ const presentation = await sdjwt.present(credential, {});
770
+
771
+ const result = await sdjwt.safeVerify(presentation, {
772
+ requiredClaimKeys: ['foo', 'missing_claim'],
773
+ });
774
+
775
+ expect(result.success).toBe(false);
776
+ if (!result.success) {
777
+ expect(
778
+ result.errors.some((e) => e.code === 'MISSING_REQUIRED_CLAIMS'),
779
+ ).toBe(true);
780
+ }
781
+ });
630
782
  });