Skip to main content

luct_scanner/
report.rs

1use crate::{Scanner, ScannerImpl, Validated};
2use chrono::{DateTime, Local, TimeDelta};
3use luct_core::{LogId, v1::SignedTreeHead};
4use luct_store::StringStoreValue;
5use serde::{Deserialize, Serialize};
6use web_time::{Duration, UNIX_EPOCH};
7
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9pub struct Report {
10    pub(crate) ca_issuer: String,
11    pub(crate) ca_subject: String,
12    pub(crate) cert_issuer: String,
13    pub(crate) cert_subject: String,
14    pub(crate) fingerprint: String,
15    pub(crate) ca_fingerprint: String,
16    pub(crate) not_before: DateTime<Local>,
17    pub(crate) not_after: DateTime<Local>,
18    // TODO: Precert fingerprint
19    #[serde(skip_serializing_if = "Vec::is_empty", default)]
20    pub(crate) scts: Vec<SctReport>,
21    #[serde(skip_serializing_if = "Option::is_none", default)]
22    pub(crate) error_description: Option<String>,
23}
24
25impl Report {
26    pub fn get_error(&self) -> Option<String> {
27        self.error_description.clone()
28    }
29
30    fn error_description(mut self, err: String) -> Self {
31        self.error_description = Some(err);
32        self
33    }
34}
35
36impl<S: ScannerImpl> Scanner<S> {
37    pub(crate) fn evaluate_policy(&self, report: Report, current_time: DateTime<Local>) -> Report {
38        // TODO: Check that expiration date matches logs submission bracket?
39
40        // Calculate the number of scts we expect
41        let num_expected_scts = match report.not_after - report.not_before {
42            time if time <= TimeDelta::days(180) => 2,
43            _ => 3,
44        };
45
46        // Calculate the number of scts that the report contains from known logs
47        // TODO: Make sure that the logs are from different operators
48        let num_scts_from_known_logs = report
49            .scts
50            .iter()
51            // NOTE: Having a signature that passed validation means the log is known
52            .filter(|sct| sct.signature_validation_time.is_some())
53            .count();
54
55        // Check that we have enough SCTs from known logs
56        if num_scts_from_known_logs < num_expected_scts {
57            return report.error_description(format!(
58                "Insufficient number of SCTs from known logs. Expected {} but got {}",
59                num_expected_scts, num_scts_from_known_logs
60            ));
61        }
62
63        let mut fresh_inclusion_proofs = 0;
64        let mut old_inclusion_proofs = 0;
65        for sct in report.scts.iter() {
66            // Scts with error cannot be valid
67            if sct.error_description.is_some() {
68                continue;
69            }
70
71            // Check that the SCT has a a fresh STH
72            let Some(latest_sth) = &sct.latest_sth else {
73                // Could not find a fresh STH for this SCT
74                continue;
75            };
76            if latest_sth.verification_time
77                < current_time - time_delta_from_duration(self.config.sth_freshness_threshold)
78            {
79                // The logs latest STH is too old and the log is considered state
80                continue;
81            }
82
83            // Check whether the proofs are old or fresh
84            let Some(inclusion_proof) = &sct.inclusion_proof else {
85                // Could not find an inclusion proof for this SCT
86                continue;
87            };
88            if inclusion_proof.verification_time
89                < current_time - time_delta_from_duration(self.config.sth_freshness_threshold)
90            {
91                old_inclusion_proofs += 1;
92            } else {
93                fresh_inclusion_proofs += 1;
94            }
95        }
96
97        if old_inclusion_proofs == 0 && fresh_inclusion_proofs < num_expected_scts {
98            return report.error_description(
99                "Insufficient number of inclusion proofs with fresh sths could be verified!"
100                    .to_string(),
101            );
102        }
103
104        report
105    }
106}
107
108fn time_delta_from_duration(duration: Duration) -> TimeDelta {
109    TimeDelta::new(duration.as_secs() as i64, duration.subsec_nanos())
110        .expect("Failed to translate duration into timedelta")
111}
112
113impl StringStoreValue for Report {
114    fn serialize_value(&self) -> String {
115        serde_json::to_string(self).unwrap()
116    }
117
118    fn deserialize_value(value: &str) -> Option<Self> {
119        serde_json::from_str(value).ok()
120    }
121}
122
123#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
124pub struct SctReport {
125    log_id: String,
126    #[serde(skip_serializing_if = "Option::is_none", default)]
127    signature_validation_time: Option<DateTime<Local>>,
128    #[serde(skip_serializing_if = "Option::is_none", default)]
129    log_name: Option<String>,
130    #[serde(skip_serializing_if = "Option::is_none", default)]
131    latest_sth: Option<SthReport>,
132    #[serde(skip_serializing_if = "Option::is_none", default)]
133    index: Option<u64>,
134    #[serde(skip_serializing_if = "Option::is_none", default)]
135    inclusion_proof: Option<SthReport>,
136    #[serde(skip_serializing_if = "Option::is_none", default)]
137    error_description: Option<String>,
138}
139
140impl SctReport {
141    pub(crate) fn new(log_id: LogId) -> Self {
142        Self {
143            log_id: log_id.to_string(),
144            signature_validation_time: None,
145            log_name: None,
146            latest_sth: None,
147            index: None,
148            inclusion_proof: None,
149            error_description: None,
150        }
151    }
152
153    pub(crate) fn signature_validation_time(mut self, time: DateTime<Local>) -> Self {
154        self.signature_validation_time = Some(time);
155        self
156    }
157
158    pub(crate) fn log_name(mut self, name: String) -> Self {
159        self.log_name = Some(name);
160        self
161    }
162
163    pub(crate) fn latest_sth(mut self, sth: SthReport) -> Self {
164        self.latest_sth = Some(sth);
165        self
166    }
167
168    pub(crate) fn index(mut self, index: u64) -> Self {
169        self.index = Some(index);
170        self
171    }
172
173    pub(crate) fn inclusion_proof(mut self, sth: SthReport) -> Self {
174        self.inclusion_proof = Some(sth);
175        self
176    }
177
178    pub(crate) fn error_description(mut self, err: String) -> Self {
179        self.error_description = Some(err);
180        self
181    }
182}
183
184#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
185pub struct SthReport {
186    height: u64,
187    timestamp: DateTime<Local>,
188    verification_time: DateTime<Local>,
189}
190
191impl From<&Validated<SignedTreeHead>> for SthReport {
192    fn from(value: &Validated<SignedTreeHead>) -> Self {
193        Self {
194            height: value.tree_size(),
195            timestamp: DateTime::from_timestamp_millis(value.timestamp() as i64)
196                .unwrap()
197                .into(),
198            verification_time: DateTime::from_timestamp_millis(
199                value
200                    .validated_at()
201                    .duration_since(UNIX_EPOCH)
202                    .unwrap()
203                    .as_millis() as i64,
204            )
205            .unwrap()
206            .into(),
207        }
208    }
209}
210
211// TODO: Tests for policy evaluation