1use core_types::SecurityLevel;
27use std::collections::HashMap;
28
29#[derive(Debug, Clone)]
34pub struct DaemonIdentity {
35 pub current_pubkey: [u8; 32],
37 pub pending_pubkey: Option<[u8; 32]>,
41 pub security_level: SecurityLevel,
43 pub generation: u64,
47}
48
49#[derive(Debug, Clone, Default)]
51pub struct ClearanceRegistry {
52 identities: HashMap<String, DaemonIdentity>,
54 pubkey_index: HashMap<[u8; 32], String>,
56}
57
58impl ClearanceRegistry {
59 #[must_use]
60 pub fn new() -> Self {
61 Self {
62 identities: HashMap::new(),
63 pubkey_index: HashMap::new(),
64 }
65 }
66
67 pub fn register(&mut self, name: String, pubkey: [u8; 32], level: SecurityLevel) {
70 if let Some(existing) = self.identities.get(&name) {
72 self.pubkey_index.remove(&existing.current_pubkey);
73 if let Some(pending) = existing.pending_pubkey {
74 self.pubkey_index.remove(&pending);
75 }
76 }
77
78 let identity = DaemonIdentity {
79 current_pubkey: pubkey,
80 pending_pubkey: None,
81 security_level: level,
82 generation: 0,
83 };
84 self.identities.insert(name.clone(), identity);
85 self.pubkey_index.insert(pubkey, name);
86 self.debug_assert_consistent();
87 }
88
89 pub fn register_with_generation(
91 &mut self,
92 name: String,
93 pubkey: [u8; 32],
94 level: SecurityLevel,
95 generation: u64,
96 ) {
97 if let Some(existing) = self.identities.get(&name) {
99 self.pubkey_index.remove(&existing.current_pubkey);
100 if let Some(pending) = existing.pending_pubkey {
101 self.pubkey_index.remove(&pending);
102 }
103 }
104
105 let identity = DaemonIdentity {
106 current_pubkey: pubkey,
107 pending_pubkey: None,
108 security_level: level,
109 generation,
110 };
111 self.identities.insert(name.clone(), identity);
112 self.pubkey_index.insert(pubkey, name);
113 self.debug_assert_consistent();
114 }
115
116 #[must_use]
119 pub fn lookup(&self, pubkey: &[u8; 32]) -> Option<&DaemonIdentity> {
120 let name = self.pubkey_index.get(pubkey)?;
121 self.identities.get(name)
122 }
123
124 #[must_use]
126 pub fn lookup_name(&self, pubkey: &[u8; 32]) -> Option<&str> {
127 self.pubkey_index.get(pubkey).map(String::as_str)
128 }
129
130 #[must_use]
132 pub fn find_by_name(&self, name: &str) -> Option<&DaemonIdentity> {
133 self.identities.get(name)
134 }
135
136 pub fn register_pending(&mut self, daemon_name: &str, new_pubkey: [u8; 32]) -> bool {
149 let Some(identity) = self.identities.get_mut(daemon_name) else {
150 return false;
151 };
152
153 if let Some(old_pending) = identity.pending_pubkey {
155 self.pubkey_index.remove(&old_pending);
156 }
157
158 identity.pending_pubkey = Some(new_pubkey);
159 self.pubkey_index.insert(new_pubkey, daemon_name.to_owned());
160 self.debug_assert_consistent();
161 true
162 }
163
164 pub fn finalize_rotation(&mut self, daemon_name: &str) -> bool {
170 let Some(identity) = self.identities.get_mut(daemon_name) else {
171 return false;
172 };
173
174 let Some(new_pubkey) = identity.pending_pubkey.take() else {
175 return false;
176 };
177
178 self.pubkey_index.remove(&identity.current_pubkey);
180
181 identity.current_pubkey = new_pubkey;
183 identity.generation += 1;
184 self.debug_assert_consistent();
187 true
188 }
189
190 pub fn revoke_by_name(&mut self, daemon_name: &str) -> Option<DaemonIdentity> {
197 let identity = self.identities.remove(daemon_name)?;
198
199 self.pubkey_index.remove(&identity.current_pubkey);
201 if let Some(pending) = identity.pending_pubkey {
202 self.pubkey_index.remove(&pending);
203 }
204
205 self.debug_assert_consistent();
206 Some(identity)
207 }
208
209 #[must_use]
211 pub fn snapshot_generations(&self) -> HashMap<String, u64> {
212 self.identities
213 .iter()
214 .map(|(name, id)| (name.clone(), id.generation))
215 .collect()
216 }
217
218 fn debug_assert_consistent(&self) {
226 #[cfg(debug_assertions)]
227 {
228 for (pubkey, name) in &self.pubkey_index {
230 let identity = self
231 .identities
232 .get(name)
233 .unwrap_or_else(|| panic!("pubkey_index points to nonexistent daemon: {name}"));
234 let matches_current = identity.current_pubkey == *pubkey;
235 let matches_pending = identity.pending_pubkey.as_ref() == Some(pubkey);
236 assert!(
237 matches_current || matches_pending,
238 "pubkey_index entry for {name} matches neither current nor pending pubkey"
239 );
240 }
241
242 for (name, identity) in &self.identities {
244 assert_eq!(
245 self.pubkey_index.get(&identity.current_pubkey),
246 Some(name),
247 "current_pubkey for {name} missing from pubkey_index"
248 );
249 if let Some(pending) = &identity.pending_pubkey {
250 assert_eq!(
251 self.pubkey_index.get(pending),
252 Some(name),
253 "pending_pubkey for {name} missing from pubkey_index"
254 );
255 }
256 }
257 }
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264 use core_types::SecurityLevel;
265
266 #[test]
267 fn register_and_lookup() {
268 let mut reg = ClearanceRegistry::new();
269 let key = [0xAA; 32];
270 reg.register("daemon-secrets".into(), key, SecurityLevel::SecretsOnly);
271 let identity = reg.lookup(&key).unwrap();
272 assert_eq!(identity.current_pubkey, key);
273 assert_eq!(identity.security_level, SecurityLevel::SecretsOnly);
274 assert_eq!(identity.generation, 0);
275 assert!(identity.pending_pubkey.is_none());
276 }
277
278 #[test]
279 fn lookup_name() {
280 let mut reg = ClearanceRegistry::new();
281 let key = [0xAA; 32];
282 reg.register("daemon-wm".into(), key, SecurityLevel::Internal);
283 assert_eq!(reg.lookup_name(&key), Some("daemon-wm"));
284 assert_eq!(reg.lookup_name(&[0xBB; 32]), None);
285 }
286
287 #[test]
288 fn lookup_miss() {
289 let reg = ClearanceRegistry::new();
290 assert!(reg.lookup(&[0xBB; 32]).is_none());
291 }
292
293 #[test]
294 fn find_by_name_deterministic() {
295 let mut reg = ClearanceRegistry::new();
296 let key = [0xAA; 32];
297 reg.register("daemon-wm".into(), key, SecurityLevel::Internal);
298 let identity = reg.find_by_name("daemon-wm").unwrap();
299 assert_eq!(identity.current_pubkey, key);
300 assert!(reg.find_by_name("nonexistent").is_none());
301 }
302
303 #[test]
304 fn register_overwrites_existing() {
305 let mut reg = ClearanceRegistry::new();
306 let key_a = [0xAA; 32];
307 let key_b = [0xBB; 32];
308 reg.register("daemon-wm".into(), key_a, SecurityLevel::Internal);
309 reg.register("daemon-wm".into(), key_b, SecurityLevel::SecretsOnly);
310
311 assert!(reg.lookup(&key_a).is_none());
313 let identity = reg.lookup(&key_b).unwrap();
315 assert_eq!(identity.security_level, SecurityLevel::SecretsOnly);
316 }
317
318 #[test]
319 fn register_pending_allows_dual_key_lookup() {
320 let mut reg = ClearanceRegistry::new();
321 let old_key = [0xAA; 32];
322 let new_key = [0xBB; 32];
323 reg.register("daemon-wm".into(), old_key, SecurityLevel::Internal);
324
325 assert!(reg.register_pending("daemon-wm", new_key));
326
327 let id_old = reg.lookup(&old_key).unwrap();
329 let id_new = reg.lookup(&new_key).unwrap();
330 assert_eq!(id_old.current_pubkey, old_key);
331 assert_eq!(id_new.current_pubkey, old_key);
332 assert_eq!(id_old.pending_pubkey, Some(new_key));
333 assert_eq!(id_new.pending_pubkey, Some(new_key));
334 assert_eq!(id_old.security_level, SecurityLevel::Internal);
335 assert_eq!(id_new.security_level, SecurityLevel::Internal);
336 }
337
338 #[test]
339 fn register_pending_returns_false_for_unknown_daemon() {
340 let mut reg = ClearanceRegistry::new();
341 assert!(!reg.register_pending("nonexistent", [0xFF; 32]));
342 }
343
344 #[test]
345 fn register_pending_clones_security_level() {
346 let mut reg = ClearanceRegistry::new();
347 reg.register(
348 "daemon-secrets".into(),
349 [0xAA; 32],
350 SecurityLevel::SecretsOnly,
351 );
352 reg.register_pending("daemon-secrets", [0xBB; 32]);
353
354 let identity = reg.lookup(&[0xBB; 32]).unwrap();
356 assert_eq!(identity.security_level, SecurityLevel::SecretsOnly);
357 }
358
359 #[test]
360 fn register_pending_replaces_existing_pending() {
361 let mut reg = ClearanceRegistry::new();
362 reg.register("daemon-wm".into(), [0xAA; 32], SecurityLevel::Internal);
363 reg.register_pending("daemon-wm", [0xBB; 32]);
364 reg.register_pending("daemon-wm", [0xCC; 32]);
365
366 assert!(reg.lookup(&[0xBB; 32]).is_none());
368 assert!(reg.lookup(&[0xCC; 32]).is_some());
370 assert!(reg.lookup(&[0xAA; 32]).is_some());
372 }
373
374 #[test]
375 fn finalize_rotation_promotes_pending() {
376 let mut reg = ClearanceRegistry::new();
377 let old_key = [0xAA; 32];
378 let new_key = [0xBB; 32];
379 reg.register("daemon-wm".into(), old_key, SecurityLevel::Internal);
380 reg.register_pending("daemon-wm", new_key);
381
382 assert!(reg.finalize_rotation("daemon-wm"));
383
384 let identity = reg.find_by_name("daemon-wm").unwrap();
385 assert_eq!(identity.current_pubkey, new_key);
386 assert!(identity.pending_pubkey.is_none());
387 }
388
389 #[test]
390 fn finalize_rotation_removes_old_from_index() {
391 let mut reg = ClearanceRegistry::new();
392 let old_key = [0xAA; 32];
393 let new_key = [0xBB; 32];
394 reg.register("daemon-wm".into(), old_key, SecurityLevel::Internal);
395 reg.register_pending("daemon-wm", new_key);
396
397 reg.finalize_rotation("daemon-wm");
398
399 assert!(reg.lookup(&old_key).is_none());
401 assert!(reg.lookup(&new_key).is_some());
403 }
404
405 #[test]
406 fn finalize_rotation_increments_generation() {
407 let mut reg = ClearanceRegistry::new();
408 reg.register("daemon-wm".into(), [0xAA; 32], SecurityLevel::Internal);
409 reg.register_pending("daemon-wm", [0xBB; 32]);
410 reg.finalize_rotation("daemon-wm");
411
412 assert_eq!(reg.find_by_name("daemon-wm").unwrap().generation, 1);
413 }
414
415 #[test]
416 fn finalize_rotation_returns_false_without_pending() {
417 let mut reg = ClearanceRegistry::new();
418 reg.register("daemon-wm".into(), [0xAA; 32], SecurityLevel::Internal);
419
420 assert!(!reg.finalize_rotation("daemon-wm"));
422 assert_eq!(reg.find_by_name("daemon-wm").unwrap().generation, 0);
424 }
425
426 #[test]
427 fn finalize_rotation_returns_false_for_unknown() {
428 let mut reg = ClearanceRegistry::new();
429 assert!(!reg.finalize_rotation("nonexistent"));
430 }
431
432 #[test]
433 fn revoke_by_name_removes_both_keys() {
434 let mut reg = ClearanceRegistry::new();
435 let old_key = [0xAA; 32];
436 let new_key = [0xBB; 32];
437 reg.register("daemon-wm".into(), old_key, SecurityLevel::Internal);
438 reg.register_pending("daemon-wm", new_key);
439
440 let revoked = reg.revoke_by_name("daemon-wm").unwrap();
441 assert_eq!(revoked.generation, 0);
442
443 assert!(reg.lookup(&old_key).is_none());
445 assert!(reg.lookup(&new_key).is_none());
446 assert!(reg.find_by_name("daemon-wm").is_none());
448 }
449
450 #[test]
451 fn revoke_by_name_returns_none_for_unknown() {
452 let mut reg = ClearanceRegistry::new();
453 assert!(reg.revoke_by_name("nonexistent").is_none());
454 }
455
456 #[test]
457 fn revoke_by_name_returns_identity_with_generation() {
458 let mut reg = ClearanceRegistry::new();
459 reg.register("daemon-wm".into(), [0xAA; 32], SecurityLevel::Internal);
460 reg.register_pending("daemon-wm", [0xBB; 32]);
461 reg.finalize_rotation("daemon-wm"); reg.register_pending("daemon-wm", [0xCC; 32]);
463 let revoked = reg.revoke_by_name("daemon-wm").unwrap();
466 assert_eq!(revoked.generation, 1);
467 }
468
469 #[test]
470 fn register_with_generation_preserves_counter() {
471 let mut reg = ClearanceRegistry::new();
472 let key = [0xDD; 32];
473 reg.register_with_generation("daemon-wm".into(), key, SecurityLevel::Internal, 5);
474 assert_eq!(reg.lookup(&key).unwrap().generation, 5);
475 }
476
477 #[test]
478 fn snapshot_generations_captures_all_daemons() {
479 let mut reg = ClearanceRegistry::new();
480 reg.register("daemon-wm".into(), [0xAA; 32], SecurityLevel::Internal);
481 reg.register(
482 "daemon-secrets".into(),
483 [0xBB; 32],
484 SecurityLevel::SecretsOnly,
485 );
486
487 reg.register_pending("daemon-wm", [0xCC; 32]);
489 reg.finalize_rotation("daemon-wm");
490
491 let snap = reg.snapshot_generations();
492 assert_eq!(snap["daemon-wm"], 1);
493 assert_eq!(snap["daemon-secrets"], 0);
494 }
495
496 #[test]
498 fn double_finalize_is_safe() {
499 let mut reg = ClearanceRegistry::new();
500 reg.register("daemon-wm".into(), [0xAA; 32], SecurityLevel::Internal);
501 reg.register_pending("daemon-wm", [0xBB; 32]);
502
503 assert!(reg.finalize_rotation("daemon-wm"));
504 assert!(!reg.finalize_rotation("daemon-wm"));
505 assert_eq!(reg.find_by_name("daemon-wm").unwrap().generation, 1);
506 }
507
508 #[test]
510 fn double_rotation_increments_generation_twice() {
511 let mut reg = ClearanceRegistry::new();
512 reg.register("daemon-wm".into(), [0xAA; 32], SecurityLevel::Internal);
513
514 reg.register_pending("daemon-wm", [0xBB; 32]);
515 reg.finalize_rotation("daemon-wm"); reg.register_pending("daemon-wm", [0xCC; 32]);
518 reg.finalize_rotation("daemon-wm"); let identity = reg.find_by_name("daemon-wm").unwrap();
521 assert_eq!(identity.generation, 2);
522 assert_eq!(identity.current_pubkey, [0xCC; 32]);
523 assert!(reg.lookup(&[0xAA; 32]).is_none());
524 assert!(reg.lookup(&[0xBB; 32]).is_none());
525 assert!(reg.lookup(&[0xCC; 32]).is_some());
526 }
527
528 #[test]
531 fn revoke_then_reregister_preserves_generation_continuity() {
532 let mut reg = ClearanceRegistry::new();
533 reg.register("daemon-wm".into(), [0xAA; 32], SecurityLevel::Internal);
534 reg.register_pending("daemon-wm", [0xBB; 32]);
535 reg.finalize_rotation("daemon-wm"); let revoked = reg.revoke_by_name("daemon-wm").unwrap();
538 assert_eq!(revoked.generation, 1);
539
540 reg.register_with_generation(
541 "daemon-wm".into(),
542 [0xCC; 32],
543 SecurityLevel::Internal,
544 revoked.generation + 1,
545 );
546 assert_eq!(reg.find_by_name("daemon-wm").unwrap().generation, 2);
547 }
548
549 #[test]
551 fn revoke_during_pending_cleans_both() {
552 let mut reg = ClearanceRegistry::new();
553 reg.register("daemon-wm".into(), [0xAA; 32], SecurityLevel::Internal);
554 reg.register_pending("daemon-wm", [0xBB; 32]);
555
556 reg.revoke_by_name("daemon-wm");
557
558 assert!(reg.lookup(&[0xAA; 32]).is_none());
559 assert!(reg.lookup(&[0xBB; 32]).is_none());
560 assert!(reg.find_by_name("daemon-wm").is_none());
561 }
562
563 #[test]
567 fn unregistered_keys_always_return_none() {
568 let mut reg = ClearanceRegistry::new();
569 reg.register("daemon-wm".into(), [0xAA; 32], SecurityLevel::Internal);
570 reg.register(
571 "daemon-secrets".into(),
572 [0xBB; 32],
573 SecurityLevel::SecretsOnly,
574 );
575
576 for byte in [
577 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xCC, 0xDD, 0xEE, 0xFF,
578 ] {
579 assert!(
580 reg.lookup(&[byte; 32]).is_none(),
581 "key [{byte:#04X}; 32] should not be in registry"
582 );
583 }
584 }
585
586 #[test]
588 fn find_by_name_tracks_through_rotation() {
589 let mut reg = ClearanceRegistry::new();
590 reg.register("daemon-wm".into(), [0xAA; 32], SecurityLevel::Internal);
591 reg.register_pending("daemon-wm", [0xBB; 32]);
592 reg.finalize_rotation("daemon-wm");
593
594 let identity = reg.find_by_name("daemon-wm").unwrap();
595 assert_eq!(identity.current_pubkey, [0xBB; 32]);
596 assert_eq!(identity.generation, 1);
597 }
598}