From 6a8dc00f022c9f3af9815222a37eefaec9767e4d Mon Sep 17 00:00:00 2001 From: Yash Mittal Date: Fri, 8 Sep 2023 18:43:07 +0530 Subject: [PATCH] feat: add revocation functions --- .../migration.sql | 9 + prisma/schema.prisma | 7 + src/app.module.ts | 12 +- src/credentials/credentials.module.ts | 4 +- src/revocation-list/revocation-list.helper.ts | 42 +++ src/revocation-list/revocation-list.impl.ts | 15 + src/revocation-list/revocation-list.module.ts | 14 + .../revocation-list.service.spec.ts | 18 ++ .../revocation-list.service.ts | 259 ++++++++++++++++++ src/utils/utils.module.ts | 4 + 10 files changed, 381 insertions(+), 3 deletions(-) create mode 100644 prisma/migrations/20230908124838_add_revocation_list/migration.sql create mode 100644 src/revocation-list/revocation-list.helper.ts create mode 100644 src/revocation-list/revocation-list.impl.ts create mode 100644 src/revocation-list/revocation-list.module.ts create mode 100644 src/revocation-list/revocation-list.service.spec.ts create mode 100644 src/revocation-list/revocation-list.service.ts create mode 100644 src/utils/utils.module.ts diff --git a/prisma/migrations/20230908124838_add_revocation_list/migration.sql b/prisma/migrations/20230908124838_add_revocation_list/migration.sql new file mode 100644 index 0000000..7a90524 --- /dev/null +++ b/prisma/migrations/20230908124838_add_revocation_list/migration.sql @@ -0,0 +1,9 @@ +-- CreateTable +CREATE TABLE "RevocationLists" ( + "issuer" TEXT NOT NULL, + "latestRevocationListId" TEXT NOT NULL, + "lastCredentialIdx" INTEGER NOT NULL, + "allRevocationLists" TEXT[], + + CONSTRAINT "RevocationLists_pkey" PRIMARY KEY ("issuer") +); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 5f12f91..1e0cf65 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -37,3 +37,10 @@ model VerifiableCredentials { updatedBy String? tags String[] } + +model RevocationLists { + issuer String @id + latestRevocationListId String + lastCredentialIdx Int + allRevocationLists String[] +} diff --git a/src/app.module.ts b/src/app.module.ts index 407df07..8a8cffa 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -7,15 +7,23 @@ import { CredentialsModule } from './credentials/credentials.module'; import { TerminusModule } from '@nestjs/terminus'; import { HealthCheckUtilsService } from './credentials/utils/healthcheck.utils.service'; import { PrismaClient } from '@prisma/client'; +import { RevocationListService } from './revocation-list/revocation-list.service'; +import { RevocationListModule } from './revocation-list/revocation-list.module'; +import { UtilsModule } from './utils/utils.module'; +import { RevocationList } from './revocation-list/revocation-list.helper'; +import { RevocationListImpl } from './revocation-list/revocation-list.impl'; +import { IdentityUtilsService } from './credentials/utils/identity.utils.service'; @Module({ imports: [ HttpModule, ConfigModule.forRoot({ isGlobal: true }), CredentialsModule, - TerminusModule + TerminusModule, + RevocationListModule, + UtilsModule ], controllers: [AppController], - providers: [AppService, ConfigService, PrismaClient, HealthCheckUtilsService], + providers: [AppService, ConfigService, PrismaClient, HealthCheckUtilsService, RevocationListService, RevocationList, RevocationListImpl, IdentityUtilsService], }) export class AppModule {} diff --git a/src/credentials/credentials.module.ts b/src/credentials/credentials.module.ts index ef365f8..f1d6208 100644 --- a/src/credentials/credentials.module.ts +++ b/src/credentials/credentials.module.ts @@ -6,10 +6,12 @@ import { IdentityUtilsService } from './utils/identity.utils.service'; import { RenderingUtilsService } from './utils/rendering.utils.service'; import { SchemaUtilsSerivce } from './utils/schema.utils.service'; import { PrismaClient } from '@prisma/client'; +import { HealthCheckUtilsService } from './utils/healthcheck.utils.service'; @Module({ imports: [HttpModule], - providers: [CredentialsService, PrismaClient, IdentityUtilsService, RenderingUtilsService, SchemaUtilsSerivce], + providers: [CredentialsService, PrismaClient, IdentityUtilsService, RenderingUtilsService, SchemaUtilsSerivce, HealthCheckUtilsService], controllers: [CredentialsController], + exports: [HealthCheckUtilsService] }) export class CredentialsModule {} diff --git a/src/revocation-list/revocation-list.helper.ts b/src/revocation-list/revocation-list.helper.ts new file mode 100644 index 0000000..1032c43 --- /dev/null +++ b/src/revocation-list/revocation-list.helper.ts @@ -0,0 +1,42 @@ +import { Injectable } from '@nestjs/common'; +// @ts-ignore +import { Bitstring } from '@SamagraX-RCW/bitstring'; + +type BitstringConstructorParam = { + length?: number, + buffer?: Uint8Array +}; + +export class RevocationList { + + private bitstring: any; + // private length: number; + + constructor({ length, buffer }: BitstringConstructorParam = { length: 100000 }) { + // if (length === undefined) length = 100000; + this.bitstring = new Bitstring({ length, buffer }); + // this.length = this.bitstring.length; + } + + + setRevoked(index, revoked) { + if (typeof revoked !== 'boolean') { + throw new TypeError('revoked must be a boolean.'); + } + + return this.bitstring.set(index, revoked); + } + + isRevoked(index) { + return this.bitstring.get(index); + } + + async encode() { + return this.bitstring.encodeBits(); + } + + static async decode({ encodedList }) { + const buffer = await Bitstring.decodeBits({ encoded: encodedList }); + return new RevocationList({ buffer }); + } +} diff --git a/src/revocation-list/revocation-list.impl.ts b/src/revocation-list/revocation-list.impl.ts new file mode 100644 index 0000000..d157ecc --- /dev/null +++ b/src/revocation-list/revocation-list.impl.ts @@ -0,0 +1,15 @@ +import { Injectable } from '@nestjs/common'; +import { RevocationList } from './revocation-list.helper'; + +@Injectable() +export class RevocationListImpl { + constructor() {} + + public createList({ length }) { + return new RevocationList({ length }); + } + + public async decodeList({ encodedList }) { + return await RevocationList.decode({ encodedList }); + } +} diff --git a/src/revocation-list/revocation-list.module.ts b/src/revocation-list/revocation-list.module.ts new file mode 100644 index 0000000..0548910 --- /dev/null +++ b/src/revocation-list/revocation-list.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { RevocationListService } from './revocation-list.service'; +import { RevocationList } from './revocation-list.helper'; +import { PrismaClient } from '@prisma/client'; +import { RevocationListImpl } from './revocation-list.impl'; +import { CredentialsModule } from 'src/credentials/credentials.module'; +import { IdentityUtilsService } from 'src/credentials/utils/identity.utils.service'; +import { HttpModule, HttpService } from '@nestjs/axios'; + +@Module({ + imports: [HttpModule], + providers: [RevocationList, RevocationListService, PrismaClient, RevocationListImpl, IdentityUtilsService] +}) +export class RevocationListModule {} diff --git a/src/revocation-list/revocation-list.service.spec.ts b/src/revocation-list/revocation-list.service.spec.ts new file mode 100644 index 0000000..8ae93a8 --- /dev/null +++ b/src/revocation-list/revocation-list.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { RevocationListService } from './revocation-list.service'; + +describe('RevocationListService', () => { + let service: RevocationListService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [RevocationListService], + }).compile(); + + service = module.get(RevocationListService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/revocation-list/revocation-list.service.ts b/src/revocation-list/revocation-list.service.ts new file mode 100644 index 0000000..2c7507a --- /dev/null +++ b/src/revocation-list/revocation-list.service.ts @@ -0,0 +1,259 @@ +import { HttpService } from '@nestjs/axios'; +import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common'; +import { CredentialPayload, transformCredentialInput } from 'did-jwt-vc'; +import { DIDDocument } from 'did-resolver'; +import { IdentityUtilsService } from '../credentials/utils/identity.utils.service'; +// import { PrismaService } from 'src/prisma.service'; +import { PrismaClient } from '@prisma/client'; +import { IssuerType, Proof } from 'did-jwt-vc/lib/types'; +import { JwtCredentialSubject } from 'src/app.interface'; +import { RevocationLists, VerifiableCredentials } from '@prisma/client'; +import { RevocationListImpl } from './revocation-list.impl'; +import { RevocationList } from './revocation-list.helper'; + +@Injectable() +export class RevocationListService { + constructor( + private readonly prismaService: PrismaClient, + private readonly rl: RevocationListImpl, + private readonly identityService: IdentityUtilsService) {} + + private async signRevocationListCredential(revocationListCredential, issuer) { + try { + revocationListCredential['proof'] = { + proofValue: await this.identityService.signVC( + transformCredentialInput(revocationListCredential as CredentialPayload), + issuer, + ), + type: 'Ed25519Signature2020', + created: new Date().toISOString(), + verificationMethod: issuer, + }; + } catch (err) { + Logger.error('Error signing revocation list', err); + throw new InternalServerErrorException( + 'Error signing revocation list', + ); + } + + return revocationListCredential.proof; + } + + private generateRevocationListCredentialSkeleton(id: string, issuer: string) { + return { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/vc-revocation-list-2020/v1" + ], + id, + type: ["VerifiableCredential", "RevocationList2020Credential"], + issuer, + issuanceDate: new Date().toISOString(), + credentialSubject: { + id, + type: "RevocationList2020", + }, + proof: {} + }; + } + async createNewRevocationList(issuer: string) { + // generate did for the revocation list + let credDID: ReadonlyArray; + try { + credDID = await this.identityService.generateDID(['verifiable credential']); + } catch (err) { + Logger.error('Error generating DID for revocation list', err); + throw new InternalServerErrorException( + 'Error generating DID for revocation list', + ) + } + + const revocationList = await this.rl.createList({ length: 100000 }); + const encodedList = await revocationList.encode(); + + const revocationListCredential = this.generateRevocationListCredentialSkeleton(credDID[0]?.id, issuer); + revocationListCredential.credentialSubject['encodedList'] = encodedList; + // sign the revocation list + revocationListCredential.proof = await this.signRevocationListCredential(revocationListCredential, issuer); + + + // save this in the db + try { + const revocationListsOfIssuer = await this.prismaService.revocationLists.findUnique({ + where: { + issuer + } + }); + if (!revocationListsOfIssuer) { + await this.prismaService.$transaction([ + // save the revocation list as a verifiable credential + this.prismaService.verifiableCredentials.create({ + data: { + id: revocationListCredential.id, + type: revocationListCredential.type, + issuer: revocationListCredential.issuer as IssuerType as string, + issuanceDate: revocationListCredential.issuanceDate, + expirationDate: '', + subject: revocationListCredential.credentialSubject as JwtCredentialSubject, + subjectId: (revocationListCredential.credentialSubject as JwtCredentialSubject).id, + proof: revocationListCredential.proof as Proof, + credential_schema: '', // HOST A JSONLD for this in the github repo and link to that + signed: revocationListCredential as object, + tags: ['RevocationList2020Credential', 'RevocationList2020'], + } + }), + // update the revocation list data + + this.prismaService.revocationLists.create({ + data: { + issuer, + latestRevocationListId: revocationListCredential.id, + lastCredentialIdx: 0, + allRevocationLists: [revocationListCredential.id] + } + }) + ]) + } else { + await this.prismaService.$transaction([ + this.prismaService.verifiableCredentials.create({ + data: { + id: revocationListCredential.id, + type: revocationListCredential.type, + issuer: revocationListCredential.issuer as IssuerType as string, + issuanceDate: revocationListCredential.issuanceDate, + expirationDate: '', + subject: revocationListCredential.credentialSubject as JwtCredentialSubject, + subjectId: (revocationListCredential.credentialSubject as JwtCredentialSubject).id, + proof: revocationListCredential.proof as Proof, + credential_schema: '', // HOST A JSONLD for this in the github repo and link to that + signed: revocationListCredential as object, + tags: ['RevocationList2020Credential', 'RevocationList2020'], + } + }), + // update the revocation list data + this.prismaService.revocationLists.update({ + where: { + issuer + }, + data: { + latestRevocationListId: revocationListCredential.id, + lastCredentialIdx: 0, + allRevocationLists: [revocationListCredential.id, ...revocationListsOfIssuer.allRevocationLists] + } + }) + ]) + } + } catch (err) { + Logger.error('Error saving the revocation list credential into db: ', err); + throw new InternalServerErrorException( + 'Error saving the revocation list credential into db', + ); + } + + return revocationListCredential; + } + + async updateRevocationList(issuer: string, idx: number) { + // fetch the revocation list from the db + let revocationList: VerifiableCredentials; + let revocationListInfo: RevocationLists; + try { + revocationListInfo = await this.prismaService.revocationLists.findUnique({ + where: { + issuer + } + }); + + revocationList = await this.prismaService.verifiableCredentials.findUnique({ + where: { + id: revocationListInfo.latestRevocationListId + } + }); + } catch (err) { + Logger.error('Error fetching revocation list from db', err); + throw new InternalServerErrorException( + 'Error fetching revocation list from db', + ); + } + + let revocationListCredential; + try { + const encodedList = (revocationList.subject as any).encodedList; + const decodedList: RevocationList = await this.rl.decodeList({ encodedList }); + decodedList.setRevoked(idx, true); + const updatedEncodedList = await decodedList.encode(); + // update the RevocationListCredential by resigning it + revocationListCredential = this.generateRevocationListCredentialSkeleton(revocationList.id, issuer); + revocationListCredential.credentialSubject['encodedList'] = updatedEncodedList; + } catch (err) { + Logger.error('Error updating the revocation list bits: ', err); + throw new InternalServerErrorException( + 'Error updating the revocation list bits', + ); + } + // sign this again + try { + revocationListCredential.proof = await this.signRevocationListCredential(revocationListCredential, issuer); + } catch (err) { + Logger.error('Error signing the revocation list credential', err); + throw new InternalServerErrorException( + 'Error signing the revocation list credential', + ); + } + + // update the db + let revocationListCredentialId = revocationListInfo.latestRevocationListId; + try { + // update the proof of revocation list credential + await this.prismaService.verifiableCredentials.update({ + where: { + id: revocationList.id + }, + data: { + proof: revocationListCredential.proof as Proof, + } + }); + if ((revocationListInfo.lastCredentialIdx) < 100000) { + // update the counter of index in the revocation list + this.prismaService.revocationLists.update({ + where: { + issuer + }, + data: { + lastCredentialIdx: idx + 1, + } + }) + } else { + // create a new revocation list + const newRevocationList = await this.createNewRevocationList(issuer); + revocationListCredentialId = newRevocationList.id; + } + } catch (err) { + Logger.error('Error updating the revocation list credential into db: ', err); + throw new InternalServerErrorException( + 'Error updating the revocation list credential into db', + ); + } + + return revocationListCredentialId; + } + + async getDecodedRevocationString(revocationCredentialId: string) { + // fetch the credential from db + try { + const revocationListCredential = await this.prismaService.verifiableCredentials.findUnique({ + where: { + id: revocationCredentialId + } + }); + const encodedList = (revocationListCredential.subject as any).encodedList; + const decodedList = await this.rl.decodeList({ encodedList }); + return decodedList; + } catch (err) { + Logger.error('Error fetching the RevocationListCredential from db', err); + throw new InternalServerErrorException( + 'Error fetching the RevocationListCredential from db', + ); + } + } +} diff --git a/src/utils/utils.module.ts b/src/utils/utils.module.ts new file mode 100644 index 0000000..6ab7dfe --- /dev/null +++ b/src/utils/utils.module.ts @@ -0,0 +1,4 @@ +import { Module } from '@nestjs/common'; + +@Module({}) +export class UtilsModule {}