@sd-jwt/sd-jwt-vc 0.6.2-next.8 → 0.7.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 +8 -0
- package/README.md +5 -0
- package/dist/index.d.mts +39 -3
- package/dist/index.d.ts +39 -3
- package/dist/index.js +98 -6
- package/dist/index.mjs +101 -7
- package/package.json +8 -6
- package/src/index.ts +106 -5
- package/src/sd-jwt-vc-config.ts +11 -0
- package/src/sd-jwt-vc-payload.ts +3 -3
- package/src/sd-jwt-vc-status-reference.ts +9 -0
- package/src/test/index.spec.ts +110 -2
- package/test/app-e2e.spec.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
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.7.0](https://github.com/openwallet-foundation-labs/sd-jwt-js/compare/v0.6.1...v0.7.0) (2024-05-13)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @sd-jwt/sd-jwt-vc
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
6
14
|
## [0.6.1](https://github.com/openwallet-foundation-labs/sd-jwt-js/compare/v0.6.0...v0.6.1) (2024-03-18)
|
|
7
15
|
|
|
8
16
|
|
package/README.md
CHANGED
|
@@ -83,8 +83,13 @@ const verified = await sdjwt.verify(presentation);
|
|
|
83
83
|
|
|
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
|
+
### Revocation
|
|
87
|
+
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
|
+
|
|
86
90
|
### Dependencies
|
|
87
91
|
|
|
88
92
|
- @sd-jwt/core
|
|
89
93
|
- @sd-jwt/types
|
|
90
94
|
- @sd-jwt/utils
|
|
95
|
+
- @sd-jwt/jwt-status-list
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { SdJwtPayload, SDJwtInstance } from '@sd-jwt/core';
|
|
2
|
-
import { DisclosureFrame } from '@sd-jwt/types';
|
|
2
|
+
import { SDJWTConfig, DisclosureFrame } from '@sd-jwt/types';
|
|
3
|
+
|
|
4
|
+
interface SDJWTVCStatusReference {
|
|
5
|
+
status_list: {
|
|
6
|
+
idx: number;
|
|
7
|
+
uri: string;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
3
10
|
|
|
4
11
|
interface SdJwtVcPayload extends SdJwtPayload {
|
|
5
12
|
iss: string;
|
|
@@ -7,21 +14,50 @@ interface SdJwtVcPayload extends SdJwtPayload {
|
|
|
7
14
|
exp?: number;
|
|
8
15
|
cnf?: unknown;
|
|
9
16
|
vct: string;
|
|
10
|
-
status?:
|
|
17
|
+
status?: SDJWTVCStatusReference;
|
|
11
18
|
sub?: string;
|
|
12
19
|
iat?: number;
|
|
13
20
|
}
|
|
14
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Configuration for SD-JWT-VC
|
|
24
|
+
*/
|
|
25
|
+
type SDJWTVCConfig = SDJWTConfig & {
|
|
26
|
+
statusListFetcher?: (uri: string) => Promise<string>;
|
|
27
|
+
statusValidator?: (status: number) => Promise<void>;
|
|
28
|
+
};
|
|
29
|
+
|
|
15
30
|
declare class SDJwtVcInstance extends SDJwtInstance<SdJwtVcPayload> {
|
|
16
31
|
/**
|
|
17
32
|
* The type of the SD-JWT-VC set in the header.typ field.
|
|
18
33
|
*/
|
|
19
34
|
protected type: string;
|
|
35
|
+
protected userConfig: SDJWTVCConfig;
|
|
36
|
+
constructor(userConfig?: SDJWTVCConfig);
|
|
20
37
|
/**
|
|
21
38
|
* Validates if the disclosureFrame contains any reserved fields. If so it will throw an error.
|
|
22
39
|
* @param disclosureFrame
|
|
23
40
|
*/
|
|
24
41
|
protected validateReservedFields(disclosureFrame: DisclosureFrame<SdJwtVcPayload>): void;
|
|
42
|
+
/**
|
|
43
|
+
* Fetches the status list from the uri with a timeout of 10 seconds.
|
|
44
|
+
* @param uri The URI to fetch from.
|
|
45
|
+
* @returns A promise that resolves to a compact JWT.
|
|
46
|
+
*/
|
|
47
|
+
private statusListFetcher;
|
|
48
|
+
/**
|
|
49
|
+
* Validates the status, throws an error if the status is not 0.
|
|
50
|
+
* @param status
|
|
51
|
+
* @returns
|
|
52
|
+
*/
|
|
53
|
+
private statusValidator;
|
|
54
|
+
/**
|
|
55
|
+
* Verifies the SD-JWT-VC.
|
|
56
|
+
*/
|
|
57
|
+
verify(encodedSDJwt: string, requiredClaimKeys?: string[], requireKeyBindings?: boolean): Promise<{
|
|
58
|
+
payload: SdJwtVcPayload;
|
|
59
|
+
header: Record<string, unknown> | undefined;
|
|
60
|
+
}>;
|
|
25
61
|
}
|
|
26
62
|
|
|
27
|
-
export { SDJwtVcInstance
|
|
63
|
+
export { SDJwtVcInstance };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { SdJwtPayload, SDJwtInstance } from '@sd-jwt/core';
|
|
2
|
-
import { DisclosureFrame } from '@sd-jwt/types';
|
|
2
|
+
import { SDJWTConfig, DisclosureFrame } from '@sd-jwt/types';
|
|
3
|
+
|
|
4
|
+
interface SDJWTVCStatusReference {
|
|
5
|
+
status_list: {
|
|
6
|
+
idx: number;
|
|
7
|
+
uri: string;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
3
10
|
|
|
4
11
|
interface SdJwtVcPayload extends SdJwtPayload {
|
|
5
12
|
iss: string;
|
|
@@ -7,21 +14,50 @@ interface SdJwtVcPayload extends SdJwtPayload {
|
|
|
7
14
|
exp?: number;
|
|
8
15
|
cnf?: unknown;
|
|
9
16
|
vct: string;
|
|
10
|
-
status?:
|
|
17
|
+
status?: SDJWTVCStatusReference;
|
|
11
18
|
sub?: string;
|
|
12
19
|
iat?: number;
|
|
13
20
|
}
|
|
14
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Configuration for SD-JWT-VC
|
|
24
|
+
*/
|
|
25
|
+
type SDJWTVCConfig = SDJWTConfig & {
|
|
26
|
+
statusListFetcher?: (uri: string) => Promise<string>;
|
|
27
|
+
statusValidator?: (status: number) => Promise<void>;
|
|
28
|
+
};
|
|
29
|
+
|
|
15
30
|
declare class SDJwtVcInstance extends SDJwtInstance<SdJwtVcPayload> {
|
|
16
31
|
/**
|
|
17
32
|
* The type of the SD-JWT-VC set in the header.typ field.
|
|
18
33
|
*/
|
|
19
34
|
protected type: string;
|
|
35
|
+
protected userConfig: SDJWTVCConfig;
|
|
36
|
+
constructor(userConfig?: SDJWTVCConfig);
|
|
20
37
|
/**
|
|
21
38
|
* Validates if the disclosureFrame contains any reserved fields. If so it will throw an error.
|
|
22
39
|
* @param disclosureFrame
|
|
23
40
|
*/
|
|
24
41
|
protected validateReservedFields(disclosureFrame: DisclosureFrame<SdJwtVcPayload>): void;
|
|
42
|
+
/**
|
|
43
|
+
* Fetches the status list from the uri with a timeout of 10 seconds.
|
|
44
|
+
* @param uri The URI to fetch from.
|
|
45
|
+
* @returns A promise that resolves to a compact JWT.
|
|
46
|
+
*/
|
|
47
|
+
private statusListFetcher;
|
|
48
|
+
/**
|
|
49
|
+
* Validates the status, throws an error if the status is not 0.
|
|
50
|
+
* @param status
|
|
51
|
+
* @returns
|
|
52
|
+
*/
|
|
53
|
+
private statusValidator;
|
|
54
|
+
/**
|
|
55
|
+
* Verifies the SD-JWT-VC.
|
|
56
|
+
*/
|
|
57
|
+
verify(encodedSDJwt: string, requiredClaimKeys?: string[], requireKeyBindings?: boolean): Promise<{
|
|
58
|
+
payload: SdJwtVcPayload;
|
|
59
|
+
header: Record<string, unknown> | undefined;
|
|
60
|
+
}>;
|
|
25
61
|
}
|
|
26
62
|
|
|
27
|
-
export { SDJwtVcInstance
|
|
63
|
+
export { SDJwtVcInstance };
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __reflectGet = Reflect.get;
|
|
8
9
|
var __commonJS = (cb, mod) => function __require() {
|
|
9
10
|
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
10
11
|
};
|
|
@@ -29,6 +30,27 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
29
30
|
mod
|
|
30
31
|
));
|
|
31
32
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
33
|
+
var __superGet = (cls, obj, key) => __reflectGet(__getProtoOf(cls), key, obj);
|
|
34
|
+
var __async = (__this, __arguments, generator) => {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
var fulfilled = (value) => {
|
|
37
|
+
try {
|
|
38
|
+
step(generator.next(value));
|
|
39
|
+
} catch (e) {
|
|
40
|
+
reject(e);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
var rejected = (value) => {
|
|
44
|
+
try {
|
|
45
|
+
step(generator.throw(value));
|
|
46
|
+
} catch (e) {
|
|
47
|
+
reject(e);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
|
|
51
|
+
step((generator = generator.apply(__this, __arguments)).next());
|
|
52
|
+
});
|
|
53
|
+
};
|
|
32
54
|
|
|
33
55
|
// ../../node_modules/.pnpm/js-base64@3.7.6/node_modules/js-base64/base64.js
|
|
34
56
|
var require_base64 = __commonJS({
|
|
@@ -303,7 +325,7 @@ var require_dist = __commonJS({
|
|
|
303
325
|
return to;
|
|
304
326
|
};
|
|
305
327
|
var __toCommonJS2 = (mod) => __copyProps2(__defProp2({}, "__esModule", { value: true }), mod);
|
|
306
|
-
var
|
|
328
|
+
var __async2 = (__this, __arguments, generator) => {
|
|
307
329
|
return new Promise((resolve, reject) => {
|
|
308
330
|
var fulfilled = (value) => {
|
|
309
331
|
try {
|
|
@@ -368,7 +390,7 @@ var require_dist = __commonJS({
|
|
|
368
390
|
// After decode process, we use JSON.stringify to encode the data.
|
|
369
391
|
// This can be different from the original encoded data.
|
|
370
392
|
static fromEncode(s, hash) {
|
|
371
|
-
return
|
|
393
|
+
return __async2(this, null, function* () {
|
|
372
394
|
const { hasher, alg } = hash;
|
|
373
395
|
const digest = yield hasher(s, alg);
|
|
374
396
|
const digestStr = uint8ArrayToBase64Url(digest);
|
|
@@ -396,7 +418,7 @@ var require_dist = __commonJS({
|
|
|
396
418
|
return this.key ? [this.salt, this.key, this.value] : [this.salt, this.value];
|
|
397
419
|
}
|
|
398
420
|
digest(hash) {
|
|
399
|
-
return
|
|
421
|
+
return __async2(this, null, function* () {
|
|
400
422
|
const { hasher, alg } = hash;
|
|
401
423
|
if (!this._digest) {
|
|
402
424
|
const hash2 = yield hasher(this.encode(), alg);
|
|
@@ -425,13 +447,18 @@ __export(src_exports, {
|
|
|
425
447
|
module.exports = __toCommonJS(src_exports);
|
|
426
448
|
var import_core = require("@sd-jwt/core");
|
|
427
449
|
var import_dist = __toESM(require_dist());
|
|
428
|
-
var
|
|
429
|
-
|
|
430
|
-
|
|
450
|
+
var import_jwt_status_list = require("@sd-jwt/jwt-status-list");
|
|
451
|
+
var SDJwtVcInstance = class _SDJwtVcInstance extends import_core.SDJwtInstance {
|
|
452
|
+
constructor(userConfig) {
|
|
453
|
+
super(userConfig);
|
|
431
454
|
/**
|
|
432
455
|
* The type of the SD-JWT-VC set in the header.typ field.
|
|
433
456
|
*/
|
|
434
457
|
this.type = "vc+sd-jwt";
|
|
458
|
+
this.userConfig = {};
|
|
459
|
+
if (userConfig) {
|
|
460
|
+
this.userConfig = userConfig;
|
|
461
|
+
}
|
|
435
462
|
}
|
|
436
463
|
/**
|
|
437
464
|
* Validates if the disclosureFrame contains any reserved fields. If so it will throw an error.
|
|
@@ -446,6 +473,71 @@ var SDJwtVcInstance = class extends import_core.SDJwtInstance {
|
|
|
446
473
|
}
|
|
447
474
|
}
|
|
448
475
|
}
|
|
476
|
+
/**
|
|
477
|
+
* Fetches the status list from the uri with a timeout of 10 seconds.
|
|
478
|
+
* @param uri The URI to fetch from.
|
|
479
|
+
* @returns A promise that resolves to a compact JWT.
|
|
480
|
+
*/
|
|
481
|
+
statusListFetcher(uri) {
|
|
482
|
+
return __async(this, null, function* () {
|
|
483
|
+
const controller = new AbortController();
|
|
484
|
+
const timeoutId = setTimeout(() => controller.abort(), 1e4);
|
|
485
|
+
try {
|
|
486
|
+
const response = yield fetch(uri, { signal: controller.signal });
|
|
487
|
+
if (!response.ok) {
|
|
488
|
+
throw new Error(
|
|
489
|
+
`Error fetching status list: ${response.status} ${yield response.text()}`
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
return response.text();
|
|
493
|
+
} finally {
|
|
494
|
+
clearTimeout(timeoutId);
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Validates the status, throws an error if the status is not 0.
|
|
500
|
+
* @param status
|
|
501
|
+
* @returns
|
|
502
|
+
*/
|
|
503
|
+
statusValidator(status) {
|
|
504
|
+
return __async(this, null, function* () {
|
|
505
|
+
if (status !== 0)
|
|
506
|
+
throw new import_dist.SDJWTException("Status is not valid");
|
|
507
|
+
return Promise.resolve();
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Verifies the SD-JWT-VC.
|
|
512
|
+
*/
|
|
513
|
+
verify(encodedSDJwt, requiredClaimKeys, requireKeyBindings) {
|
|
514
|
+
return __async(this, null, function* () {
|
|
515
|
+
var _a, _b, _c;
|
|
516
|
+
const result = yield __superGet(_SDJwtVcInstance.prototype, this, "verify").call(this, encodedSDJwt, requiredClaimKeys, requireKeyBindings).then((res) => {
|
|
517
|
+
return { payload: res.payload, header: res.header };
|
|
518
|
+
});
|
|
519
|
+
if (result.payload.status) {
|
|
520
|
+
if (result.payload.status.status_list) {
|
|
521
|
+
const fetcher = (_a = this.userConfig.statusListFetcher) != null ? _a : this.statusListFetcher;
|
|
522
|
+
const statusListJWT = yield fetcher(
|
|
523
|
+
result.payload.status.status_list.uri
|
|
524
|
+
);
|
|
525
|
+
const slJWT = import_core.Jwt.fromEncode(statusListJWT);
|
|
526
|
+
yield slJWT.verify(this.userConfig.verifier);
|
|
527
|
+
if (((_b = slJWT.payload) == null ? void 0 : _b.exp) && slJWT.payload.exp < Date.now() / 1e3) {
|
|
528
|
+
throw new import_dist.SDJWTException("Status list is expired");
|
|
529
|
+
}
|
|
530
|
+
const statusList = (0, import_jwt_status_list.getListFromStatusListJWT)(statusListJWT);
|
|
531
|
+
const status = statusList.getStatus(
|
|
532
|
+
result.payload.status.status_list.idx
|
|
533
|
+
);
|
|
534
|
+
const statusValidator = (_c = this.userConfig.statusValidator) != null ? _c : this.statusValidator;
|
|
535
|
+
yield statusValidator(status);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return result;
|
|
539
|
+
});
|
|
540
|
+
}
|
|
449
541
|
};
|
|
450
542
|
// Annotate the CommonJS export names for ESM import in node:
|
|
451
543
|
0 && (module.exports = {
|
package/dist/index.mjs
CHANGED
|
@@ -4,6 +4,7 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
4
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
5
|
var __getProtoOf = Object.getPrototypeOf;
|
|
6
6
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __reflectGet = Reflect.get;
|
|
7
8
|
var __commonJS = (cb, mod) => function __require() {
|
|
8
9
|
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
9
10
|
};
|
|
@@ -23,6 +24,27 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
24
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
24
25
|
mod
|
|
25
26
|
));
|
|
27
|
+
var __superGet = (cls, obj, key) => __reflectGet(__getProtoOf(cls), key, obj);
|
|
28
|
+
var __async = (__this, __arguments, generator) => {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
var fulfilled = (value) => {
|
|
31
|
+
try {
|
|
32
|
+
step(generator.next(value));
|
|
33
|
+
} catch (e) {
|
|
34
|
+
reject(e);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
var rejected = (value) => {
|
|
38
|
+
try {
|
|
39
|
+
step(generator.throw(value));
|
|
40
|
+
} catch (e) {
|
|
41
|
+
reject(e);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
|
|
45
|
+
step((generator = generator.apply(__this, __arguments)).next());
|
|
46
|
+
});
|
|
47
|
+
};
|
|
26
48
|
|
|
27
49
|
// ../../node_modules/.pnpm/js-base64@3.7.6/node_modules/js-base64/base64.js
|
|
28
50
|
var require_base64 = __commonJS({
|
|
@@ -297,7 +319,7 @@ var require_dist = __commonJS({
|
|
|
297
319
|
return to;
|
|
298
320
|
};
|
|
299
321
|
var __toCommonJS = (mod) => __copyProps2(__defProp2({}, "__esModule", { value: true }), mod);
|
|
300
|
-
var
|
|
322
|
+
var __async2 = (__this, __arguments, generator) => {
|
|
301
323
|
return new Promise((resolve, reject) => {
|
|
302
324
|
var fulfilled = (value) => {
|
|
303
325
|
try {
|
|
@@ -362,7 +384,7 @@ var require_dist = __commonJS({
|
|
|
362
384
|
// After decode process, we use JSON.stringify to encode the data.
|
|
363
385
|
// This can be different from the original encoded data.
|
|
364
386
|
static fromEncode(s, hash) {
|
|
365
|
-
return
|
|
387
|
+
return __async2(this, null, function* () {
|
|
366
388
|
const { hasher, alg } = hash;
|
|
367
389
|
const digest = yield hasher(s, alg);
|
|
368
390
|
const digestStr = uint8ArrayToBase64Url(digest);
|
|
@@ -390,7 +412,7 @@ var require_dist = __commonJS({
|
|
|
390
412
|
return this.key ? [this.salt, this.key, this.value] : [this.salt, this.value];
|
|
391
413
|
}
|
|
392
414
|
digest(hash) {
|
|
393
|
-
return
|
|
415
|
+
return __async2(this, null, function* () {
|
|
394
416
|
const { hasher, alg } = hash;
|
|
395
417
|
if (!this._digest) {
|
|
396
418
|
const hash2 = yield hasher(this.encode(), alg);
|
|
@@ -413,14 +435,21 @@ var require_dist = __commonJS({
|
|
|
413
435
|
|
|
414
436
|
// src/index.ts
|
|
415
437
|
var import_dist = __toESM(require_dist());
|
|
416
|
-
import { SDJwtInstance } from "@sd-jwt/core";
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
438
|
+
import { Jwt, SDJwtInstance } from "@sd-jwt/core";
|
|
439
|
+
import {
|
|
440
|
+
getListFromStatusListJWT
|
|
441
|
+
} from "@sd-jwt/jwt-status-list";
|
|
442
|
+
var SDJwtVcInstance = class _SDJwtVcInstance extends SDJwtInstance {
|
|
443
|
+
constructor(userConfig) {
|
|
444
|
+
super(userConfig);
|
|
420
445
|
/**
|
|
421
446
|
* The type of the SD-JWT-VC set in the header.typ field.
|
|
422
447
|
*/
|
|
423
448
|
this.type = "vc+sd-jwt";
|
|
449
|
+
this.userConfig = {};
|
|
450
|
+
if (userConfig) {
|
|
451
|
+
this.userConfig = userConfig;
|
|
452
|
+
}
|
|
424
453
|
}
|
|
425
454
|
/**
|
|
426
455
|
* Validates if the disclosureFrame contains any reserved fields. If so it will throw an error.
|
|
@@ -435,6 +464,71 @@ var SDJwtVcInstance = class extends SDJwtInstance {
|
|
|
435
464
|
}
|
|
436
465
|
}
|
|
437
466
|
}
|
|
467
|
+
/**
|
|
468
|
+
* Fetches the status list from the uri with a timeout of 10 seconds.
|
|
469
|
+
* @param uri The URI to fetch from.
|
|
470
|
+
* @returns A promise that resolves to a compact JWT.
|
|
471
|
+
*/
|
|
472
|
+
statusListFetcher(uri) {
|
|
473
|
+
return __async(this, null, function* () {
|
|
474
|
+
const controller = new AbortController();
|
|
475
|
+
const timeoutId = setTimeout(() => controller.abort(), 1e4);
|
|
476
|
+
try {
|
|
477
|
+
const response = yield fetch(uri, { signal: controller.signal });
|
|
478
|
+
if (!response.ok) {
|
|
479
|
+
throw new Error(
|
|
480
|
+
`Error fetching status list: ${response.status} ${yield response.text()}`
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
return response.text();
|
|
484
|
+
} finally {
|
|
485
|
+
clearTimeout(timeoutId);
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Validates the status, throws an error if the status is not 0.
|
|
491
|
+
* @param status
|
|
492
|
+
* @returns
|
|
493
|
+
*/
|
|
494
|
+
statusValidator(status) {
|
|
495
|
+
return __async(this, null, function* () {
|
|
496
|
+
if (status !== 0)
|
|
497
|
+
throw new import_dist.SDJWTException("Status is not valid");
|
|
498
|
+
return Promise.resolve();
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Verifies the SD-JWT-VC.
|
|
503
|
+
*/
|
|
504
|
+
verify(encodedSDJwt, requiredClaimKeys, requireKeyBindings) {
|
|
505
|
+
return __async(this, null, function* () {
|
|
506
|
+
var _a, _b, _c;
|
|
507
|
+
const result = yield __superGet(_SDJwtVcInstance.prototype, this, "verify").call(this, encodedSDJwt, requiredClaimKeys, requireKeyBindings).then((res) => {
|
|
508
|
+
return { payload: res.payload, header: res.header };
|
|
509
|
+
});
|
|
510
|
+
if (result.payload.status) {
|
|
511
|
+
if (result.payload.status.status_list) {
|
|
512
|
+
const fetcher = (_a = this.userConfig.statusListFetcher) != null ? _a : this.statusListFetcher;
|
|
513
|
+
const statusListJWT = yield fetcher(
|
|
514
|
+
result.payload.status.status_list.uri
|
|
515
|
+
);
|
|
516
|
+
const slJWT = Jwt.fromEncode(statusListJWT);
|
|
517
|
+
yield slJWT.verify(this.userConfig.verifier);
|
|
518
|
+
if (((_b = slJWT.payload) == null ? void 0 : _b.exp) && slJWT.payload.exp < Date.now() / 1e3) {
|
|
519
|
+
throw new import_dist.SDJWTException("Status list is expired");
|
|
520
|
+
}
|
|
521
|
+
const statusList = getListFromStatusListJWT(statusListJWT);
|
|
522
|
+
const status = statusList.getStatus(
|
|
523
|
+
result.payload.status.status_list.idx
|
|
524
|
+
);
|
|
525
|
+
const statusValidator = (_c = this.userConfig.statusValidator) != null ? _c : this.statusValidator;
|
|
526
|
+
yield statusValidator(status);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return result;
|
|
530
|
+
});
|
|
531
|
+
}
|
|
438
532
|
};
|
|
439
533
|
export {
|
|
440
534
|
SDJwtVcInstance
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sd-jwt/sd-jwt-vc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "sd-jwt draft 7 implementation in typescript",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"sd-jwt-vc"
|
|
27
27
|
],
|
|
28
28
|
"engines": {
|
|
29
|
-
"node": ">=
|
|
29
|
+
"node": ">=18"
|
|
30
30
|
},
|
|
31
31
|
"repository": {
|
|
32
32
|
"type": "git",
|
|
@@ -39,11 +39,13 @@
|
|
|
39
39
|
},
|
|
40
40
|
"license": "Apache-2.0",
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@sd-jwt/core": "0.
|
|
42
|
+
"@sd-jwt/core": "0.7.0",
|
|
43
|
+
"@sd-jwt/jwt-status-list": "0.7.0"
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
45
|
-
"@sd-jwt/crypto-nodejs": "0.
|
|
46
|
-
"@sd-jwt/types": "0.
|
|
46
|
+
"@sd-jwt/crypto-nodejs": "0.7.0",
|
|
47
|
+
"@sd-jwt/types": "0.7.0",
|
|
48
|
+
"jose": "^5.2.2"
|
|
47
49
|
},
|
|
48
50
|
"publishConfig": {
|
|
49
51
|
"access": "public"
|
|
@@ -61,5 +63,5 @@
|
|
|
61
63
|
"esm"
|
|
62
64
|
]
|
|
63
65
|
},
|
|
64
|
-
"gitHead": "
|
|
66
|
+
"gitHead": "bb867ba65960062413375591aee49ad78722ad93"
|
|
65
67
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,16 +1,28 @@
|
|
|
1
|
-
import { SDJwtInstance } from '@sd-jwt/core';
|
|
2
|
-
import type { DisclosureFrame } from '@sd-jwt/types';
|
|
1
|
+
import { Jwt, SDJwtInstance } from '@sd-jwt/core';
|
|
2
|
+
import type { DisclosureFrame, Verifier } from '@sd-jwt/types';
|
|
3
3
|
import { SDJWTException } from '../../utils/dist';
|
|
4
4
|
import type { SdJwtVcPayload } from './sd-jwt-vc-payload';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
import type { SDJWTVCConfig } from './sd-jwt-vc-config';
|
|
6
|
+
import {
|
|
7
|
+
type StatusListJWTHeaderParameters,
|
|
8
|
+
type StatusListJWTPayload,
|
|
9
|
+
getListFromStatusListJWT,
|
|
10
|
+
} from '@sd-jwt/jwt-status-list';
|
|
8
11
|
export class SDJwtVcInstance extends SDJwtInstance<SdJwtVcPayload> {
|
|
9
12
|
/**
|
|
10
13
|
* The type of the SD-JWT-VC set in the header.typ field.
|
|
11
14
|
*/
|
|
12
15
|
protected type = 'vc+sd-jwt';
|
|
13
16
|
|
|
17
|
+
protected userConfig: SDJWTVCConfig = {};
|
|
18
|
+
|
|
19
|
+
constructor(userConfig?: SDJWTVCConfig) {
|
|
20
|
+
super(userConfig);
|
|
21
|
+
if (userConfig) {
|
|
22
|
+
this.userConfig = userConfig;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
14
26
|
/**
|
|
15
27
|
* Validates if the disclosureFrame contains any reserved fields. If so it will throw an error.
|
|
16
28
|
* @param disclosureFrame
|
|
@@ -34,4 +46,93 @@ export class SDJwtVcInstance extends SDJwtInstance<SdJwtVcPayload> {
|
|
|
34
46
|
}
|
|
35
47
|
}
|
|
36
48
|
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Fetches the status list from the uri with a timeout of 10 seconds.
|
|
52
|
+
* @param uri The URI to fetch from.
|
|
53
|
+
* @returns A promise that resolves to a compact JWT.
|
|
54
|
+
*/
|
|
55
|
+
private async statusListFetcher(uri: string): Promise<string> {
|
|
56
|
+
const controller = new AbortController();
|
|
57
|
+
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const response = await fetch(uri, { signal: controller.signal });
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Error fetching status list: ${
|
|
64
|
+
response.status
|
|
65
|
+
} ${await response.text()}`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return response.text();
|
|
70
|
+
} finally {
|
|
71
|
+
clearTimeout(timeoutId);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Validates the status, throws an error if the status is not 0.
|
|
77
|
+
* @param status
|
|
78
|
+
* @returns
|
|
79
|
+
*/
|
|
80
|
+
private async statusValidator(status: number): Promise<void> {
|
|
81
|
+
if (status !== 0) throw new SDJWTException('Status is not valid');
|
|
82
|
+
return Promise.resolve();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Verifies the SD-JWT-VC.
|
|
87
|
+
*/
|
|
88
|
+
async verify(
|
|
89
|
+
encodedSDJwt: string,
|
|
90
|
+
requiredClaimKeys?: string[],
|
|
91
|
+
requireKeyBindings?: boolean,
|
|
92
|
+
) {
|
|
93
|
+
// Call the parent class's verify method
|
|
94
|
+
const result = await super
|
|
95
|
+
.verify(encodedSDJwt, requiredClaimKeys, requireKeyBindings)
|
|
96
|
+
.then((res) => {
|
|
97
|
+
return { payload: res.payload as SdJwtVcPayload, header: res.header };
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (result.payload.status) {
|
|
101
|
+
//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
|
|
102
|
+
if (result.payload.status.status_list) {
|
|
103
|
+
// fetch the status list from the uri
|
|
104
|
+
const fetcher =
|
|
105
|
+
this.userConfig.statusListFetcher ?? this.statusListFetcher;
|
|
106
|
+
// fetch the status list from the uri
|
|
107
|
+
const statusListJWT = await fetcher(
|
|
108
|
+
result.payload.status.status_list.uri,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const slJWT = Jwt.fromEncode<
|
|
112
|
+
StatusListJWTHeaderParameters,
|
|
113
|
+
StatusListJWTPayload
|
|
114
|
+
>(statusListJWT);
|
|
115
|
+
// check if the status list has a valid signature. The presence of the verifier is checked in the parent class.
|
|
116
|
+
await slJWT.verify(this.userConfig.verifier as Verifier);
|
|
117
|
+
|
|
118
|
+
//check if the status list is expired
|
|
119
|
+
if (slJWT.payload?.exp && slJWT.payload.exp < Date.now() / 1000) {
|
|
120
|
+
throw new SDJWTException('Status list is expired');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// get the status list from the status list JWT
|
|
124
|
+
const statusList = getListFromStatusListJWT(statusListJWT);
|
|
125
|
+
const status = statusList.getStatus(
|
|
126
|
+
result.payload.status.status_list.idx,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// validate the status
|
|
130
|
+
const statusValidator =
|
|
131
|
+
this.userConfig.statusValidator ?? this.statusValidator;
|
|
132
|
+
await statusValidator(status);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
37
138
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SDJWTConfig } from '@sd-jwt/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Configuration for SD-JWT-VC
|
|
5
|
+
*/
|
|
6
|
+
export type SDJWTVCConfig = SDJWTConfig & {
|
|
7
|
+
// 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>;
|
|
9
|
+
// 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>;
|
|
11
|
+
};
|
package/src/sd-jwt-vc-payload.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SdJwtPayload } from '@sd-jwt/core';
|
|
2
|
+
import type { SDJWTVCStatusReference } from './sd-jwt-vc-status-reference';
|
|
2
3
|
|
|
3
4
|
export interface SdJwtVcPayload extends SdJwtPayload {
|
|
4
5
|
// REQUIRED. The Issuer of the Verifiable Credential. The value of iss MUST be a URI. See [RFC7519] for more information.
|
|
@@ -11,9 +12,8 @@ export interface SdJwtVcPayload extends SdJwtPayload {
|
|
|
11
12
|
cnf?: unknown;
|
|
12
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.
|
|
13
14
|
vct: string;
|
|
14
|
-
// OPTIONAL. The information on how to read the status of the Verifiable Credential. See [
|
|
15
|
-
status?:
|
|
16
|
-
|
|
15
|
+
// 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
|
+
status?: SDJWTVCStatusReference;
|
|
17
17
|
// 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.
|
|
18
18
|
sub?: string;
|
|
19
19
|
// OPTIONAL. The time of issuance of the Verifiable Credential. See [RFC7519] for more information.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface SDJWTVCStatusReference {
|
|
2
|
+
// REQUIRED. implenentation according to https://www.ietf.org/archive/id/draft-ietf-oauth-status-list-02.html
|
|
3
|
+
status_list: {
|
|
4
|
+
// REQUIRED. index in the list of statuses
|
|
5
|
+
idx: number;
|
|
6
|
+
// REQUIRED. the reference to fetch the status list
|
|
7
|
+
uri: string;
|
|
8
|
+
};
|
|
9
|
+
}
|
package/src/test/index.spec.ts
CHANGED
|
@@ -1,14 +1,61 @@
|
|
|
1
1
|
import { digest, generateSalt } from '@sd-jwt/crypto-nodejs';
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
DisclosureFrame,
|
|
4
|
+
Signer,
|
|
5
|
+
Verifier,
|
|
6
|
+
JwtPayload,
|
|
7
|
+
} from '@sd-jwt/types';
|
|
3
8
|
import { describe, test, expect } from 'vitest';
|
|
4
9
|
import { SDJwtVcInstance } from '..';
|
|
5
|
-
import { createSignerVerifier } from '../../test/app-e2e.spec';
|
|
6
10
|
import type { SdJwtVcPayload } from '../sd-jwt-vc-payload';
|
|
11
|
+
import Crypto from 'node:crypto';
|
|
12
|
+
import {
|
|
13
|
+
StatusList,
|
|
14
|
+
type StatusListJWTHeaderParameters,
|
|
15
|
+
createHeaderAndPayload,
|
|
16
|
+
} from '@sd-jwt/jwt-status-list';
|
|
17
|
+
import { SignJWT } from 'jose';
|
|
7
18
|
|
|
8
19
|
const iss = 'ExampleIssuer';
|
|
9
20
|
const vct = 'https://example.com/schema/1';
|
|
10
21
|
const iat = new Date().getTime() / 1000;
|
|
11
22
|
|
|
23
|
+
const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519');
|
|
24
|
+
const createSignerVerifier = () => {
|
|
25
|
+
const signer: Signer = async (data: string) => {
|
|
26
|
+
const sig = Crypto.sign(null, Buffer.from(data), privateKey);
|
|
27
|
+
return Buffer.from(sig).toString('base64url');
|
|
28
|
+
};
|
|
29
|
+
const verifier: Verifier = async (data: string, sig: string) => {
|
|
30
|
+
return Crypto.verify(
|
|
31
|
+
null,
|
|
32
|
+
Buffer.from(data),
|
|
33
|
+
publicKey,
|
|
34
|
+
Buffer.from(sig, 'base64url'),
|
|
35
|
+
);
|
|
36
|
+
};
|
|
37
|
+
return { signer, verifier };
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const generateStatusList = async (): Promise<string> => {
|
|
41
|
+
const statusList = new StatusList([0, 1, 0, 0, 0, 0, 1, 1], 1);
|
|
42
|
+
const payload: JwtPayload = {
|
|
43
|
+
iss: 'https://example.com',
|
|
44
|
+
sub: 'https://example.com/status/1',
|
|
45
|
+
iat: new Date().getTime() / 1000,
|
|
46
|
+
};
|
|
47
|
+
const header: StatusListJWTHeaderParameters = {
|
|
48
|
+
alg: 'EdDSA',
|
|
49
|
+
typ: 'statuslist+jwt',
|
|
50
|
+
};
|
|
51
|
+
const values = createHeaderAndPayload(statusList, payload, header);
|
|
52
|
+
return new SignJWT(values.payload)
|
|
53
|
+
.setProtectedHeader(values.header)
|
|
54
|
+
.sign(privateKey);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const statusListJWT = await generateStatusList();
|
|
58
|
+
|
|
12
59
|
describe('App', () => {
|
|
13
60
|
test('Example', async () => {
|
|
14
61
|
const { signer, verifier } = createSignerVerifier();
|
|
@@ -36,3 +83,64 @@ describe('App', () => {
|
|
|
36
83
|
expect(encodedSdjwt).rejects.toThrowError();
|
|
37
84
|
});
|
|
38
85
|
});
|
|
86
|
+
|
|
87
|
+
describe('Revocation', () => {
|
|
88
|
+
const { signer, verifier } = createSignerVerifier();
|
|
89
|
+
const sdjwt = new SDJwtVcInstance({
|
|
90
|
+
signer,
|
|
91
|
+
signAlg: 'EdDSA',
|
|
92
|
+
verifier,
|
|
93
|
+
hasher: digest,
|
|
94
|
+
hashAlg: 'SHA-256',
|
|
95
|
+
saltGenerator: generateSalt,
|
|
96
|
+
statusListFetcher(uri: string) {
|
|
97
|
+
// we emulate fetching the status list from the uri. Validation of the JWT is not done here in the test but should be done in the implementation.
|
|
98
|
+
return Promise.resolve(statusListJWT);
|
|
99
|
+
},
|
|
100
|
+
// statusValidator(status: number) {
|
|
101
|
+
// // we are only accepting status 0
|
|
102
|
+
// if (status === 0) return Promise.resolve();
|
|
103
|
+
// throw new Error('Status is not valid');
|
|
104
|
+
// },
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('Test with a non revcoked credential', async () => {
|
|
108
|
+
const claims = {
|
|
109
|
+
firstname: 'John',
|
|
110
|
+
status: {
|
|
111
|
+
status_list: {
|
|
112
|
+
uri: 'https://example.com/status-list',
|
|
113
|
+
idx: 0,
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
const expectedPayload: SdJwtVcPayload = { iat, iss, vct, ...claims };
|
|
118
|
+
const encodedSdjwt = await sdjwt.issue(expectedPayload);
|
|
119
|
+
const result = await sdjwt.verify(encodedSdjwt);
|
|
120
|
+
expect(result).toBeDefined();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('Test with a revoked credential', async () => {
|
|
124
|
+
const claims = {
|
|
125
|
+
firstname: 'John',
|
|
126
|
+
status: {
|
|
127
|
+
status_list: {
|
|
128
|
+
uri: 'https://example.com/status-list',
|
|
129
|
+
idx: 1,
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
const expectedPayload: SdJwtVcPayload = { iat, iss, vct, ...claims };
|
|
134
|
+
const encodedSdjwt = await sdjwt.issue(expectedPayload);
|
|
135
|
+
const result = sdjwt.verify(encodedSdjwt);
|
|
136
|
+
expect(result).rejects.toThrowError('Status is not valid');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('test to fetch the statuslist', async () => {
|
|
140
|
+
//TODO: not implemented yet since we need to either mock the fetcher or use a real fetcher
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('test with an expired status list', async () => {
|
|
144
|
+
//TODO: needs to be implemented
|
|
145
|
+
});
|
|
146
|
+
});
|
package/test/app-e2e.spec.ts
CHANGED
|
@@ -11,7 +11,7 @@ import path from 'node:path';
|
|
|
11
11
|
import { describe, expect, test } from 'vitest';
|
|
12
12
|
import { digest, generateSalt } from '@sd-jwt/crypto-nodejs';
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
const createSignerVerifier = () => {
|
|
15
15
|
const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519');
|
|
16
16
|
const signer: Signer = async (data: string) => {
|
|
17
17
|
const sig = Crypto.sign(null, Buffer.from(data), privateKey);
|