Ejemplo n.º 1
0
    async def handle(self, context: RequestContext, responder: BaseResponder):
        # Verify connection exists
        session = await context.session()
        manager = MediationManager(session)
        try:
            connection = await ConnRecord.retrieve_by_id(
                session, context.message.connection_id)
        except StorageNotFoundError:
            report = ProblemReport(explain_ltxt='Connection not found.',
                                   who_retries='none')
            report.assign_thread_from(context.message)
            await responder.send_reply(report)
            return

        _record, request = await manager.prepare_request(
            connection.connection_id)
        # Send mediation request
        await responder.send(
            request,
            connection_id=connection.connection_id,
        )

        # Send notification of mediation request sent
        sent = MediationRequestSent(connection_id=connection.connection_id)
        sent.assign_thread_from(context.message)
        await responder.send_reply(sent)
Ejemplo n.º 2
0
 async def handle(self, context: RequestContext, responder: BaseResponder):
     """Handle RotuesGet."""
     session = await context.session()
     manager = MediationManager(session)
     routes = Routes(
         routes=await manager.get_my_keylist(context.message.connection_id))
     routes.assign_thread_from(context.message)
     await responder.send_reply(routes)
    async def handle(self, context: RequestContext, responder: BaseResponder):
        """Handle mediation deny request."""
        session = await context.session()
        manager = MediationManager(session)
        record = await MediationRecord.retrieve_by_id(
            session, context.message.mediation_id
        )

        deny = await manager.deny_request(record)
        await responder.send(deny, connection_id=record.connection_id)

        denied = MediationDenied(mediation_id=record.mediation_id)
        denied.assign_thread_from(context.message)
        await responder.send_reply(denied)
Ejemplo n.º 4
0
    async def handle(self, context: RequestContext, responder: BaseResponder):
        """Handle KeylistUpdateSend messages."""
        session = await context.session()
        manager = MediationManager(session)
        if context.message.action == KeylistUpdateRule.RULE_ADD:
            update = await manager.add_key(context.message.verkey,
                                           context.message.connection_id)
        elif context.message.action == KeylistUpdateRule.RULE_REMOVE:
            update = await manager.remove_key(context.message.verkey,
                                              context.message.connection_id)

        await responder.send(
            update,
            connection_id=context.message.connection_id,
        )

        sent = KeylistUpdateSent(connection_id=context.message.connection_id,
                                 verkey=context.message.verkey,
                                 action=context.message.action)
        sent.assign_thread_from(context.message)
        await responder.send_reply(sent)
Ejemplo n.º 5
0
    async def accept_response(self, response: ConnectionResponse,
                              receipt: MessageReceipt) -> ConnRecord:
        """
        Accept a connection response.

        Process a ConnectionResponse message by looking up
        the connection request and setting up the pairwise connection.

        Args:
            response: The `ConnectionResponse` to accept
            receipt: The message receipt

        Returns:
            The updated `ConnRecord` representing the connection

        Raises:
            ConnectionManagerError: If there is no DID associated with the
                connection response
            ConnectionManagerError: If the corresponding connection is not
                at the request or response stage

        """

        connection = None
        if response._thread:
            # identify the request by the thread ID
            try:
                connection = await ConnRecord.retrieve_by_request_id(
                    self._session, response._thread_id)
            except StorageNotFoundError:
                pass

        if not connection and receipt.sender_did:
            # identify connection by the DID they used for us
            try:
                connection = await ConnRecord.retrieve_by_did(
                    self._session, receipt.sender_did, receipt.recipient_did)
            except StorageNotFoundError:
                pass

        if not connection:
            raise ConnectionManagerError(
                "No corresponding connection request found",
                error_code=ProblemReportReason.RESPONSE_NOT_ACCEPTED,
            )

        if ConnRecord.State.get(connection.state) not in (
                ConnRecord.State.REQUEST,
                ConnRecord.State.RESPONSE,
        ):
            raise ConnectionManagerError(
                f"Cannot accept connection response for connection"
                f" in state: {connection.state}")

        their_did = response.connection.did
        conn_did_doc = response.connection.did_doc
        if not conn_did_doc:
            raise ConnectionManagerError(
                "No DIDDoc provided; cannot connect to public DID")
        if their_did != conn_did_doc.did:
            raise ConnectionManagerError(
                "Connection DID does not match DIDDoc id")
        await self.store_did_document(conn_did_doc)

        connection.their_did = their_did
        connection.state = ConnRecord.State.RESPONSE.rfc160

        await connection.save(self._session,
                              reason="Accepted connection response")

        send_mediation_request = await connection.metadata_get(
            self._session, MediationManager.SEND_REQ_AFTER_CONNECTION)
        if send_mediation_request:
            mgr = MediationManager(self._session.profile)
            _record, request = await mgr.prepare_request(
                connection.connection_id)
            responder = self._session.inject(BaseResponder)
            await responder.send(request,
                                 connection_id=connection.connection_id)

        return connection
Ejemplo n.º 6
0
    async def create_invitation(
        self,
        my_label: str = None,
        my_endpoint: str = None,
        auto_accept: bool = None,
        public: bool = False,
        multi_use: bool = False,
        alias: str = None,
        routing_keys: Sequence[str] = None,
        recipient_keys: Sequence[str] = None,
        metadata: dict = None,
        mediation_id: str = None,
    ) -> Tuple[ConnRecord, ConnectionInvitation]:
        """
        Generate new connection invitation.

        This interaction represents an out-of-band communication channel. In the future
        and in practice, these sort of invitations will be received over any number of
        channels such as SMS, Email, QR Code, NFC, etc.

        Structure of an invite message:

        ::

            {
                "@type": "https://didcomm.org/connections/1.0/invitation",
                "label": "Alice",
                "did": "did:sov:QmWbsNYhMrjHiqZDTUTEJs"
            }

        Or, in the case of a peer DID:

        ::

            {
                "@type": "https://didcomm.org/connections/1.0/invitation",
                "label": "Alice",
                "did": "did:peer:oiSqsNYhMrjHiqZDTUthsw",
                "recipient_keys": ["8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K"],
                "service_endpoint": "https://example.com/endpoint"
                "routing_keys": ["9EH5gYEeNc3z7PYXmd53d5x6qAfCNrqQqEB4nS7Zfu6K"],
            }

        Args:
            my_label: label for this connection
            my_endpoint: endpoint where other party can reach me
            auto_accept: auto-accept a corresponding connection request
                (None to use config)
            public: set to create an invitation from the public DID
            multi_use: set to True to create an invitation for multiple use
            alias: optional alias to apply to connection for later use

        Returns:
            A tuple of the new `ConnRecord` and `ConnectionInvitation` instances

        """
        # Mediation Record can still be None after this operation if no
        # mediation id passed and no default
        mediation_record = await mediation_record_if_id(
            self._session,
            mediation_id,
            or_default=True,
        )
        keylist_updates = None
        image_url = self._session.context.settings.get("image_url")

        # Multitenancy setup
        multitenant_mgr = self._session.inject(MultitenantManager,
                                               required=False)
        wallet_id = self._session.settings.get("wallet.id")

        if not my_label:
            my_label = self._session.settings.get("default_label")
        wallet = self._session.inject(BaseWallet)
        if public:
            if not self._session.settings.get("public_invites"):
                raise ConnectionManagerError(
                    "Public invitations are not enabled")

            public_did = await wallet.get_public_did()
            if not public_did:
                raise ConnectionManagerError(
                    "Cannot create public invitation with no public DID")

            if multi_use:
                raise ConnectionManagerError(
                    "Cannot use public and multi_use at the same time")

            if metadata:
                raise ConnectionManagerError(
                    "Cannot use public and set metadata at the same time")

            # FIXME - allow ledger instance to format public DID with prefix?
            invitation = ConnectionInvitation(label=my_label,
                                              did=f"did:sov:{public_did.did}",
                                              image_url=image_url)

            # Add mapping for multitenant relaying.
            # Mediation of public keys is not supported yet
            if multitenant_mgr and wallet_id:
                await multitenant_mgr.add_key(wallet_id,
                                              public_did.verkey,
                                              skip_if_exists=True)

            return None, invitation

        invitation_mode = ConnRecord.INVITATION_MODE_ONCE
        if multi_use:
            invitation_mode = ConnRecord.INVITATION_MODE_MULTI

        if recipient_keys:
            # TODO: register recipient keys for relay
            # TODO: check that recipient keys are in wallet
            invitation_key = recipient_keys[0]  # TODO first key appropriate?
        else:
            # Create and store new invitation key
            invitation_signing_key = await wallet.create_signing_key()
            invitation_key = invitation_signing_key.verkey
            recipient_keys = [invitation_key]
            mediation_mgr = MediationManager(self._session.profile)
            keylist_updates = await mediation_mgr.add_key(
                invitation_key, keylist_updates)

            if multitenant_mgr and wallet_id:
                await multitenant_mgr.add_key(wallet_id, invitation_key)

        accept = (ConnRecord.ACCEPT_AUTO if
                  (auto_accept or
                   (auto_accept is None and
                    self._session.settings.get("debug.auto_accept_requests")))
                  else ConnRecord.ACCEPT_MANUAL)

        # Create connection record
        connection = ConnRecord(
            invitation_key=invitation_key,  # TODO: determine correct key to use
            their_role=ConnRecord.Role.REQUESTER.rfc160,
            state=ConnRecord.State.INVITATION.rfc160,
            accept=accept,
            invitation_mode=invitation_mode,
            alias=alias,
        )

        await connection.save(self._session, reason="Created new invitation")

        routing_keys = []
        my_endpoint = my_endpoint or self._session.settings.get(
            "default_endpoint")

        # The base wallet can act as a mediator for all tenants
        if multitenant_mgr and wallet_id:
            base_mediation_record = await multitenant_mgr.get_default_mediator(
            )

            if base_mediation_record:
                routing_keys = base_mediation_record.routing_keys
                my_endpoint = base_mediation_record.endpoint

                # If we use a mediator for the base wallet we don't
                # need to register the key at the subwallet mediator
                # because it only needs to know the key of the base mediator
                # sub wallet mediator -> base wallet mediator -> agent
                keylist_updates = None
        if mediation_record:
            routing_keys = [*routing_keys, *mediation_record.routing_keys]
            my_endpoint = mediation_record.endpoint

            # Save that this invitation was created with mediation
            await connection.metadata_set(self._session, "mediation",
                                          {"id": mediation_id})

            if keylist_updates:
                responder = self._session.inject(BaseResponder, required=False)
                await responder.send(
                    keylist_updates,
                    connection_id=mediation_record.connection_id)

        # Create connection invitation message
        # Note: Need to split this into two stages to support inbound routing of invites
        # Would want to reuse create_did_document and convert the result
        invitation = ConnectionInvitation(
            label=my_label,
            recipient_keys=recipient_keys,
            routing_keys=routing_keys,
            endpoint=my_endpoint,
            image_url=image_url,
        )
        await connection.attach_invitation(self._session, invitation)

        if metadata:
            for key, value in metadata.items():
                await connection.metadata_set(self._session, key, value)

        return connection, invitation
