chie_crypto/
key_rotation_scheduler.rs

1//! Key rotation scheduler with configurable policies.
2//!
3//! This module provides automated key rotation scheduling with various policies
4//! for different key types. It supports time-based, usage-based, and event-driven
5//! rotation strategies.
6//!
7//! # Key Rotation Policies
8//!
9//! - **Time-based**: Rotate keys after a specific duration (e.g., every 30 days)
10//! - **Usage-based**: Rotate keys after a certain number of operations
11//! - **Hybrid**: Combine time and usage limits (whichever comes first)
12//! - **Event-driven**: Rotate on specific events (e.g., suspected compromise)
13//!
14//! # Example
15//!
16//! ```
17//! use chie_crypto::key_rotation_scheduler::*;
18//! use std::time::Duration;
19//!
20//! // Create a time-based rotation policy (rotate every 30 days)
21//! let policy = KeyRotationPolicy::TimeBased {
22//!     max_age: Duration::from_secs(30 * 24 * 3600),
23//! };
24//!
25//! // Create scheduler
26//! let mut scheduler = KeyRotationScheduler::new(policy);
27//!
28//! // Register a key
29//! let key_id = "signing-key-001".to_string();
30//! scheduler.register_key(key_id.clone());
31//!
32//! // Check if rotation is needed
33//! if scheduler.should_rotate(&key_id) {
34//!     println!("Time to rotate key: {}", key_id);
35//!     // Perform rotation...
36//!     scheduler.mark_rotated(&key_id);
37//! }
38//! ```
39
40use serde::{Deserialize, Serialize};
41use std::collections::HashMap;
42use std::time::{Duration, SystemTime};
43
44/// Key rotation policy for scheduler
45#[derive(Clone, Debug, Serialize, Deserialize)]
46pub enum KeyRotationPolicy {
47    /// Rotate after a specific time period
48    TimeBased {
49        /// Maximum age before rotation required
50        max_age: Duration,
51    },
52    /// Rotate after a certain number of operations
53    UsageBased {
54        /// Maximum number of operations before rotation
55        max_operations: u64,
56    },
57    /// Rotate based on both time and usage (whichever comes first)
58    Hybrid {
59        /// Maximum age before rotation required
60        max_age: Duration,
61        /// Maximum number of operations before rotation
62        max_operations: u64,
63    },
64    /// Manual rotation only (no automatic triggers)
65    Manual,
66}
67
68impl KeyRotationPolicy {
69    /// Create a time-based policy with specified duration
70    pub fn time_based(max_age: Duration) -> Self {
71        Self::TimeBased { max_age }
72    }
73
74    /// Create a usage-based policy with specified operation limit
75    pub fn usage_based(max_operations: u64) -> Self {
76        Self::UsageBased { max_operations }
77    }
78
79    /// Create a hybrid policy with both time and usage limits
80    pub fn hybrid(max_age: Duration, max_operations: u64) -> Self {
81        Self::Hybrid {
82            max_age,
83            max_operations,
84        }
85    }
86
87    /// Create a manual-only policy
88    pub fn manual() -> Self {
89        Self::Manual
90    }
91}
92
93/// Key metadata for rotation tracking
94#[derive(Clone, Debug, Serialize, Deserialize)]
95pub struct KeyMetadata {
96    /// Unique identifier for the key
97    pub key_id: String,
98    /// When the key was created or last rotated
99    pub created_at: SystemTime,
100    /// Number of operations performed with this key
101    pub operation_count: u64,
102    /// Whether the key is currently active
103    pub active: bool,
104    /// Optional expiration time
105    pub expires_at: Option<SystemTime>,
106}
107
108impl KeyMetadata {
109    /// Create new key metadata
110    pub fn new(key_id: String) -> Self {
111        Self {
112            key_id,
113            created_at: SystemTime::now(),
114            operation_count: 0,
115            active: true,
116            expires_at: None,
117        }
118    }
119
120    /// Get the age of the key
121    pub fn age(&self) -> Duration {
122        SystemTime::now()
123            .duration_since(self.created_at)
124            .unwrap_or(Duration::ZERO)
125    }
126
127    /// Check if the key has expired
128    pub fn is_expired(&self) -> bool {
129        if let Some(expires_at) = self.expires_at {
130            SystemTime::now() >= expires_at
131        } else {
132            false
133        }
134    }
135
136    /// Increment operation counter
137    pub fn increment_operations(&mut self) {
138        self.operation_count += 1;
139    }
140
141    /// Mark key as rotated (reset counters, update timestamp)
142    pub fn mark_rotated(&mut self) {
143        self.created_at = SystemTime::now();
144        self.operation_count = 0;
145    }
146
147    /// Mark key as inactive (after rotation)
148    pub fn deactivate(&mut self) {
149        self.active = false;
150    }
151}
152
153/// Key rotation scheduler
154pub struct KeyRotationScheduler {
155    /// Rotation policy
156    policy: KeyRotationPolicy,
157    /// Tracked keys
158    keys: HashMap<String, KeyMetadata>,
159    /// Grace period after rotation trigger before forcing rotation
160    grace_period: Option<Duration>,
161}
162
163impl KeyRotationScheduler {
164    /// Create a new scheduler with the specified policy
165    pub fn new(policy: KeyRotationPolicy) -> Self {
166        Self {
167            policy,
168            keys: HashMap::new(),
169            grace_period: None,
170        }
171    }
172
173    /// Set grace period for rotation
174    pub fn with_grace_period(mut self, grace_period: Duration) -> Self {
175        self.grace_period = Some(grace_period);
176        self
177    }
178
179    /// Register a new key for tracking
180    pub fn register_key(&mut self, key_id: String) -> &KeyMetadata {
181        self.keys
182            .entry(key_id.clone())
183            .or_insert_with(|| KeyMetadata::new(key_id.clone()));
184        &self.keys[&key_id]
185    }
186
187    /// Register a key with custom metadata
188    pub fn register_key_with_metadata(&mut self, metadata: KeyMetadata) {
189        self.keys.insert(metadata.key_id.clone(), metadata);
190    }
191
192    /// Record an operation for a key
193    pub fn record_operation(&mut self, key_id: &str) -> Result<(), String> {
194        let metadata = self
195            .keys
196            .get_mut(key_id)
197            .ok_or_else(|| format!("Key not registered: {}", key_id))?;
198
199        if !metadata.active {
200            return Err(format!("Key is inactive: {}", key_id));
201        }
202
203        metadata.increment_operations();
204        Ok(())
205    }
206
207    /// Check if a key should be rotated according to the policy
208    pub fn should_rotate(&self, key_id: &str) -> bool {
209        let metadata = match self.keys.get(key_id) {
210            Some(m) => m,
211            None => return false,
212        };
213
214        // Inactive keys don't need rotation
215        if !metadata.active {
216            return false;
217        }
218
219        // Check expiration
220        if metadata.is_expired() {
221            return true;
222        }
223
224        // Check policy-specific triggers
225        match &self.policy {
226            KeyRotationPolicy::TimeBased { max_age } => metadata.age() >= *max_age,
227            KeyRotationPolicy::UsageBased { max_operations } => {
228                metadata.operation_count >= *max_operations
229            }
230            KeyRotationPolicy::Hybrid {
231                max_age,
232                max_operations,
233            } => metadata.age() >= *max_age || metadata.operation_count >= *max_operations,
234            KeyRotationPolicy::Manual => false,
235        }
236    }
237
238    /// Force rotation check (ignore grace period)
239    pub fn must_rotate(&self, key_id: &str) -> bool {
240        if !self.should_rotate(key_id) {
241            return false;
242        }
243
244        // If there's a grace period, check if we're past it
245        if let Some(grace_period) = self.grace_period {
246            let metadata = self.keys.get(key_id).unwrap();
247            let time_since_trigger = match &self.policy {
248                KeyRotationPolicy::TimeBased { max_age } => metadata.age().saturating_sub(*max_age),
249                KeyRotationPolicy::Hybrid { max_age, .. } => {
250                    metadata.age().saturating_sub(*max_age)
251                }
252                _ => Duration::ZERO,
253            };
254            time_since_trigger >= grace_period
255        } else {
256            true
257        }
258    }
259
260    /// Mark a key as rotated
261    pub fn mark_rotated(&mut self, key_id: &str) -> Result<(), String> {
262        let metadata = self
263            .keys
264            .get_mut(key_id)
265            .ok_or_else(|| format!("Key not registered: {}", key_id))?;
266
267        metadata.mark_rotated();
268        Ok(())
269    }
270
271    /// Deactivate a key (after successful rotation)
272    pub fn deactivate_key(&mut self, key_id: &str) -> Result<(), String> {
273        let metadata = self
274            .keys
275            .get_mut(key_id)
276            .ok_or_else(|| format!("Key not registered: {}", key_id))?;
277
278        metadata.deactivate();
279        Ok(())
280    }
281
282    /// Get all keys that need rotation
283    pub fn keys_needing_rotation(&self) -> Vec<String> {
284        self.keys
285            .iter()
286            .filter(|(key_id, _)| self.should_rotate(key_id))
287            .map(|(key_id, _)| key_id.clone())
288            .collect()
289    }
290
291    /// Get all keys that must be rotated immediately
292    pub fn keys_requiring_immediate_rotation(&self) -> Vec<String> {
293        self.keys
294            .iter()
295            .filter(|(key_id, _)| self.must_rotate(key_id))
296            .map(|(key_id, _)| key_id.clone())
297            .collect()
298    }
299
300    /// Get metadata for a key
301    pub fn get_metadata(&self, key_id: &str) -> Option<&KeyMetadata> {
302        self.keys.get(key_id)
303    }
304
305    /// Get all active keys
306    pub fn active_keys(&self) -> Vec<String> {
307        self.keys
308            .iter()
309            .filter(|(_, metadata)| metadata.active)
310            .map(|(key_id, _)| key_id.clone())
311            .collect()
312    }
313
314    /// Get rotation policy
315    pub fn policy(&self) -> &KeyRotationPolicy {
316        &self.policy
317    }
318}
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323    use std::thread::sleep;
324
325    #[test]
326    fn test_time_based_policy() {
327        let policy = KeyRotationPolicy::time_based(Duration::from_millis(100));
328        let mut scheduler = KeyRotationScheduler::new(policy);
329
330        let key_id = "test-key".to_string();
331        scheduler.register_key(key_id.clone());
332
333        // Should not need rotation immediately
334        assert!(!scheduler.should_rotate(&key_id));
335
336        // Wait for policy duration
337        sleep(Duration::from_millis(150));
338
339        // Should need rotation now
340        assert!(scheduler.should_rotate(&key_id));
341    }
342
343    #[test]
344    fn test_usage_based_policy() {
345        let policy = KeyRotationPolicy::usage_based(5);
346        let mut scheduler = KeyRotationScheduler::new(policy);
347
348        let key_id = "test-key".to_string();
349        scheduler.register_key(key_id.clone());
350
351        // Perform operations
352        for _ in 0..4 {
353            scheduler.record_operation(&key_id).unwrap();
354        }
355
356        // Should not need rotation yet
357        assert!(!scheduler.should_rotate(&key_id));
358
359        // One more operation
360        scheduler.record_operation(&key_id).unwrap();
361
362        // Should need rotation now
363        assert!(scheduler.should_rotate(&key_id));
364    }
365
366    #[test]
367    fn test_hybrid_policy() {
368        let policy = KeyRotationPolicy::hybrid(Duration::from_millis(100), 10);
369        let mut scheduler = KeyRotationScheduler::new(policy);
370
371        let key_id = "test-key".to_string();
372        scheduler.register_key(key_id.clone());
373
374        // Test time trigger
375        sleep(Duration::from_millis(150));
376        assert!(scheduler.should_rotate(&key_id));
377
378        // Reset
379        scheduler.mark_rotated(&key_id).unwrap();
380
381        // Test usage trigger
382        for _ in 0..10 {
383            scheduler.record_operation(&key_id).unwrap();
384        }
385        assert!(scheduler.should_rotate(&key_id));
386    }
387
388    #[test]
389    fn test_manual_policy() {
390        let policy = KeyRotationPolicy::manual();
391        let mut scheduler = KeyRotationScheduler::new(policy);
392
393        let key_id = "test-key".to_string();
394        scheduler.register_key(key_id.clone());
395
396        // Perform many operations
397        for _ in 0..1000 {
398            scheduler.record_operation(&key_id).unwrap();
399        }
400
401        // Should never trigger automatic rotation
402        assert!(!scheduler.should_rotate(&key_id));
403    }
404
405    #[test]
406    fn test_grace_period() {
407        let policy = KeyRotationPolicy::time_based(Duration::from_millis(50));
408        let mut scheduler =
409            KeyRotationScheduler::new(policy).with_grace_period(Duration::from_millis(50));
410
411        let key_id = "test-key".to_string();
412        scheduler.register_key(key_id.clone());
413
414        sleep(Duration::from_millis(75));
415
416        // Should rotate but not must rotate (within grace period)
417        assert!(scheduler.should_rotate(&key_id));
418        assert!(!scheduler.must_rotate(&key_id));
419
420        sleep(Duration::from_millis(50));
421
422        // Now must rotate (past grace period)
423        assert!(scheduler.must_rotate(&key_id));
424    }
425
426    #[test]
427    fn test_deactivate_key() {
428        let policy = KeyRotationPolicy::manual();
429        let mut scheduler = KeyRotationScheduler::new(policy);
430
431        let key_id = "test-key".to_string();
432        scheduler.register_key(key_id.clone());
433
434        // Key should be active
435        let metadata = scheduler.get_metadata(&key_id).unwrap();
436        assert!(metadata.active);
437
438        // Deactivate
439        scheduler.deactivate_key(&key_id).unwrap();
440
441        // Should be inactive now
442        let metadata = scheduler.get_metadata(&key_id).unwrap();
443        assert!(!metadata.active);
444
445        // Should not be able to record operations
446        assert!(scheduler.record_operation(&key_id).is_err());
447    }
448
449    #[test]
450    fn test_keys_needing_rotation() {
451        let policy = KeyRotationPolicy::usage_based(5);
452        let mut scheduler = KeyRotationScheduler::new(policy);
453
454        scheduler.register_key("key1".to_string());
455        scheduler.register_key("key2".to_string());
456        scheduler.register_key("key3".to_string());
457
458        // Trigger rotation for key1 and key3
459        for _ in 0..5 {
460            scheduler.record_operation("key1").unwrap();
461            scheduler.record_operation("key3").unwrap();
462        }
463
464        let keys = scheduler.keys_needing_rotation();
465        assert_eq!(keys.len(), 2);
466        assert!(keys.contains(&"key1".to_string()));
467        assert!(keys.contains(&"key3".to_string()));
468    }
469
470    #[test]
471    fn test_mark_rotated_resets_counters() {
472        let policy = KeyRotationPolicy::usage_based(5);
473        let mut scheduler = KeyRotationScheduler::new(policy);
474
475        let key_id = "test-key".to_string();
476        scheduler.register_key(key_id.clone());
477
478        // Trigger rotation
479        for _ in 0..5 {
480            scheduler.record_operation(&key_id).unwrap();
481        }
482        assert!(scheduler.should_rotate(&key_id));
483
484        // Mark as rotated
485        scheduler.mark_rotated(&key_id).unwrap();
486
487        // Should no longer need rotation
488        assert!(!scheduler.should_rotate(&key_id));
489
490        // Operation count should be reset
491        let metadata = scheduler.get_metadata(&key_id).unwrap();
492        assert_eq!(metadata.operation_count, 0);
493    }
494
495    #[test]
496    fn test_active_keys() {
497        let policy = KeyRotationPolicy::manual();
498        let mut scheduler = KeyRotationScheduler::new(policy);
499
500        scheduler.register_key("key1".to_string());
501        scheduler.register_key("key2".to_string());
502        scheduler.register_key("key3".to_string());
503
504        assert_eq!(scheduler.active_keys().len(), 3);
505
506        scheduler.deactivate_key("key2").unwrap();
507
508        let active = scheduler.active_keys();
509        assert_eq!(active.len(), 2);
510        assert!(!active.contains(&"key2".to_string()));
511    }
512
513    #[test]
514    fn test_key_expiration() {
515        let mut metadata = KeyMetadata::new("test-key".to_string());
516
517        // Set expiration in the past
518        metadata.expires_at = Some(SystemTime::now() - Duration::from_secs(1));
519
520        assert!(metadata.is_expired());
521
522        // Set expiration in the future
523        metadata.expires_at = Some(SystemTime::now() + Duration::from_secs(3600));
524
525        assert!(!metadata.is_expired());
526    }
527
528    #[test]
529    fn test_policy_serialization() {
530        let policy = KeyRotationPolicy::hybrid(Duration::from_secs(3600), 1000);
531
532        let serialized = crate::codec::encode(&policy).unwrap();
533        let deserialized: KeyRotationPolicy = crate::codec::decode(&serialized).unwrap();
534
535        match deserialized {
536            KeyRotationPolicy::Hybrid {
537                max_age,
538                max_operations,
539            } => {
540                assert_eq!(max_age, Duration::from_secs(3600));
541                assert_eq!(max_operations, 1000);
542            }
543            _ => panic!("Wrong policy type"),
544        }
545    }
546
547    #[test]
548    fn test_metadata_serialization() {
549        let metadata = KeyMetadata::new("test-key".to_string());
550
551        let serialized = crate::codec::encode(&metadata).unwrap();
552        let deserialized: KeyMetadata = crate::codec::decode(&serialized).unwrap();
553
554        assert_eq!(metadata.key_id, deserialized.key_id);
555        assert_eq!(metadata.active, deserialized.active);
556    }
557}