use std::time::Duration;
#[derive(Debug, Clone)]
pub struct AirGappedConfig {
pub max_signature_age: Option<Duration>,
pub check_revocations: bool,
pub max_chain_depth: u8,
pub identity_requirements: Option<IdentityRequirements>,
pub grace_period_behavior: GracePeriodBehavior,
pub enforce_rollback_protection: bool,
}
impl Default for AirGappedConfig {
fn default() -> Self {
Self {
max_signature_age: None,
check_revocations: true,
max_chain_depth: 4,
identity_requirements: None,
grace_period_behavior: GracePeriodBehavior::WarnDuringGrace,
enforce_rollback_protection: false,
}
}
}
impl AirGappedConfig {
pub fn fully_airgapped() -> Self {
Self {
max_signature_age: None,
check_revocations: true,
max_chain_depth: 4,
identity_requirements: None,
grace_period_behavior: GracePeriodBehavior::WarnOnly,
enforce_rollback_protection: false,
}
}
pub fn intermittent() -> Self {
Self {
max_signature_age: Some(Duration::from_secs(90 * 24 * 3600)), check_revocations: true,
max_chain_depth: 4,
identity_requirements: None,
grace_period_behavior: GracePeriodBehavior::WarnDuringGrace,
enforce_rollback_protection: true,
}
}
pub fn high_security() -> Self {
Self {
max_signature_age: Some(Duration::from_secs(7 * 24 * 3600)), check_revocations: true,
max_chain_depth: 2,
identity_requirements: None,
grace_period_behavior: GracePeriodBehavior::Strict,
enforce_rollback_protection: true,
}
}
pub fn with_max_age(mut self, age: Duration) -> Self {
self.max_signature_age = Some(age);
self
}
pub fn with_identity_requirements(mut self, requirements: IdentityRequirements) -> Self {
self.identity_requirements = Some(requirements);
self
}
pub fn with_rollback_protection(mut self) -> Self {
self.enforce_rollback_protection = true;
self
}
}
#[derive(Debug, Clone)]
pub struct IdentityRequirements {
pub allowed_issuers: Vec<String>,
pub allowed_subjects: Vec<String>,
}
impl IdentityRequirements {
pub fn github_actions(org: &str) -> Self {
Self {
allowed_issuers: vec!["https://token.actions.githubusercontent.com".to_string()],
allowed_subjects: vec![format!("https://github.com/{}/*", org)],
}
}
pub fn matches_issuer(&self, issuer: &str) -> bool {
self.allowed_issuers.iter().any(|pattern| {
if pattern.contains('*') {
glob_match(pattern, issuer)
} else {
pattern == issuer
}
})
}
pub fn matches_subject(&self, subject: &str) -> bool {
self.allowed_subjects.iter().any(|pattern| {
if pattern.contains('*') {
glob_match(pattern, subject)
} else {
pattern == subject
}
})
}
}
fn glob_match(pattern: &str, text: &str) -> bool {
let parts: Vec<&str> = pattern.split('*').collect();
if parts.is_empty() {
return pattern == text;
}
let mut pos = 0;
for (i, part) in parts.iter().enumerate() {
if part.is_empty() {
continue;
}
if let Some(found) = text[pos..].find(part) {
if i == 0 && found != 0 {
return false;
}
pos += found + part.len();
} else {
return false;
}
}
if !pattern.ends_with('*') && pos != text.len() {
return false;
}
true
}
#[derive(Debug, Clone, Default)]
pub enum GracePeriodBehavior {
Strict,
#[default]
WarnDuringGrace,
WarnOnly,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = AirGappedConfig::default();
assert!(config.max_signature_age.is_none());
assert!(config.check_revocations);
assert!(!config.enforce_rollback_protection);
}
#[test]
fn test_high_security_config() {
let config = AirGappedConfig::high_security();
assert!(config.max_signature_age.is_some());
assert!(config.enforce_rollback_protection);
assert!(matches!(config.grace_period_behavior, GracePeriodBehavior::Strict));
}
#[test]
fn test_identity_requirements_exact_match() {
let req = IdentityRequirements {
allowed_issuers: vec!["https://issuer.example.com".to_string()],
allowed_subjects: vec!["user@example.com".to_string()],
};
assert!(req.matches_issuer("https://issuer.example.com"));
assert!(!req.matches_issuer("https://other.example.com"));
assert!(req.matches_subject("user@example.com"));
assert!(!req.matches_subject("other@example.com"));
}
#[test]
fn test_identity_requirements_glob_match() {
let req = IdentityRequirements::github_actions("myorg");
assert!(req.matches_issuer("https://token.actions.githubusercontent.com"));
assert!(req.matches_subject("https://github.com/myorg/repo/.github/workflows/ci.yml@refs/heads/main"));
assert!(!req.matches_subject("https://github.com/otherorg/repo"));
}
#[test]
fn test_glob_match() {
assert!(glob_match("hello*", "hello world"));
assert!(glob_match("*world", "hello world"));
assert!(glob_match("hello*world", "hello beautiful world"));
assert!(glob_match("*", "anything"));
assert!(glob_match("exact", "exact"));
assert!(!glob_match("hello*", "world hello"));
assert!(!glob_match("*world", "world hello"));
assert!(!glob_match("exact", "not exact"));
}
}