diff --git a/.github/workflows/branch-checks.yml b/.github/workflows/branch-checks.yml index 7713febb6..7c5ca78a6 100644 --- a/.github/workflows/branch-checks.yml +++ b/.github/workflows/branch-checks.yml @@ -125,6 +125,9 @@ jobs: - name: Verify telemetry can be compiled out run: mise run rust:verify:telemetry-off + - name: Verify system-ca-roots feature compiles and excludes bundled Mozilla roots + run: mise run rust:verify:system-ca-roots + - name: sccache stats if: always() run: | diff --git a/Cargo.lock b/Cargo.lock index c86773bb7..176e29646 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -549,24 +549,6 @@ dependencies = [ "sha2 0.10.9", ] -[[package]] -name = "bindgen" -version = "0.72.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" -dependencies = [ - "bitflags 2.11.1", - "cexpr", - "clang-sys", - "itertools 0.13.0", - "proc-macro2", - "quote", - "regex", - "rustc-hash 2.1.2", - "shlex", - "syn 2.0.117", -] - [[package]] name = "bitflags" version = "1.3.2" @@ -710,15 +692,6 @@ dependencies = [ "either", ] -[[package]] -name = "bzip2" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c" -dependencies = [ - "libbz2-rs-sys", -] - [[package]] name = "capctl" version = "0.2.4" @@ -772,15 +745,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - [[package]] name = "cfg-if" version = "1.0.4" @@ -828,17 +792,6 @@ dependencies = [ "inout", ] -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - [[package]] name = "clap" version = "4.6.1" @@ -990,12 +943,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "constant_time_eq" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" - [[package]] name = "core-foundation" version = "0.10.1" @@ -1304,12 +1251,6 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" -[[package]] -name = "deflate64" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" - [[package]] name = "delegate" version = "0.13.5" @@ -1671,7 +1612,6 @@ checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", - "zlib-rs", ] [[package]] @@ -1882,12 +1822,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi 6.0.0", "wasip2", "wasip3", - "wasm-bindgen", ] [[package]] @@ -2236,7 +2174,6 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.7", ] [[package]] @@ -2914,12 +2851,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" -[[package]] -name = "libbz2-rs-sys" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3a6a8c165077efc8f3a971534c50ea6a1a18b329ef4a66e897a7e3a1494565f" - [[package]] name = "libc" version = "0.2.185" @@ -3089,15 +3020,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" -[[package]] -name = "lzma-rust2" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47bb1e988e6fb779cf720ad431242d3f03167c1b3f2b1aae7f1a94b2495b36ae" -dependencies = [ - "sha2 0.10.9", -] - [[package]] name = "matchers" version = "0.2.0" @@ -3330,20 +3252,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "num" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" -dependencies = [ - "num-bigint", - "num-complex", - "num-integer", - "num-iter", - "num-rational", - "num-traits", -] - [[package]] name = "num-bigint" version = "0.4.6" @@ -3372,15 +3280,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "num-complex" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" -dependencies = [ - "num-traits", -] - [[package]] name = "num-conv" version = "0.2.1" @@ -3407,17 +3306,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -3950,6 +3838,7 @@ dependencies = [ "regorus", "reqwest 0.12.28", "rustls", + "rustls-native-certs", "rustls-pemfile", "serde", "serde_json", @@ -3966,7 +3855,7 @@ dependencies = [ "tower-mcp-types", "tracing", "uuid", - "webpki-roots 1.0.7", + "webpki-roots", ] [[package]] @@ -4452,12 +4341,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" -[[package]] -name = "ppmd-rust" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24" - [[package]] name = "ppv-lite86" version = "0.2.21" @@ -4911,7 +4794,6 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.7", ] [[package]] @@ -5818,6 +5700,7 @@ dependencies = [ "once_cell", "percent-encoding", "rustls", + "rustls-native-certs", "serde", "serde_json", "sha2 0.10.9", @@ -5827,7 +5710,6 @@ dependencies = [ "tokio-stream", "tracing", "url", - "webpki-roots 0.26.11", ] [[package]] @@ -5860,7 +5742,6 @@ dependencies = [ "serde_json", "sha2 0.10.9", "sqlx-core", - "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", "syn 2.0.117", @@ -5899,7 +5780,6 @@ dependencies = [ "percent-encoding", "rand 0.8.6", "rsa 0.9.10", - "serde", "sha1 0.10.6", "sha2 0.10.9", "smallvec", @@ -6296,7 +6176,6 @@ checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", - "js-sys", "num-conv", "powerfmt", "serde_core", @@ -6796,12 +6675,6 @@ dependencies = [ "thiserror 2.0.18", ] -[[package]] -name = "typed-path" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" - [[package]] name = "typenum" version = "1.19.0" @@ -7162,15 +7035,6 @@ dependencies = [ "rustls-pki-types", ] -[[package]] -name = "webpki-roots" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" -dependencies = [ - "webpki-roots 1.0.7", -] - [[package]] name = "webpki-roots" version = "1.0.7" @@ -7827,27 +7691,31 @@ dependencies = [ [[package]] name = "z3" -version = "0.19.15" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "107cca65ed27d28b11f7c492298a51383333fd48ba6ebe49a432aba96162f678" +checksum = "80c4de445f5c9e3013703a6b8a40c80b4a64c925f0a19e7d0a23a7a9b70e854d" dependencies = [ "log", - "num", "z3-sys", ] [[package]] -name = "z3-sys" -version = "0.10.9" +name = "z3-src" +version = "416.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c82b97329d02d87da6802ed9fda083f1b255d822ab13d5b1fb961196b58a69a1" +checksum = "f2af0c6527de39877cf55cb87f233016573eeeb7cf77afdc1469e4b32faef832" dependencies = [ - "bindgen", "cmake", +] + +[[package]] +name = "z3-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c18b0a91a13522d21b3414847667de2b2056a721a3edcb5b6ee6858352d58db4" +dependencies = [ "pkg-config", - "reqwest 0.12.28", - "serde_json", - "zip", + "z3-src", ] [[package]] @@ -7944,57 +7812,12 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "zip" -version = "8.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcab981e19633ebcf0b001ddd37dd802996098bc1864f90b7c5d970ce76c1d59" -dependencies = [ - "aes", - "bzip2", - "constant_time_eq", - "crc32fast", - "deflate64", - "flate2", - "getrandom 0.4.2", - "hmac", - "indexmap", - "lzma-rust2", - "memchr", - "pbkdf2", - "ppmd-rust", - "sha1 0.10.6", - "time", - "typed-path", - "zeroize", - "zopfli", - "zstd", -] - -[[package]] -name = "zlib-rs" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" - [[package]] name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" -[[package]] -name = "zopfli" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" -dependencies = [ - "bumpalo", - "crc32fast", - "log", - "simd-adler32", -] - [[package]] name = "zstd" version = "0.13.3" diff --git a/Cargo.toml b/Cargo.toml index f450cd5c8..b5e38967e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ rustls = { version = "0.23", default-features = false, features = ["std", "loggi rustls-pemfile = "2" rcgen = { version = "0.13", features = ["crypto", "pem"] } webpki-roots = "1" +rustls-native-certs = "0.8" # CLI clap = { version = "4.5", features = ["derive", "env"] } @@ -79,7 +80,7 @@ tower-mcp-types = "0.12.0" reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls-native-roots"] } # WebSocket -tokio-tungstenite = { version = "0.26", features = ["rustls-tls-native-roots"] } +tokio-tungstenite = { version = "0.26", default-features = false, features = ["connect", "rustls-tls-native-roots"] } # Clipboard (OSC 52) base64 = "0.22" @@ -106,10 +107,10 @@ protobuf-src = "1.1.0" url = "2" # Database -sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres", "sqlite", "migrate"] } +sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio", "tls-rustls-ring-native-roots", "postgres", "sqlite", "migrate", "macros"] } # Kubernetes -kube = { version = "0.90", features = ["runtime", "derive"] } +kube = { version = "0.90", default-features = false, features = ["client", "runtime", "derive", "rustls-tls"] } kube-runtime = "0.90" k8s-openapi = { version = "0.21.1", features = ["v1_26"] } @@ -117,7 +118,7 @@ k8s-openapi = { version = "0.21.1", features = ["v1_26"] } uuid = { version = "1.10", features = ["v4"] } # SMT solver (uses system libz3; enable z3/bundled via the prover's bundled-z3 feature for local dev without system z3) -z3 = "0.19" +z3 = "0.20" [workspace.lints.rust] unsafe_code = "warn" diff --git a/architecture/build.md b/architecture/build.md index a3cb2e25f..258fa2537 100644 --- a/architecture/build.md +++ b/architecture/build.md @@ -39,6 +39,21 @@ are no-ops, so the data-model types stay available and dependent crates compile unchanged. The runtime `OPENSHELL_TELEMETRY_ENABLED` switch remains the way to disable telemetry in a default (telemetry-enabled) build. +Supervisor upstream TLS root-store selection is controlled by mutually +exclusive Cargo features. Default builds enable `bundled-ca-roots`, which +preserves upstream behavior by using Mozilla roots through `webpki-roots` plus +locally-installed CAs from the system bundle. Linux distribution builds should +use `--no-default-features --features system-ca-roots` so supervisor upstream +TLS uses the native trust store and the workspace dependency graph excludes +bundled Mozilla root crates such as `webpki-roots` and `webpki-root-certs`. +Other Rustls clients use native roots directly because that already satisfies +Linux distribution trust-store policy. + +The workspace uses `z3` versions whose `z3-sys` dependency keeps downloader +HTTP/TLS support behind explicit build features, so default system-Z3 builds do +not reintroduce bundled Mozilla roots. Release builds that need bundled Z3 +continue to opt in with `bundled-z3`. + ## Linux Runtime Environments OpenShell uses different Linux libc environments for different host artifacts. diff --git a/crates/openshell-cli/Cargo.toml b/crates/openshell-cli/Cargo.toml index 8e4cb3fb2..bcfb7887d 100644 --- a/crates/openshell-cli/Cargo.toml +++ b/crates/openshell-cli/Cargo.toml @@ -46,7 +46,7 @@ bytes = { workspace = true } http-body-util = { workspace = true } hyper = { workspace = true } hyper-util = { workspace = true } -hyper-rustls = { version = "0.27", default-features = false, features = ["native-tokio", "http1", "http2", "tls12", "logging", "ring", "webpki-tokio"] } +hyper-rustls = { version = "0.27", default-features = false, features = ["native-tokio", "http1", "http2", "tls12", "logging", "ring"] } rustls = { workspace = true } rustls-pemfile = { workspace = true } tokio-rustls = { workspace = true } @@ -63,7 +63,7 @@ tar = "0.4" tempfile = "3" # OIDC/Auth -oauth2 = "5" +oauth2 = { version = "5", default-features = false, features = ["reqwest"] } base64 = { workspace = true } # WebSocket (Cloudflare tunnel proxy) diff --git a/crates/openshell-core/Cargo.toml b/crates/openshell-core/Cargo.toml index 0ff6d06d6..5b1b97e4a 100644 --- a/crates/openshell-core/Cargo.toml +++ b/crates/openshell-core/Cargo.toml @@ -25,7 +25,7 @@ url = { workspace = true } ipnet = "2" base64 = { workspace = true } chrono = { version = "0.4", default-features = false, features = ["clock", "std"], optional = true } -reqwest = { workspace = true, features = ["blocking", "rustls-tls-webpki-roots"], optional = true } +reqwest = { workspace = true, features = ["blocking", "rustls-tls-native-roots"], optional = true } [features] default = ["telemetry"] diff --git a/crates/openshell-sandbox/Cargo.toml b/crates/openshell-sandbox/Cargo.toml index 086dbe02c..037f5c739 100644 --- a/crates/openshell-sandbox/Cargo.toml +++ b/crates/openshell-sandbox/Cargo.toml @@ -18,7 +18,7 @@ path = "src/main.rs" openshell-core = { path = "../openshell-core", default-features = false } openshell-ocsf = { path = "../openshell-ocsf" } openshell-policy = { path = "../openshell-policy" } -openshell-supervisor-network = { path = "../openshell-supervisor-network" } +openshell-supervisor-network = { path = "../openshell-supervisor-network", default-features = false } openshell-supervisor-process = { path = "../openshell-supervisor-process" } # Async runtime @@ -45,11 +45,13 @@ tracing-subscriber = { workspace = true } tracing-appender = { workspace = true } [features] -default = ["telemetry"] +default = ["telemetry", "bundled-ca-roots"] ## Compile in telemetry activity collection (forwards to openshell-core/telemetry). ## On by default; build with `--no-default-features` for a telemetry-free sandbox ## supervisor that never collects or forwards activity summaries. telemetry = ["openshell-core/telemetry"] +bundled-ca-roots = ["openshell-supervisor-network/bundled-ca-roots"] +system-ca-roots = ["openshell-supervisor-network/system-ca-roots"] [dev-dependencies] openshell-core = { path = "../openshell-core", features = ["test-helpers"] } diff --git a/crates/openshell-server/Cargo.toml b/crates/openshell-server/Cargo.toml index b5c9b34d7..f04a9e5e8 100644 --- a/crates/openshell-server/Cargo.toml +++ b/crates/openshell-server/Cargo.toml @@ -108,7 +108,7 @@ bundled-z3 = ["openshell-prover/bundled-z3"] test-support = [] [dev-dependencies] -hyper-rustls = { version = "0.27", default-features = false, features = ["native-tokio", "http1", "tls12", "logging", "ring", "webpki-tokio"] } +hyper-rustls = { version = "0.27", default-features = false, features = ["native-tokio", "http1", "tls12", "logging", "ring"] } rcgen = { version = "0.13", features = ["crypto", "pem"] } tokio-tungstenite = { workspace = true } futures-util = "0.3" diff --git a/crates/openshell-supervisor-network/Cargo.toml b/crates/openshell-supervisor-network/Cargo.toml index 7d0079f7b..de77344df 100644 --- a/crates/openshell-supervisor-network/Cargo.toml +++ b/crates/openshell-supervisor-network/Cargo.toml @@ -32,6 +32,7 @@ rcgen = { workspace = true } regorus = { version = "0.9", default-features = false, features = ["std", "arc", "glob"] } reqwest = { workspace = true } rustls = { workspace = true } +rustls-native-certs = { workspace = true, optional = true } rustls-pemfile = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } @@ -45,7 +46,12 @@ tokio-rustls = { workspace = true } tower-mcp-types = { workspace = true } tracing = { workspace = true } uuid = { workspace = true } -webpki-roots = { workspace = true } +webpki-roots = { workspace = true, optional = true } + +[features] +default = ["bundled-ca-roots"] +bundled-ca-roots = ["dep:webpki-roots"] +system-ca-roots = ["dep:rustls-native-certs"] [dev-dependencies] openshell-core = { path = "../openshell-core", features = ["test-helpers"] } diff --git a/crates/openshell-supervisor-network/src/l7/tls.rs b/crates/openshell-supervisor-network/src/l7/tls.rs index 70e198f42..942e796e1 100644 --- a/crates/openshell-supervisor-network/src/l7/tls.rs +++ b/crates/openshell-supervisor-network/src/l7/tls.rs @@ -8,7 +8,7 @@ //! store, terminates TLS from the client (presenting dynamic certs per hostname), //! inspects the plaintext HTTP, then re-encrypts to upstream using real root CAs. -use miette::{IntoDiagnostic, Result}; +use miette::{IntoDiagnostic, Result, miette}; use rcgen::{CertificateParams, DnType, IsCa, KeyPair, KeyUsagePurpose}; use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName}; use rustls::{ClientConfig, ServerConfig}; @@ -20,6 +20,12 @@ use tokio::io::{AsyncRead, AsyncWrite}; use tokio::net::TcpStream; use tokio_rustls::{TlsAcceptor, TlsConnector}; +#[cfg(all(feature = "bundled-ca-roots", feature = "system-ca-roots"))] +compile_error!("features bundled-ca-roots and system-ca-roots are mutually exclusive"); + +#[cfg(not(any(feature = "bundled-ca-roots", feature = "system-ca-roots")))] +compile_error!("one of bundled-ca-roots or system-ca-roots must be enabled"); + const MAX_CACHED_CERTS: usize = 256; /// System CA bundle search paths (common Linux locations). @@ -180,7 +186,7 @@ pub async fn tls_terminate_client( Ok(tls_stream) } -/// Connect TLS to an upstream server, verifying against webpki-roots. +/// Connect TLS to an upstream server, verifying against the configured CA roots. /// /// Returns a TLS stream for re-encrypted upstream communication. pub async fn tls_connect_upstream( @@ -197,34 +203,75 @@ pub async fn tls_connect_upstream( Ok(tls_stream) } -/// Build a rustls `ClientConfig` with Mozilla + system root CAs for upstream connections. +/// Build a rustls `ClientConfig` using the configured CA root source. /// -/// `system_ca_bundle` is the pre-read PEM contents of the system CA bundle -/// (from [`read_system_ca_bundle`]). Pass the same string to [`write_ca_files`] -/// to avoid reading the bundle from disk twice. -pub fn build_upstream_client_config(system_ca_bundle: &str) -> Arc { +/// In `bundled-ca-roots` mode this uses Mozilla roots from `webpki-roots` overlaid +/// with any locally-installed CAs from `system_ca_bundle` (e.g. corporate or private +/// CAs added to `/etc/pki/ca-trust`). Duplicates with the Mozilla bundle are harmless. +/// +/// In `system-ca-roots` mode this uses the platform/native trust store exclusively; +/// `system_ca_bundle` is ignored because the native store already reflects all +/// operator-installed trust anchors. +pub fn build_upstream_client_config(system_ca_bundle: &str) -> Result> { + let mut config = ClientConfig::builder() + .with_root_certificates(build_upstream_root_store(system_ca_bundle)?) + .with_no_client_auth(); + config.alpn_protocols = vec![b"http/1.1".to_vec()]; + + Ok(Arc::new(config)) +} + +fn build_upstream_root_store(system_ca_bundle: &str) -> Result { let mut root_store = rustls::RootCertStore::empty(); - root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); - // System bundles typically overlap with webpki-roots (Mozilla roots); - // duplicates are harmless and ensure we also pick up any custom/corporate CAs. - let (added, ignored) = load_pem_certs_into_store(&mut root_store, system_ca_bundle); - if added > 0 { - tracing::debug!(added, "Loaded system CA certificates for upstream TLS"); + #[cfg(feature = "bundled-ca-roots")] + { + root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); + // Overlay system/corporate CAs so custom trust anchors are honoured in + // default upstream builds. Duplicates with webpki-roots are harmless. + let (added, ignored) = load_pem_certs_into_store(&mut root_store, system_ca_bundle); + if added > 0 { + tracing::debug!(added, "loaded system CA certificates for upstream TLS"); + } + if ignored > 0 { + tracing::warn!( + ignored, + "some system CA certificates could not be parsed and were ignored" + ); + } } + + #[cfg(feature = "system-ca-roots")] + { + let _ = system_ca_bundle; // native store already includes operator-installed CAs + add_native_roots(&mut root_store)?; + } + + if root_store.is_empty() { + return Err(miette!("no TLS root certificates available")); + } + + Ok(root_store) +} + +#[cfg(feature = "system-ca-roots")] +fn add_native_roots(root_store: &mut rustls::RootCertStore) -> Result<()> { + let native_certs = rustls_native_certs::load_native_certs(); + let cert_count = native_certs.certs.len(); + let (added, ignored) = root_store.add_parsable_certificates(native_certs.certs); + let ignored = ignored + native_certs.errors.len(); + if ignored > 0 { - tracing::warn!( - ignored, - "Some system CA certificates could not be parsed and were ignored" - ); + tracing::debug!(ignored, "ignored unparsable native root certificates"); } - let mut config = ClientConfig::builder() - .with_root_certificates(root_store) - .with_no_client_auth(); - config.alpn_protocols = vec![b"http/1.1".to_vec()]; + if added == 0 { + return Err(miette!( + "no usable native TLS root certificates found ({cert_count} loaded, {ignored} ignored)" + )); + } - Arc::new(config) + Ok(()) } /// Write CA certificate files for the sandbox trust store. @@ -234,8 +281,7 @@ pub fn build_upstream_client_config(system_ca_bundle: &str) -> Arc /// 2. Combined bundle: system CAs + sandbox CA (for `SSL_CERT_FILE` which replaces default) /// /// `system_ca_bundle` is the pre-read PEM contents of the system CA bundle -/// (from [`read_system_ca_bundle`]). Pass the same string to -/// [`build_upstream_client_config`] to avoid reading the bundle from disk twice. +/// (from [`read_system_ca_bundle`]). /// /// Returns `(ca_cert_path, combined_bundle_path)`. pub fn write_ca_files( @@ -266,6 +312,7 @@ pub fn write_ca_files( /// Returns `(added, ignored)` counts. Invalid or unparseable certificates /// are silently ignored, matching the behavior of /// `RootCertStore::add_parsable_certificates`. +#[cfg_attr(feature = "system-ca-roots", allow(dead_code))] fn load_pem_certs_into_store( root_store: &mut rustls::RootCertStore, pem_data: &str, @@ -289,7 +336,7 @@ fn load_pem_certs_into_store( /// /// Returns the PEM contents of the first non-empty bundle found, or an empty /// string if none of the well-known paths exist. Call once and pass the result -/// to both [`write_ca_files`] and [`build_upstream_client_config`]. +/// to [`write_ca_files`]. pub fn read_system_ca_bundle() -> String { for path in SYSTEM_CA_PATHS { if let Ok(contents) = std::fs::read_to_string(path) @@ -299,7 +346,6 @@ pub fn read_system_ca_bundle() -> String { } } // No system bundle found — combined file will contain only the sandbox CA. - // This is acceptable since the proxy uses webpki-roots independently. String::new() } @@ -426,7 +472,7 @@ mod tests { #[test] fn upstream_config_alpn() { let _ = rustls::crypto::ring::default_provider().install_default(); - let config = build_upstream_client_config(""); + let config = build_upstream_client_config("").unwrap(); assert_eq!(config.alpn_protocols, vec![b"http/1.1".to_vec()]); } diff --git a/crates/openshell-supervisor-network/src/run.rs b/crates/openshell-supervisor-network/src/run.rs index 9553e0673..3fe810391 100644 --- a/crates/openshell-supervisor-network/src/run.rs +++ b/crates/openshell-supervisor-network/src/run.rs @@ -209,7 +209,7 @@ pub async fn run_networking( // path injected by enrich_*_baseline_paths(), so no // explicit Landlock entry is needed here. - let upstream_config = build_upstream_client_config(&system_ca_bundle); + let upstream_config = build_upstream_client_config(&system_ca_bundle)?; let cert_cache = CertCache::new(ca); let state = Arc::new(ProxyTlsState::new(cert_cache, upstream_config)); ocsf_emit!( diff --git a/tasks/rust.toml b/tasks/rust.toml index a8856377f..c72d12a1b 100644 --- a/tasks/rust.toml +++ b/tasks/rust.toml @@ -39,3 +39,14 @@ run = [ "cargo build -p openshell-sandbox --bin openshell-sandbox --no-default-features", "tasks/scripts/verify-telemetry-compiled-out.sh absent target/debug/openshell-sandbox", ] + +["rust:verify:system-ca-roots"] +description = "Verify system-ca-roots feature compiles and excludes bundled Mozilla root crates" +run = [ + # Check that the workspace, including test targets, compiles cleanly under system-ca-roots. + "cargo check --workspace --all-targets --no-default-features --features system-ca-roots", + # Guard: webpki-roots must not appear in the production dependency graph. + "bash -c 'if cargo tree -i webpki-roots --no-default-features --features system-ca-roots 2>/dev/null | grep -q webpki-roots; then echo \"ERROR: webpki-roots found in system-ca-roots production build\" >&2; exit 1; fi'", + # Guard: webpki-root-certs must not appear either (webpki-roots re-exports it). + "bash -c 'if cargo tree -i webpki-root-certs --no-default-features --features system-ca-roots 2>/dev/null | grep -q webpki-root-certs; then echo \"ERROR: webpki-root-certs found in system-ca-roots production build\" >&2; exit 1; fi'", +]