diff --git a/packages/core/src/modules/connections/ConnectionsModuleConfig.ts b/packages/core/src/modules/connections/ConnectionsModuleConfig.ts index 59aaf8cb5b..e3bd0c0408 100644 --- a/packages/core/src/modules/connections/ConnectionsModuleConfig.ts +++ b/packages/core/src/modules/connections/ConnectionsModuleConfig.ts @@ -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 diff --git a/packages/core/src/modules/connections/services/ConnectionService.ts b/packages/core/src/modules/connections/services/ConnectionService.ts index 2d2b433a24..05ec71bc77 100644 --- a/packages/core/src/modules/connections/services/ConnectionService.ts +++ b/packages/core/src/modules/connections/services/ConnectionService.ts @@ -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' @@ -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( diff --git a/packages/core/src/modules/didcomm/services/DidCommDocumentService.ts b/packages/core/src/modules/didcomm/services/DidCommDocumentService.ts index 43b517f9a4..ca1b1a88d4 100644 --- a/packages/core/src/modules/didcomm/services/DidCommDocumentService.ts +++ b/packages/core/src/modules/didcomm/services/DidCommDocumentService.ts @@ -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' @@ -19,14 +19,19 @@ export class DidCommDocumentService { public async resolveServicesFromDid(agentContext: AgentContext, did: string): Promise { 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) || [], @@ -54,7 +59,7 @@ export class DidCommDocumentService { return key }) - didCommServices.push({ + resolvedServices.push({ id: didCommService.id, recipientKeys, routingKeys, @@ -63,6 +68,6 @@ export class DidCommDocumentService { } } - return didCommServices + return resolvedServices } } diff --git a/packages/core/src/modules/didcomm/services/__tests__/DidCommDocumentService.test.ts b/packages/core/src/modules/didcomm/services/__tests__/DidCommDocumentService.test.ts index 8019274c46..422db7f2a5 100644 --- a/packages/core/src/modules/didcomm/services/__tests__/DidCommDocumentService.test.ts +++ b/packages/core/src/modules/didcomm/services/__tests__/DidCommDocumentService.test.ts @@ -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], + }) + }) }) }) diff --git a/packages/core/src/modules/oob/OutOfBandApi.ts b/packages/core/src/modules/oob/OutOfBandApi.ts index 08e1609d8a..b6f48b97e2 100644 --- a/packages/core/src/modules/oob/OutOfBandApi.ts +++ b/packages/core/src/modules/oob/OutOfBandApi.ts @@ -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, }) @@ -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 } diff --git a/packages/core/src/modules/oob/__tests__/implicit.e2e.test.ts b/packages/core/src/modules/oob/__tests__/implicit.e2e.test.ts index 846a139a87..2453b55e6c 100644 --- a/packages/core/src/modules/oob/__tests__/implicit.e2e.test.ts +++ b/packages/core/src/modules/oob/__tests__/implicit.e2e.test.ts @@ -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], @@ -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], @@ -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 () => { @@ -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], @@ -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], @@ -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]) @@ -212,5 +244,5 @@ async function createPublicDid(agent: Agent, unqualifiedSubmitterDid: string, en await sleep(1000) - return createResult.didState.did + return createResult.didState }