1use 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
24pub type Server = servers::ServerImpl<Details>;
26
27pub 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 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 #[expect(dead_code)]
95 pub phc_ss: kem::SharedSecret,
96
97 pub attr_signing_key: jwt::HS256,
101
102 #[expect(dead_code)]
104 pub phc_sealing_secret: crypto::SealingKey,
105}
106
107pub 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#[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
138impl 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 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#[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 pub exp: api::NumericDate,
165
166 pub yivi_chained_session: Option<ChainedSessionSetup>,
168
169 pub yivi_ati2at: Vec<HashMap<yivi::AttributeTypeIdentifier, handle::Handle>>,
173}
174
175#[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 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 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 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 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 let Constellation {
271 inner:
272 constellation::Inner {
273 auths_verifying_key,
275 auths_encap_key_id,
276
277 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: _, },
292 id: _,
293 created_at: _,
294 } = constellation;
295
296 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#[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}