From a9bc2b4639bd8cbb98cfae8863a962a18f47576e Mon Sep 17 00:00:00 2001 From: Berend Sliedrecht <berend@animo.id> Date: Tue, 28 Nov 2023 15:36:37 +0100 Subject: [PATCH] feat(ssi): block connection by id or did Signed-off-by: Berend Sliedrecht <berend@animo.id> --- .../events/__tests__/connectionEvents.spec.ts | 18 ++++ apps/shared/src/events/connectionEvents.ts | 36 +++---- apps/shared/src/events/didEvents.ts | 32 ++++++ apps/shared/src/index.ts | 1 + .../src/agent/agent.service.ts | 14 ++- .../connections/connections.controller.ts | 12 +++ .../agent/connections/connections.service.ts | 32 ++++++ .../dids/__tests__/dids.controller.spec.ts | 36 +++++++ .../src/agent/dids/dids.controller.ts | 15 +++ .../src/agent/dids/dids.module.ts | 13 +++ .../src/agent/dids/dids.service.ts | 31 ++++++ .../test/connections.e2e-spec.ts | 13 +++ apps/ssi-abstraction/test/dids.e2e-spec.ts | 97 +++++++++++++++++++ 13 files changed, 328 insertions(+), 22 deletions(-) create mode 100644 apps/shared/src/events/didEvents.ts create mode 100644 apps/ssi-abstraction/src/agent/dids/__tests__/dids.controller.spec.ts create mode 100644 apps/ssi-abstraction/src/agent/dids/dids.controller.ts create mode 100644 apps/ssi-abstraction/src/agent/dids/dids.module.ts create mode 100644 apps/ssi-abstraction/src/agent/dids/dids.service.ts create mode 100644 apps/ssi-abstraction/test/dids.e2e-spec.ts diff --git a/apps/shared/src/events/__tests__/connectionEvents.spec.ts b/apps/shared/src/events/__tests__/connectionEvents.spec.ts index 42ea6e6..70f97f0 100644 --- a/apps/shared/src/events/__tests__/connectionEvents.spec.ts +++ b/apps/shared/src/events/__tests__/connectionEvents.spec.ts @@ -5,6 +5,7 @@ import { } from '@aries-framework/core'; import { + EventDidcommConnectionsBlock, EventDidcommConnectionsCreateWithSelf, EventDidcommConnectionsGetAll, EventDidcommConnectionsGetById, @@ -49,4 +50,21 @@ describe('Connection Events', () => { state: DidExchangeState.Completed, }); }); + + it('should create a new connections block event', () => { + const event = new EventDidcommConnectionsBlock( + new ConnectionRecord({ + role: DidExchangeRole.Requester, + state: DidExchangeState.Completed, + }), + ); + + expect(typeof event.id).toStrictEqual('string'); + expect(event.type).toStrictEqual('EventDidcommConnectionsBlock'); + expect(event.timestamp).toBeInstanceOf(Date); + expect(event.instance).toMatchObject({ + role: DidExchangeRole.Requester, + state: DidExchangeState.Completed, + }); + }); }); diff --git a/apps/shared/src/events/connectionEvents.ts b/apps/shared/src/events/connectionEvents.ts index 1831f5c..1ec09a5 100644 --- a/apps/shared/src/events/connectionEvents.ts +++ b/apps/shared/src/events/connectionEvents.ts @@ -1,23 +1,7 @@ -import { - ConnectionRecord, - DidDocument, - JsonTransformer, -} from '@aries-framework/core'; +import { ConnectionRecord, JsonTransformer } from '@aries-framework/core'; import { BaseEvent } from './baseEvents.js'; -export class EventInfoPublicDid extends BaseEvent<DidDocument> { - public static token = 'didcomm.info.publicDid'; - - public get instance() { - return JsonTransformer.fromJSON(this.data, DidDocument); - } - - public static fromEvent(e: EventInfoPublicDid) { - return new EventInfoPublicDid(e.data, e.id, e.type, e.timestamp); - } -} - export class EventDidcommConnectionsGetAll extends BaseEvent< Array<ConnectionRecord> > { @@ -55,9 +39,7 @@ export class EventDidcommConnectionsCreateWithSelf extends BaseEvent<ConnectionR public static token = 'didcomm.connections.createWithSelf'; public get instance() { - return JsonTransformer.fromJSON(this.data, ConnectionRecord, { - validate: true, - }); + return JsonTransformer.fromJSON(this.data, ConnectionRecord); } public static fromEvent(e: EventDidcommConnectionsCreateWithSelf) { @@ -69,3 +51,17 @@ export class EventDidcommConnectionsCreateWithSelf extends BaseEvent<ConnectionR ); } } + +export class EventDidcommConnectionsBlock extends BaseEvent<ConnectionRecord | null> { + public static token = 'didcomm.connections.block'; + + public get instance() { + return this.data + ? JsonTransformer.fromJSON(this.data, ConnectionRecord) + : null; + } + + public static fromEvent(e: EventDidcommConnectionsBlock) { + return new EventDidcommConnectionsBlock(e.data, e.id, e.type, e.timestamp); + } +} diff --git a/apps/shared/src/events/didEvents.ts b/apps/shared/src/events/didEvents.ts new file mode 100644 index 0000000..c6a2d68 --- /dev/null +++ b/apps/shared/src/events/didEvents.ts @@ -0,0 +1,32 @@ +import { DidDocument, JsonTransformer } from '@aries-framework/core'; + +import { BaseEvent } from './baseEvents.js'; + +/** + * + * @todo: this should be removed as it is a weird event that should not be needed + * + */ +export class EventInfoPublicDid extends BaseEvent<DidDocument> { + public static token = 'dids.publicDid'; + + public get instance() { + return JsonTransformer.fromJSON(this.data, DidDocument); + } + + public static fromEvent(e: EventInfoPublicDid) { + return new EventInfoPublicDid(e.data, e.id, e.type, e.timestamp); + } +} + +export class EventDidsResolve extends BaseEvent<DidDocument> { + public static token = 'dids.resolve'; + + public get instance() { + return JsonTransformer.fromJSON(this.data, DidDocument); + } + + public static fromEvent(e: EventDidsResolve) { + return new EventDidsResolve(e.data, e.id, e.type, e.timestamp); + } +} diff --git a/apps/shared/src/index.ts b/apps/shared/src/index.ts index bde9dc3..8a7c542 100644 --- a/apps/shared/src/index.ts +++ b/apps/shared/src/index.ts @@ -5,3 +5,4 @@ export * from './logging/logger.js'; export * from './logging/logAxiosError.js'; export * from './events/connectionEvents.js'; +export * from './events/didEvents.js'; diff --git a/apps/ssi-abstraction/src/agent/agent.service.ts b/apps/ssi-abstraction/src/agent/agent.service.ts index 0d68b5e..ef1ee73 100644 --- a/apps/ssi-abstraction/src/agent/agent.service.ts +++ b/apps/ssi-abstraction/src/agent/agent.service.ts @@ -12,6 +12,8 @@ import { CredentialsModule, DidsModule, HttpOutboundTransport, + JwkDidRegistrar, + JwkDidResolver, KeyDidRegistrar, KeyDidResolver, KeyType, @@ -19,6 +21,7 @@ import { PeerDidRegistrar, PeerDidResolver, TypedArrayEncoder, + WebDidResolver, } from '@aries-framework/core'; import { IndyVdrAnonCredsRegistry, @@ -107,8 +110,10 @@ export class AgentService implements OnApplicationShutdown { new IndyVdrSovDidResolver(), new PeerDidResolver(), new KeyDidResolver(), + new JwkDidResolver(), + new WebDidResolver(), ], - registrars: [new PeerDidRegistrar(), new KeyDidRegistrar()], + registrars: [new PeerDidRegistrar(), new KeyDidRegistrar(), new JwkDidRegistrar()], }), askar: new AskarModule({ ariesAskar }), @@ -203,6 +208,11 @@ export class AgentService implements OnApplicationShutdown { public async onApplicationShutdown() { if (!this.agent.isInitialized) return; - await this.agent.shutdown(); + // If we cannot shutdown the wallet on application shutdown, no error will occur + // This is done because the Askar shutdown procedure is a bit buggy + try { + await this.agent.shutdown(); + // eslint-disable-next-line no-empty + } catch {} } } diff --git a/apps/ssi-abstraction/src/agent/connections/connections.controller.ts b/apps/ssi-abstraction/src/agent/connections/connections.controller.ts index 322f337..0eb6bc7 100644 --- a/apps/ssi-abstraction/src/agent/connections/connections.controller.ts +++ b/apps/ssi-abstraction/src/agent/connections/connections.controller.ts @@ -4,6 +4,7 @@ import { EventDidcommConnectionsGetById, EventDidcommConnectionsGetAll, EventDidcommConnectionsCreateWithSelf, + EventDidcommConnectionsBlock, } from '@ocm/shared'; import { ConnectionsService } from './connections.service.js'; @@ -36,4 +37,15 @@ export class ConnectionsController { await this.connectionsService.createConnectionWithSelf(), ); } + + @MessagePattern(EventDidcommConnectionsBlock.token) + public async blockConnection({ + idOrDid, + }: { + idOrDid: string; + }): Promise<EventDidcommConnectionsBlock> { + return new EventDidcommConnectionsBlock( + await this.connectionsService.blockByIdOrDid(idOrDid), + ); + } } diff --git a/apps/ssi-abstraction/src/agent/connections/connections.service.ts b/apps/ssi-abstraction/src/agent/connections/connections.service.ts index 4c6901c..ba72b2c 100644 --- a/apps/ssi-abstraction/src/agent/connections/connections.service.ts +++ b/apps/ssi-abstraction/src/agent/connections/connections.service.ts @@ -9,6 +9,7 @@ import { ConnectionRepository, DidExchangeState, } from '@aries-framework/core'; +import { isDid } from '@aries-framework/core/build/utils/did.js'; import { Injectable } from '@nestjs/common'; import { MetadataTokens } from '../../common/constants.js'; @@ -30,6 +31,37 @@ export class ConnectionsService { return await this.agent.connections.findById(id); } + public async blockByIdOrDid( + idOrDid: string, + ): Promise<ConnectionRecord | null> { + if (isDid(idOrDid)) { + const records = await this.agent.connections.findAllByQuery({ + theirDid: idOrDid, + }); + + if (records.length === 0) { + return null; + } + + if (records.length > 1) { + throw new Error( + 'Found multiple records with the same DID. This should not be possible', + ); + } + + await this.agent.connections.deleteById(records[0].id); + + return records[0]; + } + + const record = await this.agent.connections.findById(idOrDid); + if (!record) return null; + + await this.agent.connections.deleteById(record.id); + + return record; + } + public async createConnectionWithSelf(): Promise<ConnectionRecord> { const outOfBandRecord = await this.agent.oob.createInvitation(); const invitation = outOfBandRecord.outOfBandInvitation; diff --git a/apps/ssi-abstraction/src/agent/dids/__tests__/dids.controller.spec.ts b/apps/ssi-abstraction/src/agent/dids/__tests__/dids.controller.spec.ts new file mode 100644 index 0000000..c6507d0 --- /dev/null +++ b/apps/ssi-abstraction/src/agent/dids/__tests__/dids.controller.spec.ts @@ -0,0 +1,36 @@ +import { DidDocument } from '@aries-framework/core'; +import { Test } from '@nestjs/testing'; + +import { mockConfigModule } from '../../../config/__tests__/mockConfig.js'; +import { AgentModule } from '../../agent.module.js'; +import { DidsController } from '../dids.controller.js'; +import { DidsService } from '../dids.service.js'; + +describe('DidsController', () => { + let didsController: DidsController; + let didsService: DidsService; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [mockConfigModule(), AgentModule], + controllers: [DidsController], + providers: [DidsService], + }).compile(); + + didsService = moduleRef.get(DidsService); + didsController = moduleRef.get(DidsController); + }); + + describe('resolve', () => { + it('should resolve a basic did', async () => { + const result = new DidDocument({ id: 'did:key:foo' }); + jest.spyOn(didsService, 'resolve').mockResolvedValue(result); + + const event = await didsController.resolve({ + did: 'did:key:foo', + }); + + expect(event.data).toStrictEqual(result); + }); + }); +}); diff --git a/apps/ssi-abstraction/src/agent/dids/dids.controller.ts b/apps/ssi-abstraction/src/agent/dids/dids.controller.ts new file mode 100644 index 0000000..70074c7 --- /dev/null +++ b/apps/ssi-abstraction/src/agent/dids/dids.controller.ts @@ -0,0 +1,15 @@ +import { Controller } from '@nestjs/common'; +import { MessagePattern } from '@nestjs/microservices'; +import { EventDidsResolve } from '@ocm/shared'; + +import { DidsService } from './dids.service.js'; + +@Controller('dids') +export class DidsController { + public constructor(private didsService: DidsService) {} + + @MessagePattern('dids.resolve') + public async resolve({ did }: { did: string }) { + return new EventDidsResolve(await this.didsService.resolve(did)); + } +} diff --git a/apps/ssi-abstraction/src/agent/dids/dids.module.ts b/apps/ssi-abstraction/src/agent/dids/dids.module.ts new file mode 100644 index 0000000..d4428a9 --- /dev/null +++ b/apps/ssi-abstraction/src/agent/dids/dids.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; + +import { AgentModule } from '../agent.module.js'; + +import { DidsController } from './dids.controller.js'; +import { DidsService } from './dids.service.js'; + +@Module({ + imports: [AgentModule], + providers: [DidsService], + controllers: [DidsController], +}) +export class DidsModule {} diff --git a/apps/ssi-abstraction/src/agent/dids/dids.service.ts b/apps/ssi-abstraction/src/agent/dids/dids.service.ts new file mode 100644 index 0000000..d1c4e86 --- /dev/null +++ b/apps/ssi-abstraction/src/agent/dids/dids.service.ts @@ -0,0 +1,31 @@ +import type { AppAgent } from '../agent.service.js'; + +import { Injectable } from '@nestjs/common'; + +import { AgentService } from '../agent.service.js'; + +@Injectable() +export class DidsService { + public agent: AppAgent; + + public constructor(agentService: AgentService) { + this.agent = agentService.agent; + } + + public async resolve(did: string) { + const { + didDocument, + didResolutionMetadata: { message, error }, + } = await this.agent.dids.resolve(did); + + if (!didDocument) { + throw new Error( + `Could not resolve did: '${did}'. Error: ${error ?? 'None'} Message: ${ + message ?? 'None' + }`, + ); + } + + return didDocument; + } +} diff --git a/apps/ssi-abstraction/test/connections.e2e-spec.ts b/apps/ssi-abstraction/test/connections.e2e-spec.ts index 8fa8364..3f52769 100644 --- a/apps/ssi-abstraction/test/connections.e2e-spec.ts +++ b/apps/ssi-abstraction/test/connections.e2e-spec.ts @@ -8,6 +8,7 @@ import { EventDidcommConnectionsGetById, EventDidcommConnectionsGetAll, EventDidcommConnectionsCreateWithSelf, + EventDidcommConnectionsBlock, } from '@ocm/shared'; import { firstValueFrom } from 'rxjs'; @@ -83,4 +84,16 @@ describe('Connections', () => { ); expect(metadata).toMatchObject({ trusted: true }); }); + + it(EventDidcommConnectionsBlock.token, async () => { + const response$: Observable<EventDidcommConnectionsBlock> = client.send( + EventDidcommConnectionsBlock.token, + { idOrDid: 'some-id' }, + ); + + const response = await firstValueFrom(response$); + const eventInstance = EventDidcommConnectionsBlock.fromEvent(response); + + expect(eventInstance.instance).toBeNull(); + }); }); diff --git a/apps/ssi-abstraction/test/dids.e2e-spec.ts b/apps/ssi-abstraction/test/dids.e2e-spec.ts new file mode 100644 index 0000000..e276f36 --- /dev/null +++ b/apps/ssi-abstraction/test/dids.e2e-spec.ts @@ -0,0 +1,97 @@ +import type { INestApplication } from '@nestjs/common'; +import type { ClientProxy } from '@nestjs/microservices'; +import type { Observable } from 'rxjs'; + +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { Test } from '@nestjs/testing'; +import { + EventDidsResolve, +} from '@ocm/shared'; +import { firstValueFrom } from 'rxjs'; + +import { AgentModule } from '../src/agent/agent.module.js'; +import { DidsModule } from '../src/agent/dids/dids.module.js'; +import { mockConfigModule } from '../src/config/__tests__/mockConfig.js'; + +describe('Dids', () => { + const TOKEN = 'DIDS_CLIENT_SERVICE'; + let app: INestApplication; + let client: ClientProxy; + + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + mockConfigModule(3005), + AgentModule, + DidsModule, + ClientsModule.register([{ name: TOKEN, transport: Transport.NATS }]), + ], + }).compile(); + + app = moduleRef.createNestApplication(); + + app.connectMicroservice({ transport: Transport.NATS }); + + await app.startAllMicroservices(); + await app.init(); + + client = app.get(TOKEN); + await client.connect(); + }); + + afterAll(async () => { + await app.close(); + client.close(); + }); + + it(EventDidsResolve.token, async () => { + const response$: Observable<EventDidsResolve> = client.send( + EventDidsResolve.token, + { + did: 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH', + }, + ); + + const response = await firstValueFrom(response$); + const eventInstance = EventDidsResolve.fromEvent(response); + + expect(eventInstance.instance.toJSON()).toStrictEqual({ + '@context': [ + 'https://w3id.org/did/v1', + 'https://w3id.org/security/suites/ed25519-2018/v1', + 'https://w3id.org/security/suites/x25519-2019/v1', + ], + id: 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH', + verificationMethod: [ + { + id: 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH#z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH', + type: 'Ed25519VerificationKey2018', + controller: + 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH', + publicKeyBase58: 'B12NYF8RrR3h41TDCTJojY59usg3mbtbjnFs7Eud1Y6u', + }, + ], + authentication: [ + 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH#z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH', + ], + assertionMethod: [ + 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH#z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH', + ], + keyAgreement: [ + { + id: 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH#z6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc', + type: 'X25519KeyAgreementKey2019', + controller: + 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH', + publicKeyBase58: 'JhNWeSVLMYccCk7iopQW4guaSJTojqpMEELgSLhKwRr', + }, + ], + capabilityInvocation: [ + 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH#z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH', + ], + capabilityDelegation: [ + 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH#z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH', + ], + }); + }); +}); -- GitLab