From 72f0eb949309fac1a01668773e7addba82d8fd7a Mon Sep 17 00:00:00 2001 From: timedout Date: Sun, 2 Nov 2025 04:25:35 +0000 Subject: [PATCH] feat: Fetch policy server signatures --- Cargo.lock | 22 +-- Cargo.toml | 2 +- .../rooms/event_handler/policy_server.rs | 185 ++++++++++++++++-- .../event_handler/upgrade_outlier_pdu.rs | 7 +- src/service/rooms/timeline/create.rs | 2 +- 5 files changed, 184 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 81cb6e58..4d903bb6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4063,7 +4063,7 @@ checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3" [[package]] name = "ruma" version = "0.10.1" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=50b2a91b2ab8f9830eea80b9911e11234e0eac66#50b2a91b2ab8f9830eea80b9911e11234e0eac66" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=c091171279f76d34e530f7b7d7008d12e7429b1a#c091171279f76d34e530f7b7d7008d12e7429b1a" dependencies = [ "assign", "js_int", @@ -4083,7 +4083,7 @@ dependencies = [ [[package]] name = "ruma-appservice-api" version = "0.10.0" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=50b2a91b2ab8f9830eea80b9911e11234e0eac66#50b2a91b2ab8f9830eea80b9911e11234e0eac66" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=c091171279f76d34e530f7b7d7008d12e7429b1a#c091171279f76d34e530f7b7d7008d12e7429b1a" dependencies = [ "js_int", "ruma-common", @@ -4095,7 +4095,7 @@ dependencies = [ [[package]] name = "ruma-client-api" version = "0.18.0" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=50b2a91b2ab8f9830eea80b9911e11234e0eac66#50b2a91b2ab8f9830eea80b9911e11234e0eac66" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=c091171279f76d34e530f7b7d7008d12e7429b1a#c091171279f76d34e530f7b7d7008d12e7429b1a" dependencies = [ "as_variant", "assign", @@ -4118,7 +4118,7 @@ dependencies = [ [[package]] name = "ruma-common" version = "0.13.0" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=50b2a91b2ab8f9830eea80b9911e11234e0eac66#50b2a91b2ab8f9830eea80b9911e11234e0eac66" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=c091171279f76d34e530f7b7d7008d12e7429b1a#c091171279f76d34e530f7b7d7008d12e7429b1a" dependencies = [ "as_variant", "base64 0.22.1", @@ -4150,7 +4150,7 @@ dependencies = [ [[package]] name = "ruma-events" version = "0.28.1" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=50b2a91b2ab8f9830eea80b9911e11234e0eac66#50b2a91b2ab8f9830eea80b9911e11234e0eac66" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=c091171279f76d34e530f7b7d7008d12e7429b1a#c091171279f76d34e530f7b7d7008d12e7429b1a" dependencies = [ "as_variant", "indexmap", @@ -4175,7 +4175,7 @@ dependencies = [ [[package]] name = "ruma-federation-api" version = "0.9.0" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=50b2a91b2ab8f9830eea80b9911e11234e0eac66#50b2a91b2ab8f9830eea80b9911e11234e0eac66" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=c091171279f76d34e530f7b7d7008d12e7429b1a#c091171279f76d34e530f7b7d7008d12e7429b1a" dependencies = [ "bytes", "headers", @@ -4197,7 +4197,7 @@ dependencies = [ [[package]] name = "ruma-identifiers-validation" version = "0.9.5" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=50b2a91b2ab8f9830eea80b9911e11234e0eac66#50b2a91b2ab8f9830eea80b9911e11234e0eac66" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=c091171279f76d34e530f7b7d7008d12e7429b1a#c091171279f76d34e530f7b7d7008d12e7429b1a" dependencies = [ "js_int", "thiserror 2.0.17", @@ -4206,7 +4206,7 @@ dependencies = [ [[package]] name = "ruma-identity-service-api" version = "0.9.0" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=50b2a91b2ab8f9830eea80b9911e11234e0eac66#50b2a91b2ab8f9830eea80b9911e11234e0eac66" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=c091171279f76d34e530f7b7d7008d12e7429b1a#c091171279f76d34e530f7b7d7008d12e7429b1a" dependencies = [ "js_int", "ruma-common", @@ -4216,7 +4216,7 @@ dependencies = [ [[package]] name = "ruma-macros" version = "0.13.0" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=50b2a91b2ab8f9830eea80b9911e11234e0eac66#50b2a91b2ab8f9830eea80b9911e11234e0eac66" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=c091171279f76d34e530f7b7d7008d12e7429b1a#c091171279f76d34e530f7b7d7008d12e7429b1a" dependencies = [ "cfg-if", "proc-macro-crate", @@ -4231,7 +4231,7 @@ dependencies = [ [[package]] name = "ruma-push-gateway-api" version = "0.9.0" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=50b2a91b2ab8f9830eea80b9911e11234e0eac66#50b2a91b2ab8f9830eea80b9911e11234e0eac66" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=c091171279f76d34e530f7b7d7008d12e7429b1a#c091171279f76d34e530f7b7d7008d12e7429b1a" dependencies = [ "js_int", "ruma-common", @@ -4243,7 +4243,7 @@ dependencies = [ [[package]] name = "ruma-signatures" version = "0.15.0" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=50b2a91b2ab8f9830eea80b9911e11234e0eac66#50b2a91b2ab8f9830eea80b9911e11234e0eac66" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=c091171279f76d34e530f7b7d7008d12e7429b1a#c091171279f76d34e530f7b7d7008d12e7429b1a" dependencies = [ "base64 0.22.1", "ed25519-dalek", diff --git a/Cargo.toml b/Cargo.toml index 14e6d233..a657e198 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -351,7 +351,7 @@ version = "0.1.2" # Used for matrix spec type definitions and helpers [workspace.dependencies.ruma] git = "https://forgejo.ellis.link/continuwuation/ruwuma" -rev = "50b2a91b2ab8f9830eea80b9911e11234e0eac66" +rev = "c091171279f76d34e530f7b7d7008d12e7429b1a" features = [ "compat", "rand", diff --git a/src/service/rooms/event_handler/policy_server.rs b/src/service/rooms/event_handler/policy_server.rs index d577f92a..087ec95a 100644 --- a/src/service/rooms/event_handler/policy_server.rs +++ b/src/service/rooms/event_handler/policy_server.rs @@ -3,14 +3,21 @@ //! This module implements a check against a room-specific policy server, as //! described in the relevant Matrix spec proposal (see: https://github.com/matrix-org/matrix-spec-proposals/pull/4284). -use std::time::Duration; +use std::{collections::BTreeMap, time::Duration}; -use conduwuit::{Err, Event, PduEvent, Result, debug, debug_info, implement, trace, warn}; +use conduwuit::{ + Err, Event, PduEvent, Result, debug, debug_error, debug_info, debug_warn, implement, trace, + warn, +}; use ruma::{ - CanonicalJsonObject, RoomId, ServerName, - api::federation::room::policy::v1::Request as PolicyRequest, + CanonicalJsonObject, CanonicalJsonValue, KeyId, RoomId, ServerName, + api::federation::room::{ + policy_check::unstable::Request as PolicyCheckRequest, + policy_sign::unstable::Request as PolicySignRequest, + }, events::{StateEventType, room::policy::RoomPolicyEventContent}, }; +use serde_json::value::RawValue; /// Asks a remote policy server if the event is allowed. /// @@ -24,25 +31,18 @@ use ruma::{ /// contacted for whatever reason, Err(e) is returned, which generally is a /// fail-open operation. #[implement(super::Service)] -#[tracing::instrument(skip_all, level = "debug")] +#[tracing::instrument(skip(self, pdu, pdu_json))] pub async fn ask_policy_server( &self, pdu: &PduEvent, - pdu_json: &CanonicalJsonObject, + pdu_json: &mut CanonicalJsonObject, room_id: &RoomId, + incoming: bool, ) -> Result { if !self.services.server.config.enable_msc4284_policy_servers { + trace!("policy server checking is disabled"); return Ok(true); // don't ever contact policy servers } - if self.services.server.config.policy_server_check_own_events - && pdu.origin.is_some() - && self - .services - .server - .is_ours(pdu.origin.as_ref().unwrap().as_str()) - { - return Ok(true); // don't contact policy servers for locally generated events - } if *pdu.event_type() == StateEventType::RoomPolicy.into() { debug!( @@ -52,16 +52,29 @@ pub async fn ask_policy_server( ); return Ok(true); } + let Ok(policyserver) = self .services .state_accessor .room_state_get_content(room_id, &StateEventType::RoomPolicy, "") .await + .inspect_err(|e| debug_error!("failed to load room policy server state event: {e}")) .map(|c: RoomPolicyEventContent| c) else { + debug!("room has no policy server configured"); return Ok(true); }; + if self.services.server.config.policy_server_check_own_events + && !incoming + && policyserver.public_key.is_none() + { + // don't contact policy servers for locally generated events, but only when the + // policy server does not require signatures + trace!("won't contact policy server for locally generated event"); + return Ok(true); + } + let via = match policyserver.via { | Some(ref via) => ServerName::parse(via)?, | None => { @@ -75,7 +88,6 @@ pub async fn ask_policy_server( } if !self.services.state_cache.server_in_room(via, room_id).await { debug!( - room_id = %room_id, via = %via, "Policy server is not in the room, skipping spam check" ); @@ -86,17 +98,33 @@ pub async fn ask_policy_server( .sending .convert_to_outgoing_federation_event(pdu_json.clone()) .await; + if policyserver.public_key.is_some() { + if !incoming { + debug_info!( + via = %via, + outgoing = ?pdu_json, + "Getting policy server signature on event" + ); + return self + .fetch_policy_server_signature(pdu, pdu_json, via, outgoing, room_id) + .await; + } + debug!("Event is not local, performing legacy spam check"); + // If we got this far, that means that there was likely no policy server + // signature on the given event. Fall through to asking the PS if it's + // spam. + // TODO: this should probably be marking it as failed, but for now fall + // thru + } debug_info!( - room_id = %room_id, via = %via, - outgoing = ?pdu_json, - "Checking event for spam with policy server" + "Checking event for spam with policy server via legacy check" ); let response = tokio::time::timeout( Duration::from_secs(self.services.server.config.policy_server_request_timeout), self.services .sending - .send_federation_request(via, PolicyRequest { + .send_federation_request(via, PolicyCheckRequest { event_id: pdu.event_id().to_owned(), pdu: Some(outgoing), }), @@ -142,3 +170,120 @@ pub async fn ask_policy_server( Ok(true) } + +/// Asks a remote policy server for a signature on this event. +/// If the policy server signs this event, the original data is mutated. +#[implement(super::Service)] +#[tracing::instrument(skip_all)] +pub async fn fetch_policy_server_signature( + &self, + pdu: &PduEvent, + pdu_json: &mut CanonicalJsonObject, + via: &ServerName, + outgoing: Box, + room_id: &RoomId, +) -> Result { + debug!("Requesting policy server signature"); + let response = tokio::time::timeout( + Duration::from_secs(self.services.server.config.policy_server_request_timeout), + self.services + .sending + .send_federation_request(via, PolicySignRequest { pdu: outgoing }), + ) + .await; + + let response = match response { + | Ok(Ok(response)) => { + debug!("Response from policy server: {:?}", response); + response + }, + | Ok(Err(e)) => { + warn!( + via = %via, + event_id = %pdu.event_id(), + room_id = %room_id, + "Failed to contact policy server: {e}" + ); + // Network or policy server errors are treated as non-fatal: event is allowed by + // default. + return Err(e); + }, + | Err(elapsed) => { + warn!( + %via, + event_id = %pdu.event_id(), + %room_id, + %elapsed, + "Policy server request timed out after 10 seconds" + ); + return Err!("Request to policy server timed out"); + }, + }; + if response.signatures.is_none() { + debug!("Policy server refused to sign event"); + return Ok(false); + } + let sigs: ruma::Signatures = + response.signatures.unwrap(); + if !sigs.contains_key(via) { + debug_warn!( + "Policy server returned signatures, but did not include the expected server name \ + '{}': {:?}", + via, + sigs + ); + return Ok(false); + } + let keypairs = sigs.get(via).unwrap(); + let wanted_key_id = KeyId::parse("ed25519:policy_server")?; + if !keypairs.contains_key(wanted_key_id) { + debug_warn!( + "Policy server returned signature, but did not use the key ID \ + 'ed25519:policy_server'." + ); + return Ok(false); + } + let signatures_entry = pdu_json + .entry("signatures".to_owned()) + .or_insert_with(|| CanonicalJsonValue::Object(BTreeMap::default())); + + if let CanonicalJsonValue::Object(signatures_map) = signatures_entry { + let sig_value = keypairs.get(wanted_key_id).unwrap().to_owned(); + + match signatures_map.get_mut(via.as_str()) { + | Some(CanonicalJsonValue::Object(inner_map)) => { + trace!("inserting PS signature: {}", sig_value); + inner_map.insert( + "ed25519:policy_server".to_owned(), + CanonicalJsonValue::String(sig_value), + ); + }, + | Some(_) => { + debug_warn!( + "Existing `signatures[{}]` field is not an object; cannot insert policy \ + signature", + via + ); + return Ok(false); + }, + | None => { + let mut inner = BTreeMap::new(); + inner.insert( + "ed25519:policy_server".to_owned(), + CanonicalJsonValue::String(sig_value.clone()), + ); + trace!( + "created new signatures object for {via} with the signature {}", + sig_value + ); + signatures_map.insert(via.as_str().to_owned(), CanonicalJsonValue::Object(inner)); + }, + } + } else { + debug_warn!( + "Existing `signatures` field is not an object; cannot insert policy signature" + ); + return Ok(false); + } + Ok(true) +} diff --git a/src/service/rooms/event_handler/upgrade_outlier_pdu.rs b/src/service/rooms/event_handler/upgrade_outlier_pdu.rs index 42f8354b..a60c2b66 100644 --- a/src/service/rooms/event_handler/upgrade_outlier_pdu.rs +++ b/src/service/rooms/event_handler/upgrade_outlier_pdu.rs @@ -256,7 +256,12 @@ where if incoming_pdu.state_key.is_none() { debug!(event_id = %incoming_pdu.event_id, "Checking policy server for event"); match self - .ask_policy_server(&incoming_pdu, &incoming_pdu.to_canonical_object(), room_id) + .ask_policy_server( + &incoming_pdu, + &mut incoming_pdu.to_canonical_object(), + room_id, + true, + ) .await { | Ok(false) => { diff --git a/src/service/rooms/timeline/create.rs b/src/service/rooms/timeline/create.rs index 793f0309..731ae13e 100644 --- a/src/service/rooms/timeline/create.rs +++ b/src/service/rooms/timeline/create.rs @@ -308,7 +308,7 @@ pub async fn create_hash_and_sign_event( match self .services .event_handler - .ask_policy_server(&pdu, &pdu_json, pdu.room_id().expect("has room ID")) + .ask_policy_server(&pdu, &mut pdu_json, pdu.room_id().expect("has room ID"), false) .await { | Ok(true) => {},