From f10b8427f0cd1f26f7722e2837e0d2e9c218788a Mon Sep 17 00:00:00 2001 From: Krishna Waske Date: Tue, 2 Jul 2024 15:01:05 +0530 Subject: [PATCH 01/15] refactor: add patch to credo to handle change in credential format to support revocation of jsonld credentials Signed-off-by: Krishna Waske --- ...ue and receive revocable credentials.patch | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 patches/@credo-ts+core+0.5.3+003+Change in credential Format for issue and receive revocable credentials.patch diff --git a/patches/@credo-ts+core+0.5.3+003+Change in credential Format for issue and receive revocable credentials.patch b/patches/@credo-ts+core+0.5.3+003+Change in credential Format for issue and receive revocable credentials.patch new file mode 100644 index 00000000..30ce8aaf --- /dev/null +++ b/patches/@credo-ts+core+0.5.3+003+Change in credential Format for issue and receive revocable credentials.patch @@ -0,0 +1,122 @@ +diff --git a/node_modules/@credo-ts/core/build/modules/credentials/formats/jsonld/JsonLdCredentialFormat.d.ts b/node_modules/@credo-ts/core/build/modules/credentials/formats/jsonld/JsonLdCredentialFormat.d.ts +index d12468b..09bd3b1 100644 +--- a/node_modules/@credo-ts/core/build/modules/credentials/formats/jsonld/JsonLdCredentialFormat.d.ts ++++ b/node_modules/@credo-ts/core/build/modules/credentials/formats/jsonld/JsonLdCredentialFormat.d.ts +@@ -10,7 +10,79 @@ export interface JsonCredential { + issuanceDate: string; + expirationDate?: string; + credentialSubject: SingleOrArray; +- [key: string]: unknown; ++ credentialStatus?: SingleOrArray ++ [key: string]: unknown ++} ++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 ++} ++ ++/** ++* "credentialStatus": { ++ "id": "https://example.com/credentials/status/8#492847", ++ "type": "BitstringStatusListEntry", ++ "statusPurpose": "message", ++ "statusListIndex": "492847", ++ "statusSize": 2, ++ "statusListCredential": "https://example.com/credentials/status/8", ++ "statusMessage": [ ++ {"status":"0x0", "message":"pending_review"}, ++ {"status":"0x1", "message":"accepted"}, ++ {"status":"0x2", "message":"rejected"}, ++ ... ++ ], ++ "statusReference": "https://example.org/status-dictionary/" ++} ++*/ ++ ++/** ++* "credentialStatus": [{ ++ "id": "https://example.com/credentials/status/3#94567", ++ "type": "BitstringStatusListEntry", ++ "statusPurpose": "revocation", ++ "statusListIndex": "94567", ++ "statusListCredential": "https://example.com/credentials/status/3" ++}, { ++ "id": "https://example.com/credentials/status/4#23452", ++ "type": "BitstringStatusListEntry", ++ "statusPurpose": "suspension", ++ "statusListIndex": "23452", ++ "statusListCredential": "https://example.com/credentials/status/4" ++}] ++*/ ++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 + } + /** + * Format for creating a jsonld proposal, offer or request. +diff --git a/node_modules/@credo-ts/core/build/modules/credentials/formats/jsonld/JsonLdCredentialFormatService.d.ts b/node_modules/@credo-ts/core/build/modules/credentials/formats/jsonld/JsonLdCredentialFormatService.d.ts +index 2006259..afc834e 100644 +--- a/node_modules/@credo-ts/core/build/modules/credentials/formats/jsonld/JsonLdCredentialFormatService.d.ts ++++ b/node_modules/@credo-ts/core/build/modules/credentials/formats/jsonld/JsonLdCredentialFormatService.d.ts +@@ -27,6 +27,7 @@ export declare class JsonLdCredentialFormatService implements CredentialFormatSe + * @returns object containing associated attachment, formats and offersAttach elements + * + */ ++ // Trail: W3C revocation: Change in payload + createOffer(agentContext: AgentContext, { credentialFormats, attachmentId }: CredentialFormatCreateOfferOptions): Promise; + processOffer(agentContext: AgentContext, { attachment }: CredentialFormatProcessOptions): Promise; + acceptOffer(agentContext: AgentContext, { attachmentId, offerAttachment }: CredentialFormatAcceptOfferOptions): Promise; +diff --git a/node_modules/@credo-ts/core/build/modules/vc/data-integrity/W3cJsonLdCredentialService.js b/node_modules/@credo-ts/core/build/modules/vc/data-integrity/W3cJsonLdCredentialService.js +index 3fa8bf2..9ad5f2e 100644 +--- a/node_modules/@credo-ts/core/build/modules/vc/data-integrity/W3cJsonLdCredentialService.js ++++ b/node_modules/@credo-ts/core/build/modules/vc/data-integrity/W3cJsonLdCredentialService.js +@@ -99,13 +99,15 @@ let W3cJsonLdCredentialService = class W3cJsonLdCredentialService { + suite: suites, + documentLoader: this.w3cCredentialsModuleConfig.documentLoader(agentContext), + checkStatus: ({ credential }) => { ++ // Note: currety comment this change to avoid passing credetials with credentialStatus + // Only throw error if credentialStatus is present +- if (verifyCredentialStatus && 'credentialStatus' in credential) { +- throw new error_1.CredoError('Verifying credential status for JSON-LD credentials is currently not supported'); +- } ++ // if (verifyCredentialStatus && 'credentialStatus' in credential) { ++ // TODO: add logic to verify credentialStatus ++ // throw new CredoError('Verifying credential status for JSON-LD credentials is currently not supported') ++ // } + return { +- verified: true, +- }; ++ verified: true, ++ } + }, + }; + // this is a hack because vcjs throws if purpose is passed as undefined or null From 94fcabacb13d9586b35bfc3eb91c796892c20ee4 Mon Sep 17 00:00:00 2001 From: Krishna Waske Date: Wed, 3 Jul 2024 17:42:25 +0530 Subject: [PATCH 02/15] chore: start w3c revocation controller Signed-off-by: Krishna Waske --- package.json | 1 + .../w3cRevocation/w3cRevocationController.ts | 80 +++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 src/controllers/w3cRevocation/w3cRevocationController.ts diff --git a/package.json b/package.json index 020a0c5c..81f93364 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "@credo-ts/push-notifications": "^0.7.0", "@credo-ts/question-answer": "0.5.3", "@credo-ts/tenants": "0.5.3", + "@digitalbazaar/vc-status-list": "^7.1.0", "@hyperledger/anoncreds-nodejs": "0.2.2", "@hyperledger/aries-askar-nodejs": "0.2.1", "@hyperledger/indy-vdr-nodejs": "0.2.2", diff --git a/src/controllers/w3cRevocation/w3cRevocationController.ts b/src/controllers/w3cRevocation/w3cRevocationController.ts new file mode 100644 index 00000000..f08f31c9 --- /dev/null +++ b/src/controllers/w3cRevocation/w3cRevocationController.ts @@ -0,0 +1,80 @@ +import type { RestAgentModules } from '../../cliAgent' + +import { Agent } from '@credo-ts/core' +import { createList } from '@digitalbazaar/vc-status-list' +import { injectable } from 'tsyringe' + +import { RecordId } from '../examples' + +import { Tags, Route, Controller, Post, Get, Path, Security } from 'tsoa' + +@Tags('Status') +@Route('/status') +@Security('apiKey') +@injectable() +export class StatusController extends Controller { + private agent: Agent + + public constructor(agent: Agent) { + super() + this.agent = agent + } + + /** + * Create Status List Credential + */ + // Creates a new StatusListCredential that can be used for revocation + @Post('/createStatusListCredential') + public async createStatusListCredential() { + // createCredential({ id, list, statusPurpose }: { id: string; list: vc.StatusList; statusPurpose: string; } + // vc.createCredential() + const list = await this.createBitStringStatusList() + console.log('this is list in createStatusListCredential', list) + return 'success' + // const bitstring = new Bitstring({ length: 10 }) + } + + /** + * Create Entry for status list credential + */ + // Create a new revocable credential + // But do we even need this additional endpoint? + @Post('/createEntryForStatusListCredential') + public async createEntryForStatusListCredential() { + return 'success' + } + + /** + * Retrieve status of a credential + */ + // Return if the status is revoked or not + @Get('/:credentialRecordId') + public async getCredentialStatus(@Path('credentialRecordId') credentialRecordId: RecordId) { + return `success retrieveing credentialRecordId ${credentialRecordId}` + } + + /** + * Change status of an entry in a StatusListCredential + */ + // Can this be a PUT operation? + @Post('/changeCredentialStatus') + public async changeCredentialStatus() { + return 'success' + } + + /** + * Retrieve statusListCredential according to their id + */ + // Get statusListCredential from the id passed + @Get('/:id') + public async getStatusListCredential(@Path('id') id: string) { + return `success with id: ${id}` + } + + private async createBitStringStatusList() { + const list = createList({ length: 100000 }) + console.log('vcStatusList', JSON.stringify(list)) + return list + // return list + } +} From a48c58be9f10d1118af9b29c747af6f3641e87e1 Mon Sep 17 00:00:00 2001 From: Krishna Waske Date: Thu, 4 Jul 2024 19:43:05 +0530 Subject: [PATCH 03/15] feat: able to create statusListCredential Signed-off-by: Krishna Waske --- src/controllers/types.ts | 9 +++++++ .../w3cRevocation/w3cRevocationController.ts | 26 ++++++++++++------- src/lib/nonEsModule.ts | 3 +++ tsconfig.build.json | 2 ++ tsconfig.json | 3 ++- 5 files changed, 33 insertions(+), 10 deletions(-) create mode 100644 src/lib/nonEsModule.ts diff --git a/src/controllers/types.ts b/src/controllers/types.ts index a4650b5e..c131b066 100644 --- a/src/controllers/types.ts +++ b/src/controllers/types.ts @@ -392,3 +392,12 @@ export interface SchemaMetadata { * @example "ea4e5e69-fc04-465a-90d2-9f8ff78aa71d" */ export type ThreadId = string + +export interface StatusList { + decode({ encodedList }: { encodedList: any }): Promise + bitstring: any + length: any + setStatus(index: any, status: any): any + getStatus(index: any): any + encode(): Promise +} diff --git a/src/controllers/w3cRevocation/w3cRevocationController.ts b/src/controllers/w3cRevocation/w3cRevocationController.ts index f08f31c9..3cc876ef 100644 --- a/src/controllers/w3cRevocation/w3cRevocationController.ts +++ b/src/controllers/w3cRevocation/w3cRevocationController.ts @@ -1,9 +1,10 @@ import type { RestAgentModules } from '../../cliAgent' +import type { StatusList } from '../types' import { Agent } from '@credo-ts/core' -import { createList } from '@digitalbazaar/vc-status-list' import { injectable } from 'tsyringe' +import { loadStatusList } from '../../lib/nonEsModule' import { RecordId } from '../examples' import { Tags, Route, Controller, Post, Get, Path, Security } from 'tsoa' @@ -19,18 +20,22 @@ export class StatusController extends Controller { super() this.agent = agent } + private statusList: any + private list!: StatusList /** * Create Status List Credential */ // Creates a new StatusListCredential that can be used for revocation @Post('/createStatusListCredential') + // accepts size, minimum 131,072 public async createStatusListCredential() { - // createCredential({ id, list, statusPurpose }: { id: string; list: vc.StatusList; statusPurpose: string; } - // vc.createCredential() + // Maintain an incremental index for statusListCredential + // Add Id with agentEndpoint/status/number + // Note: This endpoint should actually be an API to get StatusListCredential with id(as path param) const list = await this.createBitStringStatusList() - console.log('this is list in createStatusListCredential', list) - return 'success' + const listCred = await this._createStatusListCredential(list) + return listCred // const bitstring = new Bitstring({ length: 10 }) } @@ -72,9 +77,12 @@ export class StatusController extends Controller { } private async createBitStringStatusList() { - const list = createList({ length: 100000 }) - console.log('vcStatusList', JSON.stringify(list)) - return list - // return list + this.statusList = await loadStatusList() + this.list = await this.statusList.createList({ length: 100000 }) + return this.list + } + + private async _createStatusListCredential(list: StatusList) { + return this.statusList.createCredential({ id: '1', list: list, statusPurpose: 'suspension' }) } } diff --git a/src/lib/nonEsModule.ts b/src/lib/nonEsModule.ts new file mode 100644 index 00000000..22c87076 --- /dev/null +++ b/src/lib/nonEsModule.ts @@ -0,0 +1,3 @@ +export async function loadStatusList() { + return import('@digitalbazaar/vc-status-list') +} diff --git a/tsconfig.build.json b/tsconfig.build.json index 0b81e7f6..c425bec3 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -12,7 +12,9 @@ "resolveJsonModule": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, + "moduleResolution": "node", "types": ["jest"], + "typeRoots": ["node_modules/@types", "src/types"], "outDir": "./build" }, "include": ["src/**/*", "src/routes"], diff --git a/tsconfig.json b/tsconfig.json index 49cb417d..44c85af3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,8 @@ "lib": ["ES2021.Promise"], "experimentalDecorators": true, "emitDecoratorMetadata": true, - "types": ["jest", "node"] + "types": ["jest", "node"], + "typeRoots": ["src/types", "node_modules/@types"], }, "exclude": ["node_modules", "build"] } From 1a8697cd708fa7763ce32086dfe1085dca39e114 Mon Sep 17 00:00:00 2001 From: Krishna Waske Date: Tue, 23 Jul 2024 16:43:01 +0530 Subject: [PATCH 04/15] fix: add signing and storing capability of w3c status list credential Signed-off-by: Krishna Waske --- .../w3cRevocation/w3cRevocationController.ts | 67 +++++++++++++--- .../w3cRevocation/w3cRevocationTypes.ts | 25 ++++++ src/securityMiddleware.ts | 1 + src/types/vc-status-list.d.ts | 77 +++++++++++++++++++ 4 files changed, 158 insertions(+), 12 deletions(-) create mode 100644 src/controllers/w3cRevocation/w3cRevocationTypes.ts create mode 100644 src/types/vc-status-list.d.ts diff --git a/src/controllers/w3cRevocation/w3cRevocationController.ts b/src/controllers/w3cRevocation/w3cRevocationController.ts index 3cc876ef..e56c27f6 100644 --- a/src/controllers/w3cRevocation/w3cRevocationController.ts +++ b/src/controllers/w3cRevocation/w3cRevocationController.ts @@ -1,17 +1,19 @@ +import type { StatusListCredential } from './w3cRevocationTypes' import type { RestAgentModules } from '../../cliAgent' import type { StatusList } from '../types' +import type { StoreCredentialOptions, W3cJsonLdSignCredentialOptions } from '@credo-ts/core' import { Agent } from '@credo-ts/core' +import * as fs from 'fs' import { injectable } from 'tsyringe' import { loadStatusList } from '../../lib/nonEsModule' import { RecordId } from '../examples' -import { Tags, Route, Controller, Post, Get, Path, Security } from 'tsoa' +import { Tags, Route, Controller, Post, Get, Path, Security, Body } from 'tsoa' @Tags('Status') @Route('/status') -@Security('apiKey') @injectable() export class StatusController extends Controller { private agent: Agent @@ -27,16 +29,20 @@ export class StatusController extends Controller { * Create Status List Credential */ // Creates a new StatusListCredential that can be used for revocation - @Post('/createStatusListCredential') + @Security('apiKey') + @Post('/createStatusListCredential/:statusId') // accepts size, minimum 131,072 - public async createStatusListCredential() { + public async createStatusListCredential(@Path('statusId') statusId: string) { // Maintain an incremental index for statusListCredential // Add Id with agentEndpoint/status/number // Note: This endpoint should actually be an API to get StatusListCredential with id(as path param) + const agentEndpoints = await this.agent.config const list = await this.createBitStringStatusList() - const listCred = await this._createStatusListCredential(list) + const configFileData = fs.readFileSync('config.json', 'utf-8') + const config = JSON.parse(configFileData) + const statusListCredentialId = `yourIpAndPort:${config.port}/status/${statusId}` + const listCred = await this._createStatusListCredential(statusListCredentialId, list) return listCred - // const bitstring = new Bitstring({ length: 10 }) } /** @@ -44,16 +50,20 @@ export class StatusController extends Controller { */ // Create a new revocable credential // But do we even need this additional endpoint? - @Post('/createEntryForStatusListCredential') - public async createEntryForStatusListCredential() { - return 'success' + @Security('apiKey') + @Post('/signAndStoreStausListCredential') + // public async createEntryForStatusListCredential') + public async createEntryForStatusListCredential(@Body() credentialPayload: unknown) { + const storedCredential = await this.storeSighnedCredential() + return storedCredential } /** * Retrieve status of a credential */ // Return if the status is revoked or not - @Get('/:credentialRecordId') + @Security('apiKey') + @Get('/credential/:credentialRecordId') public async getCredentialStatus(@Path('credentialRecordId') credentialRecordId: RecordId) { return `success retrieveing credentialRecordId ${credentialRecordId}` } @@ -62,6 +72,7 @@ export class StatusController extends Controller { * Change status of an entry in a StatusListCredential */ // Can this be a PUT operation? + @Security('apiKey') @Post('/changeCredentialStatus') public async changeCredentialStatus() { return 'success' @@ -82,7 +93,39 @@ export class StatusController extends Controller { return this.list } - private async _createStatusListCredential(list: StatusList) { - return this.statusList.createCredential({ id: '1', list: list, statusPurpose: 'suspension' }) + private async _createStatusListCredential(id: string, list: StatusList): Promise { + return this.statusList.createCredential({ id: id, list: list, statusPurpose: 'suspension' }) + } + + public async storeSighnedCredential() { + const signedCred = { + '@context': ['https://www.w3.org/2018/credentials/v1', 'https://w3id.org/vc/status-list/2021/v1'], + id: 'http://yopurIp:yopurPort/status/1', + type: ['VerifiableCredential', 'StatusList2021Credential'], + issuer: { + id: 'did:key:z6Mkty8b4M1arFSmxYVtM3nsoQvyFurHPhRxRms7vZ6cVZbN', + }, + issuanceDate: '2019-10-12T07:20:50.52Z', + credentialSubject: { + id: 'http://yopurIp:yopurPort/status/1#list', + claims: { + type: 'StatusList2021', + encodedList: 'H4sIAAAAAAAAA-3BMQEAAADCoPVPbQsvoAAAAAAAAAAAAAAAAP4GcwM92tQwAAA', + statusPurpose: 'suspension', + }, + }, + proof: { + verificationMethod: + 'did:key:z6Mkty8b4M1arFSmxYVtM3nsoQvyFurHPhRxRms7vZ6cVZbN#z6Mkty8b4M1arFSmxYVtM3nsoQvyFurHPhRxRms7vZ6cVZbN', + type: 'Ed25519Signature2018', + created: '2024-07-08T12:24:04Z', + proofPurpose: 'assertionMethod', + jws: 'eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..hOr9nyr4dlQx1VOMgBow5AeLNrIQ1We0kvR1dFT0AQKkS_lIu-AruZpNVgVCMVlHiFrj-qFYr36YUTwTzUwiAw', + }, + } + console.log('this is before storing') + const storedCred = await this.agent.w3cCredentials.storeCredential(signedCred as unknown as StoreCredentialOptions) + console.log('this is storedCred', storedCred) + return storedCred } } diff --git a/src/controllers/w3cRevocation/w3cRevocationTypes.ts b/src/controllers/w3cRevocation/w3cRevocationTypes.ts new file mode 100644 index 00000000..e1ce210c --- /dev/null +++ b/src/controllers/w3cRevocation/w3cRevocationTypes.ts @@ -0,0 +1,25 @@ +import type { ClaimFormat, W3cCredentialSubject, W3cIssuer } from '@credo-ts/core' +import type { SingleOrArray } from '@credo-ts/core/build/utils' + +export interface StatusListCredential { + // Inside credential + // '@context': string[] + // id: string + // type: Array + // issuer: string | W3cIssuer + // issuanceDate: string + // credentialSubject: SingleOrArray + // Others + format: ClaimFormat.LdpVc + proofType: string + verificationMethod: string + credential: Credential +} +export interface Credential { + '@context': string[] + id: string + type: Array + issuer: string | W3cIssuer + issuanceDate: string + credentialSubject: SingleOrArray +} diff --git a/src/securityMiddleware.ts b/src/securityMiddleware.ts index 75654c49..08b82bae 100644 --- a/src/securityMiddleware.ts +++ b/src/securityMiddleware.ts @@ -20,6 +20,7 @@ export class SecurityMiddleware { { path: '/url/', method: 'GET' }, { path: '/multi-tenancy/url/', method: 'GET' }, { path: '/agent', method: 'GET' }, + { path: '/status', method: 'GET' }, ] // Check if authentication should be skipped for this route or controller diff --git a/src/types/vc-status-list.d.ts b/src/types/vc-status-list.d.ts new file mode 100644 index 00000000..77780e90 --- /dev/null +++ b/src/types/vc-status-list.d.ts @@ -0,0 +1,77 @@ +declare module '@digitalbazaar/vc-status-list' { + export class StatusList { + public static decode({ encodedList }: { encodedList: any }): Promise + public constructor({ length, buffer }?: { length: any; buffer: any }) + public bitstring: any + public length: any + public setStatus(index: any, status: any): any + public getStatus(index: any): any + public encode(): Promise + } + + export function createList({ length }: { length: any }): Promise + export function decodeList({ encodedList }: { encodedList: any }): Promise + /** + * Creates a StatusList Credential. + * + * @param {object} options - Options to use. + * @param {string} options.id - The id for StatusList Credential. + * @param {StatusList} options.list - An instance of StatusList. + * @param {string} options.statusPurpose - The purpose of the status entry. + * + * @returns {object} The resulting `StatusList Credential`. + */ + export function createCredential({ + id, + list, + statusPurpose, + }: { + id: string + list: StatusList + statusPurpose: string + }): object + export function checkStatus({ + credential, + documentLoader, + suite, + verifyStatusListCredential, + verifyMatchingIssuers, + }?: { + credential: any + documentLoader: any + suite: any + verifyStatusListCredential?: any + verifyMatchingIssuers?: any + }): Promise< + | { + verified: any + results: any + } + | { + verified: boolean + error: any + } + > + export function statusTypeMatches({ credential }?: { credential: any }): boolean + export function assertStatusList2021Context({ credential }?: { credential: any }): void + /** + * Gets the `credentialStatus` of a credential based on its status purpose + * (`statusPurpose`). + * + * @param {object} options - Options to use. + * @param {object} options.credential - A VC. + * @param {'revocation'|'suspension'} options.statusPurpose - A + * `statusPurpose`. + * + * @throws If the `credentialStatus` is invalid or missing. + * + * @returns {object} The resulting `credentialStatus`. + */ + export function getCredentialStatus({ + credential, + statusPurpose, + }?: { + credential: object + statusPurpose: 'revocation' | 'suspension' + }): object +} From 8b1c1c65dd027e763e1f39d570bf0eac768d4d2d Mon Sep 17 00:00:00 2001 From: KulkarniShashank Date: Thu, 8 Aug 2024 18:35:38 +0530 Subject: [PATCH 05/15] feat: implement w3c revoke functionality Signed-off-by: KulkarniShashank --- config.json | 5 + src/cli.ts | 2 + src/cliAgent.ts | 3 + .../credentials/CredentialController.ts | 60 +++- src/controllers/types.ts | 20 ++ .../w3cRevocation/w3cRevocationController.ts | 330 ++++++++++++------ src/utils/ServerConfig.ts | 1 + 7 files changed, 319 insertions(+), 102 deletions(-) create mode 100644 config.json diff --git a/config.json b/config.json new file mode 100644 index 00000000..ca002dc4 --- /dev/null +++ b/config.json @@ -0,0 +1,5 @@ +{ + "port": 4001, + "schemaFileServerURL": "https://schema.credebl.id/schemas/", + "bitStringStatusListURL": "http://192.168.1.125:5005/credentials/status/1" +} \ No newline at end of file diff --git a/src/cli.ts b/src/cli.ts index 54aa3358..caadc170 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -43,6 +43,7 @@ interface Parsed { rpcUrl?: string fileServerUrl?: string fileServerToken?: string + bitStringStatusListURL?: string } interface InboundTransport { @@ -253,5 +254,6 @@ export async function runCliServer() { rpcUrl: parsed.rpcUrl, fileServerUrl: parsed.fileServerUrl, fileServerToken: parsed.fileServerToken, + bitStringStatusListURL: parsed.bitStringStatusListURL, } as AriesRestConfig) } diff --git a/src/cliAgent.ts b/src/cliAgent.ts index a9a34f29..e2a46459 100644 --- a/src/cliAgent.ts +++ b/src/cliAgent.ts @@ -108,6 +108,7 @@ export interface AriesRestConfig { fileServerToken?: string walletScheme?: AskarMultiWalletDatabaseScheme schemaFileServerURL?: string + bitStringStatusListURL?: string } export async function readRestConfig(path: string) { @@ -260,6 +261,7 @@ async function generateSecretKey(length: number = 32): Promise { export async function runRestAgent(restConfig: AriesRestConfig) { const { + bitStringStatusListURL, schemaFileServerURL, logLevel, inboundTransports = [], @@ -439,6 +441,7 @@ export async function runRestAgent(restConfig: AriesRestConfig) { webhookUrl, port: adminPort, schemaFileServerURL, + bitStringStatusListURL, }, token ) diff --git a/src/controllers/credentials/CredentialController.ts b/src/controllers/credentials/CredentialController.ts index f7068dfa..7c3318cc 100644 --- a/src/controllers/credentials/CredentialController.ts +++ b/src/controllers/credentials/CredentialController.ts @@ -1,8 +1,17 @@ import type { RestAgentModules } from '../../cliAgent' -import type { CredentialExchangeRecordProps, CredentialProtocolVersionType, Routing } from '@credo-ts/core' +import type { BitStringCredential } from '../types' +import type { + CredentialExchangeRecordProps, + CredentialProtocolVersionType, + CredentialStatus, + Routing, +} from '@credo-ts/core' import { CredentialState, Agent, W3cCredentialService, Key, KeyType, CredentialRole } from '@credo-ts/core' +import * as fs from 'fs' import { injectable } from 'tsyringe' +import { promisify } from 'util' +import * as zlib from 'zlib' import ErrorHandlingService from '../../errorHandlingService' import { CredentialExchangeRecordExample, RecordId } from '../examples' @@ -155,6 +164,12 @@ export class CredentialController extends Controller { @Post('/create-offer') public async createOffer(@Body() createOfferOptions: CreateOfferOptions) { try { + if (createOfferOptions.credentialFormats.jsonld) { + if (createOfferOptions.isRevocable) { + const credentialStatus = await this.getCredentialStatus(createOfferOptions) + createOfferOptions.credentialFormats.jsonld.credential.credentialStatus = credentialStatus + } + } const offer = await this.agent.credentials.offerCredential(createOfferOptions) return offer } catch (error) { @@ -162,6 +177,49 @@ export class CredentialController extends Controller { } } + private async getCredentialStatus(createOfferOptions: CreateOfferOptions) { + try { + const bitStringStatusListURL = fs.readFileSync('config.json', 'utf-8') + const configJson = JSON.parse(bitStringStatusListURL) + + if (!configJson.bitStringStatusListURL) { + throw new Error('Please provide valid bitStringStatusList server URL') + } + + const bitStringStatusListCredential = await fetch(configJson.bitStringStatusListURL, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }) + + if (!bitStringStatusListCredential.ok) { + throw new Error(`HTTP error! Status: ${bitStringStatusListCredential.status}`) + } + + const bitStringCredential = (await bitStringStatusListCredential.json()) as BitStringCredential + const encodedBitString = bitStringCredential.credential.credentialSubject.encodedList + const gunzip = promisify(zlib.gunzip) + + const compressedBuffer = Buffer.from(encodedBitString, 'base64') + const decompressedBuffer = await gunzip(compressedBuffer) + const decodedBitString = decompressedBuffer.toString('binary') + const index = decodedBitString.indexOf('0') + + const credentialStatus = { + id: `${configJson.bitStringStatusListURL}#${index}`, + type: 'BitstringStatusListEntry', + statusPurpose: createOfferOptions.statusPurpose, + statusListIndex: index.toString(), + statusListCredential: configJson.bitStringStatusListURL, + } as unknown as CredentialStatus + + return credentialStatus + } catch (error) { + throw ErrorHandlingService.handle(error) + } + } + @Post('/create-offer-oob') public async createOfferOob(@Body() outOfBandOption: CreateOfferOobOptions) { try { diff --git a/src/controllers/types.ts b/src/controllers/types.ts index 0b25a62d..c95c62b4 100644 --- a/src/controllers/types.ts +++ b/src/controllers/types.ts @@ -25,6 +25,7 @@ import type { Attachment, KeyType, JsonLdCredentialFormat, + CredentialStatusPurpose, } from '@credo-ts/core' import type { DIDDocument } from 'did-resolver' @@ -88,12 +89,16 @@ export interface AcceptCredentialProposalOptions { comment?: string } +export type CredentialStatusType = 'BitstringStatusListEntry' + export interface CreateOfferOptions { + isRevocable: boolean protocolVersion: ProtocolVersion connectionId: RecordId credentialFormats: CredentialFormatPayload autoAcceptCredential?: AutoAcceptCredential comment?: string + statusPurpose: CredentialStatusPurpose } type CredentialFormatType = LegacyIndyCredentialFormat | JsonLdCredentialFormat | AnonCredsCredentialFormat @@ -397,3 +402,18 @@ export interface StatusList { getStatus(index: any): any encode(): Promise } + +export interface SignCredentialPayload { + id: string + issuerId: string + statusPurpose: string + bitStringLength: number +} + +export interface BitStringCredential { + credential: { + credentialSubject: { + encodedList: string + } + } +} diff --git a/src/controllers/w3cRevocation/w3cRevocationController.ts b/src/controllers/w3cRevocation/w3cRevocationController.ts index e56c27f6..d1f5166b 100644 --- a/src/controllers/w3cRevocation/w3cRevocationController.ts +++ b/src/controllers/w3cRevocation/w3cRevocationController.ts @@ -1,19 +1,21 @@ -import type { StatusListCredential } from './w3cRevocationTypes' import type { RestAgentModules } from '../../cliAgent' -import type { StatusList } from '../types' -import type { StoreCredentialOptions, W3cJsonLdSignCredentialOptions } from '@credo-ts/core' +import type { BitStringCredential } from '../types' +import type { W3cCredentialRecord, W3cJsonLdSignCredentialOptions } from '@credo-ts/core' -import { Agent } from '@credo-ts/core' -import * as fs from 'fs' +import { Agent, ClaimFormat } from '@credo-ts/core' +import axios from 'axios' import { injectable } from 'tsyringe' +import { promisify } from 'util' +import * as zlib from 'zlib' -import { loadStatusList } from '../../lib/nonEsModule' -import { RecordId } from '../examples' +import ErrorHandlingService from '../../errorHandlingService' +import { SignCredentialPayload } from '../types' -import { Tags, Route, Controller, Post, Get, Path, Security, Body } from 'tsoa' +import { Tags, Route, Controller, Post, Security, Body, Path } from 'tsoa' @Tags('Status') -@Route('/status') +@Route('/w3c/revocation') +@Security('apiKey') @injectable() export class StatusController extends Controller { private agent: Agent @@ -22,110 +24,236 @@ export class StatusController extends Controller { super() this.agent = agent } - private statusList: any - private list!: StatusList - - /** - * Create Status List Credential - */ - // Creates a new StatusListCredential that can be used for revocation - @Security('apiKey') - @Post('/createStatusListCredential/:statusId') - // accepts size, minimum 131,072 - public async createStatusListCredential(@Path('statusId') statusId: string) { - // Maintain an incremental index for statusListCredential - // Add Id with agentEndpoint/status/number - // Note: This endpoint should actually be an API to get StatusListCredential with id(as path param) - const agentEndpoints = await this.agent.config - const list = await this.createBitStringStatusList() - const configFileData = fs.readFileSync('config.json', 'utf-8') - const config = JSON.parse(configFileData) - const statusListCredentialId = `yourIpAndPort:${config.port}/status/${statusId}` - const listCred = await this._createStatusListCredential(statusListCredentialId, list) - return listCred - } - /** - * Create Entry for status list credential - */ - // Create a new revocable credential - // But do we even need this additional endpoint? - @Security('apiKey') - @Post('/signAndStoreStausListCredential') - // public async createEntryForStatusListCredential') - public async createEntryForStatusListCredential(@Body() credentialPayload: unknown) { - const storedCredential = await this.storeSighnedCredential() - return storedCredential + // Function to generate a bit string status + public async generateBitStringStatus(length: number): Promise { + return Array.from({ length }, () => (Math.random() > 0.5 ? '1' : '0')).join('') } - /** - * Retrieve status of a credential - */ - // Return if the status is revoked or not - @Security('apiKey') - @Get('/credential/:credentialRecordId') - public async getCredentialStatus(@Path('credentialRecordId') credentialRecordId: RecordId) { - return `success retrieveing credentialRecordId ${credentialRecordId}` + // Function to encode the bit string status + public async encodeBitString(bitString: string): Promise { + const gzip = promisify(zlib.gzip) + const buffer = Buffer.from(bitString, 'binary') + const compressedBuffer = await gzip(buffer) + return compressedBuffer.toString('base64') } - /** - * Change status of an entry in a StatusListCredential - */ - // Can this be a PUT operation? - @Security('apiKey') - @Post('/changeCredentialStatus') - public async changeCredentialStatus() { - return 'success' + public async decodeBitSting(bitString: string): Promise { + const gunzip = promisify(zlib.gunzip) + const compressedBuffer = Buffer.from(bitString, 'base64') + const decompressedBuffer = await gunzip(compressedBuffer) + return decompressedBuffer.toString('binary') } - /** - * Retrieve statusListCredential according to their id - */ - // Get statusListCredential from the id passed - @Get('/:id') - public async getStatusListCredential(@Path('id') id: string) { - return `success with id: ${id}` - } + @Post('/sign-credential') + public async createBitstringStatusListCredential( + @Body() signCredentialPayload: SignCredentialPayload + ): Promise { + try { + const { id, issuerId, statusPurpose, bitStringLength } = signCredentialPayload + const bitStringStatus = await this.generateBitStringStatus(bitStringLength) + const encodedList = await this.encodeBitString(bitStringStatus) + const didIdentifier = issuerId.split(':')[2] + const data = { + format: ClaimFormat.LdpVc, + credential: { + '@context': ['https://www.w3.org/2018/credentials/v1', 'https://w3id.org/vc/status-list/2021/v1'], + id, + type: ['VerifiableCredential', 'BitstringStatusListCredential'], + issuer: { + id: issuerId, + }, + issuanceDate: new Date().toISOString(), + credentialSubject: { + id, + type: 'BitstringStatusList', + encodedList, + statusPurpose, + }, + }, + verificationMethod: `${issuerId}#${didIdentifier}`, + proofType: 'Ed25519Signature2018', + } - private async createBitStringStatusList() { - this.statusList = await loadStatusList() - this.list = await this.statusList.createList({ length: 100000 }) - return this.list - } + await axios.post( + id, + { credentialsData: data }, + { + headers: { + 'Content-Type': 'application/json', + }, + } + ) + + const signCredential = await this.agent.w3cCredentials.signCredential( + data as unknown as W3cJsonLdSignCredentialOptions + ) + + const storCredential = await this.agent.w3cCredentials.storeCredential({ credential: signCredential }) - private async _createStatusListCredential(id: string, list: StatusList): Promise { - return this.statusList.createCredential({ id: id, list: list, statusPurpose: 'suspension' }) + return storCredential + } catch (error) { + throw ErrorHandlingService.handle(error) + } } - public async storeSighnedCredential() { - const signedCred = { - '@context': ['https://www.w3.org/2018/credentials/v1', 'https://w3id.org/vc/status-list/2021/v1'], - id: 'http://yopurIp:yopurPort/status/1', - type: ['VerifiableCredential', 'StatusList2021Credential'], - issuer: { - id: 'did:key:z6Mkty8b4M1arFSmxYVtM3nsoQvyFurHPhRxRms7vZ6cVZbN', - }, - issuanceDate: '2019-10-12T07:20:50.52Z', - credentialSubject: { - id: 'http://yopurIp:yopurPort/status/1#list', - claims: { - type: 'StatusList2021', - encodedList: 'H4sIAAAAAAAAA-3BMQEAAADCoPVPbQsvoAAAAAAAAAAAAAAAAP4GcwM92tQwAAA', - statusPurpose: 'suspension', + @Post('/revoke-credential/:id') + public async revokeW3C(@Path('id') id: string): Promise { + try { + const credential = await this.agent.credentials.getFormatData(id) + let credentialIndex + let statusListCredentialURL + + if (!Array.isArray(credential.credential?.jsonld?.credentialStatus)) { + credentialIndex = credential.credential?.jsonld?.credentialStatus?.statusListIndex as string + statusListCredentialURL = credential.credential?.jsonld?.credentialStatus?.statusListCredential as string + } else { + credentialIndex = credential.credential?.jsonld?.credentialStatus[0].statusListIndex as string + statusListCredentialURL = credential.credential?.jsonld?.credentialStatus[0].statusListCredential as string + } + + const bitStringStatusListCredential = await fetch(statusListCredentialURL, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', }, - }, - proof: { - verificationMethod: - 'did:key:z6Mkty8b4M1arFSmxYVtM3nsoQvyFurHPhRxRms7vZ6cVZbN#z6Mkty8b4M1arFSmxYVtM3nsoQvyFurHPhRxRms7vZ6cVZbN', - type: 'Ed25519Signature2018', - created: '2024-07-08T12:24:04Z', - proofPurpose: 'assertionMethod', - jws: 'eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..hOr9nyr4dlQx1VOMgBow5AeLNrIQ1We0kvR1dFT0AQKkS_lIu-AruZpNVgVCMVlHiFrj-qFYr36YUTwTzUwiAw', - }, + }) + + if (!bitStringStatusListCredential.ok) { + throw new Error(`HTTP error! Status: ${bitStringStatusListCredential.status}`) + } + + const bitStringCredential = (await bitStringStatusListCredential.json()) as BitStringCredential + const encodedBitString = bitStringCredential.credential.credentialSubject.encodedList + const decodeBitString = await this.decodeBitSting(encodedBitString) + + const findBitStringIndex = decodeBitString.charAt(parseInt(credentialIndex)) + if (findBitStringIndex === '1') { + throw new Error('The credential already revoked') + } + + const updateBitString = + decodeBitString.slice(0, parseInt(credentialIndex)) + 1 + decodeBitString.slice(parseInt(credentialIndex) + 1) + + const encodeUpdatedBitString = await this.encodeBitString(updateBitString) + bitStringCredential.credential.credentialSubject.encodedList = encodeUpdatedBitString + await axios.post( + statusListCredentialURL, + { credentialsData: bitStringCredential }, + { + headers: { + 'Content-Type': 'application/json', + }, + } + ) + + return `Credential revoke successfully` + } catch (error) { + throw ErrorHandlingService.handle(error) } - console.log('this is before storing') - const storedCred = await this.agent.w3cCredentials.storeCredential(signedCred as unknown as StoreCredentialOptions) - console.log('this is storedCred', storedCred) - return storedCred } + + // /** + // * Create Status List Credential + // */ + // // Creates a new StatusListCredential that can be used for revocation + // @Security('apiKey') + // @Post('/createStatusListCredential/:statusId') + // // accepts size, minimum 131,072 + // public async createStatusListCredential(@Path('statusId') statusId: string) { + // // Maintain an incremental index for statusListCredential + // // Add Id with agentEndpoint/status/number + // // Note: This endpoint should actually be an API to get StatusListCredential with id(as path param) + // const agentEndpoints = await this.agent.config + // const list = await this.createBitStringStatusList() + // const configFileData = fs.readFileSync('config.json', 'utf-8') + // const config = JSON.parse(configFileData) + // const statusListCredentialId = `yourIpAndPort:${config.port}/status/${statusId}` + // const listCred = await this._createStatusListCredential(statusListCredentialId, list) + // return listCred + // } + + // /** + // * Create Entry for status list credential + // */ + // // Create a new revocable credential + // // But do we even need this additional endpoint? + // @Security('apiKey') + // @Post('/signAndStoreStausListCredential') + // // public async createEntryForStatusListCredential') + // public async createEntryForStatusListCredential(@Body() credentialPayload: unknown) { + // const storedCredential = await this.storeSighnedCredential() + // return storedCredential + // } + + // /** + // * Retrieve status of a credential + // */ + // // Return if the status is revoked or not + // @Security('apiKey') + // @Get('/credential/:credentialRecordId') + // public async getCredentialStatus(@Path('credentialRecordId') credentialRecordId: RecordId) { + // return `success retrieveing credentialRecordId ${credentialRecordId}` + // } + + // /** + // * Change status of an entry in a StatusListCredential + // */ + // // Can this be a PUT operation? + // @Security('apiKey') + // @Post('/changeCredentialStatus') + // public async changeCredentialStatus() { + // return 'success' + // } + + // /** + // * Retrieve statusListCredential according to their id + // */ + // // Get statusListCredential from the id passed + // @Get('/:id') + // public async getStatusListCredential(@Path('id') id: string) { + // return `success with id: ${id}` + // } + + // private async createBitStringStatusList() { + // this.statusList = await loadStatusList() + // this.list = await this.statusList.createList({ length: 100000 }) + // return this.list + // } + + // private async _createStatusListCredential(id: string, list: StatusList): Promise { + // return this.statusList.createCredential({ id: id, list: list, statusPurpose: 'suspension' }) + // } + + // public async storeSighnedCredential() { + // const signedCred = { + // '@context': ['https://www.w3.org/2018/credentials/v1', 'https://w3id.org/vc/status-list/2021/v1'], + // id: 'http://yopurIp:yopurPort/status/1', + // type: ['VerifiableCredential', 'StatusList2021Credential'], + // issuer: { + // id: 'did:key:z6Mkty8b4M1arFSmxYVtM3nsoQvyFurHPhRxRms7vZ6cVZbN', + // }, + // issuanceDate: '2019-10-12T07:20:50.52Z', + // credentialSubject: { + // id: 'http://yopurIp:yopurPort/status/1#list', + // claims: { + // type: 'StatusList2021', + // encodedList: 'H4sIAAAAAAAAA-3BMQEAAADCoPVPbQsvoAAAAAAAAAAAAAAAAP4GcwM92tQwAAA', + // statusPurpose: 'suspension', + // }, + // }, + // proof: { + // verificationMethod: + // 'did:key:z6Mkty8b4M1arFSmxYVtM3nsoQvyFurHPhRxRms7vZ6cVZbN#z6Mkty8b4M1arFSmxYVtM3nsoQvyFurHPhRxRms7vZ6cVZbN', + // type: 'Ed25519Signature2018', + // created: '2024-07-08T12:24:04Z', + // proofPurpose: 'assertionMethod', + // jws: 'eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..hOr9nyr4dlQx1VOMgBow5AeLNrIQ1We0kvR1dFT0AQKkS_lIu-AruZpNVgVCMVlHiFrj-qFYr36YUTwTzUwiAw', + // }, + // } + // console.log('this is before storing') + // const storedCred = await this.agent.w3cCredentials.storeCredential(signedCred as unknown as StoreCredentialOptions) + // console.log('this is storedCred', storedCred) + // return storedCred + // } } diff --git a/src/utils/ServerConfig.ts b/src/utils/ServerConfig.ts index 4f66c053..1f537faf 100644 --- a/src/utils/ServerConfig.ts +++ b/src/utils/ServerConfig.ts @@ -9,4 +9,5 @@ export interface ServerConfig { /* Socket server is used for sending events over websocket to clients */ socketServer?: Server schemaFileServerURL?: string + bitStringStatusListURL?: string } From dfc729e5a19960fed43cf253ae28f431d3e822a0 Mon Sep 17 00:00:00 2001 From: KulkarniShashank Date: Fri, 9 Aug 2024 19:55:07 +0530 Subject: [PATCH 06/15] feat: implement index find and updation when sending offer Signed-off-by: KulkarniShashank --- .../credentials/CredentialController.ts | 115 +++++++++++++++--- src/controllers/types.ts | 14 ++- .../w3cRevocation/w3cRevocationController.ts | 72 ++++++----- src/utils/util.ts | 1 + 4 files changed, 155 insertions(+), 47 deletions(-) diff --git a/src/controllers/credentials/CredentialController.ts b/src/controllers/credentials/CredentialController.ts index 7c3318cc..b440d6fd 100644 --- a/src/controllers/credentials/CredentialController.ts +++ b/src/controllers/credentials/CredentialController.ts @@ -1,5 +1,5 @@ import type { RestAgentModules } from '../../cliAgent' -import type { BitStringCredential } from '../types' +import type { BitStringCredential, IndexRecord } from '../types' import type { CredentialExchangeRecordProps, CredentialProtocolVersionType, @@ -8,12 +8,13 @@ import type { } from '@credo-ts/core' import { CredentialState, Agent, W3cCredentialService, Key, KeyType, CredentialRole } from '@credo-ts/core' -import * as fs from 'fs' import { injectable } from 'tsyringe' import { promisify } from 'util' import * as zlib from 'zlib' import ErrorHandlingService from '../../errorHandlingService' +import { BadRequestError, ConflictError, InternalServerError } from '../../errors/errors' +import { BIT_STRING_STATUS_INDEX_URL } from '../../utils/util' import { CredentialExchangeRecordExample, RecordId } from '../examples' import { OutOfBandController } from '../outofband/OutOfBandController' import { @@ -164,29 +165,46 @@ export class CredentialController extends Controller { @Post('/create-offer') public async createOffer(@Body() createOfferOptions: CreateOfferOptions) { try { + let offer if (createOfferOptions.credentialFormats.jsonld) { if (createOfferOptions.isRevocable) { const credentialStatus = await this.getCredentialStatus(createOfferOptions) createOfferOptions.credentialFormats.jsonld.credential.credentialStatus = credentialStatus + offer = await this.agent.credentials.offerCredential(createOfferOptions) + + const credentialsIndexes = { + index: credentialStatus.statusListIndex, + statusListCredentialURL: credentialStatus.statusListCredential, + id: offer.id, + } + await fetch(credentialStatus.statusListCredential, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ credentialsIndexes }), + }) } } - const offer = await this.agent.credentials.offerCredential(createOfferOptions) + offer = await this.agent.credentials.offerCredential(createOfferOptions) return offer } catch (error) { throw ErrorHandlingService.handle(error) } } - private async getCredentialStatus(createOfferOptions: CreateOfferOptions) { + private async getCredentialStatus(createOfferOptions: CreateOfferOptions): Promise { try { - const bitStringStatusListURL = fs.readFileSync('config.json', 'utf-8') - const configJson = JSON.parse(bitStringStatusListURL) - - if (!configJson.bitStringStatusListURL) { - throw new Error('Please provide valid bitStringStatusList server URL') + if (!createOfferOptions.credentialSubjectUrl || !createOfferOptions.statusPurpose) { + throw new BadRequestError(`Please provide valid credentialSubjectUrl and statusPurpose`) + } + const url = createOfferOptions.credentialSubjectUrl + const validateUrl = await this.isValidUrl(url) + if (!validateUrl) { + throw new BadRequestError(`Please provide a valid credentialSubjectUrl`) } - const bitStringStatusListCredential = await fetch(configJson.bitStringStatusListURL, { + const bitStringStatusListCredential = await fetch(url, { method: 'GET', headers: { 'Content-Type': 'application/json', @@ -194,24 +212,64 @@ export class CredentialController extends Controller { }) if (!bitStringStatusListCredential.ok) { - throw new Error(`HTTP error! Status: ${bitStringStatusListCredential.status}`) + throw new InternalServerError(`${bitStringStatusListCredential.statusText}`) } const bitStringCredential = (await bitStringStatusListCredential.json()) as BitStringCredential + + if (!bitStringCredential?.credential && !bitStringCredential?.credential?.credentialSubject) { + throw new BadRequestError(`Invalid credentialSubjectUrl`) + } + + if (bitStringCredential?.credential?.credentialSubject?.statusPurpose !== createOfferOptions?.statusPurpose) { + throw new BadRequestError( + `Invalid statusPurpose! Please provide valid statusPurpose. '${createOfferOptions.statusPurpose}'` + ) + } + const encodedBitString = bitStringCredential.credential.credentialSubject.encodedList const gunzip = promisify(zlib.gunzip) const compressedBuffer = Buffer.from(encodedBitString, 'base64') const decompressedBuffer = await gunzip(compressedBuffer) const decodedBitString = decompressedBuffer.toString('binary') - const index = decodedBitString.indexOf('0') + // const getIndex = await this.agent.genericRecords.findAllByQuery({ + // statusListCredentialURL: createOfferOptions.credentialSubjectUrl, + // }) + const segments = createOfferOptions.credentialSubjectUrl.split('/') + const lastSegment = segments[segments.length - 1] + const getIndexesList = await fetch(`${BIT_STRING_STATUS_INDEX_URL}/${lastSegment}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }) + + let index + const arrayIndex: number[] = [] + if (getIndexesList.status === 404) { + index = decodedBitString.indexOf('0') + } else { + const getIndex = (await getIndexesList.json()) as IndexRecord[] + getIndex.find((record) => { + arrayIndex.push(Number(record.content.index)) + }) + + index = await this.getAvailableIndex(decodedBitString, arrayIndex) + } + + if (index === -1) { + throw new ConflictError( + `The provided bit string credential revocation list for ${createOfferOptions.credentialSubjectUrl} has been exhausted. Please supply a valid credentialSubjectUrl.` + ) + } const credentialStatus = { - id: `${configJson.bitStringStatusListURL}#${index}`, + id: `${createOfferOptions.credentialSubjectUrl}#${index}`, type: 'BitstringStatusListEntry', statusPurpose: createOfferOptions.statusPurpose, statusListIndex: index.toString(), - statusListCredential: configJson.bitStringStatusListURL, + statusListCredential: createOfferOptions.credentialSubjectUrl, } as unknown as CredentialStatus return credentialStatus @@ -220,6 +278,35 @@ export class CredentialController extends Controller { } } + private async getAvailableIndex(str: string, usedIndices: number[]) { + // Find all indices of the character '0' + const indices = [] + for (let i = 0; i < str.length; i++) { + if (str[i] === '0') { + indices.push(i) + } + } + + // Find the first available index that is not in the usedIndices array + for (const index of indices) { + if (!usedIndices.includes(index)) { + return index + } + } + + // If no available index is found, return -1 or any indication of 'not found' + return -1 + } + + private async isValidUrl(url: string) { + try { + new URL(url) + return true + } catch (err) { + return false + } + } + @Post('/create-offer-oob') public async createOfferOob(@Body() outOfBandOption: CreateOfferOobOptions) { try { diff --git a/src/controllers/types.ts b/src/controllers/types.ts index c95c62b4..522b181e 100644 --- a/src/controllers/types.ts +++ b/src/controllers/types.ts @@ -92,13 +92,14 @@ export interface AcceptCredentialProposalOptions { export type CredentialStatusType = 'BitstringStatusListEntry' export interface CreateOfferOptions { - isRevocable: boolean protocolVersion: ProtocolVersion connectionId: RecordId credentialFormats: CredentialFormatPayload autoAcceptCredential?: AutoAcceptCredential comment?: string - statusPurpose: CredentialStatusPurpose + statusPurpose?: CredentialStatusPurpose + credentialSubjectUrl?: string + isRevocable?: boolean } type CredentialFormatType = LegacyIndyCredentialFormat | JsonLdCredentialFormat | AnonCredsCredentialFormat @@ -413,7 +414,16 @@ export interface SignCredentialPayload { export interface BitStringCredential { credential: { credentialSubject: { + id: string + type: string encodedList: string + statusPurpose: string } } } + +export interface IndexRecord { + content: { + index: string + } +} diff --git a/src/controllers/w3cRevocation/w3cRevocationController.ts b/src/controllers/w3cRevocation/w3cRevocationController.ts index d1f5166b..c3ff6a3b 100644 --- a/src/controllers/w3cRevocation/w3cRevocationController.ts +++ b/src/controllers/w3cRevocation/w3cRevocationController.ts @@ -1,14 +1,20 @@ import type { RestAgentModules } from '../../cliAgent' import type { BitStringCredential } from '../types' -import type { W3cCredentialRecord, W3cJsonLdSignCredentialOptions } from '@credo-ts/core' +import type { AnonCredsCredentialFormat, LegacyIndyCredentialFormat } from '@credo-ts/anoncreds' +import type { + GetCredentialFormatDataReturn, + JsonLdCredentialFormat, + W3cCredentialRecord, + W3cJsonLdSignCredentialOptions, +} from '@credo-ts/core' import { Agent, ClaimFormat } from '@credo-ts/core' -import axios from 'axios' import { injectable } from 'tsyringe' import { promisify } from 'util' import * as zlib from 'zlib' import ErrorHandlingService from '../../errorHandlingService' +import { BadRequestError, InternalServerError } from '../../errors/errors' import { SignCredentialPayload } from '../types' import { Tags, Route, Controller, Post, Security, Body, Path } from 'tsoa' @@ -75,15 +81,13 @@ export class StatusController extends Controller { proofType: 'Ed25519Signature2018', } - await axios.post( - id, - { credentialsData: data }, - { - headers: { - 'Content-Type': 'application/json', - }, - } - ) + await fetch(id, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ credentialsData: data }), + }) const signCredential = await this.agent.w3cCredentials.signCredential( data as unknown as W3cJsonLdSignCredentialOptions @@ -98,18 +102,24 @@ export class StatusController extends Controller { } @Post('/revoke-credential/:id') - public async revokeW3C(@Path('id') id: string): Promise { + public async revokeW3C( + @Path('id') id: string + ): Promise< + GetCredentialFormatDataReturn<(LegacyIndyCredentialFormat | JsonLdCredentialFormat | AnonCredsCredentialFormat)[]> + > { try { const credential = await this.agent.credentials.getFormatData(id) let credentialIndex let statusListCredentialURL + const revocationStatus = 1 - if (!Array.isArray(credential.credential?.jsonld?.credentialStatus)) { - credentialIndex = credential.credential?.jsonld?.credentialStatus?.statusListIndex as string - statusListCredentialURL = credential.credential?.jsonld?.credentialStatus?.statusListCredential as string + if (!Array.isArray(credential.offer?.jsonld?.credential?.credentialStatus)) { + credentialIndex = credential.offer?.jsonld?.credential?.credentialStatus?.statusListIndex as string + statusListCredentialURL = credential.offer?.jsonld?.credential?.credentialStatus?.statusListCredential as string } else { - credentialIndex = credential.credential?.jsonld?.credentialStatus[0].statusListIndex as string - statusListCredentialURL = credential.credential?.jsonld?.credentialStatus[0].statusListCredential as string + credentialIndex = credential.offer?.jsonld?.credential?.credentialStatus[0].statusListIndex as string + statusListCredentialURL = credential.offer?.jsonld?.credential?.credentialStatus[0] + .statusListCredential as string } const bitStringStatusListCredential = await fetch(statusListCredentialURL, { @@ -120,7 +130,7 @@ export class StatusController extends Controller { }) if (!bitStringStatusListCredential.ok) { - throw new Error(`HTTP error! Status: ${bitStringStatusListCredential.status}`) + throw new InternalServerError(`${bitStringStatusListCredential.statusText}`) } const bitStringCredential = (await bitStringStatusListCredential.json()) as BitStringCredential @@ -128,26 +138,26 @@ export class StatusController extends Controller { const decodeBitString = await this.decodeBitSting(encodedBitString) const findBitStringIndex = decodeBitString.charAt(parseInt(credentialIndex)) - if (findBitStringIndex === '1') { - throw new Error('The credential already revoked') + if (findBitStringIndex === revocationStatus.toString()) { + throw new BadRequestError('The credential already revoked') } const updateBitString = - decodeBitString.slice(0, parseInt(credentialIndex)) + 1 + decodeBitString.slice(parseInt(credentialIndex) + 1) + decodeBitString.slice(0, parseInt(credentialIndex)) + + revocationStatus + + decodeBitString.slice(parseInt(credentialIndex) + 1) const encodeUpdatedBitString = await this.encodeBitString(updateBitString) bitStringCredential.credential.credentialSubject.encodedList = encodeUpdatedBitString - await axios.post( - statusListCredentialURL, - { credentialsData: bitStringCredential }, - { - headers: { - 'Content-Type': 'application/json', - }, - } - ) + await fetch(statusListCredentialURL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ credentialsData: bitStringCredential }), + }) - return `Credential revoke successfully` + return credential } catch (error) { throw ErrorHandlingService.handle(error) } diff --git a/src/utils/util.ts b/src/utils/util.ts index 7051fcc5..0d141ac3 100644 --- a/src/utils/util.ts +++ b/src/utils/util.ts @@ -169,3 +169,4 @@ export const CONNECT_TIMEOUT = 10 export const MAX_CONNECTIONS = 1000 export const IDLE_TIMEOUT = 30000 export const LOG_LEVEL = 2 +export const BIT_STRING_STATUS_INDEX_URL = 'http://192.168.1.125:5005/credentials/indexes' From a67acf4055d5e688726aa389b4c829a5db9529c6 Mon Sep 17 00:00:00 2001 From: KulkarniShashank Date: Tue, 13 Aug 2024 19:49:43 +0530 Subject: [PATCH 07/15] feat: implemented w3c revocation with verification Signed-off-by: KulkarniShashank --- .../credentials/CredentialController.ts | 274 +++++++----------- .../multi-tenancy/MultiTenancyController.ts | 119 ++++++-- src/controllers/types.ts | 15 +- .../w3cRevocation/w3cRevocationController.ts | 173 ++++------- src/enums/enum.ts | 6 + src/utils/credentialStatusList.ts | 116 ++++++++ src/utils/util.ts | 1 - 7 files changed, 392 insertions(+), 312 deletions(-) create mode 100644 src/utils/credentialStatusList.ts diff --git a/src/controllers/credentials/CredentialController.ts b/src/controllers/credentials/CredentialController.ts index b440d6fd..3bb33b9f 100644 --- a/src/controllers/credentials/CredentialController.ts +++ b/src/controllers/credentials/CredentialController.ts @@ -1,20 +1,19 @@ import type { RestAgentModules } from '../../cliAgent' -import type { BitStringCredential, IndexRecord } from '../types' +import type { CredentialStatusList } from '../types' import type { + AgentMessage, CredentialExchangeRecordProps, CredentialProtocolVersionType, - CredentialStatus, Routing, } from '@credo-ts/core' import { CredentialState, Agent, W3cCredentialService, Key, KeyType, CredentialRole } from '@credo-ts/core' import { injectable } from 'tsyringe' -import { promisify } from 'util' -import * as zlib from 'zlib' +import { BitStringCredentialStatusPurpose } from '../../enums/enum' import ErrorHandlingService from '../../errorHandlingService' -import { BadRequestError, ConflictError, InternalServerError } from '../../errors/errors' -import { BIT_STRING_STATUS_INDEX_URL } from '../../utils/util' +import { BadRequestError } from '../../errors/errors' +import getCredentialStatus from '../../utils/credentialStatusList' import { CredentialExchangeRecordExample, RecordId } from '../examples' import { OutOfBandController } from '../outofband/OutOfBandController' import { @@ -165,196 +164,145 @@ export class CredentialController extends Controller { @Post('/create-offer') public async createOffer(@Body() createOfferOptions: CreateOfferOptions) { try { - let offer - if (createOfferOptions.credentialFormats.jsonld) { - if (createOfferOptions.isRevocable) { - const credentialStatus = await this.getCredentialStatus(createOfferOptions) - createOfferOptions.credentialFormats.jsonld.credential.credentialStatus = credentialStatus - offer = await this.agent.credentials.offerCredential(createOfferOptions) - - const credentialsIndexes = { - index: credentialStatus.statusListIndex, - statusListCredentialURL: credentialStatus.statusListCredential, - id: offer.id, - } - await fetch(credentialStatus.statusListCredential, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ credentialsIndexes }), - }) - } - } - offer = await this.agent.credentials.offerCredential(createOfferOptions) - return offer - } catch (error) { - throw ErrorHandlingService.handle(error) - } - } - - private async getCredentialStatus(createOfferOptions: CreateOfferOptions): Promise { - try { - if (!createOfferOptions.credentialSubjectUrl || !createOfferOptions.statusPurpose) { - throw new BadRequestError(`Please provide valid credentialSubjectUrl and statusPurpose`) - } - const url = createOfferOptions.credentialSubjectUrl - const validateUrl = await this.isValidUrl(url) - if (!validateUrl) { - throw new BadRequestError(`Please provide a valid credentialSubjectUrl`) - } - - const bitStringStatusListCredential = await fetch(url, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }) + const { credentialFormats, isRevocable, credentialSubjectUrl, statusPurpose } = createOfferOptions - if (!bitStringStatusListCredential.ok) { - throw new InternalServerError(`${bitStringStatusListCredential.statusText}`) + if (!credentialFormats.jsonld) { + return await this.agent.credentials.offerCredential(createOfferOptions) } - const bitStringCredential = (await bitStringStatusListCredential.json()) as BitStringCredential + if (isRevocable) { + if (!credentialSubjectUrl) { + throw new BadRequestError('Please provide valid credentialSubjectUrl') + } - if (!bitStringCredential?.credential && !bitStringCredential?.credential?.credentialSubject) { - throw new BadRequestError(`Invalid credentialSubjectUrl`) - } + const bitStringCredentialStatusPurpose = statusPurpose ?? BitStringCredentialStatusPurpose.REVOCATION + const credentialStatusData = { + credentialSubjectUrl, + statusPurpose: bitStringCredentialStatusPurpose, + } as unknown as CredentialStatusList + const getIndex = await this.agent.genericRecords.findAllByQuery({ + statusListCredentialURL: credentialSubjectUrl, + }) - if (bitStringCredential?.credential?.credentialSubject?.statusPurpose !== createOfferOptions?.statusPurpose) { - throw new BadRequestError( - `Invalid statusPurpose! Please provide valid statusPurpose. '${createOfferOptions.statusPurpose}'` - ) - } + const credentialStatus = await getCredentialStatus(credentialStatusData, getIndex) + credentialFormats.jsonld.credential.credentialStatus = credentialStatus - const encodedBitString = bitStringCredential.credential.credentialSubject.encodedList - const gunzip = promisify(zlib.gunzip) - - const compressedBuffer = Buffer.from(encodedBitString, 'base64') - const decompressedBuffer = await gunzip(compressedBuffer) - const decodedBitString = decompressedBuffer.toString('binary') - // const getIndex = await this.agent.genericRecords.findAllByQuery({ - // statusListCredentialURL: createOfferOptions.credentialSubjectUrl, - // }) - const segments = createOfferOptions.credentialSubjectUrl.split('/') - const lastSegment = segments[segments.length - 1] - const getIndexesList = await fetch(`${BIT_STRING_STATUS_INDEX_URL}/${lastSegment}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }) + const offer = await this.agent.credentials.offerCredential(createOfferOptions) - let index - const arrayIndex: number[] = [] - if (getIndexesList.status === 404) { - index = decodedBitString.indexOf('0') - } else { - const getIndex = (await getIndexesList.json()) as IndexRecord[] - getIndex.find((record) => { - arrayIndex.push(Number(record.content.index)) + await this.agent.genericRecords.save({ + content: { index: credentialStatus.statusListIndex }, + tags: { statusListCredentialURL: credentialStatus.statusListCredential }, + id: offer.id, }) - index = await this.getAvailableIndex(decodedBitString, arrayIndex) - } - - if (index === -1) { - throw new ConflictError( - `The provided bit string credential revocation list for ${createOfferOptions.credentialSubjectUrl} has been exhausted. Please supply a valid credentialSubjectUrl.` - ) + return offer } - const credentialStatus = { - id: `${createOfferOptions.credentialSubjectUrl}#${index}`, - type: 'BitstringStatusListEntry', - statusPurpose: createOfferOptions.statusPurpose, - statusListIndex: index.toString(), - statusListCredential: createOfferOptions.credentialSubjectUrl, - } as unknown as CredentialStatus - - return credentialStatus + return await this.agent.credentials.offerCredential(createOfferOptions) } catch (error) { throw ErrorHandlingService.handle(error) } } - private async getAvailableIndex(str: string, usedIndices: number[]) { - // Find all indices of the character '0' - const indices = [] - for (let i = 0; i < str.length; i++) { - if (str[i] === '0') { - indices.push(i) - } - } - - // Find the first available index that is not in the usedIndices array - for (const index of indices) { - if (!usedIndices.includes(index)) { - return index - } - } - - // If no available index is found, return -1 or any indication of 'not found' - return -1 - } - - private async isValidUrl(url: string) { - try { - new URL(url) - return true - } catch (err) { - return false - } - } - @Post('/create-offer-oob') public async createOfferOob(@Body() outOfBandOption: CreateOfferOobOptions) { try { - let routing: Routing + const { + recipientKey, + credentialFormats, + isRevocable, + credentialSubjectUrl, + statusPurpose, + protocolVersion, + autoAcceptCredential, + comment, + } = outOfBandOption + const linkSecretIds = await this.agent.modules.anoncreds.getLinkSecretIds() if (linkSecretIds.length === 0) { await this.agent.modules.anoncreds.createLinkSecret() } - if (outOfBandOption?.recipientKey) { - routing = { - endpoints: this.agent.config.endpoints, - routingKeys: [], - recipientKey: Key.fromPublicKeyBase58(outOfBandOption.recipientKey, KeyType.Ed25519), - mediatorId: undefined, + + const routing = recipientKey + ? { + endpoints: this.agent.config.endpoints, + routingKeys: [], + recipientKey: Key.fromPublicKeyBase58(recipientKey, KeyType.Ed25519), + mediatorId: undefined, + } + : await this.agent.mediationRecipient.getRouting({}) + + if (credentialFormats.jsonld && isRevocable) { + if (!credentialSubjectUrl) { + throw new BadRequestError('Please provide valid credentialSubjectUrl') } - } else { - routing = await this.agent.mediationRecipient.getRouting({}) + + const bitStringCredentialStatusPurpose = statusPurpose ?? BitStringCredentialStatusPurpose.REVOCATION + + const credentialStatusData = { + credentialSubjectUrl, + statusPurpose: bitStringCredentialStatusPurpose, + } as unknown as CredentialStatusList + const getIndex = await this.agent.genericRecords.findAllByQuery({ + statusListCredentialURL: credentialSubjectUrl, + }) + const credentialStatus = await getCredentialStatus(credentialStatusData, getIndex) + credentialFormats.jsonld.credential.credentialStatus = credentialStatus + + const offerOob = await this.agent.credentials.createOffer({ + protocolVersion: protocolVersion as CredentialProtocolVersionType<[]>, + credentialFormats, + autoAcceptCredential, + comment, + }) + + await this.agent.genericRecords.save({ + content: { index: credentialStatus.statusListIndex }, + tags: { statusListCredentialURL: credentialStatus.statusListCredential }, + id: offerOob.credentialRecord.id, + }) + + return this.createOutOfBandInvitation(outOfBandOption, routing, offerOob.message) } + const offerOob = await this.agent.credentials.createOffer({ - protocolVersion: outOfBandOption.protocolVersion as CredentialProtocolVersionType<[]>, - credentialFormats: outOfBandOption.credentialFormats, - autoAcceptCredential: outOfBandOption.autoAcceptCredential, - comment: outOfBandOption.comment, + protocolVersion: protocolVersion as CredentialProtocolVersionType<[]>, + credentialFormats, + autoAcceptCredential, + comment, }) - const credentialMessage = offerOob.message - const outOfBandRecord = await this.agent.oob.createInvitation({ - label: outOfBandOption.label, - messages: [credentialMessage], - autoAcceptConnection: true, - imageUrl: outOfBandOption?.imageUrl, - routing, - }) - return { - invitationUrl: outOfBandRecord.outOfBandInvitation.toUrl({ - domain: this.agent.config.endpoints[0], - }), - invitation: outOfBandRecord.outOfBandInvitation.toJSON({ - useDidSovPrefixWhereAllowed: this.agent.config.useDidSovPrefixWhereAllowed, - }), - outOfBandRecord: outOfBandRecord.toJSON(), - recipientKey: outOfBandOption?.recipientKey ? {} : { recipientKey: routing.recipientKey.publicKeyBase58 }, - } + return this.createOutOfBandInvitation(outOfBandOption, routing, offerOob.message) } catch (error) { throw ErrorHandlingService.handle(error) } } + private async createOutOfBandInvitation( + outOfBandOption: CreateOfferOobOptions, + routing: Routing, + credentialMessage: AgentMessage + ) { + const outOfBandRecord = await this.agent.oob.createInvitation({ + label: outOfBandOption.label, + messages: [credentialMessage], + autoAcceptConnection: true, + imageUrl: outOfBandOption?.imageUrl, + routing, + }) + + return { + invitationUrl: outOfBandRecord.outOfBandInvitation.toUrl({ + domain: this.agent.config.endpoints[0], + }), + invitation: outOfBandRecord.outOfBandInvitation.toJSON({ + useDidSovPrefixWhereAllowed: this.agent.config.useDidSovPrefixWhereAllowed, + }), + outOfBandRecord: outOfBandRecord.toJSON(), + recipientKey: outOfBandOption?.recipientKey ? {} : { recipientKey: routing.recipientKey.publicKeyBase58 }, + } + } + /** * Accept a credential offer as holder by sending an accept offer message * to the connection associated with the credential exchange record. diff --git a/src/controllers/multi-tenancy/MultiTenancyController.ts b/src/controllers/multi-tenancy/MultiTenancyController.ts index d2271405..ce9fd991 100644 --- a/src/controllers/multi-tenancy/MultiTenancyController.ts +++ b/src/controllers/multi-tenancy/MultiTenancyController.ts @@ -1,6 +1,6 @@ import type { RestAgentModules, RestMultiTenantAgentModules } from '../../cliAgent' import type { Version } from '../examples' -import type { RecipientKeyOption, SchemaMetadata } from '../types' +import type { CredentialStatusList, RecipientKeyOption, SchemaMetadata } from '../types' import type { PolygonDidCreateOptions } from '@ayanworks/credo-polygon-w3c-module/build/dids' import type { AcceptProofRequestOptions, @@ -37,7 +37,6 @@ import { Key, KeyType, OutOfBandInvitation, - RecordNotFoundError, TypedArrayEncoder, getBls12381G2Key2020, getEd25519VerificationKey2018, @@ -49,7 +48,16 @@ import { QuestionAnswerRole, QuestionAnswerState } from '@credo-ts/question-answ import axios from 'axios' import * as fs from 'fs' -import { CredentialEnum, DidMethod, EndorserMode, Network, NetworkTypes, Role, SchemaError } from '../../enums/enum' +import { + BitStringCredentialStatusPurpose, + CredentialEnum, + DidMethod, + EndorserMode, + Network, + NetworkTypes, + Role, + SchemaError, +} from '../../enums/enum' import ErrorHandlingService from '../../errorHandlingService' import { ENDORSER_DID_NOT_PRESENT } from '../../errorMessages' import { @@ -59,6 +67,7 @@ import { PaymentRequiredError, UnprocessableEntityError, } from '../../errors' +import getCredentialStatus from '../../utils/credentialStatusList' import { BCOVRIN_REGISTER_URL, INDICIO_NYM_URL } from '../../utils/util' import { SchemaId, CredentialDefinitionId, RecordId, ProofRecordExample, ConnectionRecordExample } from '../examples' import { @@ -76,22 +85,7 @@ import { CreateSchemaInput, } from '../types' -import { - Body, - Controller, - Delete, - Get, - Post, - Query, - Res, - Route, - Tags, - TsoaResponse, - Path, - Example, - Security, - Response, -} from 'tsoa' +import { Body, Controller, Delete, Get, Post, Query, Route, Tags, Path, Example, Security, Response } from 'tsoa' @Tags('MultiTenancy') @Route('/multi-tenancy') @@ -1279,12 +1273,41 @@ export class MultiTenancyController extends Controller { let offer try { await this.agent.modules.tenants.withTenantAgent({ tenantId }, async (tenantAgent) => { - offer = await tenantAgent.credentials.offerCredential({ - connectionId: createOfferOptions.connectionId, - protocolVersion: createOfferOptions.protocolVersion as CredentialProtocolVersionType<[]>, - credentialFormats: createOfferOptions.credentialFormats, - autoAcceptCredential: createOfferOptions.autoAcceptCredential, - }) + const { credentialFormats, isRevocable, credentialSubjectUrl, statusPurpose } = createOfferOptions + + if (!credentialFormats.jsonld) { + offer = await this.agent.credentials.offerCredential(createOfferOptions) + return + } + + if (isRevocable) { + if (!credentialSubjectUrl) { + throw new BadRequestError('Please provide valid credentialSubjectUrl') + } + + const bitStringCredentialStatusPurpose = statusPurpose ?? BitStringCredentialStatusPurpose.REVOCATION + const credentialStatusData = { + credentialSubjectUrl, + statusPurpose: bitStringCredentialStatusPurpose, + } as unknown as CredentialStatusList + + const getIndex = await tenantAgent.genericRecords.findAllByQuery({ + statusListCredentialURL: credentialSubjectUrl, + }) + + const credentialStatus = await getCredentialStatus(credentialStatusData, getIndex) + credentialFormats.jsonld.credential.credentialStatus = credentialStatus + + offer = await tenantAgent.credentials.offerCredential(createOfferOptions) + + await tenantAgent.genericRecords.save({ + content: { index: credentialStatus.statusListIndex }, + tags: { statusListCredentialURL: credentialStatus.statusListCredential }, + id: offer.id, + }) + } else { + offer = await tenantAgent.credentials.offerCredential(createOfferOptions) + } }) return offer @@ -1297,7 +1320,7 @@ export class MultiTenancyController extends Controller { @Post('/credentials/create-offer-oob/:tenantId') public async createOfferOob(@Path('tenantId') tenantId: string, @Body() createOfferOptions: CreateOfferOobOptions) { let createOfferOobRecord - + let offerOob try { let invitationDid: string | undefined await this.agent.modules.tenants.withTenantAgent({ tenantId }, async (tenantAgent) => { @@ -1328,12 +1351,44 @@ export class MultiTenancyController extends Controller { invitationDid = did.didState.did } - const offerOob = await tenantAgent.credentials.createOffer({ - protocolVersion: createOfferOptions.protocolVersion as CredentialProtocolVersionType<[]>, - credentialFormats: createOfferOptions.credentialFormats, - autoAcceptCredential: createOfferOptions.autoAcceptCredential, - comment: createOfferOptions.comment, - }) + if (createOfferOptions.credentialFormats.jsonld && createOfferOptions.isRevocable) { + if (!createOfferOptions.credentialSubjectUrl) { + throw new BadRequestError('Please provide valid credentialSubjectUrl') + } + + const bitStringCredentialStatusPurpose = + createOfferOptions.statusPurpose ?? BitStringCredentialStatusPurpose.REVOCATION + + const credentialStatusData = { + credentialSubjectUrl: createOfferOptions.credentialSubjectUrl, + statusPurpose: bitStringCredentialStatusPurpose, + } as unknown as CredentialStatusList + const getIndex = await this.agent.genericRecords.findAllByQuery({ + statusListCredentialURL: createOfferOptions.credentialSubjectUrl, + }) + const credentialStatus = await getCredentialStatus(credentialStatusData, getIndex) + createOfferOptions.credentialFormats.jsonld.credential.credentialStatus = credentialStatus + + offerOob = await tenantAgent.credentials.createOffer({ + protocolVersion: createOfferOptions.protocolVersion as CredentialProtocolVersionType<[]>, + credentialFormats: createOfferOptions.credentialFormats, + autoAcceptCredential: createOfferOptions.autoAcceptCredential, + comment: createOfferOptions.comment, + }) + + await this.agent.genericRecords.save({ + content: { index: credentialStatus.statusListIndex }, + tags: { statusListCredentialURL: credentialStatus.statusListCredential }, + id: offerOob.credentialRecord.id, + }) + } else { + offerOob = await tenantAgent.credentials.createOffer({ + protocolVersion: createOfferOptions.protocolVersion as CredentialProtocolVersionType<[]>, + credentialFormats: createOfferOptions.credentialFormats, + autoAcceptCredential: createOfferOptions.autoAcceptCredential, + comment: createOfferOptions.comment, + }) + } const credentialMessage = offerOob.message const outOfBandRecord = await tenantAgent.oob.createInvitation({ diff --git a/src/controllers/types.ts b/src/controllers/types.ts index 522b181e..c3e2fbda 100644 --- a/src/controllers/types.ts +++ b/src/controllers/types.ts @@ -1,5 +1,5 @@ import type { RecordId, Version } from './examples' -import type { CustomHandshakeProtocol } from '../enums/enum' +import type { BitStringCredentialStatusPurpose, CustomHandshakeProtocol } from '../enums/enum' import type { AnonCredsCredentialFormat, LegacyIndyCredentialFormat } from '@credo-ts/anoncreds' import type { AutoAcceptCredential, @@ -25,7 +25,6 @@ import type { Attachment, KeyType, JsonLdCredentialFormat, - CredentialStatusPurpose, } from '@credo-ts/core' import type { DIDDocument } from 'did-resolver' @@ -97,7 +96,7 @@ export interface CreateOfferOptions { credentialFormats: CredentialFormatPayload autoAcceptCredential?: AutoAcceptCredential comment?: string - statusPurpose?: CredentialStatusPurpose + statusPurpose?: BitStringCredentialStatusPurpose credentialSubjectUrl?: string isRevocable?: boolean } @@ -116,6 +115,9 @@ export interface CreateOfferOobOptions { imageUrl?: string recipientKey?: string invitationDid?: string + isRevocable?: boolean + credentialSubjectUrl?: string + statusPurpose?: string } export interface CredentialCreateOfferOptions { credentialRecord: CredentialExchangeRecord @@ -408,7 +410,7 @@ export interface SignCredentialPayload { id: string issuerId: string statusPurpose: string - bitStringLength: number + bitStringLength?: number } export interface BitStringCredential { @@ -427,3 +429,8 @@ export interface IndexRecord { index: string } } + +export interface CredentialStatusList { + credentialSubjectUrl: string + statusPurpose: BitStringCredentialStatusPurpose +} diff --git a/src/controllers/w3cRevocation/w3cRevocationController.ts b/src/controllers/w3cRevocation/w3cRevocationController.ts index c3ff6a3b..2511f573 100644 --- a/src/controllers/w3cRevocation/w3cRevocationController.ts +++ b/src/controllers/w3cRevocation/w3cRevocationController.ts @@ -1,12 +1,7 @@ import type { RestAgentModules } from '../../cliAgent' import type { BitStringCredential } from '../types' -import type { AnonCredsCredentialFormat, LegacyIndyCredentialFormat } from '@credo-ts/anoncreds' -import type { - GetCredentialFormatDataReturn, - JsonLdCredentialFormat, - W3cCredentialRecord, - W3cJsonLdSignCredentialOptions, -} from '@credo-ts/core' +import type { W3cCredentialRecord, W3cJsonLdSignCredentialOptions } from '@credo-ts/core' +import type { GenericRecord } from '@credo-ts/core/build/modules/generic-records/repository/GenericRecord' import { Agent, ClaimFormat } from '@credo-ts/core' import { injectable } from 'tsyringe' @@ -17,7 +12,7 @@ import ErrorHandlingService from '../../errorHandlingService' import { BadRequestError, InternalServerError } from '../../errors/errors' import { SignCredentialPayload } from '../types' -import { Tags, Route, Controller, Post, Security, Body, Path } from 'tsoa' +import { Tags, Route, Controller, Post, Security, Body, Path, Get } from 'tsoa' @Tags('Status') @Route('/w3c/revocation') @@ -51,13 +46,14 @@ export class StatusController extends Controller { return decompressedBuffer.toString('binary') } - @Post('/sign-credential') + @Post('/sign/bitstring-credential') public async createBitstringStatusListCredential( @Body() signCredentialPayload: SignCredentialPayload ): Promise { try { const { id, issuerId, statusPurpose, bitStringLength } = signCredentialPayload - const bitStringStatus = await this.generateBitStringStatus(bitStringLength) + const bitStringStatusListCredentialListLength = bitStringLength ? bitStringLength : 131072 + const bitStringStatus = await this.generateBitStringStatus(bitStringStatusListCredentialListLength) const encodedList = await this.encodeBitString(bitStringStatus) const didIdentifier = issuerId.split(':')[2] const data = { @@ -102,11 +98,9 @@ export class StatusController extends Controller { } @Post('/revoke-credential/:id') - public async revokeW3C( - @Path('id') id: string - ): Promise< - GetCredentialFormatDataReturn<(LegacyIndyCredentialFormat | JsonLdCredentialFormat | AnonCredsCredentialFormat)[]> - > { + public async revokeW3C(@Path('id') id: string): Promise<{ + message: string + }> { try { const credential = await this.agent.credentials.getFormatData(id) let credentialIndex @@ -157,113 +151,68 @@ export class StatusController extends Controller { body: JSON.stringify({ credentialsData: bitStringCredential }), }) - return credential + return { message: 'The credential has been revoked' } } catch (error) { throw ErrorHandlingService.handle(error) } } - // /** - // * Create Status List Credential - // */ - // // Creates a new StatusListCredential that can be used for revocation - // @Security('apiKey') - // @Post('/createStatusListCredential/:statusId') - // // accepts size, minimum 131,072 - // public async createStatusListCredential(@Path('statusId') statusId: string) { - // // Maintain an incremental index for statusListCredential - // // Add Id with agentEndpoint/status/number - // // Note: This endpoint should actually be an API to get StatusListCredential with id(as path param) - // const agentEndpoints = await this.agent.config - // const list = await this.createBitStringStatusList() - // const configFileData = fs.readFileSync('config.json', 'utf-8') - // const config = JSON.parse(configFileData) - // const statusListCredentialId = `yourIpAndPort:${config.port}/status/${statusId}` - // const listCred = await this._createStatusListCredential(statusListCredentialId, list) - // return listCred - // } + @Get('bitstring/status-list/:id') + public async getBitStringStatusListById(@Path('id') id: string): Promise<{ + bitStringCredential: BitStringCredential + getIndex: GenericRecord[] + }> { + try { + const validateUrl = await this.isValidUrl(id) + if (!validateUrl) { + throw new BadRequestError(`Please provide a bit string credential id`) + } - // /** - // * Create Entry for status list credential - // */ - // // Create a new revocable credential - // // But do we even need this additional endpoint? - // @Security('apiKey') - // @Post('/signAndStoreStausListCredential') - // // public async createEntryForStatusListCredential') - // public async createEntryForStatusListCredential(@Body() credentialPayload: unknown) { - // const storedCredential = await this.storeSighnedCredential() - // return storedCredential - // } + const bitStringCredentialDetails = await fetch(id, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }) - // /** - // * Retrieve status of a credential - // */ - // // Return if the status is revoked or not - // @Security('apiKey') - // @Get('/credential/:credentialRecordId') - // public async getCredentialStatus(@Path('credentialRecordId') credentialRecordId: RecordId) { - // return `success retrieveing credentialRecordId ${credentialRecordId}` - // } + if (!bitStringCredentialDetails.ok) { + throw new InternalServerError(`${bitStringCredentialDetails.statusText}`) + } - // /** - // * Change status of an entry in a StatusListCredential - // */ - // // Can this be a PUT operation? - // @Security('apiKey') - // @Post('/changeCredentialStatus') - // public async changeCredentialStatus() { - // return 'success' - // } + const bitStringCredential = (await bitStringCredentialDetails.json()) as BitStringCredential + if (!bitStringCredential?.credential && !bitStringCredential?.credential?.credentialSubject) { + throw new BadRequestError(`Invalid credentialSubjectUrl`) + } - // /** - // * Retrieve statusListCredential according to their id - // */ - // // Get statusListCredential from the id passed - // @Get('/:id') - // public async getStatusListCredential(@Path('id') id: string) { - // return `success with id: ${id}` - // } + const getIndex = await this.agent.genericRecords.findAllByQuery({ + statusListCredentialURL: id, + }) - // private async createBitStringStatusList() { - // this.statusList = await loadStatusList() - // this.list = await this.statusList.createList({ length: 100000 }) - // return this.list - // } + return { + bitStringCredential, + getIndex, + } + } catch (error) { + throw ErrorHandlingService.handle(error) + } + } - // private async _createStatusListCredential(id: string, list: StatusList): Promise { - // return this.statusList.createCredential({ id: id, list: list, statusPurpose: 'suspension' }) - // } + @Get('bitstring/status-list/') + public async getAllBitStringStatusList(): Promise { + try { + const getBitStringCredentialStatusList = await this.agent.w3cCredentials.getAllCredentialRecords() + return getBitStringCredentialStatusList + } catch (error) { + throw ErrorHandlingService.handle(error) + } + } - // public async storeSighnedCredential() { - // const signedCred = { - // '@context': ['https://www.w3.org/2018/credentials/v1', 'https://w3id.org/vc/status-list/2021/v1'], - // id: 'http://yopurIp:yopurPort/status/1', - // type: ['VerifiableCredential', 'StatusList2021Credential'], - // issuer: { - // id: 'did:key:z6Mkty8b4M1arFSmxYVtM3nsoQvyFurHPhRxRms7vZ6cVZbN', - // }, - // issuanceDate: '2019-10-12T07:20:50.52Z', - // credentialSubject: { - // id: 'http://yopurIp:yopurPort/status/1#list', - // claims: { - // type: 'StatusList2021', - // encodedList: 'H4sIAAAAAAAAA-3BMQEAAADCoPVPbQsvoAAAAAAAAAAAAAAAAP4GcwM92tQwAAA', - // statusPurpose: 'suspension', - // }, - // }, - // proof: { - // verificationMethod: - // 'did:key:z6Mkty8b4M1arFSmxYVtM3nsoQvyFurHPhRxRms7vZ6cVZbN#z6Mkty8b4M1arFSmxYVtM3nsoQvyFurHPhRxRms7vZ6cVZbN', - // type: 'Ed25519Signature2018', - // created: '2024-07-08T12:24:04Z', - // proofPurpose: 'assertionMethod', - // jws: 'eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..hOr9nyr4dlQx1VOMgBow5AeLNrIQ1We0kvR1dFT0AQKkS_lIu-AruZpNVgVCMVlHiFrj-qFYr36YUTwTzUwiAw', - // }, - // } - // console.log('this is before storing') - // const storedCred = await this.agent.w3cCredentials.storeCredential(signedCred as unknown as StoreCredentialOptions) - // console.log('this is storedCred', storedCred) - // return storedCred - // } + private async isValidUrl(url: string) { + try { + new URL(url) + return true + } catch (err) { + return false + } + } } diff --git a/src/enums/enum.ts b/src/enums/enum.ts index 6468e3af..375ecd70 100644 --- a/src/enums/enum.ts +++ b/src/enums/enum.ts @@ -71,3 +71,9 @@ export declare enum CustomHandshakeProtocol { DidExchange = 'https://didcomm.org/didexchange/1.1', Connections = 'https://didcomm.org/connections/1.0', } + +export enum BitStringCredentialStatusPurpose { + REVOCATION = 'revocation', + SUSPENSION = 'suspension', + MESSAGE = 'message', +} diff --git a/src/utils/credentialStatusList.ts b/src/utils/credentialStatusList.ts new file mode 100644 index 00000000..ae2b1a3b --- /dev/null +++ b/src/utils/credentialStatusList.ts @@ -0,0 +1,116 @@ +import type { BitStringCredential, CredentialStatusList } from '../controllers/types' +import type { CredentialStatus } from '@credo-ts/core' +import type { GenericRecord } from '@credo-ts/core/build/modules/generic-records/repository/GenericRecord' + +import { promisify } from 'util' +import * as zlib from 'zlib' + +import ErrorHandlingService from '../errorHandlingService' +import { BadRequestError, ConflictError, InternalServerError } from '../errors/errors' + +async function getCredentialStatus( + credentialStatusList: CredentialStatusList, + getIndex: GenericRecord[] +): Promise { + try { + if (!credentialStatusList.credentialSubjectUrl || !credentialStatusList.statusPurpose) { + throw new BadRequestError(`Please provide valid credentialSubjectUrl and statusPurpose`) + } + const url = credentialStatusList.credentialSubjectUrl + const validateUrl = await isValidUrl(url) + if (!validateUrl) { + throw new BadRequestError(`Please provide a valid credentialSubjectUrl`) + } + + const bitStringStatusListCredential = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }) + + if (!bitStringStatusListCredential.ok) { + throw new InternalServerError(`${bitStringStatusListCredential.statusText}`) + } + + const bitStringCredential = (await bitStringStatusListCredential.json()) as BitStringCredential + + if (!bitStringCredential?.credential && !bitStringCredential?.credential?.credentialSubject) { + throw new BadRequestError(`Invalid credentialSubjectUrl`) + } + + if (bitStringCredential?.credential?.credentialSubject?.statusPurpose !== credentialStatusList?.statusPurpose) { + throw new BadRequestError( + `Invalid statusPurpose! Please provide valid statusPurpose. '${credentialStatusList.statusPurpose}'` + ) + } + + const encodedBitString = bitStringCredential.credential.credentialSubject.encodedList + const gunzip = promisify(zlib.gunzip) + + const compressedBuffer = Buffer.from(encodedBitString, 'base64') + const decompressedBuffer = await gunzip(compressedBuffer) + const decodedBitString = decompressedBuffer.toString('binary') + + let index + const arrayIndex: number[] = [] + if (getIndex.length === 0) { + index = decodedBitString.indexOf('0') + } else { + getIndex.find((record) => { + arrayIndex.push(Number(record.content.index)) + }) + + index = await getAvailableIndex(decodedBitString, arrayIndex) + } + + if (index === -1) { + throw new ConflictError( + `The provided bit string credential revocation list for ${credentialStatusList.credentialSubjectUrl} has been exhausted. Please supply a valid credentialSubjectUrl.` + ) + } + + const credentialStatus = { + id: `${credentialStatusList.credentialSubjectUrl}#${index}`, + type: 'BitstringStatusListEntry', + statusPurpose: credentialStatusList.statusPurpose, + statusListIndex: index.toString(), + statusListCredential: credentialStatusList.credentialSubjectUrl, + } as unknown as CredentialStatus + + return credentialStatus + } catch (error) { + throw ErrorHandlingService.handle(error) + } +} + +function getAvailableIndex(str: string, usedIndices: number[]) { + // Find all indices of the character '0' + const indices = [] + for (let i = 0; i < str.length; i++) { + if (str[i] === '0') { + indices.push(i) + } + } + + // Find the first available index that is not in the usedIndices array + for (const index of indices) { + if (!usedIndices.includes(index)) { + return index + } + } + + // If no available index is found, return -1 or any indication of 'not found' + return -1 +} + +function isValidUrl(url: string) { + try { + new URL(url) + return true + } catch (err) { + return false + } +} + +export default getCredentialStatus diff --git a/src/utils/util.ts b/src/utils/util.ts index 0d141ac3..7051fcc5 100644 --- a/src/utils/util.ts +++ b/src/utils/util.ts @@ -169,4 +169,3 @@ export const CONNECT_TIMEOUT = 10 export const MAX_CONNECTIONS = 1000 export const IDLE_TIMEOUT = 30000 export const LOG_LEVEL = 2 -export const BIT_STRING_STATUS_INDEX_URL = 'http://192.168.1.125:5005/credentials/indexes' From 211e4474e866d4a7e301f7cbbbd9bf56126e30dc Mon Sep 17 00:00:00 2001 From: KulkarniShashank Date: Wed, 14 Aug 2024 16:13:23 +0530 Subject: [PATCH 08/15] feat: implemented w3c revocation for multi-tenancy Signed-off-by: KulkarniShashank --- config.json | 3 +- ...ue and receive revocable credentials.patch | 241 +++++++++++++++++- src/cli.ts | 2 - src/cliAgent.ts | 3 - .../credentials/CredentialController.ts | 6 +- .../multi-tenancy/MultiTenancyController.ts | 196 +++++++++++++- src/controllers/types.ts | 6 +- .../w3cRevocation/w3cRevocationController.ts | 74 ++---- src/utils/credentialStatusList.ts | 38 ++- 9 files changed, 483 insertions(+), 86 deletions(-) diff --git a/config.json b/config.json index ca002dc4..09b06448 100644 --- a/config.json +++ b/config.json @@ -1,5 +1,4 @@ { "port": 4001, - "schemaFileServerURL": "https://schema.credebl.id/schemas/", - "bitStringStatusListURL": "http://192.168.1.125:5005/credentials/status/1" + "schemaFileServerURL": "https://schema.credebl.id/schemas/" } \ No newline at end of file diff --git a/patches/@credo-ts+core+0.5.3+003+Change in credential Format for issue and receive revocable credentials.patch b/patches/@credo-ts+core+0.5.3+003+Change in credential Format for issue and receive revocable credentials.patch index 30ce8aaf..599b592c 100644 --- a/patches/@credo-ts+core+0.5.3+003+Change in credential Format for issue and receive revocable credentials.patch +++ b/patches/@credo-ts+core+0.5.3+003+Change in credential Format for issue and receive revocable credentials.patch @@ -95,23 +95,90 @@ index 2006259..afc834e 100644 createOffer(agentContext: AgentContext, { credentialFormats, attachmentId }: CredentialFormatCreateOfferOptions): Promise; processOffer(agentContext: AgentContext, { attachment }: CredentialFormatProcessOptions): Promise; acceptOffer(agentContext: AgentContext, { attachmentId, offerAttachment }: CredentialFormatAcceptOfferOptions): Promise; +diff --git a/node_modules/@credo-ts/core/build/modules/credentials/protocol/v2/V2CredentialProtocol.js b/node_modules/@credo-ts/core/build/modules/credentials/protocol/v2/V2CredentialProtocol.js +index fb1fb9d..ac871bf 100644 +--- a/node_modules/@credo-ts/core/build/modules/credentials/protocol/v2/V2CredentialProtocol.js ++++ b/node_modules/@credo-ts/core/build/modules/credentials/protocol/v2/V2CredentialProtocol.js +@@ -530,6 +530,7 @@ class V2CredentialProtocol extends BaseCredentialProtocol_1.BaseCredentialProtoc + if (formatServices.length === 0) { + throw new error_1.CredoError(`Unable to accept request. No supported formats provided as input or in request message`); + } ++ + const message = await this.credentialFormatCoordinator.acceptRequest(agentContext, { + credentialRecord, + formatServices, +@@ -584,6 +585,7 @@ class V2CredentialProtocol extends BaseCredentialProtocol_1.BaseCredentialProtoc + if (formatServices.length === 0) { + throw new error_1.CredoError(`Unable to process credential. No supported formats`); + } ++ console.log("--------------------------------------------------------------------"); + await this.credentialFormatCoordinator.processCredential(messageContext.agentContext, { + credentialRecord, + formatServices, +diff --git a/node_modules/@credo-ts/core/build/modules/vc/data-integrity/W3cJsonLdCredentialService.d.ts b/node_modules/@credo-ts/core/build/modules/vc/data-integrity/W3cJsonLdCredentialService.d.ts +index 4176d33..12ce7e8 100644 +--- a/node_modules/@credo-ts/core/build/modules/vc/data-integrity/W3cJsonLdCredentialService.d.ts ++++ b/node_modules/@credo-ts/core/build/modules/vc/data-integrity/W3cJsonLdCredentialService.d.ts +@@ -13,6 +13,7 @@ import { W3cJsonLdVerifiablePresentation } from './models/W3cJsonLdVerifiablePre + export declare class W3cJsonLdCredentialService { + private signatureSuiteRegistry; + private w3cCredentialsModuleConfig; ++ private w3cCredentialService; + constructor(signatureSuiteRegistry: SignatureSuiteRegistry, w3cCredentialsModuleConfig: W3cCredentialsModuleConfig); + /** + * Signs a credential diff --git a/node_modules/@credo-ts/core/build/modules/vc/data-integrity/W3cJsonLdCredentialService.js b/node_modules/@credo-ts/core/build/modules/vc/data-integrity/W3cJsonLdCredentialService.js -index 3fa8bf2..9ad5f2e 100644 +index 3fa8bf2..13dc96f 100644 --- a/node_modules/@credo-ts/core/build/modules/vc/data-integrity/W3cJsonLdCredentialService.js +++ b/node_modules/@credo-ts/core/build/modules/vc/data-integrity/W3cJsonLdCredentialService.js -@@ -99,13 +99,15 @@ let W3cJsonLdCredentialService = class W3cJsonLdCredentialService { +@@ -39,6 +39,9 @@ const jsonld_1 = __importDefault(require("./libraries/jsonld")); + const vc_1 = __importDefault(require("./libraries/vc")); + const models_1 = require("./models"); + const W3cJsonLdVerifiablePresentation_1 = require("./models/W3cJsonLdVerifiablePresentation"); ++const { promisify } = require('util'); ++const zlib = require('zlib'); ++ + /** + * Supports signing and verification of credentials according to the [Verifiable Credential Data Model](https://www.w3.org/TR/vc-data-model) + * using [Data Integrity Proof](https://www.w3.org/TR/vc-data-model/#data-integrity-proofs). +@@ -98,14 +101,41 @@ let W3cJsonLdCredentialService = class W3cJsonLdCredentialService { + credential: utils_1.JsonTransformer.toJSON(options.credential), suite: suites, documentLoader: this.w3cCredentialsModuleConfig.documentLoader(agentContext), - checkStatus: ({ credential }) => { +- checkStatus: ({ credential }) => { ++ checkStatus: async ({ credential }) => { ++ + // Note: currety comment this change to avoid passing credetials with credentialStatus // Only throw error if credentialStatus is present -- if (verifyCredentialStatus && 'credentialStatus' in credential) { + if (verifyCredentialStatus && 'credentialStatus' in credential) { - throw new error_1.CredoError('Verifying credential status for JSON-LD credentials is currently not supported'); -- } -+ // if (verifyCredentialStatus && 'credentialStatus' in credential) { + // TODO: add logic to verify credentialStatus -+ // throw new CredoError('Verifying credential status for JSON-LD credentials is currently not supported') -+ // } ++ // throw new CredoError('Verifying credential status for JSON-LD credentials is currently not supported') ++ const credentialStatusURL = credential.credentialStatus.statusListCredential; ++ const bitStringStatusListCredential = await agentContext.config.agentDependencies.fetch(credentialStatusURL, { ++ method: 'GET', ++ }); ++ ++ if (!bitStringStatusListCredential.ok) { ++ throw new error_1.CredoError(`HTTP error! Status: ${bitStringStatusListCredential.status}`); ++ } ++ ++ const bitStringCredential = await bitStringStatusListCredential.json(); ++ const encodedBitString = bitStringCredential.credential.credentialSubject.encodedList ++ const gunzip = promisify(zlib.gunzip); ++ ++ const compressedBuffer = Buffer.from(encodedBitString, 'base64'); ++ const decompressedBuffer = await gunzip(compressedBuffer); ++ const decodedBitString = decompressedBuffer.toString('binary'); ++ ++ if (credential.credentialStatus.statusListIndex < 0 || credential.credentialStatus.statusListIndex >= decodedBitString.length) { ++ throw new error_1.CredoError('Index out of bounds'); ++ } ++ ++ if(decodedBitString[credential.credentialStatus.statusListIndex] === '1'){ ++ throw new error_1.CredoError(`Credential at index ${credential.credentialStatus.statusListIndex} is revoked.`); ++ } + } return { - verified: true, - }; @@ -120,3 +187,161 @@ index 3fa8bf2..9ad5f2e 100644 }, }; // this is a hack because vcjs throws if purpose is passed as undefined or null +@@ -219,7 +249,41 @@ let W3cJsonLdCredentialService = class W3cJsonLdCredentialService { + challenge: options.challenge, + domain: options.domain, + documentLoader: this.w3cCredentialsModuleConfig.documentLoader(agentContext), ++ checkStatus: async ({ credential }) => { ++ ++ if ('credentialStatus' in credential) { ++ ++ const credentialStatusURL = credential.credentialStatus.statusListCredential; ++ const bitStringStatusListCredential = await agentContext.config.agentDependencies.fetch(credentialStatusURL, { ++ method: 'GET', ++ }); ++ ++ if (!bitStringStatusListCredential.ok) { ++ throw new error_1.CredoError(`HTTP error! Status: ${bitStringStatusListCredential.status}`); ++ } ++ ++ const bitStringCredential = await bitStringStatusListCredential.json(); ++ const encodedBitString = bitStringCredential.credential.credentialSubject.encodedList ++ const gunzip = promisify(zlib.gunzip); ++ ++ const compressedBuffer = Buffer.from(encodedBitString, 'base64'); ++ const decompressedBuffer = await gunzip(compressedBuffer); ++ const decodedBitString = decompressedBuffer.toString('binary'); ++ ++ if (credential.credentialStatus.statusListIndex < 0 || credential.credentialStatus.statusListIndex >= decodedBitString.length) { ++ throw new error_1.CredoError('Index out of bounds'); ++ } ++ ++ if(decodedBitString[credential.credentialStatus.statusListIndex] === '1'){ ++ throw new error_1.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 + if (options.purpose) { + verifyOptions['presentationPurpose'] = options.purpose; +@@ -305,7 +369,8 @@ let W3cJsonLdCredentialService = class W3cJsonLdCredentialService { + W3cJsonLdCredentialService = __decorate([ + (0, plugins_1.injectable)(), + __metadata("design:paramtypes", [SignatureSuiteRegistry_1.SignatureSuiteRegistry, +- W3cCredentialsModuleConfig_1.W3cCredentialsModuleConfig]) ++ W3cCredentialsModuleConfig_1.W3cCredentialsModuleConfig ++ ]) + ], W3cJsonLdCredentialService); + exports.W3cJsonLdCredentialService = W3cJsonLdCredentialService; + //# sourceMappingURL=W3cJsonLdCredentialService.js.map +\ No newline at end of file +diff --git a/node_modules/@credo-ts/core/build/modules/vc/models/credential/W3cCredential.d.ts b/node_modules/@credo-ts/core/build/modules/vc/models/credential/W3cCredential.d.ts +index dbd8dba..b3deac2 100644 +--- a/node_modules/@credo-ts/core/build/modules/vc/models/credential/W3cCredential.d.ts ++++ b/node_modules/@credo-ts/core/build/modules/vc/models/credential/W3cCredential.d.ts +@@ -4,7 +4,7 @@ import type { JsonObject } from '../../../../types'; + import type { ValidationOptions } from 'class-validator'; + import { SingleOrArray } from '../../../../utils/type'; + import { W3cCredentialSchema } from './W3cCredentialSchema'; +-import { W3cCredentialStatus } from './W3cCredentialStatus'; ++import { W3cCredentialStatus, W3cCredentialStatusOptions } from './W3cCredentialStatus'; + import { W3cCredentialSubject } from './W3cCredentialSubject'; + import { W3cIssuer } from './W3cIssuer'; + export interface W3cCredentialOptions { +@@ -15,7 +15,7 @@ export interface W3cCredentialOptions { + issuanceDate: string; + expirationDate?: string; + credentialSubject: SingleOrArray; +- credentialStatus?: W3cCredentialStatus; ++ credentialStatus: W3cCredentialStatus | Array; + } + export declare class W3cCredential { + constructor(options: W3cCredentialOptions); +@@ -27,7 +27,7 @@ export declare class W3cCredential { + expirationDate?: string; + credentialSubject: SingleOrArray; + credentialSchema?: SingleOrArray; +- credentialStatus?: W3cCredentialStatus; ++ credentialStatus: W3cCredentialStatus | Array; + get issuerId(): string; + get credentialSchemaIds(): string[]; + get credentialSubjectIds(): string[]; +diff --git a/node_modules/@credo-ts/core/build/modules/vc/models/credential/W3cCredential.js b/node_modules/@credo-ts/core/build/modules/vc/models/credential/W3cCredential.js +index 800214f..b064d9e 100644 +--- a/node_modules/@credo-ts/core/build/modules/vc/models/credential/W3cCredential.js ++++ b/node_modules/@credo-ts/core/build/modules/vc/models/credential/W3cCredential.js +@@ -35,10 +35,12 @@ class W3cCredential { + this.expirationDate = options.expirationDate; + this.credentialSubject = (0, utils_1.mapSingleOrArray)(options.credentialSubject, (subject) => subject instanceof W3cCredentialSubject_1.W3cCredentialSubject ? subject : new W3cCredentialSubject_1.W3cCredentialSubject(subject)); + if (options.credentialStatus) { +- this.credentialStatus = +- options.credentialStatus instanceof W3cCredentialStatus_1.W3cCredentialStatus +- ? options.credentialStatus +- : new W3cCredentialStatus_1.W3cCredentialStatus(options.credentialStatus); ++ console.log('options.credentialStatus----', options.credentialStatus); ++ this.credentialStatus = (0, utils_1.mapSingleOrArray)(options.credentialStatus, (status) => status instanceof W3cCredentialStatus_1.W3cCredentialStatus ? status : new W3cCredentialStatus_1.W3cCredentialStatus(status)); ++ // this.credentialStatus = ++ // options.credentialStatus instanceof W3cCredentialStatus_1.W3cCredentialStatus ++ // ? options.credentialStatus ++ // : new W3cCredentialStatus_1.W3cCredentialStatus(options.credentialStatus); + } + } + } +diff --git a/node_modules/@credo-ts/core/build/modules/vc/models/credential/W3cCredentialStatus.d.ts b/node_modules/@credo-ts/core/build/modules/vc/models/credential/W3cCredentialStatus.d.ts +index c1f4743..f2ac20d 100644 +--- a/node_modules/@credo-ts/core/build/modules/vc/models/credential/W3cCredentialStatus.d.ts ++++ b/node_modules/@credo-ts/core/build/modules/vc/models/credential/W3cCredentialStatus.d.ts +@@ -1,9 +1,15 @@ + export interface W3cCredentialStatusOptions { + id: string; + type: string; ++ statusPurpose: string; ++ statusListIndex: string; ++ statusListCredential: string; + } + export declare class W3cCredentialStatus { + constructor(options: W3cCredentialStatusOptions); + id: string; + type: string; ++ statusPurpose: string; ++ statusListIndex: string; ++ statusListCredential: string; + } +diff --git a/node_modules/@credo-ts/core/build/modules/vc/models/credential/W3cCredentialStatus.js b/node_modules/@credo-ts/core/build/modules/vc/models/credential/W3cCredentialStatus.js +index 81355eb..467ebd2 100644 +--- a/node_modules/@credo-ts/core/build/modules/vc/models/credential/W3cCredentialStatus.js ++++ b/node_modules/@credo-ts/core/build/modules/vc/models/credential/W3cCredentialStatus.js +@@ -17,6 +17,9 @@ class W3cCredentialStatus { + if (options) { + this.id = options.id; + this.type = options.type; ++ this.statusPurpose = options.statusPurpose; ++ this.statusListIndex = options.statusListIndex; ++ this.statusListCredential = options.statusListCredential; + } + } + } +@@ -28,5 +31,17 @@ __decorate([ + (0, class_validator_1.IsString)(), + __metadata("design:type", String) + ], W3cCredentialStatus.prototype, "type", void 0); ++__decorate([ ++ (0, class_validator_1.IsString)(), ++ __metadata("design:type", String) ++], W3cCredentialStatus.prototype, "statusPurpose", void 0); ++__decorate([ ++ (0, class_validator_1.IsString)(), ++ __metadata("design:type", String) ++], W3cCredentialStatus.prototype, "statusListIndex", void 0); ++__decorate([ ++ (0, class_validator_1.IsString)(), ++ __metadata("design:type", String) ++], W3cCredentialStatus.prototype, "statusListCredential", void 0); + exports.W3cCredentialStatus = W3cCredentialStatus; + //# sourceMappingURL=W3cCredentialStatus.js.map +\ No newline at end of file diff --git a/src/cli.ts b/src/cli.ts index caadc170..54aa3358 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -43,7 +43,6 @@ interface Parsed { rpcUrl?: string fileServerUrl?: string fileServerToken?: string - bitStringStatusListURL?: string } interface InboundTransport { @@ -254,6 +253,5 @@ export async function runCliServer() { rpcUrl: parsed.rpcUrl, fileServerUrl: parsed.fileServerUrl, fileServerToken: parsed.fileServerToken, - bitStringStatusListURL: parsed.bitStringStatusListURL, } as AriesRestConfig) } diff --git a/src/cliAgent.ts b/src/cliAgent.ts index e2a46459..a9a34f29 100644 --- a/src/cliAgent.ts +++ b/src/cliAgent.ts @@ -108,7 +108,6 @@ export interface AriesRestConfig { fileServerToken?: string walletScheme?: AskarMultiWalletDatabaseScheme schemaFileServerURL?: string - bitStringStatusListURL?: string } export async function readRestConfig(path: string) { @@ -261,7 +260,6 @@ async function generateSecretKey(length: number = 32): Promise { export async function runRestAgent(restConfig: AriesRestConfig) { const { - bitStringStatusListURL, schemaFileServerURL, logLevel, inboundTransports = [], @@ -441,7 +439,6 @@ export async function runRestAgent(restConfig: AriesRestConfig) { webhookUrl, port: adminPort, schemaFileServerURL, - bitStringStatusListURL, }, token ) diff --git a/src/controllers/credentials/CredentialController.ts b/src/controllers/credentials/CredentialController.ts index 3bb33b9f..d45ee6df 100644 --- a/src/controllers/credentials/CredentialController.ts +++ b/src/controllers/credentials/CredentialController.ts @@ -13,7 +13,7 @@ import { injectable } from 'tsyringe' import { BitStringCredentialStatusPurpose } from '../../enums/enum' import ErrorHandlingService from '../../errorHandlingService' import { BadRequestError } from '../../errors/errors' -import getCredentialStatus from '../../utils/credentialStatusList' +import utils from '../../utils/credentialStatusList' import { CredentialExchangeRecordExample, RecordId } from '../examples' import { OutOfBandController } from '../outofband/OutOfBandController' import { @@ -184,7 +184,7 @@ export class CredentialController extends Controller { statusListCredentialURL: credentialSubjectUrl, }) - const credentialStatus = await getCredentialStatus(credentialStatusData, getIndex) + const credentialStatus = await utils.getCredentialStatus(credentialStatusData, getIndex) credentialFormats.jsonld.credential.credentialStatus = credentialStatus const offer = await this.agent.credentials.offerCredential(createOfferOptions) @@ -246,7 +246,7 @@ export class CredentialController extends Controller { const getIndex = await this.agent.genericRecords.findAllByQuery({ statusListCredentialURL: credentialSubjectUrl, }) - const credentialStatus = await getCredentialStatus(credentialStatusData, getIndex) + const credentialStatus = await utils.getCredentialStatus(credentialStatusData, getIndex) credentialFormats.jsonld.credential.credentialStatus = credentialStatus const offerOob = await this.agent.credentials.createOffer({ diff --git a/src/controllers/multi-tenancy/MultiTenancyController.ts b/src/controllers/multi-tenancy/MultiTenancyController.ts index ce9fd991..e0d8999b 100644 --- a/src/controllers/multi-tenancy/MultiTenancyController.ts +++ b/src/controllers/multi-tenancy/MultiTenancyController.ts @@ -1,6 +1,6 @@ import type { RestAgentModules, RestMultiTenantAgentModules } from '../../cliAgent' import type { Version } from '../examples' -import type { CredentialStatusList, RecipientKeyOption, SchemaMetadata } from '../types' +import type { BitStringCredential, CredentialStatusList, RecipientKeyOption, SchemaMetadata } from '../types' import type { PolygonDidCreateOptions } from '@ayanworks/credo-polygon-w3c-module/build/dids' import type { AcceptProofRequestOptions, @@ -13,7 +13,10 @@ import type { ProofExchangeRecordProps, ProofsProtocolVersionType, Routing, + W3cCredentialRecord, + W3cJsonLdSignCredentialOptions, } from '@credo-ts/core' +import type { GenericRecord } from '@credo-ts/core/build/modules/generic-records/repository/GenericRecord' import type { IndyVdrDidCreateOptions, IndyVdrDidCreateResult } from '@credo-ts/indy-vdr' import type { QuestionAnswerRecord, ValidResponse } from '@credo-ts/question-answer' import type { TenantRecord } from '@credo-ts/tenants' @@ -43,6 +46,7 @@ import { injectable, createPeerDidDocumentFromServices, PeerDidNumAlgo, + ClaimFormat, } from '@credo-ts/core' import { QuestionAnswerRole, QuestionAnswerState } from '@credo-ts/question-answer' import axios from 'axios' @@ -67,7 +71,7 @@ import { PaymentRequiredError, UnprocessableEntityError, } from '../../errors' -import getCredentialStatus from '../../utils/credentialStatusList' +import utils from '../../utils/credentialStatusList' import { BCOVRIN_REGISTER_URL, INDICIO_NYM_URL } from '../../utils/util' import { SchemaId, CredentialDefinitionId, RecordId, ProofRecordExample, ConnectionRecordExample } from '../examples' import { @@ -83,6 +87,7 @@ import { CreateProofRequestOobOptions, CreateOfferOobOptions, CreateSchemaInput, + SignCredentialPayload, } from '../types' import { Body, Controller, Delete, Get, Post, Query, Route, Tags, Path, Example, Security, Response } from 'tsoa' @@ -1295,7 +1300,7 @@ export class MultiTenancyController extends Controller { statusListCredentialURL: credentialSubjectUrl, }) - const credentialStatus = await getCredentialStatus(credentialStatusData, getIndex) + const credentialStatus = await utils.getCredentialStatus(credentialStatusData, getIndex) credentialFormats.jsonld.credential.credentialStatus = credentialStatus offer = await tenantAgent.credentials.offerCredential(createOfferOptions) @@ -1366,7 +1371,7 @@ export class MultiTenancyController extends Controller { const getIndex = await this.agent.genericRecords.findAllByQuery({ statusListCredentialURL: createOfferOptions.credentialSubjectUrl, }) - const credentialStatus = await getCredentialStatus(credentialStatusData, getIndex) + const credentialStatus = await utils.getCredentialStatus(credentialStatusData, getIndex) createOfferOptions.credentialFormats.jsonld.credential.credentialStatus = credentialStatus offerOob = await tenantAgent.credentials.createOffer({ @@ -1642,6 +1647,7 @@ export class MultiTenancyController extends Controller { const requestedCredentials = await tenantAgent.proofs.selectCredentialsForRequest({ proofRecordId, }) + const acceptProofRequest: AcceptProofRequestOptions = { proofRecordId, comment: request.comment, @@ -1898,4 +1904,186 @@ export class MultiTenancyController extends Controller { throw ErrorHandlingService.handle(error) } } + + @Security('apiKey') + @Post('/sign/bitstring-credential/:tenantId') + public async createBitstringStatusListCredential( + @Body() signCredentialPayload: SignCredentialPayload, + @Path('tenantId') tenantId: string + ): Promise { + try { + const { bitStringCredentialUrl, issuerDid, statusPurpose, bitStringLength } = signCredentialPayload + const bitStringStatusListPurpose = statusPurpose ?? BitStringCredentialStatusPurpose.REVOCATION + const bitStringStatusListCredentialListLength = bitStringLength ? bitStringLength : 131072 + const bitStringStatus = await utils.generateBitStringStatus(bitStringStatusListCredentialListLength) + const encodedList = await utils.encodeBitString(bitStringStatus) + const didIdentifier = issuerDid.split(':')[2] + const data = { + format: ClaimFormat.LdpVc, + credential: { + '@context': ['https://www.w3.org/2018/credentials/v1', 'https://w3id.org/vc/status-list/2021/v1'], + id: bitStringCredentialUrl, + type: ['VerifiableCredential', 'BitstringStatusListCredential'], + issuer: { + id: issuerDid, + }, + issuanceDate: new Date().toISOString(), + credentialSubject: { + id: bitStringCredentialUrl, + type: 'BitstringStatusList', + encodedList, + statusPurpose: bitStringStatusListPurpose, + }, + }, + verificationMethod: `${issuerDid}#${didIdentifier}`, + proofType: 'Ed25519Signature2018', + } + + await fetch(bitStringCredentialUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ credentialsData: data }), + }) + + return await this.agent.modules.tenants.withTenantAgent({ tenantId }, async (tenantAgent) => { + const signCredential = await tenantAgent.w3cCredentials.signCredential( + data as unknown as W3cJsonLdSignCredentialOptions + ) + + return await tenantAgent.w3cCredentials.storeCredential({ credential: signCredential }) + }) + } catch (error) { + throw ErrorHandlingService.handle(error) + } + } + + @Security('apiKey') + @Post('/revoke-credential/:credentialId/:tenantId') + public async revokeW3C( + @Path('credentialId') credentialId: string, + @Path('tenantId') tenantId: string + ): Promise<{ + message: string + }> { + try { + let credential + return await this.agent.modules.tenants.withTenantAgent({ tenantId }, async (tenantAgent) => { + credential = await tenantAgent.credentials.getFormatData(credentialId) + + let credentialIndex + let statusListCredentialURL + const revocationStatus = 1 + + if (!Array.isArray(credential.offer?.jsonld?.credential?.credentialStatus)) { + credentialIndex = credential.offer?.jsonld?.credential?.credentialStatus?.statusListIndex as string + statusListCredentialURL = credential.offer?.jsonld?.credential?.credentialStatus + ?.statusListCredential as string + } else { + credentialIndex = credential.offer?.jsonld?.credential?.credentialStatus[0].statusListIndex as string + statusListCredentialURL = credential.offer?.jsonld?.credential?.credentialStatus[0] + .statusListCredential as string + } + + const bitStringStatusListCredential = await fetch(statusListCredentialURL, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }) + + if (!bitStringStatusListCredential.ok) { + throw new InternalServerError(`${bitStringStatusListCredential.statusText}`) + } + + const bitStringCredential = (await bitStringStatusListCredential.json()) as BitStringCredential + const encodedBitString = bitStringCredential.credential.credentialSubject.encodedList + const decodeBitString = await utils.decodeBitSting(encodedBitString) + + const findBitStringIndex = decodeBitString.charAt(parseInt(credentialIndex)) + if (findBitStringIndex === revocationStatus.toString()) { + throw new BadRequestError('The credential already revoked') + } + + const updateBitString = + decodeBitString.slice(0, parseInt(credentialIndex)) + + revocationStatus + + decodeBitString.slice(parseInt(credentialIndex) + 1) + + const encodeUpdatedBitString = await utils.encodeBitString(updateBitString) + bitStringCredential.credential.credentialSubject.encodedList = encodeUpdatedBitString + await fetch(statusListCredentialURL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ credentialsData: bitStringCredential }), + }) + + return { message: 'The credential has been revoked' } + }) + } catch (error) { + throw ErrorHandlingService.handle(error) + } + } + + @Security('apiKey') + @Get('bitstring/status-list/:bitCredentialStatusUrl/:tenantId') + public async getBitStringStatusListById( + @Path('bitCredentialStatusUrl') bitCredentialStatusUrl: string, + @Path('tenantId') tenantId: string + ): Promise<{ + bitStringCredential: BitStringCredential + getIndex: GenericRecord[] + }> { + try { + const validateUrl = await utils.isValidUrl(bitCredentialStatusUrl) + if (!validateUrl) { + throw new BadRequestError(`Please provide a bit string credential id`) + } + + const bitStringCredentialDetails = await fetch(bitCredentialStatusUrl, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }) + + if (!bitStringCredentialDetails.ok) { + throw new InternalServerError(`${bitStringCredentialDetails.statusText}`) + } + + const bitStringCredential = (await bitStringCredentialDetails.json()) as BitStringCredential + if (!bitStringCredential?.credential && !bitStringCredential?.credential?.credentialSubject) { + throw new BadRequestError(`Invalid credentialSubjectUrl`) + } + + return await this.agent.modules.tenants.withTenantAgent({ tenantId }, async (tenantAgent) => { + const getIndex = await tenantAgent.genericRecords.findAllByQuery({ + statusListCredentialURL: bitCredentialStatusUrl, + }) + + return { + bitStringCredential, + getIndex, + } + }) + } catch (error) { + throw ErrorHandlingService.handle(error) + } + } + + @Security('apiKey') + @Get('bitstring/status-list/:tenantId') + public async getAllBitStringStatusList(@Path('tenantId') tenantId: string): Promise { + try { + return await this.agent.modules.tenants.withTenantAgent({ tenantId }, async (tenantAgent) => { + const getBitStringCredentialStatusList = await tenantAgent.w3cCredentials.getAllCredentialRecords() + return getBitStringCredentialStatusList + }) + } catch (error) { + throw ErrorHandlingService.handle(error) + } + } } diff --git a/src/controllers/types.ts b/src/controllers/types.ts index c3e2fbda..b76278f4 100644 --- a/src/controllers/types.ts +++ b/src/controllers/types.ts @@ -407,9 +407,9 @@ export interface StatusList { } export interface SignCredentialPayload { - id: string - issuerId: string - statusPurpose: string + bitStringCredentialUrl: string + issuerDid: string + statusPurpose?: string bitStringLength?: number } diff --git a/src/controllers/w3cRevocation/w3cRevocationController.ts b/src/controllers/w3cRevocation/w3cRevocationController.ts index 2511f573..995409eb 100644 --- a/src/controllers/w3cRevocation/w3cRevocationController.ts +++ b/src/controllers/w3cRevocation/w3cRevocationController.ts @@ -5,11 +5,11 @@ import type { GenericRecord } from '@credo-ts/core/build/modules/generic-records import { Agent, ClaimFormat } from '@credo-ts/core' import { injectable } from 'tsyringe' -import { promisify } from 'util' -import * as zlib from 'zlib' +import { BitStringCredentialStatusPurpose } from '../../enums/enum' import ErrorHandlingService from '../../errorHandlingService' import { BadRequestError, InternalServerError } from '../../errors/errors' +import utils from '../../utils/credentialStatusList' import { SignCredentialPayload } from '../types' import { Tags, Route, Controller, Post, Security, Body, Path, Get } from 'tsoa' @@ -26,58 +26,39 @@ export class StatusController extends Controller { this.agent = agent } - // Function to generate a bit string status - public async generateBitStringStatus(length: number): Promise { - return Array.from({ length }, () => (Math.random() > 0.5 ? '1' : '0')).join('') - } - - // Function to encode the bit string status - public async encodeBitString(bitString: string): Promise { - const gzip = promisify(zlib.gzip) - const buffer = Buffer.from(bitString, 'binary') - const compressedBuffer = await gzip(buffer) - return compressedBuffer.toString('base64') - } - - public async decodeBitSting(bitString: string): Promise { - const gunzip = promisify(zlib.gunzip) - const compressedBuffer = Buffer.from(bitString, 'base64') - const decompressedBuffer = await gunzip(compressedBuffer) - return decompressedBuffer.toString('binary') - } - @Post('/sign/bitstring-credential') public async createBitstringStatusListCredential( @Body() signCredentialPayload: SignCredentialPayload ): Promise { try { - const { id, issuerId, statusPurpose, bitStringLength } = signCredentialPayload + const { bitStringCredentialUrl, issuerDid, statusPurpose, bitStringLength } = signCredentialPayload + const bitStringStatusListPurpose = statusPurpose ?? BitStringCredentialStatusPurpose.REVOCATION const bitStringStatusListCredentialListLength = bitStringLength ? bitStringLength : 131072 - const bitStringStatus = await this.generateBitStringStatus(bitStringStatusListCredentialListLength) - const encodedList = await this.encodeBitString(bitStringStatus) - const didIdentifier = issuerId.split(':')[2] + const bitStringStatus = await utils.generateBitStringStatus(bitStringStatusListCredentialListLength) + const encodedList = await utils.encodeBitString(bitStringStatus) + const didIdentifier = issuerDid.split(':')[2] const data = { format: ClaimFormat.LdpVc, credential: { '@context': ['https://www.w3.org/2018/credentials/v1', 'https://w3id.org/vc/status-list/2021/v1'], - id, + id: bitStringCredentialUrl, type: ['VerifiableCredential', 'BitstringStatusListCredential'], issuer: { - id: issuerId, + id: issuerDid, }, issuanceDate: new Date().toISOString(), credentialSubject: { - id, + id: bitStringCredentialUrl, type: 'BitstringStatusList', encodedList, - statusPurpose, + statusPurpose: bitStringStatusListPurpose, }, }, - verificationMethod: `${issuerId}#${didIdentifier}`, + verificationMethod: `${issuerDid}#${didIdentifier}`, proofType: 'Ed25519Signature2018', } - await fetch(id, { + await fetch(bitStringCredentialUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -97,12 +78,12 @@ export class StatusController extends Controller { } } - @Post('/revoke-credential/:id') - public async revokeW3C(@Path('id') id: string): Promise<{ + @Post('/revoke-credential/:credentialId') + public async revokeW3C(@Path('credentialId') credentialId: string): Promise<{ message: string }> { try { - const credential = await this.agent.credentials.getFormatData(id) + const credential = await this.agent.credentials.getFormatData(credentialId) let credentialIndex let statusListCredentialURL const revocationStatus = 1 @@ -129,7 +110,7 @@ export class StatusController extends Controller { const bitStringCredential = (await bitStringStatusListCredential.json()) as BitStringCredential const encodedBitString = bitStringCredential.credential.credentialSubject.encodedList - const decodeBitString = await this.decodeBitSting(encodedBitString) + const decodeBitString = await utils.decodeBitSting(encodedBitString) const findBitStringIndex = decodeBitString.charAt(parseInt(credentialIndex)) if (findBitStringIndex === revocationStatus.toString()) { @@ -141,7 +122,7 @@ export class StatusController extends Controller { revocationStatus + decodeBitString.slice(parseInt(credentialIndex) + 1) - const encodeUpdatedBitString = await this.encodeBitString(updateBitString) + const encodeUpdatedBitString = await utils.encodeBitString(updateBitString) bitStringCredential.credential.credentialSubject.encodedList = encodeUpdatedBitString await fetch(statusListCredentialURL, { method: 'POST', @@ -157,18 +138,18 @@ export class StatusController extends Controller { } } - @Get('bitstring/status-list/:id') - public async getBitStringStatusListById(@Path('id') id: string): Promise<{ + @Get('bitstring/status-list/:bitCredentialStatusUrl') + public async getBitStringStatusListById(@Path('bitCredentialStatusUrl') bitCredentialStatusUrl: string): Promise<{ bitStringCredential: BitStringCredential getIndex: GenericRecord[] }> { try { - const validateUrl = await this.isValidUrl(id) + const validateUrl = await utils.isValidUrl(bitCredentialStatusUrl) if (!validateUrl) { throw new BadRequestError(`Please provide a bit string credential id`) } - const bitStringCredentialDetails = await fetch(id, { + const bitStringCredentialDetails = await fetch(bitCredentialStatusUrl, { method: 'GET', headers: { 'Content-Type': 'application/json', @@ -185,7 +166,7 @@ export class StatusController extends Controller { } const getIndex = await this.agent.genericRecords.findAllByQuery({ - statusListCredentialURL: id, + statusListCredentialURL: bitCredentialStatusUrl, }) return { @@ -206,13 +187,4 @@ export class StatusController extends Controller { throw ErrorHandlingService.handle(error) } } - - private async isValidUrl(url: string) { - try { - new URL(url) - return true - } catch (err) { - return false - } - } } diff --git a/src/utils/credentialStatusList.ts b/src/utils/credentialStatusList.ts index ae2b1a3b..5b54f84b 100644 --- a/src/utils/credentialStatusList.ts +++ b/src/utils/credentialStatusList.ts @@ -8,6 +8,33 @@ import * as zlib from 'zlib' import ErrorHandlingService from '../errorHandlingService' import { BadRequestError, ConflictError, InternalServerError } from '../errors/errors' +async function generateBitStringStatus(length: number): Promise { + return Array.from({ length }, () => (Math.random() > 0.5 ? '1' : '0')).join('') +} + +async function encodeBitString(bitString: string): Promise { + const gzip = promisify(zlib.gzip) + const buffer = Buffer.from(bitString, 'binary') + const compressedBuffer = await gzip(buffer) + return compressedBuffer.toString('base64') +} + +async function decodeBitSting(bitString: string): Promise { + const gunzip = promisify(zlib.gunzip) + const compressedBuffer = Buffer.from(bitString, 'base64') + const decompressedBuffer = await gunzip(compressedBuffer) + return decompressedBuffer.toString('binary') +} + +async function isValidUrl(url: string) { + try { + new URL(url) + return true + } catch (err) { + return false + } +} + async function getCredentialStatus( credentialStatusList: CredentialStatusList, getIndex: GenericRecord[] @@ -104,13 +131,4 @@ function getAvailableIndex(str: string, usedIndices: number[]) { return -1 } -function isValidUrl(url: string) { - try { - new URL(url) - return true - } catch (err) { - return false - } -} - -export default getCredentialStatus +export default { getCredentialStatus, generateBitStringStatus, encodeBitString, decodeBitSting, isValidUrl } From 32dc1133ac0a26fdd480f31f54d3087b44150891 Mon Sep 17 00:00:00 2001 From: KulkarniShashank Date: Fri, 16 Aug 2024 17:03:12 +0530 Subject: [PATCH 09/15] fix: optimize the bit string credential function Signed-off-by: KulkarniShashank --- ...ore+0.5.3+004+added-pretty-vc-patch.patch} | 12 +- .../multi-tenancy/MultiTenancyController.ts | 120 ++---------------- .../w3cRevocation/w3cRevocationController.ts | 110 ++++++++++------ src/utils/credentialStatusList.ts | 3 +- 4 files changed, 89 insertions(+), 156 deletions(-) rename patches/{@credo-ts+core+0.5.3+004+added-prettyVc-in-JsonCredential-interface.patch => @credo-ts+core+0.5.3+004+added-pretty-vc-patch.patch} (69%) diff --git a/patches/@credo-ts+core+0.5.3+004+added-prettyVc-in-JsonCredential-interface.patch b/patches/@credo-ts+core+0.5.3+004+added-pretty-vc-patch.patch similarity index 69% rename from patches/@credo-ts+core+0.5.3+004+added-prettyVc-in-JsonCredential-interface.patch rename to patches/@credo-ts+core+0.5.3+004+added-pretty-vc-patch.patch index 32598651..087a9334 100644 --- a/patches/@credo-ts+core+0.5.3+004+added-prettyVc-in-JsonCredential-interface.patch +++ b/patches/@credo-ts+core+0.5.3+004+added-pretty-vc-patch.patch @@ -1,13 +1,13 @@ diff --git a/node_modules/@credo-ts/core/build/modules/credentials/formats/jsonld/JsonLdCredentialFormat.d.ts b/node_modules/@credo-ts/core/build/modules/credentials/formats/jsonld/JsonLdCredentialFormat.d.ts -index d12468b..ae70f36 100644 +index 09bd3b1..b902670 100644 --- a/node_modules/@credo-ts/core/build/modules/credentials/formats/jsonld/JsonLdCredentialFormat.d.ts +++ b/node_modules/@credo-ts/core/build/modules/credentials/formats/jsonld/JsonLdCredentialFormat.d.ts -@@ -10,6 +10,8 @@ export interface JsonCredential { - issuanceDate: string; - expirationDate?: string; +@@ -12,6 +12,8 @@ export interface JsonCredential { credentialSubject: SingleOrArray; + credentialStatus?: SingleOrArray + [key: string]: unknown + //TODO change type + prettyVc?: any; - [key: string]: unknown; } - /** + type CredentialStatusType = 'BitstringStatusListEntry' + diff --git a/src/controllers/multi-tenancy/MultiTenancyController.ts b/src/controllers/multi-tenancy/MultiTenancyController.ts index 535d85d7..2379911a 100644 --- a/src/controllers/multi-tenancy/MultiTenancyController.ts +++ b/src/controllers/multi-tenancy/MultiTenancyController.ts @@ -47,7 +47,6 @@ import { injectable, createPeerDidDocumentFromServices, PeerDidNumAlgo, - ClaimFormat, } from '@credo-ts/core' import { QuestionAnswerRole, QuestionAnswerState } from '@credo-ts/question-answer' import axios from 'axios' @@ -96,6 +95,7 @@ import { CreateSchemaInput, SignCredentialPayload, } from '../types' +import { W3CRevocationController } from '../w3cRevocation/w3cRevocationController' import { Body, Controller, Delete, Get, Post, Query, Route, Tags, Path, Example, Security, Response } from 'tsoa' @@ -104,10 +104,11 @@ import { Body, Controller, Delete, Get, Post, Query, Route, Tags, Path, Example, @injectable() export class MultiTenancyController extends Controller { private readonly agent: Agent - - public constructor(agent: Agent) { + private readonly w3CRevocationController!: W3CRevocationController + public constructor(agent: Agent, w3CRevocationController: W3CRevocationController) { super() this.agent = agent + this.w3CRevocationController = w3CRevocationController } //create wallet @@ -1971,41 +1972,7 @@ export class MultiTenancyController extends Controller { @Path('tenantId') tenantId: string ): Promise { try { - const { bitStringCredentialUrl, issuerDid, statusPurpose, bitStringLength } = signCredentialPayload - const bitStringStatusListPurpose = statusPurpose ?? BitStringCredentialStatusPurpose.REVOCATION - const bitStringStatusListCredentialListLength = bitStringLength ? bitStringLength : 131072 - const bitStringStatus = await utils.generateBitStringStatus(bitStringStatusListCredentialListLength) - const encodedList = await utils.encodeBitString(bitStringStatus) - const didIdentifier = issuerDid.split(':')[2] - const data = { - format: ClaimFormat.LdpVc, - credential: { - '@context': ['https://www.w3.org/2018/credentials/v1', 'https://w3id.org/vc/status-list/2021/v1'], - id: bitStringCredentialUrl, - type: ['VerifiableCredential', 'BitstringStatusListCredential'], - issuer: { - id: issuerDid, - }, - issuanceDate: new Date().toISOString(), - credentialSubject: { - id: bitStringCredentialUrl, - type: 'BitstringStatusList', - encodedList, - statusPurpose: bitStringStatusListPurpose, - }, - }, - verificationMethod: `${issuerDid}#${didIdentifier}`, - proofType: 'Ed25519Signature2018', - } - - await fetch(bitStringCredentialUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ credentialsData: data }), - }) - + const data = await this.w3CRevocationController._createBitstringStatusListCredential(signCredentialPayload) return await this.agent.modules.tenants.withTenantAgent({ tenantId }, async (tenantAgent) => { const signCredential = await tenantAgent.w3cCredentials.signCredential( data as unknown as W3cJsonLdSignCredentialOptions @@ -2027,60 +1994,9 @@ export class MultiTenancyController extends Controller { message: string }> { try { - let credential return await this.agent.modules.tenants.withTenantAgent({ tenantId }, async (tenantAgent) => { - credential = await tenantAgent.credentials.getFormatData(credentialId) - - let credentialIndex - let statusListCredentialURL - const revocationStatus = 1 - - if (!Array.isArray(credential.offer?.jsonld?.credential?.credentialStatus)) { - credentialIndex = credential.offer?.jsonld?.credential?.credentialStatus?.statusListIndex as string - statusListCredentialURL = credential.offer?.jsonld?.credential?.credentialStatus - ?.statusListCredential as string - } else { - credentialIndex = credential.offer?.jsonld?.credential?.credentialStatus[0].statusListIndex as string - statusListCredentialURL = credential.offer?.jsonld?.credential?.credentialStatus[0] - .statusListCredential as string - } - - const bitStringStatusListCredential = await fetch(statusListCredentialURL, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }) - - if (!bitStringStatusListCredential.ok) { - throw new InternalServerError(`${bitStringStatusListCredential.statusText}`) - } - - const bitStringCredential = (await bitStringStatusListCredential.json()) as BitStringCredential - const encodedBitString = bitStringCredential.credential.credentialSubject.encodedList - const decodeBitString = await utils.decodeBitSting(encodedBitString) - - const findBitStringIndex = decodeBitString.charAt(parseInt(credentialIndex)) - if (findBitStringIndex === revocationStatus.toString()) { - throw new BadRequestError('The credential already revoked') - } - - const updateBitString = - decodeBitString.slice(0, parseInt(credentialIndex)) + - revocationStatus + - decodeBitString.slice(parseInt(credentialIndex) + 1) - - const encodeUpdatedBitString = await utils.encodeBitString(updateBitString) - bitStringCredential.credential.credentialSubject.encodedList = encodeUpdatedBitString - await fetch(statusListCredentialURL, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ credentialsData: bitStringCredential }), - }) - - return { message: 'The credential has been revoked' } + const credential = await tenantAgent.credentials.getFormatData(credentialId) + return await this.w3CRevocationController._revokeW3C(credential) }) } catch (error) { throw ErrorHandlingService.handle(error) @@ -2097,27 +2013,7 @@ export class MultiTenancyController extends Controller { getIndex: GenericRecord[] }> { try { - const validateUrl = await utils.isValidUrl(bitCredentialStatusUrl) - if (!validateUrl) { - throw new BadRequestError(`Please provide a bit string credential id`) - } - - const bitStringCredentialDetails = await fetch(bitCredentialStatusUrl, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }) - - if (!bitStringCredentialDetails.ok) { - throw new InternalServerError(`${bitStringCredentialDetails.statusText}`) - } - - const bitStringCredential = (await bitStringCredentialDetails.json()) as BitStringCredential - if (!bitStringCredential?.credential && !bitStringCredential?.credential?.credentialSubject) { - throw new BadRequestError(`Invalid credentialSubjectUrl`) - } - + const bitStringCredential = await this.w3CRevocationController._getBitStringStatusListById(bitCredentialStatusUrl) return await this.agent.modules.tenants.withTenantAgent({ tenantId }, async (tenantAgent) => { const getIndex = await tenantAgent.genericRecords.findAllByQuery({ statusListCredentialURL: bitCredentialStatusUrl, diff --git a/src/controllers/w3cRevocation/w3cRevocationController.ts b/src/controllers/w3cRevocation/w3cRevocationController.ts index 995409eb..c705b04e 100644 --- a/src/controllers/w3cRevocation/w3cRevocationController.ts +++ b/src/controllers/w3cRevocation/w3cRevocationController.ts @@ -1,6 +1,12 @@ import type { RestAgentModules } from '../../cliAgent' import type { BitStringCredential } from '../types' -import type { W3cCredentialRecord, W3cJsonLdSignCredentialOptions } from '@credo-ts/core' +import type { AnonCredsCredentialFormat, LegacyIndyCredentialFormat } from '@credo-ts/anoncreds' +import type { + GetCredentialFormatDataReturn, + JsonLdCredentialFormat, + W3cCredentialRecord, + W3cJsonLdSignCredentialOptions, +} from '@credo-ts/core' import type { GenericRecord } from '@credo-ts/core/build/modules/generic-records/repository/GenericRecord' import { Agent, ClaimFormat } from '@credo-ts/core' @@ -18,7 +24,7 @@ import { Tags, Route, Controller, Post, Security, Body, Path, Get } from 'tsoa' @Route('/w3c/revocation') @Security('apiKey') @injectable() -export class StatusController extends Controller { +export class W3CRevocationController extends Controller { private agent: Agent public constructor(agent: Agent) { @@ -30,6 +36,63 @@ export class StatusController extends Controller { public async createBitstringStatusListCredential( @Body() signCredentialPayload: SignCredentialPayload ): Promise { + try { + const data = this._createBitstringStatusListCredential(signCredentialPayload) + const signCredential = await this.agent.w3cCredentials.signCredential( + data as unknown as W3cJsonLdSignCredentialOptions + ) + + const storCredential = await this.agent.w3cCredentials.storeCredential({ credential: signCredential }) + + return storCredential + } catch (error) { + throw ErrorHandlingService.handle(error) + } + } + + @Post('/revoke-credential/:credentialId') + public async revokeW3C(@Path('credentialId') credentialId: string): Promise<{ + message: string + }> { + try { + const credential = await this.agent.credentials.getFormatData(credentialId) + return await this._revokeW3C(credential) + } catch (error) { + throw ErrorHandlingService.handle(error) + } + } + + @Get('bitstring/status-list/:bitCredentialStatusUrl') + public async getBitStringStatusListById(@Path('bitCredentialStatusUrl') bitCredentialStatusUrl: string): Promise<{ + bitStringCredential: BitStringCredential + getIndex: GenericRecord[] + }> { + try { + const bitStringCredential = await this._getBitStringStatusListById(bitCredentialStatusUrl) + const getIndex = await this.agent.genericRecords.findAllByQuery({ + statusListCredentialURL: bitCredentialStatusUrl, + }) + + return { + bitStringCredential, + getIndex, + } + } catch (error) { + throw ErrorHandlingService.handle(error) + } + } + + @Get('bitstring/status-list/') + public async getAllBitStringStatusList(): Promise { + try { + const getBitStringCredentialStatusList = await this.agent.w3cCredentials.getAllCredentialRecords() + return getBitStringCredentialStatusList + } catch (error) { + throw ErrorHandlingService.handle(error) + } + } + + public async _createBitstringStatusListCredential(signCredentialPayload: SignCredentialPayload) { try { const { bitStringCredentialUrl, issuerDid, statusPurpose, bitStringLength } = signCredentialPayload const bitStringStatusListPurpose = statusPurpose ?? BitStringCredentialStatusPurpose.REVOCATION @@ -66,24 +129,18 @@ export class StatusController extends Controller { body: JSON.stringify({ credentialsData: data }), }) - const signCredential = await this.agent.w3cCredentials.signCredential( - data as unknown as W3cJsonLdSignCredentialOptions - ) - - const storCredential = await this.agent.w3cCredentials.storeCredential({ credential: signCredential }) - - return storCredential + return data } catch (error) { throw ErrorHandlingService.handle(error) } } - @Post('/revoke-credential/:credentialId') - public async revokeW3C(@Path('credentialId') credentialId: string): Promise<{ - message: string - }> { + public async _revokeW3C( + credential: GetCredentialFormatDataReturn< + (LegacyIndyCredentialFormat | JsonLdCredentialFormat | AnonCredsCredentialFormat)[] + > + ) { try { - const credential = await this.agent.credentials.getFormatData(credentialId) let credentialIndex let statusListCredentialURL const revocationStatus = 1 @@ -138,11 +195,7 @@ export class StatusController extends Controller { } } - @Get('bitstring/status-list/:bitCredentialStatusUrl') - public async getBitStringStatusListById(@Path('bitCredentialStatusUrl') bitCredentialStatusUrl: string): Promise<{ - bitStringCredential: BitStringCredential - getIndex: GenericRecord[] - }> { + public async _getBitStringStatusListById(bitCredentialStatusUrl: string) { try { const validateUrl = await utils.isValidUrl(bitCredentialStatusUrl) if (!validateUrl) { @@ -165,24 +218,7 @@ export class StatusController extends Controller { throw new BadRequestError(`Invalid credentialSubjectUrl`) } - const getIndex = await this.agent.genericRecords.findAllByQuery({ - statusListCredentialURL: bitCredentialStatusUrl, - }) - - return { - bitStringCredential, - getIndex, - } - } catch (error) { - throw ErrorHandlingService.handle(error) - } - } - - @Get('bitstring/status-list/') - public async getAllBitStringStatusList(): Promise { - try { - const getBitStringCredentialStatusList = await this.agent.w3cCredentials.getAllCredentialRecords() - return getBitStringCredentialStatusList + return bitStringCredential } catch (error) { throw ErrorHandlingService.handle(error) } diff --git a/src/utils/credentialStatusList.ts b/src/utils/credentialStatusList.ts index 5b54f84b..2ac23af0 100644 --- a/src/utils/credentialStatusList.ts +++ b/src/utils/credentialStatusList.ts @@ -2,6 +2,7 @@ import type { BitStringCredential, CredentialStatusList } from '../controllers/t import type { CredentialStatus } from '@credo-ts/core' import type { GenericRecord } from '@credo-ts/core/build/modules/generic-records/repository/GenericRecord' +import { randomInt } from 'crypto' import { promisify } from 'util' import * as zlib from 'zlib' @@ -9,7 +10,7 @@ import ErrorHandlingService from '../errorHandlingService' import { BadRequestError, ConflictError, InternalServerError } from '../errors/errors' async function generateBitStringStatus(length: number): Promise { - return Array.from({ length }, () => (Math.random() > 0.5 ? '1' : '0')).join('') + return Array.from({ length }, () => (randomInt(0, 2) === 1 ? '1' : '0')).join('') } async function encodeBitString(bitString: string): Promise { From 9572f1e66c823c02c179098baebfd89afbf58873 Mon Sep 17 00:00:00 2001 From: KulkarniShashank Date: Fri, 16 Aug 2024 17:36:38 +0530 Subject: [PATCH 10/15] fix: bug for create bit status string credential Signed-off-by: KulkarniShashank --- src/controllers/w3cRevocation/w3cRevocationController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/w3cRevocation/w3cRevocationController.ts b/src/controllers/w3cRevocation/w3cRevocationController.ts index c705b04e..fb1cb98a 100644 --- a/src/controllers/w3cRevocation/w3cRevocationController.ts +++ b/src/controllers/w3cRevocation/w3cRevocationController.ts @@ -37,7 +37,7 @@ export class W3CRevocationController extends Controller { @Body() signCredentialPayload: SignCredentialPayload ): Promise { try { - const data = this._createBitstringStatusListCredential(signCredentialPayload) + const data = await this._createBitstringStatusListCredential(signCredentialPayload) const signCredential = await this.agent.w3cCredentials.signCredential( data as unknown as W3cJsonLdSignCredentialOptions ) From 1f5a098fd77be7d2631cb9835a5c37155f98f51b Mon Sep 17 00:00:00 2001 From: KulkarniShashank Date: Wed, 28 Aug 2024 17:44:39 +0530 Subject: [PATCH 11/15] fix: added the pako package for the compress the bitstring status list credential Signed-off-by: KulkarniShashank --- package.json | 1 + ...ue and receive revocable credentials.patch | 347 ------------------ ...+core+0.5.3+004+revocable-credential.patch | 174 +++++++++ ...o-ts+core+0.5.3+005+added-pretty-vc.patch} | 6 +- src/utils/credentialStatusList.ts | 41 ++- 5 files changed, 203 insertions(+), 366 deletions(-) delete mode 100644 patches/@credo-ts+core+0.5.3+003+Change in credential Format for issue and receive revocable credentials.patch create mode 100644 patches/@credo-ts+core+0.5.3+004+revocable-credential.patch rename patches/{@credo-ts+core+0.5.3+004+added-pretty-vc-patch.patch => @credo-ts+core+0.5.3+005+added-pretty-vc.patch} (92%) diff --git a/package.json b/package.json index 796f3d2f..67a2b946 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "@types/jsonwebtoken": "^9.0.5", "@types/multer": "^1.4.7", "@types/node": "^18.18.8", + "@types/pako": "^2.0.3", "@types/ref-array-di": "^1.2.8", "@types/ref-struct-di": "^1.1.9", "@types/supertest": "^2.0.12", diff --git a/patches/@credo-ts+core+0.5.3+003+Change in credential Format for issue and receive revocable credentials.patch b/patches/@credo-ts+core+0.5.3+003+Change in credential Format for issue and receive revocable credentials.patch deleted file mode 100644 index 599b592c..00000000 --- a/patches/@credo-ts+core+0.5.3+003+Change in credential Format for issue and receive revocable credentials.patch +++ /dev/null @@ -1,347 +0,0 @@ -diff --git a/node_modules/@credo-ts/core/build/modules/credentials/formats/jsonld/JsonLdCredentialFormat.d.ts b/node_modules/@credo-ts/core/build/modules/credentials/formats/jsonld/JsonLdCredentialFormat.d.ts -index d12468b..09bd3b1 100644 ---- a/node_modules/@credo-ts/core/build/modules/credentials/formats/jsonld/JsonLdCredentialFormat.d.ts -+++ b/node_modules/@credo-ts/core/build/modules/credentials/formats/jsonld/JsonLdCredentialFormat.d.ts -@@ -10,7 +10,79 @@ export interface JsonCredential { - issuanceDate: string; - expirationDate?: string; - credentialSubject: SingleOrArray; -- [key: string]: unknown; -+ credentialStatus?: SingleOrArray -+ [key: string]: unknown -+} -+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 -+} -+ -+/** -+* "credentialStatus": { -+ "id": "https://example.com/credentials/status/8#492847", -+ "type": "BitstringStatusListEntry", -+ "statusPurpose": "message", -+ "statusListIndex": "492847", -+ "statusSize": 2, -+ "statusListCredential": "https://example.com/credentials/status/8", -+ "statusMessage": [ -+ {"status":"0x0", "message":"pending_review"}, -+ {"status":"0x1", "message":"accepted"}, -+ {"status":"0x2", "message":"rejected"}, -+ ... -+ ], -+ "statusReference": "https://example.org/status-dictionary/" -+} -+*/ -+ -+/** -+* "credentialStatus": [{ -+ "id": "https://example.com/credentials/status/3#94567", -+ "type": "BitstringStatusListEntry", -+ "statusPurpose": "revocation", -+ "statusListIndex": "94567", -+ "statusListCredential": "https://example.com/credentials/status/3" -+}, { -+ "id": "https://example.com/credentials/status/4#23452", -+ "type": "BitstringStatusListEntry", -+ "statusPurpose": "suspension", -+ "statusListIndex": "23452", -+ "statusListCredential": "https://example.com/credentials/status/4" -+}] -+*/ -+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 - } - /** - * Format for creating a jsonld proposal, offer or request. -diff --git a/node_modules/@credo-ts/core/build/modules/credentials/formats/jsonld/JsonLdCredentialFormatService.d.ts b/node_modules/@credo-ts/core/build/modules/credentials/formats/jsonld/JsonLdCredentialFormatService.d.ts -index 2006259..afc834e 100644 ---- a/node_modules/@credo-ts/core/build/modules/credentials/formats/jsonld/JsonLdCredentialFormatService.d.ts -+++ b/node_modules/@credo-ts/core/build/modules/credentials/formats/jsonld/JsonLdCredentialFormatService.d.ts -@@ -27,6 +27,7 @@ export declare class JsonLdCredentialFormatService implements CredentialFormatSe - * @returns object containing associated attachment, formats and offersAttach elements - * - */ -+ // Trail: W3C revocation: Change in payload - createOffer(agentContext: AgentContext, { credentialFormats, attachmentId }: CredentialFormatCreateOfferOptions): Promise; - processOffer(agentContext: AgentContext, { attachment }: CredentialFormatProcessOptions): Promise; - acceptOffer(agentContext: AgentContext, { attachmentId, offerAttachment }: CredentialFormatAcceptOfferOptions): Promise; -diff --git a/node_modules/@credo-ts/core/build/modules/credentials/protocol/v2/V2CredentialProtocol.js b/node_modules/@credo-ts/core/build/modules/credentials/protocol/v2/V2CredentialProtocol.js -index fb1fb9d..ac871bf 100644 ---- a/node_modules/@credo-ts/core/build/modules/credentials/protocol/v2/V2CredentialProtocol.js -+++ b/node_modules/@credo-ts/core/build/modules/credentials/protocol/v2/V2CredentialProtocol.js -@@ -530,6 +530,7 @@ class V2CredentialProtocol extends BaseCredentialProtocol_1.BaseCredentialProtoc - if (formatServices.length === 0) { - throw new error_1.CredoError(`Unable to accept request. No supported formats provided as input or in request message`); - } -+ - const message = await this.credentialFormatCoordinator.acceptRequest(agentContext, { - credentialRecord, - formatServices, -@@ -584,6 +585,7 @@ class V2CredentialProtocol extends BaseCredentialProtocol_1.BaseCredentialProtoc - if (formatServices.length === 0) { - throw new error_1.CredoError(`Unable to process credential. No supported formats`); - } -+ console.log("--------------------------------------------------------------------"); - await this.credentialFormatCoordinator.processCredential(messageContext.agentContext, { - credentialRecord, - formatServices, -diff --git a/node_modules/@credo-ts/core/build/modules/vc/data-integrity/W3cJsonLdCredentialService.d.ts b/node_modules/@credo-ts/core/build/modules/vc/data-integrity/W3cJsonLdCredentialService.d.ts -index 4176d33..12ce7e8 100644 ---- a/node_modules/@credo-ts/core/build/modules/vc/data-integrity/W3cJsonLdCredentialService.d.ts -+++ b/node_modules/@credo-ts/core/build/modules/vc/data-integrity/W3cJsonLdCredentialService.d.ts -@@ -13,6 +13,7 @@ import { W3cJsonLdVerifiablePresentation } from './models/W3cJsonLdVerifiablePre - export declare class W3cJsonLdCredentialService { - private signatureSuiteRegistry; - private w3cCredentialsModuleConfig; -+ private w3cCredentialService; - constructor(signatureSuiteRegistry: SignatureSuiteRegistry, w3cCredentialsModuleConfig: W3cCredentialsModuleConfig); - /** - * Signs a credential -diff --git a/node_modules/@credo-ts/core/build/modules/vc/data-integrity/W3cJsonLdCredentialService.js b/node_modules/@credo-ts/core/build/modules/vc/data-integrity/W3cJsonLdCredentialService.js -index 3fa8bf2..13dc96f 100644 ---- a/node_modules/@credo-ts/core/build/modules/vc/data-integrity/W3cJsonLdCredentialService.js -+++ b/node_modules/@credo-ts/core/build/modules/vc/data-integrity/W3cJsonLdCredentialService.js -@@ -39,6 +39,9 @@ const jsonld_1 = __importDefault(require("./libraries/jsonld")); - const vc_1 = __importDefault(require("./libraries/vc")); - const models_1 = require("./models"); - const W3cJsonLdVerifiablePresentation_1 = require("./models/W3cJsonLdVerifiablePresentation"); -+const { promisify } = require('util'); -+const zlib = require('zlib'); -+ - /** - * Supports signing and verification of credentials according to the [Verifiable Credential Data Model](https://www.w3.org/TR/vc-data-model) - * using [Data Integrity Proof](https://www.w3.org/TR/vc-data-model/#data-integrity-proofs). -@@ -98,14 +101,41 @@ let W3cJsonLdCredentialService = class W3cJsonLdCredentialService { - credential: utils_1.JsonTransformer.toJSON(options.credential), - suite: suites, - documentLoader: this.w3cCredentialsModuleConfig.documentLoader(agentContext), -- checkStatus: ({ credential }) => { -+ checkStatus: async ({ credential }) => { -+ -+ // Note: currety comment this change to avoid passing credetials with credentialStatus - // Only throw error if credentialStatus is present - if (verifyCredentialStatus && 'credentialStatus' in credential) { -- throw new error_1.CredoError('Verifying credential status for JSON-LD credentials is currently not supported'); -+ // TODO: add logic to verify credentialStatus -+ // throw new CredoError('Verifying credential status for JSON-LD credentials is currently not supported') -+ const credentialStatusURL = credential.credentialStatus.statusListCredential; -+ const bitStringStatusListCredential = await agentContext.config.agentDependencies.fetch(credentialStatusURL, { -+ method: 'GET', -+ }); -+ -+ if (!bitStringStatusListCredential.ok) { -+ throw new error_1.CredoError(`HTTP error! Status: ${bitStringStatusListCredential.status}`); -+ } -+ -+ const bitStringCredential = await bitStringStatusListCredential.json(); -+ const encodedBitString = bitStringCredential.credential.credentialSubject.encodedList -+ const gunzip = promisify(zlib.gunzip); -+ -+ const compressedBuffer = Buffer.from(encodedBitString, 'base64'); -+ const decompressedBuffer = await gunzip(compressedBuffer); -+ const decodedBitString = decompressedBuffer.toString('binary'); -+ -+ if (credential.credentialStatus.statusListIndex < 0 || credential.credentialStatus.statusListIndex >= decodedBitString.length) { -+ throw new error_1.CredoError('Index out of bounds'); -+ } -+ -+ if(decodedBitString[credential.credentialStatus.statusListIndex] === '1'){ -+ throw new error_1.CredoError(`Credential at index ${credential.credentialStatus.statusListIndex} is revoked.`); -+ } - } - return { -- verified: true, -- }; -+ verified: true, -+ } - }, - }; - // this is a hack because vcjs throws if purpose is passed as undefined or null -@@ -219,7 +249,41 @@ let W3cJsonLdCredentialService = class W3cJsonLdCredentialService { - challenge: options.challenge, - domain: options.domain, - documentLoader: this.w3cCredentialsModuleConfig.documentLoader(agentContext), -+ checkStatus: async ({ credential }) => { -+ -+ if ('credentialStatus' in credential) { -+ -+ const credentialStatusURL = credential.credentialStatus.statusListCredential; -+ const bitStringStatusListCredential = await agentContext.config.agentDependencies.fetch(credentialStatusURL, { -+ method: 'GET', -+ }); -+ -+ if (!bitStringStatusListCredential.ok) { -+ throw new error_1.CredoError(`HTTP error! Status: ${bitStringStatusListCredential.status}`); -+ } -+ -+ const bitStringCredential = await bitStringStatusListCredential.json(); -+ const encodedBitString = bitStringCredential.credential.credentialSubject.encodedList -+ const gunzip = promisify(zlib.gunzip); -+ -+ const compressedBuffer = Buffer.from(encodedBitString, 'base64'); -+ const decompressedBuffer = await gunzip(compressedBuffer); -+ const decodedBitString = decompressedBuffer.toString('binary'); -+ -+ if (credential.credentialStatus.statusListIndex < 0 || credential.credentialStatus.statusListIndex >= decodedBitString.length) { -+ throw new error_1.CredoError('Index out of bounds'); -+ } -+ -+ if(decodedBitString[credential.credentialStatus.statusListIndex] === '1'){ -+ throw new error_1.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 - if (options.purpose) { - verifyOptions['presentationPurpose'] = options.purpose; -@@ -305,7 +369,8 @@ let W3cJsonLdCredentialService = class W3cJsonLdCredentialService { - W3cJsonLdCredentialService = __decorate([ - (0, plugins_1.injectable)(), - __metadata("design:paramtypes", [SignatureSuiteRegistry_1.SignatureSuiteRegistry, -- W3cCredentialsModuleConfig_1.W3cCredentialsModuleConfig]) -+ W3cCredentialsModuleConfig_1.W3cCredentialsModuleConfig -+ ]) - ], W3cJsonLdCredentialService); - exports.W3cJsonLdCredentialService = W3cJsonLdCredentialService; - //# sourceMappingURL=W3cJsonLdCredentialService.js.map -\ No newline at end of file -diff --git a/node_modules/@credo-ts/core/build/modules/vc/models/credential/W3cCredential.d.ts b/node_modules/@credo-ts/core/build/modules/vc/models/credential/W3cCredential.d.ts -index dbd8dba..b3deac2 100644 ---- a/node_modules/@credo-ts/core/build/modules/vc/models/credential/W3cCredential.d.ts -+++ b/node_modules/@credo-ts/core/build/modules/vc/models/credential/W3cCredential.d.ts -@@ -4,7 +4,7 @@ import type { JsonObject } from '../../../../types'; - import type { ValidationOptions } from 'class-validator'; - import { SingleOrArray } from '../../../../utils/type'; - import { W3cCredentialSchema } from './W3cCredentialSchema'; --import { W3cCredentialStatus } from './W3cCredentialStatus'; -+import { W3cCredentialStatus, W3cCredentialStatusOptions } from './W3cCredentialStatus'; - import { W3cCredentialSubject } from './W3cCredentialSubject'; - import { W3cIssuer } from './W3cIssuer'; - export interface W3cCredentialOptions { -@@ -15,7 +15,7 @@ export interface W3cCredentialOptions { - issuanceDate: string; - expirationDate?: string; - credentialSubject: SingleOrArray; -- credentialStatus?: W3cCredentialStatus; -+ credentialStatus: W3cCredentialStatus | Array; - } - export declare class W3cCredential { - constructor(options: W3cCredentialOptions); -@@ -27,7 +27,7 @@ export declare class W3cCredential { - expirationDate?: string; - credentialSubject: SingleOrArray; - credentialSchema?: SingleOrArray; -- credentialStatus?: W3cCredentialStatus; -+ credentialStatus: W3cCredentialStatus | Array; - get issuerId(): string; - get credentialSchemaIds(): string[]; - get credentialSubjectIds(): string[]; -diff --git a/node_modules/@credo-ts/core/build/modules/vc/models/credential/W3cCredential.js b/node_modules/@credo-ts/core/build/modules/vc/models/credential/W3cCredential.js -index 800214f..b064d9e 100644 ---- a/node_modules/@credo-ts/core/build/modules/vc/models/credential/W3cCredential.js -+++ b/node_modules/@credo-ts/core/build/modules/vc/models/credential/W3cCredential.js -@@ -35,10 +35,12 @@ class W3cCredential { - this.expirationDate = options.expirationDate; - this.credentialSubject = (0, utils_1.mapSingleOrArray)(options.credentialSubject, (subject) => subject instanceof W3cCredentialSubject_1.W3cCredentialSubject ? subject : new W3cCredentialSubject_1.W3cCredentialSubject(subject)); - if (options.credentialStatus) { -- this.credentialStatus = -- options.credentialStatus instanceof W3cCredentialStatus_1.W3cCredentialStatus -- ? options.credentialStatus -- : new W3cCredentialStatus_1.W3cCredentialStatus(options.credentialStatus); -+ console.log('options.credentialStatus----', options.credentialStatus); -+ this.credentialStatus = (0, utils_1.mapSingleOrArray)(options.credentialStatus, (status) => status instanceof W3cCredentialStatus_1.W3cCredentialStatus ? status : new W3cCredentialStatus_1.W3cCredentialStatus(status)); -+ // this.credentialStatus = -+ // options.credentialStatus instanceof W3cCredentialStatus_1.W3cCredentialStatus -+ // ? options.credentialStatus -+ // : new W3cCredentialStatus_1.W3cCredentialStatus(options.credentialStatus); - } - } - } -diff --git a/node_modules/@credo-ts/core/build/modules/vc/models/credential/W3cCredentialStatus.d.ts b/node_modules/@credo-ts/core/build/modules/vc/models/credential/W3cCredentialStatus.d.ts -index c1f4743..f2ac20d 100644 ---- a/node_modules/@credo-ts/core/build/modules/vc/models/credential/W3cCredentialStatus.d.ts -+++ b/node_modules/@credo-ts/core/build/modules/vc/models/credential/W3cCredentialStatus.d.ts -@@ -1,9 +1,15 @@ - export interface W3cCredentialStatusOptions { - id: string; - type: string; -+ statusPurpose: string; -+ statusListIndex: string; -+ statusListCredential: string; - } - export declare class W3cCredentialStatus { - constructor(options: W3cCredentialStatusOptions); - id: string; - type: string; -+ statusPurpose: string; -+ statusListIndex: string; -+ statusListCredential: string; - } -diff --git a/node_modules/@credo-ts/core/build/modules/vc/models/credential/W3cCredentialStatus.js b/node_modules/@credo-ts/core/build/modules/vc/models/credential/W3cCredentialStatus.js -index 81355eb..467ebd2 100644 ---- a/node_modules/@credo-ts/core/build/modules/vc/models/credential/W3cCredentialStatus.js -+++ b/node_modules/@credo-ts/core/build/modules/vc/models/credential/W3cCredentialStatus.js -@@ -17,6 +17,9 @@ class W3cCredentialStatus { - if (options) { - this.id = options.id; - this.type = options.type; -+ this.statusPurpose = options.statusPurpose; -+ this.statusListIndex = options.statusListIndex; -+ this.statusListCredential = options.statusListCredential; - } - } - } -@@ -28,5 +31,17 @@ __decorate([ - (0, class_validator_1.IsString)(), - __metadata("design:type", String) - ], W3cCredentialStatus.prototype, "type", void 0); -+__decorate([ -+ (0, class_validator_1.IsString)(), -+ __metadata("design:type", String) -+], W3cCredentialStatus.prototype, "statusPurpose", void 0); -+__decorate([ -+ (0, class_validator_1.IsString)(), -+ __metadata("design:type", String) -+], W3cCredentialStatus.prototype, "statusListIndex", void 0); -+__decorate([ -+ (0, class_validator_1.IsString)(), -+ __metadata("design:type", String) -+], W3cCredentialStatus.prototype, "statusListCredential", void 0); - exports.W3cCredentialStatus = W3cCredentialStatus; - //# sourceMappingURL=W3cCredentialStatus.js.map -\ No newline at end of file diff --git a/patches/@credo-ts+core+0.5.3+004+revocable-credential.patch b/patches/@credo-ts+core+0.5.3+004+revocable-credential.patch new file mode 100644 index 00000000..6c878feb --- /dev/null +++ b/patches/@credo-ts+core+0.5.3+004+revocable-credential.patch @@ -0,0 +1,174 @@ +diff --git a/node_modules/@credo-ts/core/build/modules/credentials/formats/jsonld/JsonLdCredentialFormat.d.ts b/node_modules/@credo-ts/core/build/modules/credentials/formats/jsonld/JsonLdCredentialFormat.d.ts +index d12468b..78253b5 100644 +--- a/node_modules/@credo-ts/core/build/modules/credentials/formats/jsonld/JsonLdCredentialFormat.d.ts ++++ b/node_modules/@credo-ts/core/build/modules/credentials/formats/jsonld/JsonLdCredentialFormat.d.ts +@@ -11,7 +11,81 @@ export interface JsonCredential { + 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 ++} ++/** ++ "credentialStatus": { ++ "id": "https://example.com/credentials/status/8#492847", ++ "type": "BitstringStatusListEntry", ++ "statusPurpose": "message", ++ "statusListIndex": "492847", ++ "statusSize": 2, ++ "statusListCredential": "https://example.com/credentials/status/8", ++ "statusMessage": [ ++ {"status":"0x0", "message":"pending_review"}, ++ {"status":"0x1", "message":"accepted"}, ++ {"status":"0x2", "message":"rejected"}, ++ ... ++ ], ++ "statusReference": "https://example.org/status-dictionary/" ++} ++*/ ++ ++/** ++* "credentialStatus": [{ ++ "id": "https://example.com/credentials/status/3#94567", ++ "type": "BitstringStatusListEntry", ++ "statusPurpose": "revocation", ++ "statusListIndex": "94567", ++ "statusListCredential": "https://example.com/credentials/status/3" ++}, { ++ "id": "https://example.com/credentials/status/4#23452", ++ "type": "BitstringStatusListEntry", ++ "statusPurpose": "suspension", ++ "statusListIndex": "23452", ++ "statusListCredential": "https://example.com/credentials/status/4" ++}] ++*/ ++ ++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 ++} ++ + /** + * Format for creating a jsonld proposal, offer or request. + */ +diff --git a/node_modules/@credo-ts/core/build/modules/vc/data-integrity/W3cJsonLdCredentialService.js b/node_modules/@credo-ts/core/build/modules/vc/data-integrity/W3cJsonLdCredentialService.js +index 3fa8bf2..f28be5c 100644 +--- a/node_modules/@credo-ts/core/build/modules/vc/data-integrity/W3cJsonLdCredentialService.js ++++ b/node_modules/@credo-ts/core/build/modules/vc/data-integrity/W3cJsonLdCredentialService.js +@@ -39,6 +39,7 @@ const jsonld_1 = __importDefault(require("./libraries/jsonld")); + const vc_1 = __importDefault(require("./libraries/vc")); + const models_1 = require("./models"); + const W3cJsonLdVerifiablePresentation_1 = require("./models/W3cJsonLdVerifiablePresentation"); ++const pako = require('pako'); + /** + * Supports signing and verification of credentials according to the [Verifiable Credential Data Model](https://www.w3.org/TR/vc-data-model) + * using [Data Integrity Proof](https://www.w3.org/TR/vc-data-model/#data-integrity-proofs). +@@ -98,10 +99,35 @@ let W3cJsonLdCredentialService = class W3cJsonLdCredentialService { + credential: utils_1.JsonTransformer.toJSON(options.credential), + suite: suites, + documentLoader: this.w3cCredentialsModuleConfig.documentLoader(agentContext), +- checkStatus: ({ credential }) => { ++ checkStatus: async ({ credential }) => { + // Only throw error if credentialStatus is present + if (verifyCredentialStatus && 'credentialStatus' in credential) { +- throw new error_1.CredoError('Verifying credential status for JSON-LD credentials is currently not supported'); ++ // throw new error_1.CredoError('Verifying credential status for JSON-LD credentials is currently not supported'); ++ // TODO: add logic to verify credentialStatus ++ // throw new CredoError('Verifying credential status for JSON-LD credentials is currently not supported') ++ const credentialStatusURL = credential.credentialStatus.statusListCredential; ++ const bitStringStatusListCredential = await agentContext.config.agentDependencies.fetch(credentialStatusURL, { ++ method: 'GET', ++ }); ++ ++ if (!bitStringStatusListCredential.ok) { ++ throw new error_1.CredoError(`HTTP error! Status: ${bitStringStatusListCredential.status}`); ++ } ++ ++ const bitStringCredential = await bitStringStatusListCredential.json(); ++ 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' }) ++ ++ if (credential.credentialStatus.statusListIndex < 0 || credential.credentialStatus.statusListIndex >= decodedBitString.length) { ++ throw new error_1.CredoError('Index out of bounds'); ++ } ++ ++ if(decodedBitString[credential.credentialStatus.statusListIndex] === '1'){ ++ throw new error_1.CredoError(`Credential at index ${credential.credentialStatus.statusListIndex} is revoked.`); ++ } + } + return { + verified: true, +@@ -219,6 +245,37 @@ let W3cJsonLdCredentialService = class W3cJsonLdCredentialService { + challenge: options.challenge, + domain: options.domain, + documentLoader: this.w3cCredentialsModuleConfig.documentLoader(agentContext), ++ checkStatus: async ({ credential }) => { ++ if ('credentialStatus' in credential) { ++ ++ const credentialStatusURL = credential.credentialStatus.statusListCredential; ++ const bitStringStatusListCredential = await agentContext.config.agentDependencies.fetch(credentialStatusURL, { ++ method: 'GET', ++ }); ++ ++ if (!bitStringStatusListCredential.ok) { ++ throw new error_1.CredoError(`HTTP error! Status: ${bitStringStatusListCredential.status}`); ++ } ++ ++ const bitStringCredential = await bitStringStatusListCredential.json(); ++ 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' }) ++ ++ if (credential.credentialStatus.statusListIndex < 0 || credential.credentialStatus.statusListIndex >= decodedBitString.length) { ++ throw new error_1.CredoError('Index out of bounds'); ++ } ++ ++ if(decodedBitString[credential.credentialStatus.statusListIndex] === '1'){ ++ throw new error_1.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 + if (options.purpose) { diff --git a/patches/@credo-ts+core+0.5.3+004+added-pretty-vc-patch.patch b/patches/@credo-ts+core+0.5.3+005+added-pretty-vc.patch similarity index 92% rename from patches/@credo-ts+core+0.5.3+004+added-pretty-vc-patch.patch rename to patches/@credo-ts+core+0.5.3+005+added-pretty-vc.patch index 087a9334..0e92cdee 100644 --- a/patches/@credo-ts+core+0.5.3+004+added-pretty-vc-patch.patch +++ b/patches/@credo-ts+core+0.5.3+005+added-pretty-vc.patch @@ -1,13 +1,13 @@ diff --git a/node_modules/@credo-ts/core/build/modules/credentials/formats/jsonld/JsonLdCredentialFormat.d.ts b/node_modules/@credo-ts/core/build/modules/credentials/formats/jsonld/JsonLdCredentialFormat.d.ts -index 09bd3b1..b902670 100644 +index 78253b5..0b3dd52 100644 --- a/node_modules/@credo-ts/core/build/modules/credentials/formats/jsonld/JsonLdCredentialFormat.d.ts +++ b/node_modules/@credo-ts/core/build/modules/credentials/formats/jsonld/JsonLdCredentialFormat.d.ts @@ -12,6 +12,8 @@ export interface JsonCredential { credentialSubject: SingleOrArray; + [key: string]: unknown; credentialStatus?: SingleOrArray - [key: string]: unknown + //TODO change type + prettyVc?: any; } - type CredentialStatusType = 'BitstringStatusListEntry' + type CredentialStatusType = 'BitstringStatusListEntry' diff --git a/src/utils/credentialStatusList.ts b/src/utils/credentialStatusList.ts index 2ac23af0..97c8b4ae 100644 --- a/src/utils/credentialStatusList.ts +++ b/src/utils/credentialStatusList.ts @@ -3,8 +3,10 @@ import type { CredentialStatus } from '@credo-ts/core' import type { GenericRecord } from '@credo-ts/core/build/modules/generic-records/repository/GenericRecord' import { randomInt } from 'crypto' -import { promisify } from 'util' -import * as zlib from 'zlib' +// import { promisify } from 'util' +// import * as zlib from 'zlib' +// eslint-disable-next-line import/no-extraneous-dependencies +import pako from 'pako' import ErrorHandlingService from '../errorHandlingService' import { BadRequestError, ConflictError, InternalServerError } from '../errors/errors' @@ -14,17 +16,25 @@ async function generateBitStringStatus(length: number): Promise { } async function encodeBitString(bitString: string): Promise { - const gzip = promisify(zlib.gzip) - const buffer = Buffer.from(bitString, 'binary') - const compressedBuffer = await gzip(buffer) - return compressedBuffer.toString('base64') + // const gzip = promisify(zlib.gzip) + // const buffer = Buffer.from(bitString, 'binary') + // const compressedBuffer = await gzip(buffer) + // return compressedBuffer.toString('base64') + + // 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') } async function decodeBitSting(bitString: string): Promise { - const gunzip = promisify(zlib.gunzip) - const compressedBuffer = Buffer.from(bitString, 'base64') - const decompressedBuffer = await gunzip(compressedBuffer) - return decompressedBuffer.toString('binary') + // 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 } async function isValidUrl(url: string) { @@ -74,22 +84,21 @@ async function getCredentialStatus( } const encodedBitString = bitStringCredential.credential.credentialSubject.encodedList - const gunzip = promisify(zlib.gunzip) + const compressedBuffer = Uint8Array.from(atob(encodedBitString), (c) => c.charCodeAt(0)) - const compressedBuffer = Buffer.from(encodedBitString, 'base64') - const decompressedBuffer = await gunzip(compressedBuffer) - const decodedBitString = decompressedBuffer.toString('binary') + const decompressedBuffer = pako.ungzip(compressedBuffer, { to: 'string' }) + // const decodedBitString = decompressedBuffer.toString('binary') let index const arrayIndex: number[] = [] if (getIndex.length === 0) { - index = decodedBitString.indexOf('0') + index = decompressedBuffer.indexOf('0') } else { getIndex.find((record) => { arrayIndex.push(Number(record.content.index)) }) - index = await getAvailableIndex(decodedBitString, arrayIndex) + index = await getAvailableIndex(decompressedBuffer, arrayIndex) } if (index === -1) { From 3983dfce83ea500de5d90dcc65bb11c29fe25cbb Mon Sep 17 00:00:00 2001 From: KulkarniShashank Date: Tue, 3 Sep 2024 17:29:45 +0530 Subject: [PATCH 12/15] refactor: added revocation notification for dedicated and multi-tenancy Signed-off-by: KulkarniShashank --- .../multi-tenancy/MultiTenancyController.ts | 18 +++++++++++------- .../w3cRevocation/w3cRevocationController.ts | 18 +++++++++++++----- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/controllers/multi-tenancy/MultiTenancyController.ts b/src/controllers/multi-tenancy/MultiTenancyController.ts index 2379911a..227d67a3 100644 --- a/src/controllers/multi-tenancy/MultiTenancyController.ts +++ b/src/controllers/multi-tenancy/MultiTenancyController.ts @@ -1987,16 +1987,20 @@ export class MultiTenancyController extends Controller { @Security('apiKey') @Post('/revoke-credential/:credentialId/:tenantId') - public async revokeW3C( - @Path('credentialId') credentialId: string, - @Path('tenantId') tenantId: string - ): Promise<{ - message: string - }> { + public async revokeW3C(@Path('credentialId') credentialId: string, @Path('tenantId') tenantId: string) { + let sendNotification try { return await this.agent.modules.tenants.withTenantAgent({ tenantId }, async (tenantAgent) => { const credential = await tenantAgent.credentials.getFormatData(credentialId) - return await this.w3CRevocationController._revokeW3C(credential) + const { credentialIndex, statusListCredentialURL } = await this.w3CRevocationController._revokeW3C(credential) + const revocationFormat = `${statusListCredentialURL}::${credentialIndex}` + + sendNotification = await tenantAgent.credentials.sendRevocationNotification({ + credentialRecordId: credentialId, + revocationId: statusListCredentialURL, + revocationFormat, + }) + return sendNotification }) } catch (error) { throw ErrorHandlingService.handle(error) diff --git a/src/controllers/w3cRevocation/w3cRevocationController.ts b/src/controllers/w3cRevocation/w3cRevocationController.ts index fb1cb98a..7601d56d 100644 --- a/src/controllers/w3cRevocation/w3cRevocationController.ts +++ b/src/controllers/w3cRevocation/w3cRevocationController.ts @@ -38,6 +38,7 @@ export class W3CRevocationController extends Controller { ): Promise { try { const data = await this._createBitstringStatusListCredential(signCredentialPayload) + const signCredential = await this.agent.w3cCredentials.signCredential( data as unknown as W3cJsonLdSignCredentialOptions ) @@ -51,12 +52,19 @@ export class W3CRevocationController extends Controller { } @Post('/revoke-credential/:credentialId') - public async revokeW3C(@Path('credentialId') credentialId: string): Promise<{ - message: string - }> { + public async revokeW3C(@Path('credentialId') credentialId: string) { + let sendNotification try { const credential = await this.agent.credentials.getFormatData(credentialId) - return await this._revokeW3C(credential) + const { credentialIndex, statusListCredentialURL } = await this._revokeW3C(credential) + const revocationFormat = `${statusListCredentialURL}::${credentialIndex}` + + sendNotification = await this.agent.credentials.sendRevocationNotification({ + credentialRecordId: credentialId, + revocationId: statusListCredentialURL, + revocationFormat, + }) + return sendNotification } catch (error) { throw ErrorHandlingService.handle(error) } @@ -189,7 +197,7 @@ export class W3CRevocationController extends Controller { body: JSON.stringify({ credentialsData: bitStringCredential }), }) - return { message: 'The credential has been revoked' } + return { credentialIndex, statusListCredentialURL } } catch (error) { throw ErrorHandlingService.handle(error) } From 3f4f66dde8e7579f27fdf0d7f09ea32cf8afbb2e Mon Sep 17 00:00:00 2001 From: KulkarniShashank Date: Tue, 3 Sep 2024 17:37:37 +0530 Subject: [PATCH 13/15] fix: revocation notification payload Signed-off-by: KulkarniShashank --- src/controllers/multi-tenancy/MultiTenancyController.ts | 6 +++--- src/controllers/w3cRevocation/w3cRevocationController.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/controllers/multi-tenancy/MultiTenancyController.ts b/src/controllers/multi-tenancy/MultiTenancyController.ts index 227d67a3..116efdc3 100644 --- a/src/controllers/multi-tenancy/MultiTenancyController.ts +++ b/src/controllers/multi-tenancy/MultiTenancyController.ts @@ -1993,12 +1993,12 @@ export class MultiTenancyController extends Controller { return await this.agent.modules.tenants.withTenantAgent({ tenantId }, async (tenantAgent) => { const credential = await tenantAgent.credentials.getFormatData(credentialId) const { credentialIndex, statusListCredentialURL } = await this.w3CRevocationController._revokeW3C(credential) - const revocationFormat = `${statusListCredentialURL}::${credentialIndex}` + const revocationId = `${statusListCredentialURL}::${credentialIndex}` sendNotification = await tenantAgent.credentials.sendRevocationNotification({ credentialRecordId: credentialId, - revocationId: statusListCredentialURL, - revocationFormat, + revocationId, + revocationFormat: 'jsonld', }) return sendNotification }) diff --git a/src/controllers/w3cRevocation/w3cRevocationController.ts b/src/controllers/w3cRevocation/w3cRevocationController.ts index 7601d56d..2949a68b 100644 --- a/src/controllers/w3cRevocation/w3cRevocationController.ts +++ b/src/controllers/w3cRevocation/w3cRevocationController.ts @@ -57,12 +57,12 @@ export class W3CRevocationController extends Controller { try { const credential = await this.agent.credentials.getFormatData(credentialId) const { credentialIndex, statusListCredentialURL } = await this._revokeW3C(credential) - const revocationFormat = `${statusListCredentialURL}::${credentialIndex}` + const revocationId = `${statusListCredentialURL}::${credentialIndex}` sendNotification = await this.agent.credentials.sendRevocationNotification({ credentialRecordId: credentialId, - revocationId: statusListCredentialURL, - revocationFormat, + revocationId, + revocationFormat: 'jsonld', }) return sendNotification } catch (error) { From f7d0c8c4734326fde9e4dcae73f4bf6a9d298d2f Mon Sep 17 00:00:00 2001 From: KulkarniShashank Date: Wed, 4 Sep 2024 15:56:41 +0530 Subject: [PATCH 14/15] feat: w3c revocation notification Signed-off-by: KulkarniShashank --- ...re+0.5.3+006+revocation-notification.patch | 127 ++++++++++++++++++ .../multi-tenancy/MultiTenancyController.ts | 1 + .../w3cRevocation/w3cRevocationController.ts | 1 + 3 files changed, 129 insertions(+) create mode 100644 patches/@credo-ts+core+0.5.3+006+revocation-notification.patch diff --git a/patches/@credo-ts+core+0.5.3+006+revocation-notification.patch b/patches/@credo-ts+core+0.5.3+006+revocation-notification.patch new file mode 100644 index 00000000..7539d3a4 --- /dev/null +++ b/patches/@credo-ts+core+0.5.3+006+revocation-notification.patch @@ -0,0 +1,127 @@ +diff --git a/node_modules/@credo-ts/core/build/modules/credentials/formats/jsonld/JsonLdCredentialFormatService.js b/node_modules/@credo-ts/core/build/modules/credentials/formats/jsonld/JsonLdCredentialFormatService.js +index 83f99b7..152cf2a 100644 +--- a/node_modules/@credo-ts/core/build/modules/credentials/formats/jsonld/JsonLdCredentialFormatService.js ++++ b/node_modules/@credo-ts/core/build/modules/credentials/formats/jsonld/JsonLdCredentialFormatService.js +@@ -75,7 +75,7 @@ class JsonLdCredentialFormatService { + * @returns object containing associated attachment, formats and offersAttach elements + * + */ +- async createOffer(agentContext, { credentialFormats, attachmentId }) { ++ async createOffer(agentContext, { credentialFormats, attachmentId, credentialRecord }) { + // if the offer has an attachment Id use that, otherwise the generated id of the formats object + const format = new CredentialFormatSpec_1.CredentialFormatSpec({ + attachmentId, +@@ -88,6 +88,12 @@ class JsonLdCredentialFormatService { + // validate + JsonTransformer_1.JsonTransformer.fromJSON(jsonLdFormat.credential, JsonLdCredentialDetail_1.JsonLdCredentialDetail); + const attachment = this.getFormatData(jsonLdFormat, format.attachmentId); ++ if (jsonLdFormat.credential.credentialStatus) { ++ credentialRecord.setTags({ ++ statusListCredential: jsonLdFormat.credential.credentialStatus.statusListCredential, ++ statusListIndex: jsonLdFormat.credential.credentialStatus.statusListIndex, ++ }) ++ } + return { format, attachment }; + } + async processOffer(agentContext, { attachment }) { +@@ -136,7 +142,7 @@ class JsonLdCredentialFormatService { + // validate + JsonTransformer_1.JsonTransformer.fromJSON(requestJson, JsonLdCredentialDetail_1.JsonLdCredentialDetail); + } +- async acceptRequest(agentContext, { credentialFormats, attachmentId, requestAttachment }) { ++ async acceptRequest(agentContext, { credentialFormats, attachmentId, requestAttachment, credentialRecord }) { + var _a, _b; + const w3cJsonLdCredentialService = agentContext.dependencyManager.resolve(W3cJsonLdCredentialService_1.W3cJsonLdCredentialService); + // sign credential here. credential to be signed is received as the request attachment +@@ -164,6 +170,13 @@ class JsonLdCredentialFormatService { + proofType: credentialRequest.options.proofType, + verificationMethod: verificationMethod, + }); ++ // If the credential is revocable, store the revocation identifiers in the credential record ++ if (credential.credentialStatus) { ++ credentialRecord.setTags({ ++ statusListCredential: credential.credentialStatus.statusListCredential, ++ statusListIndex: credential.credentialStatus.statusListIndex, ++ }) ++ } + const attachment = this.getFormatData(JsonTransformer_1.JsonTransformer.toJSON(verifiableCredential), format.attachmentId); + return { format, attachment }; + } +@@ -216,6 +229,12 @@ class JsonLdCredentialFormatService { + const verifiableCredential = await w3cCredentialService.storeCredential(agentContext, { + credential, + }); ++ if (credential.credentialStatus) { ++ credentialRecord.setTags({ ++ statusListCredential: credential.credentialStatus.statusListCredential, ++ statusListIndex: credential.credentialStatus.statusListIndex, ++ }) ++ } + credentialRecord.credentials.push({ + credentialRecordType: this.credentialRecordType, + credentialRecordId: verifiableCredential.id, +diff --git a/node_modules/@credo-ts/core/build/modules/credentials/protocol/revocation-notification/services/RevocationNotificationService.js b/node_modules/@credo-ts/core/build/modules/credentials/protocol/revocation-notification/services/RevocationNotificationService.js +index 66ed4e5..083dfce 100644 +--- a/node_modules/@credo-ts/core/build/modules/credentials/protocol/revocation-notification/services/RevocationNotificationService.js ++++ b/node_modules/@credo-ts/core/build/modules/credentials/protocol/revocation-notification/services/RevocationNotificationService.js +@@ -32,10 +32,17 @@ let RevocationNotificationService = class RevocationNotificationService { + this.registerMessageHandlers(messageHandlerRegistry); + } + async processRevocationNotification(agentContext, anonCredsRevocationRegistryId, anonCredsCredentialRevocationId, connection, comment) { ++ let credentialRecord; ++ + // TODO: can we extract support for this revocation notification handler to the anoncreds module? + const query = { anonCredsRevocationRegistryId, anonCredsCredentialRevocationId, connectionId: connection.id }; ++ + this.logger.trace(`Getting record by query for revocation notification:`, query); +- const credentialRecord = await this.credentialRepository.getSingleByQuery(agentContext, query); ++ if(new URL(anonCredsRevocationRegistryId)){ ++ credentialRecord = await this.credentialRepository.getSingleByQuery(agentContext, { connectionId: connection.id, statusListCredential: anonCredsRevocationRegistryId, statusListIndex: anonCredsCredentialRevocationId }); ++ } else { ++ credentialRecord = await this.credentialRepository.getSingleByQuery(agentContext, query); ++ } + credentialRecord.revocationNotification = new RevocationNotification_1.RevocationNotification(comment); + await this.credentialRepository.update(agentContext, credentialRecord); + this.logger.trace('Emitting RevocationNotificationReceivedEvent'); +@@ -96,11 +103,16 @@ let RevocationNotificationService = class RevocationNotificationService { + var _a; + this.logger.info('Processing revocation notification v2', { message: messageContext.message }); + const credentialId = messageContext.message.credentialId; +- if (![revocationIdentifier_1.v2IndyRevocationFormat, revocationIdentifier_1.v2AnonCredsRevocationFormat].includes(messageContext.message.revocationFormat)) { ++ if (![revocationIdentifier_1.v2IndyRevocationFormat, revocationIdentifier_1.v2AnonCredsRevocationFormat, revocationIdentifier_1.v2JsonLdRevocationFormat].includes(messageContext.message.revocationFormat)) { + throw new CredoError_1.CredoError(`Unknown revocation format: ${messageContext.message.revocationFormat}. Supported formats are indy-anoncreds and anoncreds`); + } + try { +- const credentialIdGroups = (_a = credentialId.match(revocationIdentifier_1.v2IndyRevocationIdentifierRegex)) !== null && _a !== void 0 ? _a : credentialId.match(revocationIdentifier_1.v2AnonCredsRevocationIdentifierRegex); ++ const credentialIdGroups = ( ++ (_a = credentialId.match(revocationIdentifier_1.v2IndyRevocationIdentifierRegex)) !== null && _a !== void 0 ++ ? _a ++ : credentialId.match(revocationIdentifier_1.v2AnonCredsRevocationIdentifierRegex) ?? credentialId.match(revocationIdentifier_1.v2JsonLdRevocationRegex) ++ ); ++ + if (!credentialIdGroups) { + throw new CredoError_1.CredoError(`Incorrect revocation notification credentialId format: \n${credentialId}\ndoes not match\n"::"`); + } +diff --git a/node_modules/@credo-ts/core/build/modules/credentials/protocol/revocation-notification/util/revocationIdentifier.d.ts b/node_modules/@credo-ts/core/build/modules/credentials/protocol/revocation-notification/util/revocationIdentifier.d.ts +index 0cf5132..7b9497a 100644 +--- a/node_modules/@credo-ts/core/build/modules/credentials/protocol/revocation-notification/util/revocationIdentifier.d.ts ++++ b/node_modules/@credo-ts/core/build/modules/credentials/protocol/revocation-notification/util/revocationIdentifier.d.ts +@@ -3,3 +3,5 @@ export declare const v2IndyRevocationIdentifierRegex: RegExp; + export declare const v2IndyRevocationFormat = "indy-anoncreds"; + export declare const v2AnonCredsRevocationIdentifierRegex: RegExp; + export declare const v2AnonCredsRevocationFormat = "anoncreds"; ++export declare const v2JsonLdRevocationRegex: RegExp; ++export declare const v2JsonLdRevocationFormat = 'jsonld' +diff --git a/node_modules/@credo-ts/core/build/modules/credentials/protocol/revocation-notification/util/revocationIdentifier.js b/node_modules/@credo-ts/core/build/modules/credentials/protocol/revocation-notification/util/revocationIdentifier.js +index 55a0cec..95fbe8a 100644 +--- a/node_modules/@credo-ts/core/build/modules/credentials/protocol/revocation-notification/util/revocationIdentifier.js ++++ b/node_modules/@credo-ts/core/build/modules/credentials/protocol/revocation-notification/util/revocationIdentifier.js +@@ -10,4 +10,7 @@ exports.v2IndyRevocationFormat = 'indy-anoncreds'; + // CredentialID = :: + exports.v2AnonCredsRevocationIdentifierRegex = /([a-zA-Z0-9+\-.]+:.+)::(\d+)$/; + exports.v2AnonCredsRevocationFormat = 'anoncreds'; ++ ++exports.v2JsonLdRevocationRegex = /^(https?:\/\/)?([\w.-]+)(:\d+)?(\/[^\s]*)?$/ ++exports.v2JsonLdRevocationFormat = 'jsonld'; + //# sourceMappingURL=revocationIdentifier.js.map +\ No newline at end of file diff --git a/src/controllers/multi-tenancy/MultiTenancyController.ts b/src/controllers/multi-tenancy/MultiTenancyController.ts index 116efdc3..45063786 100644 --- a/src/controllers/multi-tenancy/MultiTenancyController.ts +++ b/src/controllers/multi-tenancy/MultiTenancyController.ts @@ -1999,6 +1999,7 @@ export class MultiTenancyController extends Controller { credentialRecordId: credentialId, revocationId, revocationFormat: 'jsonld', + comment: `Your credential has been revoked.`, }) return sendNotification }) diff --git a/src/controllers/w3cRevocation/w3cRevocationController.ts b/src/controllers/w3cRevocation/w3cRevocationController.ts index 2949a68b..7a416878 100644 --- a/src/controllers/w3cRevocation/w3cRevocationController.ts +++ b/src/controllers/w3cRevocation/w3cRevocationController.ts @@ -63,6 +63,7 @@ export class W3CRevocationController extends Controller { credentialRecordId: credentialId, revocationId, revocationFormat: 'jsonld', + comment: `Your credential has been revoked.`, }) return sendNotification } catch (error) { From 415b6141c230b75df75c79848620132eadc0d67d Mon Sep 17 00:00:00 2001 From: KulkarniShashank Date: Thu, 5 Sep 2024 12:28:24 +0530 Subject: [PATCH 15/15] refactor: oob revocation credential function Signed-off-by: KulkarniShashank --- .../credentials/CredentialController.ts | 81 ++++++++++--------- .../multi-tenancy/MultiTenancyController.ts | 12 ++- src/controllers/types.ts | 5 ++ .../w3cRevocation/w3cRevocationController.ts | 9 ++- 4 files changed, 63 insertions(+), 44 deletions(-) diff --git a/src/controllers/credentials/CredentialController.ts b/src/controllers/credentials/CredentialController.ts index 54caa4e5..f548bcc7 100644 --- a/src/controllers/credentials/CredentialController.ts +++ b/src/controllers/credentials/CredentialController.ts @@ -1,8 +1,9 @@ import type { RestAgentModules } from '../../cliAgent' -import type { CredentialStatusList } from '../types' +import type { CredentialStatusList, OobOffer } from '../types' import type { CredentialExchangeRecordProps, CredentialProtocolVersionType, + OutOfBandRecord, PeerDidNumAlgo2CreateOptions, Routing, } from '@credo-ts/core' @@ -265,21 +266,8 @@ export class CredentialController extends Controller { const credentialStatus = await utils.getCredentialStatus(credentialStatusData, getIndex) credentialFormats.jsonld.credential.credentialStatus = credentialStatus - const offerOob = await this.agent.credentials.createOffer({ - protocolVersion: outOfBandOption.protocolVersion as CredentialProtocolVersionType<[]>, - credentialFormats: outOfBandOption.credentialFormats, - autoAcceptCredential: outOfBandOption.autoAcceptCredential, - comment: outOfBandOption.comment, - }) - - const credentialMessage = offerOob.message - const outOfBandRecord = await this.agent.oob.createInvitation({ - label: outOfBandOption.label, - messages: [credentialMessage], - autoAcceptConnection: true, - imageUrl: outOfBandOption?.imageUrl, - invitationDid, - }) + const offerOob = await this._createOffer(outOfBandOption) + const outOfBandRecord = await this._createInvitation(outOfBandOption, offerOob, invitationDid) await this.agent.genericRecords.save({ content: { index: credentialStatus.statusListIndex }, @@ -287,18 +275,19 @@ export class CredentialController extends Controller { id: offerOob.credentialRecord.id, }) - return { - invitationUrl: outOfBandRecord.outOfBandInvitation.toUrl({ - domain: this.agent.config.endpoints[0], - }), - invitation: outOfBandRecord.outOfBandInvitation.toJSON({ - useDidSovPrefixWhereAllowed: this.agent.config.useDidSovPrefixWhereAllowed, - }), - outOfBandRecord: outOfBandRecord.toJSON(), - invitationDid: outOfBandOption?.invitationDid ? '' : invitationDid, - } + return this._buildOobOfferResponse(outOfBandRecord, outOfBandOption, invitationDid) } + const offerOob = await this._createOffer(outOfBandOption) + const outOfBandRecord = await this._createInvitation(outOfBandOption, offerOob, invitationDid) + return this._buildOobOfferResponse(outOfBandRecord, outOfBandOption, invitationDid) + } catch (error) { + throw ErrorHandlingService.handle(error) + } + } + + private async _createOffer(outOfBandOption: CreateOfferOobOptions): Promise { + try { const offerOob = await this.agent.credentials.createOffer({ protocolVersion: outOfBandOption.protocolVersion as CredentialProtocolVersionType<[]>, credentialFormats: outOfBandOption.credentialFormats, @@ -306,6 +295,18 @@ export class CredentialController extends Controller { comment: outOfBandOption.comment, }) + return offerOob + } catch (error) { + throw ErrorHandlingService.handle(error) + } + } + + private async _createInvitation( + outOfBandOption: CreateOfferOobOptions, + offerOob: OobOffer, + invitationDid?: string + ): Promise { + try { const credentialMessage = offerOob.message const outOfBandRecord = await this.agent.oob.createInvitation({ label: outOfBandOption.label, @@ -314,21 +315,29 @@ export class CredentialController extends Controller { imageUrl: outOfBandOption?.imageUrl, invitationDid, }) - return { - invitationUrl: outOfBandRecord.outOfBandInvitation.toUrl({ - domain: this.agent.config.endpoints[0], - }), - invitation: outOfBandRecord.outOfBandInvitation.toJSON({ - useDidSovPrefixWhereAllowed: this.agent.config.useDidSovPrefixWhereAllowed, - }), - outOfBandRecord: outOfBandRecord.toJSON(), - invitationDid: outOfBandOption?.invitationDid ? '' : invitationDid, - } + return outOfBandRecord } catch (error) { throw ErrorHandlingService.handle(error) } } + private async _buildOobOfferResponse( + outOfBandRecord: OutOfBandRecord, + outOfBandOption: CreateOfferOobOptions, + invitationDid?: string + ) { + return { + invitationUrl: outOfBandRecord.outOfBandInvitation.toUrl({ + domain: this.agent.config.endpoints[0], + }), + invitation: outOfBandRecord.outOfBandInvitation.toJSON({ + useDidSovPrefixWhereAllowed: this.agent.config.useDidSovPrefixWhereAllowed, + }), + outOfBandRecord: outOfBandRecord.toJSON(), + invitationDid: outOfBandOption?.invitationDid ? '' : invitationDid, + } + } + /** * Accept a credential offer as holder by sending an accept offer message * to the connection associated with the credential exchange record. diff --git a/src/controllers/multi-tenancy/MultiTenancyController.ts b/src/controllers/multi-tenancy/MultiTenancyController.ts index 6640f10e..31ec5d20 100644 --- a/src/controllers/multi-tenancy/MultiTenancyController.ts +++ b/src/controllers/multi-tenancy/MultiTenancyController.ts @@ -1989,21 +1989,25 @@ export class MultiTenancyController extends Controller { @Security('apiKey') @Post('/revoke-credential/:credentialId/:tenantId') - public async revokeW3C(@Path('credentialId') credentialId: string, @Path('tenantId') tenantId: string) { - let sendNotification + public async revokeW3C( + @Path('credentialId') credentialId: string, + @Path('tenantId') tenantId: string + ): Promise<{ + message: string + }> { try { return await this.agent.modules.tenants.withTenantAgent({ tenantId }, async (tenantAgent) => { const credential = await tenantAgent.credentials.getFormatData(credentialId) const { credentialIndex, statusListCredentialURL } = await this.w3CRevocationController._revokeW3C(credential) const revocationId = `${statusListCredentialURL}::${credentialIndex}` - sendNotification = await tenantAgent.credentials.sendRevocationNotification({ + await tenantAgent.credentials.sendRevocationNotification({ credentialRecordId: credentialId, revocationId, revocationFormat: 'jsonld', comment: `Your credential has been revoked.`, }) - return sendNotification + return { message: 'The credential has been successfully revoked.' } }) } catch (error) { throw ErrorHandlingService.handle(error) diff --git a/src/controllers/types.ts b/src/controllers/types.ts index b76278f4..3eb59d29 100644 --- a/src/controllers/types.ts +++ b/src/controllers/types.ts @@ -434,3 +434,8 @@ export interface CredentialStatusList { credentialSubjectUrl: string statusPurpose: BitStringCredentialStatusPurpose } + +export interface OobOffer { + message: AgentMessage + credentialRecord: CredentialExchangeRecord +} diff --git a/src/controllers/w3cRevocation/w3cRevocationController.ts b/src/controllers/w3cRevocation/w3cRevocationController.ts index 7a416878..fe34d86f 100644 --- a/src/controllers/w3cRevocation/w3cRevocationController.ts +++ b/src/controllers/w3cRevocation/w3cRevocationController.ts @@ -52,20 +52,21 @@ export class W3CRevocationController extends Controller { } @Post('/revoke-credential/:credentialId') - public async revokeW3C(@Path('credentialId') credentialId: string) { - let sendNotification + public async revokeW3C(@Path('credentialId') credentialId: string): Promise<{ + message: string + }> { try { const credential = await this.agent.credentials.getFormatData(credentialId) const { credentialIndex, statusListCredentialURL } = await this._revokeW3C(credential) const revocationId = `${statusListCredentialURL}::${credentialIndex}` - sendNotification = await this.agent.credentials.sendRevocationNotification({ + await this.agent.credentials.sendRevocationNotification({ credentialRecordId: credentialId, revocationId, revocationFormat: 'jsonld', comment: `Your credential has been revoked.`, }) - return sendNotification + return { message: 'The credential has been successfully revoked.' } } catch (error) { throw ErrorHandlingService.handle(error) }