1use std::{fmt, str::FromStr};
4
5use headers::authorization::Credentials;
6use http::HeaderValue;
7use http_auth::ChallengeParser;
8use ruma_common::{
9 CanonicalJsonObject, IdParseError, OwnedServerName, OwnedServerSigningKeyId, ServerName,
10 api::auth_scheme::AuthScheme,
11 http_headers::quote_ascii_string_if_required,
12 serde::{Base64, Base64DecodeError},
13};
14use ruma_signatures::{Ed25519KeyPair, KeyPair, PublicKeyMap};
15use thiserror::Error;
16use tracing::debug;
17
18#[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#[derive(Debug, Clone)]
55#[non_exhaustive]
56pub struct ServerSignaturesInput<'a> {
57 pub origin: OwnedServerName,
59
60 pub destination: OwnedServerName,
62
63 pub key_pair: &'a Ed25519KeyPair,
65}
66
67impl<'a> ServerSignaturesInput<'a> {
68 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#[derive(Clone)]
83#[non_exhaustive]
84pub struct XMatrix {
85 pub origin: OwnedServerName,
87 pub destination: Option<OwnedServerName>,
94 pub key: OwnedServerSigningKeyId,
97 pub sig: Base64,
99}
100
101impl XMatrix {
102 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 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 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 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 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 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#[derive(Debug, Error)]
315#[non_exhaustive]
316pub enum XMatrixFromRequestError {
317 #[error("failed to construct request object to sign: {0}")]
319 IntoJson(#[from] serde_json::Error),
320
321 #[error("invalid signing key ID: {0}")]
323 SigningKeyId(IdParseError),
324}
325
326#[derive(Debug, Error)]
328#[non_exhaustive]
329pub enum XMatrixParseError {
330 #[error(transparent)]
332 ToStr(#[from] http::header::ToStrError),
333
334 #[error("{0}")]
336 ParseStr(String),
337
338 #[error("X-Matrix credentials not found")]
340 NotFound,
341
342 #[error(transparent)]
344 ParseId(#[from] IdParseError),
345
346 #[error(transparent)]
348 ParseBase64(#[from] Base64DecodeError),
349
350 #[error("missing parameter '{0}'")]
352 MissingParameter(String),
353
354 #[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#[derive(Debug, Error)]
367#[non_exhaustive]
368pub enum XMatrixExtractError {
369 #[error("no Authorization HTTP header found, but this endpoint requires a server signature")]
371 MissingAuthorizationHeader,
372
373 #[error("failed to parse header value: {0}")]
375 Parse(#[from] XMatrixParseError),
376}
377
378#[derive(Debug, Error)]
380#[non_exhaustive]
381pub enum XMatrixVerificationError {
382 #[error("destination in XMatrix doesn't match the one to verify")]
384 DestinationMismatch,
385
386 #[error("signature verification failed: {0}")]
388 Signature(#[from] ruma_signatures::Error),
389}
390
391#[cfg(test)]
392mod tests {
393 use headers::{HeaderValue, authorization::Credentials};
394 use ruma_common::{OwnedServerName, serde::Base64};
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}