Skip to main content

pubhubs/servers/phc/
server.rs

1//! Basic server [`Details`]: [`Server`], [`App`], etc.
2use std::cell::{Cell, RefCell};
3use std::collections::HashMap;
4use std::convert::Infallible;
5use std::ops::{Deref, DerefMut};
6use std::rc::Rc;
7
8use actix_web::web;
9use sha2::digest::Digest as _;
10
11use crate::api::{self, ApiResultExt as _, EndpointDetails as _, NoPayload};
12use crate::client;
13use crate::common::secret::DigestibleSecret as _;
14use crate::handle;
15use crate::id;
16use crate::misc::crypto;
17use crate::misc::jwt;
18use crate::misc::serde_ext;
19use crate::misc::time_ext;
20use crate::phcrypto;
21use crate::servers::{
22    self, AppBase, AppCreatorBase, Constellation, DiscoverVerdict, Handle, Server as _,
23    constellation,
24};
25
26use crate::{
27    common::{elgamal, kem},
28    hub,
29};
30
31/// PubHubs Central server
32pub type Server = servers::ServerImpl<Details>;
33
34pub struct Details;
35impl servers::Details for Details {
36    const NAME: servers::Name = servers::Name::PubhubsCentral;
37    type AppT = App;
38    type AppCreatorT = AppCreator;
39    type ExtraRunningState = ExtraRunningState;
40    type RunningStateSeed = RunningStateSeed;
41    type ExtraSharedState = ExtraSharedState;
42    type ExtraServerState = ();
43    type ObjectStoreT = servers::object_store::DefaultObjectStore;
44
45    fn create_running_state(
46        _server: &Server,
47        _constellation: &Constellation,
48        seed: &RunningStateSeed,
49    ) -> anyhow::Result<Self::ExtraRunningState> {
50        // The hybrid post-quantum shared secrets PHC encapsulated for its peers during `discover`.
51        let t_ss = seed.t_ss.clone();
52        let auths_ss = seed.auths_ss.clone();
53        Ok(ExtraRunningState {
54            attr_signing_key: phcrypto::attr_signing_key(&auths_ss),
55            t_sealing_secret: phcrypto::sealing_secret(&t_ss),
56            auths_sealing_secret: phcrypto::sealing_secret(&auths_ss),
57            master_enc_key: seed.master_enc_key.clone(),
58            t_ss,
59            auths_ss,
60        })
61    }
62
63    fn create_extra_shared_state(config: &servers::Config) -> anyhow::Result<ExtraSharedState> {
64        let mut hubs: crate::map::Map<hub::BasicInfo> = Default::default();
65
66        for basic_hub_info in config.phc.as_ref().unwrap().hubs.iter() {
67            if let Some(hub_or_id) = hubs.insert_new(basic_hub_info.clone().into()) {
68                anyhow::bail!("two hubs are known as {hub_or_id}");
69            }
70        }
71
72        Ok(ExtraSharedState { hubs })
73    }
74
75    fn create_extra_server_state(_config: &servers::Config) -> anyhow::Result<()> {
76        Ok(())
77    }
78}
79
80pub struct ExtraSharedState {
81    /// Immutable hub registry, built once from config and shared by all apps.
82    pub hubs: crate::map::Map<hub::BasicInfo>,
83}
84
85pub struct App {
86    pub base: AppBase<Server>,
87    pub transcryptor_url: url::Url,
88    pub auths_url: url::Url,
89    pub global_client_url: url::Url,
90    pub master_enc_key_part: elgamal::PrivateKey,
91    pub attr_id_secret: Box<[u8]>,
92    pub auth_token_secret: crypto::SealingKey,
93    pub auth_token_validity: core::time::Duration,
94    pub auth_token_max_idle: core::time::Duration,
95    pub auth_session_max_duration: core::time::Duration,
96    pub pp_nonce_secret: crypto::SealingKey,
97    pub pp_nonce_validity: core::time::Duration,
98    pub user_object_hmac_secret: Box<[u8]>,
99    pub quota: api::phc::user::Quota,
100    pub card_pseud_validity: core::time::Duration,
101
102    /// channel for sending messages between apps
103    pub broadcast: tokio::sync::broadcast::Sender<InterAppMsg>,
104
105    pub cached_hub_info: std::cell::RefCell<api::CachedResponse<api::phc::user::CachedHubInfoEP>>,
106    pub hub_cache_config: HubCacheConfig,
107}
108
109impl Deref for App {
110    type Target = AppBase<Server>;
111
112    fn deref(&self) -> &Self::Target {
113        &self.base
114    }
115}
116
117#[derive(Clone, Debug)]
118pub struct ExtraRunningState {
119    /// Hybrid post-quantum shared secret with the transcryptor.
120    pub(super) t_ss: kem::SharedSecret,
121
122    /// Hybrid post-quantum shared secret with the authentication server.
123    pub(super) auths_ss: kem::SharedSecret,
124
125    /// Key used to sign [`Attr`]s, shared with the authentication server
126    ///
127    /// [`Attr`]: crate::attr::Attr
128    pub(super) attr_signing_key: jwt::HS256,
129
130    /// Key used to (un)seal messages to and from the transcryptor
131    pub(super) t_sealing_secret: crypto::SealingKey,
132
133    /// Key used to (un)seal messages to and from the authentication server
134    #[expect(dead_code)]
135    pub(super) auths_sealing_secret: crypto::SealingKey,
136
137    /// The master encryption key `x_T x_PHC B`, derived from the transcryptor's sealed master key
138    /// part.  `None` until PHC has been able to unseal it (e.g. while the transcryptor has not yet
139    /// published a sealed part).  Held here because it is no longer part of the (public)
140    /// constellation.
141    pub(super) master_enc_key: Option<elgamal::PublicKey>,
142}
143
144/// Data threaded out of PHC's [`discover`](crate::servers::App::discover) into
145/// [`create_running_state`](crate::servers::Details::create_running_state): the hybrid KEM shared
146/// secrets PHC encapsulated for its peers, plus the master encryption key it derived from the
147/// transcryptor's sealed master key part.
148pub struct RunningStateSeed {
149    pub(super) t_ss: kem::SharedSecret,
150    pub(super) auths_ss: kem::SharedSecret,
151    pub(super) master_enc_key: Option<elgamal::PublicKey>,
152}
153
154impl crate::servers::App<Server> for App {
155    fn configure_actix_app(self: &Rc<Self>, sc: &mut web::ServiceConfig) {
156        api::phc::hub::TicketEP::add_to(self, sc, App::handle_hub_ticket);
157        api::server::HubPingEP::add_to(self, sc, App::handle_hub_ping);
158
159        api::phc::user::WelcomeEP::caching_add_to(self, sc, App::cached_handle_user_welcome);
160        api::phc::user::EnterEP::add_to(self, sc, App::handle_user_enter);
161        api::phc::user::RefreshEP::add_to(self, sc, App::handle_user_refresh);
162        api::phc::user::StateEP::add_to(self, sc, App::handle_user_state);
163
164        api::phc::user::NewObjectEP::add_to(self, sc, App::handle_user_new_object);
165        api::phc::user::OverwriteObjectEP::add_to(self, sc, App::handle_user_overwrite_object);
166        api::phc::user::GetObjectEP::add_to(self, sc, App::handle_user_get_object);
167
168        api::phc::user::PppEP::add_to(self, sc, App::handle_user_ppp);
169        api::phc::user::HhppEP::add_to(self, sc, App::handle_user_hhpp);
170
171        api::phc::user::CardPseudEP::add_to(self, sc, App::handle_user_card_pseud);
172
173        // We add the following endpoint manually, for efficiency
174        sc.app_data(web::Data::new(self.clone())).route(
175            api::phc::user::CachedHubInfoEP::PATH,
176            web::method(api::phc::user::CachedHubInfoEP::METHOD).to(App::handle_cached_hub_info),
177        );
178    }
179
180    fn check_constellation(&self, _constellation: &Constellation) -> bool {
181        panic!("PHC creates the constellation; it has no need to check it")
182    }
183
184    async fn discover(
185        self: &Rc<Self>,
186        _phc_di: api::DiscoveryInfoResp,
187    ) -> api::Result<DiscoverVerdict<RunningStateSeed>> {
188        let (tdi_res, asdi_res) = tokio::join!(
189            self.discovery_info_of(servers::Name::Transcryptor, &self.transcryptor_url),
190            self.discovery_info_of(servers::Name::AuthenticationServer, &self.auths_url)
191        );
192
193        let tdi = tdi_res?;
194        let asdi = asdi_res?;
195
196        for (odi, other_server_name) in [
197            (&tdi, servers::Name::Transcryptor),
198            (&asdi, servers::Name::AuthenticationServer),
199        ] {
200            if let Some(ref other_version) = odi.version
201                && let Some(my_version) = &self.version
202            {
203                let other_version = crate::servers::version::to_semver(other_version).map_err(|err| {
204                    log::error!(
205                        "{my_server_name}: could not parse semantic version returned by {other_server_name}: {other_version}: {err}",
206                        my_server_name = Server::NAME
207                    );
208                    api::ErrorCode::InternalError
209                })?;
210
211                let my_version = crate::servers::version::to_semver(my_version).map_err(|err| {
212                    log::error!(
213                        "{my_server_name}: could not parse my semantic version {my_version}: {err}",
214                        my_server_name = Server::NAME
215                    );
216                    api::ErrorCode::InternalError
217                })?;
218
219                if my_version < other_version {
220                    log::warn!(
221                        "{my_server_name}: {other_server_name}'s version ({other_version}) > my version ({my_version})",
222                        my_server_name = Server::NAME,
223                    );
224                    return Ok(DiscoverVerdict::BinaryOutdated);
225                }
226            } else {
227                log::warn!(
228                    "{my_server_name}: not checking my version ({my_version}) against {other_server_name}'s version ({other_version})",
229                    my_server_name = Server::NAME,
230                    my_version = crate::servers::version::VERSION,
231                    other_version = odi.version.as_deref().unwrap_or("n/a")
232                );
233            }
234        }
235
236        let current_rs = self.running_state.as_ref();
237
238        let (transcryptor_encap_key_id, transcryptor_ss_encap, t_ss) = Self::encap_or_reuse(
239            servers::Name::Transcryptor,
240            tdi.encap_key.as_ref(),
241            current_rs.map(|rs| {
242                (
243                    &rs.constellation.transcryptor_encap_key_id,
244                    &rs.constellation.transcryptor_ss_encap,
245                    &rs.t_ss,
246                )
247            }),
248        )?;
249
250        let (auths_encap_key_id, auths_ss_encap, auths_ss) = Self::encap_or_reuse(
251            servers::Name::AuthenticationServer,
252            asdi.encap_key.as_ref(),
253            current_rs.map(|rs| {
254                (
255                    &rs.constellation.auths_encap_key_id,
256                    &rs.constellation.auths_ss_encap,
257                    &rs.auths_ss,
258                )
259            }),
260        )?;
261
262        let new_constellation_inner = constellation::Inner {
263            // the transcryptor publishes the hash of `x_T B` in its discovery info
264            transcryptor_master_enc_key_part_hash: tdi.master_enc_key_part_hash.ok_or_else(
265                || {
266                    log::error!("transcryptor's discovery info has no master_enc_key_part_hash");
267                    api::ErrorCode::InternalError
268                },
269            )?,
270            phc_master_enc_key_part_hash: phcrypto::master_enc_key_part_hash(
271                self.master_enc_key_part.public_key(),
272            ),
273            global_client_url: self.global_client_url.clone(),
274            phc_url: self.phc_url.clone(),
275            // PHC's ed25519 public key (the `ed` half), so pre-hybrid hubs can verify the EdDSA HHPP.
276            phc_jwt_key: crate::misc::serde_ext::ByteArray::from(
277                self.shared.signing_key.verifying_key().ed25519_bytes(),
278            )
279            .into(),
280            phc_verifying_key: self.shared.verifying_key_bytes.clone(),
281            transcryptor_url: self.transcryptor_url.clone(),
282            // cloned (not moved) so `tdi` stays whole for `master_enc_key_from_sealed_part` below
283            transcryptor_verifying_key: tdi.verifying_key.clone(),
284            transcryptor_encap_key_id,
285            transcryptor_ss_encap,
286            auths_url: self.auths_url.clone(),
287            auths_verifying_key: asdi.verifying_key.clone(),
288            auths_encap_key_id,
289            auths_ss_encap,
290            ph_version: self.version.clone(),
291        };
292
293        let new_constellation_id = constellation::Inner::derive_id(&new_constellation_inner);
294
295        // PHC keeps the master encryption key once derived: it never changes in a real deployment
296        // (changing it would invalidate every polymorphic pseudonym).  The exception is an ephemeral
297        // local test setup that regenerates the key parts; detect that via their hashes in the
298        // constellation, warn loudly, and drop the now-stale key so it is re-derived.
299        let prior_master_enc_key = 'prior: {
300            let Some(rs) = self.running_state.as_ref() else {
301                break 'prior None;
302            };
303            if rs.constellation.transcryptor_master_enc_key_part_hash
304                != new_constellation_inner.transcryptor_master_enc_key_part_hash
305                || rs.constellation.phc_master_enc_key_part_hash
306                    != new_constellation_inner.phc_master_enc_key_part_hash
307            {
308                log::warn!(
309                    "a master encryption key part changed; this invalidates all existing \
310                     polymorphic pseudonyms and should only happen in an ephemeral test setup"
311                );
312                break 'prior None;
313            }
314            rs.master_enc_key.clone()
315        };
316
317        // Otherwise derive it from the transcryptor's sealed master key part — but only once the
318        // transcryptor has adopted the constellation PHC is computing, so the part is sealed under
319        // the shared secret PHC currently holds and the unseal is guaranteed to succeed.
320        let master_enc_key = match prior_master_enc_key {
321            existing @ Some(_) => existing,
322            None => self.master_enc_key_from_sealed_part(&tdi, &t_ss, new_constellation_id)?,
323        };
324
325        if self.running_state.is_none()
326            || self.running_state.as_ref().unwrap().constellation.inner != new_constellation_inner
327        {
328            if let Some(ref running_state) = self.running_state {
329                log::info!(
330                    "Detected change in constellation {} -> {}",
331                    running_state.constellation.id,
332                    new_constellation_id
333                );
334            } else {
335                log::info!("Computed constellation {new_constellation_id}");
336            }
337
338            return Ok(DiscoverVerdict::ConstellationOutdated {
339                new_constellation: Box::new(Constellation {
340                    id: new_constellation_id,
341                    created_at: api::NumericDate::now(),
342                    inner: new_constellation_inner,
343                }),
344                seed: RunningStateSeed {
345                    t_ss,
346                    auths_ss,
347                    master_enc_key,
348                },
349            });
350        }
351
352        let running_state = self.running_state.as_ref().expect(
353            "running_state should be set here, but isn't (the block above returns when it is None)",
354        );
355
356        // Our constellation is unchanged.  If PHC has only now been able to derive the master
357        // encryption key (or it changed), rebuild the running state without changing the
358        // constellation, so the transcryptor and authentication server are not restarted.
359        if running_state.master_enc_key != master_enc_key {
360            log::info!("master encryption key (re)derived; updating running state only");
361            return Ok(DiscoverVerdict::RunningStateOutdated {
362                seed: RunningStateSeed {
363                    t_ss,
364                    auths_ss,
365                    master_enc_key,
366                },
367            });
368        }
369
370        let constellation = &running_state.constellation;
371
372        log::info!("My own constellation is up-to-date");
373
374        // Check whether the other servers' constellations are up-to-date
375
376        let mut js = tokio::task::JoinSet::new();
377
378        if tdi
379            .constellation_or_id
380            .as_ref()
381            .is_some_and(|c| *c.id() != constellation.id)
382        {
383            // transcryptor's constellation is out of date; invoke discovery
384            log::info!(
385                "{phc}: {t}'s constellation is out of date - invoking its discovery..",
386                phc = servers::Name::PubhubsCentral,
387                t = servers::Name::Transcryptor
388            );
389            let url = self.transcryptor_url.clone();
390            js.spawn_local(
391                self.client
392                    .query::<api::DiscoveryRun>(&url, NoPayload)
393                    .into_future(),
394            );
395        }
396
397        if asdi
398            .constellation_or_id
399            .as_ref()
400            .is_some_and(|c| *c.id() != constellation.id)
401        {
402            // authentication server's constellation is out of date; invoke discovery
403            log::info!(
404                "{phc}: {auths}'s constellation is out of date - invoking its discovery..",
405                phc = servers::Name::PubhubsCentral,
406                auths = servers::Name::AuthenticationServer
407            );
408            let url = self.auths_url.clone();
409            js.spawn_local(
410                self.client
411                    .query::<api::DiscoveryRun>(&url, NoPayload)
412                    .into_future(),
413            );
414        }
415
416        let result_maybe = js.join_next().await;
417
418        // Whatever the result, we don't want to abort the the discovery run calls
419        // prematurely when `js` is dropped.
420        js.detach_all();
421
422        match result_maybe {
423            // joinset was empty, no discovery was ran
424            None => {
425                if tdi.constellation_or_id.is_some() && asdi.constellation_or_id.is_some() {
426                    // All servers share our constellation, so the transcryptor has adopted it and
427                    // published a sealed master key part for it — meaning we must have derived the
428                    // master encryption key by now.
429                    assert!(
430                        running_state.master_enc_key.is_some(),
431                        "all servers are on our constellation, but the master encryption key was not derived"
432                    );
433                    log::info!("Constellation of all servers up to date!");
434                    Ok(DiscoverVerdict::Alright)
435                } else {
436                    log::info!("Waiting for the other servers to update their constellation.");
437                    Err(api::ErrorCode::PleaseRetry)
438                }
439            }
440            // a task ended irregularly (panicked, joined,...)
441            Some(Err(join_err)) => {
442                log::error!("discovery run task joined unexpectedly: {join_err}");
443                Err(api::ErrorCode::InternalError)
444            }
445            // we got a result from one of the tasks..
446            Some(Ok(res)) => {
447                match res.retryable() {
448                    Ok(_) => {
449                        // the discovery task was completed succesfully, or made some progress,
450                        // or we got a retryable error.
451                        // In all these cases the caller should try again.
452                        Err(api::ErrorCode::PleaseRetry)
453                    }
454                    Err(err) => {
455                        log::error!("Failed to run discovery of other server: {err}",);
456                        Err(api::ErrorCode::InternalError)
457                    }
458                }
459            }
460        }
461    }
462
463    fn master_enc_key_part(&self) -> Option<&elgamal::PrivateKey> {
464        Some(&self.master_enc_key_part)
465    }
466
467    async fn local_task(weak: std::rc::Weak<Self>) {
468        use tokio::sync::broadcast::error::RecvError;
469
470        let mut receiver: tokio::sync::broadcast::Receiver<InterAppMsg>;
471
472        {
473            let Some(app) = weak.upgrade() else {
474                log::debug!("App is gone before local task started");
475                return;
476            };
477
478            receiver = app.broadcast.subscribe();
479        }
480
481        loop {
482            let recv_result = receiver.recv().await;
483            let Ok(msg) = recv_result else {
484                match recv_result.unwrap_err() {
485                    RecvError::Closed => {
486                        return;
487                    }
488                    RecvError::Lagged(skipped) => {
489                        log::error!(
490                            "PHC local task on {:?} is lagging behind, \
491                            and has skipped processing {skipped} messages!",
492                            std::thread::current().id()
493                        );
494                        continue;
495                    }
496                }
497            };
498
499            let Some(app) = weak.upgrade() else {
500                log::warn!("Inter app message dropped because app is gone");
501                return;
502            };
503
504            match msg {
505                InterAppMsg::UpdatedHubInfo(cached_hub_info) => {
506                    app.cached_hub_info.replace(cached_hub_info);
507                }
508            }
509        }
510    }
511
512    async fn global_task(app: Rc<Self>) -> anyhow::Result<Infallible> {
513        let localset = tokio::task::LocalSet::new();
514        let _hcu = HubCacheUpdater::new(app, &localset);
515
516        localset.await;
517
518        log::error!("bug: PHC global task  exits prematurely");
519        anyhow::bail!("bug: PHC global task exits prematurely")
520    }
521}
522
523/// Configures [`HubCacheUpdater`].
524#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
525pub struct HubCacheConfig {
526    /// Query [`api::hub::InfoEP`] this often
527    #[serde(with = "time_ext::human_duration")]
528    #[serde(default = "default_hub_cache_request_interval")]
529    request_interval: core::time::Duration,
530
531    /// The request to [`api::hub::InfoEP`] times out after this amount of time
532    #[serde(with = "time_ext::human_duration")]
533    #[serde(default = "default_hub_cache_request_timeout")]
534    request_timeout: core::time::Duration,
535
536    /// Push a new version of the cache to [`App`] instances this often
537    #[serde(with = "time_ext::human_duration")]
538    #[serde(default = "default_hub_cache_push_interval")]
539    push_interval: core::time::Duration,
540}
541
542fn default_hub_cache_request_interval() -> core::time::Duration {
543    core::time::Duration::from_secs(60)
544}
545
546fn default_hub_cache_request_timeout() -> core::time::Duration {
547    core::time::Duration::from_secs(10)
548}
549
550fn default_hub_cache_push_interval() -> core::time::Duration {
551    core::time::Duration::from_secs(5)
552}
553
554impl Default for HubCacheConfig {
555    fn default() -> Self {
556        serde_ext::default_object()
557    }
558}
559
560struct HubCacheUpdater {
561    app: Rc<App>,
562    hub_info: RefCell<HashMap<handle::Handle, Option<api::hub::InfoResp>>>,
563    unpublished_updates: Cell<bool>,
564}
565
566impl HubCacheUpdater {
567    fn new(app: Rc<App>, localset: &tokio::task::LocalSet) -> Rc<Self> {
568        let hcu = Rc::new(Self {
569            app: app.clone(),
570            hub_info: RefCell::new(Default::default()),
571            unpublished_updates: Cell::new(false),
572        });
573
574        for basic_hub_info in app.shared.hubs.values() {
575            localset.spawn_local(hcu.clone().handle_hub(basic_hub_info.clone()));
576        }
577
578        localset.spawn_local(hcu.clone().push_updates());
579
580        hcu
581    }
582
583    async fn handle_hub(self: Rc<Self>, basic_hub_info: hub::BasicInfo) {
584        let hub_handle = basic_hub_info.handles.preferred();
585
586        self.hub_info.borrow_mut().insert(hub_handle.clone(), None);
587
588        let mut interval = tokio::time::interval(self.app.hub_cache_config.request_interval);
589        let mut failure_since: Option<std::time::SystemTime> = None;
590
591        loop {
592            interval.tick().await;
593
594            let hir = self
595                .app
596                .client
597                .query::<api::hub::InfoEP>(&basic_hub_info.url, api::NoPayload)
598                .quiet()
599                .timeout(self.app.hub_cache_config.request_timeout)
600                .await;
601
602            let Ok(hi) = hir else {
603                if failure_since.is_none() {
604                    log::warn!("hub {hub_handle} not reachable");
605                    failure_since = Some(std::time::SystemTime::now());
606                }
607
608                continue;
609            };
610
611            if let Some(time) = failure_since.take() {
612                log::info!(
613                    "hub {hub_handle} is reachable again;  it was unreachable since {}.)",
614                    time_ext::format_time(time)
615                );
616            }
617
618            use std::collections::hash_map::Entry;
619
620            {
621                let mut hub_info = self.hub_info.borrow_mut();
622
623                let Entry::Occupied(mut oe) = hub_info.entry(hub_handle.clone()) else {
624                    panic!("bug: hub info cache entry for hub {hub_handle} disappeared");
625                };
626
627                if oe.get().as_ref() == Some(&hi) {
628                    continue;
629                }
630
631                oe.insert(Some(hi));
632            }
633
634            if !self.unpublished_updates.replace(true) {
635                log::trace!("new hub info on {hub_handle} will be pushed to app soon");
636            }
637        }
638    }
639
640    async fn push_updates(self: Rc<Self>) {
641        let mut interval = tokio::time::interval(self.app.hub_cache_config.push_interval);
642
643        loop {
644            interval.tick().await;
645
646            if !self.unpublished_updates.get() {
647                continue;
648            }
649
650            let chir = api::phc::user::CachedHubInfoResp {
651                hubs: self.hub_info.borrow().clone(),
652            };
653
654            let cr = api::Responder(Ok(chir)).into_cached();
655
656            log::trace!("pushing updated cached hub info to apps");
657            if self
658                .app
659                .broadcast
660                .send(InterAppMsg::UpdatedHubInfo(cr))
661                .is_err()
662            {
663                log::error!("failed to internally broadcast updated hub information");
664                continue;
665            };
666
667            self.unpublished_updates.set(false);
668        }
669    }
670}
671
672impl App {
673    /// Obtains and checks [`api::DiscoveryInfoResp`] from the given server
674    async fn discovery_info_of(
675        &self,
676        name: servers::Name,
677        url: &url::Url,
678    ) -> api::Result<api::DiscoveryInfoResp> {
679        let tdi = self
680            .client
681            .query::<api::DiscoveryInfo>(url, NoPayload)
682            .await
683            .into_server_result()?;
684
685        client::discovery::DiscoveryInfoCheck {
686            phc_url: &self.phc_url,
687            name,
688            self_check_code: None,
689            constellation: None,
690        }
691        .check(tdi, url)
692    }
693
694    /// Derives the master encryption key `x_T x_PHC B` from the transcryptor's sealed master key
695    /// part in its discovery info — but only once the transcryptor has adopted the constellation
696    /// `constellation_id` PHC is on.  Only then is its part sealed under the shared secret PHC
697    /// currently holds, so the unseal is guaranteed to succeed.
698    ///
699    /// Returns `Ok(None)` only when the transcryptor has not adopted our constellation yet (e.g.
700    /// right after PHC re-encapsulated a fresh shared secret).  Returns `Err` on a genuine
701    /// inconsistency that should never happen: the transcryptor *has* adopted our constellation —
702    /// so it has a running state and must publish a sealed part — yet that part is absent, fails to
703    /// open, or does not match its published hash.
704    fn master_enc_key_from_sealed_part(
705        &self,
706        tdi: &api::DiscoveryInfoResp,
707        t_ss: &kem::SharedSecret,
708        constellation_id: id::Id,
709    ) -> api::Result<Option<elgamal::PublicKey>> {
710        if tdi.constellation_or_id.as_ref().map(|c| *c.id()) != Some(constellation_id) {
711            return Ok(None);
712        }
713
714        let Some(sealed) = tdi.master_enc_key_part_sealed.clone() else {
715            log::error!(
716                "transcryptor adopted our constellation but published no sealed master key part"
717            );
718            return Err(api::ErrorCode::InternalError);
719        };
720
721        let api::MasterEncKeyPart(transcryptor_part) =
722            sealed.open(&phcrypto::sealing_secret(t_ss)).map_err(|_| {
723                log::error!(
724                    "could not open the transcryptor's sealed master encryption key part, even \
725                     though it has adopted our constellation"
726                );
727                api::ErrorCode::InternalError
728            })?;
729
730        if tdi.master_enc_key_part_hash
731            != Some(phcrypto::master_enc_key_part_hash(&transcryptor_part))
732        {
733            log::error!(
734                "transcryptor's sealed master_enc_key_part does not match its published hash"
735            );
736            return Err(api::ErrorCode::InternalError);
737        }
738
739        Ok(Some(phcrypto::combine_master_enc_key_parts(
740            &transcryptor_part,
741            &self.master_enc_key_part,
742        )))
743    }
744
745    /// Encapsulate a fresh shared secret against the encapsulation key in `peer`'s discovery info
746    /// (error if it omits one), reusing `prior`'s ciphertext and secret when `peer`'s encapsulation
747    /// key is unchanged.
748    fn encap_or_reuse(
749        peer: servers::Name,
750        peer_encap_key: Option<&kem::EncapKeyBytes>,
751        prior: Option<(&id::Id, &kem::CiphertextBytes, &kem::SharedSecret)>,
752    ) -> api::Result<(id::Id, kem::CiphertextBytes, kem::SharedSecret)> {
753        let peer_encap_key = peer_encap_key.ok_or_else(|| {
754            log::error!("{peer}'s discovery info has no encapsulation key");
755            api::ErrorCode::InternalError
756        })?;
757
758        let new_id = peer_encap_key.id();
759
760        if let Some((prev_id, prev_ct, prev_ss)) = prior
761            && *prev_id == new_id
762        {
763            return Ok((*prev_id, prev_ct.clone(), prev_ss.clone()));
764        }
765
766        let ek = peer_encap_key.decode().map_err(|_| {
767            log::error!("failed to decode {peer}'s encap_key");
768            api::ErrorCode::InternalError
769        })?;
770        let (ct, ss) = ek.encap().map_err(|_| {
771            log::error!("failed to encapsulate for {peer}");
772            api::ErrorCode::InternalError
773        })?;
774        Ok((new_id, ct, ss))
775    }
776}
777
778#[derive(Clone)]
779pub struct AppCreator {
780    pub base: AppCreatorBase<Server>,
781    pub transcryptor_url: url::Url,
782    pub auths_url: url::Url,
783    pub global_client_url: url::Url,
784    pub master_enc_key_part: elgamal::PrivateKey,
785    pub attr_id_secret: Box<[u8]>,
786    pub auth_token_secret: crypto::SealingKey,
787    pub auth_token_validity: core::time::Duration,
788    pub auth_token_max_idle: core::time::Duration,
789    pub auth_session_max_duration: core::time::Duration,
790    pub pp_nonce_secret: crypto::SealingKey,
791    pub pp_nonce_validity: core::time::Duration,
792    pub user_object_hmac_secret: Box<[u8]>,
793    pub quota: api::phc::user::Quota,
794    pub card_pseud_validity: core::time::Duration,
795    pub hub_cache_config: HubCacheConfig,
796}
797
798impl Deref for AppCreator {
799    type Target = AppCreatorBase<Server>;
800
801    fn deref(&self) -> &Self::Target {
802        &self.base
803    }
804}
805
806impl DerefMut for AppCreator {
807    fn deref_mut(&mut self) -> &mut Self::Target {
808        &mut self.base
809    }
810}
811
812#[derive(Clone)]
813pub struct AppCreatorContext {
814    broadcast: tokio::sync::broadcast::Sender<InterAppMsg>,
815}
816
817impl Default for AppCreatorContext {
818    fn default() -> Self {
819        Self {
820            broadcast: tokio::sync::broadcast::Sender::<InterAppMsg>::new(10),
821        }
822    }
823}
824
825impl crate::servers::AppCreator<Server> for AppCreator {
826    type ContextT = AppCreatorContext;
827
828    fn into_app(self, handle: &Handle<Server>, context: &Self::ContextT, generation: usize) -> App {
829        App {
830            base: AppBase::new(self.base, handle, generation),
831            transcryptor_url: self.transcryptor_url,
832            auths_url: self.auths_url,
833            global_client_url: self.global_client_url,
834            master_enc_key_part: self.master_enc_key_part,
835            attr_id_secret: self.attr_id_secret,
836            auth_token_secret: self.auth_token_secret,
837            auth_token_validity: self.auth_token_validity,
838            auth_token_max_idle: self.auth_token_max_idle,
839            auth_session_max_duration: self.auth_session_max_duration,
840            pp_nonce_secret: self.pp_nonce_secret,
841            pp_nonce_validity: self.pp_nonce_validity,
842            user_object_hmac_secret: self.user_object_hmac_secret,
843            quota: self.quota,
844            card_pseud_validity: self.card_pseud_validity,
845            broadcast: context.broadcast.clone(),
846            // cached_hub_info will be set later
847            cached_hub_info: std::cell::RefCell::new(
848                api::Responder(Err(api::ErrorCode::PleaseRetry)).into_cached(),
849            ),
850            hub_cache_config: self.hub_cache_config,
851        }
852    }
853
854    fn new(config: &servers::Config) -> anyhow::Result<Self> {
855        let xconf = &config.phc.as_ref().unwrap();
856
857        let master_enc_key_part: elgamal::PrivateKey = xconf
858            .master_enc_key_part
859            .clone()
860            .expect("master_enc_key_part not generated");
861
862        let base = AppCreatorBase::<Server>::new(config)?;
863
864        let enc_key: &[u8] = &base.enc_key;
865        let auth_token_secret: crypto::SealingKey =
866            enc_key.derive_sealing_key(sha2::Sha256::new(), "pubhubs-phc-auth-token-secret");
867
868        let pp_nonce_secret: crypto::SealingKey =
869            enc_key.derive_sealing_key(sha2::Sha256::new(), "pubhubs-pp-nonce-secret");
870
871        Ok(Self {
872            base,
873            transcryptor_url: xconf.transcryptor_url.as_ref().clone(),
874            auths_url: xconf.auths_url.as_ref().clone(),
875            global_client_url: xconf.global_client_url.as_ref().clone(),
876            master_enc_key_part,
877            attr_id_secret: <serde_bytes::ByteBuf as Clone>::clone(
878                xconf
879                    .attr_id_secret
880                    .as_ref()
881                    .expect("attr_id_secret was not initialized"),
882            )
883            .into_vec()
884            .into_boxed_slice(),
885            auth_token_secret,
886            auth_token_validity: xconf.auth_token_validity,
887            auth_token_max_idle: xconf.auth_token_max_idle,
888            auth_session_max_duration: xconf.auth_session_max_duration,
889            pp_nonce_secret,
890            pp_nonce_validity: xconf.pp_nonce_validity,
891            user_object_hmac_secret: <serde_bytes::ByteBuf as Clone>::clone(
892                xconf
893                    .user_object_hmac_secret
894                    .as_ref()
895                    .expect("user_object_hmac_secret was not initialized"),
896            )
897            .into_vec()
898            .into_boxed_slice(),
899            quota: xconf.user_quota.clone(),
900            card_pseud_validity: xconf.card_pseud_validity,
901            hub_cache_config: xconf.hub_cache.clone(),
902        })
903    }
904}
905
906/// Message sent between [`App`] instances accross threads
907#[derive(Clone, Debug)]
908pub(crate) enum InterAppMsg {
909    UpdatedHubInfo(api::CachedResponse<api::phc::user::CachedHubInfoEP>),
910}