diff --git a/changelog.d/8078.feature b/changelog.d/8078.feature new file mode 100644 index 000000000000..4fd4f4a4ec2e --- /dev/null +++ b/changelog.d/8078.feature @@ -0,0 +1 @@ +Implement [MSC2730](https://github.com/matrix-org/matrix-doc/pull/2730): Verifiable forwarded events. diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 1a8144405ac0..f7ce2190e537 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -54,6 +54,7 @@ from synapse.events import EventBase from synapse.events.snapshot import EventContext from synapse.events.validator import EventValidator +from synapse.federation.federation_base import FederationBase, event_from_pdu_json from synapse.handlers._base import BaseHandler from synapse.logging.context import ( make_deferred_yieldable, @@ -108,7 +109,7 @@ class _NewEventInfo: auth_events = attr.ib(type=Optional[MutableStateMap[EventBase]], default=None) -class FederationHandler(BaseHandler): +class FederationHandler(BaseHandler, FederationBase): """Handles events that originated from federation. Responsible for: a) handling received Pdus before handing them on as Events to the rest @@ -683,6 +684,41 @@ async def _get_events_from_store_or_dest( return fetched_events + _forwarded_key = "net.maunium.msc2730.forwarded" + + async def _validate_forwarded_event( + self, event: EventBase + ) -> Tuple[bool, Optional[str]]: + try: + source_evt_dict = {**event.content[self._forwarded_key]} + room_version_identifier = source_evt_dict["unsigned"]["room_version"] + source_evt_dict["type"] = event.type + source_evt_dict["content"] = {**event.content} + del source_evt_dict["unsigned"] + del source_evt_dict["content"][self._forwarded_key] + except (KeyError, TypeError): + return False, None + + room_version = KNOWN_ROOM_VERSIONS.get(room_version_identifier) + if not room_version: + # We don't support the source room version, so we can't verify the event :( + return False, None + + try: + source_evt = event_from_pdu_json(source_evt_dict, room_version) + except SynapseError: + return False, None + + try: + checked_evt = await self._check_sigs_and_hash(room_version, source_evt) + # _check_sigs_and_hash returns a redacted event if hash validation failed and + # a SynapseError if signature validation failed. In both those cases, we want to + # mark the forward as invalid. + valid = not checked_evt.internal_metadata.is_redacted() + except SynapseError: + valid = False + return valid, source_evt.event_id + async def _process_received_pdu( self, origin: str, event: EventBase, state: Optional[Iterable[EventBase]], ): @@ -703,6 +739,17 @@ async def _process_received_pdu( logger.debug("[%s %s] Processing event: %s", room_id, event_id, event) + if ( + not event.is_state() + and not event.redacts + and self._forwarded_key in event.content + ): + valid, forwarded_event_id = await self._validate_forwarded_event(event) + event.unsigned[self._forwarded_key] = { + "valid": valid, + "event_id": forwarded_event_id, + } + try: await self._handle_new_event(origin, event, state=state) except AuthError as e: diff --git a/synapse/rest/__init__.py b/synapse/rest/__init__.py index 40f5c32db2df..4d2a2e2f0d22 100644 --- a/synapse/rest/__init__.py +++ b/synapse/rest/__init__.py @@ -37,6 +37,7 @@ capabilities, devices, filter, + forward_event, groups, keys, notifications, @@ -109,6 +110,7 @@ def register_servlets(client_resource, hs): tags.register_servlets(hs, client_resource) account_data.register_servlets(hs, client_resource) report_event.register_servlets(hs, client_resource) + forward_event.register_servlets(hs, client_resource) openid.register_servlets(hs, client_resource) notifications.register_servlets(hs, client_resource) devices.register_servlets(hs, client_resource) diff --git a/synapse/rest/client/v2_alpha/forward_event.py b/synapse/rest/client/v2_alpha/forward_event.py new file mode 100644 index 000000000000..e488633e914f --- /dev/null +++ b/synapse/rest/client/v2_alpha/forward_event.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Tulir Asokan +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import logging + +from synapse.api.errors import AuthError, Codes, SynapseError +from synapse.http.servlet import RestServlet, parse_integer +from synapse.logging.opentracing import set_tag + +from ._base import client_patterns + +logger = logging.getLogger(__name__) + + +class RoomEventForwardServlet(RestServlet): + """ + PUT /net.maunium.msc2730/rooms/{room_id}/event/{event_id}/forward/{target_room_id}/{txn_id} + """ + + PATTERNS = client_patterns( + ( + "/net.maunium.msc2730/rooms/(?P[^/]*)/event/(?P[^/]*)" + "/forward/(?P[^/]*)/(?P.*)" + ), + releases=(), # This is an unstable feature + ) + + _data_key = "net.maunium.msc2730.forwarded" + _err_not_forwardable = "NET.MAUNIUM.MSC2730_NOT_FORWARDABLE" + + def __init__(self, hs): + super().__init__() + self.event_creation_handler = hs.get_event_creation_handler() + self.event_handler = hs.get_event_handler() + self.store = hs.get_datastore() + self.auth = hs.get_auth() + + async def on_PUT(self, request, room_id, event_id, target_room_id, txn_id): + requester = await self.auth.get_user_by_req(request, allow_guest=True) + + try: + event = await self.event_handler.get_event( + requester.user, room_id, event_id + ) + except AuthError: + event = None + if not event: + raise SynapseError(404, "Event not found.", errcode=Codes.NOT_FOUND) + + if event.is_state(): + raise SynapseError( + 401, + "State events cannot be forwarded.", + errcode=self._err_not_forwardable, + ) + elif event.redacts: + raise SynapseError( + 401, + "Redaction events cannot be forwarded.", + errcode=self._err_not_forwardable, + ) + elif event.internal_metadata.is_redacted(): + raise SynapseError( + 401, + "Redacted events cannot be forwarded.", + errcode=self._err_not_forwardable, + ) + + event_id = event.event_id + event_dict = event.get_dict() + + content = event_dict.pop("content") + unsigned = event_dict.pop("unsigned", {}) + event_type = event_dict.pop("type") + has_forward_meta = self._data_key in content + try: + is_valid_forward = has_forward_meta and unsigned[self._data_key]["valid"] + except (KeyError, TypeError): + is_valid_forward = False + + if has_forward_meta: + if not is_valid_forward: + raise SynapseError( + 401, + "Event contains invalid forward metadata.", + errcode=self._err_not_forwardable, + ) + # Pass through the old event ID to the new unsigned data + event_id = unsigned[self._data_key]["event_id"] + elif not has_forward_meta: + content[self._data_key] = event_dict + room_version = await self.store.get_room_version(event.room_id) + content[self._data_key]["unsigned"] = { + "room_version": room_version.identifier, + # TODO add sender profile info here + } + + forward_event_dict = { + "type": event_type, + "content": content, + "room_id": target_room_id, + "sender": requester.user.to_string(), + "unsigned": {self._data_key: {"valid": True, "event_id": event_id}}, + } + + if b"ts" in request.args and requester.app_service: + forward_event_dict["origin_server_ts"] = parse_integer(request, "ts", 0) + + ( + forwarded_event, + _, + ) = await self.event_creation_handler.create_and_send_nonmember_event( + requester, forward_event_dict, txn_id=txn_id + ) + + set_tag("event_id", forwarded_event.event_id) + return 200, {"event_id": forwarded_event.event_id} + + +def register_servlets(hs, http_server): + RoomEventForwardServlet(hs).register(http_server) diff --git a/synapse/rest/client/versions.py b/synapse/rest/client/versions.py index d24a199318a2..fa3d5ad387be 100644 --- a/synapse/rest/client/versions.py +++ b/synapse/rest/client/versions.py @@ -75,6 +75,8 @@ def on_GET(self, request): "org.matrix.e2e_cross_signing": True, # Implements additional endpoints as described in MSC2432 "org.matrix.msc2432": True, + # Implements endpoint and forwarded event validation as described in MSC2730 + "net.maunium.msc2730": True, # Implements additional endpoints as described in MSC2666 "uk.half-shot.msc2666": True, # Whether new rooms will be set to encrypted or not (based on presets).