Ejemplo n.º 7
0
    async def create_response(
        self,
        connection: ConnRecord,
        my_endpoint: str = None,
        mediation_id: str = None,
    ) -> ConnectionResponse:
        """
        Create a connection response for a received connection request.

        Args:
            connection: The `ConnRecord` with a pending connection request
            my_endpoint: The endpoint I can be reached at
            mediation_id: The record id for mediation that contains routing_keys and
            service endpoint
        Returns:
            A tuple of the updated `ConnRecord` new `ConnectionResponse` message

        """
        ConnRecord.log_state(
            "Creating connection response",
            {"connection_id": connection.connection_id},
            settings=self._session.settings,
        )

        keylist_updates = None
        mediation_record = await mediation_record_if_id(
            self._session, mediation_id)

        # Multitenancy setup
        multitenant_mgr = self._session.inject(MultitenantManager,
                                               required=False)
        wallet_id = self._session.settings.get("wallet.id")
        base_mediation_record = None

        if multitenant_mgr and wallet_id:
            base_mediation_record = await multitenant_mgr.get_default_mediator(
            )

        if ConnRecord.State.get(connection.state) not in (
                ConnRecord.State.REQUEST,
                ConnRecord.State.RESPONSE,
        ):
            raise ConnectionManagerError(
                "Connection is not in the request or response state")

        request = await connection.retrieve_request(self._session)
        wallet = self._session.inject(BaseWallet)
        if connection.my_did:
            my_info = await wallet.get_local_did(connection.my_did)
        else:
            my_info = await wallet.create_local_did()
            connection.my_did = my_info.did
            mediation_mgr = MediationManager(self._session.profile)
            keylist_updates = await mediation_mgr.add_key(
                my_info.verkey, keylist_updates)
            # Add mapping for multitenant relay
            if multitenant_mgr and wallet_id:
                await multitenant_mgr.add_key(wallet_id, my_info.verkey)

        # Create connection response message
        if my_endpoint:
            my_endpoints = [my_endpoint]
        else:
            my_endpoints = []
            default_endpoint = self._session.settings.get("default_endpoint")
            if default_endpoint:
                my_endpoints.append(default_endpoint)
            my_endpoints.extend(
                self._session.settings.get("additional_endpoints", []))

        did_doc = await self.create_did_document(
            my_info,
            connection.inbound_connection_id,
            my_endpoints,
            mediation_records=list(
                filter(None, [base_mediation_record, mediation_record])),
        )

        response = ConnectionResponse(
            connection=ConnectionDetail(did=my_info.did, did_doc=did_doc))

        # Assign thread information
        response.assign_thread_from(request)
        response.assign_trace_from(request)
        # Sign connection field using the invitation key
        wallet = self._session.inject(BaseWallet)
        await response.sign_field("connection", connection.invitation_key,
                                  wallet)

        # Update connection state
        connection.state = ConnRecord.State.RESPONSE.rfc160

        await connection.save(
            self._session,
            reason="Created connection response",
            log_params={"response": response},
        )

        # Update mediator if necessary
        if keylist_updates and mediation_record:
            responder = self._session.inject(BaseResponder, required=False)
            await responder.send(keylist_updates,
                                 connection_id=mediation_record.connection_id)

        # TODO It's possible the mediation request sent here might arrive
        # before the connection response. This would result in an error condition
        # difficult to accomodate for without modifying handlers for trust ping
        # to ensure the connection is active.
        send_mediation_request = await connection.metadata_get(
            self._session, MediationManager.SEND_REQ_AFTER_CONNECTION)
        if send_mediation_request:
            mgr = MediationManager(self._session.profile)
            _record, request = await mgr.prepare_request(
                connection.connection_id)
            responder = self._session.inject(BaseResponder)
            await responder.send(request,
                                 connection_id=connection.connection_id)

        return response
