From 787cce6bf130ae2ef9042c1e5ade70eb2c854eaf Mon Sep 17 00:00:00 2001 From: KulkarniShashank Date: Mon, 2 Sep 2024 12:20:51 +0530 Subject: [PATCH 1/7] feat: support w3c revocation Signed-off-by: KulkarniShashank --- packages/core/package.json | 1 + .../W3cJsonLdCredentialService.ts | 91 ++++++++++++++++++- .../vc/models/credential/W3cJsonCredential.ts | 55 +++++++++++ 3 files changed, 144 insertions(+), 3 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 84d2c6c343..5e89ee6f26 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -44,6 +44,7 @@ "@sphereon/pex-models": "^2.2.4", "@sphereon/ssi-types": "^0.28.0", "@stablelib/ed25519": "^1.0.2", + "@types/pako": "^2.0.3", "@types/ws": "^8.5.4", "abort-controller": "^3.0.0", "big-integer": "^1.6.51", diff --git a/packages/core/src/modules/vc/data-integrity/W3cJsonLdCredentialService.ts b/packages/core/src/modules/vc/data-integrity/W3cJsonLdCredentialService.ts index 74d174e793..182b4db8d3 100644 --- a/packages/core/src/modules/vc/data-integrity/W3cJsonLdCredentialService.ts +++ b/packages/core/src/modules/vc/data-integrity/W3cJsonLdCredentialService.ts @@ -9,7 +9,9 @@ import type { W3cJsonLdVerifyPresentationOptions, } from '../W3cCredentialServiceOptions' import type { W3cVerifyCredentialResult, W3cVerifyPresentationResult } from '../models' -import type { W3cJsonCredential } from '../models/credential/W3cJsonCredential' +import type { BitStringStatusListCredential, W3cJsonCredential } from '../models/credential/W3cJsonCredential' + +import * as pako from 'pako' import { createWalletKeyPairClass } from '../../../crypto/WalletKeyPair' import { CredoError } from '../../../error' @@ -109,10 +111,47 @@ export class W3cJsonLdCredentialService { credential: JsonTransformer.toJSON(options.credential), suite: suites, documentLoader: this.w3cCredentialsModuleConfig.documentLoader(agentContext), - checkStatus: ({ credential }: { credential: W3cJsonCredential }) => { + checkStatus: async ({ credential }: { credential: W3cJsonCredential }) => { // Only throw error if credentialStatus is present if (verifyCredentialStatus && 'credentialStatus' in credential) { - throw new CredoError('Verifying credential status for JSON-LD credentials is currently not supported') + if (Array.isArray(credential.credentialStatus)) { + throw new CredoError( + 'Verifying credential status as an array for JSON-LD credentials is currently not supported' + ) + } + + // Ensure credentialStatus contains the necessary properties + if (!credential.credentialStatus || credential.credentialStatus.statusListIndex === undefined) { + throw new CredoError('Invalid credential status format') + } + + const credentialStatusURL = credential.credentialStatus.statusListCredential + const bitStringStatusListCredential = await agentContext.config.agentDependencies.fetch( + credentialStatusURL, + { + method: 'GET', + } + ) + + if (!bitStringStatusListCredential.ok) { + throw new CredoError(`HTTP error! Status: ${bitStringStatusListCredential.status}`) + } + const bitStringCredential = + (await bitStringStatusListCredential.json()) as unknown as BitStringStatusListCredential + const encodedBitString = bitStringCredential.credential.credentialSubject.encodedList + const compressedBuffer = Uint8Array.from(atob(encodedBitString), (c) => c.charCodeAt(0)) + + // Decompress using pako + const decodedBitString = pako.ungzip(compressedBuffer, { to: 'string' }) + const statusListIndex = Number(credential.credentialStatus.statusListIndex) + + if (statusListIndex < 0 || statusListIndex >= decodedBitString.length) { + throw new CredoError('Index out of bounds') + } + + if (decodedBitString[statusListIndex] === '1') { + throw new CredoError(`Credential at index ${credential.credentialStatus.statusListIndex} is revoked.`) + } } return { verified: true, @@ -265,6 +304,52 @@ export class W3cJsonLdCredentialService { challenge: options.challenge, domain: options.domain, documentLoader: this.w3cCredentialsModuleConfig.documentLoader(agentContext), + checkStatus: async ({ credential }: { credential: W3cJsonCredential }) => { + // Only throw error if credentialStatus is present + if ('credentialStatus' in credential) { + if (Array.isArray(credential.credentialStatus)) { + throw new CredoError( + 'Verifying credential status as an array for JSON-LD credentials is currently not supported' + ) + } + + // Ensure credentialStatus contains the necessary properties + if (!credential.credentialStatus || credential.credentialStatus.statusListIndex === undefined) { + throw new CredoError('Invalid credential status format') + } + + const credentialStatusURL = credential.credentialStatus.statusListCredential + const bitStringStatusListCredential = await agentContext.config.agentDependencies.fetch( + credentialStatusURL, + { + method: 'GET', + } + ) + + if (!bitStringStatusListCredential.ok) { + throw new CredoError(`HTTP error! Status: ${bitStringStatusListCredential.status}`) + } + const bitStringCredential = + (await bitStringStatusListCredential.json()) as unknown as BitStringStatusListCredential + const encodedBitString = bitStringCredential.credential.credentialSubject.encodedList + const compressedBuffer = Uint8Array.from(atob(encodedBitString), (c) => c.charCodeAt(0)) + + // Decompress using pako + const decodedBitString = pako.ungzip(compressedBuffer, { to: 'string' }) + const statusListIndex = Number(credential.credentialStatus.statusListIndex) + + if (statusListIndex < 0 || statusListIndex >= decodedBitString.length) { + throw new CredoError('Index out of bounds') + } + + if (decodedBitString[statusListIndex] === '1') { + throw new CredoError(`Credential at index ${credential.credentialStatus.statusListIndex} is revoked.`) + } + } + return { + verified: true, + } + }, } // this is a hack because vcjs throws if purpose is passed as undefined or null diff --git a/packages/core/src/modules/vc/models/credential/W3cJsonCredential.ts b/packages/core/src/modules/vc/models/credential/W3cJsonCredential.ts index fa43911d43..abd16ad6de 100644 --- a/packages/core/src/modules/vc/models/credential/W3cJsonCredential.ts +++ b/packages/core/src/modules/vc/models/credential/W3cJsonCredential.ts @@ -10,4 +10,59 @@ export interface W3cJsonCredential { expirationDate?: string credentialSubject: SingleOrArray [key: string]: unknown + credentialStatus?: SingleOrArray +} + +type CredentialStatusType = 'BitstringStatusListEntry' +// The purpose can be anything apart from this as well +export enum CredentialStatusPurpose { + 'revocation' = 'revocation', + 'suspension' = 'suspension', + 'message' = 'message', +} + +export interface StatusMessage { + // a string representing the hexadecimal value of the status prefixed with 0x + status: string + // a string used by software developers to assist with debugging which SHOULD NOT be displayed to end users + message?: string + // We can have some key value pairs as well + [key: string]: unknown +} + +export interface CredentialStatus { + id: string + // Since currenlty we are only trying to support 'BitStringStatusListEntry' + type: CredentialStatusType + statusPurpose: CredentialStatusPurpose + // Unique identifier for the specific credential + statusListIndex: string + // Must be url referencing to a VC of type 'BitstringStatusListCredential' + statusListCredential: string + // The statusSize indicates the size of the status entry in bits + statusSize?: number + // Must be preset if statusPurpose is message + /** + * the length of which MUST equal the number of possible status messages indicated by statusSize + * (e.g., statusMessage array MUST have 2 elements if statusSize has 1 bit, + * 4 elements if statusSize has 2 bits, 8 elements if statusSize has 3 bits, etc.). + */ + statusMessage?: StatusMessage[] + // An implementer MAY include the statusReference property. If present, its value MUST be a URL or an array of URLs [URL] which dereference to material related to the status + statusReference?: SingleOrArray +} + +// Define an interface for `credentialSubject` +export interface CredentialSubject { + encodedList: string +} + +// Define an interface for the `credential` object that uses `CredentialSubject` +export interface Credential { + credentialSubject: CredentialSubject +} + +// Use the `Credential` interface within `BitStringStatusListCredential` +export interface BitStringStatusListCredential { + credential: Credential } From cc1975af145b000401abbb1f7155f2b4fef907e1 Mon Sep 17 00:00:00 2001 From: KulkarniShashank Date: Mon, 2 Sep 2024 12:50:23 +0530 Subject: [PATCH 2/7] fix: update pako library in package.json Signed-off-by: KulkarniShashank --- packages/core/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index 5e89ee6f26..454be90a44 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -44,7 +44,6 @@ "@sphereon/pex-models": "^2.2.4", "@sphereon/ssi-types": "^0.28.0", "@stablelib/ed25519": "^1.0.2", - "@types/pako": "^2.0.3", "@types/ws": "^8.5.4", "abort-controller": "^3.0.0", "big-integer": "^1.6.51", @@ -58,6 +57,7 @@ "luxon": "^3.3.0", "make-error": "^1.3.6", "object-inspect": "^1.10.3", + "pako": "^2.1.0", "query-string": "^7.0.1", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.0", @@ -72,6 +72,7 @@ "@types/jsonpath": "^0.2.4", "@types/luxon": "^3.2.0", "@types/object-inspect": "^1.8.0", + "@types/pako": "^2.0.3", "@types/uuid": "^9.0.1", "@types/varint": "^6.0.0", "nock": "^13.3.0", From 67bc551b9b24be7bae4ea73cfdd8391f88f1d935 Mon Sep 17 00:00:00 2001 From: KulkarniShashank Date: Tue, 3 Sep 2024 13:00:19 +0530 Subject: [PATCH 3/7] fix: added the function for validate the BitStringStatusList credential Signed-off-by: KulkarniShashank --- .../VerifyBitStringCredentialStatus.ts | 71 ++++++++++++++++ .../W3cJsonLdCredentialService.ts | 85 +------------------ .../models/credential/W3cCredentialStatus.ts | 44 +++++++++- .../vc/models/credential/W3cJsonCredential.ts | 55 +----------- .../credential/W3cJsonCredentialStatus.ts | 55 ++++++++++++ 5 files changed, 172 insertions(+), 138 deletions(-) create mode 100644 packages/core/src/modules/vc/data-integrity/VerifyBitStringCredentialStatus.ts create mode 100644 packages/core/src/modules/vc/models/credential/W3cJsonCredentialStatus.ts diff --git a/packages/core/src/modules/vc/data-integrity/VerifyBitStringCredentialStatus.ts b/packages/core/src/modules/vc/data-integrity/VerifyBitStringCredentialStatus.ts new file mode 100644 index 0000000000..b467ab3ee9 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/VerifyBitStringCredentialStatus.ts @@ -0,0 +1,71 @@ +import type { AgentContext } from '../../../agent/context' +import type { W3cJsonCredential } from '../models/credential/W3cJsonCredential' +import type { BitStringStatusListCredential } from '../models/credential/W3cJsonCredentialStatus' + +import * as pako from 'pako' + +import { CredoError } from '../../../error' +import { validateStatus } from '../models/credential/W3cCredentialStatus' + +// Function to fetch and parse the bit string status list credential +const fetchBitStringStatusListCredential = async ( + agentContext: AgentContext, + url: string +): Promise => { + const response = await agentContext.config.agentDependencies.fetch(url, { method: 'GET' }) + + if (!response.ok) { + throw new CredoError(`Failed to fetch bit string status list. HTTP Status: ${response.status}`) + } + + try { + return (await response.json()) as BitStringStatusListCredential + } catch (error) { + throw new CredoError('Failed to parse the bit string status list credential') + } +} + +export const verifyBitStringCredentialStatus = async (credential: W3cJsonCredential, agentContext: AgentContext) => { + const { credentialStatus } = credential + + if (Array.isArray(credentialStatus)) { + throw new CredoError('Verifying credential status as an array for JSON-LD credentials is currently not supported') + } + + if (!credentialStatus || credentialStatus.statusListIndex === undefined) { + throw new CredoError('Invalid credential status format') + } + + // Validate credentialStatus using the class-based approach + const isValid = await validateStatus(credentialStatus, agentContext) + + if (!isValid) { + throw new CredoError('Invalid credential status type. Expected BitstringStatusList') + } + + // Fetch the bit string status list credential + const bitStringStatusListCredential = await fetchBitStringStatusListCredential( + agentContext, + credentialStatus.statusListCredential + ) + + // Decode the encoded bit string + const encodedBitString = bitStringStatusListCredential.credential.credentialSubject.encodedList + const compressedBuffer = Uint8Array.from(atob(encodedBitString), (char) => char.charCodeAt(0)) + + // Decompress the bit string using pako + const decodedBitString = pako.ungzip(compressedBuffer, { to: 'string' }) + const statusListIndex = Number(credentialStatus.statusListIndex) + + // Ensure the statusListIndex is within bounds + if (statusListIndex < 0 || statusListIndex >= decodedBitString.length) { + throw new CredoError('Status list index is out of bounds') + } + + // Check if the credential is revoked + if (decodedBitString[statusListIndex] === '1') { + throw new CredoError(`Credential at index ${credentialStatus.statusListIndex} is revoked.`) + } + + return true +} diff --git a/packages/core/src/modules/vc/data-integrity/W3cJsonLdCredentialService.ts b/packages/core/src/modules/vc/data-integrity/W3cJsonLdCredentialService.ts index 182b4db8d3..27011edf64 100644 --- a/packages/core/src/modules/vc/data-integrity/W3cJsonLdCredentialService.ts +++ b/packages/core/src/modules/vc/data-integrity/W3cJsonLdCredentialService.ts @@ -9,9 +9,7 @@ import type { W3cJsonLdVerifyPresentationOptions, } from '../W3cCredentialServiceOptions' import type { W3cVerifyCredentialResult, W3cVerifyPresentationResult } from '../models' -import type { BitStringStatusListCredential, W3cJsonCredential } from '../models/credential/W3cJsonCredential' - -import * as pako from 'pako' +import type { W3cJsonCredential } from '../models/credential/W3cJsonCredential' import { createWalletKeyPairClass } from '../../../crypto/WalletKeyPair' import { CredoError } from '../../../error' @@ -23,6 +21,7 @@ import { W3cCredentialsModuleConfig } from '../W3cCredentialsModuleConfig' import { w3cDate } from '../util' import { SignatureSuiteRegistry } from './SignatureSuiteRegistry' +import { verifyBitStringCredentialStatus } from './VerifyBitStringCredentialStatus' import { deriveProof } from './deriveProof' import { assertOnlyW3cJsonLdVerifiableCredentials } from './jsonldUtil' import jsonld from './libraries/jsonld' @@ -112,46 +111,8 @@ export class W3cJsonLdCredentialService { suite: suites, documentLoader: this.w3cCredentialsModuleConfig.documentLoader(agentContext), checkStatus: async ({ credential }: { credential: W3cJsonCredential }) => { - // Only throw error if credentialStatus is present if (verifyCredentialStatus && 'credentialStatus' in credential) { - if (Array.isArray(credential.credentialStatus)) { - throw new CredoError( - 'Verifying credential status as an array for JSON-LD credentials is currently not supported' - ) - } - - // Ensure credentialStatus contains the necessary properties - if (!credential.credentialStatus || credential.credentialStatus.statusListIndex === undefined) { - throw new CredoError('Invalid credential status format') - } - - const credentialStatusURL = credential.credentialStatus.statusListCredential - const bitStringStatusListCredential = await agentContext.config.agentDependencies.fetch( - credentialStatusURL, - { - method: 'GET', - } - ) - - if (!bitStringStatusListCredential.ok) { - throw new CredoError(`HTTP error! Status: ${bitStringStatusListCredential.status}`) - } - const bitStringCredential = - (await bitStringStatusListCredential.json()) as unknown as BitStringStatusListCredential - const encodedBitString = bitStringCredential.credential.credentialSubject.encodedList - const compressedBuffer = Uint8Array.from(atob(encodedBitString), (c) => c.charCodeAt(0)) - - // Decompress using pako - const decodedBitString = pako.ungzip(compressedBuffer, { to: 'string' }) - const statusListIndex = Number(credential.credentialStatus.statusListIndex) - - if (statusListIndex < 0 || statusListIndex >= decodedBitString.length) { - throw new CredoError('Index out of bounds') - } - - if (decodedBitString[statusListIndex] === '1') { - throw new CredoError(`Credential at index ${credential.credentialStatus.statusListIndex} is revoked.`) - } + await verifyBitStringCredentialStatus(credential, agentContext) } return { verified: true, @@ -305,46 +266,8 @@ export class W3cJsonLdCredentialService { domain: options.domain, documentLoader: this.w3cCredentialsModuleConfig.documentLoader(agentContext), checkStatus: async ({ credential }: { credential: W3cJsonCredential }) => { - // Only throw error if credentialStatus is present if ('credentialStatus' in credential) { - if (Array.isArray(credential.credentialStatus)) { - throw new CredoError( - 'Verifying credential status as an array for JSON-LD credentials is currently not supported' - ) - } - - // Ensure credentialStatus contains the necessary properties - if (!credential.credentialStatus || credential.credentialStatus.statusListIndex === undefined) { - throw new CredoError('Invalid credential status format') - } - - const credentialStatusURL = credential.credentialStatus.statusListCredential - const bitStringStatusListCredential = await agentContext.config.agentDependencies.fetch( - credentialStatusURL, - { - method: 'GET', - } - ) - - if (!bitStringStatusListCredential.ok) { - throw new CredoError(`HTTP error! Status: ${bitStringStatusListCredential.status}`) - } - const bitStringCredential = - (await bitStringStatusListCredential.json()) as unknown as BitStringStatusListCredential - const encodedBitString = bitStringCredential.credential.credentialSubject.encodedList - const compressedBuffer = Uint8Array.from(atob(encodedBitString), (c) => c.charCodeAt(0)) - - // Decompress using pako - const decodedBitString = pako.ungzip(compressedBuffer, { to: 'string' }) - const statusListIndex = Number(credential.credentialStatus.statusListIndex) - - if (statusListIndex < 0 || statusListIndex >= decodedBitString.length) { - throw new CredoError('Index out of bounds') - } - - if (decodedBitString[statusListIndex] === '1') { - throw new CredoError(`Credential at index ${credential.credentialStatus.statusListIndex} is revoked.`) - } + await verifyBitStringCredentialStatus(credential, agentContext) } return { verified: true, diff --git a/packages/core/src/modules/vc/models/credential/W3cCredentialStatus.ts b/packages/core/src/modules/vc/models/credential/W3cCredentialStatus.ts index cf1de83151..2ccda58103 100644 --- a/packages/core/src/modules/vc/models/credential/W3cCredentialStatus.ts +++ b/packages/core/src/modules/vc/models/credential/W3cCredentialStatus.ts @@ -1,10 +1,18 @@ -import { IsString } from 'class-validator' +import { plainToInstance } from 'class-transformer' +import { IsEnum, IsString, validateOrReject } from 'class-validator' +import { AgentContext } from '../../../../agent/context' +import { CredoError } from '../../../../error' import { IsUri } from '../../../../utils/validators' +import { CredentialStatus, CredentialStatusPurpose, CredentialStatusType } from './W3cJsonCredentialStatus' + export interface W3cCredentialStatusOptions { id: string - type: string + type: CredentialStatusType + statusPurpose: CredentialStatusPurpose + statusListIndex: string + statusListCredential: string } export class W3cCredentialStatus { @@ -12,12 +20,42 @@ export class W3cCredentialStatus { if (options) { this.id = options.id this.type = options.type + this.statusPurpose = options.statusPurpose + this.statusListIndex = options.statusListIndex + this.statusListCredential = options.statusListCredential } } @IsUri() + @IsString() public id!: string + @IsEnum(['BitstringStatusListEntry']) + @IsString() + public type!: CredentialStatusType + + @IsEnum(CredentialStatusPurpose) + @IsString() + public statusPurpose!: CredentialStatusPurpose + @IsString() - public type!: string + public statusListIndex!: string + + @IsString() + public statusListCredential!: string +} + +// Function to validate the status using the updated method +export const validateStatus = async (status: CredentialStatus, agentContext: AgentContext): Promise => { + const entry = plainToInstance(W3cCredentialStatus, status) + + try { + await validateOrReject(entry) + return true + } catch (errors) { + agentContext.config.logger.debug(`Credential status validation failed: ${errors}`, { + stack: errors, + }) + throw new CredoError(`Invalid credential status type: ${errors}`) + } } diff --git a/packages/core/src/modules/vc/models/credential/W3cJsonCredential.ts b/packages/core/src/modules/vc/models/credential/W3cJsonCredential.ts index abd16ad6de..b9cea6a4fe 100644 --- a/packages/core/src/modules/vc/models/credential/W3cJsonCredential.ts +++ b/packages/core/src/modules/vc/models/credential/W3cJsonCredential.ts @@ -1,3 +1,4 @@ +import type { CredentialStatus } from './W3cJsonCredentialStatus' import type { JsonObject } from '../../../../types' import type { SingleOrArray } from '../../../../utils' @@ -9,60 +10,6 @@ export interface W3cJsonCredential { issuanceDate: string expirationDate?: string credentialSubject: SingleOrArray - [key: string]: unknown credentialStatus?: SingleOrArray -} - -type CredentialStatusType = 'BitstringStatusListEntry' -// The purpose can be anything apart from this as well -export enum CredentialStatusPurpose { - 'revocation' = 'revocation', - 'suspension' = 'suspension', - 'message' = 'message', -} - -export interface StatusMessage { - // a string representing the hexadecimal value of the status prefixed with 0x - status: string - // a string used by software developers to assist with debugging which SHOULD NOT be displayed to end users - message?: string - // We can have some key value pairs as well [key: string]: unknown } - -export interface CredentialStatus { - id: string - // Since currenlty we are only trying to support 'BitStringStatusListEntry' - type: CredentialStatusType - statusPurpose: CredentialStatusPurpose - // Unique identifier for the specific credential - statusListIndex: string - // Must be url referencing to a VC of type 'BitstringStatusListCredential' - statusListCredential: string - // The statusSize indicates the size of the status entry in bits - statusSize?: number - // Must be preset if statusPurpose is message - /** - * the length of which MUST equal the number of possible status messages indicated by statusSize - * (e.g., statusMessage array MUST have 2 elements if statusSize has 1 bit, - * 4 elements if statusSize has 2 bits, 8 elements if statusSize has 3 bits, etc.). - */ - statusMessage?: StatusMessage[] - // An implementer MAY include the statusReference property. If present, its value MUST be a URL or an array of URLs [URL] which dereference to material related to the status - statusReference?: SingleOrArray -} - -// Define an interface for `credentialSubject` -export interface CredentialSubject { - encodedList: string -} - -// Define an interface for the `credential` object that uses `CredentialSubject` -export interface Credential { - credentialSubject: CredentialSubject -} - -// Use the `Credential` interface within `BitStringStatusListCredential` -export interface BitStringStatusListCredential { - credential: Credential -} diff --git a/packages/core/src/modules/vc/models/credential/W3cJsonCredentialStatus.ts b/packages/core/src/modules/vc/models/credential/W3cJsonCredentialStatus.ts new file mode 100644 index 0000000000..71d53b187e --- /dev/null +++ b/packages/core/src/modules/vc/models/credential/W3cJsonCredentialStatus.ts @@ -0,0 +1,55 @@ +import type { SingleOrArray } from '../../../../utils' + +export type CredentialStatusType = 'BitstringStatusListEntry' +// The purpose can be anything apart from this as well +export enum CredentialStatusPurpose { + 'revocation' = 'revocation', + 'suspension' = 'suspension', + 'message' = 'message', +} + +export interface StatusMessage { + // a string representing the hexadecimal value of the status prefixed with 0x + status: string + // a string used by software developers to assist with debugging which SHOULD NOT be displayed to end users + message?: string + // We can have some key value pairs as well + [key: string]: unknown +} + +export interface CredentialStatus { + id: string + // Since currenlty we are only trying to support 'BitStringStatusListEntry' + type: CredentialStatusType + statusPurpose: CredentialStatusPurpose + // Unique identifier for the specific credential + statusListIndex: string + // Must be url referencing to a VC of type 'BitstringStatusListCredential' + statusListCredential: string + // The statusSize indicates the size of the status entry in bits + statusSize?: number + // Must be preset if statusPurpose is message + /** + * the length of which MUST equal the number of possible status messages indicated by statusSize + * (e.g., statusMessage array MUST have 2 elements if statusSize has 1 bit, + * 4 elements if statusSize has 2 bits, 8 elements if statusSize has 3 bits, etc.). + */ + statusMessage?: StatusMessage[] + // An implementer MAY include the statusReference property. If present, its value MUST be a URL or an array of URLs [URL] which dereference to material related to the status + statusReference?: SingleOrArray +} + +// Define an interface for `credentialSubject` +export interface CredentialSubject { + encodedList: string +} + +// Define an interface for the `credential` object that uses `CredentialSubject` +export interface Credential { + credentialSubject: CredentialSubject +} + +// Use the `Credential` interface within `BitStringStatusListCredential` +export interface BitStringStatusListCredential { + credential: Credential +} From b946e5b718c69a4591d0faaa43b1070559c441d1 Mon Sep 17 00:00:00 2001 From: KulkarniShashank Date: Tue, 3 Sep 2024 17:17:38 +0530 Subject: [PATCH 4/7] feat: added the revoke w3c credential function Signed-off-by: KulkarniShashank --- .../src/modules/credentials/CredentialsApi.ts | 111 ++++++++++++++++++ .../credentials/CredentialsApiOptions.ts | 27 +++++ .../formats/jsonld/JsonLdCredentialFormat.ts | 9 ++ 3 files changed, 147 insertions(+) diff --git a/packages/core/src/modules/credentials/CredentialsApi.ts b/packages/core/src/modules/credentials/CredentialsApi.ts index 6837fb5cb2..0d28a4d8d2 100644 --- a/packages/core/src/modules/credentials/CredentialsApi.ts +++ b/packages/core/src/modules/credentials/CredentialsApi.ts @@ -17,6 +17,9 @@ import type { DeleteCredentialOptions, SendRevocationNotificationOptions, DeclineCredentialOfferOptions, + RevokeCredentialOption, + BitStringCredential, + JsonLdRevocationStatus, } from './CredentialsApiOptions' import type { CredentialProtocol } from './protocol/CredentialProtocol' import type { CredentialFormatsFromProtocols } from './protocol/CredentialProtocolOptions' @@ -24,6 +27,8 @@ import type { CredentialExchangeRecord } from './repository/CredentialExchangeRe import type { AgentMessage } from '../../agent/AgentMessage' import type { Query, QueryOptions } from '../../storage/StorageService' +import * as pako from 'pako' + import { AgentContext } from '../../agent' import { MessageSender } from '../../agent/MessageSender' import { getOutboundMessageContext } from '../../agent/getOutboundMessageContext' @@ -32,10 +37,12 @@ import { CredoError } from '../../error' import { Logger } from '../../logger' import { inject, injectable } from '../../plugins' import { DidCommMessageRepository } from '../../storage/didcomm/DidCommMessageRepository' +import { Buffer } from '../../utils' import { ConnectionService } from '../connections/services' import { RoutingService } from '../routing/services/RoutingService' import { CredentialsModuleConfig } from './CredentialsModuleConfig' +import { BitstringStatusListEntry, JsonLdCredentialFormat } from './formats' import { CredentialState } from './models/CredentialState' import { RevocationNotificationService } from './protocol/revocation-notification/services' import { CredentialRepository } from './repository/CredentialRepository' @@ -515,6 +522,93 @@ export class CredentialsApi implements Credent return credentialRecord } + /** + * Revoke a credential by issuer + * associated with the credential record. + * + * @param credentialRecordId The id of the credential record for which to revoke the credential + * @returns Revoke credential notification message + * + */ + public async revokeJsonLdCredential(options: RevokeCredentialOption): Promise { + // Default to '1' (revoked) + const revocationStatus = '1' as JsonLdRevocationStatus + + const credentialRecord = await this.getCredentialRecord(options.credentialRecordId) + const credentialStatus = this.validateCredentialStatus(credentialRecord) + + const { statusListIndex: credentialIndex, statusListCredential: statusListCredentialURL } = credentialStatus + const bitStringCredential = await this.fetchAndValidateBitStringCredential(statusListCredentialURL) + const decodedBitString = await this.decodeBitSting(bitStringCredential.credential.credentialSubject.encodedList) + + if (decodedBitString.charAt(Number(credentialIndex)) === revocationStatus) { + throw new CredoError('The JSON-LD credential is already revoked') + } + + // Update the bit string with the revocation status + const updatedBitString = this.updateBitString(decodedBitString, credentialIndex, revocationStatus) + bitStringCredential.credential.credentialSubject.encodedList = await this.encodeBitString(updatedBitString) + + await this.postUpdatedBitString(statusListCredentialURL, bitStringCredential) + + // Send the revocation notification + const revocationFormat = `${statusListCredentialURL}::${credentialIndex}` + await this.sendRevocationNotification({ + credentialRecordId: options.credentialRecordId, + revocationId: statusListCredentialURL, + revocationFormat, + }) + } + + private async getCredentialRecord( + credentialRecordId: string + ): Promise> { + return this.getFormatData(credentialRecordId) + } + + private validateCredentialStatus( + credentialRecord: GetCredentialFormatDataReturn + ): BitstringStatusListEntry { + const credentialStatus = credentialRecord.offer?.jsonld?.credential?.credentialStatus + + if (Array.isArray(credentialStatus)) { + throw new CredoError('This credential status as an array for JSON-LD credentials is currently not supported') + } + + if (!credentialStatus) { + throw new CredoError('This JSON-LD credential is non-revocable') + } + + return credentialStatus + } + + private async fetchAndValidateBitStringCredential(statusListCredentialURL: string): Promise { + const response = await fetch(statusListCredentialURL) + if (!response.ok) { + throw new CredoError(`Failed to fetch credential: ${response.statusText}`) + } + return response.json() as Promise + } + + private updateBitString(decodedBitString: string, credentialIndex: string, revocationStatus: string): string { + return [ + decodedBitString.slice(0, Number(credentialIndex)), + revocationStatus, + decodedBitString.slice(Number(credentialIndex) + 1), + ].join('') + } + + private async postUpdatedBitString( + statusListCredentialURL: string, + bitStringCredential: BitStringCredential + ): Promise { + await fetch(statusListCredentialURL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ credentialsData: bitStringCredential }), + }) + } + /** * Send a revocation notification for a credential exchange record. Currently Revocation Notification V2 protocol is supported * @@ -705,4 +799,21 @@ export class CredentialsApi implements Credent return this.getProtocol(credentialExchangeRecord.protocolVersion) } + + private async encodeBitString(bitString: string): Promise { + // Convert the bitString to a Uint8Array + const buffer = new TextEncoder().encode(bitString) + const compressedBuffer = pako.gzip(buffer) + // Convert the compressed buffer to a base64 string + return Buffer.from(compressedBuffer).toString('base64') + } + + private async decodeBitSting(bitString: string): Promise { + // Decode base64 string to Uint8Array + const compressedBuffer = Uint8Array.from(atob(bitString), (c) => c.charCodeAt(0)) + + // Decompress using pako + const decompressedBuffer = pako.ungzip(compressedBuffer, { to: 'string' }) + return decompressedBuffer + } } diff --git a/packages/core/src/modules/credentials/CredentialsApiOptions.ts b/packages/core/src/modules/credentials/CredentialsApiOptions.ts index 98d3f4ace3..783a896d4a 100644 --- a/packages/core/src/modules/credentials/CredentialsApiOptions.ts +++ b/packages/core/src/modules/credentials/CredentialsApiOptions.ts @@ -44,6 +44,8 @@ interface BaseOptions { goal?: string } +export type JsonLdRevocationStatus = '0' | '1' + /** * Interface for CredentialsApi.proposeCredential. Will send a proposal. */ @@ -171,3 +173,28 @@ export interface DeclineCredentialOfferOptions { */ problemReportDescription?: string } + +/** + * Interface for CredentialsApi.revokeCredential. revoke a jsonld credential by Issuer. + */ +export interface RevokeCredentialOption { + credentialRecordId: string +} + +export interface CredentialSubject { + id: string + type: string + encodedList: string + statusPurpose: string +} + +export interface Credential { + credentialSubject: CredentialSubject +} + +/** + * Interface for bit string credential. Representing the bit string credential status. + */ +export interface BitStringCredential { + credential: Credential +} diff --git a/packages/core/src/modules/credentials/formats/jsonld/JsonLdCredentialFormat.ts b/packages/core/src/modules/credentials/formats/jsonld/JsonLdCredentialFormat.ts index 298961370a..f4d003fa5b 100644 --- a/packages/core/src/modules/credentials/formats/jsonld/JsonLdCredentialFormat.ts +++ b/packages/core/src/modules/credentials/formats/jsonld/JsonLdCredentialFormat.ts @@ -11,9 +11,18 @@ export interface JsonCredential { issuanceDate: string expirationDate?: string credentialSubject: SingleOrArray + credentialStatus?: SingleOrArray [key: string]: unknown } +export interface BitstringStatusListEntry { + id: string + type: string + statusPurpose: string + statusListIndex: string + statusListCredential: string +} + /** * Format for creating a jsonld proposal, offer or request. */ From a3e01657a58265e82bc5d2f12d427ae767869aff Mon Sep 17 00:00:00 2001 From: KulkarniShashank Date: Tue, 3 Sep 2024 17:27:17 +0530 Subject: [PATCH 5/7] refactor: added method in the credential api class Signed-off-by: KulkarniShashank --- packages/core/src/modules/credentials/CredentialsApi.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/core/src/modules/credentials/CredentialsApi.ts b/packages/core/src/modules/credentials/CredentialsApi.ts index 0d28a4d8d2..48750fd67f 100644 --- a/packages/core/src/modules/credentials/CredentialsApi.ts +++ b/packages/core/src/modules/credentials/CredentialsApi.ts @@ -69,9 +69,12 @@ export interface CredentialsApi { // Issue Credential Methods acceptCredential(options: AcceptCredentialOptions): Promise - // Revoke Credential Methods + // Revoke JSON-LD credential Methods sendRevocationNotification(options: SendRevocationNotificationOptions): Promise + // Revoke Credential Methods + revokeJsonLdCredential(options: RevokeCredentialOption): Promise<{ message: string }> + // out of band createOffer(options: CreateCredentialOfferOptions): Promise<{ message: AgentMessage @@ -530,7 +533,7 @@ export class CredentialsApi implements Credent * @returns Revoke credential notification message * */ - public async revokeJsonLdCredential(options: RevokeCredentialOption): Promise { + public async revokeJsonLdCredential(options: RevokeCredentialOption): Promise<{ message: string }> { // Default to '1' (revoked) const revocationStatus = '1' as JsonLdRevocationStatus @@ -558,6 +561,8 @@ export class CredentialsApi implements Credent revocationId: statusListCredentialURL, revocationFormat, }) + + return { message: 'The JSON-LD credential has been successfully revoked.' } } private async getCredentialRecord( From 7975139e87458720577d823e16c7ec013d450e2c Mon Sep 17 00:00:00 2001 From: KulkarniShashank Date: Tue, 3 Sep 2024 17:35:16 +0530 Subject: [PATCH 6/7] fix: revocation notification payload Signed-off-by: KulkarniShashank --- packages/core/src/modules/credentials/CredentialsApi.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/modules/credentials/CredentialsApi.ts b/packages/core/src/modules/credentials/CredentialsApi.ts index 48750fd67f..136ccbe072 100644 --- a/packages/core/src/modules/credentials/CredentialsApi.ts +++ b/packages/core/src/modules/credentials/CredentialsApi.ts @@ -555,11 +555,11 @@ export class CredentialsApi implements Credent await this.postUpdatedBitString(statusListCredentialURL, bitStringCredential) // Send the revocation notification - const revocationFormat = `${statusListCredentialURL}::${credentialIndex}` + const revocationId = `${statusListCredentialURL}::${credentialIndex}` await this.sendRevocationNotification({ credentialRecordId: options.credentialRecordId, - revocationId: statusListCredentialURL, - revocationFormat, + revocationId, + revocationFormat: 'jsonld', }) return { message: 'The JSON-LD credential has been successfully revoked.' } From 95d558c97f55bc772e91b17b2096e7ce1d494a76 Mon Sep 17 00:00:00 2001 From: KulkarniShashank Date: Tue, 3 Sep 2024 18:52:12 +0530 Subject: [PATCH 7/7] fix: update credential API to remove the notification call in the function. Signed-off-by: KulkarniShashank --- packages/core/src/modules/credentials/CredentialsApi.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/core/src/modules/credentials/CredentialsApi.ts b/packages/core/src/modules/credentials/CredentialsApi.ts index 136ccbe072..c4a97039d9 100644 --- a/packages/core/src/modules/credentials/CredentialsApi.ts +++ b/packages/core/src/modules/credentials/CredentialsApi.ts @@ -554,14 +554,6 @@ export class CredentialsApi implements Credent await this.postUpdatedBitString(statusListCredentialURL, bitStringCredential) - // Send the revocation notification - const revocationId = `${statusListCredentialURL}::${credentialIndex}` - await this.sendRevocationNotification({ - credentialRecordId: options.credentialRecordId, - revocationId, - revocationFormat: 'jsonld', - }) - return { message: 'The JSON-LD credential has been successfully revoked.' } }