fix: Verify policy server signatures on all events, not just timeline ones

style: Clarifications

style: Clippy
This commit is contained in:
timedout
2026-04-28 16:05:46 +01:00
parent 5f9cc83b18
commit fdc9aec534
2 changed files with 55 additions and 54 deletions
@@ -6,18 +6,18 @@
use std::{collections::BTreeMap, time::Duration}; use std::{collections::BTreeMap, time::Duration};
use conduwuit::{ use conduwuit::{
Err, Error, Event, PduEvent, Result, debug, debug_error, debug_info, debug_warn, error, debug, debug_error, debug_info, debug_warn, error, implement, info, state_res::EventTypeExt, trace, utils::to_canonical_object,
implement, info, state_res::EventTypeExt, trace, utils::to_canonical_object, warn, warn, Err, Error, Event, PduEvent, Result,
}; };
use http::StatusCode; use http::StatusCode;
use ruma::{ use ruma::{
CanonicalJsonObject, CanonicalJsonValue, KeyId, RoomId, ServerName, SigningKeyAlgorithm, api::error::ErrorKind, canonical_json::redact, events::{room::policy::RoomPolicyEventContent, StateEventType}, room_version_rules::{RedactionRules, RoomVersionRules}, serde::{base64::Standard, Base64}, signatures::{to_canonical_json_string_for_signing, verify_canonical_json_bytes},
api::error::ErrorKind, CanonicalJsonObject,
canonical_json::redact, CanonicalJsonValue,
events::{StateEventType, room::policy::RoomPolicyEventContent}, KeyId,
room_version_rules::{RedactionRules, RoomVersionRules}, RoomId,
serde::{Base64, base64::Standard}, ServerName,
signatures::{to_canonical_json_string_for_signing, verify_canonical_json_bytes}, SigningKeyAlgorithm,
}; };
use ruminuwuity::policy::policy_sign::unstable::Request as PolicySignRequest; use ruminuwuity::policy::policy_sign::unstable::Request as PolicySignRequest;
use serde_json::value::RawValue; use serde_json::value::RawValue;
@@ -72,15 +72,13 @@ pub(super) fn verify_policy_signature(
/// Asks a remote policy server if the event is allowed. /// Asks a remote policy server if the event is allowed.
/// ///
/// If the event is the `org.matrix.msc4284.policy` configuration state event, /// If the event is the `m.room.policy` configuration state event,
/// this check is skipped. Similarly, if there is no policy server configured in /// this check is skipped. Similarly, if there is no policy server configured in
/// the PDU's room, or the configured server is not present in the room, the /// the PDU's room, or the configured server is not present in the room, the
/// check is also skipped. /// check is also skipped.
/// ///
/// If the policy server marks the event as spam, Ok(false) is returned, /// If the policy server marks the event as spam, the relevant error is returned. Otherwise,
/// otherwise Ok(true) allows the event. If the policy server cannot be /// the incoming PDU JSON is mutated to include the new policy server signature.
/// contacted for whatever reason, Err(e) is returned, which generally is a
/// fail-open operation.
#[implement(super::Service)] #[implement(super::Service)]
#[tracing::instrument(skip(self, pdu, pdu_json, room_version_rules), level = "info")] #[tracing::instrument(skip(self, pdu, pdu_json, room_version_rules), level = "info")]
pub async fn policy_server_allows_event( pub async fn policy_server_allows_event(
@@ -152,6 +150,7 @@ pub async fn policy_server_allows_event(
); );
return Ok(()); return Ok(());
} }
// N.B. In a future room version, this will be a soft failure specifically.
debug_info!( debug_info!(
via = %ps.via, via = %ps.via,
"Event is incoming but does not have a valid policy server signature; asking policy \ "Event is incoming but does not have a valid policy server signature; asking policy \
@@ -183,6 +182,10 @@ pub async fn policy_server_allows_event(
self.fetch_policy_server_signature(pdu, pdu_json, &ps.via, outgoing, room_id, ps_key, 0) self.fetch_policy_server_signature(pdu, pdu_json, &ps.via, outgoing, room_id, ps_key, 0)
.await .await
} }
/// Handles an error returned by the policy server. If the error is one that should be returned to
/// the user, it is propagated, otherwise the request may be retried (for example, when
/// rate-limited).
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
#[implement(super::Service)] #[implement(super::Service)]
async fn handle_policy_server_error( async fn handle_policy_server_error(
@@ -290,7 +293,8 @@ async fn handle_policy_server_error(
} }
/// Asks a remote policy server for a signature on this event. /// Asks a remote policy server for a signature on this event.
/// If the policy server signs this event, the original data is mutated. /// If the policy server signs this event, the original data is mutated. Otherwise, the error is
/// handled and potentially returned.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
#[implement(super::Service)] #[implement(super::Service)]
#[tracing::instrument(skip_all, fields(event_id=%pdu.event_id(), via=%via), level = "info")] #[tracing::instrument(skip_all, fields(event_id=%pdu.event_id(), via=%via), level = "info")]
@@ -358,7 +362,7 @@ pub async fn fetch_policy_server_signature(
response.signatures response.signatures
); );
return Err!(BadServerResponse( return Err!(BadServerResponse(
"Policy server did not include expected server name in signatures" "Policy server did not sign the event"
)); ));
} }
// Unwraps are safe here because we checked both in the above if statement // Unwraps are safe here because we checked both in the above if statement
@@ -1,18 +1,17 @@
use std::{borrow::Borrow, collections::BTreeMap, sync::Arc, time::Instant}; use std::{borrow::Borrow, collections::BTreeMap, sync::Arc, time::Instant};
use conduwuit::{ use conduwuit::{
Err, Result, debug, debug_info, debug_warn, err, implement, is_equal_to, debug, debug_info, debug_warn, err, implement, is_equal_to, matrix::{state_res, Event, EventTypeExt, PduEvent, StateKey}, trace,
matrix::{Event, EventTypeExt, PduEvent, StateKey, state_res},
trace,
utils::{ utils::{
IterStream, IterStream,
stream::{BroadbandExt, ReadyExt}, stream::{BroadbandExt, ReadyExt},
}, },
warn, Err,
Result,
}; };
use futures::{FutureExt, StreamExt, future::ready}; use futures::{future::ready, FutureExt, StreamExt};
use ruma::{ use ruma::{
CanonicalJsonValue, RoomId, ServerName, api::error::ErrorKind, events::StateEventType, api::error::ErrorKind, events::StateEventType, CanonicalJsonValue, RoomId, ServerName,
}; };
use tokio::join; use tokio::join;
@@ -249,36 +248,34 @@ where
if !soft_fail { if !soft_fail {
// Don't call the below checks on events that have already soft-failed, there's // Don't call the below checks on events that have already soft-failed, there's
// no reason to re-calculate that. // no reason to re-calculate that.
// 14-pre. If the event is not a state event, ask the policy server about it // 14-pre. ask the policy server to sign the event, if possible
if incoming_pdu.state_key.is_none() { debug!(event_id = %incoming_pdu.event_id, "Checking policy server for event");
debug!(event_id = %incoming_pdu.event_id, "Checking policy server for event"); if let Err(e) = self
if let Err(e) = self .policy_server_allows_event(
.policy_server_allows_event( &incoming_pdu,
&incoming_pdu, &mut incoming_pdu.to_canonical_object(),
&mut incoming_pdu.to_canonical_object(), room_id,
room_id, &room_version_rules,
&room_version_rules, true,
true, )
) .await
.await {
{ if matches!(e.kind(), ErrorKind::Forbidden) {
if matches!(e.kind(), ErrorKind::Forbidden) { info!(
warn!(
event_id = %incoming_pdu.event_id,
error = %e,
"Event has been marked as spam by policy server: {}",
e.message(),
);
soft_fail = true;
} else {
return Err(e);
}
} else {
debug!(
event_id = %incoming_pdu.event_id, event_id = %incoming_pdu.event_id,
"Event has passed policy server check." error = %e,
"Event has been marked as spam by policy server: {}",
e.message(),
); );
soft_fail = true;
} else {
return Err(e);
} }
} else {
debug!(
event_id = %incoming_pdu.event_id,
"Event has passed policy server check."
);
} }
// Additionally, if this is a redaction for a soft-failed event, we soft-fail it // Additionally, if this is a redaction for a soft-failed event, we soft-fail it
@@ -298,9 +295,7 @@ where
.is_event_soft_failed(&redact_id) .is_event_soft_failed(&redact_id)
.await .await
{ {
// TODO: This should avoid pushing the event to the timeline instead of using info!(
// soft-fails as a hack
warn!(
redact_id = %redact_id, redact_id = %redact_id,
"Redaction is for a soft-failed event" "Redaction is for a soft-failed event"
); );
@@ -367,9 +362,11 @@ where
self.services self.services
.pdu_metadata .pdu_metadata
.mark_event_soft_failed(incoming_pdu.event_id()); .mark_event_soft_failed(incoming_pdu.event_id());
debug_warn!(
info!(
elapsed = ?timer.elapsed(), elapsed = ?timer.elapsed(),
"Event has been soft-failed", event_id = %incoming_pdu.event_id,
"Event was soft failed"
); );
} else { } else {
debug_info!( debug_info!(