Ejemplo n.º 8
0
    async def receive_request(
        self,
        request: ConnectionRequest,
        receipt: MessageReceipt,
        mediation_id: str = None,
    ) -> ConnRecord:
        """
        Receive and store a connection request.

        Args:
            request: The `ConnectionRequest` to accept
            receipt: The message receipt

        Returns:
            The new or updated `ConnRecord` instance

        """
        ConnRecord.log_state(
            "Receiving connection request",
            {"request": request},
            settings=self._session.settings,
        )

        mediation_mgr = MediationManager(self._session.profile)
        keylist_updates = None
        connection = None
        connection_key = None
        my_info = None

        # Multitenancy setup
        multitenant_mgr = self._session.inject(MultitenantManager,
                                               required=False)
        wallet_id = self._session.settings.get("wallet.id")
        wallet = self._session.inject(BaseWallet)

        # Determine what key will need to sign the response
        if receipt.recipient_did_public:
            my_info = await wallet.get_local_did(receipt.recipient_did)
            connection_key = my_info.verkey
        else:
            connection_key = receipt.recipient_verkey
            try:
                connection = await ConnRecord.retrieve_by_invitation_key(
                    session=self._session,
                    invitation_key=connection_key,
                    their_role=ConnRecord.Role.REQUESTER.rfc160,
                )
            except StorageNotFoundError:
                raise ConnectionManagerError(
                    "No invitation found for pairwise connection")

        invitation = None
        if connection:
            invitation = await connection.retrieve_invitation(self._session)
            connection_key = connection.invitation_key
            ConnRecord.log_state(
                "Found invitation",
                {"invitation": invitation},
                settings=self._session.settings,
            )

            if connection.is_multiuse_invitation:
                wallet = self._session.inject(BaseWallet)
                my_info = await wallet.create_local_did()
                keylist_updates = await mediation_mgr.add_key(
                    my_info.verkey, keylist_updates)

                new_connection = ConnRecord(
                    invitation_key=connection_key,
                    my_did=my_info.did,
                    state=ConnRecord.State.INVITATION.rfc160,
                    accept=connection.accept,
                    their_role=connection.their_role,
                )

                await new_connection.save(
                    self._session,
                    reason=
                    "Received connection request from multi-use invitation DID",
                )

                # Transfer metadata from multi-use to new connection
                # Must come after save so there's an ID to associate with metadata
                for key, value in (await
                                   connection.metadata_get_all(self._session
                                                               )).items():
                    await new_connection.metadata_set(self._session, key,
                                                      value)

                connection = new_connection

                # Add mapping for multitenant relay
                if multitenant_mgr and wallet_id:
                    await multitenant_mgr.add_key(wallet_id, my_info.verkey)
            else:
                # remove key from mediator keylist
                keylist_updates = await mediation_mgr.remove_key(
                    connection_key, keylist_updates)
        conn_did_doc = request.connection.did_doc
        if not conn_did_doc:
            raise ConnectionManagerError(
                "No DIDDoc provided; cannot connect to public DID")
        if request.connection.did != conn_did_doc.did:
            raise ConnectionManagerError(
                "Connection DID does not match DIDDoc id",
                error_code=ProblemReportReason.REQUEST_NOT_ACCEPTED,
            )
        await self.store_did_document(conn_did_doc)

        if connection:
            connection.their_label = request.label
            connection.their_did = request.connection.did
            connection.state = ConnRecord.State.REQUEST.rfc160
            await connection.save(
                self._session,
                reason="Received connection request from invitation")
        elif not self._session.settings.get("public_invites"):
            raise ConnectionManagerError("Public invitations are not enabled")
        else:  # request from public did
            my_info = await wallet.create_local_did()
            # send update-keylist message with new recipient keys
            keylist_updates = await mediation_mgr.add_key(
                my_info.verkey, keylist_updates)

            # Add mapping for multitenant relay
            if multitenant_mgr and wallet_id:
                await multitenant_mgr.add_key(wallet_id, my_info.verkey)

            connection = ConnRecord(
                invitation_key=connection_key,
                my_did=my_info.did,
                their_role=ConnRecord.Role.RESPONDER.rfc160,
                their_did=request.connection.did,
                their_label=request.label,
                accept=(
                    ConnRecord.ACCEPT_AUTO
                    if self._session.settings.get("debug.auto_accept_requests")
                    else ConnRecord.ACCEPT_MANUAL),
                state=ConnRecord.State.REQUEST.rfc160,
            )

            await connection.save(
                self._session,
                reason="Received connection request from public DID")

        # Attach the connection request so it can be found and responded to
        await connection.attach_request(self._session, request)

        # Send keylist updates to mediator
        mediation_record = await mediation_record_if_id(
            self._session, mediation_id)
        if keylist_updates and mediation_record:
            responder = self._session.inject(BaseResponder, required=False)
            await responder.send(keylist_updates,
                                 connection_id=mediation_record.connection_id)

        if connection.accept == ConnRecord.ACCEPT_AUTO:
            response = await self.create_response(connection,
                                                  mediation_id=mediation_id)
            responder = self._session.inject(BaseResponder, required=False)
            if responder:
                await responder.send_reply(
                    response, connection_id=connection.connection_id)
                # refetch connection for accurate state
                connection = await ConnRecord.retrieve_by_id(
                    self._session, connection.connection_id)
        else:
            self._logger.debug("Connection request will await acceptance")

        return connection
