From b3cf649732a9de5d5d5a7a157aee3810492f23cb Mon Sep 17 00:00:00 2001 From: timedout Date: Thu, 18 Dec 2025 04:52:44 +0000 Subject: [PATCH] wip: Refactor event auth --- src/core/matrix/state_res/benches.rs | 552 ------------------ src/core/matrix/state_res/error.rs | 11 +- .../state_res/event_auth/auth_events.rs | 165 ++++++ .../state_res/event_auth/create_event.rs | 97 +++ .../iterative_auth_checks.rs} | 230 ++------ .../state_res/event_auth/member_event.rs | 3 + src/core/matrix/state_res/event_auth/mod.rs | 3 + src/core/matrix/state_res/mod.rs | 7 +- src/service/rooms/timeline/create.rs | 2 +- 9 files changed, 341 insertions(+), 729 deletions(-) delete mode 100644 src/core/matrix/state_res/benches.rs create mode 100644 src/core/matrix/state_res/event_auth/auth_events.rs create mode 100644 src/core/matrix/state_res/event_auth/create_event.rs rename src/core/matrix/state_res/{event_auth.rs => event_auth/iterative_auth_checks.rs} (89%) create mode 100644 src/core/matrix/state_res/event_auth/member_event.rs create mode 100644 src/core/matrix/state_res/event_auth/mod.rs diff --git a/src/core/matrix/state_res/benches.rs b/src/core/matrix/state_res/benches.rs deleted file mode 100644 index de62f266..00000000 --- a/src/core/matrix/state_res/benches.rs +++ /dev/null @@ -1,552 +0,0 @@ -#[cfg(conduwuit_bench)] -extern crate test; - -use std::{ - borrow::Borrow, - collections::{HashMap, HashSet}, - sync::atomic::{AtomicU64, Ordering::SeqCst}, -}; - -use futures::{future, future::ready}; -use maplit::{btreemap, hashmap, hashset}; -use ruma::{ - EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, RoomVersionId, Signatures, UserId, - events::{ - StateEventType, TimelineEventType, - room::{ - join_rules::{JoinRule, RoomJoinRulesEventContent}, - member::{MembershipState, RoomMemberEventContent}, - }, - }, - int, room_id, uint, user_id, -}; -use serde_json::{ - json, - value::{RawValue as RawJsonValue, to_raw_value as to_raw_json_value}, -}; - -use crate::{ - matrix::{Event, Pdu, pdu::EventHash}, - state_res::{self as state_res, Error, Result, StateMap}, -}; - -static SERVER_TIMESTAMP: AtomicU64 = AtomicU64::new(0); - -#[cfg(conduwuit_bench)] -#[cfg_attr(conduwuit_bench, bench)] -fn lexico_topo_sort(c: &mut test::Bencher) { - let graph = hashmap! { - event_id("l") => hashset![event_id("o")], - event_id("m") => hashset![event_id("n"), event_id("o")], - event_id("n") => hashset![event_id("o")], - event_id("o") => hashset![], // "o" has zero outgoing edges but 4 incoming edges - event_id("p") => hashset![event_id("o")], - }; - - c.iter(|| { - let _ = state_res::lexicographical_topological_sort(&graph, &|_| { - future::ok((int!(0), MilliSecondsSinceUnixEpoch(uint!(0)))) - }); - }); -} - -#[cfg(conduwuit_bench)] -#[cfg_attr(conduwuit_bench, bench)] -fn resolution_shallow_auth_chain(c: &mut test::Bencher) { - let mut store = TestStore(hashmap! {}); - - // build up the DAG - let (state_at_bob, state_at_charlie, _) = store.set_up(); - - c.iter(|| async { - let ev_map = store.0.clone(); - let state_sets = [&state_at_bob, &state_at_charlie]; - let fetch = |id: OwnedEventId| ready(ev_map.get(&id).map(ToOwned::to_owned)); - let exists = |id: OwnedEventId| ready(ev_map.get(&id).is_some()); - let auth_chain_sets: Vec> = state_sets - .iter() - .map(|map| { - store - .auth_event_ids(room_id(), map.values().cloned().collect()) - .unwrap() - }) - .collect(); - - let _ = match state_res::resolve( - &RoomVersionId::V6, - state_sets.into_iter(), - &auth_chain_sets, - &fetch, - &exists, - ) - .await - { - | Ok(state) => state, - | Err(e) => panic!("{e}"), - }; - }); -} - -#[cfg(conduwuit_bench)] -#[cfg_attr(conduwuit_bench, bench)] -fn resolve_deeper_event_set(c: &mut test::Bencher) { - let mut inner = INITIAL_EVENTS(); - let ban = BAN_STATE_SET(); - - inner.extend(ban); - let store = TestStore(inner.clone()); - - let state_set_a = [ - inner.get(&event_id("CREATE")).unwrap(), - inner.get(&event_id("IJR")).unwrap(), - inner.get(&event_id("IMA")).unwrap(), - inner.get(&event_id("IMB")).unwrap(), - inner.get(&event_id("IMC")).unwrap(), - inner.get(&event_id("MB")).unwrap(), - inner.get(&event_id("PA")).unwrap(), - ] - .iter() - .map(|ev| { - ( - (ev.event_type().clone().into(), ev.state_key().unwrap().into()), - ev.event_id().to_owned(), - ) - }) - .collect::>(); - - let state_set_b = [ - inner.get(&event_id("CREATE")).unwrap(), - inner.get(&event_id("IJR")).unwrap(), - inner.get(&event_id("IMA")).unwrap(), - inner.get(&event_id("IMB")).unwrap(), - inner.get(&event_id("IMC")).unwrap(), - inner.get(&event_id("IME")).unwrap(), - inner.get(&event_id("PA")).unwrap(), - ] - .iter() - .map(|ev| { - ( - (ev.event_type().clone().into(), ev.state_key().unwrap().into()), - ev.event_id().to_owned(), - ) - }) - .collect::>(); - - c.iter(|| async { - let state_sets = [&state_set_a, &state_set_b]; - let auth_chain_sets: Vec> = state_sets - .iter() - .map(|map| { - store - .auth_event_ids(room_id(), map.values().cloned().collect()) - .unwrap() - }) - .collect(); - - let fetch = |id: OwnedEventId| ready(inner.get(&id).map(ToOwned::to_owned)); - let exists = |id: OwnedEventId| ready(inner.get(&id).is_some()); - let _ = match state_res::resolve( - &RoomVersionId::V6, - state_sets.into_iter(), - &auth_chain_sets, - &fetch, - &exists, - ) - .await - { - | Ok(state) => state, - | Err(_) => panic!("resolution failed during benchmarking"), - }; - }); -} - -//*///////////////////////////////////////////////////////////////////// -// -// IMPLEMENTATION DETAILS AHEAD -// -/////////////////////////////////////////////////////////////////////*/ -struct TestStore(HashMap); - -#[allow(unused)] -impl TestStore { - fn get_event(&self, room_id: &RoomId, event_id: &EventId) -> Result { - self.0 - .get(event_id) - .cloned() - .ok_or_else(|| Error::NotFound(format!("{} not found", event_id))) - } - - /// Returns the events that correspond to the `event_ids` sorted in the same - /// order. - fn get_events(&self, room_id: &RoomId, event_ids: &[OwnedEventId]) -> Result> { - let mut events = vec![]; - for id in event_ids { - events.push(self.get_event(room_id, id)?); - } - Ok(events) - } - - /// Returns a Vec of the related auth events to the given `event`. - fn auth_event_ids( - &self, - room_id: &RoomId, - event_ids: Vec, - ) -> Result> { - let mut result = HashSet::new(); - let mut stack = event_ids; - - // DFS for auth event chain - while !stack.is_empty() { - let ev_id = stack.pop().unwrap(); - if result.contains(&ev_id) { - continue; - } - - result.insert(ev_id.clone()); - - let event = self.get_event(room_id, ev_id.borrow())?; - - stack.extend(event.auth_events().map(ToOwned::to_owned)); - } - - Ok(result) - } - - /// Returns a vector representing the difference in auth chains of the given - /// `events`. - fn auth_chain_diff( - &self, - room_id: &RoomId, - event_ids: Vec>, - ) -> Result> { - let mut auth_chain_sets = vec![]; - for ids in event_ids { - // TODO state store `auth_event_ids` returns self in the event ids list - // when an event returns `auth_event_ids` self is not contained - let chain = self - .auth_event_ids(room_id, ids)? - .into_iter() - .collect::>(); - auth_chain_sets.push(chain); - } - - if let Some(first) = auth_chain_sets.first().cloned() { - let common = auth_chain_sets - .iter() - .skip(1) - .fold(first, |a, b| a.intersection(b).cloned().collect::>()); - - Ok(auth_chain_sets - .into_iter() - .flatten() - .filter(|id| !common.contains(id)) - .collect()) - } else { - Ok(vec![]) - } - } -} - -impl TestStore { - #[allow(clippy::type_complexity)] - fn set_up( - &mut self, - ) -> (StateMap, StateMap, StateMap) { - let create_event = to_pdu_event::<&EventId>( - "CREATE", - alice(), - TimelineEventType::RoomCreate, - Some(""), - to_raw_json_value(&json!({ "creator": alice() })).unwrap(), - &[], - &[], - ); - let cre = create_event.event_id().to_owned(); - self.0.insert(cre.clone(), create_event.clone()); - - let alice_mem = to_pdu_event( - "IMA", - alice(), - TimelineEventType::RoomMember, - Some(alice().to_string().as_str()), - member_content_join(), - &[cre.clone()], - &[cre.clone()], - ); - self.0 - .insert(alice_mem.event_id().to_owned(), alice_mem.clone()); - - let join_rules = to_pdu_event( - "IJR", - alice(), - TimelineEventType::RoomJoinRules, - Some(""), - to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Public)).unwrap(), - &[cre.clone(), alice_mem.event_id().to_owned()], - &[alice_mem.event_id().to_owned()], - ); - self.0 - .insert(join_rules.event_id().to_owned(), join_rules.clone()); - - // Bob and Charlie join at the same time, so there is a fork - // this will be represented in the state_sets when we resolve - let bob_mem = to_pdu_event( - "IMB", - bob(), - TimelineEventType::RoomMember, - Some(bob().to_string().as_str()), - member_content_join(), - &[cre.clone(), join_rules.event_id().to_owned()], - &[join_rules.event_id().to_owned()], - ); - self.0 - .insert(bob_mem.event_id().to_owned(), bob_mem.clone()); - - let charlie_mem = to_pdu_event( - "IMC", - charlie(), - TimelineEventType::RoomMember, - Some(charlie().to_string().as_str()), - member_content_join(), - &[cre, join_rules.event_id().to_owned()], - &[join_rules.event_id().to_owned()], - ); - self.0 - .insert(charlie_mem.event_id().to_owned(), charlie_mem.clone()); - - let state_at_bob = [&create_event, &alice_mem, &join_rules, &bob_mem] - .iter() - .map(|ev| { - ( - (ev.event_type().clone().into(), ev.state_key().unwrap().into()), - ev.event_id().to_owned(), - ) - }) - .collect::>(); - - let state_at_charlie = [&create_event, &alice_mem, &join_rules, &charlie_mem] - .iter() - .map(|ev| { - ( - (ev.event_type().clone().into(), ev.state_key().unwrap().into()), - ev.event_id().to_owned(), - ) - }) - .collect::>(); - - let expected = [&create_event, &alice_mem, &join_rules, &bob_mem, &charlie_mem] - .iter() - .map(|ev| { - ( - (ev.event_type().clone().into(), ev.state_key().unwrap().into()), - ev.event_id().to_owned(), - ) - }) - .collect::>(); - - (state_at_bob, state_at_charlie, expected) - } -} - -fn event_id(id: &str) -> OwnedEventId { - if id.contains('$') { - return id.try_into().unwrap(); - } - format!("${}:foo", id).try_into().unwrap() -} - -fn alice() -> &'static UserId { user_id!("@alice:foo") } - -fn bob() -> &'static UserId { user_id!("@bob:foo") } - -fn charlie() -> &'static UserId { user_id!("@charlie:foo") } - -fn ella() -> &'static UserId { user_id!("@ella:foo") } - -fn room_id() -> &'static RoomId { room_id!("!test:foo") } - -fn member_content_ban() -> Box { - to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Ban)).unwrap() -} - -fn member_content_join() -> Box { - to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Join)).unwrap() -} - -fn to_pdu_event( - id: &str, - sender: &UserId, - ev_type: TimelineEventType, - state_key: Option<&str>, - content: Box, - auth_events: &[S], - prev_events: &[S], -) -> Pdu -where - S: AsRef, -{ - // We don't care if the addition happens in order just that it is atomic - // (each event has its own value) - let ts = SERVER_TIMESTAMP.fetch_add(1, SeqCst); - let id = if id.contains('$') { - id.to_owned() - } else { - format!("${}:foo", id) - }; - let auth_events = auth_events - .iter() - .map(AsRef::as_ref) - .map(event_id) - .collect::>(); - let prev_events = prev_events - .iter() - .map(AsRef::as_ref) - .map(event_id) - .collect::>(); - - Pdu { - event_id: id.try_into().unwrap(), - room_id: Some(room_id().to_owned()), - sender: sender.to_owned(), - origin_server_ts: ts.try_into().unwrap(), - state_key: state_key.map(Into::into), - kind: ev_type, - content, - origin: None, - redacts: None, - unsigned: None, - auth_events, - prev_events, - depth: uint!(0), - hashes: EventHash { sha256: String::new() }, - signatures: None, - } -} - -// all graphs start with these input events -#[allow(non_snake_case)] -fn INITIAL_EVENTS() -> HashMap { - vec![ - to_pdu_event::<&EventId>( - "CREATE", - alice(), - TimelineEventType::RoomCreate, - Some(""), - to_raw_json_value(&json!({ "creator": alice() })).unwrap(), - &[], - &[], - ), - to_pdu_event( - "IMA", - alice(), - TimelineEventType::RoomMember, - Some(alice().as_str()), - member_content_join(), - &["CREATE"], - &["CREATE"], - ), - to_pdu_event( - "IPOWER", - alice(), - TimelineEventType::RoomPowerLevels, - Some(""), - to_raw_json_value(&json!({ "users": { alice(): 100 } })).unwrap(), - &["CREATE", "IMA"], - &["IMA"], - ), - to_pdu_event( - "IJR", - alice(), - TimelineEventType::RoomJoinRules, - Some(""), - to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Public)).unwrap(), - &["CREATE", "IMA", "IPOWER"], - &["IPOWER"], - ), - to_pdu_event( - "IMB", - bob(), - TimelineEventType::RoomMember, - Some(bob().to_string().as_str()), - member_content_join(), - &["CREATE", "IJR", "IPOWER"], - &["IJR"], - ), - to_pdu_event( - "IMC", - charlie(), - TimelineEventType::RoomMember, - Some(charlie().to_string().as_str()), - member_content_join(), - &["CREATE", "IJR", "IPOWER"], - &["IMB"], - ), - to_pdu_event::<&EventId>( - "START", - charlie(), - TimelineEventType::RoomTopic, - Some(""), - to_raw_json_value(&json!({})).unwrap(), - &[], - &[], - ), - to_pdu_event::<&EventId>( - "END", - charlie(), - TimelineEventType::RoomTopic, - Some(""), - to_raw_json_value(&json!({})).unwrap(), - &[], - &[], - ), - ] - .into_iter() - .map(|ev| (ev.event_id().to_owned(), ev)) - .collect() -} - -// all graphs start with these input events -#[allow(non_snake_case)] -fn BAN_STATE_SET() -> HashMap { - vec![ - to_pdu_event( - "PA", - alice(), - TimelineEventType::RoomPowerLevels, - Some(""), - to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(), - &["CREATE", "IMA", "IPOWER"], // auth_events - &["START"], // prev_events - ), - to_pdu_event( - "PB", - alice(), - TimelineEventType::RoomPowerLevels, - Some(""), - to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(), - &["CREATE", "IMA", "IPOWER"], - &["END"], - ), - to_pdu_event( - "MB", - alice(), - TimelineEventType::RoomMember, - Some(ella().as_str()), - member_content_ban(), - &["CREATE", "IMA", "PB"], - &["PA"], - ), - to_pdu_event( - "IME", - ella(), - TimelineEventType::RoomMember, - Some(ella().as_str()), - member_content_join(), - &["CREATE", "IJR", "PA"], - &["MB"], - ), - ] - .into_iter() - .map(|ev| (ev.event_id().to_owned(), ev)) - .collect() -} diff --git a/src/core/matrix/state_res/error.rs b/src/core/matrix/state_res/error.rs index 7711d878..c048516a 100644 --- a/src/core/matrix/state_res/error.rs +++ b/src/core/matrix/state_res/error.rs @@ -14,10 +14,19 @@ pub enum Error { Unsupported(String), /// The given event was not found. - #[error("Not found error: {0}")] + #[error("Event not found: {0}")] NotFound(String), /// Invalid fields in the given PDU. #[error("Invalid PDU: {0}")] InvalidPdu(String), + + /// This event contained multiple auth events of the same type and state + /// key. + #[error("Duplicate auth events: {0}")] + DuplicateAuthEvents(String), + + /// This event contains unnecessary auth events. + #[error("Unknown or unnecessary auth events present: {0}")] + UnselectedAuthEvents(String), } diff --git a/src/core/matrix/state_res/event_auth/auth_events.rs b/src/core/matrix/state_res/event_auth/auth_events.rs new file mode 100644 index 00000000..322542dd --- /dev/null +++ b/src/core/matrix/state_res/event_auth/auth_events.rs @@ -0,0 +1,165 @@ +//! Auth checks relevant to any event's `auth_events`. +//! +//! See: https://spec.matrix.org/v1.16/rooms/v12/#authorization-rules +use std::{ + collections::{HashMap, HashSet}, + future::Future, +}; + +use ruma::{EventId, OwnedEventId, RoomId, events::StateEventType}; + +use crate::{Event, EventTypeExt, Pdu, RoomVersion, matrix::StateKey, state_res::Error, warn}; + +// Checks for duplicate auth events in the `auth_events` field of an event. +// Note: the caller should already have all of the auth events fetched. +// +// If there are multiple auth events of the same type and state key, this +// returns an error. Otherwise, it returns a map of (type, state_key) to the +// corresponding auth event. +pub async fn check_duplicate_auth_events( + auth_events: &[OwnedEventId], + fetch_event: impl Fn(&EventId) -> Fut + Send, +) -> Result, Error> +where + Fut: Future, Error>> + Send, + E: Event + Send + Sync, + for<'a> &'a E: Event + Send, +{ + let mut seen: HashMap<(StateEventType, StateKey), E> = HashMap::new(); + + // Considering all of the event's auth events: + for auth_event_id in auth_events { + if let Ok(Some(auth_event)) = fetch_event(auth_event_id).await { + let event_type = auth_event.kind(); + // If this is not a state event, reject it. + let Some(state_key) = &auth_event.state_key() else { + return Err(Error::InvalidPdu(format!( + "Auth event {:?} is not a state event", + auth_event_id + ))); + }; + let type_key_pair: (StateEventType, StateKey) = + event_type.clone().with_state_key(state_key.clone()); + + // If there are duplicate entries for a given type and state_key pair, reject. + if seen.contains_key(&type_key_pair) { + return Err(Error::DuplicateAuthEvents(format!( + "({:?},\"{:?}\")", + event_type, state_key + ))); + } + seen.insert(type_key_pair, auth_event); + } else { + return Err(Error::NotFound(auth_event_id.as_str().to_owned())); + } + } + + Ok(seen) +} + +// Checks that the event does not refer to any auth events that it does not need +// to. +pub fn check_unnecessary_auth_events( + auth_events: &HashSet<(StateEventType, StateKey)>, + expected: &Vec<(StateEventType, StateKey)>, +) -> Result<(), Error> { + // If there are entries whose type and state_key don't match those specified by + // the auth events selection algorithm described in the server specification, + // reject. + let remaining = auth_events + .iter() + .filter(|key| !expected.contains(key)) + .collect::>(); + if !remaining.is_empty() { + return Err(Error::UnselectedAuthEvents(format!("{:?}", remaining))); + } + Ok(()) +} + +// Checks that all provided auth events were not rejected previously. +// +// TODO: this is currently a no-op and always returns Ok(()). +pub fn check_all_auth_events_accepted( + _auth_events: &HashMap<(StateEventType, StateKey), E>, +) -> Result<(), Error> +where + E: Event + Send + Sync, + for<'a> &'a E: Event + Send, +{ + Ok(()) +} + +// Checks that all auth events are from the same room as the event being +// validated. +pub fn check_auth_same_room(auth_events: &Vec, room_id: &RoomId) -> bool +where + E: Event + Send + Sync, + for<'a> &'a E: Event + Send, +{ + for auth_event in auth_events { + if let Some(auth_room_id) = &auth_event.room_id() { + if auth_room_id.as_str() != room_id.as_str() { + warn!( + auth_event_id=%auth_event.event_id(), + "Auth event room id {} does not match expected room id {}", + auth_room_id, + room_id + ); + return false; + } + } else { + warn!(auth_event_id=%auth_event.event_id(), "Auth event has no room_id"); + return false; + } + } + true +} + +// Performs all auth event checks for the given event. +pub async fn check_auth_events( + event: &Pdu, + room_id: &RoomId, + room_version: &RoomVersion, + fetch_event: impl Fn(&EventId) -> Fut + Send, +) -> Result, Error> +where + Fut: Future, Error>> + Send, + E: Event + Send + Sync, + for<'a> &'a E: Event + Send, +{ + // If there are duplicate entries for a given type and state_key pair, reject. + let auth_events_map = check_duplicate_auth_events(&event.auth_events, fetch_event).await?; + let auth_events_set: HashSet<(StateEventType, StateKey)> = + auth_events_map.keys().cloned().collect(); + + // If there are entries whose type and state_key don’t match those specified by + // the auth events selection algorithm described in the server specification, + // reject. + let expected_auth_events = crate::state_res::auth_types_for_event( + event.kind(), + event.sender(), + event.state_key(), + event.content(), + room_version, + )?; + if let Err(e) = check_unnecessary_auth_events(&auth_events_set, &expected_auth_events) { + return Err(e); + } + + // If there are entries which were themselves rejected under the checks + // performed on receipt of a PDU, reject. + if let Err(e) = check_all_auth_events_accepted(&auth_events_map) { + return Err(e); + } + + // If any event in auth_events has a room_id which does not match that of the + // event being authorised, reject. + let auth_event_refs: Vec = auth_events_map.values().cloned().collect(); + if !check_auth_same_room(&auth_event_refs, room_id) { + return Err(Error::InvalidPdu( + "One or more auth events are from a different room".to_owned(), + )); + } + + Ok(auth_events_map) +} diff --git a/src/core/matrix/state_res/event_auth/create_event.rs b/src/core/matrix/state_res/event_auth/create_event.rs new file mode 100644 index 00000000..f5d1cc71 --- /dev/null +++ b/src/core/matrix/state_res/event_auth/create_event.rs @@ -0,0 +1,97 @@ +//! Auth checks relevant to the `m.room.create` event specifically. +//! +//! See: https://spec.matrix.org/v1.16/rooms/v12/#authorization-rules + +use ruma::{OwnedUserId, RoomVersionId, events::room::create::RoomCreateEventContent}; +use serde::Deserialize; +use serde_json::from_str; + +use crate::{Event, Pdu, RoomVersion, state_res::Error, trace}; + +// A raw representation of the create event content, for initial parsing. +// This allows us to extract fields without fully validating the event first. +#[derive(Deserialize)] +struct RawCreateContent { + creator: Option, + room_version: Option, + additional_creators: Option>, +} + +// Check whether an `m.room.create` event is valid. +// This ensures that: +// +// 1. The event has no `prev_events` +// 2. If the version disallows it, the event has no `room_id` present. +// 3. If the room version is present and recognised, otherwise assume invalid. +// 4. If the room version supports it, `additional_creators` is populated with +// valid user IDs. +// 5. If the room version supports it, `creator` is populated AND is a valid +// user ID. +// 6. Otherwise, this event is valid. +// +// The fully deserialized `RoomCreateEventContent` is returned for further calls +// to other checks. +pub fn check_room_create(event: &Pdu) -> Result { + // Check 1: The event has no `prev_events` + if !event.prev_events.is_empty() { + return Err(Error::InvalidPdu("m.room.create event has prev_events".to_owned())); + } + + let create_content = from_str::(event.content().get())?; + + // Note: Here we attempt to both load the raw room version string and validate + // it, and then cast it to the room features. If either step fails, we return + // an unsupported error. If the room version is missing, it defaults to "1", + // which we also do not support. + // + // This performs check 3, which then allows us to perform check 2. + let room_version = if let Some(raw_room_version) = create_content.room_version { + trace!("Parsing and interpreting room version: {}", raw_room_version); + let room_version_id = RoomVersionId::try_from(raw_room_version.as_str()) + .map_err(|_| Error::Unsupported(raw_room_version))?; + RoomVersion::new(&room_version_id) + .map_err(|_| Error::Unsupported(room_version_id.as_str().to_owned()))? + } else { + return Err(Error::Unsupported("1".to_owned())); + }; + + // Check 2: If the version disallows it, the event has no `room_id` present. + if room_version.room_ids_as_hashes && event.room_id.is_some() { + return Err(Error::InvalidPdu( + "m.room.create event has room_id but room version disallows it".to_owned(), + )); + } + + // Check 4: If the room version supports it, `additional_creators` is populated + // with valid user IDs. + if room_version.explicitly_privilege_room_creators { + if let Some(additional_creators) = create_content.additional_creators { + for creator in additional_creators { + trace!("Validating additional creator user ID: {}", creator); + if OwnedUserId::parse(&creator).is_err() { + return Err(Error::InvalidPdu(format!( + "Invalid user ID in additional_creators: {creator}" + ))); + } + } + } + } + + // Check 5: If the room version supports it, `creator` is populated AND is a + // valid user ID. + if !room_version.use_room_create_sender { + if let Some(creator) = create_content.creator { + trace!("Validating creator user ID: {}", creator); + if OwnedUserId::parse(&creator).is_err() { + return Err(Error::InvalidPdu(format!("Invalid user ID in creator: {creator}"))); + } + } else { + return Err(Error::InvalidPdu( + "m.room.create event missing creator field".to_owned(), + )); + } + } + + // Deserialise into the full create event for future checks. + Ok(from_str::(event.content().get())?) +} diff --git a/src/core/matrix/state_res/event_auth.rs b/src/core/matrix/state_res/event_auth/iterative_auth_checks.rs similarity index 89% rename from src/core/matrix/state_res/event_auth.rs rename to src/core/matrix/state_res/event_auth/iterative_auth_checks.rs index 24f03162..057bbca4 100644 --- a/src/core/matrix/state_res/event_auth.rs +++ b/src/core/matrix/state_res/event_auth/iterative_auth_checks.rs @@ -5,7 +5,7 @@ use futures::{ future::{OptionFuture, join, join3}, }; use ruma::{ - Int, OwnedUserId, RoomVersionId, UserId, + EventId, Int, OwnedUserId, RoomVersionId, UserId, events::room::{ create::RoomCreateEventContent, join_rules::{JoinRule, RoomJoinRulesEventContent}, @@ -22,7 +22,7 @@ use serde::{ }; use serde_json::{from_str as from_json_str, value::RawValue as RawJsonValue}; -use super::{ +use super::super::{ Error, Event, Result, StateEventType, StateKey, TimelineEventType, power_levels::{ deserialize_power_levels, deserialize_power_levels_content_fields, @@ -30,7 +30,11 @@ use super::{ }, room_version::RoomVersion, }; -use crate::{debug, error, trace, warn}; +use crate::{ + Pdu, debug, error, + state_res::event_auth::{auth_events::check_auth_events, create_event::check_room_create}, + trace, warn, +}; // FIXME: field extracting could be bundled for `content` #[derive(Deserialize)] @@ -147,218 +151,104 @@ pub fn auth_types_for_event( /// to know if the event passes auth against some state not a recursive /// collection of auth_events fields. #[tracing::instrument( - level = "debug", skip_all, fields( event_id = incoming_event.event_id().as_str(), + event_type = ?incoming_event.event_type().to_string() ) )] #[allow(clippy::suspicious_operation_groupings)] -pub async fn auth_check( +pub async fn iterative_auth_check( room_version: &RoomVersion, - incoming_event: &E, - current_third_party_invite: Option<&E>, - fetch_state: F, - create_event: &E, + incoming_event: &Pdu, + current_third_party_invite: Option<&Pdu>, + fetch_event: impl Fn(&EventId) -> Fut + Send, + create_event: &Pdu, ) -> Result where - F: Fn(&StateEventType, &str) -> Fut + Send, - Fut: Future> + Send, + Fut: Future, Error>> + Send, E: Event + Send + Sync, for<'a> &'a E: Event + Send, { - debug!( - event_id = %incoming_event.event_id(), - event_type = ?incoming_event.event_type(), - "auth_check beginning" - ); - - // [synapse] check that all the events are in the same room as `incoming_event` - - // [synapse] do_sig_check check the event has valid signatures for member events - + debug!("auth_check beginning"); let sender = incoming_event.sender(); - // Implementation of https://spec.matrix.org/latest/rooms/v1/#authorization-rules - // - // 1. If type is m.room.create: + // If type is m.room.create: if *incoming_event.event_type() == TimelineEventType::RoomCreate { debug!("start m.room.create check"); - - // If it has any previous events, reject - if incoming_event.prev_events().next().is_some() { - warn!("the room creation event had previous events"); + if let Err(e) = check_room_create(incoming_event) { + warn!("m.room.create event has been rejected: {}", e); return Ok(false); } - // If the domain of the room_id does not match the domain of the sender, reject - if incoming_event.room_id().is_some() { - let Some(room_id_server_name) = incoming_event.room_id().unwrap().server_name() - else { - warn!("legacy room ID has no server name"); - return Ok(false); - }; - if room_id_server_name != sender.server_name() { - warn!( - expected = %sender.server_name(), - received = %room_id_server_name, - "server name of legacy room ID does not match server name of sender" - ); - return Ok(false); - } - } - - // If content.room_version is present and is not a recognized version, reject - let content: RoomCreateContentFields = from_json_str(incoming_event.content().get())?; - if content - .room_version - .is_some_and(|v| v.deserialize().is_err()) - { - warn!("unsupported room version found in m.room.create event"); - return Ok(false); - } - - if room_version.room_ids_as_hashes && incoming_event.room_id().is_some() { - warn!("room create event incorrectly claims to have a room ID when it should not"); - return Ok(false); - } - - if !room_version.use_room_create_sender - && !room_version.explicitly_privilege_room_creators - { - // If content has no creator field, reject - if content.creator.is_none() { - warn!("m.room.create event incorrectly omits 'creator' field"); - return Ok(false); - } - } - debug!("m.room.create event was allowed"); return Ok(true); } - // NOTE(hydra): We always have a room ID from this point forward. + // TODO: we need to know if events have previously been rejected or soft failed + // For now, we'll just assume the create_event is valid. + let create_content = from_json_str::(create_event.content().get()) + .expect("provided create event must be valid"); + let room_version = RoomVersion::new(&create_content.room_version) + .expect("valid create event must have a valid room version"); - /* - // TODO: In the past this code was commented as it caused problems with Synapse. This is no - // longer the case. This needs to be implemented. - // See also: https://github.com/ruma/ruma/pull/2064 - // - // 2. Reject if auth_events - // a. auth_events cannot have duplicate keys since it's a BTree - // b. All entries are valid auth events according to spec - let expected_auth = auth_types_for_event( - incoming_event.kind, - sender, - incoming_event.state_key, - incoming_event.content().clone(), - ); - - dbg!(&expected_auth); - - for ev_key in auth_events.keys() { - // (b) - if !expected_auth.contains(ev_key) { - warn!("auth_events contained invalid auth event"); + // Since v12, If the event’s room_id is not an event ID for an accepted (not + // rejected) m.room.create event, with the sigil ! instead of $, reject. + if room_version.room_ids_as_hashes { + let calculated_room_id = create_event.event_id().as_str().replace('$', "!"); + if let Some(claimed_room_id) = create_event.room_id() { + if claimed_room_id.as_str() != calculated_room_id { + warn!( + expected = %calculated_room_id, + received = %claimed_room_id, + "event's room ID does not match the hash of the m.room.create event ID" + ); + return Ok(false); + } + } else { + warn!("event is missing a room ID"); return Ok(false); } } - */ - let (power_levels_event, sender_member_event) = join( - // fetch_state(&StateEventType::RoomCreate, ""), - fetch_state(&StateEventType::RoomPowerLevels, ""), - fetch_state(&StateEventType::RoomMember, sender.as_str()), - ) - .await; + let room_id = incoming_event.room_id().expect("event must have a room ID"); - let room_create_event = create_event.clone(); - - // Get the content of the room create event, used later. - let room_create_content: RoomCreateContentFields = - from_json_str(room_create_event.content().get())?; - if room_create_content - .room_version - .is_some_and(|v| v.deserialize().is_err()) - { - warn!( - create_event_id = %room_create_event.event_id(), - "unsupported room version found in m.room.create event" - ); - return Ok(false); - } - let expected_room_id = room_create_event.room_id_or_hash(); - - if incoming_event.room_id().expect("event must have a room ID") != expected_room_id { - warn!( - expected = %expected_room_id, - received = %incoming_event.room_id().unwrap(), - "room_id of incoming event ({}) does not match that of the m.room.create event ({})", - incoming_event.room_id().unwrap(), - expected_room_id, - ); + // Considering the event's auth_events + let auth_map = check_auth_events(incoming_event, room_id, &room_version, fetch_event).await; + if let Err(e) = auth_map { + warn!("event's auth events are invalid: {}", e); return Ok(false); } - // If the create event is referenced in the event's auth events, and this is a - // v12 room, reject - let claims_create_event = incoming_event - .auth_events() - .any(|id| id == room_create_event.event_id()); - if room_version.room_ids_as_hashes && claims_create_event { - warn!("event incorrectly references m.room.create event in auth events"); - return Ok(false); - } else if !room_version.room_ids_as_hashes && !claims_create_event { - // If the create event is not referenced in the event's auth events, and this is - // a v11 room, reject - warn!( - missing = %room_create_event.event_id(), - "event incorrectly did not reference an m.room.create in its auth events" - ); - return Ok(false); - } - - if let Some(ref pe) = power_levels_event { - if *pe.room_id().unwrap() != expected_room_id { + // If the content of the m.room.create event in the room state has the property + // m.federate set to false, and the sender domain of the event does not match + // the sender domain of the create event, reject. + if !create_content.federate { + if create_event.sender().server_name() != incoming_event.sender().server_name() { warn!( - expected = %expected_room_id, - received = %pe.room_id().unwrap(), - "room_id of referenced power levels event does not match that of the m.room.create event" + sender = %incoming_event.sender(), + create_sender = %create_event.sender(), + "room is not federated and event's sender domain does not match create event's sender domain" ); return Ok(false); } } - // If the create event content has the field m.federate set to false and the - // sender domain of the event does not match the sender domain of the create - // event, reject. - if !room_version.room_ids_as_hashes - && !room_create_content.federate - && room_create_event.sender().server_name() != incoming_event.sender().server_name() + // Only in room versions 5 and below + if room_version.special_case_aliases_auth + && *incoming_event.event_type() == TimelineEventType::RoomAliases { - warn!( - sender = %incoming_event.sender(), - create_sender = %room_create_event.sender(), - "room is not federated and event's sender domain does not match create event's sender domain" - ); - return Ok(false); - } - - // Only in some room versions 6 and below - if room_version.special_case_aliases_auth { - // 4. If type is m.room.aliases - if *incoming_event.event_type() == TimelineEventType::RoomAliases { - debug!("starting m.room.aliases check"); - + if let Some(state_key) = incoming_event.state_key() { // If sender's domain doesn't matches state_key, reject - if incoming_event.state_key() != Some(sender.server_name().as_str()) { + if state_key != sender.server_name().as_str() { warn!("state_key does not match sender"); return Ok(false); } - - debug!("m.room.aliases event was allowed"); + // Otherwise, allow return Ok(true); } + warn!("m.room.alias event has no state key"); + return Ok(false); } // If type is m.room.member diff --git a/src/core/matrix/state_res/event_auth/member_event.rs b/src/core/matrix/state_res/event_auth/member_event.rs new file mode 100644 index 00000000..c5650218 --- /dev/null +++ b/src/core/matrix/state_res/event_auth/member_event.rs @@ -0,0 +1,3 @@ +//! Auth checks relevant to the `m.room.member` event specifically. +//! +//! See: https://spec.matrix.org/v1.16/rooms/v12/#authorization-rules diff --git a/src/core/matrix/state_res/event_auth/mod.rs b/src/core/matrix/state_res/event_auth/mod.rs new file mode 100644 index 00000000..953aa9c1 --- /dev/null +++ b/src/core/matrix/state_res/event_auth/mod.rs @@ -0,0 +1,3 @@ +pub mod auth_events; +pub mod create_event; +pub mod iterative_auth_checks; diff --git a/src/core/matrix/state_res/mod.rs b/src/core/matrix/state_res/mod.rs index 35654b31..2b3ddea4 100644 --- a/src/core/matrix/state_res/mod.rs +++ b/src/core/matrix/state_res/mod.rs @@ -8,9 +8,6 @@ mod room_version; #[cfg(test)] mod test_utils; -#[cfg(test)] -mod benches; - use std::{ borrow::Borrow, cmp::{Ordering, Reverse}, @@ -32,7 +29,7 @@ use serde_json::from_str as from_json_str; pub(crate) use self::error::Error; use self::power_levels::PowerLevelsContentFields; pub use self::{ - event_auth::{auth_check, auth_types_for_event}, + event_auth::iterative_auth_checks::{auth_types_for_event, iterative_auth_check}, room_version::RoomVersion, }; use crate::{ @@ -729,7 +726,7 @@ where ) }; - let auth_result = auth_check( + let auth_result = iterative_auth_check( room_version, &event, current_third_party, diff --git a/src/service/rooms/timeline/create.rs b/src/service/rooms/timeline/create.rs index 40e41b08..ccb2d6e7 100644 --- a/src/service/rooms/timeline/create.rs +++ b/src/service/rooms/timeline/create.rs @@ -236,7 +236,7 @@ pub async fn create_hash_and_sign_event( | _ => create_pdu.as_ref().unwrap().as_pdu(), }; - let auth_check = state_res::auth_check( + let auth_check = state_res::iterative_auth_check( &room_version, &pdu, None, // TODO: third_party_invite