From c2461dd957af2325ce6bd91d1ceff477af23e7ce Mon Sep 17 00:00:00 2001 From: sovrgn <boyan.tsolov@vereign.com> Date: Tue, 26 Mar 2024 13:46:23 +0200 Subject: [PATCH] feat: automatically handling revocation definitions and lists --- README.md | 18 +- agent-swagger.json | 34 ++-- libs/askar/src/agent.utils.ts | 154 ++++++++++++++++-- libs/askar/src/askar-rest/rest.controller.ts | 3 +- libs/askar/src/askar/agent.service.ts | 153 +++++------------ .../src/dtos/generics/creddef.record.dto.ts | 4 - ...reate.credential.definition.request.dto.ts | 4 +- .../requests/offer.credential.request.dto.ts | 5 +- .../credential.offer.response.dto.ts | 1 + 9 files changed, 219 insertions(+), 157 deletions(-) diff --git a/README.md b/README.md index 431bb289..43febe1a 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,6 @@ - [Docker](#docker) - [Usage via Postman](#usage-via-postman) - [Send Didcomm messages between two OCMs](#send-didcomm-messages-between-two-ocms) - - [Revocation flow](#revocation-flow) - [License](#license) ## Introduction @@ -159,14 +158,17 @@ Example: 12. On the socket of issuer a event will be received with the message and connection id. -## Revocation flow +## License +This project is licensed under the AGPL License - see the [LICENSE](LICENSE) file for details. -1. Upon creating a credential definition, set `supportRevocation: true`, and get the revocationRegistryId from the response -2. When issuing a credential with this credential definition, set the `revocationRegistryDefinitionId` property to the revocationRegistryId -3. to check the status of a credential -> GET '/v1/credentials/:id/status (using the id of the credential you want to check the revocation status of) -4. to revoke -> GET '/v1/redentials/:id/revoke (using the id of the credential you want to revoke) -## License -This project is licensed under the AGPL License - see the [LICENSE](LICENSE) file for details. +DEF +did:indy:bcovrin:test:7wV7McGyL5MnfpDF321NS3/anoncreds/v0/CLAIM_DEF/507389/asd1ST333 + + + +id 1 +5a6aa65e-dd93-4c84-8f8a-c6d55b3552e5 +id 2 \ No newline at end of file diff --git a/agent-swagger.json b/agent-swagger.json index 5804270d..58cca66f 100644 --- a/agent-swagger.json +++ b/agent-swagger.json @@ -665,7 +665,7 @@ } }, "/api/v1/credentials/{id}/revoke": { - "get": { + "patch": { "operationId": "RestController_revokeCredentialById", "parameters": [ { @@ -1418,20 +1418,12 @@ }, "tag": { "type": "string" - }, - "revocationRegistry": { - "type": "object" - }, - "revocationStatusList": { - "type": "object" } }, "required": [ "schemaId", "issuerId", - "tag", - "revocationRegistry", - "revocationStatusList" + "tag" ] }, "CreateCredentialDefinitionRequestDto": { @@ -1444,7 +1436,8 @@ "type": "string" }, "supportRevocation": { - "type": "boolean" + "type": "boolean", + "default": false } }, "required": [ @@ -1483,18 +1476,14 @@ "$ref": "#/components/schemas/OfferCredentialAttributes" } }, - "revocationRegistryDefinitionId": { - "type": "string" - }, - "revocationRegistryIndex": { - "type": "number" + "revocable": { + "type": "boolean" } }, "required": [ "credentialDefinitionId", "attributes", - "revocationRegistryDefinitionId", - "revocationRegistryIndex" + "revocable" ] }, "CredentialRecordDto": { @@ -1562,6 +1551,12 @@ }, "credentialRecord": { "$ref": "#/components/schemas/CredentialRecordDto" + }, + "revocationStatusListIndex": { + "type": "number" + }, + "revocationRegistryId": { + "type": "string" } }, "required": [ @@ -1753,6 +1748,9 @@ }, "message": { "type": "string" + }, + "revocationNotification": { + "type": "object" } }, "required": [ diff --git a/libs/askar/src/agent.utils.ts b/libs/askar/src/agent.utils.ts index e3591cba..92b49705 100644 --- a/libs/askar/src/agent.utils.ts +++ b/libs/askar/src/agent.utils.ts @@ -77,6 +77,9 @@ import { Request, Response, Express } from "express"; import url from "url"; import { JsonLdCredentialFormatService } from "./credo/JsonLdCredentialFormatService"; import { AskarService } from "."; +import { OfferCredentialRequestDto } from "@ocm-engine/dtos"; +import { uuid } from "@credo-ts/core/build/utils/uuid"; +import { GenericRecord } from "@credo-ts/core/build/modules/generic-records/repository/GenericRecord"; @@ -662,14 +665,18 @@ export const getRevocationDetails = async ( ) => { const credential = await askar.agent.credentials.getById(credentialId); - const metadata = credential.metadata.get<AnonCredsCredentialMetadata>( - '_anoncreds/credential', - ); + // const metadata = credential.metadata.get<AnonCredsCredentialMetadata>( + // '_anoncreds/credential', + // ); + + const credentialRevocationRegistryDefinitionId = credential.getTag( + 'anonCredsRevocationRegistryId' + ) as string + const credentialRevocationIndex = credential.getTag('anonCredsCredentialRevocationId') as string if ( - !metadata || - !metadata.revocationRegistryId || - !metadata.credentialRevocationId + !credentialRevocationRegistryDefinitionId || + !credentialRevocationIndex ) { throw new Error( `Credential with id=${credentialId} has no metadata, likely issued without support for revocation.`, @@ -678,31 +685,154 @@ export const getRevocationDetails = async ( const { revocationRegistryDefinition } = await askar.agent.modules.anoncreds.getRevocationRegistryDefinition( - metadata.revocationRegistryId, + credentialRevocationRegistryDefinitionId, ); if (!revocationRegistryDefinition) { throw new Error( - `Could not find the revocation registry definition for id=${metadata.revocationRegistryId}.`, + `Could not find the revocation registry definition for id=${credentialRevocationRegistryDefinitionId}.`, ); } const timestamp = Math.floor(Date.now() / 1000); const { revocationStatusList } = await askar.agent.modules.anoncreds.getRevocationStatusList( - metadata.revocationRegistryId, + credentialRevocationRegistryDefinitionId, timestamp, ); if (!revocationStatusList) { throw new Error( - `Could not find the revocation status list for revocation registry definition id: ${metadata.revocationRegistryId} and timestamp: ${timestamp}.`, + `Could not find the revocation status list for revocation registry definition id: ${credentialRevocationRegistryDefinitionId} and timestamp: ${timestamp}.`, ); } return { - credentialRevocationId: Number(metadata.credentialRevocationId), + credentialRevocationId: Number(credentialRevocationIndex), revocationStatusList, - revocationRegistryId: metadata.revocationRegistryId, + revocationRegistryId: credentialRevocationRegistryDefinitionId, }; +} + + +export const handleRevocationForCredDef = async ( + context: AskarService, + offerCredentialDto: OfferCredentialRequestDto +): Promise<{ + revocRecord: GenericRecord[], + statusListIndex: string | number, + revocationRegistryDefinitionId: string | undefined, +}> => { + let statusListIndex = undefined; + let customRevocRecord = undefined; + let revocationRegistryDefinitionId = undefined; + + console.log('Issuing revocable credential.'); + + customRevocRecord = await context.agent.genericRecords.findAllByQuery({ + credentialDefinitionId: offerCredentialDto.credentialDefinitionId, + }) + + console.log('Using saved revocation custom record.'); + + if (!customRevocRecord || customRevocRecord.length < 1 || !customRevocRecord[0]) { + console.log("Registering new revocation records."); + + const { + revocStatusList, + revocReg, + } = await registerRevocationDefAndList(context, offerCredentialDto.credentialDefinitionId); + + customRevocRecord = await context.agent.genericRecords.save({ + id: uuid(), + tags: { + credentialDefinitionId: offerCredentialDto.credentialDefinitionId, + revocationRegistryDefinitionId: revocReg.revocationRegistryDefinitionState.revocationRegistryDefinitionId, + latestIndex: '0', + }, + content: { + credentialDefinitionId: offerCredentialDto.credentialDefinitionId, + revocationRegistryDefinitionId: revocReg.revocationRegistryDefinitionState.revocationRegistryDefinitionId, + } + }) + + customRevocRecord = [customRevocRecord] + + if (!customRevocRecord) { + throw new Error("Could not save generic record for revocation status tracking."); + } + } + + statusListIndex = Number(customRevocRecord[0].getTags()['latestIndex']) + revocationRegistryDefinitionId = customRevocRecord[0].getTags()['revocationRegistryDefinitionId'] as string; + + if ( + statusListIndex && + statusListIndex > context.agentConfig.revocationListSize + ) { + const { + revocStatusList, + revocReg, + } = await registerRevocationDefAndList(context, offerCredentialDto.credentialDefinitionId); + + customRevocRecord[0].setTag("latestIndex", "0") + customRevocRecord[0].setTag("revocationRegistryDefinitionId", revocReg.revocationRegistryDefinitionState.revocationRegistryDefinitionId) + await context.agent.genericRecords.update(customRevocRecord[0]) + + revocationRegistryDefinitionId = revocReg.revocationRegistryDefinitionState.revocationRegistryDefinitionId; + statusListIndex = "0"; + } + + + return { + revocRecord: customRevocRecord, + statusListIndex: statusListIndex, + revocationRegistryDefinitionId, + } +} + + + + +export const registerRevocationDefAndList = async ( + context: AskarService, + credentialDefinitionId: string +) => { + const dids = await context.agent.dids.getCreatedDids({ method: "indy" }); + + let revocReg = await context.agent.modules.anoncreds.registerRevocationRegistryDefinition({ + revocationRegistryDefinition: { + maximumCredentialNumber: context.agentConfig.revocationListSize, + issuerId: dids[0].did, + tag: uuid(), + credentialDefinitionId: credentialDefinitionId, + }, + options: {} + }); + + if ( + !revocReg || + revocReg.revocationRegistryDefinitionState.state !== 'finished' + ) { + throw new Error("Failed to register revocation definition on ledger."+revocReg) + } + + const revocationRegistryDefinitionId = revocReg.revocationRegistryDefinitionState.revocationRegistryDefinitionId; + + let revocStatusList = await context.agent.modules.anoncreds.registerRevocationStatusList({ + revocationStatusList: { + revocationRegistryDefinitionId, + issuerId: dids[0].did, + }, + options: {} + }) + + if (!revocStatusList || revocStatusList.revocationStatusListState.state !== 'finished') { + throw new Error("Failed to register revocation status list on ledger."+revocStatusList); + } + + return { + revocStatusList, + revocReg, + } } \ No newline at end of file diff --git a/libs/askar/src/askar-rest/rest.controller.ts b/libs/askar/src/askar-rest/rest.controller.ts index c10eae51..d631d55a 100644 --- a/libs/askar/src/askar-rest/rest.controller.ts +++ b/libs/askar/src/askar-rest/rest.controller.ts @@ -8,6 +8,7 @@ import { Post, UseFilters, UseGuards, + Patch, } from "@nestjs/common"; import { AgentService } from "../askar/agent.service"; @@ -169,7 +170,7 @@ export class RestController { return this.agentService.getCredentialStatus(credentialId); } - @Get("/credentials/:id/revoke") + @Patch("/credentials/:id/revoke") async revokeCredentialById(@Param("id") credentialId: string) { return this.agentService.revokeCredentialById(credentialId); } diff --git a/libs/askar/src/askar/agent.service.ts b/libs/askar/src/askar/agent.service.ts index 02920718..4fa362c9 100644 --- a/libs/askar/src/askar/agent.service.ts +++ b/libs/askar/src/askar/agent.service.ts @@ -54,10 +54,11 @@ import { JsonCredential, W3cJsonLdVerifiableCredential, } from "@credo-ts/core"; -import { AnonCredsRequestedAttribute } from "@credo-ts/anoncreds"; +import { AnonCredsCredentialMetadata, AnonCredsRequestedAttribute } from "@credo-ts/anoncreds"; import { uuid } from "@credo-ts/core/build/utils/uuid"; import { getRevocationDetails, + handleRevocationForCredDef, waitForCredentialExchangeRecordSubject, waitForProofExchangeRecordSubject, } from "../agent.utils"; @@ -410,63 +411,15 @@ export class AgentService { throw new CredentialNotCreatedError(); } - // TODO: add types to responses of anoncreds - - let revocReg; - if (supportRevocation) { - revocReg = await this.askar.agent.modules.anoncreds.registerRevocationRegistryDefinition({ - revocationRegistryDefinition: { - maximumCredentialNumber: this.askar.agentConfig.revocationListSize, - issuerId: dids[0].did, - tag: credentialDefinitionDto.tag, - credentialDefinitionId: credDef.credentialDefinitionState.credentialDefinitionId, - }, - options: {} - }); - console.log('revocationRegisterResponse:', revocReg); - - } - - // TODO: handle revoc registry error - - const response = new CreddefRecordDto(); - response.revocationRegistry = revocReg; - response.id = credDef.credentialDefinitionState.credentialDefinitionId; response.schemaId = credDef.credentialDefinitionState.credentialDefinition.schemaId; response.issuerId = credDef.credentialDefinitionState.credentialDefinition.issuerId; response.tag = credDef.credentialDefinitionState.credentialDefinition.tag; - - let revocStatusList; - if (revocReg && revocReg.revocationRegistryDefinitionState.state === 'finished') { - response.revocationRegistry = revocReg.revocationRegistryDefinitionState; - - const revocationRegistryDefinitionId = revocReg.revocationRegistryDefinitionState.revocationRegistryDefinitionId; - - revocStatusList = await this.askar.agent.modules.anoncreds.registerRevocationStatusList({ - revocationStatusList: { - revocationRegistryDefinitionId, - issuerId: dids[0].did, - }, - options: {} - }) - - console.log('RevocationStatusListRegisterResponse: ', revocStatusList); - } - - response.revocationStatusList = revocStatusList; - - if (revocStatusList && revocStatusList.revocationStatusListState.state === 'finished') { - response.revocationStatusList = revocStatusList.revocationStatusListState; - } - - // TODO: handle revoc status list error - return response; }; @@ -710,54 +663,16 @@ export class AgentService { JSON.stringify(offerCredentialDto, null, 2), ); - 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 ( - statusListIndex && - statusListIndex >= this.askar.agentConfig.revocationListSize - 1 - ) { - throw new Error('Revocation Status List has been exhausted. Please create a new Revocation Registry Definition for this Credential Definition. ... Not yet implemented.'); + let customRevocationRecord = undefined; + let statusListIdx = undefined; + let revocationRegistryDef = undefined; + if (offerCredentialDto.revocable) { + const { revocRecord, statusListIndex, revocationRegistryDefinitionId } = await handleRevocationForCredDef(this.askar, offerCredentialDto); + customRevocationRecord = revocRecord; + statusListIdx = statusListIndex; + revocationRegistryDef = revocationRegistryDefinitionId; } - // 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 } = await this.askar.agent.credentials.createOffer({ @@ -766,8 +681,8 @@ export class AgentService { anoncreds: { credentialDefinitionId: offerCredentialDto.credentialDefinitionId, attributes: offerCredentialDto.attributes, - revocationRegistryDefinitionId: offerCredentialDto.revocationRegistryDefinitionId || undefined, - revocationRegistryIndex: typeof statusListIndex === 'undefined' ? undefined : statusListIndex+1, + revocationRegistryDefinitionId: revocationRegistryDef ? revocationRegistryDef : undefined, + revocationRegistryIndex: typeof statusListIdx === 'undefined' ? undefined : Number(statusListIdx), }, }, autoAcceptCredential: AutoAcceptCredential.ContentApproved, @@ -777,15 +692,16 @@ export class AgentService { await this.askar.agent.credentials.update(credentialRecord); - // INFO: update with latest index + // INFO: update with latest index once it is issued if ( - offerCredentialDto.revocationRegistryDefinitionId && - customRevocRecord && - customRevocRecord.length > 0 && - typeof statusListIndex !== 'undefined' + offerCredentialDto.revocable && + revocationRegistryDef && + customRevocationRecord && + customRevocationRecord.length > 0 && + typeof statusListIdx !== 'undefined' ) { - customRevocRecord[0].setTag("latestIndex", (statusListIndex+1).toString()) - await this.askar.agent.genericRecords.update(customRevocRecord[0]) + customRevocationRecord[0].setTag("latestIndex", (Number(statusListIdx)+1).toString()) + await this.askar.agent.genericRecords.update(customRevocationRecord[0]) } @@ -812,7 +728,8 @@ export class AgentService { credentialUrl: credentialUrl, shortCredentialUrl: shortCredentialUrl, credentialRecord: dto, - revocationStatusListIndex: statusListIndex, + revocationStatusListIndex: Number(statusListIdx), + revocationRegistryId: revocationRegistryDef, }; } const credentialExchangeRecord = @@ -823,8 +740,8 @@ export class AgentService { anoncreds: { credentialDefinitionId: offerCredentialDto.credentialDefinitionId, attributes: offerCredentialDto.attributes, - revocationRegistryDefinitionId: offerCredentialDto.revocationRegistryDefinitionId || undefined, - revocationRegistryIndex: typeof statusListIndex === 'undefined' ? undefined : statusListIndex+1, + revocationRegistryDefinitionId: revocationRegistryDef || undefined, + revocationRegistryIndex: typeof statusListIdx === 'undefined' ? undefined : Number(statusListIdx), }, }, }); @@ -844,7 +761,8 @@ export class AgentService { credentialUrl: null, shortCredentialUrl: null, credentialRecord: dto, - revocationStatusListIndex: statusListIndex, + revocationStatusListIndex: Number(statusListIdx), + revocationRegistryId: revocationRegistryDef, }; }; @@ -1061,7 +979,7 @@ export class AgentService { throw new Error( `Credential with id=${credentialId}, with revocation id=${credentialRevocationId}, is already in a revoked state.`, ); - } + } const updatedList = await this.askar.agent.modules.anoncreds.updateRevocationStatusList({ revocationStatusList: { @@ -1078,12 +996,27 @@ export class AgentService { )}`, ); } + + + try { + const sentRevocationNotification = await this.askar.agent.credentials.sendRevocationNotification({ + credentialRecordId: credentialId, + revocationId: `${revocationRegistryId}::${credentialRevocationId.toString()}`, + revocationFormat: 'anoncreds', + }) + console.log(sentRevocationNotification); + } catch (err) { + console.log(err); + } + // TODO: handle error from sentRevocationNotification + + return { revocationRegistryId: revocationRegistryId, revocationId: credentialRevocationId, valid: false, - message: 'Credential Revocation Status Updated.' + message: 'Credential Revocation Status Updated.', } } diff --git a/libs/dtos/src/dtos/generics/creddef.record.dto.ts b/libs/dtos/src/dtos/generics/creddef.record.dto.ts index 01d23a1d..873b8545 100644 --- a/libs/dtos/src/dtos/generics/creddef.record.dto.ts +++ b/libs/dtos/src/dtos/generics/creddef.record.dto.ts @@ -13,8 +13,4 @@ export class CreddefRecordDto extends BaseRecordDto { @IsNotEmpty() @IsString() tag: string; - - revocationRegistry: any; // TODO: validate better once format is clear - - revocationStatusList: any; // TODO: validate better once format is clear } diff --git a/libs/dtos/src/dtos/requests/create.credential.definition.request.dto.ts b/libs/dtos/src/dtos/requests/create.credential.definition.request.dto.ts index dedf6c90..f3e35903 100644 --- a/libs/dtos/src/dtos/requests/create.credential.definition.request.dto.ts +++ b/libs/dtos/src/dtos/requests/create.credential.definition.request.dto.ts @@ -9,7 +9,7 @@ export class CreateCredentialDefinitionRequestDto { @IsString() tag: string; - @IsOptional() @IsBoolean() - supportRevocation?: boolean; + @IsOptional() + supportRevocation?: boolean = false; } 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 b23a63f8..9876716f 100644 --- a/libs/dtos/src/dtos/requests/offer.credential.request.dto.ts +++ b/libs/dtos/src/dtos/requests/offer.credential.request.dto.ts @@ -1,6 +1,7 @@ import { ArrayMinSize, IsArray, + IsBoolean, IsNotEmpty, IsNumber, IsOptional, @@ -36,9 +37,9 @@ export class OfferCredentialRequestDto { @Type(() => OfferCredentialAttributes) attributes: Array<OfferCredentialAttributes>; + @IsBoolean() @IsOptional() - @IsString() - revocationRegistryDefinitionId: string; + revocable: boolean; } 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 bdcf47f8..f8a8ecd9 100644 --- a/libs/dtos/src/dtos/responses/credential.offer.response.dto.ts +++ b/libs/dtos/src/dtos/responses/credential.offer.response.dto.ts @@ -5,4 +5,5 @@ export class CredentialOfferResponseDto { public shortCredentialUrl: string | null; public credentialRecord: CredentialRecordDto; public revocationStatusListIndex?: number | undefined; + public revocationRegistryId?: string | undefined; } -- GitLab