mirror of
https://github.com/openai/codex.git
synced 2026-05-21 03:33:41 +00:00
Refs: https://linear.app/openai/issue/SE-6311/login-fails-for-experian-users-behind-tls-inspecting-proxy ## Summary - When a custom CA bundle is configured, force the shared `codex-client` reqwest builder onto rustls before registering custom roots. - Add the `rustls-tls-native-roots` reqwest feature so the rustls client preserves native roots plus the enterprise CA bundle. - Add subprocess TLS coverage for both a direct local TLS 1.3 server and a hermetic local CONNECT TLS-intercepting proxy that forwards a token-exchange-shaped POST to a local origin. ## Plain-language explanation Experian users are behind a TLS-inspecting proxy, so the login token exchange needs to trust the enterprise CA bundle from `CODEX_CA_CERTIFICATE` or `SSL_CERT_FILE`. Before this change, that custom-CA branch still used reqwest default TLS selection, which could fail in the proxy environment. Now, only when a custom CA is configured, Codex selects rustls first and then adds the custom CA roots, matching the validated behavior from the Experian test build while leaving normal system-root clients unchanged. The new regression test recreates the enterprise-proxy shape locally: the probe client sends an HTTPS `POST /oauth/token` through an explicit HTTP CONNECT proxy, the proxy presents a leaf certificate signed by a runtime-generated test CA, decrypts the request, forwards it to a local origin, and relays the `ok` response back. ## Scope note - The actual production fix is the first commit: `8368119282 Fix custom CA reqwest clients to use rustls`. - The second commit is integration-test coverage only. It generates all test CA and localhost certificate material at runtime. ## Validation - `cd codex-rs && cargo test -p codex-client --test ca_env posts_to_token_origin_through_tls_intercepting_proxy_with_custom_ca_bundle -- --nocapture` - `cd codex-rs && cargo test -p codex-client` - `cd codex-rs && cargo test -p codex-login` - `cd codex-rs && just fmt` - `cd codex-rs && just bazel-lock-update` - `cd codex-rs && just bazel-lock-check` - `cd codex-rs && just fix -p codex-client`
101 lines
3.5 KiB
Rust
101 lines
3.5 KiB
Rust
//! Helper binary for exercising shared custom CA environment handling in tests.
|
|
//!
|
|
//! The shared reqwest client honors `CODEX_CA_CERTIFICATE` and `SSL_CERT_FILE`, but those
|
|
//! environment variables are process-global and unsafe to mutate in parallel test execution. This
|
|
//! probe keeps the behavior under test while letting integration tests (`tests/ca_env.rs`) set
|
|
//! env vars per-process, proving:
|
|
//!
|
|
//! - env precedence is respected,
|
|
//! - multi-cert PEM bundles load,
|
|
//! - error messages guide users when CA files are invalid.
|
|
//! - optional HTTPS probes can complete a request through the constructed client.
|
|
//!
|
|
//! The detailed explanation of what "hermetic" means here lives in `codex_client::custom_ca`.
|
|
//! This binary exists so the tests can exercise
|
|
//! [`codex_client::build_reqwest_client_for_subprocess_tests`] in a separate process without
|
|
//! duplicating client-construction logic.
|
|
|
|
use std::env;
|
|
use std::process;
|
|
use std::time::Duration;
|
|
|
|
const PROBE_TLS13_ENV: &str = "CODEX_CUSTOM_CA_PROBE_TLS13";
|
|
const PROBE_PROXY_ENV: &str = "CODEX_CUSTOM_CA_PROBE_PROXY";
|
|
const PROBE_URL_ENV: &str = "CODEX_CUSTOM_CA_PROBE_URL";
|
|
|
|
fn main() {
|
|
let runtime = match tokio::runtime::Builder::new_current_thread()
|
|
.enable_all()
|
|
.build()
|
|
{
|
|
Ok(runtime) => runtime,
|
|
Err(error) => {
|
|
eprintln!("failed to create probe runtime: {error}");
|
|
process::exit(1);
|
|
}
|
|
};
|
|
|
|
match runtime.block_on(run_probe()) {
|
|
Ok(()) => println!("ok"),
|
|
Err(error) => {
|
|
eprintln!("{error}");
|
|
process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn run_probe() -> Result<(), String> {
|
|
let proxy_url = env::var(PROBE_PROXY_ENV).ok();
|
|
let target_url = env::var(PROBE_URL_ENV).ok();
|
|
let mut builder = reqwest::Client::builder();
|
|
if target_url.is_some() {
|
|
builder = builder.timeout(Duration::from_secs(5));
|
|
}
|
|
if env::var_os(PROBE_TLS13_ENV).is_some() {
|
|
builder = builder.min_tls_version(reqwest::tls::Version::TLS_1_3);
|
|
}
|
|
|
|
let client = build_probe_client(builder, proxy_url.as_deref())?;
|
|
if let Some(url) = target_url {
|
|
post_probe_request(&client, &url).await?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn build_probe_client(
|
|
builder: reqwest::ClientBuilder,
|
|
proxy_url: Option<&str>,
|
|
) -> Result<reqwest::Client, String> {
|
|
if let Some(proxy_url) = proxy_url {
|
|
let proxy = reqwest::Proxy::https(proxy_url)
|
|
.map_err(|error| format!("failed to configure probe proxy {proxy_url}: {error}"))?;
|
|
return codex_client::build_reqwest_client_with_custom_ca(builder.proxy(proxy))
|
|
.map_err(|error| error.to_string());
|
|
}
|
|
|
|
codex_client::build_reqwest_client_for_subprocess_tests(builder)
|
|
.map_err(|error| error.to_string())
|
|
}
|
|
|
|
async fn post_probe_request(client: &reqwest::Client, url: &str) -> Result<(), String> {
|
|
let response = client
|
|
.post(url)
|
|
.header("Content-Type", "application/x-www-form-urlencoded")
|
|
.body("grant_type=authorization_code&code=test")
|
|
.send()
|
|
.await
|
|
.map_err(|error| format!("probe request failed: {error:?}"))?;
|
|
let status = response.status();
|
|
let body = response
|
|
.text()
|
|
.await
|
|
.map_err(|error| format!("failed to read probe response body: {error}"))?;
|
|
if !status.is_success() {
|
|
return Err(format!("probe request returned {status}: {body}"));
|
|
}
|
|
if body != "ok" {
|
|
return Err(format!("probe response body mismatch: {body}"));
|
|
}
|
|
Ok(())
|
|
}
|