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

fix: implicit invitation to specific service #1592

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
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ export interface ConnectionsModuleConfigOptions {
* Whether to automatically accept connection messages. Applies to both the connection protocol (RFC 0160)
* and the DID exchange protocol (RFC 0023).
*
* Note: this setting does not apply to implicit invitation flows, which always need to be manually accepted
* using ConnectionStateChangedEvent
*
* @default false
*/
autoAcceptConnections?: boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { DidRecordMetadataKeys } from '../../dids/repository/didRecordMetadataTy
import { OutOfBandService } from '../../oob/OutOfBandService'
import { OutOfBandRole } from '../../oob/domain/OutOfBandRole'
import { OutOfBandState } from '../../oob/domain/OutOfBandState'
import { InvitationType } from '../../oob/messages'
import { OutOfBandRepository } from '../../oob/repository'
import { OutOfBandRecordMetadataKeys } from '../../oob/repository/outOfBandRecordMetadataTypes'
import { ConnectionEventTypes } from '../ConnectionEvents'
Expand Down Expand Up @@ -579,7 +580,7 @@ export class ConnectionService {

// If the original invitation was a legacy connectionless invitation, it's okay if the message does not have a pthid.
if (
legacyInvitationMetadata?.legacyInvitationType !== 'connectionless' &&
legacyInvitationMetadata?.legacyInvitationType !== InvitationType.Connectionless &&
outOfBandRecord.outOfBandInvitation.id !== outOfBandInvitationId
) {
throw new AriesFrameworkError(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { ResolvedDidCommService } from '../types'
import { KeyType } from '../../../crypto'
import { injectable } from '../../../plugins'
import { DidResolverService } from '../../dids'
import { DidCommV1Service, IndyAgentService, keyReferenceToKey } from '../../dids/domain'
import { DidCommV1Service, IndyAgentService, keyReferenceToKey, parseDid } from '../../dids/domain'
import { verkeyToInstanceOfKey } from '../../dids/helpers'
import { findMatchingEd25519Key } from '../util/matchingEd25519Key'

Expand All @@ -19,14 +19,19 @@ export class DidCommDocumentService {
public async resolveServicesFromDid(agentContext: AgentContext, did: string): Promise<ResolvedDidCommService[]> {
const didDocument = await this.didResolverService.resolveDidDocument(agentContext, did)

const didCommServices: ResolvedDidCommService[] = []
const resolvedServices: ResolvedDidCommService[] = []

// If did specifies a particular service, filter by its id
const didCommServices = parseDid(did).fragment
? didDocument.didCommServices.filter((service) => service.id === did)
: didDocument.didCommServices

// 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) {
for (const didCommService of didCommServices) {
if (didCommService instanceof IndyAgentService) {
// IndyAgentService (DidComm v0) has keys encoded as raw publicKeyBase58 (verkeys)
didCommServices.push({
resolvedServices.push({
id: didCommService.id,
recipientKeys: didCommService.recipientKeys.map(verkeyToInstanceOfKey),
routingKeys: didCommService.routingKeys?.map(verkeyToInstanceOfKey) || [],
Expand Down Expand Up @@ -54,7 +59,7 @@ export class DidCommDocumentService {
return key
})

didCommServices.push({
resolvedServices.push({
id: didCommService.id,
recipientKeys,
routingKeys,
Expand All @@ -63,6 +68,6 @@ export class DidCommDocumentService {
}
}

return didCommServices
return resolvedServices
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,5 +117,89 @@ describe('DidCommDocumentService', () => {
routingKeys: [ed25519Key],
})
})

test('resolves specific 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: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h#test-id',
serviceEndpoint: 'https://test.com',
recipientKeys: [X25519VerificationMethod.id],
routingKeys: [Ed25519VerificationMethod.id],
priority: 5,
}),
new DidCommV1Service({
id: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h#test-id-2',
serviceEndpoint: 'wss://test.com',
recipientKeys: [X25519VerificationMethod.id],
routingKeys: [Ed25519VerificationMethod.id],
priority: 6,
}),
],
})
)

let resolved = await didCommDocumentService.resolveServicesFromDid(
agentContext,
'did:sov:Q4zqM7aXqm7gDQkUVLng9h#test-id'
)
expect(didResolverService.resolveDidDocument).toHaveBeenCalledWith(
agentContext,
'did:sov:Q4zqM7aXqm7gDQkUVLng9h#test-id'
)

let ed25519Key = Key.fromPublicKeyBase58(publicKeyBase58Ed25519, KeyType.Ed25519)
expect(resolved).toHaveLength(1)
expect(resolved[0]).toMatchObject({
id: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h#test-id',
serviceEndpoint: 'https://test.com',
recipientKeys: [ed25519Key],
routingKeys: [ed25519Key],
})

resolved = await didCommDocumentService.resolveServicesFromDid(
agentContext,
'did:sov:Q4zqM7aXqm7gDQkUVLng9h#test-id-2'
)
expect(didResolverService.resolveDidDocument).toHaveBeenCalledWith(
agentContext,
'did:sov:Q4zqM7aXqm7gDQkUVLng9h#test-id-2'
)

ed25519Key = Key.fromPublicKeyBase58(publicKeyBase58Ed25519, KeyType.Ed25519)
expect(resolved).toHaveLength(1)
expect(resolved[0]).toMatchObject({
id: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h#test-id-2',
serviceEndpoint: 'wss://test.com',
recipientKeys: [ed25519Key],
routingKeys: [ed25519Key],
})
})
})
})
4 changes: 2 additions & 2 deletions packages/core/src/modules/oob/OutOfBandApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,7 @@ export class OutOfBandApi {
}

// If the invitation was converted from another legacy format, we store this, as its needed for some flows
if (outOfBandInvitation.invitationType && outOfBandInvitation.invitationType !== 'out-of-band/1.x') {
if (outOfBandInvitation.invitationType && outOfBandInvitation.invitationType !== InvitationType.OutOfBand) {
outOfBandRecord.metadata.set(OutOfBandRecordMetadataKeys.LegacyInvitation, {
legacyInvitationType: outOfBandInvitation.invitationType,
})
Expand Down Expand Up @@ -838,7 +838,7 @@ export class OutOfBandApi {

// If the invitation is created from a legacy connectionless invitation, we don't need to set the pthid
// as that's not expected, and it's generated on our side only
if (legacyInvitationMetadata?.legacyInvitationType === 'connectionless') {
if (legacyInvitationMetadata?.legacyInvitationType === InvitationType.Connectionless) {
return
}

Expand Down
60 changes: 46 additions & 14 deletions packages/core/src/modules/oob/__tests__/implicit.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,42 @@ describe('out of band implicit', () => {

test(`make a connection with ${HandshakeProtocol.DidExchange} based on implicit OOB invitation`, async () => {
const publicDid = await createPublicDid(faberAgent, unqualifiedSubmitterDid, 'rxjs:faber')
expect(publicDid).toBeDefined()
expect(publicDid.did).toBeDefined()

let { connectionRecord: aliceFaberConnection } = await aliceAgent.oob.receiveImplicitInvitation({
did: publicDid.did!,
alias: 'Faber public',
label: 'Alice',
handshakeProtocols: [HandshakeProtocol.DidExchange],
})

// Wait for a connection event in faber agent and accept the request
let faberAliceConnection = await waitForConnectionRecord(faberAgent, { state: DidExchangeState.RequestReceived })
await faberAgent.connections.acceptRequest(faberAliceConnection.id)
faberAliceConnection = await faberAgent.connections.returnWhenIsConnected(faberAliceConnection!.id)
expect(faberAliceConnection.state).toBe(DidExchangeState.Completed)

// Alice should now be connected
aliceFaberConnection = await aliceAgent.connections.returnWhenIsConnected(aliceFaberConnection!.id)
expect(aliceFaberConnection.state).toBe(DidExchangeState.Completed)

expect(aliceFaberConnection).toBeConnectedWith(faberAliceConnection)
expect(faberAliceConnection).toBeConnectedWith(aliceFaberConnection)
expect(faberAliceConnection.theirLabel).toBe('Alice')
expect(aliceFaberConnection.alias).toBe('Faber public')
expect(aliceFaberConnection.invitationDid).toBe(publicDid.did!)

// It is possible for an agent to check if it has already a connection to a certain public entity
expect(await aliceAgent.connections.findByInvitationDid(publicDid.did!)).toEqual([aliceFaberConnection])
})

test(`make a connection with ${HandshakeProtocol.DidExchange} based on implicit OOB invitation pointing to specific service`, async () => {
const publicDid = await createPublicDid(faberAgent, unqualifiedSubmitterDid, 'rxjs:faber')
expect(publicDid.did).toBeDefined()

const serviceDidUrl = publicDid.didDocument?.didCommServices[0].id
let { connectionRecord: aliceFaberConnection } = await aliceAgent.oob.receiveImplicitInvitation({
did: publicDid!,
did: serviceDidUrl!,
alias: 'Faber public',
label: 'Alice',
handshakeProtocols: [HandshakeProtocol.DidExchange],
Expand All @@ -89,18 +121,18 @@ describe('out of band implicit', () => {
expect(faberAliceConnection).toBeConnectedWith(aliceFaberConnection)
expect(faberAliceConnection.theirLabel).toBe('Alice')
expect(aliceFaberConnection.alias).toBe('Faber public')
expect(aliceFaberConnection.invitationDid).toBe(publicDid)
expect(aliceFaberConnection.invitationDid).toBe(serviceDidUrl)

// It is possible for an agent to check if it has already a connection to a certain public entity
expect(await aliceAgent.connections.findByInvitationDid(publicDid!)).toEqual([aliceFaberConnection])
expect(await aliceAgent.connections.findByInvitationDid(serviceDidUrl!)).toEqual([aliceFaberConnection])
})

test(`make a connection with ${HandshakeProtocol.Connections} based on implicit OOB invitation`, async () => {
const publicDid = await createPublicDid(faberAgent, unqualifiedSubmitterDid, 'rxjs:faber')
expect(publicDid).toBeDefined()
expect(publicDid.did).toBeDefined()

let { connectionRecord: aliceFaberConnection } = await aliceAgent.oob.receiveImplicitInvitation({
did: publicDid!,
did: publicDid.did!,
alias: 'Faber public',
label: 'Alice',
handshakeProtocols: [HandshakeProtocol.Connections],
Expand All @@ -120,10 +152,10 @@ describe('out of band implicit', () => {
expect(faberAliceConnection).toBeConnectedWith(aliceFaberConnection)
expect(faberAliceConnection.theirLabel).toBe('Alice')
expect(aliceFaberConnection.alias).toBe('Faber public')
expect(aliceFaberConnection.invitationDid).toBe(publicDid)
expect(aliceFaberConnection.invitationDid).toBe(publicDid.did!)

// It is possible for an agent to check if it has already a connection to a certain public entity
expect(await aliceAgent.connections.findByInvitationDid(publicDid!)).toEqual([aliceFaberConnection])
expect(await aliceAgent.connections.findByInvitationDid(publicDid.did!)).toEqual([aliceFaberConnection])
})

test(`receive an implicit invitation using an unresolvable did`, async () => {
Expand All @@ -142,7 +174,7 @@ describe('out of band implicit', () => {
expect(publicDid).toBeDefined()

let { connectionRecord: aliceFaberConnection } = await aliceAgent.oob.receiveImplicitInvitation({
did: publicDid!,
did: publicDid.did!,
alias: 'Faber public',
label: 'Alice',
handshakeProtocols: [HandshakeProtocol.Connections],
Expand All @@ -162,11 +194,11 @@ describe('out of band implicit', () => {
expect(faberAliceConnection).toBeConnectedWith(aliceFaberConnection)
expect(faberAliceConnection.theirLabel).toBe('Alice')
expect(aliceFaberConnection.alias).toBe('Faber public')
expect(aliceFaberConnection.invitationDid).toBe(publicDid)
expect(aliceFaberConnection.invitationDid).toBe(publicDid.did)

// Repeat implicit invitation procedure
let { connectionRecord: aliceFaberNewConnection } = await aliceAgent.oob.receiveImplicitInvitation({
did: publicDid!,
did: publicDid.did!,
alias: 'Faber public New',
label: 'Alice New',
handshakeProtocols: [HandshakeProtocol.Connections],
Expand All @@ -186,10 +218,10 @@ describe('out of band implicit', () => {
expect(faberAliceNewConnection).toBeConnectedWith(aliceFaberNewConnection)
expect(faberAliceNewConnection.theirLabel).toBe('Alice New')
expect(aliceFaberNewConnection.alias).toBe('Faber public New')
expect(aliceFaberNewConnection.invitationDid).toBe(publicDid)
expect(aliceFaberNewConnection.invitationDid).toBe(publicDid.did)

// Both connections will be associated to the same invitation did
const connectionsFromFaberPublicDid = await aliceAgent.connections.findByInvitationDid(publicDid!)
const connectionsFromFaberPublicDid = await aliceAgent.connections.findByInvitationDid(publicDid.did!)
expect(connectionsFromFaberPublicDid).toHaveLength(2)
expect(connectionsFromFaberPublicDid).toEqual(
expect.arrayContaining([aliceFaberConnection, aliceFaberNewConnection])
Expand All @@ -212,5 +244,5 @@ async function createPublicDid(agent: Agent, unqualifiedSubmitterDid: string, en

await sleep(1000)

return createResult.didState.did
return createResult.didState
}
Loading