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