@sd-jwt/sd-jwt-vc 0.7.2-next.0 → 0.7.2-next.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +20 -0
- package/dist/index.d.mts +72 -9
- package/dist/index.d.ts +72 -9
- package/dist/index.js +157 -7
- package/dist/index.mjs +148 -7
- package/package.json +12 -9
- package/src/sd-jwt-vc-config.ts +10 -2
- package/src/sd-jwt-vc-instance.ts +182 -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 +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [0.7.2](https://github.com/openwallet-foundation-labs/sd-jwt-js/compare/v0.7.1...v0.7.2) (2024-07-19)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @sd-jwt/sd-jwt-vc
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
6
14
|
## [0.7.1](https://github.com/openwallet-foundation-labs/sd-jwt-js/compare/v0.7.0...v0.7.1) (2024-05-21)
|
|
7
15
|
|
|
8
16
|
**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,12 +1,32 @@
|
|
|
1
|
-
import { SDJWTConfig, DisclosureFrame } from '@sd-jwt/types';
|
|
1
|
+
import { SDJWTConfig, kbPayload, kbHeader, DisclosureFrame } from '@sd-jwt/types';
|
|
2
2
|
import { SdJwtPayload, SDJwtInstance } from '@sd-jwt/core';
|
|
3
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>;
|
|
4
22
|
/**
|
|
5
23
|
* Configuration for SD-JWT-VC
|
|
6
24
|
*/
|
|
7
25
|
type SDJWTVCConfig = SDJWTConfig & {
|
|
8
|
-
statusListFetcher?:
|
|
9
|
-
statusValidator?:
|
|
26
|
+
statusListFetcher?: StatusListFetcher;
|
|
27
|
+
statusValidator?: StatusValidator;
|
|
28
|
+
vctFetcher?: VcTFetcher;
|
|
29
|
+
loadTypeMetadataFormat?: boolean;
|
|
10
30
|
};
|
|
11
31
|
|
|
12
32
|
interface SDJWTVCStatusReference {
|
|
@@ -22,11 +42,22 @@ interface SdJwtVcPayload extends SdJwtPayload {
|
|
|
22
42
|
exp?: number;
|
|
23
43
|
cnf?: unknown;
|
|
24
44
|
vct: string;
|
|
45
|
+
'vct#Integrity'?: string;
|
|
25
46
|
status?: SDJWTVCStatusReference;
|
|
26
47
|
sub?: string;
|
|
27
48
|
iat?: number;
|
|
28
49
|
}
|
|
29
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
|
+
|
|
30
61
|
declare class SDJwtVcInstance extends SDJwtInstance<SdJwtVcPayload> {
|
|
31
62
|
/**
|
|
32
63
|
* The type of the SD-JWT-VC set in the header.typ field.
|
|
@@ -52,12 +83,44 @@ declare class SDJwtVcInstance extends SDJwtInstance<SdJwtVcPayload> {
|
|
|
52
83
|
*/
|
|
53
84
|
private statusValidator;
|
|
54
85
|
/**
|
|
55
|
-
* 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
|
|
56
122
|
*/
|
|
57
|
-
|
|
58
|
-
payload: SdJwtVcPayload;
|
|
59
|
-
header: Record<string, unknown> | undefined;
|
|
60
|
-
}>;
|
|
123
|
+
private verifyStatus;
|
|
61
124
|
}
|
|
62
125
|
|
|
63
|
-
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,12 +1,32 @@
|
|
|
1
|
-
import { SDJWTConfig, DisclosureFrame } from '@sd-jwt/types';
|
|
1
|
+
import { SDJWTConfig, kbPayload, kbHeader, DisclosureFrame } from '@sd-jwt/types';
|
|
2
2
|
import { SdJwtPayload, SDJwtInstance } from '@sd-jwt/core';
|
|
3
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>;
|
|
4
22
|
/**
|
|
5
23
|
* Configuration for SD-JWT-VC
|
|
6
24
|
*/
|
|
7
25
|
type SDJWTVCConfig = SDJWTConfig & {
|
|
8
|
-
statusListFetcher?:
|
|
9
|
-
statusValidator?:
|
|
26
|
+
statusListFetcher?: StatusListFetcher;
|
|
27
|
+
statusValidator?: StatusValidator;
|
|
28
|
+
vctFetcher?: VcTFetcher;
|
|
29
|
+
loadTypeMetadataFormat?: boolean;
|
|
10
30
|
};
|
|
11
31
|
|
|
12
32
|
interface SDJWTVCStatusReference {
|
|
@@ -22,11 +42,22 @@ interface SdJwtVcPayload extends SdJwtPayload {
|
|
|
22
42
|
exp?: number;
|
|
23
43
|
cnf?: unknown;
|
|
24
44
|
vct: string;
|
|
45
|
+
'vct#Integrity'?: string;
|
|
25
46
|
status?: SDJWTVCStatusReference;
|
|
26
47
|
sub?: string;
|
|
27
48
|
iat?: number;
|
|
28
49
|
}
|
|
29
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
|
+
|
|
30
61
|
declare class SDJwtVcInstance extends SDJwtInstance<SdJwtVcPayload> {
|
|
31
62
|
/**
|
|
32
63
|
* The type of the SD-JWT-VC set in the header.typ field.
|
|
@@ -52,12 +83,44 @@ declare class SDJwtVcInstance extends SDJwtInstance<SdJwtVcPayload> {
|
|
|
52
83
|
*/
|
|
53
84
|
private statusValidator;
|
|
54
85
|
/**
|
|
55
|
-
* 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
|
|
56
122
|
*/
|
|
57
|
-
|
|
58
|
-
payload: SdJwtVcPayload;
|
|
59
|
-
header: Record<string, unknown> | undefined;
|
|
60
|
-
}>;
|
|
123
|
+
private verifyStatus;
|
|
61
124
|
}
|
|
62
125
|
|
|
63
|
-
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);
|
|
@@ -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,17 +129,156 @@ 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
|
-
return {
|
|
137
|
+
return {
|
|
138
|
+
payload: res.payload,
|
|
139
|
+
header: res.header,
|
|
140
|
+
kb: res.kb
|
|
141
|
+
};
|
|
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
|
+
})
|
|
127
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;
|
|
128
279
|
if (result.payload.status) {
|
|
129
280
|
if (result.payload.status.status_list) {
|
|
130
|
-
const fetcher = (_a = this.userConfig.statusListFetcher) != null ? _a : this.statusListFetcher;
|
|
281
|
+
const fetcher = (_a = this.userConfig.statusListFetcher) != null ? _a : this.statusListFetcher.bind(this);
|
|
131
282
|
const statusListJWT = yield fetcher(
|
|
132
283
|
result.payload.status.status_list.uri
|
|
133
284
|
);
|
|
@@ -140,11 +291,10 @@ var SDJwtVcInstance = class _SDJwtVcInstance extends import_core.SDJwtInstance {
|
|
|
140
291
|
const status = statusList.getStatus(
|
|
141
292
|
result.payload.status.status_list.idx
|
|
142
293
|
);
|
|
143
|
-
const statusValidator = (_c = this.userConfig.statusValidator) != null ? _c : this.statusValidator;
|
|
294
|
+
const statusValidator = (_c = this.userConfig.statusValidator) != null ? _c : this.statusValidator.bind(this);
|
|
144
295
|
yield statusValidator(status);
|
|
145
296
|
}
|
|
146
297
|
}
|
|
147
|
-
return result;
|
|
148
298
|
});
|
|
149
299
|
}
|
|
150
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);
|
|
@@ -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,17 +97,156 @@ 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
|
-
return {
|
|
105
|
+
return {
|
|
106
|
+
payload: res.payload,
|
|
107
|
+
header: res.header,
|
|
108
|
+
kb: res.kb
|
|
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
|
+
})
|
|
104
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;
|
|
105
247
|
if (result.payload.status) {
|
|
106
248
|
if (result.payload.status.status_list) {
|
|
107
|
-
const fetcher = (_a = this.userConfig.statusListFetcher) != null ? _a : this.statusListFetcher;
|
|
249
|
+
const fetcher = (_a = this.userConfig.statusListFetcher) != null ? _a : this.statusListFetcher.bind(this);
|
|
108
250
|
const statusListJWT = yield fetcher(
|
|
109
251
|
result.payload.status.status_list.uri
|
|
110
252
|
);
|
|
@@ -117,11 +259,10 @@ var SDJwtVcInstance = class _SDJwtVcInstance extends SDJwtInstance {
|
|
|
117
259
|
const status = statusList.getStatus(
|
|
118
260
|
result.payload.status.status_list.idx
|
|
119
261
|
);
|
|
120
|
-
const statusValidator = (_c = this.userConfig.statusValidator) != null ? _c : this.statusValidator;
|
|
262
|
+
const statusValidator = (_c = this.userConfig.statusValidator) != null ? _c : this.statusValidator.bind(this);
|
|
121
263
|
yield statusValidator(status);
|
|
122
264
|
}
|
|
123
265
|
}
|
|
124
|
-
return result;
|
|
125
266
|
});
|
|
126
267
|
}
|
|
127
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.10+a1474fd",
|
|
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.10+a1474fd",
|
|
43
|
+
"@sd-jwt/jwt-status-list": "0.7.2-next.10+a1474fd",
|
|
44
|
+
"@sd-jwt/utils": "0.7.2-next.10+a1474fd",
|
|
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.10+a1474fd",
|
|
50
|
+
"@sd-jwt/types": "0.7.2-next.10+a1474fd",
|
|
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": "a1474fde96ce566138c08dd548a0d7a4a0984158"
|
|
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.
|
|
@@ -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,18 +113,177 @@ 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
|
-
return {
|
|
119
|
+
return {
|
|
120
|
+
payload: res.payload as SdJwtVcPayload,
|
|
121
|
+
header: res.header,
|
|
122
|
+
kb: res.kb,
|
|
123
|
+
};
|
|
108
124
|
});
|
|
109
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> {
|
|
110
280
|
if (result.payload.status) {
|
|
111
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
|
|
112
282
|
if (result.payload.status.status_list) {
|
|
113
283
|
// fetch the status list from the uri
|
|
114
|
-
const fetcher =
|
|
115
|
-
this.userConfig.statusListFetcher ??
|
|
284
|
+
const fetcher: StatusListFetcher =
|
|
285
|
+
this.userConfig.statusListFetcher ??
|
|
286
|
+
this.statusListFetcher.bind(this);
|
|
116
287
|
// fetch the status list from the uri
|
|
117
288
|
const statusListJWT = await fetcher(
|
|
118
289
|
result.payload.status.status_list.uri,
|
|
@@ -140,12 +311,10 @@ export class SDJwtVcInstance extends SDJwtInstance<SdJwtVcPayload> {
|
|
|
140
311
|
);
|
|
141
312
|
|
|
142
313
|
// validate the status
|
|
143
|
-
const statusValidator =
|
|
144
|
-
this.userConfig.statusValidator ?? this.statusValidator;
|
|
314
|
+
const statusValidator: StatusValidator =
|
|
315
|
+
this.userConfig.statusValidator ?? this.statusValidator.bind(this);
|
|
145
316
|
await statusValidator(status);
|
|
146
317
|
}
|
|
147
318
|
}
|
|
148
|
-
|
|
149
|
-
return result;
|
|
150
319
|
}
|
|
151
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', () => {
|
|
@@ -260,6 +260,7 @@ async function JSONtest(filename: string) {
|
|
|
260
260
|
expect(verified).toBeDefined();
|
|
261
261
|
expect(verified).toStrictEqual({
|
|
262
262
|
header: { alg: 'EdDSA', typ: 'vc+sd-jwt' },
|
|
263
|
+
kb: undefined,
|
|
263
264
|
payload,
|
|
264
265
|
});
|
|
265
266
|
}
|