@sd-jwt/jwt-status-list 0.6.2-next.27
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/README.md +94 -0
- package/dist/index.d.mts +121 -0
- package/dist/index.d.ts +121 -0
- package/dist/index.js +216 -0
- package/dist/index.mjs +176 -0
- package/package.json +66 -0
- package/src/index.ts +3 -0
- package/src/status-list-jwt.ts +73 -0
- package/src/status-list.ts +167 -0
- package/src/test/status-list-jwt.spec.ts +126 -0
- package/src/test/status-list.spec.ts +222 -0
- package/src/types.ts +43 -0
- package/tsconfig.json +7 -0
- package/vitest.config.mts +4 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createHeaderAndPayload,
|
|
3
|
+
getListFromStatusListJWT,
|
|
4
|
+
getStatusListFromJWT,
|
|
5
|
+
} from '../status-list-jwt';
|
|
6
|
+
import type {
|
|
7
|
+
StatusListJWTHeaderParameters,
|
|
8
|
+
JWTwithStatusListPayload,
|
|
9
|
+
} from '../types';
|
|
10
|
+
import { StatusList } from '../status-list';
|
|
11
|
+
import { jwtVerify, type KeyLike, SignJWT } from 'jose';
|
|
12
|
+
import { beforeAll, describe, expect, it } from 'vitest';
|
|
13
|
+
import { generateKeyPairSync } from 'node:crypto';
|
|
14
|
+
import type { JwtPayload } from '@sd-jwt/types';
|
|
15
|
+
|
|
16
|
+
describe('JWTStatusList', () => {
|
|
17
|
+
let publicKey: KeyLike;
|
|
18
|
+
let privateKey: KeyLike;
|
|
19
|
+
|
|
20
|
+
const header: StatusListJWTHeaderParameters = {
|
|
21
|
+
alg: 'ES256',
|
|
22
|
+
typ: 'statuslist+jwt',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
beforeAll(() => {
|
|
26
|
+
// Generate a key pair for testing
|
|
27
|
+
const keyPair = generateKeyPairSync('ec', {
|
|
28
|
+
namedCurve: 'P-256',
|
|
29
|
+
});
|
|
30
|
+
publicKey = keyPair.publicKey;
|
|
31
|
+
privateKey = keyPair.privateKey;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should create a JWT with a status list', async () => {
|
|
35
|
+
const statusList = new StatusList([1, 0, 1, 1, 1], 1);
|
|
36
|
+
const iss = 'https://example.com';
|
|
37
|
+
const payload: JwtPayload = {
|
|
38
|
+
iss,
|
|
39
|
+
sub: `${iss}/statuslist/1`,
|
|
40
|
+
iat: new Date().getTime() / 1000,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const values = createHeaderAndPayload(statusList, payload, header);
|
|
44
|
+
|
|
45
|
+
const jwt = await new SignJWT(values.payload)
|
|
46
|
+
.setProtectedHeader(values.header)
|
|
47
|
+
.sign(privateKey);
|
|
48
|
+
// Verify the signed JWT with the public key
|
|
49
|
+
const verified = await jwtVerify(jwt, publicKey);
|
|
50
|
+
expect(verified.payload.status_list).toEqual({
|
|
51
|
+
bits: statusList.getBitsPerStatus(),
|
|
52
|
+
lst: statusList.compressStatusList(),
|
|
53
|
+
});
|
|
54
|
+
expect(verified.protectedHeader.typ).toBe('statuslist+jwt');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should get the status list from a JWT without verifying the signature', async () => {
|
|
58
|
+
const list = [1, 0, 1, 0, 1];
|
|
59
|
+
const statusList = new StatusList(list, 1);
|
|
60
|
+
const iss = 'https://example.com';
|
|
61
|
+
const payload: JwtPayload = {
|
|
62
|
+
iss,
|
|
63
|
+
sub: `${iss}/statuslist/1`,
|
|
64
|
+
iat: new Date().getTime() / 1000,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const values = createHeaderAndPayload(statusList, payload, header);
|
|
68
|
+
|
|
69
|
+
const jwt = await new SignJWT(values.payload)
|
|
70
|
+
.setProtectedHeader(values.header)
|
|
71
|
+
.sign(privateKey);
|
|
72
|
+
|
|
73
|
+
const extractedList = getListFromStatusListJWT(jwt);
|
|
74
|
+
for (let i = 0; i < list.length; i++) {
|
|
75
|
+
expect(extractedList.getStatus(i)).toBe(list[i]);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should throw an error if the JWT is invalid', async () => {
|
|
80
|
+
const list = [1, 0, 1, 0, 1];
|
|
81
|
+
const statusList = new StatusList(list, 2);
|
|
82
|
+
const iss = 'https://example.com';
|
|
83
|
+
let payload: JwtPayload = {
|
|
84
|
+
sub: `${iss}/statuslist/1`,
|
|
85
|
+
iat: new Date().getTime() / 1000,
|
|
86
|
+
};
|
|
87
|
+
expect(() => {
|
|
88
|
+
createHeaderAndPayload(statusList, payload as JwtPayload, header);
|
|
89
|
+
}).toThrow('iss field is required');
|
|
90
|
+
|
|
91
|
+
payload = {
|
|
92
|
+
iss,
|
|
93
|
+
iat: new Date().getTime() / 1000,
|
|
94
|
+
};
|
|
95
|
+
expect(() => createHeaderAndPayload(statusList, payload, header)).toThrow(
|
|
96
|
+
'sub field is required',
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
payload = {
|
|
100
|
+
iss,
|
|
101
|
+
sub: `${iss}/statuslist/1`,
|
|
102
|
+
};
|
|
103
|
+
expect(() => createHeaderAndPayload(statusList, payload, header)).toThrow(
|
|
104
|
+
'iat field is required',
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should get the status entry from a JWT', async () => {
|
|
109
|
+
const payload: JWTwithStatusListPayload = {
|
|
110
|
+
iss: 'https://example.com',
|
|
111
|
+
sub: 'https://example.com/status/1',
|
|
112
|
+
iat: new Date().getTime() / 1000,
|
|
113
|
+
status: {
|
|
114
|
+
status_list: {
|
|
115
|
+
idx: 0,
|
|
116
|
+
uri: 'https://example.com/status/1',
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
const jwt = await new SignJWT(payload)
|
|
121
|
+
.setProtectedHeader({ alg: 'ES256' })
|
|
122
|
+
.sign(privateKey);
|
|
123
|
+
const reference = getStatusListFromJWT(jwt);
|
|
124
|
+
expect(reference).toEqual(payload.status.status_list);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { describe, expect, it, test } from 'vitest';
|
|
2
|
+
import { StatusList } from '../index';
|
|
3
|
+
import type { BitsPerStatus } from '../types';
|
|
4
|
+
|
|
5
|
+
describe('StatusList', () => {
|
|
6
|
+
const listLength = 10000;
|
|
7
|
+
|
|
8
|
+
it('test from the example with 1 bit status', () => {
|
|
9
|
+
const status: number[] = [];
|
|
10
|
+
status[0] = 1;
|
|
11
|
+
status[1] = 0;
|
|
12
|
+
status[2] = 0;
|
|
13
|
+
status[3] = 1;
|
|
14
|
+
status[4] = 1;
|
|
15
|
+
status[5] = 1;
|
|
16
|
+
status[6] = 0;
|
|
17
|
+
status[7] = 1;
|
|
18
|
+
status[8] = 1;
|
|
19
|
+
status[9] = 1;
|
|
20
|
+
status[10] = 0;
|
|
21
|
+
status[11] = 0;
|
|
22
|
+
status[12] = 0;
|
|
23
|
+
status[13] = 1;
|
|
24
|
+
status[14] = 0;
|
|
25
|
+
status[15] = 1;
|
|
26
|
+
const manager = new StatusList(status, 1);
|
|
27
|
+
const encoded = manager.compressStatusList();
|
|
28
|
+
expect(encoded).toBe('eNrbuRgAAhcBXQ');
|
|
29
|
+
const l = StatusList.decompressStatusList(encoded, 1);
|
|
30
|
+
for (let i = 0; i < status.length; i++) {
|
|
31
|
+
expect(l.getStatus(i)).toBe(status[i]);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('test from the example with 2 bit status', () => {
|
|
36
|
+
const status: number[] = [];
|
|
37
|
+
status[0] = 1;
|
|
38
|
+
status[1] = 2;
|
|
39
|
+
status[2] = 0;
|
|
40
|
+
status[3] = 3;
|
|
41
|
+
status[4] = 0;
|
|
42
|
+
status[5] = 1;
|
|
43
|
+
status[6] = 0;
|
|
44
|
+
status[7] = 1;
|
|
45
|
+
status[8] = 1;
|
|
46
|
+
status[9] = 2;
|
|
47
|
+
status[10] = 3;
|
|
48
|
+
status[11] = 3;
|
|
49
|
+
const manager = new StatusList(status, 2);
|
|
50
|
+
const encoded = manager.compressStatusList();
|
|
51
|
+
expect(encoded).toBe('eNo76fITAAPfAgc');
|
|
52
|
+
const l = StatusList.decompressStatusList(encoded, 2);
|
|
53
|
+
for (let i = 0; i < status.length; i++) {
|
|
54
|
+
expect(l.getStatus(i)).toBe(status[i]);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Test with different bitsPerStatus values
|
|
59
|
+
describe.each([
|
|
60
|
+
[1 as BitsPerStatus],
|
|
61
|
+
[2 as BitsPerStatus],
|
|
62
|
+
[4 as BitsPerStatus],
|
|
63
|
+
[8 as BitsPerStatus],
|
|
64
|
+
])('with %i bitsPerStatus', (bitsPerStatus) => {
|
|
65
|
+
let manager: StatusList;
|
|
66
|
+
|
|
67
|
+
function createListe(
|
|
68
|
+
length: number,
|
|
69
|
+
bitsPerStatus: BitsPerStatus,
|
|
70
|
+
): number[] {
|
|
71
|
+
const list: number[] = [];
|
|
72
|
+
for (let i = 0; i < length; i++) {
|
|
73
|
+
list.push(Math.floor(Math.random() * 2 ** bitsPerStatus));
|
|
74
|
+
}
|
|
75
|
+
return list;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
it('should pass an incorrect list with wrong entries', () => {
|
|
79
|
+
expect(() => {
|
|
80
|
+
new StatusList([2 ** bitsPerStatus + 1], bitsPerStatus);
|
|
81
|
+
}).toThrowError();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should compress and decompress status list correctly', () => {
|
|
85
|
+
const statusList = createListe(listLength, bitsPerStatus);
|
|
86
|
+
manager = new StatusList(statusList, bitsPerStatus);
|
|
87
|
+
const compressedStatusList = manager.compressStatusList();
|
|
88
|
+
const decodedStatuslist = StatusList.decompressStatusList(
|
|
89
|
+
compressedStatusList,
|
|
90
|
+
bitsPerStatus,
|
|
91
|
+
);
|
|
92
|
+
checkIfEqual(decodedStatuslist, statusList);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should return the bitsPerStatus value', () => {
|
|
96
|
+
const statusList = createListe(
|
|
97
|
+
listLength,
|
|
98
|
+
bitsPerStatus as BitsPerStatus,
|
|
99
|
+
);
|
|
100
|
+
manager = new StatusList(statusList, bitsPerStatus as BitsPerStatus);
|
|
101
|
+
expect(manager.getBitsPerStatus()).toBe(bitsPerStatus);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('getStatus returns the correct status', () => {
|
|
105
|
+
const statusList = createListe(
|
|
106
|
+
listLength,
|
|
107
|
+
bitsPerStatus as BitsPerStatus,
|
|
108
|
+
);
|
|
109
|
+
manager = new StatusList(statusList, bitsPerStatus as BitsPerStatus);
|
|
110
|
+
|
|
111
|
+
for (let i = 0; i < statusList.length; i++) {
|
|
112
|
+
expect(manager.getStatus(i)).toBe(statusList[i]);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('setStatus sets the correct status', () => {
|
|
117
|
+
const statusList = createListe(
|
|
118
|
+
listLength,
|
|
119
|
+
bitsPerStatus as BitsPerStatus,
|
|
120
|
+
);
|
|
121
|
+
manager = new StatusList(statusList, bitsPerStatus as BitsPerStatus);
|
|
122
|
+
|
|
123
|
+
const newValue = Math.floor(Math.random() * 2 ** bitsPerStatus);
|
|
124
|
+
manager.setStatus(0, newValue);
|
|
125
|
+
expect(manager.getStatus(0)).toBe(newValue);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('getStatus throws an error for out of bounds index', () => {
|
|
129
|
+
const statusList = createListe(
|
|
130
|
+
listLength,
|
|
131
|
+
bitsPerStatus as BitsPerStatus,
|
|
132
|
+
);
|
|
133
|
+
manager = new StatusList(statusList, bitsPerStatus as BitsPerStatus);
|
|
134
|
+
|
|
135
|
+
expect(() => manager.getStatus(-1)).toThrow('Index out of bounds');
|
|
136
|
+
expect(() => manager.getStatus(listLength)).toThrow(
|
|
137
|
+
'Index out of bounds',
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('setStatus throws an error for out of bounds index', () => {
|
|
142
|
+
const statusList = createListe(
|
|
143
|
+
listLength,
|
|
144
|
+
bitsPerStatus as BitsPerStatus,
|
|
145
|
+
);
|
|
146
|
+
manager = new StatusList(statusList, bitsPerStatus as BitsPerStatus);
|
|
147
|
+
|
|
148
|
+
expect(() => manager.setStatus(-1, 5)).toThrow('Index out of bounds');
|
|
149
|
+
expect(() => manager.setStatus(listLength, 6)).toThrow(
|
|
150
|
+
'Index out of bounds',
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('decompressStatusList throws an error when decompression fails', () => {
|
|
155
|
+
const statusList = createListe(
|
|
156
|
+
listLength,
|
|
157
|
+
bitsPerStatus as BitsPerStatus,
|
|
158
|
+
);
|
|
159
|
+
manager = new StatusList(statusList, bitsPerStatus as BitsPerStatus);
|
|
160
|
+
|
|
161
|
+
const invalidCompressedData = 'invalid data';
|
|
162
|
+
|
|
163
|
+
expect(() =>
|
|
164
|
+
StatusList.decompressStatusList(invalidCompressedData, bitsPerStatus),
|
|
165
|
+
).toThrowError();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('encodeStatusList covers remaining bits in last byte', () => {
|
|
169
|
+
const bitsPerStatus = 1;
|
|
170
|
+
const totalStatuses = 10; // Not a multiple of 8
|
|
171
|
+
const statusList = Array(totalStatuses).fill(0);
|
|
172
|
+
const manager = new StatusList(statusList, bitsPerStatus);
|
|
173
|
+
const encoded = manager.compressStatusList();
|
|
174
|
+
const decoded = StatusList.decompressStatusList(encoded, bitsPerStatus);
|
|
175
|
+
//technially we need to validate all the status but we are just checking the length
|
|
176
|
+
checkIfEqual(decoded, statusList);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Check if the status list is equal to the given list.
|
|
181
|
+
* @param statuslist1
|
|
182
|
+
* @param rawStatusList
|
|
183
|
+
*/
|
|
184
|
+
function checkIfEqual(statuslist1: StatusList, rawStatusList: number[]) {
|
|
185
|
+
for (let i = 0; i < rawStatusList.length; i++) {
|
|
186
|
+
expect(statuslist1.getStatus(i)).toBe(rawStatusList[i]);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
describe('constructor', () => {
|
|
191
|
+
test.each<[number]>([
|
|
192
|
+
[3], // Invalid bitsPerStatus value
|
|
193
|
+
[5], // Invalid bitsPerStatus value
|
|
194
|
+
[6], // Invalid bitsPerStatus value
|
|
195
|
+
[7], // Invalid bitsPerStatus value
|
|
196
|
+
[9], // Invalid bitsPerStatus value
|
|
197
|
+
[10], // Invalid bitsPerStatus value
|
|
198
|
+
])(
|
|
199
|
+
'throws an error for invalid bitsPerStatus value (%i)',
|
|
200
|
+
(bitsPerStatus) => {
|
|
201
|
+
expect(() => {
|
|
202
|
+
new StatusList([], bitsPerStatus as BitsPerStatus);
|
|
203
|
+
}).toThrowError('bitsPerStatus must be 1, 2, 4, or 8');
|
|
204
|
+
},
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
test.each<[BitsPerStatus]>([
|
|
208
|
+
[1], // Valid bitsPerStatus value
|
|
209
|
+
[2], // Valid bitsPerStatus value
|
|
210
|
+
[4], // Valid bitsPerStatus value
|
|
211
|
+
[8], // Valid bitsPerStatus value
|
|
212
|
+
])(
|
|
213
|
+
'does not throw an error for valid bitsPerStatus value (%i)',
|
|
214
|
+
(bitsPerStatus) => {
|
|
215
|
+
expect(() => {
|
|
216
|
+
new StatusList([], bitsPerStatus);
|
|
217
|
+
}).not.toThrowError();
|
|
218
|
+
},
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
});
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { JwtPayload } from '@sd-jwt/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reference to a status list entry.
|
|
5
|
+
*/
|
|
6
|
+
export interface StatusListEntry {
|
|
7
|
+
idx: number;
|
|
8
|
+
uri: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Payload for a JWT
|
|
13
|
+
*/
|
|
14
|
+
export interface JWTwithStatusListPayload extends JwtPayload {
|
|
15
|
+
status: {
|
|
16
|
+
status_list: StatusListEntry;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Payload for a JWT with a status list.
|
|
22
|
+
*/
|
|
23
|
+
export interface StatusListJWTPayload extends JwtPayload {
|
|
24
|
+
ttl?: number;
|
|
25
|
+
status_list: {
|
|
26
|
+
bits: BitsPerStatus;
|
|
27
|
+
lst: string;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* BitsPerStatus type.
|
|
33
|
+
*/
|
|
34
|
+
export type BitsPerStatus = 1 | 2 | 4 | 8;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Header parameters for a JWT.
|
|
38
|
+
*/
|
|
39
|
+
export type StatusListJWTHeaderParameters = {
|
|
40
|
+
alg: string;
|
|
41
|
+
typ: 'statuslist+jwt';
|
|
42
|
+
[key: string]: unknown;
|
|
43
|
+
};
|
package/tsconfig.json
ADDED