diff --git a/apps/shared/src/events/revocationEvents.ts b/apps/shared/src/events/revocationEvents.ts index 0e0e83a4c64d090ae216df47c79e2b8584a0389e..15cf3b17d2f2f8013a28abbf89e66f64547b0c4a 100644 --- a/apps/shared/src/events/revocationEvents.ts +++ b/apps/shared/src/events/revocationEvents.ts @@ -2,6 +2,11 @@ import type { BaseEventInput } from './baseEvents.js'; import { BaseEvent } from './baseEvents.js'; +export enum RevocationState { + Issued, + Revoked, +} + export type EventAnonCredsRevocationRevokeInput = BaseEventInput<{ credentialId: string; }>; @@ -100,3 +105,27 @@ export class EventAnonCredsRevocationTailsFile extends BaseEvent<{ ); } } + +export type EventAnonCredsRevocationCheckCredentialStatusInput = + BaseEventInput<{ + credentialId: string; + }>; +export class EventAnonCredsRevocationCheckCredentialStatus extends BaseEvent<{ + state: RevocationState; +}> { + public static token = 'anoncreds.revocation.checkCredentialStatus'; + + public get instance() { + return this.data; + } + + public static fromEvent(e: EventAnonCredsRevocationCheckCredentialStatus) { + return new EventAnonCredsRevocationCheckCredentialStatus( + e.data, + e.tenantId, + e.id, + e.type, + e.timestamp, + ); + } +} diff --git a/apps/ssi-abstraction/src/agent/revocation/revocation.controller.ts b/apps/ssi-abstraction/src/agent/revocation/revocation.controller.ts index 6b9f4be362124877e8abc4ed64fb59e3e2b4aa96..928e39b200368f32a97637883dc36cc394b386cd 100644 --- a/apps/ssi-abstraction/src/agent/revocation/revocation.controller.ts +++ b/apps/ssi-abstraction/src/agent/revocation/revocation.controller.ts @@ -1,6 +1,8 @@ import { Controller } from '@nestjs/common'; import { MessagePattern } from '@nestjs/microservices'; import { + EventAnonCredsRevocationCheckCredentialStatus, + EventAnonCredsRevocationCheckCredentialStatusInput, EventAnonCredsRevocationRegisterRevocationRegistryDefinition, EventAnonCredsRevocationRegisterRevocationRegistryDefinitionInput, EventAnonCredsRevocationRegisterRevocationStatusList, @@ -60,4 +62,14 @@ export class RevocationController { options.tenantId, ); } + + @MessagePattern(EventAnonCredsRevocationCheckCredentialStatus.token) + public async checkCredentialStatus( + options: EventAnonCredsRevocationCheckCredentialStatusInput, + ): Promise<EventAnonCredsRevocationCheckCredentialStatus> { + return new EventAnonCredsRevocationCheckCredentialStatus( + await this.revocationService.checkCredentialStatus(options), + options.tenantId, + ); + } } diff --git a/apps/ssi-abstraction/src/agent/revocation/revocation.service.ts b/apps/ssi-abstraction/src/agent/revocation/revocation.service.ts index 7e948b4010cfe4927aa5465ac7259371c0f4762a..20b0cf874f223f5f4f4d55fb1c60c715363ff58a 100644 --- a/apps/ssi-abstraction/src/agent/revocation/revocation.service.ts +++ b/apps/ssi-abstraction/src/agent/revocation/revocation.service.ts @@ -1,15 +1,19 @@ -import type { - EventAnonCredsRevocationRegisterRevocationRegistryDefinition, - EventAnonCredsRevocationRegisterRevocationRegistryDefinitionInput, - EventAnonCredsRevocationRegisterRevocationStatusList, - EventAnonCredsRevocationRegisterRevocationStatusListInput, - EventAnonCredsRevocationRevoke, - EventAnonCredsRevocationRevokeInput, - EventAnonCredsRevocationTailsFile, - EventAnonCredsRevocationTailsFileInput, -} from '@ocm/shared'; +import type { TenantAgent } from '../agent.service.js'; import { Injectable } from '@nestjs/common'; +import { + RevocationState, + type EventAnonCredsRevocationCheckCredentialStatus, + type EventAnonCredsRevocationCheckCredentialStatusInput, + type EventAnonCredsRevocationRegisterRevocationRegistryDefinition, + type EventAnonCredsRevocationRegisterRevocationRegistryDefinitionInput, + type EventAnonCredsRevocationRegisterRevocationStatusList, + type EventAnonCredsRevocationRegisterRevocationStatusListInput, + type EventAnonCredsRevocationRevoke, + type EventAnonCredsRevocationRevokeInput, + type EventAnonCredsRevocationTailsFile, + type EventAnonCredsRevocationTailsFileInput, +} from '@ocm/shared'; import { readFile } from 'node:fs/promises'; import { AgentService } from '../agent.service.js'; @@ -42,60 +46,22 @@ export class RevocationService { > { // eslint-disable-next-line @typescript-eslint/no-unused-vars return this.withTenantService.invoke(tenantId, async (t) => { - const credential = await t.credentials.getById(credentialId); - - const metadata = credential.metadata.get<AnonCredsCredentialMetadata>( - '_anoncreds/credential', - ); + const { + credentialRevocationId, + revocationStatusList, + revocationRegistryId, + } = await this.getRevocationParts(t, credentialId); - if ( - !metadata || - !metadata.revocationRegistryId || - !metadata.credentialRevocationId - ) { + if (revocationStatusList.revocationList[credentialRevocationId] === 1) { throw new Error( - `credential (${credentialId}) has no metadata, likley it was issued without support for revocation`, - ); - } - - const { revocationRegistryDefinition } = - await t.modules.anoncreds.getRevocationRegistryDefinition( - metadata.revocationRegistryId, - ); - - if (!revocationRegistryDefinition) { - throw new Error( - `Could not find the revocation registry definition for id: ${metadata.revocationRegistryId}`, - ); - } - - const timestamp = Math.floor(Date.now() / 1000); - const { revocationStatusList } = - await t.modules.anoncreds.getRevocationStatusList( - metadata.revocationRegistryId, - timestamp, - ); - - if (!revocationStatusList) { - throw new Error( - `Could not find the revocation status list for revocation registry definition id: ${metadata.revocationRegistryId} and timestamp: ${timestamp}`, - ); - } - - if ( - revocationStatusList.revocationList[ - Number(metadata.credentialRevocationId) - ] === 1 - ) { - throw new Error( - `credential (${credentialId}), with revocation id ${metadata.credentialRevocationId}, is already in a revoked state`, + `credential (${credentialId}), with revocation id ${credentialRevocationId}, is already in a revoked state`, ); } const result = await t.modules.anoncreds.updateRevocationStatusList({ revocationStatusList: { - revocationRegistryDefinitionId: metadata.revocationRegistryId, - revokedCredentialIndexes: [Number(metadata.credentialRevocationId)], + revocationRegistryDefinitionId: revocationRegistryId, + revokedCredentialIndexes: [credentialRevocationId], }, options: {}, }); @@ -249,4 +215,77 @@ export class RevocationService { }; }); } + + public async checkCredentialStatus({ + tenantId, + credentialId, + }: EventAnonCredsRevocationCheckCredentialStatusInput): Promise< + EventAnonCredsRevocationCheckCredentialStatus['data'] + > { + return this.withTenantService.invoke(tenantId, async (t) => { + const { credentialRevocationId, revocationStatusList } = + await this.getRevocationParts(t, credentialId); + + const revocationStatus = + revocationStatusList.revocationList[credentialRevocationId]; + + return { + state: + revocationStatus === 0 + ? RevocationState.Issued + : RevocationState.Revoked, + }; + }); + } + + private async getRevocationParts( + tenantAgent: TenantAgent, + credentialId: string, + ) { + const credential = await tenantAgent.credentials.getById(credentialId); + + const metadata = credential.metadata.get<AnonCredsCredentialMetadata>( + '_anoncreds/credential', + ); + + if ( + !metadata || + !metadata.revocationRegistryId || + !metadata.credentialRevocationId + ) { + throw new Error( + `credential (${credentialId}) has no metadata, likley it was issued without support for revocation`, + ); + } + + const { revocationRegistryDefinition } = + await tenantAgent.modules.anoncreds.getRevocationRegistryDefinition( + metadata.revocationRegistryId, + ); + + if (!revocationRegistryDefinition) { + throw new Error( + `Could not find the revocation registry definition for id: ${metadata.revocationRegistryId}`, + ); + } + + const timestamp = Math.floor(Date.now() / 1000); + const { revocationStatusList } = + await tenantAgent.modules.anoncreds.getRevocationStatusList( + metadata.revocationRegistryId, + timestamp, + ); + + if (!revocationStatusList) { + throw new Error( + `Could not find the revocation status list for revocation registry definition id: ${metadata.revocationRegistryId} and timestamp: ${timestamp}`, + ); + } + + return { + credentialRevocationId: Number(metadata.credentialRevocationId), + revocationStatusList, + revocationRegistryId: metadata.revocationRegistryId, + }; + } } diff --git a/apps/ssi-abstraction/test/revocation.e2e-spec.ts b/apps/ssi-abstraction/test/revocation.e2e-spec.ts index a9f14ce4b37dd44f0234a0eb9a87291acf7920d1..e6150f745df985f8049e2cec7fce7b06668135df 100644 --- a/apps/ssi-abstraction/test/revocation.e2e-spec.ts +++ b/apps/ssi-abstraction/test/revocation.e2e-spec.ts @@ -1,6 +1,7 @@ import type { INestApplication } from '@nestjs/common'; import type { ClientProxy } from '@nestjs/microservices'; import type { + EventAnonCredsRevocationCheckCredentialStatusInput, EventAnonCredsRevocationRegisterRevocationRegistryDefinitionInput, EventAnonCredsRevocationRegisterRevocationStatusListInput, EventAnonCredsRevocationRevokeInput, @@ -10,10 +11,12 @@ import type { import { ClientsModule, Transport } from '@nestjs/microservices'; import { Test } from '@nestjs/testing'; import { + EventAnonCredsRevocationCheckCredentialStatus, EventDidcommAnonCredsCredentialsOfferToSelf, EventAnonCredsRevocationRegisterRevocationRegistryDefinition, EventAnonCredsRevocationRegisterRevocationStatusList, EventAnonCredsRevocationRevoke, + RevocationState, } from '@ocm/shared'; import { randomBytes } from 'crypto'; import { firstValueFrom } from 'rxjs'; @@ -253,5 +256,22 @@ describe('Revocation', () => { const eventInstance = EventAnonCredsRevocationRevoke.fromEvent(response); expect(eventInstance.instance).toBeNull(); } + + await new Promise((r) => setTimeout(r, 2000)); + + // Check the revocation state + { + const response$ = client.send< + EventAnonCredsRevocationCheckCredentialStatus, + EventAnonCredsRevocationCheckCredentialStatusInput + >(EventAnonCredsRevocationCheckCredentialStatus.token, { + tenantId, + credentialId, + }); + const response = await firstValueFrom(response$); + const eventInstance = + EventAnonCredsRevocationCheckCredentialStatus.fromEvent(response); + expect(eventInstance.instance.state).toBe(RevocationState.Revoked); + } }); });