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::{self, AppBase, AppCreatorBase, Constellation, Handle, constellation, yivi};
10use crate::{
11 api::{self, EndpointDetails as _},
12 attr,
13 common::{elgamal, secret::DigestibleSecret as _},
14 handle, id, map,
15 misc::{crypto, jwt},
16 phcrypto,
17};
18
19use super::yivi::ChainedSessionsCtl;
20
21pub type Server = servers::ServerImpl<Details>;
23
24pub struct Details;
26impl servers::Details for Details {
27 const NAME: servers::Name = servers::Name::AuthenticationServer;
28
29 type AppT = App;
30 type AppCreatorT = AppCreator;
31 type ExtraRunningState = ExtraRunningState;
32 type ExtraSharedState = ExtraSharedState;
33 type ObjectStoreT = servers::object_store::UseNone;
34
35 fn create_running_state(
36 server: &Server,
37 constellation: &Constellation,
38 ) -> anyhow::Result<Self::ExtraRunningState> {
39 let phc_ss = server.enc_key.shared_secret(&constellation.phc_enc_key);
40
41 Ok(ExtraRunningState {
42 attr_signing_key: phcrypto::attr_signing_key(&phc_ss),
43 phc_sealing_secret: phcrypto::sealing_secret(&phc_ss),
44 phc_ss,
45 })
46 }
47
48 fn create_extra_shared_state(_config: &servers::Config) -> anyhow::Result<ExtraSharedState> {
49 Ok(ExtraSharedState {})
50 }
51}
52
53pub struct ExtraSharedState {}
54
55#[derive(Clone, Debug)]
56pub struct ExtraRunningState {
57 #[expect(dead_code)]
59 pub phc_ss: elgamal::SharedSecret,
60
61 pub attr_signing_key: jwt::HS256,
65
66 #[expect(dead_code)]
68 pub phc_sealing_secret: crypto::SealingKey,
69}
70
71pub struct App {
73 pub base: AppBase<Server>,
74 pub attribute_types: map::Map<attr::Type>,
75 pub yivi: Option<YiviCtx>,
76 pub auth_state_secret: crypto::SealingKey,
77 pub auth_window: core::time::Duration,
78 pub attr_key_secret: Vec<u8>,
79 pub chained_sessions_ctl: Option<ChainedSessionsCtl>,
80}
81
82impl Deref for App {
83 type Target = AppBase<Server>;
84
85 fn deref(&self) -> &Self::Target {
86 &self.base
87 }
88}
89
90#[derive(Debug, Clone)]
92pub struct YiviCtx {
93 pub requestor_url: url::Url,
94 pub requestor_creds: yivi::Credentials<yivi::SigningKey>,
95 pub server_creds: yivi::Credentials<yivi::VerifyingKey>,
96
97 pub chained_sessions_config: super::yivi::ChainedSessionsConfig,
98 pub card_config: super::card::CardConfig,
99}
100
101impl App {
103 pub fn get_yivi(&self) -> Result<&YiviCtx, api::ErrorCode> {
104 self.yivi.as_ref().ok_or_else(|| {
105 log::debug!("yivi requested, but not configured");
106 api::ErrorCode::BadRequest
107 })
108 }
109
110 pub fn attr_type_from_handle<'s>(
113 &'s self,
114 attr_type_handle: &handle::Handle,
115 ) -> Option<&'s attr::Type> {
116 self.attribute_types.get(attr_type_handle)
117 }
118}
119
120#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
122pub(super) struct AuthState {
123 pub source: attr::Source,
124 pub attr_type_choices: Vec<Vec<handle::Handle>>,
125
126 pub exp: api::NumericDate,
128
129 pub yivi_chained_session: Option<ChainedSessionSetup>,
131
132 pub yivi_ati2at: Vec<HashMap<yivi::AttributeTypeIdentifier, handle::Handle>>,
136}
137
138#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
140pub(super) struct ChainedSessionSetup {
141 pub id: id::Id,
142 pub drip: bool,
143}
144
145impl AuthState {
146 pub fn seal(&self, key: &crypto::SealingKey) -> api::Result<api::auths::AuthState> {
147 Ok(api::auths::AuthState::new(
148 crypto::seal(&self, key, b"")
149 .map_err(|err| {
150 log::warn!("failed to seal AuthState: {err}");
151 api::ErrorCode::InternalError
152 })?
153 .into(),
154 ))
155 }
156
157 pub fn unseal(sealed: &api::auths::AuthState, key: &crypto::SealingKey) -> Option<AuthState> {
160 let Ok(state): Result<AuthState, _> = crypto::unseal(&*sealed.inner, key, b"") else {
161 log::debug!("failed to unseal AuthState");
162 return None;
163 };
164
165 if state.exp < api::NumericDate::now() {
166 log::debug!("received expired AuthState");
167 return None;
168 }
169
170 Some(state)
171 }
172}
173
174impl App {
175 async fn handle_hub_ping(
177 app: Rc<Self>,
178 signed_req: web::Json<api::phc::hub::TicketSigned<api::server::PingReq>>,
179 ) -> api::Result<api::server::PingResp> {
180 crate::servers::AppBase::<Server>::handle_hub_ping(app, signed_req).await
181 }
182
183 fn cached_handle_welcome(app: &Self) -> api::Result<api::auths::WelcomeResp> {
185 let attr_types: HashMap<handle::Handle, attr::Type> = app
186 .attribute_types
187 .values()
188 .map(|attr_type| (attr_type.handles.preferred().clone(), attr_type.clone()))
189 .collect();
190
191 Ok(api::auths::WelcomeResp {
192 attr_types,
193 card_validity: app
194 .get_yivi()
195 .map(|yivi| yivi.card_config.valid_for.to_welcome_ep_format())
196 .ok()
197 .flatten(),
198 })
199 }
200}
201
202impl crate::servers::App<Server> for App {
203 fn configure_actix_app(self: &Rc<Self>, sc: &mut web::ServiceConfig) {
204 api::auths::WelcomeEP::caching_add_to(self, sc, App::cached_handle_welcome);
205 api::server::HubPingEP::add_to(self, sc, App::handle_hub_ping);
206
207 api::auths::AuthStartEP::add_to(self, sc, App::handle_auth_start);
208 api::auths::AuthCompleteEP::add_to(self, sc, App::handle_auth_complete);
209
210 api::auths::AttrKeysEP::add_to(self, sc, App::handle_attr_keys);
211
212 api::auths::CardEP::add_to(self, sc, App::handle_card);
213
214 api::auths::YiviWaitForResultEP::add_to(self, sc, App::handle_yivi_wait_for_result);
215 api::auths::YiviReleaseNextSessionEP::add_to(
216 self,
217 sc,
218 App::handle_yivi_release_next_session,
219 );
220
221 sc.app_data(web::Data::new(self.clone())).route(
224 api::auths::YIVI_NEXT_SESSION_PATH,
225 web::post().to(App::handle_yivi_next_session),
226 );
227 }
228
229 fn check_constellation(&self, constellation: &Constellation) -> bool {
230 let Constellation {
233 inner:
234 constellation::Inner {
235 auths_enc_key: enc_key,
237 auths_jwt_key: jwt_key,
238
239 auths_url: _,
241 transcryptor_jwt_key: _,
242 transcryptor_enc_key: _,
243 transcryptor_url: _,
244 transcryptor_master_enc_key_part: _,
245 phc_jwt_key: _,
246 phc_enc_key: _,
247 phc_url: _,
248 master_enc_key: _,
249 global_client_url: _,
250 ph_version: _, },
252 id: _,
253 created_at: _,
254 } = constellation;
255
256 enc_key == self.enc_key.public_key() && **jwt_key == self.jwt_key.verifying_key()
257 }
258}
259
260#[derive(Clone)]
262pub struct AppCreator {
263 base: AppCreatorBase<Server>,
264 attribute_types: map::Map<attr::Type>,
265 yivi: Option<YiviCtx>,
266 auth_state_secret: crypto::SealingKey,
267 auth_window: core::time::Duration,
268 attr_key_secret: Vec<u8>,
269 chained_sessions_ctl: Option<ChainedSessionsCtl>,
270}
271
272impl Deref for AppCreator {
273 type Target = AppCreatorBase<Server>;
274
275 #[inline]
276 fn deref(&self) -> &Self::Target {
277 &self.base
278 }
279}
280
281impl DerefMut for AppCreator {
282 #[inline]
283 fn deref_mut(&mut self) -> &mut Self::Target {
284 &mut self.base
285 }
286}
287
288impl crate::servers::AppCreator<Server> for AppCreator {
289 type ContextT = ();
290
291 fn new(config: &servers::Config) -> anyhow::Result<Self> {
292 let base = AppCreatorBase::<Server>::new(config)?;
293
294 let xconf = &config.auths.as_ref().unwrap();
295
296 let mut attribute_types: crate::map::Map<attr::Type> = Default::default();
297
298 for attr_type in xconf.attribute_types.iter() {
299 if let Some(handle_or_id) = attribute_types.insert_new(attr_type.clone()) {
300 anyhow::bail!("two attribute types are known as {handle_or_id}");
301 }
302 }
303
304 let yivi: Option<YiviCtx> = xconf.yivi.as_ref().map(|cfg| YiviCtx {
305 requestor_url: cfg.requestor_url.as_ref().clone(),
306 requestor_creds: cfg.requestor_creds.clone(),
307 server_creds: cfg.server_creds(),
308 chained_sessions_config: cfg.chained_sessions.clone(),
309 card_config: cfg.card.clone(),
310 });
311
312 let auth_state_secret: crypto::SealingKey = base
313 .enc_key
314 .derive_sealing_key(sha2::Sha256::new(), "pubhubs-auths-auth-state");
315
316 let auth_window = xconf.auth_window;
317
318 let attr_key_secret = xconf
319 .attr_key_secret
320 .as_ref()
321 .expect("attr_key_secret not generated")
322 .to_vec();
323
324 let chained_sessions_ctl = yivi
325 .as_ref()
326 .map(|yivi_ctx| ChainedSessionsCtl::new(yivi_ctx.clone()));
327
328 Ok(Self {
329 base,
330 attribute_types,
331 yivi,
332 auth_state_secret,
333 auth_window,
334 attr_key_secret,
335 chained_sessions_ctl,
336 })
337 }
338
339 fn into_app(
340 self,
341 handle: &Handle<Server>,
342 _context: &Self::ContextT,
343 generation: usize,
344 ) -> App {
345 App {
346 base: AppBase::new(self.base, handle, generation),
347 attribute_types: self.attribute_types,
348 yivi: self.yivi,
349 auth_state_secret: self.auth_state_secret,
350 auth_window: self.auth_window,
351 attr_key_secret: self.attr_key_secret,
352 chained_sessions_ctl: self.chained_sessions_ctl,
353 }
354 }
355}