From 7d55031deb12906d1f16e1112de858b7c28b762b Mon Sep 17 00:00:00 2001
From: sovrgn <boyan.tsolov@vereign.com>
Date: Fri, 22 Mar 2024 14:21:31 +0200
Subject: [PATCH] feat,chore: automatically increment revocation status list
 index, refactor

---
 .env.example                                  |   3 +-
 libs/askar/src/agent.utils.ts                 |  87 +++++++--
 libs/askar/src/askar/agent.service.ts         | 179 +++++++-----------
 libs/askar/src/askar/askar.service.ts         |   1 +
 libs/config/src/config/agent.config.ts        |   5 +
 .../src/interfaces/agent.config.interface.ts  |   7 +
 .../requests/offer.credential.request.dto.ts  |   5 -
 .../credential.offer.response.dto.ts          |   1 +
 8 files changed, 157 insertions(+), 131 deletions(-)

diff --git a/.env.example b/.env.example
index 3eff346f..47358c84 100644
--- a/.env.example
+++ b/.env.example
@@ -41,4 +41,5 @@ SWAGGER=false
 TAILS_SERVER_BASE_URL=https://s3.us-west-000.backblazeb2.com
 TAILS_SERVER_KEY=key
 TAILS_SERVER_SECRET=secret
-TAILS_SERVER_BUCKET_NAME=ssi-revocation-tails
\ No newline at end of file
+TAILS_SERVER_BUCKET_NAME=ssi-revocation-tails
+REVOCATION_LIST_SIZE=100
\ No newline at end of file
diff --git a/libs/askar/src/agent.utils.ts b/libs/askar/src/agent.utils.ts
index a254f49c..e3591cba 100644
--- a/libs/askar/src/agent.utils.ts
+++ b/libs/askar/src/agent.utils.ts
@@ -76,6 +76,17 @@ export type SubjectMessage = {
 import { Request, Response, Express } from "express";
 import url from "url";
 import { JsonLdCredentialFormatService } from "./credo/JsonLdCredentialFormatService";
+import { AskarService } from ".";
+
+
+
+export interface AnonCredsCredentialMetadata {
+  schemaId?: string;
+  credentialDefinitionId?: string;
+  revocationRegistryId?: string;
+  credentialRevocationId?: string;
+}
+
 
 export const importDidsToWallet = async (
   agent: Agent,
@@ -209,21 +220,7 @@ export const generateDidFromKey = (key: Key): string => {
 };
 
 //eslint-disable-next-line
-export const getAskarAnonCredsIndyModules = (networks: any) => {
-  const tailsServerBaseUrl = process.env["TAILS_SERVER_BASE_URL"] || undefined;
-  const s3AccessKey = process.env["TAILS_SERVER_KEY"] || undefined;
-  const s3Secret = process.env["TAILS_SERVER_SECRET"] || undefined;
-  const tailsServerBucketName = process.env["TAILS_SERVER_BUCKET_NAME"] || undefined;
-
-  if (
-    !tailsServerBaseUrl ||
-    !s3AccessKey ||
-    !s3Secret ||
-    !tailsServerBucketName
-  ) {
-    throw new Error("Tails Storage Information not provided.");
-  }
-
+export const getAskarAnonCredsIndyModules = (networks: any, config: IConfAgent) => {
   return {
     connections: new ConnectionsModule({
       autoAcceptConnections: true,
@@ -254,10 +251,10 @@ export const getAskarAnonCredsIndyModules = (networks: any) => {
       // @ts-ignore
       autoCreateLinkSecret: true,
       tailsFileService: new S3TailsFileService({
-        tailsServerBaseUrl,
-        s3AccessKey,
-        s3Secret,
-        tailsServerBucketName,
+        tailsServerBaseUrl: config.tailsServerBaseUrl,
+        s3AccessKey: config.s3AccessKey,
+        s3Secret: config.s3Secret,
+        tailsServerBucketName: config.tailsServerBucketName,
       }),
     }),
     indyVdr: new IndyVdrModule({
@@ -657,3 +654,55 @@ export const webHookHandler = async <T>(
     console.log(`Successfully sent web hook to ${url}/topic/${webHookTopic}`);
   }
 };
+
+
+export const getRevocationDetails = async (
+  askar: AskarService,
+  credentialId: string,
+) => {
+  const credential = await askar.agent.credentials.getById(credentialId);
+
+  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 issued without support for revocation.`,
+    );
+  }
+
+  const { revocationRegistryDefinition } =
+    await 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 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}.`,
+    );
+  }
+
+  return {
+    credentialRevocationId: Number(metadata.credentialRevocationId),
+    revocationStatusList,
+    revocationRegistryId: metadata.revocationRegistryId,
+  };
+}
\ No newline at end of file
diff --git a/libs/askar/src/askar/agent.service.ts b/libs/askar/src/askar/agent.service.ts
index 1e43ab6b..c55e4dff 100644
--- a/libs/askar/src/askar/agent.service.ts
+++ b/libs/askar/src/askar/agent.service.ts
@@ -57,18 +57,12 @@ import {
 import { AnonCredsRequestedAttribute } from "@credo-ts/anoncreds";
 import { uuid } from "@credo-ts/core/build/utils/uuid";
 import {
+  getRevocationDetails,
   waitForCredentialExchangeRecordSubject,
   waitForProofExchangeRecordSubject,
 } from "../agent.utils";
 
 
-export interface AnonCredsCredentialMetadata {
-  schemaId?: string;
-  credentialDefinitionId?: string;
-  revocationRegistryId?: string;
-  credentialRevocationId?: string;
-}
-
 
 @Injectable()
 export class AgentService {
@@ -422,7 +416,7 @@ export class AgentService {
     if (supportRevocation) {
       revocReg = await this.askar.agent.modules.anoncreds.registerRevocationRegistryDefinition({
         revocationRegistryDefinition: {
-          maximumCredentialNumber: 100, // TODO: env var?
+          maximumCredentialNumber: this.askar.agentConfig.revocationListSize,
           issuerId: dids[0].did,
           tag: credentialDefinitionDto.tag,
           credentialDefinitionId: credDef.credentialDefinitionState.credentialDefinitionId,
@@ -708,17 +702,53 @@ 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 prior information provided
+    let statusListIndex = undefined;
+    let customRevocRecord = undefined;
+    if (offerCredentialDto.revocationRegistryDefinitionId) {
+      console.log('Issuing revocable credential.');
+
+      customRevocRecord = await this.askar.agent.genericRecords.findAllByQuery({
+        credentialDefinitionId: offerCredentialDto.credentialDefinitionId,
+        revocationRegistryDefinitionId: offerCredentialDto.revocationRegistryDefinitionId,
+      })
+      
+      console.log('Using saved revocation custom record:',customRevocRecord);
+
+      if (!customRevocRecord || customRevocRecord.length < 1 || !customRevocRecord[0]) {
+        customRevocRecord = await this.askar.agent.genericRecords.save({
+          id: uuid(),
+          tags: {
+            credentialDefinitionId: offerCredentialDto.credentialDefinitionId,
+            revocationRegistryDefinitionId: offerCredentialDto.revocationRegistryDefinitionId,
+            latestIndex: '0',
+          },
+          content: {
+            credentialDefinitionId: offerCredentialDto.credentialDefinitionId,
+            revocationRegistryDefinitionId: offerCredentialDto.revocationRegistryDefinitionId,
+          }
+        })
+
+        customRevocRecord = [customRevocRecord]
+        
+        if (!customRevocRecord) {
+          throw new Error("Could not save generic record for revocation status tracking.");
+        }
+      }
+
+      statusListIndex = Number(customRevocRecord[0].getTags()['latestIndex'])
+    }
 
     if (
-      offerCredentialDto.revocationRegistryDefinitionId && 
-      offerCredentialDto.revocationRegistryIndex
+      statusListIndex && 
+      statusListIndex >= this.askar.agentConfig.revocationListSize - 1
     ) {
-      console.log('Issuing revokable credential.');
+      throw new Error('Revocation Status List has been exhausted. Please create a new Revocation Registry Definition for this Credential Definition. ... Not yet implemented.');
     }
 
+    // TODO: if latestIndex is conf.maxListSize-1 -> create new revocDef and statusList
+    // and use them instead
+    // REVIEW: is there a way to handle this automatically as well?
+
 
     if (!offerCredentialDto.connectionId) {
       const { credentialRecord, message } =
@@ -729,7 +759,7 @@ export class AgentService {
               credentialDefinitionId: offerCredentialDto.credentialDefinitionId,
               attributes: offerCredentialDto.attributes,
               revocationRegistryDefinitionId: offerCredentialDto.revocationRegistryDefinitionId || undefined,
-              revocationRegistryIndex: Number(offerCredentialDto.revocationRegistryIndex) || undefined,
+              revocationRegistryIndex: typeof statusListIndex === 'undefined' ? undefined : statusListIndex+1,
             },
           },
           autoAcceptCredential: AutoAcceptCredential.ContentApproved,
@@ -737,6 +767,19 @@ export class AgentService {
 
       credentialRecord.setTag("xRole", "issuer");
       await this.askar.agent.credentials.update(credentialRecord);
+      
+
+      // INFO: update with latest index
+      if (
+        offerCredentialDto.revocationRegistryDefinitionId && 
+        customRevocRecord &&
+        customRevocRecord.length > 0 &&
+        typeof statusListIndex !== 'undefined'
+      ) {
+        customRevocRecord[0].setTag("latestIndex", (statusListIndex+1).toString())
+        await this.askar.agent.genericRecords.update(customRevocRecord[0])
+      }
+
 
       const outOfBandRecord = await this.askar.agent.oob.createInvitation({
         messages: [message],
@@ -761,6 +804,7 @@ export class AgentService {
         credentialUrl: credentialUrl,
         shortCredentialUrl: shortCredentialUrl,
         credentialRecord: dto,
+        revocationStatusListIndex: statusListIndex,
       };
     }
     const credentialExchangeRecord =
@@ -772,7 +816,7 @@ export class AgentService {
             credentialDefinitionId: offerCredentialDto.credentialDefinitionId,
             attributes: offerCredentialDto.attributes,
             revocationRegistryDefinitionId: offerCredentialDto.revocationRegistryDefinitionId || undefined,
-            revocationRegistryIndex: Number(offerCredentialDto.revocationRegistryIndex) || undefined,
+            revocationRegistryIndex: typeof statusListIndex === 'undefined' ? undefined : statusListIndex+1,
           },
         },
       });
@@ -792,6 +836,7 @@ export class AgentService {
       credentialUrl: null,
       shortCredentialUrl: null,
       credentialRecord: dto,
+      revocationStatusListIndex: statusListIndex,
     };
   };
 
@@ -977,58 +1022,20 @@ export class AgentService {
   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',
+    const {revocationStatusList, revocationRegistryId, credentialRevocationId} = await getRevocationDetails(
+      this.askar,
+      credentialId
     );
 
-    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 revocationStatus = revocationStatusList.revocationList[Number(credentialRevocationId)];
 
     const status = revocationStatus === 0
       ? true
       : false
 
     return {
-      revocationRegistryId: metadata.revocationRegistryId,
-      revocationId: Number(metadata.credentialRevocationId),
+      revocationRegistryId,
+      revocationId: credentialRevocationId,
       valid: status,
     };
   };
@@ -1037,50 +1044,10 @@ export class AgentService {
   revokeCredentialById = async (
     credentialId: string
   ): Promise<RevocationStatusResponseDto> => {
-    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,
+    const {revocationStatusList, revocationRegistryId, credentialRevocationId} = await getRevocationDetails(
+      this.askar,
+      credentialId
     );
-    
-    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}.`,
-      );
-    }
-
-    let credentialRevocationId = Number(metadata.credentialRevocationId);
 
     if (revocationStatusList.revocationList[credentialRevocationId] === 1) {
       throw new Error(
@@ -1090,7 +1057,7 @@ export class AgentService {
 
     const updatedList = await this.askar.agent.modules.anoncreds.updateRevocationStatusList({
       revocationStatusList: {
-        revocationRegistryDefinitionId: metadata.revocationRegistryId,
+        revocationRegistryDefinitionId: revocationRegistryId,
         revokedCredentialIndexes: [credentialRevocationId],
       },
       options: {},
@@ -1105,8 +1072,8 @@ export class AgentService {
     }
 
     return {
-      revocationRegistryId: metadata.revocationRegistryId,
-      revocationId: Number(metadata.credentialRevocationId),
+      revocationRegistryId: revocationRegistryId,
+      revocationId: credentialRevocationId,
       valid: false,
       message: 'Credential Revocation Status Updated.'
     }
diff --git a/libs/askar/src/askar/askar.service.ts b/libs/askar/src/askar/askar.service.ts
index c93a9ab4..631eaeed 100644
--- a/libs/askar/src/askar/askar.service.ts
+++ b/libs/askar/src/askar/askar.service.ts
@@ -74,6 +74,7 @@ export class AskarService implements OnModuleInit, OnModuleDestroy {
       dependencies: agentDependencies,
       modules: getAskarAnonCredsIndyModules(
         this.ledgersService.ledgersConfig(),
+        this.agentConfig
       ),
     });
 
diff --git a/libs/config/src/config/agent.config.ts b/libs/config/src/config/agent.config.ts
index 57bbe5fc..da000c67 100644
--- a/libs/config/src/config/agent.config.ts
+++ b/libs/config/src/config/agent.config.ts
@@ -38,5 +38,10 @@ export const agentConfig = registerAs(
     logLevel: parseInt(process.env["LOG_LEVEL"]!) ?? LogLevel.error,
     agentWebHook: process.env["AGENT_WEBHOOK_URL"]!,
     agentOobUrl: process.env["AGENT_OOB_URL"] || undefined,
+    tailsServerBaseUrl: process.env["TAILS_SERVER_BASE_URL"]!,
+    s3AccessKey: process.env["TAILS_SERVER_KEY"]!,
+    s3Secret: process.env["TAILS_SERVER_SECRET"]!,
+    tailsServerBucketName: process.env["TAILS_SERVER_BUCKET_NAME"]!,
+    revocationListSize: Number(process.env["REVOCATION_LIST_SIZE"]!),
   }),
 );
diff --git a/libs/config/src/interfaces/agent.config.interface.ts b/libs/config/src/interfaces/agent.config.interface.ts
index eeabad51..ed585137 100644
--- a/libs/config/src/interfaces/agent.config.interface.ts
+++ b/libs/config/src/interfaces/agent.config.interface.ts
@@ -28,4 +28,11 @@ export interface IConfAgent {
   logLevel: number;
   agentWebHook: string;
   agentOobUrl: string | undefined;
+
+  tailsServerBaseUrl: string;
+  s3AccessKey: string;
+  s3Secret: string;
+  tailsServerBucketName: string;
+
+  revocationListSize: number;
 }
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 c7f60f0f..b23a63f8 100644
--- a/libs/dtos/src/dtos/requests/offer.credential.request.dto.ts
+++ b/libs/dtos/src/dtos/requests/offer.credential.request.dto.ts
@@ -39,11 +39,6 @@ export class OfferCredentialRequestDto {
   @IsOptional()
   @IsString()
   revocationRegistryDefinitionId: string;
-
-
-  @IsOptional()
-  @IsNumber()
-  revocationRegistryIndex: number;
 }
 
 export class OfferJsonCredentialRequests {
diff --git a/libs/dtos/src/dtos/responses/credential.offer.response.dto.ts b/libs/dtos/src/dtos/responses/credential.offer.response.dto.ts
index 5e951dd9..bdcf47f8 100644
--- a/libs/dtos/src/dtos/responses/credential.offer.response.dto.ts
+++ b/libs/dtos/src/dtos/responses/credential.offer.response.dto.ts
@@ -4,4 +4,5 @@ export class CredentialOfferResponseDto {
   public credentialUrl: string | null;
   public shortCredentialUrl: string | null;
   public credentialRecord: CredentialRecordDto;
+  public revocationStatusListIndex?: number | undefined;
 }
-- 
GitLab