@sd-jwt/sd-jwt-vc 0.7.2-next.6 → 0.7.2-next.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -0
- package/dist/index.d.mts +72 -14
- package/dist/index.d.ts +72 -14
- package/dist/index.js +150 -5
- package/dist/index.mjs +141 -5
- package/package.json +12 -9
- package/src/sd-jwt-vc-config.ts +10 -2
- package/src/sd-jwt-vc-instance.ts +174 -11
- 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 +2 -2
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,6 +60,8 @@ 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);
|
|
@@ -118,11 +129,10 @@ var SDJwtVcInstance = class _SDJwtVcInstance extends import_core.SDJwtInstance {
|
|
|
118
129
|
});
|
|
119
130
|
}
|
|
120
131
|
/**
|
|
121
|
-
* 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.
|
|
122
133
|
*/
|
|
123
134
|
verify(encodedSDJwt, requiredClaimKeys, requireKeyBindings) {
|
|
124
135
|
return __async(this, null, function* () {
|
|
125
|
-
var _a, _b, _c;
|
|
126
136
|
const result = yield __superGet(_SDJwtVcInstance.prototype, this, "verify").call(this, encodedSDJwt, requiredClaimKeys, requireKeyBindings).then((res) => {
|
|
127
137
|
return {
|
|
128
138
|
payload: res.payload,
|
|
@@ -130,9 +140,145 @@ var SDJwtVcInstance = class _SDJwtVcInstance extends import_core.SDJwtInstance {
|
|
|
130
140
|
kb: res.kb
|
|
131
141
|
};
|
|
132
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;
|
|
133
279
|
if (result.payload.status) {
|
|
134
280
|
if (result.payload.status.status_list) {
|
|
135
|
-
const fetcher = (_a = this.userConfig.statusListFetcher) != null ? _a : this.statusListFetcher;
|
|
281
|
+
const fetcher = (_a = this.userConfig.statusListFetcher) != null ? _a : this.statusListFetcher.bind(this);
|
|
136
282
|
const statusListJWT = yield fetcher(
|
|
137
283
|
result.payload.status.status_list.uri
|
|
138
284
|
);
|
|
@@ -145,11 +291,10 @@ var SDJwtVcInstance = class _SDJwtVcInstance extends import_core.SDJwtInstance {
|
|
|
145
291
|
const status = statusList.getStatus(
|
|
146
292
|
result.payload.status.status_list.idx
|
|
147
293
|
);
|
|
148
|
-
const statusValidator = (_c = this.userConfig.statusValidator) != null ? _c : this.statusValidator;
|
|
294
|
+
const statusValidator = (_c = this.userConfig.statusValidator) != null ? _c : this.statusValidator.bind(this);
|
|
149
295
|
yield statusValidator(status);
|
|
150
296
|
}
|
|
151
297
|
}
|
|
152
|
-
return result;
|
|
153
298
|
});
|
|
154
299
|
}
|
|
155
300
|
};
|
package/dist/index.mjs
CHANGED
|
@@ -28,6 +28,8 @@ 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);
|
|
@@ -95,11 +97,10 @@ var SDJwtVcInstance = class _SDJwtVcInstance extends SDJwtInstance {
|
|
|
95
97
|
});
|
|
96
98
|
}
|
|
97
99
|
/**
|
|
98
|
-
* 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.
|
|
99
101
|
*/
|
|
100
102
|
verify(encodedSDJwt, requiredClaimKeys, requireKeyBindings) {
|
|
101
103
|
return __async(this, null, function* () {
|
|
102
|
-
var _a, _b, _c;
|
|
103
104
|
const result = yield __superGet(_SDJwtVcInstance.prototype, this, "verify").call(this, encodedSDJwt, requiredClaimKeys, requireKeyBindings).then((res) => {
|
|
104
105
|
return {
|
|
105
106
|
payload: res.payload,
|
|
@@ -107,9 +108,145 @@ var SDJwtVcInstance = class _SDJwtVcInstance extends SDJwtInstance {
|
|
|
107
108
|
kb: res.kb
|
|
108
109
|
};
|
|
109
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;
|
|
110
247
|
if (result.payload.status) {
|
|
111
248
|
if (result.payload.status.status_list) {
|
|
112
|
-
const fetcher = (_a = this.userConfig.statusListFetcher) != null ? _a : this.statusListFetcher;
|
|
249
|
+
const fetcher = (_a = this.userConfig.statusListFetcher) != null ? _a : this.statusListFetcher.bind(this);
|
|
113
250
|
const statusListJWT = yield fetcher(
|
|
114
251
|
result.payload.status.status_list.uri
|
|
115
252
|
);
|
|
@@ -122,11 +259,10 @@ var SDJwtVcInstance = class _SDJwtVcInstance extends SDJwtInstance {
|
|
|
122
259
|
const status = statusList.getStatus(
|
|
123
260
|
result.payload.status.status_list.idx
|
|
124
261
|
);
|
|
125
|
-
const statusValidator = (_c = this.userConfig.statusValidator) != null ? _c : this.statusValidator;
|
|
262
|
+
const statusValidator = (_c = this.userConfig.statusValidator) != null ? _c : this.statusValidator.bind(this);
|
|
126
263
|
yield statusValidator(status);
|
|
127
264
|
}
|
|
128
265
|
}
|
|
129
|
-
return result;
|
|
130
266
|
});
|
|
131
267
|
}
|
|
132
268
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sd-jwt/sd-jwt-vc",
|
|
3
|
-
"version": "0.7.2-next.
|
|
3
|
+
"version": "0.7.2-next.7+96e76a9",
|
|
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.7.2-next.
|
|
43
|
-
"@sd-jwt/jwt-status-list": "0.7.2-next.
|
|
44
|
-
"@sd-jwt/utils": "0.7.2-next.
|
|
42
|
+
"@sd-jwt/core": "0.7.2-next.7+96e76a9",
|
|
43
|
+
"@sd-jwt/jwt-status-list": "0.7.2-next.7+96e76a9",
|
|
44
|
+
"@sd-jwt/utils": "0.7.2-next.7+96e76a9",
|
|
45
|
+
"ajv": "^8.17.1",
|
|
46
|
+
"ajv-formats": "^3.0.1"
|
|
45
47
|
},
|
|
46
48
|
"devDependencies": {
|
|
47
|
-
"@sd-jwt/crypto-nodejs": "0.7.2-next.
|
|
48
|
-
"@sd-jwt/types": "0.7.2-next.
|
|
49
|
-
"jose": "^5.2.2"
|
|
49
|
+
"@sd-jwt/crypto-nodejs": "0.7.2-next.7+96e76a9",
|
|
50
|
+
"@sd-jwt/types": "0.7.2-next.7+96e76a9",
|
|
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": "96e76a9d553bff34274b5ad243d0154cd220061b"
|
|
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,13 +1,23 @@
|
|
|
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.
|
|
@@ -95,7 +105,7 @@ export class SDJwtVcInstance extends SDJwtInstance<SdJwtVcPayload> {
|
|
|
95
105
|
}
|
|
96
106
|
|
|
97
107
|
/**
|
|
98
|
-
* 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.
|
|
99
109
|
*/
|
|
100
110
|
async verify(
|
|
101
111
|
encodedSDJwt: string,
|
|
@@ -103,7 +113,7 @@ export class SDJwtVcInstance extends SDJwtInstance<SdJwtVcPayload> {
|
|
|
103
113
|
requireKeyBindings?: boolean,
|
|
104
114
|
) {
|
|
105
115
|
// Call the parent class's verify method
|
|
106
|
-
const result = await super
|
|
116
|
+
const result: VerificationResult = await super
|
|
107
117
|
.verify(encodedSDJwt, requiredClaimKeys, requireKeyBindings)
|
|
108
118
|
.then((res) => {
|
|
109
119
|
return {
|
|
@@ -113,12 +123,167 @@ export class SDJwtVcInstance extends SDJwtInstance<SdJwtVcPayload> {
|
|
|
113
123
|
};
|
|
114
124
|
});
|
|
115
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> {
|
|
116
280
|
if (result.payload.status) {
|
|
117
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
|
|
118
282
|
if (result.payload.status.status_list) {
|
|
119
283
|
// fetch the status list from the uri
|
|
120
|
-
const fetcher =
|
|
121
|
-
this.userConfig.statusListFetcher ??
|
|
284
|
+
const fetcher: StatusListFetcher =
|
|
285
|
+
this.userConfig.statusListFetcher ??
|
|
286
|
+
this.statusListFetcher.bind(this);
|
|
122
287
|
// fetch the status list from the uri
|
|
123
288
|
const statusListJWT = await fetcher(
|
|
124
289
|
result.payload.status.status_list.uri,
|
|
@@ -146,12 +311,10 @@ export class SDJwtVcInstance extends SDJwtInstance<SdJwtVcPayload> {
|
|
|
146
311
|
);
|
|
147
312
|
|
|
148
313
|
// validate the status
|
|
149
|
-
const statusValidator =
|
|
150
|
-
this.userConfig.statusValidator ?? this.statusValidator;
|
|
314
|
+
const statusValidator: StatusValidator =
|
|
315
|
+
this.userConfig.statusValidator ?? this.statusValidator.bind(this);
|
|
151
316
|
await statusValidator(status);
|
|
152
317
|
}
|
|
153
318
|
}
|
|
154
|
-
|
|
155
|
-
return result;
|
|
156
319
|
}
|
|
157
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', () => {
|