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

Clean up federation event auth code #10539

Merged
1 change: 1 addition & 0 deletions changelog.d/10539.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.
118 changes: 66 additions & 52 deletions synapse/handlers/federation.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,21 +108,33 @@
)


@attr.s(slots=True)
@attr.s(slots=True, frozen=True, auto_attribs=True)
class _NewEventInfo:
"""Holds information about a received event, ready for passing to _auth_and_persist_events

Attributes:
event: the received event

state: the state at that event
state: the state at that event, according to /state_ids from a remote
homeserver. Only populated for backfilled events which are going to be a
new backwards extremity.

claimed_auth_event_map: a map of (type, state_key) => event for the event's
claimed auth_events.

This can include events which have not yet been persisted, in the case that
we are backfilling a batch of events.

Note: May be incomplete: if we were unable to find all of the claimed auth
events. Also, treat the contents with caution: the events might also have
been rejected, might not yet have been authorized themselves, or they might
be in the wrong room.

auth_events: the auth_event map for that event
"""

event = attr.ib(type=EventBase)
state = attr.ib(type=Optional[Sequence[EventBase]], default=None)
auth_events = attr.ib(type=Optional[MutableStateMap[EventBase]], default=None)
event: EventBase
state: Optional[Sequence[EventBase]]
claimed_auth_event_map: StateMap[EventBase]


