1use std::{
2 collections::BTreeMap,
3 time::{Duration, SystemTime, UNIX_EPOCH},
4};
5
6use http::StatusCode;
7use ruma::{
8 CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedServerName, RoomId, RoomVersionId,
9 ServerName, SigningKeyAlgorithm,
10 api::{
11 error::{ErrorKind, RetryAfter},
12 federation::policy::sign_event::v1 as sign_event,
13 },
14 events::{
15 StateEventType,
16 room::policy::{POLICY_SERVER_ED25519_SIGNING_KEY_ID, RoomPolicyEventContent},
17 },
18 serde::Base64,
19 signatures::{to_canonical_json_string_for_signing, verify_canonical_json_bytes},
20};
21use serde::{Deserialize, Serialize};
22use serde_json::value::to_raw_value;
23use tuwunel_core::{
24 Err, Result, at, debug, implement,
25 matrix::{Event, pdu::into_outgoing_federation, room_version::rules as room_version_rules},
26 trace,
27 utils::time::now_secs,
28 warn,
29};
30use tuwunel_database::{Cbor, Deserialized};
31
32#[cfg(test)]
33mod tests;
34
35const UNSTABLE_POLICY_TYPE: &str = "org.matrix.msc4284.policy";
40const POLICY_REFUSAL_TTL: Duration = Duration::from_hours(24);
41
42#[derive(Clone, Copy, Debug)]
44pub enum PolicyCheck {
45 NotApplicable,
49
50 Pass,
52
53 Missing,
56
57 Invalid,
60}
61
62#[derive(Debug)]
64enum FetchOutcome {
65 Signed(String),
67
68 FailOpen,
70
71 Refused {
77 status: StatusCode,
78 errcode: Option<ErrorKind>,
79 },
80
81 RateLimited {
85 until_secs: u64,
86 },
87}
88
89#[derive(Debug, Serialize, Deserialize)]
92enum PolicySigState {
93 Refused {
97 until_secs: u64,
98 },
99
100 BackoffUntil {
103 until_secs: u64,
104 },
105}
106
107#[derive(Deserialize)]
111struct UnstablePolicyContent {
112 via: OwnedServerName,
113
114 #[serde(default)]
115 public_keys: BTreeMap<SigningKeyAlgorithm, Base64>,
116
117 #[serde(default)]
118 public_key: Option<Base64>,
119}
120
121#[implement(UnstablePolicyContent)]
122fn into_stable(
123 Self { via, mut public_keys, public_key }: Self,
124) -> Option<RoomPolicyEventContent> {
125 if let Some(key) = public_key {
126 public_keys
127 .entry(SigningKeyAlgorithm::Ed25519)
128 .or_insert(key);
129 }
130
131 let ed25519 = public_keys.remove(&SigningKeyAlgorithm::Ed25519)?;
132
133 Some(RoomPolicyEventContent::new(via, ed25519))
134}
135
136#[implement(super::Service)]
140pub fn clear_policy_signature_state(&self, event_id: &EventId) {
141 self.db.eventid_policysigstate.remove(event_id);
142}
143
144#[implement(super::Service)]
145fn cache_policy_refused(&self, event_id: &EventId) {
146 let until_secs = now_secs().saturating_add(POLICY_REFUSAL_TTL.as_secs());
147
148 self.db
149 .eventid_policysigstate
150 .raw_put(event_id.as_str(), Cbor(&PolicySigState::Refused { until_secs }));
151}
152
153#[implement(super::Service)]
154fn cache_policy_backoff(&self, event_id: &EventId, until_secs: u64) {
155 self.db
156 .eventid_policysigstate
157 .raw_put(event_id.as_str(), Cbor(&PolicySigState::BackoffUntil { until_secs }));
158}
159
160#[implement(super::Service)]
161async fn cached_policy_state(&self, event_id: &EventId) -> Option<PolicySigState> {
162 let state = self
163 .db
164 .eventid_policysigstate
165 .get(event_id.as_str())
166 .await
167 .deserialized::<Cbor<_>>();
168
169 current_policy_state(state, now_secs())
170}
171
172fn current_policy_state(
173 state: Result<Cbor<PolicySigState>>,
174 current_secs: u64,
175) -> Option<PolicySigState> {
176 state
177 .map(at!(0))
178 .ok()
179 .filter(|state| match state {
180 | PolicySigState::Refused { until_secs }
181 | PolicySigState::BackoffUntil { until_secs } => *until_secs > current_secs,
182 })
183}
184
185#[implement(super::Service)]
193pub async fn lookup_policy_server(&self, room_id: &RoomId) -> Option<RoomPolicyEventContent> {
194 let read = async |event_type: &StateEventType| {
195 self.services
196 .state_accessor
197 .room_state_get_content::<UnstablePolicyContent>(room_id, event_type, "")
198 .await
199 .ok()
200 .and_then(UnstablePolicyContent::into_stable)
201 };
202
203 let content = match read(&StateEventType::RoomPolicy).await {
204 | Some(content) => content,
205 | None => read(&StateEventType::from(UNSTABLE_POLICY_TYPE.to_owned())).await?,
206 };
207
208 self.services
209 .state_cache
210 .server_in_room(&content.via, room_id)
211 .await
212 .then_some(content)
213}
214
215#[implement(super::Service)]
221#[tracing::instrument(name = "policy_sign", level = "debug", skip_all)]
222pub async fn sign_outgoing_pdu<E>(&self, pdu_json: &mut CanonicalJsonObject, pdu: &E) -> Result
223where
224 E: Event,
225{
226 if !self.services.server.config.enable_policy_servers {
227 return Ok(());
228 }
229
230 if is_policy_state_event(pdu) {
231 return Ok(());
232 }
233
234 let Ok(room_version) = self
235 .services
236 .state
237 .get_room_version(pdu.room_id())
238 .await
239 else {
240 return Ok(());
241 };
242
243 let Some(policy) = self.lookup_policy_server(pdu.room_id()).await else {
244 trace!(room_id = %pdu.room_id(), "no policy server configured");
245 return Ok(());
246 };
247
248 let event_id = pdu.event_id();
249 match self.cached_policy_state(event_id).await {
250 | Some(PolicySigState::Refused { .. }) =>
251 return Err!(Request(Forbidden("Event was rejected by the room's policy server."))),
252
253 | Some(PolicySigState::BackoffUntil { until_secs }) if until_secs > now_secs() => {
254 debug!(via = %policy.via, until_secs, "skipping outbound /sign during policy backoff");
255 return Ok(());
256 },
257 | _ => {},
258 }
259
260 match self
261 .fetch_policy_signature(&policy, pdu_json, &room_version)
262 .await
263 {
264 | FetchOutcome::Signed(signature) => {
265 insert_policy_signature(pdu_json, &policy.via, &signature);
266 debug!(via = %policy.via, event_id = %event_id, "folded policy server signature");
267 },
268 | FetchOutcome::Refused { status, errcode } => {
269 warn!(
270 via = %policy.via,
271 event_id = %event_id,
272 room_id = %pdu.room_id(),
273 status = status.as_u16(),
274 ?errcode,
275 "policy server refused to sign outbound PDU"
276 );
277
278 self.cache_policy_refused(event_id);
279 return Err!(Request(Forbidden("Event was rejected by the room's policy server.")));
280 },
281 | FetchOutcome::RateLimited { until_secs } => {
282 self.cache_policy_backoff(event_id, until_secs);
283 },
284 | FetchOutcome::FailOpen => {},
285 }
286
287 Ok(())
288}
289
290#[implement(super::Service)]
294#[tracing::instrument(
295 name = "policy_fetch",
296 level = "debug",
297 skip_all,
298 fields(via = %policy.via)
299)]
300async fn fetch_policy_signature(
301 &self,
302 policy: &RoomPolicyEventContent,
303 pdu_json: &CanonicalJsonObject,
304 room_version: &RoomVersionId,
305) -> FetchOutcome {
306 let outgoing = into_outgoing_federation(pdu_json.clone(), room_version);
307 let Ok(raw) = to_raw_value(&outgoing) else {
308 warn!(via = %policy.via, "failed to serialize PDU for policy /sign; failing open");
309 return FetchOutcome::FailOpen;
310 };
311
312 let timeout = Duration::from_secs(
313 self.services
314 .server
315 .config
316 .policy_server_request_timeout,
317 );
318
319 let response = match tokio::time::timeout(
320 timeout,
321 self.services
322 .federation
323 .execute(&policy.via, sign_event::Request::new(raw)),
324 )
325 .await
326 {
327 | Ok(Ok(response)) => response,
328 | Ok(Err(error)) => {
329 let status = error.status_code();
330 let errcode = error.kind();
331 let outcome = classify_fetch_error(status, &errcode, parse_rate_limit(&error));
332
333 if let FetchOutcome::RateLimited { until_secs } = &outcome {
334 warn!(
335 via = %policy.via,
336 status = status.as_u16(),
337 ?errcode,
338 until_secs,
339 "policy server /sign rate-limited"
340 );
341 }
342
343 if matches!(&outcome, FetchOutcome::FailOpen) {
344 let expected_not_found = status == StatusCode::NOT_FOUND
345 && (errcode == ErrorKind::NotFound || errcode == ErrorKind::Unrecognized);
346
347 if expected_not_found {
348 debug!(
349 via = %policy.via,
350 status = status.as_u16(),
351 ?errcode,
352 %error,
353 "policy server does not support /sign; failing open"
354 );
355 } else {
356 warn!(
357 via = %policy.via,
358 status = status.as_u16(),
359 ?errcode,
360 %error,
361 "policy server /sign failed; failing open"
362 );
363 }
364 }
365
366 return outcome;
367 },
368 | Err(elapsed) => {
369 warn!(via = %policy.via, %elapsed, "policy server /sign timed out; failing open");
370 return FetchOutcome::FailOpen;
371 },
372 };
373
374 response
376 .ed25519_signature(&policy.via)
377 .map(ToOwned::to_owned)
378 .map_or(
379 FetchOutcome::Refused { status: StatusCode::OK, errcode: None },
380 FetchOutcome::Signed,
381 )
382}
383
384fn classify_fetch_error(
385 status: StatusCode,
386 errcode: &ErrorKind,
387 rate_limit_until: Option<u64>,
388) -> FetchOutcome {
389 match rate_limit_until {
390 | Some(until_secs) => FetchOutcome::RateLimited { until_secs },
391 | None if status == StatusCode::BAD_REQUEST && errcode == &ErrorKind::Forbidden =>
392 FetchOutcome::Refused { status, errcode: Some(errcode.clone()) },
393 | None => FetchOutcome::FailOpen,
394 }
395}
396
397fn parse_rate_limit(error: &tuwunel_core::Error) -> Option<u64> {
398 let ErrorKind::LimitExceeded(data) = error.kind() else {
399 return None;
400 };
401
402 let until = match data.retry_after.as_ref()? {
403 | RetryAfter::Delay(d) => SystemTime::now().checked_add(*d)?,
404 | RetryAfter::DateTime(t) => *t,
405 };
406
407 until
408 .duration_since(UNIX_EPOCH)
409 .ok()
410 .map(|d| d.as_secs())
411}
412
413#[implement(super::Service)]
418#[tracing::instrument(name = "policy_verify_or_fetch", level = "debug", skip_all)]
419pub async fn verify_or_fetch_inbound_policy_signature<E>(
420 &self,
421 pdu_json: &mut CanonicalJsonObject,
422 pdu: &E,
423) -> PolicyCheck
424where
425 E: Event,
426{
427 match self
428 .check_inbound_policy_signature(pdu_json, pdu)
429 .await
430 {
431 | PolicyCheck::Missing | PolicyCheck::Invalid =>
432 self.fetch_inbound_policy_signature(pdu_json, pdu)
433 .await,
434 | other => other,
435 }
436}
437
438#[implement(super::Service)]
443#[tracing::instrument(name = "policy_fetch_inbound", level = "debug", skip_all)]
444async fn fetch_inbound_policy_signature<E>(
445 &self,
446 pdu_json: &mut CanonicalJsonObject,
447 pdu: &E,
448) -> PolicyCheck
449where
450 E: Event,
451{
452 let Some(policy) = self.lookup_policy_server(pdu.room_id()).await else {
453 return PolicyCheck::NotApplicable;
454 };
455
456 let Ok(room_version) = self
457 .services
458 .state
459 .get_room_version(pdu.room_id())
460 .await
461 else {
462 return PolicyCheck::NotApplicable;
463 };
464
465 let event_id = pdu.event_id();
466 match self.cached_policy_state(event_id).await {
467 | Some(PolicySigState::Refused { .. }) => return PolicyCheck::Invalid,
468 | Some(PolicySigState::BackoffUntil { until_secs }) if until_secs > now_secs() => {
469 debug!(
470 until_secs,
471 via = %policy.via,
472 "policy server in backoff; failing open"
473 );
474
475 return PolicyCheck::Pass;
476 },
477 | _ => {},
478 }
479
480 match self
481 .fetch_policy_signature(&policy, pdu_json, &room_version)
482 .await
483 {
484 | FetchOutcome::Signed(signature) => {
485 debug!(
486 via = %policy.via,
487 event_id = %event_id,
488 "folded inbound policy server signature"
489 );
490
491 insert_policy_signature(pdu_json, &policy.via, &signature);
492 PolicyCheck::Pass
493 },
494 | FetchOutcome::Refused { status, errcode } => {
495 warn!(
496 via = %policy.via,
497 event_id = %event_id,
498 room_id = %pdu.room_id(),
499 status = status.as_u16(),
500 ?errcode,
501 "policy server refused to sign inbound PDU; soft-failing"
502 );
503
504 self.cache_policy_refused(event_id);
505 PolicyCheck::Invalid
506 },
507 | FetchOutcome::RateLimited { until_secs } => {
508 self.cache_policy_backoff(event_id, until_secs);
509 PolicyCheck::Pass
510 },
511 | FetchOutcome::FailOpen => PolicyCheck::Pass,
512 }
513}
514
515#[implement(super::Service)]
521#[tracing::instrument(name = "policy_verify", level = "debug", skip_all)]
522pub async fn check_inbound_policy_signature<E>(
523 &self,
524 pdu_json: &CanonicalJsonObject,
525 pdu: &E,
526) -> PolicyCheck
527where
528 E: Event,
529{
530 if !self.services.server.config.enable_policy_servers {
531 return PolicyCheck::NotApplicable;
532 }
533
534 if is_policy_state_event(pdu) {
535 return PolicyCheck::NotApplicable;
536 }
537
538 let Some(policy) = self.lookup_policy_server(pdu.room_id()).await else {
539 return PolicyCheck::NotApplicable;
540 };
541
542 let Ok(room_version) = self
543 .services
544 .state
545 .get_room_version(pdu.room_id())
546 .await
547 else {
548 return PolicyCheck::NotApplicable;
549 };
550
551 let Some(public_key) = policy
553 .public_keys
554 .get(&SigningKeyAlgorithm::Ed25519)
555 else {
556 return PolicyCheck::NotApplicable;
557 };
558
559 check_policy_signature(pdu_json, &room_version, &policy.via, public_key)
560}
561
562fn check_policy_signature(
563 pdu_json: &CanonicalJsonObject,
564 room_version: &RoomVersionId,
565 via: &ServerName,
566 public_key: &Base64,
567) -> PolicyCheck {
568 let Ok(rules) = room_version_rules(room_version) else {
569 return PolicyCheck::NotApplicable;
570 };
571
572 let Some(signature_b64) = extract_policy_signature(pdu_json, via) else {
573 return PolicyCheck::Missing;
574 };
575
576 let Ok(signature) = Base64::<ruma::serde::base64::Standard>::parse(signature_b64) else {
577 return PolicyCheck::Invalid;
578 };
579
580 let Ok(redacted) = ruma::canonical_json::redact(pdu_json.clone(), &rules.redaction, None)
581 else {
582 return PolicyCheck::Invalid;
583 };
584
585 let Ok(canonical) = to_canonical_json_string_for_signing(&redacted) else {
586 return PolicyCheck::Invalid;
587 };
588
589 verify_canonical_json_bytes(
590 &SigningKeyAlgorithm::Ed25519,
591 public_key.as_bytes(),
592 signature.as_bytes(),
593 canonical.as_bytes(),
594 )
595 .map(|()| PolicyCheck::Pass)
596 .unwrap_or_else(|error| {
597 debug!(%via, %error, "policy server signature failed verification");
598 PolicyCheck::Invalid
599 })
600}
601
602fn is_policy_state_event<E: Event>(pdu: &E) -> bool {
603 if pdu.state_key() != Some("") {
604 return false;
605 }
606
607 let kind = pdu.kind().to_cow_str();
608
609 kind == "m.room.policy" || kind == UNSTABLE_POLICY_TYPE
610}
611
612fn extract_policy_signature<'a>(
613 pdu_json: &'a CanonicalJsonObject,
614 via: &ServerName,
615) -> Option<&'a str> {
616 let CanonicalJsonValue::Object(server_map) = pdu_json.get("signatures")? else {
617 return None;
618 };
619
620 let CanonicalJsonValue::Object(key_map) = server_map.get(via.as_str())? else {
621 return None;
622 };
623
624 let CanonicalJsonValue::String(signature) =
625 key_map.get(POLICY_SERVER_ED25519_SIGNING_KEY_ID)?
626 else {
627 return None;
628 };
629
630 Some(signature.as_str())
631}
632
633fn insert_policy_signature(
634 pdu_json: &mut CanonicalJsonObject,
635 via: &ServerName,
636 signature: &str,
637) {
638 let signatures = pdu_json
639 .entry("signatures".into())
640 .or_insert_with(|| CanonicalJsonValue::Object(BTreeMap::new()));
641
642 let CanonicalJsonValue::Object(server_map) = signatures else {
643 return;
644 };
645
646 let entry = server_map
647 .entry(via.as_str().into())
648 .or_insert_with(|| CanonicalJsonValue::Object(BTreeMap::new()));
649
650 if let CanonicalJsonValue::Object(key_map) = entry {
651 key_map.insert(
652 POLICY_SERVER_ED25519_SIGNING_KEY_ID.into(),
653 CanonicalJsonValue::String(signature.to_owned()),
654 );
655 }
656}