Files
synapse/scripts-dev
Kegan DougalandClaude Fable 5.1 0f3e69f9c1 B2: Add storage for MSC4430 member keys and member links
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>
2026-10-15 09:22:38 +02:00
..
2023-12-13 16:15:22 +00:00
2026-09-16 16:13:22 +01:00
2026-02-05 17:11:55 -06:00
2023-12-13 16:37:10 +00:00