Repository: seanmonstar/reqwest (language: rust)

Issue:
Only use `webpki` certs despite enabled `rustls-tls-native-roots` feature
I would like to have more fine-grained control over the root certificates added regardless of enabled crate features. [`ClientBuilder::tls_built_in_root_certs()`](https://docs.rs/reqwest/0.11.18/reqwest/struct.ClientBuilder.html#method.tls_built_in_root_certs) (added in https://github.com/seanmonstar/reqwest/pull/1150) only allows to disable/enable all of them.

One solution would be to disable all of them and add any desired ones yourself. This didn't turn out to be ideal because `webpki` offers already parsed certificates, but [`ClientBuilder::add_root_certificate()`](https://docs.rs/reqwest/0.11.18/reqwest/struct.ClientBuilder.html#method.add_root_certificate) takes a [`Certificate`](https://docs.rs/reqwest/0.11.18/reqwest/tls/struct.Certificate.html), which only takes DER or PEM encoded certificates.

**Suggested Solutions**
1. Add a new method, `Certificate::from_rustls()`, to [`Certificate`](https://docs.rs/reqwest/0.11.18/reqwest/tls/struct.Certificate.html), which takes a [`OwnedTrustAnchor`](https://docs.rs/rustls/0.21.1/rustls/struct.OwnedTrustAnchor.html). This would make it easy to add arbitrary root certificates to `rustls` without having to serialize them to DER first, only to have them deserialized by `rustls` right after.
2. Add new methods to [`ClientBuilder`](https://docs.rs/reqwest/0.11.18/reqwest/struct.ClientBuilder.html) allowing for more fine-grained control over which built-in certificates are added. E.g. `ClientBuilder::tls_webpki_root_certs()` and `ClientBuilder::tls_native_root_certs()`. Potentially removing [`ClientBuilder::tls_built_in_root_certs()`](https://docs.rs/reqwest/0.11.18/reqwest/struct.ClientBuilder.html#method.tls_built_in_root_certs) completely in the next version.

I actually find both solutions could work quite well simultaneously.


Discussion / hints:
We could do something like `no_gzip()`, `no_proxy()`, etc, where even if a feature is enabled, that `ClientBuilder` will not use them.
Will make a PR shortly.
We also had this need in uv and ended up doing the configuration out of reqwest: https://github.com/astral-sh/uv/pull/2362. It would be great if the builder API supported this!

Fix patch:
diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs
index 079bd259d..0d2361a92 100644
--- a/src/async_impl/client.rs
+++ b/src/async_impl/client.rs
@@ -113,6 +113,10 @@ struct Config {
     root_certs: Vec<Certificate>,
     #[cfg(feature = "__tls")]
     tls_built_in_root_certs: bool,
+    #[cfg(feature = "rustls-tls-webpki-roots")]
+    tls_built_in_certs_webpki: bool,
+    #[cfg(feature = "rustls-tls-native-roots")]
+    tls_built_in_certs_native: bool,
     #[cfg(feature = "__tls")]
     min_tls_version: Option<tls::Version>,
     #[cfg(feature = "__tls")]
@@ -205,6 +209,10 @@ impl ClientBuilder {
                 root_certs: Vec::new(),
                 #[cfg(feature = "__tls")]
                 tls_built_in_root_certs: true,
+                #[cfg(feature = "rustls-tls-webpki-roots")]
+                tls_built_in_certs_webpki: true,
+                #[cfg(feature = "rustls-tls-native-roots")]
+                tls_built_in_certs_native: true,
                 #[cfg(any(feature = "native-tls", feature = "__rustls"))]
                 identity: None,
                 #[cfg(feature = "__tls")]
@@ -499,12 +507,12 @@ impl ClientBuilder {
                     }
 
                     #[cfg(feature = "rustls-tls-webpki-roots")]
-                    if config.tls_built_in_root_certs {
+                    if config.tls_built_in_certs_webpki {
                         root_cert_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
                     }
 
                     #[cfg(feature = "rustls-tls-native-roots")]
-                    if config.tls_built_in_root_certs {
+                    if config.tls_built_in_certs_native {
                         let mut valid_count = 0;
                         let mut invalid_count = 0;
                         for cert in rustls_native_certs::load_native_certs()
@@ -1333,6 +1341,15 @@ impl ClientBuilder {
     ///
     /// Defaults to `true` -- built-in system certs will be used.
     ///
+    /// # Bulk Option
+    ///
+    /// If this value is `true`, _all_ enabled system certs configured with Cargo
+    /// features will be loaded.
+    ///
+    /// You can set this to `false`, and enable only a specific source with
+    /// individual methods. Do that will prevent other sources from being loaded
+    /// even if their feature Cargo feature is enabled.
+    ///
     /// # Optional
     ///
     /// This requires the optional `default-tls`, `native-tls`, or `rustls-tls(-...)`
@@ -1348,6 +1365,37 @@ impl ClientBuilder {
     )]
     pub fn tls_built_in_root_certs(mut self, tls_built_in_root_certs: bool) -> ClientBuilder {
         self.config.tls_built_in_root_certs = tls_built_in_root_certs;
+
+        #[cfg(feature = "rustls-tls-webpki-roots")]
+        {
+            self.config.tls_built_in_certs_webpki = tls_built_in_root_certs;
+        }
+
+        #[cfg(feature = "rustls-tls-native-roots")]
+        {
+            self.config.tls_built_in_certs_native = tls_built_in_root_certs;
+        }
+
+        self
+    }
+
+    /// Sets whether to load webpki root certs with rustls.
+    ///
+    /// If the feature is enabled, this value is `true` by default.
+    #[cfg(feature = "rustls-tls-webpki-roots")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls-webpki-roots")))]
+    pub fn tls_built_in_webpki_certs(mut self, enabled: bool) -> ClientBuilder {
+        self.config.tls_built_in_certs_webpki = enabled;
+        self
+    }
+
+    /// Sets whether to load native root certs with rustls.
+    ///
+    /// If the feature is enabled, this value is `true` by default.
+    #[cfg(feature = "rustls-tls-native-roots")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls-native-roots")))]
+    pub fn tls_built_in_native_certs(mut self, enabled: bool) -> ClientBuilder {
+        self.config.tls_built_in_certs_native = enabled;
         self
     }
 
diff --git a/src/blocking/client.rs b/src/blocking/client.rs
index 5b861cb3e..d0825b42e 100644
--- a/src/blocking/client.rs
+++ b/src/blocking/client.rs
@@ -615,6 +615,24 @@ impl ClientBuilder {
         self.with_inner(move |inner| inner.tls_built_in_root_certs(tls_built_in_root_certs))
     }
 
+    /// Sets whether to load webpki root certs with rustls.
+    ///
+    /// If the feature is enabled, this value is `true` by default.
+    #[cfg(feature = "rustls-tls-webpki-roots")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls-webpki-roots")))]
+    pub fn tls_built_in_webpki_certs(mut self, enabled: bool) -> ClientBuilder {
+        self.with_inner(move |inner| inner.tls_built_in_webpki_certs(enabled))
+    }
+
+    /// Sets whether to load native root certs with rustls.
+    ///
+    /// If the feature is enabled, this value is `true` by default.
+    #[cfg(feature = "rustls-tls-native-roots")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls-native-roots")))]
+    pub fn tls_built_in_native_certs(mut self, enabled: bool) -> ClientBuilder {
+        self.with_inner(move |inner| inner.tls_built_in_native_certs(enabled))
+    }
+
     /// Sets the identity to be used for client certificate authentication.
     ///
     /// # Optional


Test patch: