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