1use super::MembershipCertificate;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum CertificateState {
12 Valid {
14 expires_in_ms: u64,
16 },
17 Warning {
19 expires_in_ms: u64,
21 },
22 GracePeriod {
24 grace_remaining_ms: u64,
26 },
27 Expired,
29}
30
31impl CertificateState {
32 pub fn is_operational(&self) -> bool {
34 matches!(
35 self,
36 CertificateState::Valid { .. }
37 | CertificateState::Warning { .. }
38 | CertificateState::GracePeriod { .. }
39 )
40 }
41
42 pub fn should_reauth(&self) -> bool {
44 matches!(
45 self,
46 CertificateState::Warning { .. } | CertificateState::GracePeriod { .. }
47 )
48 }
49
50 pub fn is_expired(&self) -> bool {
52 matches!(self, CertificateState::Expired)
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub struct AuthConfig {
59 pub auth_interval_hours: u16,
61 pub grace_period_hours: u16,
63 pub warning_threshold_hours: u16,
65}
66
67impl Default for AuthConfig {
68 fn default() -> Self {
69 Self {
70 auth_interval_hours: 24,
71 grace_period_hours: 4,
72 warning_threshold_hours: 1,
73 }
74 }
75}
76
77impl AuthConfig {
78 pub fn new(
80 auth_interval_hours: u16,
81 grace_period_hours: u16,
82 warning_threshold_hours: u16,
83 ) -> Self {
84 Self {
85 auth_interval_hours,
86 grace_period_hours,
87 warning_threshold_hours,
88 }
89 }
90
91 pub fn auth_interval_ms(&self) -> u64 {
93 self.auth_interval_hours as u64 * 3_600_000
94 }
95
96 pub fn grace_period_ms(&self) -> u64 {
98 self.grace_period_hours as u64 * 3_600_000
99 }
100
101 pub fn warning_threshold_ms(&self) -> u64 {
103 self.warning_threshold_hours as u64 * 3_600_000
104 }
105}
106
107#[derive(Debug, Clone)]
109pub struct AuthStateTracker {
110 config: AuthConfig,
111}
112
113impl Default for AuthStateTracker {
114 fn default() -> Self {
115 Self::new(AuthConfig::default())
116 }
117}
118
119impl AuthStateTracker {
120 pub fn new(config: AuthConfig) -> Self {
122 Self { config }
123 }
124
125 pub fn config(&self) -> &AuthConfig {
127 &self.config
128 }
129
130 pub fn check_state(&self, cert: &MembershipCertificate, now_ms: u64) -> CertificateState {
141 let expires_at = cert.expires_at_ms;
142
143 if now_ms < expires_at {
144 let expires_in_ms = expires_at - now_ms;
146
147 if expires_in_ms <= self.config.warning_threshold_ms() {
148 CertificateState::Warning { expires_in_ms }
149 } else {
150 CertificateState::Valid { expires_in_ms }
151 }
152 } else {
153 let expired_for_ms = now_ms - expires_at;
155
156 if expired_for_ms < self.config.grace_period_ms() {
157 let grace_remaining_ms = self.config.grace_period_ms() - expired_for_ms;
158 CertificateState::GracePeriod { grace_remaining_ms }
159 } else {
160 CertificateState::Expired
161 }
162 }
163 }
164
165 pub fn needs_reauth(&self, cert: &MembershipCertificate, now_ms: u64) -> bool {
169 self.check_state(cert, now_ms).should_reauth()
170 }
171
172 pub fn is_operational(&self, cert: &MembershipCertificate, now_ms: u64) -> bool {
176 self.check_state(cert, now_ms).is_operational()
177 }
178
179 pub fn is_expired(&self, cert: &MembershipCertificate, now_ms: u64) -> bool {
181 self.check_state(cert, now_ms).is_expired()
182 }
183
184 pub fn reauth_deadline(&self, cert: &MembershipCertificate) -> u64 {
186 cert.expires_at_ms
187 .saturating_sub(self.config.warning_threshold_ms())
188 }
189
190 pub fn hard_cutoff(&self, cert: &MembershipCertificate) -> u64 {
192 cert.expires_at_ms
193 .saturating_add(self.config.grace_period_ms())
194 }
195}
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq)]
199pub enum AuthStateEvent {
200 EnteringWarning { expires_in_ms: u64 },
202 EnteringGracePeriod { grace_remaining_ms: u64 },
204 Expired,
206 Renewed { new_expires_at_ms: u64 },
208}
209
210#[derive(Debug, Clone)]
212pub struct AuthStateMonitor {
213 tracker: AuthStateTracker,
214 last_state: Option<CertificateState>,
215}
216
217impl AuthStateMonitor {
218 pub fn new(tracker: AuthStateTracker) -> Self {
220 Self {
221 tracker,
222 last_state: None,
223 }
224 }
225
226 pub fn update(&mut self, cert: &MembershipCertificate, now_ms: u64) -> Option<AuthStateEvent> {
230 let new_state = self.tracker.check_state(cert, now_ms);
231
232 let event = match (&self.last_state, &new_state) {
233 (Some(CertificateState::Valid { .. }), CertificateState::Warning { expires_in_ms })
235 | (None, CertificateState::Warning { expires_in_ms }) => {
236 Some(AuthStateEvent::EnteringWarning {
237 expires_in_ms: *expires_in_ms,
238 })
239 }
240
241 (
243 Some(CertificateState::Warning { .. }),
244 CertificateState::GracePeriod { grace_remaining_ms },
245 ) => Some(AuthStateEvent::EnteringGracePeriod {
246 grace_remaining_ms: *grace_remaining_ms,
247 }),
248
249 (Some(state), CertificateState::Expired) if *state != CertificateState::Expired => {
251 Some(AuthStateEvent::Expired)
252 }
253
254 _ => None,
255 };
256
257 self.last_state = Some(new_state);
258 event
259 }
260
261 pub fn notify_renewed(&mut self, new_cert: &MembershipCertificate) -> AuthStateEvent {
265 self.last_state = Some(CertificateState::Valid {
266 expires_in_ms: new_cert.expires_at_ms,
267 });
268 AuthStateEvent::Renewed {
269 new_expires_at_ms: new_cert.expires_at_ms,
270 }
271 }
272
273 pub fn current_state(&self) -> Option<&CertificateState> {
275 self.last_state.as_ref()
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282
283 fn test_cert(issued_at_ms: u64, expires_at_ms: u64) -> MembershipCertificate {
285 MembershipCertificate {
286 member_public_key: [0u8; 32],
287 mesh_id: "A1B2C3D4".to_string(),
288 callsign: "TEST-01".to_string(),
289 permissions: super::super::MemberPermissions::STANDARD,
290 issued_at_ms,
291 expires_at_ms,
292 issuer_public_key: [0u8; 32],
293 issuer_signature: [0u8; 64],
294 }
295 }
296
297 #[test]
298 fn test_config_defaults() {
299 let config = AuthConfig::default();
300 assert_eq!(config.auth_interval_hours, 24);
301 assert_eq!(config.grace_period_hours, 4);
302 assert_eq!(config.warning_threshold_hours, 1);
303 }
304
305 #[test]
306 fn test_config_to_ms() {
307 let config = AuthConfig::default();
308 assert_eq!(config.auth_interval_ms(), 24 * 3_600_000);
309 assert_eq!(config.grace_period_ms(), 4 * 3_600_000);
310 assert_eq!(config.warning_threshold_ms(), 3_600_000);
311 }
312
313 #[test]
314 fn test_valid_state() {
315 let tracker = AuthStateTracker::default();
316 let cert = test_cert(0, 24 * 3_600_000); let state = tracker.check_state(&cert, 0);
320 assert!(
321 matches!(state, CertificateState::Valid { expires_in_ms } if expires_in_ms == 24 * 3_600_000)
322 );
323 assert!(state.is_operational());
324 assert!(!state.should_reauth());
325
326 let state = tracker.check_state(&cert, 12 * 3_600_000);
328 assert!(
329 matches!(state, CertificateState::Valid { expires_in_ms } if expires_in_ms == 12 * 3_600_000)
330 );
331 }
332
333 #[test]
334 fn test_warning_state() {
335 let tracker = AuthStateTracker::default();
336 let cert = test_cert(0, 24 * 3_600_000);
337
338 let state = tracker.check_state(&cert, 23 * 3_600_000);
340 assert!(
341 matches!(state, CertificateState::Warning { expires_in_ms } if expires_in_ms == 3_600_000)
342 );
343 assert!(state.is_operational());
344 assert!(state.should_reauth());
345
346 let state = tracker.check_state(&cert, 23 * 3_600_000 + 1_800_000);
348 assert!(
349 matches!(state, CertificateState::Warning { expires_in_ms } if expires_in_ms == 1_800_000)
350 );
351 }
352
353 #[test]
354 fn test_grace_period_state() {
355 let tracker = AuthStateTracker::default();
356 let cert = test_cert(0, 24 * 3_600_000);
357
358 let state = tracker.check_state(&cert, 24 * 3_600_000);
360 assert!(
361 matches!(state, CertificateState::GracePeriod { grace_remaining_ms } if grace_remaining_ms == 4 * 3_600_000)
362 );
363 assert!(state.is_operational());
364 assert!(state.should_reauth());
365
366 let state = tracker.check_state(&cert, 26 * 3_600_000);
368 assert!(
369 matches!(state, CertificateState::GracePeriod { grace_remaining_ms } if grace_remaining_ms == 2 * 3_600_000)
370 );
371 }
372
373 #[test]
374 fn test_expired_state() {
375 let tracker = AuthStateTracker::default();
376 let cert = test_cert(0, 24 * 3_600_000);
377
378 let state = tracker.check_state(&cert, 28 * 3_600_000);
380 assert!(matches!(state, CertificateState::Expired));
381 assert!(!state.is_operational());
382 assert!(!state.should_reauth()); let state = tracker.check_state(&cert, 30 * 3_600_000);
386 assert!(matches!(state, CertificateState::Expired));
387 }
388
389 #[test]
390 fn test_needs_reauth() {
391 let tracker = AuthStateTracker::default();
392 let cert = test_cert(0, 24 * 3_600_000);
393
394 assert!(!tracker.needs_reauth(&cert, 0));
396 assert!(!tracker.needs_reauth(&cert, 22 * 3_600_000));
397
398 assert!(tracker.needs_reauth(&cert, 23 * 3_600_000));
400 assert!(tracker.needs_reauth(&cert, 23 * 3_600_000 + 1_800_000));
401
402 assert!(tracker.needs_reauth(&cert, 25 * 3_600_000));
404
405 assert!(!tracker.needs_reauth(&cert, 29 * 3_600_000));
407 }
408
409 #[test]
410 fn test_is_operational() {
411 let tracker = AuthStateTracker::default();
412 let cert = test_cert(0, 24 * 3_600_000);
413
414 assert!(tracker.is_operational(&cert, 0));
415 assert!(tracker.is_operational(&cert, 23 * 3_600_000)); assert!(tracker.is_operational(&cert, 26 * 3_600_000)); assert!(!tracker.is_operational(&cert, 29 * 3_600_000)); }
419
420 #[test]
421 fn test_deadlines() {
422 let tracker = AuthStateTracker::default();
423 let cert = test_cert(0, 24 * 3_600_000);
424
425 assert_eq!(tracker.reauth_deadline(&cert), 23 * 3_600_000);
427
428 assert_eq!(tracker.hard_cutoff(&cert), 28 * 3_600_000);
430 }
431
432 #[test]
433 fn test_custom_config() {
434 let config = AuthConfig::new(48, 8, 2); let tracker = AuthStateTracker::new(config);
436 let cert = test_cert(0, 48 * 3_600_000);
437
438 let state = tracker.check_state(&cert, 45 * 3_600_000);
440 assert!(matches!(state, CertificateState::Valid { .. }));
441
442 let state = tracker.check_state(&cert, 46 * 3_600_000 + 1_800_000);
444 assert!(matches!(state, CertificateState::Warning { .. }));
445
446 let state = tracker.check_state(&cert, 52 * 3_600_000);
448 assert!(
449 matches!(state, CertificateState::GracePeriod { grace_remaining_ms } if grace_remaining_ms == 4 * 3_600_000)
450 );
451
452 let state = tracker.check_state(&cert, 56 * 3_600_000);
454 assert!(matches!(state, CertificateState::Expired));
455 }
456
457 #[test]
458 fn test_monitor_transitions() {
459 let tracker = AuthStateTracker::default();
460 let mut monitor = AuthStateMonitor::new(tracker);
461 let cert = test_cert(0, 24 * 3_600_000);
462
463 let event = monitor.update(&cert, 0);
465 assert!(event.is_none()); let event = monitor.update(&cert, 22 * 3_600_000);
469 assert!(event.is_none());
470
471 let event = monitor.update(&cert, 23 * 3_600_000);
473 assert!(matches!(
474 event,
475 Some(AuthStateEvent::EnteringWarning { .. })
476 ));
477
478 let event = monitor.update(&cert, 24 * 3_600_000);
480 assert!(matches!(
481 event,
482 Some(AuthStateEvent::EnteringGracePeriod { .. })
483 ));
484
485 let event = monitor.update(&cert, 28 * 3_600_000);
487 assert!(matches!(event, Some(AuthStateEvent::Expired)));
488
489 let event = monitor.update(&cert, 30 * 3_600_000);
491 assert!(event.is_none());
492 }
493
494 #[test]
495 fn test_monitor_renewal() {
496 let tracker = AuthStateTracker::default();
497 let mut monitor = AuthStateMonitor::new(tracker);
498 let cert = test_cert(0, 24 * 3_600_000);
499
500 monitor.update(&cert, 23 * 3_600_000);
502
503 let new_cert = test_cert(23 * 3_600_000, 47 * 3_600_000);
505 let event = monitor.notify_renewed(&new_cert);
506 assert!(matches!(
507 event,
508 AuthStateEvent::Renewed {
509 new_expires_at_ms: exp
510 } if exp == 47 * 3_600_000
511 ));
512
513 let state = monitor.current_state();
515 assert!(matches!(state, Some(CertificateState::Valid { .. })));
516 }
517}