Ejemplo n.º 9
0
    async def create_request(
        self,
        connection: ConnRecord,
        my_label: str = None,
        my_endpoint: str = None,
        mediation_id: str = None,
    ) -> ConnectionRequest:
        """
        Create a new connection request for a previously-received invitation.

        Args:
            connection: The `ConnRecord` representing the invitation to accept
            my_label: My label
            my_endpoint: My endpoint

        Returns:
            A new `ConnectionRequest` message to send to the other agent

        """

        keylist_updates = None

        # Mediation Record can still be None after this operation if no
        # mediation id passed and no default
        mediation_record = await mediation_record_if_id(
            self._session,
            mediation_id,
            or_default=True,
        )

        multitenant_mgr = self._session.inject(MultitenantManager,
                                               required=False)
        wallet_id = self._session.settings.get("wallet.id")
        base_mediation_record = None

        if multitenant_mgr and wallet_id:
            base_mediation_record = await multitenant_mgr.get_default_mediator(
            )

        my_info = None
        wallet = self._session.inject(BaseWallet)
        if connection.my_did:
            my_info = await wallet.get_local_did(connection.my_did)
        else:
            # Create new DID for connection
            my_info = await wallet.create_local_did()
            connection.my_did = my_info.did
            mediation_mgr = MediationManager(self._session.profile)
            keylist_updates = await mediation_mgr.add_key(
                my_info.verkey, keylist_updates)

            # Add mapping for multitenant relay
            if multitenant_mgr and wallet_id:
                await multitenant_mgr.add_key(wallet_id, my_info.verkey)

        # Create connection request message
        if my_endpoint:
            my_endpoints = [my_endpoint]
        else:
            my_endpoints = []
            default_endpoint = self._session.settings.get("default_endpoint")
            if default_endpoint:
                my_endpoints.append(default_endpoint)
            my_endpoints.extend(
                self._session.settings.get("additional_endpoints", []))

        did_doc = await self.create_did_document(
            my_info,
            connection.inbound_connection_id,
            my_endpoints,
            mediation_records=list(
                filter(None, [base_mediation_record, mediation_record])),
        )

        if not my_label:
            my_label = self._session.settings.get("default_label")
        request = ConnectionRequest(
            label=my_label,
            connection=ConnectionDetail(did=connection.my_did,
                                        did_doc=did_doc),
            image_url=self._session.settings.get("image_url"),
        )

        # Update connection state
        connection.request_id = request._id
        connection.state = ConnRecord.State.REQUEST.rfc160

        await connection.save(self._session,
                              reason="Created connection request")

        # Notify mediator of keylist changes
        if keylist_updates and mediation_record:
            # send a update keylist message with new recipient keys.
            responder = self._session.inject(BaseResponder, required=False)
            await responder.send(keylist_updates,
                                 connection_id=mediation_record.connection_id)

        return request
