@sd-jwt/core 0.1.0 → 0.1.1-next.0

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.
Files changed (170) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/LICENSE +201 -0
  3. package/README.md +106 -0
  4. package/dist/index.d.mts +659 -0
  5. package/dist/index.d.ts +659 -0
  6. package/dist/index.js +1906 -0
  7. package/dist/index.mjs +1846 -0
  8. package/package.json +59 -47
  9. package/src/decode/decode.ts +376 -0
  10. package/src/decode/index.ts +1 -0
  11. package/src/decoy.ts +15 -0
  12. package/src/flattenJSON.ts +99 -0
  13. package/src/generalJSON.ts +148 -0
  14. package/src/index.ts +945 -0
  15. package/src/jwt.ts +190 -0
  16. package/src/kbjwt.ts +81 -0
  17. package/src/present/index.ts +1 -0
  18. package/src/present/present.ts +210 -0
  19. package/src/sdjwt.ts +354 -0
  20. package/src/test/decode/decode.spec.ts +202 -0
  21. package/src/test/decoy.spec.ts +30 -0
  22. package/src/test/flattenJSON.spec.ts +122 -0
  23. package/src/test/generalJSON.spec.ts +198 -0
  24. package/src/test/index.spec.ts +974 -0
  25. package/src/test/jwt.spec.ts +301 -0
  26. package/src/test/kbjwt.spec.ts +565 -0
  27. package/src/test/pass.spec.ts +7 -0
  28. package/src/test/present/present.spec.ts +305 -0
  29. package/src/test/sdjwt.spec.ts +384 -0
  30. package/src/test/types/type.spec.ts +88 -0
  31. package/src/test/utils/base64url.spec.ts +33 -0
  32. package/src/test/utils/disclosure.spec.ts +170 -0
  33. package/src/test/utils/error.spec.ts +15 -0
  34. package/src/types/index.ts +2 -0
  35. package/src/types/type.ts +249 -0
  36. package/src/types/verification-error.ts +55 -0
  37. package/src/utils/base64url.ts +6 -0
  38. package/src/utils/disclosure.ts +101 -0
  39. package/src/utils/error.ts +25 -0
  40. package/src/utils/index.ts +3 -0
  41. package/src/utils/strict-json.ts +17 -0
  42. package/test/app-e2e.spec.ts +292 -0
  43. package/test/array_data_types.json +31 -0
  44. package/test/array_full_sd.json +21 -0
  45. package/test/array_in_sd.json +13 -0
  46. package/test/array_nested_in_plain.json +29 -0
  47. package/test/array_none_disclosed.json +17 -0
  48. package/test/array_of_nulls.json +15 -0
  49. package/test/array_of_objects.json +65 -0
  50. package/test/array_of_scalars.json +19 -0
  51. package/test/array_recursive_sd.json +35 -0
  52. package/test/array_recursive_sd_some_disclosed.json +63 -0
  53. package/test/complex.json +43 -0
  54. package/test/header_mod.json +48 -0
  55. package/test/json_serialization.json +48 -0
  56. package/test/key_binding.json +48 -0
  57. package/test/no_sd.json +36 -0
  58. package/test/object_data_types.json +62 -0
  59. package/test/recursions.json +117 -0
  60. package/test/rfc9901-validation.spec.ts +150 -0
  61. package/tsconfig.json +7 -0
  62. package/vitest.config.mts +4 -0
  63. package/build/base64url.d.ts +0 -28
  64. package/build/base64url.js +0 -40
  65. package/build/base64url.js.map +0 -1
  66. package/build/hasherAlgorithm.d.ts +0 -70
  67. package/build/hasherAlgorithm.js +0 -75
  68. package/build/hasherAlgorithm.js.map +0 -1
  69. package/build/index.d.ts +0 -11
  70. package/build/index.js +0 -20
  71. package/build/index.js.map +0 -1
  72. package/build/jwt/compact.d.ts +0 -6
  73. package/build/jwt/compact.js +0 -27
  74. package/build/jwt/compact.js.map +0 -1
  75. package/build/jwt/error.d.ts +0 -2
  76. package/build/jwt/error.js +0 -7
  77. package/build/jwt/error.js.map +0 -1
  78. package/build/jwt/index.d.ts +0 -3
  79. package/build/jwt/index.js +0 -20
  80. package/build/jwt/index.js.map +0 -1
  81. package/build/jwt/jwt.d.ts +0 -204
  82. package/build/jwt/jwt.js +0 -324
  83. package/build/jwt/jwt.js.map +0 -1
  84. package/build/keyBinding/index.d.ts +0 -1
  85. package/build/keyBinding/index.js +0 -18
  86. package/build/keyBinding/index.js.map +0 -1
  87. package/build/keyBinding/keyBinding.d.ts +0 -56
  88. package/build/keyBinding/keyBinding.js +0 -99
  89. package/build/keyBinding/keyBinding.js.map +0 -1
  90. package/build/sdJwt/compact.d.ts +0 -8
  91. package/build/sdJwt/compact.js +0 -39
  92. package/build/sdJwt/compact.js.map +0 -1
  93. package/build/sdJwt/decoys.d.ts +0 -2
  94. package/build/sdJwt/decoys.js +0 -35
  95. package/build/sdJwt/decoys.js.map +0 -1
  96. package/build/sdJwt/disclosureFrame.d.ts +0 -7
  97. package/build/sdJwt/disclosureFrame.js +0 -89
  98. package/build/sdJwt/disclosureFrame.js.map +0 -1
  99. package/build/sdJwt/disclosureMapping.d.ts +0 -44
  100. package/build/sdJwt/disclosureMapping.js +0 -289
  101. package/build/sdJwt/disclosureMapping.js.map +0 -1
  102. package/build/sdJwt/disclosures.d.ts +0 -14
  103. package/build/sdJwt/disclosures.js +0 -59
  104. package/build/sdJwt/disclosures.js.map +0 -1
  105. package/build/sdJwt/error.d.ts +0 -2
  106. package/build/sdJwt/error.js +0 -7
  107. package/build/sdJwt/error.js.map +0 -1
  108. package/build/sdJwt/index.d.ts +0 -7
  109. package/build/sdJwt/index.js +0 -24
  110. package/build/sdJwt/index.js.map +0 -1
  111. package/build/sdJwt/presentationFrame.d.ts +0 -4
  112. package/build/sdJwt/presentationFrame.js +0 -73
  113. package/build/sdJwt/presentationFrame.js.map +0 -1
  114. package/build/sdJwt/sdJwt.d.ts +0 -204
  115. package/build/sdJwt/sdJwt.js +0 -383
  116. package/build/sdJwt/sdJwt.js.map +0 -1
  117. package/build/sdJwt/swapClaim.d.ts +0 -3
  118. package/build/sdJwt/swapClaim.js +0 -90
  119. package/build/sdJwt/swapClaim.js.map +0 -1
  120. package/build/sdJwt/types.d.ts +0 -5
  121. package/build/sdJwt/types.js +0 -3
  122. package/build/sdJwt/types.js.map +0 -1
  123. package/build/sdJwtVc/error.d.ts +0 -2
  124. package/build/sdJwtVc/error.js +0 -7
  125. package/build/sdJwtVc/error.js.map +0 -1
  126. package/build/sdJwtVc/index.d.ts +0 -2
  127. package/build/sdJwtVc/index.js +0 -19
  128. package/build/sdJwtVc/index.js.map +0 -1
  129. package/build/sdJwtVc/sdJwtVc.d.ts +0 -47
  130. package/build/sdJwtVc/sdJwtVc.js +0 -125
  131. package/build/sdJwtVc/sdJwtVc.js.map +0 -1
  132. package/build/signatureAndEncryptionAlgorithm.d.ts +0 -105
  133. package/build/signatureAndEncryptionAlgorithm.js +0 -110
  134. package/build/signatureAndEncryptionAlgorithm.js.map +0 -1
  135. package/build/types/disclosure.d.ts +0 -5
  136. package/build/types/disclosure.js +0 -3
  137. package/build/types/disclosure.js.map +0 -1
  138. package/build/types/frame.d.ts +0 -5
  139. package/build/types/frame.js +0 -3
  140. package/build/types/frame.js.map +0 -1
  141. package/build/types/hasher.d.ts +0 -14
  142. package/build/types/hasher.js +0 -3
  143. package/build/types/hasher.js.map +0 -1
  144. package/build/types/index.d.ts +0 -6
  145. package/build/types/index.js +0 -23
  146. package/build/types/index.js.map +0 -1
  147. package/build/types/present.d.ts +0 -2
  148. package/build/types/present.js +0 -3
  149. package/build/types/present.js.map +0 -1
  150. package/build/types/saltGenerator.d.ts +0 -17
  151. package/build/types/saltGenerator.js +0 -3
  152. package/build/types/saltGenerator.js.map +0 -1
  153. package/build/types/signer.d.ts +0 -2
  154. package/build/types/signer.js +0 -3
  155. package/build/types/signer.js.map +0 -1
  156. package/build/types/utils.d.ts +0 -5
  157. package/build/types/utils.js +0 -3
  158. package/build/types/utils.js.map +0 -1
  159. package/build/types/verifier.d.ts +0 -14
  160. package/build/types/verifier.js +0 -3
  161. package/build/types/verifier.js.map +0 -1
  162. package/build/utils/index.d.ts +0 -2
  163. package/build/utils/index.js +0 -19
  164. package/build/utils/index.js.map +0 -1
  165. package/build/utils/traverse.d.ts +0 -8
  166. package/build/utils/traverse.js +0 -29
  167. package/build/utils/traverse.js.map +0 -1
  168. package/build/utils/utils.d.ts +0 -7
  169. package/build/utils/utils.js +0 -109
  170. package/build/utils/utils.js.map +0 -1
