From 08972db98bafca0bb1ed682b018f0e37352a2767 Mon Sep 17 00:00:00 2001 From: Daniel McCarney Date: Wed, 9 Aug 2023 15:02:58 -0400 Subject: [PATCH] codegen: use CCADB as the source of truth. Prior to this commit the `tests/codegen.rs` generator used https://mkcert.org as its source of truth for trusted root metadata. This commit replaces that source of truth (and accompanying generator code) to use https://ccadb.org instead. The Common CA Database (CCADB) has emerged as a multi-stakeholder repository for information about certificate authorities participating in the trust stores maintained by CCADB root store operators. The `IncludedCACertificateReportPEMCSV` report made available by CCADB is a great replacement for the needs of webpki-roots: * it allows us to filter by roots that are trusted for TLS. * it allows us to filter by "distrust after" dates. * it allows us to generate imposed name constraints automatically. This removes the need to maintain a separate distrust list in webpki-roots, or a separate manually curated imposed name constraints set. To minimize the trust surface of webpki-roots we take care to pin the trust anchor used to fetch the CCADB CSV to the trust anchor in use today for serving https://ccadb-public.secure.force.com/, helping minimize the risk of person-in-the-middle attack. Note that we are not pinning the leaf/intermediates in use, just the expected root. --- Cargo.toml | 8 +- src/lib.rs | 28 +- tests/codegen.rs | 474 ++++++++++++++++++---------- tests/data/DigiCertGlobalRootCA.pem | 22 ++ 4 files changed, 354 insertions(+), 178 deletions(-) create mode 100644 tests/data/DigiCertGlobalRootCA.pem diff --git a/Cargo.toml b/Cargo.toml index 6185913..44baff6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,11 +9,17 @@ homepage = "https://github.com/rustls/webpki-roots" repository = "https://github.com/rustls/webpki-roots" [dev-dependencies] +chrono = { version = "0.4.26", default-features = false, features = ["clock"] } +csv = "1.2.2" +hex = "0.4.3" +num-bigint = "0.4.3" percent-encoding = "2.3" rcgen = "0.11.1" -reqwest = { version = "0.11", features = ["rustls-tls-native-roots"] } +reqwest = { version = "0.11", features = ["rustls-tls-manual-roots"] } ring = "0.16.20" rustls-pemfile = "1" +serde = { version = "1.0.183", features = ["derive"] } tokio = { version = "1", features = ["macros", "rt-multi-thread"] } webpki = { package = "rustls-webpki", version = "0.101.2" } x509-parser = "0.15.1" +yasna = "0.5.2" diff --git a/src/lib.rs b/src/lib.rs index 486a0e1..6c1a2f3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,6 @@ //! -//! This library is automatically generated from the Mozilla certificate -//! store via mkcert.org. Don't edit it. +//! This library is automatically generated from the Mozilla +//! IncludedCACertificateReportPEMCSV report via ccadb.org. Don't edit it. //! //! The generation is done deterministically so you can verify it //! yourself by inspecting and re-running the generation process. @@ -401,7 +401,7 @@ pub const TLS_SERVER_ROOTS: &[TrustAnchor] = &[ /* * Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5 * Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5 - * Label: "GlobalSign ECC Root CA - R5" + * Label: "GlobalSign" * Serial: 32785792099990507226680698011560947931244 * SHA256 Fingerprint: 17:9f:bc:14:8a:3d:d0:0f:d2:4e:a1:34:58:cc:43:bf:a7:f5:9c:81:82:d7:83:a5:13:f6:eb:ec:10:0c:89:24 * -----BEGIN CERTIFICATE----- @@ -714,7 +714,7 @@ pub const TLS_SERVER_ROOTS: &[TrustAnchor] = &[ /* * Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R6 * Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R6 - * Label: "GlobalSign Root CA - R6" + * Label: "GlobalSign" * Serial: 1417766617973444989252670301619537 * SHA256 Fingerprint: 2c:ab:ea:fe:37:d0:6c:a2:2a:ba:73:91:c0:03:3d:25:98:29:52:c4:53:64:73:49:76:3a:3a:b5:ad:6c:cf:69 * -----BEGIN CERTIFICATE----- @@ -2372,9 +2372,9 @@ pub const TLS_SERVER_ROOTS: &[TrustAnchor] = &[ }, /* - * Issuer: CN=Entrust.net Certification Authority (2048) O=Entrust.net OU=www.entrust.net/CPS_2048 incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited - * Subject: CN=Entrust.net Certification Authority (2048) O=Entrust.net OU=www.entrust.net/CPS_2048 incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited - * Label: "Entrust.net Premium 2048 Secure Server CA" + * Issuer: CN=Entrust.net Certification Authority (2048) O=Entrust.net OU=(c) 1999 Entrust.net Limited + * Subject: CN=Entrust.net Certification Authority (2048) O=Entrust.net OU=(c) 1999 Entrust.net Limited + * Label: "Entrust.net Certification Authority (2048)" * Serial: 946069240 * SHA256 Fingerprint: 6d:c4:71:72:e0:1c:bc:b0:bf:62:58:0d:89:5f:e2:b8:ac:9a:d4:f8:73:80:1e:0c:10:b9:c8:37:d2:1e:b1:77 * -----BEGIN CERTIFICATE----- @@ -3648,7 +3648,7 @@ pub const TLS_SERVER_ROOTS: &[TrustAnchor] = &[ /* * Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4 * Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4 - * Label: "GlobalSign ECC Root CA - R4" + * Label: "GlobalSign" * Serial: 159662223612894884239637590694 * SHA256 Fingerprint: b0:85:d7:0b:96:4f:19:1a:73:e4:af:0d:54:ae:7a:0e:07:aa:fd:af:9b:71:dd:08:62:13:8a:b7:32:5a:24:a2 * -----BEGIN CERTIFICATE----- @@ -4013,7 +4013,7 @@ pub const TLS_SERVER_ROOTS: &[TrustAnchor] = &[ /* * Issuer: O=Chunghwa Telecom Co., Ltd. OU=ePKI Root Certification Authority * Subject: O=Chunghwa Telecom Co., Ltd. OU=ePKI Root Certification Authority - * Label: "ePKI Root Certification Authority" + * Label: "Chunghwa Telecom Co., Ltd. - ePKI Root Certification Authority" * Serial: 28956088682735189655030529057352760477 * SHA256 Fingerprint: c0:a6:f4:dc:63:a2:4b:fd:cf:54:ef:2a:6a:08:2a:0a:72:de:35:80:3e:2f:f5:ff:52:7a:e5:d8:72:06:df:d5 * -----BEGIN CERTIFICATE----- @@ -4231,7 +4231,7 @@ pub const TLS_SERVER_ROOTS: &[TrustAnchor] = &[ /* * Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3 * Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3 - * Label: "GlobalSign Root CA - R3" + * Label: "GlobalSign" * Serial: 4835703278459759426209954 * SHA256 Fingerprint: cb:b5:22:d7:b7:f1:27:ad:6a:01:13:86:5b:df:1c:d4:10:2e:7d:07:59:af:63:5a:7c:f4:72:0d:c9:63:c5:3b * -----BEGIN CERTIFICATE----- @@ -4291,7 +4291,7 @@ pub const TLS_SERVER_ROOTS: &[TrustAnchor] = &[ /* * Issuer: CN=XRamp Global Certification Authority O=XRamp Security Services Inc OU=www.xrampsecurity.com * Subject: CN=XRamp Global Certification Authority O=XRamp Security Services Inc OU=www.xrampsecurity.com - * Label: "XRamp Global CA Root" + * Label: "XRamp Global Certification Authority" * Serial: 107108908803651509692980124233745014957 * SHA256 Fingerprint: ce:cd:dc:90:50:99:d8:da:df:c5:b1:d2:09:b7:37:cb:e2:c1:8c:fb:2c:10:c0:ff:0b:cf:0d:32:86:fc:1a:a2 * -----BEGIN CERTIFICATE----- @@ -4422,7 +4422,7 @@ pub const TLS_SERVER_ROOTS: &[TrustAnchor] = &[ /* * Issuer: CN=AAA Certificate Services O=Comodo CA Limited * Subject: CN=AAA Certificate Services O=Comodo CA Limited - * Label: "Comodo AAA Services root" + * Label: "AAA Certificate Services" * Serial: 1 * SHA256 Fingerprint: d7:a7:a0:fb:5d:7e:27:31:d7:71:e9:48:4e:bc:de:f7:1d:5f:0c:3e:0a:29:48:78:2b:c8:3e:e0:ea:69:9e:f4 * -----BEGIN CERTIFICATE----- @@ -4805,7 +4805,7 @@ pub const TLS_SERVER_ROOTS: &[TrustAnchor] = &[ /* * Issuer: O=SECOM Trust.net OU=Security Communication RootCA1 * Subject: O=SECOM Trust.net OU=Security Communication RootCA1 - * Label: "Security Communication Root CA" + * Label: "SECOM Trust.net - Security Communication RootCA1" * Serial: 0 * SHA256 Fingerprint: e7:5e:72:ed:9f:56:0e:ec:6e:b4:80:00:73:a4:3f:c3:ad:19:19:5a:39:22:82:01:78:95:97:4a:99:02:6b:6c * -----BEGIN CERTIFICATE----- @@ -4918,7 +4918,7 @@ pub const TLS_SERVER_ROOTS: &[TrustAnchor] = &[ /* * Issuer: O=FNMT-RCM OU=AC RAIZ FNMT-RCM * Subject: O=FNMT-RCM OU=AC RAIZ FNMT-RCM - * Label: "AC RAIZ FNMT-RCM" + * Label: "FNMT-RCM - SHA256" * Serial: 485876308206448804701554682760554759 * SHA256 Fingerprint: eb:c5:57:0c:29:01:8c:4d:67:b1:aa:12:7b:af:12:f7:03:b4:61:1e:bc:17:b7:da:b5:57:38:94:17:9b:93:fa * -----BEGIN CERTIFICATE----- diff --git a/tests/codegen.rs b/tests/codegen.rs index 1aac8d1..948aee0 100644 --- a/tests/codegen.rs +++ b/tests/codegen.rs @@ -1,118 +1,95 @@ use std::ascii::escape_default; -use std::collections::HashMap; +use std::cmp::Ordering; +use std::collections::{BTreeMap, HashSet}; use std::fmt::Write; use std::fs; -use std::io::Cursor; +use chrono::{NaiveDate, Utc}; +use num_bigint::BigUint; use ring::digest; +use serde::Deserialize; +use x509_parser::prelude::AttributeTypeAndValue; +use x509_parser::x509::X509Name; #[tokio::test] -async fn generated_code_is_fresh() { - // Fetch the list of certificates as a PEM file from mkcert.org - - let mut except = String::with_capacity(128); - for (i, ca) in EXCLUDED_CAS.iter().enumerate() { - if i > 0 { - except.push('+'); - } - - except - .write_fmt(format_args!( - "{}", - percent_encoding::percent_encode(ca.as_bytes(), percent_encoding::NON_ALPHANUMERIC,) - )) - .unwrap(); - } - - let url = format!("https://mkcert.org/generate/all/except/{except}"); - eprintln!("fetching {url}..."); - let body = reqwest::get(&url).await.unwrap().text().await.unwrap(); - fs::write("fetched.pem", &body).unwrap(); - - // Split file contents into a Vec> where each inner Vec represents one certificate - - let mut certs = Vec::with_capacity(64); - let mut current = Vec::with_capacity(16); - for ln in body.lines() { - if !ln.is_empty() { - current.push(ln); - continue; - } else if current.is_empty() { - continue; +async fn new_generated_code_is_fresh() { + // Configure a Reqwest client that only trusts the CA certificate expected to be the + // root of trust for the CCADB server. + // + // If we see Unknown CA TLS validation failures from the Reqwest client in the future it + // likely indicates that the upstream service has changed certificate authorities. In this + // case the vendored root CA will need to be updated. You can find the current root in use with + // Chrome by: + // 1. Navigating to `https://ccadb-public.secure.force.com/mozilla/` + // 2. Clicking the lock icon. + // 3. Clicking "Connection is secure" + // 4. Clicking "Certificate is valid" + // 5. Clicking the "Details" tab. + // 6. Selecting the topmost "System Trust" entry. + // 7. Clicking "Export..." and saving the certificate to `webpki-roots/tests/data/`. + // 8. Committing the updated .pem root CA, and updating the `include_bytes!` path. + let root = include_bytes!("data/DigiCertGlobalRootCA.pem"); + let root = reqwest::Certificate::from_pem(root).unwrap(); + let client = reqwest::Client::builder() + .user_agent(format!("webpki-roots/v{}", env!("CARGO_PKG_VERSION"))) + .add_root_certificate(root) + .build() + .unwrap(); + + let ccadb_url = + "https://ccadb-public.secure.force.com/mozilla/IncludedCACertificateReportPEMCSV"; + eprintln!("fetching {ccadb_url}..."); + + let req = client.get(ccadb_url).build().unwrap(); + let csv_data = client + .execute(req) + .await + .expect("failed to fetch CSV") + .text() + .await + .unwrap(); + + // Parse the CSV metadata. + let metadata = csv::ReaderBuilder::new() + .has_headers(true) + .from_reader(csv_data.as_bytes()) + .into_deserialize::() + .collect::, _>>() + .unwrap(); + + // Filter for just roots with the TLS trust bit that are not distrusted as of today's date. + let trusted_tls_roots = metadata + .into_iter() + .filter(|root| root.trusted_for_tls(&Utc::now().naive_utc().date())) + .collect::>(); + + // Create an ordered BTreeMap of the roots, panicking for any duplicates. + let mut tls_roots_map = BTreeMap::new(); + for root in trusted_tls_roots { + match tls_roots_map.get(&root.sha256_fingerprint) { + Some(_) => { + panic!("duplicate fingerprint {}", root.sha256_fingerprint); + } + None => { + tls_roots_map.insert(root.sha256_fingerprint.clone(), root); + } } - - certs.push(current); - current = Vec::with_capacity(16); } - certs.push(current); - - // Parse each certificate and check fingerprints - - let mut hashed = HashMap::, (Vec<&str>, Vec)>::default(); - for cert in certs { - let start = cert - .iter() - .position(|&ln| ln == "-----BEGIN CERTIFICATE-----") - .unwrap(); - - // Parse PEM to get the DER. - let pem = cert[start..].join("\n"); - let mut reader = Cursor::new(pem.as_bytes()); - let der = match rustls_pemfile::read_one(&mut reader).unwrap().unwrap() { - rustls_pemfile::Item::X509Certificate(der) => der, - _ => unreachable!(), - }; - - // Check if our hash matches the one in the file. - let their_fingerprint = cert - .iter() - .find_map(|&ln| ln.strip_prefix("# SHA256 Fingerprint: ")) - .unwrap(); - let hash = digest::digest(&digest::SHA256, &der); - assert!( - !hashed.contains_key(hash.as_ref()), - "duplicate hash: {:#?}", - &cert - ); - - let our_fingerprint = hash - .as_ref() - .iter() - .map(|b| format!("{:02x}", b)) - .collect::(); - assert_eq!( - their_fingerprint.replace(':', ""), - our_fingerprint, - "{:#?}", - &cert - ); - - hashed.insert(hash.as_ref().to_vec(), (cert, der)); - } - - // For the given certificate subject name, store a name constraints encoding - // which will be applied to that certificate. This data is sourced from - // https://hg.mozilla.org/projects/nss/file/tip/lib/certdb/genname.c such that - // webpki-roots implements the same policy in this respect as the Mozilla root program. - let mut imposed_constraints = HashMap::, Vec>::default(); - imposed_constraints.insert( - concat(TUBITAK1_SUBJECT_DN), - TUBITAK1_NAME_CONSTRAINTS.to_vec(), - ); - - // Generate the trust anchors, sorted by fingerprint - - let mut hashes = hashed.into_iter().collect::>(); - hashes.sort_by(|a, b| a.0.cmp(&b.0)); - - let (mut subject, mut spki, mut name_constraints) = - (String::new(), String::new(), String::new()); let mut code = String::with_capacity(256 * 1_024); code.push_str(HEADER); code.push_str("pub const TLS_SERVER_ROOTS: &[TrustAnchor] = &[\n"); - for (_, (lines, der)) in hashes { - let ta = webpki::TrustAnchor::try_from_cert_der(&der).unwrap(); + let (mut subject, mut spki, mut name_constraints) = + (String::new(), String::new(), String::new()); + + for (_, root) in tls_roots_map { + // Verify the DER FP matches the metadata FP. + let der = root.der(); + let calculated_fp = digest::digest(&digest::SHA256, &der); + let metadata_fp = hex::decode(&root.sha256_fingerprint).expect("malformed fingerprint"); + assert_eq!(calculated_fp.as_ref(), metadata_fp.as_slice()); + + let ta = webpki::TrustAnchor::try_from_cert_der(&der).expect("malformed trust anchor der"); subject.clear(); for &b in ta.subject { write!(&mut subject, "{}", escape_default(b)).unwrap(); @@ -124,35 +101,35 @@ async fn generated_code_is_fresh() { } name_constraints.clear(); - let nc = imposed_constraints - .get(ta.subject) - .map(|nc| nc.as_slice()) - .or(ta.name_constraints); - if let Some(nc) = nc { - for &b in nc { + if let Some(nc) = &root.mozilla_applied_constraints() { + for &b in nc.iter() { write!(&mut name_constraints, "{}", escape_default(b)).unwrap(); } } - // Write comment with source + let (_, parsed_cert) = + x509_parser::parse_x509_certificate(&der).expect("malformed x509 der"); + let issuer = name_to_string(parsed_cert.issuer()); + let subject_str = name_to_string(parsed_cert.subject()); + let label = root.common_name_or_certificate_name.clone(); + let serial = root.serial().to_string(); + let sha256_fp = root.sha256_fp(); + // Write comment code.push_str(" /*\n"); - for &ln in lines.iter() { - if ln.starts_with("# MD5") || ln.starts_with("# SHA1") { - continue; - } - + code.push_str(&format!(" * Issuer: {}\n", issuer)); + code.push_str(&format!(" * Subject: {}\n", subject_str)); + code.push_str(&format!(" * Label: {:?}\n", label)); + code.push_str(&format!(" * Serial: {}\n", serial)); + code.push_str(&format!(" * SHA256 Fingerprint: {}\n", sha256_fp)); + for ln in root.pem().lines() { code.push_str(" * "); - match ln.strip_prefix("# ") { - Some(ln) => code.push_str(ln), - None => code.push_str(ln), - } + code.push_str(ln.trim()); code.push('\n'); } code.push_str(" */\n"); // Write the code - code.push_str(" TrustAnchor {\n"); code.write_fmt(format_args!(" subject: b\"{subject}\",\n")) .unwrap(); @@ -168,11 +145,9 @@ async fn generated_code_is_fresh() { } code.push_str(" },\n\n"); } - code.push_str("];\n"); // Check that the generated code matches the checked-in code - let old = fs::read_to_string("src/lib.rs").unwrap(); if old != code { fs::write("src/lib.rs", code).unwrap(); @@ -180,53 +155,226 @@ async fn generated_code_is_fresh() { } } -fn concat(parts: &[&[u8]]) -> Vec { - let mut v = Vec::with_capacity(128); - for &b in parts { - v.extend(b); +/// The built-in x509_parser::X509Name Display impl uses a different sort order than +/// the one historically used by mkcert.org^[0]. We re-create that sort order here to +/// avoid unnecessary churn in the generated code. +/// +/// [0]: +fn name_to_string(name: &X509Name) -> String { + let mut ret = String::with_capacity(256); + + if let Some(cn) = name + .iter_common_name() + .next() + .and_then(|cn| cn.as_str().ok()) + { + write!(ret, "CN={}", cn).unwrap(); } - v + + let mut append_attrs = |attrs: Vec<&AttributeTypeAndValue>, label| { + let str_parts = attrs + .iter() + .filter_map(|attr| match attr.as_str() { + Ok(s) => Some(s), + Err(_) => None, + }) + .collect::>() + .join("/"); + if !str_parts.is_empty() { + if !ret.is_empty() { + ret.push(' '); + } + write!(ret, "{}={}", label, str_parts).unwrap(); + } + }; + + append_attrs(name.iter_organization().collect(), "O"); + append_attrs(name.iter_organizational_unit().collect(), "OU"); + + ret } -// TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 -const TUBITAK1_SUBJECT_DN: &[&[u8]] = &[ - b"\x31\x0b\x30\x09\x06\x03\x55\x04\x06\x13\x02", - b"TR", - b"\x31\x18\x30\x16\x06\x03\x55\x04\x07\x13\x0f", - b"Gebze - Kocaeli", - b"\x31\x42\x30\x40\x06\x03\x55\x04\x0a\x13\x39", - b"Turkiye Bilimsel ve Teknolojik Arastirma Kurumu - TUBITAK", - b"\x31\x2d\x30\x2b\x06\x03\x55\x04\x0b\x13\x24", - b"Kamu Sertifikasyon Merkezi - Kamu SM", - b"\x31\x36\x30\x34\x06\x03\x55\x04\x03\x13\x2d", - b"TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1", -]; - -const TUBITAK1_NAME_CONSTRAINTS: &[u8] = &[0xA0, 0x07, 0x30, 0x05, 0x82, 0x03, 0x2E, 0x74, 0x72]; - -const EXCLUDED_CAS: &[&str] = &[ - // See https://bugzilla.mozilla.org/show_bug.cgi?id=1266574. - "Buypass Class 2 CA 1", - // https://blog.mozilla.org/security/2015/04/02/distrusting-new-cnnic-certificates/ - // https://security.googleblog.com/2015/03/maintaining-digital-certificate-security.html - "China Internet Network Information Center", - "CNNIC", - // See https://bugzilla.mozilla.org/show_bug.cgi?id=1283326. - "RSA Security 2048 v3", - // https://bugzilla.mozilla.org/show_bug.cgi?id=1272158 - "Root CA Generalitat Valenciana", - // See https://wiki.mozilla.org/CA:WoSign_Issues. - "StartCom", - "WoSign", - // See https://cabforum.org/pipermail/public/2016-September/008475.html. - // Both the ASCII and non-ASCII names are required. - "TÜRKTRUST", - "TURKTRUST", -]; +#[derive(Debug, Clone, Hash, Eq, PartialEq, Deserialize)] +pub struct CertificateMetadata { + #[serde(rename = "Common Name or Certificate Name")] + pub common_name_or_certificate_name: String, + + #[serde(rename = "Certificate Serial Number")] + pub certificate_serial_number: String, + + #[serde(rename = "SHA-256 Fingerprint")] + pub sha256_fingerprint: String, + + #[serde(rename = "Trust Bits")] + pub trust_bits: String, + + #[serde(rename = "Distrust for TLS After Date")] + pub distrust_for_tls_after_date: String, + + #[serde(rename = "Mozilla Applied Constraints")] + pub mozilla_applied_constraints: String, + + #[serde(rename = "PEM Info")] + pub pem_info: String, +} + +impl CertificateMetadata { + /// Returns true iff the certificate has valid TrustBits that include TrustBits::Websites, + /// and the certificate has no distrust for TLS after date, or has a valid distrust + /// for TLS after date that is in the future compared to `now`. In all other cases this function + /// returns false. + fn trusted_for_tls(&self, now: &NaiveDate) -> bool { + let has_tls_trust_bit = self.trust_bits().contains(&TrustBits::Websites); + + match (has_tls_trust_bit, self.tls_distrust_after()) { + // No website trust bit - not trusted for tls. + (false, _) => false, + // Has website trust bit, no distrust after - trusted for tls. + (true, None) => true, + // Trust bit, populated distrust after - need to check date to decide. + (true, Some(tls_distrust_after)) => { + match now.cmp(&tls_distrust_after).is_ge() { + // We're past the distrust date - skip. + true => false, + // We haven't yet reached the distrust date - include. + false => true, + } + } + } + } + + /// Return the Mozilla applied constraints for the certificate (if any). The constraints + /// will be encoded in the DER form expected by the webpki crate's TrustAnchor representation. + fn mozilla_applied_constraints(&self) -> Option> { + if self.mozilla_applied_constraints.is_empty() { + return None; + } + + // NOTE: To date there's only one CA with a applied constraints value, and it has only one + // permitted subtree constraint imposed. It's not clear how multiple constraints would be + // expressed. This method makes a best guess but may need to be revisited in the future. + // https://groups.google.com/a/ccadb.org/g/public/c/TlDivISPVT4/m/jbWGuM4YAgAJ + let included_subtrees = self.mozilla_applied_constraints.split(','); + + // Important: the webpki representation of name constraints elides: + // - the outer BITSTRING of the X.509 extension value. + // - the outer NameConstraints SEQUENCE over the permitted/excluded subtrees. + // + // See https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.10 + let der = yasna::construct_der(|writer| { + // permittedSubtrees [0] + writer.write_tagged_implicit(yasna::Tag::context(0), |writer| { + // GeneralSubtrees + writer.write_sequence(|writer| { + for included_subtree in included_subtrees { + // base GeneralName + writer.next().write_sequence(|writer| { + writer + .next() + // DnsName + .write_tagged_implicit(yasna::Tag::context(2), |writer| { + writer + .write_ia5_string(included_subtree.trim_start_matches('*')) + }) + }) + // minimum [0] (absent, 0 default) + // maximum [1] (must be omitted). + } + }) + }) + }); + + Some(der) + } + + /// Return the NaiveDate after which this certificate should not be trusted for TLS (if any). + /// Panics if there is a distrust for TLS after date value that can not be parsed. + fn tls_distrust_after(&self) -> Option { + match &self.distrust_for_tls_after_date { + date if date.is_empty() => None, + date => Some( + NaiveDate::parse_from_str(date, "%Y.%m.%d") + .unwrap_or_else(|_| panic!("invalid distrust for tls after date: {:?}", date)), + ), + } + } + + /// Returns the DER encoding of the certificate contained in the metadata PEM. Panics if + /// there is an error, or no certificate in the PEM content. + fn der(&self) -> Vec { + let certs = rustls_pemfile::certs(&mut self.pem().as_bytes()).expect("invalid PEM"); + if certs.len() > 1 { + panic!("more than one certificate in metadata PEM"); + } + certs + .first() + .expect("missing certificate in metadata PEM") + .clone() + } + + /// Returns the serial number for the certificate. Panics if the certificate serial number + /// from the metadata can not be parsed as a base 16 unsigned big integer. + pub fn serial(&self) -> BigUint { + BigUint::parse_bytes(self.certificate_serial_number.as_bytes(), 16) + .expect("invalid certificate serial number") + } + + /// Returns the colon separated string with the metadata SHA256 fingerprint for the + /// certificate. Panics if the sha256 fingerprint from the metadata can't be decoded. + pub fn sha256_fp(&self) -> String { + x509_parser::utils::format_serial( + &hex::decode(&self.sha256_fingerprint).expect("invalid sha256 fingerprint"), + ) + } + + /// Returns the set of trust bits expressed for this certificate. Panics if the raw + /// trust bits are invalid/unknown. + fn trust_bits(&self) -> HashSet { + self.trust_bits.split(';').map(TrustBits::from).collect() + } + + /// Returns the PEM metadata for the certificate with the leading/trailing single quotes + /// removed. + fn pem(&self) -> &str { + self.pem_info.as_str().trim_matches('\'') + } +} + +impl PartialOrd for CertificateMetadata { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.sha256_fingerprint.cmp(&other.sha256_fingerprint)) + } +} + +impl Ord for CertificateMetadata { + fn cmp(&self, other: &Self) -> Ordering { + self.sha256_fingerprint.cmp(&other.sha256_fingerprint) + } +} + +#[derive(Debug, Hash, Eq, PartialEq, Clone, Copy)] +#[non_exhaustive] +/// TrustBits describe the possible Mozilla root certificate trust bits. +pub enum TrustBits { + /// certificate is trusted for Websites (e.g. TLS). + Websites, + /// certificate is trusted for Email (e.g. S/MIME). + Email, +} + +impl From<&str> for TrustBits { + fn from(value: &str) -> Self { + match value { + "Websites" => TrustBits::Websites, + "Email" => TrustBits::Email, + val => panic!("unknown trust bit: {:?}", val), + } + } +} const HEADER: &str = r#"//! -//! This library is automatically generated from the Mozilla certificate -//! store via mkcert.org. Don't edit it. +//! This library is automatically generated from the Mozilla +//! IncludedCACertificateReportPEMCSV report via ccadb.org. Don't edit it. //! //! The generation is done deterministically so you can verify it //! yourself by inspecting and re-running the generation process. diff --git a/tests/data/DigiCertGlobalRootCA.pem b/tests/data/DigiCertGlobalRootCA.pem new file mode 100644 index 0000000..fd4341d --- /dev/null +++ b/tests/data/DigiCertGlobalRootCA.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD +QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB +CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 +nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt +43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P +T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 +gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR +TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw +DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr +hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg +06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF +PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls +YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE-----