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:
+491
-15
@@ -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-----
|
||||
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
|
||||
-----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");
|
||||
|
||||
Reference in New Issue
Block a user