diff --git a/packages/jwt-status-list/README.md b/packages/jwt-status-list/README.md index 265555ab..d744e8e8 100644 --- a/packages/jwt-status-list/README.md +++ b/packages/jwt-status-list/README.md @@ -74,8 +74,6 @@ const status = statusList.getStatus(reference.idx); ### Integration into sd-jwt-vc The status list can be integrated into the [sd-jwt-vc](../sd-jwt-vc/README.md) library to provide a way to verify the status of a credential. In the [test folder](../sd-jwt-vc/src/test/index.spec.ts) you will find an example how to add the status reference to a credential and also how to verify the status of a credential. -```typescript - ### Caching the status list Depending on the `ttl` field if provided the status list can be cached for a certain amount of time. This library has no internal cache mechanism, so it is up to the user to implement it for example by providing a custom `fetchStatusList` function. diff --git a/packages/jwt-status-list/src/index.ts b/packages/jwt-status-list/src/index.ts index 21aa70d7..0b983ad4 100644 --- a/packages/jwt-status-list/src/index.ts +++ b/packages/jwt-status-list/src/index.ts @@ -1,3 +1,3 @@ -export * from './status-list.js'; -export * from './status-list-jwt.js'; -export * from './types.js'; +export * from './status-list'; +export * from './status-list-jwt'; +export * from './types'; diff --git a/packages/jwt-status-list/src/status-list-jwt.ts b/packages/jwt-status-list/src/status-list-jwt.ts index 11dea0e3..4aa75ab0 100644 --- a/packages/jwt-status-list/src/status-list-jwt.ts +++ b/packages/jwt-status-list/src/status-list-jwt.ts @@ -1,11 +1,11 @@ import type { JwtPayload } from '@sd-jwt/types'; -import { StatusList } from './status-list.js'; +import { StatusList } from './status-list'; import type { JWTwithStatusListPayload, StatusListJWTHeaderParameters, StatusListEntry, StatusListJWTPayload, -} from './types.js'; +} from './types'; import base64Url from 'base64url'; /** diff --git a/packages/jwt-status-list/src/status-list.ts b/packages/jwt-status-list/src/status-list.ts index e4c0b28d..8de5871f 100644 --- a/packages/jwt-status-list/src/status-list.ts +++ b/packages/jwt-status-list/src/status-list.ts @@ -1,6 +1,6 @@ import { deflate, inflate } from 'pako'; import base64Url from 'base64url'; -import type { BitsPerStatus } from './types.js'; +import type { BitsPerStatus } from './types'; /** * StatusListManager is a class that manages a list of statuses with variable bit size. */ diff --git a/packages/sd-jwt-vc/package.json b/packages/sd-jwt-vc/package.json index 8f038d31..ccdbb8bd 100644 --- a/packages/sd-jwt-vc/package.json +++ b/packages/sd-jwt-vc/package.json @@ -36,6 +36,7 @@ "license": "Apache-2.0", "dependencies": { "@sd-jwt/core": "workspace:*", + "@sd-jwt/utils": "workspace:*", "@sd-jwt/jwt-status-list": "workspace:*" }, "devDependencies": { diff --git a/packages/sd-jwt-vc/src/index.ts b/packages/sd-jwt-vc/src/index.ts index f79727c8..d3a1cb87 100644 --- a/packages/sd-jwt-vc/src/index.ts +++ b/packages/sd-jwt-vc/src/index.ts @@ -1,138 +1,4 @@ -import { Jwt, SDJwtInstance } from '@sd-jwt/core'; -import type { DisclosureFrame, Verifier } from '@sd-jwt/types'; -import { SDJWTException } from '../../utils/dist'; -import type { SdJwtVcPayload } from './sd-jwt-vc-payload'; -import type { SDJWTVCConfig } from './sd-jwt-vc-config'; -import { - type StatusListJWTHeaderParameters, - type StatusListJWTPayload, - getListFromStatusListJWT, -} from '@sd-jwt/jwt-status-list'; -export class SDJwtVcInstance extends SDJwtInstance { - /** - * The type of the SD-JWT-VC set in the header.typ field. - */ - protected type = 'vc+sd-jwt'; - - protected userConfig: SDJWTVCConfig = {}; - - constructor(userConfig?: SDJWTVCConfig) { - super(userConfig); - if (userConfig) { - this.userConfig = userConfig; - } - } - - /** - * Validates if the disclosureFrame contains any reserved fields. If so it will throw an error. - * @param disclosureFrame - */ - protected validateReservedFields( - disclosureFrame: DisclosureFrame, - ): void { - //validate disclosureFrame according to https://www.ietf.org/archive/id/draft-ietf-oauth-sd-jwt-vc-03.html#section-3.2.2.2 - if ( - disclosureFrame?._sd && - Array.isArray(disclosureFrame._sd) && - disclosureFrame._sd.length > 0 - ) { - const reservedNames = ['iss', 'nbf', 'exp', 'cnf', 'vct', 'status']; - // check if there is any reserved names in the disclosureFrame._sd array - const reservedNamesInDisclosureFrame = ( - disclosureFrame._sd as string[] - ).filter((key) => reservedNames.includes(key)); - if (reservedNamesInDisclosureFrame.length > 0) { - throw new SDJWTException('Cannot disclose protected field'); - } - } - } - - /** - * Fetches the status list from the uri with a timeout of 10 seconds. - * @param uri The URI to fetch from. - * @returns A promise that resolves to a compact JWT. - */ - private async statusListFetcher(uri: string): Promise { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 10000); - - try { - const response = await fetch(uri, { signal: controller.signal }); - if (!response.ok) { - throw new Error( - `Error fetching status list: ${ - response.status - } ${await response.text()}`, - ); - } - - return response.text(); - } finally { - clearTimeout(timeoutId); - } - } - - /** - * Validates the status, throws an error if the status is not 0. - * @param status - * @returns - */ - private async statusValidator(status: number): Promise { - if (status !== 0) throw new SDJWTException('Status is not valid'); - return Promise.resolve(); - } - - /** - * Verifies the SD-JWT-VC. - */ - async verify( - encodedSDJwt: string, - requiredClaimKeys?: string[], - requireKeyBindings?: boolean, - ) { - // Call the parent class's verify method - const result = await super - .verify(encodedSDJwt, requiredClaimKeys, requireKeyBindings) - .then((res) => { - return { payload: res.payload as SdJwtVcPayload, header: res.header }; - }); - - if (result.payload.status) { - //checks if a status field is present in the payload based on https://www.ietf.org/archive/id/draft-ietf-oauth-status-list-02.html - if (result.payload.status.status_list) { - // fetch the status list from the uri - const fetcher = - this.userConfig.statusListFetcher ?? this.statusListFetcher; - // fetch the status list from the uri - const statusListJWT = await fetcher( - result.payload.status.status_list.uri, - ); - - const slJWT = Jwt.fromEncode< - StatusListJWTHeaderParameters, - StatusListJWTPayload - >(statusListJWT); - // check if the status list has a valid signature. The presence of the verifier is checked in the parent class. - await slJWT.verify(this.userConfig.verifier as Verifier); - - //check if the status list is expired - if (slJWT.payload?.exp && slJWT.payload.exp < Date.now() / 1000) { - throw new SDJWTException('Status list is expired'); - } - - // get the status list from the status list JWT - const statusList = getListFromStatusListJWT(statusListJWT); - const status = statusList.getStatus( - result.payload.status.status_list.idx, - ); - - // validate the status - const statusValidator = - this.userConfig.statusValidator ?? this.statusValidator; - await statusValidator(status); - } - } - - return result; - } -} +export * from './sd-jwt-vc-config'; +export * from './sd-jwt-vc-instance'; +export * from './sd-jwt-vc-payload'; +export * from './sd-jwt-vc-status-reference'; diff --git a/packages/sd-jwt-vc/src/sd-jwt-vc-instance.ts b/packages/sd-jwt-vc/src/sd-jwt-vc-instance.ts new file mode 100644 index 00000000..cfe07275 --- /dev/null +++ b/packages/sd-jwt-vc/src/sd-jwt-vc-instance.ts @@ -0,0 +1,141 @@ +import { Jwt, SDJwtInstance } from '@sd-jwt/core'; +import type { DisclosureFrame, Verifier } from '@sd-jwt/types'; +import { SDJWTException } from '@sd-jwt/utils'; +import type { SdJwtVcPayload } from './sd-jwt-vc-payload'; +import type { SDJWTVCConfig } from './sd-jwt-vc-config'; +import { + type StatusListJWTPayload, + getListFromStatusListJWT, +} from '@sd-jwt/jwt-status-list'; +import type { StatusListJWTHeaderParameters } from '@sd-jwt/jwt-status-list'; +export class SDJwtVcInstance extends SDJwtInstance { + /** + * The type of the SD-JWT-VC set in the header.typ field. + */ + protected type = 'vc+sd-jwt'; + + protected userConfig: SDJWTVCConfig = {}; + + constructor(userConfig?: SDJWTVCConfig) { + super(userConfig); + if (userConfig) { + this.userConfig = userConfig; + } + } + + /** + * Validates if the disclosureFrame contains any reserved fields. If so it will throw an error. + * @param disclosureFrame + */ + protected validateReservedFields( + disclosureFrame: DisclosureFrame, + ): void { + //validate disclosureFrame according to https://www.ietf.org/archive/id/draft-ietf-oauth-sd-jwt-vc-03.html#section-3.2.2.2 + if ( + disclosureFrame?._sd && + Array.isArray(disclosureFrame._sd) && + disclosureFrame._sd.length > 0 + ) { + const reservedNames = ['iss', 'nbf', 'exp', 'cnf', 'vct', 'status']; + // check if there is any reserved names in the disclosureFrame._sd array + const reservedNamesInDisclosureFrame = ( + disclosureFrame._sd as string[] + ).filter((key) => reservedNames.includes(key)); + if (reservedNamesInDisclosureFrame.length > 0) { + throw new SDJWTException('Cannot disclose protected field'); + } + } + } + + /** + * Fetches the status list from the uri with a timeout of 10 seconds. + * @param uri The URI to fetch from. + * @returns A promise that resolves to a compact JWT. + */ + private async statusListFetcher(uri: string): Promise { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 10000); + + try { + const response = await fetch(uri, { signal: controller.signal }); + if (!response.ok) { + throw new Error( + `Error fetching status list: ${ + response.status + } ${await response.text()}`, + ); + } + + return response.text(); + } finally { + clearTimeout(timeoutId); + } + } + + /** + * Validates the status, throws an error if the status is not 0. + * @param status + * @returns + */ + private async statusValidator(status: number): Promise { + if (status !== 0) throw new SDJWTException('Status is not valid'); + return Promise.resolve(); + } + + /** + * Verifies the SD-JWT-VC. + */ + async verify( + encodedSDJwt: string, + requiredClaimKeys?: string[], + requireKeyBindings?: boolean, + ) { + // Call the parent class's verify method + const result = await super + .verify(encodedSDJwt, requiredClaimKeys, requireKeyBindings) + .then((res) => { + return { payload: res.payload as SdJwtVcPayload, header: res.header }; + }); + + if (result.payload.status) { + //checks if a status field is present in the payload based on https://www.ietf.org/archive/id/draft-ietf-oauth-status-list-02.html + if (result.payload.status.status_list) { + // fetch the status list from the uri + const fetcher = + this.userConfig.statusListFetcher ?? this.statusListFetcher; + // fetch the status list from the uri + const statusListJWT = await fetcher( + result.payload.status.status_list.uri, + ); + + const slJWT = Jwt.fromEncode< + StatusListJWTHeaderParameters, + StatusListJWTPayload + >(statusListJWT); + // check if the status list has a valid signature. The presence of the verifier is checked in the parent class. + await slJWT.verify(this.userConfig.verifier as Verifier); + + //check if the status list is expired + if ( + slJWT.payload?.exp && + (slJWT.payload.exp as number) < Date.now() / 1000 + ) { + throw new SDJWTException('Status list is expired'); + } + + // get the status list from the status list JWT + const statusList = getListFromStatusListJWT(statusListJWT); + const status = statusList.getStatus( + result.payload.status.status_list.idx, + ); + + // validate the status + const statusValidator = + this.userConfig.statusValidator ?? this.statusValidator; + await statusValidator(status); + } + } + + return result; + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a24993db..929a5355 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -228,6 +228,9 @@ importers: '@sd-jwt/jwt-status-list': specifier: workspace:* version: link:../jwt-status-list + '@sd-jwt/utils': + specifier: workspace:* + version: link:../utils devDependencies: '@sd-jwt/crypto-nodejs': specifier: workspace:*