1use std::error::Error;
2use std::fs::File;
3use std::io::{Read, Take};
4use std::path::Path;
5use std::sync::Arc;
6use std::time::Duration;
7use std::{fmt, io, result};
8
9pub type Result<T> = result::Result<T, FetchError>;
11
12#[derive(Debug)]
14#[non_exhaustive]
15pub enum FetchError {
16 #[non_exhaustive]
18 Api { source: Box<ureq::Error> },
19
20 #[non_exhaustive]
22 DataSource {
23 source: Box<ccadb_csv::DataSourceError>,
24 },
25
26 #[non_exhaustive]
28 File { source: io::Error },
29
30 #[non_exhaustive]
32 UnknownReport { name: String },
33}
34
35impl fmt::Display for FetchError {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 match self {
38 FetchError::Api { source } => {
39 write!(f, "failed to fetch CCADB CSV: {source}")
40 }
41 FetchError::DataSource { source } => {
42 write!(f, "failed to decode fetched CCADB CSV: {source}")
43 }
44 FetchError::File { source } => {
45 write!(f, "failed to write fetched CCADB CSV: {source}")
46 }
47 FetchError::UnknownReport { name } => {
48 write!(f, "unknown report type: {name}")
49 }
50 }
51 }
52}
53
54impl Error for FetchError {
55 fn source(&self) -> Option<&(dyn Error + 'static)> {
56 match self {
57 FetchError::Api { source } => Some(source),
58 FetchError::DataSource { source } => Some(source),
59 FetchError::File { source } => Some(source),
60 FetchError::UnknownReport { .. } => None,
61 }
62 }
63}
64
65impl From<ureq::Error> for FetchError {
66 fn from(source: ureq::Error) -> Self {
67 let source = Box::new(source);
68 FetchError::Api { source }
69 }
70}
71
72impl From<ccadb_csv::DataSourceError> for FetchError {
73 fn from(source: ccadb_csv::DataSourceError) -> Self {
74 let source = Box::new(source);
75 FetchError::DataSource { source }
76 }
77}
78
79impl From<io::Error> for FetchError {
80 fn from(source: io::Error) -> Self {
81 FetchError::File { source }
82 }
83}
84
85pub enum ReportType {
87 AllCertRecords,
89
90 MozillaIncludedRoots,
92}
93
94impl ReportType {
95 #[must_use]
96 pub fn url(&self) -> &str {
97 match self {
98 ReportType::AllCertRecords => ccadb_csv::all_cert_records::URL,
99 ReportType::MozillaIncludedRoots => ccadb_csv::mozilla_included_roots::URL,
100 }
101 }
102}
103
104impl fmt::Display for ReportType {
105 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106 match self {
107 ReportType::AllCertRecords => write!(f, "all-cert-records"),
108 ReportType::MozillaIncludedRoots => write!(f, "mozilla-included-roots"),
109 }
110 }
111}
112
113impl TryFrom<&str> for ReportType {
114 type Error = FetchError;
115
116 fn try_from(report_type: &str) -> result::Result<Self, Self::Error> {
117 match report_type {
118 "all-cert-records" => Ok(ReportType::AllCertRecords),
119 "mozilla-included-roots" => Ok(ReportType::MozillaIncludedRoots),
120 _ => Err(FetchError::UnknownReport {
121 name: report_type.to_owned(),
122 }),
123 }
124 }
125}
126
127pub fn fetch_report(report_type: &ReportType, output: impl AsRef<Path>) -> Result<u64> {
134 let mut output_file = File::create(output)?;
135 let mut csv_reader = read_csv_url(report_type.url())?;
136 Ok(io::copy(&mut csv_reader, &mut output_file)?)
137}
138
139fn read_csv_url(url: &str) -> Result<Take<Box<dyn Read + Send + Sync>>> {
140 let agent = ureq::builder()
141 .tls_config(Arc::new(tls_config()))
142 .timeout_read(Duration::from_secs(60))
143 .user_agent(format!("ccadb-csv-fetch/{VERSION}").as_ref())
144 .build();
145
146 Ok(agent.get(url).call()?.into_reader().take(READ_LIMIT))
147}
148
149fn tls_config() -> rustls::ClientConfig {
152 let anchor_der = rustls::pki_types::CertificateDer::from(CCADB_API_ROOT);
153 let anchor = webpki::anchor_from_trusted_cert(&anchor_der)
154 .unwrap()
155 .to_owned();
156 let root_store = rustls::RootCertStore {
157 roots: vec![anchor].into_iter().collect(),
158 };
159 rustls::ClientConfig::builder()
160 .with_root_certificates(root_store)
161 .with_no_client_auth()
162}
163
164const VERSION: &str = env!("CARGO_PKG_VERSION");
165
166const READ_LIMIT: u64 = 25_000_000; const CCADB_API_ROOT: &[u8] = include_bytes!("DigiCertGlobalRootCA.crt");
179
180#[cfg(test)]
181mod tests {
182 use crate::{read_csv_url, ReportType};
183 use std::io::{BufRead, BufReader};
184
185 #[test]
189 fn csv_header_check() {
190 let expected_headers = [
192 (
193 ReportType::AllCertRecords,
194 r#""CA Owner","Salesforce Record ID","Certificate Name","Parent Salesforce Record ID","Parent Certificate Name","Certificate Record Type","Revocation Status","SHA-256 Fingerprint","Parent SHA-256 Fingerprint","Audits Same as Parent?","Auditor","Standard Audit URL","Standard Audit Type","Standard Audit Statement Date","Standard Audit Period Start Date","Standard Audit Period End Date","NetSec Audit URL","NetSec Audit Type","NetSec Audit Statement Date","NetSec Audit Period Start Date","NetSec Audit Period End Date","TLS BR Audit URL","TLS BR Audit Type","TLS BR Audit Statement Date","TLS BR Audit Period Start Date","TLS BR Audit Period End Date","TLS EVG Audit URL","TLS EVG Audit Type","TLS EVG Audit Statement Date","TLS EVG Audit Period Start Date","TLS EVG Audit Period End Date","Code Signing Audit URL","Code Signing Audit Type","Code Signing Audit Statement Date","Code Signing Audit Period Start Date","Code Signing Audit Period End Date","S/MIME BR Audit URL","S/MIME BR Audit Type","S/MIME BR Audit Statement Date","S/MIME BR Audit Period Start Date","S/MIME BR Audit Period End Date","CP/CPS Same as Parent?","Certificate Policy (CP) URL","Certificate Practice Statement (CPS) URL","CP/CPS Last Updated Date","Test Website URL - Valid","Test Website URL - Expired","Test Website URL - Revoked","Technically Constrained","Subordinate CA Owner","Full CRL Issued By This CA","JSON Array of Partitioned CRLs","Valid From (GMT)","Valid To (GMT)","Derived Trust Bits","Chrome Status","Microsoft Status","Mozilla Status","Status of Root Cert","Authority Key Identifier","Subject Key Identifier","Country","TLS Capable","TLS EV Capable","Code Signing Capable","S/MIME Capable""#,
195 ),
196 (
197 ReportType::MozillaIncludedRoots,
198 r#""Owner","Certificate Issuer Organization","Certificate Issuer Organizational Unit","Common Name or Certificate Name","Certificate Serial Number","SHA-256 Fingerprint","Subject + SPKI SHA256","Valid From [GMT]","Valid To [GMT]","Public Key Algorithm","Signature Hash Algorithm","Trust Bits","Distrust for TLS After Date","Distrust for S/MIME After Date","EV Policy OID(s)","Approval Bug","NSS Release When First Included","Firefox Release When First Included","Test Website - Valid","Test Website - Expired","Test Website - Revoked","Mozilla Applied Constraints","Company Website","Geographic Focus","Certificate Policy (CP)","Certification Practice Statement (CPS)","Standard Audit","BR Audit","EV Audit","Auditor","Standard Audit Type","Standard Audit Statement Dt","PEM Info""#,
199 ),
200 ];
201
202 for (report_type, expected_header) in expected_headers {
203 let report_data = read_csv_url(report_type.url()).expect("CSV URL fetch failed");
204
205 let mut buf_reader = BufReader::new(report_data);
206 let mut first_line = String::new();
207 buf_reader
208 .read_line(&mut first_line)
209 .expect("CSV missing header line");
210
211 assert_eq!(first_line.trim(), expected_header);
212 }
213 }
214}