@sd-jwt/sd-jwt-vc 0.17.2-next.1 → 0.17.2-next.11
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/dist/index.d.mts +287 -106
- package/dist/index.d.ts +287 -106
- package/dist/index.js +341 -52
- package/dist/index.mjs +320 -52
- package/package.json +8 -7
- package/src/sd-jwt-vc-config.ts +2 -0
- package/src/sd-jwt-vc-instance.ts +276 -68
- package/src/sd-jwt-vc-payload.ts +1 -1
- package/src/sd-jwt-vc-type-metadata-format.ts +106 -54
- package/src/sd-jwt-vc-vct.ts +1 -1
- package/src/test/index.spec.ts +2 -2
- package/src/test/vct.spec.ts +505 -21
- package/src/verification-result.ts +3 -2
- package/test/app-e2e.spec.ts +13 -7
- package/test/array_data_types.json +1 -1
- package/test/array_full_sd.json +1 -1
- package/test/array_in_sd.json +1 -1
- package/test/array_nested_in_plain.json +1 -1
- package/test/array_none_disclosed.json +1 -1
- package/test/array_of_nulls.json +1 -1
- package/test/array_of_objects.json +1 -1
- package/test/array_of_scalars.json +1 -1
- package/test/array_recursive_sd.json +1 -1
- package/test/array_recursive_sd_some_disclosed.json +1 -1
- package/test/complex.json +1 -1
- package/test/header_mod.json +1 -1
- package/test/json_serialization.json +1 -1
- package/test/key_binding.json +1 -1
- package/test/no_sd.json +1 -1
- package/test/object_data_types.json +1 -1
- package/test/recursions.json +1 -1
package/src/test/vct.spec.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { digest, generateSalt } from '@sd-jwt/crypto-nodejs';
|
|
|
4
4
|
import type { DisclosureFrame, Signer, Verifier } from '@sd-jwt/types';
|
|
5
5
|
import { HttpResponse, http } from 'msw';
|
|
6
6
|
import { setupServer } from 'msw/node';
|
|
7
|
-
import { afterAll, beforeAll, describe, expect, test } from 'vitest';
|
|
7
|
+
import { afterAll, beforeAll, describe, expect, test, vitest } from 'vitest';
|
|
8
8
|
import { SDJwtVcInstance } from '..';
|
|
9
9
|
import type { SdJwtVcPayload } from '../sd-jwt-vc-payload';
|
|
10
10
|
import type { TypeMetadataFormat } from '../sd-jwt-vc-type-metadata-format';
|
|
@@ -15,6 +15,131 @@ const exampleVctm = {
|
|
|
15
15
|
description: 'An example credential type',
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
const baseVctm: TypeMetadataFormat = {
|
|
19
|
+
vct: 'http://example.com/base',
|
|
20
|
+
name: 'BaseCredentialType',
|
|
21
|
+
description: 'A base credential type',
|
|
22
|
+
claims: [
|
|
23
|
+
{
|
|
24
|
+
path: ['firstName'],
|
|
25
|
+
display: [{ lang: 'en', label: 'First Name' }],
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
display: [
|
|
29
|
+
{
|
|
30
|
+
lang: 'en',
|
|
31
|
+
name: 'Base Credential',
|
|
32
|
+
description: 'Base description',
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const extendingVctm: TypeMetadataFormat = {
|
|
38
|
+
vct: 'http://example.com/extending',
|
|
39
|
+
name: 'ExtendingCredentialType',
|
|
40
|
+
description: 'A credential type that extends the base',
|
|
41
|
+
extends: 'http://example.com/base',
|
|
42
|
+
claims: [
|
|
43
|
+
{
|
|
44
|
+
path: ['lastName'],
|
|
45
|
+
display: [{ lang: 'en', label: 'Last Name' }],
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
display: [
|
|
49
|
+
{
|
|
50
|
+
lang: 'en',
|
|
51
|
+
name: 'Extended Credential',
|
|
52
|
+
description: 'Extended description',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
lang: 'de',
|
|
56
|
+
name: 'Erweiterte Berechtigung',
|
|
57
|
+
description: 'Erweiterte Beschreibung',
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const middleVctm: TypeMetadataFormat = {
|
|
63
|
+
vct: 'http://example.com/middle',
|
|
64
|
+
name: 'MiddleCredentialType',
|
|
65
|
+
description: 'Middle type in chain',
|
|
66
|
+
extends: 'http://example.com/extending',
|
|
67
|
+
claims: [
|
|
68
|
+
{
|
|
69
|
+
path: ['age'],
|
|
70
|
+
display: [{ lang: 'en', label: 'Age' }],
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const overridingVctm: TypeMetadataFormat = {
|
|
76
|
+
vct: 'http://example.com/overriding',
|
|
77
|
+
name: 'OverridingCredentialType',
|
|
78
|
+
description: 'A credential type that overrides a claim from the base',
|
|
79
|
+
extends: 'http://example.com/base',
|
|
80
|
+
claims: [
|
|
81
|
+
{
|
|
82
|
+
path: ['firstName'],
|
|
83
|
+
display: [{ lang: 'en', label: 'Given Name' }], // Override with different label
|
|
84
|
+
sd: 'always' as const,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
path: ['middleName'],
|
|
88
|
+
display: [{ lang: 'en', label: 'Middle Name' }],
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const circularVctm: TypeMetadataFormat = {
|
|
94
|
+
vct: 'http://example.com/circular',
|
|
95
|
+
name: 'CircularCredentialType',
|
|
96
|
+
extends: 'http://example.com/circular',
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const deepVctm: TypeMetadataFormat = {
|
|
100
|
+
vct: 'http://example.com/deep',
|
|
101
|
+
name: 'DeepCredentialType',
|
|
102
|
+
extends: 'http://example.com/middle',
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const baseWithSdAlways: TypeMetadataFormat = {
|
|
106
|
+
vct: 'http://example.com/base-sd-always',
|
|
107
|
+
name: 'BaseWithSdAlways',
|
|
108
|
+
claims: [
|
|
109
|
+
{
|
|
110
|
+
path: ['sensitiveData'],
|
|
111
|
+
sd: 'always' as const,
|
|
112
|
+
display: [{ lang: 'en', label: 'Sensitive Data' }],
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const invalidExtendingSdChange: TypeMetadataFormat = {
|
|
118
|
+
vct: 'http://example.com/invalid-sd-change',
|
|
119
|
+
name: 'InvalidSdChange',
|
|
120
|
+
extends: 'http://example.com/base-sd-always',
|
|
121
|
+
claims: [
|
|
122
|
+
{
|
|
123
|
+
path: ['sensitiveData'],
|
|
124
|
+
sd: 'never' as const, // Invalid: trying to change from 'always' to 'never'
|
|
125
|
+
display: [{ lang: 'en', label: 'Sensitive Data' }],
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const validExtendingSdChange: TypeMetadataFormat = {
|
|
131
|
+
vct: 'http://example.com/valid-sd-change',
|
|
132
|
+
name: 'ValidSdChange',
|
|
133
|
+
extends: 'http://example.com/base',
|
|
134
|
+
claims: [
|
|
135
|
+
{
|
|
136
|
+
path: ['firstName'],
|
|
137
|
+
sd: 'always' as const, // Valid: base doesn't have sd or has 'allowed'
|
|
138
|
+
display: [{ lang: 'en', label: 'First Name' }],
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
};
|
|
142
|
+
|
|
18
143
|
const restHandlers = [
|
|
19
144
|
http.get('http://example.com/example', () => {
|
|
20
145
|
const res: TypeMetadataFormat = exampleVctm;
|
|
@@ -27,6 +152,40 @@ const restHandlers = [
|
|
|
27
152
|
}, 10000);
|
|
28
153
|
});
|
|
29
154
|
}),
|
|
155
|
+
http.get('http://example.com/base', () => {
|
|
156
|
+
return HttpResponse.json(baseVctm);
|
|
157
|
+
}),
|
|
158
|
+
http.get('http://example.com/extending', () => {
|
|
159
|
+
return HttpResponse.json(extendingVctm);
|
|
160
|
+
}),
|
|
161
|
+
http.get('http://example.com/middle', () => {
|
|
162
|
+
return HttpResponse.json(middleVctm);
|
|
163
|
+
}),
|
|
164
|
+
http.get('http://example.com/overriding', () => {
|
|
165
|
+
return HttpResponse.json(overridingVctm);
|
|
166
|
+
}),
|
|
167
|
+
http.get('http://example.com/circular', () => {
|
|
168
|
+
return HttpResponse.json(circularVctm);
|
|
169
|
+
}),
|
|
170
|
+
http.get('http://example.com/deep', () => {
|
|
171
|
+
return HttpResponse.json(deepVctm);
|
|
172
|
+
}),
|
|
173
|
+
http.get('http://example.com/base-sd-always', () => {
|
|
174
|
+
return HttpResponse.json(baseWithSdAlways);
|
|
175
|
+
}),
|
|
176
|
+
http.get('http://example.com/invalid-sd-change', () => {
|
|
177
|
+
return HttpResponse.json(invalidExtendingSdChange);
|
|
178
|
+
}),
|
|
179
|
+
http.get('http://example.com/valid-sd-change', () => {
|
|
180
|
+
return HttpResponse.json(validExtendingSdChange);
|
|
181
|
+
}),
|
|
182
|
+
http.get('http://example.com/invalid', () => {
|
|
183
|
+
// Return invalid type metadata (missing required 'vct' field)
|
|
184
|
+
return HttpResponse.json({
|
|
185
|
+
name: 'InvalidCredentialType',
|
|
186
|
+
description: 'Missing required vct field',
|
|
187
|
+
});
|
|
188
|
+
}),
|
|
30
189
|
];
|
|
31
190
|
|
|
32
191
|
//this value could be generated on demand to make it easier when changing the values
|
|
@@ -85,22 +244,52 @@ describe('App', () => {
|
|
|
85
244
|
afterEach(() => server.resetHandlers());
|
|
86
245
|
|
|
87
246
|
test('VCT Validation', async () => {
|
|
247
|
+
// The method is private, so TS complains, but you can use spies on private method just fine.
|
|
248
|
+
// @ts-expect-error
|
|
249
|
+
const validateIntegritySpy = vitest.spyOn(sdjwt, 'validateIntegrity');
|
|
250
|
+
|
|
88
251
|
const expectedPayload: SdJwtVcPayload = {
|
|
89
252
|
iat,
|
|
90
253
|
iss,
|
|
91
254
|
vct,
|
|
92
|
-
'vct#
|
|
255
|
+
'vct#integrity': vctIntegrity,
|
|
93
256
|
...claims,
|
|
94
257
|
};
|
|
258
|
+
|
|
95
259
|
const encodedSdjwt = await sdjwt.issue(
|
|
96
260
|
expectedPayload,
|
|
97
261
|
disclosureFrame as unknown as DisclosureFrame<SdJwtVcPayload>,
|
|
98
262
|
);
|
|
99
263
|
|
|
100
264
|
await sdjwt.verify(encodedSdjwt);
|
|
265
|
+
|
|
266
|
+
// Ensure validateIntegrity method was called
|
|
267
|
+
expect(validateIntegritySpy).toHaveBeenCalledWith(
|
|
268
|
+
expect.any(Response),
|
|
269
|
+
vct,
|
|
270
|
+
vctIntegrity,
|
|
271
|
+
);
|
|
101
272
|
});
|
|
102
273
|
|
|
103
|
-
test('VCT
|
|
274
|
+
test('VCT Validation with timeout', async () => {
|
|
275
|
+
const vct = 'http://example.com/timeout';
|
|
276
|
+
const expectedPayload: SdJwtVcPayload = {
|
|
277
|
+
iat,
|
|
278
|
+
iss,
|
|
279
|
+
vct,
|
|
280
|
+
...claims,
|
|
281
|
+
};
|
|
282
|
+
const encodedSdjwt = await sdjwt.issue(
|
|
283
|
+
expectedPayload,
|
|
284
|
+
disclosureFrame as unknown as DisclosureFrame<SdJwtVcPayload>,
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
await expect(sdjwt.verify(encodedSdjwt)).rejects.toThrowError(
|
|
288
|
+
`Request to ${vct} timed out`,
|
|
289
|
+
);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test('VCT Metadata retrieval', async () => {
|
|
104
293
|
const expectedPayload: SdJwtVcPayload = {
|
|
105
294
|
iat,
|
|
106
295
|
iss,
|
|
@@ -108,56 +297,351 @@ describe('App', () => {
|
|
|
108
297
|
'vct#Integrity': vctIntegrity,
|
|
109
298
|
...claims,
|
|
110
299
|
};
|
|
111
|
-
const
|
|
112
|
-
|
|
300
|
+
const encodedSdjwt = await sdjwt.issue(
|
|
301
|
+
expectedPayload,
|
|
302
|
+
disclosureFrame as unknown as DisclosureFrame<SdJwtVcPayload>,
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
const resolvedTypeMetadata = await sdjwt.getVct(encodedSdjwt);
|
|
306
|
+
|
|
307
|
+
// Check mergedTypeMetadata
|
|
308
|
+
expect(resolvedTypeMetadata?.mergedTypeMetadata).to.deep.eq({
|
|
309
|
+
description: 'An example credential type',
|
|
310
|
+
name: 'ExampleCredentialType',
|
|
311
|
+
vct: 'http://example.com/example',
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Check typeMetadataChain - should have only one document (no extends)
|
|
315
|
+
expect(resolvedTypeMetadata?.typeMetadataChain).toHaveLength(1);
|
|
316
|
+
expect(resolvedTypeMetadata?.typeMetadataChain[0].vct).toBe(
|
|
317
|
+
'http://example.com/example',
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
// Check vctValues - should have only one value
|
|
321
|
+
expect(resolvedTypeMetadata?.vctValues).toHaveLength(1);
|
|
322
|
+
expect(resolvedTypeMetadata?.vctValues[0]).toBe(
|
|
323
|
+
'http://example.com/example',
|
|
324
|
+
);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test('VCT with extends - simple chain', async () => {
|
|
328
|
+
const expectedPayload: SdJwtVcPayload = {
|
|
329
|
+
iat,
|
|
330
|
+
iss,
|
|
331
|
+
vct: 'http://example.com/extending',
|
|
332
|
+
...claims,
|
|
113
333
|
};
|
|
334
|
+
|
|
114
335
|
const encodedSdjwt = await sdjwt.issue(
|
|
115
336
|
expectedPayload,
|
|
116
337
|
disclosureFrame as unknown as DisclosureFrame<SdJwtVcPayload>,
|
|
117
|
-
{ header },
|
|
118
338
|
);
|
|
119
339
|
|
|
120
|
-
await sdjwt.
|
|
340
|
+
const resolvedTypeMetadata = await sdjwt.getVct(encodedSdjwt);
|
|
341
|
+
|
|
342
|
+
// Check mergedTypeMetadata - should merge claims from both base and extending types
|
|
343
|
+
expect(resolvedTypeMetadata?.mergedTypeMetadata.claims).toHaveLength(2);
|
|
344
|
+
expect(resolvedTypeMetadata?.mergedTypeMetadata.claims?.[0].path).toEqual([
|
|
345
|
+
'firstName',
|
|
346
|
+
]);
|
|
347
|
+
expect(resolvedTypeMetadata?.mergedTypeMetadata.claims?.[1].path).toEqual([
|
|
348
|
+
'lastName',
|
|
349
|
+
]);
|
|
350
|
+
|
|
351
|
+
// Display from extending type completely replaces base display (section 8.2)
|
|
352
|
+
expect(resolvedTypeMetadata?.mergedTypeMetadata.display).toHaveLength(2);
|
|
353
|
+
expect(resolvedTypeMetadata?.mergedTypeMetadata.display?.[0]).toEqual({
|
|
354
|
+
lang: 'en',
|
|
355
|
+
name: 'Extended Credential',
|
|
356
|
+
description: 'Extended description',
|
|
357
|
+
});
|
|
358
|
+
expect(resolvedTypeMetadata?.mergedTypeMetadata.display?.[1]).toEqual({
|
|
359
|
+
lang: 'de',
|
|
360
|
+
name: 'Erweiterte Berechtigung',
|
|
361
|
+
description: 'Erweiterte Beschreibung',
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// Top-level properties should come from extending type
|
|
365
|
+
expect(resolvedTypeMetadata?.mergedTypeMetadata.name).toBe(
|
|
366
|
+
'ExtendingCredentialType',
|
|
367
|
+
);
|
|
368
|
+
expect(resolvedTypeMetadata?.mergedTypeMetadata.description).toBe(
|
|
369
|
+
'A credential type that extends the base',
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
// Check typeMetadataChain - should have 2 documents in chain
|
|
373
|
+
expect(resolvedTypeMetadata?.typeMetadataChain).toHaveLength(2);
|
|
374
|
+
expect(resolvedTypeMetadata?.typeMetadataChain[0].vct).toBe(
|
|
375
|
+
'http://example.com/extending',
|
|
376
|
+
);
|
|
377
|
+
expect(resolvedTypeMetadata?.typeMetadataChain[1].vct).toBe(
|
|
378
|
+
'http://example.com/base',
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
// Check vctValues - should have 2 values
|
|
382
|
+
expect(resolvedTypeMetadata?.vctValues).toHaveLength(2);
|
|
383
|
+
expect(resolvedTypeMetadata?.vctValues[0]).toBe(
|
|
384
|
+
'http://example.com/extending',
|
|
385
|
+
);
|
|
386
|
+
expect(resolvedTypeMetadata?.vctValues[1]).toBe('http://example.com/base');
|
|
121
387
|
});
|
|
122
388
|
|
|
123
|
-
test('VCT
|
|
124
|
-
const vct = 'http://example.com/timeout';
|
|
389
|
+
test('VCT with extends - multi-level chain', async () => {
|
|
125
390
|
const expectedPayload: SdJwtVcPayload = {
|
|
126
391
|
iat,
|
|
127
392
|
iss,
|
|
128
|
-
vct,
|
|
393
|
+
vct: 'http://example.com/middle',
|
|
129
394
|
...claims,
|
|
130
395
|
};
|
|
396
|
+
|
|
131
397
|
const encodedSdjwt = await sdjwt.issue(
|
|
132
398
|
expectedPayload,
|
|
133
399
|
disclosureFrame as unknown as DisclosureFrame<SdJwtVcPayload>,
|
|
134
400
|
);
|
|
135
401
|
|
|
136
|
-
|
|
137
|
-
|
|
402
|
+
const resolvedTypeMetadata = await sdjwt.getVct(encodedSdjwt);
|
|
403
|
+
|
|
404
|
+
// Check mergedTypeMetadata - should merge claims from base -> extending -> middle
|
|
405
|
+
expect(resolvedTypeMetadata?.mergedTypeMetadata.claims).toHaveLength(3);
|
|
406
|
+
expect(resolvedTypeMetadata?.mergedTypeMetadata.claims?.[0].path).toEqual([
|
|
407
|
+
'firstName',
|
|
408
|
+
]);
|
|
409
|
+
expect(resolvedTypeMetadata?.mergedTypeMetadata.claims?.[1].path).toEqual([
|
|
410
|
+
'lastName',
|
|
411
|
+
]);
|
|
412
|
+
expect(resolvedTypeMetadata?.mergedTypeMetadata.claims?.[2].path).toEqual([
|
|
413
|
+
'age',
|
|
414
|
+
]);
|
|
415
|
+
|
|
416
|
+
// Top-level properties should come from the most derived type
|
|
417
|
+
expect(resolvedTypeMetadata?.mergedTypeMetadata.name).toBe(
|
|
418
|
+
'MiddleCredentialType',
|
|
419
|
+
);
|
|
420
|
+
expect(resolvedTypeMetadata?.mergedTypeMetadata.description).toBe(
|
|
421
|
+
'Middle type in chain',
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
// Check typeMetadataChain - should have 3 documents in chain
|
|
425
|
+
expect(resolvedTypeMetadata?.typeMetadataChain).toHaveLength(3);
|
|
426
|
+
expect(resolvedTypeMetadata?.typeMetadataChain[0].vct).toBe(
|
|
427
|
+
'http://example.com/middle',
|
|
138
428
|
);
|
|
429
|
+
expect(resolvedTypeMetadata?.typeMetadataChain[1].vct).toBe(
|
|
430
|
+
'http://example.com/extending',
|
|
431
|
+
);
|
|
432
|
+
expect(resolvedTypeMetadata?.typeMetadataChain[2].vct).toBe(
|
|
433
|
+
'http://example.com/base',
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
// Check vctValues - should have 3 values
|
|
437
|
+
expect(resolvedTypeMetadata?.vctValues).toHaveLength(3);
|
|
438
|
+
expect(resolvedTypeMetadata?.vctValues[0]).toBe(
|
|
439
|
+
'http://example.com/middle',
|
|
440
|
+
);
|
|
441
|
+
expect(resolvedTypeMetadata?.vctValues[1]).toBe(
|
|
442
|
+
'http://example.com/extending',
|
|
443
|
+
);
|
|
444
|
+
expect(resolvedTypeMetadata?.vctValues[2]).toBe('http://example.com/base');
|
|
139
445
|
});
|
|
140
446
|
|
|
141
|
-
test('VCT
|
|
447
|
+
test('VCT with circular dependency should throw error', async () => {
|
|
142
448
|
const expectedPayload: SdJwtVcPayload = {
|
|
143
449
|
iat,
|
|
144
450
|
iss,
|
|
145
|
-
vct,
|
|
146
|
-
'vct#Integrity': vctIntegrity,
|
|
451
|
+
vct: 'http://example.com/circular',
|
|
147
452
|
...claims,
|
|
148
453
|
};
|
|
454
|
+
|
|
149
455
|
const encodedSdjwt = await sdjwt.issue(
|
|
150
456
|
expectedPayload,
|
|
151
457
|
disclosureFrame as unknown as DisclosureFrame<SdJwtVcPayload>,
|
|
152
458
|
);
|
|
153
459
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
460
|
+
await expect(sdjwt.getVct(encodedSdjwt)).rejects.toThrowError(
|
|
461
|
+
'Circular dependency detected in VCT extends chain: http://example.com/circular',
|
|
462
|
+
);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
test('VCT with max depth exceeded should throw error', async () => {
|
|
466
|
+
const sdjwtWithShallowDepth = new SDJwtVcInstance({
|
|
467
|
+
signer,
|
|
468
|
+
signAlg: 'EdDSA',
|
|
469
|
+
verifier,
|
|
470
|
+
hasher: digest,
|
|
471
|
+
hashAlg: 'sha-256',
|
|
472
|
+
saltGenerator: generateSalt,
|
|
473
|
+
loadTypeMetadataFormat: true,
|
|
474
|
+
timeout: 1000,
|
|
475
|
+
maxVctExtendsDepth: 1, // Only allow 1 level of extends
|
|
159
476
|
});
|
|
477
|
+
|
|
478
|
+
const expectedPayload: SdJwtVcPayload = {
|
|
479
|
+
iat,
|
|
480
|
+
iss,
|
|
481
|
+
vct: 'http://example.com/middle', // This has 2 levels of extends
|
|
482
|
+
...claims,
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
const encodedSdjwt = await sdjwtWithShallowDepth.issue(
|
|
486
|
+
expectedPayload,
|
|
487
|
+
disclosureFrame as unknown as DisclosureFrame<SdJwtVcPayload>,
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
await expect(
|
|
491
|
+
sdjwtWithShallowDepth.getVct(encodedSdjwt),
|
|
492
|
+
).rejects.toThrowError('Maximum VCT extends depth of 1 exceeded');
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
test('VCT extends chain should work in verify method', async () => {
|
|
496
|
+
const expectedPayload: SdJwtVcPayload = {
|
|
497
|
+
iat,
|
|
498
|
+
iss,
|
|
499
|
+
vct: 'http://example.com/extending',
|
|
500
|
+
...claims,
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
const encodedSdjwt = await sdjwt.issue(
|
|
504
|
+
expectedPayload,
|
|
505
|
+
disclosureFrame as unknown as DisclosureFrame<SdJwtVcPayload>,
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
// Should not throw and should resolve the extends chain
|
|
509
|
+
const result = await sdjwt.verify(encodedSdjwt);
|
|
510
|
+
expect(result.payload.vct).toBe('http://example.com/extending');
|
|
511
|
+
|
|
512
|
+
// Check that typeMetadata was populated with resolved chain
|
|
513
|
+
expect(result.typeMetadata?.mergedTypeMetadata.claims).toHaveLength(2);
|
|
514
|
+
expect(result.typeMetadata?.typeMetadataChain).toHaveLength(2);
|
|
515
|
+
expect(result.typeMetadata?.vctValues).toHaveLength(2);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
test('VCT with overriding claim metadata', async () => {
|
|
519
|
+
const expectedPayload: SdJwtVcPayload = {
|
|
520
|
+
iat,
|
|
521
|
+
iss,
|
|
522
|
+
vct: 'http://example.com/overriding',
|
|
523
|
+
...claims,
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
const encodedSdjwt = await sdjwt.issue(
|
|
527
|
+
expectedPayload,
|
|
528
|
+
disclosureFrame as unknown as DisclosureFrame<SdJwtVcPayload>,
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
const resolvedTypeMetadata = await sdjwt.getVct(encodedSdjwt);
|
|
532
|
+
|
|
533
|
+
// Check mergedTypeMetadata - should have 2 claims: overridden firstName and new middleName
|
|
534
|
+
expect(resolvedTypeMetadata?.mergedTypeMetadata.claims).toHaveLength(2);
|
|
535
|
+
|
|
536
|
+
// First claim should be the overridden firstName with new label and sd property
|
|
537
|
+
expect(resolvedTypeMetadata?.mergedTypeMetadata.claims?.[0].path).toEqual([
|
|
538
|
+
'firstName',
|
|
539
|
+
]);
|
|
540
|
+
expect(
|
|
541
|
+
resolvedTypeMetadata?.mergedTypeMetadata.claims?.[0].display?.[0].label,
|
|
542
|
+
).toBe('Given Name');
|
|
543
|
+
expect(resolvedTypeMetadata?.mergedTypeMetadata.claims?.[0].sd).toBe(
|
|
544
|
+
'always',
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
// Second claim should be the new middleName
|
|
548
|
+
expect(resolvedTypeMetadata?.mergedTypeMetadata.claims?.[1].path).toEqual([
|
|
549
|
+
'middleName',
|
|
550
|
+
]);
|
|
551
|
+
expect(
|
|
552
|
+
resolvedTypeMetadata?.mergedTypeMetadata.claims?.[1].display?.[0].label,
|
|
553
|
+
).toBe('Middle Name');
|
|
554
|
+
|
|
555
|
+
// Check typeMetadataChain - should have 2 documents
|
|
556
|
+
expect(resolvedTypeMetadata?.typeMetadataChain).toHaveLength(2);
|
|
557
|
+
expect(resolvedTypeMetadata?.typeMetadataChain[0].vct).toBe(
|
|
558
|
+
'http://example.com/overriding',
|
|
559
|
+
);
|
|
560
|
+
expect(resolvedTypeMetadata?.typeMetadataChain[1].vct).toBe(
|
|
561
|
+
'http://example.com/base',
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
// Check vctValues
|
|
565
|
+
expect(resolvedTypeMetadata?.vctValues).toHaveLength(2);
|
|
566
|
+
expect(resolvedTypeMetadata?.vctValues[0]).toBe(
|
|
567
|
+
'http://example.com/overriding',
|
|
568
|
+
);
|
|
569
|
+
expect(resolvedTypeMetadata?.vctValues[1]).toBe('http://example.com/base');
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
test('VCT with valid sd property change (allowed to always)', async () => {
|
|
573
|
+
const expectedPayload: SdJwtVcPayload = {
|
|
574
|
+
iat,
|
|
575
|
+
iss,
|
|
576
|
+
vct: 'http://example.com/valid-sd-change',
|
|
577
|
+
...claims,
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
const encodedSdjwt = await sdjwt.issue(
|
|
581
|
+
expectedPayload,
|
|
582
|
+
disclosureFrame as unknown as DisclosureFrame<SdJwtVcPayload>,
|
|
583
|
+
);
|
|
584
|
+
|
|
585
|
+
const resolvedTypeMetadata = await sdjwt.getVct(encodedSdjwt);
|
|
586
|
+
|
|
587
|
+
// Check mergedTypeMetadata - should successfully merge - changing from undefined/allowed to always is valid
|
|
588
|
+
expect(resolvedTypeMetadata?.mergedTypeMetadata.claims).toHaveLength(1);
|
|
589
|
+
expect(resolvedTypeMetadata?.mergedTypeMetadata.claims?.[0].path).toEqual([
|
|
590
|
+
'firstName',
|
|
591
|
+
]);
|
|
592
|
+
expect(resolvedTypeMetadata?.mergedTypeMetadata.claims?.[0].sd).toBe(
|
|
593
|
+
'always',
|
|
594
|
+
);
|
|
595
|
+
|
|
596
|
+
// Check typeMetadataChain
|
|
597
|
+
expect(resolvedTypeMetadata?.typeMetadataChain).toHaveLength(2);
|
|
598
|
+
expect(resolvedTypeMetadata?.vctValues).toHaveLength(2);
|
|
160
599
|
});
|
|
161
600
|
|
|
162
|
-
|
|
601
|
+
test('VCT with invalid sd property change (always to never) should throw error', async () => {
|
|
602
|
+
const expectedPayload: SdJwtVcPayload = {
|
|
603
|
+
iat,
|
|
604
|
+
iss,
|
|
605
|
+
vct: 'http://example.com/invalid-sd-change',
|
|
606
|
+
...claims,
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
const encodedSdjwt = await sdjwt.issue(
|
|
610
|
+
expectedPayload,
|
|
611
|
+
disclosureFrame as unknown as DisclosureFrame<SdJwtVcPayload>,
|
|
612
|
+
);
|
|
613
|
+
|
|
614
|
+
await expect(sdjwt.getVct(encodedSdjwt)).rejects.toThrowError(
|
|
615
|
+
"Cannot change 'sd' property from 'always' to 'never' for claim at path [\"sensitiveData\"]",
|
|
616
|
+
);
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
test('VCT extending type without display should inherit base display', async () => {
|
|
620
|
+
const expectedPayload: SdJwtVcPayload = {
|
|
621
|
+
iat,
|
|
622
|
+
iss,
|
|
623
|
+
vct: 'http://example.com/middle', // middle doesn't define display
|
|
624
|
+
...claims,
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
const encodedSdjwt = await sdjwt.issue(
|
|
628
|
+
expectedPayload,
|
|
629
|
+
disclosureFrame as unknown as DisclosureFrame<SdJwtVcPayload>,
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
const resolvedTypeMetadata = await sdjwt.getVct(encodedSdjwt);
|
|
633
|
+
|
|
634
|
+
// Check mergedTypeMetadata - since middle doesn't define display, it should inherit from extending which has display
|
|
635
|
+
expect(resolvedTypeMetadata?.mergedTypeMetadata.display).toHaveLength(2);
|
|
636
|
+
expect(resolvedTypeMetadata?.mergedTypeMetadata.display?.[0].lang).toBe(
|
|
637
|
+
'en',
|
|
638
|
+
);
|
|
639
|
+
expect(resolvedTypeMetadata?.mergedTypeMetadata.display?.[1].lang).toBe(
|
|
640
|
+
'de',
|
|
641
|
+
);
|
|
642
|
+
|
|
643
|
+
// Check typeMetadataChain - should have 3 documents
|
|
644
|
+
expect(resolvedTypeMetadata?.typeMetadataChain).toHaveLength(3);
|
|
645
|
+
expect(resolvedTypeMetadata?.vctValues).toHaveLength(3);
|
|
646
|
+
});
|
|
163
647
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { kbHeader, kbPayload } from '@sd-jwt/types';
|
|
2
2
|
import type { SdJwtVcPayload } from './sd-jwt-vc-payload';
|
|
3
|
-
import type {
|
|
3
|
+
import type { ResolvedTypeMetadata } from './sd-jwt-vc-type-metadata-format';
|
|
4
4
|
|
|
5
5
|
export type VerificationResult = {
|
|
6
6
|
payload: SdJwtVcPayload;
|
|
@@ -11,5 +11,6 @@ export type VerificationResult = {
|
|
|
11
11
|
header: kbHeader;
|
|
12
12
|
}
|
|
13
13
|
| undefined;
|
|
14
|
-
|
|
14
|
+
|
|
15
|
+
typeMetadata?: ResolvedTypeMetadata;
|
|
15
16
|
};
|
package/test/app-e2e.spec.ts
CHANGED
|
@@ -145,7 +145,9 @@ describe('App', () => {
|
|
|
145
145
|
});
|
|
146
146
|
|
|
147
147
|
const requiredClaimKeys = ['firstname', 'id', 'data.ssn'];
|
|
148
|
-
const verified = await sdjwt.verify(encodedSdjwt,
|
|
148
|
+
const verified = await sdjwt.verify(encodedSdjwt, {
|
|
149
|
+
requiredClaimKeys,
|
|
150
|
+
});
|
|
149
151
|
expect(verified).toBeDefined();
|
|
150
152
|
});
|
|
151
153
|
|
|
@@ -239,7 +241,7 @@ async function JSONtest(filename: string) {
|
|
|
239
241
|
payload,
|
|
240
242
|
});
|
|
241
243
|
|
|
242
|
-
const presentedSDJwt = await sdjwt.present
|
|
244
|
+
const presentedSDJwt = await sdjwt.present(
|
|
243
245
|
encodedSdjwt,
|
|
244
246
|
test.presentationFrames,
|
|
245
247
|
);
|
|
@@ -249,13 +251,15 @@ async function JSONtest(filename: string) {
|
|
|
249
251
|
const presentationClaims = await sdjwt.getClaims(presentedSDJwt);
|
|
250
252
|
|
|
251
253
|
expect(presentationClaims).toEqual({
|
|
252
|
-
...test.
|
|
254
|
+
...test.presentedClaims,
|
|
253
255
|
iat,
|
|
254
256
|
iss,
|
|
255
257
|
vct,
|
|
256
258
|
});
|
|
257
259
|
|
|
258
|
-
const verified = await sdjwt.verify(encodedSdjwt,
|
|
260
|
+
const verified = await sdjwt.verify(encodedSdjwt, {
|
|
261
|
+
requiredClaimKeys: test.requiredClaimKeys,
|
|
262
|
+
});
|
|
259
263
|
|
|
260
264
|
expect(verified).toBeDefined();
|
|
261
265
|
expect(verified).toStrictEqual({
|
|
@@ -267,9 +271,11 @@ async function JSONtest(filename: string) {
|
|
|
267
271
|
|
|
268
272
|
type TestJson = {
|
|
269
273
|
claims: object;
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
274
|
+
// biome-ignore lint/complexity/noBannedTypes: we want an empty object in this case
|
|
275
|
+
disclosureFrame: DisclosureFrame<{}>;
|
|
276
|
+
// biome-ignore lint/complexity/noBannedTypes: we want an empty object in this case
|
|
277
|
+
presentationFrames: PresentationFrame<{}>;
|
|
278
|
+
presentedClaims: object;
|
|
273
279
|
requiredClaimKeys: string[];
|
|
274
280
|
};
|
|
275
281
|
|