diff --git a/libs/askar/src/askar-rest/rest.controller.ts b/libs/askar/src/askar-rest/rest.controller.ts index dfffbdc404afe832a78b24d39f2facc4fff72eda..247e7c7d0c2b60a2cd17c569cc0ecacc3d2751ea 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 b918e7d889e4698672f52a9b3b17ee7ba078cade..41a741d0710de64ce7b201f346c1321decac318c 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 9cea64c90950d36aadd29ab0bf2765d7576d9e2a..c7f60f0f8fe9647f1a9461486a3c982ca25e668c 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 0000000000000000000000000000000000000000..b47a77afebf34c180136e0b880561d1abb538d9e --- /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 03062d4d6de377a29760704674c7c7b2d3bebd33..3da8f443f626bae4dd4bb965b0d794a3fb09ab16 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";