diff --git a/crates/ruma-signatures/src/functions.rs b/crates/ruma-signatures/src/functions.rs index 433bd934..34df4923 100644 --- a/crates/ruma-signatures/src/functions.rs +++ b/crates/ruma-signatures/src/functions.rs @@ -50,7 +50,10 @@ fn allowed_content_keys_for(event_type: &str, version: &RoomVersionId) -> &'stat _ => &["membership"], }, "m.room.create" => &["creator"], - "m.room.join_rules" => &["join_rule"], + "m.room.join_rules" => match version { + RoomVersionId::V8 | RoomVersionId::V9 => &["join_rule", "allow"], + _ => &["join_rule"], + }, "m.room.power_levels" => &[ "ban", "events", @@ -794,6 +797,9 @@ fn object_retain_keys(object: &mut CanonicalJsonObject, keys: &[&str]) { /// /// It will return the sender's server (unless it's a third party invite) and the event id server /// (on v1 and v2 room versions) +/// +/// Starting with room version 8, if join_authorised_via_users_server is present, a signature from +/// that user is required. fn servers_to_check_signatures( object: &CanonicalJsonObject, version: &RoomVersionId, @@ -829,7 +835,28 @@ fn servers_to_check_signatures( return Err(JsonError::field_missing_from_object("event_id")); } }, - _ => (), + RoomVersionId::V3 + | RoomVersionId::V4 + | RoomVersionId::V5 + | RoomVersionId::V6 + | RoomVersionId::V7 => {} + // TODO: And for all future versions that have join_authorised_via_users_server + RoomVersionId::V8 | RoomVersionId::V9 => { + if let Some(authorized_user) = object + .get("content") + .and_then(|c| c.as_object()) + .and_then(|c| c.get("join_authorised_via_users_server")) + { + let authorized_user = authorized_user.as_str().ok_or_else(|| { + JsonError::not_of_type("join_authorised_via_users_server", JsonType::String) + })?; + let authorized_user = <&UserId>::try_from(authorized_user) + .map_err(|e| Error::from(ParseError::UserId(e)))?; + + servers_to_check.insert(authorized_user.server_name().to_owned()); + } + } + _ => unimplemented!(), } Ok(servers_to_check) @@ -968,6 +995,80 @@ mod tests { assert!(matches!(verification, Verified::Signatures)); } + #[test] + fn verify_event_check_signatures_for_authorized_user() { + let key_pair_sender = generate_key_pair(); + let key_pair_authorized = generate_key_pair(); + let mut signed_event = serde_json::from_str( + r#"{ + "event_id": "$event_id:domain-event", + "auth_events": [], + "content": {"join_authorised_via_users_server": "@authorized:domain-authorized"}, + "depth": 3, + "hashes": { + "sha256": "5jM4wQpv6lnBo7CLIghJuHdW+s2CMBJPUOGOC89ncos" + }, + "origin": "domain", + "origin_server_ts": 1000000, + "prev_events": [], + "room_id": "!x:domain", + "sender": "@name:domain-sender", + "type": "m.room.member", + "unsigned": { + "age_ts": 1000000 + } + }"#, + ) + .unwrap(); + sign_json("domain-sender", &key_pair_sender, &mut signed_event).unwrap(); + sign_json("domain-authorized", &key_pair_authorized, &mut signed_event).unwrap(); + + let mut public_key_map = BTreeMap::new(); + add_key_to_map(&mut public_key_map, "domain-sender", &key_pair_sender); + add_key_to_map(&mut public_key_map, "domain-authorized", &key_pair_authorized); + + let verification_result = verify_event(&public_key_map, &signed_event, &RoomVersionId::V9); + + assert!(verification_result.is_ok()); + let verification = verification_result.unwrap(); + assert!(matches!(verification, Verified::Signatures)); + } + + #[test] + fn verification_fails_if_missing_signatures_for_authorized_user() { + let key_pair_sender = generate_key_pair(); + let mut signed_event = serde_json::from_str( + r#"{ + "event_id": "$event_id:domain-event", + "auth_events": [], + "content": {"join_authorised_via_users_server": "@authorized:domain-authorized"}, + "depth": 3, + "hashes": { + "sha256": "5jM4wQpv6lnBo7CLIghJuHdW+s2CMBJPUOGOC89ncos" + }, + "origin": "domain", + "origin_server_ts": 1000000, + "prev_events": [], + "room_id": "!x:domain", + "sender": "@name:domain-sender", + "type": "X", + "unsigned": { + "age_ts": 1000000 + } + }"#, + ) + .unwrap(); + sign_json("domain-sender", &key_pair_sender, &mut signed_event).unwrap(); + + let mut public_key_map = BTreeMap::new(); + add_key_to_map(&mut public_key_map, "domain-sender", &key_pair_sender); + + let verification_result = verify_event(&public_key_map, &signed_event, &RoomVersionId::V9); + + assert!(verification_result.is_err()) // Should be Err(VerificationError:: + // signature_not_found("domain-authorized"))); + } + #[test] fn verification_fails_if_required_keys_are_not_given() { let key_pair_sender = generate_key_pair(); diff --git a/crates/ruma-state-res/src/event_auth.rs b/crates/ruma-state-res/src/event_auth.rs index f34a010a..99a9e0e2 100644 --- a/crates/ruma-state-res/src/event_auth.rs +++ b/crates/ruma-state-res/src/event_auth.rs @@ -65,6 +65,7 @@ pub fn auth_types_for_event( struct RoomMemberContentFields { membership: Option>, third_party_invite: Option>, + join_authorised_via_users_server: Option>>, } if let Some(state_key) = state_key { @@ -78,6 +79,15 @@ pub fn auth_types_for_event( if !auth_types.contains(&key) { auth_types.push(key); } + + if let Some(Ok(u)) = + content.join_authorised_via_users_server.map(|m| m.deserialize()) + { + let key = (EventType::RoomMember, u.to_string()); + if !auth_types.contains(&key) { + auth_types.push(key); + } + } } let key = (EventType::RoomMember, state_key.to_owned()); @@ -174,6 +184,9 @@ pub fn auth_check( } /* + // TODO: In the past this code caused problems federating with synapse, maybe this has been + // resolved already. Needs testing. + // // 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 @@ -195,34 +208,41 @@ pub fn auth_check( } */ - // 3. If event does not have m.room.create in auth_events reject let room_create_event = match fetch_state(&EventType::RoomCreate, "") { - Some(event) => event, None => { warn!("no m.room.create event in auth chain"); - return Ok(false); } + Some(e) => e, }; + // 3. If event does not have m.room.create in auth_events reject + if !incoming_event.auth_events().any(|id| id.borrow() == room_create_event.event_id().borrow()) + { + warn!("no m.room.create event in auth events"); + return Ok(false); + } + // [synapse] checks for federation here - // 4. If type is m.room.aliases - if *incoming_event.event_type() == EventType::RoomAliases - && room_version.special_case_aliases_auth - { - info!("starting m.room.aliases check"); + // 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() == EventType::RoomAliases { + info!("starting m.room.aliases check"); - // If sender's domain doesn't matches state_key, reject - if incoming_event.state_key() != Some(sender.server_name().as_str()) { - warn!("state_key does not match sender"); - return Ok(false); + // If sender's domain doesn't matches state_key, reject + if incoming_event.state_key() != Some(sender.server_name().as_str()) { + warn!("state_key does not match sender"); + return Ok(false); + } + + info!("m.room.aliases event was allowed"); + return Ok(true); } - - info!("m.room.aliases event was allowed"); - return Ok(true); } + // If type is m.room.member let power_levels_event = fetch_state(&EventType::RoomPowerLevels, ""); let sender_member_event = fetch_state(&EventType::RoomMember, sender.as_str()); @@ -245,15 +265,16 @@ pub fn auth_check( let target_user = <&UserId>::try_from(state_key).map_err(|e| Error::InvalidPdu(format!("{}", e)))?; - let join_authed_user = + let user_for_join_auth = content.join_authorised_via_users_server.as_ref().and_then(|u| u.deserialize().ok()); - let join_authed_user_membership = if let Some(auth_user) = &join_authed_user { - fetch_state(&EventType::RoomMember, auth_user.as_str()) - .and_then(|mem| from_json_str::(mem.content().get()).ok()) - .map(|mem| mem.membership) - } else { - None - }; + + let user_for_join_auth_membership = user_for_join_auth + .as_ref() + .and_then(|auth_user| fetch_state(&EventType::RoomMember, auth_user.as_str())) + .and_then(|mem| from_json_str::(mem.content().get()).ok()) + .map(|mem| mem.membership) + .unwrap_or(MembershipState::Leave); + if !valid_membership_change( room_version, target_user, @@ -264,8 +285,8 @@ pub fn auth_check( current_third_party_invite, power_levels_event.as_ref(), fetch_state(&EventType::RoomJoinRules, "").as_ref(), - join_authed_user.as_deref(), - join_authed_user_membership, + user_for_join_auth.as_deref(), + &user_for_join_auth_membership, room_create_event, )? { return Ok(false); @@ -296,6 +317,7 @@ pub fn auth_check( return Ok(false); } + // If type is m.room.third_party_invite let sender_power_level = if let Some(pl) = &power_levels_event { if let Ok(content) = from_json_str::(pl.content().get()) { if let Some(level) = content.users.get(sender) { @@ -337,6 +359,7 @@ pub fn auth_check( return Ok(false); } + // If type is m.room.power_levels if *incoming_event.event_type() == EventType::RoomPowerLevels { info!("starting m.room.power_levels check"); @@ -406,8 +429,8 @@ fn valid_membership_change( current_third_party_invite: Option, power_levels_event: Option, join_rules_event: Option, - authed_user_id: Option<&UserId>, - auth_user_membership: Option, + user_for_join_auth: Option<&UserId>, + user_for_join_auth_membership: &MembershipState, create_room: impl Event, ) -> Result { #[derive(Deserialize)] @@ -455,25 +478,17 @@ fn valid_membership_change( let target_user_membership_event_id = target_user_membership_event.as_ref().map(|e| e.event_id()); - // FIXME: `JoinRule::Restricted(_)` can contain conditions that allow a user to join if - // they are met. So far the spec talks about roomId based auth inheritance, the problem with - // this is that ruma-state-res can only request events from one room at a time :( - let restricted = matches!(join_rules, JoinRule::Restricted(_)); - - let allow_based_on_membership = - matches!(target_user_current_membership, MembershipState::Invite | MembershipState::Join) - || authed_user_id.is_none(); - - let restricted_join_rules_auth = if let Some(authed_user_id) = authed_user_id { - // Is the authorised user allowed to invite users into this rooom + let user_for_join_auth_is_valid = if let Some(user_for_join_auth) = user_for_join_auth { + // Is the authorised user allowed to invite users into this room let (auth_user_pl, invite_level) = if let Some(pl) = &power_levels_event { + // TODO Refactor all powerlevel parsing let invite = match from_json_str::(pl.content().get()) { Ok(power_levels) => power_levels.invite, _ => int!(50), }; if let Ok(content) = from_json_str::(pl.content().get()) { - let user_pl = if let Some(level) = content.users.get(authed_user_id) { + let user_pl = if let Some(level) = content.users.get(user_for_join_auth) { *level } else { content.users_default @@ -486,14 +501,16 @@ fn valid_membership_change( } else { (int!(0), int!(0)) }; - (auth_user_membership == Some(MembershipState::Join)) && (auth_user_pl >= invite_level) + (user_for_join_auth_membership == &MembershipState::Join) && (auth_user_pl >= invite_level) } else { - // If the `join_authorised_via_users_server` was empty we treat the target user as invited - true + // No auth user was given + false }; Ok(match target_membership { MembershipState::Join => { + // 1. If the only previous event is an m.room.create and the state_key is the creator, + // allow let mut prev_events = current_event.prev_events(); let prev_event_is_create_event = prev_events @@ -512,40 +529,40 @@ fn valid_membership_change( } if sender != target_user { + // If the sender does not match state_key, reject. warn!("Can't make other user join"); false } else if let MembershipState::Ban = target_user_current_membership { + // If the sender is banned, reject. warn!(?target_user_membership_event_id, "Banned user can't join"); false - } else { - let invite_allowed = (join_rules == JoinRule::Invite + } else if (join_rules == JoinRule::Invite || room_version.allow_knocking && join_rules == JoinRule::Knock) + // If the join_rule is invite then allow if membership state is invite or join && (target_user_current_membership == MembershipState::Join - || target_user_current_membership == MembershipState::Invite); - - if invite_allowed { - return Ok(true); + || target_user_current_membership == MembershipState::Invite) + { + true + } else if room_version.restricted_join_rules + && matches!(join_rules, JoinRule::Restricted(_)) + { + // If the join_rule is restricted + if matches!( + target_user_current_membership, + MembershipState::Invite | MembershipState::Join + ) { + // If membership state is join or invite, allow. + true + } else { + // If the join_authorised_via_users_server key in content is not a user with + // sufficient permission to invite other users, reject. + // Otherwise, allow. + user_for_join_auth_is_valid } - - if room_version.restricted_join_rules - && restricted - && allow_based_on_membership - && restricted_join_rules_auth - { - return Ok(true); - } - - if join_rules == JoinRule::Public { - return Ok(true); - } - - warn!( - join_rules_event_id = ?join_rules_event.as_ref().map(|e| e.event_id()), - ?target_user_membership_event_id, - "Can't join if join rules is not public and user is not invited / joined", - ); - - false + } else { + // If the join_rule is public, allow. + // Otherwise, reject. + join_rules == JoinRule::Public } } MembershipState::Invite => { @@ -965,7 +982,7 @@ mod tests { use crate::{ event_auth::valid_membership_change, test_utils::{ - alice, bob, charlie, ella, event_id, member_content_ban, member_content_join, room_id, + alice, charlie, ella, event_id, member_content_ban, member_content_join, room_id, to_pdu_event, StateEvent, INITIAL_EVENTS, INITIAL_EVENTS_CREATE_ROOM, }, Event, RoomVersion, StateMap, @@ -1009,7 +1026,7 @@ mod tests { fetch_state(EventType::RoomPowerLevels, "".to_owned()), fetch_state(EventType::RoomJoinRules, "".to_owned()), None, - None, + &MembershipState::Leave, fetch_state(EventType::RoomCreate, "".to_owned()).unwrap(), ) .unwrap()); @@ -1053,7 +1070,7 @@ mod tests { fetch_state(EventType::RoomPowerLevels, "".to_owned()), fetch_state(EventType::RoomJoinRules, "".to_owned()), None, - None, + &MembershipState::Leave, fetch_state(EventType::RoomCreate, "".to_owned()).unwrap(), ) .unwrap()); @@ -1097,7 +1114,7 @@ mod tests { fetch_state(EventType::RoomPowerLevels, "".to_owned()), fetch_state(EventType::RoomJoinRules, "".to_owned()), None, - None, + &MembershipState::Leave, fetch_state(EventType::RoomCreate, "".to_owned()).unwrap(), ) .unwrap()); @@ -1141,7 +1158,7 @@ mod tests { fetch_state(EventType::RoomPowerLevels, "".to_owned()), fetch_state(EventType::RoomJoinRules, "".to_owned()), None, - None, + &MembershipState::Leave, fetch_state(EventType::RoomCreate, "".to_owned()).unwrap(), ) .unwrap()); @@ -1167,20 +1184,8 @@ mod tests { &["IPOWER"], ); - let mut member = RoomMemberEventContent::new(MembershipState::Invite); + let mut member = RoomMemberEventContent::new(MembershipState::Join); member.join_authorized_via_users_server = Some(alice()); - events.insert( - event_id("new"), - to_pdu_event( - "new", - bob(), - EventType::RoomMember, - Some(ella().as_str()), - to_raw_json_value(&member).unwrap(), - &["CREATE", "IJR", "IPOWER"], - &["IMC"], - ), - ); let auth_events = events .values() @@ -1214,7 +1219,7 @@ mod tests { fetch_state(EventType::RoomPowerLevels, "".to_owned()), fetch_state(EventType::RoomJoinRules, "".to_owned()), Some(&alice()), - Some(MembershipState::Join), + &MembershipState::Join, fetch_state(EventType::RoomCreate, "".to_owned()).unwrap(), ) .unwrap()); @@ -1230,7 +1235,7 @@ mod tests { fetch_state(EventType::RoomPowerLevels, "".to_owned()), fetch_state(EventType::RoomJoinRules, "".to_owned()), Some(&ella()), - Some(MembershipState::Join), + &MembershipState::Leave, fetch_state(EventType::RoomCreate, "".to_owned()).unwrap(), ) .unwrap()); @@ -1283,7 +1288,7 @@ mod tests { fetch_state(EventType::RoomPowerLevels, "".to_owned()), fetch_state(EventType::RoomJoinRules, "".to_owned()), None, - None, + &MembershipState::Leave, fetch_state(EventType::RoomCreate, "".to_owned()).unwrap(), ) .unwrap());