From 9edc0b393a281812e91c5c355c7be0663897d772 Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Wed, 6 Dec 2023 01:39:13 +0000
Subject: [PATCH] feat(tls): add crate feature for rustls native root certs
 (#506)

---
 .cargo/config.toml                   | 18 ++++++++--------
 actix-tls/CHANGES.md                 |  3 ++-
 actix-tls/Cargo.toml                 | 11 ++++++++--
 actix-tls/src/connect/mod.rs         | 15 ++++++++++---
 actix-tls/src/connect/rustls_0_20.rs | 32 +++++++++++++++++++++++-----
 actix-tls/src/connect/rustls_0_21.rs | 32 +++++++++++++++++++++++-----
 justfile                             |  4 ++--
 7 files changed, 88 insertions(+), 27 deletions(-)

diff --git a/.cargo/config.toml b/.cargo/config.toml
index a114083f..bade4d02 100644
--- a/.cargo/config.toml
+++ b/.cargo/config.toml
@@ -6,20 +6,20 @@ ci-doctest = "test --workspace --all-features --doc --no-fail-fast -- --nocaptur
 
 # just check the library (without dev deps)
 ci-check-min = "hack --workspace check --no-default-features"
-ci-check-lib = "hack --workspace --feature-powerset --exclude-features=io-uring check"
-ci-check-lib-linux = "hack --workspace --feature-powerset check"
+ci-check-lib = "hack --workspace --feature-powerset --depth=3 --exclude-features=io-uring check"
+ci-check-lib-linux = "hack --workspace --feature-powerset --depth=3 check"
 
 # check everything
-ci-check = "hack --workspace --feature-powerset --exclude-features=io-uring check --tests --examples"
-ci-check-linux = "hack --workspace --feature-powerset check --tests --examples"
+ci-check = "hack --workspace --feature-powerset --depth=3 --exclude-features=io-uring check --tests --examples"
+ci-check-linux = "hack --workspace --feature-powerset --depth=3 check --tests --examples"
 
 # tests avoiding io-uring feature
-ci-test = "hack --feature-powerset --exclude-features=io-uring test --lib --tests --no-fail-fast -- --nocapture"
-ci-test-rustls-020 = "hack --feature-powerset --exclude-features=io-uring,rustls-0_21 test --lib --tests --no-fail-fast -- --nocapture"
-ci-test-rustls-021 = "hack --feature-powerset --exclude-features=io-uring,rustls-0_20 test --lib --tests --no-fail-fast -- --nocapture"
+ci-test = "hack --feature-powerset --depth=3 --exclude-features=io-uring test --lib --tests --no-fail-fast -- --nocapture"
+ci-test-rustls-020 = "hack --feature-powerset --depth=3 --exclude-features=io-uring,rustls-0_21 test --lib --tests --no-fail-fast -- --nocapture"
+ci-test-rustls-021 = "hack --feature-powerset --depth=3 --exclude-features=io-uring,rustls-0_20 test --lib --tests --no-fail-fast -- --nocapture"
 
 # tests avoiding io-uring feature on Windows
-ci-test-win = "hack --feature-powerset --depth=2 --exclude-features=io-uring test --lib --tests --no-fail-fast -- --nocapture"
+ci-test-win = "hack --feature-powerset --depth=3 --exclude-features=io-uring test --lib --tests --no-fail-fast -- --nocapture"
 
 # test with io-uring feature
-ci-test-linux = "hack --feature-powerset --exclude-features=rustls-0_20 test --lib --tests --no-fail-fast -- --nocapture"
+ci-test-linux = "hack --feature-powerset --depth=3 --exclude-features=rustls-0_20 test --lib --tests --no-fail-fast -- --nocapture"
diff --git a/actix-tls/CHANGES.md b/actix-tls/CHANGES.md
index 25e8e56e..aa0d668f 100644
--- a/actix-tls/CHANGES.md
+++ b/actix-tls/CHANGES.md
@@ -2,7 +2,8 @@
 
 ## Unreleased
 
-- Added support to `http` crate version `1.0`.
+- Add `rustls-0_21-native-roots` and `rustls-0_20-native-roots` crate features which utilize the `rustls-native-certs` crate to enable a `native_roots_cert_store()` functions in each rustls-based `connect` module.
+- Implement `Host` for `http::Uri` (`http` crate version `1`).
 
 ## 3.1.1
 
diff --git a/actix-tls/Cargo.toml b/actix-tls/Cargo.toml
index 3b8d3ae8..43235227 100755
--- a/actix-tls/Cargo.toml
+++ b/actix-tls/Cargo.toml
@@ -47,10 +47,14 @@ openssl = ["tls-openssl", "tokio-openssl"]
 rustls = ["rustls-0_20"]
 
 # use rustls v0.20 impls
-rustls-0_20 = ["tokio-rustls-023", "webpki-roots-022"]
+rustls-0_20 = ["rustls-0_20-webpki-roots"]
+rustls-0_20-webpki-roots = ["tokio-rustls-023", "webpki-roots-022"]
+rustls-0_20-native-roots = ["tokio-rustls-023", "dep:rustls-native-certs"]
 
 # use rustls v0.21 impls
-rustls-0_21 = ["tokio-rustls-024", "webpki-roots-025"]
+rustls-0_21 = ["rustls-0_21-webpki-roots"]
+rustls-0_21-webpki-roots = ["tokio-rustls-024", "webpki-roots-025"]
+rustls-0_21-native-roots = ["tokio-rustls-024", "dep:rustls-native-certs"]
 
 # use native-tls impls
 native-tls = ["tokio-native-tls"]
@@ -88,6 +92,9 @@ rustls-webpki-0101 = { package = "rustls-webpki", version = "0.101.4" }
 tokio-rustls-024 = { package = "tokio-rustls", version = "0.24", optional = true }
 webpki-roots-025 = { package = "webpki-roots", version = "0.25", optional = true }
 
+# native root certificates for both rustls impls
+rustls-native-certs = { version = "0.6", optional = true }
+
 # native-tls
 tokio-native-tls = { version = "0.3", optional = true }
 
diff --git a/actix-tls/src/connect/mod.rs b/actix-tls/src/connect/mod.rs
index 79cbb295..2e069c02 100644
--- a/actix-tls/src/connect/mod.rs
+++ b/actix-tls/src/connect/mod.rs
@@ -27,14 +27,23 @@ mod uri;
 #[cfg(feature = "openssl")]
 pub mod openssl;
 
-#[cfg(feature = "rustls-0_20")]
+#[cfg(any(
+    feature = "rustls-0_20-webpki-roots",
+    feature = "rustls-0_20-native-roots",
+))]
 pub mod rustls_0_20;
 
 #[doc(hidden)]
