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}