@sd-jwt/core 0.20.1 → 0.21.0-alpha-20260916100413

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.
Files changed (68) hide show
  1. package/LICENSE +2 -2
  2. package/README.md +82 -80
  3. package/dist/index.cjs +1526 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +696 -0
  6. package/dist/index.d.mts +537 -527
  7. package/dist/index.mjs +1422 -1998
  8. package/dist/index.mjs.map +1 -0
  9. package/package.json +29 -49
  10. package/CHANGELOG.md +0 -39
  11. package/dist/index.d.ts +0 -686
  12. package/dist/index.js +0 -2100
  13. package/src/decode/decode.ts +0 -407
  14. package/src/decode/index.ts +0 -1
  15. package/src/decoy.ts +0 -15
  16. package/src/flattenJSON.ts +0 -99
  17. package/src/generalJSON.ts +0 -157
  18. package/src/index.ts +0 -1000
  19. package/src/jwt.ts +0 -286
  20. package/src/kbjwt.ts +0 -101
  21. package/src/present/index.ts +0 -1
  22. package/src/present/present.ts +0 -216
  23. package/src/sdjwt.ts +0 -384
  24. package/src/test/decode/decode.spec.ts +0 -197
  25. package/src/test/decoy.spec.ts +0 -30
  26. package/src/test/flattenJSON.spec.ts +0 -122
  27. package/src/test/generalJSON.spec.ts +0 -198
  28. package/src/test/index.spec.ts +0 -986
  29. package/src/test/jwt.spec.ts +0 -301
  30. package/src/test/kbjwt.spec.ts +0 -555
  31. package/src/test/pass.spec.ts +0 -7
  32. package/src/test/present/present.spec.ts +0 -305
  33. package/src/test/sdjwt.spec.ts +0 -384
  34. package/src/test/types/type.spec.ts +0 -88
  35. package/src/test/utils/base64url.spec.ts +0 -33
  36. package/src/test/utils/disclosure.spec.ts +0 -170
  37. package/src/test/utils/error.spec.ts +0 -15
  38. package/src/types/index.ts +0 -2
  39. package/src/types/type.ts +0 -270
  40. package/src/types/verification-error.ts +0 -55
  41. package/src/utils/base64url.ts +0 -6
  42. package/src/utils/disclosure.ts +0 -121
  43. package/src/utils/error.ts +0 -25
  44. package/src/utils/index.ts +0 -3
  45. package/src/utils/strict-json.ts +0 -17
  46. package/test/app-e2e.spec.ts +0 -292
  47. package/test/array_data_types.json +0 -31
  48. package/test/array_full_sd.json +0 -21
  49. package/test/array_in_sd.json +0 -13
  50. package/test/array_nested_in_plain.json +0 -29
  51. package/test/array_none_disclosed.json +0 -17
  52. package/test/array_of_nulls.json +0 -15
  53. package/test/array_of_objects.json +0 -65
  54. package/test/array_of_scalars.json +0 -19
  55. package/test/array_recursive_sd.json +0 -35
  56. package/test/array_recursive_sd_some_disclosed.json +0 -63
  57. package/test/complex.json +0 -43
  58. package/test/header_mod.json +0 -48
  59. package/test/json_serialization.json +0 -48
  60. package/test/key_binding.json +0 -48
  61. package/test/no_sd.json +0 -36
  62. package/test/object_data_types.json +0 -62
  63. package/test/recursions.json +0 -117
  64. package/test/rfc9901-audit-fixes.spec.ts +0 -252
  65. package/test/rfc9901-validation.spec.ts +0 -150
  66. package/test/tsconfig.json +0 -4
  67. package/tsconfig.json +0 -7
  68. package/vitest.config.mts +0 -4
