use crate::types::error::{Result, TypeError};
use serde::{Deserialize, Serialize};
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(kani, derive(kani::Arbitrary))]
pub enum KeyLifecycleState {
Generation,
Active,
Rotating,
Retired,
Destroyed,
}
pub struct KeyStateMachine;
impl KeyStateMachine {
#[must_use]
pub fn is_valid_transition(from: Option<KeyLifecycleState>, to: KeyLifecycleState) -> bool {
match (from, to) {
(None, KeyLifecycleState::Generation) => true,
(Some(KeyLifecycleState::Generation), KeyLifecycleState::Active) => true,
(Some(KeyLifecycleState::Active), KeyLifecycleState::Rotating) => true,
(Some(KeyLifecycleState::Rotating), KeyLifecycleState::Retired) => true,
(Some(KeyLifecycleState::Active), KeyLifecycleState::Retired) => true,
(Some(KeyLifecycleState::Retired), KeyLifecycleState::Destroyed) => true,
_ => false,
}
}
#[must_use]
pub fn allowed_next_states(current: KeyLifecycleState) -> Vec<KeyLifecycleState> {
match current {
KeyLifecycleState::Generation => vec![KeyLifecycleState::Active],
KeyLifecycleState::Active => {
vec![KeyLifecycleState::Rotating, KeyLifecycleState::Retired]
}
KeyLifecycleState::Rotating => vec![KeyLifecycleState::Retired],
KeyLifecycleState::Retired => vec![KeyLifecycleState::Destroyed],
KeyLifecycleState::Destroyed => vec![],
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeyCustodian {
pub custodian_id: String,
pub name: String,
pub role: CustodianRole,
pub responsibilities: Vec<String>,
pub approved_until: chrono::DateTime<chrono::Utc>,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CustodianRole {
KeyGenerator,
KeyApprover,
KeyDestroyer,
KeyAuditor,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeyLifecycleRecord {
pub key_id: String,
pub key_type: String,
pub security_level: u32,
pub current_state: KeyLifecycleState,
pub state_history: Vec<StateTransition>,
pub generator: Option<String>,
pub approvers: Vec<String>,
pub destroyer: Option<String>,
pub generated_at: chrono::DateTime<chrono::Utc>,
pub activated_at: Option<chrono::DateTime<chrono::Utc>>,
pub rotated_at: Option<chrono::DateTime<chrono::Utc>>,
pub retired_at: Option<chrono::DateTime<chrono::Utc>>,
pub destroyed_at: Option<chrono::DateTime<chrono::Utc>>,
pub rotation_interval_days: u32,
pub overlap_period_days: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StateTransition {
pub from_state: Option<KeyLifecycleState>,
pub to_state: KeyLifecycleState,
pub timestamp: chrono::DateTime<chrono::Utc>,
pub custodian_id: String,
pub justification: String,
pub approval_id: Option<String>,
}
impl KeyLifecycleRecord {
#[must_use]
pub fn new(
key_id: String,
key_type: String,
security_level: u32,
rotation_interval_days: u32,
overlap_period_days: u32,
) -> Self {
Self {
key_id,
key_type,
security_level,
current_state: KeyLifecycleState::Generation,
state_history: Vec::new(),
generator: None,
approvers: Vec::new(),
destroyer: None,
generated_at: chrono::Utc::now(),
activated_at: None,
rotated_at: None,
retired_at: None,
destroyed_at: None,
rotation_interval_days,
overlap_period_days,
}
}
pub fn transition(
&mut self,
to_state: KeyLifecycleState,
custodian_id: String,
justification: String,
approval_id: Option<String>,
) -> Result<()> {
if !KeyStateMachine::is_valid_transition(Some(self.current_state), to_state) {
return Err(TypeError::InvalidStateTransition {
from: self.current_state,
to: to_state,
});
}
let transition = StateTransition {
from_state: Some(self.current_state),
to_state,
timestamp: chrono::Utc::now(),
custodian_id: custodian_id.clone(),
justification,
approval_id,
};
self.state_history.push(transition);
self.current_state = to_state;
match to_state {
KeyLifecycleState::Active => self.activated_at = Some(chrono::Utc::now()),
KeyLifecycleState::Rotating => self.rotated_at = Some(chrono::Utc::now()),
KeyLifecycleState::Retired => self.retired_at = Some(chrono::Utc::now()),
KeyLifecycleState::Destroyed => self.destroyed_at = Some(chrono::Utc::now()),
_ => {}
}
if let Some(last_transition) = self.state_history.last()
&& last_transition.from_state == Some(KeyLifecycleState::Generation)
&& to_state == KeyLifecycleState::Active
{
self.generator = Some(custodian_id.clone());
}
if to_state == KeyLifecycleState::Destroyed {
self.destroyer = Some(custodian_id);
}
Ok(())
}
#[must_use]
pub fn requires_rotation(&self) -> bool {
if let Some(activated_at) = self.activated_at {
let duration = chrono::Utc::now().signed_duration_since(activated_at);
let age_days_i64 = duration.num_days();
if age_days_i64 < 0 {
tracing::warn!(
"Key has negative age ({age_days_i64} days); activation_at in the future"
);
return false;
}
let age_days = u32::try_from(age_days_i64).unwrap_or(u32::MAX);
age_days >= self.rotation_interval_days
} else {
false
}
}
#[must_use]
pub fn age_days(&self) -> Option<u32> {
self.activated_at.map(|activated| {
let duration = chrono::Utc::now().signed_duration_since(activated);
let days_i64 = duration.num_days();
u32::try_from(days_i64).unwrap_or(0)
})
}
#[must_use]
pub fn is_valid_for_use(&self) -> bool {
matches!(self.current_state, KeyLifecycleState::Active | KeyLifecycleState::Rotating)
}
#[must_use]
pub fn transition_count(&self) -> usize {
self.state_history.len()
}
pub fn add_approver(&mut self, approver_id: impl Into<String>) {
let approver_id = approver_id.into();
if !self.approvers.contains(&approver_id) {
self.approvers.push(approver_id);
}
}
}
#[cfg(kani)]
mod kani_proofs {
use super::*;
#[kani::proof]
fn key_state_machine_destroyed_cannot_transition() {
let to: KeyLifecycleState = kani::any();
let is_valid = KeyStateMachine::is_valid_transition(Some(KeyLifecycleState::Destroyed), to);
kani::assert(!is_valid, "Destroyed keys should not transition");
}
#[kani::proof]
fn key_state_machine_no_backward_to_generation() {
let from: KeyLifecycleState = kani::any();
kani::assume(from != KeyLifecycleState::Generation);
let is_valid =
KeyStateMachine::is_valid_transition(Some(from), KeyLifecycleState::Generation);
kani::assert(!is_valid, "Cannot transition back to Generation");
}
#[kani::proof]
fn key_state_machine_only_generation_from_none() {
let to: KeyLifecycleState = kani::any();
kani::assume(to != KeyLifecycleState::Generation);
let is_valid = KeyStateMachine::is_valid_transition(None, to);
kani::assert(!is_valid, "Only Generation is valid from initial state (None)");
}
#[kani::proof]
fn key_state_machine_transitions_match_spec() {
let from: KeyLifecycleState = kani::any();
let to: KeyLifecycleState = kani::any();
let transition_valid = KeyStateMachine::is_valid_transition(Some(from), to);
let spec_allows = match from {
KeyLifecycleState::Generation => to == KeyLifecycleState::Active,
KeyLifecycleState::Active => {
to == KeyLifecycleState::Rotating || to == KeyLifecycleState::Retired
}
KeyLifecycleState::Rotating => to == KeyLifecycleState::Retired,
KeyLifecycleState::Retired => to == KeyLifecycleState::Destroyed,
KeyLifecycleState::Destroyed => false,
};
kani::assert(
transition_valid == spec_allows,
"is_valid_transition must match SP 800-57 specification",
);
}
#[kani::proof]
fn key_state_machine_retired_only_to_destroyed() {
let to: KeyLifecycleState = kani::any();
kani::assume(to != KeyLifecycleState::Destroyed);
let is_valid = KeyStateMachine::is_valid_transition(Some(KeyLifecycleState::Retired), to);
kani::assert(!is_valid, "Retired keys can only transition to Destroyed");
}
}
#[cfg(test)]
#[allow(
clippy::panic,
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
clippy::arithmetic_side_effects,
clippy::panic_in_result_fn,
clippy::unnecessary_wraps,
clippy::redundant_clone,
clippy::useless_vec,
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::clone_on_copy,
clippy::len_zero,
clippy::single_match,
clippy::unnested_or_patterns,
clippy::default_constructed_unit_structs,
clippy::redundant_closure_for_method_calls,
clippy::semicolon_if_nothing_returned,
clippy::unnecessary_unwrap,
clippy::redundant_pattern_matching,
clippy::missing_const_for_thread_local,
clippy::get_first,
clippy::float_cmp,
clippy::needless_borrows_for_generic_args,
unused_qualifications
)]
mod tests {
use super::*;
#[test]
fn test_valid_state_transitions_succeeds() {
assert!(KeyStateMachine::is_valid_transition(
Some(KeyLifecycleState::Generation),
KeyLifecycleState::Active
));
assert!(KeyStateMachine::is_valid_transition(
Some(KeyLifecycleState::Active),
KeyLifecycleState::Rotating
));
assert!(KeyStateMachine::is_valid_transition(
Some(KeyLifecycleState::Rotating),
KeyLifecycleState::Retired
));
assert!(KeyStateMachine::is_valid_transition(
Some(KeyLifecycleState::Retired),
KeyLifecycleState::Destroyed
));
assert!(KeyStateMachine::is_valid_transition(
Some(KeyLifecycleState::Active),
KeyLifecycleState::Retired
));
}
#[test]
fn test_invalid_state_transitions_fails() {
assert!(!KeyStateMachine::is_valid_transition(
Some(KeyLifecycleState::Active),
KeyLifecycleState::Generation
));
assert!(!KeyStateMachine::is_valid_transition(
Some(KeyLifecycleState::Generation),
KeyLifecycleState::Destroyed
));
assert!(!KeyStateMachine::is_valid_transition(
Some(KeyLifecycleState::Destroyed),
KeyLifecycleState::Active
));
}
#[test]
fn test_allowed_next_states_succeeds() {
assert_eq!(
KeyStateMachine::allowed_next_states(KeyLifecycleState::Generation),
vec![KeyLifecycleState::Active]
);
assert_eq!(
KeyStateMachine::allowed_next_states(KeyLifecycleState::Active),
vec![KeyLifecycleState::Rotating, KeyLifecycleState::Retired]
);
assert_eq!(KeyStateMachine::allowed_next_states(KeyLifecycleState::Destroyed), vec![]);
}
#[test]
fn test_key_lifecycle_record_succeeds() {
let mut record = KeyLifecycleRecord::new(
"test-key-123".to_string(),
"ML-KEM-768".to_string(),
3,
365,
30,
);
assert_eq!(record.current_state, KeyLifecycleState::Generation);
assert!(!record.is_valid_for_use());
record
.transition(
KeyLifecycleState::Active,
"alice".to_string(),
"Key generation complete".to_string(),
Some("approval-123".to_string()),
)
.unwrap();
assert_eq!(record.current_state, KeyLifecycleState::Active);
assert!(record.is_valid_for_use());
assert_eq!(record.generator, Some("alice".to_string()));
assert!(record.activated_at.is_some());
assert!(!record.requires_rotation());
}
#[test]
fn test_rotation_requirement_succeeds() {
let mut record = KeyLifecycleRecord::new(
"test-key-123".to_string(),
"AES-256".to_string(),
3,
90, 7,
);
record.activated_at = Some(chrono::Utc::now() - chrono::Duration::days(100));
assert!(record.requires_rotation());
assert_eq!(record.age_days(), Some(100));
}
#[test]
fn test_transition_validation_succeeds() {
let mut record = KeyLifecycleRecord::new(
"test-key-123".to_string(),
"ML-DSA-65".to_string(),
3,
365,
30,
);
let result = record.transition(
KeyLifecycleState::Destroyed, "alice".to_string(),
"Invalid transition".to_string(),
None,
);
assert!(result.is_err());
}
#[test]
fn test_add_approver_succeeds() {
let mut record = KeyLifecycleRecord::new(
"test-key-123".to_string(),
"ML-KEM-768".to_string(),
3,
365,
30,
);
record.add_approver("alice".to_string());
record.add_approver("bob".to_string());
record.add_approver("alice".to_string());
assert_eq!(record.approvers.len(), 2);
assert!(record.approvers.contains(&"alice".to_string()));
assert!(record.approvers.contains(&"bob".to_string()));
}
}