Skip to content

Commit

Permalink
feat: support invitationDid when creating an invitation (#1811)
Browse files Browse the repository at this point in the history
Signed-off-by: Timo Glastra <[email protected]>
Signed-off-by: Ariel Gentile <[email protected]>
Co-authored-by: Ariel Gentile <[email protected]>
  • Loading branch information
TimoGlastra and genaris authored Apr 25, 2024
1 parent b41e158 commit e5c6698
Show file tree
Hide file tree
Showing 12 changed files with 261 additions and 79 deletions.
31 changes: 25 additions & 6 deletions packages/core/src/modules/connections/ConnectionsApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,11 @@ export class ConnectionsApi {
throw new CredoError(`'routing' is disallowed when defining 'ourDid'`)
}

const routing =
config.routing ||
(await this.routingService.getRouting(this.agentContext, { mediatorId: outOfBandRecord.mediatorId }))
// Only generate routing if ourDid hasn't been provided
let routing = config.routing
if (!routing && !ourDid) {
routing = await this.routingService.getRouting(this.agentContext, { mediatorId: outOfBandRecord.mediatorId })
}

let result
if (protocol === HandshakeProtocol.DidExchange) {
Expand All @@ -126,6 +128,11 @@ export class ConnectionsApi {
if (ourDid) {
throw new CredoError('Using an externally defined did for connections protocol is unsupported')
}
// This is just to make TS happy, as we always generate routing if ourDid is not provided
// and ourDid is not supported for connection (see check above)
if (!routing) {
throw new CredoError('Routing is required for connections protocol')
}

result = await this.connectionService.createRequest(this.agentContext, outOfBandRecord, {
label,
Expand Down Expand Up @@ -169,9 +176,13 @@ export class ConnectionsApi {
throw new CredoError(`Out-of-band record ${connectionRecord.outOfBandId} not found.`)
}

// If the outOfBandRecord is reusable we need to use new routing keys for the connection, otherwise
// all connections will use the same routing keys
const routing = outOfBandRecord.reusable ? await this.routingService.getRouting(this.agentContext) : undefined
// We generate routing in two scenarios:
// 1. When the out-of-band invitation is reusable, as otherwise all connections use the same keys
// 2. When the out-of-band invitation has no inline services, as we don't want to generate a legacy did doc from a service did
const routing =
outOfBandRecord.reusable || outOfBandRecord.outOfBandInvitation.getInlineServices().length === 0
? await this.routingService.getRouting(this.agentContext)
: undefined

let outboundMessageContext
if (connectionRecord.protocol === HandshakeProtocol.DidExchange) {
Expand All @@ -186,6 +197,14 @@ export class ConnectionsApi {
connection: connectionRecord,
})
} else {
// We generate routing in two scenarios:
// 1. When the out-of-band invitation is reusable, as otherwise all connections use the same keys
// 2. When the out-of-band invitation has no inline services, as we don't want to generate a legacy did doc from a service did
const routing =
outOfBandRecord.reusable || outOfBandRecord.outOfBandInvitation.getInlineServices().length === 0
? await this.routingService.getRouting(this.agentContext)
: undefined

const { message } = await this.connectionService.createResponse(
this.agentContext,
connectionRecord,
Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/modules/connections/DidExchangeProtocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,14 +257,19 @@ export class DidExchangeProtocol {
let services: ResolvedDidCommService[] = []
if (routing) {
services = routingToServices(routing)
} else if (outOfBandRecord) {
} else if (outOfBandRecord.outOfBandInvitation.getInlineServices().length > 0) {
const inlineServices = outOfBandRecord.outOfBandInvitation.getInlineServices()
services = inlineServices.map((service) => ({
id: service.id,
serviceEndpoint: service.serviceEndpoint,
recipientKeys: service.recipientKeys.map(didKeyToInstanceOfKey),
routingKeys: service.routingKeys?.map(didKeyToInstanceOfKey) ?? [],
}))
} else {
// We don't support using a did from the OOB invitation services currently, in this case we always pass routing to this method
throw new CredoError(
'No routing provided, and no inline services found in out of band invitation. When using did services in out of band invitation, make sure to provide routing information for rotation.'
)
}

// Use the same num algo for response as received in request
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { AgentContext } from '../../../agent'
import type { Wallet } from '../../../wallet/Wallet'
import type { DidDocument } from '../../dids'
import type { Routing } from '../services/ConnectionService'

import { Subject } from 'rxjs'
Expand All @@ -27,7 +26,6 @@ import { DidDocumentRole } from '../../dids/domain/DidDocumentRole'
import { DidCommV1Service } from '../../dids/domain/service/DidCommV1Service'
import { didDocumentJsonToNumAlgo1Did } from '../../dids/methods/peer/peerDidNumAlgo1'
import { DidRecord, DidRepository } from '../../dids/repository'
import { DidRegistrarService } from '../../dids/services/DidRegistrarService'
import { OutOfBandService } from '../../oob/OutOfBandService'
import { OutOfBandRole } from '../../oob/domain/OutOfBandRole'
import { OutOfBandState } from '../../oob/domain/OutOfBandState'
Expand All @@ -50,24 +48,11 @@ import { convertToNewDidDocument } from '../services/helpers'
jest.mock('../repository/ConnectionRepository')
jest.mock('../../oob/repository/OutOfBandRepository')
jest.mock('../../oob/OutOfBandService')
jest.mock('../../dids/services/DidRegistrarService')
jest.mock('../../dids/repository/DidRepository')
const ConnectionRepositoryMock = ConnectionRepository as jest.Mock<ConnectionRepository>
const OutOfBandRepositoryMock = OutOfBandRepository as jest.Mock<OutOfBandRepository>
const OutOfBandServiceMock = OutOfBandService as jest.Mock<OutOfBandService>
const DidRepositoryMock = DidRepository as jest.Mock<DidRepository>
const DidRegistrarServiceMock = DidRegistrarService as jest.Mock<DidRegistrarService>

const didRegistrarService = new DidRegistrarServiceMock()
mockFunction(didRegistrarService.create).mockResolvedValue({
didDocumentMetadata: {},
didRegistrationMetadata: {},
didState: {
state: 'finished',
did: 'did:peer:123',
didDocument: {} as DidDocument,
},
})

const connectionImageUrl = 'https://example.com/image.png'

Expand All @@ -78,12 +63,12 @@ const agentConfig = getAgentConfig('ConnectionServiceTest', {

const outOfBandRepository = new OutOfBandRepositoryMock()
const outOfBandService = new OutOfBandServiceMock()
const didRepository = new DidRepositoryMock()

describe('ConnectionService', () => {
let wallet: Wallet
let connectionRepository: ConnectionRepository

let didRepository: DidRepository
let connectionService: ConnectionService
let eventEmitter: EventEmitter
let myRouting: Routing
Expand All @@ -97,6 +82,7 @@ describe('ConnectionService', () => {
registerInstances: [
[OutOfBandRepository, outOfBandRepository],
[OutOfBandService, outOfBandService],
[DidRepository, didRepository],
],
})
await wallet.createAndOpen(agentConfig.walletConfig)
Expand All @@ -109,7 +95,6 @@ describe('ConnectionService', () => {
beforeEach(async () => {
eventEmitter = new EventEmitter(agentConfig.agentDependencies, new Subject())
connectionRepository = new ConnectionRepositoryMock()
didRepository = new DidRepositoryMock()
connectionService = new ConnectionService(agentConfig.logger, connectionRepository, didRepository, eventEmitter)
myRouting = {
recipientKey: Key.fromFingerprint('z6MkwFkSP4uv5PhhKJCGehtjuZedkotC7VF64xtMsxuM8R3W'),
Expand All @@ -119,11 +104,14 @@ describe('ConnectionService', () => {
}

mockFunction(didRepository.getById).mockResolvedValue(
new DidRecord({
did: 'did:peer:123',
role: DidDocumentRole.Created,
})
Promise.resolve(
new DidRecord({
did: 'did:peer:123',
role: DidDocumentRole.Created,
})
)
)
mockFunction(didRepository.findByQuery).mockResolvedValue(Promise.resolve([]))
})

describe('createRequest', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,16 @@ export class ConnectionRequestHandler implements MessageHandler {
}

if (connectionRecord?.autoAcceptConnection ?? this.connectionsModuleConfig.autoAcceptConnections) {
// TODO: Allow rotation of keys used in the invitation for new ones not only when out-of-band is reusable
const routing = outOfBandRecord.reusable ? await this.routingService.getRouting(agentContext) : undefined
// TODO: Allow rotation of keys used in the invitation for new ones not only when out-of-band is reusable or
// when there are no inline services in the invitation

// We generate routing in two scenarios:
// 1. When the out-of-band invitation is reusable, as otherwise all connections use the same keys
// 2. When the out-of-band invitation has no inline services, as we don't want to generate a legacy did doc from a service did
const routing =
outOfBandRecord.reusable || outOfBandRecord.outOfBandInvitation.getInlineServices().length === 0
? await this.routingService.getRouting(agentContext)
: undefined

const { message } = await this.connectionService.createResponse(
agentContext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,14 @@ export class DidExchangeRequestHandler implements MessageHandler {
if (connectionRecord.autoAcceptConnection ?? this.connectionsModuleConfig.autoAcceptConnections) {
// TODO We should add an option to not pass routing and therefore do not rotate keys and use the keys from the invitation
// TODO: Allow rotation of keys used in the invitation for new ones not only when out-of-band is reusable
const routing = outOfBandRecord.reusable ? await this.routingService.getRouting(agentContext) : undefined

// We generate routing in two scenarios:
// 1. When the out-of-band invitation is reusable, as otherwise all connections use the same keys
// 2. When the out-of-band invitation has no inline services, as we don't want to generate a legacy did doc from a service did
const routing =
outOfBandRecord.reusable || outOfBandRecord.outOfBandInvitation.getInlineServices().length === 0
? await this.routingService.getRouting(agentContext)
: undefined

const message = await this.didExchangeProtocol.createResponse(
agentContext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ import {
import { ConnectionRecord } from '../repository/ConnectionRecord'
import { ConnectionRepository } from '../repository/ConnectionRepository'

import { convertToNewDidDocument } from './helpers'
import { assertNoCreatedDidExistsForKeys, convertToNewDidDocument } from './helpers'

export interface ConnectionRequestParams {
label?: string
Expand Down Expand Up @@ -209,9 +209,17 @@ export class ConnectionService {
connectionRecord.assertState(DidExchangeState.RequestReceived)
connectionRecord.assertRole(DidExchangeRole.Responder)

const didDoc = routing
? this.createDidDoc(routing)
: this.createDidDocFromOutOfBandDidCommServices(outOfBandRecord.outOfBandInvitation.getInlineServices())
let didDoc: DidDoc
if (routing) {
didDoc = this.createDidDoc(routing)
} else if (outOfBandRecord.outOfBandInvitation.getInlineServices().length > 0) {
didDoc = this.createDidDocFromOutOfBandDidCommServices(outOfBandRecord.outOfBandInvitation.getInlineServices())
} else {
// We don't support using a did from the OOB invitation services currently, in this case we always pass routing to this method
throw new CredoError(
'No routing provided, and no inline services found in out of band invitation. When using did services in out of band invitation, make sure to provide routing information for rotation.'
)
}

const { did: peerDid } = await this.createDid(agentContext, {
role: DidDocumentRole.Created,
Expand Down Expand Up @@ -778,6 +786,11 @@ export class ConnectionService {
// Convert the legacy did doc to a new did document
const didDocument = convertToNewDidDocument(didDoc)

// Assert that the keys we are going to use for creating a did document haven't already been used in another did document
if (role === DidDocumentRole.Created) {
await assertNoCreatedDidExistsForKeys(agentContext, didDocument.recipientKeys)
}

const peerDid = didDocumentJsonToNumAlgo1Did(didDocument.toJSON())
didDocument.id = peerDid
const didRecord = new DidRecord({
Expand Down
36 changes: 35 additions & 1 deletion packages/core/src/modules/connections/services/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
DidRepository,
DidsApi,
createPeerDidDocumentFromServices,
DidDocumentRole,
} from '../../dids'
import { didDocumentJsonToNumAlgo1Did } from '../../dids/methods/peer/peerDidNumAlgo1'
import { EmbeddedAuthentication } from '../models'
Expand Down Expand Up @@ -139,6 +140,36 @@ export async function getDidDocumentForCreatedDid(agentContext: AgentContext, di
return didRecord.didDocument
}

/**
* Asserts that the keys we are going to use for creating a did document haven't already been used in another did document
* Due to how DIDComm v1 works (only reference the key not the did in encrypted message) we can't have multiple dids containing
* the same key as we won't know which did (and thus which connection) a message is intended for.
*/
export async function assertNoCreatedDidExistsForKeys(agentContext: AgentContext, recipientKeys: Key[]) {
const didRepository = agentContext.dependencyManager.resolve(DidRepository)
const recipientKeyFingerprints = recipientKeys.map((key) => key.fingerprint)

const didsForServices = await didRepository.findByQuery(agentContext, {
role: DidDocumentRole.Created,

// We want an $or query so we query for each key individually, not one did document
// containing exactly the same keys as the did document we are trying to create
$or: recipientKeyFingerprints.map((fingerprint) => ({
recipientKeyFingerprints: [fingerprint],
})),
})

if (didsForServices.length > 0) {
const allDidRecipientKeys = didsForServices.flatMap((did) => did.getTags().recipientKeyFingerprints ?? [])
const matchingFingerprints = allDidRecipientKeys.filter((f) => recipientKeyFingerprints.includes(f))
throw new CredoError(
`A did already exists for some of the keys in the provided services. DIDComm v1 uses key based routing, and therefore it is not allowed to re-use the same key in multiple did documents for DIDComm. If you use the same 'routing' object for multiple invitations, instead provide an 'invitationDid' to the create invitation method. The following fingerprints are already in use: ${matchingFingerprints.join(
','
)}`
)
}
}

export async function createPeerDidFromServices(
agentContext: AgentContext,
services: ResolvedDidCommService[],
Expand All @@ -148,8 +179,11 @@ export async function createPeerDidFromServices(

// Create did document without the id property
const didDocument = createPeerDidDocumentFromServices(services)
// Register did:peer document. This will generate the id property and save it to a did record

// Assert that the keys we are going to use for creating a did document haven't already been used in another did document
await assertNoCreatedDidExistsForKeys(agentContext, didDocument.recipientKeys)

// Register did:peer document. This will generate the id property and save it to a did record
const result = await didsApi.create({
method: 'peer',
didDocument,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { JsonTransformer } from '../../../../../utils'
import { OutOfBandDidCommService } from '../../../../oob/domain/OutOfBandDidCommService'
import { DidDocument } from '../../../domain'
import { didToNumAlgo2DidDocument, didDocumentToNumAlgo2Did, outOfBandServiceToNumAlgo2Did } from '../peerDidNumAlgo2'
import {
didToNumAlgo2DidDocument,
didDocumentToNumAlgo2Did,
outOfBandServiceToNumAlgo2Did,
outOfBandServiceToInlineKeysNumAlgo2Did,
} from '../peerDidNumAlgo2'

import didPeer2Ez6L from './__fixtures__/didPeer2Ez6L.json'
import didPeer2Ez6LMoreServices from './__fixtures__/didPeer2Ez6LMoreServices.json'
Expand Down Expand Up @@ -44,6 +49,24 @@ describe('peerDidNumAlgo2', () => {
const peerDid = outOfBandServiceToNumAlgo2Did(service)
const peerDidDocument = didToNumAlgo2DidDocument(peerDid)

expect(peerDid).toBe(
'did:peer:2.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Ez6LSpSrLxbAhg2SHwKk7kwpsH7DM7QjFS5iK6qP87eViohud.SeyJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbIiNrZXktMSJdLCJyIjpbImRpZDprZXk6ejZNa3BUSFI4Vk5zQnhZQUFXSHV0MkdlYWRkOWpTd3VCVjh4Um9BbndXc2R2a3RII3o2TWtwVEhSOFZOc0J4WUFBV0h1dDJHZWFkZDlqU3d1QlY4eFJvQW53V3Nkdmt0SCJdfQ'
)
expect(peerDid).toBe(peerDidDocument.id)
})
})

describe('outOfBandServiceInlineKeysToNumAlgo2Did', () => {
test('transforms a did comm service into a valid method 2 did', () => {
const service = new OutOfBandDidCommService({
id: '#service-0',
serviceEndpoint: 'https://example.com/endpoint',
recipientKeys: ['did:key:z6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V'],
routingKeys: ['did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH'],
accept: ['didcomm/v2', 'didcomm/aip2;env=rfc587'],
})
const peerDid = outOfBandServiceToInlineKeysNumAlgo2Did(service)
const peerDidDocument = didToNumAlgo2DidDocument(peerDid)
expect(peerDid).toBe(
'did:peer:2.SeyJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbImRpZDprZXk6ejZNa3FSWXFRaVNndlpRZG5CeXR3ODZRYnMyWldVa0d2MjJvZDkzNVlGNHM4TTdWI3o2TWtxUllxUWlTZ3ZaUWRuQnl0dzg2UWJzMlpXVWtHdjIyb2Q5MzVZRjRzOE03ViJdLCJyIjpbImRpZDprZXk6ejZNa3BUSFI4Vk5zQnhZQUFXSHV0MkdlYWRkOWpTd3VCVjh4Um9BbndXc2R2a3RII3o2TWtwVEhSOFZOc0J4WUFBV0h1dDJHZWFkZDlqU3d1QlY4eFJvQW53V3Nkdmt0SCJdLCJhIjpbImRpZGNvbW0vdjIiLCJkaWRjb21tL2FpcDI7ZW52PXJmYzU4NyJdfQ'
)
Expand Down
23 changes: 21 additions & 2 deletions packages/core/src/modules/dids/methods/peer/peerDidNumAlgo2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ import { DidCommV1Service, DidDocumentService } from '../../domain'
import { DidDocumentBuilder } from '../../domain/DidDocumentBuilder'
import { getKeyFromVerificationMethod, getKeyDidMappingByKeyType } from '../../domain/key-type'
import { parseDid } from '../../domain/parse'
import { didKeyToInstanceOfKey } from '../../helpers'
import { DidKey } from '../key'

import { createPeerDidDocumentFromServices } from './createPeerDidDocumentFromServices'

enum DidPeerPurpose {
Assertion = 'A',
Encryption = 'E',
Expand Down Expand Up @@ -163,14 +166,30 @@ export function didDocumentToNumAlgo2Did(didDocument: DidDocument) {
}

export function outOfBandServiceToNumAlgo2Did(service: OutOfBandDidCommService) {
// FIXME: add the key entries for the recipientKeys to the did document.
const didDocument = createPeerDidDocumentFromServices([
{
id: service.id,
recipientKeys: service.recipientKeys.map(didKeyToInstanceOfKey),
serviceEndpoint: service.serviceEndpoint,
routingKeys: service.routingKeys?.map(didKeyToInstanceOfKey) ?? [],
},
])

const did = didDocumentToNumAlgo2Did(didDocument)

return did
}

// This method is kept to support searching for existing connections created by
// credo-ts <= 0.5.1
// TODO: Remove in 0.6.0 (when ConnectionRecord.invitationDid will be migrated)
export function outOfBandServiceToInlineKeysNumAlgo2Did(service: OutOfBandDidCommService) {
const didDocument = new DidDocumentBuilder('')
.addService(
new DidCommV1Service({
id: service.id,
serviceEndpoint: service.serviceEndpoint,
accept: service.accept,
// FIXME: this should actually be local key references, not did:key:123#456 references
recipientKeys: service.recipientKeys.map((recipientKey) => {
const did = DidKey.fromDid(recipientKey)
return `${did.did}#${did.key.fingerprint}`
Expand Down
Loading

0 comments on commit e5c6698

Please sign in to comment.