ca-certs: generate install artifacts and improve certdata/trust-store compatibility

- stop committing generated man/completion files in contrib/
- generate man page and shell completions during Meson install via install-generated-docs.sh
- switch reqwest to native-tls and add Mozilla hg bootstrap certs with openssl fallback fetch
- make certdata sync use a temporary auto-cleaned workspace by default
- add BLFS/make-ca compatibility outputs and symlinks under /etc/pki and /etc/ssl
- validate extracted outputs are non-empty and add tests for temp sync output behavior
This commit is contained in:
2026-02-28 00:47:16 -06:00
parent eb80972c0a
commit 11580aebcf
10 changed files with 669 additions and 1643 deletions
+491 -15
View File
@@ -9,19 +9,84 @@ use std::io::Write;
use std::os::unix::fs::{PermissionsExt, symlink};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::{SystemTime, UNIX_EPOCH};
// Default paths mirror the p11-kit / update-ca-trust layout used by this system.
const DEFAULT_ROOT: &str = "/";
const DEFAULT_ANCHORS_DIR: &str = "/etc/ca-certificates/trust-source/anchors";
const DEFAULT_EXTRACTED_DIR: &str = "/etc/ca-certificates/extracted";
const DEFAULT_SSL_CERTS_DIR: &str = "/etc/ssl/certs";
const DEFAULT_SSL_CERT_PEM_LINK: &str = "/etc/ssl/cert.pem";
const DEFAULT_SSL_CA_CERTIFICATES_BUNDLE_LINK: &str = "/etc/ssl/certs/ca-certificates.crt";
const DEFAULT_SSL_CA_BUNDLE_CRT_LINK: &str = "/etc/ssl/certs/ca-bundle.crt";
const DEFAULT_JAVA_CACERTS_LINK: &str = "/etc/ssl/certs/java/cacerts";
// BLFS make-ca compatibility paths (commonly used by OpenSSL/libgit2 builds on LFS).
const DEFAULT_PKI_TLS_CA_BUNDLE_CRT_LINK: &str = "/etc/pki/tls/certs/ca-bundle.crt";
const DEFAULT_PKI_TLS_CA_BUNDLE_TRUST_CRT_LINK: &str = "/etc/pki/tls/certs/ca-bundle.trust.crt";
const DEFAULT_PKI_TLS_JAVA_CACERTS_LINK: &str = "/etc/pki/tls/java/cacerts";
const DEFAULT_CERTDATA_URL: &str =
"https://hg.mozilla.org/projects/nss/raw-file/tip/lib/ckfw/builtins/certdata.txt";
const DEFAULT_CERTDATA_OUTPUT: &str = "certdata.txt";
const DEFAULT_MOZILLA_TRUST_P11KIT: &str =
"/usr/share/ca-certificates/trust-source/mozilla.trust.p11-kit";
const DEFAULT_PKI_MOZILLA_TRUST_P11KIT: &str = "/etc/pki/anchors/mozilla.trust.p11-kit";
// make-ca uses a pinned ISRG Root X1 to bootstrap downloads from hg.mozilla.org
// before the local trust store exists. We do the same for certdata fetch/sync.
const MOZILLA_HG_BOOTSTRAP_ROOT_PEM: &str = r#"-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
-----END CERTIFICATE-----
"#;
const MOZILLA_HG_BOOTSTRAP_DIGICERT_G2_ROOT_PEM: &str = r#"-----BEGIN CERTIFICATE-----
MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH
MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT
MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI
2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx
1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ
q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz
tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ
vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP
BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV
5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY
1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4
NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG
Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91
8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe
pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl
MrY=
-----END CERTIFICATE-----
"#;
// Top-level CLI entrypoint with `add` and `extract` subcommands.
#[derive(Parser, Debug)]
@@ -194,9 +259,10 @@ struct CertdataSyncArgs {
#[arg(long)]
log_url: Option<String>,
/// Local certdata.txt path used for fetch and convert
#[arg(long, default_value = DEFAULT_CERTDATA_OUTPUT)]
certdata_output: PathBuf,
/// Local certdata.txt path used for fetch and convert.
/// Defaults to a temporary `/tmp/.../certdata.txt` path that is cleaned up.
#[arg(long)]
certdata_output: Option<PathBuf>,
/// Target root filesystem (for chroot/image builds)
#[arg(long, default_value = DEFAULT_ROOT)]
@@ -263,6 +329,24 @@ struct ExtractJob {
relative_dest: &'static str,
}
#[derive(Debug)]
struct TempWorkspaceCleanup {
path: PathBuf,
}
impl Drop for TempWorkspaceCleanup {
fn drop(&mut self) {
if let Err(err) = fs::remove_dir_all(&self.path) {
if err.kind() != std::io::ErrorKind::NotFound {
eprintln!(
"warning: failed to remove temporary directory {}: {err}",
self.path.display()
);
}
}
}
}
// Output set mirrors `/bin/update-ca-trust extract`.
const EXTRACT_JOBS: &[ExtractJob] = &[
ExtractJob {
@@ -308,10 +392,10 @@ const EXTRACT_JOBS: &[ExtractJob] = &[
relative_dest: "java-cacerts.jks",
},
ExtractJob {
format: "pem-directory-hash",
format: "openssl-directory",
filter: "ca-anchors",
purpose: Some("server-auth"),
comment: false,
purpose: None,
comment: true,
relative_dest: "cadir",
},
];
@@ -399,11 +483,14 @@ fn run_certdata(command: CertdataCommands) -> Result<()> {
}
fn run_certdata_sync(args: CertdataSyncArgs) -> Result<()> {
let (certdata_output, _temp_workspace_guard) =
resolve_sync_certdata_output(args.certdata_output, args.dry_run)?;
// Reuse the existing subcommand implementations so behavior stays aligned.
run_certdata_fetch(CertdataFetchArgs {
url: args.url,
log_url: args.log_url,
output: args.certdata_output.clone(),
output: certdata_output.clone(),
force: args.force,
no_revision_check: args.no_revision_check,
parse: args.parse,
@@ -411,7 +498,7 @@ fn run_certdata_sync(args: CertdataSyncArgs) -> Result<()> {
})?;
run_certdata_convert(CertdataConvertArgs {
input: args.certdata_output,
input: certdata_output,
root: args.root,
output: args.mozilla_output,
force: args.force,
@@ -421,6 +508,60 @@ fn run_certdata_sync(args: CertdataSyncArgs) -> Result<()> {
})
}
fn resolve_sync_certdata_output(
requested: Option<PathBuf>,
dry_run: bool,
) -> Result<(PathBuf, Option<TempWorkspaceCleanup>)> {
if let Some(path) = requested {
return Ok((path, None));
}
if dry_run {
return Ok((build_sync_tmp_workspace_path(0).join(DEFAULT_CERTDATA_OUTPUT), None));
}
let workspace = create_sync_tmp_workspace_dir()?;
println!("Using temporary certdata workspace: {}", workspace.display());
Ok((
workspace.join(DEFAULT_CERTDATA_OUTPUT),
Some(TempWorkspaceCleanup { path: workspace }),
))
}
fn create_sync_tmp_workspace_dir() -> Result<PathBuf> {
let tmp_root = Path::new("/tmp");
for attempt in 0..32_u32 {
let candidate = build_sync_tmp_workspace_path(attempt);
match fs::create_dir(&candidate) {
Ok(()) => return Ok(candidate),
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => continue,
Err(err) => {
return Err(err).with_context(|| {
format!(
"failed to create temporary certdata directory {}",
candidate.display()
)
});
}
}
}
bail!(
"failed to create a unique temporary certdata directory under {}",
tmp_root.display()
);
}
fn build_sync_tmp_workspace_path(attempt: u32) -> PathBuf {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
Path::new("/tmp").join(format!(
"ca-certs-certdata-{}-{nonce:032x}-{attempt:02x}",
std::process::id()
))
}
fn run_gen_artifacts(args: GenArtifactsArgs) -> Result<()> {
let man_path = args.out_dir.join("ca-certs.8");
let completions_dir = args.out_dir.join("completions");
@@ -563,9 +704,7 @@ fn run_certdata_fetch(args: CertdataFetchArgs) -> Result<()> {
return Ok(());
}
let client = reqwest::blocking::Client::builder()
.build()
.context("failed to build HTTP client")?;
let client = build_http_client().context("failed to build HTTP client")?;
let existing_revision = read_certdata_revision_from_path(&args.output)
.ok()
.flatten();
@@ -613,15 +752,217 @@ fn run_certdata_fetch(args: CertdataFetchArgs) -> Result<()> {
}
fn http_get_text(client: &reqwest::blocking::Client, url: &str) -> Result<String> {
let response = client
.get(url)
.send()
.with_context(|| format!("failed to request {url}"))?
let response = match client.get(url).send() {
Ok(response) => response,
Err(err) if is_mozilla_hg_url(url) => {
println!("reqwest fetch failed for Mozilla hg URL; retrying via openssl.");
return openssl_https_get_text(url);
}
Err(err) => return Err(err).with_context(|| format!("failed to request {url}")),
}
.error_for_status()
.with_context(|| format!("server returned an error for {url}"))?;
response.text().context("failed to read response body")
}
fn is_mozilla_hg_url(url: &str) -> bool {
reqwest::Url::parse(url)
.ok()
.and_then(|parsed| parsed.host_str().map(str::to_ascii_lowercase))
.map(|host| host == "hg.mozilla.org" || host == "hg-edge.mozilla.org")
.unwrap_or(false)
}
fn openssl_https_get_text(url: &str) -> Result<String> {
let parsed = reqwest::Url::parse(url).with_context(|| format!("invalid URL: {url}"))?;
if parsed.scheme() != "https" {
bail!("openssl fallback only supports https URLs: {url}");
}
let host = parsed.host_str().context("URL missing host")?;
let port = parsed.port_or_known_default().context("URL missing port")?;
let mut path = parsed.path().to_string();
if path.is_empty() {
path.push('/');
}
if let Some(query) = parsed.query() {
path.push('?');
path.push_str(query);
}
let host_header = match parsed.port() {
Some(p) if p != 443 => format!("{host}:{p}"),
_ => host.to_string(),
};
let bootstrap_ca_path = write_bootstrap_ca_tempfile()?;
let result = (|| {
let mut cmd = Command::new("openssl");
cmd.args([
"s_client",
"-quiet",
"-verify_return_error",
"-verifyCAfile",
bootstrap_ca_path.to_str().unwrap_or(""),
"-connect",
&format!("{host}:{port}"),
"-servername",
host,
]);
if Path::new(DEFAULT_SSL_CERTS_DIR).is_dir() {
cmd.args(["-verifyCApath", DEFAULT_SSL_CERTS_DIR]);
}
cmd.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = cmd.spawn().context("failed to spawn `openssl s_client`")?;
{
let stdin = child
.stdin
.as_mut()
.context("failed to open openssl stdin")?;
write!(
stdin,
"GET {path} HTTP/1.1\r\nHost: {host_header}\r\nConnection: close\r\n\r\n"
)
.context("failed to write HTTP request to openssl stdin")?;
}
let output = child
.wait_with_output()
.context("failed to wait for `openssl s_client`")?;
if !output.status.success() {
bail!(
"openssl s_client failed: {}",
String::from_utf8_lossy(&output.stderr).trim()
);
}
parse_http_response_text(&output.stdout)
.with_context(|| format!("failed to parse HTTP response from openssl for {url}"))
})();
let _ = fs::remove_file(&bootstrap_ca_path);
result
}
fn write_bootstrap_ca_tempfile() -> Result<PathBuf> {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let path = std::env::temp_dir().join(format!(
"ca-certs-hg-bootstrap-{}-{nonce}.pem",
std::process::id()
));
let bundle = format!(
"{}{}",
MOZILLA_HG_BOOTSTRAP_ROOT_PEM, MOZILLA_HG_BOOTSTRAP_DIGICERT_G2_ROOT_PEM
);
fs::write(&path, bundle)
.with_context(|| format!("failed to write {}", path.display()))?;
#[cfg(unix)]
{
fs::set_permissions(&path, fs::Permissions::from_mode(0o600))
.with_context(|| format!("failed to set permissions on {}", path.display()))?;
}
Ok(path)
}
fn parse_http_response_text(raw: &[u8]) -> Result<String> {
let (header_bytes, body_bytes) = split_http_response(raw)?;
let header_text = String::from_utf8_lossy(header_bytes);
let mut header_lines = header_text.lines();
let status_line = header_lines.next().context("missing HTTP status line")?;
let status_code = status_line
.split_whitespace()
.nth(1)
.context("missing HTTP status code")?
.parse::<u16>()
.context("invalid HTTP status code")?;
if !(200..300).contains(&status_code) {
bail!("HTTP request failed with status {status_code}");
}
let chunked = header_lines.any(|line| {
line.split_once(':')
.map(|(name, value)| {
name.eq_ignore_ascii_case("transfer-encoding")
&& value.to_ascii_lowercase().contains("chunked")
})
.unwrap_or(false)
});
let body = if chunked {
decode_http_chunked_body(body_bytes)?
} else {
body_bytes.to_vec()
};
String::from_utf8(body).context("HTTP response body was not UTF-8")
}
fn split_http_response(raw: &[u8]) -> Result<(&[u8], &[u8])> {
if let Some(idx) = raw.windows(4).position(|w| w == b"\r\n\r\n") {
return Ok((&raw[..idx], &raw[idx + 4..]));
}
if let Some(idx) = raw.windows(2).position(|w| w == b"\n\n") {
return Ok((&raw[..idx], &raw[idx + 2..]));
}
bail!("HTTP response missing header/body separator")
}
fn decode_http_chunked_body(mut input: &[u8]) -> Result<Vec<u8>> {
let mut out = Vec::new();
loop {
let (line, rest) = take_http_line(input).context("truncated chunk header")?;
let size_hex = line.split(';').next().unwrap_or("").trim();
let size = usize::from_str_radix(size_hex, 16)
.with_context(|| format!("invalid chunk size `{size_hex}`"))?;
input = rest;
if size == 0 {
break;
}
if input.len() < size {
bail!("truncated chunk body");
}
out.extend_from_slice(&input[..size]);
input = &input[size..];
if input.starts_with(b"\r\n") {
input = &input[2..];
} else if input.starts_with(b"\n") {
input = &input[1..];
} else {
bail!("missing chunk terminator");
}
}
Ok(out)
}
fn take_http_line(input: &[u8]) -> Option<(String, &[u8])> {
if let Some(idx) = input.windows(2).position(|w| w == b"\r\n") {
let line = String::from_utf8(input[..idx].to_vec()).ok()?;
return Some((line, &input[idx + 2..]));
}
if let Some(idx) = input.iter().position(|b| *b == b'\n') {
let line = String::from_utf8(input[..idx].to_vec()).ok()?;
return Some((line, &input[idx + 1..]));
}
None
}
fn build_http_client() -> Result<reqwest::blocking::Client> {
let bootstrap_ca = reqwest::Certificate::from_pem(MOZILLA_HG_BOOTSTRAP_ROOT_PEM.as_bytes())
.context("failed to parse bundled Mozilla hg bootstrap root certificate")?;
let digicert_g2_ca =
reqwest::Certificate::from_pem(MOZILLA_HG_BOOTSTRAP_DIGICERT_G2_ROOT_PEM.as_bytes())
.context("failed to parse bundled DigiCert Global Root G2 bootstrap certificate")?;
reqwest::blocking::Client::builder()
.add_root_certificate(bootstrap_ca)
.add_root_certificate(digicert_g2_ca)
.build()
.context("failed to construct reqwest client")
}
fn derive_hg_log_url(raw_url: &str) -> Option<String> {
if raw_url.contains("/raw-file/") {
Some(raw_url.replacen("/raw-file/", "/log/", 1))
@@ -759,10 +1100,17 @@ fn run_certdata_convert(args: CertdataConvertArgs) -> Result<()> {
);
}
println!("[dry-run] Would write {}", output_host.display());
if should_write_make_ca_mirror(&args.root, &output_target) {
println!(
"[dry-run] Would also write BLFS make-ca compatibility source {}",
path_in_root(&args.root, Path::new(DEFAULT_PKI_MOZILLA_TRUST_P11KIT)).display()
);
}
} else {
let parsed = parsed.context("certdata parse result missing")?;
let p11kit = convert_certdata_to_p11kit_bundle(&parsed)?;
install_file(&output_host, p11kit.as_bytes(), args.force, false)?;
maybe_install_make_ca_mozilla_mirror(&args.root, &output_target, &p11kit, args.force)?;
}
if args.no_extract {
@@ -774,10 +1122,57 @@ fn run_certdata_convert(args: CertdataConvertArgs) -> Result<()> {
.extract_output
.unwrap_or_else(|| PathBuf::from(DEFAULT_EXTRACTED_DIR));
extract_trust(&args.root, &extract_output, args.dry_run)?;
if !args.dry_run {
ensure_nonempty_extraction_outputs(&args.root, &extract_output)?;
}
println!("certdata conversion and extraction complete.");
Ok(())
}
fn should_write_make_ca_mirror(root: &Path, output_target: &Path) -> bool {
output_target == Path::new(DEFAULT_MOZILLA_TRUST_P11KIT)
&& path_in_root(root, Path::new("/etc/pki")).exists()
}
fn maybe_install_make_ca_mozilla_mirror(
root: &Path,
output_target: &Path,
p11kit: &str,
force: bool,
) -> Result<()> {
if !should_write_make_ca_mirror(root, output_target) {
return Ok(());
}
let mirror_host = path_in_root(root, Path::new(DEFAULT_PKI_MOZILLA_TRUST_P11KIT));
println!(
"Installing BLFS make-ca compatibility trust source: {}",
mirror_host.display()
);
install_file(&mirror_host, p11kit.as_bytes(), force, false)
}
fn ensure_nonempty_extraction_outputs(root: &Path, extract_output: &Path) -> Result<()> {
let tls_bundle = path_in_root(root, &extract_output.join("tls-ca-bundle.pem"));
let trust_bundle = path_in_root(root, &extract_output.join("ca-bundle.trust.crt"));
let cadir = path_in_root(root, &extract_output.join("cadir"));
let tls_size = fs::metadata(&tls_bundle).map(|m| m.len()).unwrap_or(0);
let trust_size = fs::metadata(&trust_bundle).map(|m| m.len()).unwrap_or(0);
let cadir_count = fs::read_dir(&cadir).map(|it| it.count()).unwrap_or(0);
if tls_size == 0 || trust_size == 0 || cadir_count == 0 {
bail!(
"trust extract produced empty outputs (tls-ca-bundle.pem={} bytes, ca-bundle.trust.crt={} bytes, cadir entries={}); p11-kit may be reading a different trust source layout (e.g. BLFS make-ca uses /etc/pki/anchors)",
tls_size,
trust_size,
cadir_count
);
}
Ok(())
}
fn count_cert_and_trust_objects(doc: &CertDataDocument) -> (usize, usize) {
let mut certs = 0usize;
let mut trusts = 0usize;
@@ -1439,10 +1834,18 @@ fn run_trust_extract_job(
fn sync_system_ssl_symlinks(root: &Path, extracted_target: &Path, dry_run: bool) -> Result<()> {
let ssl_certs_host = path_in_root(root, Path::new(DEFAULT_SSL_CERTS_DIR));
let cert_pem_link_host = path_in_root(root, Path::new(DEFAULT_SSL_CERT_PEM_LINK));
let ca_bundle_link_host =
path_in_root(root, Path::new(DEFAULT_SSL_CA_CERTIFICATES_BUNDLE_LINK));
let ca_bundle_crt_link_host = path_in_root(root, Path::new(DEFAULT_SSL_CA_BUNDLE_CRT_LINK));
let java_link_host = path_in_root(root, Path::new(DEFAULT_JAVA_CACERTS_LINK));
let pki_ca_bundle_crt_link_host =
path_in_root(root, Path::new(DEFAULT_PKI_TLS_CA_BUNDLE_CRT_LINK));
let pki_ca_bundle_trust_crt_link_host =
path_in_root(root, Path::new(DEFAULT_PKI_TLS_CA_BUNDLE_TRUST_CRT_LINK));
let pki_java_link_host = path_in_root(root, Path::new(DEFAULT_PKI_TLS_JAVA_CACERTS_LINK));
let tls_bundle_src_host = path_in_root(root, &extracted_target.join("tls-ca-bundle.pem"));
let trust_bundle_src_host = path_in_root(root, &extracted_target.join("ca-bundle.trust.crt"));
let cadir_host = path_in_root(root, &extracted_target.join("cadir"));
let java_src_host = path_in_root(root, &extracted_target.join("java-cacerts.jks"));
@@ -1459,16 +1862,41 @@ fn sync_system_ssl_symlinks(root: &Path, extracted_target: &Path, dry_run: bool)
"[dry-run] Would delete broken symlinks in {}",
ssl_certs_host.display()
);
println!(
"[dry-run] Would link {} -> {}",
cert_pem_link_host.display(),
tls_bundle_src_host.display()
);
println!(
"[dry-run] Would link {} -> {}",
ca_bundle_link_host.display(),
tls_bundle_src_host.display()
);
println!(
"[dry-run] Would link {} -> {}",
ca_bundle_crt_link_host.display(),
tls_bundle_src_host.display()
);
println!(
"[dry-run] Would link {} -> {}",
java_link_host.display(),
java_src_host.display()
);
println!(
"[dry-run] Would link {} -> {}",
pki_ca_bundle_crt_link_host.display(),
tls_bundle_src_host.display()
);
println!(
"[dry-run] Would link {} -> {}",
pki_ca_bundle_trust_crt_link_host.display(),
trust_bundle_src_host.display()
);
println!(
"[dry-run] Would link {} -> {}",
pki_java_link_host.display(),
java_src_host.display()
);
return Ok(());
}
@@ -1479,6 +1907,21 @@ fn sync_system_ssl_symlinks(root: &Path, extracted_target: &Path, dry_run: bool)
.context("java cacerts link path has no parent")?;
fs::create_dir_all(java_parent)
.with_context(|| format!("failed to create {}", java_parent.display()))?;
let pki_ca_bundle_parent = pki_ca_bundle_crt_link_host
.parent()
.context("pki ca-bundle.crt link path has no parent")?;
fs::create_dir_all(pki_ca_bundle_parent)
.with_context(|| format!("failed to create {}", pki_ca_bundle_parent.display()))?;
let pki_ca_bundle_trust_parent = pki_ca_bundle_trust_crt_link_host
.parent()
.context("pki ca-bundle.trust.crt link path has no parent")?;
fs::create_dir_all(pki_ca_bundle_trust_parent)
.with_context(|| format!("failed to create {}", pki_ca_bundle_trust_parent.display()))?;
let pki_java_parent = pki_java_link_host
.parent()
.context("pki java cacerts link path has no parent")?;
fs::create_dir_all(pki_java_parent)
.with_context(|| format!("failed to create {}", pki_java_parent.display()))?;
// Link every extracted hash file into `/etc/ssl/certs`, then prune stale links.
for entry in fs::read_dir(&cadir_host)
@@ -1491,8 +1934,13 @@ fn sync_system_ssl_symlinks(root: &Path, extracted_target: &Path, dry_run: bool)
}
remove_broken_symlinks(&ssl_certs_host)?;
replace_symlink(&tls_bundle_src_host, &cert_pem_link_host)?;
replace_symlink(&tls_bundle_src_host, &ca_bundle_link_host)?;
replace_symlink(&tls_bundle_src_host, &ca_bundle_crt_link_host)?;
replace_symlink(&java_src_host, &java_link_host)?;
replace_symlink(&tls_bundle_src_host, &pki_ca_bundle_crt_link_host)?;
replace_symlink(&trust_bundle_src_host, &pki_ca_bundle_trust_crt_link_host)?;
replace_symlink(&java_src_host, &pki_java_link_host)?;
Ok(())
}
@@ -1866,6 +2314,34 @@ CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR
assert_eq!(bytes, b"ABC");
}
#[test]
fn sync_certdata_output_respects_explicit_path() {
let requested = PathBuf::from("/var/tmp/custom-certdata.txt");
let (resolved, cleanup) = resolve_sync_certdata_output(Some(requested.clone()), false).unwrap();
assert_eq!(resolved, requested);
assert!(cleanup.is_none());
}
#[test]
fn sync_certdata_output_uses_tmp_workspace_and_cleans_it() {
let (resolved, cleanup) = resolve_sync_certdata_output(None, false).unwrap();
assert!(resolved.starts_with(Path::new("/tmp")));
assert_eq!(resolved.file_name(), Some(OsStr::new(DEFAULT_CERTDATA_OUTPUT)));
let workspace = resolved.parent().unwrap().to_path_buf();
assert!(workspace.exists());
drop(cleanup);
assert!(!workspace.exists());
}
#[test]
fn sync_certdata_output_dry_run_uses_tmp_path_without_creating_workspace() {
let (resolved, cleanup) = resolve_sync_certdata_output(None, true).unwrap();
assert!(resolved.starts_with(Path::new("/tmp")));
assert_eq!(resolved.file_name(), Some(OsStr::new(DEFAULT_CERTDATA_OUTPUT)));
assert!(cleanup.is_none());
}
#[test]
fn percent_encode_bytes_encodes_lower_hex() {
assert_eq!(percent_encode_bytes(&[0x00, 0x2a, 0xff]), "%00%2a%ff");