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: OOB public did #930

Merged
merged 19 commits into from
Aug 29, 2022
Merged
Show file tree
Hide file tree
Changes from 14 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
88 changes: 30 additions & 58 deletions packages/core/src/agent/MessageSender.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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'
Expand All @@ -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
Expand All @@ -43,20 +37,26 @@ export class MessageSender {
private messageRepository: MessageRepository
private logger: Logger
private didResolverService: DidResolverService
private didCommDocumentService: DidCommDocumentService
private outOfBandRepository: OutOfBandRepository
public readonly outboundTransports: OutboundTransport[] = []

public constructor(
envelopeService: EnvelopeService,
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 = []
}

Expand Down Expand Up @@ -342,49 +342,6 @@ export class MessageSender {
throw new AriesFrameworkError(`Unable to send message to service: ${service.serviceEndpoint}`)
}

private async retrieveServicesFromDid(did: string) {
TimoGlastra marked this conversation as resolved.
Show resolved Hide resolved
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,
Expand All @@ -399,14 +356,29 @@ 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 = await this.didCommDocumentService.resolveServicesFromDid(service)

// store recipientKeyFingerprints in the oob record
const oldRecipientKeyFingerprints = outOfBand.getTags().recipientKeyFingerprints
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will only be set if the recipientKeys haven't been set yet, however the recipient keys are set in the out of band record constructor. Does this mean it will only work if there is only a did in the service array? If we have one inline service and one did this won't work I think.

If possible I'd like to make sure we're covering all cases. After thinking about it a bit more this probably isn't the best place to store this, and maybe we're better off doing it in the out of band module and take our loss with doing two resolves of the dids for now? (so method no 1 you described). If we want to keep it this way we should somehow take into account a mix of dids and inline services

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part is now removed. The recipient keys are resolved already when creating the OOB record as suggested. No caching for the resolved services is done now. Maybe it can be added later as an optimisation.

if (!oldRecipientKeyFingerprints?.length) {
const allRecipientKeys = didCommServices.reduce<Key[]>(
(aggr, { recipientKeys }) => [...aggr, ...recipientKeys],
[]
)
outOfBand.setTag(
'recipientKeyFingerprints',
allRecipientKeys.map((key) => key.fingerprint)
)
await this.outOfBandRepository.update(outOfBand)
}
} else {
// Out of band inline service contains keys encoded as did:key references
didCommServices.push({
Expand Down
45 changes: 37 additions & 8 deletions packages/core/src/agent/__tests__/MessageSender.test.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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<typeof TransportService>
const DidResolverServiceMock = DidResolverService as jest.Mock<DidResolverService>
const DidCommDocumentServiceMock = DidCommDocumentService as jest.Mock<DidCommDocumentService>
const OutOfBandRepositoryMock = OutOfBandRepository as jest.Mock<OutOfBandRepository>

class DummyHttpOutboundTransport implements OutboundTransport {
public start(): Promise<void> {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -130,7 +139,9 @@ describe('MessageSender', () => {
transportService,
messageRepository,
logger,
didResolverService
didResolverService,
didCommDocumentService,
outOfBandRepository
)
connection = getMockConnection({
id: 'test-123',
Expand All @@ -147,6 +158,10 @@ describe('MessageSender', () => {
service: [firstDidCommService, secondDidCommService],
})
didResolverServiceResolveMock.mockResolvedValue(didDocumentInstance)
didResolverServiceResolveDidServicesMock.mockResolvedValue([
getMockResolvedDidService(firstDidCommService),
getMockResolvedDidService(secondDidCommService),
])
})

afterEach(() => {
Expand All @@ -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)`
Expand All @@ -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,
Expand Down Expand Up @@ -326,7 +342,9 @@ describe('MessageSender', () => {
transportService,
new InMemoryMessageRepository(getAgentConfig('MessageSenderTest')),
logger,
didResolverService
didResolverService,
didCommDocumentService,
outOfBandRepository
)

envelopeServicePackMessageMock.mockReturnValue(Promise.resolve(encryptedMessage))
Expand Down Expand Up @@ -406,7 +424,9 @@ describe('MessageSender', () => {
transportService,
messageRepository,
logger,
didResolverService
didResolverService,
didCommDocumentService,
outOfBandRepository
)
connection = getMockConnection()

Expand Down Expand Up @@ -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: [],
}
}
2 changes: 1 addition & 1 deletion packages/core/src/agent/helpers.ts
Original file line number Diff line number Diff line change
@@ -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<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,4 +1,4 @@
import type { ResolvedDidCommService } from '../../agent/MessageSender'
import type { ResolvedDidCommService } from '../../modules/didcomm'

import { IsArray, IsOptional, IsString } from 'class-validator'

Expand Down
12 changes: 5 additions & 7 deletions packages/core/src/modules/connections/DidExchangeProtocol.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
}
TimoGlastra marked this conversation as resolved.
Show resolved Hide resolved

connectionRecord.threadId = connectionRequest.id
await this.updateState(connectionRecord, DidExchangeState.RequestSent)

return {
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/modules/didcomm/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './types'
export * from './services'
Loading