diff --git a/synapse/api/constants.py b/synapse/api/constants.py index 103729d880..e9f580a68c 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -356,18 +356,17 @@ class MemberKeys: SIGNATURE_ENTITY: Final = "sender" """The entity name under which the sender's member key signs an event. (The authorising member key of a restricted join signs under the member key string - itself.)""" + itself, and a server vouching for linkage information signs under its name.)""" - LINKAGE_SIGNATURE_ENTITY: Final = "state_key" - """The entity name under which a server co-signs the linkage information of an - `m.room.member` event: the claim that the member key in `state_key` belongs to - `content.localpart` at `content.domain`. Naming the entity for the member it is - about, rather than for the server making the claim, is what lets a third party - tell an assertion about the target from one about the sender: an invite is sent - by one member key and co-signed by the *invitee's* server. The signature is made - with the server's own key, so the key ID is a real server key ID rather than the - member keys' constant `ed25519:1`, and names the published key to check it - against.""" + STATE_KEY_SIGNATURE_ENTITY: Final = "state_key" + """The entity name under which the member key named by an `m.room.member` + event's `state_key` signs it, with the member keys' constant key ID. It is + what asserts the event's linkage information: a server vouches for a claim by + signing under its own name, but only the holder of the member key may make the + claim in the first place, so a membership event sent by somebody else (a kick, + a ban) carries no linkage information that anyone will believe. An event whose + `sender` and `state_key` are the same key needs no such signature: the `sender` + signature already is the member's own.""" KEY_ALGORITHM: Final = "ed25519" KEY_VERSION: Final = "1" diff --git a/synapse/crypto/event_signing.py b/synapse/crypto/event_signing.py index 4e99395ae7..89b15513d0 100644 --- a/synapse/crypto/event_signing.py +++ b/synapse/crypto/event_signing.py @@ -200,10 +200,12 @@ def add_hashes_and_signatures_from_signers( * A member key string (the authorising member of a restricted join, MSC-Q §2): the event must be a join naming that key in `join_authorised_via_users_server`, and the key must be that member key. - * `MemberKeys.LINKAGE_SIGNATURE_ENTITY` (`state_key`), co-signing the linkage + * `MemberKeys.STATE_KEY_SIGNATURE_ENTITY` (`state_key`), asserting the linkage information of the member named by `state_key`: the event must be an - `m.room.member` event whose `content.domain` is us, and the key must be our - server signing key. + `m.room.member` event sent by somebody else, and the key must be the member + key its `state_key` names. + * Our own server name (`server_name`), vouching for linkage information: the + event must be an `m.room.member` event whose `content.domain` is us. Anything else is refused. To add one signature to an event that is already hashed (one received from another server), use @@ -324,14 +326,29 @@ def _check_member_key_room_signer( ) return - # The co-signature on linkage information. The entity is the constant - # `state_key`, naming the member the claim is about; the claim itself is - # `content.domain`, which must be us, since this server signs for nobody else. - if signature_name != MemberKeys.LINKAGE_SIGNATURE_ENTITY: - raise refuse( - "not a signature this event may carry (expected %s)" - % (MemberKeys.LINKAGE_SIGNATURE_ENTITY,) - ) + if signature_name == MemberKeys.STATE_KEY_SIGNATURE_ENTITY: + # The member's own assertion of their linkage information on an event + # somebody else sent about them. + if signing_key.version != MemberKeys.KEY_VERSION: + raise refuse("member keys sign with key ID %s" % (MemberKeys.KEY_ID,)) + state_key = event_dict.get("state_key") + if event_dict.get("type") != EventTypes.Member or not isinstance( + state_key, str + ): + raise refuse("only m.room.member events have a state key to sign for") + if MemberKey.from_signing_key(signing_key).to_string() != state_key: + raise refuse("the signing key is not the event's state key") + if event_dict.get("sender") == state_key: + raise refuse( + "the sender signature is already this member's; a second " + "signature under %s says nothing more" + % (MemberKeys.STATE_KEY_SIGNATURE_ENTITY,) + ) + return + + # Our server name: vouching for linkage information. + if signature_name != server_name: + raise refuse("not this server's name (%s)" % (server_name,)) if event_dict.get("type") != EventTypes.Member: raise refuse("servers only co-sign m.room.member events") if content.get(EventContentFields.MSC4430_DOMAIN) != server_name: diff --git a/synapse/handlers/member_keys.py b/synapse/handlers/member_keys.py index 2328b777da..8060a787db 100644 --- a/synapse/handlers/member_keys.py +++ b/synapse/handlers/member_keys.py @@ -111,6 +111,24 @@ def record_unsupported_path(server_name: str, path: str) -> None: ) +def _may_assert_linkage(event_dict: JsonDict, content: JsonDict) -> bool: + """Whether a membership event we are building may carry its target's linkage + information. + + Only the member themselves may assert who a member key belongs to, by signing + the event with that key: a claim made by anyone else is ignored by every other + server, since otherwise a moderator could kick a member while naming a + localpart and a domain of their choosing and have it believed. So an event the + member sent carries their linkage information, and an invite carries it because + the invitee's own server signs it with their key alongside its own. A kick, a + ban or an unban carries none, and so names nobody: it would be a claim nobody + believes, and would publish a localpart in a room that has no use for it. + """ + return event_dict.get("sender") == event_dict.get("state_key") or ( + content.get(EventContentFields.MEMBERSHIP) == Membership.INVITE + ) + + class MemberKeyHandler: """Resolves room member identities (user IDs or member keys) to users and back, and chooses the member keys and signatures for the events our users create.""" @@ -353,10 +371,11 @@ class MemberKeyHandler: users gets our linkage information. Rooms without member keys are left untouched. - Linkage information is asserted by servers, never by clients: whatever - the client put in `localpart` / `domain` is dropped, and the fields are - filled in only for a target we can vouch for. An erased user gets no - `localpart`, so their name is not published again. + Linkage information is never asserted by clients: whatever the client put + in `localpart` / `domain` is dropped, and the fields are filled in only + for a target we can vouch for, on an event that may assert them at all + (see `_may_assert_linkage`). An erased user gets no `localpart`, so their + name is not published again. The `users` of an `m.room.power_levels` event are translated the same way (user IDs of ours become keys; keys stay as they are, so a client @@ -468,7 +487,7 @@ class MemberKeyHandler: Codes.INVALID_PARAM, ) - if target_user_id is not None: + if target_user_id is not None and _may_assert_linkage(event_dict, content): target = UserID.from_string(target_user_id) content[EventContentFields.MSC4430_DOMAIN] = target.domain if not await self._store.is_user_erased(target_user_id): @@ -544,11 +563,12 @@ class MemberKeyHandler: """Who signs an event we are building in a member key room: * the sender's member key, under the entity `sender`, if we hold it; - * our server signing key, under the entity `state_key`, on an - `m.room.member` event whose target is a key we hold and whose linkage - information is exactly what we vouch for (our domain, and the user's - localpart unless they are erased): that is what lets other servers - verify the member's link; + * on an `m.room.member` event whose target is a key we hold and whose + linkage information is exactly what we vouch for (our domain, and the + user's localpart unless they are erased): that member's own key, under + the entity `state_key`, if somebody else sent the event, asserting the + claim; and our server signing key, under our name, vouching for it. + Together they are what lets other servers verify the member's link; * the authorising member's key, under its own name, on a restricted join that names a key we hold. @@ -584,9 +604,20 @@ class MemberKeyHandler: if target_user_id is not None and await self._is_our_linkage( target_user_id, content ): - signers.append( - (MemberKeys.LINKAGE_SIGNATURE_ENTITY, self._server_signing_key) - ) + if state_key != sender: + # Somebody else's event about our member (an invite): their + # key asserts the claim, since ours is not enough for other + # servers to believe it. An event the member sent themselves + # needs no second signature. + target_signing_key = await self._signing_key_for(room_id, state_key) + if target_signing_key is not None: + signers.append( + ( + MemberKeys.STATE_KEY_SIGNATURE_ENTITY, + target_signing_key, + ) + ) + signers.append((self.server_name, self._server_signing_key)) authorising_member = content.get(EventContentFields.AUTHORISING_USER) if ( diff --git a/tests/crypto/test_event_signing.py b/tests/crypto/test_event_signing.py index 1b7e265bd9..fbeddf33e8 100644 --- a/tests/crypto/test_event_signing.py +++ b/tests/crypto/test_event_signing.py @@ -339,34 +339,101 @@ class MemberKeySigningTestCase(unittest.TestCase): ) def test_server_cosignature(self) -> None: - """The server co-signs linkage information under the entity `state_key`, - naming the member the claim is about rather than itself, with its own key - ID: a verifier reads `content.domain`, fetches that server's published key - of that ID, and checks the `state_key` signature.""" event = self._sign( self._member_event_dict(), (MemberKeys.SIGNATURE_ENTITY, self.member_signing_key), - (MemberKeys.LINKAGE_SIGNATURE_ENTITY, self.server_signing_key), + (self.server_name, self.server_signing_key), ) - # Both signatures are present: the second signer added to the first. The - # co-signature carries the server's own key ID, not the member keys' - # constant `ed25519:1`, and the server's name appears nowhere. + # Both signatures are present: the second signer added to the first. self.assertEqual( - {k: list(v) for k, v in self._signatures(event).items()}, - { - MemberKeys.SIGNATURE_ENTITY: [MemberKeys.KEY_ID], - MemberKeys.LINKAGE_SIGNATURE_ENTITY: ["ed25519:srv"], - }, + set(self._signatures(event)), + {MemberKeys.SIGNATURE_ENTITY, self.server_name}, ) redacted = self._redacted_pdu(event) verify_signed_json( redacted, MemberKeys.SIGNATURE_ENTITY, self.member_key.verify_key() ) + verify_signed_json( + redacted, self.server_name, get_verify_key(self.server_signing_key) + ) + + def test_state_key_signature(self) -> None: + """On an event somebody else sent about a member, the member's own key + signs under the entity `state_key`: that is what asserts the linkage + information, which the server then vouches for under its own name.""" + event_dict = self._member_event_dict() + event_dict["sender"] = self.other_member_key.to_string() + event_dict["content"]["membership"] = "invite" + event = self._sign( + event_dict, + (MemberKeys.SIGNATURE_ENTITY, self.other_signing_key), + (MemberKeys.STATE_KEY_SIGNATURE_ENTITY, self.member_signing_key), + (self.server_name, self.server_signing_key), + ) + + # The member's signature carries the member keys' constant key ID; the + # server's carries its own, which names the published key to check it + # against. + self.assertEqual( + {k: list(v) for k, v in self._signatures(event).items()}, + { + MemberKeys.SIGNATURE_ENTITY: [MemberKeys.KEY_ID], + MemberKeys.STATE_KEY_SIGNATURE_ENTITY: [MemberKeys.KEY_ID], + self.server_name: ["ed25519:srv"], + }, + ) + redacted = self._redacted_pdu(event) + verify_signed_json( + redacted, MemberKeys.SIGNATURE_ENTITY, self.other_member_key.verify_key() + ) verify_signed_json( redacted, - MemberKeys.LINKAGE_SIGNATURE_ENTITY, - get_verify_key(self.server_signing_key), + MemberKeys.STATE_KEY_SIGNATURE_ENTITY, + self.member_key.verify_key(), + ) + verify_signed_json( + redacted, self.server_name, get_verify_key(self.server_signing_key) + ) + + def test_refuses_state_key_signature_by_the_wrong_key(self) -> None: + """A `state_key` signature is the member's own assertion, so it must be + made by the key the event's `state_key` names, on a membership event, and + only where the member is not already the sender.""" + event_dict = self._member_event_dict() + event_dict["sender"] = self.other_member_key.to_string() + + # Another member's key, not the one named by `state_key`. + self._assert_refused( + event_dict, + (MemberKeys.STATE_KEY_SIGNATURE_ENTITY, self.other_signing_key), + "not the event's state key", + ) + # The right key, but the member sent the event themselves: the `sender` + # signature is already theirs. + self._assert_refused( + self._member_event_dict(), + (MemberKeys.STATE_KEY_SIGNATURE_ENTITY, self.member_signing_key), + "says nothing more", + ) + # Not a membership event. + message = self._member_event_dict() + message["type"] = "m.room.message" + del message["state_key"] + self._assert_refused( + message, + (MemberKeys.STATE_KEY_SIGNATURE_ENTITY, self.member_signing_key), + "state key to sign for", + ) + # The key ID is fixed to `ed25519:1` by the MSC. + wrong_version = generate_signing_key("2") + event_dict = self._member_event_dict() + event_dict["sender"] = self.other_member_key.to_string() + event_dict["state_key"] = MemberKey.from_signing_key(wrong_version).to_string() + self._assert_refused( + event_dict, + (MemberKeys.STATE_KEY_SIGNATURE_ENTITY, wrong_version), + "key ID", ) def test_authorising_member_key_signature(self) -> None: @@ -447,7 +514,7 @@ class MemberKeySigningTestCase(unittest.TestCase): event = self._sign( self._member_event_dict(), (MemberKeys.SIGNATURE_ENTITY, self.member_signing_key), - (MemberKeys.LINKAGE_SIGNATURE_ENTITY, self.server_signing_key), + (self.server_name, self.server_signing_key), ) redacted = prune_event(event) @@ -462,9 +529,7 @@ class MemberKeySigningTestCase(unittest.TestCase): pdu, MemberKeys.SIGNATURE_ENTITY, self.member_key.verify_key() ) verify_signed_json( - pdu, - MemberKeys.LINKAGE_SIGNATURE_ENTITY, - get_verify_key(self.server_signing_key), + pdu, self.server_name, get_verify_key(self.server_signing_key) ) # ... and cover `domain`: changing it invalidates both. tampered = copy.deepcopy(pdu) @@ -475,9 +540,7 @@ class MemberKeySigningTestCase(unittest.TestCase): ) with self.assertRaises(SignatureVerifyException): verify_signed_json( - tampered, - MemberKeys.LINKAGE_SIGNATURE_ENTITY, - get_verify_key(self.server_signing_key), + tampered, self.server_name, get_verify_key(self.server_signing_key) ) # The event ID (the reference hash) covers `domain` too... @@ -573,18 +636,21 @@ class MemberKeySigningTestCase(unittest.TestCase): def test_refuses_server_cosignature_on_wrong_event(self) -> None: """A server co-signs only `m.room.member` events whose `content.domain` is itself: our co-signature is a statement about our own user.""" - linkage = MemberKeys.LINKAGE_SIGNATURE_ENTITY - # Another domain's linkage information. + # Another server's name, even if it matches `content.domain`. event_dict = self._member_event_dict() event_dict["content"]["domain"] = "other.example" self._assert_refused( - event_dict, (linkage, self.server_signing_key), "content.domain" + event_dict, ("other.example", self.server_signing_key), "not this server" + ) + # Our name, on another domain's linkage information. + self._assert_refused( + event_dict, (self.server_name, self.server_signing_key), "content.domain" ) # No linkage information at all. event_dict = self._member_event_dict() del event_dict["content"]["domain"] self._assert_refused( - event_dict, (linkage, self.server_signing_key), "content.domain" + event_dict, (self.server_name, self.server_signing_key), "content.domain" ) # Not a membership event. event_dict = self._member_event_dict() @@ -592,21 +658,10 @@ class MemberKeySigningTestCase(unittest.TestCase): del event_dict["state_key"] self._assert_refused( event_dict, - (linkage, self.server_signing_key), + (self.server_name, self.server_signing_key), "only co-sign m.room.member", ) - def test_refuses_signature_under_a_server_name(self) -> None: - """Linkage is co-signed under the constant `state_key`, so a signature - under a server's name — ours or anyone else's — is not a signature a - member key room event may carry.""" - for entity in (self.server_name, "other.example"): - self._assert_refused( - self._member_event_dict(), - (entity, self.server_signing_key), - "not a signature this event may carry", - ) - def test_refuses_non_member_key_room(self) -> None: for fn in ( lambda: add_hashes_and_signatures_from_signers( diff --git a/tests/handlers/test_member_keys.py b/tests/handlers/test_member_keys.py index a69ebb5310..0380edd94e 100644 --- a/tests/handlers/test_member_keys.py +++ b/tests/handlers/test_member_keys.py @@ -209,9 +209,40 @@ class EventTranslationTestCase(unittest.HomeserverTestCase): ) self.assertEqual( self._signers(event_dict), - [MemberKeys.SIGNATURE_ENTITY, MemberKeys.LINKAGE_SIGNATURE_ENTITY], + [ + MemberKeys.SIGNATURE_ENTITY, + MemberKeys.STATE_KEY_SIGNATURE_ENTITY, + self.hs.hostname, + ], ) + def test_moderation_asserts_nothing_about_its_target(self) -> None: + """A kick, ban or unban of one of our users carries no linkage + information, even though we hold the target's key and know who they are. + Only the member's own key may assert who a key belongs to, and it has not + signed this event, so the claim would be ignored by every other server — + and a moderator who could make it would be able to deface a member by + naming a localpart and a domain of their choosing.""" + for membership in ("leave", "ban"): + event_dict = self._translate( + { + "type": EventTypes.Member, + "room_id": ROOM_ID, + "sender": self.alice, + "state_key": self.bob, + "content": { + "membership": membership, + EventContentFields.MSC4430_LOCALPART: "some_abusive_localpart", + EventContentFields.MSC4430_DOMAIN: "evil.example", + }, + } + ) + self.assertEqual(event_dict["state_key"], self._key(self.bob)) + self.assertEqual(event_dict["content"], {"membership": membership}) + self.assertEqual( + self._signers(event_dict), [MemberKeys.SIGNATURE_ENTITY], membership + ) + def test_erased_user_gets_no_localpart(self) -> None: self.get_success(self.store.mark_user_erased(self.bob)) event_dict = self._translate( @@ -232,7 +263,7 @@ class EventTranslationTestCase(unittest.HomeserverTestCase): ) self.assertEqual( self._signers(event_dict), - [MemberKeys.SIGNATURE_ENTITY, MemberKeys.LINKAGE_SIGNATURE_ENTITY], + [MemberKeys.SIGNATURE_ENTITY, self.hs.hostname], ) def test_remote_target_is_refused(self) -> None: @@ -305,11 +336,7 @@ class EventTranslationTestCase(unittest.HomeserverTestCase): ) self.assertEqual( self._signers(event_dict), - [ - MemberKeys.SIGNATURE_ENTITY, - MemberKeys.LINKAGE_SIGNATURE_ENTITY, - alice_key, - ], + [MemberKeys.SIGNATURE_ENTITY, self.hs.hostname, alice_key], ) def _key_for(self, user_id: str) -> str: @@ -466,7 +493,7 @@ class EventTranslationTestCase(unittest.HomeserverTestCase): } self.assertEqual( signers_for(ours), - [MemberKeys.SIGNATURE_ENTITY, MemberKeys.LINKAGE_SIGNATURE_ENTITY], + [MemberKeys.SIGNATURE_ENTITY, self.hs.hostname], ) for wrong in ( {**ours, EventContentFields.MSC4430_LOCALPART: "bob"}, @@ -481,7 +508,7 @@ class EventTranslationTestCase(unittest.HomeserverTestCase): self.assertEqual(signers_for(ours), [MemberKeys.SIGNATURE_ENTITY]) self.assertEqual( signers_for({EventContentFields.MSC4430_DOMAIN: self.hs.hostname}), - [MemberKeys.SIGNATURE_ENTITY, MemberKeys.LINKAGE_SIGNATURE_ENTITY], + [MemberKeys.SIGNATURE_ENTITY, self.hs.hostname], ) def test_remote_additional_creator_is_refused(self) -> None: diff --git a/tests/rest/client/test_member_key_rooms.py b/tests/rest/client/test_member_key_rooms.py index cd539eb95d..d40b5d11cc 100644 --- a/tests/rest/client/test_member_key_rooms.py +++ b/tests/rest/client/test_member_key_rooms.py @@ -141,18 +141,24 @@ class _MemberKeyRoomsBase(unittest.HomeserverTestCase): def _assert_linked_and_cosigned(self, event: EventBase, user_id: str) -> None: """A membership event about one of our users: it names them and we - co-signed it, next to the member key signature.""" + co-signed it, next to the member key signature. An event the member did + not send themselves — an invite — additionally carries their own key's + signature under `state_key`, which is what asserts the linkage: ours + alone is a claim no other server believes.""" localpart, domain = user_id[1:].split(":", 1) self.assertEqual(event.content[EventContentFields.MSC4430_LOCALPART], localpart) self.assertEqual(event.content[EventContentFields.MSC4430_DOMAIN], domain) + expected = { + MemberKeys.SIGNATURE_ENTITY: {MemberKeys.KEY_ID}, + self.hs.hostname: { + f"{self.hs.signing_key.alg}:{self.hs.signing_key.version}" + }, + } + if event.sender != event.state_key: + expected[MemberKeys.STATE_KEY_SIGNATURE_ENTITY] = {MemberKeys.KEY_ID} self.assertEqual( {entity: set(sigs) for entity, sigs in event.signatures.as_dict().items()}, - { - MemberKeys.SIGNATURE_ENTITY: {MemberKeys.KEY_ID}, - MemberKeys.LINKAGE_SIGNATURE_ENTITY: { - f"{self.hs.signing_key.alg}:{self.hs.signing_key.version}" - }, - }, + expected, event.signatures, ) @@ -365,8 +371,8 @@ class MemberKeyRoomsTestCase(_MemberKeyRoomsBase): self.assertEqual( leave.content[EventContentFields.MSC4430_DOMAIN], self.hs.hostname ) - # Still ours, so still co-signed. - self.assertIn(MemberKeys.LINKAGE_SIGNATURE_ENTITY, leave.signatures) + # Their own event, so still co-signed. + self.assertIn(self.hs.hostname, leave.signatures) def test_additional_creators(self) -> None: """Additional creators are named by key in the create event, which is the @@ -496,20 +502,16 @@ class MemberKeyModerationTestCase(_MemberKeyRoomsBase): def _membership(self, room_id: str, member_key: str) -> str: return self._member_event(room_id, member_key).membership - def _assert_moderated_by( - self, event: EventBase, sender_key: str, target_user_id: str | None = None - ) -> None: - """A membership event sent by `sender_key` about another member: with - the target's linkage information and our co-signature if the target is - one of our users, else with neither and signed by the sender's key alone - (we vouch for nothing about a key we do not hold).""" + def _assert_moderated_by(self, event: EventBase, sender_key: str) -> None: + """A kick, ban or unban: sent by `sender_key` about another member, and + naming nobody. Only the member's own key may assert who they are, and it + has not signed this, so linkage information here would be ignored by every + other server — including a target whose key we hold, whose localpart has + no business being published on somebody else's event.""" self.assertEqual(event.sender, sender_key) - if target_user_id is not None: - self._assert_linked_and_cosigned(event, target_user_id) - else: - self.assertNotIn(EventContentFields.MSC4430_LOCALPART, event.content) - self.assertNotIn(EventContentFields.MSC4430_DOMAIN, event.content) - self._assert_signed_by_member_key_only(event) + self.assertNotIn(EventContentFields.MSC4430_LOCALPART, event.content) + self.assertNotIn(EventContentFields.MSC4430_DOMAIN, event.content) + self._assert_signed_by_member_key_only(event) def _power_levels(self, room_id: str) -> JsonDict: return dict(self._current_state(room_id)[(EventTypes.PowerLevels, "")].content) @@ -548,12 +550,12 @@ class MemberKeyModerationTestCase(_MemberKeyRoomsBase): kick = self._member_event(room_id, bob_key) self.assertEqual(kick.membership, Membership.LEAVE) self.assertEqual(kick.content["reason"], "kick reason") - self._assert_moderated_by(kick, alice_key, self.bob) + self._assert_moderated_by(kick, alice_key) self._moderate(room_id, "ban", self.bob) ban = self._member_event(room_id, bob_key) self.assertEqual(ban.membership, Membership.BAN) - self._assert_moderated_by(ban, alice_key, self.bob) + self._assert_moderated_by(ban, alice_key) self.helper.join( room_id, self.bob, tok=self.bob_tok, expect_code=HTTPStatus.FORBIDDEN ) @@ -561,7 +563,7 @@ class MemberKeyModerationTestCase(_MemberKeyRoomsBase): self._moderate(room_id, "unban", self.bob) unban = self._member_event(room_id, bob_key) self.assertEqual(unban.membership, Membership.LEAVE) - self._assert_moderated_by(unban, alice_key, self.bob) + self._assert_moderated_by(unban, alice_key) # Back under the same key. self.helper.join(room_id, self.bob, tok=self.bob_tok) @@ -579,7 +581,7 @@ class MemberKeyModerationTestCase(_MemberKeyRoomsBase): self._moderate(room_id, "kick", bob_key) kick = self._member_event(room_id, bob_key) self.assertEqual(kick.membership, Membership.LEAVE) - self._assert_moderated_by(kick, alice_key, self.bob) + self._assert_moderated_by(kick, alice_key) self.assertEqual( self.get_success( self.store.get_local_current_membership_for_user_in_room( @@ -598,19 +600,15 @@ class MemberKeyModerationTestCase(_MemberKeyRoomsBase): self._moderate(room_id, "unban", bob_key) unban = self._member_event(room_id, bob_key) self.assertEqual(unban.membership, Membership.LEAVE) - self._assert_moderated_by(unban, alice_key, self.bob) + self._assert_moderated_by(unban, alice_key) self.helper.join(room_id, self.bob, tok=self.bob_tok) - # Once erased, Bob is still ours (co-signed, our domain), unnamed. + # Erased or not, a ban names nobody: Bob's key has not signed it. self.get_success(self.store.mark_user_erased(self.bob)) self._moderate(room_id, "ban", bob_key) ban = self._member_event(room_id, bob_key) self.assertEqual(ban.membership, Membership.BAN) - self.assertNotIn(EventContentFields.MSC4430_LOCALPART, ban.content) - self.assertEqual( - ban.content[EventContentFields.MSC4430_DOMAIN], self.hs.hostname - ) - self.assertIn(MemberKeys.LINKAGE_SIGNATURE_ENTITY, ban.signatures.as_dict()) + self._assert_moderated_by(ban, alice_key) self._assert_no_user_ids_in_room(room_id) def test_moderation_through_the_state_api(self) -> None: @@ -630,7 +628,7 @@ class MemberKeyModerationTestCase(_MemberKeyRoomsBase): ) ban = self._member_event(room_id, bob_key) self.assertEqual(ban.membership, Membership.BAN) - self._assert_moderated_by(ban, alice_key, self.bob) + self._assert_moderated_by(ban, alice_key) self.helper.send_state( room_id, @@ -1028,7 +1026,7 @@ class MemberKeyModerationTestCase(_MemberKeyRoomsBase): self.assertEqual(self._power_levels(new_room)["users"], {new_bob_key: 50}) ban = self._member_event(new_room, new_carol_key) self.assertEqual(ban.membership, Membership.BAN) - self._assert_moderated_by(ban, new_alice_key, self.carol) + self._assert_moderated_by(ban, new_alice_key) self.assertEqual(self._membership(new_room, new_alice_key), Membership.JOIN) self.assertNotIn( (EventTypes.Member, new_bob_key), self._current_state(new_room)