1#![forbid(unsafe_code)]
4
5use crate::log::{ScannerLog, builder::LogImpls};
6use chrono::{DateTime, Local};
7use futures::future::{self, join_all};
8use luct_client::{Client, ClientError};
9use luct_core::{
10 CertificateChain, CertificateError, CtLog, CtLogConfig, Fingerprint, LogId,
11 store::{SearchableStore, StoreRead, StoreWrite},
12 tiling::TilingError,
13 v1::{SignedCertificateTimestamp, SignedTreeHead},
14};
15use serde::{Deserialize, Serialize};
16use std::{collections::BTreeMap, fmt::Debug, sync::Arc};
17use thiserror::Error;
18use web_time::{Duration, SystemTime, UNIX_EPOCH};
19pub use {
20 config::{ScannerConfig, ScannerConfigBuilder},
21 report::{Report, SctReport, SthReport},
22 utils::Validated,
23};
24
25type HashOutput = [u8; 32];
26
27mod config;
28mod log;
29mod report;
30mod utils;
31
32pub trait ScannerImpl {
36 type Client: Client + Clone;
38 type ReportStore: SearchableStore<Fingerprint, Report>;
40 type SthStore: SearchableStore<u64, Validated<SignedTreeHead>>;
42}
43
44pub struct Scanner<S: ScannerImpl> {
49 config: ScannerConfig,
50 logs: BTreeMap<LogId, ScannerLog<S>>,
51 report_store: S::ReportStore,
52 client: S::Client,
53 time_source: Box<dyn Fn() -> DateTime<Local>>,
54}
55
56#[allow(clippy::type_complexity)]
57impl<S: ScannerImpl> Scanner<S> {
58 pub fn logs<'a>(&'a self) -> Box<dyn Iterator<Item = &'a CtLog> + 'a> {
59 Box::new(self.logs.values().map(|val| val.client().log()))
60 }
61
62 pub fn new<F: Fn() -> DateTime<Local> + 'static>(
63 config: ScannerConfig,
64 report_store: S::ReportStore,
65 client: S::Client,
66 time_source: F,
67 ) -> Self {
68 Self {
69 config,
70 logs: BTreeMap::new(),
71 report_store,
72 client,
73 time_source: Box::new(time_source) as _,
74 }
75 }
76
77 pub fn add_log(&mut self, log: &CtLog, sth_store: S::SthStore) -> &mut Self {
78 let impls = LogImpls {
79 client: self.client.clone(),
80 sth_store,
81 };
82 let scanner_log = ScannerLog::new(log, impls);
83 let log_id = scanner_log.client().log().log_id().clone();
84
85 self.logs.insert(log_id, scanner_log);
86 self
87 }
88
89 pub async fn refresh_all_logs(&self) -> Result<(), ScannerError> {
91 let updates = self
92 .logs
93 .values()
94 .map(|log| log.update_sth())
95 .collect::<Vec<_>>();
96
97 future::try_join_all(updates).await?;
98
99 Ok(())
100 }
101
102 pub async fn collect_report_pem(&self, data: &str) -> Result<Report, ScannerError> {
103 let cert_chain = Arc::new(CertificateChain::from_pem_chain(data)?);
104
105 if self.config.validate_cert_chain {
106 cert_chain.verify_chain()?;
107 }
108
109 self.collect_report(cert_chain).await
110 }
111
112 pub async fn collect_report(
113 &self,
114 chain: Arc<CertificateChain>,
115 ) -> Result<Report, ScannerError> {
116 let cert = chain.cert();
117 let cert_fp = cert.fingerprint_sha256();
118
119 if let Some(report) = self.report_store.get(&cert_fp) {
120 tracing::debug!("Found report for {} in cache", cert_fp.to_string());
121 return Ok(report);
122 }
123
124 let (not_before, not_after) = cert.get_validity();
125 let embedded_scts = cert.extract_scts_v1()?;
126
127 let scts = join_all(
128 embedded_scts
129 .into_iter()
130 .map(|sct| self.collect_embedded_sct_report(sct, &chain)),
131 )
132 .await;
133
134 let report = Report {
135 ca_issuer: chain.root().get_issuer_name(),
136 ca_subject: chain.root().get_subject_name(),
137 cert_issuer: chain.cert().get_issuer_name(),
138 cert_subject: chain.cert().get_subject_name(),
139 fingerprint: chain.cert().fingerprint_sha256().to_string(),
140 ca_fingerprint: chain.root().fingerprint_sha256().to_string(),
141 not_before: not_before.into(),
142 not_after: not_after.into(),
143 scts,
144 error_description: None,
145 };
146
147 let report = self.evaluate_policy(report, (self.time_source)());
148 if report.get_error().is_none() {
149 self.report_store.insert(cert_fp, report.clone());
150 }
151
152 Ok(report)
153 }
154
155 pub(crate) async fn collect_embedded_sct_report(
156 &self,
157 sct: SignedCertificateTimestamp,
158 chain: &Arc<CertificateChain>,
159 ) -> SctReport {
160 let report = SctReport::new(sct.log_id());
161
162 let Some(log) = self.logs.get(&sct.log_id()) else {
164 return report.error_description("Unknown log id".to_string());
165 };
166 let log_name = log.client().log().description().to_string();
167 let report = report.log_name(log_name);
168
169 if let Err(err) = log.client().log().validate_sct_v1(chain, &sct, true) {
171 return report.error_description(err.to_string());
172 };
173 let report = report.signature_validation_time(
174 DateTime::from_timestamp_millis(
175 SystemTime::now()
176 .duration_since(UNIX_EPOCH)
177 .unwrap()
178 .as_millis() as i64,
179 )
180 .unwrap()
181 .into(),
182 );
183
184 let fresh_sth = match self.get_fresh_sth(log).await {
186 Ok(sth) => sth,
187 Err(err) => return report.error_description(err.to_string()),
188 };
189 let report = report.latest_sth(SthReport::from(&fresh_sth));
190
191 let leaf = match chain.as_leaf_v1(&sct, true) {
192 Err(err) => {
193 return report.error_description(err.to_string());
194 }
195 Ok(leaf) => leaf,
196 };
197
198 let oldest_sth = log.oldest_viable_sth(&sct).unwrap_or(fresh_sth);
200 let report = match log.check_sct_inclusion(&sct, &oldest_sth, &leaf).await {
201 Ok(index) => report.index(index),
202 Err(err) => return report.error_description(err.to_string()),
203 };
204
205 report.inclusion_proof(SthReport::from(&oldest_sth))
206 }
207
208 async fn get_fresh_sth(
213 &self,
214 log: &ScannerLog<S>,
215 ) -> Result<Validated<SignedTreeHead>, ScannerError> {
216 let log_name = log.client().log().description();
217
218 let Some(last_sth) = log.get_latest_sth() else {
220 tracing::debug!("No prior known STHs for {}, fetching fresh STH", log_name);
221 return log.update_sth().await;
222 };
223
224 let now = SystemTime::now();
225 let timestamp = UNIX_EPOCH + Duration::from_millis(last_sth.timestamp());
226 if timestamp + self.config.sth_update_threshold < now {
227 tracing::debug!("Updating old STH for log {}", log_name);
228 log.update_sth().await
229 } else {
230 Ok(last_sth)
231 }
232 }
233}
234
235#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
236pub struct ScannerBuilder {
237 config: ScannerConfig,
238 logs: Vec<CtLogConfig>,
239}
240
241#[derive(Debug, Clone, Error)]
242pub enum ScannerError {
243 #[error("Invalid certificate: {0}")]
244 CertificateError(#[from] CertificateError),
245
246 #[error("HTTP client error {0}")]
247 ClientError(#[from] ClientError),
248
249 #[error("Failed to construct proof from tiles {0}")]
250 TilingError(#[from] TilingError),
251}