@sd-jwt/core 0.19.1-next.12 → 0.19.1-next.13

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
@@ -548,11 +548,12 @@ declare class SDJwtInstance<ExtendedPayload extends SdJwtPayload, T = unknown> {
548
548
  header?: object;
549
549
  }): Promise<SDJWTCompact>;
550
550
  /**
551
- * Validates if the disclosureFrame contains any reserved fields. If so it will throw an error.
552
- * @param disclosureFrame
551
+ * Validates if the payload contains any reserved claim names. If so it will throw an error.
552
+ * @param payload
553
553
  * @returns
554
554
  */
555
- protected validateReservedFields<T extends ExtendedPayload>(_disclosureFrame: DisclosureFrame<T>): void;
555
+ protected validateReservedFields<T extends ExtendedPayload>(payload: T): void;
556
+ protected validateDisclosureFrame<T extends ExtendedPayload>(_disclosureFrame?: DisclosureFrame<T>): void;
556
557
  present<T extends Record<string, unknown>>(encodedSDJwt: string, presentationFrame?: PresentationFrame<T>, options?: {
557
558
  kb?: KBOptions;
558
559
  }): Promise<SDJWTCompact>;
@@ -621,21 +622,22 @@ declare class SDJwtGeneralJSONInstance<ExtendedPayload extends SdJwtPayload> {
621
622
  }>;
622
623
  }): Promise<GeneralJSON>;
623
624
  /**
624
- * Validates if the disclosureFrame contains any reserved fields. If so it will throw an error.
625
- * @param disclosureFrame
625
+ * Validates if the payload contains any reserved claim names. If so it will throw an error.
626
+ * @param payload
626
627
  * @returns
627
628
  */
628
- protected validateReservedFields<T extends ExtendedPayload>(_disclosureFrame: DisclosureFrame<T>): void;
629
+ protected validateReservedFields<T extends ExtendedPayload>(payload: T): void;
630
+ protected validateDisclosureFrame<T extends ExtendedPayload>(_disclosureFrame?: DisclosureFrame<T>): void;
629
631
  present<T extends Record<string, unknown>>(generalJSON: GeneralJSON, presentationFrame?: PresentationFrame<T>, options?: {
630
632
  kb?: KBOptions;
631
633
  }): Promise<GeneralJSON>;
632
634
  verify(generalJSON: GeneralJSON, options?: VerifierOptions): Promise<{
633
635
  payload: ExtendedPayload;
634
- headers: any[];
636
+ headers: unknown[];
635
637
  kb?: undefined;
636
638
  } | {
637
639
  payload: ExtendedPayload;
638
- headers: any[];
640
+ headers: unknown[];
639
641
  kb: {
640
642
  payload: kbPayload;
641
643
  header: kbHeader;
@@ -644,7 +646,7 @@ declare class SDJwtGeneralJSONInstance<ExtendedPayload extends SdJwtPayload> {
644
646
  private calculateSDHash;
645
647
  validate(generalJSON: GeneralJSON): Promise<{
646
648
  payload: ExtendedPayload;
647
- headers: any[];
649
+ headers: unknown[];
648
650
  }>;
649
651
  config(newConfig: SDJWTConfig): void;
650
652
  encode(sdJwt: GeneralJSON, index: number): SDJWTCompact;
package/dist/index.d.ts CHANGED
@@ -548,11 +548,12 @@ declare class SDJwtInstance<ExtendedPayload extends SdJwtPayload, T = unknown> {
548
548
  header?: object;
549
549
  }): Promise<SDJWTCompact>;
550
550
  /**
551
- * Validates if the disclosureFrame contains any reserved fields. If so it will throw an error.
552
- * @param disclosureFrame
551
+ * Validates if the payload contains any reserved claim names. If so it will throw an error.
552
+ * @param payload
553
553
  * @returns
554
554
  */
555
- protected validateReservedFields<T extends ExtendedPayload>(_disclosureFrame: DisclosureFrame<T>): void;
555
+ protected validateReservedFields<T extends ExtendedPayload>(payload: T): void;
556
+ protected validateDisclosureFrame<T extends ExtendedPayload>(_disclosureFrame?: DisclosureFrame<T>): void;
556
557
  present<T extends Record<string, unknown>>(encodedSDJwt: string, presentationFrame?: PresentationFrame<T>, options?: {
557
558
  kb?: KBOptions;
558
559
  }): Promise<SDJWTCompact>;
@@ -621,21 +622,22 @@ declare class SDJwtGeneralJSONInstance<ExtendedPayload extends SdJwtPayload> {
621
622
  }>;
622
623
  }): Promise<GeneralJSON>;
