@sd-jwt/core 0.7.2 → 0.8.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.
- package/CHANGELOG.md +11 -0
- package/dist/index.d.mts +142 -2
- package/dist/index.d.ts +142 -2
- package/dist/index.js +481 -33
- package/dist/index.mjs +479 -29
- package/package.json +7 -7
- package/src/flattenJSON.ts +90 -0
- package/src/generalJSON.ts +140 -0
- package/src/index.ts +350 -3
- package/src/sdjwt.ts +14 -6
- package/src/test/flattenJSON.spec.ts +56 -0
- package/src/test/generalJSON.spec.ts +124 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { SDJWTException } from '@sd-jwt/utils';
|
|
2
|
+
import { splitSdJwt } from '@sd-jwt/decode';
|
|
3
|
+
import { SD_SEPARATOR } from '@sd-jwt/types';
|
|
4
|
+
|
|
5
|
+
export type FlattenJSONData = {
|
|
6
|
+
jwtData: {
|
|
7
|
+
protected: string;
|
|
8
|
+
payload: string;
|
|
9
|
+
signature: string;
|
|
10
|
+
};
|
|
11
|
+
disclosures: Array<string>;
|
|
12
|
+
kb_jwt?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type FlattenJSONSerialized = {
|
|
16
|
+
payload: string;
|
|
17
|
+
signature: string;
|
|
18
|
+
protected: string;
|
|
19
|
+
header: {
|
|
20
|
+
disclosures: Array<string>;
|
|
21
|
+
kb_jwt?: string;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export class FlattenJSON {
|
|
26
|
+
public disclosures: Array<string>;
|
|
27
|
+
public kb_jwt?: string;
|
|
28
|
+
|
|
29
|
+
public payload: string;
|
|
30
|
+
public signature: string;
|
|
31
|
+
public protected: string;
|
|
32
|
+
|
|
33
|
+
constructor(data: FlattenJSONData) {
|
|
34
|
+
this.disclosures = data.disclosures;
|
|
35
|
+
this.kb_jwt = data.kb_jwt;
|
|
36
|
+
this.payload = data.jwtData.payload;
|
|
37
|
+
this.signature = data.jwtData.signature;
|
|
38
|
+
this.protected = data.jwtData.protected;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
public static fromEncode(encodedSdJwt: string) {
|
|
42
|
+
const { jwt, disclosures, kbJwt } = splitSdJwt(encodedSdJwt);
|
|
43
|
+
|
|
44
|
+
const { 0: protectedHeader, 1: payload, 2: signature } = jwt.split('.');
|
|
45
|
+
if (!protectedHeader || !payload || !signature) {
|
|
46
|
+
throw new SDJWTException('Invalid JWT');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return new FlattenJSON({
|
|
50
|
+
jwtData: {
|
|
51
|
+
protected: protectedHeader,
|
|
52
|
+
payload,
|
|
53
|
+
signature,
|
|
54
|
+
},
|
|
55
|
+
disclosures,
|
|
56
|
+
kb_jwt: kbJwt,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public static fromSerialized(json: FlattenJSONSerialized) {
|
|
61
|
+
return new FlattenJSON({
|
|
62
|
+
jwtData: {
|
|
63
|
+
protected: json.protected,
|
|
64
|
+
payload: json.payload,
|
|
65
|
+
signature: json.signature,
|
|
66
|
+
},
|
|
67
|
+
disclosures: json.header.disclosures,
|
|
68
|
+
kb_jwt: json.header.kb_jwt,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public toJson(): FlattenJSONSerialized {
|
|
73
|
+
return {
|
|
74
|
+
payload: this.payload,
|
|
75
|
+
signature: this.signature,
|
|
76
|
+
protected: this.protected,
|
|
77
|
+
header: {
|
|
78
|
+
disclosures: this.disclosures,
|
|
79
|
+
kb_jwt: this.kb_jwt,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public toEncoded() {
|
|
85
|
+
const jwt = `${this.protected}.${this.payload}.${this.signature}`;
|
|
86
|
+
const disclosures = this.disclosures.join(SD_SEPARATOR);
|
|
87
|
+
const kb_jwt = this.kb_jwt ?? '';
|
|
88
|
+
return [jwt, disclosures, kb_jwt].join(SD_SEPARATOR);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { base64urlEncode, SDJWTException } from '@sd-jwt/utils';
|
|
2
|
+
import { splitSdJwt } from '@sd-jwt/decode';
|
|
3
|
+
import { SD_SEPARATOR, type Signer } from '@sd-jwt/types';
|
|
4
|
+
|
|
5
|
+
export type GeneralJSONData = {
|
|
6
|
+
payload: string;
|
|
7
|
+
disclosures: Array<string>;
|
|
8
|
+
kb_jwt?: string;
|
|
9
|
+
signatures: Array<{
|
|
10
|
+
protected: string;
|
|
11
|
+
signature: string;
|
|
12
|
+
kid?: string;
|
|
13
|
+
}>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type GeneralJSONSerialized = {
|
|
17
|
+
payload: string;
|
|
18
|
+
signatures: Array<{
|
|
19
|
+
header: {
|
|
20
|
+
disclosures?: Array<string>;
|
|
21
|
+
kid?: string;
|
|
22
|
+
kb_jwt?: string;
|
|
23
|
+
};
|
|
24
|
+
protected: string;
|
|
25
|
+
signature: string;
|
|
26
|
+
}>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export class GeneralJSON {
|
|
30
|
+
public payload: string;
|
|
31
|
+
public disclosures: Array<string>;
|
|
32
|
+
public kb_jwt?: string;
|
|
33
|
+
public signatures: Array<{
|
|
34
|
+
protected: string;
|
|
35
|
+
signature: string;
|
|
36
|
+
kid?: string;
|
|
37
|
+
}>;
|
|
38
|
+
|
|
39
|
+
constructor(data: GeneralJSONData) {
|
|
40
|
+
this.payload = data.payload;
|
|
41
|
+
this.disclosures = data.disclosures;
|
|
42
|
+
this.kb_jwt = data.kb_jwt;
|
|
43
|
+
this.signatures = data.signatures;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public static fromEncode(encodedSdJwt: string) {
|
|
47
|
+
const { jwt, disclosures, kbJwt } = splitSdJwt(encodedSdJwt);
|
|
48
|
+
|
|
49
|
+
const { 0: protectedHeader, 1: payload, 2: signature } = jwt.split('.');
|
|
50
|
+
if (!protectedHeader || !payload || !signature) {
|
|
51
|
+
throw new SDJWTException('Invalid JWT');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return new GeneralJSON({
|
|
55
|
+
payload,
|
|
56
|
+
disclosures,
|
|
57
|
+
kb_jwt: kbJwt,
|
|
58
|
+
signatures: [
|
|
59
|
+
{
|
|
60
|
+
protected: protectedHeader,
|
|
61
|
+
signature,
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public static fromSerialized(json: GeneralJSONSerialized) {
|
|
68
|
+
if (!json.signatures[0]) {
|
|
69
|
+
throw new SDJWTException('Invalid JSON');
|
|
70
|
+
}
|
|
71
|
+
const disclosures = json.signatures[0].header?.disclosures ?? [];
|
|
72
|
+
const kb_jwt = json.signatures[0].header?.kb_jwt;
|
|
73
|
+
return new GeneralJSON({
|
|
74
|
+
payload: json.payload,
|
|
75
|
+
disclosures,
|
|
76
|
+
kb_jwt,
|
|
77
|
+
signatures: json.signatures.map((s) => {
|
|
78
|
+
return {
|
|
79
|
+
protected: s.protected,
|
|
80
|
+
signature: s.signature,
|
|
81
|
+
kid: s.header?.kid,
|
|
82
|
+
};
|
|
83
|
+
}),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public toJson() {
|
|
88
|
+
return {
|
|
89
|
+
payload: this.payload,
|
|
90
|
+
signatures: this.signatures.map((s, i) => {
|
|
91
|
+
if (i !== 0) {
|
|
92
|
+
// If present, disclosures and kb_jwt, MUST be included in the first unprotected header and
|
|
93
|
+
// MUST NOT be present in any following unprotected headers.
|
|
94
|
+
return {
|
|
95
|
+
header: {
|
|
96
|
+
kid: s.kid,
|
|
97
|
+
},
|
|
98
|
+
protected: s.protected,
|
|
99
|
+
signature: s.signature,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
header: {
|
|
104
|
+
disclosures: this.disclosures,
|
|
105
|
+
kid: s.kid,
|
|
106
|
+
kb_jwt: this.kb_jwt,
|
|
107
|
+
},
|
|
108
|
+
protected: s.protected,
|
|
109
|
+
signature: s.signature,
|
|
110
|
+
};
|
|
111
|
+
}),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
public toEncoded(index: number) {
|
|
116
|
+
if (index < 0 || index >= this.signatures.length) {
|
|
117
|
+
throw new SDJWTException('Index out of bounds');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const { protected: protectedHeader, signature } = this.signatures[index];
|
|
121
|
+
const disclosures = this.disclosures.join(SD_SEPARATOR);
|
|
122
|
+
const kb_jwt = this.kb_jwt ?? '';
|
|
123
|
+
const jwt = `${protectedHeader}.${this.payload}.${signature}`;
|
|
124
|
+
return [jwt, disclosures, kb_jwt].join(SD_SEPARATOR);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
public async addSignature(
|
|
128
|
+
protectedHeader: Record<string, unknown>,
|
|
129
|
+
signer: Signer,
|
|
130
|
+
kid?: string,
|
|
131
|
+
) {
|
|
132
|
+
const header = base64urlEncode(JSON.stringify(protectedHeader));
|
|
133
|
+
const signature = await signer(`${header}.${this.payload}`);
|
|
134
|
+
this.signatures.push({
|
|
135
|
+
protected: header,
|
|
136
|
+
signature,
|
|
137
|
+
kid,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
base64urlDecode,
|
|
3
|
+
base64urlEncode,
|
|
4
|
+
SDJWTException,
|
|
5
|
+
uint8ArrayToBase64Url,
|
|
6
|
+
} from '@sd-jwt/utils';
|
|
2
7
|
import { Jwt } from './jwt';
|
|
3
8
|
import { KBJwt } from './kbjwt';
|
|
4
9
|
import { SDJwt, pack } from './sdjwt';
|
|
@@ -10,14 +15,19 @@ import {
|
|
|
10
15
|
type PresentationFrame,
|
|
11
16
|
type SDJWTCompact,
|
|
12
17
|
type SDJWTConfig,
|
|
18
|
+
type JwtPayload,
|
|
19
|
+
type Signer,
|
|
13
20
|
} from '@sd-jwt/types';
|
|
14
21
|
import { getSDAlgAndPayload } from '@sd-jwt/decode';
|
|
15
|
-
import
|
|
22
|
+
import { FlattenJSON } from './flattenJSON';
|
|
23
|
+
import { GeneralJSON } from './generalJSON';
|
|
16
24
|
|
|
17
25
|
export * from './sdjwt';
|
|
18
26
|
export * from './kbjwt';
|
|
19
27
|
export * from './jwt';
|
|
20
28
|
export * from './decoy';
|
|
29
|
+
export * from './flattenJSON';
|
|
30
|
+
export * from './generalJSON';
|
|
21
31
|
|
|
22
32
|
export type SdJwtPayload = Record<string, unknown>;
|
|
23
33
|
|
|
@@ -25,7 +35,7 @@ export class SDJwtInstance<ExtendedPayload extends SdJwtPayload> {
|
|
|
25
35
|
//header type
|
|
26
36
|
protected type?: string;
|
|
27
37
|
|
|
28
|
-
public static DEFAULT_hashAlg = 'sha-256';
|
|
38
|
+
public static readonly DEFAULT_hashAlg = 'sha-256';
|
|
29
39
|
|
|
30
40
|
protected userConfig: SDJWTConfig = {};
|
|
31
41
|
|
|
@@ -310,4 +320,341 @@ export class SDJwtInstance<ExtendedPayload extends SdJwtPayload> {
|
|
|
310
320
|
const sdjwt = await SDJwt.fromEncode(endcodedSDJwt, this.userConfig.hasher);
|
|
311
321
|
return sdjwt.getClaims(this.userConfig.hasher);
|
|
312
322
|
}
|
|
323
|
+
|
|
324
|
+
public toFlattenJSON(endcodedSDJwt: SDJWTCompact) {
|
|
325
|
+
return FlattenJSON.fromEncode(endcodedSDJwt);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
public toGeneralJSON(endcodedSDJwt: SDJWTCompact) {
|
|
329
|
+
return GeneralJSON.fromEncode(endcodedSDJwt);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export class SDJwtGeneralJSONInstance<ExtendedPayload extends SdJwtPayload> {
|
|
334
|
+
//header type
|
|
335
|
+
protected type?: string;
|
|
336
|
+
|
|
337
|
+
public static readonly DEFAULT_hashAlg = 'sha-256';
|
|
338
|
+
|
|
339
|
+
protected userConfig: SDJWTConfig = {};
|
|
340
|
+
|
|
341
|
+
constructor(userConfig?: SDJWTConfig) {
|
|
342
|
+
if (userConfig) {
|
|
343
|
+
this.userConfig = userConfig;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private async createKBJwt(
|
|
348
|
+
options: KBOptions,
|
|
349
|
+
sdHash: string,
|
|
350
|
+
): Promise<KBJwt> {
|
|
351
|
+
if (!this.userConfig.kbSigner) {
|
|
352
|
+
throw new SDJWTException('Key Binding Signer not found');
|
|
353
|
+
}
|
|
354
|
+
if (!this.userConfig.kbSignAlg) {
|
|
355
|
+
throw new SDJWTException('Key Binding sign algorithm not specified');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const { payload } = options;
|
|
359
|
+
const kbJwt = new KBJwt({
|
|
360
|
+
header: {
|
|
361
|
+
typ: KB_JWT_TYP,
|
|
362
|
+
alg: this.userConfig.kbSignAlg,
|
|
363
|
+
},
|
|
364
|
+
payload: { ...payload, sd_hash: sdHash },
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
await kbJwt.sign(this.userConfig.kbSigner);
|
|
368
|
+
return kbJwt;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private encodeObj(obj: Record<string, unknown>): string {
|
|
372
|
+
return base64urlEncode(JSON.stringify(obj));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
public async issue<Payload extends ExtendedPayload>(
|
|
376
|
+
payload: Payload,
|
|
377
|
+
disclosureFrame: DisclosureFrame<Payload> | undefined,
|
|
378
|
+
options: {
|
|
379
|
+
sigs: Array<{
|
|
380
|
+
signer: Signer;
|
|
381
|
+
alg: string;
|
|
382
|
+
kid: string;
|
|
383
|
+
header?: Record<string, unknown>;
|
|
384
|
+
}>; // multiple signers for the credential
|
|
385
|
+
},
|
|
386
|
+
): Promise<GeneralJSON> {
|
|
387
|
+
if (!this.userConfig.hasher) {
|
|
388
|
+
throw new SDJWTException('Hasher not found');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (!this.userConfig.saltGenerator) {
|
|
392
|
+
throw new SDJWTException('SaltGenerator not found');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (disclosureFrame) {
|
|
396
|
+
this.validateReservedFields<Payload>(disclosureFrame);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const hasher = this.userConfig.hasher;
|
|
400
|
+
const hashAlg = this.userConfig.hashAlg ?? SDJwtInstance.DEFAULT_hashAlg;
|
|
401
|
+
|
|
402
|
+
const { packedClaims, disclosures } = await pack(
|
|
403
|
+
payload,
|
|
404
|
+
disclosureFrame,
|
|
405
|
+
{ hasher, alg: hashAlg },
|
|
406
|
+
this.userConfig.saltGenerator,
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
const encodedDisclosures = disclosures.map((d) => d.encode());
|
|
410
|
+
const encodedSDJwtPayload = this.encodeObj({
|
|
411
|
+
...packedClaims,
|
|
412
|
+
_sd_alg: disclosureFrame ? hashAlg : undefined,
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
const signatures = await Promise.all(
|
|
416
|
+
options.sigs.map(async (s) => {
|
|
417
|
+
const { signer, alg, kid, header } = s;
|
|
418
|
+
const protectedHeader = { typ: this.type, alg, kid, ...header };
|
|
419
|
+
const encodedProtectedHeader = this.encodeObj(protectedHeader);
|
|
420
|
+
const signature = await signer(
|
|
421
|
+
`${encodedProtectedHeader}.${encodedSDJwtPayload}`,
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
protected: encodedProtectedHeader,
|
|
426
|
+
kid,
|
|
427
|
+
signature,
|
|
428
|
+
};
|
|
429
|
+
}),
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
const generalJson = new GeneralJSON({
|
|
433
|
+
payload: encodedSDJwtPayload,
|
|
434
|
+
disclosures: encodedDisclosures,
|
|
435
|
+
signatures,
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
return generalJson;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Validates if the disclosureFrame contains any reserved fields. If so it will throw an error.
|
|
443
|
+
* @param disclosureFrame
|
|
444
|
+
* @returns
|
|
445
|
+
*/
|
|
446
|
+
protected validateReservedFields<T extends ExtendedPayload>(
|
|
447
|
+
disclosureFrame: DisclosureFrame<T>,
|
|
448
|
+
) {
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
public async present<T extends Record<string, unknown>>(
|
|
453
|
+
generalJSON: GeneralJSON,
|
|
454
|
+
presentationFrame?: PresentationFrame<T>,
|
|
455
|
+
options?: {
|
|
456
|
+
kb?: KBOptions;
|
|
457
|
+
},
|
|
458
|
+
): Promise<GeneralJSON> {
|
|
459
|
+
if (!this.userConfig.hasher) {
|
|
460
|
+
throw new SDJWTException('Hasher not found');
|
|
461
|
+
}
|
|
462
|
+
const hasher = this.userConfig.hasher;
|
|
463
|
+
const encodedSDJwt = generalJSON.toEncoded(0);
|
|
464
|
+
const sdjwt = await SDJwt.fromEncode(encodedSDJwt, hasher);
|
|
465
|
+
|
|
466
|
+
if (!sdjwt.jwt?.payload) throw new SDJWTException('Payload not found');
|
|
467
|
+
const disclosures = await sdjwt.getPresentDisclosures(
|
|
468
|
+
presentationFrame,
|
|
469
|
+
hasher,
|
|
470
|
+
);
|
|
471
|
+
const encodedDisclosures = disclosures.map((d) => d.encode());
|
|
472
|
+
const presentedGeneralJSON = new GeneralJSON({
|
|
473
|
+
payload: generalJSON.payload,
|
|
474
|
+
disclosures: encodedDisclosures,
|
|
475
|
+
signatures: generalJSON.signatures,
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
if (!options?.kb) {
|
|
479
|
+
return presentedGeneralJSON;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const presentSdJwtWithoutKb = await sdjwt.present(
|
|
483
|
+
presentationFrame,
|
|
484
|
+
hasher,
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
const sdHashStr = await this.calculateSDHash(
|
|
488
|
+
presentSdJwtWithoutKb,
|
|
489
|
+
sdjwt,
|
|
490
|
+
hasher,
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
const kbJwt = await this.createKBJwt(options.kb, sdHashStr);
|
|
494
|
+
const encodedKbJwt = kbJwt.encodeJwt();
|
|
495
|
+
presentedGeneralJSON.kb_jwt = encodedKbJwt;
|
|
496
|
+
return presentedGeneralJSON;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// This function is for verifying the SD JWT
|
|
500
|
+
// If requiredClaimKeys is provided, it will check if the required claim keys are presentation in the SD JWT
|
|
501
|
+
// If requireKeyBindings is true, it will check if the key binding JWT is presentation and verify it
|
|
502
|
+
public async verify(
|
|
503
|
+
generalJSON: GeneralJSON,
|
|
504
|
+
requiredClaimKeys?: string[],
|
|
505
|
+
requireKeyBindings?: boolean,
|
|
506
|
+
) {
|
|
507
|
+
if (!this.userConfig.hasher) {
|
|
508
|
+
throw new SDJWTException('Hasher not found');
|
|
509
|
+
}
|
|
510
|
+
const hasher = this.userConfig.hasher;
|
|
511
|
+
|
|
512
|
+
const { payload, headers } = await this.validate(generalJSON);
|
|
513
|
+
|
|
514
|
+
const encodedSDJwt = generalJSON.toEncoded(0);
|
|
515
|
+
const sdjwt = await SDJwt.fromEncode(encodedSDJwt, hasher);
|
|
516
|
+
if (!sdjwt.jwt || !sdjwt.jwt.payload) {
|
|
517
|
+
throw new SDJWTException('Invalid SD JWT');
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (requiredClaimKeys) {
|
|
521
|
+
const keys = await sdjwt.keys(hasher);
|
|
522
|
+
const missingKeys = requiredClaimKeys.filter((k) => !keys.includes(k));
|
|
523
|
+
if (missingKeys.length > 0) {
|
|
524
|
+
throw new SDJWTException(
|
|
525
|
+
`Missing required claim keys: ${missingKeys.join(', ')}`,
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (!requireKeyBindings) {
|
|
531
|
+
return { payload, headers };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (!sdjwt.kbJwt) {
|
|
535
|
+
throw new SDJWTException('Key Binding JWT not exist');
|
|
536
|
+
}
|
|
537
|
+
if (!this.userConfig.kbVerifier) {
|
|
538
|
+
throw new SDJWTException('Key Binding Verifier not found');
|
|
539
|
+
}
|
|
540
|
+
const kb = await sdjwt.kbJwt.verifyKB({
|
|
541
|
+
verifier: this.userConfig.kbVerifier,
|
|
542
|
+
payload: payload as JwtPayload,
|
|
543
|
+
});
|
|
544
|
+
if (!kb) {
|
|
545
|
+
throw new Error('signature is not valid');
|
|
546
|
+
}
|
|
547
|
+
const sdHashfromKb = kb.payload.sd_hash;
|
|
548
|
+
const sdjwtWithoutKb = new SDJwt({
|
|
549
|
+
jwt: sdjwt.jwt,
|
|
550
|
+
disclosures: sdjwt.disclosures,
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
const presentSdJwtWithoutKb = sdjwtWithoutKb.encodeSDJwt();
|
|
554
|
+
const sdHashStr = await this.calculateSDHash(
|
|
555
|
+
presentSdJwtWithoutKb,
|
|
556
|
+
sdjwt,
|
|
557
|
+
hasher,
|
|
558
|
+
);
|
|
559
|
+
|
|
560
|
+
if (sdHashStr !== sdHashfromKb) {
|
|
561
|
+
throw new SDJWTException('Invalid sd_hash in Key Binding JWT');
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return { payload, headers, kb };
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
private async calculateSDHash(
|
|
568
|
+
presentSdJwtWithoutKb: string,
|
|
569
|
+
sdjwt: SDJwt,
|
|
570
|
+
hasher: Hasher,
|
|
571
|
+
) {
|
|
572
|
+
if (!sdjwt.jwt || !sdjwt.jwt.payload) {
|
|
573
|
+
throw new SDJWTException('Invalid SD JWT');
|
|
574
|
+
}
|
|
575
|
+
const { _sd_alg } = getSDAlgAndPayload(sdjwt.jwt.payload);
|
|
576
|
+
const sdHash = await hasher(presentSdJwtWithoutKb, _sd_alg);
|
|
577
|
+
const sdHashStr = uint8ArrayToBase64Url(sdHash);
|
|
578
|
+
return sdHashStr;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// This function is for validating the SD JWT
|
|
582
|
+
// Just checking signature and return its the claims
|
|
583
|
+
public async validate(generalJSON: GeneralJSON) {
|
|
584
|
+
if (!this.userConfig.hasher) {
|
|
585
|
+
throw new SDJWTException('Hasher not found');
|
|
586
|
+
}
|
|
587
|
+
if (!this.userConfig.verifier) {
|
|
588
|
+
throw new SDJWTException('Verifier not found');
|
|
589
|
+
}
|
|
590
|
+
const hasher = this.userConfig.hasher;
|
|
591
|
+
const verifier = this.userConfig.verifier;
|
|
592
|
+
|
|
593
|
+
const { payload, signatures } = generalJSON;
|
|
594
|
+
|
|
595
|
+
const results = await Promise.all(
|
|
596
|
+
signatures.map(async (s) => {
|
|
597
|
+
const { protected: encodedHeader, signature } = s;
|
|
598
|
+
const verified = await verifier(
|
|
599
|
+
`${encodedHeader}.${payload}`,
|
|
600
|
+
signature,
|
|
601
|
+
);
|
|
602
|
+
const header = JSON.parse(base64urlDecode(encodedHeader));
|
|
603
|
+
return { verified, header };
|
|
604
|
+
}),
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
const verified = results.every((r) => r.verified);
|
|
608
|
+
if (!verified) {
|
|
609
|
+
throw new SDJWTException('Signature is not valid');
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const encodedSDJwt = generalJSON.toEncoded(0);
|
|
613
|
+
const sdjwt = await SDJwt.fromEncode(encodedSDJwt, hasher);
|
|
614
|
+
if (!sdjwt.jwt) {
|
|
615
|
+
throw new SDJWTException('Invalid SD JWT');
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const claims = await sdjwt.getClaims(hasher);
|
|
619
|
+
return { payload: claims, headers: results.map((r) => r.header) };
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
public config(newConfig: SDJWTConfig) {
|
|
623
|
+
this.userConfig = { ...this.userConfig, ...newConfig };
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
public encode(sdJwt: GeneralJSON, index: number): SDJWTCompact {
|
|
627
|
+
return sdJwt.toEncoded(index);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
public decode(endcodedSDJwt: SDJWTCompact) {
|
|
631
|
+
return GeneralJSON.fromEncode(endcodedSDJwt);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
public async keys(generalSdjwt: GeneralJSON) {
|
|
635
|
+
if (!this.userConfig.hasher) {
|
|
636
|
+
throw new SDJWTException('Hasher not found');
|
|
637
|
+
}
|
|
638
|
+
const endcodedSDJwt = generalSdjwt.toEncoded(0);
|
|
639
|
+
const sdjwt = await SDJwt.fromEncode(endcodedSDJwt, this.userConfig.hasher);
|
|
640
|
+
return sdjwt.keys(this.userConfig.hasher);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
public async presentableKeys(generalSdjwt: GeneralJSON) {
|
|
644
|
+
if (!this.userConfig.hasher) {
|
|
645
|
+
throw new SDJWTException('Hasher not found');
|
|
646
|
+
}
|
|
647
|
+
const endcodedSDJwt = generalSdjwt.toEncoded(0);
|
|
648
|
+
const sdjwt = await SDJwt.fromEncode(endcodedSDJwt, this.userConfig.hasher);
|
|
649
|
+
return sdjwt.presentableKeys(this.userConfig.hasher);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
public async getClaims(generalSdjwt: GeneralJSON) {
|
|
653
|
+
if (!this.userConfig.hasher) {
|
|
654
|
+
throw new SDJWTException('Hasher not found');
|
|
655
|
+
}
|
|
656
|
+
const endcodedSDJwt = generalSdjwt.toEncoded(0);
|
|
657
|
+
const sdjwt = await SDJwt.fromEncode(endcodedSDJwt, this.userConfig.hasher);
|
|
658
|
+
return sdjwt.getClaims(this.userConfig.hasher);
|
|
659
|
+
}
|
|
313
660
|
}
|
package/src/sdjwt.ts
CHANGED
|
@@ -120,6 +120,19 @@ export class SDJwt<
|
|
|
120
120
|
presentFrame: PresentationFrame<T> | undefined,
|
|
121
121
|
hasher: Hasher,
|
|
122
122
|
): Promise<SDJWTCompact> {
|
|
123
|
+
const disclosures = await this.getPresentDisclosures(presentFrame, hasher);
|
|
124
|
+
const presentSDJwt = new SDJwt({
|
|
125
|
+
jwt: this.jwt,
|
|
126
|
+
disclosures,
|
|
127
|
+
kbJwt: this.kbJwt,
|
|
128
|
+
});
|
|
129
|
+
return presentSDJwt.encodeSDJwt();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
public async getPresentDisclosures<T extends Record<string, unknown>>(
|
|
133
|
+
presentFrame: PresentationFrame<T> | undefined,
|
|
134
|
+
hasher: Hasher,
|
|
135
|
+
): Promise<Disclosure<unknown>[]> {
|
|
123
136
|
if (!this.jwt?.payload || !this.disclosures) {
|
|
124
137
|
throw new SDJWTException('Invalid sd-jwt: jwt or disclosures is missing');
|
|
125
138
|
}
|
|
@@ -138,12 +151,7 @@ export class SDJwt<
|
|
|
138
151
|
const disclosures = keys
|
|
139
152
|
.map((k) => hashmap[disclosureKeymap[k]])
|
|
140
153
|
.filter((d) => d !== undefined);
|
|
141
|
-
|
|
142
|
-
jwt: this.jwt,
|
|
143
|
-
disclosures,
|
|
144
|
-
kbJwt: this.kbJwt,
|
|
145
|
-
});
|
|
146
|
-
return presentSDJwt.encodeSDJwt();
|
|
154
|
+
return disclosures;
|
|
147
155
|
}
|
|
148
156
|
|
|
149
157
|
public encodeSDJwt(): SDJWTCompact {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import { FlattenJSON } from '..';
|
|
3
|
+
|
|
4
|
+
describe('FlattenJSON', () => {
|
|
5
|
+
test('fromEncode', () => {
|
|
6
|
+
const compact =
|
|
7
|
+
'eyJ0eXAiOiJzZCtqd3QiLCJhbGciOiJFUzI1NiJ9.eyJpZCI6IjEyMzQiLCJfc2QiOlsiYkRUUnZtNS1Zbi1IRzdjcXBWUjVPVlJJWHNTYUJrNTdKZ2lPcV9qMVZJNCIsImV0M1VmUnlsd1ZyZlhkUEt6Zzc5aGNqRDFJdHpvUTlvQm9YUkd0TW9zRmsiLCJ6V2ZaTlMxOUF0YlJTVGJvN3NKUm4wQlpRdldSZGNob0M3VVphYkZyalk4Il0sIl9zZF9hbGciOiJzaGEtMjU2In0.n27NCtnuwytlBYtUNjgkesDP_7gN7bhaLhWNL4SWT6MaHsOjZ2ZMp987GgQRL6ZkLbJ7Cd3hlePHS84GBXPuvg~WyI1ZWI4Yzg2MjM0MDJjZjJlIiwiZmlyc3RuYW1lIiwiSm9obiJd~WyJjNWMzMWY2ZWYzNTg4MWJjIiwibGFzdG5hbWUiLCJEb2UiXQ~WyJmYTlkYTUzZWJjOTk3OThlIiwic3NuIiwiMTIzLTQ1LTY3ODkiXQ~eyJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJpYXQiOjE3MTAwNjk3MjIsImF1ZCI6ImRpZDpleGFtcGxlOjEyMyIsIm5vbmNlIjoiazh2ZGYwbmQ2Iiwic2RfaGFzaCI6Il8tTmJWSzNmczl3VzNHaDNOUktSNEt1NmZDMUwzN0R2MFFfalBXd0ppRkUifQ.pqw2OB5IA5ya9Mxf60hE3nr2gsJEIoIlnuCa4qIisijHbwg3WzTDFmW2SuNvK_ORN0WU6RoGbJx5uYZh8k4EbA';
|
|
8
|
+
const flattenJSON = FlattenJSON.fromEncode(compact);
|
|
9
|
+
expect(flattenJSON).toBeDefined();
|
|
10
|
+
|
|
11
|
+
const result = {
|
|
12
|
+
payload:
|
|
13
|
+
'eyJpZCI6IjEyMzQiLCJfc2QiOlsiYkRUUnZtNS1Zbi1IRzdjcXBWUjVPVlJJWHNTYUJrNTdKZ2lPcV9qMVZJNCIsImV0M1VmUnlsd1ZyZlhkUEt6Zzc5aGNqRDFJdHpvUTlvQm9YUkd0TW9zRmsiLCJ6V2ZaTlMxOUF0YlJTVGJvN3NKUm4wQlpRdldSZGNob0M3VVphYkZyalk4Il0sIl9zZF9hbGciOiJzaGEtMjU2In0',
|
|
14
|
+
signature:
|
|
15
|
+
'n27NCtnuwytlBYtUNjgkesDP_7gN7bhaLhWNL4SWT6MaHsOjZ2ZMp987GgQRL6ZkLbJ7Cd3hlePHS84GBXPuvg',
|
|
16
|
+
protected: 'eyJ0eXAiOiJzZCtqd3QiLCJhbGciOiJFUzI1NiJ9',
|
|
17
|
+
header: {
|
|
18
|
+
disclosures: [
|
|
19
|
+
'WyI1ZWI4Yzg2MjM0MDJjZjJlIiwiZmlyc3RuYW1lIiwiSm9obiJd',
|
|
20
|
+
'WyJjNWMzMWY2ZWYzNTg4MWJjIiwibGFzdG5hbWUiLCJEb2UiXQ',
|
|
21
|
+
'WyJmYTlkYTUzZWJjOTk3OThlIiwic3NuIiwiMTIzLTQ1LTY3ODkiXQ',
|
|
22
|
+
],
|
|
23
|
+
kb_jwt:
|
|
24
|
+
'eyJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJpYXQiOjE3MTAwNjk3MjIsImF1ZCI6ImRpZDpleGFtcGxlOjEyMyIsIm5vbmNlIjoiazh2ZGYwbmQ2Iiwic2RfaGFzaCI6Il8tTmJWSzNmczl3VzNHaDNOUktSNEt1NmZDMUwzN0R2MFFfalBXd0ppRkUifQ.pqw2OB5IA5ya9Mxf60hE3nr2gsJEIoIlnuCa4qIisijHbwg3WzTDFmW2SuNvK_ORN0WU6RoGbJx5uYZh8k4EbA',
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
expect(flattenJSON.toJson()).toEqual(result);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('fromSerialized', () => {
|
|
32
|
+
const flattenJSON = {
|
|
33
|
+
payload:
|
|
34
|
+
'eyJpZCI6IjEyMzQiLCJfc2QiOlsiYkRUUnZtNS1Zbi1IRzdjcXBWUjVPVlJJWHNTYUJrNTdKZ2lPcV9qMVZJNCIsImV0M1VmUnlsd1ZyZlhkUEt6Zzc5aGNqRDFJdHpvUTlvQm9YUkd0TW9zRmsiLCJ6V2ZaTlMxOUF0YlJTVGJvN3NKUm4wQlpRdldSZGNob0M3VVphYkZyalk4Il0sIl9zZF9hbGciOiJzaGEtMjU2In0',
|
|
35
|
+
signature:
|
|
36
|
+
'n27NCtnuwytlBYtUNjgkesDP_7gN7bhaLhWNL4SWT6MaHsOjZ2ZMp987GgQRL6ZkLbJ7Cd3hlePHS84GBXPuvg',
|
|
37
|
+
protected: 'eyJ0eXAiOiJzZCtqd3QiLCJhbGciOiJFUzI1NiJ9',
|
|
38
|
+
header: {
|
|
39
|
+
disclosures: [
|
|
40
|
+
'WyI1ZWI4Yzg2MjM0MDJjZjJlIiwiZmlyc3RuYW1lIiwiSm9obiJd',
|
|
41
|
+
'WyJjNWMzMWY2ZWYzNTg4MWJjIiwibGFzdG5hbWUiLCJEb2UiXQ',
|
|
42
|
+
'WyJmYTlkYTUzZWJjOTk3OThlIiwic3NuIiwiMTIzLTQ1LTY3ODkiXQ',
|
|
43
|
+
],
|
|
44
|
+
kb_jwt:
|
|
45
|
+
'eyJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJpYXQiOjE3MTAwNjk3MjIsImF1ZCI6ImRpZDpleGFtcGxlOjEyMyIsIm5vbmNlIjoiazh2ZGYwbmQ2Iiwic2RfaGFzaCI6Il8tTmJWSzNmczl3VzNHaDNOUktSNEt1NmZDMUwzN0R2MFFfalBXd0ppRkUifQ.pqw2OB5IA5ya9Mxf60hE3nr2gsJEIoIlnuCa4qIisijHbwg3WzTDFmW2SuNvK_ORN0WU6RoGbJx5uYZh8k4EbA',
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const result = FlattenJSON.fromSerialized(flattenJSON);
|
|
50
|
+
expect(result).toBeDefined();
|
|
51
|
+
const compact =
|
|
52
|
+
'eyJ0eXAiOiJzZCtqd3QiLCJhbGciOiJFUzI1NiJ9.eyJpZCI6IjEyMzQiLCJfc2QiOlsiYkRUUnZtNS1Zbi1IRzdjcXBWUjVPVlJJWHNTYUJrNTdKZ2lPcV9qMVZJNCIsImV0M1VmUnlsd1ZyZlhkUEt6Zzc5aGNqRDFJdHpvUTlvQm9YUkd0TW9zRmsiLCJ6V2ZaTlMxOUF0YlJTVGJvN3NKUm4wQlpRdldSZGNob0M3VVphYkZyalk4Il0sIl9zZF9hbGciOiJzaGEtMjU2In0.n27NCtnuwytlBYtUNjgkesDP_7gN7bhaLhWNL4SWT6MaHsOjZ2ZMp987GgQRL6ZkLbJ7Cd3hlePHS84GBXPuvg~WyI1ZWI4Yzg2MjM0MDJjZjJlIiwiZmlyc3RuYW1lIiwiSm9obiJd~WyJjNWMzMWY2ZWYzNTg4MWJjIiwibGFzdG5hbWUiLCJEb2UiXQ~WyJmYTlkYTUzZWJjOTk3OThlIiwic3NuIiwiMTIzLTQ1LTY3ODkiXQ~eyJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJpYXQiOjE3MTAwNjk3MjIsImF1ZCI6ImRpZDpleGFtcGxlOjEyMyIsIm5vbmNlIjoiazh2ZGYwbmQ2Iiwic2RfaGFzaCI6Il8tTmJWSzNmczl3VzNHaDNOUktSNEt1NmZDMUwzN0R2MFFfalBXd0ppRkUifQ.pqw2OB5IA5ya9Mxf60hE3nr2gsJEIoIlnuCa4qIisijHbwg3WzTDFmW2SuNvK_ORN0WU6RoGbJx5uYZh8k4EbA';
|
|
53
|
+
|
|
54
|
+
expect(result.toEncoded()).toEqual(compact);
|
|
55
|
+
});
|
|
56
|
+
});
|