package/src/index.ts ADDED
@@ -0,0 +1,945 @@
1
+ import { getSDAlgAndPayload } from './decode';
2
+ import { FlattenJSON } from './flattenJSON';
3
+ import { GeneralJSON } from './generalJSON';
4
+ import { Jwt, type VerifierOptions } from './jwt';
5
+ import { KBJwt } from './kbjwt';
6
+ import { pack, SDJwt } from './sdjwt';
7
+ import {
8
+ type DisclosureFrame,
9
+ type Hasher,
10
+ IANA_HASH_ALGORITHMS,
11
+ KB_JWT_TYP,
12
+ type KBOptions,
13
+ type PresentationFrame,
14
+ type SafeVerifyResult,
15
+ SD_DECOY,
16
+ SD_DIGEST,
17
+ type SDJWTCompact,
18
+ type SDJWTConfig,
19
+ type Signer,
20
+ type VerificationError,
21
+ type VerificationErrorCode,
22
+ } from './types';
23
+ import {
24
+ base64urlEncode,
25
+ ensureError,
26
+ SDJWTException,
27
+ uint8ArrayToBase64Url,
28
+ } from './utils';
29
+ import { decodeBase64urlJsonStrict } from './utils/strict-json';
30
+
31
+ export * from './decode';
32
+ export * from './decoy';
33
+ export * from './flattenJSON';
34
+ export * from './generalJSON';
35
+ export * from './jwt';
36
+ export * from './kbjwt';
37
+ export * from './present';
38
+ export * from './sdjwt';
39
+ // Re-export all types, utils, decode, and present functionality
40
+ export * from './types';
41
+ export * from './utils';
42
+
43
+ export type SdJwtPayload = Record<string, unknown>;
44
+
45
+ export class SDJwtInstance<ExtendedPayload extends SdJwtPayload, T = unknown> {
46
+ //header type
47
+ protected type?: string;
48
+
49
+ public static readonly DEFAULT_hashAlg = 'sha-256';
50
+
51
+ protected userConfig: SDJWTConfig<T> = {};
52
+
53
+ constructor(userConfig?: SDJWTConfig<T>) {
54
+ if (userConfig) {
55
+ if (
56
+ userConfig.hashAlg &&
57
+ !IANA_HASH_ALGORITHMS.includes(userConfig.hashAlg)
58
+ ) {
59
+ throw new SDJWTException(
60
+ `Invalid hash algorithm: ${userConfig.hashAlg}`,
61
+ );
62
+ }
63
+ this.userConfig = userConfig;
64
+ }
65
+ }
66
+
67
+ private async createKBJwt(
68
+ options: KBOptions,
69
+ sdHash: string,
70
+ ): Promise<KBJwt> {
71
+ if (!this.userConfig.kbSigner) {
72
+ throw new SDJWTException('Key Binding Signer not found');
73
+ }
74
+ if (!this.userConfig.kbSignAlg) {
75
+ throw new SDJWTException('Key Binding sign algorithm not specified');
76
+ }
77
+
78
+ const { payload } = options;
79
+ const kbJwt = new KBJwt({
80
+ header: {
81
+ typ: KB_JWT_TYP,
82
+ alg: this.userConfig.kbSignAlg,
83
+ },
84
+ payload: { ...payload, sd_hash: sdHash },
85
+ });
86
+
87
+ await kbJwt.sign(this.userConfig.kbSigner);
88
+ return kbJwt;
89
+ }
90
+
91
+ private async SignJwt(jwt: Jwt) {
92
+ if (!this.userConfig.signer) {
93
+ throw new SDJWTException('Signer not found');
94
+ }
95
+ await jwt.sign(this.userConfig.signer);
96
+ return jwt;
97
+ }
98
+
99
+ private async VerifyJwt(jwt: Jwt, options?: T & VerifierOptions) {
100
+ if (!this.userConfig.verifier) {
101
+ throw new SDJWTException('Verifier not found');
102
+ }
103
+ return jwt.verify<T>(this.userConfig.verifier, options);
104
+ }
105
+
106
+ public async issue<Payload extends ExtendedPayload>(
107
+ payload: Payload,
108
+ disclosureFrame?: DisclosureFrame<Payload>,
109
+ options?: {
110
+ header?: object; // This is for customizing the header of the jwt
111
+ },
112
+ ): Promise<SDJWTCompact> {
113
+ if (!this.userConfig.hasher) {
114
+ throw new SDJWTException('Hasher not found');
115
+ }
116
+
117
+ if (!this.userConfig.saltGenerator) {
118
+ throw new SDJWTException('SaltGenerator not found');
119
+ }
120
+
121
+ if (!this.userConfig.signAlg) {
122
+ throw new SDJWTException('sign alogrithm not specified');
123
+ }
124
+
125
+ this.validateReservedFields<Payload>(payload);
126
+ this.validateDisclosureFrame<Payload>(disclosureFrame);
127
+
128
+ const hasher = this.userConfig.hasher;
129
+ const hashAlg = this.userConfig.hashAlg ?? SDJwtInstance.DEFAULT_hashAlg;
130
+
131
+ const { packedClaims, disclosures } = await pack(
132
+ payload,
133
+ disclosureFrame,
134
+ { hasher, alg: hashAlg },
135
+ this.userConfig.saltGenerator,
136
+ );
137
+ const alg = this.userConfig.signAlg;
138
+ const OptionHeader = options?.header ?? {};
139
+ const CustomHeader = this.userConfig.omitTyp
140
+ ? OptionHeader
141
+ : { typ: this.type, ...OptionHeader };
142
+ const header = { ...CustomHeader, alg };
143
+ const jwt = new Jwt({
144
+ header,
145
+ payload: {
146
+ ...packedClaims,
147
+ _sd_alg: disclosureFrame ? hashAlg : undefined,
148
+ },
149
+ });
150
+ await this.SignJwt(jwt);
151
+
152
+ const sdJwt = new SDJwt({
153
+ jwt,
154
+ disclosures,
155
+ });
156
+
157
+ return sdJwt.encodeSDJwt();
158
+ }
159
+
160
+ /**
161
+ * Validates if the payload contains any reserved claim names. If so it will throw an error.
162
+ * @param payload
163
+ * @returns
164
+ */
165
+ protected validateReservedFields<T extends ExtendedPayload>(payload: T) {
166
+ const reservedFields = new Set([SD_DIGEST, '_sd_alg', SD_DECOY]);
167
+
168
+ const visit = (node: unknown) => {
169
+ if (!node || typeof node !== 'object') {
170
+ return;
171
+ }
172
+
173
+ for (const [key, value] of Object.entries(
174
+ node as Record<string, unknown>,
175
+ )) {
176
+ if (reservedFields.has(key)) {
177
+ throw new SDJWTException(
178
+ `Reserved field name "${key}" is not allowed`,
179
+ );
180
+ }
181
+ visit(value);
182
+ }
183
+ };
184
+
185
+ visit(payload);
186
+ }
187
+
188
+ protected validateDisclosureFrame<T extends ExtendedPayload>(
189
+ _disclosureFrame?: DisclosureFrame<T>,
190
+ ) {
191
+ return;
192
+ }
193
+
194
+ public async present<T extends Record<string, unknown>>(
195
+ encodedSDJwt: string,
196
+ presentationFrame?: PresentationFrame<T>,
197
+ options?: {
198
+ kb?: KBOptions;
199
+ },
200
+ ): Promise<SDJWTCompact> {
201
+ if (!this.userConfig.hasher) {
202
+ throw new SDJWTException('Hasher not found');
203
+ }
204
+ const hasher = this.userConfig.hasher;
205
+
206
+ const sdjwt = await SDJwt.fromEncode(encodedSDJwt, hasher);
207
+
208
+ if (!sdjwt.jwt?.payload) throw new SDJWTException('Payload not found');
209
+ const presentSdJwtWithoutKb = await sdjwt.present(
210
+ presentationFrame,
211
+ hasher,
212
+ );
213
+
214
+ if (!options?.kb) {
215
+ return presentSdJwtWithoutKb;
216
+ }
217
+
218
+ const sdHashStr = await this.calculateSDHash(
219
+ presentSdJwtWithoutKb,
220
+ sdjwt,
221
+ hasher,
222
+ );
223
+
224
+ sdjwt.kbJwt = await this.createKBJwt(options.kb, sdHashStr);
225
+ return sdjwt.present(presentationFrame, hasher);
226
+ }
227
+
228
+ // This function is for verifying the SD JWT
229
+ // If requiredClaimKeys is provided, it will check if the required claim keys are presentation in the SD JWT
230
+ // If requireKeyBindings is true, it will check if the key binding JWT is presentation and verify it
231
+ public async verify(encodedSDJwt: string, options?: T & VerifierOptions) {
232
+ if (!this.userConfig.hasher) {
233
+ throw new SDJWTException('Hasher not found');
234
+ }
235
+ const hasher = this.userConfig.hasher;
236
+
237
+ const sdjwt = await SDJwt.fromEncode(encodedSDJwt, hasher);
238
+ if (!sdjwt.jwt || !sdjwt.jwt.payload) {
239
+ throw new SDJWTException('Invalid SD JWT');
240
+ }
241
+ const { payload, header } = await this.validate(encodedSDJwt, options);
242
+
243
+ if (options?.requiredClaimKeys) {
244
+ const keys = await sdjwt.keys(hasher);
245
+ const missingKeys = options.requiredClaimKeys.filter(
246
+ (k) => !keys.includes(k),
247
+ );
248
+ if (missingKeys.length > 0) {
249
+ throw new SDJWTException(
250
+ `Missing required claim keys: ${missingKeys.join(', ')}`,
251
+ );
252
+ }
253
+ }
254
+
255
+ if (!options?.keyBindingNonce) {
256
+ return { payload, header };
257
+ }
258
+
259
+ if (!sdjwt.kbJwt) {
260
+ throw new SDJWTException('Key Binding JWT not exist');
261
+ }
262
+ if (!this.userConfig.kbVerifier) {
263
+ throw new SDJWTException('Key Binding Verifier not found');
264
+ }
265
+ const kb = await sdjwt.kbJwt.verifyKB({
266
+ verifier: this.userConfig.kbVerifier,
267
+ payload,
268
+ nonce: options.keyBindingNonce,
269
+ options,
270
+ });
271
+ if (!kb) {
272
+ throw new Error('signature is not valid');
273
+ }
274
+
275
+ const sdHashfromKb = kb.payload.sd_hash;
276
+ const sdjwtWithoutKb = new SDJwt({
277
+ jwt: sdjwt.jwt,
278
+ disclosures: sdjwt.disclosures,
279
+ });
280
+
281
+ const presentSdJwtWithoutKb = sdjwtWithoutKb.encodeSDJwt();
282
+ const sdHashStr = await this.calculateSDHash(
283
+ presentSdJwtWithoutKb,
284
+ sdjwt,
285
+ hasher,
286
+ );
287
+
288
+ if (sdHashStr !== sdHashfromKb) {
289
+ throw new SDJWTException('Invalid sd_hash in Key Binding JWT');
290
+ }
291
+
292
+ return { payload, header, kb };
293
+ }
294
+
295
+ /**
296
+ * Safe verification that collects all errors instead of failing fast.
297
+ * Returns a result object with either the verified data or an array of all errors.
298
+ *
299
+ * @param encodedSDJwt - The encoded SD-JWT to verify
300
+ * @param options - Verification options
301
+ * @returns A SafeVerifyResult containing either success data or collected errors
302
+ */
303
+ public async safeVerify(
304
+ encodedSDJwt: string,
305
+ options?: T & VerifierOptions,
306
+ ): Promise<
307
+ SafeVerifyResult<{
308
+ payload: ExtendedPayload;
309
+ header: Record<string, unknown> | undefined;
310
+ kb?: {
311
+ payload: Record<string, unknown>;
312
+ header: Record<string, unknown>;
313
+ };
314
+ }>
315
+ > {
316
+ const errors: VerificationError[] = [];
317
+
318
+ // Helper to add errors
319
+ const addError = (
320
+ code: VerificationErrorCode,
321
+ message: string,
322
+ details?: unknown,
323
+ ) => {
324
+ errors.push({ code, message, details });
325
+ };
326
+
327
+ // Helper to convert exception to error code
328
+ const exceptionToCode = (error: Error): VerificationErrorCode => {
329
+ const message = error.message.toLowerCase();
330
+ if (message.includes('hasher not found')) return 'HASHER_NOT_FOUND';
331
+ if (message.includes('verifier not found')) return 'VERIFIER_NOT_FOUND';
332
+ if (message.includes('invalid sd jwt') || message.includes('invalid jwt'))
333
+ return 'INVALID_SD_JWT';
334
+ if (message.includes('not yet valid')) return 'JWT_NOT_YET_VALID';
335
+ if (message.includes('expired')) return 'JWT_EXPIRED';
336
+ if (message.includes('signature')) return 'INVALID_JWT_SIGNATURE';
337
+ if (message.includes('missing required claim'))
338
+ return 'MISSING_REQUIRED_CLAIMS';
339
+ if (message.includes('key binding jwt not exist'))
340
+ return 'KEY_BINDING_JWT_MISSING';
341
+ if (message.includes('key binding verifier not found'))
342
+ return 'KEY_BINDING_VERIFIER_NOT_FOUND';
343
+ if (message.includes('sd_hash')) return 'KEY_BINDING_SD_HASH_INVALID';
344
+ return 'UNKNOWN_ERROR';
345
+ };
346
+
347
+ // Check basic configuration first
348
+ if (!this.userConfig.hasher) {
349
+ addError('HASHER_NOT_FOUND', 'Hasher not found');
350
+ }
351
+ if (!this.userConfig.verifier) {
352
+ addError('VERIFIER_NOT_FOUND', 'Verifier not found');
353
+ }
354
+
355
+ // If basic config is missing, return early
356
+ if (errors.length > 0) {
357
+ return { success: false, errors };
358
+ }
359
+
360
+ if (!this.userConfig.hasher) {
361
+ throw new SDJWTException('Hasher not found');
362
+ }
363
+
364
+ // hasher and verifier are guaranteed to be defined here
365
+ const hasher = this.userConfig.hasher;
366
+
367
+ // Try to decode and validate the SD-JWT
368
+ let sdjwt: SDJwt | undefined;
369
+ let payload: ExtendedPayload | undefined;
370
+ let header: Record<string, unknown> | undefined;
371
+
372
+ try {
373
+ sdjwt = await SDJwt.fromEncode(encodedSDJwt, hasher);
374
+ if (!sdjwt.jwt || !sdjwt.jwt.payload) {
375
+ addError('INVALID_SD_JWT', 'Invalid SD JWT: missing JWT or payload');
376
+ }
377
+ } catch (e) {
378
+ const error = ensureError(e);
379
+ addError(
380
+ 'INVALID_SD_JWT',
381
+ `Failed to decode SD-JWT: ${error.message}`,
382
+ error,
383
+ );
384
+ }
385
+
386
+ // Validate signature and claims
387
+ if (sdjwt?.jwt) {
388
+ try {
389
+ const result = await this.VerifyJwt(sdjwt.jwt, options);
390
+ header = result.header;
391
+ const claims = await sdjwt.getClaims(hasher);
392
+ payload = claims as ExtendedPayload;
393
+ } catch (e) {
394
+ const error = ensureError(e);
395
+ const code = exceptionToCode(error);
396
+ addError(code, error.message, error);
397
+ }
398
+ }
399
+
400
+ // Check required claim keys
401
+ if (sdjwt && options?.requiredClaimKeys) {
402
+ try {
403
+ const keys = await sdjwt.keys(hasher);
404
+ const missingKeys = options.requiredClaimKeys.filter(
405
+ (k) => !keys.includes(k),
406
+ );
407
+ if (missingKeys.length > 0) {
408
+ addError(
409
+ 'MISSING_REQUIRED_CLAIMS',
410
+ `Missing required claim keys: ${missingKeys.join(', ')}`,
411
+ { missingKeys },
412
+ );
413
+ }
414
+ } catch (e) {
415
+ const error = ensureError(e);
416
+ addError(
417
+ 'UNKNOWN_ERROR',
418
+ `Failed to check required claims: ${error.message}`,
419
+ error,
420
+ );
421
+ }
422
+ }
423
+
424
+ // Verify key binding if requested
425
+ let kb:
426
+ | { payload: Record<string, unknown>; header: Record<string, unknown> }
427
+ | undefined;
428
+ if (options?.keyBindingNonce && sdjwt) {
429
+ if (!sdjwt.kbJwt) {
430
+ addError('KEY_BINDING_JWT_MISSING', 'Key Binding JWT not exist');
431
+ } else if (!this.userConfig.kbVerifier) {
432
+ addError(
433
+ 'KEY_BINDING_VERIFIER_NOT_FOUND',
434
+ 'Key Binding Verifier not found',
435
+ );
436
+ } else if (payload) {
437
+ try {
438
+ const kbResult = await sdjwt.kbJwt.verifyKB({
439
+ verifier: this.userConfig.kbVerifier,
440
+ payload,
441
+ nonce: options.keyBindingNonce,
442
+ options,
443
+ });
444
+ if (!kbResult) {
445
+ addError(
446
+ 'KEY_BINDING_SIGNATURE_INVALID',
447
+ 'Key binding signature is not valid',
448
+ );
449
+ } else {
450
+ kb = kbResult;
451
+
452
+ // Verify sd_hash
453
+ const sdjwtWithoutKb = new SDJwt({
454
+ jwt: sdjwt.jwt,
455
+ disclosures: sdjwt.disclosures,
456
+ });
457
+ const presentSdJwtWithoutKb = sdjwtWithoutKb.encodeSDJwt();
458
+ const sdHashStr = await this.calculateSDHash(
459
+ presentSdJwtWithoutKb,
460
+ sdjwt,
461
+ hasher,
462
+ );
463
+
464
+ if (sdHashStr !== kbResult.payload.sd_hash) {
465
+ addError(
466
+ 'KEY_BINDING_SD_HASH_INVALID',
467
+ 'Invalid sd_hash in Key Binding JWT',
468
+ {
469
+ expected: sdHashStr,
470
+ received: kbResult.payload.sd_hash,
471
+ },
472
+ );
473
+ }
474
+ }
475
+ } catch (e) {
476
+ const error = ensureError(e);
477
+ addError(
478
+ 'KEY_BINDING_SIGNATURE_INVALID',
479
+ `Key binding verification failed: ${error.message}`,
480
+ error,
481
+ );
482
+ }
483
+ }
484
+ }
485
+
486
+ // Return result
487
+ if (errors.length > 0) {
488
+ return { success: false, errors };
489
+ }
490
+
491
+ return {
492
+ success: true,
493
+ data: {
494
+ payload: payload as ExtendedPayload,
495
+ header,
496
+ kb,
497
+ },
498
+ };
499
+ }
500
+
501
+ private async calculateSDHash(
502
+ presentSdJwtWithoutKb: string,
503
+ sdjwt: SDJwt,
504
+ hasher: Hasher,
505
+ ) {
506
+ if (!sdjwt.jwt || !sdjwt.jwt.payload) {
507
+ throw new SDJWTException('Invalid SD JWT');
508
+ }
509
+ const { _sd_alg } = getSDAlgAndPayload(sdjwt.jwt.payload);
510
+ const sdHash = await hasher(presentSdJwtWithoutKb, _sd_alg);
511
+ const sdHashStr = uint8ArrayToBase64Url(sdHash);
512
+ return sdHashStr;
513
+ }
514
+
515
+ /**
516
+ * This function is for validating the SD JWT
517
+ * Checking signature, if provided the iat and exp when provided and return its the claims
518
+ * @param encodedSDJwt
519
+ * @param options
520
+ * @returns
521
+ */
522
+ public async validate(encodedSDJwt: string, options?: T & VerifierOptions) {
523
+ if (!this.userConfig.hasher) {
524
+ throw new SDJWTException('Hasher not found');
525
+ }
526
+ const hasher = this.userConfig.hasher;
527
+
528
+ const sdjwt = await SDJwt.fromEncode(encodedSDJwt, hasher);
529
+ if (!sdjwt.jwt) {
530
+ throw new SDJWTException('Invalid SD JWT');
531
+ }
532
+
533
+ const verifiedPayloads = await this.VerifyJwt(sdjwt.jwt, options);
534
+ const claims = await sdjwt.getClaims<ExtendedPayload>(hasher);
535
+ return { payload: claims, header: verifiedPayloads.header };
536
+ }
537
+
538
+ public config(newConfig: SDJWTConfig) {
539
+ this.userConfig = { ...this.userConfig, ...newConfig };
540
+ }
541
+
542
+ public encode(sdJwt: SDJwt): SDJWTCompact {
543
+ return sdJwt.encodeSDJwt();
544
+ }
545
+
546
+ public decode(endcodedSDJwt: SDJWTCompact) {
547
+ if (!this.userConfig.hasher) {
548
+ throw new SDJWTException('Hasher not found');
549
+ }
550
+ return SDJwt.fromEncode(endcodedSDJwt, this.userConfig.hasher);
551
+ }
552
+
553
+ public async keys(endcodedSDJwt: SDJWTCompact) {
554
+ if (!this.userConfig.hasher) {
555
+ throw new SDJWTException('Hasher not found');
556
+ }
557
+ const sdjwt = await SDJwt.fromEncode(endcodedSDJwt, this.userConfig.hasher);
558
+ return sdjwt.keys(this.userConfig.hasher);
559
+ }
560
+
561
+ public async presentableKeys(endcodedSDJwt: SDJWTCompact) {
562
+ if (!this.userConfig.hasher) {
563
+ throw new SDJWTException('Hasher not found');
564
+ }
565
+ const sdjwt = await SDJwt.fromEncode(endcodedSDJwt, this.userConfig.hasher);
566
+ return sdjwt.presentableKeys(this.userConfig.hasher);
567
+ }
568
+
569
+ public async getClaims(endcodedSDJwt: SDJWTCompact) {
570
+ if (!this.userConfig.hasher) {
571
+ throw new SDJWTException('Hasher not found');
572
+ }
573
+ const sdjwt = await SDJwt.fromEncode(endcodedSDJwt, this.userConfig.hasher);
574
+ return sdjwt.getClaims(this.userConfig.hasher);
575
+ }
576
+
577
+ public toFlattenJSON(endcodedSDJwt: SDJWTCompact) {
578
+ return FlattenJSON.fromEncode(endcodedSDJwt);
579
+ }
580
+
581
+ public toGeneralJSON(endcodedSDJwt: SDJWTCompact) {
582
+ return GeneralJSON.fromEncode(endcodedSDJwt);
583
+ }
584
+ }
585
+
586
+ export class SDJwtGeneralJSONInstance<ExtendedPayload extends SdJwtPayload> {
587
+ //header type
588
+ protected type?: string;
589
+
590
+ public static readonly DEFAULT_hashAlg = 'sha-256';
591
+
592
+ protected userConfig: SDJWTConfig = {};
593
+
594
+ constructor(userConfig?: SDJWTConfig) {
595
+ if (userConfig) {
596
+ if (
597
+ userConfig.hashAlg &&
598
+ !IANA_HASH_ALGORITHMS.includes(userConfig.hashAlg)
599
+ ) {
600
+ throw new SDJWTException(
601
+ `Invalid hash algorithm: ${userConfig.hashAlg}`,
602
+ );
603
+ }
604
+ this.userConfig = userConfig;
605
+ }
606
+ }
607
+
608
+ private async createKBJwt(
609
+ options: KBOptions,
610
+ sdHash: string,
611
+ ): Promise<KBJwt> {
612
+ if (!this.userConfig.kbSigner) {
613
+ throw new SDJWTException('Key Binding Signer not found');
614
+ }
615
+ if (!this.userConfig.kbSignAlg) {
616
+ throw new SDJWTException('Key Binding sign algorithm not specified');
617
+ }
618
+
619
+ const { payload } = options;
620
+ const kbJwt = new KBJwt({
621
+ header: {
622
+ typ: KB_JWT_TYP,
623
+ alg: this.userConfig.kbSignAlg,
624
+ },
625
+ payload: { ...payload, sd_hash: sdHash },
626
+ });
627
+
628
+ await kbJwt.sign(this.userConfig.kbSigner);
629
+ return kbJwt;
630
+ }
631
+
632
+ private encodeObj(obj: Record<string, unknown>): string {
633
+ return base64urlEncode(JSON.stringify(obj));
634
+ }
635
+
636
+ public async issue<Payload extends ExtendedPayload>(
637
+ payload: Payload,
638
+ disclosureFrame: DisclosureFrame<Payload> | undefined,
639
+ options: {
640
+ sigs: Array<{
641
+ signer: Signer;
642
+ alg: string;
643
+ kid: string;
644
+ header?: Record<string, unknown>;
645
+ }>; // multiple signers for the credential
646
+ },
647
+ ): Promise<GeneralJSON> {
648
+ if (!this.userConfig.hasher) {
649
+ throw new SDJWTException('Hasher not found');
650
+ }
651
+
652
+ if (!this.userConfig.saltGenerator) {
653
+ throw new SDJWTException('SaltGenerator not found');
654
+ }
655
+
656
+ this.validateReservedFields<Payload>(payload);
657
+ this.validateDisclosureFrame<Payload>(disclosureFrame);
658
+
659
+ const hasher = this.userConfig.hasher;
660
+ const hashAlg = this.userConfig.hashAlg ?? SDJwtInstance.DEFAULT_hashAlg;
661
+
662
+ const { packedClaims, disclosures } = await pack(
663
+ payload,
664
+ disclosureFrame,
665
+ { hasher, alg: hashAlg },
666
+ this.userConfig.saltGenerator,
667
+ );
668
+
669
+ const encodedSDJwtPayload = this.encodeObj({
670
+ ...packedClaims,
671
+ _sd_alg: disclosureFrame ? hashAlg : undefined,
672
+ });
673
+
674
+ const encodedDisclosures = disclosures.map((disclosure) =>
675
+ disclosure.encode(),
676
+ );
677
+
678
+ const signatures = await Promise.all(
679
+ options.sigs.map(async (s) => {
680
+ const { signer, alg, kid, header } = s;
681
+ const protectedHeader = { typ: this.type, alg, kid, ...header };
682
+ const encodedProtectedHeader = this.encodeObj(protectedHeader);
683
+ const signature = await signer(
684
+ `${encodedProtectedHeader}.${encodedSDJwtPayload}`,
685
+ );
686
+
687
+ return {
688
+ protected: encodedProtectedHeader,
689
+ signature,
690
+ };
691
+ }),
692
+ );
693
+
694
+ const generalJson = new GeneralJSON({
695
+ payload: encodedSDJwtPayload,
696
+ disclosures: encodedDisclosures,
697
+ signatures,
698
+ });
699
+
700
+ return generalJson;
701
+ }
702
+
703
+ /**
704
+ * Validates if the payload contains any reserved claim names. If so it will throw an error.
705
+ * @param payload
706
+ * @returns
707
+ */
708
+ protected validateReservedFields<T extends ExtendedPayload>(payload: T) {
709
+ const reservedFields = new Set([SD_DIGEST, '_sd_alg', SD_DECOY]);
710
+
711
+ const visit = (node: unknown) => {
712
+ if (!node || typeof node !== 'object') {
713
+ return;
714
+ }
715
+
716
+ for (const [key, value] of Object.entries(
717
+ node as Record<string, unknown>,
718
+ )) {
719
+ if (reservedFields.has(key)) {
720
+ throw new SDJWTException(
721
+ `Reserved field name "${key}" is not allowed`,
722
+ );
723
+ }
724
+ visit(value);
725
+ }
726
+ };
727
+
728
+ visit(payload);
729
+ }
730
+
731
+ protected validateDisclosureFrame<T extends ExtendedPayload>(
732
+ _disclosureFrame?: DisclosureFrame<T>,
733
+ ) {
734
+ return;
735
+ }
736
+
737
+ public async present<T extends Record<string, unknown>>(
738
+ generalJSON: GeneralJSON,
739
+ presentationFrame?: PresentationFrame<T>,
740
+ options?: {
741
+ kb?: KBOptions;
742
+ },
743
+ ): Promise<GeneralJSON> {
744
+ if (!this.userConfig.hasher) {
745
+ throw new SDJWTException('Hasher not found');
746
+ }
747
+ const hasher = this.userConfig.hasher;
748
+ const encodedSDJwt = generalJSON.toEncoded(0);
749
+ const sdjwt = await SDJwt.fromEncode(encodedSDJwt, hasher);
750
+
751
+ if (!sdjwt.jwt?.payload) throw new SDJWTException('Payload not found');
752
+ const disclosures = await sdjwt.getPresentDisclosures(
753
+ presentationFrame,
754
+ hasher,
755
+ );
756
+ const encodedDisclosures = disclosures.map((d) => d.encode());
757
+ const presentedGeneralJSON = new GeneralJSON({
758
+ payload: generalJSON.payload,
759
+ disclosures: encodedDisclosures,
760
+ signatures: generalJSON.signatures,
761
+ });
762
+
763
+ if (!options?.kb) {
764
+ return presentedGeneralJSON;
765
+ }
766
+
767
+ const presentSdJwtWithoutKb = await sdjwt.present(
768
+ presentationFrame,
769
+ hasher,
770
+ );
771
+
772
+ const sdHashStr = await this.calculateSDHash(
773
+ presentSdJwtWithoutKb,
774
+ sdjwt,
775
+ hasher,
776
+ );
777
+
778
+ const kbJwt = await this.createKBJwt(options.kb, sdHashStr);
779
+ const encodedKbJwt = kbJwt.encodeJwt();
780
+ presentedGeneralJSON.kb_jwt = encodedKbJwt;
781
+ return presentedGeneralJSON;
782
+ }
783
+
784
+ // This function is for verifying the SD JWT
785
+ // If requiredClaimKeys is provided, it will check if the required claim keys are presentation in the SD JWT
786
+ // If requireKeyBindings is true, it will check if the key binding JWT is presentation and verify it
787
+ public async verify(generalJSON: GeneralJSON, options?: VerifierOptions) {
788
+ if (!this.userConfig.hasher) {
789
+ throw new SDJWTException('Hasher not found');
790
+ }
791
+ const hasher = this.userConfig.hasher;
792
+
793
+ const { payload, headers } = await this.validate(generalJSON);
794
+
795
+ const encodedSDJwt = generalJSON.toEncoded(0);
796
+ const sdjwt = await SDJwt.fromEncode(encodedSDJwt, hasher);
797
+ if (!sdjwt.jwt || !sdjwt.jwt.payload) {
798
+ throw new SDJWTException('Invalid SD JWT');
799
+ }
800
+
801
+ if (options?.requiredClaimKeys) {
802
+ const keys = await sdjwt.keys(hasher);
803
+ const missingKeys = options?.requiredClaimKeys.filter(
804
+ (k) => !keys.includes(k),
805
+ );
806
+ if (missingKeys.length > 0) {
807
+ throw new SDJWTException(
808
+ `Missing required claim keys: ${missingKeys.join(', ')}`,
809
+ );
810
+ }
811
+ }
812
+
813
+ if (!options?.keyBindingNonce) {
814
+ return { payload, headers };
815
+ }
816
+
817
+ if (!sdjwt.kbJwt) {
818
+ throw new SDJWTException('Key Binding JWT not exist');
819
+ }
820
+ if (!this.userConfig.kbVerifier) {
821
+ throw new SDJWTException('Key Binding Verifier not found');
822
+ }
823
+ const kb = await sdjwt.kbJwt.verifyKB({
824
+ verifier: this.userConfig.kbVerifier,
825
+ payload,
826
+ nonce: options.keyBindingNonce,
827
+ options,
828
+ });
829
+ if (!kb) {
830
+ throw new Error('signature is not valid');
831
+ }
832
+ const sdHashfromKb = kb.payload.sd_hash;
833
+ const sdjwtWithoutKb = new SDJwt({
834
+ jwt: sdjwt.jwt,
835
+ disclosures: sdjwt.disclosures,
836
+ });
837
+
838
+ const presentSdJwtWithoutKb = sdjwtWithoutKb.encodeSDJwt();
839
+ const sdHashStr = await this.calculateSDHash(
840
+ presentSdJwtWithoutKb,
841
+ sdjwt,
842
+ hasher,
843
+ );
844
+
845
+ if (sdHashStr !== sdHashfromKb) {
846
+ throw new SDJWTException('Invalid sd_hash in Key Binding JWT');
847
+ }
848
+
849
+ return { payload, headers, kb };
850
+ }
851
+
852
+ private async calculateSDHash(
853
+ presentSdJwtWithoutKb: string,
854
+ sdjwt: SDJwt,
855
+ hasher: Hasher,
856
+ ) {
857
+ if (!sdjwt.jwt || !sdjwt.jwt.payload) {
858
+ throw new SDJWTException('Invalid SD JWT');
859
+ }
860
+ const { _sd_alg } = getSDAlgAndPayload(sdjwt.jwt.payload);
861
+ const sdHash = await hasher(presentSdJwtWithoutKb, _sd_alg);
862
+ const sdHashStr = uint8ArrayToBase64Url(sdHash);
863
+ return sdHashStr;
864
+ }
865
+
866
+ // This function is for validating the SD JWT
867
+ // Just checking signature and return its the claims
868
+ public async validate(generalJSON: GeneralJSON) {
869
+ if (!this.userConfig.hasher) {
870
+ throw new SDJWTException('Hasher not found');
871
+ }
872
+ if (!this.userConfig.verifier) {
873
+ throw new SDJWTException('Verifier not found');
874
+ }
875
+ const hasher = this.userConfig.hasher;
876
+ const verifier = this.userConfig.verifier;
877
+
878
+ const { payload, signatures } = generalJSON;
879
+
880
+ const results = await Promise.all(
881
+ signatures.map(async (s) => {
882
+ const { protected: encodedHeader, signature } = s;
883
+ const verified = await verifier(
884
+ `${encodedHeader}.${payload}`,
885
+ signature,
886
+ );
887
+ const header = decodeBase64urlJsonStrict(encodedHeader, 'Invalid JWT');
888
+ return { verified, header };
889
+ }),
890
+ );
891
+
892
+ const verified = results.every((r) => r.verified);
893
+ if (!verified) {
894
+ throw new SDJWTException('Signature is not valid');
895
+ }
896
+
897
+ const encodedSDJwt = generalJSON.toEncoded(0);
898
+ const sdjwt = await SDJwt.fromEncode(encodedSDJwt, hasher);
899
+ if (!sdjwt.jwt) {
900
+ throw new SDJWTException('Invalid SD JWT');
901
+ }
902
+
903
+ const claims = await sdjwt.getClaims<ExtendedPayload>(hasher);
904
+ return { payload: claims, headers: results.map((r) => r.header) };
905
+ }
906
+
907
+ public config(newConfig: SDJWTConfig) {
908
+ this.userConfig = { ...this.userConfig, ...newConfig };
909
+ }
910
+
911
+ public encode(sdJwt: GeneralJSON, index: number): SDJWTCompact {
912
+ return sdJwt.toEncoded(index);
913
+ }
914
+
915
+ public decode(endcodedSDJwt: SDJWTCompact) {
916
+ return GeneralJSON.fromEncode(endcodedSDJwt);
917
+ }
918
+
919
+ public async keys(generalSdjwt: GeneralJSON) {
920
+ if (!this.userConfig.hasher) {
921
+ throw new SDJWTException('Hasher not found');
922
+ }
923
+ const endcodedSDJwt = generalSdjwt.toEncoded(0);
924
+ const sdjwt = await SDJwt.fromEncode(endcodedSDJwt, this.userConfig.hasher);
925
+ return sdjwt.keys(this.userConfig.hasher);
926
+ }
927
+
928
+ public async presentableKeys(generalSdjwt: GeneralJSON) {
929
+ if (!this.userConfig.hasher) {
930
+ throw new SDJWTException('Hasher not found');
931
+ }
932
+ const endcodedSDJwt = generalSdjwt.toEncoded(0);
933
+ const sdjwt = await SDJwt.fromEncode(endcodedSDJwt, this.userConfig.hasher);
934
+ return sdjwt.presentableKeys(this.userConfig.hasher);
935
+ }
936
+
937
+ public async getClaims(generalSdjwt: GeneralJSON) {
938
+ if (!this.userConfig.hasher) {
939
+ throw new SDJWTException('Hasher not found');
940
+ }
941
+ const endcodedSDJwt = generalSdjwt.toEncoded(0);
942
+ const sdjwt = await SDJwt.fromEncode(endcodedSDJwt, this.userConfig.hasher);
943
+ return sdjwt.getClaims(this.userConfig.hasher);
944
+ }
945
+ }