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