@sd-jwt/sd-jwt-vc 0.7.2 → 0.8.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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,22 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [0.8.0](https://github.com/openwallet-foundation-labs/sd-jwt-js/compare/v0.7.2...v0.8.0) (2024-11-26)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+ * check if the header includes the string ([#244](https://github.com/openwallet-foundation-labs/sd-jwt-js/issues/244)) ([8a48bb5](https://github.com/openwallet-foundation-labs/sd-jwt-js/commit/8a48bb57fcf9bbad349f349b0aa1ffd997c86bb2))
12
+
13
+
14
+ ### Features
15
+
16
+ * align media type with sd-jwt-vc draft 06 ([#256](https://github.com/openwallet-foundation-labs/sd-jwt-js/issues/256)) ([1aa3aea](https://github.com/openwallet-foundation-labs/sd-jwt-js/commit/1aa3aea86213e75328975e34d9bf71410fc7a12a))
17
+
18
+
19
+
20
+
21
+
6
22
  ## [0.7.2](https://github.com/openwallet-foundation-labs/sd-jwt-js/compare/v0.7.1...v0.7.2) (2024-07-19)
7
23
 
8
24
  **Note:** Version bump only for package @sd-jwt/sd-jwt-vc
package/README.md CHANGED
@@ -84,8 +84,28 @@ const verified = await sdjwt.verify(presentation);
84
84
  Check out more details in our [documentation](https://github.com/openwallet-foundation-labs/sd-jwt-js/tree/main/docs) or [examples](https://github.com/openwallet-foundation-labs/sd-jwt-js/tree/main/examples)
85
85
 
86
86
  ### Revocation
87
+
87
88
  To add revocation capabilities, you can use the `@sd-jwt/jwt-status-list` library to create a JWT Status List and include it in the SD-JWT-VC.
88
89
 
90
+ ### Type Metadata
91
+
92
+ By setting the `loadTypeMetadataFormat` to `true` like this:
93
+
94
+ ```typescript
95
+ const sdjwt = new SDJwtVcInstance({
96
+ signer,
97
+ signAlg: 'EdDSA',
98
+ verifier,
99
+ hasher: digest,
100
+ hashAlg: 'SHA-256',
101
+ saltGenerator: generateSalt,
102
+ loadTypeMetadataFormat: true,
103
+ });
104
+ ```
105
+
106
+ The library will load load the type metadata format based on the `vct` value according to the [SD-JWT-VC specification](https://www.ietf.org/archive/id/draft-ietf-oauth-sd-jwt-vc-04.html#name-type-metadata) and validate this schema.
107
+
108
+ Since at this point the display is not yet implemented, the library will only validate the schema and return the type metadata format. In the future the values of the type metadata can be fetched via a function call.
89
109
 
90
110
  ### Dependencies
91
111
 
package/dist/index.d.mts CHANGED
@@ -1,13 +1,32 @@
1
- import * as _sd_jwt_types from '@sd-jwt/types';
2
- import { SDJWTConfig, DisclosureFrame } from '@sd-jwt/types';
1
+ import { SDJWTConfig, kbPayload, kbHeader, DisclosureFrame } from '@sd-jwt/types';
3
2
  import { SdJwtPayload, SDJwtInstance } from '@sd-jwt/core';
4
3
 
4
+ /**
5
+ * https://www.ietf.org/archive/id/draft-ietf-oauth-sd-jwt-vc-04.html#name-type-metadata-format
6
+ */
7
+ type TypeMetadataFormat = {
8
+ vct: string;
9
+ name?: string;
10
+ description?: string;
11
+ extends?: string;
12
+ 'extends#Integrity'?: string;
13
+ schema?: object;
14
+ schema_uri?: string;
15
+ 'schema_uri#Integrity'?: string;
16
+ };
17
+
18
+ type VcTFetcher = (uri: string, integrity?: string) => Promise<TypeMetadataFormat>;
19
+
20
+ type StatusListFetcher = (uri: string) => Promise<string>;
21
+ type StatusValidator = (status: number) => Promise<void>;
5
22
  /**
6
23
  * Configuration for SD-JWT-VC
7
24
  */
8
25
  type SDJWTVCConfig = SDJWTConfig & {
9
- statusListFetcher?: (uri: string) => Promise<string>;
10
- statusValidator?: (status: number) => Promise<void>;
26
+ statusListFetcher?: StatusListFetcher;
27
+ statusValidator?: StatusValidator;
28
+ vctFetcher?: VcTFetcher;
29
+ loadTypeMetadataFormat?: boolean;
11
30
  };
12
31
 
13
32
  interface SDJWTVCStatusReference {
@@ -23,11 +42,22 @@ interface SdJwtVcPayload extends SdJwtPayload {
23
42
  exp?: number;
24
43
  cnf?: unknown;
25
44
  vct: string;
45
+ 'vct#Integrity'?: string;
26
46
  status?: SDJWTVCStatusReference;
27
47
  sub?: string;
28
48
  iat?: number;
29
49
  }
30
50
 
51
+ type VerificationResult = {
52
+ payload: SdJwtVcPayload;
53
+ header: Record<string, unknown> | undefined;
54
+ kb: {
55
+ payload: kbPayload;
56
+ header: kbHeader;
57
+ } | undefined;
58
+ typeMetadataFormat?: TypeMetadataFormat;
59
+ };
60
+
31
61
  declare class SDJwtVcInstance extends SDJwtInstance<SdJwtVcPayload> {
32
62
  /**
33
63
  * The type of the SD-JWT-VC set in the header.typ field.
@@ -53,16 +83,44 @@ declare class SDJwtVcInstance extends SDJwtInstance<SdJwtVcPayload> {
53
83
  */
54
84
  private statusValidator;
55
85
  /**
56
- * Verifies the SD-JWT-VC.
86
+ * Verifies the SD-JWT-VC. It will validate the signature, the keybindings when required, the status, and the VCT.
87
+ */
88
+ verify(encodedSDJwt: string, requiredClaimKeys?: string[], requireKeyBindings?: boolean): Promise<VerificationResult>;
89
+ /**
90
+ * Default function to fetch the VCT from the uri. We assume that the vct is a URL that is used to fetch the VCT.
91
+ * @param uri
92
+ * @returns
93
+ */
94
+ private vctFetcher;
95
+ /**
96
+ * Validates the integrity of the response if the integrity is passed. If the integrity does not match, an error is thrown.
97
+ * @param integrity
98
+ * @param response
99
+ */
100
+ private validateIntegrity;
101
+ /**
102
+ * Fetches the content from the url with a timeout of 10 seconds.
103
+ * @param url
104
+ * @returns
105
+ */
106
+ private fetch;
107
+ /**
108
+ * Loads the schema either from the object or as fallback from the uri.
109
+ * @param typeMetadataFormat
110
+ * @returns
111
+ */
112
+ private loadSchema;
113
+ /**
114
+ * Verifies the VCT of the SD-JWT-VC. Returns the type metadata format. If the schema does not match, an error is thrown. If it matches, it will return the type metadata format.
115
+ * @param result
116
+ * @returns
117
+ */
118
+ private verifyVct;
119
+ /**
120
+ * Verifies the status of the SD-JWT-VC.
121
+ * @param result
57
122
  */
58
- verify(encodedSDJwt: string, requiredClaimKeys?: string[], requireKeyBindings?: boolean): Promise<{
59
- payload: SdJwtVcPayload;
60
- header: Record<string, unknown> | undefined;
61
- kb: {
62
- payload: _sd_jwt_types.kbPayload;
63
- header: _sd_jwt_types.kbHeader;
64
- } | undefined;
65
- }>;
123
+ private verifyStatus;
66
124
  }
67
125
 
68
- export { type SDJWTVCConfig, type SDJWTVCStatusReference, SDJwtVcInstance, type SdJwtVcPayload };
126
+ export { type SDJWTVCConfig, type SDJWTVCStatusReference, SDJwtVcInstance, type SdJwtVcPayload, type StatusListFetcher, type StatusValidator };
package/dist/index.d.ts CHANGED
@@ -1,13 +1,32 @@
1
- import * as _sd_jwt_types from '@sd-jwt/types';
2
- import { SDJWTConfig, DisclosureFrame } from '@sd-jwt/types';
1
+ import { SDJWTConfig, kbPayload, kbHeader, DisclosureFrame } from '@sd-jwt/types';
3
2
  import { SdJwtPayload, SDJwtInstance } from '@sd-jwt/core';
4
3
 
4
+ /**
5
+ * https://www.ietf.org/archive/id/draft-ietf-oauth-sd-jwt-vc-04.html#name-type-metadata-format
6
+ */
7
+ type TypeMetadataFormat = {
8
+ vct: string;
9
+ name?: string;
10
+ description?: string;
11
+ extends?: string;
12
+ 'extends#Integrity'?: string;
13
+ schema?: object;
14
+ schema_uri?: string;
15
+ 'schema_uri#Integrity'?: string;
16
+ };
17
+
18
+ type VcTFetcher = (uri: string, integrity?: string) => Promise<TypeMetadataFormat>;
19
+
20
+ type StatusListFetcher = (uri: string) => Promise<string>;
21
+ type StatusValidator = (status: number) => Promise<void>;
5
22
  /**
6
23
  * Configuration for SD-JWT-VC
7
24
  */
8
25
  type SDJWTVCConfig = SDJWTConfig & {
9
- statusListFetcher?: (uri: string) => Promise<string>;
10
- statusValidator?: (status: number) => Promise<void>;
26
+ statusListFetcher?: StatusListFetcher;
27
+ statusValidator?: StatusValidator;
28
+ vctFetcher?: VcTFetcher;
29
+ loadTypeMetadataFormat?: boolean;
11
30
  };
12
31
 
13
32
  interface SDJWTVCStatusReference {
@@ -23,11 +42,22 @@ interface SdJwtVcPayload extends SdJwtPayload {
23
42
  exp?: number;
24
43
  cnf?: unknown;
25
44
  vct: string;
45
+ 'vct#Integrity'?: string;
26
46
  status?: SDJWTVCStatusReference;
27
47
  sub?: string;
28
48
  iat?: number;
29
49
  }
30
50
 
51
+ type VerificationResult = {
52
+ payload: SdJwtVcPayload;
53
+ header: Record<string, unknown> | undefined;
54
+ kb: {
55
+ payload: kbPayload;
56
+ header: kbHeader;
57
+ } | undefined;
58
+ typeMetadataFormat?: TypeMetadataFormat;
59
+ };
60
+
31
61
  declare class SDJwtVcInstance extends SDJwtInstance<SdJwtVcPayload> {
32
62
  /**
33
63
  * The type of the SD-JWT-VC set in the header.typ field.
@@ -53,16 +83,44 @@ declare class SDJwtVcInstance extends SDJwtInstance<SdJwtVcPayload> {
53
83
  */
54
84
  private statusValidator;
55
85
  /**
56
- * Verifies the SD-JWT-VC.
86
+ * Verifies the SD-JWT-VC. It will validate the signature, the keybindings when required, the status, and the VCT.
87
+ */
88
+ verify(encodedSDJwt: string, requiredClaimKeys?: string[], requireKeyBindings?: boolean): Promise<VerificationResult>;
89
+ /**
90
+ * Default function to fetch the VCT from the uri. We assume that the vct is a URL that is used to fetch the VCT.
91
+ * @param uri
92
+ * @returns
93
+ */
94
+ private vctFetcher;
95
+ /**
96
+ * Validates the integrity of the response if the integrity is passed. If the integrity does not match, an error is thrown.
97
+ * @param integrity
98
+ * @param response
99
+ */
100
+ private validateIntegrity;
101
+ /**
102
+ * Fetches the content from the url with a timeout of 10 seconds.
103
+ * @param url
104
+ * @returns
105
+ */
106
+ private fetch;
107
+ /**
108
+ * Loads the schema either from the object or as fallback from the uri.
109
+ * @param typeMetadataFormat
110
+ * @returns
111
+ */
112
+ private loadSchema;
113
+ /**
114
+ * Verifies the VCT of the SD-JWT-VC. Returns the type metadata format. If the schema does not match, an error is thrown. If it matches, it will return the type metadata format.
115
+ * @param result
116
+ * @returns
117
+ */
118
+ private verifyVct;
119
+ /**
120
+ * Verifies the status of the SD-JWT-VC.
121
+ * @param result
57
122
  */
58
- verify(encodedSDJwt: string, requiredClaimKeys?: string[], requireKeyBindings?: boolean): Promise<{
59
- payload: SdJwtVcPayload;
60
- header: Record<string, unknown> | undefined;
61
- kb: {
62
- payload: _sd_jwt_types.kbPayload;
63
- header: _sd_jwt_types.kbHeader;
64
- } | undefined;
65
- }>;
123
+ private verifyStatus;
66
124
  }
67
125
 
68
- export { type SDJWTVCConfig, type SDJWTVCStatusReference, SDJwtVcInstance, type SdJwtVcPayload };
126
+ export { type SDJWTVCConfig, type SDJWTVCStatusReference, SDJwtVcInstance, type SdJwtVcPayload, type StatusListFetcher, type StatusValidator };
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -17,6 +18,14 @@ var __copyProps = (to, from, except, desc) => {
17
18
  }
18
19
  return to;
19
20
  };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
20
29
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
21
30
  var __superGet = (cls, obj, key) => __reflectGet(__getProtoOf(cls), key, obj);
22
31
  var __async = (__this, __arguments, generator) => {
@@ -51,13 +60,15 @@ module.exports = __toCommonJS(src_exports);
51
60
  var import_core = require("@sd-jwt/core");
52
61
  var import_utils = require("@sd-jwt/utils");
53
62
  var import_jwt_status_list = require("@sd-jwt/jwt-status-list");
63
+ var import_ajv = __toESM(require("ajv"));
64
+ var import_ajv_formats = __toESM(require("ajv-formats"));
54
65
  var SDJwtVcInstance = class _SDJwtVcInstance extends import_core.SDJwtInstance {
55
66
  constructor(userConfig) {
56
67
  super(userConfig);
57
68
  /**
58
69
  * The type of the SD-JWT-VC set in the header.typ field.
59
70
  */
60
- this.type = "vc+sd-jwt";
71
+ this.type = "dc+sd-jwt";
61
72
  this.userConfig = {};
62
73
  if (userConfig) {
63
74
  this.userConfig = userConfig;
@@ -83,6 +94,7 @@ var SDJwtVcInstance = class _SDJwtVcInstance extends import_core.SDJwtInstance {
83
94
  */
84
95
  statusListFetcher(uri) {
85
96
  return __async(this, null, function* () {
97
+ var _a;
86
98
  const controller = new AbortController();
87
99
  const timeoutId = setTimeout(() => controller.abort(), 1e4);
88
100
  try {
@@ -95,7 +107,7 @@ var SDJwtVcInstance = class _SDJwtVcInstance extends import_core.SDJwtInstance {
95
107
  `Error fetching status list: ${response.status} ${yield response.text()}`
96
108
  );
97
109
  }
98
- if (response.headers.get("content-type") !== "application/statuslist+jwt") {
110
+ if (!((_a = response.headers.get("content-type")) == null ? void 0 : _a.includes("application/statuslist+jwt"))) {
99
111
  throw new Error("Invalid content type");
100
112
  }
101
113
  return response.text();
@@ -117,11 +129,10 @@ var SDJwtVcInstance = class _SDJwtVcInstance extends import_core.SDJwtInstance {
117
129
  });
118
130
  }
119
131
  /**
120
- * Verifies the SD-JWT-VC.
132
+ * Verifies the SD-JWT-VC. It will validate the signature, the keybindings when required, the status, and the VCT.
121
133
  */
122
134
  verify(encodedSDJwt, requiredClaimKeys, requireKeyBindings) {
123
135
  return __async(this, null, function* () {
124
- var _a, _b, _c;
125
136
  const result = yield __superGet(_SDJwtVcInstance.prototype, this, "verify").call(this, encodedSDJwt, requiredClaimKeys, requireKeyBindings).then((res) => {
126
137
  return {
127
138
  payload: res.payload,
@@ -129,9 +140,145 @@ var SDJwtVcInstance = class _SDJwtVcInstance extends import_core.SDJwtInstance {
129
140
  kb: res.kb
130
141
  };
131
142
  });
143
+ yield this.verifyStatus(result);
144
+ if (this.userConfig.loadTypeMetadataFormat) {
145
+ yield this.verifyVct(result);
146
+ }
147
+ return result;
148
+ });
149
+ }
150
+ /**
151
+ * Default function to fetch the VCT from the uri. We assume that the vct is a URL that is used to fetch the VCT.
152
+ * @param uri
153
+ * @returns
154
+ */
155
+ vctFetcher(uri, integrity) {
156
+ return __async(this, null, function* () {
157
+ const elements = uri.split("/");
158
+ elements.splice(3, 0, ".well-known/vct");
159
+ const url = elements.join("/");
160
+ return this.fetch(url, integrity);
161
+ });
162
+ }
163
+ /**
164
+ * Validates the integrity of the response if the integrity is passed. If the integrity does not match, an error is thrown.
165
+ * @param integrity
166
+ * @param response
167
+ */
168
+ validateIntegrity(response, url, integrity) {
169
+ return __async(this, null, function* () {
170
+ if (integrity) {
171
+ const arrayBuffer = yield response.arrayBuffer();
172
+ const alg = integrity.split("-")[0];
173
+ const hashBuffer = yield this.userConfig.hasher(
174
+ arrayBuffer,
175
+ alg
176
+ );
177
+ const integrityHash = integrity.split("-")[1];
178
+ const hash = Array.from(new Uint8Array(hashBuffer)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
179
+ if (hash !== integrityHash) {
180
+ throw new Error(
181
+ `Integrity check for ${url} failed: is ${hash}, but expected ${integrityHash}`
182
+ );
183
+ }
184
+ }
185
+ });
186
+ }
187
+ /**
188
+ * Fetches the content from the url with a timeout of 10 seconds.
189
+ * @param url
190
+ * @returns
191
+ */
192
+ fetch(url, integrity) {
193
+ return __async(this, null, function* () {
194
+ const controller = new AbortController();
195
+ const timeoutId = setTimeout(() => controller.abort(), 1e4);
196
+ try {
197
+ const response = yield fetch(url, {
198
+ signal: controller.signal
199
+ });
200
+ if (!response.ok) {
201
+ throw new Error(yield response.text());
202
+ }
203
+ yield this.validateIntegrity(response.clone(), url, integrity);
204
+ return response.json();
205
+ } finally {
206
+ clearTimeout(timeoutId);
207
+ }
208
+ });
209
+ }
210
+ /**
211
+ * Loads the schema either from the object or as fallback from the uri.
212
+ * @param typeMetadataFormat
213
+ * @returns
214
+ */
215
+ loadSchema(typeMetadataFormat) {
216
+ return __async(this, null, function* () {
217
+ if (typeMetadataFormat.schema)
218
+ return typeMetadataFormat.schema;
219
+ if (typeMetadataFormat.schema_uri) {
220
+ const schema = yield this.fetch(
221
+ typeMetadataFormat.schema_uri,
222
+ typeMetadataFormat["schema_uri#Integrity"]
223
+ );
224
+ return schema;
225
+ }
226
+ throw new Error("No schema or schema_uri found");
227
+ });
228
+ }
229
+ /**
230
+ * Verifies the VCT of the SD-JWT-VC. Returns the type metadata format. If the schema does not match, an error is thrown. If it matches, it will return the type metadata format.
231
+ * @param result
232
+ * @returns
233
+ */
234
+ verifyVct(result) {
235
+ return __async(this, null, function* () {
236
+ var _a;
237
+ const fetcher = (_a = this.userConfig.vctFetcher) != null ? _a : this.vctFetcher.bind(this);
238
+ const typeMetadataFormat = yield fetcher(
239
+ result.payload.vct,
240
+ result.payload["vct#Integrity"]
241
+ );
242
+ if (typeMetadataFormat.extends) {
243
+ }
244
+ const schema = yield this.loadSchema(typeMetadataFormat);
245
+ const loadedSchemas = /* @__PURE__ */ new Set();
246
+ const ajv = new import_ajv.default({
247
+ loadSchema: (uri) => __async(this, null, function* () {
248
+ if (loadedSchemas.has(uri)) {
249
+ return {};
250
+ }
251
+ const response = yield fetch(uri);
252
+ if (!response.ok) {
253
+ throw new Error(
254
+ `Error fetching schema: ${response.status} ${yield response.text()}`
255
+ );
256
+ }
257
+ loadedSchemas.add(uri);
258
+ return response.json();
259
+ })
260
+ });
261
+ (0, import_ajv_formats.default)(ajv);
262
+ const validate = yield ajv.compileAsync(schema);
263
+ const valid = validate(result.payload);
264
+ if (!valid) {
265
+ throw new import_utils.SDJWTException(
266
+ `Payload does not match the schema: ${JSON.stringify(validate.errors)}`
267
+ );
268
+ }
269
+ return typeMetadataFormat;
270
+ });
271
+ }
272
+ /**
273
+ * Verifies the status of the SD-JWT-VC.
274
+ * @param result
275
+ */
276
+ verifyStatus(result) {
277
+ return __async(this, null, function* () {
278
+ var _a, _b, _c;
132
279
  if (result.payload.status) {
133
280
  if (result.payload.status.status_list) {
134
- const fetcher = (_a = this.userConfig.statusListFetcher) != null ? _a : this.statusListFetcher;
281
+ const fetcher = (_a = this.userConfig.statusListFetcher) != null ? _a : this.statusListFetcher.bind(this);
135
282
  const statusListJWT = yield fetcher(
136
283
  result.payload.status.status_list.uri
137
284
  );
@@ -144,11 +291,10 @@ var SDJwtVcInstance = class _SDJwtVcInstance extends import_core.SDJwtInstance {
144
291
  const status = statusList.getStatus(
145
292
  result.payload.status.status_list.idx
146
293
  );
147
- const statusValidator = (_c = this.userConfig.statusValidator) != null ? _c : this.statusValidator;
294
+ const statusValidator = (_c = this.userConfig.statusValidator) != null ? _c : this.statusValidator.bind(this);
148
295
  yield statusValidator(status);
149
296
  }
150
297
  }
151
- return result;
152
298
  });
153
299
  }
154
300
  };
package/dist/index.mjs CHANGED
@@ -28,13 +28,15 @@ import { SDJWTException } from "@sd-jwt/utils";
28
28
  import {
29
29
  getListFromStatusListJWT
30
30
  } from "@sd-jwt/jwt-status-list";
31
+ import Ajv from "ajv";
32
+ import addFormats from "ajv-formats";
31
33
  var SDJwtVcInstance = class _SDJwtVcInstance extends SDJwtInstance {
32
34
  constructor(userConfig) {
33
35
  super(userConfig);
34
36
  /**
35
37
  * The type of the SD-JWT-VC set in the header.typ field.
36
38
  */
37
- this.type = "vc+sd-jwt";
39
+ this.type = "dc+sd-jwt";
38
40
  this.userConfig = {};
39
41
  if (userConfig) {
40
42
  this.userConfig = userConfig;
@@ -60,6 +62,7 @@ var SDJwtVcInstance = class _SDJwtVcInstance extends SDJwtInstance {
60
62
  */
61
63
  statusListFetcher(uri) {
62
64
  return __async(this, null, function* () {
65
+ var _a;
63
66
  const controller = new AbortController();
64
67
  const timeoutId = setTimeout(() => controller.abort(), 1e4);
65
68
  try {
@@ -72,7 +75,7 @@ var SDJwtVcInstance = class _SDJwtVcInstance extends SDJwtInstance {
72
75
  `Error fetching status list: ${response.status} ${yield response.text()}`
73
76
  );
74
77
  }
75
- if (response.headers.get("content-type") !== "application/statuslist+jwt") {
78
+ if (!((_a = response.headers.get("content-type")) == null ? void 0 : _a.includes("application/statuslist+jwt"))) {
76
79
  throw new Error("Invalid content type");
77
80
  }
78
81
  return response.text();
@@ -94,11 +97,10 @@ var SDJwtVcInstance = class _SDJwtVcInstance extends SDJwtInstance {
94
97
  });
95
98
  }
96
99
  /**
97
- * Verifies the SD-JWT-VC.
100
+ * Verifies the SD-JWT-VC. It will validate the signature, the keybindings when required, the status, and the VCT.
98
101
  */
99
102
  verify(encodedSDJwt, requiredClaimKeys, requireKeyBindings) {
100
103
  return __async(this, null, function* () {
101
- var _a, _b, _c;
102
104
  const result = yield __superGet(_SDJwtVcInstance.prototype, this, "verify").call(this, encodedSDJwt, requiredClaimKeys, requireKeyBindings).then((res) => {
103
105
  return {
104
106
  payload: res.payload,
@@ -106,9 +108,145 @@ var SDJwtVcInstance = class _SDJwtVcInstance extends SDJwtInstance {
106
108
  kb: res.kb
107
109
  };
108
110
  });
111
+ yield this.verifyStatus(result);
112
+ if (this.userConfig.loadTypeMetadataFormat) {
113
+ yield this.verifyVct(result);
114
+ }
115
+ return result;
116
+ });
117
+ }
118
+ /**
119
+ * Default function to fetch the VCT from the uri. We assume that the vct is a URL that is used to fetch the VCT.
120
+ * @param uri
121
+ * @returns
122
+ */
123
+ vctFetcher(uri, integrity) {
124
+ return __async(this, null, function* () {
125
+ const elements = uri.split("/");
126
+ elements.splice(3, 0, ".well-known/vct");
127
+ const url = elements.join("/");
128
+ return this.fetch(url, integrity);
129
+ });
130
+ }
131
+ /**
132
+ * Validates the integrity of the response if the integrity is passed. If the integrity does not match, an error is thrown.
133
+ * @param integrity
134
+ * @param response
135
+ */
136
+ validateIntegrity(response, url, integrity) {
137
+ return __async(this, null, function* () {
138
+ if (integrity) {
139
+ const arrayBuffer = yield response.arrayBuffer();
140
+ const alg = integrity.split("-")[0];
141
+ const hashBuffer = yield this.userConfig.hasher(
142
+ arrayBuffer,
143
+ alg
144
+ );
145
+ const integrityHash = integrity.split("-")[1];
146
+ const hash = Array.from(new Uint8Array(hashBuffer)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
147
+ if (hash !== integrityHash) {
148
+ throw new Error(
149
+ `Integrity check for ${url} failed: is ${hash}, but expected ${integrityHash}`
150
+ );
151
+ }
152
+ }
153
+ });
154
+ }
155
+ /**
156
+ * Fetches the content from the url with a timeout of 10 seconds.
157
+ * @param url
158
+ * @returns
159
+ */
160
+ fetch(url, integrity) {
161
+ return __async(this, null, function* () {
162
+ const controller = new AbortController();
163
+ const timeoutId = setTimeout(() => controller.abort(), 1e4);
164
+ try {
165
+ const response = yield fetch(url, {
166
+ signal: controller.signal
167
+ });
168
+ if (!response.ok) {
169
+ throw new Error(yield response.text());
170
+ }
171
+ yield this.validateIntegrity(response.clone(), url, integrity);
172
+ return response.json();
173
+ } finally {
174
+ clearTimeout(timeoutId);
175
+ }
176
+ });
177
+ }
178
+ /**
179
+ * Loads the schema either from the object or as fallback from the uri.
180
+ * @param typeMetadataFormat
181
+ * @returns
182
+ */
183
+ loadSchema(typeMetadataFormat) {
184
+ return __async(this, null, function* () {
185
+ if (typeMetadataFormat.schema)
186
+ return typeMetadataFormat.schema;
187
+ if (typeMetadataFormat.schema_uri) {
188
+ const schema = yield this.fetch(
189
+ typeMetadataFormat.schema_uri,
190
+ typeMetadataFormat["schema_uri#Integrity"]
191
+ );
192
+ return schema;
193
+ }
194
+ throw new Error("No schema or schema_uri found");
195
+ });
196
+ }
197
+ /**
198
+ * Verifies the VCT of the SD-JWT-VC. Returns the type metadata format. If the schema does not match, an error is thrown. If it matches, it will return the type metadata format.
199
+ * @param result
200
+ * @returns
201
+ */
202
+ verifyVct(result) {
203
+ return __async(this, null, function* () {
204
+ var _a;
205
+ const fetcher = (_a = this.userConfig.vctFetcher) != null ? _a : this.vctFetcher.bind(this);
206
+ const typeMetadataFormat = yield fetcher(
207
+ result.payload.vct,
208
+ result.payload["vct#Integrity"]
209
+ );
210
+ if (typeMetadataFormat.extends) {
211
+ }
212
+ const schema = yield this.loadSchema(typeMetadataFormat);
213
+ const loadedSchemas = /* @__PURE__ */ new Set();
214
+ const ajv = new Ajv({
215
+ loadSchema: (uri) => __async(this, null, function* () {
216
+ if (loadedSchemas.has(uri)) {
217
+ return {};
218
+ }
219
+ const response = yield fetch(uri);
220
+ if (!response.ok) {
221
+ throw new Error(
222
+ `Error fetching schema: ${response.status} ${yield response.text()}`
223
+ );
224
+ }
225
+ loadedSchemas.add(uri);
226
+ return response.json();
227
+ })
228
+ });
229
+ addFormats(ajv);
230
+ const validate = yield ajv.compileAsync(schema);
231
+ const valid = validate(result.payload);
232
+ if (!valid) {
233
+ throw new SDJWTException(
234
+ `Payload does not match the schema: ${JSON.stringify(validate.errors)}`
235
+ );
236
+ }
237
+ return typeMetadataFormat;
238
+ });
239
+ }
240
+ /**
241
+ * Verifies the status of the SD-JWT-VC.
242
+ * @param result
243
+ */
244
+ verifyStatus(result) {
245
+ return __async(this, null, function* () {
246
+ var _a, _b, _c;
109
247
  if (result.payload.status) {
110
248
  if (result.payload.status.status_list) {
111
- const fetcher = (_a = this.userConfig.statusListFetcher) != null ? _a : this.statusListFetcher;
249
+ const fetcher = (_a = this.userConfig.statusListFetcher) != null ? _a : this.statusListFetcher.bind(this);
112
250
  const statusListJWT = yield fetcher(
113
251
  result.payload.status.status_list.uri
114
252
  );
@@ -121,11 +259,10 @@ var SDJwtVcInstance = class _SDJwtVcInstance extends SDJwtInstance {
121
259
  const status = statusList.getStatus(
122
260
  result.payload.status.status_list.idx
123
261
  );
124
- const statusValidator = (_c = this.userConfig.statusValidator) != null ? _c : this.statusValidator;
262
+ const statusValidator = (_c = this.userConfig.statusValidator) != null ? _c : this.statusValidator.bind(this);
125
263
  yield statusValidator(status);
126
264
  }
127
265
  }
128
- return result;
129
266
  });
130
267
  }
131
268
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sd-jwt/sd-jwt-vc",
3
- "version": "0.7.2",
3
+ "version": "0.8.0",
4
4
  "description": "sd-jwt draft 7 implementation in typescript",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -15,7 +15,7 @@
15
15
  "build": "rm -rf **/dist && tsup",
16
16
  "lint": "biome lint ./src",
17
17
  "test": "pnpm run test:node && pnpm run test:browser && pnpm run test:e2e && pnpm run test:cov",
18
- "test:node": "vitest run ./src/test/*.spec.ts && vitest run ./src/test/*.spec.ts --environment jsdom",
18
+ "test:node": "vitest run ./src/test/*.spec.ts",
19
19
  "test:browser": "vitest run ./src/test/*.spec.ts --environment jsdom",
20
20
  "test:e2e": "vitest run ./test/*e2e.spec.ts --environment node",
21
21
  "test:cov": "vitest run --coverage"
@@ -39,14 +39,17 @@
39
39
  },
40
40
  "license": "Apache-2.0",
41
41
  "dependencies": {
42
- "@sd-jwt/core": "0.7.2",
43
- "@sd-jwt/jwt-status-list": "0.7.2",
44
- "@sd-jwt/utils": "0.7.2"
42
+ "@sd-jwt/core": "0.8.0",
43
+ "@sd-jwt/jwt-status-list": "0.8.0",
44
+ "@sd-jwt/utils": "0.8.0",
45
+ "ajv": "^8.17.1",
46
+ "ajv-formats": "^3.0.1"
45
47
  },
46
48
  "devDependencies": {
47
- "@sd-jwt/crypto-nodejs": "0.7.2",
48
- "@sd-jwt/types": "0.7.2",
49
- "jose": "^5.2.2"
49
+ "@sd-jwt/crypto-nodejs": "0.8.0",
50
+ "@sd-jwt/types": "0.8.0",
51
+ "jose": "^5.2.2",
52
+ "msw": "^2.3.5"
50
53
  },
51
54
  "publishConfig": {
52
55
  "access": "public"
@@ -64,5 +67,5 @@
64
67
  "esm"
65
68
  ]
66
69
  },
67
- "gitHead": "d3cc53b66be9ef40ede9f86a931b7a43f55dbd3a"
70
+ "gitHead": "0d9742cd87d643079c7828ac3689d39ac4f6f21d"
68
71
  }
@@ -1,11 +1,19 @@
1
1
  import type { SDJWTConfig } from '@sd-jwt/types';
2
+ import type { VcTFetcher } from './sd-jwt-vc-vct';
3
+
4
+ export type StatusListFetcher = (uri: string) => Promise<string>;
5
+ export type StatusValidator = (status: number) => Promise<void>;
2
6
 
3
7
  /**
4
8
  * Configuration for SD-JWT-VC
5
9
  */
6
10
  export type SDJWTVCConfig = SDJWTConfig & {
7
11
  // A function that fetches the status list from the uri. If not provided, the library will assume that the response is a compact JWT.
8
- statusListFetcher?: (uri: string) => Promise<string>;
12
+ statusListFetcher?: StatusListFetcher;
9
13
  // validte the status and decide if the status is valid or not. If not provided, the code will continue if it is 0, otherwise it will throw an error.
10
- statusValidator?: (status: number) => Promise<void>;
14
+ statusValidator?: StatusValidator;
15
+ // a function that fetches the type metadata format from the uri. If not provided, the library will assume that the response is a TypeMetadataFormat. Caching has to be implemented in this function. If the integrity value is passed, it to be validated according to https://www.w3.org/TR/SRI/
16
+ vctFetcher?: VcTFetcher;
17
+ // if set to true, it will load the metadata format based on the vct value. If not provided, it will default to false.
18
+ loadTypeMetadataFormat?: boolean;
11
19
  };
@@ -1,18 +1,28 @@
1
1
  import { Jwt, SDJwtInstance } from '@sd-jwt/core';
2
- import type { DisclosureFrame, Verifier } from '@sd-jwt/types';
2
+ import type { DisclosureFrame, Hasher, Verifier } from '@sd-jwt/types';
3
3
  import { SDJWTException } from '@sd-jwt/utils';
4
4
  import type { SdJwtVcPayload } from './sd-jwt-vc-payload';
5
- import type { SDJWTVCConfig } from './sd-jwt-vc-config';
5
+ import type {
6
+ SDJWTVCConfig,
7
+ StatusListFetcher,
8
+ StatusValidator,
9
+ } from './sd-jwt-vc-config';
6
10
  import {
7
11
  type StatusListJWTPayload,
8
12
  getListFromStatusListJWT,
13
+ type StatusListJWTHeaderParameters,
9
14
  } from '@sd-jwt/jwt-status-list';
10
- import type { StatusListJWTHeaderParameters } from '@sd-jwt/jwt-status-list';
15
+ import type { TypeMetadataFormat } from './sd-jwt-vc-type-metadata-format';
16
+ import Ajv, { type SchemaObject } from 'ajv';
17
+ import type { VerificationResult } from './verification-result';
18
+ import addFormats from 'ajv-formats';
19
+ import type { VcTFetcher } from './sd-jwt-vc-vct';
20
+
11
21
  export class SDJwtVcInstance extends SDJwtInstance<SdJwtVcPayload> {
12
22
  /**
13
23
  * The type of the SD-JWT-VC set in the header.typ field.
14
24
  */
15
- protected type = 'vc+sd-jwt';
25
+ protected type = 'dc+sd-jwt';
16
26
 
17
27
  protected userConfig: SDJWTVCConfig = {};
18
28
 
@@ -71,7 +81,9 @@ export class SDJwtVcInstance extends SDJwtInstance<SdJwtVcPayload> {
71
81
 
72
82
  // according to the spec the content type should be application/statuslist+jwt
73
83
  if (
74
- response.headers.get('content-type') !== 'application/statuslist+jwt'
84
+ !response.headers
85
+ .get('content-type')
86
+ ?.includes('application/statuslist+jwt')
75
87
  ) {
76
88
  throw new Error('Invalid content type');
77
89
  }
@@ -93,7 +105,7 @@ export class SDJwtVcInstance extends SDJwtInstance<SdJwtVcPayload> {
93
105
  }
94
106
 
95
107
  /**
96
- * Verifies the SD-JWT-VC.
108
+ * Verifies the SD-JWT-VC. It will validate the signature, the keybindings when required, the status, and the VCT.
97
109
  */
98
110
  async verify(
99
111
  encodedSDJwt: string,
@@ -101,7 +113,7 @@ export class SDJwtVcInstance extends SDJwtInstance<SdJwtVcPayload> {
101
113
  requireKeyBindings?: boolean,
102
114
  ) {
103
115
  // Call the parent class's verify method
104
- const result = await super
116
+ const result: VerificationResult = await super
105
117
  .verify(encodedSDJwt, requiredClaimKeys, requireKeyBindings)
106
118
  .then((res) => {
107
119
  return {
@@ -111,12 +123,167 @@ export class SDJwtVcInstance extends SDJwtInstance<SdJwtVcPayload> {
111
123
  };
112
124
  });
113
125
 
126
+ await this.verifyStatus(result);
127
+ if (this.userConfig.loadTypeMetadataFormat) {
128
+ await this.verifyVct(result);
129
+ }
130
+ return result;
131
+ }
132
+
133
+ /**
134
+ * Default function to fetch the VCT from the uri. We assume that the vct is a URL that is used to fetch the VCT.
135
+ * @param uri
136
+ * @returns
137
+ */
138
+ private async vctFetcher(
139
+ uri: string,
140
+ integrity?: string,
141
+ ): Promise<TypeMetadataFormat> {
142
+ // modify the uri based on https://www.ietf.org/archive/id/draft-ietf-oauth-sd-jwt-vc-04.html#section-6.3.1
143
+ const elements = uri.split('/');
144
+ //insert a new element on the thrid position, but not replace it
145
+ elements.splice(3, 0, '.well-known/vct');
146
+ const url = elements.join('/');
147
+ return this.fetch<TypeMetadataFormat>(url, integrity);
148
+ }
149
+
150
+ /**
151
+ * Validates the integrity of the response if the integrity is passed. If the integrity does not match, an error is thrown.
152
+ * @param integrity
153
+ * @param response
154
+ */
155
+ private async validateIntegrity(
156
+ response: Response,
157
+ url: string,
158
+ integrity?: string,
159
+ ) {
160
+ if (integrity) {
161
+ // validate the integrity of the response according to https://www.w3.org/TR/SRI/
162
+ const arrayBuffer = await response.arrayBuffer();
163
+ const alg = integrity.split('-')[0];
164
+ //TODO: error handling when a hasher is passed that is not supporting the required algorithm acording to the spec
165
+ const hashBuffer = await (this.userConfig.hasher as Hasher)(
166
+ arrayBuffer,
167
+ alg,
168
+ );
169
+ const integrityHash = integrity.split('-')[1];
170
+ const hash = Array.from(new Uint8Array(hashBuffer))
171
+ .map((byte) => byte.toString(16).padStart(2, '0'))
172
+ .join('');
173
+ if (hash !== integrityHash) {
174
+ throw new Error(
175
+ `Integrity check for ${url} failed: is ${hash}, but expected ${integrityHash}`,
176
+ );
177
+ }
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Fetches the content from the url with a timeout of 10 seconds.
183
+ * @param url
184
+ * @returns
185
+ */
186
+ private async fetch<T>(url: string, integrity?: string) {
187
+ const controller = new AbortController();
188
+ const timeoutId = setTimeout(() => controller.abort(), 10000);
189
+ try {
190
+ const response = await fetch(url, {
191
+ signal: controller.signal,
192
+ });
193
+ if (!response.ok) {
194
+ throw new Error(await response.text());
195
+ }
196
+ await this.validateIntegrity(response.clone(), url, integrity);
197
+ return response.json() as Promise<T>;
198
+ } finally {
199
+ clearTimeout(timeoutId);
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Loads the schema either from the object or as fallback from the uri.
205
+ * @param typeMetadataFormat
206
+ * @returns
207
+ */
208
+ private async loadSchema(typeMetadataFormat: TypeMetadataFormat) {
209
+ //if schema is present, return it
210
+ if (typeMetadataFormat.schema) return typeMetadataFormat.schema;
211
+ if (typeMetadataFormat.schema_uri) {
212
+ const schema = await this.fetch<SchemaObject>(
213
+ typeMetadataFormat.schema_uri,
214
+ typeMetadataFormat['schema_uri#Integrity'],
215
+ );
216
+ return schema;
217
+ }
218
+ throw new Error('No schema or schema_uri found');
219
+ }
220
+
221
+ /**
222
+ * Verifies the VCT of the SD-JWT-VC. Returns the type metadata format. If the schema does not match, an error is thrown. If it matches, it will return the type metadata format.
223
+ * @param result
224
+ * @returns
225
+ */
226
+ private async verifyVct(
227
+ result: VerificationResult,
228
+ ): Promise<TypeMetadataFormat | undefined> {
229
+ const fetcher: VcTFetcher =
230
+ this.userConfig.vctFetcher ?? this.vctFetcher.bind(this);
231
+ const typeMetadataFormat = await fetcher(
232
+ result.payload.vct,
233
+ result.payload['vct#Integrity'],
234
+ );
235
+
236
+ if (typeMetadataFormat.extends) {
237
+ // implement based on https://www.ietf.org/archive/id/draft-ietf-oauth-sd-jwt-vc-04.html#name-extending-type-metadata
238
+ //TODO: needs to be implemented. Unclear at this point which values will overwrite the values from the extended type metadata format
239
+ }
240
+
241
+ //init the json schema validator, load referenced schemas on demand
242
+ const schema = await this.loadSchema(typeMetadataFormat);
243
+ const loadedSchemas = new Set<string>();
244
+ // init the json schema validator
245
+ const ajv = new Ajv({
246
+ loadSchema: async (uri: string) => {
247
+ if (loadedSchemas.has(uri)) {
248
+ return {};
249
+ }
250
+ const response = await fetch(uri);
251
+ if (!response.ok) {
252
+ throw new Error(
253
+ `Error fetching schema: ${
254
+ response.status
255
+ } ${await response.text()}`,
256
+ );
257
+ }
258
+ loadedSchemas.add(uri);
259
+ return response.json();
260
+ },
261
+ });
262
+ addFormats(ajv);
263
+ const validate = await ajv.compileAsync(schema);
264
+ const valid = validate(result.payload);
265
+
266
+ if (!valid) {
267
+ throw new SDJWTException(
268
+ `Payload does not match the schema: ${JSON.stringify(validate.errors)}`,
269
+ );
270
+ }
271
+
272
+ return typeMetadataFormat;
273
+ }
274
+
275
+ /**
276
+ * Verifies the status of the SD-JWT-VC.
277
+ * @param result
278
+ */
279
+ private async verifyStatus(result: VerificationResult): Promise<void> {
114
280
  if (result.payload.status) {
115
281
  //checks if a status field is present in the payload based on https://www.ietf.org/archive/id/draft-ietf-oauth-status-list-02.html
116
282
  if (result.payload.status.status_list) {
117
283
  // fetch the status list from the uri
118
- const fetcher =
119
- this.userConfig.statusListFetcher ?? this.statusListFetcher;
284
+ const fetcher: StatusListFetcher =
285
+ this.userConfig.statusListFetcher ??
286
+ this.statusListFetcher.bind(this);
120
287
  // fetch the status list from the uri
121
288
  const statusListJWT = await fetcher(
122
289
  result.payload.status.status_list.uri,
@@ -144,12 +311,10 @@ export class SDJwtVcInstance extends SDJwtInstance<SdJwtVcPayload> {
144
311
  );
145
312
 
146
313
  // validate the status
147
- const statusValidator =
148
- this.userConfig.statusValidator ?? this.statusValidator;
314
+ const statusValidator: StatusValidator =
315
+ this.userConfig.statusValidator ?? this.statusValidator.bind(this);
149
316
  await statusValidator(status);
150
317
  }
151
318
  }
152
-
153
- return result;
154
319
  }
155
320
  }
@@ -12,6 +12,8 @@ export interface SdJwtVcPayload extends SdJwtPayload {
12
12
  cnf?: unknown;
13
13
  // REQUIRED. The type of the Verifiable Credential, e.g., https://credentials.example.com/identity_credential, as defined in Section 3.2.2.1.1.
14
14
  vct: string;
15
+ // OPTIONAL. If passed, the loaded type metadata format has to be validated according to https://www.w3.org/TR/SRI/
16
+ 'vct#Integrity'?: string;
15
17
  // OPTIONAL. The information on how to read the status of the Verifiable Credential. See [https://www.ietf.org/archive/id/draft-ietf-oauth-status-list-02.html] for more information.
16
18
  status?: SDJWTVCStatusReference;
17
19
  // OPTIONAL. The identifier of the Subject of the Verifiable Credential. The Issuer MAY use it to provide the Subject identifier known by the Issuer. There is no requirement for a binding to exist between sub and cnf claims.
@@ -0,0 +1,13 @@
1
+ /**
2
+ * https://www.ietf.org/archive/id/draft-ietf-oauth-sd-jwt-vc-04.html#name-type-metadata-format
3
+ */
4
+ export type TypeMetadataFormat = {
5
+ vct: string; // REQUIRED. A URI that uniquely identifies the type. This URI MUST be dereferenceable to a JSON document that describes the type.
6
+ name?: string; // OPTIONAL. A human-readable name for the type, intended for developers reading the JSON document.
7
+ description?: string; // OPTIONAL. A human-readable description for the type, intended for developers reading the JSON document.
8
+ extends?: string; // OPTIONAL. A URI of another type that this type extends, as described in Section 6.4.
9
+ 'extends#Integrity'?: string; // OPTIONAL. Validating the ingegrity of the extends field
10
+ schema?: object; // OPTIONAL. An embedded JSON Schema document describing the structure of the Verifiable Credential as described in Section 6.5.1. schema MUST NOT be used if schema_uri is present.
11
+ schema_uri?: string; // OPTIONAL. A URL pointing to a JSON Schema document describing the structure of the Verifiable Credential as described in Section 6.5.1. schema_uri MUST NOT be used if schema is present.
12
+ 'schema_uri#Integrity'?: string; // OPTIONAL. Validating the integrity of the schema_uri field.
13
+ };
@@ -0,0 +1,6 @@
1
+ import type { TypeMetadataFormat } from './sd-jwt-vc-type-metadata-format';
2
+
3
+ export type VcTFetcher = (
4
+ uri: string,
5
+ integrity?: string,
6
+ ) => Promise<TypeMetadataFormat>;
@@ -17,10 +17,13 @@ import {
17
17
  import { SignJWT } from 'jose';
18
18
 
19
19
  const iss = 'ExampleIssuer';
20
- const vct = 'https://example.com/schema/1';
20
+ const vct = 'ExampleCredentialType';
21
21
  const iat = new Date().getTime() / 1000;
22
22
 
23
23
  const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519');
24
+
25
+ //TODO: to simulate a hosted status list, use the same appraoch as in vct.spec.ts
26
+
24
27
  const createSignerVerifier = () => {
25
28
  const signer: Signer = async (data: string) => {
26
29
  const sig = Crypto.sign(null, Buffer.from(data), privateKey);
@@ -0,0 +1,128 @@
1
+ import { digest, generateSalt } from '@sd-jwt/crypto-nodejs';
2
+ import type { DisclosureFrame, Signer, Verifier } from '@sd-jwt/types';
3
+ import { describe, test, beforeAll, afterAll } from 'vitest';
4
+ import { SDJwtVcInstance } from '..';
5
+ import type { SdJwtVcPayload } from '../sd-jwt-vc-payload';
6
+ import Crypto from 'node:crypto';
7
+ import { setupServer } from 'msw/node';
8
+ import { HttpResponse, http } from 'msw';
9
+ import { afterEach } from 'node:test';
10
+ import type { TypeMetadataFormat } from '../sd-jwt-vc-type-metadata-format';
11
+
12
+ const restHandlers = [
13
+ http.get('http://example.com/schema/example', () => {
14
+ const res = {
15
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
16
+ type: 'object',
17
+ properties: {
18
+ vct: {
19
+ type: 'string',
20
+ },
21
+ iss: {
22
+ type: 'string',
23
+ },
24
+ nbf: {
25
+ type: 'number',
26
+ },
27
+ exp: {
28
+ type: 'number',
29
+ },
30
+ cnf: {
31
+ type: 'object',
32
+ },
33
+ status: {
34
+ type: 'object',
35
+ },
36
+ firstName: {
37
+ type: 'string',
38
+ },
39
+ },
40
+ required: ['iss', 'vct'],
41
+ };
42
+ return HttpResponse.json(res);
43
+ }),
44
+ http.get('http://exmaple.com/.well-known/vct/example', () => {
45
+ const res: TypeMetadataFormat = {
46
+ vct: 'http://example.com/example',
47
+ name: 'ExampleCredentialType',
48
+ description: 'An example credential type',
49
+ schema_uri: 'http://example.com/schema/example',
50
+ //this value could be generated on demand to make it easier when changing the values
51
+ 'schema_uri#Integrity':
52
+ 'sha256-48a61b283ded3b55e8d9a9b063327641dc4c53f76bd5daa96c23f232822167ae',
53
+ };
54
+ return HttpResponse.json(res);
55
+ }),
56
+ ];
57
+
58
+ //this value could be generated on demand to make it easier when changing the values
59
+ const vctIntegrity =
60
+ 'sha256-96bed58130a44af05ae8970aa9caa0bf0135cd15afe721ea29f553394692acef';
61
+
62
+ const server = setupServer(...restHandlers);
63
+
64
+ const iss = 'ExampleIssuer';
65
+ const vct = 'http://exmaple.com/example';
66
+ const iat = new Date().getTime() / 1000;
67
+
68
+ const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519');
69
+
70
+ const createSignerVerifier = () => {
71
+ const signer: Signer = async (data: string) => {
72
+ const sig = Crypto.sign(null, Buffer.from(data), privateKey);
73
+ return Buffer.from(sig).toString('base64url');
74
+ };
75
+ const verifier: Verifier = async (data: string, sig: string) => {
76
+ return Crypto.verify(
77
+ null,
78
+ Buffer.from(data),
79
+ publicKey,
80
+ Buffer.from(sig, 'base64url'),
81
+ );
82
+ };
83
+ return { signer, verifier };
84
+ };
85
+
86
+ describe('App', () => {
87
+ beforeAll(() => server.listen({ onUnhandledRequest: 'warn' }));
88
+
89
+ afterAll(() => server.close());
90
+
91
+ afterEach(() => server.resetHandlers());
92
+
93
+ test('VCT Validation', async () => {
94
+ const { signer, verifier } = createSignerVerifier();
95
+ const sdjwt = new SDJwtVcInstance({
96
+ signer,
97
+ signAlg: 'EdDSA',
98
+ verifier,
99
+ hasher: digest,
100
+ hashAlg: 'SHA-256',
101
+ saltGenerator: generateSalt,
102
+ loadTypeMetadataFormat: true,
103
+ });
104
+
105
+ const claims = {
106
+ firstname: 'John',
107
+ };
108
+ const disclosureFrame = {
109
+ _sd: ['firstname'],
110
+ };
111
+
112
+ const expectedPayload: SdJwtVcPayload = {
113
+ iat,
114
+ iss,
115
+ vct,
116
+ 'vct#Integrity': vctIntegrity,
117
+ ...claims,
118
+ };
119
+ const encodedSdjwt = await sdjwt.issue(
120
+ expectedPayload,
121
+ disclosureFrame as unknown as DisclosureFrame<SdJwtVcPayload>,
122
+ );
123
+
124
+ await sdjwt.verify(encodedSdjwt);
125
+ });
126
+
127
+ //TODO: we need tests with an embedded schema, extended and maybe also to test the errors when schema information is not available or the integrity is not valid
128
+ });
@@ -0,0 +1,15 @@
1
+ import type { kbPayload, kbHeader } from '@sd-jwt/types';
2
+ import type { SdJwtVcPayload } from './sd-jwt-vc-payload';
3
+ import type { TypeMetadataFormat } from './sd-jwt-vc-type-metadata-format';
4
+
5
+ export type VerificationResult = {
6
+ payload: SdJwtVcPayload;
7
+ header: Record<string, unknown> | undefined;
8
+ kb:
9
+ | {
10
+ payload: kbPayload;
11
+ header: kbHeader;
12
+ }
13
+ | undefined;
14
+ typeMetadataFormat?: TypeMetadataFormat;
15
+ };
@@ -1,5 +1,5 @@
1
1
  import Crypto from 'node:crypto';
2
- import { SDJwtVcInstance, SdJwtVcPayload } from '../src/index';
2
+ import { SDJwtVcInstance } from '../src/index';
3
3
  import type {
4
4
  DisclosureFrame,
5
5
  PresentationFrame,
@@ -29,7 +29,7 @@ const createSignerVerifier = () => {
29
29
  };
30
30
 
31
31
  const iss = 'ExampleIssuer';
32
- const vct = 'https://example.com/schema/1';
32
+ const vct = 'ExampleCredentials';
33
33
  const iat = new Date().getTime() / 1000;
34
34
 
35
35
  describe('App', () => {
@@ -235,7 +235,7 @@ async function JSONtest(filename: string) {
235
235
 
236
236
  expect(validated).toBeDefined();
237
237
  expect(validated).toStrictEqual({
238
- header: { alg: 'EdDSA', typ: 'vc+sd-jwt' },
238
+ header: { alg: 'EdDSA', typ: 'dc+sd-jwt' },
239
239
  payload,
240
240
  });
241
241
 
@@ -259,7 +259,7 @@ async function JSONtest(filename: string) {
259
259
 
260
260
  expect(verified).toBeDefined();
261
261
  expect(verified).toStrictEqual({
262
- header: { alg: 'EdDSA', typ: 'vc+sd-jwt' },
262
+ header: { alg: 'EdDSA', typ: 'dc+sd-jwt' },
263
263
  kb: undefined,
264
264
  payload,
265
265
  });