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
}
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());
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());
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);
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(),
);
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,
};
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();
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)))
}
}