diff --git a/.add/dependencies.allowlist b/.add/dependencies.allowlist index 5f23555e..1259c661 100644 --- a/.add/dependencies.allowlist +++ b/.add/dependencies.allowlist @@ -41,7 +41,7 @@ ctrlc metrics metrics-exporter-prometheus rustls -rustls-pemfile +rustls-pki-types aws-lc-rs tokio-rustls monoio-rustls diff --git a/.cargo/audit.toml b/.cargo/audit.toml index 961dc0a5..2c1fcd2b 100644 --- a/.cargo/audit.toml +++ b/.cargo/audit.toml @@ -7,7 +7,7 @@ # unsound / yanked print but do not fail) — deliberately NOT overridden # with `--deny warnings` here, matching the design that a real CVE hard # fails while informational advisories stay visible-but-non-blocking. The -# entries below are additionally silenced via `ignore` (same three +# entries below are additionally silenced via `ignore` (same two # unmaintained crates as deny.toml, plus one unsound-but-inapplicable # advisory not caught by cargo-deny's older rustsec matcher) purely to # keep CI logs clean; each has a reason and a re-check trigger. @@ -21,10 +21,6 @@ ignore = [ # pinned transitively by tikv-jemalloc-ctl 0.6.1. Revisit on next # jemalloc-ctl major bump. "RUSTSEC-2024-0436", - # rustls-pemfile 2.2.0: unmaintained; migrating to - # rustls-pki-types::PemObject is a source-code change (src/tls*) out - # of scope for this CI-wiring change. Tracked as a follow-up. - "RUSTSEC-2025-0134", # rand 0.9.2 (transitive via metrics-util 0.20.4 -> metrics -> moon): # RUSTSEC-2026-0097 is a narrow unsoundness that only triggers when a # *custom log::Log implementation* calls rand::rng() reentrantly from diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 234da0d4..216d8939 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,7 +11,7 @@ updates: async-runtime: patterns: ["tokio*", "monoio*", "futures*"] crypto-tls: - patterns: ["rustls*", "aws-lc-rs*", "rustls-pemfile*"] + patterns: ["rustls*", "aws-lc-rs*"] tracing: patterns: ["tracing*"] ignore: diff --git a/CHANGELOG.md b/CHANGELOG.md index 45de4396..50b40a9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 10×-RAM benchmark publication), cluster hardening + multi-shard replicas moved to v0.9, enterprise foundation to v0.10; debt register refreshed. +### Changed +- **TLS: migrated off the unmaintained `rustls-pemfile` onto `rustls-pki-types`'s + `PemObject` trait (task #66).** `build_tls_config` (`src/tls.rs`) now parses + certificate chains via `CertificateDer::pem_reader_iter` and private keys via + `PrivateKeyDer::from_pem_reader` — both provided directly by `rustls-pki-types` + (already in the dependency graph as rustls's own `pki_types` re-export), so no + new supply-chain surface is added. Behavior is unchanged: same fail-loud + `io::Error` wrapping per stage (`TLS cert file` / `TLS cert parse` / `TLS key + file` / `TLS key parse` / `CA cert parse`), same SIGHUP hot-reload path. Two + new unit tests (`test_build_tls_config_garbage_cert_parse_error`, + `test_build_tls_config_garbage_key_parse_error`) exercise the parser seam + directly with corrupt-but-well-formed PEM bodies — the existing suite only + covered missing-file paths, not actual DER decode failures. `rustls-pemfile` + is fully removed from `Cargo.toml`/`Cargo.lock`; the RUSTSEC-2025-0134 + ignore entries in `deny.toml` and `.cargo/audit.toml` are removed since the + advisory no longer applies. `cargo deny check advisories licenses bans + sources` and `cargo audit` both pass clean with no ignore needed. + ## [0.7.1] — 2026-07-15 Patch release closing the two follow-ups disclosed in the v0.7.0 tag notes: the diff --git a/Cargo.lock b/Cargo.lock index cd4c6a4c..eb684747 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1879,7 +1879,7 @@ dependencies = [ "rust-embed", "rust-stemmers", "rustls", - "rustls-pemfile", + "rustls-pki-types", "ryu", "serde", "serde_json", @@ -2585,15 +2585,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "rustls-pki-types" version = "1.14.0" diff --git a/Cargo.toml b/Cargo.toml index f6bb12fd..997f9f29 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,7 +52,7 @@ ctrlc = "3.4" metrics = "0.24" metrics-exporter-prometheus = { version = "0.18", default-features = false, features = ["http-listener"] } rustls = { version = "0.23", default-features = false, features = ["std", "tls12"], optional = true } -rustls-pemfile = { version = "2", optional = true } +rustls-pki-types = { version = "1", optional = true, features = ["std"] } aws-lc-rs = { version = "1", optional = true } tokio-rustls = { version = "0.26", optional = true } monoio-rustls = { version = "0.4", optional = true } @@ -103,8 +103,8 @@ jemalloc = ["dep:tikv-jemallocator", "dep:tikv-jemalloc-ctl"] # Usage: # cargo build --no-default-features --features runtime-monoio,mimalloc-alt,graph,text-index mimalloc-alt = [] -runtime-tokio = ["tokio/rt-multi-thread", "tokio/io-util", "tokio/signal", "tokio/time", "tokio/fs", "tokio/process", "dep:tokio-util", "dep:tokio-rustls", "dep:aws-lc-rs", "dep:rustls", "rustls/aws_lc_rs", "dep:rustls-pemfile"] -runtime-monoio = ["dep:monoio", "dep:monoio-rustls", "dep:aws-lc-rs", "dep:rustls", "rustls/aws_lc_rs", "dep:rustls-pemfile"] +runtime-tokio = ["tokio/rt-multi-thread", "tokio/io-util", "tokio/signal", "tokio/time", "tokio/fs", "tokio/process", "dep:tokio-util", "dep:tokio-rustls", "dep:aws-lc-rs", "dep:rustls", "rustls/aws_lc_rs", "dep:rustls-pki-types"] +runtime-monoio = ["dep:monoio", "dep:monoio-rustls", "dep:aws-lc-rs", "dep:rustls", "rustls/aws_lc_rs", "dep:rustls-pki-types"] gpu-cuda = ["dep:cudarc"] simd-avx512 = [] # OpenTelemetry exporter: reserves feature namespace for OTLP trace export. diff --git a/deny.toml b/deny.toml index e6429345..c547c853 100644 --- a/deny.toml +++ b/deny.toml @@ -32,12 +32,6 @@ ignore = [ # archived by upstream, no safe upgrade (the suggested forks — pastey / # with_builtin_macros — are not drop-in without a tikv-jemalloc-ctl bump). { id = "RUSTSEC-2024-0436", reason = "paste is unmaintained (archived upstream) with no safe upgrade; pinned transitively by tikv-jemalloc-ctl 0.6.1 — revisit on next jemalloc-ctl major bump" }, - # rustls-pemfile 2.2.0 (direct, optional dep behind runtime-tokio / - # runtime-monoio for TLS PEM loading): unmaintained, no safe drop-in - # upgrade in the 2.x line. Migrating to rustls-pki-types' PemObject API - # is a real code change (src/tls*), tracked separately — not done here - # to keep this CI-wiring change infra-only. - { id = "RUSTSEC-2025-0134", reason = "rustls-pemfile is unmaintained; migrating callers to rustls-pki-types::PemObject is a source-code change out of scope for CI wiring — tracked as a follow-up" }, ] [licenses] diff --git a/src/tls.rs b/src/tls.rs index f88c40e3..1e36d95f 100644 --- a/src/tls.rs +++ b/src/tls.rs @@ -2,6 +2,8 @@ use std::io; use std::sync::Arc; use arc_swap::ArcSwap; +use rustls_pki_types::pem::PemObject; +use rustls_pki_types::{CertificateDer, PrivateKeyDer}; use tracing::info; /// Map cipher suite name strings to rustls cipher suite constants. @@ -73,26 +75,17 @@ pub fn build_tls_config( // Load certificate chain let cert_file = std::fs::File::open(cert_path) .map_err(|e| io::Error::new(io::ErrorKind::NotFound, format!("TLS cert file: {}", e)))?; - let mut cert_reader = io::BufReader::new(cert_file); - let certs: Vec> = - rustls_pemfile::certs(&mut cert_reader) - .collect::, _>>() - .map_err(|e| { - io::Error::new(io::ErrorKind::InvalidData, format!("TLS cert parse: {}", e)) - })?; + let certs: Vec> = CertificateDer::pem_reader_iter(cert_file) + .collect::, _>>() + .map_err(|e| { + io::Error::new(io::ErrorKind::InvalidData, format!("TLS cert parse: {}", e)) + })?; // Load private key let key_file = std::fs::File::open(key_path) .map_err(|e| io::Error::new(io::ErrorKind::NotFound, format!("TLS key file: {}", e)))?; - let mut key_reader = io::BufReader::new(key_file); - let key = rustls_pemfile::private_key(&mut key_reader) - .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("TLS key parse: {}", e)))? - .ok_or_else(|| { - io::Error::new( - io::ErrorKind::InvalidData, - "No private key found in TLS key file", - ) - })?; + let key: PrivateKeyDer<'static> = PrivateKeyDer::from_pem_reader(key_file) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("TLS key parse: {}", e)))?; // Explicit default cipher suite allowlist. // @@ -135,13 +128,11 @@ pub fn build_tls_config( let ca_file = std::fs::File::open(ca_path).map_err(|e| { io::Error::new(io::ErrorKind::NotFound, format!("TLS CA cert file: {}", e)) })?; - let mut ca_reader = io::BufReader::new(ca_file); - let ca_certs: Vec> = - rustls_pemfile::certs(&mut ca_reader) - .collect::, _>>() - .map_err(|e| { - io::Error::new(io::ErrorKind::InvalidData, format!("CA cert parse: {}", e)) - })?; + let ca_certs: Vec> = CertificateDer::pem_reader_iter(ca_file) + .collect::, _>>() + .map_err(|e| { + io::Error::new(io::ErrorKind::InvalidData, format!("CA cert parse: {}", e)) + })?; let mut root_store = rustls::RootCertStore::empty(); for cert in ca_certs { @@ -334,6 +325,95 @@ mod tests { assert!(result.is_err()); } + /// Exercises the rustls-pki-types `CertificateDer::pem_reader_iter` parse + /// seam directly: a file with well-formed PEM boundary markers but a + /// corrupt base64 body must surface as a "TLS cert parse" error, not a + /// panic or a silently-empty cert chain. + #[test] + fn test_build_tls_config_garbage_cert_parse_error() { + let dir = std::env::temp_dir().join("moon-tls-garbage-cert-test"); + let _ = std::fs::create_dir_all(&dir); + let cert_path = dir.join("garbage.crt"); + let key_path = dir.join("unused.key"); + std::fs::write( + &cert_path, + "-----BEGIN CERTIFICATE-----\n!!!not valid base64!!!\n-----END CERTIFICATE-----\n", + ) + .unwrap(); + + let result = build_tls_config( + cert_path.to_str().unwrap(), + key_path.to_str().unwrap(), + None, + None, + ); + assert!(result.is_err()); + assert!( + result.unwrap_err().to_string().contains("TLS cert parse"), + "corrupt PEM body must be reported as a cert parse error" + ); + + let _ = std::fs::remove_dir_all(&dir); + } + + /// Same seam as above for the private key: `PrivateKeyDer::from_pem_reader` + /// must fail loud on a corrupt PKCS8 body rather than panicking or + /// returning a bogus key. + #[test] + fn test_build_tls_config_garbage_key_parse_error() { + let dir = std::env::temp_dir().join("moon-tls-garbage-key-test"); + let _ = std::fs::create_dir_all(&dir); + let cert_path = dir.join("valid.crt"); + let key_path = dir.join("garbage.key"); + + // Generate a real cert so we reach key parsing. + let status = std::process::Command::new("openssl") + .args([ + "req", + "-x509", + "-newkey", + "ec", + "-pkeyopt", + "ec_paramgen_curve:prime256v1", + "-keyout", + dir.join("throwaway.key").to_str().unwrap(), + "-out", + cert_path.to_str().unwrap(), + "-days", + "1", + "-nodes", + "-subj", + "/CN=moon-garbage-key-test", + ]) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status(); + if !matches!(status, Ok(s) if s.success()) { + eprintln!("openssl not available, skipping TLS garbage key test"); + let _ = std::fs::remove_dir_all(&dir); + return; + } + std::fs::write( + &key_path, + "-----BEGIN PRIVATE KEY-----\n!!!not valid base64!!!\n-----END PRIVATE KEY-----\n", + ) + .unwrap(); + + let result = build_tls_config( + cert_path.to_str().unwrap(), + key_path.to_str().unwrap(), + None, + None, + ); + assert!(result.is_err()); + assert!( + result.unwrap_err().to_string().contains("TLS key parse"), + "corrupt PEM body must be reported as a key parse error" + ); + + let _ = std::fs::remove_dir_all(&dir); + } + #[test] fn test_reload_tls_config_swaps_config() { let dir = std::env::temp_dir().join("moon-tls-reload-test");