Skip to content

Ignore SSL_CERT_FILE with empty value#16772

Closed
taearls wants to merge 3 commits intoastral-sh:mainfrom
taearls:16712/ssl-cert-file-ignores-empty-value
Closed

Ignore SSL_CERT_FILE with empty value#16772
taearls wants to merge 3 commits intoastral-sh:mainfrom
taearls:16712/ssl-cert-file-ignores-empty-value

Conversation

@taearls
Copy link
Copy Markdown

@taearls taearls commented Nov 18, 2025

Summary

Fixes #16712.

This PR adds empty value filtering to SSL_CERT_FILE environment variable handling, matching the existing behavior of SSL_CERT_DIR.

Previously, setting SSL_CERT_FILE="" would cause uv to attempt to use an empty string as a file path, resulting in a "file does not exist" warning. With this change, an empty SSL_CERT_FILE is treated as unset, falling back to default certificate handling.

The fix adds .filter(|v| !v.is_empty()) to the SSL_CERT_FILE processing in crates/uv-client/src/base_client.rs, consistent with the pattern used for SSL_CERT_DIR and other environment variables like NO_COLOR and FORCE_COLOR.

Test Plan

Added a test case to crates/uv-client/tests/it/ssl_certs.rs that:

  1. Sets SSL_CERT_FILE to an empty string
  2. Verifies the connection fails with UnknownCA (indicating default certificate handling was used, not a path-related error)

Run the test with:

cargo test --package uv-client --test it -- ssl_env_vars --exact

@konstin konstin requested a review from samypr100 November 19, 2025 09:43
@konstin konstin added the bug Something isn't working label Nov 19, 2025
@konstin konstin requested a review from zanieb November 19, 2025 09:43
@konstin konstin changed the title fix: SSL_CERT_FILE ignores empty value Ignore SSL_CERT_FILE with empty value Nov 19, 2025
Comment on lines +160 to +216
// ** Set SSL_CERT_FILE to an empty string
// ** Then verify it is treated as unset (falls back to default behavior)

unsafe {
std::env::set_var(EnvVars::SSL_CERT_FILE, "");
}
let (server_task, addr) = start_https_user_agent_server(&standalone_server_cert).await?;
let url = DisplaySafeUrl::from_str(&format!("https://{addr}"))?;
let cache = Cache::temp()?.init()?;
let client = RegistryClientBuilder::new(BaseClientBuilder::default(), cache).build();
let res = client
.cached_client()
.uncached()
.for_host(&url)
.get(Url::from(url))
.send()
.await;
unsafe {
std::env::remove_var(EnvVars::SSL_CERT_FILE);
}

// Empty SSL_CERT_FILE should be ignored, falling back to default certificate handling.
// The connection will fail because we're using a self-signed cert without providing the cert,
// but the important thing is it doesn't fail due to "empty path does not exist".
let Some(reqwest_middleware::Error::Middleware(middleware_error)) = res.err() else {
panic!("expected middleware error");
};
let reqwest_error = middleware_error
.chain()
.find_map(|err| {
err.downcast_ref::<reqwest_middleware::Error>().map(|err| {
if let reqwest_middleware::Error::Reqwest(inner) = err {
inner
} else {
panic!("expected reqwest error")
}
})
})
.expect("expected reqwest error");
assert!(reqwest_error.is_connect());

// Validate the server error - should be UnknownCA (not a path-related error)
let server_res = server_task.await?;
let expected_err = if let Err(anyhow_err) = server_res
&& let Some(io_err) = anyhow_err.downcast_ref::<std::io::Error>()
&& let Some(wrapped_err) = io_err.get_ref()
&& let Some(tls_err) = wrapped_err.downcast_ref::<rustls::Error>()
&& matches!(
tls_err,
rustls::Error::AlertReceived(AlertDescription::UnknownCA)
) {
true
} else {
false
};
assert!(expected_err);

Copy link
Copy Markdown
Collaborator

@samypr100 samypr100 Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding a test!

In this case the test with SSL_CERT_FILE="" would behave the same way before and after the code changes given this is a server using a self-signed certificate, hence I don't think its actually verifying the changes.

Having said that, I'm not quite sure if we should have a test for this change. If we'd like to have a test I believe we can add one to the bottom of this file that specifically tests the warning scenarios for these env vars, e.g. something like

