diff --git a/synapse/api/room_versions.py b/synapse/api/room_versions.py index 71ef5952c3..1e963b90b5 100644 --- a/synapse/api/room_versions.py +++ b/synapse/api/room_versions.py @@ -116,6 +116,8 @@ class RoomVersion: msc4289_creator_power_enabled: bool # MSC4291: Room IDs as hashes of the create event msc4291_room_ids_as_hashes: bool + # MSC4243: User ID localparts as Account Keys + msc4243_account_keys: bool class RoomVersions: @@ -140,6 +142,7 @@ class RoomVersions: msc3757_enabled=False, msc4289_creator_power_enabled=False, msc4291_room_ids_as_hashes=False, + msc4243_account_keys=False, ) V2 = RoomVersion( "2", @@ -162,6 +165,7 @@ class RoomVersions: msc3757_enabled=False, msc4289_creator_power_enabled=False, msc4291_room_ids_as_hashes=False, + msc4243_account_keys=False, ) V3 = RoomVersion( "3", @@ -184,6 +188,7 @@ class RoomVersions: msc3757_enabled=False, msc4289_creator_power_enabled=False, msc4291_room_ids_as_hashes=False, + msc4243_account_keys=False, ) V4 = RoomVersion( "4", @@ -206,6 +211,7 @@ class RoomVersions: msc3757_enabled=False, msc4289_creator_power_enabled=False, msc4291_room_ids_as_hashes=False, + msc4243_account_keys=False, ) V5 = RoomVersion( "5", @@ -228,6 +234,7 @@ class RoomVersions: msc3757_enabled=False, msc4289_creator_power_enabled=False, msc4291_room_ids_as_hashes=False, + msc4243_account_keys=False, ) V6 = RoomVersion( "6", @@ -250,6 +257,7 @@ class RoomVersions: msc3757_enabled=False, msc4289_creator_power_enabled=False, msc4291_room_ids_as_hashes=False, + msc4243_account_keys=False, ) V7 = RoomVersion( "7", @@ -272,6 +280,7 @@ class RoomVersions: msc3757_enabled=False, msc4289_creator_power_enabled=False, msc4291_room_ids_as_hashes=False, + msc4243_account_keys=False, ) V8 = RoomVersion( "8", @@ -294,6 +303,7 @@ class RoomVersions: msc3757_enabled=False, msc4289_creator_power_enabled=False, msc4291_room_ids_as_hashes=False, + msc4243_account_keys=False, ) V9 = RoomVersion( "9", @@ -316,6 +326,7 @@ class RoomVersions: msc3757_enabled=False, msc4289_creator_power_enabled=False, msc4291_room_ids_as_hashes=False, + msc4243_account_keys=False, ) V10 = RoomVersion( "10", @@ -338,6 +349,7 @@ class RoomVersions: msc3757_enabled=False, msc4289_creator_power_enabled=False, msc4291_room_ids_as_hashes=False, + msc4243_account_keys=False, ) MSC1767v10 = RoomVersion( # MSC1767 (Extensible Events) based on room version "10" @@ -361,6 +373,7 @@ class RoomVersions: msc3757_enabled=False, msc4289_creator_power_enabled=False, msc4291_room_ids_as_hashes=False, + msc4243_account_keys=False, ) MSC3757v10 = RoomVersion( # MSC3757 (Restricting who can overwrite a state event) based on room version "10" @@ -384,6 +397,7 @@ class RoomVersions: msc3757_enabled=True, msc4289_creator_power_enabled=False, msc4291_room_ids_as_hashes=False, + msc4243_account_keys=False, ) V11 = RoomVersion( "11", @@ -406,6 +420,7 @@ class RoomVersions: msc3757_enabled=False, msc4289_creator_power_enabled=False, msc4291_room_ids_as_hashes=False, + msc4243_account_keys=False, ) MSC3757v11 = RoomVersion( # MSC3757 (Restricting who can overwrite a state event) based on room version "11" @@ -429,6 +444,7 @@ class RoomVersions: msc3757_enabled=True, msc4289_creator_power_enabled=False, msc4291_room_ids_as_hashes=False, + msc4243_account_keys=False, ) HydraV11 = RoomVersion( "org.matrix.hydra.11", @@ -451,6 +467,7 @@ class RoomVersions: msc3757_enabled=False, msc4289_creator_power_enabled=True, # Changed from v11 msc4291_room_ids_as_hashes=True, # Changed from v11 + msc4243_account_keys=False, ) V12 = RoomVersion( "12", @@ -473,6 +490,30 @@ class RoomVersions: msc3757_enabled=False, msc4289_creator_power_enabled=True, # Changed from v11 msc4291_room_ids_as_hashes=True, # Changed from v11 + msc4243_account_keys=False, + ) + MSC4243v12 = RoomVersion( + "org.matrix.12.4243", + RoomDisposition.STABLE, + EventFormatVersions.ROOM_V11_HYDRA_PLUS, + StateResolutionVersions.V2_1, # Changed from v11 + enforce_key_validity=False, # No longer enforce key validity. + special_case_aliases_auth=False, + strict_canonicaljson=True, + limit_notifications_power_levels=True, + implicit_room_creator=True, # Used by MSC3820 + updated_redaction_rules=True, # Used by MSC3820 + restricted_join_rule=True, + restricted_join_rule_fix=True, + knock_join_rule=True, + msc3389_relation_redactions=False, + knock_restricted_join_rule=True, + enforce_int_power_levels=True, + msc3931_push_features=(), + msc3757_enabled=False, + msc4289_creator_power_enabled=True, + msc4291_room_ids_as_hashes=True, + msc4243_account_keys=True, ) @@ -494,6 +535,7 @@ KNOWN_ROOM_VERSIONS: Dict[str, RoomVersion] = { RoomVersions.MSC3757v10, RoomVersions.MSC3757v11, RoomVersions.HydraV11, + RoomVersions.MSC4243v12, ) } diff --git a/synapse/crypto/keyring.py b/synapse/crypto/keyring.py index 8c59772e56..04c49663fa 100644 --- a/synapse/crypto/keyring.py +++ b/synapse/crypto/keyring.py @@ -47,7 +47,7 @@ from synapse.events import EventBase from synapse.events.utils import prune_event_dict from synapse.logging.context import make_deferred_yieldable, run_in_background from synapse.storage.keys import FetchKeyResult -from synapse.types import JsonDict +from synapse.types import JsonDict, UserID from synapse.util import unwrapFirstError from synapse.util.async_helpers import yieldable_gather_results from synapse.util.batching_queue import BatchingQueue @@ -83,6 +83,7 @@ class VerifyJsonRequest: get_json_object: Callable[[], JsonDict] minimum_valid_until_ts: int key_ids: List[str] + key_ids_are_public_keys: bool = False @staticmethod def from_json_object( @@ -118,6 +119,7 @@ class VerifyJsonRequest: lambda: prune_event_dict(event.room_version, event.get_pdu_json()), minimum_valid_until_ms, key_ids=key_ids, + key_ids_are_public_keys=False, ) @@ -265,6 +267,38 @@ class Keyring: ) ) + async def verify_event_for_account_key( + self, + user_id_str: str, + event: EventBase, + ) -> None: + """Verify that the given event has been signed by the provided account key, as determined by + the user_id provided. + + Args: + user_id_str: The MSC4243 user ID, consisting of an account key localpart and a domain. + event: The PDU that should be signed with the account key. Room version must support MSC4243. + """ + assert event.room_version.msc4243_account_keys + user_id = UserID.from_string(user_id_str) + key_ids = list(event.signatures.get(user_id.domain, [])) + # only keep the key ID that matches the desired user ID we want to verify as. + # Events can be signed by multiple parties e.g invites, restricted joins + expected_key_id = "ed25519:" + user_id.localpart + key_ids = [key_id for key_id in key_ids if key_id == expected_key_id] + assert len(key_ids) == 1 # the user must have signed the event. + await self.process_request( + VerifyJsonRequest( + user_id.domain, + # We defer creating the redacted json object, as it uses a lot more + # memory than the Event object itself. + lambda: prune_event_dict(event.room_version, event.get_pdu_json()), + 0, # No validity times + key_ids=key_ids, + key_ids_are_public_keys=True, + ) + ) + async def process_request(self, verify_request: VerifyJsonRequest) -> None: """Processes the `VerifyJsonRequest`. Raises if the object is not signed by the server, the signatures don't match or we failed to fetch the @@ -278,6 +312,15 @@ class Keyring: Codes.UNAUTHORIZED, ) + if verify_request.key_ids_are_public_keys: + # No need to fetch keys as we have them already. + assert len(verify_request.key_ids) == 1 + key_id = verify_request.key_ids[0] + key_bytes = decode_base64(key_id.removeprefix("ed25519:")) + verify_key = decode_verify_key_bytes(key_id, key_bytes) + await self._process_json(verify_key, verify_request) + return + found_keys: Dict[str, FetchKeyResult] = {} # If we are the originating server, short-circuit the key-fetch for any keys diff --git a/synapse/federation/federation_base.py b/synapse/federation/federation_base.py index a1c9c286ac..b03f0a9fd7 100644 --- a/synapse/federation/federation_base.py +++ b/synapse/federation/federation_base.py @@ -241,16 +241,28 @@ async def _check_sigs_on_pdu( sender_domain = get_domain_from_id(pdu.sender) if not _is_invite_via_3pid(pdu): try: - await keyring.verify_event_for_server( - sender_domain, - pdu, - pdu.origin_server_ts if room_version.enforce_key_validity else 0, - ) + if pdu.room_version.msc4243_account_keys: + await keyring.verify_event_for_account_key( + pdu.sender, + pdu, + ) + else: + await keyring.verify_event_for_server( + sender_domain, + pdu, + pdu.origin_server_ts if room_version.enforce_key_validity else 0, + ) except Exception as e: - raise InvalidEventSignatureError( - f"unable to verify signature for sender domain {sender_domain}: {e}", - pdu.event_id, - ) from None + if pdu.room_version.msc4243_account_keys: + raise InvalidEventSignatureError( + f"unable to verify signature for account key {pdu.sender}: {e}", + pdu.event_id, + ) from None + else: + raise InvalidEventSignatureError( + f"unable to verify signature for sender domain {sender_domain}: {e}", + pdu.event_id, + ) from None # now let's look for events where the sender's domain is different to the # event id's domain (normally only the case for joins/leaves), and add additional @@ -283,11 +295,17 @@ async def _check_sigs_on_pdu( pdu.content[EventContentFields.AUTHORISING_USER] ) try: - await keyring.verify_event_for_server( - authorising_server, - pdu, - pdu.origin_server_ts if room_version.enforce_key_validity else 0, - ) + if pdu.room_version.msc4243_account_keys: + await keyring.verify_event_for_account_key( + pdu.content[EventContentFields.AUTHORISING_USER], + pdu, + ) + else: + await keyring.verify_event_for_server( + authorising_server, + pdu, + pdu.origin_server_ts if room_version.enforce_key_validity else 0, + ) except Exception as e: raise InvalidEventSignatureError( f"unable to verify signature for authorising serve {authorising_server}: {e}", diff --git a/tests/crypto/test_keyring.py b/tests/crypto/test_keyring.py index 80f9bd097e..79ee36beb9 100644 --- a/tests/crypto/test_keyring.py +++ b/tests/crypto/test_keyring.py @@ -34,12 +34,15 @@ from twisted.internet.defer import Deferred, ensureDeferred from twisted.internet.testing import MemoryReactor from synapse.api.errors import SynapseError +from synapse.api.room_versions import RoomVersions from synapse.crypto import keyring +from synapse.crypto.event_signing import compute_event_signature from synapse.crypto.keyring import ( PerspectivesKeyFetcher, ServerKeyFetcher, StoreKeyFetcher, ) +from synapse.events import make_event_from_dict from synapse.logging.context import ( ContextRequest, LoggingContext, @@ -388,6 +391,35 @@ class KeyringTestCase(unittest.HomeserverTestCase): mock_fetcher1.get_keys.assert_called_once() mock_fetcher2.get_keys.assert_called_once() + def test_verify_event_for_account_key(self) -> None: + """Test basic functionality of verify_event_for_account_key. + - That it parses the user ID correctly. + - That it doesn't rely on key fetchers. + """ + room_version = RoomVersions.MSC4243v12 + + # Make a signing key and replace the key ID from '1' to be the base64 public key + signing_key = signedjson.key.generate_signing_key("1") + verify_key_str = encode_verify_key_base64(get_verify_key(signing_key)) + signing_key.version = verify_key_str + domain = "can.be.anything.com" + signing_user_id = f"@{verify_key_str}:{domain}" + + event_dict = { + "type": "m.room.create", + "state_key": "", + "sender": signing_user_id, + "content": { + "room_version": room_version.identifier, + }, + } + event_dict["signatures"] = compute_event_signature( + room_version, event_dict, signature_name=domain, signing_key=signing_key + ) + event = make_event_from_dict(event_dict, room_version) + kr = keyring.Keyring(self.hs, key_fetchers=None) + self.get_success(kr.verify_event_for_account_key(signing_user_id, event)) + @logcontext_clean class ServerKeyFetcherTestCase(unittest.HomeserverTestCase):