Skip to content

Commit

Permalink
Fix/status list (openwallet-foundation#233)
Browse files Browse the repository at this point in the history
Signed-off-by: Mirko Mollik <[email protected]>
  • Loading branch information
cre8 authored May 14, 2024
1 parent 721d788 commit 92f2235
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 146 deletions.
2 changes: 0 additions & 2 deletions packages/jwt-status-list/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
6 changes: 3 additions & 3 deletions packages/jwt-status-list/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
4 changes: 2 additions & 2 deletions packages/jwt-status-list/src/status-list-jwt.ts
Original file line number Diff line number Diff line change
@@ -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';

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/jwt-status-list/src/status-list.ts
Original file line number Diff line number Diff line change
@@ -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.
*/
Expand Down
1 change: 1 addition & 0 deletions packages/sd-jwt-vc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"license": "Apache-2.0",
"dependencies": {
"@sd-jwt/core": "workspace:*",
"@sd-jwt/utils": "workspace:*",
"@sd-jwt/jwt-status-list": "workspace:*"
},
"devDependencies": {
Expand Down
142 changes: 4 additions & 138 deletions packages/sd-jwt-vc/src/index.ts
Original file line number Diff line number Diff line change
@@ -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<SdJwtVcPayload> {
/**
* 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<SdJwtVcPayload>,
): 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<string> {
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<void> {
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';
141 changes: 141 additions & 0 deletions packages/sd-jwt-vc/src/sd-jwt-vc-instance.ts
Original file line number Diff line number Diff line change
@@ -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<SdJwtVcPayload> {
/**
* 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<SdJwtVcPayload>,
): 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<string> {
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<void> {
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;
}
}
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 92f2235

Please sign in to comment.