mirror of
https://git.anonymousland.org/anonymousland/synapse.git
synced 2025-12-11 02:03:08 -05:00
Update the MSC3083 support to verify if joins are from an authorized server. (#10254)
This commit is contained in:
parent
4fb92d93ea
commit
228decfce1
17 changed files with 632 additions and 98 deletions
|
|
@ -178,6 +178,34 @@ async def _check_sigs_on_pdu(
|
|||
)
|
||||
raise SynapseError(403, errmsg, Codes.FORBIDDEN)
|
||||
|
||||
# If this is a join event for a restricted room it may have been authorised
|
||||
# via a different server from the sending server. Check those signatures.
|
||||
if (
|
||||
room_version.msc3083_join_rules
|
||||
and pdu.type == EventTypes.Member
|
||||
and pdu.membership == Membership.JOIN
|
||||
and "join_authorised_via_users_server" in pdu.content
|
||||
):
|
||||
authorising_server = get_domain_from_id(
|
||||
pdu.content["join_authorised_via_users_server"]
|
||||
)
|
||||
try:
|
||||
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:
|
||||
errmsg = (
|
||||
"event id %s: unable to verify signature for authorising server %s: %s"
|
||||
% (
|
||||
pdu.event_id,
|
||||
authorising_server,
|
||||
e,
|
||||
)
|
||||
)
|
||||
raise SynapseError(403, errmsg, Codes.FORBIDDEN)
|
||||
|
||||
|
||||
def _is_invite_via_3pid(event: EventBase) -> bool:
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import itertools
|
|||
import logging
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Collection,
|
||||
|
|
@ -79,7 +78,15 @@ class InvalidResponseError(RuntimeError):
|
|||
we couldn't parse
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@attr.s(slots=True, frozen=True, auto_attribs=True)
|
||||
class SendJoinResult:
|
||||
# The event to persist.
|
||||
event: EventBase
|
||||
# A string giving the server the event was sent to.
|
||||
origin: str
|
||||
state: List[EventBase]
|
||||
auth_chain: List[EventBase]
|
||||
|
||||
|
||||
class FederationClient(FederationBase):
|
||||
|
|
@ -677,7 +684,7 @@ class FederationClient(FederationBase):
|
|||
|
||||
async def send_join(
|
||||
self, destinations: Iterable[str], pdu: EventBase, room_version: RoomVersion
|
||||
) -> Dict[str, Any]:
|
||||
) -> SendJoinResult:
|
||||
"""Sends a join event to one of a list of homeservers.
|
||||
|
||||
Doing so will cause the remote server to add the event to the graph,
|
||||
|
|
@ -691,18 +698,38 @@ class FederationClient(FederationBase):
|
|||
did the make_join)
|
||||
|
||||
Returns:
|
||||
a dict with members ``origin`` (a string
|
||||
giving the server the event was sent to, ``state`` (?) and
|
||||
``auth_chain``.
|
||||
The result of the send join request.
|
||||
|
||||
Raises:
|
||||
SynapseError: if the chosen remote server returns a 300/400 code, or
|
||||
no servers successfully handle the request.
|
||||
"""
|
||||
|
||||
async def send_request(destination) -> Dict[str, Any]:
|
||||
async def send_request(destination) -> SendJoinResult:
|
||||
response = await self._do_send_join(room_version, destination, pdu)
|
||||
|
||||
# If an event was returned (and expected to be returned):
|
||||
#
|
||||
# * Ensure it has the same event ID (note that the event ID is a hash
|
||||
# of the event fields for versions which support MSC3083).
|
||||
# * Ensure the signatures are good.
|
||||
#
|
||||
# Otherwise, fallback to the provided event.
|
||||
if room_version.msc3083_join_rules and response.event:
|
||||
event = response.event
|
||||
|
||||
valid_pdu = await self._check_sigs_and_hash_and_fetch_one(
|
||||
pdu=event,
|
||||
origin=destination,
|
||||
outlier=True,
|
||||
room_version=room_version,
|
||||
)
|
||||
|
||||
if valid_pdu is None or event.event_id != pdu.event_id:
|
||||
raise InvalidResponseError("Returned an invalid join event")
|
||||
else:
|
||||
event = pdu
|
||||
|
||||
state = response.state
|
||||
auth_chain = response.auth_events
|
||||
|
||||
|
|
@ -784,11 +811,21 @@ class FederationClient(FederationBase):
|
|||
% (auth_chain_create_events,)
|
||||
)
|
||||
|
||||
return {
|
||||
"state": signed_state,
|
||||
"auth_chain": signed_auth,
|
||||
"origin": destination,
|
||||
}
|
||||
return SendJoinResult(
|
||||
event=event,
|
||||
state=signed_state,
|
||||
auth_chain=signed_auth,
|
||||
origin=destination,
|
||||
)
|
||||
|
||||
if room_version.msc3083_join_rules:
|
||||
# If the join is being authorised via allow rules, we need to send
|
||||
# the /send_join back to the same server that was originally used
|
||||
# with /make_join.
|
||||
if "join_authorised_via_users_server" in pdu.content:
|
||||
destinations = [
|
||||
get_domain_from_id(pdu.content["join_authorised_via_users_server"])
|
||||
]
|
||||
|
||||
return await self._try_destination_list("send_join", destinations, send_request)
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ from synapse.api.errors import (
|
|||
UnsupportedRoomVersionError,
|
||||
)
|
||||
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersion
|
||||
from synapse.crypto.event_signing import compute_event_signature
|
||||
from synapse.events import EventBase
|
||||
from synapse.events.snapshot import EventContext
|
||||
from synapse.federation.federation_base import FederationBase, event_from_pdu_json
|
||||
|
|
@ -64,7 +65,7 @@ from synapse.replication.http.federation import (
|
|||
ReplicationGetQueryRestServlet,
|
||||
)
|
||||
from synapse.storage.databases.main.lock import Lock
|
||||
from synapse.types import JsonDict
|
||||
from synapse.types import JsonDict, get_domain_from_id
|
||||
from synapse.util import glob_to_regex, json_decoder, unwrapFirstError
|
||||
from synapse.util.async_helpers import Linearizer, concurrently_execute
|
||||
from synapse.util.caches.response_cache import ResponseCache
|
||||
|
|
@ -586,7 +587,7 @@ class FederationServer(FederationBase):
|
|||
async def on_send_join_request(
|
||||
self, origin: str, content: JsonDict, room_id: str
|
||||
) -> Dict[str, Any]:
|
||||
context = await self._on_send_membership_event(
|
||||
event, context = await self._on_send_membership_event(
|
||||
origin, content, Membership.JOIN, room_id
|
||||
)
|
||||
|
||||
|
|
@ -597,6 +598,7 @@ class FederationServer(FederationBase):
|
|||
|
||||
time_now = self._clock.time_msec()
|
||||
return {
|
||||
"org.matrix.msc3083.v2.event": event.get_pdu_json(),
|
||||
"state": [p.get_pdu_json(time_now) for p in state.values()],
|
||||
"auth_chain": [p.get_pdu_json(time_now) for p in auth_chain],
|
||||
}
|
||||
|
|
@ -681,7 +683,7 @@ class FederationServer(FederationBase):
|
|||
Returns:
|
||||
The stripped room state.
|
||||
"""
|
||||
event_context = await self._on_send_membership_event(
|
||||
_, context = await self._on_send_membership_event(
|
||||
origin, content, Membership.KNOCK, room_id
|
||||
)
|
||||
|
||||
|
|
@ -690,14 +692,14 @@ class FederationServer(FederationBase):
|
|||
# related to the room while the knock request is pending.
|
||||
stripped_room_state = (
|
||||
await self.store.get_stripped_room_state_from_event_context(
|
||||
event_context, self._room_prejoin_state_types
|
||||
context, self._room_prejoin_state_types
|
||||
)
|
||||
)
|
||||
return {"knock_state_events": stripped_room_state}
|
||||
|
||||
async def _on_send_membership_event(
|
||||
self, origin: str, content: JsonDict, membership_type: str, room_id: str
|
||||
) -> EventContext:
|
||||
) -> Tuple[EventBase, EventContext]:
|
||||
"""Handle an on_send_{join,leave,knock} request
|
||||
|
||||
Does some preliminary validation before passing the request on to the
|
||||
|
|
@ -712,7 +714,7 @@ class FederationServer(FederationBase):
|
|||
in the event
|
||||
|
||||
Returns:
|
||||
The context of the event after inserting it into the room graph.
|
||||
The event and context of the event after inserting it into the room graph.
|
||||
|
||||
Raises:
|
||||
SynapseError if there is a problem with the request, including things like
|
||||
|
|
@ -748,6 +750,33 @@ class FederationServer(FederationBase):
|
|||
|
||||
logger.debug("_on_send_membership_event: pdu sigs: %s", event.signatures)
|
||||
|
||||
# Sign the event since we're vouching on behalf of the remote server that
|
||||
# the event is valid to be sent into the room. Currently this is only done
|
||||
# if the user is being joined via restricted join rules.
|
||||
if (
|
||||
room_version.msc3083_join_rules
|
||||
and event.membership == Membership.JOIN
|
||||
and "join_authorised_via_users_server" in event.content
|
||||
):
|
||||
# We can only authorise our own users.
|
||||
authorising_server = get_domain_from_id(
|
||||
event.content["join_authorised_via_users_server"]
|
||||
)
|
||||
if authorising_server != self.server_name:
|
||||
raise SynapseError(
|
||||
400,
|
||||
f"Cannot authorise request from resident server: {authorising_server}",
|
||||
)
|
||||
|
||||
event.signatures.update(
|
||||
compute_event_signature(
|
||||
room_version,
|
||||
event.get_pdu_json(),
|
||||
self.hs.hostname,
|
||||
self.hs.signing_key,
|
||||
)
|
||||
)
|
||||
|
||||
event = await self._check_sigs_and_hash(room_version, event)
|
||||
|
||||
return await self.handler.on_send_membership_event(origin, event)
|
||||
|
|
|
|||
|
|
@ -1219,8 +1219,26 @@ def _create_v2_path(path: str, *args: str) -> str:
|
|||
class SendJoinResponse:
|
||||
"""The parsed response of a `/send_join` request."""
|
||||
|
||||
# The list of auth events from the /send_join response.
|
||||
auth_events: List[EventBase]
|
||||
# The list of state from the /send_join response.
|
||||
state: List[EventBase]
|
||||
# The raw join event from the /send_join response.
|
||||
event_dict: JsonDict
|
||||
# The parsed join event from the /send_join response. This will be None if
|
||||
# "event" is not included in the response.
|
||||
event: Optional[EventBase] = None
|
||||
|
||||
|
||||
@ijson.coroutine
|
||||
def _event_parser(event_dict: JsonDict):
|
||||
"""Helper function for use with `ijson.kvitems_coro` to parse key-value pairs
|
||||
to add them to a given dictionary.
|
||||
"""
|
||||
|
||||
while True:
|
||||
key, value = yield
|
||||
event_dict[key] = value
|
||||
|
||||
|
||||
@ijson.coroutine
|
||||
|
|
@ -1246,7 +1264,8 @@ class SendJoinParser(ByteParser[SendJoinResponse]):
|
|||
CONTENT_TYPE = "application/json"
|
||||
|
||||
def __init__(self, room_version: RoomVersion, v1_api: bool):
|
||||
self._response = SendJoinResponse([], [])
|
||||
self._response = SendJoinResponse([], [], {})
|
||||
self._room_version = room_version
|
||||
|
||||
# The V1 API has the shape of `[200, {...}]`, which we handle by
|
||||
# prefixing with `item.*`.
|
||||
|
|
@ -1260,12 +1279,21 @@ class SendJoinParser(ByteParser[SendJoinResponse]):
|
|||
_event_list_parser(room_version, self._response.auth_events),
|
||||
prefix + "auth_chain.item",
|
||||
)
|
||||
self._coro_event = ijson.kvitems_coro(
|
||||
_event_parser(self._response.event_dict),
|
||||
prefix + "org.matrix.msc3083.v2.event",
|
||||
)
|
||||
|
||||
def write(self, data: bytes) -> int:
|
||||
self._coro_state.send(data)
|
||||
self._coro_auth.send(data)
|
||||
self._coro_event.send(data)
|
||||
|
||||
return len(data)
|
||||
|
||||
def finish(self) -> SendJoinResponse:
|
||||
if self._response.event_dict:
|
||||
self._response.event = make_event_from_dict(
|
||||
self._response.event_dict, self._room_version
|
||||
)
|
||||
return self._response
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue