@sd-jwt/core 0.3.0 → 2.0.4-next.58
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/LICENSE +201 -0
- package/dist/index.d.mts +106 -0
- package/dist/index.d.ts +106 -0
- package/dist/index.js +606 -0
- package/dist/index.mjs +586 -0
- package/package.json +60 -48
- package/src/decoy.ts +15 -0
- package/src/index.ts +235 -0
- package/src/jwt.ts +107 -0
- package/src/kbjwt.ts +45 -0
- package/src/sdjwt.ts +318 -0
- package/src/test/decoy.spec.ts +30 -0
- package/src/test/index.spec.ts +379 -0
- package/src/test/jwt.spec.ts +141 -0
- package/src/test/kbjwt.spec.ts +275 -0
- package/src/test/pass.spec.ts +6 -0
- package/src/test/sdjwt.spec.ts +382 -0
- package/test/app-e2e.spec.ts +248 -0
- package/test/array_data_types.json +29 -0
- package/test/array_full_sd.json +21 -0
- package/test/array_in_sd.json +13 -0
- package/test/array_nested_in_plain.json +20 -0
- package/test/array_none_disclosed.json +17 -0
- package/test/array_of_nulls.json +15 -0
- package/test/array_of_objects.json +58 -0
- package/test/array_of_scalars.json +15 -0
- package/test/array_recursive_sd.json +35 -0
- package/test/array_recursive_sd_some_disclosed.json +55 -0
- package/test/complex.json +43 -0
- package/test/header_mod.json +44 -0
- package/test/json_serialization.json +44 -0
- package/test/key_binding.json +44 -0
- package/test/no_sd.json +36 -0
- package/test/object_data_types.json +60 -0
- package/test/recursions.json +98 -0
- package/tsconfig.json +7 -0
- package/vitest.config.mts +4 -0
- package/README.md +0 -97
- package/build/index.d.ts +0 -13
- package/build/index.js +0 -20
- package/build/index.js.map +0 -1
- package/build/jwt/error.d.ts +0 -2
- package/build/jwt/error.js +0 -7
- package/build/jwt/error.js.map +0 -1
- package/build/jwt/index.d.ts +0 -2
- package/build/jwt/index.js +0 -19
- package/build/jwt/index.js.map +0 -1
- package/build/jwt/jwt.d.ts +0 -208
- package/build/jwt/jwt.js +0 -325
- package/build/jwt/jwt.js.map +0 -1
- package/build/keyBinding/index.d.ts +0 -1
- package/build/keyBinding/index.js +0 -18
- package/build/keyBinding/index.js.map +0 -1
- package/build/keyBinding/keyBinding.d.ts +0 -64
- package/build/keyBinding/keyBinding.js +0 -119
- package/build/keyBinding/keyBinding.js.map +0 -1
- package/build/sdJwt/decoys.d.ts +0 -3
- package/build/sdJwt/decoys.js +0 -35
- package/build/sdJwt/decoys.js.map +0 -1
- package/build/sdJwt/disclosureFrame.d.ts +0 -8
- package/build/sdJwt/disclosureFrame.js +0 -87
- package/build/sdJwt/disclosureFrame.js.map +0 -1
- package/build/sdJwt/disclosures.d.ts +0 -33
- package/build/sdJwt/disclosures.js +0 -114
- package/build/sdJwt/disclosures.js.map +0 -1
- package/build/sdJwt/error.d.ts +0 -2
- package/build/sdJwt/error.js +0 -7
- package/build/sdJwt/error.js.map +0 -1
- package/build/sdJwt/index.d.ts +0 -6
- package/build/sdJwt/index.js +0 -23
- package/build/sdJwt/index.js.map +0 -1
- package/build/sdJwt/sdJwt.d.ts +0 -206
- package/build/sdJwt/sdJwt.js +0 -442
- package/build/sdJwt/sdJwt.js.map +0 -1
- package/build/sdJwt/types.d.ts +0 -5
- package/build/sdJwt/types.js +0 -3
- package/build/sdJwt/types.js.map +0 -1
- package/build/sdJwtVc/error.d.ts +0 -2
- package/build/sdJwtVc/error.js +0 -7
- package/build/sdJwtVc/error.js.map +0 -1
- package/build/sdJwtVc/index.d.ts +0 -2
- package/build/sdJwtVc/index.js +0 -19
- package/build/sdJwtVc/index.js.map +0 -1
- package/build/sdJwtVc/sdJwtVc.d.ts +0 -47
- package/build/sdJwtVc/sdJwtVc.js +0 -149
- package/build/sdJwtVc/sdJwtVc.js.map +0 -1
- package/build/signatureAndEncryptionAlgorithm.d.ts +0 -105
- package/build/signatureAndEncryptionAlgorithm.js +0 -110
- package/build/signatureAndEncryptionAlgorithm.js.map +0 -1
- package/build/types/disclosure.d.ts +0 -5
- package/build/types/disclosure.js +0 -3
- package/build/types/disclosure.js.map +0 -1
- package/build/types/index.d.ts +0 -5
- package/build/types/index.js +0 -22
- package/build/types/index.js.map +0 -1
- package/build/types/saltGenerator.d.ts +0 -17
- package/build/types/saltGenerator.js +0 -3
- package/build/types/saltGenerator.js.map +0 -1
- package/build/types/signer.d.ts +0 -2
- package/build/types/signer.js +0 -3
- package/build/types/signer.js.map +0 -1
- package/build/types/utils.d.ts +0 -2
- package/build/types/utils.js +0 -3
- package/build/types/utils.js.map +0 -1
- package/build/types/verifier.d.ts +0 -14
- package/build/types/verifier.js +0 -3
- package/build/types/verifier.js.map +0 -1
package/src/sdjwt.ts
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { createDecoy } from './decoy';
|
|
2
|
+
import { SDJWTException, Disclosure } from '@sd-jwt/utils';
|
|
3
|
+
import { Jwt } from './jwt';
|
|
4
|
+
import { KBJwt } from './kbjwt';
|
|
5
|
+
import {
|
|
6
|
+
DisclosureFrame,
|
|
7
|
+
Hasher,
|
|
8
|
+
HasherAndAlg,
|
|
9
|
+
SDJWTCompact,
|
|
10
|
+
SD_DECOY,
|
|
11
|
+
SD_DIGEST,
|
|
12
|
+
SD_LIST_KEY,
|
|
13
|
+
SD_SEPARATOR,
|
|
14
|
+
SaltGenerator,
|
|
15
|
+
kbHeader,
|
|
16
|
+
kbPayload,
|
|
17
|
+
} from '@sd-jwt/types';
|
|
18
|
+
import { createHashMapping, getSDAlgAndPayload, unpack } from '@sd-jwt/decode';
|
|
19
|
+
|
|
20
|
+
export type SDJwtData<
|
|
21
|
+
Header extends Record<string, unknown>,
|
|
22
|
+
Payload extends Record<string, unknown>,
|
|
23
|
+
KBHeader extends kbHeader = kbHeader,
|
|
24
|
+
KBPayload extends kbPayload = kbPayload,
|
|
25
|
+
> = {
|
|
26
|
+
jwt?: Jwt<Header, Payload>;
|
|
27
|
+
disclosures?: Array<Disclosure>;
|
|
28
|
+
kbJwt?: KBJwt<KBHeader, KBPayload>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export class SDJwt<
|
|
32
|
+
Header extends Record<string, unknown> = Record<string, unknown>,
|
|
33
|
+
Payload extends Record<string, unknown> = Record<string, unknown>,
|
|
34
|
+
KBHeader extends kbHeader = kbHeader,
|
|
35
|
+
KBPayload extends kbPayload = kbPayload,
|
|
36
|
+
> {
|
|
37
|
+
public jwt?: Jwt<Header, Payload>;
|
|
38
|
+
public disclosures?: Array<Disclosure>;
|
|
39
|
+
public kbJwt?: KBJwt<KBHeader, KBPayload>;
|
|
40
|
+
|
|
41
|
+
constructor(data?: SDJwtData<Header, Payload, KBHeader, KBPayload>) {
|
|
42
|
+
this.jwt = data?.jwt;
|
|
43
|
+
this.disclosures = data?.disclosures;
|
|
44
|
+
this.kbJwt = data?.kbJwt;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public static async decodeSDJwt<
|
|
48
|
+
Header extends Record<string, unknown> = Record<string, unknown>,
|
|
49
|
+
Payload extends Record<string, unknown> = Record<string, unknown>,
|
|
50
|
+
KBHeader extends kbHeader = kbHeader,
|
|
51
|
+
KBPayload extends kbPayload = kbPayload,
|
|
52
|
+
>(
|
|
53
|
+
sdjwt: SDJWTCompact,
|
|
54
|
+
hasher: Hasher,
|
|
55
|
+
): Promise<{
|
|
56
|
+
jwt: Jwt<Header, Payload>;
|
|
57
|
+
disclosures: Array<Disclosure>;
|
|
58
|
+
kbJwt?: KBJwt<KBHeader, KBPayload>;
|
|
59
|
+
}> {
|
|
60
|
+
const [encodedJwt, ...encodedDisclosures] = sdjwt.split(SD_SEPARATOR);
|
|
61
|
+
const jwt = Jwt.fromEncode<Header, Payload>(encodedJwt);
|
|
62
|
+
|
|
63
|
+
if (!jwt.payload) {
|
|
64
|
+
throw new Error('Payload is undefined on the JWT. Invalid state reached');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (encodedDisclosures.length === 0) {
|
|
68
|
+
return {
|
|
69
|
+
jwt,
|
|
70
|
+
disclosures: [],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const encodedKeyBindingJwt = encodedDisclosures.pop();
|
|
75
|
+
const kbJwt = encodedKeyBindingJwt
|
|
76
|
+
? KBJwt.fromKBEncode<KBHeader, KBPayload>(encodedKeyBindingJwt)
|
|
77
|
+
: undefined;
|
|
78
|
+
|
|
79
|
+
const { _sd_alg } = getSDAlgAndPayload(jwt.payload);
|
|
80
|
+
|
|
81
|
+
const disclosures = await Promise.all(
|
|
82
|
+
(encodedDisclosures as Array<string>).map((ed) =>
|
|
83
|
+
Disclosure.fromEncode(ed, { alg: _sd_alg, hasher }),
|
|
84
|
+
),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
jwt,
|
|
89
|
+
disclosures,
|
|
90
|
+
kbJwt,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
public static async fromEncode<
|
|
95
|
+
Header extends Record<string, unknown> = Record<string, unknown>,
|
|
96
|
+
Payload extends Record<string, unknown> = Record<string, unknown>,
|
|
97
|
+
KBHeader extends kbHeader = kbHeader,
|
|
98
|
+
KBPayload extends kbPayload = kbPayload,
|
|
99
|
+
>(
|
|
100
|
+
encodedSdJwt: SDJWTCompact,
|
|
101
|
+
hasher: Hasher,
|
|
102
|
+
): Promise<SDJwt<Header, Payload>> {
|
|
103
|
+
const { jwt, disclosures, kbJwt } = await SDJwt.decodeSDJwt<
|
|
104
|
+
Header,
|
|
105
|
+
Payload,
|
|
106
|
+
KBHeader,
|
|
107
|
+
KBPayload
|
|
108
|
+
>(encodedSdJwt, hasher);
|
|
109
|
+
|
|
110
|
+
return new SDJwt<Header, Payload, KBHeader, KBPayload>({
|
|
111
|
+
jwt,
|
|
112
|
+
disclosures,
|
|
113
|
+
kbJwt,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public async present(keys: string[], hasher: Hasher): Promise<SDJWTCompact> {
|
|
118
|
+
if (!this.jwt?.payload || !this.disclosures) {
|
|
119
|
+
throw new SDJWTException('Invalid sd-jwt: jwt or disclosures is missing');
|
|
120
|
+
}
|
|
121
|
+
const { _sd_alg: alg } = getSDAlgAndPayload(this.jwt.payload);
|
|
122
|
+
const hash = { alg, hasher };
|
|
123
|
+
const hashmap = await createHashMapping(this.disclosures, hash);
|
|
124
|
+
const { disclosureKeymap } = await unpack(
|
|
125
|
+
this.jwt.payload,
|
|
126
|
+
this.disclosures,
|
|
127
|
+
hasher,
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const presentableKeys = Object.keys(disclosureKeymap);
|
|
131
|
+
const missingKeys = keys.filter((k) => !presentableKeys.includes(k));
|
|
132
|
+
if (missingKeys.length > 0) {
|
|
133
|
+
throw new SDJWTException(
|
|
134
|
+
`Invalid sd-jwt: invalid present keys: ${missingKeys.join(', ')}`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const disclosures = keys.map((k) => hashmap[disclosureKeymap[k]]);
|
|
139
|
+
const presentSDJwt = new SDJwt({
|
|
140
|
+
jwt: this.jwt,
|
|
141
|
+
disclosures,
|
|
142
|
+
kbJwt: this.kbJwt,
|
|
143
|
+
});
|
|
144
|
+
return presentSDJwt.encodeSDJwt();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
public encodeSDJwt(): SDJWTCompact {
|
|
148
|
+
const data: string[] = [];
|
|
149
|
+
|
|
150
|
+
if (!this.jwt) {
|
|
151
|
+
throw new SDJWTException('Invalid sd-jwt: jwt is missing');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const encodedJwt = this.jwt.encodeJwt();
|
|
155
|
+
data.push(encodedJwt);
|
|
156
|
+
|
|
157
|
+
if (this.disclosures && this.disclosures.length > 0) {
|
|
158
|
+
const encodeddisclosures = this.disclosures
|
|
159
|
+
.map((dc) => dc.encode())
|
|
160
|
+
.join(SD_SEPARATOR);
|
|
161
|
+
data.push(encodeddisclosures);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
data.push(this.kbJwt ? this.kbJwt.encodeJwt() : '');
|
|
165
|
+
return data.join(SD_SEPARATOR);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
public async keys(hasher: Hasher): Promise<string[]> {
|
|
169
|
+
return listKeys(await this.getClaims(hasher)).sort();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
public async presentableKeys(hasher: Hasher): Promise<string[]> {
|
|
173
|
+
if (!this.jwt?.payload || !this.disclosures) {
|
|
174
|
+
throw new SDJWTException('Invalid sd-jwt: jwt or disclosures is missing');
|
|
175
|
+
}
|
|
176
|
+
const { disclosureKeymap } = await unpack(
|
|
177
|
+
this.jwt?.payload,
|
|
178
|
+
this.disclosures,
|
|
179
|
+
hasher,
|
|
180
|
+
);
|
|
181
|
+
return Object.keys(disclosureKeymap).sort();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
public async getClaims<T>(hasher: Hasher): Promise<T> {
|
|
185
|
+
if (!this.jwt?.payload || !this.disclosures) {
|
|
186
|
+
throw new SDJWTException('Invalid sd-jwt: jwt or disclosures is missing');
|
|
187
|
+
}
|
|
188
|
+
const { unpackedObj } = await unpack(
|
|
189
|
+
this.jwt.payload,
|
|
190
|
+
this.disclosures,
|
|
191
|
+
hasher,
|
|
192
|
+
);
|
|
193
|
+
return unpackedObj as T;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export const listKeys = (obj: Record<string, unknown>, prefix = '') => {
|
|
198
|
+
const keys: string[] = [];
|
|
199
|
+
for (const key in obj) {
|
|
200
|
+
if (obj[key] === undefined) continue;
|
|
201
|
+
const newKey = prefix ? `${prefix}.${key}` : key;
|
|
202
|
+
keys.push(newKey);
|
|
203
|
+
|
|
204
|
+
if (obj[key] && typeof obj[key] === 'object' && obj[key] !== null) {
|
|
205
|
+
keys.push(...listKeys(obj[key] as Record<string, unknown>, newKey));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return keys;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export const pack = async <T extends Record<string, unknown>>(
|
|
212
|
+
claims: T,
|
|
213
|
+
disclosureFrame: DisclosureFrame<T> | undefined,
|
|
214
|
+
hash: HasherAndAlg,
|
|
215
|
+
saltGenerator: SaltGenerator,
|
|
216
|
+
): Promise<{
|
|
217
|
+
packedClaims: Record<string, unknown> | Array<Record<string, unknown>>;
|
|
218
|
+
disclosures: Array<Disclosure>;
|
|
219
|
+
}> => {
|
|
220
|
+
if (!disclosureFrame) {
|
|
221
|
+
return {
|
|
222
|
+
packedClaims: claims,
|
|
223
|
+
disclosures: [],
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const sd = disclosureFrame[SD_DIGEST] ?? [];
|
|
228
|
+
const decoyCount = disclosureFrame[SD_DECOY] ?? 0;
|
|
229
|
+
|
|
230
|
+
if (Array.isArray(claims)) {
|
|
231
|
+
const packedClaims: Array<Record<typeof SD_LIST_KEY, string>> = [];
|
|
232
|
+
const disclosures: Array<Disclosure> = [];
|
|
233
|
+
const recursivePackedClaims: Record<number, unknown> = {};
|
|
234
|
+
|
|
235
|
+
for (const key in disclosureFrame) {
|
|
236
|
+
if (key !== SD_DIGEST) {
|
|
237
|
+
const idx = parseInt(key);
|
|
238
|
+
const packed = await pack(
|
|
239
|
+
claims[idx],
|
|
240
|
+
disclosureFrame[idx],
|
|
241
|
+
hash,
|
|
242
|
+
saltGenerator,
|
|
243
|
+
);
|
|
244
|
+
recursivePackedClaims[idx] = packed.packedClaims;
|
|
245
|
+
disclosures.push(...packed.disclosures);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
for (let i = 0; i < claims.length; i++) {
|
|
250
|
+
const claim = recursivePackedClaims[i]
|
|
251
|
+
? recursivePackedClaims[i]
|
|
252
|
+
: claims[i];
|
|
253
|
+
// TODO: should this actually be the `i` or `claim`?
|
|
254
|
+
// @ts-ignore
|
|
255
|
+
if (sd.includes(i)) {
|
|
256
|
+
const salt = await saltGenerator(16);
|
|
257
|
+
const disclosure = new Disclosure([salt, claim]);
|
|
258
|
+
const digest = await disclosure.digest(hash);
|
|
259
|
+
packedClaims.push({ [SD_LIST_KEY]: digest });
|
|
260
|
+
disclosures.push(disclosure);
|
|
261
|
+
} else {
|
|
262
|
+
packedClaims.push(claim);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
for (let j = 0; j < decoyCount; j++) {
|
|
266
|
+
const decoyDigest = await createDecoy(hash, saltGenerator);
|
|
267
|
+
packedClaims.push({ [SD_LIST_KEY]: decoyDigest });
|
|
268
|
+
}
|
|
269
|
+
return { packedClaims, disclosures };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const packedClaims: Record<string, unknown> = {};
|
|
273
|
+
const disclosures: Array<Disclosure> = [];
|
|
274
|
+
const recursivePackedClaims: Record<string, unknown> = {};
|
|
275
|
+
|
|
276
|
+
for (const key in disclosureFrame) {
|
|
277
|
+
if (key !== SD_DIGEST) {
|
|
278
|
+
const packed = await pack(
|
|
279
|
+
// @ts-ignore
|
|
280
|
+
claims[key],
|
|
281
|
+
disclosureFrame[key],
|
|
282
|
+
hash,
|
|
283
|
+
saltGenerator,
|
|
284
|
+
);
|
|
285
|
+
recursivePackedClaims[key] = packed.packedClaims;
|
|
286
|
+
disclosures.push(...packed.disclosures);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const _sd: string[] = [];
|
|
291
|
+
|
|
292
|
+
for (const key in claims) {
|
|
293
|
+
const claim = recursivePackedClaims[key]
|
|
294
|
+
? recursivePackedClaims[key]
|
|
295
|
+
: claims[key];
|
|
296
|
+
// @ts-ignore
|
|
297
|
+
if (sd.includes(key)) {
|
|
298
|
+
const salt = await saltGenerator(16);
|
|
299
|
+
const disclosure = new Disclosure([salt, key, claim]);
|
|
300
|
+
const digest = await disclosure.digest(hash);
|
|
301
|
+
|
|
302
|
+
_sd.push(digest);
|
|
303
|
+
disclosures.push(disclosure);
|
|
304
|
+
} else {
|
|
305
|
+
packedClaims[key] = claim;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
for (let j = 0; j < decoyCount; j++) {
|
|
310
|
+
const decoyDigest = await createDecoy(hash, saltGenerator);
|
|
311
|
+
_sd.push(decoyDigest);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (_sd.length > 0) {
|
|
315
|
+
packedClaims[SD_DIGEST] = _sd.sort();
|
|
316
|
+
}
|
|
317
|
+
return { packedClaims, disclosures };
|
|
318
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createDecoy } from '../decoy';
|
|
2
|
+
import { describe, expect, test } from 'vitest';
|
|
3
|
+
import { Base64urlEncode } from '@sd-jwt/utils';
|
|
4
|
+
import { digest, generateSalt } from '@sd-jwt/crypto-nodejs';
|
|
5
|
+
|
|
6
|
+
const hash = {
|
|
7
|
+
hasher: digest,
|
|
8
|
+
alg: 'SHA256',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
describe('Decoy', () => {
|
|
12
|
+
test('decoy', async () => {
|
|
13
|
+
const decoyValue = await createDecoy(hash, generateSalt);
|
|
14
|
+
expect(decoyValue.length).toBe(43);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// ref https://datatracker.ietf.org/doc/draft-ietf-oauth-selective-disclosure-jwt/07/
|
|
18
|
+
// *Claim email*:
|
|
19
|
+
// * SHA-256 Hash: JzYjH4svliH0R3PyEMfeZu6Jt69u5qehZo7F7EPYlSE
|
|
20
|
+
// * Disclosure: WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgImVtYWlsIiwgImpvaG5kb2VAZXhhbXBsZS5jb20iXQ
|
|
21
|
+
// * Contents: ["6Ij7tM-a5iVPGboS5tmvVA", "email", "johndoe@example.com"]
|
|
22
|
+
test('apply hasher and saltGenerator', async () => {
|
|
23
|
+
const decoyValue = await createDecoy(hash, () =>
|
|
24
|
+
Base64urlEncode(
|
|
25
|
+
'["6Ij7tM-a5iVPGboS5tmvVA", "email", "johndoe@example.com"]',
|
|
26
|
+
),
|
|
27
|
+
);
|
|
28
|
+
expect(decoyValue).toBe('JzYjH4svliH0R3PyEMfeZu6Jt69u5qehZo7F7EPYlSE');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { SDJwtInstance } from '../index';
|
|
2
|
+
import { Signer, Verifier } from '@sd-jwt/types';
|
|
3
|
+
import Crypto from 'node:crypto';
|
|
4
|
+
import { describe, expect, test } from 'vitest';
|
|
5
|
+
import { digest, generateSalt } from '@sd-jwt/crypto-nodejs';
|
|
6
|
+
|
|
7
|
+
export const createSignerVerifier = () => {
|
|
8
|
+
const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519');
|
|
9
|
+
const signer: Signer = async (data: string) => {
|
|
10
|
+
const sig = Crypto.sign(null, Buffer.from(data), privateKey);
|
|
11
|
+
return Buffer.from(sig).toString('base64url');
|
|
12
|
+
};
|
|
13
|
+
const verifier: Verifier = async (data: string, sig: string) => {
|
|
14
|
+
return Crypto.verify(
|
|
15
|
+
null,
|
|
16
|
+
Buffer.from(data),
|
|
17
|
+
publicKey,
|
|
18
|
+
Buffer.from(sig, 'base64url'),
|
|
19
|
+
);
|
|
20
|
+
};
|
|
21
|
+
return { signer, verifier };
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
describe('index', () => {
|
|
25
|
+
test('create', async () => {
|
|
26
|
+
const sdjwt = new SDJwtInstance();
|
|
27
|
+
expect(sdjwt).toBeDefined();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('kbJwt', async () => {
|
|
31
|
+
const { signer, verifier } = createSignerVerifier();
|
|
32
|
+
const sdjwt = new SDJwtInstance({
|
|
33
|
+
signer,
|
|
34
|
+
signAlg: 'EdDSA',
|
|
35
|
+
verifier,
|
|
36
|
+
hasher: digest,
|
|
37
|
+
saltGenerator: generateSalt,
|
|
38
|
+
kbSigner: signer,
|
|
39
|
+
kbSignAlg: 'EdDSA',
|
|
40
|
+
});
|
|
41
|
+
const credential = await sdjwt.issue(
|
|
42
|
+
{
|
|
43
|
+
foo: 'bar',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
_sd: ['foo'],
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
expect(credential).toBeDefined();
|
|
51
|
+
|
|
52
|
+
const presentation = await sdjwt.present(credential, ['foo'], {
|
|
53
|
+
kb: {
|
|
54
|
+
payload: {
|
|
55
|
+
sd_hash: 'sha-256',
|
|
56
|
+
aud: '1',
|
|
57
|
+
iat: 1,
|
|
58
|
+
nonce: '342',
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(presentation).toBeDefined();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('issue', async () => {
|
|
67
|
+
const { signer, verifier } = createSignerVerifier();
|
|
68
|
+
const sdjwt = new SDJwtInstance({
|
|
69
|
+
signer,
|
|
70
|
+
signAlg: 'EdDSA',
|
|
71
|
+
verifier,
|
|
72
|
+
hasher: digest,
|
|
73
|
+
saltGenerator: generateSalt,
|
|
74
|
+
});
|
|
75
|
+
const credential = await sdjwt.issue(
|
|
76
|
+
{
|
|
77
|
+
foo: 'bar',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
_sd: ['foo'],
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
expect(credential).toBeDefined();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('verify failed', async () => {
|
|
88
|
+
const { signer } = createSignerVerifier();
|
|
89
|
+
const { publicKey } = Crypto.generateKeyPairSync('ed25519');
|
|
90
|
+
const failedverifier: Verifier = async (data: string, sig: string) => {
|
|
91
|
+
return Crypto.verify(
|
|
92
|
+
null,
|
|
93
|
+
Buffer.from(data),
|
|
94
|
+
publicKey,
|
|
95
|
+
Buffer.from(sig, 'base64url'),
|
|
96
|
+
);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const sdjwt = new SDJwtInstance({
|
|
100
|
+
signer,
|
|
101
|
+
signAlg: 'EdDSA',
|
|
102
|
+
verifier: failedverifier,
|
|
103
|
+
hasher: digest,
|
|
104
|
+
saltGenerator: generateSalt,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const credential = await sdjwt.issue(
|
|
108
|
+
{
|
|
109
|
+
foo: 'bar',
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
_sd: ['foo'],
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
await sdjwt.verify(credential);
|
|
118
|
+
} catch (e) {
|
|
119
|
+
expect(e).toBeDefined();
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('verify failed with kbJwt', async () => {
|
|
124
|
+
const { signer, verifier } = createSignerVerifier();
|
|
125
|
+
const { publicKey } = Crypto.generateKeyPairSync('ed25519');
|
|
126
|
+
const failedverifier: Verifier = async (data: string, sig: string) => {
|
|
127
|
+
return Crypto.verify(
|
|
128
|
+
null,
|
|
129
|
+
Buffer.from(data),
|
|
130
|
+
publicKey,
|
|
131
|
+
Buffer.from(sig, 'base64url'),
|
|
132
|
+
);
|
|
133
|
+
};
|
|
134
|
+
const sdjwt = new SDJwtInstance({
|
|
135
|
+
signer,
|
|
136
|
+
signAlg: 'EdDSA',
|
|
137
|
+
verifier,
|
|
138
|
+
hasher: digest,
|
|
139
|
+
saltGenerator: generateSalt,
|
|
140
|
+
kbSigner: signer,
|
|
141
|
+
kbVerifier: failedverifier,
|
|
142
|
+
kbSignAlg: 'EdDSA',
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const credential = await sdjwt.issue(
|
|
146
|
+
{
|
|
147
|
+
foo: 'bar',
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
_sd: ['foo'],
|
|
151
|
+
},
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const presentation = await sdjwt.present(credential, ['foo'], {
|
|
155
|
+
kb: {
|
|
156
|
+
payload: {
|
|
157
|
+
sd_hash: '',
|
|
158
|
+
aud: '1',
|
|
159
|
+
iat: 1,
|
|
160
|
+
nonce: '342',
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
await sdjwt.verify(presentation);
|
|
167
|
+
} catch (e) {
|
|
168
|
+
expect(e).toBeDefined();
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('verify with kbJwt', async () => {
|
|
173
|
+
const { signer, verifier } = createSignerVerifier();
|
|
174
|
+
const sdjwt = new SDJwtInstance({
|
|
175
|
+
signer,
|
|
176
|
+
signAlg: 'EdDSA',
|
|
177
|
+
verifier,
|
|
178
|
+
hasher: digest,
|
|
179
|
+
saltGenerator: generateSalt,
|
|
180
|
+
kbSigner: signer,
|
|
181
|
+
kbVerifier: verifier,
|
|
182
|
+
kbSignAlg: 'EdDSA',
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const credential = await sdjwt.issue(
|
|
186
|
+
{
|
|
187
|
+
foo: 'bar',
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
_sd: ['foo'],
|
|
191
|
+
},
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
const presentation = await sdjwt.present(credential, ['foo'], {
|
|
195
|
+
kb: {
|
|
196
|
+
payload: {
|
|
197
|
+
sd_hash: 'sha-256',
|
|
198
|
+
aud: '1',
|
|
199
|
+
iat: 1,
|
|
200
|
+
nonce: '342',
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const results = await sdjwt.verify(presentation, ['foo'], true);
|
|
206
|
+
expect(results).toBeDefined();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('Hasher not found', async () => {
|
|
210
|
+
const sdjwt = new SDJwtInstance({});
|
|
211
|
+
try {
|
|
212
|
+
const credential = await sdjwt.issue(
|
|
213
|
+
{
|
|
214
|
+
foo: 'bar',
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
_sd: ['foo'],
|
|
218
|
+
},
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
expect(credential).toBeDefined();
|
|
222
|
+
} catch (e) {
|
|
223
|
+
expect(e).toBeDefined();
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('SaltGenerator not found', async () => {
|
|
228
|
+
const sdjwt = new SDJwtInstance({
|
|
229
|
+
hasher: digest,
|
|
230
|
+
});
|
|
231
|
+
try {
|
|
232
|
+
const credential = await sdjwt.issue(
|
|
233
|
+
{
|
|
234
|
+
foo: 'bar',
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
_sd: ['foo'],
|
|
238
|
+
},
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
expect(credential).toBeDefined();
|
|
242
|
+
} catch (e) {
|
|
243
|
+
expect(e).toBeDefined();
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test('Signer not found', async () => {
|
|
248
|
+
const sdjwt = new SDJwtInstance({
|
|
249
|
+
hasher: digest,
|
|
250
|
+
saltGenerator: generateSalt,
|
|
251
|
+
});
|
|
252
|
+
try {
|
|
253
|
+
const credential = await sdjwt.issue(
|
|
254
|
+
{
|
|
255
|
+
foo: 'bar',
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
_sd: ['foo'],
|
|
259
|
+
},
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
expect(credential).toBeDefined();
|
|
263
|
+
} catch (e) {
|
|
264
|
+
expect(e).toBeDefined();
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test('Verifier not found', async () => {
|
|
269
|
+
const { signer, verifier } = createSignerVerifier();
|
|
270
|
+
const sdjwt = new SDJwtInstance({
|
|
271
|
+
signer,
|
|
272
|
+
hasher: digest,
|
|
273
|
+
saltGenerator: generateSalt,
|
|
274
|
+
kbSigner: signer,
|
|
275
|
+
kbVerifier: verifier,
|
|
276
|
+
signAlg: 'EdDSA',
|
|
277
|
+
kbSignAlg: 'EdDSA',
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const credential = await sdjwt.issue(
|
|
281
|
+
{
|
|
282
|
+
foo: 'bar',
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
_sd: ['foo'],
|
|
286
|
+
},
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
const presentation = await sdjwt.present(credential, ['foo'], {
|
|
290
|
+
kb: {
|
|
291
|
+
payload: {
|
|
292
|
+
sd_hash: 'sha-256',
|
|
293
|
+
aud: '1',
|
|
294
|
+
iat: 1,
|
|
295
|
+
nonce: '342',
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
try {
|
|
300
|
+
const results = await sdjwt.verify(presentation, ['foo'], true);
|
|
301
|
+
} catch (e) {
|
|
302
|
+
expect(e).toBeDefined();
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test('kbSigner not found', async () => {
|
|
307
|
+
const { signer, verifier } = createSignerVerifier();
|
|
308
|
+
const sdjwt = new SDJwtInstance({
|
|
309
|
+
signer,
|
|
310
|
+
verifier,
|
|
311
|
+
hasher: digest,
|
|
312
|
+
saltGenerator: generateSalt,
|
|
313
|
+
kbVerifier: verifier,
|
|
314
|
+
signAlg: 'EdDSA',
|
|
315
|
+
kbSignAlg: 'EdDSA',
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const credential = await sdjwt.issue(
|
|
319
|
+
{
|
|
320
|
+
foo: 'bar',
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
_sd: ['foo'],
|
|
324
|
+
},
|
|
325
|
+
);
|
|
326
|
+
try {
|
|
327
|
+
const presentation = await sdjwt.present(credential, ['foo'], {
|
|
328
|
+
kb: {
|
|
329
|
+
payload: {
|
|
330
|
+
sd_hash: 'sha-256',
|
|
331
|
+
aud: '1',
|
|
332
|
+
iat: 1,
|
|
333
|
+
nonce: '342',
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
} catch (e) {
|
|
338
|
+
expect(e).toBeDefined();
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test('kbVerifier not found', async () => {
|
|
343
|
+
const { signer, verifier } = createSignerVerifier();
|
|
344
|
+
const sdjwt = new SDJwtInstance({
|
|
345
|
+
signer,
|
|
346
|
+
verifier,
|
|
347
|
+
hasher: digest,
|
|
348
|
+
saltGenerator: generateSalt,
|
|
349
|
+
kbSigner: signer,
|
|
350
|
+
signAlg: 'EdDSA',
|
|
351
|
+
kbSignAlg: 'EdDSA',
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
const credential = await sdjwt.issue(
|
|
355
|
+
{
|
|
356
|
+
foo: 'bar',
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
_sd: ['foo'],
|
|
360
|
+
},
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
const presentation = await sdjwt.present(credential, ['foo'], {
|
|
364
|
+
kb: {
|
|
365
|
+
payload: {
|
|
366
|
+
sd_hash: 'sha-256',
|
|
367
|
+
aud: '1',
|
|
368
|
+
iat: 1,
|
|
369
|
+
nonce: '342',
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
try {
|
|
374
|
+
const results = await sdjwt.verify(presentation, ['foo'], true);
|
|
375
|
+
} catch (e) {
|
|
376
|
+
expect(e).toBeDefined();
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
});
|