Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support new did document in didcomm message exchange #609

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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