use crate::error::{TrazaeoError, TrazaeoResult};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::path::Path;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TrustPolicy {
allowed_keys: HashSet<String>,
revoked_keys: HashSet<String>,
audit_log: Vec<TrustDecision>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TrustDecision {
pub action: String,
pub key_id: String,
pub reason: String,
pub effective_at: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TrustStatus {
Trusted,
Revoked,
Untrusted,
}
impl TrustPolicy {
pub fn new() -> Self {
Self {
allowed_keys: HashSet::new(),
revoked_keys: HashSet::new(),
audit_log: Vec::new(),
}
}
pub fn allow_key(&mut self, key_id: &str) {
self.allow_key_with_reason(key_id, "allowlist", "unspecified");
}
pub fn revoke_key(&mut self, key_id: &str) {
self.revoke_key_with_reason(key_id, "revoke", "unspecified");
}
pub fn allow_key_with_reason(&mut self, key_id: &str, effective_at: &str, reason: &str) {
self.allowed_keys.insert(key_id.to_string());
self.audit_log.push(TrustDecision {
action: "allow".to_string(),
key_id: key_id.to_string(),
reason: reason.to_string(),
effective_at: effective_at.to_string(),
});
}
pub fn revoke_key_with_reason(&mut self, key_id: &str, effective_at: &str, reason: &str) {
self.revoked_keys.insert(key_id.to_string());
self.audit_log.push(TrustDecision {
action: "revoke".to_string(),
key_id: key_id.to_string(),
reason: reason.to_string(),
effective_at: effective_at.to_string(),
});
}
pub fn rotate_key(&mut self, old_key: &str, new_key: &str) {
self.revoke_key_with_reason(old_key, "rotate", "rotated to successor key");
self.allow_key_with_reason(new_key, "rotate", "rotated from predecessor key");
}
pub fn trust_status(&self, key_id: &str) -> TrustStatus {
if self.revoked_keys.contains(key_id) {
TrustStatus::Revoked
} else if self.allowed_keys.contains(key_id) {
TrustStatus::Trusted
} else {
TrustStatus::Untrusted
}
}
pub fn audit_log(&self) -> &[TrustDecision] {
&self.audit_log
}
pub fn save_to_path<P: AsRef<Path>>(&self, path: P) -> TrazaeoResult<()> {
let body = serde_json::to_vec_pretty(self).map_err(|e| {
TrazaeoError::serialization("save trust policy", format!("serialize trust policy: {e}"))
})?;
std::fs::write(path, body)
.map_err(|e| TrazaeoError::io("save trust policy", format!("write trust policy: {e}")))
}
pub fn load_from_path<P: AsRef<Path>>(path: P) -> TrazaeoResult<Self> {
let body = std::fs::read(path).map_err(|e| {
TrazaeoError::io("load trust policy", format!("read trust policy: {e}"))
})?;
serde_json::from_slice(&body).map_err(|e| {
TrazaeoError::serialization(
"load trust policy",
format!("deserialize trust policy: {e}"),
)
})
}
}
impl Default for TrustPolicy {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::NamedTempFile;
#[test]
fn trust_policy_allows_and_revokes_keys() {
let mut policy = TrustPolicy::new();
policy.allow_key("k1");
assert_eq!(policy.trust_status("k1"), TrustStatus::Trusted);
policy.revoke_key("k1");
assert_eq!(policy.trust_status("k1"), TrustStatus::Revoked);
}
#[test]
fn trust_policy_rotation_marks_old_revoked_and_new_trusted() {
let mut policy = TrustPolicy::new();
policy.allow_key("old");
policy.rotate_key("old", "new");
assert_eq!(policy.trust_status("old"), TrustStatus::Revoked);
assert_eq!(policy.trust_status("new"), TrustStatus::Trusted);
}
#[test]
fn unknown_key_is_untrusted() {
let policy = TrustPolicy::new();
assert_eq!(policy.trust_status("missing"), TrustStatus::Untrusted);
}
#[test]
fn trust_policy_persists_to_disk() {
let mut policy = TrustPolicy::new();
policy.allow_key("k1");
policy.revoke_key("k2");
let file = NamedTempFile::new().expect("temp file");
policy.save_to_path(file.path()).expect("save trust policy");
let loaded = TrustPolicy::load_from_path(file.path()).expect("load trust policy");
assert_eq!(loaded.trust_status("k1"), TrustStatus::Trusted);
assert_eq!(loaded.trust_status("k2"), TrustStatus::Revoked);
}
#[test]
fn trust_policy_records_audit_log_entries() {
let mut policy = TrustPolicy::new();
policy.allow_key_with_reason("k1", "2026-01-01T00:00:00Z", "bootstrap allow");
policy.revoke_key_with_reason("k1", "2026-01-02T00:00:00Z", "compromised");
assert_eq!(policy.audit_log().len(), 2);
assert_eq!(policy.audit_log()[0].action, "allow");
assert_eq!(policy.audit_log()[1].action, "revoke");
}
}