From 8c3c35e95de121021df7a475d7682f22c455d0b7 Mon Sep 17 00:00:00 2001 From: Paul Chobert Date: Fri, 9 Oct 2026 15:24:07 +0200 Subject: [PATCH] Drop federation signing key updates from non-compliant user IDs --- synapse/handlers/e2e_keys.py | 9 +++++ tests/federation/test_federation_devices.py | 39 +++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/synapse/handlers/e2e_keys.py b/synapse/handlers/e2e_keys.py index 1594112602..93543ddb5c 100644 --- a/synapse/handlers/e2e_keys.py +++ b/synapse/handlers/e2e_keys.py @@ -44,6 +44,7 @@ from synapse.types import ( UserID, get_domain_from_id, get_verify_key_from_cross_signing_key, + is_compliant_user_id, ) from synapse.util.async_helpers import Linearizer, concurrently_execute from synapse.util.cancellation import cancellable @@ -1835,6 +1836,14 @@ class SigningKeyEduUpdater: logger.warning("Got signing key update edu for %r from %r", user_id, origin) return + if not is_compliant_user_id(user_id): + logger.warning( + "Dropping signing key update edu for non-compliant user ID %r from %r", + user_id, + origin, + ) + return + room_ids = await self.store.get_rooms_for_user(user_id) if not room_ids: # We don't share any rooms with this user. Ignore update, as we diff --git a/tests/federation/test_federation_devices.py b/tests/federation/test_federation_devices.py index 1ea5e7d3cb..bff6884224 100644 --- a/tests/federation/test_federation_devices.py +++ b/tests/federation/test_federation_devices.py @@ -23,6 +23,7 @@ from unittest.mock import AsyncMock, Mock from twisted.internet.testing import MemoryReactor +from synapse.api.constants import EduTypes from synapse.handlers.device import DeviceListUpdater from synapse.server import HomeServer from synapse.types import JsonDict @@ -155,6 +156,44 @@ class DeviceListResyncTestCase(unittest.HomeserverTestCase): ) self.assertNotIn(remote_user_id, need_resync) + def test_non_compliant_user_id_signing_key_update_dropped(self) -> None: + """Tests that signing key updates from non-compliant (grandfathered + historical) user IDs are dropped, while those from compliant user IDs + sharing the same room are stored. + """ + compliant_user_id = "@john:test_remote" + non_compliant_user_id = "@héllo:test_remote" + remote_origin = "test_remote" + remote_master_key = "85T7JXPFBAySB/jwby4S3lBPTqY3+Zg53nYuGmu1ggY" + + # Share room with both users. + self.store.get_rooms_for_user = AsyncMock(return_value=["!someroom:test"]) + + federation_registry = self.hs.get_federation_registry() + for user_id in (compliant_user_id, non_compliant_user_id): + self.get_success( + federation_registry.on_edu( + EduTypes.SIGNING_KEY_UPDATE, + remote_origin, + { + "user_id": user_id, + "master_key": { + "user_id": user_id, + "usage": ["master"], + "keys": {"ed25519:" + remote_master_key: remote_master_key}, + }, + }, + ) + ) + + keys = self.get_success( + self.store.get_e2e_cross_signing_keys_bulk( + user_ids=[compliant_user_id, non_compliant_user_id] + ), + ) + self.assertIsNotNone(keys[compliant_user_id]) + self.assertIsNone(keys[non_compliant_user_id]) + def test_cross_signing_keys_retry(self) -> None: """Tests that resyncing a device list correctly processes cross-signing keys from the remote server.