@@ -1,252 +0,0 @@
1
- import Crypto from 'node:crypto';
2
- import { hasher as digest, generateSalt } from '@owf/crypto';
3
- import { describe, expect, test } from 'vitest';
4
- import { unpackObj } from '../src/decode';
5
- import { GeneralJSON, Jwt, KBJwt, SDJwtInstance } from '../src/index';
6
- import { selectDisclosures, transformPresentationFrame } from '../src/present';
7
- import type { Signer, Verifier } from '../src/types';
8
- import { Disclosure } from '../src/utils';
9
-
10
- const createSignerVerifier = () => {
11
- const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519');
12
- const signer: Signer = async (data: string) => {
13
- const sig = Crypto.sign(null, Buffer.from(data), privateKey);
14
- return Buffer.from(sig).toString('base64url');
15
- };
16
- const verifier: Verifier = async (data: string, sig: string) => {
17
- return Crypto.verify(
18
- null,
19
- Buffer.from(data),
20
- publicKey,
21
- Buffer.from(sig, 'base64url'),
22
- );
23
- };
24
- return { signer, verifier };
25
- };
26
-
27
- describe('RFC 9901 audit fixes', () => {
28
- test('rejects malformed disclosure containers', () => {
29
- const objectDisclosure = Disclosure.fromArray(['salt', 'name', 'Alice'], {
30
- digest: 'object-digest',
31
- encoded: 'object-disclosure',
32
- });
33
- const arrayDisclosure = Disclosure.fromArray(['salt', 'Alice'], {
34
- digest: 'array-digest',
35
- encoded: 'array-disclosure',
36
- });
37
-
38
- expect(() => unpackObj({ _sd: 'object-digest' }, {})).toThrow(
39
- 'Invalid _sd claim: expected array of strings',
40
- );
41
- expect(() =>
42
- unpackObj({ items: [{ '...': 'array-digest', extra: true }] }, {}),
43
- ).toThrow('Invalid array disclosure placeholder');
44
- expect(() =>
45
- unpackObj({ _sd: ['array-digest'] }, { 'array-digest': arrayDisclosure }),
46
- ).toThrow('Array disclosure cannot be used as an object property');
47
- expect(() =>
48
- unpackObj(
49
- { items: [{ '...': 'object-digest' }] },
50
- { 'object-digest': objectDisclosure },
51
- ),
52
- ).toThrow('Object-property disclosure cannot be used as an array element');
53
- });
54
-
55
- test('rejects disclosed claim name collisions at the same object level', () => {
56
- const first = Disclosure.fromArray(['salt-1', 'name', 'Alice'], {
57
- digest: 'digest-1',
58
- encoded: 'disclosure-1',
59
- });
60
- const second = Disclosure.fromArray(['salt-2', 'name', 'Mallory'], {
61
- digest: 'digest-2',
62
- encoded: 'disclosure-2',
63
- });
64
-
65
- expect(() =>
66
- unpackObj(
67
- { _sd: ['digest-1', 'digest-2'] },
68
- { 'digest-1': first, 'digest-2': second },
69
- ),
70
- ).toThrow('Disclosed claim name "name" conflicts with another disclosure');
71
- });
72
-
73
- test('validates JWT claims after disclosure processing', async () => {
74
- const { signer, verifier } = createSignerVerifier();
75
- const sdjwt = new SDJwtInstance({
76
- signer,
77
- signAlg: 'EdDSA',
78
- verifier,
79
- hasher: digest,
80
- saltGenerator: generateSalt,
81
- });
82
-
83
- const disclosure = new Disclosure(['salt', 'exp', 1]);
84
- const disclosureDigest = await disclosure.digest({
85
- hasher: digest,
86
- alg: 'sha-256',
87
- });
88
- const header = Buffer.from(JSON.stringify({ alg: 'EdDSA' })).toString(
89
- 'base64url',
90
- );
91
- const payload = Buffer.from(
92
- JSON.stringify({ _sd: [disclosureDigest], _sd_alg: 'sha-256' }),
93
- ).toString('base64url');
94
- const unsignedJwt = `${header}.${payload}`;
95
- const signature = await signer(unsignedJwt);
96
-
97
- await expect(
98
- sdjwt.validate(`${unsignedJwt}.${signature}~${disclosure.encode()}~`, {
99
- currentDate: 1,
100
- }),
101
- ).rejects.toThrow('Verify Error: JWT is expired');
102
- });
103
-
104
- test('rejects unsafe JWT algorithm and invalid validity claims', async () => {
105
- const jwt = new Jwt({
106
- header: { alg: 'none' },
107
- payload: { exp: 100 },
108
- signature: 'signature',
109
- });
110
-
111
- await expect(jwt.verify(() => true)).rejects.toThrow(
112
- 'Verify Error: alg "none" is not allowed',
113
- );
114
-
115
- const { signer, verifier } = createSignerVerifier();
116
- const signed = new Jwt({
117
- header: { alg: 'EdDSA' },
118
- payload: { exp: '100' },
119
- });
120
- await signed.sign(signer);
121
-
122
- await expect(signed.verify(verifier)).rejects.toThrow(
123
- 'Verify Error: JWT exp must be a number',
124
- );
125
- });
126
-
127
- test('validates expected issuer audience', async () => {
128
- const { signer, verifier } = createSignerVerifier();
129
- const jwt = new Jwt({
130
- header: { alg: 'EdDSA' },
131
- payload: { aud: 'verifier-a' },
132
- });
133
- await jwt.sign(signer);
134
-
135
- await expect(
136
- jwt.verify(verifier, { expectedAudience: 'verifier-b' }),
137
- ).rejects.toThrow('Verify Error: Invalid audience');
138
- });
139
-
140
- test('validates key binding audience, age, and claim types', async () => {
141
- const { signer } = createSignerVerifier();
142
- const kbJwt = new KBJwt({
143
- header: { typ: 'kb+jwt', alg: 'EdDSA' },
144
- payload: { iat: 900, aud: 'verifier-a', nonce: 'nonce', sd_hash: 'hash' },
145
- });
146
- await kbJwt.sign(signer);
147
-
148
- await expect(
149
- kbJwt.verifyKB({
150
- verifier: () => true,
151
- payload: {},
152
- nonce: 'nonce',
153
- options: { expectedKeyBindingAudience: 'verifier-b' },
154
- }),
155
- ).rejects.toThrow('Verify Error: Invalid Key Binding audience');
156
-
157
- await expect(
158
- kbJwt.verifyKB({
159
- verifier: () => true,
160
- payload: {},
161
- nonce: 'nonce',
162
- options: { currentDate: 1000, keyBindingMaxAgeSeconds: 50 },
163
- }),
164
- ).rejects.toThrow('Verify Error: Key Binding JWT is too old');
165
-
166
- (kbJwt.payload as Record<string, unknown>).iat = '900';
167
- await expect(
168
- kbJwt.verifyKB({ verifier: () => true, payload: {}, nonce: 'nonce' }),
169
- ).rejects.toThrow('Invalid Key Binding Jwt');
170
- });
171
-
172
- test('rejects insecure disclosure hash algorithms by default', () => {
173
- const { signer, verifier } = createSignerVerifier();
174
-
175
- expect(
176
- () =>
177
- new SDJwtInstance({
178
- signer,
179
- signAlg: 'EdDSA',
180
- verifier,
181
- hasher: digest,
182
- saltGenerator: generateSalt,
183
- hashAlg: 'sha-256-32',
184
- }),
185
- ).toThrow('Disallowed hash algorithm: sha-256-32');
186
- });
187
-
188
- test('rejects duplicate salts during issuance', async () => {
189
- const { signer, verifier } = createSignerVerifier();
190
- const sdjwt = new SDJwtInstance({
191
- signer,
192
- signAlg: 'EdDSA',
193
- verifier,
194
- hasher: digest,
195
- saltGenerator: () => 'same-salt',
196
- });
197
-
198
- await expect(
199
- sdjwt.issue({ first: 'a', second: 'b' }, { _sd: ['first', 'second'] }),
200
- ).rejects.toThrow('Duplicate disclosure salt detected');
201
- });
202
-
203
- test('rejects disclosures and kb_jwt outside the first General JSON header', () => {
204
- expect(() =>
205
- GeneralJSON.fromSerialized({
206
- payload: 'payload',
207
- signatures: [
208
- { protected: 'header-1', signature: 'sig-1', header: {} },
209
- {
210
- protected: 'header-2',
211
- signature: 'sig-2',
212
- header: { disclosures: [] },
213
- },
214
- ],
215
- }),
216
- ).toThrow(
217
- 'disclosures and kb_jwt MUST only appear in the first unprotected header',
218
- );
219
- });
220
-
221
- test('escapes presentation paths with literal dots in claim names', () => {
222
- expect(transformPresentationFrame({ 'a.b': true })).toEqual(['a~1b']);
223
- expect(transformPresentationFrame({ a: { b: true } })).toEqual([
224
- 'a',
225
- 'a.b',
226
- ]);
227
-
228
- const selected = selectDisclosures(
229
- { _sd: ['top'], a: { _sd: ['nested'] } },
230
- [
231
- {
232
- digest: 'top',
233
- encoded: 'top-disclosure',
234
- salt: 'salt-1',
235
- key: 'a.b',
236
- value: 'top-level',
237
- },
238
- {
239
- digest: 'nested',
240
- encoded: 'nested-disclosure',
241
- salt: 'salt-2',
242
- key: 'b',
243
- value: 'nested',
244
- },
245
- ],
246
- { a: { b: true } },
247
- );
248
-
249
- expect(selected).toHaveLength(1);
250
- expect(selected[0].digest).toBe('nested');
251
- });
252
- });
@@ -1,150 +0,0 @@
1
- import { describe, expect, test } from 'vitest';
2
- import { unpackObj } from '../src/decode';
3
- import { Disclosure } from '../src/utils';
4
-
5
- /**
6
- * Tests for RFC 9901 validation checks added to unpackObj / unpackObjInternal.
7
- *
8
- * Section 7.1 step 4: Duplicate digest rejection
9
- * Section 7.1 step 5: Unreferenced disclosure rejection
10
- * Section 7.1 step 3c.ii.3: Claim name collision rejection
11
- */
12
-
13
- const makeDisclosure = (
14
- digest: string,
15
- key: string | undefined,
16
- value: unknown,
17
- ) =>
18
- Disclosure.fromArray(key ? ['salt', key, value] : ['salt', value], {
19
- digest,
20
- encoded: `encoded-${digest}`,
21
- });
22
-
23
- describe('RFC 9901 validation', () => {
24
- // ──────────────────────────────────────────
25
- // 7.1 step 4 — Duplicate digest in _sd array
26
- // ──────────────────────────────────────────
27
- test('rejects duplicate digest in _sd array', () => {
28
- const digest = 'abc123';
29
- const payload = { _sd: [digest, digest] };
30
- const map: Record<string, Disclosure> = {
31
- [digest]: makeDisclosure(digest, 'foo', 'bar'),
32
- };
33
- expect(() => unpackObj(payload, map)).toThrow(
34
- 'Duplicate digest found in SD-JWT payload',
35
- );
36
- });
37
-
38
- // ──────────────────────────────────────────
39
- // 7.1 step 4 — Duplicate digest across nested _sd
40
- // ──────────────────────────────────────────
41
- test('rejects duplicate digest across nested _sd arrays', () => {
42
- const digest = 'dup1';
43
- const payload = {
44
- _sd: [digest],
45
- nested: {
46
- _sd: [digest],
47
- },
48
- };
49
- const map: Record<string, Disclosure> = {
50
- [digest]: makeDisclosure(digest, 'x', 'y'),
51
- };
52
- expect(() => unpackObj(payload, map)).toThrow(
53
- 'Duplicate digest found in SD-JWT payload',
54
- );
55
- });
56
-
57
- // ──────────────────────────────────────────
58
- // 7.1 step 4 — Duplicate digest in array items
59
- // ──────────────────────────────────────────
60
- test('rejects duplicate digest in array element disclosures', () => {
61
- const digest = 'arrdup';
62
- const payload = {
63
- arr: [{ '...': digest }, { '...': digest }],
64
- };
65
- const map: Record<string, Disclosure> = {
66
- [digest]: makeDisclosure(digest, undefined, 'val'),
67
- };
68
- expect(() => unpackObj(payload, map)).toThrow(
69
- 'Duplicate digest found in SD-JWT payload',
70
- );
71
- });
72
-
73
- // ──────────────────────────────────────────
74
- // 7.1 step 5 — Unreferenced disclosure
75
- // ──────────────────────────────────────────
76
- test('rejects unreferenced disclosure', () => {
77
- const usedDigest = 'used1';
78
- const unusedDigest = 'unused1';
79
- const payload = { _sd: [usedDigest] };
80
- const map: Record<string, Disclosure> = {
81
- [usedDigest]: makeDisclosure(usedDigest, 'a', 1),
82
- [unusedDigest]: makeDisclosure(unusedDigest, 'b', 2),
83
- };
84
- expect(() => unpackObj(payload, map)).toThrow(
85
- 'Unreferenced disclosure(s) detected in SD-JWT',
86
- );
87
- });
88
-
89
- test('rejects when no digests exist in payload but disclosures provided', () => {
90
- const payload = { plain: 'value' };
91
- const map: Record<string, Disclosure> = {
92
- orphan: makeDisclosure('orphan', 'k', 'v'),
93
- };
94
- expect(() => unpackObj(payload, map)).toThrow(
95
- 'Unreferenced disclosure(s) detected in SD-JWT',
96
- );
97
- });
98
-
99
- // ──────────────────────────────────────────
100
- // 7.1 step 3c.ii.3 — Claim name collision
101
- // ──────────────────────────────────────────
102
- test('rejects disclosed claim name that conflicts with plaintext key', () => {
103
- const digest = 'col1';
104
- // The payload has both a plaintext "name" and a disclosure that would add "name"
105
- const payload = { name: 'Alice', _sd: [digest] };
106
- const map: Record<string, Disclosure> = {
107
- [digest]: makeDisclosure(digest, 'name', 'Mallory'),
108
- };
109
- expect(() => unpackObj(payload, map)).toThrow(
110
- 'Disclosed claim name "name" conflicts with existing payload key',
111
- );
112
- });
113
-
114
- // ──────────────────────────────────────────
115
- // Positive: valid SD-JWT unpacks without error
116
- // ──────────────────────────────────────────
117
- test('unpacks valid SD-JWT without errors', () => {
118
- const d1 = 'digest1';
119
- const d2 = 'digest2';
120
- const payload = { _sd: [d1, d2], plain: 'hello' };
121
- const map: Record<string, Disclosure> = {
122
- [d1]: makeDisclosure(d1, 'foo', 'bar'),
123
- [d2]: makeDisclosure(d2, 'baz', 42),
124
- };
125
- const { unpackedObj, disclosureKeymap } = unpackObj(payload, map);
126
- expect(unpackedObj).toEqual({ plain: 'hello', foo: 'bar', baz: 42 });
127
- expect(disclosureKeymap).toEqual({ foo: d1, baz: d2 });
128
- });
129
-
130
- test('unpacks valid SD-JWT with array disclosures', () => {
131
- const d1 = 'arrdig1';
132
- const d2 = 'arrdig2';
133
- const payload = {
134
- arr: [{ '...': d1 }, 'plainItem', { '...': d2 }],
135
- };
136
- const map: Record<string, Disclosure> = {
137
- [d1]: makeDisclosure(d1, undefined, 'secret1'),
138
- [d2]: makeDisclosure(d2, undefined, 'secret2'),
139
- };
140
- const { unpackedObj } = unpackObj(payload, map);
141
- expect(unpackedObj).toEqual({ arr: ['secret1', 'plainItem', 'secret2'] });
142
- });
143
-
144
- test('allows empty disclosure map with no _sd in payload', () => {
145
- const payload = { plain: 'value' };
146
- const map: Record<string, Disclosure> = {};
147
- const { unpackedObj } = unpackObj(payload, map);
148
- expect(unpackedObj).toEqual({ plain: 'value' });
149
- });
150
- });
@@ -1,4 +0,0 @@
1
- {
2
- "extends": "../../../tsconfig.test.json",
3
- "include": ["./**/*.spec.ts"]
4
- }
package/tsconfig.json DELETED
@@ -1,7 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "rootDir": "src",
5
- "outDir": "dist"
6
- }
7
- }
package/vitest.config.mts DELETED
@@ -1,4 +0,0 @@
1
- // vite.config.ts
2
- import { allEnvs } from '../../vitest.shared';
3
-
4
- export default allEnvs;