ccadb_csv/
lib.rs

1//! ccadb-csv is a crate offering helpers for processing CSV data from [Common CA Database] (CCADB)
2//! reports. These reports offer metadata about root and intermediate certificate authorities that
3//! have been disclosed to participating root programs (e.g. Mozilla, Microsoft, and Google).
4//!
5//! The structs in this crate are very thin wrappers around the CSV content, preserving values
6//! unprocessed and in String form, like the raw CSV data. Consumers that wish to process this data
7//! will likely want to create newtype wrappers that further refine the data.
8//!
9//! Presently there is support for reading the "All Certificate Records" report in [`all_cert_records`],
10//! and the "Mozilla Included CA Certificate Report" in [`mozilla_included_roots`]. See
11//! [CCADB Resources] for more information.
12//!
13//! To download the CSV data required for use with this crate see the companion
14//! ccadb-csv-fetch crate.
15//!
16//! [Common CA Database]: https://www.ccadb.org/
17//! [CCADB Resources]: https://www.ccadb.org/resources
18use std::error::Error;
19use std::io::Read;
20use std::{fmt, result};
21
22use serde::Deserialize;
23
24/// Convenience type for functions that return a `T` on success or a [`DataSourceError`] otherwise.
25pub type Result<T> = result::Result<T, DataSourceError>;
26
27#[derive(Debug)]
28#[non_exhaustive]
29/// An error that can occur while parsing a CCADB data source.
30pub enum DataSourceError {
31    #[non_exhaustive]
32    /// An error that occurred while processing CCADB CSV data.
33    Csv { source: Box<csv::Error> },
34}
35
36impl fmt::Display for DataSourceError {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        match self {
39            DataSourceError::Csv { source } => {
40                write!(f, "failed to decode CCADB CSV: {source}")
41            }
42        }
43    }
44}
45
46impl Error for DataSourceError {
47    fn source(&self) -> Option<&(dyn Error + 'static)> {
48        match self {
49            DataSourceError::Csv { source } => Some(source),
50        }
51    }
52}
53
54impl From<csv::Error> for DataSourceError {
55    fn from(source: csv::Error) -> Self {
56        DataSourceError::Csv {
57            source: Box::new(source),
58        }
59    }
60}
61
62/// Module for processing the CCADB "all certificate records version 2" CSV report.
63///
64/// This report contains information on both root certificates and intermediates, in a variety
65/// of inclusion and trust states. It does not include the PEM of the certificates themselves,
66/// but does include helpful metadata like CPS and CRL URLs.
67///
68/// If you are interested strictly in root certificates that are included in the Mozilla root
69/// program, prefer the [`mozilla_included_roots`] module.
70pub mod all_cert_records {
71    use std::io::Read;
72
73    use serde::Deserialize;
74
75    use super::{csv_metadata_iter, Result};
76
77    /// URL for the CCADB all certificate records version 2 CSV report.
78    pub const URL: &str =
79        "https://ccadb.my.salesforce-sites.com/ccadb/AllCertificateRecordsCSVFormatv2";
80
81    #[allow(dead_code)]
82    #[derive(Debug, Clone, Hash, Eq, PartialEq, Deserialize)]
83    /// Metadata related to an issuing certificate from the "all certificate records" CCADB CSV
84    /// report.
85    pub struct CertificateMetadata {
86        #[serde(rename = "CA Owner")]
87        pub ca_owner: String,
88
89        #[serde(rename = "Salesforce Record ID")]
90        pub salesforce_record_id: String,
91
92        #[serde(rename = "Certificate Name")]
93        pub certificate_name: String,
94
95        #[serde(rename = "Parent Salesforce Record ID")]
96        pub parent_salesforce_record_id: String,
97
98        #[serde(rename = "Parent Certificate Name")]
99        pub parent_certificate_name: String,
100
101        #[serde(rename = "Certificate Record Type")]
102        pub certificate_record_type: String,
103
104        #[serde(rename = "Revocation Status")]
105        pub revocation_status: String,
106
107        #[serde(rename = "SHA-256 Fingerprint")]
108        pub sha256_fingerprint: String,
109
110        #[serde(rename = "Parent SHA-256 Fingerprint")]
111        pub parent_sha256_fingerprint: String,
112
113        #[serde(rename = "Audits Same as Parent?")]
114        pub audits_same_as_parent: String,
115
116        #[serde(rename = "Auditor")]
117        pub auditor: String,
118
119        #[serde(rename = "Standard Audit URL")]
120        pub standard_audit_url: String,
121
122        #[serde(rename = "Standard Audit Type")]
123        pub standard_audit_type: String,
124
125        #[serde(rename = "Standard Audit Statement Date")]
126        pub standard_audit_statement_date: String,
127
128        #[serde(rename = "Standard Audit Period Start Date")]
129        pub standard_audit_period_start_date: String,
130
131        #[serde(rename = "Standard Audit Period End Date")]
132        pub standard_audit_period_end_date: String,
133
134        #[serde(rename = "NetSec Audit URL")]
135        pub netsec_audit_url: String,
136
137        #[serde(rename = "NetSec Audit Type")]
138        pub netsec_audit_type: String,
139
140        #[serde(rename = "NetSec Audit Statement Date")]
141        pub netsec_audit_statement_date: String,
142
143        #[serde(rename = "NetSec Audit Period Start Date")]
144        pub netsec_audit_period_start_date: String,
145
146        #[serde(rename = "NetSec Audit Period End Date")]
147        pub netsec_audit_period_end_date: String,
148
149        #[serde(rename = "TLS BR Audit URL")]
150        pub tls_br_audit_url: String,
151
152        #[serde(rename = "TLS BR Audit Type")]
153        pub tls_br_audit_type: String,
154
155        #[serde(rename = "TLS BR Audit Statement Date")]
156        pub tls_br_audit_statement_date: String,
157
158        #[serde(rename = "TLS BR Audit Period Start Date")]
159        pub tls_br_audit_period_start_date: String,
160
161        #[serde(rename = "TLS BR Audit Period End Date")]
162        pub tls_br_audit_period_end_date: String,
163
164        #[serde(rename = "TLS EVG Audit URL")]
165        pub tls_evg_audit_url: String,
166
167        #[serde(rename = "TLS EVG Audit Type")]
168        pub tls_evg_audit_type: String,
169
170        #[serde(rename = "TLS EVG Audit Statement Date")]
171        pub tls_evg_audit_statement_date: String,
172
173        #[serde(rename = "TLS EVG Audit Period Start Date")]
174        pub tls_evg_audit_period_start_date: String,
175
176        #[serde(rename = "TLS EVG Audit Period End Date")]
177        pub tls_evg_audit_period_end_date: String,
178
179        #[serde(rename = "Code Signing Audit URL")]
180        pub code_signing_audit_url: String,
181
182        #[serde(rename = "Code Signing Audit Type")]
183        pub code_signing_audit_type: String,
184
185        #[serde(rename = "Code Signing Audit Statement Date")]
186        pub code_signing_audit_statement_date: String,
187
188        #[serde(rename = "Code Signing Audit Period Start Date")]
189        pub code_signing_audit_period_start_date: String,
190
191        #[serde(rename = "Code Signing Audit Period End Date")]
192        pub code_signing_audit_period_end_date: String,
193
194        #[serde(rename = "CP/CPS Same as Parent?")]
195        pub cp_cps_same_as_parent: String,
196
197        #[serde(rename = "Certificate Policy (CP) URL")]
198        pub certificate_policy_url: String,
199
200        #[serde(rename = "Certificate Practice Statement (CPS) URL")]
201        pub certificate_practice_statement_cps_url: String,
202
203        #[serde(rename = "CP/CPS Last Updated Date")]
204        pub cp_cps_last_updated_date: String,
205
206        #[serde(rename = "Test Website URL - Valid")]
207        pub test_website_url_valid: String,
208
209        #[serde(rename = "Test Website URL - Expired")]
210        pub test_website_url_expired: String,
211
212        #[serde(rename = "Test Website URL - Revoked")]
213        pub test_website_url_revoked: String,
214
215        #[serde(rename = "Technically Constrained")]
216        pub technically_constrained: String,
217
218        #[serde(rename = "Subordinate CA Owner")]
219        pub subordinate_ca_owner: String,
220
221        #[serde(rename = "Full CRL Issued By This CA")]
222        pub full_crl_issued_by_this_ca: String,
223
224        #[serde(rename = "JSON Array of Partitioned CRLs")]
225        pub json_array_of_partitioned_crls: String,
226
227        #[serde(rename = "Valid From (GMT)")]
228        pub valid_from_gmt: String,
229
230        #[serde(rename = "Valid To (GMT)")]
231        pub valid_to_gmt: String,
232
233        #[serde(rename = "Derived Trust Bits")]
234        pub derived_trust_bits: String,
235
236        #[serde(rename = "Chrome Status")]
237        pub chrome_status: String,
238
239        #[serde(rename = "Microsoft Status")]
240        pub microsoft_status: String,
241
242        #[serde(rename = "Mozilla Status")]
243        pub mozilla_status: String,
244
245        #[serde(rename = "Status of Root Cert")]
246        pub status_of_root_cert: String,
247
248        #[serde(rename = "Authority Key Identifier")]
249        pub authority_key_identifier: String,
250
251        #[serde(rename = "Subject Key Identifier")]
252        pub subject_key_identifier: String,
253
254        #[serde(rename = "Country")]
255        pub country: String,
256
257        #[serde(rename = "TLS Capable")]
258        pub tls_capable: String,
259
260        #[serde(rename = "TLS EV Capable")]
261        pub tls_ev_capable: String,
262
263        #[serde(rename = "Code Signing Capable")]
264        pub code_signing_capable: String,
265
266        #[serde(rename = "S/MIME Capable")]
267        pub smime_capable: String,
268    }
269
270    /// Read the provided CSV data, producing an iterator of [`CertificateMetadata`] parse results
271    /// for each of the rows.
272    pub fn read_csv<'csv>(
273        data: impl Read + 'csv,
274    ) -> impl Iterator<Item = Result<CertificateMetadata>> {
275        csv_metadata_iter(data)
276    }
277}
278
279/// Module for processing the CCADB "included CA certificate PEM" CSV report.
280///
281/// This report contains information about root CA certificates (not intermediates) that are
282/// included in the Mozilla root program. PEM content for each root is available.
283///
284/// If you are interested in issuers included in other programs, for purposes other than TLS,
285/// or for metadata such as CPS or CRL URLs, prefer the broader [`all_cert_records`] module.
286pub mod mozilla_included_roots {
287    use std::io::Read;
288
289    use serde::Deserialize;
290
291    use super::{csv_metadata_iter, Result};
292
293    /// URL for the CCADB Mozilla included CA certificate PEM CSV report.
294    pub const URL: &str =
295        "https://ccadb.my.salesforce-sites.com/mozilla/IncludedCACertificateReportPEMCSV";
296
297    #[allow(dead_code)]
298    #[derive(Debug, Clone, Hash, Eq, PartialEq, Deserialize)]
299    /// Metadata related to an included root CA certificate from the Mozilla
300    /// "included CA certificate PEM" CCADB CSV report.
301    pub struct CertificateMetadata {
302        #[serde(rename = "Owner")]
303        pub owner: String,
304
305        #[serde(rename = "Certificate Issuer Organization")]
306        pub certificate_issuer_organization: String,
307
308        #[serde(rename = "Certificate Issuer Organizational Unit")]
309        pub certificate_issuer_organizational_unit: String,
310
311        #[serde(rename = "Common Name or Certificate Name")]
312        pub common_name_or_certificate_name: String,
313
314        #[serde(rename = "Certificate Serial Number")]
315        pub certificate_serial_number: String,
316
317        #[serde(rename = "SHA-256 Fingerprint")]
318        pub sha256_fingerprint: String,
319
320        #[serde(rename = "Subject + SPKI SHA256")]
321        pub subject_spki_sha256: String,
322
323        #[serde(rename = "Valid From [GMT]")]
324        pub valid_from_gmt: String,
325
326        #[serde(rename = "Valid To [GMT]")]
327        pub valid_to_gmt: String,
328
329        #[serde(rename = "Public Key Algorithm")]
330        pub public_key_algorithm: String,
331
332        #[serde(rename = "Signature Hash Algorithm")]
333        pub signature_hash_algorithm: String,
334
335        #[serde(rename = "Trust Bits")]
336        pub trust_bits: String,
337
338        #[serde(rename = "Distrust for TLS After Date")]
339        pub distrust_for_tls_after_date: String,
340
341        #[serde(rename = "Distrust for S/MIME After Date")]
342        pub distrust_for_smime_after_date: String,
343
344        #[serde(rename = "EV Policy OID(s)")]
345        pub ev_policy_oids: String,
346
347        #[serde(rename = "Approval Bug")]
348        pub approval_bug: String,
349
350        #[serde(rename = "NSS Release When First Included")]
351        pub nss_release_when_first_included: String,
352
353        #[serde(rename = "Firefox Release When First Included")]
354        pub firefox_release_when_first_included: String,
355
356        #[serde(rename = "Test Website - Valid")]
357        pub test_website_valid: String,
358
359        #[serde(rename = "Test Website - Expired")]
360        pub test_website_expired: String,
361
362        #[serde(rename = "Test Website - Revoked")]
363        pub test_website_revoked: String,
364
365        #[serde(rename = "Mozilla Applied Constraints")]
366        pub mozilla_applied_constraints: String,
367
368        #[serde(rename = "Company Website")]
369        pub company_website: String,
370
371        #[serde(rename = "Geographic Focus")]
372        pub geographic_focus: String,
373
374        #[serde(rename = "Certificate Policy (CP)")]
375        pub certificate_policy_cp: String,
376
377        #[serde(rename = "Certification Practice Statement (CPS)")]
378        pub certificate_practice_statement_cps: String,
379
380        #[serde(rename = "Standard Audit")]
381        pub standard_audit: String,
382
383        #[serde(rename = "BR Audit")]
384        pub br_audit: String,
385
386        #[serde(rename = "EV Audit")]
387        pub ev_audit: String,
388
389        #[serde(rename = "Auditor")]
390        pub auditor: String,
391
392        #[serde(rename = "Standard Audit Type")]
393        pub standard_audit_type: String,
394
395        #[serde(rename = "Standard Audit Statement Dt")]
396        pub standard_audit_statement_dt: String,
397
398        #[serde(rename = "PEM Info")]
399        pub pem_info: String,
400    }
401
402    /// Read the provided CSV data, producing an iterator of [`CertificateMetadata`] parse results
403    /// for each of the rows.
404    pub fn read_csv(data: impl Read) -> impl Iterator<Item = Result<CertificateMetadata>> {
405        csv_metadata_iter(data)
406    }
407}
408
409#[cfg(test)]
410// simple smoke tests against test data files with 1 record each.
411mod tests {
412    use std::fs::File;
413    use std::path::{Path, PathBuf};
414
415    use super::all_cert_records;
416    use super::mozilla_included_roots;
417    use super::Result;
418
419    #[test]
420    fn test_included_roots_read_csv() {
421        let data_file = File::open(test_resource_path(
422            "IncludedCACertificateReportPEMCSV.test.csv",
423        ))
424        .unwrap();
425
426        let records = mozilla_included_roots::read_csv(data_file)
427            .collect::<Result<Vec<_>>>()
428            .expect("failed to parse included certificates records CSV");
429        assert!(!records.is_empty());
430
431        let expected = mozilla_included_roots::CertificateMetadata {
432            owner: "Internet Security Research Group".to_owned(),
433            certificate_issuer_organization: "Internet Security Research Group".to_string(),
434            certificate_issuer_organizational_unit: "".to_string(),
435            common_name_or_certificate_name: "ISRG Root X1".to_string(),
436            certificate_serial_number: "008210CFB0D240E3594463E0BB63828B00".to_string(),
437            sha256_fingerprint: "96BCEC06264976F37460779ACF28C5A7CFE8A3C0AAE11A8FFCEE05C0BDDF08C6"
438                .to_string(),
439            subject_spki_sha256: "DA43F86604EB9619893C744D6AFBC37A7A57A0FBA3841E8D95488F5C798B150A"
440                .to_string(),
441            valid_from_gmt: "2015.06.04".to_string(),
442            valid_to_gmt: "2035.06.04".to_string(),
443            public_key_algorithm: "RSA 4096 bits".to_string(),
444            signature_hash_algorithm: "SHA256WithRSA".to_string(),
445            trust_bits: "Websites".to_string(),
446            distrust_for_tls_after_date: "".to_string(),
447            distrust_for_smime_after_date: "".to_string(),
448            ev_policy_oids: "Not EV".to_string(),
449            approval_bug: "https://bugzilla.mozilla.org/show_bug.cgi?id=1204656".to_string(),
450            nss_release_when_first_included: "NSS 3.26".to_string(),
451            firefox_release_when_first_included: "Firefox 50".to_string(),
452            test_website_valid: "https://valid-isrgrootx1.letsencrypt.org/".to_string(),
453            test_website_expired: "https://expired-isrgrootx1.letsencrypt.org/".to_string(),
454            test_website_revoked: "https://revoked-isrgrootx1.letsencrypt.org/".to_string(),
455            mozilla_applied_constraints: "".to_string(),
456            company_website: "https://letsencrypt.org/".to_string(),
457            geographic_focus: "Global".to_string(),
458            certificate_policy_cp: "https://letsencrypt.org/documents/isrg-cp-v3.4/; https://letsencrypt.org/documents/isrg-cp-v3.3/; https://letsencrypt.org/documents/isrg-cp-v3.1/; https://letsencrypt.org/documents/isrg-cp-v2.7/; https://letsencrypt.org/documents/isrg-cp-v2.6/; https://letsencrypt.org/documents/isrg-cp-v2.5/; https://letsencrypt.org/documents/isrg-cp-v2.4/".to_string(),
459            certificate_practice_statement_cps: "https://letsencrypt.org/documents/isrg-cps-v4.5/; https://letsencrypt.org/documents/isrg-cps-v4.4/; https://letsencrypt.org/documents/isrg-cps-v4.3/; https://letsencrypt.org/documents/isrg-cps-v4.1/; https://letsencrypt.org/documents/isrg-cps-v3.3/; https://letsencrypt.org/documents/isrg-cps-v3.1/; https://letsencrypt.org/documents/isrg-cps-v3.0/; https://letsencrypt.org/documents/isrg-cps-v2.9/; https://letsencrypt.org/documents/isrg-cps-v2.7/".to_string(),
460            standard_audit: "https://www.cpacanada.ca/generichandlers/CPACHandler.ashx?attachmentid=cd221a0a-aa3c-49a9-bd8a-ad336588075a".to_string(),
461            br_audit: "https://www.cpacanada.ca/generichandlers/CPACHandler.ashx?attachmentid=7f5e9f87-ecfd-4120-ae6f-e136e8637a4b".to_string(),
462            ev_audit: "".to_string(),
463            auditor: "Schellman & Company, LLC.".to_string(),
464            standard_audit_type: "WebTrust".to_string(),
465            standard_audit_statement_dt: "2022.11.08".to_string(),
466            pem_info: "'-----BEGIN CERTIFICATE-----\r\nMIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw\r\nTzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh\r\ncmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4\r\nWhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu\r\nZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY\r\nMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc\r\nh77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+\r\n0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U\r\nA5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW\r\nT8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH\r\nB5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC\r\nB5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv\r\nKBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn\r\nOlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn\r\njh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw\r\nqHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI\r\nrU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV\r\nHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq\r\nhkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL\r\nubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ\r\n3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK\r\nNFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5\r\nORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur\r\nTkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC\r\njNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc\r\noyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq\r\n4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA\r\nmRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d\r\nemyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=\r\n-----END CERTIFICATE-----'".to_string(),
467        };
468
469        assert_eq!(records.first().unwrap(), &expected)
470    }
471
472    #[test]
473    fn test_all_records_read_csv() {
474        let data_file = File::open(test_resource_path(
475            "AllCertificateRecordsCSVFormat.test.csv",
476        ))
477        .unwrap();
478
479        let records = all_cert_records::read_csv(data_file)
480            .collect::<Result<Vec<_>>>()
481            .unwrap();
482        assert!(!records.is_empty());
483
484        let expected = all_cert_records::CertificateMetadata {
485            ca_owner: "QuoVadis".to_string(),
486            salesforce_record_id: "0018Z00002vyRdNQAU".to_string(),
487            certificate_name: "DigiCert QuoVadis G3 Qualified BE itsme RSA4096 SHA256 2023 CA1"
488                .to_string(),
489            parent_salesforce_record_id: "001o000000HshFQAAZ".to_string(),
490            parent_certificate_name: "QuoVadis Root CA 1 G3".to_string(),
491            certificate_record_type: "Intermediate Certificate".to_string(),
492            revocation_status: "Not Revoked".to_string(),
493            sha256_fingerprint: "C0EE0CCED463096DF07D27257AF79C986FF92B678F669C109FFF570F32AB433F"
494                .to_string(),
495            parent_sha256_fingerprint:
496            "8A866FD1B276B57E578E921C65828A2BED58E9F2F288054134B7F1F4BFC9CC74".to_string(),
497            audits_same_as_parent: "true".to_string(),
498            auditor: "".to_string(),
499            standard_audit_url: "".to_string(),
500            standard_audit_type: "".to_string(),
501            standard_audit_statement_date: "".to_string(),
502            standard_audit_period_start_date: "".to_string(),
503            standard_audit_period_end_date: "".to_string(),
504            netsec_audit_url: "".to_string(),
505            netsec_audit_type: "".to_string(),
506            netsec_audit_statement_date: "".to_string(),
507            netsec_audit_period_start_date: "".to_string(),
508            netsec_audit_period_end_date: "".to_string(),
509            tls_br_audit_url: "".to_string(),
510            tls_br_audit_type: "".to_string(),
511            tls_br_audit_statement_date: "".to_string(),
512            tls_br_audit_period_start_date: "".to_string(),
513            tls_br_audit_period_end_date: "".to_string(),
514            tls_evg_audit_url: "".to_string(),
515            tls_evg_audit_type: "".to_string(),
516            tls_evg_audit_statement_date: "".to_string(),
517            tls_evg_audit_period_start_date: "".to_string(),
518            tls_evg_audit_period_end_date: "".to_string(),
519            code_signing_audit_url: "".to_string(),
520            code_signing_audit_type: "".to_string(),
521            code_signing_audit_statement_date: "".to_string(),
522            code_signing_audit_period_start_date: "".to_string(),
523            code_signing_audit_period_end_date: "".to_string(),
524            cp_cps_same_as_parent: "true".to_string(),
525            certificate_policy_url: "".to_string(),
526            certificate_practice_statement_cps_url: "".to_string(),
527            cp_cps_last_updated_date: "".to_string(),
528            test_website_url_valid: "".to_string(),
529            test_website_url_expired: "".to_string(),
530            test_website_url_revoked: "".to_string(),
531            technically_constrained: "false".to_string(),
532            mozilla_status: "Provided by CA".to_string(),
533            microsoft_status: "Not Included".to_string(),
534            subordinate_ca_owner: "".to_string(),
535            full_crl_issued_by_this_ca: "".to_string(),
536            json_array_of_partitioned_crls: "[\"http://crl.digicert.eu/DigiCertQuoVadisG3QualifiedBEitsmeRSA4096SHA2562023CA1.crl\"]".to_string(),
537            valid_from_gmt: "2023.03.14".to_string(),
538            valid_to_gmt: "2032.03.11".to_string(),
539            chrome_status: "Not Included".to_string(),
540            derived_trust_bits: "Client Authentication;Secure Email;Document Signing".to_string(),
541            status_of_root_cert: "Apple: Included; Google Chrome: Included; Microsoft: Included; Mozilla: Included".to_string(),
542            authority_key_identifier: "o5fW816iEOGrRZ88F2Q87gFwnMw=".to_string(),
543            subject_key_identifier: "7RAkwGs8hi1E+nylj8w5J87dR7c=".to_string(),
544            country: "Bermuda".to_string(),
545            tls_capable: "False".to_string(),
546            tls_ev_capable: "False".to_string(),
547            code_signing_capable: "False".to_string(),
548            smime_capable: "True".to_string(),
549        };
550
551        assert_eq!(records.first().unwrap(), &expected);
552    }
553
554    fn test_resource_path(filename: impl AsRef<Path>) -> PathBuf {
555        let mut resource_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
556        resource_path.push("testdata/");
557        resource_path.push(filename);
558        resource_path
559    }
560}
561
562// read the provided data as CSV with a header line, producing an iterator over the
563// deserialized records.
564fn csv_metadata_iter<T: for<'a> Deserialize<'a>>(
565    data: impl Read,
566) -> impl Iterator<Item = Result<T>> {
567    csv::ReaderBuilder::new()
568        .has_headers(true)
569        .from_reader(data)
570        .into_deserialize::<T>()
571        .map(|r| r.map_err(Into::into))
572}