1use std::time::Duration;
4
5#[derive(Debug, Clone)]
7pub struct AirGappedConfig {
8 pub max_signature_age: Option<Duration>,
13
14 pub check_revocations: bool,
16
17 pub max_chain_depth: u8,
19
20 pub identity_requirements: Option<IdentityRequirements>,
22
23 pub grace_period_behavior: GracePeriodBehavior,
25
26 pub enforce_rollback_protection: bool,
31}
32
33impl Default for AirGappedConfig {
34 fn default() -> Self {
35 Self {
36 max_signature_age: None,
37 check_revocations: true,
38 max_chain_depth: 4,
39 identity_requirements: None,
40 grace_period_behavior: GracePeriodBehavior::WarnDuringGrace,
41 enforce_rollback_protection: false,
42 }
43 }
44}
45
46impl AirGappedConfig {
47 pub fn fully_airgapped() -> Self {
49 Self {
50 max_signature_age: None,
51 check_revocations: true,
52 max_chain_depth: 4,
53 identity_requirements: None,
54 grace_period_behavior: GracePeriodBehavior::WarnOnly,
55 enforce_rollback_protection: false,
56 }
57 }
58
59 pub fn intermittent() -> Self {
61 Self {
62 max_signature_age: Some(Duration::from_secs(90 * 24 * 3600)), check_revocations: true,
64 max_chain_depth: 4,
65 identity_requirements: None,
66 grace_period_behavior: GracePeriodBehavior::WarnDuringGrace,
67 enforce_rollback_protection: true,
68 }
69 }
70
71 pub fn high_security() -> Self {
73 Self {
74 max_signature_age: Some(Duration::from_secs(7 * 24 * 3600)), check_revocations: true,
76 max_chain_depth: 2,
77 identity_requirements: None,
78 grace_period_behavior: GracePeriodBehavior::Strict,
79 enforce_rollback_protection: true,
80 }
81 }
82
83 pub fn with_max_age(mut self, age: Duration) -> Self {
85 self.max_signature_age = Some(age);
86 self
87 }
88
89 pub fn with_identity_requirements(mut self, requirements: IdentityRequirements) -> Self {
91 self.identity_requirements = Some(requirements);
92 self
93 }
94
95 pub fn with_rollback_protection(mut self) -> Self {
97 self.enforce_rollback_protection = true;
98 self
99 }
100}
101
102#[derive(Debug, Clone)]
104pub struct IdentityRequirements {
105 pub allowed_issuers: Vec<String>,
109
110 pub allowed_subjects: Vec<String>,
114}
115
116impl IdentityRequirements {
117 pub fn github_actions(org: &str) -> Self {
119 Self {
120 allowed_issuers: vec!["https://token.actions.githubusercontent.com".to_string()],
121 allowed_subjects: vec![format!("https://github.com/{}/*", org)],
122 }
123 }
124
125 pub fn matches_issuer(&self, issuer: &str) -> bool {
127 self.allowed_issuers.iter().any(|pattern| {
128 if pattern.contains('*') {
129 glob_match(pattern, issuer)
130 } else {
131 pattern == issuer
132 }
133 })
134 }
135
136 pub fn matches_subject(&self, subject: &str) -> bool {
138 self.allowed_subjects.iter().any(|pattern| {
139 if pattern.contains('*') {
140 glob_match(pattern, subject)
141 } else {
142 pattern == subject
143 }
144 })
145 }
146}
147
148fn glob_match(pattern: &str, text: &str) -> bool {
150 let parts: Vec<&str> = pattern.split('*').collect();
151
152 if parts.is_empty() {
153 return pattern == text;
154 }
155
156 let mut pos = 0;
157 for (i, part) in parts.iter().enumerate() {
158 if part.is_empty() {
159 continue;
160 }
161
162 if let Some(found) = text[pos..].find(part) {
163 if i == 0 && found != 0 {
164 return false;
166 }
167 pos += found + part.len();
168 } else {
169 return false;
170 }
171 }
172
173 if !pattern.ends_with('*') && pos != text.len() {
175 return false;
176 }
177
178 true
179}
180
181#[derive(Debug, Clone, Default)]
183pub enum GracePeriodBehavior {
184 Strict,
186
187 #[default]
189 WarnDuringGrace,
190
191 WarnOnly,
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 fn test_default_config() {
201 let config = AirGappedConfig::default();
202 assert!(config.max_signature_age.is_none());
203 assert!(config.check_revocations);
204 assert!(!config.enforce_rollback_protection);
205 }
206
207 #[test]
208 fn test_high_security_config() {
209 let config = AirGappedConfig::high_security();
210 assert!(config.max_signature_age.is_some());
211 assert!(config.enforce_rollback_protection);
212 assert!(matches!(config.grace_period_behavior, GracePeriodBehavior::Strict));
213 }
214
215 #[test]
216 fn test_identity_requirements_exact_match() {
217 let req = IdentityRequirements {
218 allowed_issuers: vec!["https://issuer.example.com".to_string()],
219 allowed_subjects: vec!["user@example.com".to_string()],
220 };
221
222 assert!(req.matches_issuer("https://issuer.example.com"));
223 assert!(!req.matches_issuer("https://other.example.com"));
224
225 assert!(req.matches_subject("user@example.com"));
226 assert!(!req.matches_subject("other@example.com"));
227 }
228
229 #[test]
230 fn test_identity_requirements_glob_match() {
231 let req = IdentityRequirements::github_actions("myorg");
232
233 assert!(req.matches_issuer("https://token.actions.githubusercontent.com"));
234 assert!(req.matches_subject("https://github.com/myorg/repo/.github/workflows/ci.yml@refs/heads/main"));
235 assert!(!req.matches_subject("https://github.com/otherorg/repo"));
236 }
237
238 #[test]
239 fn test_glob_match() {
240 assert!(glob_match("hello*", "hello world"));
241 assert!(glob_match("*world", "hello world"));
242 assert!(glob_match("hello*world", "hello beautiful world"));
243 assert!(glob_match("*", "anything"));
244 assert!(glob_match("exact", "exact"));
245
246 assert!(!glob_match("hello*", "world hello"));
247 assert!(!glob_match("*world", "world hello"));
248 assert!(!glob_match("exact", "not exact"));
249 }
250}