ruma_federation_api/
authentication.rs

1//! Common types for implementing federation authorization.
2
3use std::{fmt, str::FromStr};
4
5use headers::authorization::Credentials;
6use http::HeaderValue;
7use http_auth::ChallengeParser;
8use ruma_common::{
9    api::auth_scheme::AuthScheme,
10    http_headers::quote_ascii_string_if_required,
11    serde::{Base64, Base64DecodeError},
12    CanonicalJsonObject, IdParseError, OwnedServerName, OwnedServerSigningKeyId, ServerName,
13};
14use ruma_signatures::{Ed25519KeyPair, KeyPair, PublicKeyMap};
15use thiserror::Error;
16use tracing::debug;
17
18/// Authentication is performed by adding an `X-Matrix` header including a signature in the request
19/// headers, as defined in the [Matrix Server-Server API][spec].
20///
21/// [spec]: https://spec.matrix.org/latest/server-server-api/#request-authentication
22#[derive(Debug, Clone, Copy, Default)]
23#[allow(clippy::exhaustive_structs)]
24pub struct ServerSignatures;
25
26impl AuthScheme for ServerSignatures {
27    type Input<'a> = ServerSignaturesInput<'a>;
28    type AddAuthenticationError = XMatrixFromRequestError;
29    type Output = XMatrix;
30    type ExtractAuthenticationError = XMatrixExtractError;
31
32    fn add_authentication<T: AsRef<[u8]>>(
33        request: &mut http::Request<T>,
34        input: ServerSignaturesInput<'_>,
35    ) -> Result<(), Self::AddAuthenticationError> {
36        let authorization = HeaderValue::from(&XMatrix::try_from_http_request(request, input)?);
37        request.headers_mut().insert(http::header::AUTHORIZATION, authorization);
38
39        Ok(())
40    }
41
42    fn extract_authentication<T: AsRef<[u8]>>(
43        request: &http::Request<T>,
44    ) -> Result<Self::Output, Self::ExtractAuthenticationError> {
45        let value = request
46            .headers()
47            .get(http::header::AUTHORIZATION)
48            .ok_or(XMatrixExtractError::MissingAuthorizationHeader)?;
49        Ok(value.try_into()?)
50    }
51}
52
53/// The input necessary to generate the [`ServerSignatures`] authentication scheme.
54#[derive(Debug, Clone)]
55#[non_exhaustive]
56pub struct ServerSignaturesInput<'a> {
57    /// The server making the request.
58    pub origin: OwnedServerName,
59
60    /// The server receiving the request.
61    pub destination: OwnedServerName,
62
63    /// The key pair to use to sign the request.
64    pub key_pair: &'a Ed25519KeyPair,
65}
66
67impl<'a> ServerSignaturesInput<'a> {
68    /// Construct a new `ServerSignaturesInput` with the given origin and key pair.
69    pub fn new(
70        origin: OwnedServerName,
71        destination: OwnedServerName,
72        key_pair: &'a Ed25519KeyPair,
73    ) -> Self {
74        Self { origin, destination, key_pair }
75    }
76}
77
78/// Typed representation of an `Authorization` header of scheme `X-Matrix`, as defined in the
79/// [Matrix Server-Server API][spec].
80///
81/// [spec]: https://spec.matrix.org/latest/server-server-api/#request-authentication
82#[derive(Clone)]
83#[non_exhaustive]
84pub struct XMatrix {
85    /// The server name of the sending server.
86    pub origin: OwnedServerName,
87    /// The server name of the receiving sender.
88    ///
89    /// For compatibility with older servers, recipients should accept requests without this
90    /// parameter, but MUST always send it. If this property is included, but the value does
91    /// not match the receiving server's name, the receiving server must deny the request with
92    /// an HTTP status code 401 Unauthorized.
93    pub destination: Option<OwnedServerName>,
94    /// The ID - including the algorithm name - of the sending server's key that was used to sign
95    /// the request.
96    pub key: OwnedServerSigningKeyId,
97    /// The signature of the JSON.
98    pub sig: Base64,
99}
100
101impl XMatrix {
102    /// Construct a new X-Matrix Authorization header.
103    pub fn new(
104        origin: OwnedServerName,
105        destination: OwnedServerName,
106        key: OwnedServerSigningKeyId,
107        sig: Base64,
108    ) -> Self {
109        Self { origin, destination: Some(destination), key, sig }
110    }
111
112    /// Parse an X-Matrix Authorization header from the given string.
113    pub fn parse(s: impl AsRef<str>) -> Result<Self, XMatrixParseError> {
114        let parser = ChallengeParser::new(s.as_ref());
115        let mut xmatrix = None;
116
117        for challenge in parser {
118            let challenge = challenge?;
119
120            if challenge.scheme.eq_ignore_ascii_case(XMatrix::SCHEME) {
121                xmatrix = Some(challenge);
122                break;
123            }
124        }
125
126        let Some(xmatrix) = xmatrix else {
127            return Err(XMatrixParseError::NotFound);
128        };
129
130        let mut origin = None;
131        let mut destination = None;
132        let mut key = None;
133        let mut sig = None;
134
135        for (name, value) in xmatrix.params {
136            if name.eq_ignore_ascii_case("origin") {
137                if origin.is_some() {
138                    return Err(XMatrixParseError::DuplicateParameter("origin".to_owned()));
139                } else {
140                    origin = Some(OwnedServerName::try_from(value.to_unescaped())?);
141                }
142            } else if name.eq_ignore_ascii_case("destination") {
143                if destination.is_some() {
144                    return Err(XMatrixParseError::DuplicateParameter("destination".to_owned()));
145                } else {
146                    destination = Some(OwnedServerName::try_from(value.to_unescaped())?);
147                }
148            } else if name.eq_ignore_ascii_case("key") {
149                if key.is_some() {
150                    return Err(XMatrixParseError::DuplicateParameter("key".to_owned()));
151                } else {
152                    key = Some(OwnedServerSigningKeyId::try_from(value.to_unescaped())?);
153                }
154            } else if name.eq_ignore_ascii_case("sig") {
155                if sig.is_some() {
156                    return Err(XMatrixParseError::DuplicateParameter("sig".to_owned()));
157                } else {
158                    sig = Some(Base64::parse(value.to_unescaped())?);
159                }
160            } else {
161                debug!("Unknown parameter {name} in X-Matrix Authorization header");
162            }
163        }
164
165        Ok(Self {
166            origin: origin
167                .ok_or_else(|| XMatrixParseError::MissingParameter("origin".to_owned()))?,
168            destination,
169            key: key.ok_or_else(|| XMatrixParseError::MissingParameter("key".to_owned()))?,
170            sig: sig.ok_or_else(|| XMatrixParseError::MissingParameter("sig".to_owned()))?,
171        })
172    }
173
174    /// Construct the canonical JSON object representation of the request to sign for the `XMatrix`
175    /// scheme.
176    pub fn request_object<T: AsRef<[u8]>>(
177        request: &http::Request<T>,
178        origin: &ServerName,
179        destination: &ServerName,
180    ) -> Result<CanonicalJsonObject, serde_json::Error> {
181        let body = request.body().as_ref();
182        let uri = request.uri().path_and_query().expect("http::Request should have a path");
183
184        let mut request_object = CanonicalJsonObject::from([
185            ("destination".to_owned(), destination.as_str().into()),
186            ("method".to_owned(), request.method().as_str().into()),
187            ("origin".to_owned(), origin.as_str().into()),
188            ("uri".to_owned(), uri.as_str().into()),
189        ]);
190
191        if !body.is_empty() {
192            let content = serde_json::from_slice(body)?;
193            request_object.insert("content".to_owned(), content);
194        }
195
196        Ok(request_object)
197    }
198
199    /// Try to construct this header from the given HTTP request and input.
200    pub fn try_from_http_request<T: AsRef<[u8]>>(
201        request: &http::Request<T>,
202        input: ServerSignaturesInput<'_>,
203    ) -> Result<Self, XMatrixFromRequestError> {
204        let ServerSignaturesInput { origin, destination, key_pair } = input;
205
206        let request_object = Self::request_object(request, &origin, &destination)?;
207
208        // The spec says to use the algorithm to sign JSON, so we could use
209        // ruma_signatures::sign_json, however since we would need to extract the signature from the
210        // JSON afterwards let's be a bit more efficient about it.
211        let serialized_request_object = serde_json::to_vec(&request_object)?;
212        let (key_id, signature) = key_pair.sign(&serialized_request_object).into_parts();
213
214        let key = OwnedServerSigningKeyId::try_from(key_id.as_str())
215            .map_err(XMatrixFromRequestError::SigningKeyId)?;
216        let sig = Base64::new(signature);
217
218        Ok(Self { origin, destination: Some(destination), key, sig })
219    }
220
221    /// Verify that the signature in the `sig` field is valid for the given incoming HTTP request,
222    /// with the given public keys map from the origin.
223    pub fn verify_request<T: AsRef<[u8]>>(
224        &self,
225        request: &http::Request<T>,
226        destination: &ServerName,
227        public_key_map: &PublicKeyMap,
228    ) -> Result<(), XMatrixVerificationError> {
229        if self
230            .destination
231            .as_deref()
232            .is_some_and(|xmatrix_destination| xmatrix_destination != destination)
233        {
234            return Err(XMatrixVerificationError::DestinationMismatch);
235        }
236
237        let mut request_object = Self::request_object(request, &self.origin, destination)
238            .map_err(|error| ruma_signatures::Error::Json(error.into()))?;
239        let entity_signature =
240            CanonicalJsonObject::from([(self.key.to_string(), self.sig.encode().into())]);
241        let signatures =
242            CanonicalJsonObject::from([(self.origin.to_string(), entity_signature.into())]);
243        request_object.insert("signatures".to_owned(), signatures.into());
244
245        Ok(ruma_signatures::verify_json(public_key_map, &request_object)?)
246    }
247}
248
249impl fmt::Debug for XMatrix {
250    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
251        f.debug_struct("XMatrix")
252            .field("origin", &self.origin)
253            .field("destination", &self.destination)
254            .field("key", &self.key)
255            .finish_non_exhaustive()
256    }
257}
258
259impl fmt::Display for XMatrix {
260    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261        let Self { origin, destination, key, sig } = self;
262
263        let origin = quote_ascii_string_if_required(origin.as_str());
264        let key = quote_ascii_string_if_required(key.as_str());
265        let sig = sig.encode();
266        let sig = quote_ascii_string_if_required(&sig);
267
268        write!(f, r#"{} "#, Self::SCHEME)?;
269
270        if let Some(destination) = destination {
271            let destination = quote_ascii_string_if_required(destination.as_str());
272            write!(f, r#"destination={destination},"#)?;
273        }
274
275        write!(f, "key={key},origin={origin},sig={sig}")
276    }
277}
278
279impl FromStr for XMatrix {
280    type Err = XMatrixParseError;
281
282    fn from_str(s: &str) -> Result<Self, Self::Err> {
283        Self::parse(s)
284    }
285}
286
287impl TryFrom<&HeaderValue> for XMatrix {
288    type Error = XMatrixParseError;
289
290    fn try_from(value: &HeaderValue) -> Result<Self, Self::Error> {
291        Self::parse(value.to_str()?)
292    }
293}
294
295impl From<&XMatrix> for HeaderValue {
296    fn from(value: &XMatrix) -> Self {
297        value.to_string().try_into().expect("header format is static")
298    }
299}
300
301impl Credentials for XMatrix {
302    const SCHEME: &'static str = "X-Matrix";
303
304    fn decode(value: &HeaderValue) -> Option<Self> {
305        value.try_into().ok()
306    }
307
308    fn encode(&self) -> HeaderValue {
309        self.into()
310    }
311}
312
313/// An error when trying to construct an [`XMatrix`] from a [`http::Request`].
314#[derive(Debug, Error)]
315#[non_exhaustive]
316pub enum XMatrixFromRequestError {
317    /// Failed to construct the request object to sign.
318    #[error("failed to construct request object to sign: {0}")]
319    IntoJson(#[from] serde_json::Error),
320
321    /// The signing key ID is invalid.
322    #[error("invalid signing key ID: {0}")]
323    SigningKeyId(IdParseError),
324}
325
326/// An error when trying to parse an X-Matrix Authorization header.
327#[derive(Debug, Error)]
328#[non_exhaustive]
329pub enum XMatrixParseError {
330    /// The `HeaderValue` could not be converted to a `str`.
331    #[error(transparent)]
332    ToStr(#[from] http::header::ToStrError),
333
334    /// The string could not be parsed as a valid Authorization string.
335    #[error("{0}")]
336    ParseStr(String),
337
338    /// The credentials with the X-Matrix scheme were not found.
339    #[error("X-Matrix credentials not found")]
340    NotFound,
341
342    /// The parameter value could not be parsed as a Matrix ID.
343    #[error(transparent)]
344    ParseId(#[from] IdParseError),
345
346    /// The parameter value could not be parsed as base64.
347    #[error(transparent)]
348    ParseBase64(#[from] Base64DecodeError),
349
350    /// The parameter with the given name was not found.
351    #[error("missing parameter '{0}'")]
352    MissingParameter(String),
353
354    /// The parameter with the given name was found more than once.
355    #[error("duplicate parameter '{0}'")]
356    DuplicateParameter(String),
357}
358
359impl<'a> From<http_auth::parser::Error<'a>> for XMatrixParseError {
360    fn from(value: http_auth::parser::Error<'a>) -> Self {
361        Self::ParseStr(value.to_string())
362    }
363}
364
365/// An error when trying to extract an [`XMatrix`] from an HTTP request.
366#[derive(Debug, Error)]
367#[non_exhaustive]
368pub enum XMatrixExtractError {
369    /// No Authorization HTTP header was found, but the endpoint requires a server signature.
370    #[error("no Authorization HTTP header found, but this endpoint requires a server signature")]
371    MissingAuthorizationHeader,
372
373    /// Failed to convert the header value to an [`XMatrix`].
374    #[error("failed to parse header value: {0}")]
375    Parse(#[from] XMatrixParseError),
376}
377
378/// An error when trying to verify the signature in an [`XMatrix`] for an HTTP request.
379#[derive(Debug, Error)]
380#[non_exhaustive]
381pub enum XMatrixVerificationError {
382    /// The `destination` in [`XMatrix`] doesn't match the one to verify.
383    #[error("destination in XMatrix doesn't match the one to verify")]
384    DestinationMismatch,
385
386    /// The signature verification failed.
387    #[error("signature verification failed: {0}")]
388    Signature(#[from] ruma_signatures::Error),
389}
390
391#[cfg(test)]
392mod tests {
393    use headers::{authorization::Credentials, HeaderValue};
394    use ruma_common::{serde::Base64, OwnedServerName};
395
396    use super::XMatrix;
397
398    #[test]
399    fn xmatrix_auth_pre_1_3() {
400        let header = HeaderValue::from_static(
401            "X-Matrix origin=\"origin.hs.example.com\",key=\"ed25519:key1\",sig=\"dGVzdA==\"",
402        );
403        let origin = "origin.hs.example.com".try_into().unwrap();
404        let key = "ed25519:key1".try_into().unwrap();
405        let sig = Base64::new(b"test".to_vec());
406        let credentials = XMatrix::try_from(&header).unwrap();
407        assert_eq!(credentials.origin, origin);
408        assert_eq!(credentials.destination, None);
409        assert_eq!(credentials.key, key);
410        assert_eq!(credentials.sig, sig);
411
412        let credentials = XMatrix { origin, destination: None, key, sig };
413
414        assert_eq!(
415            credentials.encode(),
416            "X-Matrix key=\"ed25519:key1\",origin=origin.hs.example.com,sig=dGVzdA"
417        );
418    }
419
420    #[test]
421    fn xmatrix_auth_1_3() {
422        let header = HeaderValue::from_static(
423            "X-Matrix origin=\"origin.hs.example.com\",destination=\"destination.hs.example.com\",key=\"ed25519:key1\",sig=\"dGVzdA==\"",
424        );
425        let origin: OwnedServerName = "origin.hs.example.com".try_into().unwrap();
426        let destination: OwnedServerName = "destination.hs.example.com".try_into().unwrap();
427        let key = "ed25519:key1".try_into().unwrap();
428        let sig = Base64::new(b"test".to_vec());
429        let credentials = XMatrix::try_from(&header).unwrap();
430        assert_eq!(credentials.origin, origin);
431        assert_eq!(credentials.destination, Some(destination.clone()));
432        assert_eq!(credentials.key, key);
433        assert_eq!(credentials.sig, sig);
434
435        let credentials = XMatrix::new(origin, destination, key, sig);
436
437        assert_eq!(
438            credentials.encode(),
439            "X-Matrix destination=destination.hs.example.com,key=\"ed25519:key1\",origin=origin.hs.example.com,sig=dGVzdA"
440        );
441    }
442
443    #[test]
444    fn xmatrix_quoting() {
445        let header = HeaderValue::from_static(
446            r#"X-Matrix origin="example.com:1234",key="abc\"def\\:ghi",sig=dGVzdA,"#,
447        );
448
449        let origin: OwnedServerName = "example.com:1234".try_into().unwrap();
450        let key = r#"abc"def\:ghi"#.try_into().unwrap();
451        let sig = Base64::new(b"test".to_vec());
452        let credentials = XMatrix::try_from(&header).unwrap();
453        assert_eq!(credentials.origin, origin);
454        assert_eq!(credentials.destination, None);
455        assert_eq!(credentials.key, key);
456        assert_eq!(credentials.sig, sig);
457
458        let credentials = XMatrix { origin, destination: None, key, sig };
459
460        assert_eq!(
461            credentials.encode(),
462            r#"X-Matrix key="abc\"def\\:ghi",origin="example.com:1234",sig=dGVzdA"#
463        );
464    }
465
466    #[test]
467    fn xmatrix_auth_1_3_with_extra_spaces() {
468        let header = HeaderValue::from_static(
469            "X-Matrix origin=\"origin.hs.example.com\"  ,     destination=\"destination.hs.example.com\",key=\"ed25519:key1\", sig=\"dGVzdA\"",
470        );
471        let credentials = XMatrix::try_from(&header).unwrap();
472        let sig = Base64::new(b"test".to_vec());
473
474        assert_eq!(credentials.origin, "origin.hs.example.com");
475        assert_eq!(credentials.destination.unwrap(), "destination.hs.example.com");
476        assert_eq!(credentials.key, "ed25519:key1");
477        assert_eq!(credentials.sig, sig);
478    }
479}