luct-scanner 0.2.2

Tool to provide additional checks on TLS certificates
Documentation
use crate::{Report, Scanner, ScannerError, ScannerImpl, SctReport, SthReport};
use chrono::DateTime;
use futures::future::join_all;
use luct_core::{
    CertificateChain, LogId,
    store::{AsyncStoreRead, AsyncStoreWrite},
    v1::{self, SignedCertificateTimestamp},
};
use std::sync::Arc;
use web_time::{SystemTime, UNIX_EPOCH};

impl<S: ScannerImpl> Scanner<S> {
    pub async fn collect_report_pem(&self, data: &str) -> Result<Report, ScannerError> {
        let cert_chain = Arc::new(CertificateChain::from_pem_chain(data)?);

        if self.config.validate_cert_chain {
            cert_chain.verify_chain()?;
        }

        self.collect_report(cert_chain).await
    }

    // TODO: Remove Result here
    pub async fn collect_report(
        &self,
        chain: Arc<CertificateChain>,
    ) -> Result<Report, ScannerError> {
        let cert = chain.cert();
        let cert_fp = cert.fingerprint_sha256();

        let report = match self.report_store.get(cert_fp.clone()).await {
            Some(report) => {
                tracing::debug!("Found report for {} in cache", cert_fp.to_string());

                // We need to update the report. If we fail, this is likely because the report
                // does no longer serialize, e.g. due to an update
                match self.update_report(report, &chain).await {
                    Err(()) => {
                        tracing::info!(
                            "Found an invalid report (likely generated by outdated version). Will generate fresh report"
                        );
                        self.create_report(chain).await
                    }
                    Ok(report) => report,
                }
            }
            None => {
                tracing::debug!("Could not find report for {} in cache", cert_fp.to_string());
                self.create_report(chain).await
            }
        };

        let report = self.evaluate_policy(report, (self.time_source)());
        if report.get_error().is_none() {
            self.report_store.insert(cert_fp, report.clone()).await;
        }

        Ok(report)
    }

    async fn create_report(&self, chain: Arc<CertificateChain>) -> Report {
        let cert = chain.cert();

        let mut report = Report::from(chain.as_ref());

        let embedded_scts = match cert.extract_scts_v1() {
            Err(err) => {
                return report
                    .error_description(format!("Failed to parse SCTs from certificate:s {}", err));
            }
            Ok(scts) => scts,
        };

        let sct_reports = join_all(
            embedded_scts
                .into_iter()
                .map(|sct| self.collect_embedded_sct_report(sct, &chain)),
        )
        .await;

        report.scts = sct_reports;
        report
    }

    async fn collect_embedded_sct_report(
        &self,
        sct: SignedCertificateTimestamp,
        chain: &Arc<CertificateChain>,
    ) -> SctReport {
        let now = SystemTime::now();
        let report = SctReport::new(sct.log_id());

        // Find the log this sct belongs to
        let Some(log) = self.logs.get(&sct.log_id()) else {
            return report.error_description("Unknown log id".to_string());
        };
        let log_name = log.client().log().description().to_string();
        let report = report.log_name(log_name);

        // Validate the signature
        if let Err(err) = log.client().log().validate_sct_v1(chain, &sct, true) {
            return report.error_description(format!("Failed to validate signature: {}", err));
        };
        let report = report.signature_validation_time(
            DateTime::from_timestamp_millis(
                now.duration_since(UNIX_EPOCH).unwrap().as_millis() as i64
            )
            .unwrap()
            .into(),
        );

        // Get a fresh sth
        let fresh_sth = match self.update_fresh_sth(now, log, chain.cert()).await {
            Ok(sth) => sth,
            Err(err) => {
                return report.error_description(format!("Failed to fetch a fresh STH: {}", err));
            }
        };
        let report = report.latest_sth(SthReport::from(&fresh_sth));

        let leaf = match chain.as_leaf_v1(&sct, true) {
            Err(err) => {
                return report.error_description(err.to_string());
            }
            Ok(leaf) => leaf,
        };

        // Check inclusion
        let oldest_sth = log.oldest_viable_sth(&sct).await.unwrap_or(fresh_sth);
        let report = match log.check_sct_inclusion(&sct, &oldest_sth, &leaf).await {
            Ok(index) => report.index(index),
            Err(err) => return report.error_description(err.to_string()),
        };

        report.inclusion_proof(SthReport::from(&oldest_sth))
    }

    async fn update_report(
        &self,
        mut report: Report,
        chain: &Arc<CertificateChain>,
    ) -> Result<Report, ()> {
        let new_sct_reports = join_all(
            report
                .scts
                .drain(..)
                .map(|sct_report| self.update_sct_report(sct_report, chain)),
        )
        .await;

        report.scts = new_sct_reports.into_iter().collect::<Result<_, _>>()?;
        Ok(report)
    }

    async fn update_sct_report(
        &self,
        report: SctReport,
        chain: &Arc<CertificateChain>,
    ) -> Result<SctReport, ()> {
        let now = SystemTime::now();

        // Find the log this sct belongs to
        let log_id = v1::LogId::try_from(report.log_id.as_str())?;
        let log_id = LogId::V1(log_id);

        let Some(log) = self.logs.get(&log_id) else {
            return Ok(report.error_description("Unknown log id".to_string()));
        };

        let fresh_sth = match self.update_fresh_sth(now, log, chain.cert()).await {
            Ok(sth) => sth,
            Err(err) => {
                return Ok(
                    report.error_description(format!("Failed to fetch a fresh STH: {}", err))
                );
            }
        };

        Ok(report.latest_sth(SthReport::from(&fresh_sth)))
    }
}