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