Skip to main content

tuwunel_api/router/auth/
server.rs

1use axum::RequestPartsExt;
2use axum_extra::{TypedHeader, headers::Authorization, typed_header::TypedHeaderRejectionReason};
3use http::uri::PathAndQuery;
4use ruma::{
5	CanonicalJsonName, CanonicalJsonObject, CanonicalJsonValue,
6	api::federation::authentication::XMatrix,
7};
8use tuwunel_core::{Err, Result, debug_error, err, warn};
9use tuwunel_service::{
10	Services,
11	server_keys::{PubKeyMap, PubKeys},
12};
13
14use super::{Auth, Request};
15
16pub(super) async fn auth_server(
17	services: &Services,
18	request: &mut Request,
19	body: Option<&CanonicalJsonValue>,
20) -> Result<Auth> {
21	type Member = (CanonicalJsonName, CanonicalJsonValue);
22	type Object = CanonicalJsonObject;
23	type Value = CanonicalJsonValue;
24
25	let x_matrix = parse_x_matrix(request).await?;
26	auth_server_checks(services, &x_matrix)?;
27
28	let destination = services.globals.server_name();
29	let origin = &x_matrix.origin;
30	let signature_uri = request
31		.parts
32		.uri
33		.path_and_query()
34		.map(PathAndQuery::as_str)
35		.unwrap_or("/");
36
37	let signature: [Member; 1] =
38		[(x_matrix.key.as_str().into(), Value::String(x_matrix.sig.to_string()))];
39
40	let signatures: [Member; 1] = [(origin.as_str().into(), Value::Object(signature.into()))];
41
42	let authorization: Object = if let Some(body) = body.cloned() {
43		let authorization: [Member; 6] = [
44			("content".into(), body),
45			("destination".into(), Value::String(destination.into())),
46			("method".into(), Value::String(request.parts.method.as_str().into())),
47			("origin".into(), Value::String(origin.as_str().into())),
48			("signatures".into(), Value::Object(signatures.into())),
49			("uri".into(), Value::String(signature_uri.into())),
50		];
51
52		authorization.into()
53	} else {
54		let authorization: [Member; 5] = [
55			("destination".into(), Value::String(destination.into())),
56			("method".into(), Value::String(request.parts.method.as_str().into())),
57			("origin".into(), Value::String(origin.as_str().into())),
58			("signatures".into(), Value::Object(signatures.into())),
59			("uri".into(), Value::String(signature_uri.into())),
60		];
61
62		authorization.into()
63	};
64
65	let key = services
66		.server_keys
67		.get_verify_key(origin, &x_matrix.key)
68		.await
69		.map_err(|e| {
70			err!(Request(Forbidden(debug_warn!("Failed to fetch signing keys: {e}"))))
71		})?;
72
73	let keys: PubKeys = [(x_matrix.key.as_str().into(), key.key)].into();
74	let keys: PubKeyMap = [(origin.as_str().into(), keys)].into();
75	if let Err(e) = ruma::signatures::verify_json(&keys, &authorization) {
76		debug_error!("Failed to verify federation request from {origin}: {e}");
77		if request.parts.uri.to_string().contains('@') {
78			warn!(
79				"Request uri contained '@' character. Make sure your reverse proxy gives \
80				 tuwunel the raw uri (apache: use nocanon)"
81			);
82		}
83
84		return Err!(Request(Forbidden("Failed to verify X-Matrix signatures.")));
85	}
86
87	Ok(Auth {
88		origin: origin.to_owned().into(),
89		..Auth::default()
90	})
91}
92
93fn auth_server_checks(services: &Services, x_matrix: &XMatrix) -> Result {
94	if !services.server.config.allow_federation {
95		return Err!(Config("allow_federation", "Federation is disabled."));
96	}
97
98	let destination = services.globals.server_name();
99	if x_matrix.destination.as_deref() != Some(destination) {
100		return Err!(Request(Forbidden("Invalid destination.")));
101	}
102
103	let origin = &x_matrix.origin;
104	if services
105		.config
106		.is_forbidden_remote_server_name(origin)
107	{
108		return Err!(Request(Forbidden(debug_warn!(
109			"Federation requests from {origin} denied."
110		))));
111	}
112
113	Ok(())
114}
115
116async fn parse_x_matrix(request: &mut Request) -> Result<XMatrix> {
117	let TypedHeader(Authorization(x_matrix)) = request
118		.parts
119		.extract::<TypedHeader<Authorization<XMatrix>>>()
120		.await
121		.map_err(|e| {
122			let msg = match e.reason() {
123				| TypedHeaderRejectionReason::Missing => "Missing Authorization header.",
124				| TypedHeaderRejectionReason::Error(_) => "Invalid X-Matrix signatures.",
125				| _ => "Unknown header-related error",
126			};
127
128			err!(Request(Forbidden(debug_warn!("{msg}: {e}"))))
129		})?;
130
131	Ok(x_matrix)
132}