@vess-id/status-list 0.1.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/LICENSE +51 -0
- package/README.md +710 -0
- package/dist/index.cjs +1576 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1634 -0
- package/dist/index.d.ts +1634 -0
- package/dist/index.js +1508 -0
- package/dist/index.js.map +1 -0
- package/package.json +79 -0
package/README.md
ADDED
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
# @vess-id/status-list
|
|
2
|
+
|
|
3
|
+
IETF OAuth Status List implementation for TypeScript/Node.js, supporting both JWT and CWT formats.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@vess-id/status-list)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
This library implements the [IETF OAuth Status List](https://www.ietf.org/archive/id/draft-ietf-oauth-status-list-14.html) specification for managing credential status information in a privacy-preserving, space-efficient manner.
|
|
11
|
+
|
|
12
|
+
**Key Features:**
|
|
13
|
+
- ✅ **Dual Format Support**: JWT and CWT (CBOR Web Token) formats
|
|
14
|
+
- ✅ **Space Efficient**: ZLIB compression reduces storage by 90%+
|
|
15
|
+
- ✅ **Privacy Preserving**: Herd privacy through large status lists
|
|
16
|
+
- ✅ **Flexible Status Values**: Support for 1, 2, 4, or 8-bit status values
|
|
17
|
+
- ✅ **Full Lifecycle**: Create, sign, verify, and query status lists
|
|
18
|
+
- ✅ **Standards Compliant**: Follows IETF draft-ietf-oauth-status-list-14
|
|
19
|
+
- ✅ **TypeScript Native**: Full type safety with comprehensive types
|
|
20
|
+
- ✅ **Production Ready**: Extensive test coverage (86+ tests)
|
|
21
|
+
|
|
22
|
+
## Use Cases
|
|
23
|
+
|
|
24
|
+
- **SD-JWT Verifiable Credentials**: Revocation for Selective Disclosure JWTs
|
|
25
|
+
- **mdoc/mDL**: Status management for mobile Driver's Licenses and ISO 18013-5 documents
|
|
26
|
+
- **W3C Verifiable Credentials**: Credential status tracking
|
|
27
|
+
- **OAuth Token Revocation**: Scalable token status lists
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install @vess-id/status-list
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Requirements:**
|
|
36
|
+
- Node.js >= 20
|
|
37
|
+
- TypeScript >= 5.0 (if using TypeScript)
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
### Creating a JWT Status List
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import {
|
|
45
|
+
StatusList,
|
|
46
|
+
createJWTStatusListPayload,
|
|
47
|
+
signStatusListJWT,
|
|
48
|
+
StandardStatusValues,
|
|
49
|
+
} from '@vess-id/status-list';
|
|
50
|
+
import { importPKCS8 } from 'jose';
|
|
51
|
+
|
|
52
|
+
// 1. Create a status list (1 bit per entry: 0=valid, 1=revoked)
|
|
53
|
+
const list = StatusList.create(10000, 1);
|
|
54
|
+
|
|
55
|
+
// 2. Revoke some credentials
|
|
56
|
+
list.setStatus(42, StandardStatusValues.INVALID);
|
|
57
|
+
list.setStatus(100, StandardStatusValues.INVALID);
|
|
58
|
+
|
|
59
|
+
// 3. Create JWT payload
|
|
60
|
+
const { payload } = createJWTStatusListPayload({
|
|
61
|
+
statusList: list,
|
|
62
|
+
iss: 'https://issuer.example.com',
|
|
63
|
+
sub: 'https://issuer.example.com/status/1',
|
|
64
|
+
iat: Math.floor(Date.now() / 1000),
|
|
65
|
+
exp: Math.floor(Date.now() / 1000) + 86400, // 24 hours
|
|
66
|
+
ttl: 3600, // Cache for 1 hour
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// 4. Sign the JWT
|
|
70
|
+
const privateKey = await importPKCS8(pemPrivateKey, 'ES256');
|
|
71
|
+
const jwt = await signStatusListJWT(payload, privateKey, { alg: 'ES256' });
|
|
72
|
+
|
|
73
|
+
console.log('Status List JWT:', jwt);
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Verifying Credential Status
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
import {
|
|
80
|
+
StatusListTokenHelper,
|
|
81
|
+
StandardStatusValues,
|
|
82
|
+
} from '@vess-id/status-list';
|
|
83
|
+
|
|
84
|
+
// Extract status reference from credential
|
|
85
|
+
const credentialStatus = {
|
|
86
|
+
idx: 42,
|
|
87
|
+
uri: 'https://issuer.example.com/status/1',
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Fetch and parse status list
|
|
91
|
+
const helper = await StatusListTokenHelper.fromStatusReference(credentialStatus);
|
|
92
|
+
|
|
93
|
+
// Check if expired
|
|
94
|
+
if (helper.isExpired()) {
|
|
95
|
+
throw new Error('Status list expired');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Check credential status
|
|
99
|
+
const status = helper.getStatus(credentialStatus.idx);
|
|
100
|
+
|
|
101
|
+
if (status === StandardStatusValues.INVALID) {
|
|
102
|
+
console.log('❌ Credential is REVOKED');
|
|
103
|
+
} else if (status === StandardStatusValues.VALID) {
|
|
104
|
+
console.log('✅ Credential is VALID');
|
|
105
|
+
} else if (status === StandardStatusValues.SUSPENDED) {
|
|
106
|
+
console.log('⏸️ Credential is SUSPENDED');
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Creating a CWT Status List (for mdoc)
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import {
|
|
114
|
+
StatusList,
|
|
115
|
+
createCWTStatusListPayload,
|
|
116
|
+
encodeCWTPayload,
|
|
117
|
+
} from '@vess-id/status-list';
|
|
118
|
+
|
|
119
|
+
// 1. Create status list
|
|
120
|
+
const list = StatusList.create(10000, 2); // 2 bits: supports 4 status values
|
|
121
|
+
|
|
122
|
+
// 2. Set statuses
|
|
123
|
+
list.setStatus(0, 0); // Valid
|
|
124
|
+
list.setStatus(1, 1); // Invalid
|
|
125
|
+
list.setStatus(2, 2); // Suspended
|
|
126
|
+
|
|
127
|
+
// 3. Create CWT payload
|
|
128
|
+
const payload = createCWTStatusListPayload({
|
|
129
|
+
sub: 'https://issuer.example.com/status/1',
|
|
130
|
+
iss: 'https://issuer.example.com',
|
|
131
|
+
lst: list.compressToBytes(),
|
|
132
|
+
bits: 2,
|
|
133
|
+
iat: Math.floor(Date.now() / 1000),
|
|
134
|
+
exp: Math.floor(Date.now() / 1000) + 86400,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// 4. Encode to CBOR
|
|
138
|
+
const cwtBytes = encodeCWTPayload(payload);
|
|
139
|
+
|
|
140
|
+
console.log('CWT Status List:', cwtBytes);
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## API Reference
|
|
144
|
+
|
|
145
|
+
### Core Classes
|
|
146
|
+
|
|
147
|
+
#### `StatusList`
|
|
148
|
+
|
|
149
|
+
Main class for managing bit-packed status arrays.
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
// Create new list
|
|
153
|
+
const list = StatusList.create(size: number, bits: BitsPerStatus);
|
|
154
|
+
|
|
155
|
+
// From existing statuses
|
|
156
|
+
const list = StatusList.fromArray(statuses: StatusValue[], bits: BitsPerStatus);
|
|
157
|
+
|
|
158
|
+
// Decompress from JWT
|
|
159
|
+
const list = StatusList.decompressFromBase64URL(compressed: string, bits: BitsPerStatus);
|
|
160
|
+
|
|
161
|
+
// Decompress from CWT
|
|
162
|
+
const list = StatusList.decompressFromBytes(compressed: Uint8Array, bits: BitsPerStatus);
|
|
163
|
+
|
|
164
|
+
// Get/Set status
|
|
165
|
+
const status = list.getStatus(index: number): StatusValue;
|
|
166
|
+
list.setStatus(index: number, value: StatusValue): void;
|
|
167
|
+
|
|
168
|
+
// Compress for JWT
|
|
169
|
+
const compressed = list.compressToBase64URL(): string;
|
|
170
|
+
|
|
171
|
+
// Compress for CWT
|
|
172
|
+
const compressed = list.compressToBytes(): Uint8Array;
|
|
173
|
+
|
|
174
|
+
// Metadata
|
|
175
|
+
const size = list.getSize(): number;
|
|
176
|
+
const bits = list.getBitsPerStatus(): BitsPerStatus;
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
#### `StatusListTokenHelper`
|
|
180
|
+
|
|
181
|
+
High-level helper for working with Status List Tokens.
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
// From token (auto-detects JWT/CWT)
|
|
185
|
+
const helper = StatusListTokenHelper.fromToken(token: string | Uint8Array);
|
|
186
|
+
|
|
187
|
+
// From URI (fetches via HTTP)
|
|
188
|
+
const helper = await StatusListTokenHelper.fromStatusReference(
|
|
189
|
+
reference: { idx: number; uri: string },
|
|
190
|
+
options?: FetchOptions
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// Query status
|
|
194
|
+
const status = helper.getStatus(index: number): StatusValue;
|
|
195
|
+
|
|
196
|
+
// Check expiration
|
|
197
|
+
const expired = helper.isExpired(currentTime?: number): boolean;
|
|
198
|
+
|
|
199
|
+
// Access metadata
|
|
200
|
+
const iss = helper.iss;
|
|
201
|
+
const sub = helper.sub;
|
|
202
|
+
const iat = helper.iat;
|
|
203
|
+
const exp = helper.exp;
|
|
204
|
+
const ttl = helper.ttl;
|
|
205
|
+
const bits = helper.bits;
|
|
206
|
+
const size = helper.size;
|
|
207
|
+
const format = helper.tokenFormat; // 'jwt' | 'cwt'
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### JWT Format
|
|
211
|
+
|
|
212
|
+
#### Creating JWT Status Lists
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
import {
|
|
216
|
+
createJWTStatusListPayload,
|
|
217
|
+
signStatusListJWT,
|
|
218
|
+
type CreateJWTStatusListOptions,
|
|
219
|
+
} from '@vess-id/status-list';
|
|
220
|
+
|
|
221
|
+
// Create payload
|
|
222
|
+
const { header, payload } = createJWTStatusListPayload({
|
|
223
|
+
statusList: list,
|
|
224
|
+
iss: string,
|
|
225
|
+
sub: string,
|
|
226
|
+
iat?: number, // defaults to now
|
|
227
|
+
exp?: number,
|
|
228
|
+
ttl?: number,
|
|
229
|
+
aggregationUri?: string,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Sign
|
|
233
|
+
const jwt = await signStatusListJWT(
|
|
234
|
+
payload,
|
|
235
|
+
privateKey: KeyLike,
|
|
236
|
+
options?: {
|
|
237
|
+
alg?: string;
|
|
238
|
+
kid?: string;
|
|
239
|
+
additionalHeaders?: Record<string, unknown>;
|
|
240
|
+
}
|
|
241
|
+
): Promise<string>;
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
#### Parsing JWT Status Lists
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
import {
|
|
248
|
+
parseJWTStatusList,
|
|
249
|
+
verifyStatusListJWT,
|
|
250
|
+
extractStatusListReference,
|
|
251
|
+
} from '@vess-id/status-list';
|
|
252
|
+
|
|
253
|
+
// Parse (without verification)
|
|
254
|
+
const { header, payload, statusList } = parseJWTStatusList(jwt: string);
|
|
255
|
+
|
|
256
|
+
// Verify signature
|
|
257
|
+
const { header, payload } = await verifyStatusListJWT(
|
|
258
|
+
jwt: string,
|
|
259
|
+
publicKey: KeyLike,
|
|
260
|
+
options?: {
|
|
261
|
+
issuer?: string;
|
|
262
|
+
validateExp?: boolean;
|
|
263
|
+
}
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
// Extract reference from credential
|
|
267
|
+
const reference = extractStatusListReference(credentialJWT: string);
|
|
268
|
+
// Returns: { idx: number, uri: string }
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### CWT Format
|
|
272
|
+
|
|
273
|
+
#### Creating CWT Status Lists
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
import {
|
|
277
|
+
createCWTStatusListPayload,
|
|
278
|
+
encodeCWTPayload,
|
|
279
|
+
signCWTStatusList,
|
|
280
|
+
type COSEKey,
|
|
281
|
+
} from '@vess-id/status-list';
|
|
282
|
+
|
|
283
|
+
// Create payload
|
|
284
|
+
const payload = createCWTStatusListPayload({
|
|
285
|
+
sub?: string,
|
|
286
|
+
iss?: string,
|
|
287
|
+
lst: Uint8Array,
|
|
288
|
+
bits: BitsPerStatus,
|
|
289
|
+
iat?: number,
|
|
290
|
+
exp?: number,
|
|
291
|
+
ttl?: number,
|
|
292
|
+
aggregation_uri?: string,
|
|
293
|
+
additionalClaims?: Record<number, unknown>,
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Encode to CBOR (unsigned)
|
|
297
|
+
const cwtBytes = encodeCWTPayload(payload);
|
|
298
|
+
|
|
299
|
+
// Sign with COSE Sign1 (advanced)
|
|
300
|
+
const cwtSigned = signCWTStatusList(
|
|
301
|
+
payload,
|
|
302
|
+
privateKey: COSEKey,
|
|
303
|
+
options?: {
|
|
304
|
+
alg?: number;
|
|
305
|
+
kid?: Uint8Array;
|
|
306
|
+
additionalHeaders?: Map<number, unknown>;
|
|
307
|
+
}
|
|
308
|
+
);
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
#### Parsing CWT Status Lists
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
import {
|
|
315
|
+
parseCWTStatusList,
|
|
316
|
+
parseCWTStatusListSigned,
|
|
317
|
+
extractStatusListReferenceCBOR,
|
|
318
|
+
} from '@vess-id/status-list';
|
|
319
|
+
|
|
320
|
+
// Parse unsigned CWT
|
|
321
|
+
const { payload, statusList } = parseCWTStatusList(cwtBytes: Uint8Array);
|
|
322
|
+
|
|
323
|
+
// Parse and verify signed CWT
|
|
324
|
+
const { payload, statusList } = parseCWTStatusListSigned(
|
|
325
|
+
cwtBytes: Uint8Array,
|
|
326
|
+
publicKey: COSEKey
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
// Extract reference from mdoc credential
|
|
330
|
+
const reference = extractStatusListReferenceCBOR(credentialCBOR: Uint8Array);
|
|
331
|
+
// Returns: { idx: number, uri: string }
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Validation
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
import {
|
|
338
|
+
validateJWTPayload,
|
|
339
|
+
validateCWTPayload,
|
|
340
|
+
validateExpiry,
|
|
341
|
+
validateTTLBounds,
|
|
342
|
+
isExpired,
|
|
343
|
+
} from '@vess-id/status-list';
|
|
344
|
+
|
|
345
|
+
// Validate payload structure
|
|
346
|
+
const result = validateJWTPayload(payload);
|
|
347
|
+
if (!result.valid) {
|
|
348
|
+
console.error('Errors:', result.errors);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Check expiration
|
|
352
|
+
const expiryResult = validateExpiry(exp, iat, ttl);
|
|
353
|
+
if (!expiryResult.valid) {
|
|
354
|
+
console.error('Token expired');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Validate TTL bounds (DoS prevention)
|
|
358
|
+
const ttlResult = validateTTLBounds(ttl);
|
|
359
|
+
if (!ttlResult.valid) {
|
|
360
|
+
console.warn('TTL outside recommended bounds:', ttlResult.errors);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Simple expiration check
|
|
364
|
+
if (isExpired(exp, iat, ttl)) {
|
|
365
|
+
throw new Error('Token expired');
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Fetching Status Lists
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
import {
|
|
373
|
+
fetchStatusListToken,
|
|
374
|
+
isValidStatusListUri,
|
|
375
|
+
type FetchOptions,
|
|
376
|
+
} from '@vess-id/status-list';
|
|
377
|
+
|
|
378
|
+
// Fetch status list (auto-detects JWT/CWT)
|
|
379
|
+
const token = await fetchStatusListToken(
|
|
380
|
+
uri: string,
|
|
381
|
+
options?: {
|
|
382
|
+
timeout?: number; // default: 10000ms
|
|
383
|
+
headers?: Record<string, string>;
|
|
384
|
+
maxRedirects?: number; // default: 3
|
|
385
|
+
fetchImpl?: typeof fetch;
|
|
386
|
+
}
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
// Validate URI
|
|
390
|
+
if (!isValidStatusListUri(uri)) {
|
|
391
|
+
throw new Error('Invalid status list URI');
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
## Types
|
|
396
|
+
|
|
397
|
+
### Status Values
|
|
398
|
+
|
|
399
|
+
```typescript
|
|
400
|
+
type BitsPerStatus = 1 | 2 | 4 | 8;
|
|
401
|
+
type StatusValue = number; // 0-255 depending on bits
|
|
402
|
+
|
|
403
|
+
enum StandardStatusValues {
|
|
404
|
+
VALID = 0x00,
|
|
405
|
+
INVALID = 0x01,
|
|
406
|
+
SUSPENDED = 0x02,
|
|
407
|
+
APPLICATION_SPECIFIC = 0x03,
|
|
408
|
+
}
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### Status Format
|
|
412
|
+
|
|
413
|
+
```typescript
|
|
414
|
+
type StatusFormat = 'jwt' | 'cwt';
|
|
415
|
+
|
|
416
|
+
interface StatusListReference {
|
|
417
|
+
idx: number;
|
|
418
|
+
uri: string;
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
## Advanced Examples
|
|
423
|
+
|
|
424
|
+
### Large-Scale Status List (100K credentials)
|
|
425
|
+
|
|
426
|
+
```typescript
|
|
427
|
+
import { StatusList, StandardStatusValues } from '@vess-id/status-list';
|
|
428
|
+
|
|
429
|
+
// Create list for 100,000 credentials
|
|
430
|
+
const list = StatusList.create(100000, 1);
|
|
431
|
+
|
|
432
|
+
// Revoke every 1000th credential
|
|
433
|
+
for (let i = 0; i < 100000; i += 1000) {
|
|
434
|
+
list.setStatus(i, StandardStatusValues.INVALID);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Compress
|
|
438
|
+
const compressed = list.compressToBase64URL();
|
|
439
|
+
console.log('Compressed size:', compressed.length, 'characters');
|
|
440
|
+
// Expected: ~4-5KB (90%+ compression)
|
|
441
|
+
|
|
442
|
+
// Original size: 100,000 bits = 12,500 bytes
|
|
443
|
+
// Compressed: ~5,000 bytes (~60% compression for sparse data)
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
### Multi-Status System (2-bit)
|
|
447
|
+
|
|
448
|
+
```typescript
|
|
449
|
+
import { StatusList } from '@vess-id/status-list';
|
|
450
|
+
|
|
451
|
+
const STATUS = {
|
|
452
|
+
ACTIVE: 0,
|
|
453
|
+
REVOKED: 1,
|
|
454
|
+
SUSPENDED: 2,
|
|
455
|
+
EXPIRED: 3,
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
// 2 bits per status = 4 possible values
|
|
459
|
+
const list = StatusList.create(1000, 2);
|
|
460
|
+
|
|
461
|
+
list.setStatus(0, STATUS.ACTIVE);
|
|
462
|
+
list.setStatus(1, STATUS.REVOKED);
|
|
463
|
+
list.setStatus(2, STATUS.SUSPENDED);
|
|
464
|
+
list.setStatus(3, STATUS.EXPIRED);
|
|
465
|
+
|
|
466
|
+
const status = list.getStatus(1);
|
|
467
|
+
console.log(status === STATUS.REVOKED); // true
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### Custom Validation
|
|
471
|
+
|
|
472
|
+
```typescript
|
|
473
|
+
import {
|
|
474
|
+
StatusListTokenHelper,
|
|
475
|
+
validateExpiry,
|
|
476
|
+
validateTTLBounds,
|
|
477
|
+
} from '@vess-id/status-list';
|
|
478
|
+
|
|
479
|
+
async function validateCredentialStatus(reference: { idx: number; uri: string }) {
|
|
480
|
+
// Fetch status list
|
|
481
|
+
const helper = await StatusListTokenHelper.fromStatusReference(reference);
|
|
482
|
+
|
|
483
|
+
// Validate expiry
|
|
484
|
+
const expiryResult = validateExpiry(helper.exp, helper.iat, helper.ttl);
|
|
485
|
+
if (!expiryResult.valid) {
|
|
486
|
+
throw new Error(`Status list expired: ${expiryResult.errors.join(', ')}`);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Validate TTL bounds
|
|
490
|
+
const ttlResult = validateTTLBounds(helper.ttl);
|
|
491
|
+
if (!ttlResult.valid) {
|
|
492
|
+
console.warn(`TTL warning: ${ttlResult.errors.join(', ')}`);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Check status
|
|
496
|
+
const status = helper.getStatus(reference.idx);
|
|
497
|
+
return {
|
|
498
|
+
valid: status === 0,
|
|
499
|
+
status,
|
|
500
|
+
issuer: helper.iss,
|
|
501
|
+
expiresAt: helper.exp,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### Credential with Status (SD-JWT Example)
|
|
507
|
+
|
|
508
|
+
```typescript
|
|
509
|
+
import { createJWTStatusListPayload, signStatusListJWT } from '@vess-id/status-list';
|
|
510
|
+
|
|
511
|
+
// 1. Issue credential with status claim
|
|
512
|
+
const credential = {
|
|
513
|
+
iss: 'https://issuer.example.com',
|
|
514
|
+
sub: 'user@example.com',
|
|
515
|
+
iat: Math.floor(Date.now() / 1000),
|
|
516
|
+
exp: Math.floor(Date.now() / 1000) + 31536000, // 1 year
|
|
517
|
+
vc: {
|
|
518
|
+
type: ['VerifiableCredential', 'UniversityDegree'],
|
|
519
|
+
credentialSubject: {
|
|
520
|
+
name: 'Alice Smith',
|
|
521
|
+
degree: 'Bachelor of Science',
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
status: {
|
|
525
|
+
idx: 42,
|
|
526
|
+
uri: 'https://issuer.example.com/status/1',
|
|
527
|
+
},
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
// 2. Create status list
|
|
531
|
+
const list = StatusList.create(10000, 1);
|
|
532
|
+
const { payload } = createJWTStatusListPayload({
|
|
533
|
+
statusList: list,
|
|
534
|
+
iss: 'https://issuer.example.com',
|
|
535
|
+
sub: 'https://issuer.example.com/status/1',
|
|
536
|
+
iat: Math.floor(Date.now() / 1000),
|
|
537
|
+
exp: Math.floor(Date.now() / 1000) + 86400,
|
|
538
|
+
ttl: 3600,
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
// 3. Sign status list
|
|
542
|
+
const statusListJWT = await signStatusListJWT(payload, privateKey);
|
|
543
|
+
|
|
544
|
+
// 4. Verifier checks status
|
|
545
|
+
const helper = await StatusListTokenHelper.fromStatusReference(credential.status);
|
|
546
|
+
const status = helper.getStatus(credential.status.idx);
|
|
547
|
+
|
|
548
|
+
if (status === 1) {
|
|
549
|
+
throw new Error('Credential has been revoked');
|
|
550
|
+
}
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
## Security Considerations
|
|
554
|
+
|
|
555
|
+
This library follows the security recommendations from the IETF specification:
|
|
556
|
+
|
|
557
|
+
### 1. TTL Bounds (Section 11.5)
|
|
558
|
+
```typescript
|
|
559
|
+
// Default limits prevent DoS attacks
|
|
560
|
+
const MAX_TTL = 31536000; // 1 year
|
|
561
|
+
const MIN_TTL = 60; // 1 minute
|
|
562
|
+
|
|
563
|
+
// Validate TTL
|
|
564
|
+
const result = validateTTLBounds(ttl);
|
|
565
|
+
if (!result.valid) {
|
|
566
|
+
console.warn('TTL outside recommended bounds');
|
|
567
|
+
}
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
### 2. Redirect Limits (Section 11.4)
|
|
571
|
+
```typescript
|
|
572
|
+
// Default maximum: 3 redirects
|
|
573
|
+
const token = await fetchStatusListToken(uri, {
|
|
574
|
+
maxRedirects: 3,
|
|
575
|
+
});
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
### 3. Timeout Protection
|
|
579
|
+
```typescript
|
|
580
|
+
// Default timeout: 10 seconds
|
|
581
|
+
const token = await fetchStatusListToken(uri, {
|
|
582
|
+
timeout: 10000,
|
|
583
|
+
});
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
### 4. Signature Verification
|
|
587
|
+
```typescript
|
|
588
|
+
// Always verify signatures in production
|
|
589
|
+
const verified = await verifyStatusListJWT(jwt, publicKey);
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
### 5. Herd Privacy
|
|
593
|
+
```typescript
|
|
594
|
+
// Use large lists for privacy (recommended: 10,000+ entries)
|
|
595
|
+
const list = StatusList.create(100000, 1);
|
|
596
|
+
// Even with few revoked credentials, observers cannot correlate
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
## Performance
|
|
600
|
+
|
|
601
|
+
### Compression Efficiency
|
|
602
|
+
|
|
603
|
+
| Entries | Bits | Uncompressed | Compressed | Ratio |
|
|
604
|
+
|---------|------|--------------|------------|-------|
|
|
605
|
+
| 10,000 | 1 | 1.22 KB | ~500 B | 60% |
|
|
606
|
+
| 100,000 | 1 | 12.2 KB | ~5 KB | 60% |
|
|
607
|
+
| 10,000 | 2 | 2.44 KB | ~1 KB | 60% |
|
|
608
|
+
| 100,000 | 8 | 97.7 KB | ~20 KB | 80% |
|
|
609
|
+
|
|
610
|
+
*Compression ratio depends on data entropy. Sparse lists compress better.*
|
|
611
|
+
|
|
612
|
+
### Memory Usage
|
|
613
|
+
|
|
614
|
+
```typescript
|
|
615
|
+
// Minimal memory footprint
|
|
616
|
+
const list = StatusList.create(100000, 1);
|
|
617
|
+
// Memory: ~12.5 KB (uncompressed in memory)
|
|
618
|
+
// Network: ~5 KB (compressed over wire)
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
## Testing
|
|
622
|
+
|
|
623
|
+
```bash
|
|
624
|
+
# Run all tests
|
|
625
|
+
npm test
|
|
626
|
+
|
|
627
|
+
# Run tests in watch mode
|
|
628
|
+
npm run test:watch
|
|
629
|
+
|
|
630
|
+
# Run tests with coverage
|
|
631
|
+
npm run test:coverage
|
|
632
|
+
|
|
633
|
+
# Build
|
|
634
|
+
npm run build
|
|
635
|
+
|
|
636
|
+
# Lint
|
|
637
|
+
npm run lint
|
|
638
|
+
|
|
639
|
+
# Format
|
|
640
|
+
npm run format
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
### Test Coverage
|
|
644
|
+
|
|
645
|
+
```
|
|
646
|
+
Test Files: 5 passed (5)
|
|
647
|
+
Tests: 86 passed (86)
|
|
648
|
+
- Core (bitpack, compression, StatusList): 58 tests
|
|
649
|
+
- JWT integration: 10 tests
|
|
650
|
+
- CWT integration: 18 tests
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
## Compatibility
|
|
654
|
+
|
|
655
|
+
### Node.js Versions
|
|
656
|
+
- ✅ Node.js 20+
|
|
657
|
+
- ✅ Node.js 22+
|
|
658
|
+
|
|
659
|
+
### Module Systems
|
|
660
|
+
- ✅ ESM (import)
|
|
661
|
+
- ✅ CommonJS (require)
|
|
662
|
+
|
|
663
|
+
### TypeScript
|
|
664
|
+
- ✅ TypeScript 5.0+
|
|
665
|
+
- ✅ Full type definitions included
|
|
666
|
+
- ✅ Strict mode compatible
|
|
667
|
+
|
|
668
|
+
## IETF Specification Compliance
|
|
669
|
+
|
|
670
|
+
This library implements [draft-ietf-oauth-status-list-14](https://www.ietf.org/archive/id/draft-ietf-oauth-status-list-14.html):
|
|
671
|
+
|
|
672
|
+
- ✅ Section 3: Status List Structure
|
|
673
|
+
- ✅ Section 4: Bit Packing (LSB-first)
|
|
674
|
+
- ✅ Section 5: JWT Status List
|
|
675
|
+
- ✅ Section 5.2: CWT Status List
|
|
676
|
+
- ✅ Section 6: Status Reference
|
|
677
|
+
- ✅ Section 11: Security Considerations
|
|
678
|
+
|
|
679
|
+
## Related Projects
|
|
680
|
+
|
|
681
|
+
- [EUDI Wallet IT (Python)](https://github.com/italia/eudi-wallet-it-python) - Reference implementation
|
|
682
|
+
- [@sd-jwt/jwt-status-list](https://github.com/lukasjhan/sd-jwt-js) - SD-JWT TypeScript implementation
|
|
683
|
+
|
|
684
|
+
## Contributing
|
|
685
|
+
|
|
686
|
+
Contributions are welcome! Please follow these guidelines:
|
|
687
|
+
|
|
688
|
+
1. Fork the repository
|
|
689
|
+
2. Create a feature branch
|
|
690
|
+
3. Write tests for new features
|
|
691
|
+
4. Ensure all tests pass
|
|
692
|
+
5. Submit a pull request
|
|
693
|
+
|
|
694
|
+
## License
|
|
695
|
+
|
|
696
|
+
Apache License 2.0
|
|
697
|
+
|
|
698
|
+
Copyright (c) 2024 VESS Project
|
|
699
|
+
|
|
700
|
+
See [LICENSE](LICENSE) file for details.
|
|
701
|
+
|
|
702
|
+
## Support
|
|
703
|
+
|
|
704
|
+
- **Issues**: [GitHub Issues](https://github.com/your-org/status-list/issues)
|
|
705
|
+
- **Documentation**: [API Reference](#api-reference)
|
|
706
|
+
- **Specification**: [IETF Draft](https://www.ietf.org/archive/id/draft-ietf-oauth-status-list-14.html)
|
|
707
|
+
|
|
708
|
+
---
|
|
709
|
+
|
|
710
|
+
**Made with ❤️ for the Verifiable Credentials ecosystem**
|