Skip to content

Commit

Permalink
fix: implicit invitation to specific service (openwallet-foundation#1592
Browse files Browse the repository at this point in the history
)

Signed-off-by: Ariel Gentile <[email protected]>
Signed-off-by: Martin Auer <[email protected]>
  • Loading branch information
genaris authored and auer-martin committed Dec 4, 2023
1 parent d385763 commit 2c4bc61
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 23 deletions.
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
}

0 comments on commit 2c4bc61

Please sign in to comment.