@sd-jwt/sd-jwt-vc 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,286 @@
1
+ import Crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { hasher as digest, generateSalt } from '@owf/crypto';
5
+ import type {
6
+ DisclosureFrame,
7
+ PresentationFrame,
8
+ Signer,
9
+ Verifier,
10
+ } from '@sd-jwt/core';
11
+ import { describe, expect, test } from 'vitest';
12
+ import { SDJwtVcInstance } from '../src/index';
13
+
14
+ const createSignerVerifier = () => {
15
+ const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519');
16
+ const signer: Signer = async (data: string) => {
17
+ const sig = Crypto.sign(null, Buffer.from(data), privateKey);
18
+ return Buffer.from(sig).toString('base64url');
19
+ };
20
+ const verifier: Verifier = async (data: string, sig: string) => {
21
+ return Crypto.verify(
22
+ null,
23
+ Buffer.from(data),
24
+ publicKey,
25
+ Buffer.from(sig, 'base64url'),
26
+ );
27
+ };
28
+ return { signer, verifier };
29
+ };
30
+
31
+ const iss = 'ExampleIssuer';
32
+ const vct = 'ExampleCredentials';
33
+ const iat = Math.floor(Date.now() / 1000); // current time in seconds
34
+
35
+ describe('App', () => {
36
+ test('Example', async () => {
37
+ const { signer, verifier } = createSignerVerifier();
38
+ const sdjwt = new SDJwtVcInstance({
39
+ signer,
40
+ signAlg: 'EdDSA',
41
+ verifier,
42
+ hasher: digest,
43
+ hashAlg: 'sha-256',
44
+ saltGenerator: generateSalt,
45
+ });
46
+
47
+ const claims = {
48
+ firstname: 'John',
49
+ lastname: 'Doe',
50
+ ssn: '123-45-6789',
51
+ id: '1234',
52
+ data: {
53
+ firstname: 'John',
54
+ lastname: 'Doe',
55
+ ssn: '123-45-6789',
56
+ list: [{ r: '1' }, 'b', 'c'],
57
+ },
58
+ data2: {
59
+ hi: 'bye',
60
+ },
61
+ iat,
62
+ iss,
63
+ vct,
64
+ };
65
+ const disclosureFrame: DisclosureFrame<typeof claims> = {
66
+ _sd: ['firstname', 'id', 'data2'],
67
+ data: {
68
+ _sd: ['list'],
69
+ _sd_decoy: 2,
70
+ list: {
71
+ _sd: [0, 2],
72
+ _sd_decoy: 1,
73
+ 0: {
74
+ _sd: ['r'],
75
+ },
76
+ },
77
+ },
78
+ data2: {
79
+ _sd: ['hi'],
80
+ },
81
+ };
82
+
83
+ const encodedSdjwt = await sdjwt.issue(claims, disclosureFrame);
84
+ expect(encodedSdjwt).toBeDefined();
85
+ const validated = await sdjwt.validate(encodedSdjwt);
86
+ expect(validated).toBeDefined();
87
+
88
+ const decoded = await sdjwt.decode(encodedSdjwt);
89
+ const keys = await decoded.keys(digest);
90
+ expect(keys).toEqual([
91
+ 'data',
92
+ 'data.firstname',
93
+ 'data.lastname',
94
+ 'data.list',
95
+ 'data.list.0',
96
+ 'data.list.0.r',
97
+ 'data.list.1',
98
+ 'data.list.2',
99
+ 'data.ssn',
100
+ 'data2',
101
+ 'data2.hi',
102
+ 'firstname',
103
+ 'iat',
104
+ 'id',
105
+ 'iss',
106
+ 'lastname',
107
+ 'ssn',
108
+ 'vct',
109
+ ]);
110
+ const payloads = await decoded.getClaims(digest);
111
+ expect(payloads).toEqual(claims);
112
+ const presentableKeys = await decoded.presentableKeys(digest);
113
+ expect(presentableKeys).toEqual([
114
+ 'data.list',
115
+ 'data.list.0',
116
+ 'data.list.0.r',
117
+ 'data.list.2',
118
+ 'data2',
119
+ 'data2.hi',
120
+ 'firstname',
121
+ 'id',
122
+ ]);
123
+
124
+ const presentationFrame = {
125
+ firstname: true,
126
+ id: true,
127
+ };
128
+ const presentedSDJwt = await sdjwt.present<typeof claims>(
129
+ encodedSdjwt,
130
+ presentationFrame,
131
+ );
132
+ expect(presentedSDJwt).toBeDefined();
133
+
134
+ const presentationClaims = await sdjwt.getClaims(presentedSDJwt);
135
+ expect(presentationClaims).toBeDefined();
136
+ expect(presentationClaims).toEqual({
137
+ lastname: 'Doe',
138
+ ssn: '123-45-6789',
139
+ data: { firstname: 'John', lastname: 'Doe', ssn: '123-45-6789' },
140
+ id: '1234',
141
+ firstname: 'John',
142
+ iat,
143
+ iss,
144
+ vct,
145
+ });
146
+
147
+ const requiredClaimKeys = ['firstname', 'id', 'data.ssn'];
148
+ const verified = await sdjwt.verify(encodedSdjwt, {
149
+ requiredClaimKeys,
150
+ });
151
+ expect(verified).toBeDefined();
152
+ });
153
+
154
+ test('From JSON (complex)', async () => {
155
+ await JSONtest('./complex.json');
156
+ });
157
+
158
+ test('From JSON (array_data_types)', async () => {
159
+ await JSONtest('./array_data_types.json');
160
+ });
161
+
162
+ test('From JSON (array_full_sd)', async () => {
163
+ await JSONtest('./array_full_sd.json');
164
+ });
165
+
166
+ test('From JSON (array_in_sd)', async () => {
167
+ await JSONtest('./array_in_sd.json');
168
+ });
169
+
170
+ test('From JSON (array_recursive_sd_some_disclosed)', async () => {
171
+ await JSONtest('./array_recursive_sd_some_disclosed.json');
172
+ });
173
+
174
+ test('From JSON (header_mod)', async () => {
175
+ await JSONtest('./header_mod.json');
176
+ });
177
+
178
+ test('From JSON (json_serialization)', async () => {
179
+ await JSONtest('./json_serialization.json');
180
+ });
181
+
182
+ test('From JSON (key_binding)', async () => {
183
+ await JSONtest('./key_binding.json');
184
+ });
185
+
186
+ test('From JSON (no_sd)', async () => {
187
+ await JSONtest('./no_sd.json');
188
+ });
189
+
190
+ test('From JSON (object_data_types)', async () => {
191
+ await JSONtest('./object_data_types.json');
192
+ });
193
+
194
+ test('From JSON (recursions)', async () => {
195
+ await JSONtest('./recursions.json');
196
+ });
197
+
198
+ test('From JSON (array_recursive_sd)', async () => {
199
+ await JSONtest('./array_recursive_sd.json');
200
+ });
201
+
202
+ test('From JSON (array_of_scalars)', async () => {
203
+ await JSONtest('./array_of_scalars.json');
204
+ });
205
+
206
+ test('From JSON (array_of_objects)', async () => {
207
+ await JSONtest('./array_of_objects.json');
208
+ });
209
+
210
+ test('From JSON (array_of_nulls)', async () => {
211
+ await JSONtest('./array_of_nulls.json');
212
+ });
213
+
214
+ test('From JSON (array_nested_in_plain)', async () => {
215
+ await JSONtest('./array_nested_in_plain.json');
216
+ });
217
+ });
218
+
219
+ async function JSONtest(filename: string) {
220
+ const test = loadTestJsonFile(filename);
221
+ const { signer, verifier } = createSignerVerifier();
222
+ const sdjwt = new SDJwtVcInstance({
223
+ signer,
224
+ signAlg: 'EdDSA',
225
+ verifier,
226
+ hasher: digest,
227
+ hashAlg: 'sha-256',
228
+ saltGenerator: generateSalt,
229
+ });
230
+
231
+ const payload = { iss, vct, iat, ...test.claims };
232
+ const encodedSdjwt = await sdjwt.issue(payload, test.disclosureFrame);
233
+
234
+ expect(encodedSdjwt).toBeDefined();
235
+
236
+ const validated = await sdjwt.validate(encodedSdjwt);
237
+
238
+ expect(validated).toBeDefined();
239
+ expect(validated).toStrictEqual({
240
+ header: { alg: 'EdDSA', typ: 'dc+sd-jwt' },
241
+ payload,
242
+ });
243
+
244
+ const presentedSDJwt = await sdjwt.present(
245
+ encodedSdjwt,
246
+ test.presentationFrames,
247
+ );
248
+
249
+ expect(presentedSDJwt).toBeDefined();
250
+
251
+ const presentationClaims = await sdjwt.getClaims(presentedSDJwt);
252
+
253
+ expect(presentationClaims).toEqual({
254
+ ...test.presentedClaims,
255
+ iat,
256
+ iss,
257
+ vct,
258
+ });
259
+
260
+ const verified = await sdjwt.verify(encodedSdjwt, {
261
+ requiredClaimKeys: test.requiredClaimKeys,
262
+ });
263
+
264
+ expect(verified).toBeDefined();
265
+ expect(verified).toStrictEqual({
266
+ header: { alg: 'EdDSA', typ: 'dc+sd-jwt' },
267
+ kb: undefined,
268
+ payload,
269
+ });
270
+ }
271
+
272
+ type TestJson = {
273
+ claims: object;
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;
279
+ requiredClaimKeys: string[];
280
+ };
281
+
282
+ function loadTestJsonFile(filename: string) {
283
+ const filepath = path.join(__dirname, filename);
284
+ const fileContents = fs.readFileSync(filepath, 'utf8');
285
+ return JSON.parse(fileContents) as TestJson;
286
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "claims": {
3
+ "data_types": [null, 42, 3.14, "foo", ["Test"], { "foo": "bar" }]
4
+ },
5
+ "disclosureFrame": {
6
+ "data_types": {
7
+ "_sd": [0, 1, 2, 3, 4, 5]
8
+ }
9
+ },
10
+ "presentationFrames": {
11
+ "data_types": {
12
+ "0": true,
13
+ "1": true,
14
+ "2": true,
15
+ "3": true,
16
+ "4": true,
17
+ "5": true
18
+ }
19
+ },
20
+ "presentedClaims": {
21
+ "data_types": [null, 42, 3.14, "foo", ["Test"], { "foo": "bar" }]
22
+ },
23
+ "requiredClaimKeys": [
24
+ "data_types.0",
25
+ "data_types.1",
26
+ "data_types.2",
27
+ "data_types.3",
28
+ "data_types.4",
29
+ "data_types.5"
30
+ ]
31
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "claims": {
3
+ "is_over": {
4
+ "13": true,
5
+ "18": false,
6
+ "21": false
7
+ }
8
+ },
9
+ "disclosureFrame": {
10
+ "is_over": {
11
+ "_sd": ["13", "18", "21"]
12
+ }
13
+ },
14
+ "presentationFrames": { "is_over": { "18": true } },
15
+ "presentedClaims": {
16
+ "is_over": {
17
+ "18": false
18
+ }
19
+ },
20
+ "requiredClaimKeys": ["is_over.18"]
21
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "claims": {
3
+ "sd_array": ["32", "23"]
4
+ },
5
+ "disclosureFrame": {
6
+ "_sd": ["sd_array"]
7
+ },
8
+ "presentationFrames": { "sd_array": true },
9
+ "presentedClaims": {
10
+ "sd_array": ["32", "23"]
11
+ },
12
+ "requiredClaimKeys": ["sd_array"]
13
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "claims": {
3
+ "nested_array": [["foo", "bar"], ["baz", "qux"]]
4
+ },
5
+ "disclosureFrame": {
6
+ "nested_array": {
7
+ "0": {
8
+ "_sd": [0, 1]
9
+ },
10
+ "1": {
11
+ "_sd": [0, 1]
12
+ }
13
+ }
14
+ },
15
+ "presentationFrames": {
16
+ "nested_array": {
17
+ "0": {
18
+ "0": true
19
+ },
20
+ "1": {
21
+ "1": true
22
+ }
23
+ }
24
+ },
25
+ "presentedClaims": {
26
+ "nested_array": [["foo"], ["qux"]]
27
+ },
28
+ "requiredClaimKeys": ["nested_array.0.0", "nested_array.1.0"]
29
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "claims": {
3
+ "is_over": {
4
+ "13": false,
5
+ "18": true,
6
+ "21": false
7
+ }
8
+ },
9
+ "disclosureFrame": {
10
+ "is_over": {
11
+ "_sd": ["13", "18", "21"]
12
+ }
13
+ },
14
+ "presentationFrames": {},
15
+ "presentedClaims": {},
16
+ "requiredClaimKeys": []
17
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "claims": {
3
+ "null_values": [null, null, null, null]
4
+ },
5
+ "disclosureFrame": {
6
+ "null_values": {
7
+ "_sd": [1, 2]
8
+ }
9
+ },
10
+ "presentationFrames": {},
11
+ "presentedClaims": {
12
+ "null_values": [null, null]
13
+ },
14
+ "requiredClaimKeys": ["null_values.0", "null_values.1"]
15
+ }
@@ -0,0 +1,65 @@
1
+ {
2
+ "claims": {
3
+ "addresses": [
4
+ {
5
+ "street": "123 Main St",
6
+ "city": "Anytown",
7
+ "state": "NY",
8
+ "zip": "12345",
9
+ "type": "main_address"
10
+ },
11
+ {
12
+ "street": "456 Main St",
13
+ "city": "Anytown",
14
+ "state": "NY",
15
+ "zip": "12345",
16
+ "type": "secondary_address"
17
+ }
18
+ ],
19
+ "array_with_one_sd_object": {
20
+ "foo": "bar"
21
+ }
22
+ },
23
+ "disclosureFrame": {
24
+ "addresses": {
25
+ "_sd": [1]
26
+ },
27
+ "array_with_one_sd_object": {
28
+ "_sd": ["foo"]
29
+ }
30
+ },
31
+ "presentationFrames": {
32
+ "addresses": {
33
+ "1": true
34
+ },
35
+ "array_with_one_sd_object": {
36
+ "foo": true
37
+ }
38
+ },
39
+ "presentedClaims": {
40
+ "addresses": [
41
+ {
42
+ "street": "123 Main St",
43
+ "city": "Anytown",
44
+ "state": "NY",
45
+ "zip": "12345",
46
+ "type": "main_address"
47
+ },
48
+ {
49
+ "street": "456 Main St",
50
+ "city": "Anytown",
51
+ "state": "NY",
52
+ "zip": "12345",
53
+ "type": "secondary_address"
54
+ }
55
+ ],
56
+ "array_with_one_sd_object": {
57
+ "foo": "bar"
58
+ }
59
+ },
60
+ "requiredClaimKeys": [
61
+ "addresses.0.type",
62
+ "addresses.1.city",
63
+ "array_with_one_sd_object.foo"
64
+ ]
65
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "claims": {
3
+ "nationalities": ["US", "CA", "DE"]
4
+ },
5
+ "disclosureFrame": {
6
+ "nationalities": {
7
+ "_sd": [0, 1]
8
+ }
9
+ },
10
+ "presentationFrames": {
11
+ "nationalities": {
12
+ "1": true
13
+ }
14
+ },
15
+ "presentedClaims": {
16
+ "nationalities": ["CA", "DE"]
17
+ },
18
+ "requiredClaimKeys": ["nationalities.0", "nationalities.1"]
19
+ }
@@ -0,0 +1,35 @@
1
+ {
2
+ "claims": {
3
+ "array_with_recursive_sd": [
4
+ "boring",
5
+ {
6
+ "foo": "bar",
7
+ "baz": {
8
+ "qux": "quxx"
9
+ }
10
+ },
11
+ ["foo", "bar"]
12
+ ],
13
+ "test2": ["foo", "bar"]
14
+ },
15
+ "disclosureFrame": {
16
+ "array_with_recursive_sd": {
17
+ "_sd": [1],
18
+ "1": {
19
+ "_sd": ["baz"]
20
+ },
21
+ "2": {
22
+ "_sd": [0, 1]
23
+ }
24
+ },
25
+ "test2": {
26
+ "_sd": [0, 1]
27
+ }
28
+ },
29
+ "presentationFrames": {},
30
+ "presentedClaims": {
31
+ "array_with_recursive_sd": ["boring", []],
32
+ "test2": []
33
+ },
34
+ "requiredClaimKeys": ["array_with_recursive_sd.0", "test2"]
35
+ }
@@ -0,0 +1,63 @@
1
+ {
2
+ "claims": {
3
+ "array_with_recursive_sd": [
4
+ "boring",
5
+ {
6
+ "foo": "bar",
7
+ "baz": {
8
+ "qux": "quxx"
9
+ }
10
+ },
11
+ ["foo", "bar"]
12
+ ],
13
+ "test2": ["foo", "bar"]
14
+ },
15
+ "disclosureFrame": {
16
+ "array_with_recursive_sd": {
17
+ "1": {
18
+ "_sd": ["baz"]
19
+ },
20
+ "2": {
21
+ "_sd": [0, 1]
22
+ }
23
+ },
24
+ "test2": {
25
+ "_sd": [0, 1]
26
+ }
27
+ },
28
+ "presentationFrames": {
29
+ "array_with_recursive_sd": {
30
+ "1": {
31
+ "baz": true
32
+ },
33
+ "2": {
34
+ "1": true
35
+ }
36
+ },
37
+ "test2": {
38
+ "0": true,
39
+ "1": true
40
+ }
41
+ },
42
+ "presentedClaims": {
43
+ "array_with_recursive_sd": [
44
+ "boring",
45
+ {
46
+ "foo": "bar",
47
+ "baz": {
48
+ "qux": "quxx"
49
+ }
50
+ },
51
+ ["bar"]
52
+ ],
53
+ "test2": ["foo", "bar"]
54
+ },
55
+ "requiredClaimKeys": [
56
+ "array_with_recursive_sd.1",
57
+ "array_with_recursive_sd.2",
58
+ "array_with_recursive_sd.1.baz",
59
+ "array_with_recursive_sd.2.1",
60
+ "test2.0",
61
+ "test2.1"
62
+ ]
63
+ }
@@ -0,0 +1,43 @@
1
+ {
2
+ "claims": {
3
+ "firstname": "John",
4
+ "lastname": "Doe",
5
+ "ssn": "123-45-6789",
6
+ "id": "1234",
7
+ "data": {
8
+ "firstname": "John",
9
+ "lastname": "Doe",
10
+ "ssn": "123-45-6789",
11
+ "list": [{ "r": "1" }, "b", "c"]
12
+ },
13
+ "data2": {
14
+ "hi": "bye"
15
+ }
16
+ },
17
+ "disclosureFrame": {
18
+ "_sd": ["firstname", "id", "data2"],
19
+ "data": {
20
+ "_sd": ["list"],
21
+ "_sd_decoy": 2,
22
+ "list": {
23
+ "_sd": [0, 2],
24
+ "_sd_decoy": 1,
25
+ "0": {
26
+ "_sd": ["r"]
27
+ }
28
+ }
29
+ },
30
+ "data2": {
31
+ "_sd": ["hi"]
32
+ }
33
+ },
34
+ "presentationFrames": { "firstname": true, "id": true },
35
+ "presentedClaims": {
36
+ "lastname": "Doe",
37
+ "ssn": "123-45-6789",
38
+ "data": { "firstname": "John", "lastname": "Doe", "ssn": "123-45-6789" },
39
+ "id": "1234",
40
+ "firstname": "John"
41
+ },
42
+ "requiredClaimKeys": ["firstname", "id", "data.ssn"]
43
+ }
@@ -0,0 +1,48 @@
1
+ {
2
+ "claims": {
3
+ "sub": "john_deo_42",
4
+ "given_name": "John",
5
+ "family_name": "Deo",
6
+ "email": "johndeo@example.com",
7
+ "phone": "+1-202-555-0101",
8
+ "address": {
9
+ "street_address": "123 Main St",
10
+ "locality": "Anytown",
11
+ "region": "Anystate",
12
+ "country": "US"
13
+ },
14
+ "birthdate": "1940-01-01"
15
+ },
16
+ "disclosureFrame": {
17
+ "_sd": [
18
+ "sub",
19
+ "given_name",
20
+ "family_name",
21
+ "email",
22
+ "phone",
23
+ "address",
24
+ "birthdate"
25
+ ]
26
+ },
27
+ "presentationFrames": {
28
+ "given_name": true,
29
+ "family_name": true,
30
+ "address": true
31
+ },
32
+ "presentedClaims": {
33
+ "given_name": "John",
34
+ "family_name": "Deo",
35
+ "address": {
36
+ "street_address": "123 Main St",
37
+ "locality": "Anytown",
38
+ "region": "Anystate",
39
+ "country": "US"
40
+ }
41
+ },
42
+ "requiredClaimKeys": [
43
+ "given_name",
44
+ "family_name",
45
+ "address.street_address",
46
+ "address.country"
47
+ ]
48
+ }