1use super::*;
17
18impl AuthRuntime {
19 pub async fn from_process(config: AuthConfig) -> Result<Self, AuthFailure> {
20 let state_lock = prepare_state_dir_and_lock(&config.state_dir)?;
21 let credential = load_server_admin_credential(&config.state_dir)?;
22 let Credential::Admin(admin_key) = credential else {
23 return Err(AuthFailure::new(
24 "administrator_key_required",
25 "the relay server must start with the administrator credential",
26 false,
27 ));
28 };
29 Self::start_locked(admin_key, config, true, state_lock).await
30 }
31
32 pub async fn from_isolated_state(config: AuthConfig) -> Result<Self, AuthFailure> {
37 let state_lock = prepare_state_dir_and_lock(&config.state_dir)?;
38 let credential = load_isolated_server_admin_credential(&config.state_dir)?;
39 let Credential::Admin(admin_key) = credential else {
40 return Err(AuthFailure::new(
41 "administrator_key_required",
42 "the embedded relay must start with an administrator credential",
43 false,
44 ));
45 };
46 Self::start_locked(admin_key, config, false, state_lock).await
47 }
48
49 pub async fn start(admin_key: AesKeyType, config: AuthConfig) -> Result<Self, AuthFailure> {
50 let state_lock = prepare_state_dir_and_lock(&config.state_dir)?;
51 Self::start_locked(admin_key, config, true, state_lock).await
52 }
53
54 async fn start_locked(
55 admin_key: AesKeyType,
56 config: AuthConfig,
57 sync_process_credential: bool,
58 state_lock: Arc<File>,
59 ) -> Result<Self, AuthFailure> {
60 let instance_id = load_or_create_instance_id(&config.state_dir)?;
61 let instance_id =
62 recover_instance_id_after_reset(&config.state_dir, &admin_key, instance_id)?;
63 let (mut loaded, safe_mode) = load_persisted_state(&config, &admin_key, instance_id);
64 let now = unix_seconds();
65 if let Some(state) = loaded.as_mut()
66 && normalize_tombstone_times(state, now)
67 {
68 write_snapshot_and_truncate_wal(&config, &admin_key, state)?;
69 }
70 let mut slots = (0..config.max_temporary_keys)
71 .map(|_| SlotHot::default())
72 .collect::<Vec<_>>()
73 .into_boxed_slice();
74 let mut cold = HashMap::new();
75 let mut restored_leases = Vec::new();
76
77 let admin_lease = Arc::new(AuthLease::new(ADMIN_KEY_ID, u64::MAX));
78 if let Some(state) = loaded.as_ref() {
79 for (index, generation) in state.generations.iter().copied().enumerate() {
80 if let Some(slot) = slots.get_mut(index) {
81 slot.generation = generation;
82 }
83 }
84 for entry in &state.entries {
85 let index = entry.key_id.slot().as_index();
86 let Some(slot) = slots.get_mut(index) else {
87 continue;
88 };
89 if slot.generation != entry.key_id.generation() {
90 continue;
91 }
92 let state = if entry.state == SlotState::Active && entry.expires_at <= now {
93 SlotState::Expired
94 } else {
95 entry.state
96 };
97 slot.state = state;
98 slot.expires_at = entry.expires_at;
99 cold.insert(
100 entry.key_id,
101 ColdMetadata {
102 issued_at: entry.issued_at,
103 label: entry.label.clone(),
104 tombstoned_at: match state {
105 SlotState::Expired => entry.tombstoned_at.unwrap_or(entry.expires_at),
106 SlotState::Revoked => entry.tombstoned_at.unwrap_or(now),
107 SlotState::Free | SlotState::Active => 0,
108 },
109 },
110 );
111 if state == SlotState::Active {
112 let lease = Arc::new(AuthLease::new(entry.key_id, entry.expires_at));
113 slot.lease = Arc::downgrade(&lease);
114 restored_leases.push(lease);
117 }
118 }
119 }
120
121 let legacy_protocol = if safe_mode {
122 LegacyProtocolPolicy::Deny
123 } else {
124 loaded
125 .as_ref()
126 .map(|state| state.legacy_protocol)
127 .unwrap_or(config.legacy_protocol)
128 };
129 let mut admin_replay_order = loaded
130 .as_ref()
131 .map(|state| {
132 state
133 .admin_replays
134 .iter()
135 .filter(|record| record.within_retention(now))
136 .cloned()
137 .collect::<VecDeque<_>>()
138 })
139 .unwrap_or_default();
140 while admin_replay_order.len() > ADMIN_REPLAY_CAPACITY {
141 admin_replay_order.pop_front();
142 }
143 let admin_replays = admin_replay_order
144 .iter()
145 .map(|record| record.fingerprint)
146 .collect::<HashSet<_>>();
147 let mut audit_records: VecDeque<AuditRecord> = loaded
148 .as_ref()
149 .map(|state| state.audit_records.iter().cloned().collect())
150 .unwrap_or_default();
151 while audit_records.len() > AUDIT_RECORD_CAPACITY {
152 audit_records.pop_front();
153 }
154 let (high_slot_generations, mut high_slot_entries) = loaded
155 .as_ref()
156 .map(|state| split_high_slot_state(state, config.max_temporary_keys))
157 .unwrap_or_default();
158 for entry in &mut high_slot_entries {
159 if entry.state == SlotState::Active && entry.expires_at <= now {
160 entry.state = SlotState::Expired;
161 entry.tombstoned_at = Some(entry.tombstoned_at.unwrap_or(entry.expires_at));
162 }
163 }
164 let inner = Arc::new(AuthStateInner {
165 admin: RwLock::new(AdminState {
166 key: admin_key,
167 lease: Arc::downgrade(&admin_lease),
168 }),
169 sync_process_credential,
170 instance_id: RwLock::new(instance_id),
171 slots: RwLock::new(slots),
172 high_slot_generations: RwLock::new(high_slot_generations),
173 high_slot_entries: RwLock::new(high_slot_entries),
174 safe_mode: AtomicBool::new(safe_mode),
175 legacy_protocol_allowed: AtomicBool::new(legacy_protocol.is_allowed()),
176 active_legacy_connections: AtomicU64::new(0),
177 last_legacy_connection_at: AtomicU64::new(0),
178 auth_successes: AtomicU64::new(0),
179 auth_failures: AtomicU64::new(0),
180 root_epoch: AtomicU64::new(loaded.as_ref().map(|state| state.root_epoch).unwrap_or(0)),
181 previous_root: RwLock::new(None),
182 audit_records: RwLock::new(audit_records),
183 cold: RwLock::new(cold),
184 });
185 let (command_tx, command_rx) = mpsc::channel(256);
186 let actor = tokio::spawn(run_auth_actor(
187 inner.clone(),
188 admin_lease,
189 command_rx,
190 config.clone(),
191 AuthActorState::new(
192 Leases::restored(&inner, now),
193 admin_replays,
194 admin_replay_order,
195 ),
196 state_lock.clone(),
197 ));
198 let actor_abort = actor.abort_handle();
199 let runtime = Self {
200 inner: Arc::downgrade(&inner),
201 command_tx,
202 config: config.clone(),
203 _state_lock: state_lock.clone(),
204 actor: Arc::new(Mutex::new(Some(actor))),
205 actor_abort,
206 };
207 Ok(runtime)
208 }
209
210 pub async fn shutdown_actor(&self) {
211 let (response, receiver) = oneshot::channel();
212 let _ = self
213 .command_tx
214 .send(AuthCommand::Shutdown { response })
215 .await;
216 let _ = receiver.await;
217 let handle = self.actor.lock().take();
218 if let Some(handle) = handle {
219 let _ = handle.await;
220 }
221 }
222
223 pub async fn abort_actor(&self) -> Result<(), AuthFailure> {
224 self.actor_abort.abort();
225 let handle = self.actor.lock().take();
226 if let Some(handle) = handle {
227 let _ = handle.await;
228 }
229 tokio::time::timeout(Duration::from_secs(5), async {
230 while self.inner.upgrade().is_some() {
231 tokio::time::sleep(Duration::from_millis(10)).await;
232 }
233 })
234 .await
235 .map_err(|_| {
236 AuthFailure::new(
237 "auth_state_unavailable",
238 "authentication actor did not drop after abort",
239 true,
240 )
241 })
242 }
243
244 pub fn config(&self) -> &AuthConfig {
245 &self.config
246 }
247
248 fn inner(&self) -> Result<Arc<AuthStateInner>, AuthFailure> {
249 self.inner.upgrade().ok_or_else(|| {
250 AuthFailure::new(
251 "auth_state_unavailable",
252 "authentication state manager is not running",
253 true,
254 )
255 })
256 }
257
258 pub fn admin_key(&self) -> Result<AesKeyType, AuthFailure> {
259 Ok(self.inner()?.admin_key())
260 }
261
262 pub fn derive_key(&self, key_id: KeyId) -> Result<AesKeyType, AuthFailure> {
263 let inner = self.inner()?;
264 if key_id.is_admin() {
265 return Ok(inner.admin_key());
266 }
267 derive_temporary_key(&inner.admin_key(), &inner.instance_id(), key_id)
268 }
269
270 #[cfg(test)]
271 pub(crate) fn high_slot_entry_count(&self) -> usize {
272 self.inner().map(|inner| inner.high().len()).unwrap_or(0)
273 }
274
275 pub fn derive_previous_key(&self, key_id: KeyId) -> Option<AesKeyType> {
276 let inner = self.inner().ok()?;
277 let previous = inner.previous_root.read().clone()?;
278 if key_id.is_admin() {
279 Some(previous.admin_key)
280 } else {
281 derive_temporary_key(&previous.admin_key, &previous.instance_id, key_id).ok()
282 }
283 }
284
285 pub fn authenticate_presented(
286 &self,
287 key_id: KeyId,
288 presented_key: &AesKeyType,
289 ) -> Result<AuthContext, AuthFailure> {
290 let inner = self.inner()?;
291 if key_id.is_admin() {
292 let admin = inner.admin.read();
293 if !bool::from(presented_key.ct_eq(&admin.key)) {
294 inner.auth_failures.fetch_add(1, Ordering::Relaxed);
295 return Err(AuthFailure::new(
296 "administrator_key_invalid",
297 "administrator credential does not match the active root key",
298 false,
299 ));
300 }
301 let lease = admin.lease.upgrade().ok_or_else(|| {
302 AuthFailure::new(
303 "administrator_key_rotated",
304 "administrator credential was rotated",
305 false,
306 )
307 })?;
308 inner.auth_successes.fetch_add(1, Ordering::Relaxed);
309 return Ok(AuthContext::from_lease(ADMIN_KEY_ID, true, &lease));
310 }
311 if inner.safe_mode.load(Ordering::Acquire) {
312 inner.auth_failures.fetch_add(1, Ordering::Relaxed);
313 return Err(AuthFailure::new(
314 "temporary_key_store_unavailable",
315 "temporary key state is unavailable; administrator reset is required",
316 false,
317 ));
318 }
319
320 let expected_key = derive_temporary_key(&inner.admin_key(), &inner.instance_id(), key_id)?;
321 if !bool::from(presented_key.ct_eq(&expected_key)) {
322 inner.auth_failures.fetch_add(1, Ordering::Relaxed);
323 return Err(temporary_key_material_mismatch(&inner, key_id));
324 }
325
326 let index = key_id.slot().as_index();
327 let generation = key_id.generation();
328 let slots = inner.slots();
329 let Some(slot) = slots.get(index) else {
330 inner.auth_failures.fetch_add(1, Ordering::Relaxed);
331 return Err(AuthFailure::new(
332 "temporary_key_not_found",
333 "temporary key id is outside the configured slot table",
334 false,
335 ));
336 };
337 if slot.generation != generation {
338 inner.auth_failures.fetch_add(1, Ordering::Relaxed);
339 return Err(AuthFailure::new(
340 "temporary_key_generation_mismatch",
341 "temporary key generation does not match the current slot",
342 false,
343 ));
344 }
345 let failure = match slot.state {
346 SlotState::Free => Some(AuthFailure::new(
347 "temporary_key_not_found",
348 "temporary key does not exist",
349 false,
350 )),
351 SlotState::Expired => Some(AuthFailure::new(
352 "temporary_key_expired",
353 "temporary key has expired",
354 false,
355 )),
356 SlotState::Revoked => Some(AuthFailure::new(
357 "temporary_key_revoked",
358 "temporary key was revoked",
359 false,
360 )),
361 SlotState::Active if slot.expires_at <= unix_seconds() => {
362 if let Some(lease) = slot.lease.upgrade() {
363 lease.cancel_expired();
364 }
365 Some(AuthFailure::new(
366 "temporary_key_expired",
367 "temporary key has expired",
368 false,
369 ))
370 }
371 SlotState::Active => None,
372 };
373 if let Some(failure) = failure {
374 inner.auth_failures.fetch_add(1, Ordering::Relaxed);
375 return Err(failure);
376 }
377 let lease = slot.lease.upgrade().ok_or_else(|| {
378 inner.auth_failures.fetch_add(1, Ordering::Relaxed);
379 AuthFailure::new(
380 "temporary_key_inactive",
381 "temporary key lease is no longer active",
382 true,
383 )
384 })?;
385 inner.auth_successes.fetch_add(1, Ordering::Relaxed);
386 Ok(AuthContext::from_lease(key_id, false, &lease))
387 }
388
389 pub fn legacy_protocol_allowed(&self) -> Result<bool, AuthFailure> {
390 Ok(self
391 .inner()?
392 .legacy_protocol_allowed
393 .load(Ordering::Acquire))
394 }
395
396 pub fn record_legacy_connection(&self) -> Result<LegacyConnectionGuard, AuthFailure> {
397 let inner = self.inner()?;
398 inner
399 .active_legacy_connections
400 .fetch_add(1, Ordering::AcqRel);
401 inner
402 .last_legacy_connection_at
403 .store(unix_seconds(), Ordering::Release);
404 Ok(LegacyConnectionGuard {
405 inner: Arc::downgrade(&inner),
406 })
407 }
408
409 async fn request<T>(
410 &self,
411 build: impl FnOnce(oneshot::Sender<Result<T, AuthFailure>>) -> AuthCommand,
412 ) -> Result<T, AuthFailure> {
413 let (response, receiver) = oneshot::channel();
414 self.command_tx.send(build(response)).await.map_err(|_| {
415 AuthFailure::new(
416 "auth_state_unavailable",
417 "authentication state manager is not running",
418 true,
419 )
420 })?;
421 receiver.await.map_err(|_| {
422 AuthFailure::new(
423 "auth_state_unavailable",
424 "authentication state manager dropped the response",
425 true,
426 )
427 })?
428 }
429
430 pub async fn claim_admin_mutation(
431 &self,
432 authorization: &AuthContext,
433 fingerprint: [u8; 32],
434 client_timestamp: u64,
435 ) -> Result<(), AuthFailure> {
436 let authority = authorization.admin_authority()?;
437 self.request(|response| AuthCommand::ClaimAdminMutation {
438 authority,
439 fingerprint,
440 client_timestamp,
441 response,
442 })
443 .await
444 }
445
446 pub async fn issue(
447 &self,
448 authorization: &AuthContext,
449 ttl: Duration,
450 label: Option<String>,
451 ) -> Result<IssuedTemporaryKey, AuthFailure> {
452 let authority = authorization.admin_authority()?;
453 self.request(|response| AuthCommand::Issue {
454 authority,
455 ttl,
456 label,
457 response,
458 })
459 .await
460 }
461
462 pub async fn list(
463 &self,
464 authorization: &AuthContext,
465 page: u32,
466 page_size: u16,
467 ) -> Result<KeyPage, AuthFailure> {
468 let authority = authorization.admin_authority()?;
469 self.request(|response| AuthCommand::List {
470 authority,
471 page,
472 page_size,
473 response,
474 })
475 .await
476 }
477
478 pub async fn show(
479 &self,
480 authorization: &AuthContext,
481 key_id: KeyId,
482 reveal: bool,
483 ) -> Result<IssuedTemporaryKey, AuthFailure> {
484 let authority = authorization.admin_authority()?;
485 self.request(|response| AuthCommand::Show {
486 authority,
487 key_id,
488 reveal,
489 response,
490 })
491 .await
492 }
493
494 pub async fn renew(
495 &self,
496 authorization: &AuthContext,
497 key_id: KeyId,
498 ttl: Duration,
499 ) -> Result<IssuedTemporaryKey, AuthFailure> {
500 let authority = authorization.admin_authority()?;
501 self.request(|response| AuthCommand::Renew {
502 authority,
503 key_id,
504 ttl,
505 response,
506 })
507 .await
508 }
509
510 pub async fn revoke(
511 &self,
512 authorization: &AuthContext,
513 key_id: KeyId,
514 ) -> Result<TemporaryKeyMetadata, AuthFailure> {
515 let authority = authorization.admin_authority()?;
516 self.request(|response| AuthCommand::Revoke {
517 authority,
518 key_id,
519 response,
520 })
521 .await
522 }
523
524 pub async fn gc(&self, authorization: &AuthContext) -> Result<u64, AuthFailure> {
525 let authority = authorization.admin_authority()?;
526 self.request(|response| AuthCommand::Gc {
527 authority,
528 response,
529 })
530 .await
531 }
532
533 pub async fn reset(&self, authorization: &AuthContext) -> Result<(), AuthFailure> {
534 let authority = authorization.admin_authority()?;
535 self.request(|response| AuthCommand::Reset {
536 authority,
537 response,
538 })
539 .await
540 }
541
542 pub async fn rotate_root(
543 &self,
544 authorization: &AuthContext,
545 new_key: AesKeyType,
546 ) -> Result<(), AuthFailure> {
547 let authority = authorization.admin_authority()?;
548 self.request(|response| AuthCommand::RotateRoot {
549 authority,
550 new_key,
551 response,
552 })
553 .await
554 }
555
556 pub async fn set_legacy_protocol(
557 &self,
558 authorization: &AuthContext,
559 policy: LegacyProtocolPolicy,
560 ) -> Result<(), AuthFailure> {
561 let authority = authorization.admin_authority()?;
562 self.request(|response| AuthCommand::SetLegacyProtocol {
563 authority,
564 policy,
565 response,
566 })
567 .await
568 }
569
570 pub async fn status(&self, authorization: &AuthContext) -> Result<AuthStatus, AuthFailure> {
571 let authority = authorization.admin_authority()?;
572 self.request(|response| AuthCommand::Status {
573 authority,
574 response,
575 })
576 .await
577 }
578
579 pub async fn audit_admin(
580 &self,
581 authorization: &AuthContext,
582 action: impl Into<String>,
583 key_id: Option<KeyId>,
584 detail: Option<String>,
585 ) -> Result<(), AuthFailure> {
586 let authority = authorization.admin_authority()?;
587 let action = action.into();
588 self.request(|response| AuthCommand::Audit {
589 authority,
590 action,
591 key_id,
592 detail,
593 response,
594 })
595 .await
596 }
597}
598
599fn temporary_key_material_mismatch(inner: &AuthStateInner, key_id: KeyId) -> AuthFailure {
600 let index = key_id.slot().as_index();
601 let generation = key_id.generation();
602 let slots = inner.slots();
603 let current_generation = match slots.get(index) {
604 Some(slot) => Some(slot.generation),
605 None => {
606 let high = inner.high_slot_generations.read();
607 index
608 .checked_sub(slots.len())
609 .and_then(|offset| high.get(offset).copied())
610 }
611 };
612 let slot_is_active = slots
613 .get(index)
614 .is_some_and(|slot| slot.state == SlotState::Active && slot.generation == generation);
615 if slot_is_active {
616 return AuthFailure::new(
617 "temporary_key_invalid",
618 "temporary credential does not match the active relay key material",
619 false,
620 );
621 }
622 let current_epoch = inner.root_epoch.load(Ordering::Acquire);
623 if current_epoch > 0
624 && generation > Generation::FIRST
625 && current_generation.is_some_and(|issued| generation <= issued)
626 {
627 return AuthFailure::new(
628 "temporary_key_rotated",
629 "temporary credential was invalidated by administrator root rotation or auth-state reset",
630 false,
631 );
632 }
633 AuthFailure::new(
634 "temporary_key_invalid",
635 "temporary credential does not match the active relay key material",
636 false,
637 )
638}