Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Split event_auth.check into two parts #10940

1 change: 1 addition & 0 deletions changelog.d/10940.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Clean up some of the federation event authentication code for clarity.
153 changes: 92 additions & 61 deletions synapse/event_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,42 +41,112 @@
logger = logging.getLogger(__name__)


def check(
room_version_obj: RoomVersion,
event: EventBase,
auth_events: StateMap[EventBase],
do_sig_check: bool = True,
do_size_check: bool = True,
def validate_event_for_room_version(
room_version_obj: RoomVersion, event: EventBase
) -> None:
"""Checks if this event is correctly authed.
"""Ensure that the event complies with the limits, and has the right signatures

NB: does not *validate* the signatures - it assumes that any signatures present
have already been checked.

NB: it does not check that the event satisfies the auth rules (that is done in
check_auth_rules_for_event) - these tests are independent of the rest of the state
in the room.

NB: This is used to check events that have been received over federation. As such,
it can only enforce the checks specified in the relevant room version, to avoid
a split-brain situation where some servers accept such events, and others reject
them.

TODO: consider moving this into EventValidator

Args:
room_version_obj: the version of the room
event: the event being checked.
auth_events: the existing room state.
do_sig_check: True if it should be verified that the sending server
signed the event.
do_size_check: True if the size of the event fields should be verified.
room_version_obj: the version of the room which contains this event
event: the event to be checked

Raises:
AuthError if the checks fail

Returns:
if the auth checks pass.
SynapseError if there is a problem with the event
"""
assert isinstance(auth_events, dict)

if do_size_check:
_check_size_limits(event)
_check_size_limits(event)

if not hasattr(event, "room_id"):
raise AuthError(500, "Event has no room_id: %s" % event)

room_id = event.room_id
# check that the event has the correct signatures
sender_domain = get_domain_from_id(event.sender)

is_invite_via_3pid = (
event.type == EventTypes.Member
and event.membership == Membership.INVITE
and "third_party_invite" in event.content
)

# Check the sender's domain has signed the event
if not event.signatures.get(sender_domain):
# We allow invites via 3pid to have a sender from a different
# HS, as the sender must match the sender of the original
# 3pid invite. This is checked further down with the
# other dedicated membership checks.
if not is_invite_via_3pid:
raise AuthError(403, "Event not signed by sender's server")

if event.format_version in (EventFormatVersions.V1,):
# Only older room versions have event IDs to check.
event_id_domain = get_domain_from_id(event.event_id)

# Check the origin domain has signed the event
if not event.signatures.get(event_id_domain):
raise AuthError(403, "Event not signed by sending server")

is_invite_via_allow_rule = (
room_version_obj.msc3083_join_rules
and event.type == EventTypes.Member
and event.membership == Membership.JOIN
and "join_authorised_via_users_server" in event.content
)
if is_invite_via_allow_rule:
authoriser_domain = get_domain_from_id(
event.content["join_authorised_via_users_server"]
)
if not event.signatures.get(authoriser_domain):
raise AuthError(403, "Event not signed by authorising server")


def check_auth_rules_for_event(
room_version_obj: RoomVersion, event: EventBase, auth_events: StateMap[EventBase]
) -> None:
"""Check that an event complies with the auth rules

Checks whether an event passes the auth rules with a given set of state events

Assumes that we have already checked that the event is the right shape (it has
enough signatures, has a room ID, etc). In other words:

- it's fine for use in state resolution, when we have already decided whether to
accept the event or not, and are now trying to decide whether it should make it
into the room state

- when we're doing the initial event auth, it is only suitable in combination with
a bunch of other tests.

Args:
room_version_obj: the version of the room
event: the event being checked.
auth_events: the room state to check the events against.

Raises:
AuthError if the checks fail
"""
assert isinstance(auth_events, dict)

# We need to ensure that the auth events are actually for the same room, to
# stop people from using powers they've been granted in other rooms for
# example.
#
# Arguably we don't need to do this when we're just doing state res, as presumably
# the state res algorithm isn't silly enough to give us events from different rooms.
# Still, it's easier to do it anyway.
room_id = event.room_id
for auth_event in auth_events.values():
if auth_event.room_id != room_id:
raise AuthError(
Expand All @@ -86,45 +156,6 @@ def check(
% (event.event_id, room_id, auth_event.event_id, auth_event.room_id),
)

if do_sig_check:
sender_domain = get_domain_from_id(event.sender)

is_invite_via_3pid = (
event.type == EventTypes.Member
and event.membership == Membership.INVITE
and "third_party_invite" in event.content
)

# Check the sender's domain has signed the event
if not event.signatures.get(sender_domain):
# We allow invites via 3pid to have a sender from a different
# HS, as the sender must match the sender of the original
# 3pid invite. This is checked further down with the
# other dedicated membership checks.
if not is_invite_via_3pid:
raise AuthError(403, "Event not signed by sender's server")

if event.format_version in (EventFormatVersions.V1,):
# Only older room versions have event IDs to check.
event_id_domain = get_domain_from_id(event.event_id)

# Check the origin domain has signed the event
if not event.signatures.get(event_id_domain):
raise AuthError(403, "Event not signed by sending server")

is_invite_via_allow_rule = (
room_version_obj.msc3083_join_rules
and event.type == EventTypes.Member
and event.membership == Membership.JOIN
and "join_authorised_via_users_server" in event.content
)
if is_invite_via_allow_rule:
authoriser_domain = get_domain_from_id(
event.content["join_authorised_via_users_server"]
)
if not event.signatures.get(authoriser_domain):
raise AuthError(403, "Event not signed by authorising server")

# Implementation of https://matrix.org/docs/spec/rooms/v1#authorization-rules
#
# 1. If type is m.room.create:
Expand Down
15 changes: 6 additions & 9 deletions synapse/handlers/event_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
RestrictedJoinRuleTypes,
)
from synapse.api.errors import AuthError, Codes, SynapseError
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersion
from synapse.api.room_versions import RoomVersion
from synapse.event_auth import check_auth_rules_for_event
from synapse.events import EventBase
from synapse.events.builder import EventBuilder
from synapse.events.snapshot import EventContext
Expand All @@ -45,21 +46,17 @@ def __init__(self, hs: "HomeServer"):
self._store = hs.get_datastore()
self._server_name = hs.hostname

async def check_from_context(
async def check_auth_rules_from_context(
self,
room_version: str,
room_version_obj: RoomVersion,
event: EventBase,
context: EventContext,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the eagle-eyed reviewer will notice that context is unused, so may wonder why I'm not getting rid of it, and why the function is still called ..._from_context.

The answer is that I'm planning to add some extra tests based on the context, in the near future.

do_sig_check: bool = True,
) -> None:
"""Check an event passes the auth rules at its own auth events"""
auth_event_ids = event.auth_event_ids()
auth_events_by_id = await self._store.get_events(auth_event_ids)
auth_events = {(e.type, e.state_key): e for e in auth_events_by_id.values()}

room_version_obj = KNOWN_ROOM_VERSIONS[room_version]
event_auth.check(
room_version_obj, event, auth_events=auth_events, do_sig_check=do_sig_check
)
check_auth_rules_for_event(room_version_obj, event, auth_events)

def compute_auth_events(
self,
Expand Down
30 changes: 18 additions & 12 deletions synapse/handlers/federation.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@
)
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersion, RoomVersions
from synapse.crypto.event_signing import compute_event_signature
from synapse.event_auth import (
check_auth_rules_for_event,
validate_event_for_room_version,
)
from synapse.events import EventBase
from synapse.events.snapshot import EventContext
from synapse.events.validator import EventValidator
Expand Down Expand Up @@ -742,10 +746,9 @@ async def on_make_join_request(

# The remote hasn't signed it yet, obviously. We'll do the full checks
# when we get the event back in `on_send_join_request`
await self._event_auth_handler.check_from_context(
room_version.identifier, event, context, do_sig_check=False
await self._event_auth_handler.check_auth_rules_from_context(
room_version, event, context
)

return event

async def on_invite_request(
Expand Down Expand Up @@ -916,8 +919,8 @@ async def on_make_leave_request(
try:
# The remote hasn't signed it yet, obviously. We'll do the full checks
# when we get the event back in `on_send_leave_request`
await self._event_auth_handler.check_from_context(
room_version_obj.identifier, event, context, do_sig_check=False
await self._event_auth_handler.check_auth_rules_from_context(
room_version_obj, event, context
)
except AuthError as e:
logger.warning("Failed to create new leave %r because %s", event, e)
Expand Down Expand Up @@ -978,8 +981,8 @@ async def on_make_knock_request(
try:
# The remote hasn't signed it yet, obviously. We'll do the full checks
# when we get the event back in `on_send_knock_request`
await self._event_auth_handler.check_from_context(
room_version_obj.identifier, event, context, do_sig_check=False
await self._event_auth_handler.check_auth_rules_from_context(
room_version_obj, event, context
)
except AuthError as e:
logger.warning("Failed to create new knock %r because %s", event, e)
Expand Down Expand Up @@ -1168,7 +1171,8 @@ async def _persist_auth_tree(
auth_for_e[(EventTypes.Create, "")] = create_event

try:
event_auth.check(room_version, e, auth_events=auth_for_e)
validate_event_for_room_version(room_version, e)
check_auth_rules_for_event(room_version, e, auth_for_e)
except SynapseError as err:
# we may get SynapseErrors here as well as AuthErrors. For
# instance, there are a couple of (ancient) events in some
Expand Down Expand Up @@ -1266,8 +1270,9 @@ async def exchange_third_party_invite(
event.internal_metadata.send_on_behalf_of = self.hs.hostname

try:
await self._event_auth_handler.check_from_context(
room_version_obj.identifier, event, context
validate_event_for_room_version(room_version_obj, event)
await self._event_auth_handler.check_auth_rules_from_context(
room_version_obj, event, context
)
except AuthError as e:
logger.warning("Denying new third party invite %r because %s", event, e)
Expand Down Expand Up @@ -1317,8 +1322,9 @@ async def on_exchange_third_party_invite_request(
)

try:
await self._event_auth_handler.check_from_context(
room_version_obj.identifier, event, context
validate_event_for_room_version(room_version_obj, event)
await self._event_auth_handler.check_auth_rules_from_context(
room_version_obj, event, context
)
except AuthError as e:
logger.warning("Denying third party invite %r because %s", event, e)
Expand Down
18 changes: 13 additions & 5 deletions synapse/handlers/federation_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@

from prometheus_client import Counter

from synapse import event_auth
from synapse.api.constants import (
EventContentFields,
EventTypes,
Expand All @@ -47,7 +46,11 @@
SynapseError,
)
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS
from synapse.event_auth import auth_types_for_event
from synapse.event_auth import (
auth_types_for_event,
check_auth_rules_for_event,
validate_event_for_room_version,
)
from synapse.events import EventBase
from synapse.events.snapshot import EventContext
from synapse.federation.federation_client import InvalidResponseError
Expand Down Expand Up @@ -1207,7 +1210,8 @@ def prep(event: EventBase) -> Optional[Tuple[EventBase, EventContext]]:

context = EventContext.for_outlier()
try:
event_auth.check(room_version_obj, event, auth_events=auth)
validate_event_for_room_version(room_version_obj, event)
check_auth_rules_for_event(room_version_obj, event, auth)
except AuthError as e:
logger.warning("Rejecting %r because %s", event, e)
context.rejected = RejectedReason.AUTH_ERROR
Expand Down Expand Up @@ -1282,7 +1286,8 @@ async def _check_event_auth(
auth_events_for_auth = calculated_auth_event_map

try:
event_auth.check(room_version_obj, event, auth_events=auth_events_for_auth)
validate_event_for_room_version(room_version_obj, event)
check_auth_rules_for_event(room_version_obj, event, auth_events_for_auth)
except AuthError as e:
logger.warning("Failed auth resolution for %r because %s", event, e)
context.rejected = RejectedReason.AUTH_ERROR
Expand Down Expand Up @@ -1394,7 +1399,10 @@ async def _check_for_soft_fail(
}

try:
event_auth.check(room_version_obj, event, auth_events=current_auth_events)
# TODO: skip the call to validate_event_for_room_version? we should already
# have validated the event.
validate_event_for_room_version(room_version_obj, event)
check_auth_rules_for_event(room_version_obj, event, current_auth_events)
except AuthError as e:
logger.warning(
"Soft-failing %r (from %s) because %s",
Expand Down
6 changes: 4 additions & 2 deletions synapse/handlers/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
)
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersions
from synapse.api.urls import ConsentURIBuilder
from synapse.event_auth import validate_event_for_room_version
from synapse.events import EventBase
from synapse.events.builder import EventBuilder
from synapse.events.snapshot import EventContext
Expand Down Expand Up @@ -1098,8 +1099,9 @@ async def handle_new_client_event(
assert event.content["membership"] == Membership.LEAVE
else:
try:
await self._event_auth_handler.check_from_context(
room_version_obj.identifier, event, context
validate_event_for_room_version(room_version_obj, event)
await self._event_auth_handler.check_auth_rules_from_context(
room_version_obj, event, context
)
except AuthError as err:
logger.warning("Denying new event %r because %s", event, err)
Expand Down
6 changes: 4 additions & 2 deletions synapse/handlers/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
)
from synapse.api.filtering import Filter
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersion
from synapse.event_auth import validate_event_for_room_version
from synapse.events import EventBase
from synapse.events.utils import copy_power_levels_contents
from synapse.rest.admin._base import assert_user_is_admin
Expand Down Expand Up @@ -238,8 +239,9 @@ async def _upgrade_room(
},
)
old_room_version = await self.store.get_room_version(old_room_id)
await self._event_auth_handler.check_from_context(
old_room_version.identifier, tombstone_event, tombstone_context
validate_event_for_room_version(old_room_version, tombstone_event)
await self._event_auth_handler.check_auth_rules_from_context(
old_room_version, tombstone_event, tombstone_context
)

await self.clone_existing_room(
Expand Down
Loading