Repository: hyperledger/aries-cloudagent-python

Issue:
How are connection and did exchange response signatures verified against the invitation recipient keys?
I'm currently implementing signed attachment in AFJ for use in DIDExchange. I'm leaning heavily on the aca-py implementation, but can't figure out where the response message diddoc signature verkeys are being matched against the invitation recipient keys.

For the connection protocol looking at the signature decorator [`verify` method](https://github.com/hyperledger/aries-cloudagent-python/blob/7024c5abf0a6b8b1fe84a9c2c092d6d5492cbbe3/aries_cloudagent/messaging/decorators/signature_decorator.py#L96) is only called [in one method](https://github.com/hyperledger/aries-cloudagent-python/blob/main/aries_cloudagent/messaging/agent_message.py#L202), but that method is not called anywhere else. This leads me to assume the connection implementation in ACA-Py doesn't check the invitation key against the signature decorator verkey.

The connection protocol mentions:

> The signature data must be used to verify against the invitation's recipientKeys for continuity.

For the DIDExchange protocol the attach decorator has a [`verify` method](https://github.com/hyperledger/aries-cloudagent-python/blob/7024c5abf0a6b8b1fe84a9c2c092d6d5492cbbe3/aries_cloudagent/messaging/decorators/attach_decorator.py#L417) which is called in the DIDExchange manager twice ([1](https://github.com/hyperledger/aries-cloudagent-python/blob/7024c5abf0a6b8b1fe84a9c2c092d6d5492cbbe3/aries_cloudagent/protocols/didexchange/v1_0/manager.py#L471), [2](https://github.com/hyperledger/aries-cloudagent-python/blob/7024c5abf0a6b8b1fe84a9c2c092d6d5492cbbe3/aries_cloudagent/protocols/didexchange/v1_0/manager.py#L843)), but I can't find where the invitation key is verified against the signed attachment diddoc verkey

Although I wasn't able to find a statement mentioning the didexchange response MUST be checked against the invitation's `recipientKeys`, I think it could lead to security issues if this isn't checked. (someone else could send a connection response with a different verkey, and sign the diddoc with a different key than used in the invitation)

Maybe someone can help me understand the ACA-Py implementation better to make sure we can correctly implement this in AFJ 

cc @andrewwhitehead, @shaangill025, @swcurran


Discussion:
@shaangill025 -- once you have a break in the persistent queues work, please look at this.

Resolving patch:
diff --git a/aries_cloudagent/messaging/decorators/attach_decorator.py b/aries_cloudagent/messaging/decorators/attach_decorator.py
--- a/aries_cloudagent/messaging/decorators/attach_decorator.py
+++ b/aries_cloudagent/messaging/decorators/attach_decorator.py
@@ -414,7 +414,7 @@ def build_protected(verkey: str):
                 )
             self.jws_ = AttachDecoratorDataJWS.deserialize(jws)
 
-    async def verify(self, wallet: BaseWallet) -> bool:
+    async def verify(self, wallet: BaseWallet, signer_verkey: str = None) -> bool:
         """
         Verify the signature(s).
 
@@ -428,7 +428,7 @@ async def verify(self, wallet: BaseWallet) -> bool:
         assert self.jws
 
         b64_payload = unpad(set_urlsafe_b64(self.base64, True))
-
+        verkey_to_check = []
         for sig in [self.jws] if self.signatures == 1 else self.jws.signatures:
             b64_protected = sig.protected
             b64_sig = sig.signature
@@ -438,10 +438,18 @@ async def verify(self, wallet: BaseWallet) -> bool:
             sign_input = (b64_protected + "." + b64_payload).encode("ascii")
             b_sig = b64_to_bytes(b64_sig, urlsafe=True)
             verkey = bytes_to_b58(b64_to_bytes(protected["jwk"]["x"], urlsafe=True))
+            encoded_pk = DIDKey.from_did(protected["jwk"]["kid"]).public_key_b58
+            verkey_to_check.append(encoded_pk)
             if not await wallet.verify_message(
                 sign_input, b_sig, verkey, KeyType.ED25519
             ):
                 return False
+            if not await wallet.verify_message(
+                sign_input, b_sig, encoded_pk, KeyType.ED25519
+            ):
+                return False
+        if signer_verkey and signer_verkey not in verkey_to_check:
+            return False
         return True
 
     def __eq__(self, other):
diff --git a/aries_cloudagent/protocols/connections/v1_0/manager.py b/aries_cloudagent/protocols/connections/v1_0/manager.py
--- a/aries_cloudagent/protocols/connections/v1_0/manager.py
+++ b/aries_cloudagent/protocols/connections/v1_0/manager.py
@@ -835,6 +835,17 @@ async def accept_response(
             )
         if their_did != conn_did_doc.did:
             raise ConnectionManagerError("Connection DID does not match DIDDoc id")
+        # Verify connection response using connection field
+        async with self.profile.session() as session:
+            wallet = session.inject(BaseWallet)
+            try:
+                await response.verify_signed_field(
+                    "connection", wallet, connection.invitation_key
+                )
+            except ValueError:
+                raise ConnectionManagerError(
+                    "connection field verification using invitation_key failed"
+                )
         await self.store_did_document(conn_did_doc)
 
         connection.their_did = their_did
diff --git a/aries_cloudagent/protocols/didexchange/v1_0/manager.py b/aries_cloudagent/protocols/didexchange/v1_0/manager.py
--- a/aries_cloudagent/protocols/didexchange/v1_0/manager.py
+++ b/aries_cloudagent/protocols/didexchange/v1_0/manager.py
@@ -2,6 +2,9 @@
 
 import json
 import logging
+import pydid
+
+from pydid import BaseDIDDocument as ResolvedDocument, DIDCommService
 
 from ....connections.models.conn_record import ConnRecord
 from ....connections.models.diddoc import DIDDoc
@@ -12,6 +15,8 @@
 from ....messaging.decorators.attach_decorator import AttachDecorator
 from ....messaging.responder import BaseResponder
 from ....multitenant.base import BaseMultitenantManager
+from ....resolver.base import ResolverError
+from ....resolver.did_resolver import DIDResolver
 from ....storage.error import StorageNotFoundError
 from ....transport.inbound.receipt import MessageReceipt
 from ....wallet.base import BaseWallet
@@ -141,7 +146,13 @@ async def receive_invitation(
 
             # Save the invitation for later processing
             await conn_rec.attach_invitation(session, invitation)
-
+            if not conn_rec.invitation_key and conn_rec.their_public_did:
+                did_document = await self.get_resolved_did_document(
+                    conn_rec.their_public_did
+                )
+                conn_rec.invitation_key = did_document.verification_method[
+                    0
+                ].public_key_base58
         if conn_rec.accept == ConnRecord.ACCEPT_AUTO:
             request = await self.create_request(conn_rec, mediation_id=mediation_id)
             responder = self.profile.inject_or(BaseResponder)
@@ -296,14 +307,11 @@ async def create_request(
                 filter(None, [base_mediation_record, mediation_record])
             ),
         )
-        if (
-            conn_rec.their_public_did is not None
-            and conn_rec.their_public_did.startswith("did:")
-        ):
+        if conn_rec.their_public_did is not None:
             qualified_did = conn_rec.their_public_did
-        else:
-            qualified_did = f"did:sov:{conn_rec.their_public_did}"
-        pthid = conn_rec.invitation_msg_id or qualified_did
+            did_document = await self.get_resolved_did_document(qualified_did)
+            did_url = await self.get_first_applicable_didcomm_service(did_document)
+        pthid = conn_rec.invitation_msg_id or did_url
         attach = AttachDecorator.data_base64(did_doc.serialize())
         async with self.profile.session() as session:
             wallet = session.inject(BaseWallet)
@@ -468,9 +476,7 @@ async def receive_request(
             )
         async with self.profile.session() as session:
             wallet = session.inject(BaseWallet)
-            if not await request.did_doc_attach.data.verify(wallet):
-                raise DIDXManagerError("DID Doc signature failed verification")
-        conn_did_doc = DIDDoc.from_json(request.did_doc_attach.data.signed.decode())
+            conn_did_doc = await self.verify_diddoc(wallet, request.did_doc_attach)
         if request.did != conn_did_doc.did:
             raise DIDXManagerError(
                 (
@@ -751,7 +757,9 @@ async def accept_response(
             raise DIDXManagerError("No DIDDoc attached; cannot connect to public DID")
         async with self.profile.session() as session:
             wallet = session.inject(BaseWallet)
-            conn_did_doc = await self.verify_diddoc(wallet, response.did_doc_attach)
+            conn_did_doc = await self.verify_diddoc(
+                wallet, response.did_doc_attach, conn_rec.invitation_key
+            )
         if their_did != conn_did_doc.did:
             raise DIDXManagerError(
                 f"Connection DID {their_did} "
@@ -861,12 +869,53 @@ async def verify_diddoc(
         self,
         wallet: BaseWallet,
         attached: AttachDecorator,
+        invi_key: str = None,
     ) -> DIDDoc:
         """Verify DIDDoc attachment and return signed data."""
         signed_diddoc_bytes = attached.data.signed
         if not signed_diddoc_bytes:
             raise DIDXManagerError("DID doc attachment is not signed.")
-        if not await attached.data.verify(wallet):
+        if not await attached.data.verify(wallet, invi_key):
             raise DIDXManagerError("DID doc attachment signature failed verification")
 
         return DIDDoc.deserialize(json.loads(signed_diddoc_bytes.decode()))
+
+    async def get_resolved_did_document(self, qualified_did: str) -> ResolvedDocument:
+        """Return resolved DID document."""
+        resolver = self._profile.inject(DIDResolver)
+        if not qualified_did.startswith("did:"):
+            qualified_did = f"did:sov:{qualified_did}"
+        try:
+            doc_dict: dict = await resolver.resolve(self._profile, qualified_did)
+            doc = pydid.deserialize_document(doc_dict, strict=True)
+            return doc
+        except ResolverError as error:
+            raise DIDXManagerError(
+                "Failed to resolve public DID in invitation"
+            ) from error
+
+    async def get_first_applicable_didcomm_service(
+        self, did_doc: ResolvedDocument
+    ) -> str:
+        """Return first applicable DIDComm service url with highest priority."""
+        if not did_doc.service:
+            raise DIDXManagerError(
+                "Cannot connect via public DID that has no associated services"
+            )
+
+        didcomm_services = sorted(
+            [
+                service
+                for service in did_doc.service
+                if isinstance(service, DIDCommService)
+            ],
+            key=lambda service: service.priority,
+        )
+
+        if not didcomm_services:
+            raise DIDXManagerError(
+                "Cannot connect via public DID that has no associated DIDComm services"
+            )
+
+        first_didcomm_service, *_ = didcomm_services
+        return first_didcomm_service.id


Test patch:
diff --git a/aries_cloudagent/messaging/decorators/tests/test_attach_decorator.py b/aries_cloudagent/messaging/decorators/tests/test_attach_decorator.py
--- a/aries_cloudagent/messaging/decorators/tests/test_attach_decorator.py
+++ b/aries_cloudagent/messaging/decorators/tests/test_attach_decorator.py
@@ -475,6 +475,7 @@ async def test_indy_sign(self, wallet, seed):
         assert deco_indy.data.header_map()["kid"] == did_key(did_info[0].verkey)
         assert deco_indy.data.header_map()["jwk"]["kid"] == did_key(did_info[0].verkey)
         assert await deco_indy.data.verify(wallet)
+        assert await deco_indy.data.verify(wallet, did_info[0].verkey)
 
         indy_cred = json.loads(deco_indy.data.signed.decode())
         assert indy_cred == INDY_CRED
diff --git a/aries_cloudagent/protocols/connections/v1_0/tests/test_manager.py b/aries_cloudagent/protocols/connections/v1_0/tests/test_manager.py
--- a/aries_cloudagent/protocols/connections/v1_0/tests/test_manager.py
+++ b/aries_cloudagent/protocols/connections/v1_0/tests/test_manager.py
@@ -1277,7 +1277,9 @@ async def test_accept_response_find_by_thread_id(self):
         mock_response.connection.did = self.test_target_did
         mock_response.connection.did_doc = async_mock.MagicMock()
         mock_response.connection.did_doc.did = self.test_target_did
-
+        mock_response.verify_signed_field = async_mock.CoroutineMock(
+            return_value="sig_verkey"
+        )
         receipt = MessageReceipt(recipient_did=self.test_did, recipient_did_public=True)
 
         with async_mock.patch.object(
@@ -1294,6 +1296,7 @@ async def test_accept_response_find_by_thread_id(self):
                 save=async_mock.CoroutineMock(),
                 metadata_get=async_mock.CoroutineMock(),
                 connection_id="test-conn-id",
+                invitation_key="test-invitation-key",
             )
             conn_rec = await self.manager.accept_response(mock_response, receipt)
             assert conn_rec.their_did == self.test_target_did
@@ -1306,6 +1309,9 @@ async def test_accept_response_not_found_by_thread_id_receipt_has_sender_did(sel
         mock_response.connection.did = self.test_target_did
         mock_response.connection.did_doc = async_mock.MagicMock()
         mock_response.connection.did_doc.did = self.test_target_did
+        mock_response.verify_signed_field = async_mock.CoroutineMock(
+            return_value="sig_verkey"
+        )
 
         receipt = MessageReceipt(sender_did=self.test_target_did)
 
@@ -1326,6 +1332,7 @@ async def test_accept_response_not_found_by_thread_id_receipt_has_sender_did(sel
                 save=async_mock.CoroutineMock(),
                 metadata_get=async_mock.CoroutineMock(return_value=False),
                 connection_id="test-conn-id",
+                invitation_key="test-invitation-id",
             )
 
             conn_rec = await self.manager.accept_response(mock_response, receipt)
@@ -1426,14 +1433,47 @@ async def test_accept_response_find_by_thread_id_did_mismatch(self):
             with self.assertRaises(ConnectionManagerError):
                 await self.manager.accept_response(mock_response, receipt)
 
-    async def test_accept_response_auto_send_mediation_request(self):
+    async def test_accept_response_verify_invitation_key_sign_failure(self):
         mock_response = async_mock.MagicMock()
         mock_response._thread = async_mock.MagicMock()
         mock_response.connection = async_mock.MagicMock()
         mock_response.connection.did = self.test_target_did
         mock_response.connection.did_doc = async_mock.MagicMock()
         mock_response.connection.did_doc.did = self.test_target_did
+        mock_response.verify_signed_field = async_mock.CoroutineMock(
+            side_effect=ValueError
+        )
+        receipt = MessageReceipt(recipient_did=self.test_did, recipient_did_public=True)
+
+        with async_mock.patch.object(
+            ConnRecord, "save", autospec=True
+        ) as mock_conn_rec_save, async_mock.patch.object(
+            ConnRecord, "retrieve_by_request_id", async_mock.CoroutineMock()
+        ) as mock_conn_retrieve_by_req_id, async_mock.patch.object(
+            MediationManager, "get_default_mediator", async_mock.CoroutineMock()
+        ):
+            mock_conn_retrieve_by_req_id.return_value = async_mock.MagicMock(
+                did=self.test_target_did,
+                did_doc=async_mock.MagicMock(did=self.test_target_did),
+                state=ConnRecord.State.RESPONSE.rfc23,
+                save=async_mock.CoroutineMock(),
+                metadata_get=async_mock.CoroutineMock(),
+                connection_id="test-conn-id",
+                invitation_key="test-invitation-key",
+            )
+            with self.assertRaises(ConnectionManagerError):
+                await self.manager.accept_response(mock_response, receipt)
 
+    async def test_accept_response_auto_send_mediation_request(self):
+        mock_response = async_mock.MagicMock()
+        mock_response._thread = async_mock.MagicMock()
+        mock_response.connection = async_mock.MagicMock()
+        mock_response.connection.did = self.test_target_did
+        mock_response.connection.did_doc = async_mock.MagicMock()
+        mock_response.connection.did_doc.did = self.test_target_did
+        mock_response.verify_signed_field = async_mock.CoroutineMock(
+            return_value="sig_verkey"
+        )
         receipt = MessageReceipt(recipient_did=self.test_did, recipient_did_public=True)
 
         with async_mock.patch.object(
@@ -1450,6 +1490,7 @@ async def test_accept_response_auto_send_mediation_request(self):
                 save=async_mock.CoroutineMock(),
                 metadata_get=async_mock.CoroutineMock(return_value=True),
                 connection_id="test-conn-id",
+                invitation_key="test-invitation-key",
             )
             conn_rec = await self.manager.accept_response(mock_response, receipt)
             assert conn_rec.their_did == self.test_target_did
diff --git a/aries_cloudagent/protocols/didexchange/v1_0/tests/test_manager.py b/aries_cloudagent/protocols/didexchange/v1_0/tests/test_manager.py
--- a/aries_cloudagent/protocols/didexchange/v1_0/tests/test_manager.py
+++ b/aries_cloudagent/protocols/didexchange/v1_0/tests/test_manager.py
@@ -1,6 +1,7 @@
 import json
 
 from asynctest import mock as async_mock, TestCase as AsyncTestCase
+from pydid import DIDDocument
 
 from .....cache.base import BaseCache
 from .....cache.in_memory import InMemoryCache
@@ -18,6 +19,9 @@
 from .....messaging.decorators.attach_decorator import AttachDecorator
 from .....multitenant.base import BaseMultitenantManager
 from .....multitenant.manager import MultitenantManager
+from .....resolver.base import ResolverError
+from .....resolver.did_resolver import DIDResolver
+from .....resolver.tests import DOC
 from .....storage.error import StorageNotFoundError
 from .....transport.inbound.receipt import MessageReceipt
 from .....wallet.did_info import DIDInfo
@@ -102,6 +106,10 @@ async def setUp(self):
             return_value=TestConfig.test_endpoint
         )
         self.context.injector.bind_instance(BaseLedger, self.ledger)
+        self.resolver = async_mock.MagicMock()
+        did_doc = DIDDocument.deserialize(DOC)
+        self.resolver.resolve = async_mock.CoroutineMock(return_value=did_doc)
+        self.context.injector.bind_instance(DIDResolver, self.resolver)
 
         self.multitenant_mgr = async_mock.MagicMock(MultitenantManager, autospec=True)
         self.context.injector.bind_instance(
@@ -168,6 +176,37 @@ async def test_receive_invitation(self):
                 invitee_record = await self.manager.receive_invitation(invi_msg)
                 assert invitee_record.state == ConnRecord.State.REQUEST.rfc23
 
+    async def test_receive_invitation_oob_public_did(self):
+        async with self.profile.session() as session:
+            self.profile.context.update_settings({"public_invites": True})
+            public_did_info = None
+            await session.wallet.create_public_did(
+                DIDMethod.SOV,
+                KeyType.ED25519,
+            )
+            public_did_info = await session.wallet.get_public_did()
+            with async_mock.patch.object(
+                test_module, "AttachDecorator", autospec=True
+            ) as mock_attach_deco, async_mock.patch.object(
+                self.multitenant_mgr, "get_default_mediator"
+            ) as mock_get_default_mediator:
+                mock_get_default_mediator.return_value = None
+                invi_rec = await self.oob_manager.create_invitation(
+                    my_endpoint="testendpoint",
+                    hs_protos=[HSProto.RFC23],
+                )
+                invi_msg = invi_rec.invitation
+                invi_msg.services = [public_did_info.did]
+                mock_attach_deco.data_base64 = async_mock.MagicMock(
+                    return_value=async_mock.MagicMock(
+                        data=async_mock.MagicMock(sign=async_mock.CoroutineMock())
+                    )
+                )
+                invitee_record = await self.manager.receive_invitation(
+                    invi_msg, their_public_did=public_did_info.did
+                )
+                assert invitee_record.state == ConnRecord.State.REQUEST.rfc23
+
     async def test_receive_invitation_no_auto_accept(self):
         async with self.profile.session() as session:
             mediation_record = MediationRecord(
@@ -440,7 +479,6 @@ async def test_receive_request_explicit_public_did(self):
                 did=TestConfig.test_did,
                 did_doc_attach=async_mock.MagicMock(
                     data=async_mock.MagicMock(
-                        verify=async_mock.CoroutineMock(return_value=True),
                         signed=async_mock.MagicMock(
                             decode=async_mock.MagicMock(return_value="dummy-did-doc")
                         ),
@@ -479,6 +517,10 @@ async def test_receive_request_explicit_public_did(self):
             ) as mock_attach_deco, async_mock.patch.object(
                 test_module, "DIDXResponse", autospec=True
             ) as mock_response, async_mock.patch.object(
+                self.manager,
+                "verify_diddoc",
+                async_mock.CoroutineMock(return_value=DIDDoc(TestConfig.test_did)),
+            ), async_mock.patch.object(
                 self.manager, "create_did_document", async_mock.CoroutineMock()
             ) as mock_create_did_doc, async_mock.patch.object(
                 MediationManager, "prepare_request", autospec=True
@@ -853,7 +895,6 @@ async def test_receive_request_public_did_x_wrong_did(self):
                 did=TestConfig.test_did,
                 did_doc_attach=async_mock.MagicMock(
                     data=async_mock.MagicMock(
-                        verify=async_mock.CoroutineMock(return_value=True),
                         signed=async_mock.MagicMock(
                             decode=async_mock.MagicMock(return_value="dummy-did-doc")
                         ),
@@ -877,7 +918,11 @@ async def test_receive_request_public_did_x_wrong_did(self):
                 test_module, "DIDPosture", autospec=True
             ) as mock_did_posture, async_mock.patch.object(
                 test_module.DIDDoc, "from_json", async_mock.MagicMock()
-            ) as mock_did_doc_from_json:
+            ) as mock_did_doc_from_json, async_mock.patch.object(
+                self.manager,
+                "verify_diddoc",
+                async_mock.CoroutineMock(return_value=DIDDoc("LjgpST2rjsoxYegQDRm7EL")),
+            ):
                 mock_conn_record = async_mock.MagicMock(
                     accept=ConnRecord.ACCEPT_MANUAL,
                     my_did=None,
@@ -918,7 +963,9 @@ async def test_receive_request_public_did_x_did_doc_attach_bad_sig(self):
                 did=TestConfig.test_did,
                 did_doc_attach=async_mock.MagicMock(
                     data=async_mock.MagicMock(
-                        verify=async_mock.CoroutineMock(return_value=False)
+                        signed=async_mock.MagicMock(
+                            decode=async_mock.MagicMock(return_value="dummy-did-doc")
+                        ),
                     )
                 ),
                 _thread=async_mock.MagicMock(pthid="did:sov:publicdid0000000000000"),
@@ -937,7 +984,11 @@ async def test_receive_request_public_did_x_did_doc_attach_bad_sig(self):
                 test_module, "ConnRecord", async_mock.MagicMock()
             ) as mock_conn_rec_cls, async_mock.patch.object(
                 test_module, "DIDPosture", autospec=True
-            ) as mock_did_posture:
+            ) as mock_did_posture, async_mock.patch.object(
+                self.manager,
+                "verify_diddoc",
+                async_mock.CoroutineMock(side_effect=DIDXManagerError),
+            ):
                 mock_conn_record = async_mock.MagicMock(
                     accept=ConnRecord.ACCEPT_MANUAL,
                     my_did=None,
@@ -956,7 +1007,7 @@ async def test_receive_request_public_did_x_did_doc_attach_bad_sig(self):
                     return_value=test_module.DIDPosture.PUBLIC
                 )
 
-                with self.assertRaises(DIDXManagerError) as context:
+                with self.assertRaises(DIDXManagerError):
                     await self.manager.receive_request(
                         request=mock_request,
                         recipient_did=TestConfig.test_did,
@@ -966,7 +1017,6 @@ async def test_receive_request_public_did_x_did_doc_attach_bad_sig(self):
                         auto_accept_implicit=False,
                         mediation_id=None,
                     )
-                assert "DID Doc signature failed" in str(context.exception)
 
     async def test_receive_request_public_did_no_public_invites(self):
         async with self.profile.session() as session:
@@ -1023,7 +1073,6 @@ async def test_receive_request_public_did_no_auto_accept(self):
                 did=TestConfig.test_did,
                 did_doc_attach=async_mock.MagicMock(
                     data=async_mock.MagicMock(
-                        verify=async_mock.CoroutineMock(return_value=True),
                         signed=async_mock.MagicMock(
                             decode=async_mock.MagicMock(return_value="dummy-did-doc")
                         ),
@@ -1055,7 +1104,11 @@ async def test_receive_request_public_did_no_auto_accept(self):
                 test_module, "DIDXResponse", autospec=True
             ) as mock_response, async_mock.patch.object(
                 self.manager, "create_did_document", async_mock.CoroutineMock()
-            ) as mock_create_did_doc:
+            ) as mock_create_did_doc, async_mock.patch.object(
+                self.manager,
+                "verify_diddoc",
+                async_mock.CoroutineMock(return_value=DIDDoc(TestConfig.test_did)),
+            ):
                 mock_conn_record = async_mock.MagicMock(
                     accept=ConnRecord.ACCEPT_MANUAL,
                     my_did=None,
@@ -1097,7 +1150,6 @@ async def test_receive_request_peer_did(self):
                 did=TestConfig.test_did,
                 did_doc_attach=async_mock.MagicMock(
                     data=async_mock.MagicMock(
-                        verify=async_mock.CoroutineMock(return_value=True),
                         signed=async_mock.MagicMock(
                             decode=async_mock.MagicMock(return_value="dummy-did-doc")
                         ),
@@ -1139,7 +1191,11 @@ async def test_receive_request_peer_did(self):
                 test_module, "AttachDecorator", autospec=True
             ) as mock_attach_deco, async_mock.patch.object(
                 test_module, "DIDXResponse", autospec=True
-            ) as mock_response:
+            ) as mock_response, async_mock.patch.object(
+                self.manager,
+                "verify_diddoc",
+                async_mock.CoroutineMock(return_value=DIDDoc(TestConfig.test_did)),
+            ):
                 mock_conn_rec_cls.retrieve_by_invitation_key = async_mock.CoroutineMock(
                     return_value=mock_conn
                 )
@@ -1194,7 +1250,6 @@ async def test_receive_request_multiuse_multitenant(self):
                 did=TestConfig.test_did,
                 did_doc_attach=async_mock.MagicMock(
                     data=async_mock.MagicMock(
-                        verify=async_mock.CoroutineMock(return_value=True),
                         signed=async_mock.MagicMock(
                             decode=async_mock.MagicMock(return_value="dummy-did-doc")
                         ),
@@ -1213,7 +1268,11 @@ async def test_receive_request_multiuse_multitenant(self):
                 InMemoryWallet, "create_local_did", autospec=True
             ) as mock_wallet_create_local_did, async_mock.patch.object(
                 test_module, "DIDDoc", autospec=True
-            ) as mock_did_doc:
+            ) as mock_did_doc, async_mock.patch.object(
+                self.manager,
+                "verify_diddoc",
+                async_mock.CoroutineMock(return_value=DIDDoc(TestConfig.test_did)),
+            ):
                 mock_conn_rec = async_mock.CoroutineMock(
                     connection_id="dummy",
                     accept=ACCEPT_MANUAL,
@@ -1263,7 +1322,6 @@ async def test_receive_request_implicit_multitenant(self):
                 did=TestConfig.test_did,
                 did_doc_attach=async_mock.MagicMock(
                     data=async_mock.MagicMock(
-                        verify=async_mock.CoroutineMock(return_value=True),
                         signed=async_mock.MagicMock(
                             decode=async_mock.MagicMock(return_value="dummy-did-doc")
                         ),
@@ -1292,7 +1350,11 @@ async def test_receive_request_implicit_multitenant(self):
                 test_module, "DIDPosture", autospec=True
             ) as mock_did_posture, async_mock.patch.object(
                 test_module, "DIDDoc", autospec=True
-            ) as mock_did_doc:
+            ) as mock_did_doc, async_mock.patch.object(
+                self.manager,
+                "verify_diddoc",
+                async_mock.CoroutineMock(return_value=DIDDoc(TestConfig.test_did)),
+            ):
                 mock_conn_rec = async_mock.CoroutineMock(
                     connection_id="dummy",
                     accept=ACCEPT_MANUAL,
@@ -2183,3 +2245,20 @@ async def test_diddoc_connection_targets_diddoc_underspecified(self):
         x_did_doc._service = {}
         with self.assertRaises(BaseConnectionManagerError):
             self.manager.diddoc_connection_targets(x_did_doc, TestConfig.test_verkey)
+
+    async def test_resolve_did_document_error(self):
+        public_did_info = None
+        async with self.profile.session() as session:
+            await session.wallet.create_public_did(
+                DIDMethod.SOV,
+                KeyType.ED25519,
+            )
+            public_did_info = await session.wallet.get_public_did()
+        with async_mock.patch.object(
+            self.resolver,
+            "resolve",
+            async_mock.CoroutineMock(side_effect=ResolverError()),
+        ):
+            with self.assertRaises(DIDXManagerError) as ctx:
+                await self.manager.get_resolved_did_document(public_did_info.did)
+            assert "Failed to resolve public DID in invitation" in str(ctx.exception)