Skip to content

Commit

Permalink
feat: support new did document in didcomm message exchange (#609)
Browse files Browse the repository at this point in the history
* refactor: unify did document services
* feat: integrate did resolver with message sender
* feat: support new did docoument for msg receiver

Signed-off-by: Timo Glastra <timo@animo.id>
  • Loading branch information
TimoGlastra committed Feb 8, 2022
1 parent 575f89d commit 4694037
Show file tree
Hide file tree
Showing 30 changed files with 326 additions and 463 deletions.
80 changes: 61 additions & 19 deletions packages/core/src/agent/MessageReceiver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import type { TransportSession } from './TransportService'
import { Lifecycle, scoped } from 'tsyringe'

import { AriesFrameworkError } from '../error'
import { ConnectionService } from '../modules/connections/services/ConnectionService'
import { ConnectionRepository } from '../modules/connections'
import { DidRepository } from '../modules/dids/repository/DidRepository'
import { ProblemReportError, ProblemReportMessage, ProblemReportReason } from '../modules/problem-reports'
import { JsonTransformer } from '../utils/JsonTransformer'
import { MessageValidator } from '../utils/MessageValidator'
Expand All @@ -28,25 +29,28 @@ export class MessageReceiver {
private envelopeService: EnvelopeService
private transportService: TransportService
private messageSender: MessageSender
private connectionService: ConnectionService
private dispatcher: Dispatcher
private logger: Logger
private didRepository: DidRepository
private connectionRepository: ConnectionRepository
public readonly inboundTransports: InboundTransport[] = []

public constructor(
config: AgentConfig,
envelopeService: EnvelopeService,
transportService: TransportService,
messageSender: MessageSender,
connectionService: ConnectionService,
dispatcher: Dispatcher
connectionRepository: ConnectionRepository,
dispatcher: Dispatcher,
didRepository: DidRepository
) {
this.config = config
this.envelopeService = envelopeService
this.transportService = transportService
this.messageSender = messageSender
this.connectionService = connectionService
this.connectionRepository = connectionRepository
this.dispatcher = dispatcher
this.didRepository = didRepository
this.logger = this.config.logger
}

Expand Down Expand Up @@ -77,21 +81,10 @@ export class MessageReceiver {
}

private async receiveEncryptedMessage(encryptedMessage: EncryptedMessage, session?: TransportSession) {
const { plaintextMessage, senderKey, recipientKey } = await this.decryptMessage(encryptedMessage)
const decryptedMessage = await this.decryptMessage(encryptedMessage)
const { plaintextMessage, senderKey, recipientKey } = decryptedMessage

let connection: ConnectionRecord | null = null

// Only fetch connection if recipientKey and senderKey are present (AuthCrypt)
if (senderKey && recipientKey) {
connection = await this.connectionService.findByVerkey(recipientKey)

// Throw error if the recipient key (ourKey) does not match the key of the connection record
if (connection && connection.theirKey !== null && connection.theirKey !== senderKey) {
throw new AriesFrameworkError(
`Inbound message senderKey '${senderKey}' is different from connection.theirKey '${connection.theirKey}'`
)
}
}
const connection = await this.findConnectionByMessageKeys(decryptedMessage)

this.logger.info(
`Received message with type '${plaintextMessage['@type']}' from connection ${connection?.id} (${connection?.theirLabel})`,
Expand Down Expand Up @@ -171,6 +164,55 @@ export class MessageReceiver {
return message
}

private async findConnectionByMessageKeys({
recipientKey,
senderKey,
}: DecryptedMessageContext): Promise<ConnectionRecord | null> {
// We only fetch connections that are sent in AuthCrypt mode
if (!recipientKey || !senderKey) return null

let connection: ConnectionRecord | null = null

// Try to find the did records that holds the sender and recipient keys
const ourDidRecord = await this.didRepository.findByVerkey(recipientKey)

// If both our did record and their did record is available we can find a matching did record
if (ourDidRecord) {
const theirDidRecord = await this.didRepository.findByVerkey(senderKey)

if (theirDidRecord) {
connection = await this.connectionRepository.findSingleByQuery({
did: ourDidRecord.id,
theirDid: theirDidRecord.id,
})
} else {
connection = await this.connectionRepository.findSingleByQuery({
did: ourDidRecord.id,
})

// If theirDidRecord was not found, and connection.theirDid is set, it means the sender is not authenticated
// to send messages to use
if (connection && connection.theirDid) {
throw new AriesFrameworkError(`Inbound message senderKey '${senderKey}' is different from connection did`)
}
}
}

// If no connection was found, we search in the connection record, where legacy did documents are stored
if (!connection) {
connection = await this.connectionRepository.findByVerkey(recipientKey)

// Throw error if the recipient key (ourKey) does not match the key of the connection record
if (connection && connection.theirKey !== null && connection.theirKey !== senderKey) {
throw new AriesFrameworkError(
`Inbound message senderKey '${senderKey}' is different from connection.theirKey '${connection.theirKey}'`
)
}
}

return connection
}

/**
* Transform an plaintext DIDComm message into it's corresponding message class. Will look at all message types in the registered handlers.
*
Expand Down
45 changes: 36 additions & 9 deletions packages/core/src/agent/MessageSender.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { DidCommService, ConnectionRecord } from '../modules/connections'
import type { ConnectionRecord } from '../modules/connections'
import type { DidCommService, IndyAgentService } from '../modules/dids/domain/service'
import type { OutboundTransport } from '../transport/OutboundTransport'
import type { OutboundMessage, OutboundPackage, EncryptedMessage } from '../types'
import type { AgentMessage } from './AgentMessage'
Expand All @@ -11,6 +12,7 @@ import { DID_COMM_TRANSPORT_QUEUE, InjectionSymbols } from '../constants'
import { ReturnRouteTypes } from '../decorators/transport/TransportDecorator'
import { AriesFrameworkError } from '../error'
import { Logger } from '../logger'
import { DidResolverService } from '../modules/dids/services/DidResolverService'
import { MessageRepository } from '../storage/MessageRepository'
import { MessageValidator } from '../utils/MessageValidator'

Expand All @@ -28,18 +30,21 @@ export class MessageSender {
private transportService: TransportService
private messageRepository: MessageRepository
private logger: Logger
private didResolverService: DidResolverService
public readonly outboundTransports: OutboundTransport[] = []

public constructor(
envelopeService: EnvelopeService,
transportService: TransportService,
@inject(InjectionSymbols.MessageRepository) messageRepository: MessageRepository,
@inject(InjectionSymbols.Logger) logger: Logger
@inject(InjectionSymbols.Logger) logger: Logger,
didResolverService: DidResolverService
) {
this.envelopeService = envelopeService
this.transportService = transportService
this.messageRepository = messageRepository
this.logger = logger
this.didResolverService = didResolverService
this.outboundTransports = []
}

Expand Down Expand Up @@ -292,22 +297,44 @@ export class MessageSender {
this.logger.debug(`Retrieving services for connection '${connection.id}' (${connection.theirLabel})`, {
transportPriority,
})
// Retrieve DIDComm services
const allServices = this.transportService.findDidCommServices(connection)

//Separate queue service out
let services = allServices.filter((s) => !isDidCommTransportQueue(s.serviceEndpoint))
const queueService = allServices.find((s) => isDidCommTransportQueue(s.serviceEndpoint))
let didCommServices: Array<IndyAgentService | DidCommService>

// If theirDid starts with a did: prefix it means we're using the new did syntax
// and we should use the did resolver
if (connection.theirDid?.startsWith('did:')) {
const {
didDocument,
didResolutionMetadata: { error, message },
} = await this.didResolverService.resolve(connection.theirDid)

if (!didDocument) {
throw new AriesFrameworkError(
`Unable to resolve did document for did '${connection.theirDid}': ${error} ${message}`
)
}

didCommServices = didDocument.didCommServices
}
// Old school method, did document is stored inside the connection record
else {
// Retrieve DIDComm services
didCommServices = this.transportService.findDidCommServices(connection)
}

// Separate queue service out
let services = didCommServices.filter((s) => !isDidCommTransportQueue(s.serviceEndpoint))
const queueService = didCommServices.find((s) => isDidCommTransportQueue(s.serviceEndpoint))

//If restrictive will remove services not listed in schemes list
// If restrictive will remove services not listed in schemes list
if (transportPriority?.restrictive) {
services = services.filter((service) => {
const serviceSchema = service.protocolScheme
return transportPriority.schemes.includes(serviceSchema)
})
}

//If transport priority is set we will sort services by our priority
// If transport priority is set we will sort services by our priority
if (transportPriority?.schemes) {
services = services.sort(function (a, b) {
const aScheme = a.protocolScheme
Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/agent/TransportService.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import type { DidDoc, IndyAgentService } from '../modules/connections/models'
import type { DidDoc } from '../modules/connections/models'
import type { ConnectionRecord } from '../modules/connections/repository'
import type { IndyAgentService } from '../modules/dids/domain/service'
import type { EncryptedMessage } from '../types'
import type { AgentMessage } from './AgentMessage'
import type { EnvelopeKeys } from './EnvelopeService'

import { Lifecycle, scoped } from 'tsyringe'

import { DID_COMM_TRANSPORT_QUEUE } from '../constants'
import { ConnectionRole, DidCommService } from '../modules/connections/models'
import { ConnectionRole } from '../modules/connections/models'
import { DidCommService } from '../modules/dids/domain/service'

@scoped(Lifecycle.ContainerScoped)
export class TransportService {
Expand Down
78 changes: 73 additions & 5 deletions packages/core/src/agent/__tests__/MessageSender.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import { TestMessage } from '../../../tests/TestMessage'
import { getAgentConfig, getMockConnection, mockFunction } from '../../../tests/helpers'
import testLogger from '../../../tests/logger'
import { ReturnRouteTypes } from '../../decorators/transport/TransportDecorator'
import { DidCommService } from '../../modules/connections'
import { DidDocument } from '../../modules/dids'
import { DidCommService } from '../../modules/dids/domain/service/DidCommService'
import { DidResolverService } from '../../modules/dids/services/DidResolverService'
import { InMemoryMessageRepository } from '../../storage/InMemoryMessageRepository'
import { EnvelopeService as EnvelopeServiceImpl } from '../EnvelopeService'
import { MessageSender } from '../MessageSender'
Expand All @@ -18,10 +20,11 @@ import { DummyTransportSession } from './stubs'

jest.mock('../TransportService')
jest.mock('../EnvelopeService')
jest.mock('../../modules/dids/services/DidResolverService')

const TransportServiceMock = TransportService as jest.MockedClass<typeof TransportService>
const DidResolverServiceMock = DidResolverService as jest.Mock<DidResolverService>
const logger = testLogger

class DummyOutboundTransport implements OutboundTransport {
public start(): Promise<void> {
throw new Error('Method not implemented.')
Expand Down Expand Up @@ -88,14 +91,23 @@ describe('MessageSender', () => {
let messageRepository: MessageRepository
let connection: ConnectionRecord
let outboundMessage: OutboundMessage
let didResolverService: DidResolverService

describe('sendMessage', () => {
beforeEach(() => {
TransportServiceMock.mockClear()
transportServiceHasInboundEndpoint.mockReturnValue(true)

didResolverService = new DidResolverServiceMock()
outboundTransport = new DummyOutboundTransport()
messageRepository = new InMemoryMessageRepository(getAgentConfig('MessageSender'))
messageSender = new MessageSender(enveloperService, transportService, messageRepository, logger)
messageSender = new MessageSender(
enveloperService,
transportService,
messageRepository,
logger,
didResolverService
)
connection = getMockConnection({ id: 'test-123', theirLabel: 'Test 123' })

outboundMessage = createOutboundMessage(connection, new TestMessage())
Expand Down Expand Up @@ -140,6 +152,55 @@ describe('MessageSender', () => {
expect(sendMessageSpy).toHaveBeenCalledTimes(1)
})

test("resolves the did document using the did resolver if connection.theirDid starts with 'did:'", async () => {
messageSender.registerOutboundTransport(outboundTransport)

const did = 'did:peer:1exampledid'
const sendMessageSpy = jest.spyOn(outboundTransport, 'sendMessage')
const resolveMock = mockFunction(didResolverService.resolve)

connection.theirDid = did
resolveMock.mockResolvedValue({
didDocument: new DidDocument({
id: did,
service: [firstDidCommService, secondDidCommService],
}),
didResolutionMetadata: {},
didDocumentMetadata: {},
})

await messageSender.sendMessage(outboundMessage)

expect(resolveMock).toHaveBeenCalledWith(did)
expect(sendMessageSpy).toHaveBeenCalledWith({
connectionId: 'test-123',
payload: encryptedMessage,
endpoint: firstDidCommService.serviceEndpoint,
responseRequested: false,
})
expect(sendMessageSpy).toHaveBeenCalledTimes(1)
})

test("throws an error if connection.theirDid starts with 'did:' but the resolver can't resolve the did document", async () => {
messageSender.registerOutboundTransport(outboundTransport)

const did = 'did:peer:1exampledid'
const resolveMock = mockFunction(didResolverService.resolve)

connection.theirDid = did
resolveMock.mockResolvedValue({
didDocument: null,
didResolutionMetadata: {
error: 'notFound',
},
didDocumentMetadata: {},
})

await expect(messageSender.sendMessage(outboundMessage)).rejects.toThrowError(
`Unable to resolve did document for did '${did}': notFound`
)
})

test('call send message when session send method fails with missing keys', async () => {
messageSender.registerOutboundTransport(outboundTransport)
transportServiceFindSessionMock.mockReturnValue(sessionWithoutKeys)
Expand Down Expand Up @@ -212,7 +273,8 @@ describe('MessageSender', () => {
enveloperService,
transportService,
new InMemoryMessageRepository(getAgentConfig('MessageSenderTest')),
logger
logger,
didResolverService
)

envelopeServicePackMessageMock.mockReturnValue(Promise.resolve(encryptedMessage))
Expand Down Expand Up @@ -276,7 +338,13 @@ describe('MessageSender', () => {
beforeEach(() => {
outboundTransport = new DummyOutboundTransport()
messageRepository = new InMemoryMessageRepository(getAgentConfig('PackMessage'))
messageSender = new MessageSender(enveloperService, transportService, messageRepository, logger)
messageSender = new MessageSender(
enveloperService,
transportService,
messageRepository,
logger,
didResolverService
)
connection = getMockConnection({ id: 'test-123' })

envelopeServicePackMessageMock.mockReturnValue(Promise.resolve(encryptedMessage))
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/agent/__tests__/TransportService.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getMockConnection } from '../../../tests/helpers'
import { ConnectionInvitationMessage, ConnectionRole, DidDoc, DidCommService } from '../../modules/connections'
import { ConnectionInvitationMessage, ConnectionRole, DidDoc } from '../../modules/connections'
import { DidCommService } from '../../modules/dids/domain/service/DidCommService'
import { TransportService } from '../TransportService'

import { DummyTransportSession } from './stubs'
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/agent/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { ConnectionRecord } from '../modules/connections'
import type { OutboundMessage, OutboundServiceMessage } from '../types'
import type { AgentMessage } from './AgentMessage'

import { DidCommService } from '../modules/connections/models/did/service/DidCommService'
import { DidCommService } from '../modules/dids/domain/service/DidCommService'

export function createOutboundMessage<T extends AgentMessage = AgentMessage>(
connection: ConnectionRecord,
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/decorators/service/ServiceDecorator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { IsArray, IsOptional, IsString } from 'class-validator'

import { DidCommService } from '../../modules/connections/models/did/service/DidCommService'
import { DidCommService } from '../../modules/dids/domain/service/DidCommService'
import { uuid } from '../../utils/uuid'

export interface ServiceDecoratorOptions {
Expand Down
Loading

0 comments on commit 4694037

Please sign in to comment.