@sd-jwt/core 0.19.1-next.5 → 0.19.1-next.6
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 +5 -7
- package/dist/index.d.mts +343 -16
- package/dist/index.d.ts +343 -16
- package/dist/index.js +644 -142
- package/dist/index.mjs +585 -127
- package/package.json +4 -7
- package/src/decode/decode.ts +366 -0
- package/src/decode/index.ts +1 -0
- package/src/decoy.ts +2 -2
- package/src/flattenJSON.ts +3 -3
- package/src/generalJSON.ts +3 -3
- package/src/index.ts +39 -25
- package/src/jwt.ts +10 -15
- package/src/kbjwt.ts +5 -6
- package/src/present/index.ts +1 -0
- package/src/present/present.ts +210 -0
- package/src/sdjwt.ts +11 -10
- package/src/test/decode/decode.spec.ts +202 -0
- package/src/test/decoy.spec.ts +2 -2
- package/src/test/generalJSON.spec.ts +1 -1
- package/src/test/index.spec.ts +2 -2
- package/src/test/jwt.spec.ts +7 -13
- package/src/test/kbjwt.spec.ts +5 -5
- package/src/test/present/present.spec.ts +305 -0
- package/src/test/sdjwt.spec.ts +4 -4
- package/src/test/types/type.spec.ts +88 -0
- package/src/test/utils/base64url.spec.ts +33 -0
- package/src/test/utils/disclosure.spec.ts +170 -0
- package/src/test/utils/error.spec.ts +15 -0
- package/src/types/index.ts +2 -0
- package/src/types/type.ts +249 -0
- package/src/types/verification-error.ts +55 -0
- package/src/utils/base64url.ts +6 -0
- package/src/utils/disclosure.ts +98 -0
- package/src/utils/error.ts +25 -0
- package/src/utils/index.ts +3 -0
- package/test/app-e2e.spec.ts +8 -8
- package/test/rfc9901-validation.spec.ts +150 -0
- package/CHANGELOG.md +0 -240
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sd-jwt/core",
|
|
3
|
-
"version": "0.19.1-next.
|
|
3
|
+
"version": "0.19.1-next.6+2bc47b2",
|
|
4
4
|
"description": "sd-jwt draft 7 implementation in typescript",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -37,13 +37,10 @@
|
|
|
37
37
|
},
|
|
38
38
|
"license": "Apache-2.0",
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@
|
|
40
|
+
"@owf/crypto": "^0.1.0-alpha-20260312123226"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@
|
|
44
|
-
"@sd-jwt/present": "0.19.1-next.5+7e24764",
|
|
45
|
-
"@sd-jwt/types": "0.19.1-next.5+7e24764",
|
|
46
|
-
"@sd-jwt/utils": "0.19.1-next.5+7e24764"
|
|
43
|
+
"@owf/identity-common": "^0.1.0-alpha-20260312123226"
|
|
47
44
|
},
|
|
48
45
|
"publishConfig": {
|
|
49
46
|
"access": "public"
|
|
@@ -61,5 +58,5 @@
|
|
|
61
58
|
"esm"
|
|
62
59
|
]
|
|
63
60
|
},
|
|
64
|
-
"gitHead": "
|
|
61
|
+
"gitHead": "2bc47b207fc23ea7ef340d81fac91e84c13ad58b"
|
|
65
62
|
}
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import type { HasherAndAlgSync, HasherSync } from '../types';
|
|
2
|
+
import {
|
|
3
|
+
type Hasher,
|
|
4
|
+
type HasherAndAlg,
|
|
5
|
+
SD_DIGEST,
|
|
6
|
+
SD_LIST_KEY,
|
|
7
|
+
SD_SEPARATOR,
|
|
8
|
+
} from '../types';
|
|
9
|
+
import { base64urlDecode, Disclosure, SDJWTException } from '../utils';
|
|
10
|
+
|
|
11
|
+
export const decodeJwt = <
|
|
12
|
+
H extends Record<string, unknown>,
|
|
13
|
+
T extends Record<string, unknown>,
|
|
14
|
+
>(
|
|
15
|
+
jwt: string,
|
|
16
|
+
): { header: H; payload: T; signature: string } => {
|
|
17
|
+
const { 0: header, 1: payload, 2: signature, length } = jwt.split('.');
|
|
18
|
+
if (length !== 3) {
|
|
19
|
+
throw new SDJWTException('Invalid JWT as input');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
header: JSON.parse(base64urlDecode(header)),
|
|
24
|
+
payload: JSON.parse(base64urlDecode(payload)),
|
|
25
|
+
signature: signature,
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Split the sdjwt into 3 parts: jwt, disclosures and keybinding jwt. each part is base64url encoded
|
|
30
|
+
// It's separated by the ~ character
|
|
31
|
+
//
|
|
32
|
+
// If there is no keybinding jwt, the third part will be undefined
|
|
33
|
+
// If there are no disclosures, the second part will be an empty array
|
|
34
|
+
export const splitSdJwt = (
|
|
35
|
+
sdjwt: string,
|
|
36
|
+
): { jwt: string; disclosures: string[]; kbJwt?: string } => {
|
|
37
|
+
const [encodedJwt, ...encodedDisclosures] = sdjwt.split(SD_SEPARATOR);
|
|
38
|
+
if (encodedDisclosures.length === 0) {
|
|
39
|
+
// if input is just jwt, then return here.
|
|
40
|
+
// This is for compatibility with jwt
|
|
41
|
+
return {
|
|
42
|
+
jwt: encodedJwt,
|
|
43
|
+
disclosures: [],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const encodedKeyBindingJwt = encodedDisclosures.pop();
|
|
48
|
+
return {
|
|
49
|
+
jwt: encodedJwt,
|
|
50
|
+
disclosures: encodedDisclosures,
|
|
51
|
+
kbJwt: encodedKeyBindingJwt || undefined,
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Decode the sdjwt into the jwt, disclosures and keybinding jwt
|
|
56
|
+
// jwt, disclosures and keybinding jwt are also decoded
|
|
57
|
+
export const decodeSdJwt = async (
|
|
58
|
+
sdjwt: string,
|
|
59
|
+
hasher: Hasher,
|
|
60
|
+
): Promise<DecodedSDJwt> => {
|
|
61
|
+
const [encodedJwt, ...encodedDisclosures] = sdjwt.split(SD_SEPARATOR);
|
|
62
|
+
const jwt = decodeJwt(encodedJwt);
|
|
63
|
+
|
|
64
|
+
if (encodedDisclosures.length === 0) {
|
|
65
|
+
// if input is just jwt, then return here.
|
|
66
|
+
// This is for compatibility with jwt
|
|
67
|
+
return {
|
|
68
|
+
jwt,
|
|
69
|
+
disclosures: [],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const encodedKeyBindingJwt = encodedDisclosures.pop();
|
|
74
|
+
const kbJwt = encodedKeyBindingJwt
|
|
75
|
+
? decodeJwt(encodedKeyBindingJwt)
|
|
76
|
+
: undefined;
|
|
77
|
+
|
|
78
|
+
const { _sd_alg } = getSDAlgAndPayload(jwt.payload);
|
|
79
|
+
|
|
80
|
+
const disclosures = await Promise.all(
|
|
81
|
+
encodedDisclosures.map((ed) =>
|
|
82
|
+
Disclosure.fromEncode(ed, { alg: _sd_alg, hasher }),
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
jwt,
|
|
88
|
+
disclosures,
|
|
89
|
+
kbJwt,
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const decodeSdJwtSync = (
|
|
94
|
+
sdjwt: string,
|
|
95
|
+
hasher: HasherSync,
|
|
96
|
+
): DecodedSDJwt => {
|
|
97
|
+
const [encodedJwt, ...encodedDisclosures] = sdjwt.split(SD_SEPARATOR);
|
|
98
|
+
const jwt = decodeJwt(encodedJwt);
|
|
99
|
+
|
|
100
|
+
if (encodedDisclosures.length === 0) {
|
|
101
|
+
// if input is just jwt, then return here.
|
|
102
|
+
// This is for compatibility with jwt
|
|
103
|
+
return {
|
|
104
|
+
jwt,
|
|
105
|
+
disclosures: [],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const encodedKeyBindingJwt = encodedDisclosures.pop();
|
|
110
|
+
const kbJwt = encodedKeyBindingJwt
|
|
111
|
+
? decodeJwt(encodedKeyBindingJwt)
|
|
112
|
+
: undefined;
|
|
113
|
+
|
|
114
|
+
const { _sd_alg } = getSDAlgAndPayload(jwt.payload);
|
|
115
|
+
|
|
116
|
+
const disclosures = encodedDisclosures.map((ed) =>
|
|
117
|
+
Disclosure.fromEncodeSync(ed, { alg: _sd_alg, hasher }),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
jwt,
|
|
122
|
+
disclosures,
|
|
123
|
+
kbJwt,
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Get the claims from jwt and disclosures
|
|
128
|
+
// The digested values are matched with the disclosures and the claims are extracted
|
|
129
|
+
export const getClaims = async <T = Record<string, unknown>>(
|
|
130
|
+
rawPayload: Record<string, unknown>,
|
|
131
|
+
disclosures: Array<Disclosure>,
|
|
132
|
+
hasher: Hasher,
|
|
133
|
+
): Promise<T> => {
|
|
134
|
+
const { unpackedObj } = await unpack(rawPayload, disclosures, hasher);
|
|
135
|
+
// The caller supplies T to match their expected shape
|
|
136
|
+
return unpackedObj as T;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export const getClaimsSync = <T = Record<string, unknown>>(
|
|
140
|
+
rawPayload: Record<string, unknown>,
|
|
141
|
+
disclosures: Array<Disclosure>,
|
|
142
|
+
hasher: HasherSync,
|
|
143
|
+
): T => {
|
|
144
|
+
const { unpackedObj } = unpackSync(rawPayload, disclosures, hasher);
|
|
145
|
+
// The caller supplies T to match their expected shape
|
|
146
|
+
return unpackedObj as T;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
150
|
+
typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
151
|
+
|
|
152
|
+
const unpackArray = (
|
|
153
|
+
arr: Array<unknown>,
|
|
154
|
+
map: Record<string, Disclosure>,
|
|
155
|
+
prefix = '',
|
|
156
|
+
seenDigests?: Set<string>,
|
|
157
|
+
): { unpackedObj: unknown; disclosureKeymap: Record<string, string> } => {
|
|
158
|
+
const keys: Record<string, string> = {};
|
|
159
|
+
const unpackedArray: unknown[] = [];
|
|
160
|
+
arr.forEach((item, idx) => {
|
|
161
|
+
if (isRecord(item)) {
|
|
162
|
+
const hash = item[SD_LIST_KEY];
|
|
163
|
+
if (typeof hash === 'string') {
|
|
164
|
+
// RFC 9901 Section 7.1 step 4: reject duplicate digests
|
|
165
|
+
if (seenDigests) {
|
|
166
|
+
if (seenDigests.has(hash)) {
|
|
167
|
+
throw new SDJWTException(
|
|
168
|
+
'Duplicate digest found in SD-JWT payload',
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
seenDigests.add(hash);
|
|
172
|
+
}
|
|
173
|
+
const disclosed = map[hash];
|
|
174
|
+
if (disclosed) {
|
|
175
|
+
const presentKey = prefix ? `${prefix}.${idx}` : `${idx}`;
|
|
176
|
+
keys[presentKey] = hash;
|
|
177
|
+
|
|
178
|
+
const { unpackedObj, disclosureKeymap: disclosureKeys } =
|
|
179
|
+
unpackObjInternal(disclosed.value, map, presentKey, seenDigests);
|
|
180
|
+
unpackedArray.push(unpackedObj);
|
|
181
|
+
Object.assign(keys, disclosureKeys);
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
const newKey = prefix ? `${prefix}.${idx}` : `${idx}`;
|
|
185
|
+
const { unpackedObj, disclosureKeymap: disclosureKeys } =
|
|
186
|
+
unpackObjInternal(item, map, newKey, seenDigests);
|
|
187
|
+
unpackedArray.push(unpackedObj);
|
|
188
|
+
Object.assign(keys, disclosureKeys);
|
|
189
|
+
}
|
|
190
|
+
} else if (Array.isArray(item)) {
|
|
191
|
+
const newKey = prefix ? `${prefix}.${idx}` : `${idx}`;
|
|
192
|
+
const { unpackedObj, disclosureKeymap: disclosureKeys } =
|
|
193
|
+
unpackObjInternal(item, map, newKey, seenDigests);
|
|
194
|
+
unpackedArray.push(unpackedObj);
|
|
195
|
+
Object.assign(keys, disclosureKeys);
|
|
196
|
+
} else {
|
|
197
|
+
unpackedArray.push(item);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
return { unpackedObj: unpackedArray, disclosureKeymap: keys };
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
export const unpackObj = (obj: unknown, map: Record<string, Disclosure>) => {
|
|
204
|
+
const copiedObj = JSON.parse(JSON.stringify(obj));
|
|
205
|
+
const seenDigests = new Set<string>();
|
|
206
|
+
const result = unpackObjInternal(copiedObj, map, '', seenDigests);
|
|
207
|
+
|
|
208
|
+
// RFC 9901 Section 7.1 step 5: reject unreferenced disclosures
|
|
209
|
+
const mapDigests = Object.keys(map);
|
|
210
|
+
const unusedDigests = mapDigests.filter((d) => !seenDigests.has(d));
|
|
211
|
+
if (unusedDigests.length > 0) {
|
|
212
|
+
throw new SDJWTException('Unreferenced disclosure(s) detected in SD-JWT');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return result;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const unpackObjInternal = (
|
|
219
|
+
obj: unknown,
|
|
220
|
+
map: Record<string, Disclosure>,
|
|
221
|
+
prefix = '',
|
|
222
|
+
seenDigests?: Set<string>,
|
|
223
|
+
): { unpackedObj: unknown; disclosureKeymap: Record<string, string> } => {
|
|
224
|
+
const keys: Record<string, string> = {};
|
|
225
|
+
if (typeof obj === 'object' && obj !== null) {
|
|
226
|
+
if (Array.isArray(obj)) {
|
|
227
|
+
return unpackArray(obj, map, prefix, seenDigests);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const record = obj as Record<string, unknown>;
|
|
231
|
+
for (const key in record) {
|
|
232
|
+
if (
|
|
233
|
+
key !== SD_DIGEST &&
|
|
234
|
+
key !== SD_LIST_KEY &&
|
|
235
|
+
typeof record[key] === 'object'
|
|
236
|
+
) {
|
|
237
|
+
const newKey = prefix ? `${prefix}.${key}` : key;
|
|
238
|
+
const { unpackedObj, disclosureKeymap: disclosureKeys } =
|
|
239
|
+
unpackObjInternal(record[key], map, newKey, seenDigests);
|
|
240
|
+
record[key] = unpackedObj;
|
|
241
|
+
Object.assign(keys, disclosureKeys);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const { _sd, ...payload } = record as Record<string, unknown> & {
|
|
246
|
+
_sd?: Array<string>;
|
|
247
|
+
};
|
|
248
|
+
const claims: Record<string, unknown> = {};
|
|
249
|
+
if (_sd) {
|
|
250
|
+
for (const hash of _sd) {
|
|
251
|
+
// RFC 9901 Section 7.1 step 4: reject duplicate digests
|
|
252
|
+
if (seenDigests) {
|
|
253
|
+
if (seenDigests.has(hash)) {
|
|
254
|
+
throw new SDJWTException(
|
|
255
|
+
'Duplicate digest found in SD-JWT payload',
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
seenDigests.add(hash);
|
|
259
|
+
}
|
|
260
|
+
const disclosed = map[hash];
|
|
261
|
+
if (disclosed?.key) {
|
|
262
|
+
// RFC 9901 Section 7.1 step 3c.ii.3: reject if claim name already exists
|
|
263
|
+
if (disclosed.key in payload) {
|
|
264
|
+
throw new SDJWTException(
|
|
265
|
+
`Disclosed claim name "${disclosed.key}" conflicts with existing payload key`,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const presentKey = prefix
|
|
270
|
+
? `${prefix}.${disclosed.key}`
|
|
271
|
+
: disclosed.key;
|
|
272
|
+
keys[presentKey] = hash;
|
|
273
|
+
|
|
274
|
+
const { unpackedObj, disclosureKeymap: disclosureKeys } =
|
|
275
|
+
unpackObjInternal(disclosed.value, map, presentKey, seenDigests);
|
|
276
|
+
claims[disclosed.key] = unpackedObj;
|
|
277
|
+
Object.assign(keys, disclosureKeys);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const unpackedObj = Object.assign(payload, claims);
|
|
283
|
+
return { unpackedObj, disclosureKeymap: keys };
|
|
284
|
+
}
|
|
285
|
+
return { unpackedObj: obj, disclosureKeymap: keys };
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// Creates a mapping of the digests of the disclosures to the actual disclosures
|
|
289
|
+
export const createHashMapping = async (
|
|
290
|
+
disclosures: Array<Disclosure>,
|
|
291
|
+
hash: HasherAndAlg,
|
|
292
|
+
) => {
|
|
293
|
+
const map: Record<string, Disclosure> = {};
|
|
294
|
+
for (let i = 0; i < disclosures.length; i++) {
|
|
295
|
+
const disclosure = disclosures[i];
|
|
296
|
+
const digest = await disclosure.digest(hash);
|
|
297
|
+
map[digest] = disclosure;
|
|
298
|
+
}
|
|
299
|
+
return map;
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
export const createHashMappingSync = (
|
|
303
|
+
disclosures: Array<Disclosure>,
|
|
304
|
+
hash: HasherAndAlgSync,
|
|
305
|
+
) => {
|
|
306
|
+
const map: Record<string, Disclosure> = {};
|
|
307
|
+
for (let i = 0; i < disclosures.length; i++) {
|
|
308
|
+
const disclosure = disclosures[i];
|
|
309
|
+
const digest = disclosure.digestSync(hash);
|
|
310
|
+
map[digest] = disclosure;
|
|
311
|
+
}
|
|
312
|
+
return map;
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
// Extract _sd_alg. If it is not present, it is assumed to be sha-256
|
|
316
|
+
export const getSDAlgAndPayload = (SdJwtPayload: Record<string, unknown>) => {
|
|
317
|
+
const { _sd_alg, ...payload } = SdJwtPayload;
|
|
318
|
+
if (typeof _sd_alg !== 'string') {
|
|
319
|
+
// This is for compatibility
|
|
320
|
+
return { _sd_alg: 'sha-256', payload };
|
|
321
|
+
}
|
|
322
|
+
return { _sd_alg, payload };
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// Match the digests of the disclosures with the claims and extract the claims
|
|
326
|
+
// unpack function use unpackObjInternal and unpackArray to recursively unpack the claims
|
|
327
|
+
// Since getSDAlgAndPayload create new object So we don't need to clone it again
|
|
328
|
+
export const unpack = async (
|
|
329
|
+
SdJwtPayload: Record<string, unknown>,
|
|
330
|
+
disclosures: Array<Disclosure>,
|
|
331
|
+
hasher: Hasher,
|
|
332
|
+
) => {
|
|
333
|
+
const { _sd_alg, payload } = getSDAlgAndPayload(SdJwtPayload);
|
|
334
|
+
const hash = { hasher, alg: _sd_alg };
|
|
335
|
+
const map = await createHashMapping(disclosures, hash);
|
|
336
|
+
|
|
337
|
+
return unpackObj(payload, map);
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
export const unpackSync = (
|
|
341
|
+
SdJwtPayload: Record<string, unknown>,
|
|
342
|
+
disclosures: Array<Disclosure>,
|
|
343
|
+
hasher: HasherSync,
|
|
344
|
+
) => {
|
|
345
|
+
const { _sd_alg, payload } = getSDAlgAndPayload(SdJwtPayload);
|
|
346
|
+
const hash = { hasher, alg: _sd_alg };
|
|
347
|
+
const map = createHashMappingSync(disclosures, hash);
|
|
348
|
+
|
|
349
|
+
return unpackObj(payload, map);
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
// This is the type of the object that is returned by the decodeSdJwt function
|
|
353
|
+
// It is a combination of the decoded jwt, the disclosures and the keybinding jwt
|
|
354
|
+
export type DecodedSDJwt = {
|
|
355
|
+
jwt: {
|
|
356
|
+
header: Record<string, unknown>;
|
|
357
|
+
payload: Record<string, unknown>; // raw payload of sd-jwt
|
|
358
|
+
signature: string;
|
|
359
|
+
};
|
|
360
|
+
disclosures: Array<Disclosure>;
|
|
361
|
+
kbJwt?: {
|
|
362
|
+
header: Record<string, unknown>;
|
|
363
|
+
payload: Record<string, unknown>;
|
|
364
|
+
signature: string;
|
|
365
|
+
};
|
|
366
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './decode';
|
package/src/decoy.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { HasherAndAlg, SaltGenerator } from '
|
|
2
|
-
import { uint8ArrayToBase64Url } from '
|
|
1
|
+
import type { HasherAndAlg, SaltGenerator } from './types';
|
|
2
|
+
import { uint8ArrayToBase64Url } from './utils';
|
|
3
3
|
|
|
4
4
|
// This function creates a decoy value that can be used to obscure SD JWT payload.
|
|
5
5
|
// The value is basically a hash of a random salt. So the value is not predictable.
|
package/src/flattenJSON.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { splitSdJwt } from '
|
|
2
|
-
import { SD_SEPARATOR } from '
|
|
3
|
-
import { SDJWTException } from '
|
|
1
|
+
import { splitSdJwt } from './decode';
|
|
2
|
+
import { SD_SEPARATOR } from './types';
|
|
3
|
+
import { SDJWTException } from './utils';
|
|
4
4
|
|
|
5
5
|
export type FlattenJSONData = {
|
|
6
6
|
jwtData: {
|
package/src/generalJSON.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { splitSdJwt } from '
|
|
2
|
-
import { SD_SEPARATOR, type Signer } from '
|
|
3
|
-
import { base64urlEncode, SDJWTException } from '
|
|
1
|
+
import { splitSdJwt } from './decode';
|
|
2
|
+
import { SD_SEPARATOR, type Signer } from './types';
|
|
3
|
+
import { base64urlEncode, SDJWTException } from './utils';
|
|
4
4
|
|
|
5
5
|
export type GeneralJSONData = {
|
|
6
6
|
payload: string;
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { getSDAlgAndPayload } from '
|
|
1
|
+
import { getSDAlgAndPayload } from './decode';
|
|
2
|
+
import { FlattenJSON } from './flattenJSON';
|
|
3
|
+
import { GeneralJSON } from './generalJSON';
|
|
4
|
+
import { Jwt, type VerifierOptions } from './jwt';
|
|
5
|
+
import { KBJwt } from './kbjwt';
|
|
6
|
+
import { pack, SDJwt } from './sdjwt';
|
|
2
7
|
import {
|
|
3
8
|
type DisclosureFrame,
|
|
4
9
|
type Hasher,
|
|
5
10
|
IANA_HASH_ALGORITHMS,
|
|
6
|
-
type JwtPayload,
|
|
7
11
|
KB_JWT_TYP,
|
|
8
12
|
type KBOptions,
|
|
9
13
|
type PresentationFrame,
|
|
@@ -13,25 +17,26 @@ import {
|
|
|
13
17
|
type Signer,
|
|
14
18
|
type VerificationError,
|
|
15
19
|
type VerificationErrorCode,
|
|
16
|
-
} from '
|
|
20
|
+
} from './types';
|
|
17
21
|
import {
|
|
18
22
|
base64urlDecode,
|
|
19
23
|
base64urlEncode,
|
|
24
|
+
ensureError,
|
|
20
25
|
SDJWTException,
|
|
21
26
|
uint8ArrayToBase64Url,
|
|
22
|
-
} from '
|
|
23
|
-
import { FlattenJSON } from './flattenJSON';
|
|
24
|
-
import { GeneralJSON } from './generalJSON';
|
|
25
|
-
import { Jwt, type VerifierOptions } from './jwt';
|
|
26
|
-
import { KBJwt } from './kbjwt';
|
|
27
|
-
import { pack, SDJwt } from './sdjwt';
|
|
27
|
+
} from './utils';
|
|
28
28
|
|
|
29
|
+
export * from './decode';
|
|
29
30
|
export * from './decoy';
|
|
30
31
|
export * from './flattenJSON';
|
|
31
32
|
export * from './generalJSON';
|
|
32
33
|
export * from './jwt';
|
|
33
34
|
export * from './kbjwt';
|
|
35
|
+
export * from './present';
|
|
34
36
|
export * from './sdjwt';
|
|
37
|
+
// Re-export all types, utils, decode, and present functionality
|
|
38
|
+
export * from './types';
|
|
39
|
+
export * from './utils';
|
|
35
40
|
|
|
36
41
|
export type SdJwtPayload = Record<string, unknown>;
|
|
37
42
|
|
|
@@ -235,7 +240,7 @@ export class SDJwtInstance<ExtendedPayload extends SdJwtPayload, T = unknown> {
|
|
|
235
240
|
}
|
|
236
241
|
const kb = await sdjwt.kbJwt.verifyKB({
|
|
237
242
|
verifier: this.userConfig.kbVerifier,
|
|
238
|
-
payload
|
|
243
|
+
payload,
|
|
239
244
|
nonce: options.keyBindingNonce,
|
|
240
245
|
});
|
|
241
246
|
if (!kb) {
|
|
@@ -327,7 +332,12 @@ export class SDJwtInstance<ExtendedPayload extends SdJwtPayload, T = unknown> {
|
|
|
327
332
|
return { success: false, errors };
|
|
328
333
|
}
|
|
329
334
|
|
|
330
|
-
|
|
335
|
+
if (!this.userConfig.hasher) {
|
|
336
|
+
throw new SDJWTException('Hasher not found');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// hasher and verifier are guaranteed to be defined here
|
|
340
|
+
const hasher = this.userConfig.hasher;
|
|
331
341
|
|
|
332
342
|
// Try to decode and validate the SD-JWT
|
|
333
343
|
let sdjwt: SDJwt | undefined;
|
|
@@ -339,10 +349,11 @@ export class SDJwtInstance<ExtendedPayload extends SdJwtPayload, T = unknown> {
|
|
|
339
349
|
if (!sdjwt.jwt || !sdjwt.jwt.payload) {
|
|
340
350
|
addError('INVALID_SD_JWT', 'Invalid SD JWT: missing JWT or payload');
|
|
341
351
|
}
|
|
342
|
-
} catch (
|
|
352
|
+
} catch (e) {
|
|
353
|
+
const error = ensureError(e);
|
|
343
354
|
addError(
|
|
344
355
|
'INVALID_SD_JWT',
|
|
345
|
-
`Failed to decode SD-JWT: ${
|
|
356
|
+
`Failed to decode SD-JWT: ${error.message}`,
|
|
346
357
|
error,
|
|
347
358
|
);
|
|
348
359
|
}
|
|
@@ -354,9 +365,10 @@ export class SDJwtInstance<ExtendedPayload extends SdJwtPayload, T = unknown> {
|
|
|
354
365
|
header = result.header;
|
|
355
366
|
const claims = await sdjwt.getClaims(hasher);
|
|
356
367
|
payload = claims as ExtendedPayload;
|
|
357
|
-
} catch (
|
|
358
|
-
const
|
|
359
|
-
|
|
368
|
+
} catch (e) {
|
|
369
|
+
const error = ensureError(e);
|
|
370
|
+
const code = exceptionToCode(error);
|
|
371
|
+
addError(code, error.message, error);
|
|
360
372
|
}
|
|
361
373
|
}
|
|
362
374
|
|
|
@@ -374,10 +386,11 @@ export class SDJwtInstance<ExtendedPayload extends SdJwtPayload, T = unknown> {
|
|
|
374
386
|
{ missingKeys },
|
|
375
387
|
);
|
|
376
388
|
}
|
|
377
|
-
} catch (
|
|
389
|
+
} catch (e) {
|
|
390
|
+
const error = ensureError(e);
|
|
378
391
|
addError(
|
|
379
392
|
'UNKNOWN_ERROR',
|
|
380
|
-
`Failed to check required claims: ${
|
|
393
|
+
`Failed to check required claims: ${error.message}`,
|
|
381
394
|
error,
|
|
382
395
|
);
|
|
383
396
|
}
|
|
@@ -399,7 +412,7 @@ export class SDJwtInstance<ExtendedPayload extends SdJwtPayload, T = unknown> {
|
|
|
399
412
|
try {
|
|
400
413
|
const kbResult = await sdjwt.kbJwt.verifyKB({
|
|
401
414
|
verifier: this.userConfig.kbVerifier,
|
|
402
|
-
payload
|
|
415
|
+
payload,
|
|
403
416
|
nonce: options.keyBindingNonce,
|
|
404
417
|
});
|
|
405
418
|
if (!kbResult) {
|
|
@@ -433,10 +446,11 @@ export class SDJwtInstance<ExtendedPayload extends SdJwtPayload, T = unknown> {
|
|
|
433
446
|
);
|
|
434
447
|
}
|
|
435
448
|
}
|
|
436
|
-
} catch (
|
|
449
|
+
} catch (e) {
|
|
450
|
+
const error = ensureError(e);
|
|
437
451
|
addError(
|
|
438
452
|
'KEY_BINDING_SIGNATURE_INVALID',
|
|
439
|
-
`Key binding verification failed: ${
|
|
453
|
+
`Key binding verification failed: ${error.message}`,
|
|
440
454
|
error,
|
|
441
455
|
);
|
|
442
456
|
}
|
|
@@ -491,7 +505,7 @@ export class SDJwtInstance<ExtendedPayload extends SdJwtPayload, T = unknown> {
|
|
|
491
505
|
}
|
|
492
506
|
|
|
493
507
|
const verifiedPayloads = await this.VerifyJwt(sdjwt.jwt, options);
|
|
494
|
-
const claims = await sdjwt.getClaims(hasher);
|
|
508
|
+
const claims = await sdjwt.getClaims<ExtendedPayload>(hasher);
|
|
495
509
|
return { payload: claims, header: verifiedPayloads.header };
|
|
496
510
|
}
|
|
497
511
|
|
|
@@ -758,8 +772,8 @@ export class SDJwtGeneralJSONInstance<ExtendedPayload extends SdJwtPayload> {
|
|
|
758
772
|
}
|
|
759
773
|
const kb = await sdjwt.kbJwt.verifyKB({
|
|
760
774
|
verifier: this.userConfig.kbVerifier,
|
|
761
|
-
payload
|
|
762
|
-
nonce: options.keyBindingNonce
|
|
775
|
+
payload,
|
|
776
|
+
nonce: options.keyBindingNonce,
|
|
763
777
|
});
|
|
764
778
|
if (!kb) {
|
|
765
779
|
throw new Error('signature is not valid');
|
|
@@ -835,7 +849,7 @@ export class SDJwtGeneralJSONInstance<ExtendedPayload extends SdJwtPayload> {
|
|
|
835
849
|
throw new SDJWTException('Invalid SD JWT');
|
|
836
850
|
}
|
|
837
851
|
|
|
838
|
-
const claims = await sdjwt.getClaims(hasher);
|
|
852
|
+
const claims = await sdjwt.getClaims<ExtendedPayload>(hasher);
|
|
839
853
|
return { payload: claims, headers: results.map((r) => r.header) };
|
|
840
854
|
}
|
|
841
855
|
|
package/src/jwt.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { decodeJwt } from '
|
|
2
|
-
import type { Base64urlString, Signer, Verifier } from '
|
|
3
|
-
import { base64urlEncode, SDJWTException } from '
|
|
1
|
+
import { decodeJwt } from './decode';
|
|
2
|
+
import type { Base64urlString, Signer, Verifier } from './types';
|
|
3
|
+
import { base64urlEncode, SDJWTException } from './utils';
|
|
4
4
|
|
|
5
5
|
export type JwtData<
|
|
6
6
|
Header extends Record<string, unknown>,
|
|
@@ -154,23 +154,18 @@ export class Jwt<
|
|
|
154
154
|
public async verify<T>(verifier: Verifier<T>, options?: T & VerifierOptions) {
|
|
155
155
|
const skew = options?.skewSeconds ? options.skewSeconds : 0;
|
|
156
156
|
const currentDate = options?.currentDate ?? Math.floor(Date.now() / 1000);
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
157
|
+
const iat = this.payload?.iat;
|
|
158
|
+
const nbf = this.payload?.nbf;
|
|
159
|
+
const exp = this.payload?.exp;
|
|
160
|
+
|
|
161
|
+
if (typeof iat === 'number' && iat - skew > currentDate) {
|
|
161
162
|
throw new SDJWTException('Verify Error: JWT is not yet valid');
|
|
162
163
|
}
|
|
163
164
|
|
|
164
|
-
if (
|
|
165
|
-
this.payload?.nbf &&
|
|
166
|
-
(this.payload.nbf as number) - skew > currentDate
|
|
167
|
-
) {
|
|
165
|
+
if (typeof nbf === 'number' && nbf - skew > currentDate) {
|
|
168
166
|
throw new SDJWTException('Verify Error: JWT is not yet valid');
|
|
169
167
|
}
|
|
170
|
-
if (
|
|
171
|
-
this.payload?.exp &&
|
|
172
|
-
(this.payload.exp as number) + skew < currentDate
|
|
173
|
-
) {
|
|
168
|
+
if (typeof exp === 'number' && exp + skew < currentDate) {
|
|
174
169
|
throw new SDJWTException('Verify Error: JWT is expired');
|
|
175
170
|
}
|
|
176
171
|
|
package/src/kbjwt.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
+
import { Jwt } from './jwt';
|
|
1
2
|
import {
|
|
2
|
-
type JwtPayload,
|
|
3
3
|
KB_JWT_TYP,
|
|
4
4
|
type KbVerifier,
|
|
5
5
|
type kbHeader,
|
|
6
6
|
type kbPayload,
|
|
7
|
-
} from '
|
|
8
|
-
import { SDJWTException } from '
|
|
9
|
-
import { Jwt } from './jwt';
|
|
7
|
+
} from './types';
|
|
8
|
+
import { SDJWTException } from './utils';
|
|
10
9
|
|
|
11
10
|
export class KBJwt<
|
|
12
11
|
Header extends kbHeader = kbHeader,
|
|
@@ -16,7 +15,7 @@ export class KBJwt<
|
|
|
16
15
|
// the type unknown is not good, but we don't know at this point how to get the public key of the signer, this is defined in the kbVerifier
|
|
17
16
|
public async verifyKB(values: {
|
|
18
17
|
verifier: KbVerifier;
|
|
19
|
-
payload:
|
|
18
|
+
payload: Record<string, unknown>;
|
|
20
19
|
nonce: string;
|
|
21
20
|
}) {
|
|
22
21
|
if (!this.header || !this.payload || !this.signature) {
|
|
@@ -34,7 +33,7 @@ export class KBJwt<
|
|
|
34
33
|
// this is for backward compatibility with version 06
|
|
35
34
|
!(
|
|
36
35
|
this.payload.sd_hash ||
|
|
37
|
-
(this.payload
|
|
36
|
+
('_sd_hash' in this.payload && this.payload._sd_hash)
|
|
38
37
|
)
|
|
39
38
|
) {
|
|
40
39
|
throw new SDJWTException('Invalid Key Binding Jwt');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './present';
|