-#[cfg(feature = "rustls-0_20")]
+#[cfg(any(
+    feature = "rustls-0_20-webpki-roots",
+    feature = "rustls-0_20-native-roots",
+))]
 pub use rustls_0_20 as rustls;
 
-#[cfg(feature = "rustls-0_21")]
+#[cfg(any(
+    feature = "rustls-0_21-webpki-roots",
+    feature = "rustls-0_21-native-roots",
+))]
 pub mod rustls_0_21;
 
 #[cfg(feature = "native-tls")]
diff --git a/actix-tls/src/connect/rustls_0_20.rs b/actix-tls/src/connect/rustls_0_20.rs
index 4547854e..52e73028 100644
--- a/actix-tls/src/connect/rustls_0_20.rs
+++ b/actix-tls/src/connect/rustls_0_20.rs
@@ -17,7 +17,7 @@ use actix_utils::future::{ok, Ready};
 use futures_core::ready;
 use tokio_rustls::{
     client::TlsStream as AsyncTlsStream,
-    rustls::{client::ServerName, ClientConfig, OwnedTrustAnchor, RootCertStore},
+    rustls::{client::ServerName, ClientConfig, RootCertStore},
     Connect as RustlsConnect, TlsConnector as RustlsTlsConnector,
 };
 use tokio_rustls_023 as tokio_rustls;
@@ -25,17 +25,38 @@ use tokio_rustls_023 as tokio_rustls;
 use crate::connect::{Connection, Host};
 
 pub mod reexports {
-    //! Re-exports from `rustls` and `webpki_roots` that are useful for connectors.
+    //! Re-exports from the `rustls` v0.20 ecosystem that are useful for connectors.
 
     pub use tokio_rustls_023::{client::TlsStream as AsyncTlsStream, rustls::ClientConfig};
+    #[cfg(feature = "rustls-0_20-webpki-roots")]
     pub use webpki_roots_022::TLS_SERVER_ROOTS;
 }
 
