blob: 948aee0b8bdcdef5e2e0660154e1ecb9b9c74cf3 [file] [log] [blame]
use std::ascii::escape_default;
use std::cmp::Ordering;
use std::collections::{BTreeMap, HashSet};
use std::fmt::Write;
use std::fs;
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 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::<CertificateMetadata>()
.collect::<Result<Vec<_>, _>>()
.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::<Vec<CertificateMetadata>>();
// 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);
}
}
}
let mut code = String::with_capacity(256 * 1_024);
code.push_str(HEADER);
code.push_str("pub const TLS_SERVER_ROOTS: &[TrustAnchor] = &[\n");
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();
}
spki.clear();
for &b in ta.spki {
write!(&mut spki, "{}", escape_default(b)).unwrap();
}
name_constraints.clear();
if let Some(nc) = &root.mozilla_applied_constraints() {
for &b in nc.iter() {
write!(&mut name_constraints, "{}", escape_default(b)).unwrap();
}
}
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");
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(" * ");
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();
code.write_fmt(format_args!(" spki: b\"{spki}\",\n"))
.unwrap();
match name_constraints.is_empty() {
false => code
.write_fmt(format_args!(
" name_constraints: Some(b\"{name_constraints}\")\n"
))
.unwrap(),
true => code.push_str(" name_constraints: None\n"),
}
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();
panic!("generated code changed");
}
}
/// 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]: <https://github.com/Lukasa/mkcert/blob/6911a8f68681f4d6a795c1f6db7b063f75b03b5a/certs/convert_mozilla_certdata.go#L405-L428>
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();
}
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::<Vec<_>>()
.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
}
#[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<Vec<u8>> {
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<NaiveDate> {
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<u8> {
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<TrustBits> {
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<Ordering> {
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
//! 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.
//!
#![forbid(unsafe_code, unstable_features)]
#![deny(
trivial_casts,
trivial_numeric_casts,
unused_import_braces,
unused_extern_crates,
unused_qualifications
)]
/// A trust anchor (sometimes called a root) for validating X.509 certificates
pub struct TrustAnchor<'a> {
pub subject: &'a [u8],
pub spki: &'a [u8],
pub name_constraints: Option<&'a [u8]>,
}
"#;