use chrono::{DateTime, Utc};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
compose_policy_outcomes, CoreError, CoreResult, PolicyContribution, PolicyDecision,
PolicyOutcome,
};
use crate::{FailingEdge, ProofEdgeFailure, ProofEdgeKind};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum KeyLifecycleState {
Active,
Retired,
Revoked,
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum TrustTier {
Untrusted,
Observed,
Verified,
Operator,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum TemporalAuthorityReason {
SignedBeforeActivation,
SignedAfterRevocation,
SignedAfterRetirement,
RevokedAfterSigning,
HistoricalRetiredKey,
TrustTierDowngraded,
InsufficientTrustAtSigning,
PrincipalRemoved,
TrustReviewExpired,
KeyUnknown,
PrincipalUnknown,
}
impl TemporalAuthorityReason {
#[must_use]
pub const fn wire_str(self) -> &'static str {
match self {
Self::SignedBeforeActivation => "signed_before_activation",
Self::SignedAfterRevocation => "signed_after_revocation",
Self::SignedAfterRetirement => "signed_after_retirement",
Self::RevokedAfterSigning => "revoked_after_signing",
Self::HistoricalRetiredKey => "historical_retired_key",
Self::TrustTierDowngraded => "trust_tier_downgraded",
Self::InsufficientTrustAtSigning => "insufficient_trust_at_signing",
Self::PrincipalRemoved => "principal_removed",
Self::TrustReviewExpired => "trust_review_expired",
Self::KeyUnknown => "key_unknown",
Self::PrincipalUnknown => "principal_unknown",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct TemporalAuthorityEvidence {
pub key_id: String,
pub principal_id: Option<String>,
pub event_time: DateTime<Utc>,
pub now: DateTime<Utc>,
pub key_activated_at: Option<DateTime<Utc>>,
pub key_retired_at: Option<DateTime<Utc>>,
pub key_revoked_at: Option<DateTime<Utc>>,
pub trust_tier_at_event_time: Option<TrustTier>,
pub current_trust_tier: Option<TrustTier>,
pub current_trust_tier_effective_at: Option<DateTime<Utc>>,
pub minimum_trust_tier: TrustTier,
pub principal_removed_at: Option<DateTime<Utc>>,
pub trust_review_due_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct TemporalAuthorityReport {
pub key_id: String,
pub principal_id: Option<String>,
pub event_time: DateTime<Utc>,
pub now: DateTime<Utc>,
pub valid_at_event_time: bool,
pub valid_now: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub invalidated_after: Option<DateTime<Utc>>,
pub reasons: Vec<TemporalAuthorityReason>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trust_tier_at_event_time: Option<TrustTier>,
#[serde(skip_serializing_if = "Option::is_none")]
pub current_trust_tier: Option<TrustTier>,
}
impl TemporalAuthorityReport {
#[must_use]
pub fn current_use_failing_edge(&self, target_ref: impl Into<String>) -> Option<FailingEdge> {
if self.valid_now {
return None;
}
let reason = self
.reasons
.iter()
.map(|reason| reason.wire_str())
.collect::<Vec<_>>()
.join(",");
Some(FailingEdge::broken(
ProofEdgeKind::AuthorityFold,
target_ref,
self.key_id.clone(),
ProofEdgeFailure::AuthorityMismatch,
reason,
))
}
#[must_use]
pub fn has_reason(&self, reason: TemporalAuthorityReason) -> bool {
self.reasons.contains(&reason)
}
#[must_use]
pub fn policy_decision(&self) -> PolicyDecision {
let outcome = if self.valid_now {
PolicyOutcome::Allow
} else if self.valid_at_event_time {
PolicyOutcome::Quarantine
} else {
PolicyOutcome::Reject
};
let reason = if self.valid_now {
"temporal authority is valid for current use"
} else if self.valid_at_event_time {
"temporal authority remains historical evidence but is invalid for current use"
} else {
"temporal authority was invalid at event time"
};
compose_policy_outcomes(
vec![
PolicyContribution::new("authority.temporal.current_use", outcome, reason)
.expect("static policy contribution is valid"),
],
None,
)
}
pub fn require_current_use_allowed(&self) -> CoreResult<()> {
let policy = self.policy_decision();
match policy.final_outcome {
PolicyOutcome::Reject | PolicyOutcome::Quarantine => {
Err(CoreError::Validation(format!(
"temporal authority current use blocked by policy outcome {:?}",
policy.final_outcome
)))
}
PolicyOutcome::Allow | PolicyOutcome::Warn | PolicyOutcome::BreakGlass => Ok(()),
}
}
}
#[must_use]
pub fn revalidate_temporal_authority(
evidence: TemporalAuthorityEvidence,
) -> TemporalAuthorityReport {
let mut valid_at_event_time = true;
let mut valid_now = true;
let mut invalidated_after = None;
let mut reasons = Vec::new();
match evidence.key_activated_at {
Some(activated_at) if evidence.event_time < activated_at => {
valid_at_event_time = false;
valid_now = false;
reasons.push(TemporalAuthorityReason::SignedBeforeActivation);
}
Some(_) => {}
None => {
valid_at_event_time = false;
valid_now = false;
reasons.push(TemporalAuthorityReason::KeyUnknown);
}
}
if let Some(revoked_at) = evidence.key_revoked_at {
if evidence.event_time >= revoked_at {
valid_at_event_time = false;
valid_now = false;
reasons.push(TemporalAuthorityReason::SignedAfterRevocation);
} else if evidence.now >= revoked_at {
valid_now = false;
invalidated_after = min_time(invalidated_after, revoked_at);
reasons.push(TemporalAuthorityReason::RevokedAfterSigning);
}
}
if let Some(retired_at) = evidence.key_retired_at {
if evidence.event_time >= retired_at {
valid_at_event_time = false;
valid_now = false;
reasons.push(TemporalAuthorityReason::SignedAfterRetirement);
} else if evidence.now >= retired_at {
reasons.push(TemporalAuthorityReason::HistoricalRetiredKey);
}
}
match evidence.trust_tier_at_event_time {
Some(tier) if tier < evidence.minimum_trust_tier => {
valid_at_event_time = false;
valid_now = false;
reasons.push(TemporalAuthorityReason::InsufficientTrustAtSigning);
}
Some(_) => {}
None => {
valid_at_event_time = false;
valid_now = false;
reasons.push(TemporalAuthorityReason::PrincipalUnknown);
}
}
match evidence.current_trust_tier {
Some(current) if current < evidence.minimum_trust_tier => {
valid_now = false;
if let Some(changed_at) = evidence.current_trust_tier_effective_at {
invalidated_after = min_time(invalidated_after, changed_at);
}
reasons.push(TemporalAuthorityReason::TrustTierDowngraded);
}
Some(_) => {}
None => {
valid_now = false;
reasons.push(TemporalAuthorityReason::PrincipalUnknown);
}
}
if let Some(removed_at) = evidence.principal_removed_at {
if evidence.now >= removed_at {
valid_now = false;
invalidated_after = min_time(invalidated_after, removed_at);
reasons.push(TemporalAuthorityReason::PrincipalRemoved);
}
}
if let Some(review_due_at) = evidence.trust_review_due_at {
if evidence.now > review_due_at {
valid_now = false;
invalidated_after = min_time(invalidated_after, review_due_at);
reasons.push(TemporalAuthorityReason::TrustReviewExpired);
}
}
TemporalAuthorityReport {
key_id: evidence.key_id,
principal_id: evidence.principal_id,
event_time: evidence.event_time,
now: evidence.now,
valid_at_event_time,
valid_now: valid_at_event_time && valid_now,
invalidated_after,
reasons,
trust_tier_at_event_time: evidence.trust_tier_at_event_time,
current_trust_tier: evidence.current_trust_tier,
}
}
fn min_time(current: Option<DateTime<Utc>>, candidate: DateTime<Utc>) -> Option<DateTime<Utc>> {
Some(current.map_or(candidate, |current| current.min(candidate)))
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone;
fn at(day: u32) -> DateTime<Utc> {
Utc.with_ymd_and_hms(2026, 1, day, 12, 0, 0).unwrap()
}
fn evidence() -> TemporalAuthorityEvidence {
TemporalAuthorityEvidence {
key_id: "key_1".into(),
principal_id: Some("principal_1".into()),
event_time: at(2),
now: at(4),
key_activated_at: Some(at(1)),
key_retired_at: None,
key_revoked_at: None,
trust_tier_at_event_time: Some(TrustTier::Operator),
current_trust_tier: Some(TrustTier::Operator),
current_trust_tier_effective_at: Some(at(1)),
minimum_trust_tier: TrustTier::Verified,
principal_removed_at: None,
trust_review_due_at: None,
}
}
#[test]
fn revoked_after_signing_is_historical_but_not_valid_now() {
let mut evidence = evidence();
evidence.key_revoked_at = Some(at(3));
let report = revalidate_temporal_authority(evidence);
assert!(report.valid_at_event_time);
assert!(!report.valid_now);
assert_eq!(report.invalidated_after, Some(at(3)));
assert!(report.has_reason(TemporalAuthorityReason::RevokedAfterSigning));
}
#[test]
fn signed_after_revocation_is_invalid_at_event_time() {
let mut evidence = evidence();
evidence.event_time = at(4);
evidence.key_revoked_at = Some(at(3));
let report = revalidate_temporal_authority(evidence);
assert!(!report.valid_at_event_time);
assert!(!report.valid_now);
assert!(report.has_reason(TemporalAuthorityReason::SignedAfterRevocation));
}
#[test]
fn trust_tier_downgrade_invalidates_current_use() {
let mut evidence = evidence();
evidence.current_trust_tier = Some(TrustTier::Observed);
let report = revalidate_temporal_authority(evidence);
assert!(report.valid_at_event_time);
assert!(!report.valid_now);
assert!(report.has_reason(TemporalAuthorityReason::TrustTierDowngraded));
assert!(report.current_use_failing_edge("principle:1").is_some());
assert_eq!(
report.policy_decision().final_outcome,
PolicyOutcome::Quarantine
);
assert!(report.require_current_use_allowed().is_err());
}
#[test]
fn invalid_at_event_time_maps_to_policy_reject() {
let mut evidence = evidence();
evidence.event_time = at(4);
evidence.key_revoked_at = Some(at(3));
let report = revalidate_temporal_authority(evidence);
assert_eq!(
report.policy_decision().final_outcome,
PolicyOutcome::Reject
);
assert!(report.require_current_use_allowed().is_err());
}
#[test]
fn currently_valid_authority_maps_to_policy_allow() {
let report = revalidate_temporal_authority(evidence());
assert_eq!(report.policy_decision().final_outcome, PolicyOutcome::Allow);
report
.require_current_use_allowed()
.expect("currently valid authority supports current use");
}
}