Skip to main content

pb_mapper_auth/
runtime.rs

1//! Public authentication runtime facade and hot-path credential checks.
2//!
3//! ```text
4//! process credential + persisted state
5//!                 |
6//!                 v
7//!       hot slot table (Weak leases) <---- request authentication
8//!                 |
9//!                 +----> lifecycle actor (strong leases + time wheel)
10//! ```
11//!
12//! Read-only authentication stays synchronous and allocation-light. Every administrator
13//! API captures a weak authority lease and sends it to the actor, where it is compared
14//! with the current lease immediately before the operation executes.
15
16use 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    /// Start an embedded relay with an administrator key owned only by its state directory.
33    ///
34    /// This deliberately leaves the process credential untouched because the containing UI uses
35    /// that credential for its outbound register, connect, status, and stream connections.
36    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                    // Held only until the schedule below adopts them; the wheel
115                    // is the lasting owner.
116                    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}