mirror of
https://github.com/element-hq/synapse.git
synced 2026-11-07 08:38:02 +00:00
With `msc4430_enabled` on, the member key room version is registered and a user can create such a room, join it, send events, change their own membership, invite another user of this server and leave, all by user ID as today, while every event in the room names member keys and is signed by them. One homeserver only; members of other servers, moderation (kick, ban, power levels), restricted joins and what clients are served back (`/sync`, `/event`, `/members`) are outside this change. Where a user ID becomes a key. Clients speak in user IDs, so the translation happens once, in `EventCreationHandler.create_event`, before the event is built: `MemberKeyHandler.translate_event_dict` replaces the `sender`, the `state_key` of an `m.room.member` event and the `additional_creators` of a create event with the users' member keys in the room, creating a key for a user who has none (a key never changes, so a user who leaves and re-joins keeps theirs). A membership event about one of our users gets our linkage information (`content.localpart` and `content.domain`); whatever the client put there is dropped first, since linkage information is a server's assertion, and an erased user gets no `localpart`, so their name is not published again. A user ID of another server, as sender, target or additional creator, is refused (403 or 400) rather than quietly sent as a user ID the room cannot hold, and so is a member key the client chose itself as a target or additional creator: a key nobody vouches for would be a seat in the room for whoever holds its private half (the membership servlets still parse user IDs, so a key as a target is refused there too). A room with a remote invitee is refused before anything is created, not by the invite after the room exists, and a room cannot be upgraded into this version (the clone names user IDs in power levels, bans and invites). The handler is also where the rest of the server asks what an identity means: `local_user_id_for_identity` (ours, by user ID or by a key we hold in that room), `user_id_for_identity` (also the verified link of another server's key), `domain_for_identity` (routing only: our name for our keys, the claimed domain of a linked key) and `room_identity_for_user` (how to look a user of ours up in a room's state: their key where they have one, their user ID otherwise, which the state then does not contain, as it should not). Three other places compared the sender with a user ID and now resolve it: a suspended user redacting their own event, the admin-redaction rate limit, and room participation. The create event. In these room versions the room ID is derived from the create event, and the create event names and is signed by the creator's key, so the key has to exist before the room ID does. `translate_event_dict` generates such keys as "pending" (an expiring in-memory cache), they sign the create event from there, and once the event is built `create_event` stores them under the new room ID; if building fails they are dropped at once. A pending key is ours only for the create event itself (no room yet): in any existing room it is nobody's, so a key minted for one room cannot act in another while it waits. A user named twice by one create event (as creator and additional creator) gets one key; a create event sent into an existing room is refused before any key is made; and the room ID collision retry starts each attempt from the content the client sent, not the translated one. Room creation then looks the creator's membership up by their key in the state map it batches the rest of the initial state against, and refuses the version when the flag is off even though the registry (process wide) may know it, which is also why the tests can run flag-off cases. `default_room_version` cannot name this version: the server config validates it before the experimental config registers it, so rooms are created with an explicit `room_version`. Signatures. `EventBuilder.build` asks `MemberKeyHandler.get_signers_for_event_dict` who signs and `create_local_event_from_event_dict` signs with exactly those: the sender's member key under the entity `sender`, if we hold it; our server signing key on an `m.room.member` event whose target is a key we hold and whose linkage information is exactly what we vouch for about that user (our domain, their localpart unless they are erased, in which case none), which is what lets other servers verify the member's link; and the authorising member's key on a restricted join that names one of ours. An event whose sender is not a key we hold gets no signature at all (it is only hashed), so the server never signs as a key it does not own; the co-signature is decided from the store, never from the dict, so no caller can have us vouch for a name that is not the user's; and the signing helper refuses any signer the event may not carry, so a wrong choice here could not produce one either. A third-party rules module that replaces an event is as untrusted as a client: its dict is translated again, and it may not change who authorised a join. The builder's validation accepts canonical member keys as sender and membership target in these rooms, and nothing else. Membership handling. `update_membership_locked` looks the target and the requester up in the room's state under their room identity (their key once they have one), so re-joins, duplicate events and the ban checks see the right membership; the previous membership of a new event is read under the event's own state key; whether this server is in the room is answered from the members that map to our users rather than by domain; a pending invite is read by user ID (that table is keyed by user) and the inviter's server is found through `domain_for_identity`, so a join via a remote inviter would still route; the join notification maps the joining key to its user; and an invite in such a room is refused with 400 unless the invitee is a key we hold. Hosts in a room. Registering the version makes the "which servers are in this room" paths reachable for the first time, and they are what the federation sender consults for every event. They took the domain of every member; a member key has none, and on Postgres the SQL that extracts domains from state keys returned a `None` host for such a room. `get_hosts_for_identities` on the member keys store now gives the server behind each identity (a user ID's domain, our own name for keys we hold, the claimed domain of a key we have a link for, nothing for the rest), and `_get_joined_hosts`, `get_current_hosts_in_room` and `get_current_hosts_in_room_ordered` use it for member key rooms (the ordered form is unordered there: nothing in the state keys to sort by). The sender itself decided whether an event was ours by the sender's domain, which for a key said "not ours" and dropped every event of such a room before reaching any of this; it now asks the identity map, and finds the server of a rescinded invite's target the same way rather than by parsing the key, which would have raised outside the guard that protects the sending loop. A member key room holds only our own members, so its hosts are this server and nothing is sent. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>