Skip to main content

tuwunel_service/fetcher/
validate.rs

1//! Two-tier response validation for fetched federation data.
2//!
3//! A cheap conformance check can cover every operation, followed by an opt-in
4//! deep PDU pass only for event and auth-event fetches. When either cryptographic
5//! flag is enabled, the deep pass invokes event verification, but currently
6//! ignores its `Verified` status and therefore does not reject a content-hash
7//! mismatch by itself.
8
9use ruma::{CanonicalJsonObject, RoomVersionId};
10use serde::de::IgnoredAny;
11use tuwunel_core::{Err, Result, err, implement, matrix::event::gen_event_id};
12
13use super::{Op, Opts};
14
15/// Applies poison detection before a fetched response is accepted.
16///
17/// When `check_conforms` is enabled, malformed JSON rolls over to the next
18/// candidate; Backfill also rejects an empty batch while MissingEvents accepts
19/// one. Deep validation is limited to event and auth-event operations.
20#[implement(super::Service)]
21#[tracing::instrument(name = "validate", level = "trace", skip_all)]
22pub(super) async fn validate(&self, opts: &Opts, bytes: &[u8]) -> Result {
23	if opts.check_conforms {
24		match opts.op {
25			| Op::Backfill => serde_json::from_slice(bytes)
26				.map(|pdus: Vec<IgnoredAny>| !pdus.is_empty())
27				.map_err(|e| err!(BadServerResponse("malformed federation response: {e}")))
28				.and_then(|populated| {
29					populated
30						.then_some(())
31						.ok_or_else(|| err!(BadServerResponse("empty backfill response")))
32				}),
33			| _ => serde_json::from_slice(bytes)
34				.map(|_: IgnoredAny| ())
35				.map_err(|e| err!(BadServerResponse("malformed federation response: {e}"))),
36		}?;
37	}
38
39	let deep = opts.check_event_id || opts.check_hashes || opts.check_signature;
40	if matches!(opts.op, Op::Event | Op::AuthEvent) && deep {
41		self.verify_pdu(opts, bytes).await?;
42	}
43
44	Ok(())
45}
46
47/// Applies enabled event-ID and cryptographic checks to one PDU response.
48///
49/// Event verification errors are propagated, but its `Verified` classification
50/// is currently discarded, so `check_hashes` does not enforce a hash match.
51#[implement(super::Service)]
52#[tracing::instrument(level = "trace", skip_all)]
53async fn verify_pdu(&self, opts: &Opts, bytes: &[u8]) -> Result {
54	let value: CanonicalJsonObject = serde_json::from_slice(bytes)
55		.map_err(|e| err!(BadServerResponse("PDU is not a canonical JSON object: {e}")))?;
56
57	let v11 = RoomVersionId::V11;
58	let room_version = opts.room_version.as_ref().unwrap_or(&v11);
59
60	if opts.check_event_id
61		&& let Some(expected) = opts.event_id.as_ref()
62	{
63		let calculated = gen_event_id(&value, room_version)?;
64		if calculated != *expected {
65			return Err!(BadServerResponse("server returned the wrong event id"));
66		}
67	}
68
69	if opts.check_signature || opts.check_hashes {
70		self.services
71			.server_keys
72			.verify_event(&value, Some(room_version))
73			.await?;
74	}
75
76	Ok(())
77}