@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 +16 -0
- package/README.md +20 -0
- package/dist/index.d.mts +72 -14
- package/dist/index.d.ts +72 -14
- package/dist/index.js +153 -7
- package/dist/index.mjs +144 -7
- package/package.json +12 -9
- package/src/sd-jwt-vc-config.ts +10 -2
- package/src/sd-jwt-vc-instance.ts +178 -13
- package/src/sd-jwt-vc-payload.ts +2 -0
- package/src/sd-jwt-vc-type-metadata-format.ts +13 -0
- package/src/sd-jwt-vc-vct.ts +6 -0
- package/src/test/index.spec.ts +4 -1
- package/src/test/vct.spec.ts +128 -0
- package/src/verification-result.ts +15 -0
- package/test/app-e2e.spec.ts +4 -4
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
|
|
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?:
|
|
10
|
-
statusValidator?:
|
|
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
|
-
|
|
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
|
|
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?:
|
|
10
|
-
statusValidator?:
|
|
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
|
-
|
|
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 = "
|
|
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")
|
|
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 = "
|
|
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")
|
|
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.
|
|
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
|
|
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.
|
|
43
|
-
"@sd-jwt/jwt-status-list": "0.
|
|
44
|
-
"@sd-jwt/utils": "0.
|
|
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.
|
|
48
|
-
"@sd-jwt/types": "0.
|
|
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": "
|
|
70
|
+
"gitHead": "0d9742cd87d643079c7828ac3689d39ac4f6f21d"
|
|
68
71
|
}
|
package/src/sd-jwt-vc-config.ts
CHANGED
|
@@ -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?:
|
|
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?:
|
|
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 {
|
|
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 {
|
|
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 = '
|
|
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
|
|
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 ??
|
|
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
|
}
|
package/src/sd-jwt-vc-payload.ts
CHANGED
|
@@ -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
|
+
};
|
package/src/test/index.spec.ts
CHANGED
|
@@ -17,10 +17,13 @@ import {
|
|
|
17
17
|
import { SignJWT } from 'jose';
|
|
18
18
|
|
|
19
19
|
const iss = 'ExampleIssuer';
|
|
20
|
-
const vct = '
|
|
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
|
+
};
|
package/test/app-e2e.spec.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Crypto from 'node:crypto';
|
|
2
|
-
import { SDJwtVcInstance
|
|
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 = '
|
|
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: '
|
|
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: '
|
|
262
|
+
header: { alg: 'EdDSA', typ: 'dc+sd-jwt' },
|
|
263
263
|
kb: undefined,
|
|
264
264
|
payload,
|
|
265
265
|
});
|