@sd-jwt/core 0.19.1-next.1 → 0.19.1-next.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -5
- package/dist/index.d.mts +359 -16
- package/dist/index.d.ts +359 -16
- package/dist/index.js +795 -138
- package/dist/index.mjs +736 -123
- package/package.json +4 -7
- package/src/decode/decode.ts +366 -0
- package/src/decode/index.ts +1 -0
- package/src/decoy.ts +2 -2
- package/src/flattenJSON.ts +3 -3
- package/src/generalJSON.ts +3 -3
- package/src/index.ts +227 -14
- package/src/jwt.ts +10 -15
- package/src/kbjwt.ts +5 -6
- package/src/present/index.ts +1 -0
- package/src/present/present.ts +210 -0
- package/src/sdjwt.ts +11 -10
- package/src/test/decode/decode.spec.ts +202 -0
- package/src/test/decoy.spec.ts +2 -2
- package/src/test/generalJSON.spec.ts +1 -1
- package/src/test/index.spec.ts +154 -2
- package/src/test/jwt.spec.ts +7 -13
- package/src/test/kbjwt.spec.ts +5 -5
- package/src/test/present/present.spec.ts +305 -0
- package/src/test/sdjwt.spec.ts +4 -4
- package/src/test/types/type.spec.ts +88 -0
- package/src/test/utils/base64url.spec.ts +33 -0
- package/src/test/utils/disclosure.spec.ts +170 -0
- package/src/test/utils/error.spec.ts +15 -0
- package/src/types/index.ts +2 -0
- package/src/types/type.ts +249 -0
- package/src/types/verification-error.ts +55 -0
- package/src/utils/base64url.ts +6 -0
- package/src/utils/disclosure.ts +98 -0
- package/src/utils/error.ts +25 -0
- package/src/utils/index.ts +3 -0
- package/test/app-e2e.spec.ts +8 -8
- package/test/rfc9901-validation.spec.ts +150 -0
- package/CHANGELOG.md +0 -240
package/src/index.ts
CHANGED
|
@@ -1,34 +1,42 @@
|
|
|
1
|
-
import { getSDAlgAndPayload } from '
|
|
1
|
+
import { getSDAlgAndPayload } from './decode';
|
|
2
|
+
import { FlattenJSON } from './flattenJSON';
|
|
3
|
+
import { GeneralJSON } from './generalJSON';
|
|
4
|
+
import { Jwt, type VerifierOptions } from './jwt';
|
|
5
|
+
import { KBJwt } from './kbjwt';
|
|
6
|
+
import { pack, SDJwt } from './sdjwt';
|
|
2
7
|
import {
|
|
3
8
|
type DisclosureFrame,
|
|
4
9
|
type Hasher,
|
|
5
10
|
IANA_HASH_ALGORITHMS,
|
|
6
|
-
type JwtPayload,
|
|
7
11
|
KB_JWT_TYP,
|
|
8
12
|
type KBOptions,
|
|
9
13
|
type PresentationFrame,
|
|
14
|
+
type SafeVerifyResult,
|
|
10
15
|
type SDJWTCompact,
|
|
11
16
|
type SDJWTConfig,
|
|
12
17
|
type Signer,
|
|
13
|
-
|
|
18
|
+
type VerificationError,
|
|
19
|
+
type VerificationErrorCode,
|
|
20
|
+
} from './types';
|
|
14
21
|
import {
|
|
15
22
|
base64urlDecode,
|
|
16
23
|
base64urlEncode,
|
|
24
|
+
ensureError,
|
|
17
25
|
SDJWTException,
|
|
18
26
|
uint8ArrayToBase64Url,
|
|
19
|
-
} from '
|
|
20
|
-
import { FlattenJSON } from './flattenJSON';
|
|
21
|
-
import { GeneralJSON } from './generalJSON';
|
|
22
|
-
import { Jwt, type VerifierOptions } from './jwt';
|
|
23
|
-
import { KBJwt } from './kbjwt';
|
|
24
|
-
import { pack, SDJwt } from './sdjwt';
|
|
27
|
+
} from './utils';
|
|
25
28
|
|
|
29
|
+
export * from './decode';
|
|
26
30
|
export * from './decoy';
|
|
27
31
|
export * from './flattenJSON';
|
|
28
32
|
export * from './generalJSON';
|
|
29
33
|
export * from './jwt';
|
|
30
34
|
export * from './kbjwt';
|
|
35
|
+
export * from './present';
|
|
31
36
|
export * from './sdjwt';
|
|
37
|
+
// Re-export all types, utils, decode, and present functionality
|
|
38
|
+
export * from './types';
|
|
39
|
+
export * from './utils';
|
|
32
40
|
|
|
33
41
|
export type SdJwtPayload = Record<string, unknown>;
|
|
34
42
|
|
|
@@ -232,7 +240,7 @@ export class SDJwtInstance<ExtendedPayload extends SdJwtPayload, T = unknown> {
|
|
|
232
240
|
}
|
|
233
241
|
const kb = await sdjwt.kbJwt.verifyKB({
|
|
234
242
|
verifier: this.userConfig.kbVerifier,
|
|
235
|
-
payload
|
|
243
|
+
payload,
|
|
236
244
|
nonce: options.keyBindingNonce,
|
|
237
245
|
});
|
|
238
246
|
if (!kb) {
|
|
@@ -259,6 +267,211 @@ export class SDJwtInstance<ExtendedPayload extends SdJwtPayload, T = unknown> {
|
|
|
259
267
|
return { payload, header, kb };
|
|
260
268
|
}
|
|
261
269
|
|
|
270
|
+
/**
|
|
271
|
+
* Safe verification that collects all errors instead of failing fast.
|
|
272
|
+
* Returns a result object with either the verified data or an array of all errors.
|
|
273
|
+
*
|
|
274
|
+
* @param encodedSDJwt - The encoded SD-JWT to verify
|
|
275
|
+
* @param options - Verification options
|
|
276
|
+
* @returns A SafeVerifyResult containing either success data or collected errors
|
|
277
|
+
*/
|
|
278
|
+
public async safeVerify(
|
|
279
|
+
encodedSDJwt: string,
|
|
280
|
+
options?: T & VerifierOptions,
|
|
281
|
+
): Promise<
|
|
282
|
+
SafeVerifyResult<{
|
|
283
|
+
payload: ExtendedPayload;
|
|
284
|
+
header: Record<string, unknown> | undefined;
|
|
285
|
+
kb?: {
|
|
286
|
+
payload: Record<string, unknown>;
|
|
287
|
+
header: Record<string, unknown>;
|
|
288
|
+
};
|
|
289
|
+
}>
|
|
290
|
+
> {
|
|
291
|
+
const errors: VerificationError[] = [];
|
|
292
|
+
|
|
293
|
+
// Helper to add errors
|
|
294
|
+
const addError = (
|
|
295
|
+
code: VerificationErrorCode,
|
|
296
|
+
message: string,
|
|
297
|
+
details?: unknown,
|
|
298
|
+
) => {
|
|
299
|
+
errors.push({ code, message, details });
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// Helper to convert exception to error code
|
|
303
|
+
const exceptionToCode = (error: Error): VerificationErrorCode => {
|
|
304
|
+
const message = error.message.toLowerCase();
|
|
305
|
+
if (message.includes('hasher not found')) return 'HASHER_NOT_FOUND';
|
|
306
|
+
if (message.includes('verifier not found')) return 'VERIFIER_NOT_FOUND';
|
|
307
|
+
if (message.includes('invalid sd jwt') || message.includes('invalid jwt'))
|
|
308
|
+
return 'INVALID_SD_JWT';
|
|
309
|
+
if (message.includes('not yet valid')) return 'JWT_NOT_YET_VALID';
|
|
310
|
+
if (message.includes('expired')) return 'JWT_EXPIRED';
|
|
311
|
+
if (message.includes('signature')) return 'INVALID_JWT_SIGNATURE';
|
|
312
|
+
if (message.includes('missing required claim'))
|
|
313
|
+
return 'MISSING_REQUIRED_CLAIMS';
|
|
314
|
+
if (message.includes('key binding jwt not exist'))
|
|
315
|
+
return 'KEY_BINDING_JWT_MISSING';
|
|
316
|
+
if (message.includes('key binding verifier not found'))
|
|
317
|
+
return 'KEY_BINDING_VERIFIER_NOT_FOUND';
|
|
318
|
+
if (message.includes('sd_hash')) return 'KEY_BINDING_SD_HASH_INVALID';
|
|
319
|
+
return 'UNKNOWN_ERROR';
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
// Check basic configuration first
|
|
323
|
+
if (!this.userConfig.hasher) {
|
|
324
|
+
addError('HASHER_NOT_FOUND', 'Hasher not found');
|
|
325
|
+
}
|
|
326
|
+
if (!this.userConfig.verifier) {
|
|
327
|
+
addError('VERIFIER_NOT_FOUND', 'Verifier not found');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// If basic config is missing, return early
|
|
331
|
+
if (errors.length > 0) {
|
|
332
|
+
return { success: false, errors };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (!this.userConfig.hasher) {
|
|
336
|
+
throw new SDJWTException('Hasher not found');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// hasher and verifier are guaranteed to be defined here
|
|
340
|
+
const hasher = this.userConfig.hasher;
|
|
341
|
+
|
|
342
|
+
// Try to decode and validate the SD-JWT
|
|
343
|
+
let sdjwt: SDJwt | undefined;
|
|
344
|
+
let payload: ExtendedPayload | undefined;
|
|
345
|
+
let header: Record<string, unknown> | undefined;
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
sdjwt = await SDJwt.fromEncode(encodedSDJwt, hasher);
|
|
349
|
+
if (!sdjwt.jwt || !sdjwt.jwt.payload) {
|
|
350
|
+
addError('INVALID_SD_JWT', 'Invalid SD JWT: missing JWT or payload');
|
|
351
|
+
}
|
|
352
|
+
} catch (e) {
|
|
353
|
+
const error = ensureError(e);
|
|
354
|
+
addError(
|
|
355
|
+
'INVALID_SD_JWT',
|
|
356
|
+
`Failed to decode SD-JWT: ${error.message}`,
|
|
357
|
+
error,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Validate signature and claims
|
|
362
|
+
if (sdjwt?.jwt) {
|
|
363
|
+
try {
|
|
364
|
+
const result = await this.VerifyJwt(sdjwt.jwt, options);
|
|
365
|
+
header = result.header;
|
|
366
|
+
const claims = await sdjwt.getClaims(hasher);
|
|
367
|
+
payload = claims as ExtendedPayload;
|
|
368
|
+
} catch (e) {
|
|
369
|
+
const error = ensureError(e);
|
|
370
|
+
const code = exceptionToCode(error);
|
|
371
|
+
addError(code, error.message, error);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Check required claim keys
|
|
376
|
+
if (sdjwt && options?.requiredClaimKeys) {
|
|
377
|
+
try {
|
|
378
|
+
const keys = await sdjwt.keys(hasher);
|
|
379
|
+
const missingKeys = options.requiredClaimKeys.filter(
|
|
380
|
+
(k) => !keys.includes(k),
|
|
381
|
+
);
|
|
382
|
+
if (missingKeys.length > 0) {
|
|
383
|
+
addError(
|
|
384
|
+
'MISSING_REQUIRED_CLAIMS',
|
|
385
|
+
`Missing required claim keys: ${missingKeys.join(', ')}`,
|
|
386
|
+
{ missingKeys },
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
} catch (e) {
|
|
390
|
+
const error = ensureError(e);
|
|
391
|
+
addError(
|
|
392
|
+
'UNKNOWN_ERROR',
|
|
393
|
+
`Failed to check required claims: ${error.message}`,
|
|
394
|
+
error,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Verify key binding if requested
|
|
400
|
+
let kb:
|
|
401
|
+
| { payload: Record<string, unknown>; header: Record<string, unknown> }
|
|
402
|
+
| undefined;
|
|
403
|
+
if (options?.keyBindingNonce && sdjwt) {
|
|
404
|
+
if (!sdjwt.kbJwt) {
|
|
405
|
+
addError('KEY_BINDING_JWT_MISSING', 'Key Binding JWT not exist');
|
|
406
|
+
} else if (!this.userConfig.kbVerifier) {
|
|
407
|
+
addError(
|
|
408
|
+
'KEY_BINDING_VERIFIER_NOT_FOUND',
|
|
409
|
+
'Key Binding Verifier not found',
|
|
410
|
+
);
|
|
411
|
+
} else if (payload) {
|
|
412
|
+
try {
|
|
413
|
+
const kbResult = await sdjwt.kbJwt.verifyKB({
|
|
414
|
+
verifier: this.userConfig.kbVerifier,
|
|
415
|
+
payload,
|
|
416
|
+
nonce: options.keyBindingNonce,
|
|
417
|
+
});
|
|
418
|
+
if (!kbResult) {
|
|
419
|
+
addError(
|
|
420
|
+
'KEY_BINDING_SIGNATURE_INVALID',
|
|
421
|
+
'Key binding signature is not valid',
|
|
422
|
+
);
|
|
423
|
+
} else {
|
|
424
|
+
kb = kbResult;
|
|
425
|
+
|
|
426
|
+
// Verify sd_hash
|
|
427
|
+
const sdjwtWithoutKb = new SDJwt({
|
|
428
|
+
jwt: sdjwt.jwt,
|
|
429
|
+
disclosures: sdjwt.disclosures,
|
|
430
|
+
});
|
|
431
|
+
const presentSdJwtWithoutKb = sdjwtWithoutKb.encodeSDJwt();
|
|
432
|
+
const sdHashStr = await this.calculateSDHash(
|
|
433
|
+
presentSdJwtWithoutKb,
|
|
434
|
+
sdjwt,
|
|
435
|
+
hasher,
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
if (sdHashStr !== kbResult.payload.sd_hash) {
|
|
439
|
+
addError(
|
|
440
|
+
'KEY_BINDING_SD_HASH_INVALID',
|
|
441
|
+
'Invalid sd_hash in Key Binding JWT',
|
|
442
|
+
{
|
|
443
|
+
expected: sdHashStr,
|
|
444
|
+
received: kbResult.payload.sd_hash,
|
|
445
|
+
},
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
} catch (e) {
|
|
450
|
+
const error = ensureError(e);
|
|
451
|
+
addError(
|
|
452
|
+
'KEY_BINDING_SIGNATURE_INVALID',
|
|
453
|
+
`Key binding verification failed: ${error.message}`,
|
|
454
|
+
error,
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Return result
|
|
461
|
+
if (errors.length > 0) {
|
|
462
|
+
return { success: false, errors };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return {
|
|
466
|
+
success: true,
|
|
467
|
+
data: {
|
|
468
|
+
payload: payload as ExtendedPayload,
|
|
469
|
+
header,
|
|
470
|
+
kb,
|
|
471
|
+
},
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
262
475
|
private async calculateSDHash(
|
|
263
476
|
presentSdJwtWithoutKb: string,
|
|
264
477
|
sdjwt: SDJwt,
|
|
@@ -292,7 +505,7 @@ export class SDJwtInstance<ExtendedPayload extends SdJwtPayload, T = unknown> {
|
|
|
292
505
|
}
|
|
293
506
|
|
|
294
507
|
const verifiedPayloads = await this.VerifyJwt(sdjwt.jwt, options);
|
|
295
|
-
const claims = await sdjwt.getClaims(hasher);
|
|
508
|
+
const claims = await sdjwt.getClaims<ExtendedPayload>(hasher);
|
|
296
509
|
return { payload: claims, header: verifiedPayloads.header };
|
|
297
510
|
}
|
|
298
511
|
|
|
@@ -559,8 +772,8 @@ export class SDJwtGeneralJSONInstance<ExtendedPayload extends SdJwtPayload> {
|
|
|
559
772
|
}
|
|
560
773
|
const kb = await sdjwt.kbJwt.verifyKB({
|
|
561
774
|
verifier: this.userConfig.kbVerifier,
|
|
562
|
-
payload
|
|
563
|
-
nonce: options.keyBindingNonce
|
|
775
|
+
payload,
|
|
776
|
+
nonce: options.keyBindingNonce,
|
|
564
777
|
});
|
|
565
778
|
if (!kb) {
|
|
566
779
|
throw new Error('signature is not valid');
|
|
@@ -636,7 +849,7 @@ export class SDJwtGeneralJSONInstance<ExtendedPayload extends SdJwtPayload> {
|
|
|
636
849
|
throw new SDJWTException('Invalid SD JWT');
|
|
637
850
|
}
|
|
638
851
|
|
|
639
|
-
const claims = await sdjwt.getClaims(hasher);
|
|
852
|
+
const claims = await sdjwt.getClaims<ExtendedPayload>(hasher);
|
|
640
853
|
return { payload: claims, headers: results.map((r) => r.header) };
|
|
641
854
|
}
|
|
642
855
|
|
package/src/jwt.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { decodeJwt } from '
|
|
2
|
-
import type { Base64urlString, Signer, Verifier } from '
|
|
3
|
-
import { base64urlEncode, SDJWTException } from '
|
|
1
|
+
import { decodeJwt } from './decode';
|
|
2
|
+
import type { Base64urlString, Signer, Verifier } from './types';
|
|
3
|
+
import { base64urlEncode, SDJWTException } from './utils';
|
|
4
4
|
|
|
5
5
|
export type JwtData<
|
|
6
6
|
Header extends Record<string, unknown>,
|
|
@@ -154,23 +154,18 @@ export class Jwt<
|
|
|
154
154
|
public async verify<T>(verifier: Verifier<T>, options?: T & VerifierOptions) {
|
|
155
155
|
const skew = options?.skewSeconds ? options.skewSeconds : 0;
|
|
156
156
|
const currentDate = options?.currentDate ?? Math.floor(Date.now() / 1000);
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
157
|
+
const iat = this.payload?.iat;
|
|
158
|
+
const nbf = this.payload?.nbf;
|
|
159
|
+
const exp = this.payload?.exp;
|
|
160
|
+
|
|
161
|
+
if (typeof iat === 'number' && iat - skew > currentDate) {
|
|
161
162
|
throw new SDJWTException('Verify Error: JWT is not yet valid');
|
|
162
163
|
}
|
|
163
164
|
|
|
164
|
-
if (
|
|
165
|
-
this.payload?.nbf &&
|
|
166
|
-
(this.payload.nbf as number) - skew > currentDate
|
|
167
|
-
) {
|
|
165
|
+
if (typeof nbf === 'number' && nbf - skew > currentDate) {
|
|
168
166
|
throw new SDJWTException('Verify Error: JWT is not yet valid');
|
|
169
167
|
}
|
|
170
|
-
if (
|
|
171
|
-
this.payload?.exp &&
|
|
172
|
-
(this.payload.exp as number) + skew < currentDate
|
|
173
|
-
) {
|
|
168
|
+
if (typeof exp === 'number' && exp + skew < currentDate) {
|
|
174
169
|
throw new SDJWTException('Verify Error: JWT is expired');
|
|
175
170
|
}
|
|
176
171
|
|
package/src/kbjwt.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
+
import { Jwt } from './jwt';
|
|
1
2
|
import {
|
|
2
|
-
type JwtPayload,
|
|
3
3
|
KB_JWT_TYP,
|
|
4
4
|
type KbVerifier,
|
|
5
5
|
type kbHeader,
|
|
6
6
|
type kbPayload,
|
|
7
|
-
} from '
|
|
8
|
-
import { SDJWTException } from '
|
|
9
|
-
import { Jwt } from './jwt';
|
|
7
|
+
} from './types';
|
|
8
|
+
import { SDJWTException } from './utils';
|
|
10
9
|
|
|
11
10
|
export class KBJwt<
|
|
12
11
|
Header extends kbHeader = kbHeader,
|
|
@@ -16,7 +15,7 @@ export class KBJwt<
|
|
|
16
15
|
// the type unknown is not good, but we don't know at this point how to get the public key of the signer, this is defined in the kbVerifier
|
|
17
16
|
public async verifyKB(values: {
|
|
18
17
|
verifier: KbVerifier;
|
|
19
|
-
payload:
|
|
18
|
+
payload: Record<string, unknown>;
|
|
20
19
|
nonce: string;
|
|
21
20
|
}) {
|
|
22
21
|
if (!this.header || !this.payload || !this.signature) {
|
|
@@ -34,7 +33,7 @@ export class KBJwt<
|
|
|
34
33
|
// this is for backward compatibility with version 06
|
|
35
34
|
!(
|
|
36
35
|
this.payload.sd_hash ||
|
|
37
|
-
(this.payload
|
|
36
|
+
('_sd_hash' in this.payload && this.payload._sd_hash)
|
|
38
37
|
)
|
|
39
38
|
) {
|
|
40
39
|
throw new SDJWTException('Invalid Key Binding Jwt');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './present';
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createHashMapping,
|
|
3
|
+
createHashMappingSync,
|
|
4
|
+
decodeSdJwt,
|
|
5
|
+
decodeSdJwtSync,
|
|
6
|
+
getSDAlgAndPayload,
|
|
7
|
+
splitSdJwt,
|
|
8
|
+
unpack,
|
|
9
|
+
unpackObj,
|
|
10
|
+
unpackSync,
|
|
11
|
+
} from '../decode';
|
|
12
|
+
import type { Extensible, HasherSync } from '../types';
|
|
13
|
+
import { type Hasher, type PresentationFrame, SD_SEPARATOR } from '../types';
|
|
14
|
+
import { Disclosure, SDJWTException } from '../utils';
|
|
15
|
+
|
|
16
|
+
// Presentable keys
|
|
17
|
+
// The presentable keys are the path of JSON object that are presentable in the SD JWT
|
|
18
|
+
// e.g. if the SD JWT has the following payload and set sd like this:
|
|
19
|
+
// {
|
|
20
|
+
// "foo": "bar", // sd
|
|
21
|
+
// "arr": [ // sd
|
|
22
|
+
// "1", // sd
|
|
23
|
+
// "2",
|
|
24
|
+
// {
|
|
25
|
+
// "a": "1" // sd
|
|
26
|
+
// }
|
|
27
|
+
// ],
|
|
28
|
+
// "test": {
|
|
29
|
+
// "zzz": "xxx" // sd
|
|
30
|
+
// }
|
|
31
|
+
// }
|
|
32
|
+
// The presentable keys are: ["arr", "arr.0", "arr.2.a", "foo", "test.zzz"]
|
|
33
|
+
export const presentableKeys = async (
|
|
34
|
+
rawPayload: Record<string, unknown>,
|
|
35
|
+
disclosures: Array<Disclosure>,
|
|
36
|
+
hasher: Hasher,
|
|
37
|
+
): Promise<string[]> => {
|
|
38
|
+
const { disclosureKeymap } = await unpack(rawPayload, disclosures, hasher);
|
|
39
|
+
return Object.keys(disclosureKeymap).sort();
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const presentableKeysSync = (
|
|
43
|
+
rawPayload: Record<string, unknown>,
|
|
44
|
+
disclosures: Array<Disclosure>,
|
|
45
|
+
hasher: HasherSync,
|
|
46
|
+
): string[] => {
|
|
47
|
+
const { disclosureKeymap } = unpackSync(rawPayload, disclosures, hasher);
|
|
48
|
+
return Object.keys(disclosureKeymap).sort();
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const present = async <T extends Record<string, unknown>>(
|
|
52
|
+
sdJwt: string,
|
|
53
|
+
presentFrame: PresentationFrame<T>,
|
|
54
|
+
hasher: Hasher,
|
|
55
|
+
): Promise<string> => {
|
|
56
|
+
const { jwt, kbJwt } = splitSdJwt(sdJwt);
|
|
57
|
+
const {
|
|
58
|
+
jwt: { payload },
|
|
59
|
+
disclosures,
|
|
60
|
+
} = await decodeSdJwt(sdJwt, hasher);
|
|
61
|
+
|
|
62
|
+
const { _sd_alg: alg } = getSDAlgAndPayload(payload);
|
|
63
|
+
const hash = { alg, hasher };
|
|
64
|
+
const keys = transformPresentationFrame(presentFrame);
|
|
65
|
+
|
|
66
|
+
// hashmap: <digest> => <disclosure>
|
|
67
|
+
// to match the digest with the disclosure
|
|
68
|
+
const hashmap = await createHashMapping(disclosures, hash);
|
|
69
|
+
const { disclosureKeymap } = await unpack(payload, disclosures, hasher);
|
|
70
|
+
const presentedDisclosures = keys
|
|
71
|
+
.map((k) => hashmap[disclosureKeymap[k]])
|
|
72
|
+
.filter((d) => d !== undefined);
|
|
73
|
+
|
|
74
|
+
return [
|
|
75
|
+
jwt,
|
|
76
|
+
...presentedDisclosures.map((d) => d.encode()),
|
|
77
|
+
kbJwt ?? '',
|
|
78
|
+
].join(SD_SEPARATOR);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const presentSync = <T extends Record<string, unknown>>(
|
|
82
|
+
sdJwt: string,
|
|
83
|
+
presentFrame: PresentationFrame<T>,
|
|
84
|
+
hasher: HasherSync,
|
|
85
|
+
): string => {
|
|
86
|
+
const { jwt, kbJwt } = splitSdJwt(sdJwt);
|
|
87
|
+
const {
|
|
88
|
+
jwt: { payload },
|
|
89
|
+
disclosures,
|
|
90
|
+
} = decodeSdJwtSync(sdJwt, hasher);
|
|
91
|
+
|
|
92
|
+
const { _sd_alg: alg } = getSDAlgAndPayload(payload);
|
|
93
|
+
const hash = { alg, hasher };
|
|
94
|
+
const keys = transformPresentationFrame(presentFrame);
|
|
95
|
+
|
|
96
|
+
// hashmap: <digest> => <disclosure>
|
|
97
|
+
// to match the digest with the disclosure
|
|
98
|
+
const hashmap = createHashMappingSync(disclosures, hash);
|
|
99
|
+
const { disclosureKeymap } = unpackSync(payload, disclosures, hasher);
|
|
100
|
+
|
|
101
|
+
const presentedDisclosures = keys
|
|
102
|
+
.map((k) => hashmap[disclosureKeymap[k]])
|
|
103
|
+
.filter((d) => d !== undefined);
|
|
104
|
+
|
|
105
|
+
return [
|
|
106
|
+
jwt,
|
|
107
|
+
...presentedDisclosures.map((d) => d.encode()),
|
|
108
|
+
kbJwt ?? '',
|
|
109
|
+
].join(SD_SEPARATOR);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Transform the object keys into an array of strings. We are not sorting the array in any way.
|
|
114
|
+
* @param obj The object to transform
|
|
115
|
+
* @param prefix The prefix to add to the keys
|
|
116
|
+
* @returns
|
|
117
|
+
*/
|
|
118
|
+
export const transformPresentationFrame = (
|
|
119
|
+
obj: PresentationFrame<Extensible>,
|
|
120
|
+
prefix = '',
|
|
121
|
+
): string[] => {
|
|
122
|
+
return Object.entries(obj).reduce<string[]>((acc, [key, value]) => {
|
|
123
|
+
const newPrefix = prefix ? `${prefix}.${key}` : key;
|
|
124
|
+
if (typeof value === 'boolean') {
|
|
125
|
+
// only add it, when it's true
|
|
126
|
+
if (value) {
|
|
127
|
+
acc.push(newPrefix);
|
|
128
|
+
}
|
|
129
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
130
|
+
acc.push(
|
|
131
|
+
newPrefix,
|
|
132
|
+
...transformPresentationFrame(
|
|
133
|
+
value as PresentationFrame<Extensible>,
|
|
134
|
+
newPrefix,
|
|
135
|
+
),
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return acc;
|
|
139
|
+
}, []);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export type SerializedDisclosure = {
|
|
143
|
+
digest: string;
|
|
144
|
+
encoded: string;
|
|
145
|
+
salt: string;
|
|
146
|
+
key: string | undefined;
|
|
147
|
+
value: unknown;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export const createHashMappingForSerializedDisclosure = (
|
|
151
|
+
disclosures: SerializedDisclosure[],
|
|
152
|
+
) => {
|
|
153
|
+
const map: Record<string, Disclosure> = {};
|
|
154
|
+
for (let i = 0; i < disclosures.length; i++) {
|
|
155
|
+
const disclosure = disclosures[i];
|
|
156
|
+
const { digest, encoded, key, salt, value } = disclosure;
|
|
157
|
+
// we made Disclosure to fit the interface of unpack
|
|
158
|
+
map[digest] = Disclosure.fromArray(
|
|
159
|
+
key ? [salt, key, value] : [salt, value],
|
|
160
|
+
{ digest, encoded },
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
return map;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* This function selects the serialized disclosures from the payload
|
|
168
|
+
* and array of serialized disclosure based on the presentation frame.
|
|
169
|
+
* If you want to know what is serialized disclosures, check type SerializedDisclosure.
|
|
170
|
+
* @param payload: Record<string, unknown>
|
|
171
|
+
* @param disclosures: SerializedDisclosure[]
|
|
172
|
+
* @param presentationFrame: PresentationFrame<T>
|
|
173
|
+
*/
|
|
174
|
+
export const selectDisclosures = <T extends Record<string, unknown>>(
|
|
175
|
+
payload: Record<string, unknown>,
|
|
176
|
+
disclosures: SerializedDisclosure[],
|
|
177
|
+
presentationFrame: PresentationFrame<T>,
|
|
178
|
+
) => {
|
|
179
|
+
if (disclosures.length === 0) {
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const hashmap = createHashMappingForSerializedDisclosure(disclosures);
|
|
184
|
+
const { disclosureKeymap } = unpackObj(payload, hashmap);
|
|
185
|
+
const keys = transformPresentationFrame(presentationFrame);
|
|
186
|
+
|
|
187
|
+
const presentedDisclosures = keys
|
|
188
|
+
.map((k) => hashmap[disclosureKeymap[k]])
|
|
189
|
+
.filter((d) => d !== undefined);
|
|
190
|
+
|
|
191
|
+
const selectedDisclosures: SerializedDisclosure[] = presentedDisclosures.map(
|
|
192
|
+
(d) => {
|
|
193
|
+
const { salt, key, value, _digest } = d;
|
|
194
|
+
if (!_digest) {
|
|
195
|
+
throw new SDJWTException(
|
|
196
|
+
'Implementation error: _digest is not defined',
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
digest: _digest,
|
|
201
|
+
encoded: d.encode(),
|
|
202
|
+
salt,
|
|
203
|
+
key,
|
|
204
|
+
value,
|
|
205
|
+
};
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
return selectedDisclosures;
|
|
210
|
+
};
|
package/src/sdjwt.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import { createHashMapping, getSDAlgAndPayload, unpack } from '
|
|
2
|
-
import {
|
|
1
|
+
import { createHashMapping, getSDAlgAndPayload, unpack } from './decode';
|
|
2
|
+
import { createDecoy } from './decoy';
|
|
3
|
+
import { Jwt } from './jwt';
|
|
4
|
+
import { KBJwt } from './kbjwt';
|
|
5
|
+
import { transformPresentationFrame } from './present';
|
|
3
6
|
import {
|
|
4
7
|
type DisclosureFrame,
|
|
5
8
|
type Hasher,
|
|
@@ -13,11 +16,8 @@ import {
|
|
|
13
16
|
SD_LIST_KEY,
|
|
14
17
|
SD_SEPARATOR,
|
|
15
18
|
type SDJWTCompact,
|
|
16
|
-
} from '
|
|
17
|
-
import { Disclosure, SDJWTException } from '
|
|
18
|
-
import { createDecoy } from './decoy';
|
|
19
|
-
import { Jwt } from './jwt';
|
|
20
|
-
import { KBJwt } from './kbjwt';
|
|
19
|
+
} from './types';
|
|
20
|
+
import { Disclosure, SDJWTException } from './utils';
|
|
21
21
|
|
|
22
22
|
export type SDJwtData<
|
|
23
23
|
Header extends Record<string, unknown>,
|
|
@@ -81,7 +81,7 @@ export class SDJwt<
|
|
|
81
81
|
const { _sd_alg } = getSDAlgAndPayload(jwt.payload);
|
|
82
82
|
|
|
83
83
|
const disclosures = await Promise.all(
|
|
84
|
-
|
|
84
|
+
encodedDisclosures.map((ed) =>
|
|
85
85
|
Disclosure.fromEncode(ed, { alg: _sd_alg, hasher }),
|
|
86
86
|
),
|
|
87
87
|
);
|
|
@@ -221,8 +221,9 @@ export const listKeys = (obj: Record<string, unknown>, prefix = '') => {
|
|
|
221
221
|
const newKey = prefix ? `${prefix}.${key}` : key;
|
|
222
222
|
keys.push(newKey);
|
|
223
223
|
|
|
224
|
-
|
|
225
|
-
|
|
224
|
+
const value = obj[key];
|
|
225
|
+
if (value && typeof value === 'object') {
|
|
226
|
+
keys.push(...listKeys(value as Record<string, unknown>, newKey));
|
|
226
227
|
}
|
|
227
228
|
}
|
|
228
229
|
return keys;
|