Skip to main content

pubhubs/servers/auths/
server.rs

1//! Authentication server core code
2use std::collections::HashMap;
3use std::ops::{Deref, DerefMut};
4use std::rc::Rc;
5
6use actix_web::web;
7use sha2::digest::Digest as _;
8
9use crate::servers::{self, AppBase, AppCreatorBase, Constellation, Handle, constellation, yivi};
10use crate::{
11    api::{self, EndpointDetails as _},
12    attr,
13    common::{elgamal, secret::DigestibleSecret as _},
14    handle, id, map,
15    misc::{crypto, jwt},
16    phcrypto,
17};
18
19use super::yivi::ChainedSessionsCtl;
20
21/// Authentication server type
22pub type Server = servers::ServerImpl<Details>;
23
24/// [`servers::Details`] used to define [`Server`].
25pub struct Details;
26impl servers::Details for Details {
27    const NAME: servers::Name = servers::Name::AuthenticationServer;
28
29    type AppT = App;
30    type AppCreatorT = AppCreator;
31    type ExtraRunningState = ExtraRunningState;
32    type ExtraSharedState = ExtraSharedState;
33    type ObjectStoreT = servers::object_store::UseNone;
34
35    fn create_running_state(
36        server: &Server,
37        constellation: &Constellation,
38    ) -> anyhow::Result<Self::ExtraRunningState> {
39        let phc_ss = server.enc_key.shared_secret(&constellation.phc_enc_key);
40
41        Ok(ExtraRunningState {
42            attr_signing_key: phcrypto::attr_signing_key(&phc_ss),
43            phc_sealing_secret: phcrypto::sealing_secret(&phc_ss),
44            phc_ss,
45        })
46    }
47
48    fn create_extra_shared_state(_config: &servers::Config) -> anyhow::Result<ExtraSharedState> {
49        Ok(ExtraSharedState {})
50    }
51}
52
53pub struct ExtraSharedState {}
54
55#[derive(Clone, Debug)]
56pub struct ExtraRunningState {
57    /// Shared secret with pubhubs central
58    #[expect(dead_code)]
59    pub phc_ss: elgamal::SharedSecret,
60
61    /// Key used to sign [`Attr`]s, shared with pubhubs central.
62    ///
63    /// [`Attr`]: attr::Attr
64    pub attr_signing_key: jwt::HS256,
65
66    /// key used to seal messages to PHC
67    #[expect(dead_code)]
68    pub phc_sealing_secret: crypto::SealingKey,
69}
70
71/// Authentication server per-thread [`App`] that handles incoming requests.
72pub struct App {
73    pub base: AppBase<Server>,
74    pub attribute_types: map::Map<attr::Type>,
75    pub yivi: Option<YiviCtx>,
76    pub auth_state_secret: crypto::SealingKey,
77    pub auth_window: core::time::Duration,
78    pub attr_key_secret: Vec<u8>,
79    pub chained_sessions_ctl: Option<ChainedSessionsCtl>,
80}
81
82impl Deref for App {
83    type Target = AppBase<Server>;
84
85    fn deref(&self) -> &Self::Target {
86        &self.base
87    }
88}
89
90/// Details on the Yivi server trusted by this authentication server.
91#[derive(Debug, Clone)]
92pub struct YiviCtx {
93    pub requestor_url: url::Url,
94    pub requestor_creds: yivi::Credentials<yivi::SigningKey>,
95    pub server_creds: yivi::Credentials<yivi::VerifyingKey>,
96
97    pub chained_sessions_config: super::yivi::ChainedSessionsConfig,
98    pub card_config: super::card::CardConfig,
99}
100
101/// # Helper functions
102impl App {
103    pub fn get_yivi(&self) -> Result<&YiviCtx, api::ErrorCode> {
104        self.yivi.as_ref().ok_or_else(|| {
105            log::debug!("yivi requested, but not configured");
106            api::ErrorCode::BadRequest
107        })
108    }
109
110    /// Get [`attr::Type`] by [`handle::Handle`], returning [`None`]
111    /// when it cannot be found.
112    pub fn attr_type_from_handle<'s>(
113        &'s self,
114        attr_type_handle: &handle::Handle,
115    ) -> Option<&'s attr::Type> {
116        self.attribute_types.get(attr_type_handle)
117    }
118}
119
120/// Plaintext content of [`api::auths::AuthState`].
121#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
122pub(super) struct AuthState {
123    pub source: attr::Source,
124    pub attr_type_choices: Vec<Vec<handle::Handle>>,
125
126    /// When this request expires
127    pub exp: api::NumericDate,
128
129    /// Set when [`api::auths::AuthStartReq::yivi_chained_session`] is enabled.
130    pub yivi_chained_session: Option<ChainedSessionSetup>,
131
132    /// Under [`attr::Source::Yivi`] this will contain for each `AuthState::attr_type_choice`
133    /// a map using which the original [`attr::Type`] handle can be recovered from the yivi
134    /// attribute type identifier.
135    pub yivi_ati2at: Vec<HashMap<yivi::AttributeTypeIdentifier, handle::Handle>>,
136}
137
138/// Type of [`AuthState::yivi_chained_session`].
139#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
140pub(super) struct ChainedSessionSetup {
141    pub id: id::Id,
142    pub drip: bool,
143}
144
145impl AuthState {
146    pub fn seal(&self, key: &crypto::SealingKey) -> api::Result<api::auths::AuthState> {
147        Ok(api::auths::AuthState::new(
148            crypto::seal(&self, key, b"")
149                .map_err(|err| {
150                    log::warn!("failed to seal AuthState: {err}");
151                    api::ErrorCode::InternalError
152                })?
153                .into(),
154        ))
155    }
156
157    /// Unseals the given [`AuthState`] returning `None` of the signature is invalid
158    /// or the auth state is expired
159    pub fn unseal(sealed: &api::auths::AuthState, key: &crypto::SealingKey) -> Option<AuthState> {
160        let Ok(state): Result<AuthState, _> = crypto::unseal(&*sealed.inner, key, b"") else {
161            log::debug!("failed to unseal AuthState");
162            return None;
163        };
164
165        if state.exp < api::NumericDate::now() {
166            log::debug!("received expired AuthState");
167            return None;
168        }
169
170        Some(state)
171    }
172}
173
174impl App {
175    /// Implements [`api::server::HubPingEP`].
176    async fn handle_hub_ping(
177        app: Rc<Self>,
178        signed_req: web::Json<api::phc::hub::TicketSigned<api::server::PingReq>>,
179    ) -> api::Result<api::server::PingResp> {
180        crate::servers::AppBase::<Server>::handle_hub_ping(app, signed_req).await
181    }
182
183    /// Implements [`api::auths::WelcomeEP`].
184    fn cached_handle_welcome(app: &Self) -> api::Result<api::auths::WelcomeResp> {
185        let attr_types: HashMap<handle::Handle, attr::Type> = app
186            .attribute_types
187            .values()
188            .map(|attr_type| (attr_type.handles.preferred().clone(), attr_type.clone()))
189            .collect();
190
191        Ok(api::auths::WelcomeResp {
192            attr_types,
193            card_validity: app
194                .get_yivi()
195                .map(|yivi| yivi.card_config.valid_for.to_welcome_ep_format())
196                .ok()
197                .flatten(),
198        })
199    }
200}
201
202impl crate::servers::App<Server> for App {
203    fn configure_actix_app(self: &Rc<Self>, sc: &mut web::ServiceConfig) {
204        api::auths::WelcomeEP::caching_add_to(self, sc, App::cached_handle_welcome);
205        api::server::HubPingEP::add_to(self, sc, App::handle_hub_ping);
206
207        api::auths::AuthStartEP::add_to(self, sc, App::handle_auth_start);
208        api::auths::AuthCompleteEP::add_to(self, sc, App::handle_auth_complete);
209
210        api::auths::AttrKeysEP::add_to(self, sc, App::handle_attr_keys);
211
212        api::auths::CardEP::add_to(self, sc, App::handle_card);
213
214        api::auths::YiviWaitForResultEP::add_to(self, sc, App::handle_yivi_wait_for_result);
215        api::auths::YiviReleaseNextSessionEP::add_to(
216            self,
217            sc,
218            App::handle_yivi_release_next_session,
219        );
220
221        // NOTE: the yivi next-session endpoint does conform to our API's endpoint format, so we
222        // register it manually, and not via the `add_to` method
223        sc.app_data(web::Data::new(self.clone())).route(
224            api::auths::YIVI_NEXT_SESSION_PATH,
225            web::post().to(App::handle_yivi_next_session),
226        );
227    }
228
229    fn check_constellation(&self, constellation: &Constellation) -> bool {
230        // Dear maintainer: this destructuring is intentional, making sure that this `check_constellation` function
231        // is updated when new fields are added to the constellation
232        let Constellation {
233            inner:
234                constellation::Inner {
235                    // These fields we must check:
236                    auths_enc_key: enc_key,
237                    auths_jwt_key: jwt_key,
238
239                    // These fields we don't care about:
240                    auths_url: _,
241                    transcryptor_jwt_key: _,
242                    transcryptor_enc_key: _,
243                    transcryptor_url: _,
244                    transcryptor_master_enc_key_part: _,
245                    phc_jwt_key: _,
246                    phc_enc_key: _,
247                    phc_url: _,
248                    master_enc_key: _,
249                    global_client_url: _,
250                    ph_version: _, // (already checked)
251                },
252            id: _,
253            created_at: _,
254        } = constellation;
255
256        enc_key == self.enc_key.public_key() && **jwt_key == self.jwt_key.verifying_key()
257    }
258}
259
260/// Moves accross threads to create [`App`]s.
261#[derive(Clone)]
262pub struct AppCreator {
263    base: AppCreatorBase<Server>,
264    attribute_types: map::Map<attr::Type>,
265    yivi: Option<YiviCtx>,
266    auth_state_secret: crypto::SealingKey,
267    auth_window: core::time::Duration,
268    attr_key_secret: Vec<u8>,
269    chained_sessions_ctl: Option<ChainedSessionsCtl>,
270}
271
272impl Deref for AppCreator {
273    type Target = AppCreatorBase<Server>;
274
275    #[inline]
276    fn deref(&self) -> &Self::Target {
277        &self.base
278    }
279}
280
281impl DerefMut for AppCreator {
282    #[inline]
283    fn deref_mut(&mut self) -> &mut Self::Target {
284        &mut self.base
285    }
286}
287
288impl crate::servers::AppCreator<Server> for AppCreator {
289    type ContextT = ();
290
291    fn new(config: &servers::Config) -> anyhow::Result<Self> {
292        let base = AppCreatorBase::<Server>::new(config)?;
293
294        let xconf = &config.auths.as_ref().unwrap();
295
296        let mut attribute_types: crate::map::Map<attr::Type> = Default::default();
297
298        for attr_type in xconf.attribute_types.iter() {
299            if let Some(handle_or_id) = attribute_types.insert_new(attr_type.clone()) {
300                anyhow::bail!("two attribute types are known as {handle_or_id}");
301            }
302        }
303
304        let yivi: Option<YiviCtx> = xconf.yivi.as_ref().map(|cfg| YiviCtx {
305            requestor_url: cfg.requestor_url.as_ref().clone(),
306            requestor_creds: cfg.requestor_creds.clone(),
307            server_creds: cfg.server_creds(),
308            chained_sessions_config: cfg.chained_sessions.clone(),
309            card_config: cfg.card.clone(),
310        });
311
312        let auth_state_secret: crypto::SealingKey = base
313            .enc_key
314            .derive_sealing_key(sha2::Sha256::new(), "pubhubs-auths-auth-state");
315
316        let auth_window = xconf.auth_window;
317
318        let attr_key_secret = xconf
319            .attr_key_secret
320            .as_ref()
321            .expect("attr_key_secret not generated")
322            .to_vec();
323
324        let chained_sessions_ctl = yivi
325            .as_ref()
326            .map(|yivi_ctx| ChainedSessionsCtl::new(yivi_ctx.clone()));
327
328        Ok(Self {
329            base,
330            attribute_types,
331            yivi,
332            auth_state_secret,
333            auth_window,
334            attr_key_secret,
335            chained_sessions_ctl,
336        })
337    }
338
339    fn into_app(
340        self,
341        handle: &Handle<Server>,
342        _context: &Self::ContextT,
343        generation: usize,
344    ) -> App {
345        App {
346            base: AppBase::new(self.base, handle, generation),
347            attribute_types: self.attribute_types,
348            yivi: self.yivi,
349            auth_state_secret: self.auth_state_secret,
350            auth_window: self.auth_window,
351            attr_key_secret: self.attr_key_secret,
352            chained_sessions_ctl: self.chained_sessions_ctl,
353        }
354    }
355}