class FederationHandler(BaseHandler):
Expand Down Expand Up @@ -1000,7 +1012,7 @@ async def backfill(
_NewEventInfo(
event=ev,
state=events_to_state[e_id],
auth_events={
claimed_auth_event_map={
(
auth_events[a_id].type,
auth_events[a_id].state_key,
Expand Down Expand Up @@ -2208,7 +2220,7 @@ async def _auth_and_persist_event(
event: EventBase,
context: EventContext,
state: Optional[Iterable[EventBase]] = None,
auth_events: Optional[MutableStateMap[EventBase]] = None,
claimed_auth_event_map: Optional[StateMap[EventBase]] = None,
backfilled: bool = False,
) -> None:
"""
Expand All @@ -2220,25 +2232,26 @@ async def _auth_and_persist_event(
context:
The event context.

NB that this function potentially modifies it.
state:
The state events used to check the event for soft-fail. If this is
not provided the current state events will be used.
auth_events:
Map from (event_type, state_key) to event

Normally, our calculated auth_events based on the state of the room
at the event's position in the DAG, though occasionally (eg if the
event is an outlier), may be the auth events claimed by the remote
server.
claimed_auth_event_map:
A map of (type, state_key) => event for the event's claimed auth_events.
Possibly incomplete, and possibly including events that are not yet
persisted, or authed, or in the right room.

Only populated where we may not already have persisted these events -
for example, when populating outliers.

backfilled: True if the event was backfilled.
"""
context = await self._check_event_auth(
origin,
event,
context,
state=state,
auth_events=auth_events,
claimed_auth_event_map=claimed_auth_event_map,
backfilled=backfilled,
)

Expand Down Expand Up @@ -2302,7 +2315,7 @@ async def prep(ev_info: _NewEventInfo):
event,
res,
state=ev_info.state,
auth_events=ev_info.auth_events,
claimed_auth_event_map=ev_info.claimed_auth_event_map,
backfilled=backfilled,
)
return res
Expand Down Expand Up @@ -2568,7 +2581,7 @@ async def _check_event_auth(
event: EventBase,
context: EventContext,
state: Optional[Iterable[EventBase]] = None,
auth_events: Optional[MutableStateMap[EventBase]] = None,
claimed_auth_event_map: Optional[StateMap[EventBase]] = None,
backfilled: bool = False,
) -> EventContext:
"""
Expand All @@ -2580,21 +2593,19 @@ async def _check_event_auth(
context:
The event context.

NB that this function potentially modifies it.
state:
The state events used to check the event for soft-fail. If this is
not provided the current state events will be used.
auth_events:
Map from (event_type, state_key) to event

Normally, our calculated auth_events based on the state of the room
at the event's position in the DAG, though occasionally (eg if the
event is an outlier), may be the auth events claimed by the remote
server.
claimed_auth_event_map:
A map of (type, state_key) => event for the event's claimed auth_events.
Possibly incomplete, and possibly including events that are not yet
persisted, or authed, or in the right room.

Also NB that this function adds entries to it.
Only populated where we may not already have persisted these events -
for example, when populating outliers, or the state for a backwards
extremity.

If this is not provided, it is calculated from the previous state IDs.
backfilled: True if the event was backfilled.

Returns:
Expand All @@ -2603,26 +2614,24 @@ async def _check_event_auth(
room_version = await self.store.get_room_version_id(event.room_id)
room_version_obj = KNOWN_ROOM_VERSIONS[room_version]

if not auth_events:
if claimed_auth_event_map:
# if we have a copy of the auth events from the event, use that as the
# basis for auth.
auth_events = claimed_auth_event_map
else:
# otherwise, we calculate what the auth events *should* be, and use that
prev_state_ids = await context.get_prev_state_ids()
auth_events_ids = self._event_auth_handler.compute_auth_events(
event, prev_state_ids, for_verification=True
)
auth_events_x = await self.store.get_events(auth_events_ids)
auth_events = {(e.type, e.state_key): e for e in auth_events_x.values()}

# This is a hack to fix some old rooms where the initial join event
# didn't reference the create event in its auth events.
if event.type == EventTypes.Member and not event.auth_event_ids():
if len(event.prev_event_ids()) == 1 and event.depth < 5:
c = await self.store.get_event(
event.prev_event_ids()[0], allow_none=True
)
if c and c.type == EventTypes.Create:
auth_events[(c.type, c.state_key)] = c
Copy link
Member

Choose a reason for hiding this comment

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

What's the reasoning for not needing this anymore? Such old rooms may still be floating around? Possibly an alternative is to only do this for v1 rooms?

Copy link
Member Author

Choose a reason for hiding this comment

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

This workaround was added in 858e87a, from which I infer it was fixed before then. This is only a week after ae9c2ab, which we know was a critical security fix. I'm pretty sure we previously checked that there weren't any remaining rooms affected by the SYN-149 bug, but if there are any other rooms that predate that fix... I don't think I want to join them.

Copy link
Member

Choose a reason for hiding this comment

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

Hmm, OK fair


try:
context = await self._update_auth_events_and_context_for_auth(
(
context,
auth_events_for_auth,
) = await self._update_auth_events_and_context_for_auth(
origin, event, context, auth_events
)
except Exception:
Expand All @@ -2635,9 +2644,10 @@ async def _check_event_auth(
"Ignoring failure and continuing processing of event.",
event.event_id,
)
auth_events_for_auth = auth_events

try:
event_auth.check(room_version_obj, event, auth_events=auth_events)
event_auth.check(room_version_obj, event, auth_events=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 All @@ -2662,8 +2672,8 @@ async def _update_auth_events_and_context_for_auth(
origin: str,
event: EventBase,
context: EventContext,
auth_events: MutableStateMap[EventBase],
) -> EventContext:
input_auth_events: StateMap[EventBase],
) -> Tuple[EventContext, StateMap[EventBase]]:
"""Helper for _check_event_auth. See there for docs.

Checks whether a given event has the expected auth events. If it
Expand All @@ -2680,19 +2690,20 @@ async def _update_auth_events_and_context_for_auth(
event:
context:

auth_events:
input_auth_events:
Map from (event_type, state_key) to event

Normally, our calculated auth_events based on the state of the room
at the event's position in the DAG, though occasionally (eg if the
event is an outlier), may be the auth events claimed by the remote
server.

Also NB that this function adds entries to it.

Returns:
updated context
updated context, updated auth event map
"""
# take a copy of input_auth_events before we modify it.
auth_events: MutableStateMap[EventBase] = dict(input_auth_events)

event_auth_events = set(event.auth_event_ids())

# missing_auth is the set of the event's auth_events which we don't yet have
Expand Down Expand Up @@ -2721,7 +2732,7 @@ async def _update_auth_events_and_context_for_auth(
# The other side isn't around or doesn't implement the
# endpoint, so lets just bail out.
logger.info("Failed to get event auth from remote: %s", e1)
return context
return context, auth_events

seen_remotes = await self.store.have_seen_events(
event.room_id, [e.event_id for e in remote_auth_chain]
Expand Down Expand Up @@ -2752,7 +2763,10 @@ async def _update_auth_events_and_context_for_auth(
await self.state_handler.compute_event_context(e)
)
await self._auth_and_persist_event(
origin, e, missing_auth_event_context, auth_events=auth
origin,
e,
missing_auth_event_context,
claimed_auth_event_map=auth,
)

if e.event_id in event_auth_events:
Expand All @@ -2770,14 +2784,14 @@ async def _update_auth_events_and_context_for_auth(
# obviously be empty
# (b) alternatively, why don't we do it earlier?
logger.info("Skipping auth_event fetch for outlier")
return context
return context, auth_events

different_auth = event_auth_events.difference(
e.event_id for e in auth_events.values()
)

if not different_auth:
return context
return context, auth_events

logger.info(
"auth_events refers to events which are not in our calculated auth "
Expand All @@ -2803,7 +2817,7 @@ async def _update_auth_events_and_context_for_auth(
# XXX: should we reject the event in this case? It feels like we should,
# but then shouldn't we also do so if we've failed to fetch any of the
# auth events?
return context
return context, auth_events

# now we state-resolve between our own idea of the auth events, and the remote's
# idea of them.
Expand Down Expand Up @@ -2833,7 +2847,7 @@ async def _update_auth_events_and_context_for_auth(
event, context, auth_events
)

return context
return context, auth_events

async def _update_context_for_auth_events(
self, event: EventBase, context: EventContext, auth_events: StateMap[EventBase]
Expand Down
6 changes: 2 additions & 4 deletions tests/test_federation.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,8 @@ def setUp(self):
)

self.handler = self.homeserver.get_federation_handler()
self.handler._check_event_auth = (
lambda origin, event, context, state, auth_events, backfilled: succeed(
context
)
self.handler._check_event_auth = lambda origin, event, context, state, claimed_auth_event_map, backfilled: succeed(
context
)
self.client = self.homeserver.get_federation_client()
self.client._check_sigs_and_hash_and_fetch = lambda dest, pdus, **k: succeed(
Expand Down