@sd-jwt/core 0.19.1-next.10 → 0.19.1-next.11

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/dist/index.d.mts CHANGED
@@ -376,6 +376,11 @@ declare class KBJwt<Header extends kbHeader = kbHeader, Payload extends kbPayloa
376
376
  verifier: KbVerifier;
377
377
  payload: Record<string, unknown>;
378
378
  nonce: string;
379
+ /**
380
+ * Options forwarded to the common JWT verification, e.g. currentDate and
381
+ * skewSeconds used to validate the iat, nbf and exp claims.
382
+ */
383
+ options?: VerifierOptions;
379
384
  }): Promise<{
380
385
  payload: Payload;
381
386
  header: Header;
package/dist/index.d.ts CHANGED
@@ -376,6 +376,11 @@ declare class KBJwt<Header extends kbHeader = kbHeader, Payload extends kbPayloa
376
376
  verifier: KbVerifier;
377
377
  payload: Record<string, unknown>;
378
378
  nonce: string;
379
+ /**
380
+ * Options forwarded to the common JWT verification, e.g. currentDate and
381
+ * skewSeconds used to validate the iat, nbf and exp claims.
382
+ */
383
+ options?: VerifierOptions;
379
384
  }): Promise<{
380
385
  payload: Payload;
381
386
  header: Header;
package/dist/index.js CHANGED
@@ -730,18 +730,13 @@ var KBJwt = class _KBJwt extends Jwt {
730
730
  !(this.payload.sd_hash || "_sd_hash" in this.payload && this.payload._sd_hash)) {
731
731
  throw new SDJWTException("Invalid Key Binding Jwt");
732
732
  }
733
- const data = this.getUnsignedToken();
734
- const verified = yield values.verifier(
735
- data,
736
- this.signature,
737
- values.payload
738
- );
739
- if (!verified) {
740
- throw new SDJWTException("Verify Error: Invalid JWT Signature");
741
- }
742
733
  if (this.payload.nonce !== values.nonce) {
743
734
  throw new SDJWTException("Verify Error: Invalid Nonce");
744
735
  }
736
+ yield this.verify(
737
+ (data, sig) => values.verifier(data, sig, values.payload),
738
+ values.options
739
+ );
745
740
  return { payload: this.payload, header: this.header };
746
741
  });
747
742
  }
