Skip to main content

pubhubs/servers/transcryptor/
server.rs

1use std::ops::{Deref, DerefMut};
2use std::rc::Rc;
3
4use actix_web::web;
5
6use crate::common::{elgamal, kem};
7use crate::misc::crypto;
8use crate::misc::serde_ext::bytes_wrapper::B64UU;
9use crate::phcrypto;
10use crate::{
11    api::{self, EndpointDetails as _},
12    servers::{
13        self, AppBase, AppCreatorBase, Constellation, DiscoverVerdict, Handle, Server as _,
14        constellation,
15    },
16};
17
18use api::tr::*;
19
20/// Transcryptor
21pub type Server = servers::ServerImpl<Details>;
22
23pub struct Details;
24impl servers::Details for Details {
25    const NAME: servers::Name = servers::Name::Transcryptor;
26    type AppT = App;
27    type AppCreatorT = AppCreator;
28    type ExtraRunningState = ExtraRunningState;
29    type RunningStateSeed = ();
30    type ExtraSharedState = ExtraSharedState;
31    type ExtraServerState = ExtraServerState;
32    type ObjectStoreT = servers::object_store::UseNone;
33
34    fn create_running_state(
35        server: &Server,
36        constellation: &Constellation,
37        _seed: &(),
38    ) -> anyhow::Result<Self::ExtraRunningState> {
39        let phc_ss = server
40            .extra()
41            .decap_key
42            .decap(&constellation.transcryptor_ss_encap)
43            .map_err(|_| anyhow::anyhow!("decapsulating shared secret from PHC failed"))?;
44
45        Ok(ExtraRunningState {
46            phc_sealing_secret: phcrypto::sealing_secret(&phc_ss),
47            phc_ss,
48        })
49    }
50
51    fn create_extra_shared_state(_config: &servers::Config) -> anyhow::Result<ExtraSharedState> {
52        Ok(ExtraSharedState {})
53    }
54
55    fn create_extra_server_state(config: &servers::Config) -> anyhow::Result<ExtraServerState> {
56        let xconf = config.transcryptor.as_ref().unwrap();
57        let decap_key = xconf
58            .decap_key
59            .as_ref()
60            .expect("decap_key was not set nor generated")
61            .decode()
62            .map_err(|_| anyhow::anyhow!("decoding kem decapsulation key"))?;
63        Ok(ExtraServerState { decap_key })
64    }
65}
66
67pub struct ExtraSharedState {}
68
69pub struct ExtraServerState {
70    pub(super) decap_key: kem::DecapKey,
71}
72
73#[derive(Clone, Debug)]
74pub struct ExtraRunningState {
75    /// Hybrid post-quantum shared secret with pubhubs central
76    #[expect(dead_code)]
77    phc_ss: kem::SharedSecret,
78
79    /// Key used to (un)seal messages to and from PHC
80    pub(super) phc_sealing_secret: crypto::SealingKey,
81}
82
83pub struct App {
84    base: AppBase<Server>,
85    master_enc_key_part: elgamal::PrivateKey,
86    master_enc_key_part_inv: curve25519_dalek::Scalar,
87    master_enc_key_part_hash: crate::id::Id,
88    pseud_factor_secret: B64UU,
89    encap_key: kem::EncapKeyBytes,
90}
91
92impl Deref for App {
93    type Target = AppBase<Server>;
94
95    fn deref(&self) -> &Self::Target {
96        &self.base
97    }
98}
99
100impl crate::servers::App<Server> for App {
101    fn configure_actix_app(self: &Rc<Self>, sc: &mut web::ServiceConfig) {
102        EhppEP::add_to(self, sc, App::handle_ehpp);
103        api::server::HubPingEP::add_to(self, sc, App::handle_hub_ping);
104    }
105
106    fn check_constellation(&self, constellation: &Constellation) -> bool {
107        // Dear maintainer: this destructuring is intentional, making sure that this `check_constellation` function
108        // is updated when new fields are added to the constellation
109        let Constellation {
110            inner:
111                constellation::Inner {
112                    // These fields we must check:
113                    transcryptor_verifying_key,
114                    transcryptor_master_enc_key_part_hash,
115                    transcryptor_encap_key_id,
116
117                    // These fields we don't care about:
118                    transcryptor_url: _,
119                    transcryptor_ss_encap: _,
120                    auths_verifying_key: _,
121                    auths_url: _,
122                    auths_encap_key_id: _,
123                    auths_ss_encap: _,
124                    phc_jwt_key: _,
125                    phc_verifying_key: _,
126                    phc_master_enc_key_part_hash: _,
127                    phc_url: _,
128                    global_client_url: _,
129                    ph_version: _, // (already checked)
130                },
131            id: _,
132            created_at: _,
133        } = constellation;
134
135        // PHC must have encapsulated against our current encapsulation key; otherwise reject so that
136        // discovery re-runs and PHC (re)publishes a matching ciphertext.
137        if *transcryptor_encap_key_id != self.encap_key.id() {
138            return false;
139        }
140
141        transcryptor_verifying_key == &self.shared.verifying_key_bytes
142            && *transcryptor_master_enc_key_part_hash == self.master_enc_key_part_hash
143    }
144
145    fn master_enc_key_part(&self) -> Option<&elgamal::PrivateKey> {
146        Some(&self.master_enc_key_part)
147    }
148
149    fn encap_key(&self) -> Option<&kem::EncapKeyBytes> {
150        Some(&self.encap_key)
151    }
152
153    fn master_enc_key_part_sealing_key(&self) -> Option<&api::SealingKey> {
154        self.running_state.as_ref().map(|rs| &rs.phc_sealing_secret)
155    }
156
157    async fn discover(
158        self: &Rc<Self>,
159        phc_inf: api::DiscoveryInfoResp,
160    ) -> api::Result<DiscoverVerdict<()>> {
161        self.discover_as_non_phc(phc_inf).await
162    }
163}
164
165impl App {
166    /// Implements [`api::server::HubPingEP`].
167    async fn handle_hub_ping(
168        app: Rc<Self>,
169        signed_req: web::Json<api::phc::hub::TicketSigned<api::server::PingReq>>,
170    ) -> api::Result<api::server::PingResp> {
171        crate::servers::AppBase::<Server>::handle_hub_ping(app, signed_req).await
172    }
173
174    /// Implements [`EhppEP`]
175    async fn handle_ehpp(app: Rc<Self>, req: web::Json<EhppReq>) -> api::Result<EhppResp> {
176        let running_state = app.running_state_or_please_retry()?;
177
178        let EhppReq {
179            hub_nonce,
180            hub,
181            ppp,
182            hub_mac_key,
183        } = req.into_inner();
184
185        let Ok(api::sso::PolymorphicPseudonymPackage {
186            polymorphic_pseudonym,
187            nonce: phc_nonce,
188        }) = ppp.open(&running_state.phc_sealing_secret)
189        else {
190            return Ok(EhppResp::RetryWithNewPpp);
191        };
192
193        let encrypted_hub_pseudonym: elgamal::Triple = phcrypto::t_encrypted_hub_pseudonym(
194            polymorphic_pseudonym,
195            &***app.pseud_factor_secret,
196            &app.master_enc_key_part_inv,
197            hub,
198        );
199
200        let hub_id_mac = hub_mac_key.map(|key| key.mac(&hub));
201
202        Ok(EhppResp::Success(api::Sealed::new(
203            &api::sso::EncryptedHubPseudonymPackage {
204                encrypted_hub_pseudonym,
205                hub_nonce,
206                phc_nonce,
207                hub_id_mac,
208            },
209            &running_state.phc_sealing_secret,
210        )?))
211    }
212}
213
214#[derive(Clone)]
215pub struct AppCreator {
216    base: AppCreatorBase<Server>,
217    master_enc_key_part: elgamal::PrivateKey,
218    master_enc_key_part_inv: curve25519_dalek::Scalar,
219    pseud_factor_secret: B64UU,
220    encap_key: kem::EncapKeyBytes,
221}
222
223impl Deref for AppCreator {
224    type Target = AppCreatorBase<Server>;
225
226    fn deref(&self) -> &Self::Target {
227        &self.base
228    }
229}
230
231impl DerefMut for AppCreator {
232    fn deref_mut(&mut self) -> &mut Self::Target {
233        &mut self.base
234    }
235}
236
237impl crate::servers::AppCreator<Server> for AppCreator {
238    type ContextT = ();
239
240    fn new(config: &servers::Config) -> anyhow::Result<Self> {
241        let xconf = &config.transcryptor.as_ref().unwrap();
242
243        let master_enc_key_part: elgamal::PrivateKey = xconf
244            .master_enc_key_part
245            .clone()
246            .expect("master_enc_key_part was not generated");
247
248        let pseud_factor_secret = xconf
249            .pseud_factor_secret
250            .clone()
251            .expect("pseud_factor_secret was not generated");
252
253        let encap_key = xconf
254            .decap_key
255            .as_ref()
256            .expect("decap_key was not set nor generated")
257            .decode()
258            .and_then(|dk| dk.encap_key().encode())
259            .map_err(|_| anyhow::anyhow!("deriving kem encapsulation key"))?;
260
261        Ok(Self {
262            base: AppCreatorBase::<Server>::new(config)?,
263            master_enc_key_part_inv: master_enc_key_part.as_scalar().invert(),
264            master_enc_key_part,
265            pseud_factor_secret,
266            encap_key,
267        })
268    }
269
270    fn into_app(
271        self,
272        handle: &Handle<Server>,
273        _context: &Self::ContextT,
274        generation: usize,
275    ) -> App {
276        App {
277            base: AppBase::new(self.base, handle, generation),
278            master_enc_key_part_hash: phcrypto::master_enc_key_part_hash(
279                self.master_enc_key_part.public_key(),
280            ),
281            master_enc_key_part: self.master_enc_key_part,
282            master_enc_key_part_inv: self.master_enc_key_part_inv,
283            pseud_factor_secret: self.pseud_factor_secret,
284            encap_key: self.encap_key,
285        }
286    }
287}