Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
795ad38
feat: implement credential status management with StatusArray, Status…
dinkar-jain May 29, 2025
0e82b46
refactor: update StatusArray and StatusList constructors to use optio…
dinkar-jain Jun 4, 2025
bd500cf
refactor: update StatusArray and StatusList to use AllowedBitsPerEntr…
dinkar-jain Jun 4, 2025
8a552a4
refactor: update enum values in CwtStatusListClaims and CwtStandardCl…
dinkar-jain Jun 4, 2025
6f54b85
feat: added CWT status token verification
dinkar-jain Jun 8, 2025
113785c
refactor: rename constructor parameter in StatusArray and update vari…
dinkar-jain Jun 8, 2025
f95c6f9
refactor: simplify return statements in StatusList and CWTStatusToken…
dinkar-jain Jun 8, 2025
1ed21b0
feat: implement CoseType enum and update CWTStatusToken to use CoseTy…
dinkar-jain Jun 15, 2025
4c33d5d
fix: standardize error message formatting in CWTStatusToken verification
dinkar-jain Jun 15, 2025
fcb5c8f
feat: migrate zlib usage to pako for compression and decompression in…
dinkar-jain Jun 15, 2025
19a72b6
feat: add fetchStatusListUri method to retrieve status list from a gi…
dinkar-jain Jun 16, 2025
e4af828
feat: add abort-controller and node-fetch for improved fetch handling…
dinkar-jain Jun 16, 2025
21fdb40
refactor: rename CoseType to CoseStructureType and update related ref…
dinkar-jain Jun 17, 2025
142974b
refactor: update interface visibility and method names in CwtStatusTo…
dinkar-jain Jun 17, 2025
8328285
refactor: remove unused dependencies and update TypeScript lib config…
dinkar-jain Jun 17, 2025
b354d29
refactor: update StatusArray property naming and improve error messag…
dinkar-jain Jun 17, 2025
13caadc
refactor: update CoseStructureType usage in CwtStatusTokenOptions and…
dinkar-jain Jun 17, 2025
e7cb9af
refactor: add mdocContext to CwtStatusTokenOptions and CwtVerifyOptio…
dinkar-jain Jun 17, 2025
e6b52b7
refactor: update CwtStatusToken to use new claims enumeration and dat…
dinkar-jain Jun 17, 2025
6b2285f
refactor: introduce custom error classes for status list validation; …
dinkar-jain Jun 19, 2025
6e9ca7b
refactor: add StatusInfo model and integrate status handling in Mobil…
dinkar-jain Jun 20, 2025
87a85a5
refactor: update StatusInfo handling in IssuerSignedBuilder and Mobil…
dinkar-jain Jun 30, 2025
e239bb1
refactor: simplify StatusInfo structure by removing unnecessary claim…
dinkar-jain Jun 30, 2025
9ffcc8b
refactor: rename fetchStatusListUri to fetchStatusList for consistenc…
dinkar-jain Jul 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/credential-status/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './status-array'
export * from './status-list'
export * from './status-token'
58 changes: 58 additions & 0 deletions src/credential-status/status-array.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import * as zlib from "zlib";

const allowedBitsPerEntry = [1, 2, 4, 8] as const
type AllowedBitsPerEntry = typeof allowedBitsPerEntry[number]

export class StatusArray {
private readonly bitsPerEntry: 1 | 2 | 4 | 8;
private readonly statusBitMask: number;
private readonly data: Uint8Array;

constructor(bitsPerEntry: AllowedBitsPerEntry, totalEntries: number) {
if (!allowedBitsPerEntry.includes(bitsPerEntry)) {
throw new Error("Only 1, 2, 4, or 8 bits per entry are allowed.");
}

this.bitsPerEntry = bitsPerEntry;
this.statusBitMask = (1 << bitsPerEntry) - 1;

const totalBits = totalEntries * bitsPerEntry;
const byteSize = Math.ceil(totalBits / 8);
this.data = new Uint8Array(byteSize);
}

private computeByteAndOffset(index: number): [number, number] {
const byteIndex = Math.floor((index * this.bitsPerEntry) / 8);
const bitOffset = (index * this.bitsPerEntry) % 8;

return [byteIndex, bitOffset];
}

getBitsPerEntry(): 1 | 2 | 4 | 8 {
return this.bitsPerEntry;
}

set(index: number, status: number): void {
if (status < 0 || status > this.statusBitMask) {
throw new Error(`Invalid status: ${status}. Must be between 0 and ${this.statusBitMask}.`);
}

const [byteIndex, bitOffset] = this.computeByteAndOffset(index);

// Clear current bits
this.data[byteIndex] &= ~(this.statusBitMask << bitOffset);

// Set new status bits
this.data[byteIndex] |= (status & this.statusBitMask) << bitOffset;
}

get(index: number): number {
const [byteIndex, bitOffset] = this.computeByteAndOffset(index);

return (this.data[byteIndex] >> bitOffset) & this.statusBitMask;
}

compress(): Uint8Array {
return zlib.deflateSync(this.data, { level: zlib.constants.Z_BEST_COMPRESSION });
}
}
23 changes: 23 additions & 0 deletions src/credential-status/status-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { StatusArray } from "./status-array";
import { cborEncode } from "../cbor";

