Skip to main content

luct_core/log_list/
v3.rs

1use crate::{CtLog, CtLogConfig, Version, utils::base64::Base64};
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4use url::Url;
5
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7pub struct LogList {
8    version: String,
9    log_list_timestamp: DateTime<Utc>,
10    operators: Vec<Operators>,
11}
12
13impl LogList {
14    pub fn currently_active_logs(&self) -> Vec<CtLog> {
15        self.active_logs(Utc::now())
16    }
17
18    pub fn active_logs(&self, time: DateTime<Utc>) -> Vec<CtLog> {
19        self.logs(
20            // Check that the interval of included logs is not in the past.
21            // If it is, this log can not contain certificates, that are still valid
22            // and therefore we don't need to include it.
23            |interval| {
24                interval
25                    .as_ref()
26                    .is_some_and(|interval| interval.end_exclusive > time)
27            },
28            // Only logs in qualified, usable and readonly states should be considered active
29            // See https://googlechrome.github.io/CertificateTransparency/log_states.html
30            |state| {
31                state.as_ref().is_some_and(|state| {
32                    matches!(
33                        state,
34                        State::Qualified { .. } | State::Usable { .. } | State::Readonly { .. }
35                    )
36                })
37            },
38            // Logs that have no type, or that are marked Prod are considered active
39            |log_type| {
40                log_type
41                    .as_ref()
42                    .is_none_or(|log_type| matches!(log_type, LogType::Prod))
43            },
44        )
45    }
46
47    pub fn all_logs(&self) -> Vec<CtLog> {
48        self.logs(|_| true, |_| true, |_| true)
49    }
50
51    fn logs<TF, SF, TYF>(&self, time_filter: TF, state_filter: SF, type_filter: TYF) -> Vec<CtLog>
52    where
53        TF: Fn(&Option<Interval>) -> bool,
54        SF: Fn(&Option<State>) -> bool,
55        TYF: Fn(&Option<LogType>) -> bool,
56    {
57        self.operators
58            .iter()
59            .flat_map(|op| op.logs.iter().chain(op.tiled_logs.iter()))
60            .filter(|&log| time_filter(&log.temporal_interval))
61            .filter(|&log| state_filter(&log.state))
62            .filter(|&log| type_filter(&log.log_type))
63            .filter_map(|log| {
64                let config = CtLogConfig {
65                    description: log.description.clone(),
66                    version: Version::V1,
67                    url: match &log.url {
68                        LogUrl::Log { url } => url.clone(),
69
70                        LogUrl::TiledLog { submission_url, .. } => submission_url.clone(),
71                    },
72                    tile_url: match &log.url {
73                        LogUrl::Log { .. } => None,
74                        LogUrl::TiledLog { monitoring_url, .. } => Some(monitoring_url.clone()),
75                    },
76                    key: log.key.clone(),
77                    mmd: log.mmd,
78                };
79                let log = CtLog::new(config);
80
81                if log.log_id() == &log.log_id {
82                    Some(log)
83                } else {
84                    None
85                }
86            })
87            .collect()
88    }
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92struct Operators {
93    name: String,
94    email: Vec<String>,
95    logs: Vec<Logs>,
96    tiled_logs: Vec<Logs>,
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
100struct Logs {
101    description: String,
102    log_id: Base64<Vec<u8>>,
103    key: Base64<Vec<u8>>,
104    mmd: u64,
105    dns: Option<String>,
106    state: Option<State>,
107    temporal_interval: Option<Interval>,
108    log_type: Option<LogType>,
109    #[serde(skip_serializing_if = "Vec::is_empty", default)]
110    previous_owners: Vec<PreviousOwner>,
111    #[serde(flatten)]
112    url: LogUrl,
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
116#[serde(untagged)]
117enum LogUrl {
118    Log {
119        url: Url,
120    },
121    TiledLog {
122        submission_url: Url,
123        monitoring_url: Url,
124    },
125}
126
127#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
128#[serde(rename_all = "snake_case")]
129enum State {
130    Pending {
131        timestamp: DateTime<Utc>,
132    },
133    Qualified {
134        timestamp: DateTime<Utc>,
135    },
136    Usable {
137        timestamp: DateTime<Utc>,
138    },
139    Readonly {
140        timestamp: DateTime<Utc>,
141        final_tree_head: FinalTreeHead,
142    },
143    Retired {
144        timestamp: DateTime<Utc>,
145    },
146    Rejected {
147        timestamp: DateTime<Utc>,
148    },
149}
150
151#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
152struct Interval {
153    start_inclusive: DateTime<Utc>,
154    end_exclusive: DateTime<Utc>,
155}
156
157#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
158#[serde(rename_all = "snake_case")]
159enum LogType {
160    Prod,
161    Test,
162    MonitoringOnly,
163}
164
165#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
166struct PreviousOwner {
167    name: String,
168    end_time: DateTime<Utc>,
169}
170
171#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
172struct FinalTreeHead {
173    sha256_root_hash: Base64<Vec<u8>>,
174    tree_size: u64,
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180    use chrono::{NaiveDate, TimeZone};
181
182    const ALL_LOG_LIST: &str = include_str!("../../../testdata/all_logs_list.json");
183
184    #[test]
185    fn parse_log_list() {
186        let time = Utc
187            .from_local_datetime(
188                &NaiveDate::from_ymd_opt(2025, 12, 14)
189                    .unwrap()
190                    .and_hms_milli_opt(1, 0, 0, 0)
191                    .unwrap(),
192            )
193            .unwrap();
194
195        let log_list: LogList = serde_json::from_str(ALL_LOG_LIST).unwrap();
196        let all_logs = log_list.all_logs();
197        assert_eq!(all_logs.len(), 247);
198
199        let active_logs = log_list.active_logs(time);
200        assert_eq!(active_logs.len(), 71);
201    }
202}