Skip to content
Snippets Groups Projects
test_e2e_keys.py 70.2 KiB
Newer Older
  • Learn to ignore specific revisions
  •                             "user_id": local_user,
                                "usage": ["master"],
                                "keys": {"ed25519:" + master_pubkey: master_pubkey},
                                "signatures": {
                                    local_user: {"ed25519:" + device_pubkey: "something"}
                                },
    
    Hubert Chathi's avatar
    Hubert Chathi committed
                            },
                        },
    
                        other_user: {
                            # fails because the device is not the user's master-signing key
                            # should fail with NOT_FOUND
                            "unknown": {
                                "user_id": other_user,
                                "device_id": "unknown",
                                "signatures": {
                                    local_user: {
                                        "ed25519:" + usersigning_pubkey: "something"
                                    }
                                },
    
    Hubert Chathi's avatar
    Hubert Chathi committed
                            },
    
                            other_master_pubkey: {
                                # fails because the key doesn't match what the server has
                                # should fail with UNKNOWN
                                "user_id": other_user,
                                "usage": ["master"],
                                "keys": {
                                    "ed25519:" + other_master_pubkey: other_master_pubkey
                                },
                                "something": "random",
                                "signatures": {
                                    local_user: {
                                        "ed25519:" + usersigning_pubkey: "something"
                                    }
                                },
    
            )
    
            user_failures = ret["failures"][local_user]
    
            self.assertEqual(user_failures[device_id]["errcode"], Codes.INVALID_SIGNATURE)
    
    Hubert Chathi's avatar
    Hubert Chathi committed
            self.assertEqual(
    
                user_failures[master_pubkey]["errcode"], Codes.INVALID_SIGNATURE
    
    Hubert Chathi's avatar
    Hubert Chathi committed
            )
    
            self.assertEqual(user_failures["unknown"]["errcode"], Codes.NOT_FOUND)
    
    
            other_user_failures = ret["failures"][other_user]
    
            self.assertEqual(other_user_failures["unknown"]["errcode"], Codes.NOT_FOUND)
    
    Hubert Chathi's avatar
    Hubert Chathi committed
            self.assertEqual(
    
                other_user_failures[other_master_pubkey]["errcode"], Codes.UNKNOWN
    
    Hubert Chathi's avatar
    Hubert Chathi committed
            )
    
    
            # test successful signatures
            del device_key["signatures"]
            sign.sign_json(device_key, local_user, selfsigning_signing_key)
            sign.sign_json(master_key, local_user, device_signing_key)
            sign.sign_json(other_master_key, local_user, usersigning_signing_key)
    
                self.handler.upload_signatures_for_device_keys(
                    local_user,
                    {
                        local_user: {device_id: device_key, master_pubkey: master_key},
                        other_user: {other_master_pubkey: other_master_key},
                    },
                )
    
            )
    
            self.assertEqual(ret["failures"], {})
    
            # fetch the signed keys/devices and make sure that the signatures are there
    
                    {"device_keys": {local_user: [], other_user: []}},
                    0,
                    local_user,
                    "device123",
    
    Hubert Chathi's avatar
    Hubert Chathi committed
                ret["device_keys"][local_user]["xyz"]["signatures"][local_user][
                    "ed25519:" + selfsigning_pubkey
                ],
                device_key["signatures"][local_user]["ed25519:" + selfsigning_pubkey],
    
    Hubert Chathi's avatar
    Hubert Chathi committed
                ret["master_keys"][local_user]["signatures"][local_user][
                    "ed25519:" + device_id
                ],
                master_key["signatures"][local_user]["ed25519:" + device_id],
    
    Hubert Chathi's avatar
    Hubert Chathi committed
                ret["master_keys"][other_user]["signatures"][local_user][
                    "ed25519:" + usersigning_pubkey
                ],
                other_master_key["signatures"][local_user]["ed25519:" + usersigning_pubkey],
    
        def test_query_devices_remote_no_sync(self) -> None:
    
            """Tests that querying keys for a remote user that we don't share a room
            with returns the cross signing keys correctly.
            """
    
            remote_user_id = "@test:other"
            local_user_id = "@test:test"
    
            remote_master_key = "85T7JXPFBAySB/jwby4S3lBPTqY3+Zg53nYuGmu1ggY"
            remote_self_signing_key = "QeIiFEjluPBtI7WQdG365QKZcFs9kqmHir6RBD0//nQ"
    
    
            self.hs.get_federation_client().query_client_keys = mock.AsyncMock(  # type: ignore[method-assign]
    
                return_value={
                    "device_keys": {remote_user_id: {}},
                    "master_keys": {
                        remote_user_id: {
                            "user_id": remote_user_id,
                            "usage": ["master"],
                            "keys": {"ed25519:" + remote_master_key: remote_master_key},
    
                    },
                    "self_signing_keys": {
                        remote_user_id: {
                            "user_id": remote_user_id,
                            "usage": ["self_signing"],
                            "keys": {
                                "ed25519:"
                                + remote_self_signing_key: remote_self_signing_key
                            },
                        }
                    },
                }
    
            )
    
            e2e_handler = self.hs.get_e2e_keys_handler()
    
            query_result = self.get_success(
                e2e_handler.query_devices(
                    {
                        "device_keys": {remote_user_id: []},
                    },
                    timeout=10,
                    from_user_id=local_user_id,
                    from_device_id="some_device_id",
                )
            )
    
            self.assertEqual(query_result["failures"], {})
            self.assertEqual(
                query_result["master_keys"],
                {
                    remote_user_id: {
                        "user_id": remote_user_id,
                        "usage": ["master"],
                        "keys": {"ed25519:" + remote_master_key: remote_master_key},
                    },
                },
            )
            self.assertEqual(
                query_result["self_signing_keys"],
                {
                    remote_user_id: {
                        "user_id": remote_user_id,
                        "usage": ["self_signing"],
                        "keys": {
                            "ed25519:" + remote_self_signing_key: remote_self_signing_key
                        },
                    }
                },
            )
    
    
        def test_has_different_keys(self) -> None:
            """check that has_different_keys returns True when the keys provided are different to what
            is in the database."""
            local_user = "@boris:" + self.hs.hostname
            keys1 = {
                "master_key": {
                    # private key: 2lonYOM6xYKdEsO+6KrC766xBcHnYnim1x/4LFGF8B0
                    "user_id": local_user,
                    "usage": ["master"],
                    "keys": {
                        "ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk": "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk"
                    },
                }
            }
            self.get_success(self.handler.upload_signing_keys_for_user(local_user, keys1))
            is_different = self.get_success(
                self.handler.has_different_keys(
                    local_user,
                    {
                        "master_key": keys1["master_key"],
                    },
                )
            )
            self.assertEqual(is_different, False)
            # change the usage => different keys
            keys1["master_key"]["usage"] = ["develop"]
            is_different = self.get_success(
                self.handler.has_different_keys(
                    local_user,
                    {
                        "master_key": keys1["master_key"],
                    },
                )
            )
            self.assertEqual(is_different, True)
            keys1["master_key"]["usage"] = ["master"]  # reset
            # change the key => different keys
            keys1["master_key"]["keys"] = {
                "ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unIc0rncs": "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unIc0rncs"
            }
            is_different = self.get_success(
                self.handler.has_different_keys(
                    local_user,
                    {
                        "master_key": keys1["master_key"],
                    },
                )
            )
            self.assertEqual(is_different, True)
    
    
        def test_query_devices_remote_sync(self) -> None:
    
            """Tests that querying keys for a remote user that we share a room with,
            but haven't yet fetched the keys for, returns the cross signing keys
            correctly.
            """
    
            remote_user_id = "@test:other"
            local_user_id = "@test:test"
    
    
            # Pretend we're sharing a room with the user we're querying. If not,
            # `_query_devices_for_destination` will return early.
    
            self.store.get_rooms_for_user = mock.AsyncMock(return_value={"some_room_id"})
    
    
            remote_master_key = "85T7JXPFBAySB/jwby4S3lBPTqY3+Zg53nYuGmu1ggY"
            remote_self_signing_key = "QeIiFEjluPBtI7WQdG365QKZcFs9kqmHir6RBD0//nQ"
    
    
            self.hs.get_federation_client().query_user_devices = mock.AsyncMock(  # type: ignore[method-assign]
    
                return_value={
                    "user_id": remote_user_id,
                    "stream_id": 1,
                    "devices": [],
                    "master_key": {
    
                        "usage": ["master"],
                        "keys": {"ed25519:" + remote_master_key: remote_master_key},
                    },
                    "self_signing_key": {
                        "user_id": remote_user_id,
                        "usage": ["self_signing"],
                        "keys": {
                            "ed25519:" + remote_self_signing_key: remote_self_signing_key
    
            )
    
            e2e_handler = self.hs.get_e2e_keys_handler()
    
            query_result = self.get_success(
                e2e_handler.query_devices(
                    {
                        "device_keys": {remote_user_id: []},
                    },
                    timeout=10,
                    from_user_id=local_user_id,
                    from_device_id="some_device_id",
                )
            )
    
            self.assertEqual(query_result["failures"], {})
            self.assertEqual(
                query_result["master_keys"],
                {
                    remote_user_id: {
                        "user_id": remote_user_id,
                        "usage": ["master"],
                        "keys": {"ed25519:" + remote_master_key: remote_master_key},
                    }
                },
            )
            self.assertEqual(
                query_result["self_signing_keys"],
                {
                    remote_user_id: {
                        "user_id": remote_user_id,
                        "usage": ["self_signing"],
                        "keys": {
                            "ed25519:" + remote_self_signing_key: remote_self_signing_key
                        },
                    }
                },
            )
    
        def test_query_devices_remote_down(self) -> None:
            """Tests that querying keys for a remote user on an unreachable server returns
            results in the "failures" property
            """
    
            remote_user_id = "@test:other"
            local_user_id = "@test:test"
    
            # The backoff code treats time zero as special
            self.reactor.advance(5)
    
            self.hs.get_federation_http_client().agent.request = mock.AsyncMock(  # type: ignore[method-assign]
                side_effect=Exception("boop")
            )
    
            e2e_handler = self.hs.get_e2e_keys_handler()
    
            query_result = self.get_success(
                e2e_handler.query_devices(
                    {
                        "device_keys": {remote_user_id: []},
                    },
                    timeout=10,
                    from_user_id=local_user_id,
                    from_device_id="some_device_id",
                )
            )
    
            self.assertEqual(
                query_result["failures"],
                {
                    "other": {
                        "message": "Failed to send request: Exception: boop",
                        "status": 503,
                    }
                },
            )
    
            # Do it again: we should hit the backoff
            query_result = self.get_success(
                e2e_handler.query_devices(
                    {
                        "device_keys": {remote_user_id: []},
                    },
                    timeout=10,
                    from_user_id=local_user_id,
                    from_device_id="some_device_id",
                )
            )
    
            self.assertEqual(
                query_result["failures"],
                {"other": {"message": "Not ready for retry", "status": 503}},
            )
    
    
        @parameterized.expand(
            [
                # The remote homeserver's response indicates that this user has 0/1/2 devices.
                ([],),
                (["device_1"],),
                (["device_1", "device_2"],),
            ]
        )
    
        def test_query_all_devices_caches_result(self, device_ids: Iterable[str]) -> None:
    
            """Test that requests for all of a remote user's devices are cached.
    
            We do this by asserting that only one call over federation was made, and that
            the two queries to the local homeserver produce the same response.
            """
            local_user_id = "@test:test"
            remote_user_id = "@test:other"
    
            request_body: JsonDict = {"device_keys": {remote_user_id: []}}
    
    
            response_devices = [
                {
                    "device_id": device_id,
                    "keys": {
                        "algorithms": ["dummy"],
                        "device_id": device_id,
                        "keys": {f"dummy:{device_id}": "dummy"},
                        "signatures": {device_id: {f"dummy:{device_id}": "dummy"}},
                        "unsigned": {},
                        "user_id": "@test:other",
                    },
                }
                for device_id in device_ids
            ]
    
            response_body = {
                "devices": response_devices,
                "user_id": remote_user_id,
                "stream_id": 12345,  # an integer, according to the spec
            }
    
            e2e_handler = self.hs.get_e2e_keys_handler()
    
            # Pretend we're sharing a room with the user we're querying. If not,
            # `_query_devices_for_destination` will return early.
            mock_get_rooms = mock.patch.object(
                self.store,
                "get_rooms_for_user",
    
                new_callable=mock.AsyncMock,
                return_value=["some_room_id"],
    
            mock_get_users = mock.patch.object(
                self.store,
                "get_users_server_still_shares_room_with",
    
                new_callable=mock.AsyncMock,
                return_value={remote_user_id},
    
            mock_request = mock.patch.object(
                self.hs.get_federation_client(),
                "query_user_devices",
    
                new_callable=mock.AsyncMock,
                return_value=response_body,
    
            with mock_get_rooms, mock_get_users, mock_request as mocked_federation_request:
    
                # Make the first query and sanity check it succeeds.
                response_1 = self.get_success(
                    e2e_handler.query_devices(
                        request_body,
                        timeout=10,
                        from_user_id=local_user_id,
                        from_device_id="some_device_id",
                    )
                )
                self.assertEqual(response_1["failures"], {})
    
                # We should have made a federation request to do so.
                mocked_federation_request.assert_called_once()
    
                # Reset the mock so we can prove we don't make a second federation request.
                mocked_federation_request.reset_mock()
    
                # Repeat the query.
                response_2 = self.get_success(
                    e2e_handler.query_devices(
                        request_body,
                        timeout=10,
                        from_user_id=local_user_id,
                        from_device_id="some_device_id",
                    )
                )
                self.assertEqual(response_2["failures"], {})
    
                # We should not have made a second federation request.
                mocked_federation_request.assert_not_called()
    
                # The two requests to the local homeserver should be identical.
                self.assertEqual(response_1, response_2)
    
    
        @override_config({"experimental_features": {"msc3983_appservice_otk_claims": True}})
        def test_query_appservice(self) -> None:
            local_user = "@boris:" + self.hs.hostname
            device_id_1 = "xyz"
            fallback_key = {"alg1:k1": "fallback_key1"}
            device_id_2 = "abc"
            otk = {"alg1:k2": "key2"}
    
            # Inject an appservice interested in this user.
            appservice = ApplicationService(
                token="i_am_an_app_service",
                id="1234",
    
                namespaces={"users": [{"regex": r"@boris:.+", "exclusive": True}]},
    
                # Note: this user does not have to match the regex above
                sender="@as_main:test",
            )
            self.hs.get_datastores().main.services_cache = [appservice]
            self.hs.get_datastores().main.exclusive_user_regex = _make_exclusive_regex(
                [appservice]
            )
    
            # Setup a response, but only for device 2.
    
            self.appservice_api.claim_client_keys.return_value = (
                {local_user: {device_id_2: otk}},
                [(local_user, device_id_1, "alg1", 1)],
    
            )
    
            # we shouldn't have any unused fallback keys yet
            res = self.get_success(
                self.store.get_e2e_unused_fallback_key_types(local_user, device_id_1)
            )
            self.assertEqual(res, [])
    
            self.get_success(
                self.handler.upload_keys_for_user(
                    local_user,
                    device_id_1,
                    {"fallback_keys": fallback_key},
                )
            )
    
            # we should now have an unused alg1 key
            fallback_res = self.get_success(
                self.store.get_e2e_unused_fallback_key_types(local_user, device_id_1)
            )
            self.assertEqual(fallback_res, ["alg1"])
    
            # claiming an OTK when no OTKs are available should ask the appservice, then
            # query the fallback keys.
            claim_res = self.get_success(
                self.handler.claim_one_time_keys(
    
                    {local_user: {device_id_1: {"alg1": 1}, device_id_2: {"alg1": 1}}},
    
                )
            )
            self.assertEqual(
                claim_res,
                {
                    "failures": {},
                    "one_time_keys": {
                        local_user: {device_id_1: fallback_key, device_id_2: otk}
                    },
                },
            )
    
        @override_config({"experimental_features": {"msc3983_appservice_otk_claims": True}})
        def test_query_appservice_with_fallback(self) -> None:
            local_user = "@boris:" + self.hs.hostname
            device_id_1 = "xyz"
            fallback_key = {"alg1:k1": {"desc": "fallback_key1", "fallback": True}}
            otk = {"alg1:k2": {"desc": "key2"}}
            as_fallback_key = {"alg1:k3": {"desc": "fallback_key3", "fallback": True}}
            as_otk = {"alg1:k4": {"desc": "key4"}}
    
            # Inject an appservice interested in this user.
            appservice = ApplicationService(
                token="i_am_an_app_service",
                id="1234",
                namespaces={"users": [{"regex": r"@boris:.+", "exclusive": True}]},
                # Note: this user does not have to match the regex above
                sender="@as_main:test",
            )
            self.hs.get_datastores().main.services_cache = [appservice]
            self.hs.get_datastores().main.exclusive_user_regex = _make_exclusive_regex(
                [appservice]
            )
    
            # Setup a response.
    
            response: Dict[str, Dict[str, Dict[str, JsonDict]]] = {
                local_user: {device_id_1: {**as_otk, **as_fallback_key}}
            }
            self.appservice_api.claim_client_keys.return_value = (response, [])
    
    
            # Claim OTKs, which will ask the appservice and do nothing else.
            claim_res = self.get_success(
                self.handler.claim_one_time_keys(
    
                    {local_user: {device_id_1: {"alg1": 1}}},
    
                    timeout=None,
                    always_include_fallback_keys=True,
                )
            )
            self.assertEqual(
                claim_res,
                {
                    "failures": {},
                    "one_time_keys": {
                        local_user: {device_id_1: {**as_otk, **as_fallback_key}}
                    },
                },
            )
    
            # Now upload a fallback key.
            res = self.get_success(
                self.store.get_e2e_unused_fallback_key_types(local_user, device_id_1)
            )
            self.assertEqual(res, [])
    
            self.get_success(
                self.handler.upload_keys_for_user(
                    local_user,
                    device_id_1,
                    {"fallback_keys": fallback_key},
                )
            )
    
            # we should now have an unused alg1 key
            fallback_res = self.get_success(
                self.store.get_e2e_unused_fallback_key_types(local_user, device_id_1)
            )
            self.assertEqual(fallback_res, ["alg1"])
    
            # The appservice will return only the OTK.
    
            self.appservice_api.claim_client_keys.return_value = (
                {local_user: {device_id_1: as_otk}},
                [],
    
            )
    
            # Claim OTKs, which should return the OTK from the appservice and the
            # uploaded fallback key.
            claim_res = self.get_success(
                self.handler.claim_one_time_keys(
    
                    {local_user: {device_id_1: {"alg1": 1}}},
    
                    timeout=None,
                    always_include_fallback_keys=True,
                )
            )
            self.assertEqual(
                claim_res,
                {
                    "failures": {},
                    "one_time_keys": {
                        local_user: {device_id_1: {**as_otk, **fallback_key}}
                    },
                },
            )
    
            # But the fallback key should not be marked as used.
            fallback_res = self.get_success(
                self.store.get_e2e_unused_fallback_key_types(local_user, device_id_1)
            )
            self.assertEqual(fallback_res, ["alg1"])
    
            # Now upload a OTK.
            self.get_success(
                self.handler.upload_keys_for_user(
                    local_user,
                    device_id_1,
                    {"one_time_keys": otk},
                )
            )
    
            # Claim OTKs, which will return information only from the database.
            claim_res = self.get_success(
                self.handler.claim_one_time_keys(
    
                    {local_user: {device_id_1: {"alg1": 1}}},
    
                    timeout=None,
                    always_include_fallback_keys=True,
                )
            )
            self.assertEqual(
                claim_res,
                {
                    "failures": {},
                    "one_time_keys": {local_user: {device_id_1: {**otk, **fallback_key}}},
                },
            )
    
            # But the fallback key should not be marked as used.
            fallback_res = self.get_success(
                self.store.get_e2e_unused_fallback_key_types(local_user, device_id_1)
            )
            self.assertEqual(fallback_res, ["alg1"])
    
            # Finally, return only the fallback key from the appservice.
    
            self.appservice_api.claim_client_keys.return_value = (
                {local_user: {device_id_1: as_fallback_key}},
                [],
    
            )
    
            # Claim OTKs, which will return only the fallback key from the database.
            claim_res = self.get_success(
                self.handler.claim_one_time_keys(
    
                    {local_user: {device_id_1: {"alg1": 1}}},
    
                    timeout=None,
                    always_include_fallback_keys=True,
                )
            )
            self.assertEqual(
                claim_res,
                {
                    "failures": {},
                    "one_time_keys": {local_user: {device_id_1: as_fallback_key}},
                },
            )
    
    
        @override_config({"experimental_features": {"msc3984_appservice_key_query": True}})
        def test_query_local_devices_appservice(self) -> None:
            """Test that querying of appservices for keys overrides responses from the database."""
            local_user = "@boris:" + self.hs.hostname
            device_1 = "abc"
            device_2 = "def"
            device_3 = "ghi"
    
            # There are 3 devices:
            #
            # 1. One which is uploaded to the homeserver.
            # 2. One which is uploaded to the homeserver, but a newer copy is returned
            #     by the appservice.
            # 3. One which is only returned by the appservice.
            device_key_1: JsonDict = {
                "user_id": local_user,
                "device_id": device_1,
                "algorithms": [
                    "m.olm.curve25519-aes-sha2",
                    RoomEncryptionAlgorithms.MEGOLM_V1_AES_SHA2,
                ],
                "keys": {
                    "ed25519:abc": "base64+ed25519+key",
                    "curve25519:abc": "base64+curve25519+key",
                },
                "signatures": {local_user: {"ed25519:abc": "base64+signature"}},
            }
            device_key_2a: JsonDict = {
                "user_id": local_user,
                "device_id": device_2,
                "algorithms": [
                    "m.olm.curve25519-aes-sha2",
                    RoomEncryptionAlgorithms.MEGOLM_V1_AES_SHA2,
                ],
                "keys": {
                    "ed25519:def": "base64+ed25519+key",
                    "curve25519:def": "base64+curve25519+key",
                },
                "signatures": {local_user: {"ed25519:def": "base64+signature"}},
            }
    
            device_key_2b: JsonDict = {
                "user_id": local_user,
                "device_id": device_2,
                "algorithms": [
                    "m.olm.curve25519-aes-sha2",
                    RoomEncryptionAlgorithms.MEGOLM_V1_AES_SHA2,
                ],
                # The device ID is the same (above), but the keys are different.
                "keys": {
                    "ed25519:xyz": "base64+ed25519+key",
                    "curve25519:xyz": "base64+curve25519+key",
                },
                "signatures": {local_user: {"ed25519:xyz": "base64+signature"}},
            }
            device_key_3: JsonDict = {
                "user_id": local_user,
                "device_id": device_3,
                "algorithms": [
                    "m.olm.curve25519-aes-sha2",
                    RoomEncryptionAlgorithms.MEGOLM_V1_AES_SHA2,
                ],
                "keys": {
                    "ed25519:jkl": "base64+ed25519+key",
                    "curve25519:jkl": "base64+curve25519+key",
                },
                "signatures": {local_user: {"ed25519:jkl": "base64+signature"}},
            }
    
            # Upload keys for devices 1 & 2a.
            self.get_success(
                self.handler.upload_keys_for_user(
                    local_user, device_1, {"device_keys": device_key_1}
                )
            )
            self.get_success(
                self.handler.upload_keys_for_user(
                    local_user, device_2, {"device_keys": device_key_2a}
                )
            )
    
            # Inject an appservice interested in this user.
            appservice = ApplicationService(
                token="i_am_an_app_service",
                id="1234",
                namespaces={"users": [{"regex": r"@boris:.+", "exclusive": True}]},
                # Note: this user does not have to match the regex above
                sender="@as_main:test",
            )
            self.hs.get_datastores().main.services_cache = [appservice]
            self.hs.get_datastores().main.exclusive_user_regex = _make_exclusive_regex(
                [appservice]
            )
    
            # Setup a response.
    
            self.appservice_api.query_keys.return_value = {
                "device_keys": {
                    local_user: {device_2: device_key_2b, device_3: device_key_3}
    
    
            # Request all devices.
            res = self.get_success(self.handler.query_local_devices({local_user: None}))
            self.assertIn(local_user, res)
            for res_key in res[local_user].values():
                res_key.pop("unsigned", None)
            self.assertDictEqual(
                res,
                {
                    local_user: {
                        device_1: device_key_1,
                        device_2: device_key_2b,
                        device_3: device_key_3,
                    }
                },
            )
    
    
        def test_check_cross_signing_setup(self) -> None:
            # First check what happens with no master key.
            alice = "@alice:test"
            exists, replaceable_without_uia = self.get_success(
                self.handler.check_cross_signing_setup(alice)
            )
            self.assertIs(exists, False)
            self.assertIs(replaceable_without_uia, False)
    
            # Upload a master key but don't specify a replacement timestamp.
            dummy_key = {"keys": {"a": "b"}}
            self.get_success(
                self.store.set_e2e_cross_signing_key("@alice:test", "master", dummy_key)
            )
    
            # Should now find the key exists.
            exists, replaceable_without_uia = self.get_success(
                self.handler.check_cross_signing_setup(alice)
            )
            self.assertIs(exists, True)
            self.assertIs(replaceable_without_uia, False)
    
            # Set an expiry timestamp in the future.
            self.get_success(
                self.store.allow_master_cross_signing_key_replacement_without_uia(
                    alice,
                    1000,
                )
            )
    
            # Should now be allowed to replace the key without UIA.
            exists, replaceable_without_uia = self.get_success(
                self.handler.check_cross_signing_setup(alice)
            )
            self.assertIs(exists, True)
            self.assertIs(replaceable_without_uia, True)
    
            # Wait 2 seconds, so that the timestamp is in the past.
            self.reactor.advance(2.0)
    
            # Should no longer be allowed to replace the key without UIA.
            exists, replaceable_without_uia = self.get_success(
                self.handler.check_cross_signing_setup(alice)
            )
            self.assertIs(exists, True)
            self.assertIs(replaceable_without_uia, False)
    
    
        def test_delete_old_one_time_keys(self) -> None:
            """Test the db migration that clears out old OTKs"""
    
            # We upload two sets of keys, one just over a week ago, and one just less than
            # a week ago. Each batch contains some keys that match the deletion pattern
            # (key IDs of 6 chars), and some that do not.
            #
            # Finally, set the scheduled task going, and check what gets deleted.
    
            user_id = "@user000:" + self.hs.hostname
            device_id = "xyz"
    
            # The scheduled task should be for "now" in real, wallclock time, so
            # set the test reactor to just over a week ago.
            self.reactor.advance(time.time() - 7.5 * 24 * 3600)
    
            # Upload some keys
            self.get_success(
                self.handler.upload_keys_for_user(
                    user_id,
                    device_id,
                    {
                        "one_time_keys": {
                            # some keys to delete
                            "alg1:AAAAAA": "key1",
                            "alg2:AAAAAB": {"key": "key2", "signatures": {"k1": "sig1"}},
                            # A key to *not* delete
                            "alg2:AAAAAAAAAA": {"key": "key3"},
                        }
                    },
                )
            )
    
            # A day passes
            self.reactor.advance(24 * 3600)
    
            # Upload some more keys
            self.get_success(
                self.handler.upload_keys_for_user(
                    user_id,
                    device_id,
                    {
                        "one_time_keys": {
                            # some keys which match the pattern
                            "alg1:BAAAAA": "key1",
                            "alg2:BAAAAB": {"key": "key2", "signatures": {"k1": "sig1"}},
                            # A key to *not* delete
                            "alg2:BAAAAAAAAA": {"key": "key3"},
                        }
                    },
                )
            )
    
            # The rest of the week passes, which should set the scheduled task going.
            self.reactor.advance(6.5 * 24 * 3600)
    
            # Check what we're left with in the database
            remaining_key_ids = {
                row[0]
                for row in self.get_success(
                    self.handler.store.db_pool.simple_select_list(
                        "e2e_one_time_keys_json", None, ["key_id"]
                    )
                )
            }
            self.assertEqual(
                remaining_key_ids, {"AAAAAAAAAA", "BAAAAA", "BAAAAB", "BAAAAAAAAA"}
            )