Skip to main content

ruma_federation_api/
authentication.rs

1//! Common types for implementing federation authorization.
2
3use 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/// Authentication is performed by adding an `X-Matrix` authentication scheme as the request
18/// `Authorization` HTTP header, as defined in the [Matrix Server-Server API][spec].
19///
20/// The `X-Matrix` authentication scheme includes a signature of the request.
21///
22/// [spec]: https://spec.matrix.org/v1.19/server-server-api/#request-authentication
23#[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/// The input necessary to generate an `X-Matrix` authentication scheme by signing an HTTP request.
51#[derive(Debug, Clone)]
52#[non_exhaustive]
53pub struct XMatrixSigningInput<'a> {
54    /// The server making the request.
55    pub origin: OwnedServerName,
56
57    /// The server receiving the request.
58    pub destination: OwnedServerName,
59
60    /// The key pair to use to sign the request.
61    pub key_pair: &'a Ed25519KeyPair,
62}
63
64impl<'a> XMatrixSigningInput<'a> {
65    /// Construct a new `XMatrixSigningInput` with the given origin, destination and signing key
66    /// pair.
67    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/// Typed representation of an `X-Matrix` authentication scheme, as defined in the [Matrix
77/// Server-Server API][spec].
78///
79/// This is a scheme used in an `Authorization` HTTP header, as defined in [RFC 7235].
80///
81/// It can be extracted from an incoming HTTP request using [`XMatrix::parse()`] or
82/// [`XMatrix::extract_from_http_headers()`]. The HTTP request should then be verified with
83/// [`XMatrix::verify_http_request()`].
84///
85/// It can also be generated by signing an outgoing request with [`XMatrix::sign_http_request()`].
86///
87/// [spec]: https://spec.matrix.org/v1.19/server-server-api/#request-authentication
88/// [RFC 7235]: https://datatracker.ietf.org/doc/html/rfc7235
89#[derive(Clone)]
90#[non_exhaustive]
91pub struct XMatrix {
92    /// The server name of the sending server.
93    pub origin: OwnedServerName,
94
95    /// The server name of the receiving sender.
96    ///
97    /// For compatibility with older servers, recipients should accept requests without this
98    /// parameter, but MUST always send it. If this property is included, but the value does
99    /// not match the receiving server's name, the receiving server must deny the request with
100    /// a `401 Unauthorized` HTTP status code.
101    pub destination: Option<OwnedServerName>,
102
103    /// The ID - including the algorithm name - of the sending server's key that was used to sign
104    /// the request.
105    pub key: OwnedServerSigningKeyId,
106
107    /// The signature of the canonical JSON request object.
108    pub sig: Base64,
109}
110
111impl XMatrix {
112    /// The `auth-scheme` token used to identify the `X-Matrix` authentication scheme in the
113    /// `Authorization` HTTP header.
114    pub const AUTH_SCHEME: &'static str = "X-Matrix";
115
116    /// Construct a new `X-Matrix` authentication scheme from its parts.
117    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    /// Parse an `X-Matrix` authentication scheme from the given string.
127    ///
128    /// The string should be the value of an `Authorization` HTTP header.
129    ///
130    /// # Errors
131    ///
132    /// Returns an error if the scheme could not be parsed.
133    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    /// Try to extract an `X-Matrix` authentication scheme from the given HTTP headers.
195    ///
196    /// # Errors
197    ///
198    /// Returns an error if the `Authorization` header is not found in the map, or if the scheme
199    /// could not be parsed.
200    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    /// Construct the canonical JSON object representation to sign to generate or verify the
208    /// `X-Matrix` authentication scheme for the given request, with the given origin and
209    /// destination.
210    ///
211    /// # Errors
212    ///
213    /// Returns an error if the body of the request could not be serialized to canonical JSON.
214    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    /// Try to generate an `X-Matrix` authentication scheme by signing the given HTTP request with
238    /// the given signing input.
239    ///
240    /// The returned scheme should be added as the value of the `Authorization` header of the
241    /// request.
242    ///
243    /// # Errors
244    ///
245    /// Returns an error if the body of the request could not be serialized to canonical JSON or if
246    /// the ID of the signing key pair is invalid.
247    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        // The spec says to use the algorithm to sign JSON, so we could use
256        // ruma_signatures::sign_json, however since we would need to extract the signature from the
257        // JSON afterwards let's be a bit more efficient about it.
258        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    /// Verify that the signature in the `sig` field of this `X-Matrix` authentication scheme is
269    /// valid for the given incoming HTTP request and destination, with the given public keys map
270    /// from the `origin`.
271    ///
272    /// # Errors
273    ///
274    /// Returns an error if the given destination doesn't match the one present in the scheme, if
275    /// the body of the request could not be serialized to canonical JSON, or it the verification of
276    /// the signature failed.
277    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/// An error when trying to construct an [`XMatrix`] from a [`http::Request`].
356#[derive(Debug, Error)]
357#[non_exhaustive]
358pub enum XMatrixFromRequestError {
359    /// Failed to construct the request object to sign.
360    #[error("failed to construct request object to sign: {0}")]
361    IntoJson(#[from] serde_json::Error),
362
363    /// The signing key ID is invalid.
364    #[error("invalid signing key ID: {0}")]
365    SigningKeyId(IdParseError),
366}
367
368/// An error when trying to parse an X-Matrix Authorization header.
369#[derive(Debug, Error)]
370#[non_exhaustive]
371pub enum XMatrixParseError {
372    /// The `HeaderValue` could not be converted to a `str`.
373    #[error(transparent)]
374    ToStr(#[from] http::header::ToStrError),
375
376    /// The string could not be parsed as a valid Authorization string.
377    #[error("{0}")]
378    ParseStr(String),
379
380    /// The credentials with the X-Matrix scheme were not found.
381    #[error("X-Matrix credentials not found")]
382    NotFound,
383
384    /// The parameter value could not be parsed as a Matrix ID.
385    #[error(transparent)]
386    ParseId(#[from] IdParseError),
387
388    /// The parameter value could not be parsed as base64.
389    #[error(transparent)]
390    ParseBase64(#[from] Base64DecodeError),
391
392    /// The parameter with the given name was not found.
393    #[error("missing parameter '{0}'")]
394    MissingParameter(String),
395
396    /// The parameter with the given name was found more than once.
397    #[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/// An error when trying to extract an [`XMatrix`] from the headers of an HTTP request.
408#[derive(Debug, Error)]
409#[non_exhaustive]
410pub enum XMatrixExtractError {
411    /// No `Authorization` HTTP header was found, but the endpoint requires a server signature.
412    #[error("no Authorization HTTP header found, but this endpoint requires a server signature")]
413    MissingAuthorizationHeader,
414
415    /// Failed to parse the header value as an [`XMatrix`].
416    #[error("failed to parse header value: {0}")]
417    Parse(#[from] XMatrixParseError),
418}
419
420/// An error when trying to verify the signature in an [`XMatrix`] for an HTTP request.
421#[derive(Debug, Error)]
422#[non_exhaustive]
423pub enum XMatrixVerificationError {
424    /// The `destination` in [`XMatrix`] doesn't match the one to verify.
425    #[error("destination in XMatrix doesn't match the one to verify")]
426    DestinationMismatch,
427
428    /// The signature verification failed.
429    #[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}