1use std::{fmt, str::FromStr};
4
5use http::{HeaderMap, HeaderValue};
6use http_auth::ChallengeParser;
7use ruma_common::{
8 CanonicalJsonObject, IdParseError, OwnedServerName, OwnedServerSigningKeyId, ServerName,
9 api::auth_scheme::AuthScheme,
10 http_headers::quote_ascii_string_if_required,
11 serde::{Base64, Base64DecodeError},
12};
13use ruma_signatures::{Ed25519KeyPair, KeyPair, PublicKeyMap};
14use thiserror::Error;
15use tracing::debug;
16
17#[derive(Debug, Clone, Copy, Default)]
24#[allow(clippy::exhaustive_structs)]
25pub struct ServerSignatures;
26
27impl AuthScheme for ServerSignatures {
28 type Input<'a> = XMatrixSigningInput<'a>;
29 type AddAuthenticationError = XMatrixFromRequestError;
30 type Output = XMatrix;
31 type ExtractAuthenticationError = XMatrixExtractError;
32
33 fn add_authentication<T: AsRef<[u8]>>(
34 request: &mut http::Request<T>,
35 input: XMatrixSigningInput<'_>,
36 ) -> Result<(), Self::AddAuthenticationError> {
37 let authorization = HeaderValue::from(&XMatrix::sign_http_request(request, input)?);
38 request.headers_mut().insert(http::header::AUTHORIZATION, authorization);
39
40 Ok(())
41 }
42
43 fn extract_authentication<T>(
44 request: &http::Request<T>,
45 ) -> Result<Self::Output, Self::ExtractAuthenticationError> {
46 XMatrix::extract_from_http_headers(request.headers())
47 }
48}
49
50#[derive(Debug, Clone)]
52#[non_exhaustive]
53pub struct XMatrixSigningInput<'a> {
54 pub origin: OwnedServerName,
56
57 pub destination: OwnedServerName,
59
60 pub key_pair: &'a Ed25519KeyPair,
62}
63
64impl<'a> XMatrixSigningInput<'a> {
65 pub fn new(
68 origin: OwnedServerName,
69 destination: OwnedServerName,
70 key_pair: &'a Ed25519KeyPair,
71 ) -> Self {
72 Self { origin, destination, key_pair }
73 }
74}
75
76#[derive(Clone)]
90#[non_exhaustive]
91pub struct XMatrix {
92 pub origin: OwnedServerName,
94
95 pub destination: Option<OwnedServerName>,
102
103 pub key: OwnedServerSigningKeyId,
106
107 pub sig: Base64,
109}
110
111impl XMatrix {
112 pub const AUTH_SCHEME: &'static str = "X-Matrix";
115
116 pub fn new(
118 origin: OwnedServerName,
119 destination: OwnedServerName,
120 key: OwnedServerSigningKeyId,
121 sig: Base64,
122 ) -> Self {
123 Self { origin, destination: Some(destination), key, sig }
124 }
125
126 pub fn parse(s: impl AsRef<str>) -> Result<Self, XMatrixParseError> {
134 let parser = ChallengeParser::new(s.as_ref());
135 let mut xmatrix = None;
136
137 for challenge in parser {
138 let challenge = challenge?;
139
140 if challenge.scheme.eq_ignore_ascii_case(XMatrix::AUTH_SCHEME) {
141 xmatrix = Some(challenge);
142 break;
143 }
144 }
145
146 let Some(xmatrix) = xmatrix else {
147 return Err(XMatrixParseError::NotFound);
148 };
149
150 let mut origin = None;
151 let mut destination = None;
152 let mut key = None;
153 let mut sig = None;
154
155 for (name, value) in xmatrix.params {
156 if name.eq_ignore_ascii_case("origin") {
157 if origin.is_some() {
158 return Err(XMatrixParseError::DuplicateParameter("origin".to_owned()));
159 } else {
160 origin = Some(OwnedServerName::try_from(value.to_unescaped())?);
161 }
162 } else if name.eq_ignore_ascii_case("destination") {
163 if destination.is_some() {
164 return Err(XMatrixParseError::DuplicateParameter("destination".to_owned()));
165 } else {
166 destination = Some(OwnedServerName::try_from(value.to_unescaped())?);
167 }
168 } else if name.eq_ignore_ascii_case("key") {
169 if key.is_some() {
170 return Err(XMatrixParseError::DuplicateParameter("key".to_owned()));
171 } else {
172 key = Some(OwnedServerSigningKeyId::try_from(value.to_unescaped())?);
173 }
174 } else if name.eq_ignore_ascii_case("sig") {
175 if sig.is_some() {
176 return Err(XMatrixParseError::DuplicateParameter("sig".to_owned()));
177 } else {
178 sig = Some(Base64::parse(value.to_unescaped())?);
179 }
180 } else {
181 debug!("Unknown parameter {name} in X-Matrix Authorization header");
182 }
183 }
184
185 Ok(Self {
186 origin: origin
187 .ok_or_else(|| XMatrixParseError::MissingParameter("origin".to_owned()))?,
188 destination,
189 key: key.ok_or_else(|| XMatrixParseError::MissingParameter("key".to_owned()))?,
190 sig: sig.ok_or_else(|| XMatrixParseError::MissingParameter("sig".to_owned()))?,
191 })
192 }
193
194 pub fn extract_from_http_headers(headers: &HeaderMap) -> Result<Self, XMatrixExtractError> {
201 let value = headers
202 .get(http::header::AUTHORIZATION)
203 .ok_or(XMatrixExtractError::MissingAuthorizationHeader)?;
204 Ok(value.try_into()?)
205 }
206
207 pub fn request_object<T: AsRef<[u8]>>(
215 request: &http::Request<T>,
216 origin: &ServerName,
217 destination: &ServerName,
218 ) -> Result<CanonicalJsonObject, serde_json::Error> {
219 let body = request.body().as_ref();
220 let uri = request.uri().path_and_query().expect("http::Request should have a path");
221
222 let mut request_object = CanonicalJsonObject::from([
223 ("destination".to_owned(), destination.as_str().into()),
224 ("method".to_owned(), request.method().as_str().into()),
225 ("origin".to_owned(), origin.as_str().into()),
226 ("uri".to_owned(), uri.as_str().into()),
227 ]);
228
229 if !body.is_empty() {
230 let content = serde_json::from_slice(body)?;
231 request_object.insert("content".to_owned(), content);
232 }
233
234 Ok(request_object)
235 }
236
237 pub fn sign_http_request<T: AsRef<[u8]>>(
248 request: &http::Request<T>,
249 input: XMatrixSigningInput<'_>,
250 ) -> Result<Self, XMatrixFromRequestError> {
251 let XMatrixSigningInput { origin, destination, key_pair } = input;
252
253 let request_object = Self::request_object(request, &origin, &destination)?;
254
255 let serialized_request_object = serde_json::to_vec(&request_object)?;
259 let (key_id, signature) = key_pair.sign(&serialized_request_object).into_parts();
260
261 let key = OwnedServerSigningKeyId::try_from(key_id.as_str())
262 .map_err(XMatrixFromRequestError::SigningKeyId)?;
263 let sig = Base64::new(signature);
264
265 Ok(Self { origin, destination: Some(destination), key, sig })
266 }
267
268 pub fn verify_http_request<T: AsRef<[u8]>>(
278 &self,
279 request: &http::Request<T>,
280 destination: &ServerName,
281 public_key_map: &PublicKeyMap,
282 ) -> Result<(), XMatrixVerificationError> {
283 if self
284 .destination
285 .as_deref()
286 .is_some_and(|xmatrix_destination| xmatrix_destination != destination)
287 {
288 return Err(XMatrixVerificationError::DestinationMismatch);
289 }
290
291 let mut request_object = Self::request_object(request, &self.origin, destination)
292 .map_err(|error| ruma_signatures::VerificationError::Json(error.into()))?;
293 let entity_signature =
294 CanonicalJsonObject::from([(self.key.to_string(), self.sig.encode().into())]);
295 let signatures =
296 CanonicalJsonObject::from([(self.origin.to_string(), entity_signature.into())]);
297 request_object.insert("signatures".to_owned(), signatures.into());
298
299 Ok(ruma_signatures::verify_json(public_key_map, &request_object)?)
300 }
301}
302
303impl fmt::Debug for XMatrix {
304 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
305 f.debug_struct("XMatrix")
306 .field("origin", &self.origin)
307 .field("destination", &self.destination)
308 .field("key", &self.key)
309 .finish_non_exhaustive()
310 }
311}
312
313impl fmt::Display for XMatrix {
314 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
315 let Self { origin, destination, key, sig } = self;
316
317 let origin = quote_ascii_string_if_required(origin.as_str());
318 let key = quote_ascii_string_if_required(key.as_str());
319 let sig = sig.encode();
320 let sig = quote_ascii_string_if_required(&sig);
321
322 write!(f, r#"{} "#, Self::AUTH_SCHEME)?;
323
324 if let Some(destination) = destination {
325 let destination = quote_ascii_string_if_required(destination.as_str());
326 write!(f, r#"destination={destination},"#)?;
327 }
328
329 write!(f, "key={key},origin={origin},sig={sig}")
330 }
331}
332
333impl FromStr for XMatrix {
334 type Err = XMatrixParseError;
335
336 fn from_str(s: &str) -> Result<Self, Self::Err> {
337 Self::parse(s)
338 }
339}
340
341impl TryFrom<&HeaderValue> for XMatrix {
342 type Error = XMatrixParseError;
343
344 fn try_from(value: &HeaderValue) -> Result<Self, Self::Error> {
345 Self::parse(value.to_str()?)
346 }
347}
348
349impl From<&XMatrix> for HeaderValue {
350 fn from(value: &XMatrix) -> Self {
351 value.to_string().try_into().expect("header format is static")
352 }
353}
354
355#[derive(Debug, Error)]
357#[non_exhaustive]
358pub enum XMatrixFromRequestError {
359 #[error("failed to construct request object to sign: {0}")]
361 IntoJson(#[from] serde_json::Error),
362
363 #[error("invalid signing key ID: {0}")]
365 SigningKeyId(IdParseError),
366}
367
368#[derive(Debug, Error)]
370#[non_exhaustive]
371pub enum XMatrixParseError {
372 #[error(transparent)]
374 ToStr(#[from] http::header::ToStrError),
375
376 #[error("{0}")]
378 ParseStr(String),
379
380 #[error("X-Matrix credentials not found")]
382 NotFound,
383
384 #[error(transparent)]
386 ParseId(#[from] IdParseError),
387
388 #[error(transparent)]
390 ParseBase64(#[from] Base64DecodeError),
391
392 #[error("missing parameter '{0}'")]
394 MissingParameter(String),
395
396 #[error("duplicate parameter '{0}'")]
398 DuplicateParameter(String),
399}
400
401impl<'a> From<http_auth::parser::Error<'a>> for XMatrixParseError {
402 fn from(value: http_auth::parser::Error<'a>) -> Self {
403 Self::ParseStr(value.to_string())
404 }
405}
406
407#[derive(Debug, Error)]
409#[non_exhaustive]
410pub enum XMatrixExtractError {
411 #[error("no Authorization HTTP header found, but this endpoint requires a server signature")]
413 MissingAuthorizationHeader,
414
415 #[error("failed to parse header value: {0}")]
417 Parse(#[from] XMatrixParseError),
418}
419
420#[derive(Debug, Error)]
422#[non_exhaustive]
423pub enum XMatrixVerificationError {
424 #[error("destination in XMatrix doesn't match the one to verify")]
426 DestinationMismatch,
427
428 #[error("signature verification failed: {0}")]
430 Signature(#[from] ruma_signatures::VerificationError),
431}
432
433#[cfg(test)]
434mod tests {
435 use http::header::HeaderValue;
436 use ruma_common::{OwnedServerName, serde::Base64};
437
438 use super::XMatrix;
439
440 #[test]
441 fn xmatrix_auth_pre_1_3() {
442 let header = HeaderValue::from_static(
443 "X-Matrix origin=\"origin.hs.example.com\",key=\"ed25519:key1\",sig=\"dGVzdA==\"",
444 );
445 let origin = "origin.hs.example.com".try_into().unwrap();
446 let key = "ed25519:key1".try_into().unwrap();
447 let sig = Base64::new(b"test".to_vec());
448 let credentials = XMatrix::try_from(&header).unwrap();
449 assert_eq!(credentials.origin, origin);
450 assert_eq!(credentials.destination, None);
451 assert_eq!(credentials.key, key);
452 assert_eq!(credentials.sig, sig);
453
454 let credentials = XMatrix { origin, destination: None, key, sig };
455
456 assert_eq!(
457 credentials.to_string(),
458 "X-Matrix key=\"ed25519:key1\",origin=origin.hs.example.com,sig=dGVzdA"
459 );
460 }
461
462 #[test]
463 fn xmatrix_auth_1_3() {
464 let header = HeaderValue::from_static(
465 "X-Matrix origin=\"origin.hs.example.com\",destination=\"destination.hs.example.com\",key=\"ed25519:key1\",sig=\"dGVzdA==\"",
466 );
467 let origin: OwnedServerName = "origin.hs.example.com".try_into().unwrap();
468 let destination: OwnedServerName = "destination.hs.example.com".try_into().unwrap();
469 let key = "ed25519:key1".try_into().unwrap();
470 let sig = Base64::new(b"test".to_vec());
471 let credentials = XMatrix::try_from(&header).unwrap();
472 assert_eq!(credentials.origin, origin);
473 assert_eq!(credentials.destination, Some(destination.clone()));
474 assert_eq!(credentials.key, key);
475 assert_eq!(credentials.sig, sig);
476
477 let credentials = XMatrix::new(origin, destination, key, sig);
478
479 assert_eq!(
480 credentials.to_string(),
481 "X-Matrix destination=destination.hs.example.com,key=\"ed25519:key1\",origin=origin.hs.example.com,sig=dGVzdA"
482 );
483 }
484
485 #[test]
486 fn xmatrix_quoting() {
487 let header = HeaderValue::from_static(
488 r#"X-Matrix origin="example.com:1234",key="abc\"def\\:ghi",sig=dGVzdA,"#,
489 );
490
491 let origin: OwnedServerName = "example.com:1234".try_into().unwrap();
492 let key = r#"abc"def\:ghi"#.try_into().unwrap();
493 let sig = Base64::new(b"test".to_vec());
494 let credentials = XMatrix::try_from(&header).unwrap();
495 assert_eq!(credentials.origin, origin);
496 assert_eq!(credentials.destination, None);
497 assert_eq!(credentials.key, key);
498 assert_eq!(credentials.sig, sig);
499
500 let credentials = XMatrix { origin, destination: None, key, sig };
501
502 assert_eq!(
503 credentials.to_string(),
504 r#"X-Matrix key="abc\"def\\:ghi",origin="example.com:1234",sig=dGVzdA"#
505 );
506 }
507
508 #[test]
509 fn xmatrix_auth_1_3_with_extra_spaces() {
510 let header = HeaderValue::from_static(
511 "X-Matrix origin=\"origin.hs.example.com\" , destination=\"destination.hs.example.com\",key=\"ed25519:key1\", sig=\"dGVzdA\"",
512 );
513 let credentials = XMatrix::try_from(&header).unwrap();
514 let sig = Base64::new(b"test".to_vec());
515
516 assert_eq!(credentials.origin, "origin.hs.example.com");
517 assert_eq!(credentials.destination.unwrap(), "destination.hs.example.com");
518 assert_eq!(credentials.key, "ed25519:key1");
519 assert_eq!(credentials.sig, sig);
520 }
521}