diff --git a/Cargo.toml b/Cargo.toml index 3406566f..7f87ff0d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -176,6 +176,7 @@ features = [ "state-res", "unstable-msc2448", "unstable-msc4186", + "unstable-msc4311", ] git = "https://github.com/ruma/ruma.git" @@ -197,6 +198,8 @@ jemalloc = ["tikv-jemallocator"] sqlite = ["parking_lot", "rusqlite", "tokio/signal"] systemd = ["sd-notify"] +enforce_msc4311 = [] + [[bin]] name = "conduit" path = "src/main.rs" diff --git a/src/api/client_server/membership.rs b/src/api/client_server/membership.rs index b33314e8..e1fcc3bb 100644 --- a/src/api/client_server/membership.rs +++ b/src/api/client_server/membership.rs @@ -188,6 +188,9 @@ pub async fn knock_room_route( } _ => return Err(Error::BadServerResponse("Room version is not supported")), }; + let rules = room_version_id + .rules() + .expect("Supported room version has rules"); let (event_id, knock_event, _) = services().rooms.helpers.populate_membership_template( &knock_template.event, @@ -212,6 +215,8 @@ pub async fn knock_room_route( ) .await?; + utils::check_stripped_state(&send_kock_response.knock_room_state, &room_id, &rules)?; + info!("send_knock finished"); let mut stripped_state = send_kock_response.knock_room_state; diff --git a/src/api/server_server.rs b/src/api/server_server.rs index 0d8d1140..f03c0890 100644 --- a/src/api/server_server.rs +++ b/src/api/server_server.rs @@ -2101,6 +2101,12 @@ pub async fn create_invite_route( )); } + let rules = room_version + .rules() + .expect("Supported room version has rules"); + + utils::check_stripped_state(&invite_room_state, &room_id, &rules)?; + let mut signed_event = utils::to_canonical_object(&event) .map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invite event is invalid."))?; @@ -2108,23 +2114,15 @@ pub async fn create_invite_route( services().globals.server_name().as_str(), services().globals.keypair(), &mut signed_event, - &room_version - .rules() - .expect("Supported room version has rules") - .redaction, + &rules.redaction, ) .map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Failed to sign event."))?; // Generate event id let event_id = EventId::parse(format!( "${}", - ruma::signatures::reference_hash( - &signed_event, - &room_version - .rules() - .expect("Supported room version has rules") - ) - .expect("Event format validated when event was hashed") + ruma::signatures::reference_hash(&signed_event, &rules) + .expect("Event format validated when event was hashed") )) .expect("ruma's reference hashes are valid event ids"); diff --git a/src/service/rooms/state/mod.rs b/src/service/rooms/state/mod.rs index b6dce017..d903188b 100644 --- a/src/service/rooms/state/mod.rs +++ b/src/service/rooms/state/mod.rs @@ -276,8 +276,25 @@ impl Service { .room_state_get(room_id, state_event_type, "") .transpose() }) - .map(|e| e.map(|e| RawStrippedState::Stripped(e.to_stripped_state_event()))) - .collect() + .map(|e| { + if e.as_ref() + .is_ok_and(|e| e.kind == TimelineEventType::RoomCreate) + { + e.and_then(|e| { + services() + .rooms + .timeline + .get_pdu_json(&e.event_id) + .transpose() + .expect("Event must be present for it to make up the current state") + .map(PduEvent::convert_to_outgoing_federation_event) + .map(RawStrippedState::Pdu) + }) + } else { + e.map(|e| RawStrippedState::Stripped(e.to_stripped_state_event())) + } + }) + .collect::>>() } pub fn stripped_state_client(&self, room_id: &RoomId) -> Result>> { diff --git a/src/utils/mod.rs b/src/utils/mod.rs index dc498d92..12ba3d51 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -5,18 +5,24 @@ use cmp::Ordering; use rand::prelude::*; use ring::digest; use ruma::{ - api::{client::sync::sync_events::StrippedState, federation::membership::RawStrippedState}, + api::{ + client::{error::ErrorKind, sync::sync_events::StrippedState}, + federation::membership::RawStrippedState, + }, canonical_json::try_from_json_map, + events::{AnyStateEvent, StateEventType}, + room_version_rules::RoomVersionRules, serde::Raw, - CanonicalJsonError, CanonicalJsonObject, RoomId, + CanonicalJsonError, CanonicalJsonObject, CanonicalJsonValue, RoomId, }; +use serde_json::value::to_raw_value; use std::{ cmp, fmt, str::FromStr, time::{SystemTime, UNIX_EPOCH}, }; -use crate::Result; +use crate::{service::pdu::gen_event_id_canonical_json, services, Result}; pub fn millis_since_unix_epoch() -> u64 { SystemTime::now() @@ -196,12 +202,119 @@ impl fmt::Display for HtmlEscape<'_> { /// Converts `RawStrippedState` (federation format) into `Raw` (client format) pub fn convert_stripped_state( stripped_state: Vec, - _room_id: &RoomId, + room_id: &RoomId, ) -> Result>> { stripped_state .into_iter() .map(|stripped_state| match stripped_state { RawStrippedState::Stripped(state) => Ok(state.cast()), + RawStrippedState::Pdu(state) => { + let rules = services() + .rooms + .state + .get_room_version(room_id)? + .rules() + .expect("Supported room version must have rules."); + let (event_id, mut event) = gen_event_id_canonical_json(&state, &rules)?; + + event.retain(|k, _| { + matches!( + k.as_str(), + "content" + | "event_id" + | "origin_server_ts" + | "room_id" + | "sender" + | "state_key" + | "type" + | "unsigned" + ) + }); + + event.insert("event_id".to_owned(), event_id.as_str().into()); + + let raw_value = to_raw_value(&CanonicalJsonValue::Object(event)) + .expect("To raw json should not fail since only change was adding signature"); + + Ok(Raw::::from_json(raw_value).cast()) + } }) .collect() } + +pub fn check_stripped_state( + stripped_state: &Vec, + room_id: &RoomId, + rules: &RoomVersionRules, +) -> Result<()> { + // Nothing needs to be done for legacy room ids + if room_id.server_name().is_some() && !rules.authorization.room_create_event_id_as_room_id { + return Ok(()); + } + + let mut seen_create_event = false; + #[cfg(feature = "enforce_msc4311")] + let mut seen_valid_create_event = false; + + for state in stripped_state { + match state { + RawStrippedState::Pdu(pdu) => { + let Ok((event_id, value)) = gen_event_id_canonical_json(pdu, rules) else { + continue; + }; + let Some(event_type) = value.get("type").and_then(|t| t.as_str()) else { + continue; + }; + if event_type != "m.room.create" { + continue; + } + if seen_create_event { + return Err(error::Error::BadRequest( + ErrorKind::InvalidParam, + "Stripped state has multiple create events", + )); + } + if event_id.localpart() != room_id.strip_sigil() { + return Err(error::Error::BadRequest( + ErrorKind::InvalidParam, + "Room ID generated from create event does not match that from the request", + )); + } + + seen_create_event = true; + #[cfg(feature = "enforce_msc4311")] + { + seen_valid_create_event = true; + } + } + RawStrippedState::Stripped(event) => { + let Ok(event) = event.deserialize() else { + continue; + }; + + if event.event_type() != StateEventType::RoomCreate { + continue; + } + + if seen_create_event { + return Err(error::Error::BadRequest( + ErrorKind::InvalidParam, + "Stripped state has multiple create events", + )); + } + + seen_create_event = true; + } + } + } + + #[cfg(feature = "enforce_msc4311")] + if !seen_valid_create_event { + return Err(error::Error::BadRequest( + ErrorKind::InvalidParam, + "Stripped state contained no valid create PDUs", + )); + } + + Ok(()) +}