// SAFETY: This test is meant to run in isolation
#[tokio::test]
#[allow(unsafe_code)]
async fn ssl_env_vars_warnings() -> Result<()> {
    // Enable user-facing warnings
    uv_warnings::enable();

    unsafe {
        std::env::set_var(EnvVars::SSL_CERT_FILE, "");
    }
    let (server_task, addr) = start_http_user_agent_server().await?;
    let url = DisplaySafeUrl::from_str(&format!("http://{addr}"))?;
    let cache = Cache::temp()?.init()?;
    let client = RegistryClientBuilder::new(BaseClientBuilder::default(), cache).build();
    let _ = client
        .cached_client()
        .uncached()
        .for_host(&url)
        .get(Url::from(url))
        .send()
        .await;
    let _ = server_task.await?;
    unsafe {
        std::env::remove_var(EnvVars::SSL_CERT_FILE);
    }

    // Check no warning was emitted
    let warning = uv_warnings::WARNINGS
        .lock()
        .expect("Failed to retrieve warnings")
        .iter()
        .any(|msg| msg.starts_with("Ignoring invalid `SSL_CERT_FILE`."));
    assert!(!warning, "Unexpected warning emitted");

    unsafe {
        std::env::set_var(EnvVars::SSL_CERT_FILE, "foo");
    }
    let (server_task, addr) = start_http_user_agent_server().await?;
    let url = DisplaySafeUrl::from_str(&format!("http://{addr}"))?;
    let cache = Cache::temp()?.init()?;
    let client = RegistryClientBuilder::new(BaseClientBuilder::default(), cache).build();
    let _ = client
        .cached_client()
        .uncached()
        .for_host(&url)
        .get(Url::from(url))
        .send()
        .await;
    let _ = server_task.await?;
    unsafe {
        std::env::remove_var(EnvVars::SSL_CERT_FILE);
    }

    // Check warning was emitted
    let warning = uv_warnings::WARNINGS
        .lock()
        .expect("Failed to retrieve warnings")
        .iter()
        .any(|msg| msg.starts_with("Ignoring invalid `SSL_CERT_FILE`."));
    assert!(warning, "Expected warning to be emitted");

    // Disable user-facing warnings
    uv_warnings::disable();

    Ok(())
}

Note, this would require you to use nextest to run these tests as you'll need process isolation.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the feedback! I see what you're saying, good catch.

It seems like maybe a test isn't necessary for this, but I'd be happy to add the test you suggested if you'd like.

for now I'll remove the test case I added.

Copy link
Copy Markdown
Collaborator

@samypr100 samypr100 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good to me

@samypr100
Copy link
Copy Markdown
Collaborator

@zanieb I believe this one is good to go

@taearls
Copy link
Copy Markdown
Author

taearls commented Jan 16, 2026

@samypr100 hey! I just wanted to check in. Is there anything I can do to help move this forward?

@zanieb
Copy link
Copy Markdown
Member

zanieb commented Jan 16, 2026

Sorry! I lost track of this over the holidays. Can you rebase since this will conflict with #17503 ?

@taearls
Copy link
Copy Markdown
Author

taearls commented Jan 16, 2026

Sorry! I lost track of this over the holidays. Can you rebase since this will conflict with #17503 ?

No worries, totally understand! I'm happy to do it. I'm out of town this weekend, so it will be a few days before I rebase.

@taearls taearls force-pushed the 16712/ssl-cert-file-ignores-empty-value branch from 8c4ae03 to bec7d89 Compare January 22, 2026 18:51
@taearls
Copy link
Copy Markdown
Author

taearls commented Jan 22, 2026

@samypr100 @zanieb I rebased, and I realized that this PR might not be needed after all? I think the existing code (which I think was added after I originally opened this PR) addresses this issue.

this is the snippet I'm referring to:

let ssl_cert_file_exists = env::var_os(EnvVars::SSL_CERT_FILE).is_some_and(|path| {

please let me know if I'm missing anything, happy to iterate on this further. but if I'm not mistaken, this issue may be resolved by existing code so this PR (and the associated issue) can be closed.

@samypr100
Copy link
Copy Markdown
Collaborator

@taearls I believe its still ok to add the .filter(|v| !v.is_empty())

@taearls
Copy link
Copy Markdown
Author

taearls commented Jan 25, 2026

@taearls I believe its still ok to add the .filter(|v| !v.is_empty())

done!

@samypr100
Copy link
Copy Markdown
Collaborator

Addressed by #18550

@samypr100 samypr100 closed this Mar 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SSL_CERT_FILE ignores empty value

4 participants