@sd-jwt/core 0.20.1-next.5 → 0.20.1-next.7

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sd-jwt/core",
3
- "version": "0.20.1-next.5+8649e8f",
4
- "description": "sd-jwt draft 7 implementation in typescript",
3
+ "version": "0.20.1-next.7+a33cef4",
4
+ "description": "SD-JWT RFC 9901 implementation in TypeScript",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
7
7
  "types": "dist/index.d.ts",
@@ -58,5 +58,5 @@
58
58
  "esm"
59
59
  ]
60
60
  },
61
- "gitHead": "8649e8f6498562a03ba931a9940c16d9ace3d11f"
61
+ "gitHead": "a33cef4f55b0575b60038843a84654060af1bcd5"
62
62
  }
@@ -1,4 +1,7 @@
1
1
  import {
2
+ DEFAULT_SECURE_HASH_ALGORITHMS,
3
+ encodePathSegment,
4
+ type HashAlgorithm,
2
5
  type Hasher,
3
6
  type HasherAndAlg,
4
7
  type HasherAndAlgSync,
@@ -39,12 +42,7 @@ export const splitSdJwt = (
39
42
  ): { jwt: string; disclosures: string[]; kbJwt?: string } => {
40
43
  const [encodedJwt, ...encodedDisclosures] = sdjwt.split(SD_SEPARATOR);
41
44
  if (encodedDisclosures.length === 0) {
42
- // if input is just jwt, then return here.
43
- // This is for compatibility with jwt
44
- return {
45
- jwt: encodedJwt,
46
- disclosures: [],
47
- };
45
+ throw new SDJWTException('Invalid SD-JWT: missing SD-JWT separator');
48
46
  }
49
47
 
50
48
  const encodedKeyBindingJwt = encodedDisclosures.pop();
@@ -65,12 +63,7 @@ export const decodeSdJwt = async (
65
63
  const jwt = decodeJwt(encodedJwt);
66
64
 
67
65
  if (encodedDisclosures.length === 0) {
68
- // if input is just jwt, then return here.
69
- // This is for compatibility with jwt
70
- return {
71
- jwt,
72
- disclosures: [],
73
- };
66
+ throw new SDJWTException('Invalid SD-JWT: missing SD-JWT separator');
74
67
  }
75
68
 
76
69
  const encodedKeyBindingJwt = encodedDisclosures.pop();
@@ -101,12 +94,7 @@ export const decodeSdJwtSync = (
101
94
  const jwt = decodeJwt(encodedJwt);
102
95
 
103
96
  if (encodedDisclosures.length === 0) {
104
- // if input is just jwt, then return here.
105
- // This is for compatibility with jwt
106
- return {
107
- jwt,
108
- disclosures: [],
109
- };
97
+ throw new SDJWTException('Invalid SD-JWT: missing SD-JWT separator');
110
98
  }
111
99
 
112
100
  const encodedKeyBindingJwt = encodedDisclosures.pop();
@@ -163,7 +151,10 @@ const unpackArray = (
163
151
  arr.forEach((item, idx) => {
164
152
  if (isRecord(item)) {
165
153
  const hash = item[SD_LIST_KEY];
166
- if (typeof hash === 'string') {
154
+ if (SD_LIST_KEY in item) {
155
+ if (Object.keys(item).length !== 1 || typeof hash !== 'string') {
156
+ throw new SDJWTException('Invalid array disclosure placeholder');
157
+ }
167
158
  // RFC 9901 Section 7.1 step 4: reject duplicate digests
168
159
  if (seenDigests) {
169
160
  if (seenDigests.has(hash)) {
@@ -175,6 +166,11 @@ const unpackArray = (
175
166
  }
176
167
  const disclosed = map[hash];
177
168
  if (disclosed) {
169
+ if (typeof disclosed.key === 'string') {
170
+ throw new SDJWTException(
171
+ 'Object-property disclosure cannot be used as an array element',
172
+ );
173
+ }
178
174
  const presentKey = prefix ? `${prefix}.${idx}` : `${idx}`;
179
175
  keys[presentKey] = hash;
180
176
 
@@ -232,12 +228,16 @@ const unpackObjInternal = (
232
228
 
233
229
  const record = obj as Record<string, unknown>;
234
230
  for (const key in record) {
231
+ if (prefix && key === '_sd_alg') {
232
+ throw new SDJWTException('Nested _sd_alg is not allowed');
233
+ }
235
234
  if (
236
235
  key !== SD_DIGEST &&
237
236
  key !== SD_LIST_KEY &&
238
237
  typeof record[key] === 'object'
239
238
  ) {
240
- const newKey = prefix ? `${prefix}.${key}` : key;
239
+ const escapedKey = encodePathSegment(key);
240
+ const newKey = prefix ? `${prefix}.${escapedKey}` : escapedKey;
241
241
  const { unpackedObj, disclosureKeymap: disclosureKeys } =
242
242
  unpackObjInternal(record[key], map, newKey, seenDigests);
243
243
  record[key] = unpackedObj;
@@ -249,7 +249,15 @@ const unpackObjInternal = (
249
249
  _sd?: Array<string>;
250
250
  };
251
251
  const claims: Record<string, unknown> = {};
252
- if (_sd) {
252
+ if (_sd !== undefined) {
253
+ if (
254
+ !Array.isArray(_sd) ||
255
+ !_sd.every((hash) => typeof hash === 'string')
256
+ ) {
257
+ throw new SDJWTException(
258
+ 'Invalid _sd claim: expected array of strings',
259
+ );
260
+ }
253
261
  for (const hash of _sd) {
254
262
  // RFC 9901 Section 7.1 step 4: reject duplicate digests
255
263
  if (seenDigests) {
@@ -261,17 +269,26 @@ const unpackObjInternal = (
261
269
  seenDigests.add(hash);
262
270
  }
263
271
  const disclosed = map[hash];
264
- if (disclosed?.key) {
272
+ if (disclosed) {
273
+ if (typeof disclosed.key !== 'string') {
274
+ throw new SDJWTException(
275
+ 'Array disclosure cannot be used as an object property',
276
+ );
277
+ }
265
278
  // RFC 9901 Section 7.1 step 3c.ii.3: reject if claim name already exists
266
279
  if (disclosed.key in payload) {
267
280
  throw new SDJWTException(
268
281
  `Disclosed claim name "${disclosed.key}" conflicts with existing payload key`,
269
282
  );
270
283
  }
284
+ if (disclosed.key in claims) {
285
+ throw new SDJWTException(
286
+ `Disclosed claim name "${disclosed.key}" conflicts with another disclosure`,
287
+ );
288
+ }
271
289
 
272
- const presentKey = prefix
273
- ? `${prefix}.${disclosed.key}`
274
- : disclosed.key;
290
+ const escapedKey = encodePathSegment(disclosed.key);
291
+ const presentKey = prefix ? `${prefix}.${escapedKey}` : escapedKey;
275
292
  keys[presentKey] = hash;
276
293
 
277
294
  const { unpackedObj, disclosureKeymap: disclosureKeys } =
@@ -297,6 +314,9 @@ export const createHashMapping = async (
297
314
  for (let i = 0; i < disclosures.length; i++) {
298
315
  const disclosure = disclosures[i];
299
316
  const digest = await disclosure.digest(hash);
317
+ if (digest in map) {
318
+ throw new SDJWTException('Duplicate disclosure digest detected');
319
+ }
300
320
  map[digest] = disclosure;
301
321
  }
302
322
  return map;
@@ -310,18 +330,26 @@ export const createHashMappingSync = (
310
330
  for (let i = 0; i < disclosures.length; i++) {
311
331
  const disclosure = disclosures[i];
312
332
  const digest = disclosure.digestSync(hash);
333
+ if (digest in map) {
334
+ throw new SDJWTException('Duplicate disclosure digest detected');
335
+ }
313
336
  map[digest] = disclosure;
314
337
  }
315
338
  return map;
316
339
  };
317
340
 
318
341
  // Extract _sd_alg. If it is not present, it is assumed to be sha-256
319
- export const getSDAlgAndPayload = (SdJwtPayload: Record<string, unknown>) => {
342
+ export const getSDAlgAndPayload = (
343
+ SdJwtPayload: Record<string, unknown>,
344
+ allowedAlgorithms: ReadonlyArray<HashAlgorithm> = DEFAULT_SECURE_HASH_ALGORITHMS,
345
+ ) => {
320
346
  const { _sd_alg, ...payload } = SdJwtPayload;
321
- if (typeof _sd_alg !== 'string') {
322
- // This is for compatibility
347
+ if (_sd_alg === undefined) {
323
348
  return { _sd_alg: 'sha-256', payload };
324
349
  }
350
+ if (typeof _sd_alg !== 'string') {
351
+ throw new SDJWTException('Invalid _sd_alg: expected string');
352
+ }
325
353
  if (
326
354
  !IANA_HASH_ALGORITHMS.includes(
327
355
  _sd_alg as (typeof IANA_HASH_ALGORITHMS)[number],
@@ -329,6 +357,9 @@ export const getSDAlgAndPayload = (SdJwtPayload: Record<string, unknown>) => {
329
357
  ) {
330
358
  throw new SDJWTException(`Invalid _sd_alg: ${_sd_alg}`);
331
359
  }
360
+ if (!allowedAlgorithms.includes(_sd_alg as HashAlgorithm)) {
361
+ throw new SDJWTException(`Disallowed _sd_alg: ${_sd_alg}`);
362
+ }
332
363
  return { _sd_alg, payload };
333
364
  };
334
365
 
@@ -20,6 +20,7 @@ export type GeneralJSONSerialized = {
20
20
  disclosures?: Array<string>;
21
21
  kid?: string;
22
22
  kb_jwt?: string;
23
+ [key: string]: unknown;
23
24
  };
24
25
  protected: string;
25
26
  signature: string;
@@ -68,6 +69,14 @@ export class GeneralJSON {
68
69
  if (!json.signatures[0]) {
69
70
  throw new SDJWTException('Invalid JSON');
70
71
  }
72
+ for (let index = 1; index < json.signatures.length; index++) {
73
+ const header = json.signatures[index].header;
74
+ if (header && ('disclosures' in header || 'kb_jwt' in header)) {
75
+ throw new SDJWTException(
76
+ 'disclosures and kb_jwt MUST only appear in the first unprotected header',
77
+ );
78
+ }
79
+ }
71
80
  const disclosures = json.signatures[0].header?.disclosures ?? [];
72
81
  const kb_jwt = json.signatures[0].header?.kb_jwt;
73
82
  return new GeneralJSON({
package/src/index.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  import { getSDAlgAndPayload } from './decode';
2
2
  import { FlattenJSON } from './flattenJSON';
3
3
  import { GeneralJSON } from './generalJSON';
4
- import { Jwt, type VerifierOptions } from './jwt';
4
+ import { Jwt, type VerifierOptions, validateJwtPayload } from './jwt';
5
5
  import { KBJwt } from './kbjwt';
6
6
  import { pack, SDJwt } from './sdjwt';
7
7
  import {
8
+ DEFAULT_SECURE_HASH_ALGORITHMS,
8
9
  type DisclosureFrame,
9
10
  type Hasher,
10
11
  IANA_HASH_ALGORITHMS,
@@ -87,6 +88,17 @@ export class SDJwtInstance<ExtendedPayload extends SdJwtPayload, T = unknown> {
87
88
  `Invalid hash algorithm: ${userConfig.hashAlg}`,
88
89
  );
89
90
  }
91
+ const allowedDisclosureHashAlgorithms =
92
+ userConfig.allowedDisclosureHashAlgorithms ??
93
+ DEFAULT_SECURE_HASH_ALGORITHMS;
94
+ if (
95
+ userConfig.hashAlg &&
96
+ !allowedDisclosureHashAlgorithms.includes(userConfig.hashAlg)
97
+ ) {
98
+ throw new SDJWTException(
99
+ `Disallowed hash algorithm: ${userConfig.hashAlg}`,
100
+ );
101
+ }
90
102
  this.userConfig = userConfig;
91
103
  }
92
104
  }
@@ -148,12 +160,21 @@ export class SDJwtInstance<ExtendedPayload extends SdJwtPayload, T = unknown> {
148
160
  if (!this.userConfig.signAlg) {
149
161
  throw new SDJWTException('sign alogrithm not specified');
150
162
  }
163
+ if (this.userConfig.signAlg === 'none') {
164
+ throw new SDJWTException('sign algorithm "none" is not allowed');
165
+ }
151
166
 
152
167
  this.validateReservedFields<Payload>(payload);
153
168
  this.validateDisclosureFrame<Payload>(disclosureFrame);
154
169
 
155
170
  const hasher = this.userConfig.hasher;
156
171
  const hashAlg = this.userConfig.hashAlg ?? SDJwtInstance.DEFAULT_hashAlg;
172
+ const allowedDisclosureHashAlgorithms =
173
+ this.userConfig.allowedDisclosureHashAlgorithms ??
174
+ DEFAULT_SECURE_HASH_ALGORITHMS;
175
+ if (!allowedDisclosureHashAlgorithms.includes(hashAlg)) {
176
+ throw new SDJWTException(`Disallowed hash algorithm: ${hashAlg}`);
177
+ }
157
178
 
158
179
  const { packedClaims, disclosures } = await pack(
159
180
  payload,
@@ -212,6 +233,9 @@ export class SDJwtInstance<ExtendedPayload extends SdJwtPayload, T = unknown> {
212
233
  const hasher = this.userConfig.hasher;
213
234
 
214
235
  const sdjwt = await SDJwt.fromEncode(encodedSDJwt, hasher);
236
+ if (sdjwt.kbJwt) {
237
+ throw new SDJWTException('Holder cannot present an SD-JWT with KB-JWT');
238
+ }
215
239
 
216
240
  if (!sdjwt.jwt?.payload) throw new SDJWTException('Payload not found');
217
241
  const presentSdJwtWithoutKb = await sdjwt.present(
@@ -394,10 +418,14 @@ export class SDJwtInstance<ExtendedPayload extends SdJwtPayload, T = unknown> {
394
418
  // Validate signature and claims
395
419
  if (sdjwt?.jwt) {
396
420
  try {
397
- const result = await this.VerifyJwt(sdjwt.jwt, options);
421
+ const result = await this.VerifyJwt(sdjwt.jwt, {
422
+ ...options,
423
+ skipJwtClaimValidation: true,
424
+ } as T & VerifierOptions);
398
425
  header = result.header;
399
426
  const claims = await sdjwt.getClaims(hasher);
400
427
  payload = claims as ExtendedPayload;
428
+ validateJwtPayload(payload, options);
401
429
  } catch (e) {
402
430
  const error = ensureError(e);
403
431
  const code = exceptionToCode(error);
@@ -538,10 +566,14 @@ export class SDJwtInstance<ExtendedPayload extends SdJwtPayload, T = unknown> {
538
566
  throw new SDJWTException('Invalid SD JWT');
539
567
  }
540
568
 
541
- const verifiedPayloads = await this.VerifyJwt(sdjwt.jwt, options);
569
+ const verifiedPayloads = await this.VerifyJwt(sdjwt.jwt, {
570
+ ...options,
571
+ skipJwtClaimValidation: true,
572
+ } as T & VerifierOptions);
542
573
  const claims = await sdjwt.getClaims<ExtendedPayload>(hasher);
543
574
  // Validate that unpacked claims do not contain reserved field names
544
575
  validateReservedFieldsInternal(claims);
576
+ validateJwtPayload(claims, options);
545
577
  return { payload: claims, header: verifiedPayloads.header };
546
578
  }
547
579
 
@@ -611,6 +643,17 @@ export class SDJwtGeneralJSONInstance<ExtendedPayload extends SdJwtPayload> {
611
643
  `Invalid hash algorithm: ${userConfig.hashAlg}`,
612
644
  );
613
645
  }
646
+ const allowedDisclosureHashAlgorithms =
647
+ userConfig.allowedDisclosureHashAlgorithms ??
648
+ DEFAULT_SECURE_HASH_ALGORITHMS;
649
+ if (
650
+ userConfig.hashAlg &&
651
+ !allowedDisclosureHashAlgorithms.includes(userConfig.hashAlg)
652
+ ) {
653
+ throw new SDJWTException(
654
+ `Disallowed hash algorithm: ${userConfig.hashAlg}`,
655
+ );
656
+ }
614
657
  this.userConfig = userConfig;
615
658
  }
616
659
  }
@@ -738,6 +781,9 @@ export class SDJwtGeneralJSONInstance<ExtendedPayload extends SdJwtPayload> {
738
781
  const hasher = this.userConfig.hasher;
739
782
  const encodedSDJwt = generalJSON.toEncoded(0);
740
783
  const sdjwt = await SDJwt.fromEncode(encodedSDJwt, hasher);
784
+ if (sdjwt.kbJwt) {
785
+ throw new SDJWTException('Holder cannot present an SD-JWT with KB-JWT');
786
+ }
741
787
 
742
788
  if (!sdjwt.jwt?.payload) throw new SDJWTException('Payload not found');
743
789
  const disclosures = await sdjwt.getPresentDisclosures(
@@ -781,7 +827,7 @@ export class SDJwtGeneralJSONInstance<ExtendedPayload extends SdJwtPayload> {
781
827
  }
782
828
  const hasher = this.userConfig.hasher;
783
829
 
784
- const { payload, headers } = await this.validate(generalJSON);
830
+ const { payload, headers } = await this.validate(generalJSON, options);
785
831
 
786
832
  const encodedSDJwt = generalJSON.toEncoded(0);
787
833
  const sdjwt = await SDJwt.fromEncode(encodedSDJwt, hasher);
@@ -856,7 +902,7 @@ export class SDJwtGeneralJSONInstance<ExtendedPayload extends SdJwtPayload> {
856
902
 
857
903
  // This function is for validating the SD JWT
858
904
  // Just checking signature and return its the claims
859
- public async validate(generalJSON: GeneralJSON) {
905
+ public async validate(generalJSON: GeneralJSON, options?: VerifierOptions) {
860
906
  if (!this.userConfig.hasher) {
861
907
  throw new SDJWTException('Hasher not found');
862
908
  }
@@ -874,8 +920,23 @@ export class SDJwtGeneralJSONInstance<ExtendedPayload extends SdJwtPayload> {
874
920
  const verified = await verifier(
875
921
  `${encodedHeader}.${payload}`,
876
922
  signature,
923
+ { ...options, skipJwtClaimValidation: true },
924
+ );
925
+ const header = decodeBase64urlJsonStrict<Record<string, unknown>>(
926
+ encodedHeader,
927
+ 'Invalid JWT',
877
928
  );
878
- const header = decodeBase64urlJsonStrict(encodedHeader, 'Invalid JWT');
929
+ if (typeof header.alg !== 'string' || header.alg === 'none') {
930
+ throw new SDJWTException('Verify Error: alg "none" is not allowed');
931
+ }
932
+ if (
933
+ options?.allowedIssuerAlgorithms &&
934
+ !options.allowedIssuerAlgorithms.includes(header.alg)
935
+ ) {
936
+ throw new SDJWTException(
937
+ `Verify Error: Disallowed alg ${header.alg}`,
938
+ );
939
+ }
879
940
  return { verified, header };
880
941
  }),
881
942
  );
@@ -894,6 +955,7 @@ export class SDJwtGeneralJSONInstance<ExtendedPayload extends SdJwtPayload> {
894
955
  const claims = await sdjwt.getClaims<ExtendedPayload>(hasher);
895
956
  // Validate that unpacked claims do not contain reserved field names
896
957
  validateReservedFieldsInternal(claims);
958
+ validateJwtPayload(claims, options);
897
959
  return { payload: claims, headers: results.map((r) => r.header) };
898
960
  }
899
961
 
package/src/jwt.ts CHANGED
@@ -32,11 +32,37 @@ export type VerifierOptions = {
32
32
  */
33
33
  requiredClaimKeys?: string[];
34
34
 
35
+ /**
36
+ * Expected audience for the processed SD-JWT payload.
37
+ */
38
+ expectedAudience?: string | string[];
39
+
40
+ /**
41
+ * Allowed JOSE algorithms for issuer-signed JWTs. `none` is always rejected.
42
+ */
43
+ allowedIssuerAlgorithms?: string[];
44
+
35
45
  /**
36
46
  * nonce used to verify the key binding jwt to prevent replay attacks.
37
47
  */
38
48
  keyBindingNonce?: string;
39
49
 
50
+ /**
51
+ * Expected audience for the Key Binding JWT.
52
+ */
53
+ expectedKeyBindingAudience?: string | string[];
54
+
55
+ /**
56
+ * Maximum acceptable age of the Key Binding JWT, in seconds.
57
+ */
58
+ keyBindingMaxAgeSeconds?: number;
59
+
60
+ /**
61
+ * Internal option used by SD-JWT validation to defer claim checks until after
62
+ * disclosures are processed.
63
+ */
64
+ skipJwtClaimValidation?: boolean;
65
+
40
66
  /**
41
67
  * disable the verification of the status claim in the payload.
42
68
  *
@@ -50,6 +76,75 @@ export type VerifierOptions = {
50
76
  [key: string]: unknown;
51
77
  };
52
78
 
79
+ const isStringArray = (value: unknown): value is string[] =>
80
+ Array.isArray(value) && value.every((item) => typeof item === 'string');
81
+
82
+ const validateNumericDate = (
83
+ payload: Record<string, unknown>,
84
+ claim: 'iat' | 'nbf' | 'exp',
85
+ ) => {
86
+ const value = payload[claim];
87
+ if (value === undefined) return undefined;
88
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
89
+ throw new SDJWTException(`Verify Error: JWT ${claim} must be a number`);
90
+ }
91
+ return value;
92
+ };
93
+
94
+ const getAudiences = (audience: unknown): string[] | undefined => {
95
+ if (typeof audience === 'string') return [audience];
96
+ if (isStringArray(audience)) return audience;
97
+ return undefined;
98
+ };
99
+
100
+ const validateAudience = (
101
+ payload: Record<string, unknown>,
102
+ expectedAudience: string | string[] | undefined,
103
+ ) => {
104
+ if (expectedAudience === undefined) return;
105
+
106
+ const expectedAudiences = Array.isArray(expectedAudience)
107
+ ? expectedAudience
108
+ : [expectedAudience];
109
+ const audiences = getAudiences(payload.aud);
110
+
111
+ if (
112
+ !audiences ||
113
+ !expectedAudiences.some((expected) => audiences.includes(expected))
114
+ ) {
115
+ throw new SDJWTException('Verify Error: Invalid audience');
116
+ }
117
+ };
118
+
119
+ export const validateJwtPayload = (
120
+ payload: Record<string, unknown> | undefined,
121
+ options?: VerifierOptions,
122
+ ) => {
123
+ if (!payload) {
124
+ throw new SDJWTException('Verify Error: JWT payload is missing');
125
+ }
126
+
127
+ const skew = options?.skewSeconds ? options.skewSeconds : 0;
128
+ const currentDate = options?.currentDate ?? Math.floor(Date.now() / 1000);
129
+ const iat = validateNumericDate(payload, 'iat');
130
+ const nbf = validateNumericDate(payload, 'nbf');
131
+ const exp = validateNumericDate(payload, 'exp');
132
+
133
+ if (iat !== undefined && iat - skew > currentDate) {
134
+ throw new SDJWTException('Verify Error: JWT is not yet valid');
135
+ }
136
+
137
+ if (nbf !== undefined && nbf - skew > currentDate) {
138
+ throw new SDJWTException('Verify Error: JWT is not yet valid');
139
+ }
140
+
141
+ if (exp !== undefined && exp + skew <= currentDate) {
142
+ throw new SDJWTException('Verify Error: JWT is expired');
143
+ }
144
+
145
+ validateAudience(payload, options?.expectedAudience);
146
+ };
147
+
53
148
  // This class is used to create and verify JWT
54
149
  // Contains header, payload, and signature
55
150
  export class Jwt<
@@ -127,6 +222,9 @@ export class Jwt<
127
222
  }
128
223
 
129
224
  public async sign(signer: Signer) {
225
+ if (!this.header || this.header.alg === 'none') {
226
+ throw new SDJWTException('Sign Error: alg "none" is not allowed');
227
+ }
130
228
  const data = this.getUnsignedToken();
131
229
  this.signature = await signer(data);
132
230
 
@@ -159,21 +257,19 @@ export class Jwt<
159
257
  * @returns
160
258
  */
161
259
  public async verify<T>(verifier: Verifier<T>, options?: T & VerifierOptions) {
162
- const skew = options?.skewSeconds ? options.skewSeconds : 0;
163
- const currentDate = options?.currentDate ?? Math.floor(Date.now() / 1000);
164
- const iat = this.payload?.iat;
165
- const nbf = this.payload?.nbf;
166
- const exp = this.payload?.exp;
167
-
168
- if (typeof iat === 'number' && iat - skew > currentDate) {
169
- throw new SDJWTException('Verify Error: JWT is not yet valid');
260
+ const alg = this.header?.alg;
261
+ if (typeof alg !== 'string' || alg === 'none') {
262
+ throw new SDJWTException('Verify Error: alg "none" is not allowed');
170
263
  }
171
-
172
- if (typeof nbf === 'number' && nbf - skew > currentDate) {
173
- throw new SDJWTException('Verify Error: JWT is not yet valid');
264
+ if (
265
+ options?.allowedIssuerAlgorithms &&
266
+ !options.allowedIssuerAlgorithms.includes(alg)
267
+ ) {
268
+ throw new SDJWTException(`Verify Error: Disallowed alg ${alg}`);
174
269
  }
175
- if (typeof exp === 'number' && exp + skew < currentDate) {
176
- throw new SDJWTException('Verify Error: JWT is expired');
270
+
271
+ if (!options?.skipJwtClaimValidation) {
272
+ validateJwtPayload(this.payload, options);
177
273
  }
178
274
 
179
275
  if (!this.signature) {
package/src/kbjwt.ts CHANGED
@@ -28,18 +28,15 @@ export class KBJwt<
28
28
  }
29
29
 
30
30
  if (
31
- !this.header.alg ||
31
+ typeof this.header.alg !== 'string' ||
32
32
  this.header.alg === 'none' ||
33
- !this.header.typ ||
33
+ typeof this.header.typ !== 'string' ||
34
34
  this.header.typ !== KB_JWT_TYP ||
35
- !this.payload.iat ||
36
- !this.payload.aud ||
37
- !this.payload.nonce ||
38
- // this is for backward compatibility with version 06
39
- !(
40
- this.payload.sd_hash ||
41
- ('_sd_hash' in this.payload && this.payload._sd_hash)
42
- )
35
+ typeof this.payload.iat !== 'number' ||
36
+ typeof this.payload.aud !== 'string' ||
37
+ typeof this.payload.nonce !== 'string' ||
38
+ typeof this.payload.sd_hash !== 'string' ||
39
+ this.payload.sd_hash.length === 0
43
40
  ) {
44
41
  throw new SDJWTException('Invalid Key Binding Jwt');
45
42
  }
@@ -48,6 +45,29 @@ export class KBJwt<
48
45
  throw new SDJWTException('Verify Error: Invalid Nonce');
49
46
  }
50
47
 
48
+ if (values.options?.expectedKeyBindingAudience !== undefined) {
49
+ const expectedAudiences = Array.isArray(
50
+ values.options.expectedKeyBindingAudience,
51
+ )
52
+ ? values.options.expectedKeyBindingAudience
53
+ : [values.options.expectedKeyBindingAudience];
54
+ if (!expectedAudiences.includes(this.payload.aud)) {
55
+ throw new SDJWTException('Verify Error: Invalid Key Binding audience');
56
+ }
57
+ }
58
+
59
+ if (values.options?.keyBindingMaxAgeSeconds !== undefined) {
60
+ const currentDate =
61
+ values.options.currentDate ?? Math.floor(Date.now() / 1000);
62
+ const skew = values.options.skewSeconds ?? 0;
63
+ if (
64
+ this.payload.iat + values.options.keyBindingMaxAgeSeconds + skew <
65
+ currentDate
66
+ ) {
67
+ throw new SDJWTException('Verify Error: Key Binding JWT is too old');
68
+ }
69
+ }
70
+
51
71
  // Delegate signature verification and common JWT claim validation
52
72
  // (iat, nbf, exp) to the shared Jwt.verify implementation. The kbVerifier
53
73
  // needs the kb+jwt payload (e.g. the holder's cnf key), so we wrap it to
@@ -10,7 +10,12 @@ import {
10
10
  unpackSync,
11
11
  } from '../decode';
12
12
  import type { Extensible, HasherSync } from '../types';
13
- import { type Hasher, type PresentationFrame, SD_SEPARATOR } from '../types';
13
+ import {
14
+ encodePath,
15
+ type Hasher,
16
+ type PresentationFrame,
17
+ SD_SEPARATOR,
18
+ } from '../types';
14
19
  import { Disclosure, SDJWTException } from '../utils';
15
20
 
16
21
  // Presentable keys
@@ -117,18 +122,19 @@ export const presentSync = <T extends Record<string, unknown>>(
117
122
  */
118
123
  export const transformPresentationFrame = (
119
124
  obj: PresentationFrame<Extensible>,
120
- prefix = '',
125
+ prefix: string[] = [],
121
126
  ): string[] => {
122
127
  return Object.entries(obj).reduce<string[]>((acc, [key, value]) => {
123
- const newPrefix = prefix ? `${prefix}.${key}` : key;
128
+ const newPrefix = [...prefix, key];
129
+ const encodedPrefix = encodePath(newPrefix);
124
130
  if (typeof value === 'boolean') {
125
131
  // only add it, when it's true
126
132
  if (value) {
127
- acc.push(newPrefix);
133
+ acc.push(encodedPrefix);
128
134
  }
129
135
  } else if (typeof value === 'object' && value !== null) {
130
136
  acc.push(
131
- newPrefix,
137
+ encodedPrefix,
132
138
  ...transformPresentationFrame(
133
139
  value as PresentationFrame<Extensible>,
134
140
  newPrefix,