Skip to main content

luct_scanner/report/
generate.rs

1use crate::{Report, Scanner, ScannerError, ScannerImpl, SctReport, SthReport};
2use chrono::DateTime;
3use futures::future::join_all;
4use luct_core::{
5    CertificateChain, LogId,
6    store::{AsyncStoreRead, AsyncStoreWrite},
7    v1::{self, SignedCertificateTimestamp},
8};
9use std::sync::Arc;
10use web_time::{SystemTime, UNIX_EPOCH};
11
12impl<S: ScannerImpl> Scanner<S> {
13    pub async fn collect_report_pem(&self, data: &str) -> Result<Report, ScannerError> {
14        let cert_chain = Arc::new(CertificateChain::from_pem_chain(data)?);
15
16        if self.config.validate_cert_chain {
17            cert_chain.verify_chain()?;
18        }
19
20        self.collect_report(cert_chain).await
21    }
22
23    // TODO: Remove Result here
24    pub async fn collect_report(
25        &self,
26        chain: Arc<CertificateChain>,
27    ) -> Result<Report, ScannerError> {
28        let cert = chain.cert();
29        let cert_fp = cert.fingerprint_sha256();
30
31        let report = match self.report_store.get(cert_fp.clone()).await {
32            Some(report) => {
33                tracing::debug!("Found report for {} in cache", cert_fp.to_string());
34
35                // We need to update the report. If we fail, this is likely because the report
36                // does no longer serialize, e.g. due to an update
37                match self.update_report(report, &chain).await {
38                    Err(()) => {
39                        tracing::info!(
40                            "Found an invalid report (likely generated by outdated version). Will generate fresh report"
41                        );
42                        self.create_report(chain).await
43                    }
44                    Ok(report) => report,
45                }
46            }
47            None => {
48                tracing::debug!("Could not find report for {} in cache", cert_fp.to_string());
49                self.create_report(chain).await
50            }
51        };
52
53        let report = self.evaluate_policy(report, (self.time_source)());
54        if report.get_error().is_none() {
55            self.report_store.insert(cert_fp, report.clone()).await;
56        }
57
58        Ok(report)
59    }
60
61    async fn create_report(&self, chain: Arc<CertificateChain>) -> Report {
62        let cert = chain.cert();
63
64        let mut report = Report::from(chain.as_ref());
65
66        let embedded_scts = match cert.extract_scts_v1() {
67            Err(err) => {
68                return report
69                    .error_description(format!("Failed to parse SCTs from certificate:s {}", err));
70            }
71            Ok(scts) => scts,
72        };
73
74        let sct_reports = join_all(
75            embedded_scts
76                .into_iter()
77                .map(|sct| self.collect_embedded_sct_report(sct, &chain)),
78        )
79        .await;
80
81        report.scts = sct_reports;
82        report
83    }
84
85    async fn collect_embedded_sct_report(
86        &self,
87        sct: SignedCertificateTimestamp,
88        chain: &Arc<CertificateChain>,
89    ) -> SctReport {
90        let now = SystemTime::now();
91        let report = SctReport::new(sct.log_id());
92
93        // Find the log this sct belongs to
94        let Some(log) = self.logs.get(&sct.log_id()) else {
95            return report.error_description("Unknown log id".to_string());
96        };
97        let log_name = log.client().log().description().to_string();
98        let report = report.log_name(log_name);
99
100        // Validate the signature
101        if let Err(err) = log.client().log().validate_sct_v1(chain, &sct, true) {
102            return report.error_description(format!("Failed to validate signature: {}", err));
103        };
104        let report = report.signature_validation_time(
105            DateTime::from_timestamp_millis(
106                now.duration_since(UNIX_EPOCH).unwrap().as_millis() as i64
107            )
108            .unwrap()
109            .into(),
110        );
111
112        // Get a fresh sth
113        let fresh_sth = match self.update_fresh_sth(now, log, chain.cert()).await {
114            Ok(sth) => sth,
115            Err(err) => {
116                return report.error_description(format!("Failed to fetch a fresh STH: {}", err));
117            }
118        };
119        let report = report.latest_sth(SthReport::from(&fresh_sth));
120
121        let leaf = match chain.as_leaf_v1(&sct, true) {
122            Err(err) => {
123                return report.error_description(err.to_string());
124            }
125            Ok(leaf) => leaf,
126        };
127
128        // Check inclusion
129        let oldest_sth = log.oldest_viable_sth(&sct).await.unwrap_or(fresh_sth);
130        let report = match log.check_sct_inclusion(&sct, &oldest_sth, &leaf).await {
131            Ok(index) => report.index(index),
132            Err(err) => return report.error_description(err.to_string()),
133        };
134
135        report.inclusion_proof(SthReport::from(&oldest_sth))
136    }
137
138    async fn update_report(
139        &self,
140        mut report: Report,
141        chain: &Arc<CertificateChain>,
142    ) -> Result<Report, ()> {
143        let new_sct_reports = join_all(
144            report
145                .scts
146                .drain(..)
147                .map(|sct_report| self.update_sct_report(sct_report, chain)),
148        )
149        .await;
150
151        report.scts = new_sct_reports.into_iter().collect::<Result<_, _>>()?;
152        Ok(report)
153    }
154
155    async fn update_sct_report(
156        &self,
157        report: SctReport,
158        chain: &Arc<CertificateChain>,
159    ) -> Result<SctReport, ()> {
160        let now = SystemTime::now();
161
162        // Find the log this sct belongs to
163        let log_id = v1::LogId::try_from(report.log_id.as_str())?;
164        let log_id = LogId::V1(log_id);
165
166        let Some(log) = self.logs.get(&log_id) else {
167            return Ok(report.error_description("Unknown log id".to_string()));
168        };
169
170        let fresh_sth = match self.update_fresh_sth(now, log, chain.cert()).await {
171            Ok(sth) => sth,
172            Err(err) => {
173                return Ok(
174                    report.error_description(format!("Failed to fetch a fresh STH: {}", err))
175                );
176            }
177        };
178
179        Ok(report.latest_sth(SthReport::from(&fresh_sth)))
180    }
181}