From 795ad38aeebae4695ee5fc037e0ff43dd3c67087 Mon Sep 17 00:00:00 2001 From: Dinkar Date: Thu, 29 May 2025 15:10:41 +0530 Subject: [PATCH 01/24] feat: implement credential status management with StatusArray, StatusList, and CWTStatusToken classes --- src/credential-status/index.ts | 3 ++ src/credential-status/status-array.ts | 55 +++++++++++++++++++ src/credential-status/status-list.ts | 18 +++++++ src/credential-status/status-token.ts | 22 ++++++++ src/cwt/index.ts | 58 +++++++++++++++++++++ src/index.ts | 3 ++ tests/credential-status/cred-status.test.ts | 17 ++++++ 7 files changed, 176 insertions(+) create mode 100644 src/credential-status/index.ts create mode 100644 src/credential-status/status-array.ts create mode 100644 src/credential-status/status-list.ts create mode 100644 src/credential-status/status-token.ts create mode 100644 src/cwt/index.ts create mode 100644 tests/credential-status/cred-status.test.ts diff --git a/src/credential-status/index.ts b/src/credential-status/index.ts new file mode 100644 index 0000000..f6dc145 --- /dev/null +++ b/src/credential-status/index.ts @@ -0,0 +1,3 @@ +export * from './status-array' +export * from './status-list' +export * from './status-token' \ No newline at end of file diff --git a/src/credential-status/status-array.ts b/src/credential-status/status-array.ts new file mode 100644 index 0000000..999f839 --- /dev/null +++ b/src/credential-status/status-array.ts @@ -0,0 +1,55 @@ +import * as zlib from "zlib"; + +export class StatusArray { + private readonly bitsPerEntry: 1 | 2 | 4 | 8; + private readonly statusBitMask: number; + private readonly data: Uint8Array; + + constructor(bitsPerEntry: 1 | 2 | 4 | 8, totalEntries: number) { + if (![1, 2, 4, 8].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 }); + } +} \ No newline at end of file diff --git a/src/credential-status/status-list.ts b/src/credential-status/status-list.ts new file mode 100644 index 0000000..a10b715 --- /dev/null +++ b/src/credential-status/status-list.ts @@ -0,0 +1,18 @@ +import { StatusArray } from "./status-array"; +import { cborEncode } from "../cbor"; + +export class StatusList { + static buildCborStatusList(statusArray: StatusArray, aggregationUri?: string): Uint8Array { + const compressed = statusArray.compress(); + + const statusList: Record = { + bits: statusArray.getBitsPerEntry(), + lst: compressed, + }; + + if (aggregationUri) { + statusList.aggregation_uri = aggregationUri; + } + return cborEncode(statusList); + } +} \ No newline at end of file diff --git a/src/credential-status/status-token.ts b/src/credential-status/status-token.ts new file mode 100644 index 0000000..b23ddc1 --- /dev/null +++ b/src/credential-status/status-token.ts @@ -0,0 +1,22 @@ +import { StatusArray } from "./status-array"; +import { StatusList } from "./status-list"; +import { cborEncode } from "../cbor"; +import { CoseKey } from "../cose"; +import { CWT } from "../cwt"; + +export class CWTStatusToken { + static async build(statusArray: StatusArray, type: 'sign1' | 'mac0' = 'sign1', key: CoseKey, aggregationUri?: string): Promise { + const cwt = new CWT() + cwt.setHeaders({ + protected: { + type: 'application/statuslist+cwt', + } + }); + cwt.setClaims({ + 2: 'https://example.com/statuslist', // Where the status list is going to be hosted + 6: Math.floor(Date.now() / 1000), + 65533: StatusList.buildCborStatusList(statusArray, aggregationUri), + }); + return cborEncode(await cwt.create({ type, key })) + } +} \ No newline at end of file diff --git a/src/cwt/index.ts b/src/cwt/index.ts new file mode 100644 index 0000000..3ab3bc0 --- /dev/null +++ b/src/cwt/index.ts @@ -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; + unprotected?: Record; +}; + +type CWTOptions = { + type: 'sign1' | 'mac0' | 'encrypt0'; + key: CoseKey; +}; + +export class CWT { + private claimsSet: Record = {}; + private headers: Header = {}; + + setClaims(claims: Record): void { + this.claimsSet = claims; + } + + setHeaders(headers: Header): void { + this.headers = headers; + } + + async create({ type, key }: CWTOptions): Promise { + 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'); + } + } +} diff --git a/src/index.ts b/src/index.ts index 3887586..f1b3f04 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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' diff --git a/tests/credential-status/cred-status.test.ts b/tests/credential-status/cred-status.test.ts new file mode 100644 index 0000000..31b1819 --- /dev/null +++ b/tests/credential-status/cred-status.test.ts @@ -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, 'sign1', CoseKey.fromJwk(ISSUER_PRIVATE_KEY_JWK))); + }) +}) From 0e82b468a0016379aac9510b5db6ffa1f2388100 Mon Sep 17 00:00:00 2001 From: Dinkar Date: Wed, 4 Jun 2025 13:03:05 +0530 Subject: [PATCH 02/24] refactor: update StatusArray and StatusList constructors to use options objects for better clarity and maintainability --- src/credential-status/status-array.ts | 7 ++++-- src/credential-status/status-list.ts | 15 +++++++---- src/credential-status/status-token.ts | 28 ++++++++++++++++----- tests/credential-status/cred-status.test.ts | 2 +- 4 files changed, 38 insertions(+), 14 deletions(-) diff --git a/src/credential-status/status-array.ts b/src/credential-status/status-array.ts index 999f839..01e60ec 100644 --- a/src/credential-status/status-array.ts +++ b/src/credential-status/status-array.ts @@ -1,12 +1,15 @@ 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: 1 | 2 | 4 | 8, totalEntries: number) { - if (![1, 2, 4, 8].includes(bitsPerEntry)) { + constructor(bitsPerEntry: AllowedBitsPerEntry, totalEntries: number) { + if (!allowedBitsPerEntry.includes(bitsPerEntry)) { throw new Error("Only 1, 2, 4, or 8 bits per entry are allowed."); } diff --git a/src/credential-status/status-list.ts b/src/credential-status/status-list.ts index a10b715..7dca7b7 100644 --- a/src/credential-status/status-list.ts +++ b/src/credential-status/status-list.ts @@ -1,17 +1,22 @@ import { StatusArray } from "./status-array"; import { cborEncode } from "../cbor"; +interface CborStatusListOptions { + statusArray: StatusArray; + aggregationUri?: string; +} + export class StatusList { - static buildCborStatusList(statusArray: StatusArray, aggregationUri?: string): Uint8Array { - const compressed = statusArray.compress(); + static buildCborStatusList(options: CborStatusListOptions): Uint8Array { + const compressed = options.statusArray.compress(); const statusList: Record = { - bits: statusArray.getBitsPerEntry(), + bits: options.statusArray.getBitsPerEntry(), lst: compressed, }; - if (aggregationUri) { - statusList.aggregation_uri = aggregationUri; + if (options.aggregationUri) { + statusList.aggregation_uri = options.aggregationUri; } return cborEncode(statusList); } diff --git a/src/credential-status/status-token.ts b/src/credential-status/status-token.ts index b23ddc1..65a5283 100644 --- a/src/credential-status/status-token.ts +++ b/src/credential-status/status-token.ts @@ -4,19 +4,35 @@ 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(statusArray: StatusArray, type: 'sign1' | 'mac0' = 'sign1', key: CoseKey, aggregationUri?: string): Promise { + static async build(options: CWTStatusTokenOptions): Promise { const cwt = new CWT() cwt.setHeaders({ protected: { - type: 'application/statuslist+cwt', + [CWTProtectedHeaders.TYPE]: 'application/statuslist+cwt', } }); cwt.setClaims({ - 2: 'https://example.com/statuslist', // Where the status list is going to be hosted - 6: Math.floor(Date.now() / 1000), - 65533: StatusList.buildCborStatusList(statusArray, aggregationUri), + [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, key })) + return cborEncode(await cwt.create({ type: options.type, key: options.key })) } } \ No newline at end of file diff --git a/tests/credential-status/cred-status.test.ts b/tests/credential-status/cred-status.test.ts index 31b1819..e6c4778 100644 --- a/tests/credential-status/cred-status.test.ts +++ b/tests/credential-status/cred-status.test.ts @@ -12,6 +12,6 @@ describe('status-array', () => { expect(statusArray.get(1)).toBe(3); // Will remove it before merging - console.log(await CWTStatusToken.build(statusArray, 'sign1', CoseKey.fromJwk(ISSUER_PRIVATE_KEY_JWK))); + console.log(await CWTStatusToken.build({ statusArray, type: 'sign1', key: CoseKey.fromJwk(ISSUER_PRIVATE_KEY_JWK) })); }) }) From bd500cf1cafb62bb0aa097ff589e2416e1fc2c1c Mon Sep 17 00:00:00 2001 From: Dinkar Date: Wed, 4 Jun 2025 16:06:52 +0530 Subject: [PATCH 03/24] refactor: update StatusArray and StatusList to use AllowedBitsPerEntry type and export CborStatusListOptions interface --- src/credential-status/status-array.ts | 4 ++-- src/credential-status/status-list.ts | 2 +- src/credential-status/status-token.ts | 12 +++++----- src/cwt/index.ts | 32 +++++++++++++++++++++++++++ 4 files changed, 41 insertions(+), 9 deletions(-) diff --git a/src/credential-status/status-array.ts b/src/credential-status/status-array.ts index 01e60ec..9c239c0 100644 --- a/src/credential-status/status-array.ts +++ b/src/credential-status/status-array.ts @@ -4,7 +4,7 @@ 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 bitsPerEntry: AllowedBitsPerEntry; private readonly statusBitMask: number; private readonly data: Uint8Array; @@ -28,7 +28,7 @@ export class StatusArray { return [byteIndex, bitOffset]; } - getBitsPerEntry(): 1 | 2 | 4 | 8 { + getBitsPerEntry(): AllowedBitsPerEntry { return this.bitsPerEntry; } diff --git a/src/credential-status/status-list.ts b/src/credential-status/status-list.ts index 7dca7b7..6a6f646 100644 --- a/src/credential-status/status-list.ts +++ b/src/credential-status/status-list.ts @@ -1,7 +1,7 @@ import { StatusArray } from "./status-array"; import { cborEncode } from "../cbor"; -interface CborStatusListOptions { +export interface CborStatusListOptions { statusArray: StatusArray; aggregationUri?: string; } diff --git a/src/credential-status/status-token.ts b/src/credential-status/status-token.ts index 65a5283..f673a4d 100644 --- a/src/credential-status/status-token.ts +++ b/src/credential-status/status-token.ts @@ -11,10 +11,10 @@ interface CWTStatusTokenOptions { key: CoseKey; } -enum CWTProtectedHeaders { +enum CwtProtectedHeaders { TYPE = 16 } -enum CWTClaims { +enum CwtStatusListClaims { STATUS_LIST_URI = 2, ISSUED_AT = 6, STATUS_LIST = 65533 @@ -25,13 +25,13 @@ export class CWTStatusToken { const cwt = new CWT() cwt.setHeaders({ protected: { - [CWTProtectedHeaders.TYPE]: 'application/statuslist+cwt', + [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 }), + [CwtStatusListClaims.STATUS_LIST_URI]: 'https://example.com/statuslist', // Where the status list is going to be hosted + [CwtStatusListClaims.ISSUED_AT]: Math.floor(Date.now() / 1000), + [CwtStatusListClaims.STATUS_LIST]: StatusList.buildCborStatusList({ statusArray: options.statusArray, aggregationUri: options.aggregationUri }), }); return cborEncode(await cwt.create({ type: options.type, key: options.key })) } diff --git a/src/cwt/index.ts b/src/cwt/index.ts index 3ab3bc0..684e6c9 100644 --- a/src/cwt/index.ts +++ b/src/cwt/index.ts @@ -12,10 +12,42 @@ type CWTOptions = { key: CoseKey; }; +enum CwtStandardClaims { + ISS = 1, + SUB = 2, + AUD = 3, + EXP = 4, + NBF = 5, + IAT = 6, + CTI = 7 +} + export class CWT { private claimsSet: Record = {}; private headers: Header = {}; + setIss(iss: string): void { + this.claimsSet[CwtStandardClaims.ISS] = iss; + } + setSub(sub: string): void { + this.claimsSet[CwtStandardClaims.SUB] = sub; + } + setAud(aud: string): void { + this.claimsSet[CwtStandardClaims.AUD] = aud; + } + setExp(exp: number): void { + this.claimsSet[CwtStandardClaims.EXP] = exp; + } + setNbf(nbf: number): void { + this.claimsSet[CwtStandardClaims.NBF] = nbf; + } + setIat(iat: number): void { + this.claimsSet[CwtStandardClaims.IAT] = iat; + } + setCti(cti: Uint8Array): void { + this.claimsSet[CwtStandardClaims.CTI] = cti; + } + setClaims(claims: Record): void { this.claimsSet = claims; } From 8a552a453c6efffb1adf78409917f5ee747e7eea Mon Sep 17 00:00:00 2001 From: Dinkar Date: Wed, 4 Jun 2025 18:13:48 +0530 Subject: [PATCH 04/24] refactor: update enum values in CwtStatusListClaims and CwtStandardClaims to use camelCase for consistency --- src/credential-status/status-token.ts | 12 ++++++------ src/cwt/index.ts | 28 +++++++++++++-------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/credential-status/status-token.ts b/src/credential-status/status-token.ts index f673a4d..ba50eaa 100644 --- a/src/credential-status/status-token.ts +++ b/src/credential-status/status-token.ts @@ -15,9 +15,9 @@ enum CwtProtectedHeaders { TYPE = 16 } enum CwtStatusListClaims { - STATUS_LIST_URI = 2, - ISSUED_AT = 6, - STATUS_LIST = 65533 + StatusListUri = 2, + IssuedAt = 6, + StatusList = 65533 } export class CWTStatusToken { @@ -29,9 +29,9 @@ export class CWTStatusToken { } }); cwt.setClaims({ - [CwtStatusListClaims.STATUS_LIST_URI]: 'https://example.com/statuslist', // Where the status list is going to be hosted - [CwtStatusListClaims.ISSUED_AT]: Math.floor(Date.now() / 1000), - [CwtStatusListClaims.STATUS_LIST]: StatusList.buildCborStatusList({ statusArray: options.statusArray, aggregationUri: options.aggregationUri }), + [CwtStatusListClaims.StatusListUri]: 'https://example.com/statuslist', // Where the status list is going to be hosted + [CwtStatusListClaims.IssuedAt]: Math.floor(Date.now() / 1000), + [CwtStatusListClaims.StatusList]: StatusList.buildCborStatusList({ statusArray: options.statusArray, aggregationUri: options.aggregationUri }), }); return cborEncode(await cwt.create({ type: options.type, key: options.key })) } diff --git a/src/cwt/index.ts b/src/cwt/index.ts index 684e6c9..195e015 100644 --- a/src/cwt/index.ts +++ b/src/cwt/index.ts @@ -13,13 +13,13 @@ type CWTOptions = { }; enum CwtStandardClaims { - ISS = 1, - SUB = 2, - AUD = 3, - EXP = 4, - NBF = 5, - IAT = 6, - CTI = 7 + Iss = 1, + Sub = 2, + Aud = 3, + Exp = 4, + Nbf = 5, + Iat = 6, + Cti = 7 } export class CWT { @@ -27,25 +27,25 @@ export class CWT { private headers: Header = {}; setIss(iss: string): void { - this.claimsSet[CwtStandardClaims.ISS] = iss; + this.claimsSet[CwtStandardClaims.Iss] = iss; } setSub(sub: string): void { - this.claimsSet[CwtStandardClaims.SUB] = sub; + this.claimsSet[CwtStandardClaims.Sub] = sub; } setAud(aud: string): void { - this.claimsSet[CwtStandardClaims.AUD] = aud; + this.claimsSet[CwtStandardClaims.Aud] = aud; } setExp(exp: number): void { - this.claimsSet[CwtStandardClaims.EXP] = exp; + this.claimsSet[CwtStandardClaims.Exp] = exp; } setNbf(nbf: number): void { - this.claimsSet[CwtStandardClaims.NBF] = nbf; + this.claimsSet[CwtStandardClaims.Nbf] = nbf; } setIat(iat: number): void { - this.claimsSet[CwtStandardClaims.IAT] = iat; + this.claimsSet[CwtStandardClaims.Iat] = iat; } setCti(cti: Uint8Array): void { - this.claimsSet[CwtStandardClaims.CTI] = cti; + this.claimsSet[CwtStandardClaims.Cti] = cti; } setClaims(claims: Record): void { From 6f54b85de2713e74fad475d24eb3146b8d7690cc Mon Sep 17 00:00:00 2001 From: Dinkar Date: Sun, 8 Jun 2025 12:54:02 +0530 Subject: [PATCH 05/24] feat: added CWT status token verification --- src/credential-status/index.ts | 2 +- src/credential-status/status-array.ts | 83 +++++---- src/credential-status/status-list.ts | 56 ++++-- src/credential-status/status-token.ts | 140 ++++++++++++--- src/cwt/index.ts | 179 ++++++++++++-------- tests/credential-status/cred-status.test.ts | 32 ++-- 6 files changed, 322 insertions(+), 170 deletions(-) diff --git a/src/credential-status/index.ts b/src/credential-status/index.ts index f6dc145..a3bf3e3 100644 --- a/src/credential-status/index.ts +++ b/src/credential-status/index.ts @@ -1,3 +1,3 @@ export * from './status-array' export * from './status-list' -export * from './status-token' \ No newline at end of file +export * from './status-token' diff --git a/src/credential-status/status-array.ts b/src/credential-status/status-array.ts index 9c239c0..fc9b086 100644 --- a/src/credential-status/status-array.ts +++ b/src/credential-status/status-array.ts @@ -1,58 +1,57 @@ -import * as zlib from "zlib"; +import * as zlib from 'node:zlib' -const allowedBitsPerEntry = [1, 2, 4, 8] as const -type AllowedBitsPerEntry = typeof allowedBitsPerEntry[number] +const arraySize = 1024 +export const allowedBitsPerEntry = [1, 2, 4, 8] as const +export type AllowedBitsPerEntry = (typeof allowedBitsPerEntry)[number] export class StatusArray { - private readonly bitsPerEntry: AllowedBitsPerEntry; - private readonly statusBitMask: number; - private readonly data: Uint8Array; + private readonly bitsPerEntry: AllowedBitsPerEntry + 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."); - } + constructor(bitsPerEntry: AllowedBitsPerEntry, bitArr?: Uint8Array) { + 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; + this.bitsPerEntry = bitsPerEntry + this.statusBitMask = (1 << bitsPerEntry) - 1 - const totalBits = totalEntries * bitsPerEntry; - const byteSize = Math.ceil(totalBits / 8); - this.data = new Uint8Array(byteSize); - } + this.data = bitArr ? bitArr : new Uint8Array(arraySize) + } - private computeByteAndOffset(index: number): [number, number] { - const byteIndex = Math.floor((index * this.bitsPerEntry) / 8); - const bitOffset = (index * this.bitsPerEntry) % 8; + private computeByteAndOffset(index: number): [number, number] { + const byteIndex = Math.floor((index * this.bitsPerEntry) / 8) + const bitOffset = (index * this.bitsPerEntry) % 8 - return [byteIndex, bitOffset]; - } + return [byteIndex, bitOffset] + } - getBitsPerEntry(): AllowedBitsPerEntry { - return this.bitsPerEntry; - } + getBitsPerEntry(): AllowedBitsPerEntry { + 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}.`); - } + 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); + const [byteIndex, bitOffset] = this.computeByteAndOffset(index) - // Clear current bits - this.data[byteIndex] &= ~(this.statusBitMask << bitOffset); + // Clear current bits + this.data[byteIndex] &= ~(this.statusBitMask << bitOffset) - // Set new status bits - this.data[byteIndex] |= (status & this.statusBitMask) << bitOffset; - } + // Set new status bits + this.data[byteIndex] |= (status & this.statusBitMask) << bitOffset + } - get(index: number): number { - const [byteIndex, bitOffset] = this.computeByteAndOffset(index); + get(index: number): number { + const [byteIndex, bitOffset] = this.computeByteAndOffset(index) - return (this.data[byteIndex] >> bitOffset) & this.statusBitMask; - } + return (this.data[byteIndex] >> bitOffset) & this.statusBitMask + } - compress(): Uint8Array { - return zlib.deflateSync(this.data, { level: zlib.constants.Z_BEST_COMPRESSION }); - } -} \ No newline at end of file + compress(): Uint8Array { + return zlib.deflateSync(this.data, { level: zlib.constants.Z_BEST_COMPRESSION }) + } +} diff --git a/src/credential-status/status-list.ts b/src/credential-status/status-list.ts index 6a6f646..cce6b27 100644 --- a/src/credential-status/status-list.ts +++ b/src/credential-status/status-list.ts @@ -1,23 +1,45 @@ -import { StatusArray } from "./status-array"; -import { cborEncode } from "../cbor"; +import * as zlib from 'node:zlib' +import { cborDecode, cborEncode } from '../cbor' +import { type AllowedBitsPerEntry, StatusArray, allowedBitsPerEntry } from './status-array' export interface CborStatusListOptions { - statusArray: StatusArray; - aggregationUri?: string; + statusArray: StatusArray + aggregationUri?: string } export class StatusList { - static buildCborStatusList(options: CborStatusListOptions): Uint8Array { - const compressed = options.statusArray.compress(); - - const statusList: Record = { - bits: options.statusArray.getBitsPerEntry(), - lst: compressed, - }; - - if (options.aggregationUri) { - statusList.aggregation_uri = options.aggregationUri; - } - return cborEncode(statusList); + static buildCborStatusList(options: CborStatusListOptions): Uint8Array { + const compressed = options.statusArray.compress() + + const statusList: Record = { + bits: options.statusArray.getBitsPerEntry(), + lst: compressed, + } + + if (options.aggregationUri) { + statusList.aggregation_uri = options.aggregationUri + } + return cborEncode(statusList) + } + + static verifyStatus(compressedData: Uint8Array, index: number, expectedStatus: number): boolean { + const statusList = cborDecode(compressedData) as Map + const bits = statusList.get('bits') as AllowedBitsPerEntry + const lst = statusList.get('lst') as Uint8Array + + if (!statusList || !lst || !bits) { + throw new Error('Invalid status list format.') } -} \ No newline at end of file + if (!allowedBitsPerEntry.includes(bits)) { + throw new Error(`Invalid bits per entry: ${bits}. Allowed values are ${allowedBitsPerEntry.join(', ')}.`) + } + + const statusArray = new StatusArray(bits, zlib.inflateSync(lst)) + const actualStatus = statusArray.get(index) + if (actualStatus !== expectedStatus) { + return false + } + + return true + } +} diff --git a/src/credential-status/status-token.ts b/src/credential-status/status-token.ts index ba50eaa..d5c64cf 100644 --- a/src/credential-status/status-token.ts +++ b/src/credential-status/status-token.ts @@ -1,38 +1,122 @@ -import { StatusArray } from "./status-array"; -import { StatusList } from "./status-list"; -import { cborEncode } from "../cbor"; -import { CoseKey } from "../cose"; -import { CWT } from "../cwt"; +import { cborDecode, cborEncode } from '../cbor' +import type { CoseKey, Mac0Structure, Sign1Structure } from '../cose' +import { CWT } from '../cwt' +import type { StatusArray } from './status-array' +import { StatusList } from './status-list' interface CWTStatusTokenOptions { - statusArray: StatusArray; - aggregationUri?: string; - type: 'sign1' | 'mac0'; - key: CoseKey; + claimsSet: { + statusArray: StatusArray + aggregationUri?: string + expirationTime?: number + timeToLive?: number + } + type: 'sign1' | 'mac0' + key: CoseKey +} + +interface CWTStatusTokenVerifyOptions { + type: 'sign1' | 'mac0' // Remove this + token: Uint8Array + key?: CoseKey +} + +interface CWTStatusVerifyOptions extends CWTStatusTokenVerifyOptions { + index: number + expectedStatus: number } enum CwtProtectedHeaders { - TYPE = 16 + TYPE = 16, } + enum CwtStatusListClaims { - StatusListUri = 2, - IssuedAt = 6, - StatusList = 65533 + StatusListUri = 2, + ExpirationTime = 4, + IssuedAt = 6, + StatusList = 65533, + TimeToLive = 65534, } export class CWTStatusToken { - static async build(options: CWTStatusTokenOptions): Promise { - const cwt = new CWT() - cwt.setHeaders({ - protected: { - [CwtProtectedHeaders.TYPE]: 'application/statuslist+cwt', - } - }); - cwt.setClaims({ - [CwtStatusListClaims.StatusListUri]: 'https://example.com/statuslist', // Where the status list is going to be hosted - [CwtStatusListClaims.IssuedAt]: Math.floor(Date.now() / 1000), - [CwtStatusListClaims.StatusList]: StatusList.buildCborStatusList({ statusArray: options.statusArray, aggregationUri: options.aggregationUri }), - }); - return cborEncode(await cwt.create({ type: options.type, key: options.key })) - } -} \ No newline at end of file + static async build(options: CWTStatusTokenOptions): Promise { + const cwt = new CWT() + cwt.setHeaders({ + protected: { + [CwtProtectedHeaders.TYPE]: 'application/statuslist+cwt', + }, + }) + + const claims: { [key: number]: string | number | Uint8Array } = { + [CwtStatusListClaims.StatusListUri]: 'https://example.com/statuslist', // Where the status list is going to be hosted + [CwtStatusListClaims.IssuedAt]: Math.floor(Date.now() / 1000), + [CwtStatusListClaims.StatusList]: StatusList.buildCborStatusList({ + statusArray: options.claimsSet.statusArray, + aggregationUri: options.claimsSet.aggregationUri, + }), + } + if (options.claimsSet.expirationTime) { + claims[CwtStatusListClaims.ExpirationTime] = options.claimsSet.expirationTime + } + if (options.claimsSet.timeToLive) { + claims[CwtStatusListClaims.TimeToLive] = options.claimsSet.timeToLive + } + + cwt.setClaims(claims) + return cborEncode(await cwt.create({ type: options.type, key: options.key })) + } + + static async verifyStatusToken(options: CWTStatusTokenVerifyOptions): Promise { + const cwt = cborDecode(options.token) as Sign1Structure | Mac0Structure + const protectedHeaders = cborDecode(cwt[0]) as Map + + const type = protectedHeaders.get(String(CwtProtectedHeaders.TYPE)) + if (!type || type !== 'application/statuslist+cwt') { + throw new Error('CWT status token does not have the correct type in protected headers') + } + + if (!cwt[2]) { + throw new Error('CWT status token does not contain claims') + } + const claims = cborDecode(cwt[2]) as Map + + // Check if is the same as the one used to fetch the token + if (!claims.has(String(CwtStatusListClaims.StatusListUri))) { + throw new Error('CWT status token does not contain status list URI') + } + if (!claims.has(String(CwtStatusListClaims.IssuedAt))) { + throw new Error('CWT status token does not contain issued at claim') + } + if (!claims.has(String(CwtStatusListClaims.StatusList))) { + throw new Error('CWT status token does not contain status list') + } + + const expirationTime = claims.get(String(CwtStatusListClaims.ExpirationTime)) + if (expirationTime && typeof expirationTime === 'number' && expirationTime < Math.floor(Date.now() / 1000)) { + throw new Error('CWT status token has expired') + } + + const validSignature = await CWT.verify({ type: options.type, token: options.token, key: options.key }) + if (!validSignature) { + throw new Error('Invalid signature for CWT status token') + } + + return true + } + + static async verifyStatus(options: CWTStatusVerifyOptions): Promise { + const validStatusToken = await CWTStatusToken.verifyStatusToken(options) + if (validStatusToken) { + const cwt = cborDecode(options.token) as Sign1Structure | Mac0Structure + if (!cwt[2]) { + throw new Error('CWT status token does not contain claims') + } + + const claims = cborDecode(cwt[2]) as Map + const statusList = claims.get(String(CwtStatusListClaims.StatusList)) + return StatusList.verifyStatus(statusList as Uint8Array, options.index, options.expectedStatus) + } + + return false + } +} diff --git a/src/cwt/index.ts b/src/cwt/index.ts index 195e015..6529751 100644 --- a/src/cwt/index.ts +++ b/src/cwt/index.ts @@ -1,90 +1,125 @@ -import { CoseKey, Mac0, Mac0Options, Mac0Structure, Sign1, Sign1Options, Sign1Structure } from '../cose'; -import { mdocContext } from '../../tests/context'; -import { cborEncode } from '../cbor'; +import { mdocContext } from '../../tests/context' +import { cborDecode, cborEncode } from '../cbor' +import { + type CoseKey, + Mac0, + type Mac0Options, + type Mac0Structure, + Sign1, + type Sign1Options, + type Sign1Structure, +} from '../cose' type Header = { - protected?: Record; - unprotected?: Record; -}; + protected?: Record + unprotected?: Record +} type CWTOptions = { - type: 'sign1' | 'mac0' | 'encrypt0'; - key: CoseKey; -}; + type: 'sign1' | 'mac0' | 'encrypt0' + key: CoseKey +} + +interface CWTVerifyOptions { + type: 'sign1' | 'mac0' + token: Uint8Array + key?: CoseKey +} enum CwtStandardClaims { - Iss = 1, - Sub = 2, - Aud = 3, - Exp = 4, - Nbf = 5, - Iat = 6, - Cti = 7 + Iss = 1, + Sub = 2, + Aud = 3, + Exp = 4, + Nbf = 5, + Iat = 6, + Cti = 7, } export class CWT { - private claimsSet: Record = {}; - private headers: Header = {}; + private claimsSet: Record = {} + private headers: Header = {} - setIss(iss: string): void { - this.claimsSet[CwtStandardClaims.Iss] = iss; - } - setSub(sub: string): void { - this.claimsSet[CwtStandardClaims.Sub] = sub; - } - setAud(aud: string): void { - this.claimsSet[CwtStandardClaims.Aud] = aud; - } - setExp(exp: number): void { - this.claimsSet[CwtStandardClaims.Exp] = exp; - } - setNbf(nbf: number): void { - this.claimsSet[CwtStandardClaims.Nbf] = nbf; - } - setIat(iat: number): void { - this.claimsSet[CwtStandardClaims.Iat] = iat; - } - setCti(cti: Uint8Array): void { - this.claimsSet[CwtStandardClaims.Cti] = cti; - } + setIss(iss: string): void { + this.claimsSet[CwtStandardClaims.Iss] = iss + } + setSub(sub: string): void { + this.claimsSet[CwtStandardClaims.Sub] = sub + } + setAud(aud: string): void { + this.claimsSet[CwtStandardClaims.Aud] = aud + } + setExp(exp: number): void { + this.claimsSet[CwtStandardClaims.Exp] = exp + } + setNbf(nbf: number): void { + this.claimsSet[CwtStandardClaims.Nbf] = nbf + } + setIat(iat: number): void { + this.claimsSet[CwtStandardClaims.Iat] = iat + } + setCti(cti: Uint8Array): void { + this.claimsSet[CwtStandardClaims.Cti] = cti + } - setClaims(claims: Record): void { - this.claimsSet = claims; - } + setClaims(claims: Record): void { + this.claimsSet = claims + } - setHeaders(headers: Header): void { - this.headers = headers; - } + setHeaders(headers: Header): void { + this.headers = headers + } - async create({ type, key }: CWTOptions): Promise { - 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 - }; + async create({ type, key }: CWTOptions): Promise { + 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, + } - 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 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, + } + + const mac0 = new Mac0(mac0Options) + // await mac0.addTag({ privateKey: key, ephemeralKey: key, sessionTranscript: new SessionTranscript({ handover: new QrHandover() }) }, mdocContext); + // return mac0.encodedStructure(); + throw new Error('MAC0 is not yet implemented') + } + case 'encrypt0': + throw new Error('Encrypt0 is not yet implemented') + default: + throw new Error('Unsupported CWT type') + } + } - 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'); + static async verify({ type, token, key }: CWTVerifyOptions): Promise { + const cwt = cborDecode(token) as Sign1Structure | Mac0Structure + switch (type) { + case 'sign1': { + const sign1Options: Sign1Options = { + protectedHeaders: cwt[0], + unprotectedHeaders: cwt[1], + payload: cwt[2], + signature: cwt[3], } + const sign1 = new Sign1(sign1Options) + return await sign1.verify({ key }, mdocContext) + } + default: + throw new Error('Unsupported CWT type for verification') } + } } diff --git a/tests/credential-status/cred-status.test.ts b/tests/credential-status/cred-status.test.ts index e6c4778..b8c3db0 100644 --- a/tests/credential-status/cred-status.test.ts +++ b/tests/credential-status/cred-status.test.ts @@ -1,17 +1,29 @@ import { describe, expect, test } from 'vitest' -import { CoseKey, CWTStatusToken, StatusArray } from '../../src' -import { ISSUER_PRIVATE_KEY_JWK } from '../issuing/config'; +import { CWTStatusToken, CoseKey, 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); + test('should create and verify a CWTStatusToken with a StatusArray', async () => { + const statusArray = new StatusArray(2) - statusArray.set(0, 2); - statusArray.set(1, 3); - expect(statusArray.get(0)).toBe(2); - expect(statusArray.get(1)).toBe(3); + 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) })); + const cwtStatusToken = await CWTStatusToken.build({ + claimsSet: { statusArray }, + type: 'sign1', + key: CoseKey.fromJwk(ISSUER_PRIVATE_KEY_JWK), }) + const verify = await CWTStatusToken.verifyStatus({ + type: 'sign1', + token: cwtStatusToken, + key: CoseKey.fromJwk(ISSUER_PRIVATE_KEY_JWK), + index: 0, + expectedStatus: 2, + }) + + expect(verify).toBeTruthy() + }) }) From 113785c52c09b1087cbcedbabe9bc43718782f6d Mon Sep 17 00:00:00 2001 From: Dinkar Date: Sun, 8 Jun 2025 13:11:17 +0530 Subject: [PATCH 06/24] refactor: rename constructor parameter in StatusArray and update variable names for clarity; --- src/credential-status/status-array.ts | 5 ++--- src/credential-status/status-list.ts | 9 +++++---- src/credential-status/status-token.ts | 6 +++--- tests/credential-status/cred-status.test.ts | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/credential-status/status-array.ts b/src/credential-status/status-array.ts index fc9b086..fc6a0d1 100644 --- a/src/credential-status/status-array.ts +++ b/src/credential-status/status-array.ts @@ -9,15 +9,14 @@ export class StatusArray { private readonly statusBitMask: number private readonly data: Uint8Array - constructor(bitsPerEntry: AllowedBitsPerEntry, bitArr?: Uint8Array) { + constructor(bitsPerEntry: AllowedBitsPerEntry, byteArr?: Uint8Array) { 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 - - this.data = bitArr ? bitArr : new Uint8Array(arraySize) + this.data = byteArr ? byteArr : new Uint8Array(arraySize) } private computeByteAndOffset(index: number): [number, number] { diff --git a/src/credential-status/status-list.ts b/src/credential-status/status-list.ts index cce6b27..e46875b 100644 --- a/src/credential-status/status-list.ts +++ b/src/credential-status/status-list.ts @@ -22,8 +22,8 @@ export class StatusList { return cborEncode(statusList) } - static verifyStatus(compressedData: Uint8Array, index: number, expectedStatus: number): boolean { - const statusList = cborDecode(compressedData) as Map + static verifyStatus(cborStatusList: Uint8Array, index: number, expectedStatus: number): boolean { + const statusList = cborDecode(cborStatusList) as Map const bits = statusList.get('bits') as AllowedBitsPerEntry const lst = statusList.get('lst') as Uint8Array @@ -39,7 +39,8 @@ export class StatusList { if (actualStatus !== expectedStatus) { return false } - - return true + else { + return true + } } } diff --git a/src/credential-status/status-token.ts b/src/credential-status/status-token.ts index d5c64cf..5915b0f 100644 --- a/src/credential-status/status-token.ts +++ b/src/credential-status/status-token.ts @@ -79,7 +79,6 @@ export class CWTStatusToken { throw new Error('CWT status token does not contain claims') } const claims = cborDecode(cwt[2]) as Map - // Check if is the same as the one used to fetch the token if (!claims.has(String(CwtStatusListClaims.StatusListUri))) { throw new Error('CWT status token does not contain status list URI') @@ -116,7 +115,8 @@ export class CWTStatusToken { const statusList = claims.get(String(CwtStatusListClaims.StatusList)) return StatusList.verifyStatus(statusList as Uint8Array, options.index, options.expectedStatus) } - - return false + else { + return false + } } } diff --git a/tests/credential-status/cred-status.test.ts b/tests/credential-status/cred-status.test.ts index b8c3db0..745994f 100644 --- a/tests/credential-status/cred-status.test.ts +++ b/tests/credential-status/cred-status.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test } from 'vitest' import { CWTStatusToken, CoseKey, StatusArray } from '../../src' import { ISSUER_PRIVATE_KEY_JWK } from '../issuing/config' -describe('status-array', () => { +describe('CWTStatusToken', () => { test('should create and verify a CWTStatusToken with a StatusArray', async () => { const statusArray = new StatusArray(2) From f95c6f9144e9ead67d5eb07fb7a62481834bed9c Mon Sep 17 00:00:00 2001 From: Dinkar Date: Sun, 8 Jun 2025 13:41:58 +0530 Subject: [PATCH 07/24] refactor: simplify return statements in StatusList and CWTStatusToken verification methods --- src/credential-status/status-list.ts | 5 ++--- src/credential-status/status-token.ts | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/credential-status/status-list.ts b/src/credential-status/status-list.ts index e46875b..b0043ff 100644 --- a/src/credential-status/status-list.ts +++ b/src/credential-status/status-list.ts @@ -39,8 +39,7 @@ export class StatusList { if (actualStatus !== expectedStatus) { return false } - else { - return true - } + + return true } } diff --git a/src/credential-status/status-token.ts b/src/credential-status/status-token.ts index 5915b0f..35f0eac 100644 --- a/src/credential-status/status-token.ts +++ b/src/credential-status/status-token.ts @@ -115,8 +115,7 @@ export class CWTStatusToken { const statusList = claims.get(String(CwtStatusListClaims.StatusList)) return StatusList.verifyStatus(statusList as Uint8Array, options.index, options.expectedStatus) } - else { - return false - } + + return false } } From 1ed21b069e41e3a860d91cb91d42ef5bdd199d2d Mon Sep 17 00:00:00 2001 From: Dinkar Date: Sun, 15 Jun 2025 16:30:06 +0530 Subject: [PATCH 08/24] feat: implement CoseType enum and update CWTStatusToken to use CoseType for type definitions --- src/cose/index.ts | 1 + src/cose/type.ts | 24 +++++++++++++++ src/credential-status/status-token.ts | 34 +++++++++++++-------- src/cwt/index.ts | 27 ++++++++-------- tests/credential-status/cred-status.test.ts | 4 +-- 5 files changed, 61 insertions(+), 29 deletions(-) create mode 100644 src/cose/type.ts diff --git a/src/cose/index.ts b/src/cose/index.ts index 4a20ea5..c70033c 100644 --- a/src/cose/index.ts +++ b/src/cose/index.ts @@ -3,3 +3,4 @@ export * from './mac0' export * from './sign1' export * from './error' export * from './key' +export * from './type' diff --git a/src/cose/type.ts b/src/cose/type.ts new file mode 100644 index 0000000..607bdae --- /dev/null +++ b/src/cose/type.ts @@ -0,0 +1,24 @@ +export enum CoseType { + Sign = 'sign', + Sign1 = 'sign1', + Encrypt = 'encrypt', + Encrypt0 = 'encrypt0', + Mac = 'mac', + Mac0 = 'mac0', +} +export enum CoseTag { + Sign = 98, + Sign1 = 18, + Encrypt = 96, + Encrypt0 = 16, + Mac = 97, + Mac0 = 17, +} +export const CoseTypeToTag: Record = { + [CoseType.Sign]: CoseTag.Sign, + [CoseType.Sign1]: CoseTag.Sign1, + [CoseType.Encrypt]: CoseTag.Encrypt, + [CoseType.Encrypt0]: CoseTag.Encrypt0, + [CoseType.Mac]: CoseTag.Mac, + [CoseType.Mac0]: CoseTag.Mac0, +} diff --git a/src/credential-status/status-token.ts b/src/credential-status/status-token.ts index 35f0eac..5a631b2 100644 --- a/src/credential-status/status-token.ts +++ b/src/credential-status/status-token.ts @@ -1,5 +1,7 @@ import { cborDecode, cborEncode } from '../cbor' -import type { CoseKey, Mac0Structure, Sign1Structure } from '../cose' +import { Tag } from '../cbor/cbor-x' +import type { CoseKey } from '../cose' +import { CoseType, CoseTypeToTag, Mac0, Sign1 } from '../cose' import { CWT } from '../cwt' import type { StatusArray } from './status-array' import { StatusList } from './status-list' @@ -11,12 +13,11 @@ interface CWTStatusTokenOptions { expirationTime?: number timeToLive?: number } - type: 'sign1' | 'mac0' + type: CoseType key: CoseKey } interface CWTStatusTokenVerifyOptions { - type: 'sign1' | 'mac0' // Remove this token: Uint8Array key?: CoseKey } @@ -63,22 +64,21 @@ export class CWTStatusToken { } cwt.setClaims(claims) - return cborEncode(await cwt.create({ type: options.type, key: options.key })) + return cborEncode(new Tag(await cwt.create({ type: options.type, key: options.key }), CoseTypeToTag[options.type])) } static async verifyStatusToken(options: CWTStatusTokenVerifyOptions): Promise { - const cwt = cborDecode(options.token) as Sign1Structure | Mac0Structure - const protectedHeaders = cborDecode(cwt[0]) as Map + const cwt = cborDecode(options.token) as Sign1 | Mac0 - const type = protectedHeaders.get(String(CwtProtectedHeaders.TYPE)) + const type = cwt.protectedHeaders.headers?.get(String(CwtProtectedHeaders.TYPE)) if (!type || type !== 'application/statuslist+cwt') { throw new Error('CWT status token does not have the correct type in protected headers') } - if (!cwt[2]) { + if (!cwt.payload) { throw new Error('CWT status token does not contain claims') } - const claims = cborDecode(cwt[2]) as Map + const claims = cborDecode(cwt.payload) as Map // Check if is the same as the one used to fetch the token if (!claims.has(String(CwtStatusListClaims.StatusListUri))) { throw new Error('CWT status token does not contain status list URI') @@ -95,7 +95,15 @@ export class CWTStatusToken { throw new Error('CWT status token has expired') } - const validSignature = await CWT.verify({ type: options.type, token: options.token, key: options.key }) + let coseType: CoseType + if (cwt instanceof Sign1) { + coseType = CoseType.Sign1 + } else if (cwt instanceof Mac0) { + coseType = CoseType.Mac0 + } else { + throw new Error(`Unimplemented/Unsupported CWT type`) + } + const validSignature = await CWT.verify({ type: coseType, token: options.token, key: options.key }) if (!validSignature) { throw new Error('Invalid signature for CWT status token') } @@ -106,12 +114,12 @@ export class CWTStatusToken { static async verifyStatus(options: CWTStatusVerifyOptions): Promise { const validStatusToken = await CWTStatusToken.verifyStatusToken(options) if (validStatusToken) { - const cwt = cborDecode(options.token) as Sign1Structure | Mac0Structure - if (!cwt[2]) { + const cwt = cborDecode(options.token) as Sign1 | Mac0 + if (!cwt.payload) { throw new Error('CWT status token does not contain claims') } - const claims = cborDecode(cwt[2]) as Map + const claims = cborDecode(cwt.payload) as Map const statusList = claims.get(String(CwtStatusListClaims.StatusList)) return StatusList.verifyStatus(statusList as Uint8Array, options.index, options.expectedStatus) } diff --git a/src/cwt/index.ts b/src/cwt/index.ts index 6529751..1999106 100644 --- a/src/cwt/index.ts +++ b/src/cwt/index.ts @@ -9,6 +9,7 @@ import { type Sign1Options, type Sign1Structure, } from '../cose' +import { CoseType } from '../cose' type Header = { protected?: Record @@ -16,12 +17,12 @@ type Header = { } type CWTOptions = { - type: 'sign1' | 'mac0' | 'encrypt0' + type: CoseType key: CoseKey } interface CWTVerifyOptions { - type: 'sign1' | 'mac0' + type: CoseType token: Uint8Array key?: CoseKey } @@ -72,7 +73,7 @@ export class CWT { async create({ type, key }: CWTOptions): Promise { switch (type) { - case 'sign1': { + case CoseType.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, @@ -83,7 +84,7 @@ export class CWT { await sign1.addSignature({ signingKey: key }, { cose: mdocContext.cose }) return sign1.encodedStructure() } - case 'mac0': { + case CoseType.Mac0: { if (!this.headers.protected || !this.headers.unprotected) { throw new Error('Protected and unprotected headers must be defined for MAC0') } @@ -98,28 +99,26 @@ export class CWT { // return mac0.encodedStructure(); throw new Error('MAC0 is not yet implemented') } - case 'encrypt0': - throw new Error('Encrypt0 is not yet implemented') default: - throw new Error('Unsupported CWT type') + throw new Error(`${type} is not yet implemented`) } } static async verify({ type, token, key }: CWTVerifyOptions): Promise { - const cwt = cborDecode(token) as Sign1Structure | Mac0Structure + const cwt = cborDecode(token) as Sign1 | Mac0 switch (type) { - case 'sign1': { + case CoseType.Sign1: { const sign1Options: Sign1Options = { - protectedHeaders: cwt[0], - unprotectedHeaders: cwt[1], - payload: cwt[2], - signature: cwt[3], + protectedHeaders: cwt.protectedHeaders, + unprotectedHeaders: cwt.unprotectedHeaders, + payload: cwt.payload, + signature: (cwt as Sign1).signature, } const sign1 = new Sign1(sign1Options) return await sign1.verify({ key }, mdocContext) } default: - throw new Error('Unsupported CWT type for verification') + throw new Error(`${type} is not yet implemented for verification`) } } } diff --git a/tests/credential-status/cred-status.test.ts b/tests/credential-status/cred-status.test.ts index 745994f..bd055a1 100644 --- a/tests/credential-status/cred-status.test.ts +++ b/tests/credential-status/cred-status.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from 'vitest' import { CWTStatusToken, CoseKey, StatusArray } from '../../src' +import { CoseType } from '../../src/cose' import { ISSUER_PRIVATE_KEY_JWK } from '../issuing/config' describe('CWTStatusToken', () => { @@ -13,11 +14,10 @@ describe('CWTStatusToken', () => { const cwtStatusToken = await CWTStatusToken.build({ claimsSet: { statusArray }, - type: 'sign1', + type: CoseType.Sign1, key: CoseKey.fromJwk(ISSUER_PRIVATE_KEY_JWK), }) const verify = await CWTStatusToken.verifyStatus({ - type: 'sign1', token: cwtStatusToken, key: CoseKey.fromJwk(ISSUER_PRIVATE_KEY_JWK), index: 0, From 4c33d5d5cd50cfc83712736d76109c047bc41c7f Mon Sep 17 00:00:00 2001 From: Dinkar Date: Sun, 15 Jun 2025 16:31:56 +0530 Subject: [PATCH 09/24] fix: standardize error message formatting in CWTStatusToken verification --- src/credential-status/status-token.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/credential-status/status-token.ts b/src/credential-status/status-token.ts index 5a631b2..364d03e 100644 --- a/src/credential-status/status-token.ts +++ b/src/credential-status/status-token.ts @@ -101,7 +101,7 @@ export class CWTStatusToken { } else if (cwt instanceof Mac0) { coseType = CoseType.Mac0 } else { - throw new Error(`Unimplemented/Unsupported CWT type`) + throw new Error('Unimplemented/Unsupported CWT type') } const validSignature = await CWT.verify({ type: coseType, token: options.token, key: options.key }) if (!validSignature) { From fcb5c8fa04bb724cf969ff430d6ec4403e8f77e0 Mon Sep 17 00:00:00 2001 From: Dinkar Date: Sun, 15 Jun 2025 16:48:51 +0530 Subject: [PATCH 10/24] feat: migrate zlib usage to pako for compression and decompression in StatusArray and StatusList --- package.json | 2 ++ pnpm-lock.yaml | 16 ++++++++++++++++ src/credential-status/status-array.ts | 4 ++-- src/credential-status/status-list.ts | 4 ++-- 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 34c8737..53148f0 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,9 @@ "@panva/hkdf": "^1.2.1", "@peculiar/x509": "^1.12.3", "@types/node": "^20.14.11", + "@types/pako": "^2.0.3", "jose": "^5.9.3", + "pako": "^2.1.0", "tsup": "^8.3.5", "typescript": "^5.6.3", "vitest": "^2.1.5" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9ddd2a9..923f601 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,9 +33,15 @@ importers: '@types/node': specifier: ^20.14.11 version: 20.17.6 + '@types/pako': + specifier: ^2.0.3 + version: 2.0.3 jose: specifier: ^5.9.3 version: 5.9.6 + pako: + specifier: ^2.1.0 + version: 2.1.0 tsup: specifier: ^8.3.5 version: 8.3.5(postcss@8.5.2)(typescript@5.6.3) @@ -728,6 +734,9 @@ packages: '@types/node@20.17.6': resolution: {integrity: sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ==} + '@types/pako@2.0.3': + resolution: {integrity: sha512-bq0hMV9opAcrmE0Byyo0fY3Ew4tgOevJmQ9grUhpXQhYfyLJ1Kqg3P33JT5fdbT2AjeAjR51zqqVjAL/HMkx7Q==} + '@vitest/expect@2.1.9': resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} @@ -1159,6 +1168,9 @@ packages: package-manager-detector@0.2.4: resolution: {integrity: sha512-H/OUu9/zUfP89z1APcBf2X8Us0tt8dUK4lUmKqz12QNXif3DxAs1/YqjGtcutZi1zQqeNQRWr9C+EbQnnvSSFA==} + pako@2.1.0: + resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -2151,6 +2163,8 @@ snapshots: dependencies: undici-types: 6.19.8 + '@types/pako@2.0.3': {} + '@vitest/expect@2.1.9': dependencies: '@vitest/spy': 2.1.9 @@ -2581,6 +2595,8 @@ snapshots: package-manager-detector@0.2.4: {} + pako@2.1.0: {} + path-exists@4.0.0: {} path-key@3.1.1: {} diff --git a/src/credential-status/status-array.ts b/src/credential-status/status-array.ts index fc6a0d1..474c59f 100644 --- a/src/credential-status/status-array.ts +++ b/src/credential-status/status-array.ts @@ -1,4 +1,4 @@ -import * as zlib from 'node:zlib' +import * as zlib from 'pako' const arraySize = 1024 export const allowedBitsPerEntry = [1, 2, 4, 8] as const @@ -51,6 +51,6 @@ export class StatusArray { } compress(): Uint8Array { - return zlib.deflateSync(this.data, { level: zlib.constants.Z_BEST_COMPRESSION }) + return zlib.deflate(this.data) } } diff --git a/src/credential-status/status-list.ts b/src/credential-status/status-list.ts index b0043ff..1cbe853 100644 --- a/src/credential-status/status-list.ts +++ b/src/credential-status/status-list.ts @@ -1,4 +1,4 @@ -import * as zlib from 'node:zlib' +import * as zlib from 'pako' import { cborDecode, cborEncode } from '../cbor' import { type AllowedBitsPerEntry, StatusArray, allowedBitsPerEntry } from './status-array' @@ -34,7 +34,7 @@ export class StatusList { throw new Error(`Invalid bits per entry: ${bits}. Allowed values are ${allowedBitsPerEntry.join(', ')}.`) } - const statusArray = new StatusArray(bits, zlib.inflateSync(lst)) + const statusArray = new StatusArray(bits, zlib.inflate(lst)) const actualStatus = statusArray.get(index) if (actualStatus !== expectedStatus) { return false From 19a72b6a41560233130141137b21054fdd558843 Mon Sep 17 00:00:00 2001 From: Dinkar Date: Mon, 16 Jun 2025 14:49:08 +0530 Subject: [PATCH 11/24] feat: add fetchStatusListUri method to retrieve status list from a given URI with timeout handling --- src/credential-status/status-token.ts | 28 +++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/credential-status/status-token.ts b/src/credential-status/status-token.ts index 364d03e..0865593 100644 --- a/src/credential-status/status-token.ts +++ b/src/credential-status/status-token.ts @@ -126,4 +126,32 @@ export class CWTStatusToken { return false } + + static async fetchStatusListUri(statusListUri: string): Promise { + if (!statusListUri.startsWith('https://')) { + throw new Error(`Status list URI must be HTTPS: ${statusListUri}`) + } + + const abortController = new AbortController() + setTimeout(() => { + abortController.abort() + }, 5000) + try { + const response = await fetch(statusListUri, { + signal: abortController.signal, + headers: { + Accept: 'application/statuslist+cwt', + }, + }) + const buffer = await response.arrayBuffer() + return new Uint8Array(buffer) + } catch (error) { + if (error instanceof Error && error.name === 'AbortError') { + throw new Error(`Fetch operation timed out for status list URI: ${statusListUri}`) + } + throw new Error( + `Error fetching status list from ${statusListUri}: ${error instanceof Error ? error.message : String(error)}` + ) + } + } } From e4af8281f34ff9d1ea7946c1df2ed299d8432b95 Mon Sep 17 00:00:00 2001 From: Dinkar Date: Mon, 16 Jun 2025 16:10:54 +0530 Subject: [PATCH 12/24] feat: add abort-controller and node-fetch for improved fetch handling in CWTStatusToken --- package.json | 7 +- pnpm-lock.yaml | 228 +++++++++++++++++++++++++- src/credential-status/status-token.ts | 4 +- 3 files changed, 233 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 53148f0..e63056e 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,10 @@ "changeset-version": "pnpm changeset version && pnpm style:fix" }, "dependencies": { - "buffer": "^6.0.3" + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "node-fetch": "2", + "pako": "^2.1.0" }, "devDependencies": { "@biomejs/biome": "^1.9.4", @@ -40,9 +43,9 @@ "@panva/hkdf": "^1.2.1", "@peculiar/x509": "^1.12.3", "@types/node": "^20.14.11", + "@types/node-fetch": "^2.6.12", "@types/pako": "^2.0.3", "jose": "^5.9.3", - "pako": "^2.1.0", "tsup": "^8.3.5", "typescript": "^5.6.3", "vitest": "^2.1.5" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 923f601..53d8e2b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,18 @@ importers: .: dependencies: + abort-controller: + specifier: ^3.0.0 + version: 3.0.0 buffer: specifier: ^6.0.3 version: 6.0.3 + node-fetch: + specifier: '2' + version: 2.7.0 + pako: + specifier: ^2.1.0 + version: 2.1.0 devDependencies: '@biomejs/biome': specifier: ^1.9.4 @@ -33,15 +42,15 @@ importers: '@types/node': specifier: ^20.14.11 version: 20.17.6 + '@types/node-fetch': + specifier: ^2.6.12 + version: 2.6.12 '@types/pako': specifier: ^2.0.3 version: 2.0.3 jose: specifier: ^5.9.3 version: 5.9.6 - pako: - specifier: ^2.1.0 - version: 2.1.0 tsup: specifier: ^8.3.5 version: 8.3.5(postcss@8.5.2)(typescript@5.6.3) @@ -728,6 +737,9 @@ packages: '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + '@types/node-fetch@2.6.12': + resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==} + '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} @@ -766,6 +778,10 @@ packages: '@vitest/utils@2.1.9': resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -804,6 +820,9 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -834,6 +853,10 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + chai@5.1.2: resolution: {integrity: sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==} engines: {node: '>=12'} @@ -860,6 +883,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -897,6 +924,10 @@ packages: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} @@ -905,6 +936,10 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -918,9 +953,25 @@ packages: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + es-module-lexer@1.6.0: resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==} + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} @@ -939,6 +990,10 @@ packages: estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + expect-type@1.1.0: resolution: {integrity: sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==} engines: {node: '>=12.0.0'} @@ -977,6 +1032,10 @@ packages: resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} + form-data@4.0.3: + resolution: {integrity: sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==} + engines: {node: '>= 6'} + fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -990,6 +1049,17 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -1002,9 +1072,25 @@ packages: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + human-id@1.0.2: resolution: {integrity: sha512-UNopramDEhHJD+VR+ehk8rOslwSfByxPIZyJRfV739NDhN5LF1fa1MqnzKm2lGTQRjNrjK19Q5fhkgIfjlVUKw==} @@ -1100,6 +1186,10 @@ packages: magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1108,6 +1198,14 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + minimatch@9.0.5: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} @@ -1131,6 +1229,15 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -1419,6 +1526,9 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@1.0.1: resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} @@ -1531,9 +1641,15 @@ packages: jsdom: optional: true + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + whatwg-url@7.1.0: resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} @@ -2157,6 +2273,11 @@ snapshots: '@types/estree@1.0.6': {} + '@types/node-fetch@2.6.12': + dependencies: + '@types/node': 20.17.6 + form-data: 4.0.3 + '@types/node@12.20.55': {} '@types/node@20.17.6': @@ -2205,6 +2326,10 @@ snapshots: loupe: 3.1.3 tinyrainbow: 1.2.0 + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + ansi-colors@4.1.3: {} ansi-regex@5.0.1: {} @@ -2233,6 +2358,8 @@ snapshots: assertion-error@2.0.1: {} + asynckit@0.4.0: {} + balanced-match@1.0.2: {} base64-js@1.5.1: {} @@ -2261,6 +2388,11 @@ snapshots: cac@6.7.14: {} + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + chai@5.1.2: dependencies: assertion-error: 2.0.1 @@ -2285,6 +2417,10 @@ snapshots: color-name@1.1.4: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + commander@4.1.1: {} consola@3.2.3: {} @@ -2311,12 +2447,20 @@ snapshots: deep-eql@5.0.2: {} + delayed-stream@1.0.0: {} + detect-indent@6.1.0: {} dir-glob@3.0.1: dependencies: path-type: 4.0.0 + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + eastasianwidth@0.2.0: {} emoji-regex@8.0.0: {} @@ -2328,8 +2472,23 @@ snapshots: ansi-colors: 4.1.3 strip-ansi: 6.0.1 + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + es-module-lexer@1.6.0: {} + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + esbuild@0.21.5: optionalDependencies: '@esbuild/aix-ppc64': 0.21.5 @@ -2389,6 +2548,8 @@ snapshots: dependencies: '@types/estree': 1.0.6 + event-target-shim@5.0.1: {} + expect-type@1.1.0: {} extendable-error@0.1.7: {} @@ -2429,6 +2590,14 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + form-data@4.0.3: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11 @@ -2444,6 +2613,26 @@ snapshots: fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -2466,8 +2655,20 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 + gopd@1.2.0: {} + graceful-fs@4.2.11: {} + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + human-id@1.0.2: {} iconv-lite@0.4.24: @@ -2544,6 +2745,8 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + math-intrinsics@1.1.0: {} + merge2@1.4.1: {} micromatch@4.0.8: @@ -2551,6 +2754,12 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + minimatch@9.0.5: dependencies: brace-expansion: 2.0.1 @@ -2569,6 +2778,10 @@ snapshots: nanoid@3.3.8: {} + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + object-assign@4.1.1: {} os-tmpdir@1.0.2: {} @@ -2826,6 +3039,8 @@ snapshots: dependencies: is-number: 7.0.0 + tr46@0.0.3: {} + tr46@1.0.1: dependencies: punycode: 2.3.1 @@ -2937,8 +3152,15 @@ snapshots: - supports-color - terser + webidl-conversions@3.0.1: {} + webidl-conversions@4.0.2: {} + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + whatwg-url@7.1.0: dependencies: lodash.sortby: 4.7.0 diff --git a/src/credential-status/status-token.ts b/src/credential-status/status-token.ts index 0865593..a1ffdf0 100644 --- a/src/credential-status/status-token.ts +++ b/src/credential-status/status-token.ts @@ -1,3 +1,5 @@ +import AbortController from 'abort-controller' +import fetch from 'node-fetch' import { cborDecode, cborEncode } from '../cbor' import { Tag } from '../cbor/cbor-x' import type { CoseKey } from '../cose' @@ -138,7 +140,7 @@ export class CWTStatusToken { }, 5000) try { const response = await fetch(statusListUri, { - signal: abortController.signal, + signal: abortController.signal as NonNullable, headers: { Accept: 'application/statuslist+cwt', }, From 21fdb404ef9a017af98ad157a6ab3d5afe7ccd12 Mon Sep 17 00:00:00 2001 From: Dinkar Date: Tue, 17 Jun 2025 13:51:21 +0530 Subject: [PATCH 13/24] refactor: rename CoseType to CoseStructureType and update related references --- src/cose/type.ts | 18 ++++++------- src/credential-status/status-list.ts | 28 ++++++++++++++------- src/credential-status/status-token.ts | 14 +++++------ src/cwt/index.ts | 12 ++++----- tests/credential-status/cred-status.test.ts | 4 +-- 5 files changed, 43 insertions(+), 33 deletions(-) diff --git a/src/cose/type.ts b/src/cose/type.ts index 607bdae..e9c6179 100644 --- a/src/cose/type.ts +++ b/src/cose/type.ts @@ -1,4 +1,4 @@ -export enum CoseType { +export enum CoseStructureType { Sign = 'sign', Sign1 = 'sign1', Encrypt = 'encrypt', @@ -6,7 +6,7 @@ export enum CoseType { Mac = 'mac', Mac0 = 'mac0', } -export enum CoseTag { +export enum CoseStructureTag { Sign = 98, Sign1 = 18, Encrypt = 96, @@ -14,11 +14,11 @@ export enum CoseTag { Mac = 97, Mac0 = 17, } -export const CoseTypeToTag: Record = { - [CoseType.Sign]: CoseTag.Sign, - [CoseType.Sign1]: CoseTag.Sign1, - [CoseType.Encrypt]: CoseTag.Encrypt, - [CoseType.Encrypt0]: CoseTag.Encrypt0, - [CoseType.Mac]: CoseTag.Mac, - [CoseType.Mac0]: CoseTag.Mac0, +export const CoseTypeToTag: Record = { + [CoseStructureType.Sign]: CoseStructureTag.Sign, + [CoseStructureType.Sign1]: CoseStructureTag.Sign1, + [CoseStructureType.Encrypt]: CoseStructureTag.Encrypt, + [CoseStructureType.Encrypt0]: CoseStructureTag.Encrypt0, + [CoseStructureType.Mac]: CoseStructureTag.Mac, + [CoseStructureType.Mac0]: CoseStructureTag.Mac0, } diff --git a/src/credential-status/status-list.ts b/src/credential-status/status-list.ts index 1cbe853..587051a 100644 --- a/src/credential-status/status-list.ts +++ b/src/credential-status/status-list.ts @@ -7,11 +7,17 @@ export interface CborStatusListOptions { aggregationUri?: string } +interface CborStatusList { + bits: AllowedBitsPerEntry + lst: Uint8Array + aggregation_uri?: string +} + export class StatusList { static buildCborStatusList(options: CborStatusListOptions): Uint8Array { const compressed = options.statusArray.compress() - const statusList: Record = { + const statusList: CborStatusList = { bits: options.statusArray.getBitsPerEntry(), lst: compressed, } @@ -23,9 +29,17 @@ export class StatusList { } static verifyStatus(cborStatusList: Uint8Array, index: number, expectedStatus: number): boolean { - const statusList = cborDecode(cborStatusList) as Map - const bits = statusList.get('bits') as AllowedBitsPerEntry - const lst = statusList.get('lst') as Uint8Array + const decoded = cborDecode(cborStatusList) + if (!(decoded instanceof Map)) { + throw new Error('Decoded CBOR data is not a Map.') + } + + const statusList: CborStatusList = { + bits: decoded.get('bits') as AllowedBitsPerEntry, + lst: decoded.get('lst') as Uint8Array, + aggregation_uri: decoded.get('aggregation_uri') as string | undefined, + } + const { bits, lst } = statusList if (!statusList || !lst || !bits) { throw new Error('Invalid status list format.') @@ -36,10 +50,6 @@ export class StatusList { const statusArray = new StatusArray(bits, zlib.inflate(lst)) const actualStatus = statusArray.get(index) - if (actualStatus !== expectedStatus) { - return false - } - - return true + return actualStatus === expectedStatus } } diff --git a/src/credential-status/status-token.ts b/src/credential-status/status-token.ts index a1ffdf0..4c686b7 100644 --- a/src/credential-status/status-token.ts +++ b/src/credential-status/status-token.ts @@ -3,19 +3,19 @@ import fetch from 'node-fetch' import { cborDecode, cborEncode } from '../cbor' import { Tag } from '../cbor/cbor-x' import type { CoseKey } from '../cose' -import { CoseType, CoseTypeToTag, Mac0, Sign1 } from '../cose' +import { CoseStructureType, CoseTypeToTag, Mac0, Sign1 } from '../cose' import { CWT } from '../cwt' import type { StatusArray } from './status-array' import { StatusList } from './status-list' -interface CWTStatusTokenOptions { +interface CwtStatusTokenOptions { claimsSet: { statusArray: StatusArray aggregationUri?: string expirationTime?: number timeToLive?: number } - type: CoseType + type: CoseStructureType key: CoseKey } @@ -42,7 +42,7 @@ enum CwtStatusListClaims { } export class CWTStatusToken { - static async build(options: CWTStatusTokenOptions): Promise { + static async build(options: CwtStatusTokenOptions): Promise { const cwt = new CWT() cwt.setHeaders({ protected: { @@ -97,11 +97,11 @@ export class CWTStatusToken { throw new Error('CWT status token has expired') } - let coseType: CoseType + let coseType: CoseStructureType if (cwt instanceof Sign1) { - coseType = CoseType.Sign1 + coseType = CoseStructureType.Sign1 } else if (cwt instanceof Mac0) { - coseType = CoseType.Mac0 + coseType = CoseStructureType.Mac0 } else { throw new Error('Unimplemented/Unsupported CWT type') } diff --git a/src/cwt/index.ts b/src/cwt/index.ts index 1999106..b3445a1 100644 --- a/src/cwt/index.ts +++ b/src/cwt/index.ts @@ -9,7 +9,7 @@ import { type Sign1Options, type Sign1Structure, } from '../cose' -import { CoseType } from '../cose' +import { CoseStructureType } from '../cose' type Header = { protected?: Record @@ -17,12 +17,12 @@ type Header = { } type CWTOptions = { - type: CoseType + type: CoseStructureType key: CoseKey } interface CWTVerifyOptions { - type: CoseType + type: CoseStructureType token: Uint8Array key?: CoseKey } @@ -73,7 +73,7 @@ export class CWT { async create({ type, key }: CWTOptions): Promise { switch (type) { - case CoseType.Sign1: { + case CoseStructureType.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, @@ -84,7 +84,7 @@ export class CWT { await sign1.addSignature({ signingKey: key }, { cose: mdocContext.cose }) return sign1.encodedStructure() } - case CoseType.Mac0: { + case CoseStructureType.Mac0: { if (!this.headers.protected || !this.headers.unprotected) { throw new Error('Protected and unprotected headers must be defined for MAC0') } @@ -107,7 +107,7 @@ export class CWT { static async verify({ type, token, key }: CWTVerifyOptions): Promise { const cwt = cborDecode(token) as Sign1 | Mac0 switch (type) { - case CoseType.Sign1: { + case CoseStructureType.Sign1: { const sign1Options: Sign1Options = { protectedHeaders: cwt.protectedHeaders, unprotectedHeaders: cwt.unprotectedHeaders, diff --git a/tests/credential-status/cred-status.test.ts b/tests/credential-status/cred-status.test.ts index bd055a1..28f255e 100644 --- a/tests/credential-status/cred-status.test.ts +++ b/tests/credential-status/cred-status.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from 'vitest' import { CWTStatusToken, CoseKey, StatusArray } from '../../src' -import { CoseType } from '../../src/cose' +import { CoseStructureType } from '../../src/cose' import { ISSUER_PRIVATE_KEY_JWK } from '../issuing/config' describe('CWTStatusToken', () => { @@ -14,7 +14,7 @@ describe('CWTStatusToken', () => { const cwtStatusToken = await CWTStatusToken.build({ claimsSet: { statusArray }, - type: CoseType.Sign1, + type: CoseStructureType.Sign1, key: CoseKey.fromJwk(ISSUER_PRIVATE_KEY_JWK), }) const verify = await CWTStatusToken.verifyStatus({ From 142974be6c1c4533181f78a595a75a153808c0ff Mon Sep 17 00:00:00 2001 From: Dinkar Date: Tue, 17 Jun 2025 16:39:45 +0530 Subject: [PATCH 14/24] refactor: update interface visibility and method names in CwtStatusToken and CWT classes --- src/credential-status/status-list.ts | 2 +- src/credential-status/status-token.ts | 61 ++++++++++----------- src/cwt/index.ts | 14 +++-- tests/credential-status/cred-status.test.ts | 7 ++- 4 files changed, 43 insertions(+), 41 deletions(-) diff --git a/src/credential-status/status-list.ts b/src/credential-status/status-list.ts index 587051a..732df45 100644 --- a/src/credential-status/status-list.ts +++ b/src/credential-status/status-list.ts @@ -7,7 +7,7 @@ export interface CborStatusListOptions { aggregationUri?: string } -interface CborStatusList { +export interface CborStatusList { bits: AllowedBitsPerEntry lst: Uint8Array aggregation_uri?: string diff --git a/src/credential-status/status-token.ts b/src/credential-status/status-token.ts index 4c686b7..53a2c50 100644 --- a/src/credential-status/status-token.ts +++ b/src/credential-status/status-token.ts @@ -4,11 +4,12 @@ import { cborDecode, cborEncode } from '../cbor' import { Tag } from '../cbor/cbor-x' import type { CoseKey } from '../cose' import { CoseStructureType, CoseTypeToTag, Mac0, Sign1 } from '../cose' -import { CWT } from '../cwt' +import { CWT, CwtProtectedHeaders } from '../cwt' import type { StatusArray } from './status-array' import { StatusList } from './status-list' -interface CwtStatusTokenOptions { +export interface CwtStatusTokenOptions { + statusListUri: string claimsSet: { statusArray: StatusArray aggregationUri?: string @@ -19,20 +20,16 @@ interface CwtStatusTokenOptions { key: CoseKey } -interface CWTStatusTokenVerifyOptions { +export interface CwtStatusTokenVerifyOptions { token: Uint8Array key?: CoseKey } -interface CWTStatusVerifyOptions extends CWTStatusTokenVerifyOptions { +export interface CwtStatusVerifyOptions extends CwtStatusTokenVerifyOptions { index: number expectedStatus: number } -enum CwtProtectedHeaders { - TYPE = 16, -} - enum CwtStatusListClaims { StatusListUri = 2, ExpirationTime = 4, @@ -41,17 +38,19 @@ enum CwtStatusListClaims { TimeToLive = 65534, } -export class CWTStatusToken { - static async build(options: CwtStatusTokenOptions): Promise { +const CWT_STATUS_LIST_HEADER_TYPE = 'application/statuslist+cwt' + +export class CwtStatusToken { + static async sign(options: CwtStatusTokenOptions): Promise { const cwt = new CWT() cwt.setHeaders({ protected: { - [CwtProtectedHeaders.TYPE]: 'application/statuslist+cwt', + [CwtProtectedHeaders.Typ]: CWT_STATUS_LIST_HEADER_TYPE, }, }) const claims: { [key: number]: string | number | Uint8Array } = { - [CwtStatusListClaims.StatusListUri]: 'https://example.com/statuslist', // Where the status list is going to be hosted + [CwtStatusListClaims.StatusListUri]: options.statusListUri, [CwtStatusListClaims.IssuedAt]: Math.floor(Date.now() / 1000), [CwtStatusListClaims.StatusList]: StatusList.buildCborStatusList({ statusArray: options.claimsSet.statusArray, @@ -69,11 +68,11 @@ export class CWTStatusToken { return cborEncode(new Tag(await cwt.create({ type: options.type, key: options.key }), CoseTypeToTag[options.type])) } - static async verifyStatusToken(options: CWTStatusTokenVerifyOptions): Promise { + static async verifyStatusToken(options: CwtStatusTokenVerifyOptions): Promise { const cwt = cborDecode(options.token) as Sign1 | Mac0 - const type = cwt.protectedHeaders.headers?.get(String(CwtProtectedHeaders.TYPE)) - if (!type || type !== 'application/statuslist+cwt') { + const type = cwt.protectedHeaders.headers?.get(String(CwtProtectedHeaders.Typ)) + if (!type || type !== CWT_STATUS_LIST_HEADER_TYPE) { throw new Error('CWT status token does not have the correct type in protected headers') } @@ -103,49 +102,45 @@ export class CWTStatusToken { } else if (cwt instanceof Mac0) { coseType = CoseStructureType.Mac0 } else { - throw new Error('Unimplemented/Unsupported CWT type') + throw new Error('Unsupported CWT structure type. Supported values are sign1 and mac0') } const validSignature = await CWT.verify({ type: coseType, token: options.token, key: options.key }) if (!validSignature) { throw new Error('Invalid signature for CWT status token') } - return true + return cwt } - static async verifyStatus(options: CWTStatusVerifyOptions): Promise { - const validStatusToken = await CWTStatusToken.verifyStatusToken(options) - if (validStatusToken) { - const cwt = cborDecode(options.token) as Sign1 | Mac0 - if (!cwt.payload) { - throw new Error('CWT status token does not contain claims') - } - - const claims = cborDecode(cwt.payload) as Map - const statusList = claims.get(String(CwtStatusListClaims.StatusList)) - return StatusList.verifyStatus(statusList as Uint8Array, options.index, options.expectedStatus) + static async verifyStatus(options: CwtStatusVerifyOptions): Promise { + const cwt = await CwtStatusToken.verifyStatusToken(options) + if (!cwt.payload) { + throw new Error('CWT status token does not contain claims') } - return false + const claims = cborDecode(cwt.payload) as Map + const statusList = claims.get(String(CwtStatusListClaims.StatusList)) + return StatusList.verifyStatus(statusList as Uint8Array, options.index, options.expectedStatus) } - static async fetchStatusListUri(statusListUri: string): Promise { + static async fetchStatusListUri(statusListUri: string, timeoutMs = 5000): Promise { if (!statusListUri.startsWith('https://')) { throw new Error(`Status list URI must be HTTPS: ${statusListUri}`) } const abortController = new AbortController() - setTimeout(() => { + const timeout = setTimeout(() => { abortController.abort() - }, 5000) + }, timeoutMs) try { const response = await fetch(statusListUri, { signal: abortController.signal as NonNullable, headers: { - Accept: 'application/statuslist+cwt', + Accept: CWT_STATUS_LIST_HEADER_TYPE, }, }) const buffer = await response.arrayBuffer() + clearTimeout(timeout) return new Uint8Array(buffer) } catch (error) { if (error instanceof Error && error.name === 'AbortError') { diff --git a/src/cwt/index.ts b/src/cwt/index.ts index b3445a1..ac22d89 100644 --- a/src/cwt/index.ts +++ b/src/cwt/index.ts @@ -16,17 +16,21 @@ type Header = { unprotected?: Record } -type CWTOptions = { +type CwtOptions = { type: CoseStructureType key: CoseKey } -interface CWTVerifyOptions { +export interface CwtVerifyOptions { type: CoseStructureType token: Uint8Array key?: CoseKey } +export enum CwtProtectedHeaders { + Typ = 16, +} + enum CwtStandardClaims { Iss = 1, Sub = 2, @@ -71,7 +75,7 @@ export class CWT { this.headers = headers } - async create({ type, key }: CWTOptions): Promise { + async create({ type, key }: CwtOptions): Promise { switch (type) { case CoseStructureType.Sign1: { const sign1Options: Sign1Options = { @@ -95,6 +99,7 @@ export class CWT { } const mac0 = new Mac0(mac0Options) + // Todo: Implement MAC0 signing logic // await mac0.addTag({ privateKey: key, ephemeralKey: key, sessionTranscript: new SessionTranscript({ handover: new QrHandover() }) }, mdocContext); // return mac0.encodedStructure(); throw new Error('MAC0 is not yet implemented') @@ -104,7 +109,7 @@ export class CWT { } } - static async verify({ type, token, key }: CWTVerifyOptions): Promise { + static async verify({ type, token, key }: CwtVerifyOptions): Promise { const cwt = cborDecode(token) as Sign1 | Mac0 switch (type) { case CoseStructureType.Sign1: { @@ -118,6 +123,7 @@ export class CWT { return await sign1.verify({ key }, mdocContext) } default: + // Todo: Implement verification for MAC0 throw new Error(`${type} is not yet implemented for verification`) } } diff --git a/tests/credential-status/cred-status.test.ts b/tests/credential-status/cred-status.test.ts index 28f255e..3349825 100644 --- a/tests/credential-status/cred-status.test.ts +++ b/tests/credential-status/cred-status.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from 'vitest' -import { CWTStatusToken, CoseKey, StatusArray } from '../../src' +import { CoseKey, CwtStatusToken, StatusArray } from '../../src' import { CoseStructureType } from '../../src/cose' import { ISSUER_PRIVATE_KEY_JWK } from '../issuing/config' @@ -12,12 +12,13 @@ describe('CWTStatusToken', () => { expect(statusArray.get(0)).toBe(2) expect(statusArray.get(1)).toBe(3) - const cwtStatusToken = await CWTStatusToken.build({ + const cwtStatusToken = await CwtStatusToken.sign({ + statusListUri: 'https://example.com/status-list', claimsSet: { statusArray }, type: CoseStructureType.Sign1, key: CoseKey.fromJwk(ISSUER_PRIVATE_KEY_JWK), }) - const verify = await CWTStatusToken.verifyStatus({ + const verify = await CwtStatusToken.verifyStatus({ token: cwtStatusToken, key: CoseKey.fromJwk(ISSUER_PRIVATE_KEY_JWK), index: 0, From 8328285bb9c770618b8e55a63a93292b8cb28737 Mon Sep 17 00:00:00 2001 From: Dinkar Date: Tue, 17 Jun 2025 16:47:48 +0530 Subject: [PATCH 15/24] refactor: remove unused dependencies and update TypeScript lib configuration --- package.json | 3 - pnpm-lock.yaml | 222 -------------------------- src/credential-status/status-token.ts | 2 - tsconfig.json | 2 +- 4 files changed, 1 insertion(+), 228 deletions(-) diff --git a/package.json b/package.json index e63056e..9e98a16 100644 --- a/package.json +++ b/package.json @@ -30,9 +30,7 @@ "changeset-version": "pnpm changeset version && pnpm style:fix" }, "dependencies": { - "abort-controller": "^3.0.0", "buffer": "^6.0.3", - "node-fetch": "2", "pako": "^2.1.0" }, "devDependencies": { @@ -43,7 +41,6 @@ "@panva/hkdf": "^1.2.1", "@peculiar/x509": "^1.12.3", "@types/node": "^20.14.11", - "@types/node-fetch": "^2.6.12", "@types/pako": "^2.0.3", "jose": "^5.9.3", "tsup": "^8.3.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 53d8e2b..744d25a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,15 +8,9 @@ importers: .: dependencies: - abort-controller: - specifier: ^3.0.0 - version: 3.0.0 buffer: specifier: ^6.0.3 version: 6.0.3 - node-fetch: - specifier: '2' - version: 2.7.0 pako: specifier: ^2.1.0 version: 2.1.0 @@ -42,9 +36,6 @@ importers: '@types/node': specifier: ^20.14.11 version: 20.17.6 - '@types/node-fetch': - specifier: ^2.6.12 - version: 2.6.12 '@types/pako': specifier: ^2.0.3 version: 2.0.3 @@ -737,9 +728,6 @@ packages: '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} - '@types/node-fetch@2.6.12': - resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==} - '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} @@ -778,10 +766,6 @@ packages: '@vitest/utils@2.1.9': resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} - abort-controller@3.0.0: - resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} - engines: {node: '>=6.5'} - ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -820,9 +804,6 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -853,10 +834,6 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} - call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} - chai@5.1.2: resolution: {integrity: sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==} engines: {node: '>=12'} @@ -883,10 +860,6 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - combined-stream@1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} - commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -924,10 +897,6 @@ packages: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} - delayed-stream@1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} - detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} @@ -936,10 +905,6 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} - dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} - eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -953,25 +918,9 @@ packages: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} - es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} - - es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} - es-module-lexer@1.6.0: resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==} - es-object-atoms@1.1.1: - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} - engines: {node: '>= 0.4'} - - es-set-tostringtag@2.1.0: - resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} - engines: {node: '>= 0.4'} - esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} @@ -990,10 +939,6 @@ packages: estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - event-target-shim@5.0.1: - resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} - engines: {node: '>=6'} - expect-type@1.1.0: resolution: {integrity: sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==} engines: {node: '>=12.0.0'} @@ -1032,10 +977,6 @@ packages: resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} - form-data@4.0.3: - resolution: {integrity: sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==} - engines: {node: '>= 6'} - fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -1049,17 +990,6 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - - get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} - engines: {node: '>= 0.4'} - - get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} - engines: {node: '>= 0.4'} - glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -1072,25 +1002,9 @@ packages: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} - gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} - graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} - - has-tostringtag@1.0.2: - resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} - engines: {node: '>= 0.4'} - - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} - human-id@1.0.2: resolution: {integrity: sha512-UNopramDEhHJD+VR+ehk8rOslwSfByxPIZyJRfV739NDhN5LF1fa1MqnzKm2lGTQRjNrjK19Q5fhkgIfjlVUKw==} @@ -1186,10 +1100,6 @@ packages: magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} - math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} - merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1198,14 +1108,6 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} - mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} - - mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} - minimatch@9.0.5: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} @@ -1229,15 +1131,6 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - node-fetch@2.7.0: - resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -1526,9 +1419,6 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - tr46@1.0.1: resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} @@ -1641,15 +1531,9 @@ packages: jsdom: optional: true - webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} - whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - whatwg-url@7.1.0: resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} @@ -2273,11 +2157,6 @@ snapshots: '@types/estree@1.0.6': {} - '@types/node-fetch@2.6.12': - dependencies: - '@types/node': 20.17.6 - form-data: 4.0.3 - '@types/node@12.20.55': {} '@types/node@20.17.6': @@ -2326,10 +2205,6 @@ snapshots: loupe: 3.1.3 tinyrainbow: 1.2.0 - abort-controller@3.0.0: - dependencies: - event-target-shim: 5.0.1 - ansi-colors@4.1.3: {} ansi-regex@5.0.1: {} @@ -2358,8 +2233,6 @@ snapshots: assertion-error@2.0.1: {} - asynckit@0.4.0: {} - balanced-match@1.0.2: {} base64-js@1.5.1: {} @@ -2388,11 +2261,6 @@ snapshots: cac@6.7.14: {} - call-bind-apply-helpers@1.0.2: - dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 - chai@5.1.2: dependencies: assertion-error: 2.0.1 @@ -2417,10 +2285,6 @@ snapshots: color-name@1.1.4: {} - combined-stream@1.0.8: - dependencies: - delayed-stream: 1.0.0 - commander@4.1.1: {} consola@3.2.3: {} @@ -2447,20 +2311,12 @@ snapshots: deep-eql@5.0.2: {} - delayed-stream@1.0.0: {} - detect-indent@6.1.0: {} dir-glob@3.0.1: dependencies: path-type: 4.0.0 - dunder-proto@1.0.1: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-errors: 1.3.0 - gopd: 1.2.0 - eastasianwidth@0.2.0: {} emoji-regex@8.0.0: {} @@ -2472,23 +2328,8 @@ snapshots: ansi-colors: 4.1.3 strip-ansi: 6.0.1 - es-define-property@1.0.1: {} - - es-errors@1.3.0: {} - es-module-lexer@1.6.0: {} - es-object-atoms@1.1.1: - dependencies: - es-errors: 1.3.0 - - es-set-tostringtag@2.1.0: - dependencies: - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - has-tostringtag: 1.0.2 - hasown: 2.0.2 - esbuild@0.21.5: optionalDependencies: '@esbuild/aix-ppc64': 0.21.5 @@ -2548,8 +2389,6 @@ snapshots: dependencies: '@types/estree': 1.0.6 - event-target-shim@5.0.1: {} - expect-type@1.1.0: {} extendable-error@0.1.7: {} @@ -2590,14 +2429,6 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - form-data@4.0.3: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - es-set-tostringtag: 2.1.0 - hasown: 2.0.2 - mime-types: 2.1.35 - fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11 @@ -2613,26 +2444,6 @@ snapshots: fsevents@2.3.3: optional: true - function-bind@1.1.2: {} - - get-intrinsic@1.3.0: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - function-bind: 1.1.2 - get-proto: 1.0.1 - gopd: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.2 - math-intrinsics: 1.1.0 - - get-proto@1.0.1: - dependencies: - dunder-proto: 1.0.1 - es-object-atoms: 1.1.1 - glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -2655,20 +2466,8 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 - gopd@1.2.0: {} - graceful-fs@4.2.11: {} - has-symbols@1.1.0: {} - - has-tostringtag@1.0.2: - dependencies: - has-symbols: 1.1.0 - - hasown@2.0.2: - dependencies: - function-bind: 1.1.2 - human-id@1.0.2: {} iconv-lite@0.4.24: @@ -2745,8 +2544,6 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 - math-intrinsics@1.1.0: {} - merge2@1.4.1: {} micromatch@4.0.8: @@ -2754,12 +2551,6 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 - mime-db@1.52.0: {} - - mime-types@2.1.35: - dependencies: - mime-db: 1.52.0 - minimatch@9.0.5: dependencies: brace-expansion: 2.0.1 @@ -2778,10 +2569,6 @@ snapshots: nanoid@3.3.8: {} - node-fetch@2.7.0: - dependencies: - whatwg-url: 5.0.0 - object-assign@4.1.1: {} os-tmpdir@1.0.2: {} @@ -3039,8 +2826,6 @@ snapshots: dependencies: is-number: 7.0.0 - tr46@0.0.3: {} - tr46@1.0.1: dependencies: punycode: 2.3.1 @@ -3152,15 +2937,8 @@ snapshots: - supports-color - terser - webidl-conversions@3.0.1: {} - webidl-conversions@4.0.2: {} - whatwg-url@5.0.0: - dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 - whatwg-url@7.1.0: dependencies: lodash.sortby: 4.7.0 diff --git a/src/credential-status/status-token.ts b/src/credential-status/status-token.ts index 53a2c50..2a0c8dc 100644 --- a/src/credential-status/status-token.ts +++ b/src/credential-status/status-token.ts @@ -1,5 +1,3 @@ -import AbortController from 'abort-controller' -import fetch from 'node-fetch' import { cborDecode, cborEncode } from '../cbor' import { Tag } from '../cbor/cbor-x' import type { CoseKey } from '../cose' diff --git a/tsconfig.json b/tsconfig.json index e97c047..056aab3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,7 @@ "strict": true, "skipLibCheck": true, "noEmitOnError": true, - "lib": ["ES2020"], + "lib": ["ES2020", "DOM"], "types": [], "esModuleInterop": true, "allowSyntheticDefaultImports": true From b354d297ddc62192bd0b2caae4a95223405e1802 Mon Sep 17 00:00:00 2001 From: Dinkar Date: Tue, 17 Jun 2025 16:58:54 +0530 Subject: [PATCH 16/24] refactor: update StatusArray property naming and improve error messages; adjust StatusList to use updated bitsPerEntry accessor --- src/credential-status/status-array.ts | 22 +++++++++++----------- src/credential-status/status-list.ts | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/credential-status/status-array.ts b/src/credential-status/status-array.ts index 474c59f..4543236 100644 --- a/src/credential-status/status-array.ts +++ b/src/credential-status/status-array.ts @@ -5,29 +5,29 @@ export const allowedBitsPerEntry = [1, 2, 4, 8] as const export type AllowedBitsPerEntry = (typeof allowedBitsPerEntry)[number] export class StatusArray { - private readonly bitsPerEntry: AllowedBitsPerEntry + private readonly _bitsPerEntry: AllowedBitsPerEntry private readonly statusBitMask: number private readonly data: Uint8Array constructor(bitsPerEntry: AllowedBitsPerEntry, byteArr?: Uint8Array) { if (!allowedBitsPerEntry.includes(bitsPerEntry)) { - throw new Error('Only 1, 2, 4, or 8 bits per entry are allowed.') + throw new Error(`Only bits ${allowedBitsPerEntry.join(', ')} per entry are allowed.`) } - this.bitsPerEntry = bitsPerEntry + this._bitsPerEntry = bitsPerEntry this.statusBitMask = (1 << bitsPerEntry) - 1 this.data = byteArr ? byteArr : new Uint8Array(arraySize) } - private computeByteAndOffset(index: number): [number, number] { - const byteIndex = Math.floor((index * this.bitsPerEntry) / 8) - const bitOffset = (index * this.bitsPerEntry) % 8 + private computeByteAndOffset(index: number): { byteIndex: number; bitOffset: number } { + const byteIndex = Math.floor((index * this._bitsPerEntry) / 8) + const bitOffset = (index * this._bitsPerEntry) % 8 - return [byteIndex, bitOffset] + return { byteIndex, bitOffset } } - getBitsPerEntry(): AllowedBitsPerEntry { - return this.bitsPerEntry + get bitsPerEntry(): AllowedBitsPerEntry { + return this._bitsPerEntry } set(index: number, status: number): void { @@ -35,7 +35,7 @@ export class StatusArray { throw new Error(`Invalid status: ${status}. Must be between 0 and ${this.statusBitMask}.`) } - const [byteIndex, bitOffset] = this.computeByteAndOffset(index) + const { byteIndex, bitOffset } = this.computeByteAndOffset(index) // Clear current bits this.data[byteIndex] &= ~(this.statusBitMask << bitOffset) @@ -45,7 +45,7 @@ export class StatusArray { } get(index: number): number { - const [byteIndex, bitOffset] = this.computeByteAndOffset(index) + const { byteIndex, bitOffset } = this.computeByteAndOffset(index) return (this.data[byteIndex] >> bitOffset) & this.statusBitMask } diff --git a/src/credential-status/status-list.ts b/src/credential-status/status-list.ts index 732df45..9b5ee86 100644 --- a/src/credential-status/status-list.ts +++ b/src/credential-status/status-list.ts @@ -18,7 +18,7 @@ export class StatusList { const compressed = options.statusArray.compress() const statusList: CborStatusList = { - bits: options.statusArray.getBitsPerEntry(), + bits: options.statusArray.bitsPerEntry, lst: compressed, } From 13caadc88e8c0a7802409ed2b86f0b2ccfb996cc Mon Sep 17 00:00:00 2001 From: Dinkar Date: Tue, 17 Jun 2025 17:08:26 +0530 Subject: [PATCH 17/24] refactor: update CoseStructureType usage in CwtStatusTokenOptions and CwtOptions interfaces --- src/credential-status/status-token.ts | 2 +- src/cwt/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/credential-status/status-token.ts b/src/credential-status/status-token.ts index 2a0c8dc..8ad8c94 100644 --- a/src/credential-status/status-token.ts +++ b/src/credential-status/status-token.ts @@ -14,7 +14,7 @@ export interface CwtStatusTokenOptions { expirationTime?: number timeToLive?: number } - type: CoseStructureType + type: CoseStructureType.Sign1 | CoseStructureType.Mac0 key: CoseKey } diff --git a/src/cwt/index.ts b/src/cwt/index.ts index ac22d89..3055ae6 100644 --- a/src/cwt/index.ts +++ b/src/cwt/index.ts @@ -17,7 +17,7 @@ type Header = { } type CwtOptions = { - type: CoseStructureType + type: CoseStructureType.Sign1 | CoseStructureType.Mac0 key: CoseKey } From e7cb9af59613b59bbce4e38bfdeb34713c878eb8 Mon Sep 17 00:00:00 2001 From: Dinkar Date: Tue, 17 Jun 2025 17:27:15 +0530 Subject: [PATCH 18/24] refactor: add mdocContext to CwtStatusTokenOptions and CwtVerifyOptions; update related methods and tests --- src/credential-status/status-token.ts | 17 +++++++++++++++-- src/cwt/index.ts | 8 +++++--- tests/credential-status/cred-status.test.ts | 3 +++ 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/credential-status/status-token.ts b/src/credential-status/status-token.ts index 8ad8c94..c6dd012 100644 --- a/src/credential-status/status-token.ts +++ b/src/credential-status/status-token.ts @@ -1,5 +1,6 @@ import { cborDecode, cborEncode } from '../cbor' import { Tag } from '../cbor/cbor-x' +import type { MdocContext } from '../context' import type { CoseKey } from '../cose' import { CoseStructureType, CoseTypeToTag, Mac0, Sign1 } from '../cose' import { CWT, CwtProtectedHeaders } from '../cwt' @@ -7,6 +8,7 @@ import type { StatusArray } from './status-array' import { StatusList } from './status-list' export interface CwtStatusTokenOptions { + mdocContext: Pick statusListUri: string claimsSet: { statusArray: StatusArray @@ -19,6 +21,7 @@ export interface CwtStatusTokenOptions { } export interface CwtStatusTokenVerifyOptions { + mdocContext: Pick token: Uint8Array key?: CoseKey } @@ -63,7 +66,12 @@ export class CwtStatusToken { } cwt.setClaims(claims) - return cborEncode(new Tag(await cwt.create({ type: options.type, key: options.key }), CoseTypeToTag[options.type])) + return cborEncode( + new Tag( + await cwt.create({ type: options.type, key: options.key, mdocContext: options.mdocContext }), + CoseTypeToTag[options.type] + ) + ) } static async verifyStatusToken(options: CwtStatusTokenVerifyOptions): Promise { @@ -102,7 +110,12 @@ export class CwtStatusToken { } else { throw new Error('Unsupported CWT structure type. Supported values are sign1 and mac0') } - const validSignature = await CWT.verify({ type: coseType, token: options.token, key: options.key }) + const validSignature = await CWT.verify({ + type: coseType, + token: options.token, + key: options.key, + mdocContext: options.mdocContext, + }) if (!validSignature) { throw new Error('Invalid signature for CWT status token') } diff --git a/src/cwt/index.ts b/src/cwt/index.ts index 3055ae6..0799f3e 100644 --- a/src/cwt/index.ts +++ b/src/cwt/index.ts @@ -1,5 +1,5 @@ -import { mdocContext } from '../../tests/context' import { cborDecode, cborEncode } from '../cbor' +import type { MdocContext } from '../context' import { type CoseKey, Mac0, @@ -17,11 +17,13 @@ type Header = { } type CwtOptions = { + mdocContext: Pick type: CoseStructureType.Sign1 | CoseStructureType.Mac0 key: CoseKey } export interface CwtVerifyOptions { + mdocContext: Pick type: CoseStructureType token: Uint8Array key?: CoseKey @@ -75,7 +77,7 @@ export class CWT { this.headers = headers } - async create({ type, key }: CwtOptions): Promise { + async create({ type, key, mdocContext }: CwtOptions): Promise { switch (type) { case CoseStructureType.Sign1: { const sign1Options: Sign1Options = { @@ -109,7 +111,7 @@ export class CWT { } } - static async verify({ type, token, key }: CwtVerifyOptions): Promise { + static async verify({ type, token, key, mdocContext }: CwtVerifyOptions): Promise { const cwt = cborDecode(token) as Sign1 | Mac0 switch (type) { case CoseStructureType.Sign1: { diff --git a/tests/credential-status/cred-status.test.ts b/tests/credential-status/cred-status.test.ts index 3349825..029ab86 100644 --- a/tests/credential-status/cred-status.test.ts +++ b/tests/credential-status/cred-status.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from 'vitest' import { CoseKey, CwtStatusToken, StatusArray } from '../../src' import { CoseStructureType } from '../../src/cose' +import { mdocContext } from '../context' import { ISSUER_PRIVATE_KEY_JWK } from '../issuing/config' describe('CWTStatusToken', () => { @@ -13,12 +14,14 @@ describe('CWTStatusToken', () => { expect(statusArray.get(1)).toBe(3) const cwtStatusToken = await CwtStatusToken.sign({ + mdocContext, statusListUri: 'https://example.com/status-list', claimsSet: { statusArray }, type: CoseStructureType.Sign1, key: CoseKey.fromJwk(ISSUER_PRIVATE_KEY_JWK), }) const verify = await CwtStatusToken.verifyStatus({ + mdocContext, token: cwtStatusToken, key: CoseKey.fromJwk(ISSUER_PRIVATE_KEY_JWK), index: 0, From e6b52b74d956725a699b271606e5e5c4c3749c4c Mon Sep 17 00:00:00 2001 From: Dinkar Date: Tue, 17 Jun 2025 17:58:28 +0530 Subject: [PATCH 19/24] refactor: update CwtStatusToken to use new claims enumeration and date utility; improve claim handling --- src/credential-status/status-token.ts | 35 ++++++++++++++------------- src/utils/transformers.ts | 2 ++ 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/credential-status/status-token.ts b/src/credential-status/status-token.ts index c6dd012..27d6771 100644 --- a/src/credential-status/status-token.ts +++ b/src/credential-status/status-token.ts @@ -4,6 +4,7 @@ import type { MdocContext } from '../context' import type { CoseKey } from '../cose' import { CoseStructureType, CoseTypeToTag, Mac0, Sign1 } from '../cose' import { CWT, CwtProtectedHeaders } from '../cwt' +import { dateToSeconds } from '../utils' import type { StatusArray } from './status-array' import { StatusList } from './status-list' @@ -32,11 +33,11 @@ export interface CwtStatusVerifyOptions extends CwtStatusTokenVerifyOptions { } enum CwtStatusListClaims { - StatusListUri = 2, - ExpirationTime = 4, - IssuedAt = 6, - StatusList = 65533, - TimeToLive = 65534, + Sub = 2, + Exp = 4, + Iat = 6, + Sli = 65533, // Status List + Ttl = 65534, } const CWT_STATUS_LIST_HEADER_TYPE = 'application/statuslist+cwt' @@ -51,18 +52,18 @@ export class CwtStatusToken { }) const claims: { [key: number]: string | number | Uint8Array } = { - [CwtStatusListClaims.StatusListUri]: options.statusListUri, - [CwtStatusListClaims.IssuedAt]: Math.floor(Date.now() / 1000), - [CwtStatusListClaims.StatusList]: StatusList.buildCborStatusList({ + [CwtStatusListClaims.Sub]: options.statusListUri, + [CwtStatusListClaims.Iat]: dateToSeconds(), + [CwtStatusListClaims.Sli]: StatusList.buildCborStatusList({ statusArray: options.claimsSet.statusArray, aggregationUri: options.claimsSet.aggregationUri, }), } if (options.claimsSet.expirationTime) { - claims[CwtStatusListClaims.ExpirationTime] = options.claimsSet.expirationTime + claims[CwtStatusListClaims.Exp] = options.claimsSet.expirationTime } if (options.claimsSet.timeToLive) { - claims[CwtStatusListClaims.TimeToLive] = options.claimsSet.timeToLive + claims[CwtStatusListClaims.Ttl] = options.claimsSet.timeToLive } cwt.setClaims(claims) @@ -86,19 +87,19 @@ export class CwtStatusToken { throw new Error('CWT status token does not contain claims') } const claims = cborDecode(cwt.payload) as Map - // Check if is the same as the one used to fetch the token - if (!claims.has(String(CwtStatusListClaims.StatusListUri))) { + // Todo: Check if is the same as the one used to fetch the token + if (!claims.has(String(CwtStatusListClaims.Sub))) { throw new Error('CWT status token does not contain status list URI') } - if (!claims.has(String(CwtStatusListClaims.IssuedAt))) { + if (!claims.has(String(CwtStatusListClaims.Iat))) { throw new Error('CWT status token does not contain issued at claim') } - if (!claims.has(String(CwtStatusListClaims.StatusList))) { + if (!claims.has(String(CwtStatusListClaims.Sli))) { throw new Error('CWT status token does not contain status list') } - const expirationTime = claims.get(String(CwtStatusListClaims.ExpirationTime)) - if (expirationTime && typeof expirationTime === 'number' && expirationTime < Math.floor(Date.now() / 1000)) { + const expirationTime = claims.get(String(CwtStatusListClaims.Exp)) + if (expirationTime && typeof expirationTime === 'number' && expirationTime < dateToSeconds()) { throw new Error('CWT status token has expired') } @@ -130,7 +131,7 @@ export class CwtStatusToken { } const claims = cborDecode(cwt.payload) as Map - const statusList = claims.get(String(CwtStatusListClaims.StatusList)) + const statusList = claims.get(String(CwtStatusListClaims.Sli)) return StatusList.verifyStatus(statusList as Uint8Array, options.index, options.expectedStatus) } diff --git a/src/utils/transformers.ts b/src/utils/transformers.ts index 19c0c97..387d529 100644 --- a/src/utils/transformers.ts +++ b/src/utils/transformers.ts @@ -33,3 +33,5 @@ export const compareBytes = (lhs: Uint8Array, rhs: Uint8Array) => { if (lhs.byteLength !== rhs.byteLength) return false return lhs.every((b, i) => b === rhs[i]) } + +export const dateToSeconds = (date?: Date): number => Math.floor((date?.getTime() ?? Date.now()) / 1000) From 6b2285fbfc42bc6e2a0e1cbc831c0cfa98c6e08c Mon Sep 17 00:00:00 2001 From: Dinkar Date: Thu, 19 Jun 2025 21:05:35 +0530 Subject: [PATCH 20/24] refactor: introduce custom error classes for status list validation; replace generic errors with specific ones --- src/credential-status/error.ts | 9 +++++++++ src/credential-status/status-list.ts | 5 +++-- 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 src/credential-status/error.ts diff --git a/src/credential-status/error.ts b/src/credential-status/error.ts new file mode 100644 index 0000000..af648a1 --- /dev/null +++ b/src/credential-status/error.ts @@ -0,0 +1,9 @@ +// biome-ignore format: +class StatusListError extends Error { constructor(message: string = new.target.name) { super(message) } } + +export class InvalidStatusListFormatError extends StatusListError {} +export class InvalidStatusListBitsError extends StatusListError { + constructor(bits: number, allowedBits: readonly number[]) { + super(`Invalid bits per entry: ${bits}. Allowed values are ${allowedBits.join(', ')}.`) + } +} diff --git a/src/credential-status/status-list.ts b/src/credential-status/status-list.ts index 9b5ee86..853d565 100644 --- a/src/credential-status/status-list.ts +++ b/src/credential-status/status-list.ts @@ -1,5 +1,6 @@ import * as zlib from 'pako' import { cborDecode, cborEncode } from '../cbor' +import { InvalidStatusListBitsError, InvalidStatusListFormatError } from './error' import { type AllowedBitsPerEntry, StatusArray, allowedBitsPerEntry } from './status-array' export interface CborStatusListOptions { @@ -42,10 +43,10 @@ export class StatusList { const { bits, lst } = statusList if (!statusList || !lst || !bits) { - throw new Error('Invalid status list format.') + throw new InvalidStatusListFormatError() } if (!allowedBitsPerEntry.includes(bits)) { - throw new Error(`Invalid bits per entry: ${bits}. Allowed values are ${allowedBitsPerEntry.join(', ')}.`) + throw new InvalidStatusListBitsError(bits, allowedBitsPerEntry) } const statusArray = new StatusArray(bits, zlib.inflate(lst)) From 6e9ca7be1f7956284f4ceb3975cb16a20d849626 Mon Sep 17 00:00:00 2001 From: Dinkar Date: Fri, 20 Jun 2025 23:31:42 +0530 Subject: [PATCH 21/24] refactor: add StatusInfo model and integrate status handling in MobileSecurityObject and IssuerSignedBuilder --- src/mdoc/builders/issuer-signed-builder.ts | 12 ++- src/mdoc/models/index.ts | 1 + src/mdoc/models/mobile-security-object.ts | 22 ++++- src/mdoc/models/status-info.ts | 87 ++++++++++++++++++++ tests/builders/issuer-signed-builder.test.ts | 1 + 5 files changed, 117 insertions(+), 6 deletions(-) create mode 100644 src/mdoc/models/status-info.ts diff --git a/src/mdoc/builders/issuer-signed-builder.ts b/src/mdoc/builders/issuer-signed-builder.ts index 02d7c6c..0931b72 100644 --- a/src/mdoc/builders/issuer-signed-builder.ts +++ b/src/mdoc/builders/issuer-signed-builder.ts @@ -19,7 +19,10 @@ import { IssuerSigned, IssuerSignedItem, MobileSecurityObject, + type MobileSecurityObjectOptions, type Namespace, + StatusInfo, + type StatusInfoStructure, ValidityInfo, type ValidityInfoOptions, ValueDigests, @@ -86,6 +89,7 @@ export class IssuerSignedBuilder { validityInfo: ValidityInfo | ValidityInfoOptions deviceKeyInfo: DeviceKeyInfo | DeviceKeyInfoOptions certificate: Uint8Array + statusList?: StatusInfoStructure }): Promise { const validityInfo = options.validityInfo instanceof ValidityInfo ? options.validityInfo : new ValidityInfo(options.validityInfo) @@ -93,13 +97,17 @@ export class IssuerSignedBuilder { const deviceKeyInfo = options.deviceKeyInfo instanceof DeviceKeyInfo ? options.deviceKeyInfo : new DeviceKeyInfo(options.deviceKeyInfo) - const mso = new MobileSecurityObject({ + const payload: MobileSecurityObjectOptions = { docType: this.docType, validityInfo, digestAlgorithm: options.digestAlgorithm, deviceKeyInfo, valueDigests: await this.convertIssuerNamespacesIntoValueDigests(options.digestAlgorithm), - }) + } + if (options.statusList) { + payload.status = new StatusInfo({ key: options.signingKey, statusList: options.statusList }) + } + const mso = new MobileSecurityObject(payload) const protectedHeaders = new ProtectedHeaders({ protectedHeaders: new Map([[Header.Algorithm, options.algorithm]]), diff --git a/src/mdoc/models/index.ts b/src/mdoc/models/index.ts index fa0f047..a18f921 100644 --- a/src/mdoc/models/index.ts +++ b/src/mdoc/models/index.ts @@ -40,6 +40,7 @@ export * from './nfc-options' export * from './oidc' export * from './pex-limit-disclosure' export * from './presentation-definition' +export * from './status-info' export * from './protocol-info' export * from './qr-handover' export * from './reader-auth' diff --git a/src/mdoc/models/mobile-security-object.ts b/src/mdoc/models/mobile-security-object.ts index dccd074..feb9bd0 100644 --- a/src/mdoc/models/mobile-security-object.ts +++ b/src/mdoc/models/mobile-security-object.ts @@ -2,6 +2,7 @@ import { type CborDecodeOptions, CborStructure, cborDecode } from '../../cbor' import type { DigestAlgorithm } from '../../cose' import { DeviceKeyInfo, type DeviceKeyInfoStructure } from './device-key-info' import type { DocType } from './doctype' +import { StatusInfo } from './status-info' import { ValidityInfo, type ValidityInfoStructure } from './validity-info' import { ValueDigests, type ValueDigestsStructure } from './value-digests' @@ -12,6 +13,7 @@ export type MobileSecurityObjectStructure = { valueDigests: ValueDigestsStructure deviceKeyInfo: DeviceKeyInfoStructure validityInfo: ValidityInfoStructure + status?: Uint8Array } export type MobileSecurityObjectOptions = { @@ -21,6 +23,7 @@ export type MobileSecurityObjectOptions = { valueDigests: ValueDigests validityInfo: ValidityInfo deviceKeyInfo: DeviceKeyInfo + status?: StatusInfo } export class MobileSecurityObject extends CborStructure { @@ -30,6 +33,7 @@ export class MobileSecurityObject extends CborStructure { public validityInfo: ValidityInfo public valueDigests: ValueDigests public deviceKeyInfo: DeviceKeyInfo + public status?: StatusInfo public constructor(options: MobileSecurityObjectOptions) { super() @@ -39,10 +43,12 @@ export class MobileSecurityObject extends CborStructure { this.validityInfo = options.validityInfo this.valueDigests = options.valueDigests this.deviceKeyInfo = options.deviceKeyInfo + this.status = options.status } - public encodedStructure(): MobileSecurityObjectStructure { - return { + // Todo: Is it fine to make it async? + public async encodedStructure(): Promise { + const structure: MobileSecurityObjectStructure = { version: this.version, digestAlgorithm: this.digestAlgorithm, valueDigests: this.valueDigests.encodedStructure(), @@ -50,6 +56,10 @@ export class MobileSecurityObject extends CborStructure { docType: this.docType, validityInfo: this.validityInfo.encodedStructure(), } + if (this.status) { + structure.status = await this.status.encodedStructure() + } + return structure } public static override fromEncodedStructure( @@ -61,14 +71,18 @@ export class MobileSecurityObject extends CborStructure { structure = Object.fromEntries(encodedStructure.entries()) as MobileSecurityObjectStructure } - return new MobileSecurityObject({ + const mobileSecurityObject: MobileSecurityObjectOptions = { version: structure.version, digestAlgorithm: structure.digestAlgorithm as DigestAlgorithm, docType: structure.docType, validityInfo: ValidityInfo.fromEncodedStructure(structure.validityInfo), valueDigests: ValueDigests.fromEncodedStructure(structure.valueDigests), deviceKeyInfo: DeviceKeyInfo.fromEncodedStructure(structure.deviceKeyInfo), - }) + } + if (structure.status) { + mobileSecurityObject.status = StatusInfo.fromEncodedStructure(structure.status) + } + return new MobileSecurityObject(mobileSecurityObject) } public static override decode(bytes: Uint8Array, options?: CborDecodeOptions): MobileSecurityObject { diff --git a/src/mdoc/models/status-info.ts b/src/mdoc/models/status-info.ts new file mode 100644 index 0000000..c3f7409 --- /dev/null +++ b/src/mdoc/models/status-info.ts @@ -0,0 +1,87 @@ +import { cborDecode, cborEncode } from '../../cbor' +import { Tag } from '../../cbor/cbor-x' +import type { MdocContext } from '../../context' +import { type CoseKey, CoseStructureType, CoseTypeToTag, Mac0, Sign1 } from '../../cose' +import { CWT } from '../../cwt' + +export type StatusInfoStructure = { + idx: number + uri: string +} +export type StatusInfoOptions = { + statusList: StatusInfoStructure + key?: CoseKey + mdocContext?: Pick +} + +export enum StatusInfoClaim { + StatusList = 65535, +} + +export class StatusInfo { + public statusList: StatusInfoStructure + public mdocContext?: Pick + public key?: CoseKey + + public constructor(statusInfo: StatusInfoOptions) { + this.statusList = { + idx: statusInfo.statusList.idx, + uri: statusInfo.statusList.uri, + } + this.mdocContext = statusInfo.mdocContext + this.key = statusInfo.key + } + + public setKey(key: CoseKey): void { + this.key = key + } + public setMdocContext(mdocContext: Pick): void { + this.mdocContext = mdocContext + } + + public async encodedStructure(): Promise { + const cwt = new CWT() + cwt.setClaims({ + [StatusInfoClaim.StatusList]: { + status_list: cborEncode(this.statusList), + }, + }) + if (!this.key) { + throw new Error('Signing key is required to encode StatusInfo') + } + if (!this.mdocContext) { + throw new Error('MdocContext is required to encode StatusInfo') + } + // Todo: Add support for Mac0? + const type = CoseStructureType.Sign1 + return cborEncode( + new Tag(await cwt.create({ type, key: this.key, mdocContext: this.mdocContext }), CoseTypeToTag[type]) + ) + } + + public static fromEncodedStructure(encodedStructure: Uint8Array): StatusInfo { + const decoded = cborDecode(encodedStructure) as Sign1 | Mac0 + if (!(decoded instanceof Sign1 || decoded instanceof Mac0)) { + throw new Error('Unsupported CWT type') + } + if (!decoded.payload) { + throw new Error('CWT payload is missing') + } + const payload = cborDecode(decoded.payload) as { + [StatusInfoClaim.StatusList]: { status_list: StatusInfoStructure } + } + if (!payload || typeof payload !== 'object' || !(StatusInfoClaim.StatusList in payload)) { + throw new Error('Invalid status list structure') + } + const statusList = payload[StatusInfoClaim.StatusList].status_list + if (!statusList || typeof statusList !== 'object' || !('idx' in statusList) || !('uri' in statusList)) { + throw new Error('Invalid status list structure') + } + return new StatusInfo({ + statusList: { + idx: statusList.idx, + uri: statusList.uri, + }, + }) + } +} diff --git a/tests/builders/issuer-signed-builder.test.ts b/tests/builders/issuer-signed-builder.test.ts index 5b486cf..264082c 100644 --- a/tests/builders/issuer-signed-builder.test.ts +++ b/tests/builders/issuer-signed-builder.test.ts @@ -51,6 +51,7 @@ describe('issuer signed builder', () => { digestAlgorithm: 'SHA-256', deviceKeyInfo: { deviceKey: CoseKey.fromJwk(DEVICE_JWK) }, validityInfo: { signed, validFrom, validUntil }, + statusList: { idx: 0, uri: 'https://status.example.com/status-list' }, }) issuerSignedEncoded = issuerSigned.encode() From 87a85a53bdca73a93cb6a657481fd7a69e375ebe Mon Sep 17 00:00:00 2001 From: Dinkar Date: Mon, 30 Jun 2025 17:05:18 +0530 Subject: [PATCH 22/24] refactor: update StatusInfo handling in IssuerSignedBuilder and MobileSecurityObject; replace StatusInfoStructure with StatusInfoOptions --- src/mdoc/builders/issuer-signed-builder.ts | 6 +- src/mdoc/models/mobile-security-object.ts | 9 ++- src/mdoc/models/status-info.ts | 80 ++++++++-------------- 3 files changed, 34 insertions(+), 61 deletions(-) diff --git a/src/mdoc/builders/issuer-signed-builder.ts b/src/mdoc/builders/issuer-signed-builder.ts index 0931b72..8adce25 100644 --- a/src/mdoc/builders/issuer-signed-builder.ts +++ b/src/mdoc/builders/issuer-signed-builder.ts @@ -22,7 +22,7 @@ import { type MobileSecurityObjectOptions, type Namespace, StatusInfo, - type StatusInfoStructure, + type StatusInfoOptions, ValidityInfo, type ValidityInfoOptions, ValueDigests, @@ -89,7 +89,7 @@ export class IssuerSignedBuilder { validityInfo: ValidityInfo | ValidityInfoOptions deviceKeyInfo: DeviceKeyInfo | DeviceKeyInfoOptions certificate: Uint8Array - statusList?: StatusInfoStructure + statusList?: StatusInfoOptions }): Promise { const validityInfo = options.validityInfo instanceof ValidityInfo ? options.validityInfo : new ValidityInfo(options.validityInfo) @@ -105,7 +105,7 @@ export class IssuerSignedBuilder { valueDigests: await this.convertIssuerNamespacesIntoValueDigests(options.digestAlgorithm), } if (options.statusList) { - payload.status = new StatusInfo({ key: options.signingKey, statusList: options.statusList }) + payload.status = new StatusInfo(options.statusList) } const mso = new MobileSecurityObject(payload) diff --git a/src/mdoc/models/mobile-security-object.ts b/src/mdoc/models/mobile-security-object.ts index feb9bd0..6166336 100644 --- a/src/mdoc/models/mobile-security-object.ts +++ b/src/mdoc/models/mobile-security-object.ts @@ -2,7 +2,7 @@ import { type CborDecodeOptions, CborStructure, cborDecode } from '../../cbor' import type { DigestAlgorithm } from '../../cose' import { DeviceKeyInfo, type DeviceKeyInfoStructure } from './device-key-info' import type { DocType } from './doctype' -import { StatusInfo } from './status-info' +import { StatusInfo, type StatusInfoStructure } from './status-info' import { ValidityInfo, type ValidityInfoStructure } from './validity-info' import { ValueDigests, type ValueDigestsStructure } from './value-digests' @@ -13,7 +13,7 @@ export type MobileSecurityObjectStructure = { valueDigests: ValueDigestsStructure deviceKeyInfo: DeviceKeyInfoStructure validityInfo: ValidityInfoStructure - status?: Uint8Array + status?: StatusInfoStructure } export type MobileSecurityObjectOptions = { @@ -46,8 +46,7 @@ export class MobileSecurityObject extends CborStructure { this.status = options.status } - // Todo: Is it fine to make it async? - public async encodedStructure(): Promise { + public encodedStructure(): MobileSecurityObjectStructure { const structure: MobileSecurityObjectStructure = { version: this.version, digestAlgorithm: this.digestAlgorithm, @@ -57,7 +56,7 @@ export class MobileSecurityObject extends CborStructure { validityInfo: this.validityInfo.encodedStructure(), } if (this.status) { - structure.status = await this.status.encodedStructure() + structure.status = this.status.encodedStructure() } return structure } diff --git a/src/mdoc/models/status-info.ts b/src/mdoc/models/status-info.ts index c3f7409..c170ff6 100644 --- a/src/mdoc/models/status-info.ts +++ b/src/mdoc/models/status-info.ts @@ -1,17 +1,14 @@ import { cborDecode, cborEncode } from '../../cbor' -import { Tag } from '../../cbor/cbor-x' -import type { MdocContext } from '../../context' -import { type CoseKey, CoseStructureType, CoseTypeToTag, Mac0, Sign1 } from '../../cose' -import { CWT } from '../../cwt' export type StatusInfoStructure = { - idx: number - uri: string + [StatusInfoClaim.StatusList]: { + status_list: Uint8Array + } } + export type StatusInfoOptions = { - statusList: StatusInfoStructure - key?: CoseKey - mdocContext?: Pick + idx: number + uri: string } export enum StatusInfoClaim { @@ -19,69 +16,46 @@ export enum StatusInfoClaim { } export class StatusInfo { - public statusList: StatusInfoStructure - public mdocContext?: Pick - public key?: CoseKey + public statusList: StatusInfoOptions public constructor(statusInfo: StatusInfoOptions) { this.statusList = { - idx: statusInfo.statusList.idx, - uri: statusInfo.statusList.uri, + idx: statusInfo.idx, + uri: statusInfo.uri, } - this.mdocContext = statusInfo.mdocContext - this.key = statusInfo.key - } - - public setKey(key: CoseKey): void { - this.key = key - } - public setMdocContext(mdocContext: Pick): void { - this.mdocContext = mdocContext } - public async encodedStructure(): Promise { - const cwt = new CWT() - cwt.setClaims({ + public encodedStructure(): StatusInfoStructure { + return { [StatusInfoClaim.StatusList]: { status_list: cborEncode(this.statusList), }, - }) - if (!this.key) { - throw new Error('Signing key is required to encode StatusInfo') } - if (!this.mdocContext) { - throw new Error('MdocContext is required to encode StatusInfo') - } - // Todo: Add support for Mac0? - const type = CoseStructureType.Sign1 - return cborEncode( - new Tag(await cwt.create({ type, key: this.key, mdocContext: this.mdocContext }), CoseTypeToTag[type]) - ) } - public static fromEncodedStructure(encodedStructure: Uint8Array): StatusInfo { - const decoded = cborDecode(encodedStructure) as Sign1 | Mac0 - if (!(decoded instanceof Sign1 || decoded instanceof Mac0)) { - throw new Error('Unsupported CWT type') + public static fromEncodedStructure(encodedStructure: StatusInfoStructure): StatusInfo { + let structure = encodedStructure as StatusInfoStructure + if (structure instanceof Map) { + structure = Object.fromEntries(structure.entries()) as StatusInfoStructure } - if (!decoded.payload) { - throw new Error('CWT payload is missing') + if (!(StatusInfoClaim.StatusList in structure)) { + throw new Error('Invalid status list structure') } - const payload = cborDecode(decoded.payload) as { - [StatusInfoClaim.StatusList]: { status_list: StatusInfoStructure } + if (structure[StatusInfoClaim.StatusList] instanceof Map) { + structure[StatusInfoClaim.StatusList] = Object.fromEntries(structure[StatusInfoClaim.StatusList].entries()) } - if (!payload || typeof payload !== 'object' || !(StatusInfoClaim.StatusList in payload)) { - throw new Error('Invalid status list structure') + + let statusList = cborDecode(structure[StatusInfoClaim.StatusList].status_list) as StatusInfoOptions + if (statusList instanceof Map) { + statusList = Object.fromEntries(statusList.entries()) as StatusInfoOptions } - const statusList = payload[StatusInfoClaim.StatusList].status_list - if (!statusList || typeof statusList !== 'object' || !('idx' in statusList) || !('uri' in statusList)) { + if (!('idx' in statusList) || !('uri' in statusList)) { throw new Error('Invalid status list structure') } + return new StatusInfo({ - statusList: { - idx: statusList.idx, - uri: statusList.uri, - }, + idx: statusList.idx, + uri: statusList.uri, }) } } From e239bb1e3c16ceb36f088b47c0b86d8a6eb65780 Mon Sep 17 00:00:00 2001 From: Dinkar Date: Mon, 30 Jun 2025 17:20:10 +0530 Subject: [PATCH 23/24] refactor: simplify StatusInfo structure by removing unnecessary claims and encoding logic --- src/mdoc/models/status-info.ts | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/src/mdoc/models/status-info.ts b/src/mdoc/models/status-info.ts index c170ff6..4e55152 100644 --- a/src/mdoc/models/status-info.ts +++ b/src/mdoc/models/status-info.ts @@ -1,9 +1,5 @@ -import { cborDecode, cborEncode } from '../../cbor' - export type StatusInfoStructure = { - [StatusInfoClaim.StatusList]: { - status_list: Uint8Array - } + status_list: StatusInfoOptions } export type StatusInfoOptions = { @@ -11,10 +7,6 @@ export type StatusInfoOptions = { uri: string } -export enum StatusInfoClaim { - StatusList = 65535, -} - export class StatusInfo { public statusList: StatusInfoOptions @@ -27,9 +19,7 @@ export class StatusInfo { public encodedStructure(): StatusInfoStructure { return { - [StatusInfoClaim.StatusList]: { - status_list: cborEncode(this.statusList), - }, + status_list: this.statusList, } } @@ -38,14 +28,8 @@ export class StatusInfo { if (structure instanceof Map) { structure = Object.fromEntries(structure.entries()) as StatusInfoStructure } - if (!(StatusInfoClaim.StatusList in structure)) { - throw new Error('Invalid status list structure') - } - if (structure[StatusInfoClaim.StatusList] instanceof Map) { - structure[StatusInfoClaim.StatusList] = Object.fromEntries(structure[StatusInfoClaim.StatusList].entries()) - } - let statusList = cborDecode(structure[StatusInfoClaim.StatusList].status_list) as StatusInfoOptions + let statusList = structure.status_list as StatusInfoOptions if (statusList instanceof Map) { statusList = Object.fromEntries(statusList.entries()) as StatusInfoOptions } From 9ffcc8b900f6abf7ff4ddb86f87297ec066cd2e7 Mon Sep 17 00:00:00 2001 From: Dinkar Date: Thu, 3 Jul 2025 17:52:57 +0530 Subject: [PATCH 24/24] refactor: rename fetchStatusListUri to fetchStatusList for consistency; update issuer signed builder tests to include status token verification --- src/credential-status/status-token.ts | 2 +- tests/builders/issuer-signed-builder.test.ts | 41 +++++++++++++++++++- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/src/credential-status/status-token.ts b/src/credential-status/status-token.ts index 27d6771..5caad22 100644 --- a/src/credential-status/status-token.ts +++ b/src/credential-status/status-token.ts @@ -135,7 +135,7 @@ export class CwtStatusToken { return StatusList.verifyStatus(statusList as Uint8Array, options.index, options.expectedStatus) } - static async fetchStatusListUri(statusListUri: string, timeoutMs = 5000): Promise { + static async fetchStatusList(statusListUri: string, timeoutMs = 5000): Promise { if (!statusListUri.startsWith('https://')) { throw new Error(`Status list URI must be HTTPS: ${statusListUri}`) } diff --git a/tests/builders/issuer-signed-builder.test.ts b/tests/builders/issuer-signed-builder.test.ts index 264082c..d9709a3 100644 --- a/tests/builders/issuer-signed-builder.test.ts +++ b/tests/builders/issuer-signed-builder.test.ts @@ -1,6 +1,14 @@ import { X509Certificate } from '@peculiar/x509' import { describe, expect, test } from 'vitest' -import { CoseKey, DateOnly, type IssuerSigned, SignatureAlgorithm } from '../../src' +import { + CoseKey, + CoseStructureType, + CwtStatusToken, + DateOnly, + type IssuerSigned, + SignatureAlgorithm, + StatusArray, +} from '../../src' import { IssuerSignedBuilder } from '../../src/mdoc/builders/issuer-signed-builder' import { mdocContext } from '../context' import { DEVICE_JWK, ISSUER_CERTIFICATE, ISSUER_PRIVATE_KEY_JWK } from '../issuing/config' @@ -29,7 +37,7 @@ const claims = { ], } -describe('issuer signed builder', () => { +describe('issuer signed builder', async () => { let issuerSigned: IssuerSigned let issuerSignedEncoded: Uint8Array @@ -39,6 +47,18 @@ describe('issuer signed builder', () => { const validUntil = new Date(signed) validUntil.setFullYear(signed.getFullYear() + 30) + const statusArray = new StatusArray(1) + statusArray.set(0, 0) + const statusToken = await CwtStatusToken.sign({ + mdocContext, + statusListUri: 'https://status.example.com/status-list', + claimsSet: { + statusArray, + }, + type: CoseStructureType.Sign1, + key: CoseKey.fromJwk(ISSUER_PRIVATE_KEY_JWK), + }) + test('correctly instantiate an issuer signed object', async () => { const issuerSignedBuilder = new IssuerSignedBuilder('org.iso.18013.5.1.mDL', mdocContext).addIssuerNamespace( 'org.iso.18013.5.1', @@ -85,6 +105,23 @@ describe('issuer signed builder', () => { expect(validityInfo.expectedUpdate).toBeUndefined() }) + test('verify status info', async () => { + const { status } = issuerSigned.issuerAuth.mobileSecurityObject + expect(status).toBeDefined() + + if (status) { + expect( + await CwtStatusToken.verifyStatus({ + mdocContext, + key: CoseKey.fromJwk(ISSUER_PRIVATE_KEY_JWK), + token: statusToken, + index: status.statusList.idx, + expectedStatus: 0, + }) + ).toBeTruthy() + } + }) + test('set correct digest algorithm', () => { const { digestAlgorithm } = issuerSigned.issuerAuth.mobileSecurityObject expect(digestAlgorithm).toEqual('SHA-256')