diff --git a/packages/core/src/agent/MessageSender.ts b/packages/core/src/agent/MessageSender.ts index 83e5a717b7..e8a284e450 100644 --- a/packages/core/src/agent/MessageSender.ts +++ b/packages/core/src/agent/MessageSender.ts @@ -1,4 +1,5 @@ import type { ConnectionRecord } from '../modules/connections' +import type { ResolvedDidCommService } from '../modules/didcomm' import type { DidDocument, Key } from '../modules/dids' import type { OutOfBandRecord } from '../modules/oob/repository' import type { OutboundTransport } from '../transport/OutboundTransport' @@ -11,11 +12,11 @@ import { DID_COMM_TRANSPORT_QUEUE, InjectionSymbols } from '../constants' import { ReturnRouteTypes } from '../decorators/transport/TransportDecorator' import { AriesFrameworkError } from '../error' import { Logger } from '../logger' -import { keyReferenceToKey } from '../modules/dids' +import { DidCommDocumentService } from '../modules/didcomm' import { getKeyDidMappingByVerificationMethod } from '../modules/dids/domain/key-type' -import { DidCommV1Service, IndyAgentService } from '../modules/dids/domain/service' -import { didKeyToInstanceOfKey, verkeyToInstanceOfKey } from '../modules/dids/helpers' +import { didKeyToInstanceOfKey } from '../modules/dids/helpers' import { DidResolverService } from '../modules/dids/services/DidResolverService' +import { OutOfBandRepository } from '../modules/oob/repository' import { inject, injectable } from '../plugins' import { MessageRepository } from '../storage/MessageRepository' import { MessageValidator } from '../utils/MessageValidator' @@ -24,13 +25,6 @@ import { getProtocolScheme } from '../utils/uri' import { EnvelopeService } from './EnvelopeService' import { TransportService } from './TransportService' -export interface ResolvedDidCommService { - id: string - serviceEndpoint: string - recipientKeys: Key[] - routingKeys: Key[] -} - export interface TransportPriorityOptions { schemes: string[] restrictive?: boolean @@ -43,6 +37,8 @@ export class MessageSender { private messageRepository: MessageRepository private logger: Logger private didResolverService: DidResolverService + private didCommDocumentService: DidCommDocumentService + private outOfBandRepository: OutOfBandRepository public readonly outboundTransports: OutboundTransport[] = [] public constructor( @@ -50,13 +46,17 @@ export class MessageSender { transportService: TransportService, @inject(InjectionSymbols.MessageRepository) messageRepository: MessageRepository, @inject(InjectionSymbols.Logger) logger: Logger, - didResolverService: DidResolverService + didResolverService: DidResolverService, + didCommDocumentService: DidCommDocumentService, + outOfBandRepository: OutOfBandRepository ) { this.envelopeService = envelopeService this.transportService = transportService this.messageRepository = messageRepository this.logger = logger this.didResolverService = didResolverService + this.didCommDocumentService = didCommDocumentService + this.outOfBandRepository = outOfBandRepository this.outboundTransports = [] } @@ -342,49 +342,6 @@ export class MessageSender { throw new AriesFrameworkError(`Unable to send message to service: ${service.serviceEndpoint}`) } - private async retrieveServicesFromDid(did: string) { - this.logger.debug(`Resolving services for did ${did}.`) - const didDocument = await this.didResolverService.resolveDidDocument(did) - - const didCommServices: ResolvedDidCommService[] = [] - - // FIXME: we currently retrieve did documents for all didcomm services in the did document, and we don't have caching - // yet so this will re-trigger ledger resolves for each one. Should we only resolve the first service, then the second service, etc...? - for (const didCommService of didDocument.didCommServices) { - if (didCommService instanceof IndyAgentService) { - // IndyAgentService (DidComm v0) has keys encoded as raw publicKeyBase58 (verkeys) - didCommServices.push({ - id: didCommService.id, - recipientKeys: didCommService.recipientKeys.map(verkeyToInstanceOfKey), - routingKeys: didCommService.routingKeys?.map(verkeyToInstanceOfKey) || [], - serviceEndpoint: didCommService.serviceEndpoint, - }) - } else if (didCommService instanceof DidCommV1Service) { - // Resolve dids to DIDDocs to retrieve routingKeys - const routingKeys = [] - for (const routingKey of didCommService.routingKeys ?? []) { - const routingDidDocument = await this.didResolverService.resolveDidDocument(routingKey) - routingKeys.push(keyReferenceToKey(routingDidDocument, routingKey)) - } - - // Dereference recipientKeys - const recipientKeys = didCommService.recipientKeys.map((recipientKey) => - keyReferenceToKey(didDocument, recipientKey) - ) - - // DidCommV1Service has keys encoded as key references - didCommServices.push({ - id: didCommService.id, - recipientKeys, - routingKeys, - serviceEndpoint: didCommService.serviceEndpoint, - }) - } - } - - return didCommServices - } - private async retrieveServicesByConnection( connection: ConnectionRecord, transportPriority?: TransportPriorityOptions, @@ -399,14 +356,15 @@ export class MessageSender { if (connection.theirDid) { this.logger.debug(`Resolving services for connection theirDid ${connection.theirDid}.`) - didCommServices = await this.retrieveServicesFromDid(connection.theirDid) + didCommServices = await this.didCommDocumentService.resolveServicesFromDid(connection.theirDid) } else if (outOfBand) { - this.logger.debug(`Resolving services from out-of-band record ${outOfBand?.id}.`) + this.logger.debug(`Resolving services from out-of-band record ${outOfBand.id}.`) if (connection.isRequester) { - for (const service of outOfBand.outOfBandInvitation.services) { + for (const service of outOfBand.outOfBandInvitation.getServices()) { // Resolve dids to DIDDocs to retrieve services if (typeof service === 'string') { - didCommServices = await this.retrieveServicesFromDid(service) + this.logger.debug(`Resolving services for did ${service}.`) + didCommServices.push(...(await this.didCommDocumentService.resolveServicesFromDid(service))) } else { // Out of band inline service contains keys encoded as did:key references didCommServices.push({ diff --git a/packages/core/src/agent/__tests__/MessageSender.test.ts b/packages/core/src/agent/__tests__/MessageSender.test.ts index f2c0d363e1..3a9f9df1cb 100644 --- a/packages/core/src/agent/__tests__/MessageSender.test.ts +++ b/packages/core/src/agent/__tests__/MessageSender.test.ts @@ -1,18 +1,20 @@ import type { ConnectionRecord } from '../../modules/connections' +import type { ResolvedDidCommService } from '../../modules/didcomm' import type { DidDocumentService } from '../../modules/dids' import type { MessageRepository } from '../../storage/MessageRepository' import type { OutboundTransport } from '../../transport' import type { OutboundMessage, EncryptedMessage } from '../../types' -import type { ResolvedDidCommService } from '../MessageSender' import { TestMessage } from '../../../tests/TestMessage' import { getAgentConfig, getMockConnection, mockFunction } from '../../../tests/helpers' import testLogger from '../../../tests/logger' import { KeyType } from '../../crypto' import { ReturnRouteTypes } from '../../decorators/transport/TransportDecorator' -import { Key, DidDocument, VerificationMethod } from '../../modules/dids' +import { DidCommDocumentService } from '../../modules/didcomm' +import { DidResolverService, Key, DidDocument, VerificationMethod } from '../../modules/dids' import { DidCommV1Service } from '../../modules/dids/domain/service/DidCommV1Service' -import { DidResolverService } from '../../modules/dids/services/DidResolverService' +import { verkeyToInstanceOfKey } from '../../modules/dids/helpers' +import { OutOfBandRepository } from '../../modules/oob' import { InMemoryMessageRepository } from '../../storage/InMemoryMessageRepository' import { EnvelopeService as EnvelopeServiceImpl } from '../EnvelopeService' import { MessageSender } from '../MessageSender' @@ -24,11 +26,15 @@ import { DummyTransportSession } from './stubs' jest.mock('../TransportService') jest.mock('../EnvelopeService') jest.mock('../../modules/dids/services/DidResolverService') +jest.mock('../../modules/didcomm/services/DidCommDocumentService') +jest.mock('../../modules/oob/repository/OutOfBandRepository') const logger = testLogger const TransportServiceMock = TransportService as jest.MockedClass const DidResolverServiceMock = DidResolverService as jest.Mock +const DidCommDocumentServiceMock = DidCommDocumentService as jest.Mock +const OutOfBandRepositoryMock = OutOfBandRepository as jest.Mock class DummyHttpOutboundTransport implements OutboundTransport { public start(): Promise { @@ -76,7 +82,10 @@ describe('MessageSender', () => { const envelopeServicePackMessageMock = mockFunction(enveloperService.packMessage) const didResolverService = new DidResolverServiceMock() + const didCommDocumentService = new DidCommDocumentServiceMock() + const outOfBandRepository = new OutOfBandRepositoryMock() const didResolverServiceResolveMock = mockFunction(didResolverService.resolveDidDocument) + const didResolverServiceResolveDidServicesMock = mockFunction(didCommDocumentService.resolveServicesFromDid) const inboundMessage = new TestMessage() inboundMessage.setReturnRouting(ReturnRouteTypes.all) @@ -130,7 +139,9 @@ describe('MessageSender', () => { transportService, messageRepository, logger, - didResolverService + didResolverService, + didCommDocumentService, + outOfBandRepository ) connection = getMockConnection({ id: 'test-123', @@ -147,6 +158,10 @@ describe('MessageSender', () => { service: [firstDidCommService, secondDidCommService], }) didResolverServiceResolveMock.mockResolvedValue(didDocumentInstance) + didResolverServiceResolveDidServicesMock.mockResolvedValue([ + getMockResolvedDidService(firstDidCommService), + getMockResolvedDidService(secondDidCommService), + ]) }) afterEach(() => { @@ -161,6 +176,7 @@ describe('MessageSender', () => { messageSender.registerOutboundTransport(outboundTransport) didResolverServiceResolveMock.mockResolvedValue(getMockDidDocument({ service: [] })) + didResolverServiceResolveDidServicesMock.mockResolvedValue([]) await expect(messageSender.sendMessage(outboundMessage)).rejects.toThrow( `Message is undeliverable to connection test-123 (Test 123)` @@ -186,14 +202,14 @@ describe('MessageSender', () => { expect(sendMessageSpy).toHaveBeenCalledTimes(1) }) - test("resolves the did document using the did resolver if connection.theirDid starts with 'did:'", async () => { + test("resolves the did service using the did resolver if connection.theirDid starts with 'did:'", async () => { messageSender.registerOutboundTransport(outboundTransport) const sendMessageSpy = jest.spyOn(outboundTransport, 'sendMessage') await messageSender.sendMessage(outboundMessage) - expect(didResolverServiceResolveMock).toHaveBeenCalledWith(connection.theirDid) + expect(didResolverServiceResolveDidServicesMock).toHaveBeenCalledWith(connection.theirDid) expect(sendMessageSpy).toHaveBeenCalledWith({ connectionId: 'test-123', payload: encryptedMessage, @@ -326,7 +342,9 @@ describe('MessageSender', () => { transportService, new InMemoryMessageRepository(getAgentConfig('MessageSenderTest')), logger, - didResolverService + didResolverService, + didCommDocumentService, + outOfBandRepository ) envelopeServicePackMessageMock.mockReturnValue(Promise.resolve(encryptedMessage)) @@ -406,7 +424,9 @@ describe('MessageSender', () => { transportService, messageRepository, logger, - didResolverService + didResolverService, + didCommDocumentService, + outOfBandRepository ) connection = getMockConnection() @@ -454,3 +474,12 @@ function getMockDidDocument({ service }: { service: DidDocumentService[] }) { ], }) } + +function getMockResolvedDidService(service: DidDocumentService): ResolvedDidCommService { + return { + id: service.id, + serviceEndpoint: service.serviceEndpoint, + recipientKeys: [verkeyToInstanceOfKey('EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d')], + routingKeys: [], + } +} diff --git a/packages/core/src/agent/helpers.ts b/packages/core/src/agent/helpers.ts index b3516a1f25..029814362b 100644 --- a/packages/core/src/agent/helpers.ts +++ b/packages/core/src/agent/helpers.ts @@ -1,9 +1,9 @@ import type { ConnectionRecord } from '../modules/connections' +import type { ResolvedDidCommService } from '../modules/didcomm' import type { Key } from '../modules/dids/domain/Key' import type { OutOfBandRecord } from '../modules/oob/repository' import type { OutboundMessage, OutboundServiceMessage } from '../types' import type { AgentMessage } from './AgentMessage' -import type { ResolvedDidCommService } from './MessageSender' export function createOutboundMessage( connection: ConnectionRecord, diff --git a/packages/core/src/decorators/service/ServiceDecorator.ts b/packages/core/src/decorators/service/ServiceDecorator.ts index 72ee1226fe..0a105c4831 100644 --- a/packages/core/src/decorators/service/ServiceDecorator.ts +++ b/packages/core/src/decorators/service/ServiceDecorator.ts @@ -1,4 +1,4 @@ -import type { ResolvedDidCommService } from '../../agent/MessageSender' +import type { ResolvedDidCommService } from '../../modules/didcomm' import { IsArray, IsOptional, IsString } from 'class-validator' diff --git a/packages/core/src/modules/connections/DidExchangeProtocol.ts b/packages/core/src/modules/connections/DidExchangeProtocol.ts index e3f2ad741b..bc2a4e939e 100644 --- a/packages/core/src/modules/connections/DidExchangeProtocol.ts +++ b/packages/core/src/modules/connections/DidExchangeProtocol.ts @@ -1,8 +1,7 @@ -import type { ResolvedDidCommService } from '../../agent/MessageSender' import type { InboundMessageContext } from '../../agent/models/InboundMessageContext' import type { Logger } from '../../logger' import type { ParsedMessageType } from '../../utils/messageType' -import type { OutOfBandDidCommService } from '../oob/domain/OutOfBandDidCommService' +import type { ResolvedDidCommService } from '../didcomm' import type { OutOfBandRecord } from '../oob/repository' import type { ConnectionRecord } from './repository' import type { Routing } from './services/ConnectionService' @@ -221,10 +220,7 @@ export class DidExchangeProtocol { if (routing) { services = this.routingToServices(routing) } else if (outOfBandRecord) { - const inlineServices = outOfBandRecord.outOfBandInvitation.services.filter( - (service) => typeof service !== 'string' - ) as OutOfBandDidCommService[] - + const inlineServices = outOfBandRecord.outOfBandInvitation.getInlineServices() services = inlineServices.map((service) => ({ id: service.id, serviceEndpoint: service.serviceEndpoint, @@ -300,7 +296,9 @@ export class DidExchangeProtocol { const didDocument = await this.extractDidDocument( message, - outOfBandRecord.outOfBandInvitation.getRecipientKeys().map((key) => key.publicKeyBase58) + outOfBandRecord + .getTags() + .recipientKeyFingerprints.map((fingerprint) => Key.fromFingerprint(fingerprint).publicKeyBase58) ) const didRecord = new DidRecord({ id: message.did, diff --git a/packages/core/src/modules/connections/handlers/ConnectionResponseHandler.ts b/packages/core/src/modules/connections/handlers/ConnectionResponseHandler.ts index 48ca476cb4..13d6a67a05 100644 --- a/packages/core/src/modules/connections/handlers/ConnectionResponseHandler.ts +++ b/packages/core/src/modules/connections/handlers/ConnectionResponseHandler.ts @@ -66,7 +66,6 @@ export class ConnectionResponseHandler implements Handler { } messageContext.connection = connectionRecord - // The presence of outOfBandRecord is not mandatory when the old connection invitation is used const connection = await this.connectionService.processResponse(messageContext, outOfBandRecord) // TODO: should we only send ping message in case of autoAcceptConnection or always? diff --git a/packages/core/src/modules/connections/services/ConnectionService.ts b/packages/core/src/modules/connections/services/ConnectionService.ts index 570c314de9..1ca5adf5ce 100644 --- a/packages/core/src/modules/connections/services/ConnectionService.ts +++ b/packages/core/src/modules/connections/services/ConnectionService.ts @@ -109,6 +109,19 @@ export class ConnectionService { didDoc, }) + const { label, imageUrl } = config + const connectionRequest = new ConnectionRequestMessage({ + label: label ?? this.config.label, + did: didDoc.id, + didDoc, + imageUrl: imageUrl ?? this.config.connectionImageUrl, + }) + + connectionRequest.setThread({ + threadId: connectionRequest.id, + parentThreadId: outOfBandInvitation.id, + }) + const connectionRecord = await this.createConnection({ protocol: HandshakeProtocol.Connections, role: DidExchangeRole.Requester, @@ -121,22 +134,9 @@ export class ConnectionService { outOfBandId: outOfBandRecord.id, invitationDid, imageUrl: outOfBandInvitation.imageUrl, + threadId: connectionRequest.id, }) - const { label, imageUrl, autoAcceptConnection } = config - - const connectionRequest = new ConnectionRequestMessage({ - label: label ?? this.config.label, - did: didDoc.id, - didDoc, - imageUrl: imageUrl ?? this.config.connectionImageUrl, - }) - - if (autoAcceptConnection !== undefined || autoAcceptConnection !== null) { - connectionRecord.autoAcceptConnection = config?.autoAcceptConnection - } - - connectionRecord.threadId = connectionRequest.id await this.updateState(connectionRecord, DidExchangeState.RequestSent) return { @@ -204,11 +204,7 @@ export class ConnectionService { const didDoc = routing ? this.createDidDoc(routing) - : this.createDidDocFromOutOfBandDidCommServices( - outOfBandRecord.outOfBandInvitation.services.filter( - (s): s is OutOfBandDidCommService => typeof s !== 'string' - ) - ) + : this.createDidDocFromOutOfBandDidCommServices(outOfBandRecord.outOfBandInvitation.getInlineServices()) const { did: peerDid } = await this.createDid({ role: DidDocumentRole.Created, diff --git a/packages/core/src/modules/didcomm/index.ts b/packages/core/src/modules/didcomm/index.ts new file mode 100644 index 0000000000..ff4d44346c --- /dev/null +++ b/packages/core/src/modules/didcomm/index.ts @@ -0,0 +1,2 @@ +export * from './types' +export * from './services' diff --git a/packages/core/src/modules/didcomm/services/DidCommDocumentService.ts b/packages/core/src/modules/didcomm/services/DidCommDocumentService.ts new file mode 100644 index 0000000000..18c7c9958c --- /dev/null +++ b/packages/core/src/modules/didcomm/services/DidCommDocumentService.ts @@ -0,0 +1,71 @@ +import type { Logger } from '../../../logger' +import type { ResolvedDidCommService } from '../types' + +import { AgentConfig } from '../../../agent/AgentConfig' +import { KeyType } from '../../../crypto' +import { injectable } from '../../../plugins' +import { DidResolverService } from '../../dids' +import { DidCommV1Service, IndyAgentService, keyReferenceToKey } from '../../dids/domain' +import { verkeyToInstanceOfKey } from '../../dids/helpers' +import { findMatchingEd25519Key } from '../util/matchingEd25519Key' + +@injectable() +export class DidCommDocumentService { + private logger: Logger + private didResolverService: DidResolverService + + public constructor(agentConfig: AgentConfig, didResolverService: DidResolverService) { + this.logger = agentConfig.logger + this.didResolverService = didResolverService + } + + public async resolveServicesFromDid(did: string): Promise { + const didDocument = await this.didResolverService.resolveDidDocument(did) + + const didCommServices: ResolvedDidCommService[] = [] + + // FIXME: we currently retrieve did documents for all didcomm services in the did document, and we don't have caching + // yet so this will re-trigger ledger resolves for each one. Should we only resolve the first service, then the second service, etc...? + for (const didCommService of didDocument.didCommServices) { + if (didCommService instanceof IndyAgentService) { + // IndyAgentService (DidComm v0) has keys encoded as raw publicKeyBase58 (verkeys) + didCommServices.push({ + id: didCommService.id, + recipientKeys: didCommService.recipientKeys.map(verkeyToInstanceOfKey), + routingKeys: didCommService.routingKeys?.map(verkeyToInstanceOfKey) || [], + serviceEndpoint: didCommService.serviceEndpoint, + }) + } else if (didCommService instanceof DidCommV1Service) { + // Resolve dids to DIDDocs to retrieve routingKeys + const routingKeys = [] + for (const routingKey of didCommService.routingKeys ?? []) { + const routingDidDocument = await this.didResolverService.resolveDidDocument(routingKey) + routingKeys.push(keyReferenceToKey(routingDidDocument, routingKey)) + } + + // DidCommV1Service has keys encoded as key references + + // Dereference recipientKeys + const recipientKeys = didCommService.recipientKeys.map((recipientKeyReference) => { + const key = keyReferenceToKey(didDocument, recipientKeyReference) + + // try to find a matching Ed25519 key (https://sovrin-foundation.github.io/sovrin/spec/did-method-spec-template.html#did-document-notes) + if (key.keyType === KeyType.X25519) { + const matchingEd25519Key = findMatchingEd25519Key(key, didDocument) + if (matchingEd25519Key) return matchingEd25519Key + } + return key + }) + + didCommServices.push({ + id: didCommService.id, + recipientKeys, + routingKeys, + serviceEndpoint: didCommService.serviceEndpoint, + }) + } + } + + return didCommServices + } +} diff --git a/packages/core/src/modules/didcomm/services/__tests__/DidCommDocumentService.test.ts b/packages/core/src/modules/didcomm/services/__tests__/DidCommDocumentService.test.ts new file mode 100644 index 0000000000..6dacdc4e5e --- /dev/null +++ b/packages/core/src/modules/didcomm/services/__tests__/DidCommDocumentService.test.ts @@ -0,0 +1,113 @@ +import type { VerificationMethod } from '../../../dids' + +import { getAgentConfig, mockFunction } from '../../../../../tests/helpers' +import { KeyType } from '../../../../crypto' +import { DidCommV1Service, DidDocument, IndyAgentService, Key } from '../../../dids' +import { verkeyToInstanceOfKey } from '../../../dids/helpers' +import { DidResolverService } from '../../../dids/services/DidResolverService' +import { DidCommDocumentService } from '../DidCommDocumentService' + +jest.mock('../../../dids/services/DidResolverService') +const DidResolverServiceMock = DidResolverService as jest.Mock + +describe('DidCommDocumentService', () => { + const agentConfig = getAgentConfig('DidCommDocumentService') + let didCommDocumentService: DidCommDocumentService + let didResolverService: DidResolverService + + beforeEach(async () => { + didResolverService = new DidResolverServiceMock() + didCommDocumentService = new DidCommDocumentService(agentConfig, didResolverService) + }) + + describe('resolveServicesFromDid', () => { + test('throw error when resolveDidDocument fails', async () => { + const error = new Error('test') + mockFunction(didResolverService.resolveDidDocument).mockRejectedValue(error) + + await expect(didCommDocumentService.resolveServicesFromDid('did')).rejects.toThrowError(error) + }) + + test('resolves IndyAgentService', async () => { + mockFunction(didResolverService.resolveDidDocument).mockResolvedValue( + new DidDocument({ + context: ['https://w3id.org/did/v1'], + id: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h', + service: [ + new IndyAgentService({ + id: 'test-id', + serviceEndpoint: 'https://test.com', + recipientKeys: ['Q4zqM7aXqm7gDQkUVLng9h'], + routingKeys: ['DADEajsDSaksLng9h'], + priority: 5, + }), + ], + }) + ) + + const resolved = await didCommDocumentService.resolveServicesFromDid('did:sov:Q4zqM7aXqm7gDQkUVLng9h') + expect(didResolverService.resolveDidDocument).toHaveBeenCalledWith('did:sov:Q4zqM7aXqm7gDQkUVLng9h') + + expect(resolved).toHaveLength(1) + expect(resolved[0]).toMatchObject({ + id: 'test-id', + serviceEndpoint: 'https://test.com', + recipientKeys: [verkeyToInstanceOfKey('Q4zqM7aXqm7gDQkUVLng9h')], + routingKeys: [verkeyToInstanceOfKey('DADEajsDSaksLng9h')], + }) + }) + + test('resolves DidCommV1Service', async () => { + const publicKeyBase58Ed25519 = 'GyYtYWU1vjwd5PFJM4VSX5aUiSV3TyZMuLBJBTQvfdF8' + const publicKeyBase58X25519 = 'S3AQEEKkGYrrszT9D55ozVVX2XixYp8uynqVm4okbud' + + const Ed25519VerificationMethod: VerificationMethod = { + type: 'Ed25519VerificationKey2018', + controller: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h', + id: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h#key-1', + publicKeyBase58: publicKeyBase58Ed25519, + } + const X25519VerificationMethod: VerificationMethod = { + type: 'X25519KeyAgreementKey2019', + controller: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h', + id: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h#key-agreement-1', + publicKeyBase58: publicKeyBase58X25519, + } + + mockFunction(didResolverService.resolveDidDocument).mockResolvedValue( + new DidDocument({ + context: [ + 'https://w3id.org/did/v1', + 'https://w3id.org/security/suites/ed25519-2018/v1', + 'https://w3id.org/security/suites/x25519-2019/v1', + ], + id: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h', + verificationMethod: [Ed25519VerificationMethod, X25519VerificationMethod], + authentication: [Ed25519VerificationMethod.id], + keyAgreement: [X25519VerificationMethod.id], + service: [ + new DidCommV1Service({ + id: 'test-id', + serviceEndpoint: 'https://test.com', + recipientKeys: [X25519VerificationMethod.id], + routingKeys: [Ed25519VerificationMethod.id], + priority: 5, + }), + ], + }) + ) + + const resolved = await didCommDocumentService.resolveServicesFromDid('did:sov:Q4zqM7aXqm7gDQkUVLng9h') + expect(didResolverService.resolveDidDocument).toHaveBeenCalledWith('did:sov:Q4zqM7aXqm7gDQkUVLng9h') + + const ed25519Key = Key.fromPublicKeyBase58(publicKeyBase58Ed25519, KeyType.Ed25519) + expect(resolved).toHaveLength(1) + expect(resolved[0]).toMatchObject({ + id: 'test-id', + serviceEndpoint: 'https://test.com', + recipientKeys: [ed25519Key], + routingKeys: [ed25519Key], + }) + }) + }) +}) diff --git a/packages/core/src/modules/didcomm/services/index.ts b/packages/core/src/modules/didcomm/services/index.ts new file mode 100644 index 0000000000..ae2cb50e2f --- /dev/null +++ b/packages/core/src/modules/didcomm/services/index.ts @@ -0,0 +1 @@ +export * from './DidCommDocumentService' diff --git a/packages/core/src/modules/didcomm/types.ts b/packages/core/src/modules/didcomm/types.ts new file mode 100644 index 0000000000..3282c62507 --- /dev/null +++ b/packages/core/src/modules/didcomm/types.ts @@ -0,0 +1,8 @@ +import type { Key } from '../dids/domain' + +export interface ResolvedDidCommService { + id: string + serviceEndpoint: string + recipientKeys: Key[] + routingKeys: Key[] +} diff --git a/packages/core/src/modules/didcomm/util/__tests__/matchingEd25519Key.test.ts b/packages/core/src/modules/didcomm/util/__tests__/matchingEd25519Key.test.ts new file mode 100644 index 0000000000..dac0af79c8 --- /dev/null +++ b/packages/core/src/modules/didcomm/util/__tests__/matchingEd25519Key.test.ts @@ -0,0 +1,84 @@ +import type { VerificationMethod } from '../../../dids' + +import { KeyType } from '../../../../crypto' +import { DidDocument, Key } from '../../../dids' +import { findMatchingEd25519Key } from '../matchingEd25519Key' + +describe('findMatchingEd25519Key', () => { + const publicKeyBase58Ed25519 = 'GyYtYWU1vjwd5PFJM4VSX5aUiSV3TyZMuLBJBTQvfdF8' + const Ed25519VerificationMethod: VerificationMethod = { + type: 'Ed25519VerificationKey2018', + controller: 'did:sov:WJz9mHyW9BZksioQnRsrAo', + id: 'did:sov:WJz9mHyW9BZksioQnRsrAo#key-1', + publicKeyBase58: publicKeyBase58Ed25519, + } + + const publicKeyBase58X25519 = 'S3AQEEKkGYrrszT9D55ozVVX2XixYp8uynqVm4okbud' + const X25519VerificationMethod: VerificationMethod = { + type: 'X25519KeyAgreementKey2019', + controller: 'did:sov:WJz9mHyW9BZksioQnRsrAo', + id: 'did:sov:WJz9mHyW9BZksioQnRsrAo#key-agreement-1', + publicKeyBase58: publicKeyBase58X25519, + } + + describe('referenced verification method', () => { + const didDocument = new DidDocument({ + context: [ + 'https://w3id.org/did/v1', + 'https://w3id.org/security/suites/ed25519-2018/v1', + 'https://w3id.org/security/suites/x25519-2019/v1', + ], + id: 'did:sov:WJz9mHyW9BZksioQnRsrAo', + verificationMethod: [Ed25519VerificationMethod, X25519VerificationMethod], + authentication: [Ed25519VerificationMethod.id], + assertionMethod: [Ed25519VerificationMethod.id], + keyAgreement: [X25519VerificationMethod.id], + }) + + test('returns matching Ed25519 key if corresponding X25519 key supplied', () => { + const x25519Key = Key.fromPublicKeyBase58(publicKeyBase58X25519, KeyType.X25519) + const ed25519Key = findMatchingEd25519Key(x25519Key, didDocument) + expect(ed25519Key?.publicKeyBase58).toBe(Ed25519VerificationMethod.publicKeyBase58) + }) + + test('returns undefined if non-corresponding X25519 key supplied', () => { + const differentX25519Key = Key.fromPublicKeyBase58('Fbv17ZbnUSbafsiUBJbdGeC62M8v8GEscVMMcE59mRPt', KeyType.X25519) + expect(findMatchingEd25519Key(differentX25519Key, didDocument)).toBeUndefined() + }) + + test('returns undefined if ed25519 key supplied', () => { + const ed25519Key = Key.fromPublicKeyBase58(publicKeyBase58Ed25519, KeyType.Ed25519) + expect(findMatchingEd25519Key(ed25519Key, didDocument)).toBeUndefined() + }) + }) + + describe('non-referenced authentication', () => { + const didDocument = new DidDocument({ + context: [ + 'https://w3id.org/did/v1', + 'https://w3id.org/security/suites/ed25519-2018/v1', + 'https://w3id.org/security/suites/x25519-2019/v1', + ], + id: 'did:sov:WJz9mHyW9BZksioQnRsrAo', + authentication: [Ed25519VerificationMethod], + assertionMethod: [Ed25519VerificationMethod], + keyAgreement: [X25519VerificationMethod], + }) + + test('returns matching Ed25519 key if corresponding X25519 key supplied', () => { + const x25519Key = Key.fromPublicKeyBase58(publicKeyBase58X25519, KeyType.X25519) + const ed25519Key = findMatchingEd25519Key(x25519Key, didDocument) + expect(ed25519Key?.publicKeyBase58).toBe(Ed25519VerificationMethod.publicKeyBase58) + }) + + test('returns undefined if non-corresponding X25519 key supplied', () => { + const differentX25519Key = Key.fromPublicKeyBase58('Fbv17ZbnUSbafsiUBJbdGeC62M8v8GEscVMMcE59mRPt', KeyType.X25519) + expect(findMatchingEd25519Key(differentX25519Key, didDocument)).toBeUndefined() + }) + + test('returns undefined if ed25519 key supplied', () => { + const ed25519Key = Key.fromPublicKeyBase58(publicKeyBase58Ed25519, KeyType.Ed25519) + expect(findMatchingEd25519Key(ed25519Key, didDocument)).toBeUndefined() + }) + }) +}) diff --git a/packages/core/src/modules/didcomm/util/matchingEd25519Key.ts b/packages/core/src/modules/didcomm/util/matchingEd25519Key.ts new file mode 100644 index 0000000000..e163f0d38f --- /dev/null +++ b/packages/core/src/modules/didcomm/util/matchingEd25519Key.ts @@ -0,0 +1,32 @@ +import type { DidDocument, VerificationMethod } from '../../dids' + +import { KeyType } from '../../../crypto' +import { Key, keyReferenceToKey } from '../../dids' +import { convertPublicKeyToX25519 } from '../../dids/domain/key-type/ed25519' + +/** + * Tries to find a matching Ed25519 key to the supplied X25519 key + * @param x25519Key X25519 key + * @param didDocument Did document containing all the keys + * @returns a matching Ed25519 key or `undefined` (if no matching key found) + */ +export function findMatchingEd25519Key(x25519Key: Key, didDocument: DidDocument): Key | undefined { + if (x25519Key.keyType !== KeyType.X25519) return undefined + + const verificationMethods = didDocument.verificationMethod ?? [] + const keyAgreements = didDocument.keyAgreement ?? [] + const authentications = didDocument.authentication ?? [] + const allKeyReferences: VerificationMethod[] = [ + ...verificationMethods, + ...authentications.filter((keyAgreement): keyAgreement is VerificationMethod => typeof keyAgreement !== 'string'), + ...keyAgreements.filter((keyAgreement): keyAgreement is VerificationMethod => typeof keyAgreement !== 'string'), + ] + + return allKeyReferences + .map((keyReference) => keyReferenceToKey(didDocument, keyReference.id)) + .filter((key) => key?.keyType === KeyType.Ed25519) + .find((keyEd25519) => { + const keyX25519 = Key.fromPublicKey(convertPublicKeyToX25519(keyEd25519.publicKey), KeyType.X25519) + return keyX25519.publicKeyBase58 === x25519Key.publicKeyBase58 + }) +} diff --git a/packages/core/src/modules/dids/domain/createPeerDidFromServices.ts b/packages/core/src/modules/dids/domain/createPeerDidFromServices.ts index 3fe2375a35..b866b0fa33 100644 --- a/packages/core/src/modules/dids/domain/createPeerDidFromServices.ts +++ b/packages/core/src/modules/dids/domain/createPeerDidFromServices.ts @@ -1,4 +1,4 @@ -import type { ResolvedDidCommService } from '../../../agent/MessageSender' +import type { ResolvedDidCommService } from '../../didcomm' import { convertPublicKeyToX25519 } from '@stablelib/ed25519' diff --git a/packages/core/src/modules/oob/OutOfBandModule.ts b/packages/core/src/modules/oob/OutOfBandModule.ts index 333af57332..a513fea180 100644 --- a/packages/core/src/modules/oob/OutOfBandModule.ts +++ b/packages/core/src/modules/oob/OutOfBandModule.ts @@ -24,9 +24,9 @@ import { DidCommMessageRepository, DidCommMessageRole } from '../../storage' import { JsonEncoder, JsonTransformer } from '../../utils' import { parseMessageType, supportsIncomingMessageType } from '../../utils/messageType' import { parseInvitationUrl, parseInvitationShortUrl } from '../../utils/parseInvitation' +import { DidCommDocumentService } from '../didcomm' import { DidKey } from '../dids' import { didKeyToVerkey } from '../dids/helpers' -import { outOfBandServiceToNumAlgo2Did } from '../dids/methods/peer/peerDidNumAlgo2' import { RoutingService } from '../routing/services/RoutingService' import { OutOfBandService } from './OutOfBandService' @@ -89,6 +89,7 @@ export class OutOfBandModule { private eventEmitter: EventEmitter private agentConfig: AgentConfig private logger: Logger + private didCommDocumentService: DidCommDocumentService public constructor( dispatcher: Dispatcher, @@ -98,7 +99,8 @@ export class OutOfBandModule { connectionsModule: ConnectionsModule, didCommMessageRepository: DidCommMessageRepository, messageSender: MessageSender, - eventEmitter: EventEmitter + eventEmitter: EventEmitter, + didCommDocumentService: DidCommDocumentService ) { this.dispatcher = dispatcher this.agentConfig = agentConfig @@ -109,6 +111,7 @@ export class OutOfBandModule { this.didCommMessageRepository = didCommMessageRepository this.messageSender = messageSender this.eventEmitter = eventEmitter + this.didCommDocumentService = didCommDocumentService this.registerHandlers(dispatcher) } @@ -207,6 +210,11 @@ export class OutOfBandModule { outOfBandInvitation: outOfBandInvitation, reusable: multiUseInvitation, autoAcceptConnection, + tags: { + recipientKeyFingerprints: services + .reduce((aggr, { recipientKeys }) => [...aggr, ...recipientKeys], []) + .map((didKey) => DidKey.fromDid(didKey).key.fingerprint), + }, }) await this.outOfBandService.save(outOfBandRecord) @@ -350,12 +358,30 @@ export class OutOfBandModule { ) } + const recipientKeyFingerprints: string[] = [] + for (const service of outOfBandInvitation.getServices()) { + // Resolve dids to DIDDocs to retrieve services + if (typeof service === 'string') { + this.logger.debug(`Resolving services for did ${service}.`) + const resolvedDidCommServices = await this.didCommDocumentService.resolveServicesFromDid(service) + recipientKeyFingerprints.push( + ...resolvedDidCommServices + .reduce((aggr, { recipientKeys }) => [...aggr, ...recipientKeys], []) + .map((key) => key.fingerprint) + ) + } else { + recipientKeyFingerprints.push(...service.recipientKeys.map((didKey) => DidKey.fromDid(didKey).key.fingerprint)) + } + } + outOfBandRecord = new OutOfBandRecord({ role: OutOfBandRole.Receiver, state: OutOfBandState.Initial, outOfBandInvitation: outOfBandInvitation, autoAcceptConnection, + tags: { recipientKeyFingerprints }, }) + await this.outOfBandService.save(outOfBandRecord) this.outOfBandService.emitStateChangedEvent(outOfBandRecord, null) @@ -403,10 +429,11 @@ export class OutOfBandModule { const { outOfBandInvitation } = outOfBandRecord const { label, alias, imageUrl, autoAcceptConnection, reuseConnection, routing } = config - const { handshakeProtocols, services } = outOfBandInvitation + const { handshakeProtocols } = outOfBandInvitation + const services = outOfBandInvitation.getServices() const messages = outOfBandInvitation.getRequests() - const existingConnection = await this.findExistingConnection(services) + const existingConnection = await this.findExistingConnection(outOfBandInvitation) await this.outOfBandService.updateState(outOfBandRecord, OutOfBandState.PrepareResponse) @@ -578,26 +605,20 @@ export class OutOfBandModule { return handshakeProtocol } - private async findExistingConnection(services: Array) { - this.logger.debug('Searching for an existing connection for out-of-band invitation services.', { services }) + private async findExistingConnection(outOfBandInvitation: OutOfBandInvitation) { + this.logger.debug('Searching for an existing connection for out-of-band invitation.', { outOfBandInvitation }) - // TODO: for each did we should look for a connection with the invitation did OR a connection with theirDid that matches the service did - for (const didOrService of services) { - // We need to check if the service is an instance of string because of limitations from class-validator - if (typeof didOrService === 'string' || didOrService instanceof String) { - // TODO await this.connectionsModule.findByTheirDid() - throw new AriesFrameworkError('Dids are not currently supported in out-of-band invitation services attribute.') - } - - const did = outOfBandServiceToNumAlgo2Did(didOrService) - const connections = await this.connectionsModule.findByInvitationDid(did) - this.logger.debug(`Retrieved ${connections.length} connections for invitation did ${did}`) + for (const invitationDid of outOfBandInvitation.invitationDids) { + const connections = await this.connectionsModule.findByInvitationDid(invitationDid) + this.logger.debug(`Retrieved ${connections.length} connections for invitation did ${invitationDid}`) if (connections.length === 1) { const [firstConnection] = connections return firstConnection } else if (connections.length > 1) { - this.logger.warn(`There is more than one connection created from invitationDid ${did}. Taking the first one.`) + this.logger.warn( + `There is more than one connection created from invitationDid ${invitationDid}. Taking the first one.` + ) const [firstConnection] = connections return firstConnection } @@ -644,19 +665,36 @@ export class OutOfBandModule { this.logger.debug(`Message with type ${plaintextMessage['@type']} can be processed.`) + let serviceEndpoint: string | undefined + let recipientKeys: string[] | undefined + let routingKeys: string[] = [] + // The framework currently supports only older OOB messages with `~service` decorator. // TODO: support receiving messages with other services so we don't have to transform the service // to ~service decorator const [service] = services if (typeof service === 'string') { - throw new AriesFrameworkError('Dids are not currently supported in out-of-band invitation services attribute.') + const [didService] = await this.didCommDocumentService.resolveServicesFromDid(service) + if (didService) { + serviceEndpoint = didService.serviceEndpoint + recipientKeys = didService.recipientKeys.map((key) => key.publicKeyBase58) + routingKeys = didService.routingKeys.map((key) => key.publicKeyBase58) || [] + } + } else { + serviceEndpoint = service.serviceEndpoint + recipientKeys = service.recipientKeys.map(didKeyToVerkey) + routingKeys = service.routingKeys?.map(didKeyToVerkey) || [] + } + + if (!serviceEndpoint || !recipientKeys) { + throw new AriesFrameworkError('Service not found') } const serviceDecorator = new ServiceDecorator({ - recipientKeys: service.recipientKeys.map(didKeyToVerkey), - routingKeys: service.routingKeys?.map(didKeyToVerkey) || [], - serviceEndpoint: service.serviceEndpoint, + recipientKeys, + routingKeys, + serviceEndpoint, }) plaintextMessage['~service'] = JsonTransformer.toJSON(serviceDecorator) diff --git a/packages/core/src/modules/oob/helpers.ts b/packages/core/src/modules/oob/helpers.ts index e3677ee76d..be2fdc1b6e 100644 --- a/packages/core/src/modules/oob/helpers.ts +++ b/packages/core/src/modules/oob/helpers.ts @@ -37,7 +37,7 @@ export function convertToNewInvitation(oldInvitation: ConnectionInvitationMessag export function convertToOldInvitation(newInvitation: OutOfBandInvitation) { // Taking first service, as we can only include one service in a legacy invitation. - const [service] = newInvitation.services + const [service] = newInvitation.getServices() let options if (typeof service === 'string') { diff --git a/packages/core/src/modules/oob/messages/OutOfBandInvitation.ts b/packages/core/src/modules/oob/messages/OutOfBandInvitation.ts index 5b6b776499..39aec65941 100644 --- a/packages/core/src/modules/oob/messages/OutOfBandInvitation.ts +++ b/packages/core/src/modules/oob/messages/OutOfBandInvitation.ts @@ -1,6 +1,5 @@ import type { PlaintextMessage } from '../../../types' import type { HandshakeProtocol } from '../../connections' -import type { Key } from '../../dids' import { Expose, Transform, TransformationType, Type } from 'class-transformer' import { ArrayNotEmpty, IsArray, IsInstance, IsOptional, IsUrl, ValidateNested } from 'class-validator' @@ -13,7 +12,6 @@ import { JsonEncoder } from '../../../utils/JsonEncoder' import { JsonTransformer } from '../../../utils/JsonTransformer' import { IsValidMessageType, parseMessageType, replaceLegacyDidSovPrefix } from '../../../utils/messageType' import { IsStringOrInstance } from '../../../utils/validators' -import { DidKey } from '../../dids' import { outOfBandServiceToNumAlgo2Did } from '../../dids/methods/peer/peerDidNumAlgo2' import { OutOfBandDidCommService } from '../domain/OutOfBandDidCommService' @@ -89,7 +87,7 @@ export class OutOfBandInvitation extends AgentMessage { } public get invitationDids() { - const dids = this.services.map((didOrService) => { + const dids = this.getServices().map((didOrService) => { if (typeof didOrService === 'string') { return didOrService } @@ -98,13 +96,18 @@ export class OutOfBandInvitation extends AgentMessage { return dids } - // TODO: this only takes into account inline didcomm services, won't work for public dids - public getRecipientKeys(): Key[] { - return this.services - .filter((s): s is OutOfBandDidCommService => typeof s !== 'string' && !(s instanceof String)) - .map((s) => s.recipientKeys) - .reduce((acc, curr) => [...acc, ...curr], []) - .map((didKey) => DidKey.fromDid(didKey).key) + // shorthand for services without the need to deal with the String DIDs + public getServices(): Array { + return this.services.map((service) => { + if (service instanceof String) return service.toString() + return service + }) + } + public getDidServices(): Array { + return this.getServices().filter((service): service is string => typeof service === 'string') + } + public getInlineServices(): Array { + return this.getServices().filter((service): service is OutOfBandDidCommService => typeof service !== 'string') } @Transform(({ value }) => replaceLegacyDidSovPrefix(value), { @@ -141,7 +144,8 @@ export class OutOfBandInvitation extends AgentMessage { @OutOfBandServiceTransformer() @IsStringOrInstance(OutOfBandDidCommService, { each: true }) @ValidateNested({ each: true }) - public services!: Array + // eslint-disable-next-line @typescript-eslint/ban-types + private services!: Array /** * Custom property. It is not part of the RFC. @@ -152,13 +156,8 @@ export class OutOfBandInvitation extends AgentMessage { } /** - * Decorator that transforms authentication json to corresponding class instances - * - * @example - * class Example { - * VerificationMethodTransformer() - * private authentication: VerificationMethod - * } + * Decorator that transforms services json to corresponding class instances + * @note Because of ValidateNested limitation, this produces instances of String for DID services except plain js string */ function OutOfBandServiceTransformer() { return Transform(({ value, type }: { value: Array; type: TransformationType }) => { diff --git a/packages/core/src/modules/oob/repository/OutOfBandRecord.ts b/packages/core/src/modules/oob/repository/OutOfBandRecord.ts index 02b821b004..3a67aa4aa7 100644 --- a/packages/core/src/modules/oob/repository/OutOfBandRecord.ts +++ b/packages/core/src/modules/oob/repository/OutOfBandRecord.ts @@ -9,11 +9,21 @@ import { BaseRecord } from '../../../storage/BaseRecord' import { uuid } from '../../../utils/uuid' import { OutOfBandInvitation } from '../messages' +type DefaultOutOfBandRecordTags = { + role: OutOfBandRole + state: OutOfBandState + invitationId: string +} + +interface CustomOutOfBandRecordTags extends TagsBase { + recipientKeyFingerprints: string[] +} + export interface OutOfBandRecordProps { id?: string createdAt?: Date updatedAt?: Date - tags?: TagsBase + tags?: CustomOutOfBandRecordTags outOfBandInvitation: OutOfBandInvitation role: OutOfBandRole state: OutOfBandState @@ -23,14 +33,7 @@ export interface OutOfBandRecordProps { reuseConnectionId?: string } -type DefaultOutOfBandRecordTags = { - role: OutOfBandRole - state: OutOfBandState - invitationId: string - recipientKeyFingerprints: string[] -} - -export class OutOfBandRecord extends BaseRecord { +export class OutOfBandRecord extends BaseRecord { @Type(() => OutOfBandInvitation) public outOfBandInvitation!: OutOfBandInvitation public role!: OutOfBandRole @@ -56,7 +59,7 @@ export class OutOfBandRecord extends BaseRecord { this.reusable = props.reusable ?? false this.mediatorId = props.mediatorId this.reuseConnectionId = props.reuseConnectionId - this._tags = props.tags ?? {} + this._tags = props.tags ?? { recipientKeyFingerprints: [] } } } @@ -66,7 +69,6 @@ export class OutOfBandRecord extends BaseRecord { role: this.role, state: this.state, invitationId: this.outOfBandInvitation.id, - recipientKeyFingerprints: this.outOfBandInvitation.getRecipientKeys().map((key) => key.fingerprint), } } diff --git a/packages/core/src/modules/oob/repository/__tests__/OutOfBandRecord.test.ts b/packages/core/src/modules/oob/repository/__tests__/OutOfBandRecord.test.ts index ee649b7710..6c5cef483e 100644 --- a/packages/core/src/modules/oob/repository/__tests__/OutOfBandRecord.test.ts +++ b/packages/core/src/modules/oob/repository/__tests__/OutOfBandRecord.test.ts @@ -22,6 +22,9 @@ describe('OutOfBandRecord', () => { ], id: 'a-message-id', }), + tags: { + recipientKeyFingerprints: ['z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th'], + }, }) expect(outOfBandRecord.getTags()).toEqual({ diff --git a/packages/core/src/storage/migration/__tests__/__snapshots__/0.1.test.ts.snap b/packages/core/src/storage/migration/__tests__/__snapshots__/0.1.test.ts.snap index 838bdd61b0..fa62c1baad 100644 --- a/packages/core/src/storage/migration/__tests__/__snapshots__/0.1.test.ts.snap +++ b/packages/core/src/storage/migration/__tests__/__snapshots__/0.1.test.ts.snap @@ -782,7 +782,11 @@ Object { }, "type": "OutOfBandRecord", "value": Object { - "_tags": Object {}, + "_tags": Object { + "recipientKeyFingerprints": Array [ + "z6MkfiPMPxCQeSDZGMkCvm1Y2rBoPsmw4ZHMv71jXtcWRRiM", + ], + }, "autoAcceptConnection": undefined, "createdAt": "2022-04-30T13:02:21.577Z", "id": "1-4e4f-41d9-94c4-f49351b811f1", @@ -841,7 +845,11 @@ Object { }, "type": "OutOfBandRecord", "value": Object { - "_tags": Object {}, + "_tags": Object { + "recipientKeyFingerprints": Array [ + "z6MktCZAQNGvWb4WHAjwBqPtXhZdDYorbSJkGW9vj1uhw1HD", + ], + }, "autoAcceptConnection": undefined, "createdAt": "2022-04-30T13:02:21.608Z", "id": "2-4e4f-41d9-94c4-f49351b811f1", @@ -900,7 +908,11 @@ Object { }, "type": "OutOfBandRecord", "value": Object { - "_tags": Object {}, + "_tags": Object { + "recipientKeyFingerprints": Array [ + "z6Mkt1tsp15cnDD7wBCFgehiR2SxHX1aPxt4sueE24twH9Bd", + ], + }, "autoAcceptConnection": false, "createdAt": "2022-04-30T13:02:21.628Z", "id": "3-4e4f-41d9-94c4-f49351b811f1", @@ -959,7 +971,11 @@ Object { }, "type": "OutOfBandRecord", "value": Object { - "_tags": Object {}, + "_tags": Object { + "recipientKeyFingerprints": Array [ + "z6Mkmod8vp2nURVktVC5ceQeyr2VUz26iu2ZANLNVg9pMawa", + ], + }, "autoAcceptConnection": undefined, "createdAt": "2022-04-30T13:02:21.635Z", "id": "4-4e4f-41d9-94c4-f49351b811f1", @@ -1018,7 +1034,11 @@ Object { }, "type": "OutOfBandRecord", "value": Object { - "_tags": Object {}, + "_tags": Object { + "recipientKeyFingerprints": Array [ + "z6MkjDJL4X7YGoH6gjamhZR2NzowPZqtJfX5kPuNuWiVdjMr", + ], + }, "autoAcceptConnection": false, "createdAt": "2022-04-30T13:02:21.641Z", "id": "5-4e4f-41d9-94c4-f49351b811f1", @@ -1135,7 +1155,11 @@ Object { }, "type": "OutOfBandRecord", "value": Object { - "_tags": Object {}, + "_tags": Object { + "recipientKeyFingerprints": Array [ + "z6MkuWTEmH1mUo6W96zSWyH612hFHowRzNEscPYBL2CCMyC2", + ], + }, "autoAcceptConnection": true, "createdAt": "2022-04-30T13:02:21.653Z", "id": "7-4e4f-41d9-94c4-f49351b811f1", diff --git a/packages/core/src/storage/migration/updates/0.1-0.2/__tests__/connection.test.ts b/packages/core/src/storage/migration/updates/0.1-0.2/__tests__/connection.test.ts index c68f5e14d1..24616cd3f3 100644 --- a/packages/core/src/storage/migration/updates/0.1-0.2/__tests__/connection.test.ts +++ b/packages/core/src/storage/migration/updates/0.1-0.2/__tests__/connection.test.ts @@ -383,7 +383,7 @@ describe('0.1-0.2 | Connection', () => { expect(outOfBandRecord.toJSON()).toEqual({ id: expect.any(String), - _tags: {}, + _tags: { recipientKeyFingerprints: ['z6MksYU4MHtfmNhNm1uGMvANr9j4CBv2FymjiJtRgA36bSVH'] }, metadata: {}, // Checked below outOfBandInvitation: { diff --git a/packages/core/src/storage/migration/updates/0.1-0.2/connection.ts b/packages/core/src/storage/migration/updates/0.1-0.2/connection.ts index 30d5058729..5d6ce948be 100644 --- a/packages/core/src/storage/migration/updates/0.1-0.2/connection.ts +++ b/packages/core/src/storage/migration/updates/0.1-0.2/connection.ts @@ -12,6 +12,7 @@ import { DidExchangeRole, } from '../../../../modules/connections' import { convertToNewDidDocument } from '../../../../modules/connections/services/helpers' +import { DidKey } from '../../../../modules/dids' import { DidDocumentRole } from '../../../../modules/dids/domain/DidDocumentRole' import { DidRecord, DidRepository } from '../../../../modules/dids/repository' import { DidRecordMetadataKeys } from '../../../../modules/dids/repository/didRecordMetadataTypes' @@ -310,9 +311,15 @@ export async function migrateToOobRecord( const outOfBandInvitation = convertToNewInvitation(oldInvitation) // If both the recipientKeys and the @id match we assume the connection was created using the same invitation. + const recipientKeyFingerprints = outOfBandInvitation + .getInlineServices() + .map((s) => s.recipientKeys) + .reduce((acc, curr) => [...acc, ...curr], []) + .map((didKey) => DidKey.fromDid(didKey).key.fingerprint) + const oobRecords = await oobRepository.findByQuery({ invitationId: oldInvitation.id, - recipientKeyFingerprints: outOfBandInvitation.getRecipientKeys().map((key) => key.fingerprint), + recipientKeyFingerprints, }) let oobRecord: OutOfBandRecord | undefined = oobRecords[0] @@ -335,6 +342,7 @@ export async function migrateToOobRecord( reusable: oldMultiUseInvitation, mediatorId: connectionRecord.mediatorId, createdAt: connectionRecord.createdAt, + tags: { recipientKeyFingerprints }, }) await oobRepository.save(oobRecord) diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 482a446aca..6b29421d2f 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1,8 +1,8 @@ import type { AgentMessage } from './agent/AgentMessage' -import type { ResolvedDidCommService } from './agent/MessageSender' import type { Logger } from './logger' import type { ConnectionRecord } from './modules/connections' import type { AutoAcceptCredential } from './modules/credentials/models/CredentialAutoAcceptType' +import type { ResolvedDidCommService } from './modules/didcomm' import type { Key } from './modules/dids/domain/Key' import type { IndyPoolConfig } from './modules/ledger/IndyPool' import type { OutOfBandRecord } from './modules/oob/repository' diff --git a/packages/core/src/utils/validators.ts b/packages/core/src/utils/validators.ts index 3a822fdca9..6a5dccd528 100644 --- a/packages/core/src/utils/validators.ts +++ b/packages/core/src/utils/validators.ts @@ -4,12 +4,12 @@ import type { ValidationOptions } from 'class-validator' import { isString, ValidateBy, isInstance, buildMessage } from 'class-validator' /** - * Checks if the value is an instance of the specified object. + * Checks if the value is a string or the specified instance */ export function IsStringOrInstance(targetType: Constructor, validationOptions?: ValidationOptions): PropertyDecorator { return ValidateBy( { - name: 'isStringOrVerificationMethod', + name: 'IsStringOrInstance', constraints: [targetType], validator: { validate: (value, args): boolean => isString(value) || isInstance(value, args?.constraints[0]), @@ -17,9 +17,7 @@ export function IsStringOrInstance(targetType: Constructor, validationOptions?: if (args?.constraints[0]) { return eachPrefix + `$property must be of type string or instance of ${args.constraints[0].name as string}` } else { - return ( - eachPrefix + `isStringOrVerificationMethod decorator expects and object as value, but got falsy value.` - ) + return eachPrefix + `IsStringOrInstance decorator expects an object as value, but got falsy value.` } }, validationOptions), }, diff --git a/packages/core/tests/helpers.ts b/packages/core/tests/helpers.ts index d021344fb8..7124b7ddc7 100644 --- a/packages/core/tests/helpers.ts +++ b/packages/core/tests/helpers.ts @@ -275,7 +275,9 @@ export function getMockConnection({ export function getMockOutOfBand({ label, serviceEndpoint, - recipientKeys, + recipientKeys = [ + new DidKey(Key.fromPublicKeyBase58('ByHnpUCFb1vAfh9CFZ8ZkmUZguURW8nSw889hy6rD8L7', KeyType.Ed25519)).did, + ], mediatorId, role, state, @@ -303,9 +305,7 @@ export function getMockOutOfBand({ id: `#inline-0`, priority: 0, serviceEndpoint: serviceEndpoint ?? 'http://example.com', - recipientKeys: recipientKeys || [ - new DidKey(Key.fromPublicKeyBase58('ByHnpUCFb1vAfh9CFZ8ZkmUZguURW8nSw889hy6rD8L7', KeyType.Ed25519)).did, - ], + recipientKeys, routingKeys: [], }), ], @@ -318,6 +318,9 @@ export function getMockOutOfBand({ outOfBandInvitation: outOfBandInvitation, reusable, reuseConnectionId, + tags: { + recipientKeyFingerprints: recipientKeys.map((didKey) => DidKey.fromDid(didKey).key.fingerprint), + }, }) return outOfBandRecord } diff --git a/packages/core/tests/oob.test.ts b/packages/core/tests/oob.test.ts index b256a4849e..467532e64a 100644 --- a/packages/core/tests/oob.test.ts +++ b/packages/core/tests/oob.test.ts @@ -172,7 +172,7 @@ describe('out of band', () => { expect(outOfBandInvitation.getRequests()).toBeUndefined() // expect contains services - const [service] = outOfBandInvitation.services as OutOfBandDidCommService[] + const [service] = outOfBandInvitation.getInlineServices() expect(service).toMatchObject( new OutOfBandDidCommService({ id: expect.any(String), @@ -196,7 +196,7 @@ describe('out of band', () => { expect(outOfBandInvitation.getRequests()).toHaveLength(1) // expect contains services - const [service] = outOfBandInvitation.services + const [service] = outOfBandInvitation.getServices() expect(service).toMatchObject( new OutOfBandDidCommService({ id: expect.any(String), @@ -220,7 +220,7 @@ describe('out of band', () => { expect(outOfBandInvitation.getRequests()).toHaveLength(1) // expect contains services - const [service] = outOfBandInvitation.services as OutOfBandDidCommService[] + const [service] = outOfBandInvitation.getInlineServices() expect(service).toMatchObject( new OutOfBandDidCommService({ id: expect.any(String), @@ -467,8 +467,8 @@ describe('out of band', () => { const outOfBandRecord2 = await faberAgent.oob.createInvitation(makeConnectionConfig) // Take over the recipientKeys from the first invitation so they match when encoded - const firstInvitationService = outOfBandRecord.outOfBandInvitation.services[0] as OutOfBandDidCommService - const secondInvitationService = outOfBandRecord2.outOfBandInvitation.services[0] as OutOfBandDidCommService + const [firstInvitationService] = outOfBandRecord.outOfBandInvitation.getInlineServices() + const [secondInvitationService] = outOfBandRecord2.outOfBandInvitation.getInlineServices() secondInvitationService.recipientKeys = firstInvitationService.recipientKeys aliceAgent.events.on(OutOfBandEventTypes.HandshakeReused, aliceReuseListener) @@ -680,19 +680,6 @@ describe('out of band', () => { new AriesFrameworkError('There is no message in requests~attach supported by agent.') ) }) - - test('throw an error when a did is used in the out of band message', async () => { - const { message } = await faberAgent.credentials.createOffer(credentialTemplate) - const { outOfBandInvitation } = await faberAgent.oob.createInvitation({ - ...issueCredentialConfig, - messages: [message], - }) - outOfBandInvitation.services = ['somedid'] - - await expect(aliceAgent.oob.receiveInvitation(outOfBandInvitation, receiveInvitationConfig)).rejects.toEqual( - new AriesFrameworkError('Dids are not currently supported in out-of-band invitation services attribute.') - ) - }) }) describe('createLegacyConnectionlessInvitation', () => {