Skip to main content

tuwunel_admin/query/feds/
event.rs

1use std::{
2	cmp::Ordering,
3	collections::BTreeMap,
4	fmt::{Result as FmtResult, Write as _},
5	num::NonZeroUsize,
6	time::{Duration, Instant},
7};
8
9use futures::{StreamExt, stream::iter as stream_iter};
10use ruma::{
11	CanonicalJsonObject, CanonicalJsonValue, OwnedEventId, OwnedRoomOrAliasId, OwnedServerName,
12	OwnedServerSigningKeyId, RoomVersionId,
13	api::federation::event::get_event::v1::{Request, Response},
14	canonical_json::{redact, redact_in_place},
15	serde::{Base64, base64::Standard},
16	signatures::{
17		PublicKeyMap, PublicKeySet, Verified, content_hash,
18		required_server_signatures_to_verify_event, verify_json as verify_signed_json,
19	},
20};
21use tuwunel_core::{
22	Err, Error, Result, err,
23	matrix::{event::gen_event_id, room_version::rules as room_version_rules},
24	utils::{stream::BroadbandExt, time::Elapsed},
25};
26use tuwunel_service::federation::feds::{Fault, Outcome};
27
28use super::{SweepArgs, count_results, fault_message, markdown_cell, prepare, render_totals};
29use crate::{Context, admin_command};
30
31pub(super) const WIDTH_DEFAULT: NonZeroUsize = NonZeroUsize::new(192).expect("192 is nonzero");
32
33type SigningKeys = BTreeMap<OwnedServerName, Vec<OwnedServerSigningKeyId>>;
34
35#[derive(Clone, Copy, Debug, Eq, PartialEq)]
36enum HashStatus {
37	Valid,
38	Redacted,
39}
40
41#[derive(Clone, Copy, Debug, Eq, PartialEq)]
42struct Verification {
43	hash: Option<HashStatus>,
44	signature: bool,
45}
46
47#[admin_command]
48pub(super) async fn feds_event(
49	&self,
50	event_id: OwnedEventId,
51	room: Option<OwnedRoomOrAliasId>,
52	verify_hash: bool,
53	verify_signature: bool,
54	sweep: SweepArgs,
55) -> Result {
56	let validation_width = sweep.width.unwrap_or(WIDTH_DEFAULT).get();
57
58	let room = match room {
59		| Some(room) => room,
60		| None => self
61			.services
62			.timeline
63			.get_pdu(&event_id)
64			.await
65			.map_err(|error| {
66				err!(
67					"Could not infer the room for {event_id}; supply the room explicitly: \
68					 {error}"
69				)
70			})?
71			.room_id
72			.into(),
73	};
74
75	let prepared = prepare(self, &room, sweep, WIDTH_DEFAULT).await?;
76
77	let room_version = self
78		.services
79		.state
80		.get_room_version(&prepared.room_id)
81		.await?;
82
83	let started = Instant::now();
84	let outcomes = self
85		.services
86		.federation
87		.for_room(&prepared.room_id, |_| Request { event_id: event_id.clone() }, prepared.opts)
88		.collect::<Vec<_>>()
89		.await;
90
91	let total = started.elapsed();
92
93	let event_id = &event_id;
94	let room_version = &room_version;
95	let verified = stream_iter(outcomes)
96		.broadn_then(validation_width, async |outcome| {
97			let result = match outcome.result {
98				| Ok(response) => validate_response(
99					self,
100					response,
101					event_id,
102					room_version,
103					verify_hash,
104					verify_signature,
105				)
106				.await
107				.map_err(Fault::Error),
108				| Err(fault) => Err(fault),
109			};
110
111			Outcome {
112				origin: outcome.origin,
113				elapsed: outcome.elapsed,
114				result,
115			}
116		})
117		.collect::<Vec<_>>()
118		.await;
119
120	let output = render(verified, total);
121
122	self.write_str(&output).await
123}
124
125async fn validate_response(
126	context: &Context<'_>,
127	response: Response,
128	event_id: &OwnedEventId,
129	room_version: &RoomVersionId,
130	verify_hash: bool,
131	verify_signature: bool,
132) -> Result<Verification> {
133	let event: CanonicalJsonObject = serde_json::from_str(response.pdu.get())
134		.map_err(|error| err!(BadServerResponse("Invalid event JSON: {error}")))?;
135
136	let received_event_id = gen_event_id(&event, room_version)?;
137
138	if received_event_id != *event_id {
139		return Err!(BadServerResponse("Requested {event_id}, but received {received_event_id}"));
140	}
141
142	match (verify_hash, verify_signature) {
143		| (true, true) => verify_hash_and_signatures(context, &event, room_version).await,
144		| (true, false) => Ok(Verification {
145			hash: Some(verify_content_hash(&event, room_version)?),
146			signature: false,
147		}),
148		| (false, true) => {
149			verify_signatures(context, &event, room_version).await?;
150
151			Ok(Verification { hash: None, signature: true })
152		},
153		| (false, false) => Ok(Verification { hash: None, signature: false }),
154	}
155}
156
157async fn verify_hash_and_signatures(
158	context: &Context<'_>,
159	event: &CanonicalJsonObject,
160	room_version: &RoomVersionId,
161) -> Result<Verification> {
162	let hash = match context
163		.services
164		.server_keys
165		.verify_event(event, Some(room_version))
166		.await?
167	{
168		| Verified::All => HashStatus::Valid,
169		| Verified::Signatures => verify_content_hash(event, room_version)?,
170	};
171
172	Ok(Verification { hash: Some(hash), signature: true })
173}
174
175fn verify_content_hash(
176	event: &CanonicalJsonObject,
177	room_version: &RoomVersionId,
178) -> Result<HashStatus> {
179	let expected = event
180		.get("hashes")
181		.and_then(CanonicalJsonValue::as_object)
182		.and_then(|hashes| hashes.get("sha256"))
183		.and_then(CanonicalJsonValue::as_str)
184		.ok_or_else(|| err!(BadServerResponse("Event is missing hashes.sha256")))?;
185
186	let expected = Base64::<Standard, [u8; 32]>::parse(expected)
187		.map_err(|error| err!(BadServerResponse("Invalid hashes.sha256: {error}")))?;
188
189	let calculated = content_hash(event).map_err(|error| {
190		err!(BadServerResponse("Could not calculate the event content hash: {error}"))
191	})?;
192
193	if expected.as_bytes() == calculated.as_bytes() {
194		return Ok(HashStatus::Valid);
195	}
196
197	if is_redacted(event, room_version)? {
198		return Ok(HashStatus::Redacted);
199	}
200
201	Err!(BadServerResponse("Event content hash does not match the received content"))
202}
203
204fn is_redacted(event: &CanonicalJsonObject, room_version: &RoomVersionId) -> Result<bool> {
205	let rules = room_version_rules(room_version)?;
206	let mut redacted = event.clone();
207
208	redacted.remove("unsigned");
209
210	redact_in_place(&mut redacted, &rules.redaction, None).map_err(|error| {
211		err!(BadServerResponse("Could not classify event redaction: {error}"))
212	})?;
213
214	Ok(event
215		.iter()
216		.filter(|(key, _)| key.as_str() != "unsigned")
217		.eq(redacted.iter()))
218}
219
220#[tracing::instrument(
221	level = "debug",
222	skip_all,
223	fields(
224		room_version = ?room_version,
225	)
226)]
227async fn verify_signatures(
228	context: &Context<'_>,
229	event: &CanonicalJsonObject,
230	room_version: &RoomVersionId,
231) -> Result {
232	let (signed_event, keys) = signature_input(event, room_version)?;
233	let (public_keys, key_error) = get_public_keys(context, &keys).await;
234
235	verify_signature(&public_keys, &signed_event, key_error.as_ref())
236}
237
238async fn get_public_keys(
239	context: &Context<'_>,
240	keys: &SigningKeys,
241) -> (PublicKeyMap, Option<Error>) {
242	let mut public_keys = PublicKeyMap::new();
243	let mut first_error = None;
244
245	for (server, key_ids) in keys {
246		let mut server_keys = PublicKeySet::new();
247
248		for key_id in key_ids {
249			match context
250				.services
251				.server_keys
252				.get_verify_key(server, key_id)
253				.await
254			{
255				| Ok(verify_key) => {
256					server_keys.insert(key_id.as_str().into(), verify_key.key);
257				},
258				| Err(error) if first_error.is_none() => first_error = Some(error),
259				| Err(_) => (),
260			}
261		}
262
263		public_keys.insert(server.as_str().into(), server_keys);
264	}
265
266	(public_keys, first_error)
267}
268
269fn verify_signature(
270	public_keys: &PublicKeyMap,
271	event: &CanonicalJsonObject,
272	key_error: Option<&Error>,
273) -> Result {
274	verify_signed_json(public_keys, event).map_err(|error| {
275		key_error.map_or_else(
276			|| err!(BadServerResponse("Signature verification failed: {error}")),
277			|key_error| {
278				err!(BadServerResponse(
279					"Signature verification failed: {error}; signing-key acquisition also \
280					 failed: {key_error}"
281				))
282			},
283		)
284	})
285}
286
287fn signature_input(
288	event: &CanonicalJsonObject,
289	room_version: &RoomVersionId,
290) -> Result<(CanonicalJsonObject, SigningKeys)> {
291	let rules = room_version_rules(room_version)?;
292	let required = required_server_signatures_to_verify_event(event, &rules.signatures).map_err(
293		|error| err!(BadServerResponse("Could not determine required signatures: {error}")),
294	)?;
295
296	let signatures = event
297		.get("signatures")
298		.and_then(CanonicalJsonValue::as_object)
299		.ok_or_else(|| err!(BadServerResponse("Event is missing signatures")))?;
300
301	let mut filtered = CanonicalJsonObject::new();
302	let mut keys = SigningKeys::new();
303
304	for server in required {
305		let signature_set = signatures
306			.get(server.as_str())
307			.and_then(CanonicalJsonValue::as_object)
308			.ok_or_else(|| {
309				err!(BadServerResponse("Event is missing a signature object from {server}"))
310			})?;
311
312		let key_ids = signature_set
313			.keys()
314			.filter_map(|key_id| key_id.as_str().try_into().ok())
315			.collect();
316
317		filtered.insert(server.as_str().into(), signature_set.clone().into());
318		keys.insert(server, key_ids);
319	}
320
321	let mut event = redact(event.clone(), &rules.redaction, None).map_err(|error| {
322		err!(BadServerResponse("Could not redact the event for verification: {error}"))
323	})?;
324
325	event.insert("signatures".into(), filtered.into());
326
327	Ok((event, keys))
328}
329
330fn render(mut outcomes: Vec<Outcome<Verification>>, total: Duration) -> String {
331	outcomes.sort_unstable_by(outcome_order);
332
333	let mut output = String::new();
334
335	render_into(&mut output, &outcomes, total).expect("writing to a String cannot fail");
336	output
337}
338
339fn outcome_order(left: &Outcome<Verification>, right: &Outcome<Verification>) -> Ordering {
340	match (
341		matches!(left.result, Err(Fault::NotAttempted)),
342		matches!(right.result, Err(Fault::NotAttempted)),
343	) {
344		| (true, false) => Ordering::Greater,
345		| (false, true) => Ordering::Less,
346		| (true, true) => left.origin.cmp(&right.origin),
347		| _ => left
348			.elapsed
349			.cmp(&right.elapsed)
350			.then_with(|| left.origin.cmp(&right.origin)),
351	}
352}
353
354fn render_into(
355	output: &mut String,
356	outcomes: &[Outcome<Verification>],
357	total: Duration,
358) -> FmtResult {
359	writeln!(output, "| rank | origin | elapsed | hash | signature | fault |")?;
360	writeln!(output, "| ---: | :--- | ---: | :--- | :--- | :--- |")?;
361
362	let mut rank = 0_usize;
363
364	for outcome in outcomes {
365		match &outcome.result {
366			| Err(fault @ Fault::NotAttempted) => {
367				let fault = fault_message(fault);
368				let fault = markdown_cell(&fault);
369
370				writeln!(output, "| | {} | | | | {fault} |", outcome.origin)?;
371			},
372			| Err(fault) => {
373				rank = rank.saturating_add(1);
374
375				let fault = fault_message(fault);
376				let fault = markdown_cell(&fault);
377
378				writeln!(
379					output,
380					"| {rank} | {} | {} | | | {fault} |",
381					outcome.origin,
382					Elapsed::from(outcome.elapsed),
383				)?;
384			},
385			| Ok(verification) => {
386				rank = rank.saturating_add(1);
387
388				let signature = if verification.signature { "ok" } else { "" };
389
390				writeln!(
391					output,
392					"| {rank} | {} | {} | {} | {signature} | |",
393					outcome.origin,
394					Elapsed::from(outcome.elapsed),
395					hash_cell(verification.hash),
396				)?;
397			},
398		}
399	}
400
401	render_totals(output, count_results(outcomes), total)
402}
403
404fn hash_cell(status: Option<HashStatus>) -> &'static str {
405	match status {
406		| Some(HashStatus::Valid) => "ok",
407		| Some(HashStatus::Redacted) => "redacted",
408		| None => "",
409	}
410}
411
412#[cfg(test)]
413mod tests {
414	use ruma::{ServerName, server_name};
415	use serde_json::json;
416
417	use super::*;
418
419	#[test]
420	fn content_hash_distinguishes_valid_redacted_and_invalid_events() {
421		let mut event = event();
422		let hash = content_hash(&event)
423			.expect("event content should hash")
424			.encode();
425
426		event
427			.get_mut("hashes")
428			.and_then(CanonicalJsonValue::as_object_mut)
429			.expect("test event should contain hashes")
430			.insert("sha256".into(), hash.into());
431
432		assert_eq!(
433			verify_content_hash(&event, &RoomVersionId::V11).expect("valid event should verify"),
434			HashStatus::Valid,
435		);
436
437		let rules =
438			room_version_rules(&RoomVersionId::V11).expect("room version should be supported");
439
440		let redacted =
441			redact(event.clone(), &rules.redaction, None).expect("test event should redact");
442
443		assert_eq!(
444			verify_content_hash(&redacted, &RoomVersionId::V11)
445				.expect("canonical redaction should be accepted"),
446			HashStatus::Redacted,
447		);
448
449		let mut malformed = redacted;
450
451		malformed
452			.get_mut("hashes")
453			.and_then(CanonicalJsonValue::as_object_mut)
454			.expect("test event should contain hashes")
455			.insert("sha256".into(), "not a hash".into());
456
457		assert!(
458			verify_content_hash(&malformed, &RoomVersionId::V11).is_err(),
459			"redaction must not excuse a malformed hash",
460		);
461
462		event.insert("content".into(), json!({ "body": "modified" }).try_into().unwrap());
463		assert!(
464			verify_content_hash(&event, &RoomVersionId::V11).is_err(),
465			"a mismatched unredacted event must fail",
466		);
467	}
468
469	#[test]
470	fn signature_input_selects_keys_and_bad_signature_is_an_error() {
471		let mut event = event();
472
473		event.remove("hashes");
474
475		let (event, keys) = signature_input(&event, &RoomVersionId::V11)
476			.expect("signature-only preparation should not require a hash");
477
478		let signatures = event
479			.get("signatures")
480			.and_then(CanonicalJsonValue::as_object)
481			.expect("prepared event should contain signatures");
482
483		assert!(!event.contains_key("hashes"));
484		assert!(signatures.contains_key("example.org"));
485		assert!(!signatures.contains_key("elsewhere.example"));
486
487		let key_ids = keys
488			.get(server_name!("example.org"))
489			.expect("origin keys should be requested");
490
491		assert_eq!(key_ids.len(), 1);
492		assert_eq!(key_ids[0].as_str(), "ed25519:1");
493		assert!(!keys.contains_key(server_name!("elsewhere.example")));
494
495		let error = verify_signature(&PublicKeyMap::new(), &event, None)
496			.expect_err("missing public keys should fail signature verification");
497
498		assert!(
499			error
500				.to_string()
501				.contains("Signature verification failed")
502		);
503	}
504
505	#[test]
506	fn render_ranks_attempted_outcomes_by_latency_and_leaves_undispatched_unranked() {
507		let outcomes = vec![
508			success(server_name!("slow.example"), 40, HashStatus::Redacted),
509			Outcome {
510				origin: server_name!("alpha-skipped.example").to_owned(),
511				elapsed: Duration::from_millis(100),
512				result: Err(Fault::NotAttempted),
513			},
514			Outcome {
515				origin: server_name!("skipped.example").to_owned(),
516				elapsed: Duration::ZERO,
517				result: Err(Fault::NotAttempted),
518			},
519			success(server_name!("fast.example"), 10, HashStatus::Valid),
520			verification(server_name!("no-hash.example"), 30, None, true),
521			verification(
522				server_name!("no-signature.example"),
523				35,
524				Some(HashStatus::Valid),
525				false,
526			),
527			verification(server_name!("unchecked.example"), 37, None, false),
528			Outcome {
529				origin: server_name!("timeout.example").to_owned(),
530				elapsed: Duration::from_nanos(12_559_999),
531				result: Err(Fault::Elapsed),
532			},
533		];
534
535		let output = render(outcomes, Duration::from_millis(15_499));
536		let fast = output
537			.find("| 1 | fast.example | 10ms | ok | ok | |")
538			.unwrap();
539
540		let timeout = output
541			.find("| 2 | timeout.example | 12.55ms | | | request deadline exceeded |")
542			.unwrap();
543
544		let no_hash = output
545			.find("| 3 | no-hash.example | 30ms |  | ok | |")
546			.unwrap();
547
548		let no_signature = output
549			.find("| 4 | no-signature.example | 35ms | ok |  | |")
550			.unwrap();
551
552		let unchecked = output
553			.find("| 5 | unchecked.example | 37ms |  |  | |")
554			.unwrap();
555
556		let slow = output
557			.find("| 6 | slow.example | 40ms | redacted | ok | |")
558			.unwrap();
559
560		let alpha_skipped = output
561			.find("| | alpha-skipped.example | | | | sweep budget exhausted before dispatch |")
562			.unwrap();
563
564		let skipped = output
565			.find("| | skipped.example | | | | sweep budget exhausted before dispatch |")
566			.unwrap();
567
568		assert!(
569			fast < timeout
570				&& timeout < no_hash
571				&& no_hash < no_signature
572				&& no_signature < unchecked
573				&& unchecked < slow
574				&& slow < alpha_skipped
575				&& alpha_skipped < skipped
576		);
577
578		assert!(output.ends_with("\n5 results in 15.49s.\n"));
579	}
580
581	fn event() -> CanonicalJsonObject {
582		serde_json::from_value(json!({
583			"auth_events": [],
584			"content": { "body": "original" },
585			"depth": 1,
586			"hashes": { "sha256": "" },
587			"origin_server_ts": 1,
588			"prev_events": [],
589			"sender": "@alice:example.org",
590			"signatures": {
591				"example.org": { "ed25519:1": "signature" },
592				"elsewhere.example": { "ed25519:1": "signature" }
593			},
594			"type": "m.room.message"
595		}))
596		.expect("test event should be canonical JSON")
597	}
598
599	fn success(origin: &ServerName, elapsed_ms: u64, hash: HashStatus) -> Outcome<Verification> {
600		verification(origin, elapsed_ms, Some(hash), true)
601	}
602
603	fn verification(
604		origin: &ServerName,
605		elapsed_ms: u64,
606		hash: Option<HashStatus>,
607		signature: bool,
608	) -> Outcome<Verification> {
609		Outcome {
610			origin: origin.to_owned(),
611			elapsed: Duration::from_millis(elapsed_ms),
612			result: Ok(Verification { hash, signature }),
613		}
614	}
615}