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 pki_types::CertificateDer;
use ring::digest;
use serde::Deserialize;
use webpki::extract_trust_anchor;
use x509_parser::prelude::AttributeTypeAndValue;
use x509_parser::x509::X509Name;
#[tokio::test]
async fn new_generated_code_is_fresh() {
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();
let metadata = csv::ReaderBuilder::new()
.has_headers(true)
.from_reader(csv_data.as_bytes())
.into_deserialize::<CertificateMetadata>()
.collect::<Result<Vec<_>, _>>()
.unwrap();
let trusted_tls_roots = metadata
.into_iter()
.filter(|root| root.trusted_for_tls(&Utc::now().naive_utc().date()))
.collect::<Vec<CertificateMetadata>>();
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 {
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_der = CertificateDer::from(der.as_ref());
let ta = extract_trust_anchor(&ta_der).expect("malformed trust anchor der");
subject.clear();
for &b in ta.subject.as_ref() {
write!(&mut subject, "{}", escape_default(b)).unwrap();
}
spki.clear();
for &b in ta.subject_public_key_info.as_ref() {
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();
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");
code.push_str(" TrustAnchor {\n");
code.write_fmt(format_args!(
" subject: Der::from_slice(b\"{subject}\"),\n"
))
.unwrap();
code.write_fmt(format_args!(
" subject_public_key_info: Der::from_slice(b\"{spki}\"),\n"
))
.unwrap();
match name_constraints.is_empty() {
false => code
.write_fmt(format_args!(
" name_constraints: Some(Der::from_slice(b\"{name_constraints}\"))\n"
))
.unwrap(),
true => code.push_str(" name_constraints: None\n"),
}
code.push_str(" },\n\n");
}
code.push_str("];\n");
let old = fs::read_to_string("src/lib.rs").unwrap();
if old != code {
fs::write("src/lib.rs", code).unwrap();
panic!("generated code changed");
}
}
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 {
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()) {
(false, _) => false,
(true, None) => true,
(true, Some(tls_distrust_after)) => {
match now.cmp(&tls_distrust_after).is_ge() {
true => false,
false => true,
}
}
}
}
fn mozilla_applied_constraints(&self) -> Option<Vec<u8>> {
if self.mozilla_applied_constraints.is_empty() {
return None;
}
let included_subtrees = self.mozilla_applied_constraints.split(',');
let der = yasna::construct_der(|writer| {
writer.write_tagged_implicit(yasna::Tag::context(0), |writer| {
writer.write_sequence(|writer| {
for included_subtree in included_subtrees {
writer.next().write_sequence(|writer| {
writer
.next()
.write_tagged_implicit(yasna::Tag::context(2), |writer| {
writer
.write_ia5_string(included_subtree.trim_start_matches('*'))
})
})
}
})
})
});
Some(der)
}
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)),
),
}
}
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()
}
pub fn serial(&self) -> BigUint {
BigUint::parse_bytes(self.certificate_serial_number.as_bytes(), 16)
.expect("invalid certificate serial number")
}
pub fn sha256_fp(&self) -> String {
x509_parser::utils::format_serial(
&hex::decode(&self.sha256_fingerprint).expect("invalid sha256 fingerprint"),
)
}
fn trust_bits(&self) -> HashSet<TrustBits> {
self.trust_bits.split(';').map(TrustBits::from).collect()
}
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]
pub enum TrustBits {
Websites,
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
)]
use pki_types::{Der, TrustAnchor};
"#;