@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
@@ -0,0 +1,974 @@
1
+ import Crypto, { type KeyLike } from 'node:crypto';
2
+ import { hasher as digest, ES256, generateSalt } from '@owf/crypto';
3
+ import { exportJWK, importJWK, type JWK } from 'jose';
4
+ import { describe, expect, test } from 'vitest';
5
+ import { SDJwtInstance, type SdJwtPayload } from '../index';
6
+ import type { JwtPayload, KbVerifier, Signer, Verifier } from '../types';
7
+
8
+ // Extract the major version as a number
9
+ const nodeVersionMajor = Number.parseInt(
10
+ process.version.split('.')[0].substring(1),
11
+ 10,
12
+ );
13
+
14
+ const createSignerVerifier = () => {
15
+ const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519');
16
+ const signer: Signer = async (data: string) => {
17
+ const sig = Crypto.sign(null, Buffer.from(data), privateKey);
18
+ return Buffer.from(sig).toString('base64url');
19
+ };
20
+ const verifier: Verifier = async (data: string, sig: string) => {
21
+ return Crypto.verify(
22
+ null,
23
+ Buffer.from(data),
24
+ publicKey,
25
+ Buffer.from(sig, 'base64url'),
26
+ );
27
+ };
28
+ return { signer, verifier };
29
+ };
30
+
31
+ describe('index', () => {
32
+ test('create', async () => {
33
+ const sdjwt = new SDJwtInstance<SdJwtPayload>();
34
+ expect(sdjwt).toBeDefined();
35
+ });
36
+
37
+ test('kbJwt', async () => {
38
+ const { signer, verifier } = createSignerVerifier();
39
+ const sdjwt = new SDJwtInstance<SdJwtPayload>({
40
+ signer,
41
+ signAlg: 'EdDSA',
42
+ verifier,
43
+ hasher: digest,
44
+ saltGenerator: generateSalt,
45
+ kbSigner: signer,
46
+ kbSignAlg: 'EdDSA',
47
+ });
48
+ const credential = await sdjwt.issue(
49
+ {
50
+ foo: 'bar',
51
+ iss: 'Issuer',
52
+ iat: Math.floor(Date.now() / 1000),
53
+ vct: '',
54
+ },
55
+ {
56
+ _sd: ['foo'],
57
+ },
58
+ );
59
+
60
+ expect(credential).toBeDefined();
61
+
62
+ const presentation = await sdjwt.present(
63
+ credential,
64
+ { foo: true },
65
+ {
66
+ kb: {
67
+ payload: {
68
+ aud: '1',
69
+ iat: 1,
70
+ nonce: '342',
71
+ },
72
+ },
73
+ },
74
+ );
75
+
76
+ expect(presentation).toBeDefined();
77
+ });
78
+
79
+ test('issue', async () => {
80
+ const { signer, verifier } = createSignerVerifier();
81
+ const sdjwt = new SDJwtInstance<SdJwtPayload>({
82
+ signer,
83
+ signAlg: 'EdDSA',
84
+ verifier,
85
+ hasher: digest,
86
+ saltGenerator: generateSalt,
87
+ });
88
+ const credential = await sdjwt.issue(
89
+ {
90
+ foo: 'bar',
91
+ iss: 'Issuer',
92
+ iat: Math.floor(Date.now() / 1000),
93
+ vct: '',
94
+ },
95
+ {
96
+ _sd: ['foo'],
97
+ },
98
+ );
99
+
100
+ expect(credential).toBeDefined();
101
+ });
102
+
103
+ test('issue rejects reserved disclosure frame keys', async () => {
104
+ const { signer, verifier } = createSignerVerifier();
105
+ const sdjwt = new SDJwtInstance<SdJwtPayload>({
106
+ signer,
107
+ signAlg: 'EdDSA',
108
+ verifier,
109
+ hasher: digest,
110
+ saltGenerator: generateSalt,
111
+ });
112
+
113
+ await expect(
114
+ sdjwt.issue(
115
+ {
116
+ foo: 'bar',
117
+ _sd: 'reserved',
118
+ iss: 'Issuer',
119
+ iat: Math.floor(Date.now() / 1000),
120
+ vct: '',
121
+ },
122
+ {
123
+ _sd: ['foo'],
124
+ },
125
+ ),
126
+ ).rejects.toThrow('Reserved field name "_sd" is not allowed');
127
+ });
128
+
129
+ test('verify failed', async () => {
130
+ const { signer } = createSignerVerifier();
131
+ const { publicKey } = Crypto.generateKeyPairSync('ed25519');
132
+ const failedverifier: Verifier = async (data: string, sig: string) => {
133
+ return Crypto.verify(
134
+ null,
135
+ Buffer.from(data),
136
+ publicKey,
137
+ Buffer.from(sig, 'base64url'),
138
+ );
139
+ };
140
+
141
+ const sdjwt = new SDJwtInstance<SdJwtPayload>({
142
+ signer,
143
+ signAlg: 'EdDSA',
144
+ verifier: failedverifier,
145
+ hasher: digest,
146
+ saltGenerator: generateSalt,
147
+ });
148
+
149
+ const credential = await sdjwt.issue(
150
+ {
151
+ foo: 'bar',
152
+ iss: 'Issuer',
153
+ iat: Math.floor(Date.now() / 1000),
154
+ vct: '',
155
+ },
156
+ {
157
+ _sd: ['foo'],
158
+ },
159
+ );
160
+
161
+ try {
162
+ await sdjwt.verify(credential);
163
+ } catch (e) {
164
+ expect(e).toBeDefined();
165
+ }
166
+ });
167
+
168
+ test('verify failed with kbJwt', async () => {
169
+ const { signer, verifier } = createSignerVerifier();
170
+ const { publicKey } = Crypto.generateKeyPairSync('ed25519');
171
+ const failedverifier: Verifier = async (data: string, sig: string) => {
172
+ return Crypto.verify(
173
+ null,
174
+ Buffer.from(data),
175
+ publicKey,
176
+ Buffer.from(sig, 'base64url'),
177
+ );
178
+ };
179
+ const sdjwt = new SDJwtInstance<SdJwtPayload>({
180
+ signer,
181
+ signAlg: 'EdDSA',
182
+ verifier,
183
+ hasher: digest,
184
+ saltGenerator: generateSalt,
185
+ kbSigner: signer,
186
+ kbVerifier: failedverifier,
187
+ kbSignAlg: 'EdDSA',
188
+ });
189
+
190
+ const credential = await sdjwt.issue(
191
+ {
192
+ foo: 'bar',
193
+ iss: 'Issuer',
194
+ iat: Math.floor(Date.now() / 1000),
195
+ vct: '',
196
+ },
197
+ {
198
+ _sd: ['foo'],
199
+ },
200
+ );
201
+
202
+ const presentation = await sdjwt.present(
203
+ credential,
204
+ { foo: true },
205
+ {
206
+ kb: {
207
+ payload: {
208
+ aud: '',
209
+ iat: 1,
210
+ nonce: '342',
211
+ },
212
+ },
213
+ },
214
+ );
215
+
216
+ try {
217
+ await sdjwt.verify(presentation);
218
+ } catch (e) {
219
+ expect(e).toBeDefined();
220
+ }
221
+ });
222
+
223
+ test('verify with kbJwt', async () => {
224
+ const { signer, verifier } = createSignerVerifier();
225
+
226
+ const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519');
227
+
228
+ //TODO: maybe we can pass a minial class of the jwt to pass the token
229
+ const kbVerifier: KbVerifier = async (
230
+ data: string,
231
+ sig: string,
232
+ payload: JwtPayload,
233
+ ) => {
234
+ let publicKey: JsonWebKey;
235
+ if (payload.cnf) {
236
+ // use the key from the cnf
237
+ publicKey = payload.cnf.jwk;
238
+ } else {
239
+ throw Error('key binding not supported');
240
+ }
241
+ // get the key of the holder to verify the signature
242
+ return Crypto.verify(
243
+ null,
244
+ Buffer.from(data),
245
+ (await importJWK(publicKey as JWK, 'EdDSA')) as KeyLike,
246
+ Buffer.from(sig, 'base64url'),
247
+ );
248
+ };
249
+
250
+ const kbSigner = (data: string) => {
251
+ const sig = Crypto.sign(null, Buffer.from(data), privateKey);
252
+ return Buffer.from(sig).toString('base64url');
253
+ };
254
+
255
+ const sdjwt = new SDJwtInstance<SdJwtPayload>({
256
+ signer,
257
+ signAlg: 'EdDSA',
258
+ verifier,
259
+ hasher: digest,
260
+ saltGenerator: generateSalt,
261
+ kbSigner: kbSigner,
262
+ kbVerifier: kbVerifier,
263
+ kbSignAlg: 'EdDSA',
264
+ });
265
+ const credential = await sdjwt.issue(
266
+ {
267
+ foo: 'bar',
268
+ iat: Math.floor(Date.now() / 1000),
269
+ cnf: {
270
+ jwk: await exportJWK(publicKey),
271
+ },
272
+ },
273
+ {
274
+ _sd: ['foo'],
275
+ },
276
+ );
277
+
278
+ const presentation = await sdjwt.present(
279
+ credential,
280
+ { foo: true },
281
+ {
282
+ kb: {
283
+ payload: {
284
+ aud: '1',
285
+ iat: 1,
286
+ nonce: '342',
287
+ },
288
+ },
289
+ },
290
+ );
291
+
292
+ const results = await sdjwt.verify(presentation, {
293
+ requiredClaimKeys: ['foo'],
294
+ });
295
+ expect(results).toBeDefined();
296
+ });
297
+
298
+ test('verify rejects an expired kbJwt during full verification', async () => {
299
+ const { signer, verifier } = createSignerVerifier();
300
+ const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519');
301
+
302
+ const kbVerifier: KbVerifier = async (
303
+ data: string,
304
+ sig: string,
305
+ payload: JwtPayload,
306
+ ) => {
307
+ if (!payload.cnf) throw Error('key binding not supported');
308
+ return Crypto.verify(
309
+ null,
310
+ Buffer.from(data),
311
+ (await importJWK(payload.cnf.jwk as JWK, 'EdDSA')) as KeyLike,
312
+ Buffer.from(sig, 'base64url'),
313
+ );
314
+ };
315
+
316
+ const kbSigner = (data: string) => {
317
+ const sig = Crypto.sign(null, Buffer.from(data), privateKey);
318
+ return Buffer.from(sig).toString('base64url');
319
+ };
320
+
321
+ const sdjwt = new SDJwtInstance<SdJwtPayload>({
322
+ signer,
323
+ signAlg: 'EdDSA',
324
+ verifier,
325
+ hasher: digest,
326
+ saltGenerator: generateSalt,
327
+ kbSigner,
328
+ kbVerifier,
329
+ kbSignAlg: 'EdDSA',
330
+ });
331
+
332
+ const now = Math.floor(Date.now() / 1000);
333
+ const credential = await sdjwt.issue(
334
+ {
335
+ foo: 'bar',
336
+ iat: now,
337
+ cnf: { jwk: await exportJWK(publicKey) },
338
+ },
339
+ { _sd: ['foo'] },
340
+ );
341
+
342
+ const presentation = await sdjwt.present(
343
+ credential,
344
+ { foo: true },
345
+ {
346
+ kb: {
347
+ payload: {
348
+ aud: '1',
349
+ iat: now - 7200,
350
+ nonce: '342',
351
+ // kb+jwt expired one hour ago
352
+ exp: now - 3600,
353
+ } as never,
354
+ },
355
+ },
356
+ );
357
+
358
+ await expect(
359
+ sdjwt.verify(presentation, {
360
+ requiredClaimKeys: ['foo'],
361
+ keyBindingNonce: '342',
362
+ }),
363
+ ).rejects.toThrow('Verify Error: JWT is expired');
364
+ });
365
+
366
+ test('verify accepts a kbJwt with a future exp during full verification', async () => {
367
+ const { signer, verifier } = createSignerVerifier();
368
+ const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519');
369
+
370
+ const kbVerifier: KbVerifier = async (
371
+ data: string,
372
+ sig: string,
373
+ payload: JwtPayload,
374
+ ) => {
375
+ if (!payload.cnf) throw Error('key binding not supported');
376
+ return Crypto.verify(
377
+ null,
378
+ Buffer.from(data),
379
+ (await importJWK(payload.cnf.jwk as JWK, 'EdDSA')) as KeyLike,
380
+ Buffer.from(sig, 'base64url'),
381
+ );
382
+ };
383
+
384
+ const kbSigner = (data: string) => {
385
+ const sig = Crypto.sign(null, Buffer.from(data), privateKey);
386
+ return Buffer.from(sig).toString('base64url');
387
+ };
388
+
389
+ const sdjwt = new SDJwtInstance<SdJwtPayload>({
390
+ signer,
391
+ signAlg: 'EdDSA',
392
+ verifier,
393
+ hasher: digest,
394
+ saltGenerator: generateSalt,
395
+ kbSigner,
396
+ kbVerifier,
397
+ kbSignAlg: 'EdDSA',
398
+ });
399
+
400
+ const now = Math.floor(Date.now() / 1000);
401
+ const credential = await sdjwt.issue(
402
+ {
403
+ foo: 'bar',
404
+ iat: now,
405
+ cnf: { jwk: await exportJWK(publicKey) },
406
+ },
407
+ { _sd: ['foo'] },
408
+ );
409
+
410
+ const presentation = await sdjwt.present(
411
+ credential,
412
+ { foo: true },
413
+ {
414
+ kb: {
415
+ payload: {
416
+ aud: '1',
417
+ iat: now,
418
+ nonce: '342',
419
+ exp: now + 3600,
420
+ } as never,
421
+ },
422
+ },
423
+ );
424
+
425
+ const results = await sdjwt.verify(presentation, {
426
+ requiredClaimKeys: ['foo'],
427
+ keyBindingNonce: '342',
428
+ });
429
+ expect(results.kb).toBeDefined();
430
+ });
431
+
432
+ test('Hasher not found', async () => {
433
+ const sdjwt = new SDJwtInstance<SdJwtPayload>({});
434
+ try {
435
+ const credential = await sdjwt.issue(
436
+ {
437
+ foo: 'bar',
438
+ iss: 'Issuer',
439
+ iat: Math.floor(Date.now() / 1000),
440
+ vct: '',
441
+ },
442
+ {
443
+ _sd: ['foo'],
444
+ },
445
+ );
446
+
447
+ expect(credential).toBeDefined();
448
+ } catch (e) {
449
+ expect(e).toBeDefined();
450
+ }
451
+ });
452
+
453
+ test('decode rejects invalid _sd_alg values', async () => {
454
+ const { signer, verifier } = createSignerVerifier();
455
+ const sdjwt = new SDJwtInstance<SdJwtPayload>({
456
+ signer,
457
+ signAlg: 'EdDSA',
458
+ verifier,
459
+ hasher: digest,
460
+ saltGenerator: generateSalt,
461
+ });
462
+
463
+ const credential = await sdjwt.issue(
464
+ {
465
+ foo: 'bar',
466
+ iss: 'Issuer',
467
+ iat: Math.floor(Date.now() / 1000),
468
+ vct: '',
469
+ },
470
+ {
471
+ _sd: ['foo'],
472
+ },
473
+ );
474
+
475
+ const parts = credential.split('.');
476
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
477
+ payload._sd_alg = 'SHA-256';
478
+ parts[1] = Buffer.from(JSON.stringify(payload)).toString('base64url');
479
+
480
+ await expect(sdjwt.decode(parts.join('.'))).rejects.toThrow(
481
+ 'Invalid _sd_alg: SHA-256',
482
+ );
483
+ });
484
+
485
+ test('SaltGenerator not found', async () => {
486
+ const sdjwt = new SDJwtInstance<SdJwtPayload>({
487
+ hasher: digest,
488
+ });
489
+ try {
490
+ const credential = await sdjwt.issue(
491
+ {
492
+ foo: 'bar',
493
+ iss: 'Issuer',
494
+ iat: Math.floor(Date.now() / 1000),
495
+ vct: '',
496
+ },
497
+ {
498
+ _sd: ['foo'],
499
+ },
500
+ );
501
+
502
+ expect(credential).toBeDefined();
503
+ } catch (e) {
504
+ expect(e).toBeDefined();
505
+ }
506
+ });
507
+
508
+ test('Signer not found', async () => {
509
+ const sdjwt = new SDJwtInstance<SdJwtPayload>({
510
+ hasher: digest,
511
+ saltGenerator: generateSalt,
512
+ });
513
+ try {
514
+ const credential = await sdjwt.issue(
515
+ {
516
+ foo: 'bar',
517
+ iss: 'Issuer',
518
+ iat: Math.floor(Date.now() / 1000),
519
+ vct: '',
520
+ },
521
+ {
522
+ _sd: ['foo'],
523
+ },
524
+ );
525
+
526
+ expect(credential).toBeDefined();
527
+ } catch (e) {
528
+ expect(e).toBeDefined();
529
+ }
530
+ });
531
+
532
+ test('Verifier not found', async () => {
533
+ const { signer, verifier } = createSignerVerifier();
534
+ const sdjwt = new SDJwtInstance<SdJwtPayload>({
535
+ signer,
536
+ hasher: digest,
537
+ saltGenerator: generateSalt,
538
+ kbSigner: signer,
539
+ kbVerifier: verifier,
540
+ signAlg: 'EdDSA',
541
+ kbSignAlg: 'EdDSA',
542
+ });
543
+
544
+ const credential = await sdjwt.issue(
545
+ {
546
+ foo: 'bar',
547
+ iss: 'Issuer',
548
+ iat: Math.floor(Date.now() / 1000),
549
+ vct: '',
550
+ },
551
+ {
552
+ _sd: ['foo'],
553
+ },
554
+ );
555
+
556
+ const presentation = await sdjwt.present(
557
+ credential,
558
+ { foo: true },
559
+ {
560
+ kb: {
561
+ payload: {
562
+ aud: '1',
563
+ iat: 1,
564
+ nonce: '342',
565
+ },
566
+ },
567
+ },
568
+ );
569
+ try {
570
+ await sdjwt.verify(presentation, { requiredClaimKeys: ['foo'] });
571
+ } catch (e) {
572
+ expect(e).toBeDefined();
573
+ }
574
+ });
575
+
576
+ test('kbSigner not found', async () => {
577
+ const { signer, verifier } = createSignerVerifier();
578
+ const sdjwt = new SDJwtInstance<SdJwtPayload>({
579
+ signer,
580
+ verifier,
581
+ hasher: digest,
582
+ saltGenerator: generateSalt,
583
+ kbVerifier: verifier,
584
+ signAlg: 'EdDSA',
585
+ kbSignAlg: 'EdDSA',
586
+ });
587
+
588
+ const credential = await sdjwt.issue(
589
+ {
590
+ foo: 'bar',
591
+ iss: 'Issuer',
592
+ iat: Math.floor(Date.now() / 1000),
593
+ vct: '',
594
+ },
595
+ {
596
+ _sd: ['foo'],
597
+ },
598
+ );
599
+ try {
600
+ await sdjwt.present(
601
+ credential,
602
+ { foo: true },
603
+ {
604
+ kb: {
605
+ payload: {
606
+ aud: '1',
607
+ iat: 1,
608
+ nonce: '342',
609
+ },
610
+ },
611
+ },
612
+ );
613
+ } catch (e) {
614
+ expect(e).toBeDefined();
615
+ }
616
+ });
617
+
618
+ test('kbVerifier not found', async () => {
619
+ const { signer, verifier } = createSignerVerifier();
620
+ const sdjwt = new SDJwtInstance<SdJwtPayload>({
621
+ signer,
622
+ verifier,
623
+ hasher: digest,
624
+ saltGenerator: generateSalt,
625
+ kbSigner: signer,
626
+ signAlg: 'EdDSA',
627
+ kbSignAlg: 'EdDSA',
628
+ });
629
+
630
+ const credential = await sdjwt.issue(
631
+ {
632
+ foo: 'bar',
633
+ iss: 'Issuer',
634
+ iat: Math.floor(Date.now() / 1000),
635
+ vct: '',
636
+ },
637
+ {
638
+ _sd: ['foo'],
639
+ },
640
+ );
641
+
642
+ const presentation = await sdjwt.present(
643
+ credential,
644
+ { foo: true },
645
+ {
646
+ kb: {
647
+ payload: {
648
+ aud: '1',
649
+ iat: 1,
650
+ nonce: '342',
651
+ },
652
+ },
653
+ },
654
+ );
655
+ try {
656
+ await sdjwt.verify(presentation, { requiredClaimKeys: ['foo'] });
657
+ } catch (e) {
658
+ expect(e).toBeDefined();
659
+ }
660
+ });
661
+
662
+ test('kbSignAlg not found', async () => {
663
+ const { signer, verifier } = createSignerVerifier();
664
+ const sdjwt = new SDJwtInstance<SdJwtPayload>({
665
+ signer,
666
+ verifier,
667
+ hasher: digest,
668
+ saltGenerator: generateSalt,
669
+ kbSigner: signer,
670
+ signAlg: 'EdDSA',
671
+ });
672
+
673
+ const credential = await sdjwt.issue(
674
+ {
675
+ foo: 'bar',
676
+ iss: 'Issuer',
677
+ iat: Math.floor(Date.now() / 1000),
678
+ vct: '',
679
+ },
680
+ {
681
+ _sd: ['foo'],
682
+ },
683
+ );
684
+
685
+ const presentation = sdjwt.present(
686
+ credential,
687
+ { foo: true },
688
+ {
689
+ kb: {
690
+ payload: {
691
+ aud: '1',
692
+ iat: 1,
693
+ nonce: '342',
694
+ },
695
+ },
696
+ },
697
+ );
698
+ await expect(presentation).rejects.toThrow(
699
+ 'Key Binding sign algorithm not specified',
700
+ );
701
+ });
702
+
703
+ test('hasher is not found', async () => {
704
+ const { signer } = createSignerVerifier();
705
+ const sdjwt_create = new SDJwtInstance<SdJwtPayload>({
706
+ signer,
707
+ hasher: digest,
708
+ saltGenerator: generateSalt,
709
+ signAlg: 'EdDSA',
710
+ });
711
+ const credential = await sdjwt_create.issue(
712
+ {
713
+ foo: 'bar',
714
+ iss: 'Issuer',
715
+ iat: Math.floor(Date.now() / 1000),
716
+ vct: '',
717
+ },
718
+ {
719
+ _sd: ['foo'],
720
+ },
721
+ );
722
+ const sdjwt = new SDJwtInstance<SdJwtPayload>({});
723
+ await expect(sdjwt.keys('')).rejects.toThrow('Hasher not found');
724
+ await expect(sdjwt.presentableKeys('')).rejects.toThrow('Hasher not found');
725
+ await expect(sdjwt.getClaims('')).rejects.toThrow('Hasher not found');
726
+ expect(() => sdjwt.decode('')).toThrowError('Hasher not found');
727
+ await expect(sdjwt.present(credential, { foo: true })).rejects.toThrow(
728
+ 'Hasher not found',
729
+ );
730
+ });
731
+
732
+ test('presentableKeys', async () => {
733
+ const { signer } = createSignerVerifier();
734
+ const sdjwt = new SDJwtInstance<SdJwtPayload>({
735
+ signer,
736
+ hasher: digest,
737
+ saltGenerator: generateSalt,
738
+ signAlg: 'EdDSA',
739
+ });
740
+ const credential = await sdjwt.issue(
741
+ {
742
+ foo: 'bar',
743
+ iss: 'Issuer',
744
+ iat: Math.floor(Date.now() / 1000),
745
+ vct: '',
746
+ },
747
+ {
748
+ _sd: ['foo'],
749
+ },
750
+ );
751
+ const keys = await sdjwt.presentableKeys(credential);
752
+ expect(keys).toBeDefined();
753
+ expect(keys).toEqual(['foo']);
754
+ });
755
+
756
+ test('present all disclosures with kb jwt', async () => {
757
+ const { signer } = createSignerVerifier();
758
+ const sdjwt = new SDJwtInstance<SdJwtPayload>({
759
+ signer,
760
+ kbSigner: signer,
761
+ hasher: digest,
762
+ saltGenerator: generateSalt,
763
+ signAlg: 'EdDSA',
764
+ kbSignAlg: 'EdDSA',
765
+ });
766
+ const credential = await sdjwt.issue(
767
+ {
768
+ foo: 'bar',
769
+ iss: 'Issuer',
770
+ iat: Math.floor(Date.now() / 1000),
771
+ vct: '',
772
+ },
773
+ {
774
+ _sd: ['foo'],
775
+ },
776
+ );
777
+
778
+ const presentation = await sdjwt.present(credential, undefined, {
779
+ kb: {
780
+ payload: {
781
+ aud: '1',
782
+ iat: 1,
783
+ nonce: '342',
784
+ },
785
+ },
786
+ });
787
+
788
+ const decoded = await sdjwt.decode(presentation);
789
+ expect(decoded.jwt).toBeDefined();
790
+ expect(decoded.disclosures).toBeDefined();
791
+ expect(decoded.kbJwt).toBeDefined();
792
+ });
793
+
794
+ (nodeVersionMajor < 20 ? test.skip : test)(
795
+ 'validate sd-jwt that created in other implemenation',
796
+ async () => {
797
+ const publicKeyExampleJwt: JsonWebKey = {
798
+ kty: 'EC',
799
+ crv: 'P-256',
800
+ x: 'b28d4MwZMjw8-00CG4xfnn9SLMVMM19SlqZpVb_uNtQ',
801
+ y: 'Xv5zWwuoaTgdS6hV43yI6gBwTnjukmFQQnJ_kCxzqk8',
802
+ };
803
+ const kbPubkey: JsonWebKey = {
804
+ kty: 'EC',
805
+ crv: 'P-256',
806
+ x: 'TCAER19Zvu3OHF4j4W4vfSVoHIP1ILilDls7vCeGemc',
807
+ y: 'ZxjiWWbZMQGHVWKVQ4hbSIirsVfuecCE6t4jT9F2HZQ',
808
+ };
809
+ const encodedJwt =
810
+ 'eyJhbGciOiAiRVMyNTYiLCAidHlwIjogInZjK3NkLWp3dCIsICJraWQiOiAiZG9jLXNpZ25lci0wNS0yNS0yMDIyIn0.eyJfc2QiOiBbIjA5dktySk1PbHlUV00wc2pwdV9wZE9CVkJRMk0xeTNLaHBINTE1blhrcFkiLCAiMnJzakdiYUMwa3k4bVQwcEpyUGlvV1RxMF9kYXcxc1g3NnBvVWxnQ3diSSIsICJFa084ZGhXMGRIRUpidlVIbEVfVkNldUM5dVJFTE9pZUxaaGg3WGJVVHRBIiwgIklsRHpJS2VpWmREd3BxcEs2WmZieXBoRnZ6NUZnbldhLXNONndxUVhDaXciLCAiSnpZakg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQWWxTRSIsICJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwgIlRHZjRvTGJnd2Q1SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCAiamRyVEU4WWNiWTRFaWZ1Z2loaUFlX0JQZWt4SlFaSUNlaVVRd1k5UXF4SSIsICJqc3U5eVZ1bHdRUWxoRmxNXzNKbHpNYVNGemdsaFFHMERwZmF5UXdMVUs0Il0sICJpc3MiOiAiaHR0cHM6Ly9leGFtcGxlLmNvbS9pc3N1ZXIiLCAiaWF0IjogMTY4MzAwMDAwMCwgImV4cCI6IDE4ODMwMDAwMDAsICJ2Y3QiOiAiaHR0cHM6Ly9jcmVkZW50aWFscy5leGFtcGxlLmNvbS9pZGVudGl0eV9jcmVkZW50aWFsIiwgIl9zZF9hbGciOiAic2hhLTI1NiIsICJjbmYiOiB7Imp3ayI6IHsia3R5IjogIkVDIiwgImNydiI6ICJQLTI1NiIsICJ4IjogIlRDQUVSMTladnUzT0hGNGo0VzR2ZlNWb0hJUDFJTGlsRGxzN3ZDZUdlbWMiLCAieSI6ICJaeGppV1diWk1RR0hWV0tWUTRoYlNJaXJzVmZ1ZWNDRTZ0NGpUOUYySFpRIn19fQ.QXgzrePAdq_WZVGCwDxP-l8h0iyckrHBNidxVqGtKJ0LMzObqgaXUD1cgGEf7d9TexPkBcgQYqjuzlfbeCxxuA~WyJRZ19PNjR6cUF4ZTQxMmExMDhpcm9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgImVtYWlsIiwgImpvaG5kb2VAZXhhbXBsZS5jb20iXQ~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImtiK2p3dCJ9.eyJub25jZSI6ICIxMjM0NTY3ODkwIiwgImF1ZCI6ICJodHRwczovL2V4YW1wbGUuY29tL3ZlcmlmaWVyIiwgImlhdCI6IDE3MDk5OTYxODUsICJzZF9oYXNoIjogIjc4cFFEazJOblNEM1dKQm5SN015aWpmeUVqcGJ5a01yRnlpb2ZYSjlsN0kifQ.7k4goAlxM4a3tHnvCBCe70j_I-BCwtzhBRXQNk9cWJnQWxxt2kIqCyzcwzzUc0gTwtbGWVQoeWCiL5K6y3a4VQ';
811
+
812
+ const sdjwt = new SDJwtInstance({
813
+ hasher: digest,
814
+ verifier: await ES256.getVerifier(publicKeyExampleJwt),
815
+ kbVerifier: await ES256.getVerifier(kbPubkey),
816
+ });
817
+
818
+ const decode = await sdjwt.verify(encodedJwt);
819
+ expect(decode).toBeDefined();
820
+ },
821
+ );
822
+
823
+ test('safeVerify - success case', async () => {
824
+ const { signer, verifier } = createSignerVerifier();
825
+ const sdjwt = new SDJwtInstance<SdJwtPayload>({
826
+ signer,
827
+ signAlg: 'EdDSA',
828
+ verifier,
829
+ hasher: digest,
830
+ saltGenerator: generateSalt,
831
+ });
832
+
833
+ const credential = await sdjwt.issue(
834
+ {
835
+ foo: 'bar',
836
+ iss: 'Issuer',
837
+ iat: Math.floor(Date.now() / 1000),
838
+ },
839
+ {
840
+ _sd: ['foo'],
841
+ },
842
+ );
843
+
844
+ const result = await sdjwt.safeVerify(credential);
845
+
846
+ expect(result.success).toBe(true);
847
+ if (result.success) {
848
+ expect(result.data.payload).toBeDefined();
849
+ expect(result.data.payload.foo).toBe('bar');
850
+ }
851
+ });
852
+
853
+ test('safeVerify - collect multiple errors', async () => {
854
+ const sdjwt = new SDJwtInstance<SdJwtPayload>({});
855
+
856
+ const result = await sdjwt.safeVerify('invalid.jwt.token');
857
+
858
+ expect(result.success).toBe(false);
859
+ if (!result.success) {
860
+ // Should have multiple errors: hasher not found, verifier not found
861
+ expect(result.errors.length).toBeGreaterThanOrEqual(2);
862
+ const errorCodes = result.errors.map((e) => e.code);
863
+ expect(errorCodes).toContain('HASHER_NOT_FOUND');
864
+ expect(errorCodes).toContain('VERIFIER_NOT_FOUND');
865
+ }
866
+ });
867
+
868
+ test('safeVerify - invalid signature error', async () => {
869
+ const { signer } = createSignerVerifier();
870
+ const { verifier: wrongVerifier } = createSignerVerifier(); // Different key pair
871
+
872
+ const issuer = new SDJwtInstance<SdJwtPayload>({
873
+ signer,
874
+ signAlg: 'EdDSA',
875
+ hasher: digest,
876
+ saltGenerator: generateSalt,
877
+ });
878
+
879
+ const verifierInstance = new SDJwtInstance<SdJwtPayload>({
880
+ verifier: wrongVerifier,
881
+ hasher: digest,
882
+ });
883
+
884
+ const credential = await issuer.issue(
885
+ {
886
+ foo: 'bar',
887
+ iss: 'Issuer',
888
+ iat: Math.floor(Date.now() / 1000),
889
+ },
890
+ {
891
+ _sd: ['foo'],
892
+ },
893
+ );
894
+
895
+ const result = await verifierInstance.safeVerify(credential);
896
+
897
+ expect(result.success).toBe(false);
898
+ if (!result.success) {
899
+ // The error message from the verifier contains 'signature' which maps to INVALID_JWT_SIGNATURE
900
+ const hasSignatureError = result.errors.some(
901
+ (e) =>
902
+ e.code === 'INVALID_JWT_SIGNATURE' ||
903
+ e.message.toLowerCase().includes('signature'),
904
+ );
905
+ expect(hasSignatureError).toBe(true);
906
+ }
907
+ });
908
+
909
+ test('safeVerify - expired JWT error', async () => {
910
+ const { signer, verifier } = createSignerVerifier();
911
+ const sdjwt = new SDJwtInstance<SdJwtPayload>({
912
+ signer,
913
+ signAlg: 'EdDSA',
914
+ verifier,
915
+ hasher: digest,
916
+ saltGenerator: generateSalt,
917
+ });
918
+
919
+ const credential = await sdjwt.issue(
920
+ {
921
+ foo: 'bar',
922
+ iss: 'Issuer',
923
+ iat: Math.floor(Date.now() / 1000) - 3600, // 1 hour ago
924
+ exp: Math.floor(Date.now() / 1000) - 1800, // Expired 30 min ago
925
+ },
926
+ {
927
+ _sd: ['foo'],
928
+ },
929
+ );
930
+
931
+ const result = await sdjwt.safeVerify(credential);
932
+
933
+ expect(result.success).toBe(false);
934
+ if (!result.success) {
935
+ expect(result.errors.some((e) => e.code === 'JWT_EXPIRED')).toBe(true);
936
+ }
937
+ });
938
+
939
+ test('safeVerify - missing required claims error', async () => {
940
+ const { signer, verifier } = createSignerVerifier();
941
+ const sdjwt = new SDJwtInstance<SdJwtPayload>({
942
+ signer,
943
+ signAlg: 'EdDSA',
944
+ verifier,
945
+ hasher: digest,
946
+ saltGenerator: generateSalt,
947
+ });
948
+
949
+ const credential = await sdjwt.issue(
950
+ {
951
+ foo: 'bar',
952
+ iss: 'Issuer',
953
+ iat: Math.floor(Date.now() / 1000),
954
+ },
955
+ {
956
+ _sd: ['foo'],
957
+ },
958
+ );
959
+
960
+ // Present without disclosing 'foo'
961
+ const presentation = await sdjwt.present(credential, {});
962
+
963
+ const result = await sdjwt.safeVerify(presentation, {
964
+ requiredClaimKeys: ['foo', 'missing_claim'],
965
+ });
966
+
967
+ expect(result.success).toBe(false);
968
+ if (!result.success) {
969
+ expect(
970
+ result.errors.some((e) => e.code === 'MISSING_REQUIRED_CLAIMS'),
971
+ ).toBe(true);
972
+ }
973
+ });
974
+ });