mirror of
https://github.com/element-hq/synapse.git
synced 2026-11-04 17:47:28 +00:00
MSC4430 identifies room members by a per-user, per-room Ed25519 "member key" rather than a user ID, and lets a member's `m.room.member` event carry the user ID it claims to be (`content.localpart`, `content.domain`), co-signed by the claimed domain's server signing key. This adds the two tables and the store behind that, with no callers: nothing in the server creates keys or learns links yet, and the member key room version cannot be used. `msc4430_member_keys` holds the keys we hold for our own users, one per (local user, room), with the private seed. `MemberKeysWorkerStore.create_local_member_key` is insert-if-absent and returns whichever key is stored, so a user's key in a room can never change once it exists, however many concurrent callers try (the earlier monolithic attempt raised `IntegrityError` here). The store derives the public form from the signing key it is given, so the two columns cannot disagree, and rejects any key ID other than `ed25519:1`. Every lookup by key takes the room the key was seen in: a key of ours from room A appearing in room B's events does not resolve there (the earlier attempt mapped keys without a room and gave users phantom memberships). Lookups are cached per (room, user), per (room, key), per user and per room, and `create_local_member_key` invalidates all four on the cache stream, so workers see new keys. `msc4430_member_links` holds what we have learned about keys we do not hold: the claimed domain (routing information, kept whether verified or not), the claimed localpart (NULL once the event that carried it is redacted, after which the key maps to no user), and whether the link is verified. A verified link always names the `m.room.member` event and the server signing key ID that proved it; the `MemberLink` type and a CHECK constraint both enforce this, so "verified" means exactly one thing. Our own keys get no link row: a link is a claim someone else made, and the keys we hold are already known to be ours. `upsert_member_link` is a plain replace that returns the previously stored link, so a caller can see what changed (the mapped user, the domain); which events may update a link is the caller's policy, not the store's, and `get_member_link_txn` plus `upsert_member_link_txn` let it read, decide and write in one transaction. `mark_links_unverified(domain, key_id)` downgrades every link a rotated or withdrawn server key vouched for, in one bulk cache invalidation, keeping the event and key ID so the links can be found again (`get_links_for_signing_key`) if the events are re-signed; a key ID alone does not identify a key, so re-checking a link means re-checking the co-signature on its event against the domain's current key material, which the schema says. `get_member_keys_for_domain_in_room(verified_only=...)` separates routing (claimed) from trust (verified), and answers for our own server name from the keys we hold, never from links: a link claiming our domain for a key we do not hold is somebody else's claim. `get_member_links_for_claimed_user` is the reverse mapping; it returns every claimant (verified first) rather than picking one, because an unverified claim is anyone's to make and that lookup is how a user ID in an invite, kick or power level change becomes a key. `clear_link_localpart_for_redacted_event_txn` is the erasure hook (it clears every link learned from the redacted event). Purging a room clears its links; the keys we hold deliberately survive a purge, since a user's key in a room must not change if the room is re-joined. The stored seed cannot be revoked (MSC4430 has no member key rotation; only rotating the server key helps), and the schema says so. The store is mixed into `RoomMemberWorkerStore` (so it is present on workers) rather than registered on `DataStore`, because the membership queries are the code that will need it. `LocalMemberKey` and `MemberLink` are frozen attrs types in `synapse/storage/member_keys.py`, next to the other storage return types; `SigningKey` is added to the mypy plugin's immutable types so a `LocalMemberKey` can be a cached value. `synapse_port_db` learns the boolean column. `MemberKeyHandler.is_local_identity` (and so `hs.is_mine_identity`) now answers for member keys by looking the key up in its room, instead of raising. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>