-/// Returns standard root certificates from `webpki-roots` crate as a rustls certificate store.
-pub fn webpki_roots_cert_store() -> RootCertStore {
+/// Returns root certificates via `rustls-native-certs` crate as a rustls certificate store.
+///
+/// See [`rustls_native_certs::load_native_certs()`] for more info on behavior and errors.
+#[cfg(feature = "rustls-0_20-native-roots")]
+pub fn native_roots_cert_store() -> io::Result<RootCertStore> {
     let mut root_certs = RootCertStore::empty();
+
+    for cert in rustls_native_certs::load_native_certs()? {
+        root_certs
+            .add(&tokio_rustls_023::rustls::Certificate(cert.0))
+            .unwrap();
+    }
+
+    Ok(root_certs)
+}
+
+/// Returns standard root certificates from `webpki-roots` crate as a rustls certificate store.
+#[cfg(feature = "rustls-0_20-webpki-roots")]
+pub fn webpki_roots_cert_store() -> RootCertStore {
+    use tokio_rustls_023::rustls;
+
+    let mut root_certs = RootCertStore::empty();
+
     for cert in webpki_roots_022::TLS_SERVER_ROOTS.0 {
-        let cert = OwnedTrustAnchor::from_subject_spki_name_constraints(
+        let cert = rustls::OwnedTrustAnchor::from_subject_spki_name_constraints(
             cert.subject,
             cert.spki,
             cert.name_constraints,
@@ -43,6 +64,7 @@ pub fn webpki_roots_cert_store() -> RootCertStore {
         let certs = vec![cert].into_iter();
         root_certs.add_server_trust_anchors(certs);
     }
+
     root_certs
 }
 
diff --git a/actix-tls/src/connect/rustls_0_21.rs b/actix-tls/src/connect/rustls_0_21.rs
index cc0d6de3..7c3ab24b 100644
--- a/actix-tls/src/connect/rustls_0_21.rs
+++ b/actix-tls/src/connect/rustls_0_21.rs
@@ -17,7 +17,7 @@ use actix_utils::future::{ok, Ready};
 use futures_core::ready;
 use tokio_rustls::{
     client::TlsStream as AsyncTlsStream,
-    rustls::{client::ServerName, ClientConfig, OwnedTrustAnchor, RootCertStore},
+    rustls::{client::ServerName, ClientConfig, RootCertStore},
     Connect as RustlsConnect, TlsConnector as RustlsTlsConnector,
 };
 use tokio_rustls_024 as tokio_rustls;
@@ -25,17 +25,38 @@ use tokio_rustls_024 as tokio_rustls;
 use crate::connect::{Connection, Host};
 
 pub mod reexports {
-    //! Re-exports from `rustls` and `webpki_roots` that are useful for connectors.
+    //! Re-exports from the `rustls` v0.21 ecosystem that are useful for connectors.
 
     pub use tokio_rustls_024::{client::TlsStream as AsyncTlsStream, rustls::ClientConfig};
+    #[cfg(feature = "rustls-0_21-webpki-roots")]
     pub use webpki_roots_025::TLS_SERVER_ROOTS;
 }
 
-/// Returns standard root certificates from `webpki-roots` crate as a rustls certificate store.
-pub fn webpki_roots_cert_store() -> RootCertStore {
+/// Returns root certificates via `rustls-native-certs` crate as a rustls certificate store.
+///
+/// See [`rustls_native_certs::load_native_certs()`] for more info on behavior and errors.
+#[cfg(feature = "rustls-0_21-native-roots")]
+pub fn native_roots_cert_store() -> io::Result<RootCertStore> {
     let mut root_certs = RootCertStore::empty();
+
+    for cert in rustls_native_certs::load_native_certs()? {
+        root_certs
+            .add(&tokio_rustls_024::rustls::Certificate(cert.0))
+            .unwrap();
+    }
+
+    Ok(root_certs)
+}
+
+/// Returns standard root certificates from `webpki-roots` crate as a rustls certificate store.
+#[cfg(feature = "rustls-0_21-webpki-roots")]
+pub fn webpki_roots_cert_store() -> RootCertStore {
+    use tokio_rustls_024::rustls;
+
+    let mut root_certs = RootCertStore::empty();
+
     for cert in webpki_roots_025::TLS_SERVER_ROOTS {
-        let cert = OwnedTrustAnchor::from_subject_spki_name_constraints(
+        let cert = rustls::OwnedTrustAnchor::from_subject_spki_name_constraints(
             cert.subject,
             cert.spki,
             cert.name_constraints,
@@ -43,6 +64,7 @@ pub fn webpki_roots_cert_store() -> RootCertStore {
         let certs = vec![cert].into_iter();
         root_certs.add_trust_anchors(certs);
     }
+
     root_certs
 }
 
diff --git a/justfile b/justfile
index 72331f9e..86bd8412 100644
--- a/justfile
+++ b/justfile
@@ -7,8 +7,8 @@ doc:
 
 # Document crates in workspace and watch for changes.
 doc-watch:
-    RUSTDOCFLAGS="--cfg=docsrs" cargo +nightly doc --no-deps --workspace --features=rustls,openssl --open
-    cargo watch -- RUSTDOCFLAGS="--cfg=docsrs" cargo +nightly doc --no-deps --workspace --features=rustls,openssl
+    RUSTDOCFLAGS="--cfg=docsrs"                cargo +nightly doc --no-deps --workspace --features=rustls-0_20,rustls-0_21,rustls-0_20-native-roots,rustls-0_21-native-roots,openssl --open
+    cargo watch -- RUSTDOCFLAGS="--cfg=docsrs" cargo +nightly doc --no-deps --workspace --features=rustls-0_20,rustls-0_21,rustls-0_20-native-roots,rustls-0_21-native-roots,openssl
 
 # Check for unintentional external type exposure on all crates in workspace.
 check-external-types-all toolchain="+nightly":