Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix/status list #233

Merged
merged 4 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.