pubhubs/api/hub.rs
1//! Endpoints provided by a hub
2use actix_web::http;
3use serde::{Deserialize, Serialize};
4
5use crate::api::*;
6use crate::id;
7use crate::misc::serde_ext::{self, bytes_wrapper::B64UU};
8
9/// Basic information advertised by the hub
10pub struct InfoEP {}
11impl EndpointDetails for InfoEP {
12 type RequestType = NoPayload;
13 type ResponseType = Result<InfoResp>;
14
15 const METHOD: http::Method = http::Method::GET;
16 const PATH: &'static str = ".ph/info";
17}
18
19#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
20#[serde(deny_unknown_fields)]
21#[must_use]
22pub struct InfoResp {
23 /// Key used by the hub to sign requests to the other hubs with
24 ///
25 /// (Not currently returned by actual hubs.)
26 #[serde(default)]
27 #[serde(skip_serializing_if = "Option::is_none")]
28 pub verifying_key: Option<VerifyingKeyBytes>,
29
30 /// String describing the hub version, likely the result of `git describe --tags`
31 pub hub_version: String,
32
33 /// URL to this hub's client
34 pub hub_client_url: url::Url,
35
36 /// The database engine the hub is currently running on, which lets us track which hubs
37 /// have completed the sqlite3 -> postgres migration. Defaults to [`DatabaseEngine::Unknown`]
38 /// for hubs predating this field.
39 #[serde(default)]
40 pub database_engine: DatabaseEngine,
41
42 /// Changeable information about this hub.
43 #[serde(default)]
44 #[serde(skip_serializing_if = "Option::is_none")]
45 pub dynamic: Option<DynamicHubInfo>,
46}
47
48/// Database engine a hub runs on; reported by [`InfoResp::database_engine`].
49#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
50#[serde(rename_all = "lowercase")]
51pub enum DatabaseEngine {
52 /// Synapse's built-in sqlite3 database: a hub that has not (yet) migrated.
53 Sqlite3,
54
55 /// The embedded postgres database: a hub that has completed the migration.
56 Postgres,
57
58 /// Reported by hubs predating this field, or running an unrecognised engine. Also the
59 /// catch-all for any engine string we don't know, so a new engine never fails to parse.
60 #[default]
61 #[serde(other)]
62 Unknown,
63}
64
65/// Type for [`InfoResp::dynamic`]
66#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
67#[serde(deny_unknown_fields)]
68#[must_use]
69pub struct DynamicHubInfo {
70 /// The last time these settings were reloaded by the hub, which a hub generally does every
71 /// minute.
72 pub last_reload: NumericDate,
73
74 /// Hub settings set by admin
75 pub settings: serde_json::Value,
76}
77
78/// Endpoint that start the authentication of a (not yet existing) user
79pub struct EnterStartEP {}
80impl EndpointDetails for EnterStartEP {
81 type RequestType = NoPayload;
82 type ResponseType = Result<EnterStartResp>;
83
84 const METHOD: http::Method = http::Method::POST; // to dissuade caching
85 const PATH: &'static str = ".ph/enter-start";
86}
87
88/// What's returned by [`EnterStartEP`].
89#[derive(Serialize, Deserialize, Debug, Clone)]
90#[serde(deny_unknown_fields)]
91#[must_use]
92pub struct EnterStartResp {
93 /// Opaque state that needs to be send to the [`EnterCompleteEP`] later on
94 pub state: EnterState,
95
96 /// Opaque number used only once to be included in the hub pseudonym package.
97 pub nonce: EnterNonce,
98
99 /// The HHPP signature scheme this hub can verify. The global client must relay it to
100 /// [`HhppReq::hhpp_signature_scheme`](crate::api::phc::user::HhppReq::hhpp_signature_scheme) so
101 /// PHC signs with a key the hub accepts. Absent ⇒
102 /// [`Ed25519`](sso::HhppSignatureScheme::Ed25519), for hubs predating the hybrid migration.
103 #[serde(default, skip_serializing_if = "sso::HhppSignatureScheme::is_default")]
104 pub hhpp_signature_scheme: sso::HhppSignatureScheme,
105
106 /// Per-session key the hub uses to bind its id; see [`HubMacKey`]. Relayed by the global client to
107 /// [`EhppReq::hub_mac_key`](crate::api::tr::EhppReq::hub_mac_key). `None` for hubs predating the
108 /// check.
109 ///
110 /// TODO: remove once all hubs are >3.4.0.
111 #[serde(default, skip_serializing_if = "Option::is_none")]
112 pub hub_mac_key: Option<HubMacKey>,
113}
114
115/// Type of [`EnterStartResp::state`]
116#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
117#[serde(transparent)]
118pub struct EnterState {
119 pub(crate) inner: B64UU,
120}
121
122impl From<B64UU> for EnterState {
123 fn from(inner: B64UU) -> Self {
124 Self { inner }
125 }
126}
127
128/// Type of [`EnterStartResp::nonce`]
129#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
130#[serde(transparent)]
131pub struct EnterNonce {
132 pub(crate) inner: B64UU,
133}
134impl From<B64UU> for EnterNonce {
135 fn from(inner: B64UU) -> Self {
136 Self { inner }
137 }
138}
139
140/// Symmetric key to bind the hub id, but 'blind' the hub id from PHC.
141///
142/// The [`HubMacKey`] is randomly generated by the hub for each call to [`EnterStartEP`], stored in
143/// the hub-encrypted [`EnterStartResp::state`] — which, unlike the [`EnterNonce`], never transits
144/// the transcryptor or PHC — and returned to the global client via [`EnterStartResp::hub_mac_key`].
145/// The global client relays it to the transcryptor via
146/// [`tr::EhppReq::hub_mac_key`](crate::api::tr::EhppReq::hub_mac_key).
147///
148/// The transcryptor uses the [`HubMacKey`] to HMAC the hub id it pseudonymises for (see
149/// [`HubMacKey::mac`]). The mac is passed via the [`sso::EncryptedHubPseudonymPackage`] to PHC,
150/// which includes it in the [`sso::HashedHubPseudonymPackage`] that eventually ends up back at the
151/// hub, via [`EnterCompleteReq`]. The hub can then check the mac is indeed the mac of its own hub
152/// id, using the [`HubMacKey`] recovered from its state.
153///
154/// This [`HubMacKey`] is known to the hub and the global client, but must remain hidden from PHC.
155#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, zeroize::ZeroizeOnDrop)]
156#[serde(transparent)]
157pub struct HubMacKey {
158 inner: B64UU<serde_ext::ByteArray<32>>,
159}
160
161impl HubMacKey {
162 /// Generates a new random key.
163 pub fn random() -> Self {
164 Self {
165 inner: serde_ext::ByteArray::<32>::from(crate::misc::crypto::random_32_bytes()).into(),
166 }
167 }
168
169 /// Macs a `hub_id`: returns `HMAC-SHA256(self, hub_id)`, the hub id blinded so PHC can not
170 /// see what hub is being visited.
171 pub fn mac(&self, hub_id: &id::Id) -> id::Id {
172 use hmac::Mac as _;
173
174 // Standalone HMAC-SHA256 (cf. `jwt::HS256`), vector-locked with the Python hub's
175 // `verify_hub_id_mac` via the `hub_id_mac_vector` test — kept off the JWT layer on purpose.
176 let mut mac = hmac::Hmac::<sha2::Sha256>::new_from_slice(self.inner.as_slice())
177 .expect("HMAC did not accept key - but it should accept any key");
178 mac.update(hub_id.as_slice());
179 let bytes: [u8; 32] = mac.finalize().into_bytes().into();
180 id::Id::from(bytes)
181 }
182}
183
184/// Endpoint to complete user authentication
185pub struct EnterCompleteEP {}
186impl EndpointDetails for EnterCompleteEP {
187 type RequestType = EnterCompleteReq;
188 type ResponseType = Result<EnterCompleteResp>;
189
190 const METHOD: http::Method = http::Method::POST; // to dissuade caching
191 const PATH: &'static str = ".ph/enter-complete";
192}
193
194/// What's sent to [`EnterCompleteEP`]
195#[derive(Serialize, Deserialize, Debug, Clone)]
196#[serde(deny_unknown_fields)]
197pub struct EnterCompleteReq {
198 /// The one you got from [`EnterStartResp::state`]
199 pub state: EnterState,
200
201 /// The hashed hub pseudonym package obtained from pubhubs central.
202 /// Should include the [`EnterStartResp::nonce`] belonging to the [`Self::state`].
203 pub hhpp: Signed<sso::HashedHubPseudonymPackage>,
204}
205
206/// What's returned by [`EnterCompleteEP`].
207#[derive(Serialize, Deserialize, Debug, Clone)]
208#[serde(deny_unknown_fields)]
209#[must_use]
210pub enum EnterCompleteResp {
211 /// Start again at [`EnterStartEP`]
212 RetryFromStart,
213
214 Entered {
215 /// Synapse access token
216 access_token: String,
217
218 /// Device ID. (Not sure if it's useful to the global client, though.)
219 device_id: String,
220
221 /// True if this is the first time this user enters this hub.
222 new_user: bool,
223
224 /// Matrix id to use in combination with the [`Self::Entered::access_token`] to log in.
225 mxid: String,
226 },
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 /// Shares a fixed HMAC-SHA256 vector with `pubhubs_hub/test/hub_id_mac_test.py`, locking the
234 /// transcryptor's [`HubMacKey::mac`] and the hub's Python check to the same bytes.
235 #[test]
236 fn hub_id_mac_vector() {
237 // key = 32 bytes of 0x01, hub id = 32 bytes of 0x02 (both unpadded base64url)
238 let key: HubMacKey =
239 serde_json::from_str("\"AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE\"").unwrap();
240 let hub_id = id::Id::from([2u8; 32]);
241 let expected: id::Id =
242 serde_json::from_str("\"cyFAbgyID9kll4mL6zk68q6-1Rq1zfHVLlmR-a-t0t4\"").unwrap();
243 assert_eq!(key.mac(&hub_id), expected);
244 }
245}