@@ -1259,7 +1254,8 @@ var _SDJwtInstance = class _SDJwtInstance {
1259
1254
  const kb = yield sdjwt.kbJwt.verifyKB({
1260
1255
  verifier: this.userConfig.kbVerifier,
1261
1256
  payload,
1262
- nonce: options.keyBindingNonce
1257
+ nonce: options.keyBindingNonce,
1258
+ options
1263
1259
  });
1264
1260
  if (!kb) {
1265
1261
  throw new Error("signature is not valid");
@@ -1390,7 +1386,8 @@ var _SDJwtInstance = class _SDJwtInstance {
1390
1386
  const kbResult = yield sdjwt.kbJwt.verifyKB({
1391
1387
  verifier: this.userConfig.kbVerifier,
1392
1388
  payload,
1393
- nonce: options.keyBindingNonce
1389
+ nonce: options.keyBindingNonce,
1390
+ options
1394
1391
  });
1395
1392
  if (!kbResult) {
1396
1393
  addError(
@@ -1690,7 +1687,8 @@ var SDJwtGeneralJSONInstance = class {
1690
1687
  const kb = yield sdjwt.kbJwt.verifyKB({
1691
1688
  verifier: this.userConfig.kbVerifier,
1692
1689
  payload,
1693
- nonce: options.keyBindingNonce
1690
+ nonce: options.keyBindingNonce,
1691
+ options
1694
1692
  });
1695
1693
  if (!kb) {
1696
1694
  throw new Error("signature is not valid");
package/dist/index.mjs CHANGED
@@ -671,18 +671,13 @@ var KBJwt = class _KBJwt extends Jwt {
671
671
  !(this.payload.sd_hash || "_sd_hash" in this.payload && this.payload._sd_hash)) {
672
672
  throw new SDJWTException("Invalid Key Binding Jwt");
673
673
  }
674
- const data = this.getUnsignedToken();
675
- const verified = yield values.verifier(
676
- data,
677
- this.signature,
678
- values.payload
679
- );
680
- if (!verified) {
681
- throw new SDJWTException("Verify Error: Invalid JWT Signature");
682
- }
683
674
  if (this.payload.nonce !== values.nonce) {
684
675
  throw new SDJWTException("Verify Error: Invalid Nonce");
685
676
  }
677
+ yield this.verify(
678
+ (data, sig) => values.verifier(data, sig, values.payload),
679
+ values.options
680
+ );
686
681
  return { payload: this.payload, header: this.header };
687
682
  });
688
683
  }
@@ -1200,7 +1195,8 @@ var _SDJwtInstance = class _SDJwtInstance {
1200
1195
  const kb = yield sdjwt.kbJwt.verifyKB({
1201
1196
  verifier: this.userConfig.kbVerifier,
1202
1197
  payload,
1203
- nonce: options.keyBindingNonce
1198
+ nonce: options.keyBindingNonce,
1199
+ options
1204
1200
  });
1205
1201
  if (!kb) {
1206
1202
  throw new Error("signature is not valid");
@@ -1331,7 +1327,8 @@ var _SDJwtInstance = class _SDJwtInstance {
1331
1327
  const kbResult = yield sdjwt.kbJwt.verifyKB({
1332
1328
  verifier: this.userConfig.kbVerifier,
1333
1329
  payload,
1334
- nonce: options.keyBindingNonce
1330
+ nonce: options.keyBindingNonce,
1331
+ options
1335
1332
  });
1336
1333
  if (!kbResult) {
1337
1334
  addError(
@@ -1631,7 +1628,8 @@ var SDJwtGeneralJSONInstance = class {
1631
1628
  const kb = yield sdjwt.kbJwt.verifyKB({
1632
1629
  verifier: this.userConfig.kbVerifier,
1633
1630
  payload,
1634
- nonce: options.keyBindingNonce
1631
+ nonce: options.keyBindingNonce,
1632
+ options
1635
1633
  });
1636
1634
  if (!kb) {
1637
1635
  throw new Error("signature is not valid");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sd-jwt/core",
3
- "version": "0.19.1-next.10+fe18c35",
3
+ "version": "0.19.1-next.11+9e9bca1",
4
4
  "description": "sd-jwt draft 7 implementation in typescript",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -58,5 +58,5 @@
58
58
  "esm"
59
59
  ]
60
60
  },
61
- "gitHead": "fe18c3572055c3307d05f43f738c1d3c3f15de38"
61
+ "gitHead": "9e9bca10de3ad8574bfcbc875c85e37cee541505"
62
62
  }
package/src/index.ts CHANGED
@@ -242,6 +242,7 @@ export class SDJwtInstance<ExtendedPayload extends SdJwtPayload, T = unknown> {
242
242
  verifier: this.userConfig.kbVerifier,
243
243
  payload,
244
244
  nonce: options.keyBindingNonce,
245
+ options,
245
246
  });
246
247
  if (!kb) {
247
248
  throw new Error('signature is not valid');
@@ -414,6 +415,7 @@ export class SDJwtInstance<ExtendedPayload extends SdJwtPayload, T = unknown> {
414
415
  verifier: this.userConfig.kbVerifier,
415
416
  payload,
416
417
  nonce: options.keyBindingNonce,
418
+ options,
417
419
  });
418
420
  if (!kbResult) {
419
421
  addError(
@@ -774,6 +776,7 @@ export class SDJwtGeneralJSONInstance<ExtendedPayload extends SdJwtPayload> {
774
776
  verifier: this.userConfig.kbVerifier,
775
777
  payload,
776
778
  nonce: options.keyBindingNonce,
779
+ options,
777
780
  });
778
781
  if (!kb) {
779
782
  throw new Error('signature is not valid');
package/src/kbjwt.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Jwt } from './jwt';
1
+ import { Jwt, type VerifierOptions } from './jwt';
2
2
  import {
3
3
  KB_JWT_TYP,
4
4
  type KbVerifier,
@@ -17,6 +17,11 @@ export class KBJwt<
17
17
  verifier: KbVerifier;
18
18
  payload: Record<string, unknown>;
19
19
  nonce: string;
20
+ /**
21
+ * Options forwarded to the common JWT verification, e.g. currentDate and
22
+ * skewSeconds used to validate the iat, nbf and exp claims.
23
+ */
24
+ options?: VerifierOptions;
20
25
  }) {
21
26
  if (!this.header || !this.payload || !this.signature) {
22
27
  throw new SDJWTException('Verify Error: Invalid JWT');
@@ -39,19 +44,19 @@ export class KBJwt<
39
44
  throw new SDJWTException('Invalid Key Binding Jwt');
40
45
  }
41
46
 
42
- const data = this.getUnsignedToken();
43
- const verified = await values.verifier(
44
- data,
45
- this.signature,
46
- values.payload,
47
- );
48
- if (!verified) {
49
- throw new SDJWTException('Verify Error: Invalid JWT Signature');
50
- }
51
47
  if (this.payload.nonce !== values.nonce) {
52
48
  throw new SDJWTException('Verify Error: Invalid Nonce');
53
49
  }
54
50
 
51
+ // Delegate signature verification and common JWT claim validation
52
+ // (iat, nbf, exp) to the shared Jwt.verify implementation. The kbVerifier
53
+ // needs the kb+jwt payload (e.g. the holder's cnf key), so we wrap it to
54
+ // forward values.payload instead of the base verifier's options argument.
55
+ await this.verify(
56
+ (data, sig) => values.verifier(data, sig, values.payload),
57
+ values.options,
58
+ );
59
+
55
60
  return { payload: this.payload, header: this.header };
56
61
  }
57
62
 
@@ -269,6 +269,140 @@ describe('index', () => {
269
269
  expect(results).toBeDefined();
270
270
  });
271
271
 
272
+ test('verify rejects an expired kbJwt during full verification', async () => {
273
+ const { signer, verifier } = createSignerVerifier();
274
+ const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519');
275
+
276
+ const kbVerifier: KbVerifier = async (
277
+ data: string,
278
+ sig: string,
279
+ payload: JwtPayload,
280
+ ) => {
281
+ if (!payload.cnf) throw Error('key binding not supported');
282
+ return Crypto.verify(
283
+ null,
284
+ Buffer.from(data),
285
+ (await importJWK(payload.cnf.jwk as JWK, 'EdDSA')) as KeyLike,
286
+ Buffer.from(sig, 'base64url'),
287
+ );
288
+ };
289
+
290
+ const kbSigner = (data: string) => {
291
+ const sig = Crypto.sign(null, Buffer.from(data), privateKey);
292
+ return Buffer.from(sig).toString('base64url');
293
+ };
294
+
295
+ const sdjwt = new SDJwtInstance<SdJwtPayload>({
296
+ signer,
297
+ signAlg: 'EdDSA',
298
+ verifier,
299
+ hasher: digest,
300
+ saltGenerator: generateSalt,
301
+ kbSigner,
302
+ kbVerifier,
303
+ kbSignAlg: 'EdDSA',
304
+ });
305
+
306
+ const now = Math.floor(Date.now() / 1000);
307
+ const credential = await sdjwt.issue(
308
+ {
309
+ foo: 'bar',
310
+ iat: now,
311
+ cnf: { jwk: await exportJWK(publicKey) },
312
+ },
313
+ { _sd: ['foo'] },
314
+ );
315
+
316
+ const presentation = await sdjwt.present(
317
+ credential,
318
+ { foo: true },
319
+ {
320
+ kb: {
321
+ payload: {
322
+ aud: '1',
323
+ iat: now - 7200,
324
+ nonce: '342',
325
+ // kb+jwt expired one hour ago
326
+ exp: now - 3600,
327
+ } as never,
328
+ },
329
+ },
330
+ );
331
+
332
+ await expect(
333
+ sdjwt.verify(presentation, {
334
+ requiredClaimKeys: ['foo'],
335
+ keyBindingNonce: '342',
336
+ }),
337
+ ).rejects.toThrow('Verify Error: JWT is expired');
338
+ });
339
+
340
+ test('verify accepts a kbJwt with a future exp during full verification', async () => {
341
+ const { signer, verifier } = createSignerVerifier();
342
+ const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519');
343
+
344
+ const kbVerifier: KbVerifier = async (
345
+ data: string,
346
+ sig: string,
347
+ payload: JwtPayload,
348
+ ) => {
349
+ if (!payload.cnf) throw Error('key binding not supported');
350
+ return Crypto.verify(
351
+ null,
352
+ Buffer.from(data),
353
+ (await importJWK(payload.cnf.jwk as JWK, 'EdDSA')) as KeyLike,
354
+ Buffer.from(sig, 'base64url'),
355
+ );
356
+ };
357
+
358
+ const kbSigner = (data: string) => {
359
+ const sig = Crypto.sign(null, Buffer.from(data), privateKey);
360
+ return Buffer.from(sig).toString('base64url');
361
+ };
362
+
363
+ const sdjwt = new SDJwtInstance<SdJwtPayload>({
364
+ signer,
365
+ signAlg: 'EdDSA',
366
+ verifier,
367
+ hasher: digest,
368
+ saltGenerator: generateSalt,
369
+ kbSigner,
370
+ kbVerifier,
371
+ kbSignAlg: 'EdDSA',
372
+ });
373
+
374
+ const now = Math.floor(Date.now() / 1000);
375
+ const credential = await sdjwt.issue(
376
+ {
377
+ foo: 'bar',
378
+ iat: now,
379
+ cnf: { jwk: await exportJWK(publicKey) },
380
+ },
381
+ { _sd: ['foo'] },
382
+ );
383
+
384
+ const presentation = await sdjwt.present(
385
+ credential,
386
+ { foo: true },
387
+ {
388
+ kb: {
389
+ payload: {
390
+ aud: '1',
391
+ iat: now,
392
+ nonce: '342',
393
+ exp: now + 3600,
394
+ } as never,
395
+ },
396
+ },
397
+ );
398
+
399
+ const results = await sdjwt.verify(presentation, {
400
+ requiredClaimKeys: ['foo'],
401
+ keyBindingNonce: '342',
402
+ });
403
+ expect(results.kb).toBeDefined();
404
+ });
405
+
272
406
  test('Hasher not found', async () => {
273
407
  const sdjwt = new SDJwtInstance<SdJwtPayload>({});
274
408
  try {
@@ -6,6 +6,7 @@ import {
6
6
  type JwtPayload,
7
7
  KB_JWT_TYP,
8
8
  type KbVerifier,
9
+ type kbPayload,
9
10
  type Signer,
10
11
  } from '../types';
11
12
  import type { SDJWTException } from '../utils';
@@ -287,6 +288,216 @@ describe('KB JWT', () => {
287
288
  }
288
289
  });
289
290
 
291
+ test('verify failed with expired exp claim', async () => {
292
+ const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519');
293
+ const testSigner: Signer = async (data: string) => {
294
+ const sig = Crypto.sign(null, Buffer.from(data), privateKey);
295
+ return Buffer.from(sig).toString('base64url');
296
+ };
297
+
298
+ const payload = {
299
+ cnf: {
300
+ jwk: await exportJWK(publicKey),
301
+ },
302
+ };
303
+ const testVerifier: KbVerifier = async (
304
+ data: string,
305
+ sig: string,
306
+ payload: JwtPayload,
307
+ ) => {
308
+ const publicKey = payload.cnf?.jwk;
309
+ return Crypto.verify(
310
+ null,
311
+ Buffer.from(data),
312
+ (await importJWK(publicKey as JWK, 'EdDSA')) as KeyLike,
313
+ Buffer.from(sig, 'base64url'),
314
+ );
315
+ };
316
+
317
+ const kbJwt = new KBJwt({
318
+ header: {
319
+ typ: KB_JWT_TYP,
320
+ alg: 'EdDSA',
321
+ },
322
+ payload: {
323
+ iat: 1,
324
+ aud: 'aud',
325
+ nonce: 'nonce',
326
+ sd_hash: 'hash',
327
+ // expired one hour before the current date used below
328
+ exp: 1000,
329
+ } as kbPayload,
330
+ });
331
+ const encodedKbJwt = await kbJwt.sign(testSigner);
332
+ const decoded = KBJwt.fromKBEncode(encodedKbJwt);
333
+
334
+ await expect(
335
+ decoded.verifyKB({
336
+ verifier: testVerifier,
337
+ payload,
338
+ nonce: 'nonce',
339
+ options: { currentDate: 5000 },
340
+ }),
341
+ ).rejects.toThrow('Verify Error: JWT is expired');
342
+ });
343
+
344
+ test('verify failed with iat in the future', async () => {
345
+ const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519');
346
+ const testSigner: Signer = async (data: string) => {
347
+ const sig = Crypto.sign(null, Buffer.from(data), privateKey);
348
+ return Buffer.from(sig).toString('base64url');
349
+ };
350
+
351
+ const payload = {
352
+ cnf: {
353
+ jwk: await exportJWK(publicKey),
354
+ },
355
+ };
356
+ const testVerifier: KbVerifier = async (
357
+ data: string,
358
+ sig: string,
359
+ payload: JwtPayload,
360
+ ) => {
361
+ const publicKey = payload.cnf?.jwk;
362
+ return Crypto.verify(
363
+ null,
364
+ Buffer.from(data),
365
+ (await importJWK(publicKey as JWK, 'EdDSA')) as KeyLike,
366
+ Buffer.from(sig, 'base64url'),
367
+ );
368
+ };
369
+
370
+ const kbJwt = new KBJwt({
371
+ header: {
372
+ typ: KB_JWT_TYP,
373
+ alg: 'EdDSA',
374
+ },
375
+ payload: {
376
+ iat: 5000,
377
+ aud: 'aud',
378
+ nonce: 'nonce',
379
+ sd_hash: 'hash',
380
+ },
381
+ });
382
+ const encodedKbJwt = await kbJwt.sign(testSigner);
383
+ const decoded = KBJwt.fromKBEncode(encodedKbJwt);
384
+
385
+ await expect(
386
+ decoded.verifyKB({
387
+ verifier: testVerifier,
388
+ payload,
389
+ nonce: 'nonce',
390
+ // iat (5000) is after the current date (1000)
391
+ options: { currentDate: 1000 },
392
+ }),
393
+ ).rejects.toThrow('Verify Error: JWT is not yet valid');
394
+ });
395
+
396
+ test('verify failed with nbf in the future', async () => {
397
+ const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519');
398
+ const testSigner: Signer = async (data: string) => {
399
+ const sig = Crypto.sign(null, Buffer.from(data), privateKey);
400
+ return Buffer.from(sig).toString('base64url');
401
+ };
402
+
403
+ const payload = {
404
+ cnf: {
405
+ jwk: await exportJWK(publicKey),
406
+ },
407
+ };
408
+ const testVerifier: KbVerifier = async (
409
+ data: string,
410
+ sig: string,
411
+ payload: JwtPayload,
412
+ ) => {
413
+ const publicKey = payload.cnf?.jwk;
414
+ return Crypto.verify(
415
+ null,
416
+ Buffer.from(data),
417
+ (await importJWK(publicKey as JWK, 'EdDSA')) as KeyLike,
418
+ Buffer.from(sig, 'base64url'),
419
+ );
420
+ };
421
+
422
+ const kbJwt = new KBJwt({
423
+ header: {
424
+ typ: KB_JWT_TYP,
425
+ alg: 'EdDSA',
426
+ },
427
+ payload: {
428
+ iat: 1,
429
+ aud: 'aud',
430
+ nonce: 'nonce',
431
+ sd_hash: 'hash',
432
+ nbf: 5000,
433
+ } as kbPayload,
434
+ });
435
+ const encodedKbJwt = await kbJwt.sign(testSigner);
436
+ const decoded = KBJwt.fromKBEncode(encodedKbJwt);
437
+
438
+ await expect(
439
+ decoded.verifyKB({
440
+ verifier: testVerifier,
441
+ payload,
442
+ nonce: 'nonce',
443
+ options: { currentDate: 1000 },
444
+ }),
445
+ ).rejects.toThrow('Verify Error: JWT is not yet valid');
446
+ });
447
+
448
+ test('verify succeeds for expired exp within the allowed skew', async () => {
449
+ const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519');
450
+ const testSigner: Signer = async (data: string) => {
451
+ const sig = Crypto.sign(null, Buffer.from(data), privateKey);
452
+ return Buffer.from(sig).toString('base64url');
453
+ };
454
+
455
+ const payload = {
456
+ cnf: {
457
+ jwk: await exportJWK(publicKey),
458
+ },
459
+ };
460
+ const testVerifier: KbVerifier = async (
461
+ data: string,
462
+ sig: string,
463
+ payload: JwtPayload,
464
+ ) => {
465
+ const publicKey = payload.cnf?.jwk;
466
+ return Crypto.verify(
467
+ null,
468
+ Buffer.from(data),
469
+ (await importJWK(publicKey as JWK, 'EdDSA')) as KeyLike,
470
+ Buffer.from(sig, 'base64url'),
471
+ );
472
+ };
473
+
474
+ const kbJwt = new KBJwt({
475
+ header: {
476
+ typ: KB_JWT_TYP,
477
+ alg: 'EdDSA',
478
+ },
479
+ payload: {
480
+ iat: 1,
481
+ aud: 'aud',
482
+ nonce: 'nonce',
483
+ sd_hash: 'hash',
484
+ exp: 1000,
485
+ } as kbPayload,
486
+ });
487
+ const encodedKbJwt = await kbJwt.sign(testSigner);
488
+ const decoded = KBJwt.fromKBEncode(encodedKbJwt);
489
+
490
+ // exp (1000) is before the current date (1100) but within the 200s skew
491
+ const verified = await decoded.verifyKB({
492
+ verifier: testVerifier,
493
+ payload,
494
+ nonce: 'nonce',
495
+ options: { currentDate: 1100, skewSeconds: 200 },
496
+ });
497
+
498
+ expect(verified.payload.exp).toBe(1000);
499
+ });
500
+
290
501
  test('compatibility test for version 06', async () => {
291
502
  const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519');
292
503
  const testSigner: Signer = async (data: string) => {