@sd-jwt/core 0.19.1-next.8 → 0.20.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 +28 -0
- package/dist/index.d.mts +22 -9
- package/dist/index.d.ts +22 -9
- package/dist/index.js +89 -31
- package/dist/index.mjs +89 -31
- package/package.json +2 -2
- package/src/decode/decode.ts +14 -4
- package/src/index.ts +69 -18
- package/src/jwt.ts +7 -0
- package/src/kbjwt.ts +15 -10
- package/src/test/index.spec.ts +192 -0
- package/src/test/kbjwt.spec.ts +211 -0
- package/src/utils/disclosure.ts +10 -7
- package/src/utils/strict-json.ts +17 -0
- package/test/app-e2e.spec.ts +31 -0
package/src/decode/decode.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import type { HasherAndAlgSync, HasherSync } from '../types';
|
|
2
1
|
import {
|
|
3
2
|
type Hasher,
|
|
4
3
|
type HasherAndAlg,
|
|
4
|
+
type HasherAndAlgSync,
|
|
5
|
+
type HasherSync,
|
|
6
|
+
IANA_HASH_ALGORITHMS,
|
|
5
7
|
SD_DIGEST,
|
|
6
8
|
SD_LIST_KEY,
|
|
7
9
|
SD_SEPARATOR,
|
|
8
10
|
} from '../types';
|
|
9
|
-
import {
|
|
11
|
+
import { Disclosure, SDJWTException } from '../utils';
|
|
12
|
+
import { decodeBase64urlJsonStrict } from '../utils/strict-json';
|
|
10
13
|
|
|
11
14
|
export const decodeJwt = <
|
|
12
15
|
H extends Record<string, unknown>,
|
|
@@ -20,8 +23,8 @@ export const decodeJwt = <
|
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
return {
|
|
23
|
-
header:
|
|
24
|
-
payload:
|
|
26
|
+
header: decodeBase64urlJsonStrict(header, 'Invalid JWT as input'),
|
|
27
|
+
payload: decodeBase64urlJsonStrict(payload, 'Invalid JWT as input'),
|
|
25
28
|
signature: signature,
|
|
26
29
|
};
|
|
27
30
|
};
|
|
@@ -319,6 +322,13 @@ export const getSDAlgAndPayload = (SdJwtPayload: Record<string, unknown>) => {
|
|
|
319
322
|
// This is for compatibility
|
|
320
323
|
return { _sd_alg: 'sha-256', payload };
|
|
321
324
|
}
|
|
325
|
+
if (
|
|
326
|
+
!IANA_HASH_ALGORITHMS.includes(
|
|
327
|
+
_sd_alg as (typeof IANA_HASH_ALGORITHMS)[number],
|
|
328
|
+
)
|
|
329
|
+
) {
|
|
330
|
+
throw new SDJWTException(`Invalid _sd_alg: ${_sd_alg}`);
|
|
331
|
+
}
|
|
322
332
|
return { _sd_alg, payload };
|
|
323
333
|
};
|
|
324
334
|
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
type KBOptions,
|
|
13
13
|
type PresentationFrame,
|
|
14
14
|
type SafeVerifyResult,
|
|
15
|
+
SD_DECOY,
|
|
16
|
+
SD_DIGEST,
|
|
15
17
|
type SDJWTCompact,
|
|
16
18
|
type SDJWTConfig,
|
|
17
19
|
type Signer,
|
|
@@ -19,12 +21,12 @@ import {
|
|
|
19
21
|
type VerificationErrorCode,
|
|
20
22
|
} from './types';
|
|
21
23
|
import {
|
|
22
|
-
base64urlDecode,
|
|
23
24
|
base64urlEncode,
|
|
24
25
|
ensureError,
|
|
25
26
|
SDJWTException,
|
|
26
27
|
uint8ArrayToBase64Url,
|
|
27
28
|
} from './utils';
|
|
29
|
+
import { decodeBase64urlJsonStrict } from './utils/strict-json';
|
|
28
30
|
|
|
29
31
|
export * from './decode';
|
|
30
32
|
export * from './decoy';
|
|
@@ -120,9 +122,8 @@ export class SDJwtInstance<ExtendedPayload extends SdJwtPayload, T = unknown> {
|
|
|
120
122
|
throw new SDJWTException('sign alogrithm not specified');
|
|
121
123
|
}
|
|
122
124
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
125
|
+
this.validateReservedFields<Payload>(payload);
|
|
126
|
+
this.validateDisclosureFrame<Payload>(disclosureFrame);
|
|
126
127
|
|
|
127
128
|
const hasher = this.userConfig.hasher;
|
|
128
129
|
const hashAlg = this.userConfig.hashAlg ?? SDJwtInstance.DEFAULT_hashAlg;
|
|
@@ -157,12 +158,35 @@ export class SDJwtInstance<ExtendedPayload extends SdJwtPayload, T = unknown> {
|
|
|
157
158
|
}
|
|
158
159
|
|
|
159
160
|
/**
|
|
160
|
-
* Validates if the
|
|
161
|
-
* @param
|
|
161
|
+
* Validates if the payload contains any reserved claim names. If so it will throw an error.
|
|
162
|
+
* @param payload
|
|
162
163
|
* @returns
|
|
163
164
|
*/
|
|
164
|
-
protected validateReservedFields<T extends ExtendedPayload>(
|
|
165
|
-
|
|
165
|
+
protected validateReservedFields<T extends ExtendedPayload>(payload: T) {
|
|
166
|
+
const reservedFields = new Set([SD_DIGEST, '_sd_alg', SD_DECOY]);
|
|
167
|
+
|
|
168
|
+
const visit = (node: unknown) => {
|
|
169
|
+
if (!node || typeof node !== 'object') {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
for (const [key, value] of Object.entries(
|
|
174
|
+
node as Record<string, unknown>,
|
|
175
|
+
)) {
|
|
176
|
+
if (reservedFields.has(key)) {
|
|
177
|
+
throw new SDJWTException(
|
|
178
|
+
`Reserved field name "${key}" is not allowed`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
visit(value);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
visit(payload);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
protected validateDisclosureFrame<T extends ExtendedPayload>(
|
|
189
|
+
_disclosureFrame?: DisclosureFrame<T>,
|
|
166
190
|
) {
|
|
167
191
|
return;
|
|
168
192
|
}
|
|
@@ -242,6 +266,7 @@ export class SDJwtInstance<ExtendedPayload extends SdJwtPayload, T = unknown> {
|
|
|
242
266
|
verifier: this.userConfig.kbVerifier,
|
|
243
267
|
payload,
|
|
244
268
|
nonce: options.keyBindingNonce,
|
|
269
|
+
options,
|
|
245
270
|
});
|
|
246
271
|
if (!kb) {
|
|
247
272
|
throw new Error('signature is not valid');
|
|
@@ -414,6 +439,7 @@ export class SDJwtInstance<ExtendedPayload extends SdJwtPayload, T = unknown> {
|
|
|
414
439
|
verifier: this.userConfig.kbVerifier,
|
|
415
440
|
payload,
|
|
416
441
|
nonce: options.keyBindingNonce,
|
|
442
|
+
options,
|
|
417
443
|
});
|
|
418
444
|
if (!kbResult) {
|
|
419
445
|
addError(
|
|
@@ -627,9 +653,8 @@ export class SDJwtGeneralJSONInstance<ExtendedPayload extends SdJwtPayload> {
|
|
|
627
653
|
throw new SDJWTException('SaltGenerator not found');
|
|
628
654
|
}
|
|
629
655
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
}
|
|
656
|
+
this.validateReservedFields<Payload>(payload);
|
|
657
|
+
this.validateDisclosureFrame<Payload>(disclosureFrame);
|
|
633
658
|
|
|
634
659
|
const hasher = this.userConfig.hasher;
|
|
635
660
|
const hashAlg = this.userConfig.hashAlg ?? SDJwtInstance.DEFAULT_hashAlg;
|
|
@@ -641,12 +666,15 @@ export class SDJwtGeneralJSONInstance<ExtendedPayload extends SdJwtPayload> {
|
|
|
641
666
|
this.userConfig.saltGenerator,
|
|
642
667
|
);
|
|
643
668
|
|
|
644
|
-
const encodedDisclosures = disclosures.map((d) => d.encode());
|
|
645
669
|
const encodedSDJwtPayload = this.encodeObj({
|
|
646
670
|
...packedClaims,
|
|
647
671
|
_sd_alg: disclosureFrame ? hashAlg : undefined,
|
|
648
672
|
});
|
|
649
673
|
|
|
674
|
+
const encodedDisclosures = disclosures.map((disclosure) =>
|
|
675
|
+
disclosure.encode(),
|
|
676
|
+
);
|
|
677
|
+
|
|
650
678
|
const signatures = await Promise.all(
|
|
651
679
|
options.sigs.map(async (s) => {
|
|
652
680
|
const { signer, alg, kid, header } = s;
|
|
@@ -658,7 +686,6 @@ export class SDJwtGeneralJSONInstance<ExtendedPayload extends SdJwtPayload> {
|
|
|
658
686
|
|
|
659
687
|
return {
|
|
660
688
|
protected: encodedProtectedHeader,
|
|
661
|
-
kid,
|
|
662
689
|
signature,
|
|
663
690
|
};
|
|
664
691
|
}),
|
|
@@ -674,12 +701,35 @@ export class SDJwtGeneralJSONInstance<ExtendedPayload extends SdJwtPayload> {
|
|
|
674
701
|
}
|
|
675
702
|
|
|
676
703
|
/**
|
|
677
|
-
* Validates if the
|
|
678
|
-
* @param
|
|
704
|
+
* Validates if the payload contains any reserved claim names. If so it will throw an error.
|
|
705
|
+
* @param payload
|
|
679
706
|
* @returns
|
|
680
707
|
*/
|
|
681
|
-
protected validateReservedFields<T extends ExtendedPayload>(
|
|
682
|
-
|
|
708
|
+
protected validateReservedFields<T extends ExtendedPayload>(payload: T) {
|
|
709
|
+
const reservedFields = new Set([SD_DIGEST, '_sd_alg', SD_DECOY]);
|
|
710
|
+
|
|
711
|
+
const visit = (node: unknown) => {
|
|
712
|
+
if (!node || typeof node !== 'object') {
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
for (const [key, value] of Object.entries(
|
|
717
|
+
node as Record<string, unknown>,
|
|
718
|
+
)) {
|
|
719
|
+
if (reservedFields.has(key)) {
|
|
720
|
+
throw new SDJWTException(
|
|
721
|
+
`Reserved field name "${key}" is not allowed`,
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
visit(value);
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
visit(payload);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
protected validateDisclosureFrame<T extends ExtendedPayload>(
|
|
732
|
+
_disclosureFrame?: DisclosureFrame<T>,
|
|
683
733
|
) {
|
|
684
734
|
return;
|
|
685
735
|
}
|
|
@@ -774,6 +824,7 @@ export class SDJwtGeneralJSONInstance<ExtendedPayload extends SdJwtPayload> {
|
|
|
774
824
|
verifier: this.userConfig.kbVerifier,
|
|
775
825
|
payload,
|
|
776
826
|
nonce: options.keyBindingNonce,
|
|
827
|
+
options,
|
|
777
828
|
});
|
|
778
829
|
if (!kb) {
|
|
779
830
|
throw new Error('signature is not valid');
|
|
@@ -833,7 +884,7 @@ export class SDJwtGeneralJSONInstance<ExtendedPayload extends SdJwtPayload> {
|
|
|
833
884
|
`${encodedHeader}.${payload}`,
|
|
834
885
|
signature,
|
|
835
886
|
);
|
|
836
|
-
const header =
|
|
887
|
+
const header = decodeBase64urlJsonStrict(encodedHeader, 'Invalid JWT');
|
|
837
888
|
return { verified, header };
|
|
838
889
|
}),
|
|
839
890
|
);
|
package/src/jwt.ts
CHANGED
|
@@ -37,6 +37,13 @@ export type VerifierOptions = {
|
|
|
37
37
|
*/
|
|
38
38
|
keyBindingNonce?: string;
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* disable the verification of the status claim in the payload.
|
|
42
|
+
*
|
|
43
|
+
* @default false
|
|
44
|
+
*/
|
|
45
|
+
disableStatusVerification?: boolean;
|
|
46
|
+
|
|
40
47
|
/**
|
|
41
48
|
* any other custom options
|
|
42
49
|
*/
|
package/src/kbjwt.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Jwt } from './jwt';
|
|
1
|
+
import { Jwt, type VerifierOptions } from './jwt';
|
|
2
2
|
import {
|
|
3
3
|
KB_JWT_TYP,
|
|
4
4
|
type KbVerifier,
|
|
@@ -17,6 +17,11 @@ export class KBJwt<
|
|
|
17
17
|
verifier: KbVerifier;
|
|
18
18
|
payload: Record<string, unknown>;
|
|
19
19
|
nonce: string;
|
|
20
|
+
/**
|
|
21
|
+
* Options forwarded to the common JWT verification, e.g. currentDate and
|
|
22
|
+
* skewSeconds used to validate the iat, nbf and exp claims.
|
|
23
|
+
*/
|
|
24
|
+
options?: VerifierOptions;
|
|
20
25
|
}) {
|
|
21
26
|
if (!this.header || !this.payload || !this.signature) {
|
|
22
27
|
throw new SDJWTException('Verify Error: Invalid JWT');
|
|
@@ -39,19 +44,19 @@ export class KBJwt<
|
|
|
39
44
|
throw new SDJWTException('Invalid Key Binding Jwt');
|
|
40
45
|
}
|
|
41
46
|
|
|
42
|
-
const data = this.getUnsignedToken();
|
|
43
|
-
const verified = await values.verifier(
|
|
44
|
-
data,
|
|
45
|
-
this.signature,
|
|
46
|
-
values.payload,
|
|
47
|
-
);
|
|
48
|
-
if (!verified) {
|
|
49
|
-
throw new SDJWTException('Verify Error: Invalid JWT Signature');
|
|
50
|
-
}
|
|
51
47
|
if (this.payload.nonce !== values.nonce) {
|
|
52
48
|
throw new SDJWTException('Verify Error: Invalid Nonce');
|
|
53
49
|
}
|
|
54
50
|
|
|
51
|
+
// Delegate signature verification and common JWT claim validation
|
|
52
|
+
// (iat, nbf, exp) to the shared Jwt.verify implementation. The kbVerifier
|
|
53
|
+
// needs the kb+jwt payload (e.g. the holder's cnf key), so we wrap it to
|
|
54
|
+
// forward values.payload instead of the base verifier's options argument.
|
|
55
|
+
await this.verify(
|
|
56
|
+
(data, sig) => values.verifier(data, sig, values.payload),
|
|
57
|
+
values.options,
|
|
58
|
+
);
|
|
59
|
+
|
|
55
60
|
return { payload: this.payload, header: this.header };
|
|
56
61
|
}
|
|
57
62
|
|
package/src/test/index.spec.ts
CHANGED
|
@@ -100,6 +100,32 @@ describe('index', () => {
|
|
|
100
100
|
expect(credential).toBeDefined();
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
+
test('issue rejects reserved disclosure frame keys', async () => {
|
|
104
|
+
const { signer, verifier } = createSignerVerifier();
|
|
105
|
+
const sdjwt = new SDJwtInstance<SdJwtPayload>({
|
|
106
|
+
signer,
|
|
107
|
+
signAlg: 'EdDSA',
|
|
108
|
+
verifier,
|
|
109
|
+
hasher: digest,
|
|
110
|
+
saltGenerator: generateSalt,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await expect(
|
|
114
|
+
sdjwt.issue(
|
|
115
|
+
{
|
|
116
|
+
foo: 'bar',
|
|
117
|
+
_sd: 'reserved',
|
|
118
|
+
iss: 'Issuer',
|
|
119
|
+
iat: Math.floor(Date.now() / 1000),
|
|
120
|
+
vct: '',
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
_sd: ['foo'],
|
|
124
|
+
},
|
|
125
|
+
),
|
|
126
|
+
).rejects.toThrow('Reserved field name "_sd" is not allowed');
|
|
127
|
+
});
|
|
128
|
+
|
|
103
129
|
test('verify failed', async () => {
|
|
104
130
|
const { signer } = createSignerVerifier();
|
|
105
131
|
const { publicKey } = Crypto.generateKeyPairSync('ed25519');
|
|
@@ -269,6 +295,140 @@ describe('index', () => {
|
|
|
269
295
|
expect(results).toBeDefined();
|
|
270
296
|
});
|
|
271
297
|
|
|
298
|
+
test('verify rejects an expired kbJwt during full verification', async () => {
|
|
299
|
+
const { signer, verifier } = createSignerVerifier();
|
|
300
|
+
const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519');
|
|
301
|
+
|
|
302
|
+
const kbVerifier: KbVerifier = async (
|
|
303
|
+
data: string,
|
|
304
|
+
sig: string,
|
|
305
|
+
payload: JwtPayload,
|
|
306
|
+
) => {
|
|
307
|
+
if (!payload.cnf) throw Error('key binding not supported');
|
|
308
|
+
return Crypto.verify(
|
|
309
|
+
null,
|
|
310
|
+
Buffer.from(data),
|
|
311
|
+
(await importJWK(payload.cnf.jwk as JWK, 'EdDSA')) as KeyLike,
|
|
312
|
+
Buffer.from(sig, 'base64url'),
|
|
313
|
+
);
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const kbSigner = (data: string) => {
|
|
317
|
+
const sig = Crypto.sign(null, Buffer.from(data), privateKey);
|
|
318
|
+
return Buffer.from(sig).toString('base64url');
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const sdjwt = new SDJwtInstance<SdJwtPayload>({
|
|
322
|
+
signer,
|
|
323
|
+
signAlg: 'EdDSA',
|
|
324
|
+
verifier,
|
|
325
|
+
hasher: digest,
|
|
326
|
+
saltGenerator: generateSalt,
|
|
327
|
+
kbSigner,
|
|
328
|
+
kbVerifier,
|
|
329
|
+
kbSignAlg: 'EdDSA',
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const now = Math.floor(Date.now() / 1000);
|
|
333
|
+
const credential = await sdjwt.issue(
|
|
334
|
+
{
|
|
335
|
+
foo: 'bar',
|
|
336
|
+
iat: now,
|
|
337
|
+
cnf: { jwk: await exportJWK(publicKey) },
|
|
338
|
+
},
|
|
339
|
+
{ _sd: ['foo'] },
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
const presentation = await sdjwt.present(
|
|
343
|
+
credential,
|
|
344
|
+
{ foo: true },
|
|
345
|
+
{
|
|
346
|
+
kb: {
|
|
347
|
+
payload: {
|
|
348
|
+
aud: '1',
|
|
349
|
+
iat: now - 7200,
|
|
350
|
+
nonce: '342',
|
|
351
|
+
// kb+jwt expired one hour ago
|
|
352
|
+
exp: now - 3600,
|
|
353
|
+
} as never,
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
await expect(
|
|
359
|
+
sdjwt.verify(presentation, {
|
|
360
|
+
requiredClaimKeys: ['foo'],
|
|
361
|
+
keyBindingNonce: '342',
|
|
362
|
+
}),
|
|
363
|
+
).rejects.toThrow('Verify Error: JWT is expired');
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test('verify accepts a kbJwt with a future exp during full verification', async () => {
|
|
367
|
+
const { signer, verifier } = createSignerVerifier();
|
|
368
|
+
const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519');
|
|
369
|
+
|
|
370
|
+
const kbVerifier: KbVerifier = async (
|
|
371
|
+
data: string,
|
|
372
|
+
sig: string,
|
|
373
|
+
payload: JwtPayload,
|
|
374
|
+
) => {
|
|
375
|
+
if (!payload.cnf) throw Error('key binding not supported');
|
|
376
|
+
return Crypto.verify(
|
|
377
|
+
null,
|
|
378
|
+
Buffer.from(data),
|
|
379
|
+
(await importJWK(payload.cnf.jwk as JWK, 'EdDSA')) as KeyLike,
|
|
380
|
+
Buffer.from(sig, 'base64url'),
|
|
381
|
+
);
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const kbSigner = (data: string) => {
|
|
385
|
+
const sig = Crypto.sign(null, Buffer.from(data), privateKey);
|
|
386
|
+
return Buffer.from(sig).toString('base64url');
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const sdjwt = new SDJwtInstance<SdJwtPayload>({
|
|
390
|
+
signer,
|
|
391
|
+
signAlg: 'EdDSA',
|
|
392
|
+
verifier,
|
|
393
|
+
hasher: digest,
|
|
394
|
+
saltGenerator: generateSalt,
|
|
395
|
+
kbSigner,
|
|
396
|
+
kbVerifier,
|
|
397
|
+
kbSignAlg: 'EdDSA',
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const now = Math.floor(Date.now() / 1000);
|
|
401
|
+
const credential = await sdjwt.issue(
|
|
402
|
+
{
|
|
403
|
+
foo: 'bar',
|
|
404
|
+
iat: now,
|
|
405
|
+
cnf: { jwk: await exportJWK(publicKey) },
|
|
406
|
+
},
|
|
407
|
+
{ _sd: ['foo'] },
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
const presentation = await sdjwt.present(
|
|
411
|
+
credential,
|
|
412
|
+
{ foo: true },
|
|
413
|
+
{
|
|
414
|
+
kb: {
|
|
415
|
+
payload: {
|
|
416
|
+
aud: '1',
|
|
417
|
+
iat: now,
|
|
418
|
+
nonce: '342',
|
|
419
|
+
exp: now + 3600,
|
|
420
|
+
} as never,
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
const results = await sdjwt.verify(presentation, {
|
|
426
|
+
requiredClaimKeys: ['foo'],
|
|
427
|
+
keyBindingNonce: '342',
|
|
428
|
+
});
|
|
429
|
+
expect(results.kb).toBeDefined();
|
|
430
|
+
});
|
|
431
|
+
|
|
272
432
|
test('Hasher not found', async () => {
|
|
273
433
|
const sdjwt = new SDJwtInstance<SdJwtPayload>({});
|
|
274
434
|
try {
|
|
@@ -290,6 +450,38 @@ describe('index', () => {
|
|
|
290
450
|
}
|
|
291
451
|
});
|
|
292
452
|
|
|
453
|
+
test('decode rejects invalid _sd_alg values', async () => {
|
|
454
|
+
const { signer, verifier } = createSignerVerifier();
|
|
455
|
+
const sdjwt = new SDJwtInstance<SdJwtPayload>({
|
|
456
|
+
signer,
|
|
457
|
+
signAlg: 'EdDSA',
|
|
458
|
+
verifier,
|
|
459
|
+
hasher: digest,
|
|
460
|
+
saltGenerator: generateSalt,
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
const credential = await sdjwt.issue(
|
|
464
|
+
{
|
|
465
|
+
foo: 'bar',
|
|
466
|
+
iss: 'Issuer',
|
|
467
|
+
iat: Math.floor(Date.now() / 1000),
|
|
468
|
+
vct: '',
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
_sd: ['foo'],
|
|
472
|
+
},
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
const parts = credential.split('.');
|
|
476
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
|
|
477
|
+
payload._sd_alg = 'SHA-256';
|
|
478
|
+
parts[1] = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
479
|
+
|
|
480
|
+
await expect(sdjwt.decode(parts.join('.'))).rejects.toThrow(
|
|
481
|
+
'Invalid _sd_alg: SHA-256',
|
|
482
|
+
);
|
|
483
|
+
});
|
|
484
|
+
|
|
293
485
|
test('SaltGenerator not found', async () => {
|
|
294
486
|
const sdjwt = new SDJwtInstance<SdJwtPayload>({
|
|
295
487
|
hasher: digest,
|