fix(net): load configured CA roots without default cost
This commit is contained in:
1 parent
0522831dcc
commit
f46f741e8c
4 files changed
+81
-82
No files matched your search
Generated
-68
@@ -510,22 +510,6 @@ version = "5.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147be55d677052dabc6b22252d5dd0fd4c29c8c27aa4f2fbef0f94aa003b406f"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
@@ -1231,7 +1215,6 @@ dependencies = [
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
@@ -1836,12 +1819,6 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
||||
|
||||
[[package]]
|
||||
name = "outref"
|
||||
version = "0.5.2"
|
||||
@@ -2272,7 +2249,6 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -2351,18 +2327,6 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-native-certs"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
|
||||
dependencies = [
|
||||
"openssl-probe",
|
||||
"rustls-pki-types",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.14.1"
|
||||
@@ -2396,44 +2360,12 @@ version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "3.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "selectors"
|
||||
version = "0.26.0"
|
||||
|
||||
+1
-5
@@ -36,11 +36,7 @@ tokio-tungstenite = "0.26"
|
||||
# (obscura-net/src/cookies.rs) and never uses reqwest's cookie store. Pulling
|
||||
# it in only added the cookie/cookie_store crates, and cookie 0.18.1's blanket
|
||||
# From impl fails to compile under coherence on some toolchains (issue #295).
|
||||
# "rustls-tls-native-roots" adds the platform trust store (and SSL_CERT_FILE /
|
||||
# SSL_CERT_DIR) on top of the bundled webpki roots, so deployments behind a
|
||||
# corporate CA or an inspecting egress proxy can trust their own roots. The
|
||||
# two root sets merge, so the default trust surface only grows.
|
||||
reqwest = { version = "0.12", features = ["gzip", "brotli", "deflate", "rustls-tls", "rustls-tls-native-roots", "socks"], default-features = false }
|
||||
reqwest = { version = "0.12", features = ["gzip", "brotli", "deflate", "rustls-tls", "socks"], default-features = false }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
url = "2"
|
||||
|
||||
@@ -26,7 +26,7 @@ tempfile = "3"
|
||||
wreq-util = { version = "=3.0.0-rc.12", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
# TLS fixtures for the native-roots tests: rcgen mints a throwaway CA + leaf,
|
||||
# TLS fixtures for the configured-roots tests: rcgen mints a throwaway CA + leaf,
|
||||
# tokio-rustls serves one HTTPS response with them. Pinned to the ring
|
||||
# provider so the test binary has a single rustls CryptoProvider (reqwest's
|
||||
# rustls also uses ring; adding aws-lc-rs would make provider auto-selection
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use std::time::Duration;
|
||||
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
@@ -14,6 +14,52 @@ use url::Url;
|
||||
use crate::cookies::CookieJar;
|
||||
use crate::interceptor::{InterceptAction, RequestInterceptor};
|
||||
|
||||
fn configured_root_paths() -> Vec<std::path::PathBuf> {
|
||||
let mut paths = Vec::new();
|
||||
if let Some(path) = std::env::var_os("SSL_CERT_FILE").filter(|path| !path.is_empty()) {
|
||||
paths.push(path.into());
|
||||
}
|
||||
if let Some(directory) = std::env::var_os("SSL_CERT_DIR").filter(|path| !path.is_empty()) {
|
||||
match std::fs::read_dir(directory) {
|
||||
Ok(entries) => {
|
||||
paths.extend(entries.filter_map(|entry| entry.ok().map(|entry| entry.path())));
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, "failed to read SSL_CERT_DIR");
|
||||
}
|
||||
}
|
||||
}
|
||||
paths.sort();
|
||||
paths
|
||||
}
|
||||
|
||||
fn configured_root_certificates() -> &'static [reqwest::Certificate] {
|
||||
static ROOTS: OnceLock<Vec<reqwest::Certificate>> = OnceLock::new();
|
||||
|
||||
ROOTS.get_or_init(|| {
|
||||
let mut certificates = Vec::new();
|
||||
for path in configured_root_paths() {
|
||||
let bytes = match std::fs::read(&path) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, path = %path.display(), "failed to read CA certificate file");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
match reqwest::Certificate::from_pem_bundle(&bytes) {
|
||||
Ok(mut bundle) if !bundle.is_empty() => certificates.append(&mut bundle),
|
||||
_ => match reqwest::Certificate::from_der(&bytes) {
|
||||
Ok(certificate) => certificates.push(certificate),
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, path = %path.display(), "failed to parse CA certificate file");
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
certificates
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Response {
|
||||
pub url: Url,
|
||||
@@ -456,6 +502,14 @@ impl ObscuraHttpClient {
|
||||
.dns_resolver(Arc::new(SsrfGuardResolver::new(self.allow_private_network)))
|
||||
;
|
||||
|
||||
if std::env::var_os("SSL_CERT_FILE").is_some()
|
||||
|| std::env::var_os("SSL_CERT_DIR").is_some()
|
||||
{
|
||||
for certificate in configured_root_certificates() {
|
||||
builder = builder.add_root_certificate(certificate.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref proxy) = self.proxy_url {
|
||||
if let Ok(p) = reqwest::Proxy::all(proxy.as_str()) {
|
||||
builder = builder.proxy(p);
|
||||
@@ -911,13 +965,13 @@ mod ssrf_tests {
|
||||
(port, ca_cert.pem())
|
||||
}
|
||||
|
||||
// The two native-roots tests set/rely on SSL_CERT_FILE, which
|
||||
// rustls-native-certs reads once per process at client build. They are
|
||||
// only correct under `cargo nextest` (one process per test) — the same
|
||||
// constraint the whole workspace already has.
|
||||
// The two configured-roots tests set/rely on SSL_CERT_FILE, which is
|
||||
// cached once per process at client build. They are only correct under
|
||||
// `cargo nextest` (one process per test), the same constraint the whole
|
||||
// workspace already has.
|
||||
|
||||
#[tokio::test]
|
||||
async fn native_roots_trusts_a_private_ca_via_ssl_cert_file() {
|
||||
async fn configured_roots_trust_a_private_ca_via_ssl_cert_file() {
|
||||
let (port, ca_pem) = https_fixture_with_private_ca().await;
|
||||
let ca_file = tempfile::NamedTempFile::new().unwrap();
|
||||
std::fs::write(ca_file.path(), ca_pem).unwrap();
|
||||
@@ -931,10 +985,27 @@ mod ssrf_tests {
|
||||
assert_eq!(resp.text(), "private ca ok");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn configured_roots_trust_a_private_ca_via_ssl_cert_dir() {
|
||||
let (port, ca_pem) = https_fixture_with_private_ca().await;
|
||||
let ca_dir = tempfile::tempdir().unwrap();
|
||||
std::fs::write(ca_dir.path().join("private-ca.pem"), ca_pem).unwrap();
|
||||
std::env::set_var("SSL_CERT_DIR", ca_dir.path());
|
||||
|
||||
let client =
|
||||
ObscuraHttpClient::with_full_options(Arc::new(CookieJar::new()), None, true);
|
||||
let url = Url::parse(&format!("https://127.0.0.1:{port}/")).unwrap();
|
||||
let resp = client
|
||||
.fetch(&url)
|
||||
.await
|
||||
.expect("private CA in SSL_CERT_DIR must be trusted");
|
||||
assert_eq!(resp.status, 200);
|
||||
assert_eq!(resp.text(), "private ca ok");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn private_ca_is_still_rejected_without_ssl_cert_file() {
|
||||
// Guards against the native-roots feature weakening verification: the
|
||||
// same fixture that the SSL_CERT_FILE test trusts must fail here. The
|
||||
// The same fixture that the SSL_CERT_FILE test trusts must fail here. The
|
||||
// listener is reachable (same setup), so an Err can only be TLS.
|
||||
let (port, _ca_pem) = https_fixture_with_private_ca().await;
|
||||
let client =
|
||||
|
||||
Reference in new issue
Block a user