Ejemplo n.º 10
0
    async def create_invitation(
        self,
        my_label: str = None,
        my_endpoint: str = None,
        auto_accept: bool = None,
        public: bool = False,
        hs_protos: Sequence[HSProto] = None,
        multi_use: bool = False,
        alias: str = None,
        attachments: Sequence[Mapping] = None,
        metadata: dict = None,
        mediation_id: str = None,
    ) -> InvitationRecord:
        """
        Generate new connection invitation.

        This interaction represents an out-of-band communication channel. In the future
        and in practice, these sort of invitations will be received over any number of
        channels such as SMS, Email, QR Code, NFC, etc.

        Args:
            my_label: label for this connection
            my_endpoint: endpoint where other party can reach me
            auto_accept: auto-accept a corresponding connection request
                (None to use config)
            public: set to create an invitation from the public DID
            hs_protos: list of handshake protocols to include
            multi_use: set to True to create an invitation for multiple-use connection
            alias: optional alias to apply to connection for later use
            attachments: list of dicts in form of {"id": ..., "type": ...}

        Returns:
            Invitation record

        """
        mediation_mgr = MediationManager(self._session.profile)
        mediation_record = await mediation_record_if_id(
            self._session,
            mediation_id,
            or_default=True,
        )
        keylist_updates = None

        if not (hs_protos or attachments):
            raise OutOfBandManagerError(
                "Invitation must include handshake protocols, "
                "request attachments, or both")

        wallet = self._session.inject(BaseWallet)

        # Multitenancy setup
        multitenant_mgr = self._session.inject(MultitenantManager,
                                               required=False)
        wallet_id = self._session.settings.get("wallet.id")

        accept = bool(
            auto_accept
            or (auto_accept is None
                and self._session.settings.get("debug.auto_accept_requests")))
        if public:
            if multi_use:
                raise OutOfBandManagerError(
                    "Cannot create public invitation with multi_use")
            if metadata:
                raise OutOfBandManagerError(
                    "Cannot store metadata on public invitations")

        message_attachments = []
        for atch in attachments or []:
            a_type = atch.get("type")
            a_id = atch.get("id")

            if a_type == "credential-offer":
                try:
                    cred_ex_rec = await V10CredentialExchange.retrieve_by_id(
                        self._session,
                        a_id,
                    )
                    message_attachments.append(
                        InvitationMessage.wrap_message(
                            cred_ex_rec.credential_offer_dict))
                except StorageNotFoundError:
                    cred_ex_rec = await V20CredExRecord.retrieve_by_id(
                        self._session,
                        a_id,
                    )
                    message_attachments.append(
                        InvitationMessage.wrap_message(
                            V20CredOffer.deserialize(
                                cred_ex_rec.cred_offer).offer()))
            elif a_type == "present-proof":
                try:
                    pres_ex_rec = await V10PresentationExchange.retrieve_by_id(
                        self._session,
                        a_id,
                    )
                    message_attachments.append(
                        InvitationMessage.wrap_message(
                            pres_ex_rec.presentation_request_dict))
                except StorageNotFoundError:
                    pres_ex_rec = await V20PresExRecord.retrieve_by_id(
                        self._session,
                        a_id,
                    )
                    message_attachments.append(
                        InvitationMessage.wrap_message(
                            V20PresRequest.deserialize(
                                pres_ex_rec.pres_request).attachment()))
            else:
                raise OutOfBandManagerError(
                    f"Unknown attachment type: {a_type}")

        handshake_protocols = [
            DIDCommPrefix.qualify_current(hsp.name) for hsp in hs_protos or []
        ] or None
        if public:
            if not self._session.settings.get("public_invites"):
                raise OutOfBandManagerError(
                    "Public invitations are not enabled")

            public_did = await wallet.get_public_did()
            if not public_did:
                raise OutOfBandManagerError(
                    "Cannot create public invitation with no public DID")

            invi_msg = InvitationMessage(  # create invitation message
                label=my_label or self._session.settings.get("default_label"),
                handshake_protocols=handshake_protocols,
                request_attach=message_attachments,
                service=[f"did:sov:{public_did.did}"],
            )
            keylist_updates = await mediation_mgr.add_key(
                public_did.verkey, keylist_updates)
            ledger = self._session.inject(BaseLedger)
            try:
                async with ledger:
                    base_url = await ledger.get_endpoint_for_did(public_did.did
                                                                 )
                    invi_url = invi_msg.to_url(base_url)
            except LedgerError as ledger_x:
                raise OutOfBandManagerError(
                    "Error getting endpoint for public DID "
                    f"{public_did.did}: {ledger_x}")

            conn_rec = ConnRecord(  # create connection record
                invitation_key=public_did.verkey,
                invitation_msg_id=invi_msg._id,
                their_role=ConnRecord.Role.REQUESTER.rfc23,
                state=ConnRecord.State.INVITATION.rfc23,
                accept=ConnRecord.ACCEPT_AUTO
                if accept else ConnRecord.ACCEPT_MANUAL,
                alias=alias,
            )

            await conn_rec.save(self._session, reason="Created new invitation")
            await conn_rec.attach_invitation(self._session, invi_msg)

            if multitenant_mgr and wallet_id:  # add mapping for multitenant relay
                await multitenant_mgr.add_key(wallet_id,
                                              public_did.verkey,
                                              skip_if_exists=True)

        else:
            invitation_mode = (ConnRecord.INVITATION_MODE_MULTI if multi_use
                               else ConnRecord.INVITATION_MODE_ONCE)

            if not my_endpoint:
                my_endpoint = self._session.settings.get("default_endpoint")

            # Create and store new invitation key
            connection_key = await wallet.create_signing_key()
            keylist_updates = await mediation_mgr.add_key(
                connection_key.verkey, keylist_updates)
            # Add mapping for multitenant relay
            if multitenant_mgr and wallet_id:
                await multitenant_mgr.add_key(wallet_id, connection_key.verkey)

            # Create connection record
            conn_rec = ConnRecord(
                invitation_key=connection_key.verkey,
                their_role=ConnRecord.Role.REQUESTER.rfc23,
                state=ConnRecord.State.INVITATION.rfc23,
                accept=ConnRecord.ACCEPT_AUTO
                if accept else ConnRecord.ACCEPT_MANUAL,
                invitation_mode=invitation_mode,
                alias=alias,
            )
            await conn_rec.save(self._session, reason="Created new connection")

            routing_keys = []
            # The base wallet can act as a mediator for all tenants
            if multitenant_mgr and wallet_id:
                base_mediation_record = await multitenant_mgr.get_default_mediator(
                )

                if base_mediation_record:
                    routing_keys = base_mediation_record.routing_keys
                    my_endpoint = base_mediation_record.endpoint

                    # If we use a mediator for the base wallet we don't
                    # need to register the key at the subwallet mediator
                    # because it only needs to know the key of the base mediator
                    # sub wallet mediator -> base wallet mediator -> agent
                    keylist_updates = None
            if mediation_record:
                routing_keys = [*routing_keys, *mediation_record.routing_keys]
                my_endpoint = mediation_record.endpoint

                # Save that this invitation was created with mediation
                await conn_rec.metadata_set(self._session, "mediation",
                                            {"id": mediation_id})

                if keylist_updates:
                    responder = self._session.inject(BaseResponder,
                                                     required=False)
                    await responder.send(
                        keylist_updates,
                        connection_id=mediation_record.connection_id)
            routing_keys = [
                key if len(key.split(":")) == 3 else naked_to_did_key(key)
                for key in routing_keys
            ]
            # Create connection invitation message
            # Note: Need to split this into two stages to support inbound routing
            # of invitations
            # Would want to reuse create_did_document and convert the result
            invi_msg = InvitationMessage(
                label=my_label or self._session.settings.get("default_label"),
                handshake_protocols=handshake_protocols,
                request_attach=message_attachments,
                service=[
                    ServiceMessage(
                        _id="#inline",
                        _type="did-communication",
                        recipient_keys=[
                            naked_to_did_key(connection_key.verkey)
                        ],
                        service_endpoint=my_endpoint,
                        routing_keys=routing_keys,
                    )
                ],
            )
            invi_url = invi_msg.to_url()

            # Update connection record
            conn_rec.invitation_msg_id = invi_msg._id
            await conn_rec.save(self._session, reason="Added Invitation")
            await conn_rec.attach_invitation(self._session, invi_msg)

            if metadata:
                for key, value in metadata.items():
                    await conn_rec.metadata_set(self._session, key, value)

        return InvitationRecord(  # for return via admin API, not storage
            state=InvitationRecord.STATE_INITIAL,
            invi_msg_id=invi_msg._id,
            invitation=invi_msg.serialize(),
            invitation_url=invi_url,
        )