From 77c7bdeacbcaae23fcb7de52a58975b3fb23b2e2 Mon Sep 17 00:00:00 2001
From: Berend Sliedrecht <berend@animo.id>
Date: Mon, 5 Feb 2024 11:00:05 +0100
Subject: [PATCH] feat(ssi-abstraction): check revocation state

Signed-off-by: Berend Sliedrecht <berend@animo.id>
---
 apps/shared/src/events/revocationEvents.ts    |  29 ++++
 .../agent/revocation/revocation.controller.ts |  12 ++
 .../agent/revocation/revocation.service.ts    | 153 +++++++++++-------
 .../test/revocation.e2e-spec.ts               |  20 +++
 4 files changed, 157 insertions(+), 57 deletions(-)

diff --git a/apps/shared/src/events/revocationEvents.ts b/apps/shared/src/events/revocationEvents.ts
index 0e0e83a..15cf3b1 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 6b9f4be..928e39b 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 7e948b4..20b0cf8 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 a9f14ce..e6150f7 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);
+    }
   });
 });
-- 
GitLab