From 14471e310d0ecab164257228b62a150bf52bd1dd Mon Sep 17 00:00:00 2001
From: sovrgn <boyan.tsolov@vereign.com>
Date: Fri, 22 Mar 2024 09:54:41 +0200
Subject: [PATCH] feat: issue a credential based on revocation registry, check
 credential revocation status

---
 libs/askar/src/askar-rest/rest.controller.ts  |  5 ++
 libs/askar/src/askar/agent.service.ts         | 86 +++++++++++++++++++
 .../requests/offer.credential.request.dto.ts  | 10 +++
 .../revocation.status.response.dto.ts         | 11 +++
 libs/dtos/src/index.ts                        |  1 +
 5 files changed, 113 insertions(+)
 create mode 100644 libs/dtos/src/dtos/responses/revocation.status.response.dto.ts

diff --git a/libs/askar/src/askar-rest/rest.controller.ts b/libs/askar/src/askar-rest/rest.controller.ts
index dfffbdc4..247e7c7d 100644
--- a/libs/askar/src/askar-rest/rest.controller.ts
+++ b/libs/askar/src/askar-rest/rest.controller.ts
@@ -164,6 +164,11 @@ export class RestController {
     return this.agentService.getCredentialById(credentialId);
   }
 
+  @Get("/credentials/:id/status")
+  async getCredentialStatusById(@Param("id") credentialId: string) {
+    return this.agentService.getCredentialStatus(credentialId);
+  }
+
   @Get("/credentials/:id/format-data")
   async getCredentialFormatDataById(@Param("id") credentialId: string) {
     return this.agentService.getCredentialFormatDataById(credentialId);
diff --git a/libs/askar/src/askar/agent.service.ts b/libs/askar/src/askar/agent.service.ts
index b918e7d8..41a741d0 100644
--- a/libs/askar/src/askar/agent.service.ts
+++ b/libs/askar/src/askar/agent.service.ts
@@ -33,6 +33,7 @@ import {
   BaseRecordDto,
   W3cJsonLdVerifiableCredentialDto,
   W3cJsonLdVerifiablePresentationDto,
+  RevocationStatusResponseDto,
 } from "@ocm-engine/dtos";
 import {
   AutoAcceptCredential,
@@ -60,6 +61,15 @@ import {
   waitForProofExchangeRecordSubject,
 } from "../agent.utils";
 
+
+export interface AnonCredsCredentialMetadata {
+  schemaId?: string;
+  credentialDefinitionId?: string;
+  revocationRegistryId?: string;
+  credentialRevocationId?: string;
+}
+
+
 @Injectable()
 export class AgentService {
   private readonly logger = new Logger(AgentService.name);
@@ -698,6 +708,18 @@ export class AgentService {
       JSON.stringify(offerCredentialDto, null, 2),
     );
 
+    // TODO: IMPORTANT!
+    // Strategy for automatically filling the indices of the revocation status list
+    // currently: the user has to pass a valid index with no information provided
+
+    if (
+      offerCredentialDto.revocationRegistryDefinitionId && 
+      offerCredentialDto.revocationRegistryIndex
+    ) {
+      console.log('Issuing revokable credential.');
+    }
+
+
     if (!offerCredentialDto.connectionId) {
       const { credentialRecord, message } =
         await this.askar.agent.credentials.createOffer({
@@ -706,6 +728,8 @@ export class AgentService {
             anoncreds: {
               credentialDefinitionId: offerCredentialDto.credentialDefinitionId,
               attributes: offerCredentialDto.attributes,
+              revocationRegistryDefinitionId: offerCredentialDto.revocationRegistryDefinitionId || undefined,
+              revocationRegistryIndex: Number(offerCredentialDto.revocationRegistryIndex) || undefined,
             },
           },
           autoAcceptCredential: AutoAcceptCredential.ContentApproved,
@@ -747,6 +771,8 @@ export class AgentService {
           anoncreds: {
             credentialDefinitionId: offerCredentialDto.credentialDefinitionId,
             attributes: offerCredentialDto.attributes,
+            revocationRegistryDefinitionId: offerCredentialDto.revocationRegistryDefinitionId || undefined,
+            revocationRegistryIndex: Number(offerCredentialDto.revocationRegistryIndex) || undefined,
           },
         },
       });
@@ -947,6 +973,66 @@ export class AgentService {
     return dto;
   };
 
+
+  getCredentialStatus = async (
+    credentialId: string
+  ): Promise<RevocationStatusResponseDto | null> => {
+    const credential = await this.askar.agent.credentials.findById(credentialId);
+
+    if (!credential) {
+      throw new EntityNotFoundError();
+    }
+
+    const metadata = credential.metadata.get<AnonCredsCredentialMetadata>(
+      '_anoncreds/credential',
+    );
+
+    if (
+      !metadata ||
+      !metadata.revocationRegistryId ||
+      !metadata.credentialRevocationId
+    ) {
+      throw new Error(
+        `Credential with id=${credentialId} has no metadata, likely it was issued without support for revocation.`,
+      );
+    }
+
+    const { revocationRegistryDefinition } = await this.askar.agent.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 this.askar.agent.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}`,
+      );
+    }
+
+    const revocationStatus = revocationStatusList.revocationList[Number(metadata.credentialRevocationId)];
+
+    const status = revocationStatus === 0
+      ? 'Valid'
+      : 'Revoked'
+
+    return {
+      revocationRegistryId: metadata.revocationRegistryId,
+      status,
+    };
+  }
+
+
   requestProof = async (
     requestProofDto: RequestProofDto,
   ): Promise<RequestProofResponseDto> => {
diff --git a/libs/dtos/src/dtos/requests/offer.credential.request.dto.ts b/libs/dtos/src/dtos/requests/offer.credential.request.dto.ts
index 9cea64c9..c7f60f0f 100644
--- a/libs/dtos/src/dtos/requests/offer.credential.request.dto.ts
+++ b/libs/dtos/src/dtos/requests/offer.credential.request.dto.ts
@@ -2,6 +2,7 @@ import {
   ArrayMinSize,
   IsArray,
   IsNotEmpty,
+  IsNumber,
   IsOptional,
   IsString,
   ValidateNested,
@@ -34,6 +35,15 @@ export class OfferCredentialRequestDto {
   @ValidateNested({ each: true })
   @Type(() => OfferCredentialAttributes)
   attributes: Array<OfferCredentialAttributes>;
+
+  @IsOptional()
+  @IsString()
+  revocationRegistryDefinitionId: string;
+
+
+  @IsOptional()
+  @IsNumber()
+  revocationRegistryIndex: number;
 }
 
 export class OfferJsonCredentialRequests {
diff --git a/libs/dtos/src/dtos/responses/revocation.status.response.dto.ts b/libs/dtos/src/dtos/responses/revocation.status.response.dto.ts
new file mode 100644
index 00000000..b47a77af
--- /dev/null
+++ b/libs/dtos/src/dtos/responses/revocation.status.response.dto.ts
@@ -0,0 +1,11 @@
+import { IsNotEmpty, IsString } from "class-validator";
+
+export class RevocationStatusResponseDto {
+  @IsString()
+  @IsNotEmpty()
+  revocationRegistryId: string;
+
+  @IsString()
+  @IsNotEmpty()
+  status: string;
+}
diff --git a/libs/dtos/src/index.ts b/libs/dtos/src/index.ts
index 03062d4d..3da8f443 100644
--- a/libs/dtos/src/index.ts
+++ b/libs/dtos/src/index.ts
@@ -42,6 +42,7 @@ export * from "./dtos/responses/request.proof.response.dto";
 export * from "./dtos/responses/credential.offer.response.dto";
 export * from "./dtos/responses/create.invitation.response.dto";
 export * from "./dtos/responses/gateway.accepted.response.dto";
+export * from "./dtos/responses/revocation.status.response.dto";
 
 export * from "./errors/ocm.error";
 export * from "./errors/entity.not.found.error";
-- 
GitLab