1use std::collections::BTreeMap;
17
18use chrono::{DateTime, Duration, Utc};
19use serde::{Deserialize, Serialize};
20
21pub const DEFAULT_GRACE_PERIOD: Duration = Duration::hours(24);
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "snake_case")]
27pub enum RevocationReason {
28 KeyCompromise,
30 CessationOfOperation,
32 Superseded,
34 Withdrawn,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct RevokedEntry {
41 pub fingerprint: String,
43 pub revoked_at: DateTime<Utc>,
45 pub reason: RevocationReason,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct Crl {
53 pub issuer: String,
55 pub revoked: Vec<RevokedEntry>,
57 pub this_update: DateTime<Utc>,
59 pub next_update: DateTime<Utc>,
61 pub log_sequence: u64,
63 pub signature: Vec<u8>,
65}
66
67impl Crl {
68 pub fn signing_bytes(&self) -> crate::error::SignatifResult<Vec<u8>> {
74 let mut copy = self.clone();
75 copy.signature = Vec::new();
76 Ok(
77 crate::jcs::canonicalize(&serde_json::to_value(©).expect("crl serializes"))?
78 .into_bytes(),
79 )
80 }
81
82 pub fn revokes(&self, fingerprint: &str, at: DateTime<Utc>) -> Option<&RevokedEntry> {
85 self.revoked
86 .iter()
87 .find(|e| e.fingerprint == fingerprint && e.revoked_at <= at)
88 }
89
90 pub fn is_stale(&self, now: DateTime<Utc>) -> bool {
92 now > self.next_update
93 }
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct AuthorityStateBinding {
102 pub artifact_hash: String,
104 pub authority_fingerprints: Vec<String>,
106 pub bound_at: DateTime<Utc>,
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112pub enum RevocationStatus {
113 Good,
115 GraceDowngrade,
117 Revoked,
119}
120
121pub trait RevocationView {
123 fn authority_status(&self, id: &str, now: DateTime<Utc>) -> RevocationStatus;
125
126 fn max_crl_age(&self, now: DateTime<Utc>) -> Duration;
128}
129
130#[derive(Debug, Clone, Copy)]
133pub struct NoRevocations;
134
135impl RevocationView for NoRevocations {
136 fn authority_status(&self, _id: &str, _now: DateTime<Utc>) -> RevocationStatus {
137 RevocationStatus::Good
138 }
139
140 fn max_crl_age(&self, _now: DateTime<Utc>) -> Duration {
141 Duration::zero()
142 }
143}
144
145#[derive(Debug, Clone, Default)]
147pub struct CrlView {
148 pub authority_fingerprints: BTreeMap<String, String>,
150 pub crls: Vec<Crl>,
152}
153
154impl RevocationView for CrlView {
155 fn authority_status(&self, id: &str, now: DateTime<Utc>) -> RevocationStatus {
156 let Some(fp) = self.authority_fingerprints.get(id) else {
157 return RevocationStatus::Good;
158 };
159 let stale = self.crls.iter().all(|c| c.is_stale(now));
160 for crl in &self.crls {
161 if crl.revokes(fp, now).is_some() {
162 return RevocationStatus::Revoked;
163 }
164 }
165 if stale && !self.crls.is_empty() {
166 RevocationStatus::GraceDowngrade
167 } else {
168 RevocationStatus::Good
169 }
170 }
171
172 fn max_crl_age(&self, now: DateTime<Utc>) -> Duration {
173 self.crls
174 .iter()
175 .map(|c| now.signed_duration_since(c.this_update))
176 .max()
177 .unwrap_or_else(Duration::zero)
178 }
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
183#[serde(rename_all = "snake_case")]
184pub enum ArtifactMark {
185 Marked,
187 Cleared,
189}
190
191#[derive(Debug, Clone, Default)]
193pub struct RevocationIndex {
194 bindings: Vec<AuthorityStateBinding>,
195 marks: BTreeMap<String, ArtifactMark>,
196 revoked_states: Vec<String>,
197}
198
199impl RevocationIndex {
200 pub fn new() -> Self {
202 Self::default()
203 }
204
205 pub fn bind(&mut self, binding: AuthorityStateBinding) {
207 self.bindings.push(binding);
208 }
209
210 pub fn revoke_state(&mut self, fingerprint: &str) {
214 self.revoked_states.push(fingerprint.to_string());
215 for b in &self.bindings {
216 if b.authority_fingerprints.iter().any(|f| f == fingerprint) {
217 self.marks
218 .insert(b.artifact_hash.clone(), ArtifactMark::Marked);
219 }
220 }
221 }
222
223 pub fn correct_state(&mut self, fingerprint: &str) {
225 self.revoked_states.retain(|f| f != fingerprint);
226 for b in &self.bindings {
227 if b.authority_fingerprints.iter().any(|f| f == fingerprint) {
228 self.marks
229 .insert(b.artifact_hash.clone(), ArtifactMark::Cleared);
230 }
231 }
232 }
233
234 pub fn artifact_status(&self, artifact_hash: &str) -> (Vec<String>, Option<ArtifactMark>) {
237 let states = self
238 .bindings
239 .iter()
240 .find(|b| b.artifact_hash == artifact_hash)
241 .map(|b| b.authority_fingerprints.clone())
242 .unwrap_or_default();
243 (states, self.marks.get(artifact_hash).copied())
244 }
245
246 pub fn artifacts_for_state(&self, fingerprint: &str) -> Vec<String> {
248 self.bindings
249 .iter()
250 .filter(|b| b.authority_fingerprints.iter().any(|f| f == fingerprint))
251 .map(|b| b.artifact_hash.clone())
252 .collect()
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 fn crl(revoked: Vec<RevokedEntry>) -> Crl {
261 Crl {
262 issuer: "root".into(),
263 revoked,
264 this_update: Utc::now() - Duration::hours(1),
265 next_update: Utc::now() + Duration::days(7),
266 log_sequence: 42,
267 signature: vec![],
268 }
269 }
270
271 #[test]
272 fn crl_matches_by_fingerprint_and_time() {
273 let fp = "abc";
274 let c = crl(vec![RevokedEntry {
275 fingerprint: fp.into(),
276 revoked_at: Utc::now() - Duration::minutes(5),
277 reason: RevocationReason::KeyCompromise,
278 }]);
279 assert!(c.revokes(fp, Utc::now()).is_some());
280 assert!(c.revokes(fp, Utc::now() - Duration::hours(1)).is_none());
281 assert!(c.revokes("other", Utc::now()).is_none());
282 }
283
284 #[test]
285 fn view_reports_revoked_and_stale_grace() {
286 let fp = "abc";
287 let mut view = CrlView::default();
288 view.authority_fingerprints.insert("end".into(), fp.into());
289 view.crls.push(crl(vec![RevokedEntry {
290 fingerprint: fp.into(),
291 revoked_at: Utc::now(),
292 reason: RevocationReason::Withdrawn,
293 }]));
294 assert_eq!(
295 view.authority_status("end", Utc::now()),
296 RevocationStatus::Revoked
297 );
298
299 let fresh_view = CrlView {
300 authority_fingerprints: [("end".to_string(), "abc".to_string())]
301 .into_iter()
302 .collect(),
303 crls: vec![crl(vec![])],
304 };
305 assert_eq!(
306 fresh_view.authority_status("end", Utc::now()),
307 RevocationStatus::Good
308 );
309
310 let mut stale = fresh_view.clone();
311 stale.crls[0].next_update = Utc::now() - Duration::hours(1);
312 assert_eq!(
313 stale.authority_status("end", Utc::now()),
314 RevocationStatus::GraceDowngrade
315 );
316 }
317
318 #[test]
319 fn propagation_marks_reversibly() {
320 let mut idx = RevocationIndex::new();
321 idx.bind(AuthorityStateBinding {
322 artifact_hash: "h1".into(),
323 authority_fingerprints: vec!["fp-root".into(), "fp-end".into()],
324 bound_at: Utc::now(),
325 });
326 idx.bind(AuthorityStateBinding {
327 artifact_hash: "h2".into(),
328 authority_fingerprints: vec!["fp-other".into()],
329 bound_at: Utc::now(),
330 });
331
332 idx.revoke_state("fp-end");
333 assert_eq!(idx.artifact_status("h1").1, Some(ArtifactMark::Marked));
334 assert_eq!(idx.artifact_status("h2").1, None);
335 assert_eq!(idx.artifacts_for_state("fp-end"), vec!["h1".to_string()]);
336
337 idx.correct_state("fp-end");
338 assert_eq!(idx.artifact_status("h1").1, Some(ArtifactMark::Cleared));
339 }
340}