623
624
  /**
624
- * Validates if the disclosureFrame contains any reserved fields. If so it will throw an error.
625
- * @param disclosureFrame
625
+ * Validates if the payload contains any reserved claim names. If so it will throw an error.
626
+ * @param payload
626
627
  * @returns
627
628
  */
628
- protected validateReservedFields<T extends ExtendedPayload>(_disclosureFrame: DisclosureFrame<T>): void;
629
+ protected validateReservedFields<T extends ExtendedPayload>(payload: T): void;
630
+ protected validateDisclosureFrame<T extends ExtendedPayload>(_disclosureFrame?: DisclosureFrame<T>): void;
629
631
  present<T extends Record<string, unknown>>(generalJSON: GeneralJSON, presentationFrame?: PresentationFrame<T>, options?: {
630
632
  kb?: KBOptions;
631
633
  }): Promise<GeneralJSON>;
632
634
  verify(generalJSON: GeneralJSON, options?: VerifierOptions): Promise<{
633
635
  payload: ExtendedPayload;
634
- headers: any[];
636
+ headers: unknown[];
635
637
  kb?: undefined;
636
638
  } | {
637
639
  payload: ExtendedPayload;
638
- headers: any[];
640
+ headers: unknown[];
639
641
  kb: {
640
642
  payload: kbPayload;
641
643
  header: kbHeader;
@@ -644,7 +646,7 @@ declare class SDJwtGeneralJSONInstance<ExtendedPayload extends SdJwtPayload> {
644
646
  private calculateSDHash;
645
647
  validate(generalJSON: GeneralJSON): Promise<{
646
648
  payload: ExtendedPayload;
647
- headers: any[];
649
+ headers: unknown[];
648
650
  }>;
649
651
  config(newConfig: SDJWTConfig): void;
650
652
  encode(sdJwt: GeneralJSON, index: number): SDJWTCompact;
package/dist/index.js CHANGED
@@ -161,6 +161,18 @@ function ensureError(value) {
161
161
  return new Error(String(value));
162
162
  }
163
163
 
164
+ // src/utils/strict-json.ts
165
+ var utf8Decoder = new TextDecoder("utf-8", { fatal: true });
166
+ var decodeBase64urlJsonStrict = (encoded, errorMessage) => {
167
+ try {
168
+ const bytes = (0, import_identity_common.base64UrlToUint8Array)(encoded);
169
+ const decoded = utf8Decoder.decode(bytes);
170
+ return JSON.parse(decoded);
171
+ } catch (e) {
172
+ throw new SDJWTException(errorMessage);
173
+ }
174
+ };
175
+
164
176
  // src/utils/disclosure.ts
165
177
  var Disclosure = class _Disclosure {
166
178
  constructor(data, _meta) {
@@ -187,7 +199,10 @@ var Disclosure = class _Disclosure {
187
199
  const { hasher, alg } = hash;
188
200
  const digest = yield hasher(s, alg);
189
201
  const digestStr = (0, import_identity_common.uint8ArrayToBase64Url)(digest);
190
- const item = JSON.parse((0, import_identity_common.base64urlDecode)(s));
202
+ const item = decodeBase64urlJsonStrict(
203
+ s,
204
+ "Invalid disclosure data"
205
+ );
191
206
  return _Disclosure.fromArray(item, { digest: digestStr, encoded: s });
192
207
  });
193
208
  }
@@ -195,7 +210,10 @@ var Disclosure = class _Disclosure {
195
210
  const { hasher, alg } = hash;
196
211
  const digest = hasher(s, alg);
197
212
  const digestStr = (0, import_identity_common.uint8ArrayToBase64Url)(digest);
198
- const item = JSON.parse((0, import_identity_common.base64urlDecode)(s));
213
+ const item = decodeBase64urlJsonStrict(
214
+ s,
215
+ "Invalid disclosure data"
216
+ );
199
217
  return _Disclosure.fromArray(item, { digest: digestStr, encoded: s });
200
218
  }
201
219
  static fromArray(item, _meta) {
@@ -237,8 +255,8 @@ var decodeJwt = (jwt) => {
237
255
  throw new SDJWTException("Invalid JWT as input");
238
256
  }
239
257
  return {
240
- header: JSON.parse((0, import_identity_common.base64urlDecode)(header)),
241
- payload: JSON.parse((0, import_identity_common.base64urlDecode)(payload)),
258
+ header: decodeBase64urlJsonStrict(header, "Invalid JWT as input"),
259
+ payload: decodeBase64urlJsonStrict(payload, "Invalid JWT as input"),
242
260
  signature
243
261
  };
244
262
  };
@@ -431,6 +449,11 @@ var getSDAlgAndPayload = (SdJwtPayload) => {
431
449
  if (typeof _sd_alg !== "string") {
432
450
  return { _sd_alg: "sha-256", payload };
433
451
  }
452
+ if (!IANA_HASH_ALGORITHMS.includes(
453
+ _sd_alg
454
+ )) {
455
+ throw new SDJWTException(`Invalid _sd_alg: ${_sd_alg}`);
456
+ }
434
457
  return { _sd_alg, payload };
435
458
  };
436
459
  var unpack = (SdJwtPayload, disclosures, hasher) => __async(null, null, function* () {
@@ -1155,9 +1178,8 @@ var _SDJwtInstance = class _SDJwtInstance {
1155
1178
  if (!this.userConfig.signAlg) {
1156
1179
  throw new SDJWTException("sign alogrithm not specified");
1157
1180
  }
1158
- if (disclosureFrame) {
1159
- this.validateReservedFields(disclosureFrame);
1160
- }
1181
+ this.validateReservedFields(payload);
1182
+ this.validateDisclosureFrame(disclosureFrame);
1161
1183
  const hasher = this.userConfig.hasher;
1162
1184
  const hashAlg = (_a = this.userConfig.hashAlg) != null ? _a : _SDJwtInstance.DEFAULT_hashAlg;
1163
1185
  const { packedClaims, disclosures } = yield pack(
@@ -1185,11 +1207,30 @@ var _SDJwtInstance = class _SDJwtInstance {
1185
1207
  });
1186
1208
  }
1187
1209
  /**
1188
- * Validates if the disclosureFrame contains any reserved fields. If so it will throw an error.
1189
- * @param disclosureFrame
1210
+ * Validates if the payload contains any reserved claim names. If so it will throw an error.
1211
+ * @param payload
1190
1212
  * @returns
1191
1213
  */
1192
- validateReservedFields(_disclosureFrame) {
1214
+ validateReservedFields(payload) {
1215
+ const reservedFields = /* @__PURE__ */ new Set([SD_DIGEST, "_sd_alg", SD_DECOY]);
1216
+ const visit = (node) => {
1217
+ if (!node || typeof node !== "object") {
1218
+ return;
1219
+ }
1220
+ for (const [key, value] of Object.entries(
1221
+ node
1222
+ )) {
1223
+ if (reservedFields.has(key)) {
1224
+ throw new SDJWTException(
1225
+ `Reserved field name "${key}" is not allowed`
1226
+ );
1227
+ }
1228
+ visit(value);
1229
+ }
1230
+ };
1231
+ visit(payload);
1232
+ }
1233
+ validateDisclosureFrame(_disclosureFrame) {
1193
1234
  return;
1194
1235
  }
1195
1236
  present(encodedSDJwt, presentationFrame, options) {
@@ -1565,9 +1606,8 @@ var SDJwtGeneralJSONInstance = class {
1565
1606
  if (!this.userConfig.saltGenerator) {
1566
1607
  throw new SDJWTException("SaltGenerator not found");
1567
1608
  }
1568
- if (disclosureFrame) {
1569
- this.validateReservedFields(disclosureFrame);
1570
- }
1609
+ this.validateReservedFields(payload);
1610
+ this.validateDisclosureFrame(disclosureFrame);
1571
1611
  const hasher = this.userConfig.hasher;
1572
1612
  const hashAlg = (_a = this.userConfig.hashAlg) != null ? _a : SDJwtInstance.DEFAULT_hashAlg;
1573
1613
  const { packedClaims, disclosures } = yield pack(
@@ -1576,10 +1616,12 @@ var SDJwtGeneralJSONInstance = class {
1576
1616
  { hasher, alg: hashAlg },
1577
1617
  this.userConfig.saltGenerator
1578
1618
  );
1579
- const encodedDisclosures = disclosures.map((d) => d.encode());
1580
1619
  const encodedSDJwtPayload = this.encodeObj(__spreadProps(__spreadValues({}, packedClaims), {
1581
1620
  _sd_alg: disclosureFrame ? hashAlg : void 0
1582
1621
  }));
1622
+ const encodedDisclosures = disclosures.map(
1623
+ (disclosure) => disclosure.encode()
1624
+ );
1583
1625
  const signatures = yield Promise.all(
1584
1626
  options.sigs.map((s) => __async(this, null, function* () {
1585
1627
  const { signer, alg, kid, header } = s;
@@ -1590,7 +1632,6 @@ var SDJwtGeneralJSONInstance = class {
1590
1632
  );
1591
1633
  return {
1592
1634
  protected: encodedProtectedHeader,
1593
- kid,
1594
1635
  signature
1595
1636
  };
1596
1637
  }))
@@ -1604,11 +1645,30 @@ var SDJwtGeneralJSONInstance = class {
1604
1645
  });
1605
1646
  }
1606
1647
  /**
1607
- * Validates if the disclosureFrame contains any reserved fields. If so it will throw an error.
1608
- * @param disclosureFrame
1648
+ * Validates if the payload contains any reserved claim names. If so it will throw an error.
1649
+ * @param payload
1609
1650
  * @returns
1610
1651
  */
1611
- validateReservedFields(_disclosureFrame) {
1652
+ validateReservedFields(payload) {
1653
+ const reservedFields = /* @__PURE__ */ new Set([SD_DIGEST, "_sd_alg", SD_DECOY]);
1654
+ const visit = (node) => {
1655
+ if (!node || typeof node !== "object") {
1656
+ return;
1657
+ }
1658
+ for (const [key, value] of Object.entries(
1659
+ node
1660
+ )) {
1661
+ if (reservedFields.has(key)) {
1662
+ throw new SDJWTException(
1663
+ `Reserved field name "${key}" is not allowed`
1664
+ );
1665
+ }
1666
+ visit(value);
1667
+ }
1668
+ };
1669
+ visit(payload);
1670
+ }
1671
+ validateDisclosureFrame(_disclosureFrame) {
1612
1672
  return;
1613
1673
  }
1614
1674
  present(generalJSON, presentationFrame, options) {
@@ -1741,7 +1801,7 @@ var SDJwtGeneralJSONInstance = class {
1741
1801
  `${encodedHeader}.${payload}`,
1742
1802
  signature
1743
1803
  );
1744
- const header = JSON.parse((0, import_identity_common.base64urlDecode)(encodedHeader));
1804
+ const header = decodeBase64urlJsonStrict(encodedHeader, "Invalid JWT");
1745
1805
  return { verified: verified2, header };
1746
1806
  }))
1747
1807
  );
package/dist/index.mjs CHANGED
@@ -102,6 +102,18 @@ function ensureError(value) {
102
102
  return new Error(String(value));
103
103
  }
104
104
 
105
+ // src/utils/strict-json.ts
106
+ var utf8Decoder = new TextDecoder("utf-8", { fatal: true });
107
+ var decodeBase64urlJsonStrict = (encoded, errorMessage) => {
108
+ try {
109
+ const bytes = base64UrlToUint8Array(encoded);
110
+ const decoded = utf8Decoder.decode(bytes);
111
+ return JSON.parse(decoded);
112
+ } catch (e) {
113
+ throw new SDJWTException(errorMessage);
114
+ }
115
+ };
116
+
105
117
  // src/utils/disclosure.ts
106
118
  var Disclosure = class _Disclosure {
107
119
  constructor(data, _meta) {
@@ -128,7 +140,10 @@ var Disclosure = class _Disclosure {
128
140
  const { hasher, alg } = hash;
129
141
  const digest = yield hasher(s, alg);
130
142
  const digestStr = uint8ArrayToBase64Url(digest);
131
- const item = JSON.parse(base64urlDecode(s));
143
+ const item = decodeBase64urlJsonStrict(
144
+ s,
145
+ "Invalid disclosure data"
146
+ );
132
147
  return _Disclosure.fromArray(item, { digest: digestStr, encoded: s });
133
148
  });
134
149
  }
@@ -136,7 +151,10 @@ var Disclosure = class _Disclosure {
136
151
  const { hasher, alg } = hash;
137
152
  const digest = hasher(s, alg);
138
153
  const digestStr = uint8ArrayToBase64Url(digest);
139
- const item = JSON.parse(base64urlDecode(s));
154
+ const item = decodeBase64urlJsonStrict(
155
+ s,
156
+ "Invalid disclosure data"
157
+ );
140
158
  return _Disclosure.fromArray(item, { digest: digestStr, encoded: s });
141
159
  }
142
160
  static fromArray(item, _meta) {
@@ -178,8 +196,8 @@ var decodeJwt = (jwt) => {
178
196
  throw new SDJWTException("Invalid JWT as input");
179
197
  }
180
198
  return {
181
- header: JSON.parse(base64urlDecode(header)),
182
- payload: JSON.parse(base64urlDecode(payload)),
199
+ header: decodeBase64urlJsonStrict(header, "Invalid JWT as input"),
200
+ payload: decodeBase64urlJsonStrict(payload, "Invalid JWT as input"),
183
201
  signature
184
202
  };
185
203
  };
@@ -372,6 +390,11 @@ var getSDAlgAndPayload = (SdJwtPayload) => {
372
390
  if (typeof _sd_alg !== "string") {
373
391
  return { _sd_alg: "sha-256", payload };
374
392
  }
393
+ if (!IANA_HASH_ALGORITHMS.includes(
394
+ _sd_alg
395
+ )) {
396
+ throw new SDJWTException(`Invalid _sd_alg: ${_sd_alg}`);
397
+ }
375
398
  return { _sd_alg, payload };
376
399
  };
377
400
  var unpack = (SdJwtPayload, disclosures, hasher) => __async(null, null, function* () {
@@ -1096,9 +1119,8 @@ var _SDJwtInstance = class _SDJwtInstance {
1096
1119
  if (!this.userConfig.signAlg) {
1097
1120
  throw new SDJWTException("sign alogrithm not specified");
1098
1121
  }
1099
- if (disclosureFrame) {
1100
- this.validateReservedFields(disclosureFrame);
1101
- }
1122
+ this.validateReservedFields(payload);
1123
+ this.validateDisclosureFrame(disclosureFrame);
1102
1124
  const hasher = this.userConfig.hasher;
1103
1125
  const hashAlg = (_a = this.userConfig.hashAlg) != null ? _a : _SDJwtInstance.DEFAULT_hashAlg;
1104
1126
  const { packedClaims, disclosures } = yield pack(
@@ -1126,11 +1148,30 @@ var _SDJwtInstance = class _SDJwtInstance {
1126
1148
  });
1127
1149
  }
1128
1150
  /**
1129
- * Validates if the disclosureFrame contains any reserved fields. If so it will throw an error.
1130
- * @param disclosureFrame
1151
+ * Validates if the payload contains any reserved claim names. If so it will throw an error.
1152
+ * @param payload
1131
1153
  * @returns
1132
1154
  */
1133
- validateReservedFields(_disclosureFrame) {
1155
+ validateReservedFields(payload) {
1156
+ const reservedFields = /* @__PURE__ */ new Set([SD_DIGEST, "_sd_alg", SD_DECOY]);
1157
+ const visit = (node) => {
1158
+ if (!node || typeof node !== "object") {
1159
+ return;
1160
+ }
1161
+ for (const [key, value] of Object.entries(
1162
+ node
1163
+ )) {
1164
+ if (reservedFields.has(key)) {
1165
+ throw new SDJWTException(
1166
+ `Reserved field name "${key}" is not allowed`
1167
+ );
1168
+ }
1169
+ visit(value);
1170
+ }
1171
+ };
1172
+ visit(payload);
1173
+ }
1174
+ validateDisclosureFrame(_disclosureFrame) {
1134
1175
  return;
1135
1176
  }
1136
1177
  present(encodedSDJwt, presentationFrame, options) {
@@ -1506,9 +1547,8 @@ var SDJwtGeneralJSONInstance = class {
1506
1547
  if (!this.userConfig.saltGenerator) {
1507
1548
  throw new SDJWTException("SaltGenerator not found");
1508
1549
  }
1509
- if (disclosureFrame) {
1510
- this.validateReservedFields(disclosureFrame);
1511
- }
1550
+ this.validateReservedFields(payload);
1551
+ this.validateDisclosureFrame(disclosureFrame);
1512
1552
  const hasher = this.userConfig.hasher;
1513
1553
  const hashAlg = (_a = this.userConfig.hashAlg) != null ? _a : SDJwtInstance.DEFAULT_hashAlg;
1514
1554
  const { packedClaims, disclosures } = yield pack(
@@ -1517,10 +1557,12 @@ var SDJwtGeneralJSONInstance = class {
1517
1557
  { hasher, alg: hashAlg },
1518
1558
  this.userConfig.saltGenerator
1519
1559
  );
1520
- const encodedDisclosures = disclosures.map((d) => d.encode());
1521
1560
  const encodedSDJwtPayload = this.encodeObj(__spreadProps(__spreadValues({}, packedClaims), {
1522
1561
  _sd_alg: disclosureFrame ? hashAlg : void 0
1523
1562
  }));
1563
+ const encodedDisclosures = disclosures.map(
1564
+ (disclosure) => disclosure.encode()
1565
+ );
1524
1566
  const signatures = yield Promise.all(
1525
1567
  options.sigs.map((s) => __async(this, null, function* () {
1526
1568
  const { signer, alg, kid, header } = s;
@@ -1531,7 +1573,6 @@ var SDJwtGeneralJSONInstance = class {
1531
1573
  );
1532
1574
  return {
1533
1575
  protected: encodedProtectedHeader,
1534
- kid,
1535
1576
  signature
1536
1577
  };
1537
1578
  }))
@@ -1545,11 +1586,30 @@ var SDJwtGeneralJSONInstance = class {
1545
1586
  });
1546
1587
  }
1547
1588
  /**
1548
- * Validates if the disclosureFrame contains any reserved fields. If so it will throw an error.
1549
- * @param disclosureFrame
1589
+ * Validates if the payload contains any reserved claim names. If so it will throw an error.
1590
+ * @param payload
1550
1591
  * @returns
1551
1592
  */
1552
- validateReservedFields(_disclosureFrame) {
1593
+ validateReservedFields(payload) {
1594
+ const reservedFields = /* @__PURE__ */ new Set([SD_DIGEST, "_sd_alg", SD_DECOY]);
1595
+ const visit = (node) => {
1596
+ if (!node || typeof node !== "object") {
1597
+ return;
1598
+ }
1599
+ for (const [key, value] of Object.entries(
1600
+ node
1601
+ )) {
1602
+ if (reservedFields.has(key)) {
1603
+ throw new SDJWTException(
1604
+ `Reserved field name "${key}" is not allowed`
1605
+ );
1606
+ }
1607
+ visit(value);
1608
+ }
1609
+ };
1610
+ visit(payload);
1611
+ }
1612
+ validateDisclosureFrame(_disclosureFrame) {
1553
1613
  return;
1554
1614
  }
1555
1615
  present(generalJSON, presentationFrame, options) {
@@ -1682,7 +1742,7 @@ var SDJwtGeneralJSONInstance = class {
1682
1742
  `${encodedHeader}.${payload}`,
1683
1743
  signature
1684
1744
  );
1685
- const header = JSON.parse(base64urlDecode(encodedHeader));
1745
+ const header = decodeBase64urlJsonStrict(encodedHeader, "Invalid JWT");
1686
1746
  return { verified: verified2, header };
1687
1747
  }))
1688
1748
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sd-jwt/core",
3
- "version": "0.19.1-next.12+c9d73e4",
3
+ "version": "0.19.1-next.13+3d8a72a",
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": "c9d73e4db2a925e5096988fffe7eded62db1b6a7"
61
+ "gitHead": "3d8a72a0a279d67439db038c9c2b52621568b866"
62
62
  }
@@ -1,12 +1,15 @@
1
- import type { HasherAndAlgSync, HasherSync } from '../types';
2
1
  import {
3
2
  type Hasher,
4
3
  type HasherAndAlg,
4
+ type HasherAndAlgSync,
5
+ type HasherSync,
6
+ IANA_HASH_ALGORITHMS,
5
7
  SD_DIGEST,
6
8
  SD_LIST_KEY,
7
9
  SD_SEPARATOR,
8
10
  } from '../types';
9
- import { base64urlDecode, Disclosure, SDJWTException } from '../utils';
11
+ import { Disclosure, SDJWTException } from '../utils';
12
+ import { decodeBase64urlJsonStrict } from '../utils/strict-json';
10
13
 
11
14
  export const decodeJwt = <
12
15
  H extends Record<string, unknown>,
@@ -20,8 +23,8 @@ export const decodeJwt = <
20
23
  }
21
24
 
22
25
  return {
23
- header: JSON.parse(base64urlDecode(header)),
24
- payload: JSON.parse(base64urlDecode(payload)),
26
+ header: decodeBase64urlJsonStrict(header, 'Invalid JWT as input'),
27
+ payload: decodeBase64urlJsonStrict(payload, 'Invalid JWT as input'),
25
28
  signature: signature,
26
29
  };
27
30
  };
@@ -319,6 +322,13 @@ export const getSDAlgAndPayload = (SdJwtPayload: Record<string, unknown>) => {
319
322
  // This is for compatibility
320
323
  return { _sd_alg: 'sha-256', payload };
321
324
  }
325
+ if (
326
+ !IANA_HASH_ALGORITHMS.includes(
327
+ _sd_alg as (typeof IANA_HASH_ALGORITHMS)[number],
328
+ )
329
+ ) {
330
+ throw new SDJWTException(`Invalid _sd_alg: ${_sd_alg}`);
331
+ }
322
332
  return { _sd_alg, payload };
323
333
  };
324
334
 
package/src/index.ts CHANGED
@@ -12,6 +12,8 @@ import {
12
12
  type KBOptions,
13
13
  type PresentationFrame,
14
14
  type SafeVerifyResult,
15
+ SD_DECOY,
16
+ SD_DIGEST,
15
17
  type SDJWTCompact,
16
18
  type SDJWTConfig,
17
19
  type Signer,
@@ -19,12 +21,12 @@ import {
19
21
  type VerificationErrorCode,
20
22
  } from './types';
21
23
  import {
22
- base64urlDecode,
23
24
  base64urlEncode,
24
25
  ensureError,
25
26
  SDJWTException,
26
27
  uint8ArrayToBase64Url,
27
28
  } from './utils';
29
+ import { decodeBase64urlJsonStrict } from './utils/strict-json';
28
30
 
29
31
  export * from './decode';
30
32
  export * from './decoy';
@@ -120,9 +122,8 @@ export class SDJwtInstance<ExtendedPayload extends SdJwtPayload, T = unknown> {
120
122
  throw new SDJWTException('sign alogrithm not specified');
121
123
  }
122
124
 
123
- if (disclosureFrame) {
124
- this.validateReservedFields<Payload>(disclosureFrame);
125
- }
125
+ this.validateReservedFields<Payload>(payload);
126
+ this.validateDisclosureFrame<Payload>(disclosureFrame);
126
127
 
127
128
  const hasher = this.userConfig.hasher;
128
129
  const hashAlg = this.userConfig.hashAlg ?? SDJwtInstance.DEFAULT_hashAlg;
@@ -157,12 +158,35 @@ export class SDJwtInstance<ExtendedPayload extends SdJwtPayload, T = unknown> {
157
158
  }
158
159
 
159
160
  /**
160
- * Validates if the disclosureFrame contains any reserved fields. If so it will throw an error.
161
- * @param disclosureFrame
161
+ * Validates if the payload contains any reserved claim names. If so it will throw an error.
162
+ * @param payload
162
163
  * @returns
163
164
  */
164
- protected validateReservedFields<T extends ExtendedPayload>(
165
- _disclosureFrame: DisclosureFrame<T>,
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>,
166
190
  ) {
167
191
  return;
168
192
  }
@@ -629,9 +653,8 @@ export class SDJwtGeneralJSONInstance<ExtendedPayload extends SdJwtPayload> {
629
653
  throw new SDJWTException('SaltGenerator not found');
630
654
  }
631
655
 
632
- if (disclosureFrame) {
633
- this.validateReservedFields<Payload>(disclosureFrame);
634
- }
656
+ this.validateReservedFields<Payload>(payload);
657
+ this.validateDisclosureFrame<Payload>(disclosureFrame);
635
658
 
636
659
  const hasher = this.userConfig.hasher;
637
660
  const hashAlg = this.userConfig.hashAlg ?? SDJwtInstance.DEFAULT_hashAlg;
@@ -643,12 +666,15 @@ export class SDJwtGeneralJSONInstance<ExtendedPayload extends SdJwtPayload> {
643
666
  this.userConfig.saltGenerator,
644
667
  );
645
668
 
646
- const encodedDisclosures = disclosures.map((d) => d.encode());
647
669
  const encodedSDJwtPayload = this.encodeObj({
648
670
  ...packedClaims,
649
671
  _sd_alg: disclosureFrame ? hashAlg : undefined,
650
672
  });
651
673
 
674
+ const encodedDisclosures = disclosures.map((disclosure) =>
675
+ disclosure.encode(),
676
+ );
677
+
652
678
  const signatures = await Promise.all(
653
679
  options.sigs.map(async (s) => {
654
680
  const { signer, alg, kid, header } = s;
@@ -660,7 +686,6 @@ export class SDJwtGeneralJSONInstance<ExtendedPayload extends SdJwtPayload> {
660
686
 
661
687
  return {
662
688
  protected: encodedProtectedHeader,
663
- kid,
664
689
  signature,
665
690
  };
666
691
  }),
@@ -676,12 +701,35 @@ export class SDJwtGeneralJSONInstance<ExtendedPayload extends SdJwtPayload> {
676
701
  }
677
702
 
678
703
  /**
679
- * Validates if the disclosureFrame contains any reserved fields. If so it will throw an error.
680
- * @param disclosureFrame
704
+ * Validates if the payload contains any reserved claim names. If so it will throw an error.
705
+ * @param payload
681
706
  * @returns
682
707
  */
683
- protected validateReservedFields<T extends ExtendedPayload>(
684
- _disclosureFrame: DisclosureFrame<T>,
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>,
685
733
  ) {
686
734
  return;
687
735
  }
@@ -836,7 +884,7 @@ export class SDJwtGeneralJSONInstance<ExtendedPayload extends SdJwtPayload> {
836
884
  `${encodedHeader}.${payload}`,
837
885
  signature,
838
886
  );
839
- const header = JSON.parse(base64urlDecode(encodedHeader));
887
+ const header = decodeBase64urlJsonStrict(encodedHeader, 'Invalid JWT');
840
888
  return { verified, header };
841
889
  }),
842
890
  );
@@ -100,6 +100,32 @@ describe('index', () => {
100
100
  expect(credential).toBeDefined();
101
101
  });
102
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
+
103
129
  test('verify failed', async () => {
104
130
  const { signer } = createSignerVerifier();
105
131
  const { publicKey } = Crypto.generateKeyPairSync('ed25519');
@@ -424,6 +450,38 @@ describe('index', () => {
424
450
  }
425
451
  });
426
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
+
427
485
  test('SaltGenerator not found', async () => {
428
486
  const sdjwt = new SDJwtInstance<SdJwtPayload>({
429
487
  hasher: digest,
@@ -1,10 +1,7 @@
1
1
  import type { DisclosureData, HasherAndAlg, HasherAndAlgSync } from '../types';
2
- import {
3
- base64urlDecode,
4
- base64urlEncode,
5
- uint8ArrayToBase64Url,
6
- } from './base64url';
2
+ import { base64urlEncode, uint8ArrayToBase64Url } from './base64url';
7
3
  import { SDJWTException } from './error';
4
+ import { decodeBase64urlJsonStrict } from './strict-json';
8
5
 
9
6
  export class Disclosure<T = unknown> {
10
7
  public salt: string;
@@ -42,7 +39,10 @@ export class Disclosure<T = unknown> {
42
39
  const { hasher, alg } = hash;
43
40
  const digest = await hasher(s, alg);
44
41
  const digestStr = uint8ArrayToBase64Url(digest);
45
- const item = JSON.parse(base64urlDecode(s)) as DisclosureData<T>;
42
+ const item = decodeBase64urlJsonStrict<DisclosureData<T>>(
43
+ s,
44
+ 'Invalid disclosure data',
45
+ );
46
46
  return Disclosure.fromArray<T>(item, { digest: digestStr, encoded: s });
47
47
  }
48
48
 
@@ -50,7 +50,10 @@ export class Disclosure<T = unknown> {
50
50
  const { hasher, alg } = hash;
51
51
  const digest = hasher(s, alg);
52
52
  const digestStr = uint8ArrayToBase64Url(digest);
53
- const item = JSON.parse(base64urlDecode(s)) as DisclosureData<T>;
53
+ const item = decodeBase64urlJsonStrict<DisclosureData<T>>(
54
+ s,
55
+ 'Invalid disclosure data',
56
+ );
54
57
  return Disclosure.fromArray<T>(item, { digest: digestStr, encoded: s });
55
58
  }
56
59
 
@@ -0,0 +1,17 @@
1
+ import { base64UrlToUint8Array } from './base64url';
2
+ import { SDJWTException } from './error';
3
+
4
+ const utf8Decoder = new TextDecoder('utf-8', { fatal: true });
5
+
6
+ export const decodeBase64urlJsonStrict = <T>(
7
+ encoded: string,
8
+ errorMessage: string,
9
+ ): T => {
10
+ try {
11
+ const bytes = base64UrlToUint8Array(encoded);
12
+ const decoded = utf8Decoder.decode(bytes);
13
+ return JSON.parse(decoded) as T;
14
+ } catch {
15
+ throw new SDJWTException(errorMessage);
16
+ }
17
+ };
@@ -135,6 +135,37 @@ describe('App', () => {
135
135
  expect(verified).toBeDefined();
136
136
  });
137
137
 
138
+ test('rejects tampered disclosure bytes', async () => {
139
+ const { signer, verifier } = createSignerVerifier();
140
+ const sdjwt = new SDJwtInstance<SdJwtPayload>({
141
+ signer,
142
+ signAlg: 'EdDSA',
143
+ verifier,
144
+ hasher: digest,
145
+ hashAlg: 'sha-256',
146
+ saltGenerator: generateSalt,
147
+ });
148
+
149
+ const claims = {
150
+ firstname: 'John',
151
+ lastname: 'Doe',
152
+ };
153
+ const disclosureFrame: DisclosureFrame<typeof claims> = {
154
+ _sd: ['firstname', 'lastname'],
155
+ };
156
+
157
+ const encodedSdjwt = await sdjwt.issue(claims, disclosureFrame);
158
+ const parts = encodedSdjwt.split('~');
159
+ const disclosureBytes = Buffer.from(parts[1], 'base64url');
160
+ disclosureBytes[2] = 0xf2;
161
+ parts[1] = Buffer.from(disclosureBytes).toString('base64url');
162
+ const tamperedSdjwt = parts.join('~');
163
+
164
+ await expect(sdjwt.verify(tamperedSdjwt)).rejects.toThrow(
165
+ 'Invalid disclosure data',
166
+ );
167
+ });
168
+
138
169
  test('From JSON (complex)', async () => {
139
170
  await JSONtest('./complex.json');
140
171
  });