interface CborStatusListOptions {
statusArray: StatusArray;
aggregationUri?: string;
}

export class StatusList {
static buildCborStatusList(options: CborStatusListOptions): Uint8Array {
const compressed = options.statusArray.compress();

const statusList: Record<string, any> = {
bits: options.statusArray.getBitsPerEntry(),
lst: compressed,
};

if (options.aggregationUri) {
statusList.aggregation_uri = options.aggregationUri;
}
return cborEncode(statusList);
}
}
38 changes: 38 additions & 0 deletions src/credential-status/status-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { StatusArray } from "./status-array";
import { StatusList } from "./status-list";
import { cborEncode } from "../cbor";
import { CoseKey } from "../cose";
import { CWT } from "../cwt";

interface CWTStatusTokenOptions {
statusArray: StatusArray;
aggregationUri?: string;
type: 'sign1' | 'mac0';
key: CoseKey;
}

enum CWTProtectedHeaders {
TYPE = 16
}
enum CWTClaims {
STATUS_LIST_URI = 2,
ISSUED_AT = 6,
STATUS_LIST = 65533
}

export class CWTStatusToken {
static async build(options: CWTStatusTokenOptions): Promise<Uint8Array> {
const cwt = new CWT()
cwt.setHeaders({
protected: {
[CWTProtectedHeaders.TYPE]: 'application/statuslist+cwt',
}
});
cwt.setClaims({
[CWTClaims.STATUS_LIST_URI]: 'https://example.com/statuslist', // Where the status list is going to be hosted
[CWTClaims.ISSUED_AT]: Math.floor(Date.now() / 1000),
[CWTClaims.STATUS_LIST]: StatusList.buildCborStatusList({ statusArray: options.statusArray, aggregationUri: options.aggregationUri }),
});
return cborEncode(await cwt.create({ type: options.type, key: options.key }))
}
}
58 changes: 58 additions & 0 deletions src/cwt/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { CoseKey, Mac0, Mac0Options, Mac0Structure, Sign1, Sign1Options, Sign1Structure } from '../cose';
import { mdocContext } from '../../tests/context';
import { cborEncode } from '../cbor';

type Header = {
protected?: Record<string, any>;
unprotected?: Record<string, any>;
};

type CWTOptions = {
type: 'sign1' | 'mac0' | 'encrypt0';
key: CoseKey;
};

export class CWT {
private claimsSet: Record<string, any> = {};
private headers: Header = {};

setClaims(claims: Record<string, any>): void {
this.claimsSet = claims;
}

setHeaders(headers: Header): void {
this.headers = headers;
}

async create({ type, key }: CWTOptions): Promise<Sign1Structure | Mac0Structure> {
switch (type) {
case 'sign1':
const sign1Options: Sign1Options = {
protectedHeaders: this.headers.protected ? cborEncode(this.headers.protected) : undefined,
unprotectedHeaders: this.headers.unprotected ? new Map(Object.entries(this.headers.unprotected)) : undefined,
payload: this.claimsSet ? cborEncode(this.claimsSet) : null, // Need to encode this to binary format
};

const sign1 = new Sign1(sign1Options);
await sign1.addSignature({ signingKey: key }, { cose: mdocContext.cose });
return sign1.encodedStructure()
case 'mac0':
if (!this.headers.protected || !this.headers.unprotected) {
throw new Error('Protected and unprotected headers must be defined for MAC0');
}
const mac0Options: Mac0Options = {
protectedHeaders: this.headers.protected ? cborEncode(this.headers.protected) : undefined,
unprotectedHeaders: this.headers.unprotected ? new Map(Object.entries(this.headers.unprotected)) : undefined,
payload: this.claimsSet ? cborEncode(this.claimsSet) : null, // Need to encode this to binary format
};

const mac0 = new Mac0(mac0Options);
// await mac0.addTag({ privateKey: key, ephemeralKey: key, sessionTranscript: new SessionTranscript({ handover: new QrHandover() }) }, mdocContext);
// return mac0.encodedStructure();
case 'encrypt0':
throw new Error('Encrypt0 is not yet implemented');
default:
throw new Error('Unsupported CWT type');
}
}
}
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ export * from './mdoc'
export * from './cose'
export * from './utils'

export * from './cwt'
export * from './credential-status'

export * from './holder'
export * from './verifier'
export * from './issuer'
17 changes: 17 additions & 0 deletions tests/credential-status/cred-status.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { describe, expect, test } from 'vitest'
import { CoseKey, CWTStatusToken, StatusArray } from '../../src'
import { ISSUER_PRIVATE_KEY_JWK } from '../issuing/config';

describe('status-array', () => {
test('should create a status array and set/get values', async () => {
const statusArray = new StatusArray(2, 10);

statusArray.set(0, 2);
statusArray.set(1, 3);
expect(statusArray.get(0)).toBe(2);
expect(statusArray.get(1)).toBe(3);

// Will remove it before merging
console.log(await CWTStatusToken.build({ statusArray, type: 'sign1', key: CoseKey.fromJwk(ISSUER_PRIVATE_KEY_JWK) }));
})
})
Loading