feat: Enforce new federation invite checks

These were introduced in spec v1.16 however we didn't implement them until now for compatibility.
This commit is contained in:
timedout authored and Ellis Git committed 2026-07-12 02:11:50 +00:00
1 parent 556141b404
commit 995726923b
2 files changed
+113 -20

No files matched your search

+109 -18
View File
@@ -1,19 +1,24 @@
use std::collections::{HashMap, hash_map::Entry};
use axum::extract::State;
use axum_client_ip::ClientIp;
use base64::{Engine as _, engine::general_purpose};
use conduwuit::{
Err, Error, PduEvent, Result, err, error,
matrix::{Event, event::gen_event_id},
Err, Error, EventTypeExt, PduEvent, Result, err, error,
matrix::{Event, StateKey, event::gen_event_id},
utils::{self, hash::sha256},
warn,
};
use ruma::{
CanonicalJsonValue, UserId,
CanonicalJsonValue, OwnedEventId, UserId,
api::{
error::{ErrorKind, IncompatibleRoomVersionErrorData},
federation::membership::{RawStrippedState, create_invite},
},
events::room::member::{MembershipState, RoomMemberEventContent},
events::{
StateEventType,
room::member::{MembershipState, RoomMemberEventContent},
},
serde::JsonObject,
};
@@ -67,6 +72,14 @@ pub(crate) async fn create_invite_route(
let mut signed_event = utils::to_canonical_object(&body.event)
.map_err(|_| err!(Request(InvalidParam("Invite event is invalid."))))?;
services
.server_keys
.verify_event(&signed_event, &room_version_rules)
.await
.map_err(|e| {
err!(Request(InvalidParam("Signature verification failed on invite event: {e}")))
})?;
// Ensure this is a membership event
if signed_event
.get("type")
@@ -123,23 +136,83 @@ pub(crate) async fn create_invite_route(
return Err!(Request(InvalidParam("User does not belong to this homeserver.")));
}
// Make sure we're not ACL'ed from their room.
services
.rooms
.event_handler
.acl_check(recipient_user.server_name(), &body.room_id)
.await?;
// Validate the invite room state.
let mut invite_state_map: HashMap<(StateEventType, StateKey), _> =
HashMap::with_capacity(body.invite_room_state.len());
let mut create_event_id: Option<OwnedEventId> = None;
for (idx, invite_state_event) in body.invite_room_state.iter().cloned().enumerate() {
// Stripped state hasn't been sent over federation since v1.16.
let RawStrippedState::Pdu(raw_pdu) = invite_state_event else {
return Err!(Request(InvalidParam(
"PDU in invite state (index {idx}) violates the room event format"
)));
};
let (state_event_room_id, state_event_id, state_event_json) = services
.rooms
.event_handler
.parse_incoming_pdu(&raw_pdu)
.await
.map_err(|e| err!(Request(InvalidParam("Invalid PDU in invite state: {e}"))))?;
services
.server_keys
.hash_and_sign_event(&mut signed_event, &room_version_rules)
.map_err(|e| err!(Request(InvalidParam("Failed to sign event: {e}"))))?;
if state_event_room_id != body.room_id {
return Err!(Request(InvalidParam(
"PDU in invite state ({state_event_id}) belongs to the wrong room"
)));
}
// Generate event id
let event_id = gen_event_id(&signed_event, &room_version_rules)?;
services
.server_keys
.verify_event(&signed_event, &room_version_rules)
.await
.map_err(|e| {
err!(Request(InvalidParam("Signature verification failed on invite event: {e}")))
})?;
// Add event_id back
signed_event.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.to_string()));
let Some(state_key) = state_event_json.get("state_key").and_then(|k| k.as_str()) else {
return Err!(Request(InvalidParam(
"PDU in invite state ({state_event_id}) is not a state event"
)));
};
let Some(event_type) = state_event_json.get("event_type").and_then(|k| k.as_str()) else {
return Err!(Request(InvalidParam(
"PDU in invite state ({state_event_id}) is not an event?"
)));
};
let key = StateEventType::from(event_type).with_state_key(state_key);
match invite_state_map.entry(key) {
| Entry::Occupied(entry) =>
return Err!(Request(InvalidParam(
"Duplicate state events in invite state for state key: {:?}",
entry.key(),
))),
| Entry::Vacant(entry) => {
if entry.key().0 == StateEventType::RoomCreate {
let event_id = gen_event_id(&state_event_json, &room_version_rules)
.expect("must be able to generate room ID");
create_event_id = Some(event_id);
}
entry.insert(state_event_json);
},
}
}
let Some(create_event_id) = create_event_id else {
return Err!(Request(InvalidParam(
"Invite state does not contain the m.room.create event"
)));
};
invite_state_map.iter().try_for_each(|(key, event_json)| {
service::rooms::event_handler::Service::pdu_format_check_1(
event_json,
&room_version_rules,
&create_event_id,
)
.map_err(|e| {
err!(Request(InvalidParam(
"PDU in invite state for {key:?} violates the room event format: {e}"
)))
})
})?;
if services.rooms.metadata.is_banned(&body.room_id).await
&& !services.users.is_admin(&recipient_user).await
@@ -161,6 +234,24 @@ pub(crate) async fn create_invite_route(
return Err!(Request(Forbidden("Invite rejected by antispam service.")));
}
// Make sure we're not ACL'ed from their room.
services
.rooms
.event_handler
.acl_check(recipient_user.server_name(), &body.room_id)
.await?;
services
.server_keys
.hash_and_sign_event(&mut signed_event, &room_version_rules)
.map_err(|e| err!(Request(InvalidParam("Failed to sign event: {e}"))))?;
// Generate event id
let event_id = gen_event_id(&signed_event, &room_version_rules)?;
// Add event_id back
signed_event.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.to_string()));
let mut invite_state = body.invite_room_state.clone();
let mut event: JsonObject = serde_json::from_str(body.event.get())
@@ -18,7 +18,7 @@ impl super::Service {
/// Checks that the PDU conforms to the PDU format (check 1). This is
/// already mostly done during deserialisation, so this function just checks
/// that the PDU isn't a too large.
pub(super) fn pdu_format_check_1(
pub fn pdu_format_check_1(
pdu_json: &CanonicalJsonObject,
room_version_rules: &RoomVersionRules,
create_event_id: &EventId,
@@ -45,6 +45,8 @@ impl super::Service {
let create_event_in_auth_events = auth_events.iter().any(|id| id == create_event_id);
if !event_format.allow_room_create_in_auth_events && create_event_in_auth_events {
return Err!(Request(BadJson("PDU references a create event")));
} else if event_format.allow_room_create_in_auth_events && !create_event_in_auth_events {
return Err!(Request(BadJson("PDU does not reference the room create event")));
}
let prev_events = expect_event_id_array(pdu_json, "prev_events")?;
@@ -59,7 +61,7 @@ impl super::Service {
/// the content hash verification fails (check 3), returning the
/// potentially modified JSON. Returns an error if the PDU cannot be
/// redacted, or fails signature verification.
pub(super) async fn signature_hash_check_2_3(
pub async fn signature_hash_check_2_3(
&self,
pdu_json: CanonicalJsonObject,
room_version_rules: &RoomVersionRules,