From a0841602dbbc61ef39a057372415d56254b09ec6 Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Tue, 5 Dec 2023 21:06:44 -0500
Subject: [PATCH] feat: add rustls v0.22 support

---
 .cargo/config.toml                   |  18 +--
 actix-tls/CHANGES.md                 |   2 +
 actix-tls/Cargo.toml                 |  28 +++--
 actix-tls/src/connect/mod.rs         |   6 +
 actix-tls/src/connect/rustls_0_20.rs |   2 +-
 actix-tls/src/connect/rustls_0_21.rs |   2 +-
 actix-tls/src/connect/rustls_0_22.rs | 162 +++++++++++++++++++++++++++
 actix-tls/tests/accept-openssl.rs    |  61 ++++++----
 actix-tls/tests/accept-rustls.rs     |   8 +-
 actix-tls/tests/test_connect.rs      |   8 +-
 10 files changed, 248 insertions(+), 49 deletions(-)
 create mode 100644 actix-tls/src/connect/rustls_0_22.rs

diff --git a/.cargo/config.toml b/.cargo/config.toml
index bade4d02..8f1ff8a7 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 --depth=3 --exclude-features=io-uring check"
-ci-check-lib-linux = "hack --workspace --feature-powerset --depth=3 check"
+ci-check-lib = "hack --workspace --feature-powerset --depth=2 --exclude-features=io-uring check"
+ci-check-lib-linux = "hack --workspace --feature-powerset --depth=2 check"
 
 # check everything
-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"
+ci-check = "hack --workspace --feature-powerset --depth=2 --exclude-features=io-uring check --tests --examples"
+ci-check-linux = "hack --workspace --feature-powerset --depth=2 check --tests --examples"
 
 # tests avoiding io-uring feature
-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"
+ci-test = "hack --feature-powerset --depth=2 --exclude-features=io-uring test --lib --tests --no-fail-fast -- --nocapture"
+ci-test-rustls-020 = "hack --feature-powerset --depth=2 --exclude-features=io-uring,rustls-0_21 test --lib --tests --no-fail-fast -- --nocapture"
+ci-test-rustls-021 = "hack --feature-powerset --depth=2 --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=3 --exclude-features=io-uring test --lib --tests --no-fail-fast -- --nocapture"
+ci-test-win = "hack --feature-powerset --depth=2 --exclude-features=io-uring test --lib --tests --no-fail-fast -- --nocapture"
 
 # test with io-uring feature
-ci-test-linux = "hack --feature-powerset --depth=3 --exclude-features=rustls-0_20 test --lib --tests --no-fail-fast -- --nocapture"
+ci-test-linux = "hack --feature-powerset --depth=2 --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 aa0d668f..2c49bebc 100644
--- a/actix-tls/CHANGES.md
+++ b/actix-tls/CHANGES.md
@@ -2,6 +2,8 @@
 
 ## Unreleased
 
+- Support Rustls v0.22.
+- Added `{accept, connect}::rustls_0_22` modules.
 - 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`).
 
diff --git a/actix-tls/Cargo.toml b/actix-tls/Cargo.toml
index 43235227..c3ed89b4 100755
--- a/actix-tls/Cargo.toml
+++ b/actix-tls/Cargo.toml
@@ -49,12 +49,16 @@ rustls = ["rustls-0_20"]
 # use rustls v0.20 impls
 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"]
+rustls-0_20-native-roots = ["tokio-rustls-023", "dep:rustls-native-certs-06"]
 
 # use rustls v0.21 impls
 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"]
+rustls-0_21-native-roots = ["tokio-rustls-024", "dep:rustls-native-certs-06"]
+
+# use rustls v0.22 impls
+rustls-0_22-webpki-roots = ["dep:tokio-rustls-025", "dep:rustls-pki-types-1", "dep:webpki-roots-026"]
+rustls-0_22-native-roots = ["dep:tokio-rustls-025", "dep:rustls-pki-types-1", "dep:rustls-native-certs-07"]
 
 # use native-tls impls
 native-tls = ["tokio-native-tls"]
@@ -87,13 +91,19 @@ tokio-rustls-023 = { package = "tokio-rustls", version = "0.23", optional = true
 webpki-roots-022 = { package = "webpki-roots", version = "0.22", optional = true }
 
 # rustls v0.21
-rustls-021 = { package = "rustls", version = "0.21.6" }
-rustls-webpki-0101 = { package = "rustls-webpki", version = "0.101.4" }
+rustls-021 = { package = "rustls", version = "0.21.6", optional = true }
+rustls-webpki-0101 = { package = "rustls-webpki", version = "0.101.4", optional = true }
 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 }
+# rustls v0.22
+rustls-pki-types-1 = { package = "rustls-pki-types", version = "1", optional = true }
+tokio-rustls-025 = { package = "tokio-rustls", version = "0.25", optional = true }
+webpki-roots-026 = { package = "webpki-roots", version = "0.26", optional = true }
+
+# native root certificates for rustls impls
+rustls-native-certs-06 = { package = "rustls-native-certs", version = "0.6", optional = true }
+rustls-native-certs-07 = { package = "rustls-native-certs", version = "0.7", optional = true }
 
 # native-tls
 tokio-native-tls = { version = "0.3", optional = true }
@@ -106,10 +116,10 @@ bytes = "1"
 env_logger = "0.10"
 futures-util = { version = "0.3.17", default-features = false, features = ["sink"] }
 rcgen = "0.11"
-rustls-pemfile = "1"
-tokio-rustls-024 = { package = "tokio-rustls", version = "0.24", features = ["dangerous_configuration"] }
+rustls-pemfile = "2"
+tokio-rustls-025 = { package = "tokio-rustls", version = "0.25" }
 trust-dns-resolver = "0.23"
 
 [[example]]
 name = "accept-rustls"
-required-features = ["accept", "rustls-0_21"]
+required-features = ["accept", "rustls-0_22"]
diff --git a/actix-tls/src/connect/mod.rs b/actix-tls/src/connect/mod.rs
index 2e069c02..b742e76e 100644
--- a/actix-tls/src/connect/mod.rs
+++ b/actix-tls/src/connect/mod.rs
@@ -46,6 +46,12 @@ pub use rustls_0_20 as rustls;
 ))]
 pub mod rustls_0_21;
 
+#[cfg(any(
+    feature = "rustls-0_22-webpki-roots",
+    feature = "rustls-0_22-native-roots",
+))]
+pub mod rustls_0_22;
+
 #[cfg(feature = "native-tls")]
 pub mod native_tls;
 
diff --git a/actix-tls/src/connect/rustls_0_20.rs b/actix-tls/src/connect/rustls_0_20.rs
index 52e73028..19553044 100644
--- a/actix-tls/src/connect/rustls_0_20.rs
+++ b/actix-tls/src/connect/rustls_0_20.rs
@@ -39,7 +39,7 @@ pub mod reexports {
 pub fn native_roots_cert_store() -> io::Result<RootCertStore> {
     let mut root_certs = RootCertStore::empty();
 
-    for cert in rustls_native_certs::load_native_certs()? {
+    for cert in rustls_native_certs_06::load_native_certs()? {
         root_certs
             .add(&tokio_rustls_023::rustls::Certificate(cert.0))
             .unwrap();
diff --git a/actix-tls/src/connect/rustls_0_21.rs b/actix-tls/src/connect/rustls_0_21.rs
index 7c3ab24b..7474327a 100644
--- a/actix-tls/src/connect/rustls_0_21.rs
+++ b/actix-tls/src/connect/rustls_0_21.rs
@@ -39,7 +39,7 @@ pub mod reexports {
 pub fn native_roots_cert_store() -> io::Result<RootCertStore> {
     let mut root_certs = RootCertStore::empty();
 
-    for cert in rustls_native_certs::load_native_certs()? {
+    for cert in rustls_native_certs_06::load_native_certs()? {
         root_certs
             .add(&tokio_rustls_024::rustls::Certificate(cert.0))
             .unwrap();
diff --git a/actix-tls/src/connect/rustls_0_22.rs b/actix-tls/src/connect/rustls_0_22.rs
new file mode 100644
index 00000000..bb6f5f83
--- /dev/null
+++ b/actix-tls/src/connect/rustls_0_22.rs
@@ -0,0 +1,162 @@
+//! Rustls based connector service.
+//!
+//! See [`TlsConnector`] for main connector service factory docs.
+
+use std::{
+    future::Future,
+    io,
+    pin::Pin,
+    sync::Arc,
+    task::{Context, Poll},
+};
+
+use actix_rt::net::ActixStream;
+use actix_service::{Service, ServiceFactory};
+use actix_utils::future::{ok, Ready};
+use futures_core::ready;
+use rustls_pki_types_1::ServerName;
+use tokio_rustls::{
+    client::TlsStream as AsyncTlsStream,
+    rustls::{ClientConfig, RootCertStore},
+    Connect as RustlsConnect, TlsConnector as RustlsTlsConnector,
+};
+use tokio_rustls_025 as tokio_rustls;
+
+use crate::connect::{Connection, Host};
+
+pub mod reexports {
+    //! Re-exports from the `rustls` v0.22 ecosystem that are useful for connectors.
+
+    pub use tokio_rustls_025::{client::TlsStream as AsyncTlsStream, rustls::ClientConfig};
+    #[cfg(feature = "rustls-0_22-webpki-roots")]
+    pub use webpki_roots_026::TLS_SERVER_ROOTS;
+}
+
+/// 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_22-native-roots")]
+pub fn native_roots_cert_store() -> io::Result<RootCertStore> {
+    let mut root_certs = RootCertStore::empty();
+
+    for cert in rustls_native_certs_07::load_native_certs()? {
+        root_certs.add(cert).unwrap();
+    }
+
+    Ok(root_certs)
+}
+
+/// Returns standard root certificates from `webpki-roots` crate as a rustls certificate store.
+#[cfg(feature = "rustls-0_22-webpki-roots")]
+pub fn webpki_roots_cert_store() -> RootCertStore {
+    let mut root_certs = RootCertStore::empty();
+    root_certs.extend(webpki_roots_026::TLS_SERVER_ROOTS.to_owned());
+    root_certs
+}
+
+/// Connector service factory using `rustls`.
+#[derive(Clone)]
+pub struct TlsConnector {
+    connector: Arc<ClientConfig>,
+}
+
+impl TlsConnector {
+    /// Constructs new connector service factory from a `rustls` client configuration.
+    pub fn new(connector: Arc<ClientConfig>) -> Self {
+        TlsConnector { connector }
+    }
+
+    /// Constructs new connector service from a `rustls` client configuration.
+    pub fn service(connector: Arc<ClientConfig>) -> TlsConnectorService {
+        TlsConnectorService { connector }
+    }
+}
+
+impl<R, IO> ServiceFactory<Connection<R, IO>> for TlsConnector
+where
+    R: Host,
+    IO: ActixStream + 'static,
+{
+    type Response = Connection<R, AsyncTlsStream<IO>>;
+    type Error = io::Error;
+    type Config = ();
+    type Service = TlsConnectorService;
+    type InitError = ();
+    type Future = Ready<Result<Self::Service, Self::InitError>>;
+
+    fn new_service(&self, _: ()) -> Self::Future {
+        ok(TlsConnectorService {
+            connector: self.connector.clone(),
+        })
+    }
+}
+
+/// Connector service using `rustls`.
+#[derive(Clone)]
+pub struct TlsConnectorService {
+    connector: Arc<ClientConfig>,
+}
+
+impl<R, IO> Service<Connection<R, IO>> for TlsConnectorService
+where
+    R: Host,
+    IO: ActixStream,
+{
+    type Response = Connection<R, AsyncTlsStream<IO>>;
+    type Error = io::Error;
+    type Future = ConnectFut<R, IO>;
+
+    actix_service::always_ready!();
+
+    fn call(&self, connection: Connection<R, IO>) -> Self::Future {
+        tracing::trace!("TLS handshake start for: {:?}", connection.hostname());
+        let (stream, conn) = connection.replace_io(());
+
+        match ServerName::try_from(conn.hostname()) {
+            Ok(host) => ConnectFut::Future {
+                connect: RustlsTlsConnector::from(Arc::clone(&self.connector))
+                    .connect(host.to_owned(), stream),
+                connection: Some(conn),
+            },
+            Err(_) => ConnectFut::InvalidServerName,
+        }
+    }
+}
+
+/// Connect future for Rustls service.
+#[doc(hidden)]
+#[allow(clippy::large_enum_variant)]
+pub enum ConnectFut<R, IO> {
+    InvalidServerName,
+    Future {
+        connect: RustlsConnect<IO>,
+        connection: Option<Connection<R, ()>>,
+    },
+}
+
+impl<R, IO> Future for ConnectFut<R, IO>
+where
+    R: Host,
+    IO: ActixStream,
+{
+    type Output = io::Result<Connection<R, AsyncTlsStream<IO>>>;
+
+    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+        match self.get_mut() {
+            Self::InvalidServerName => Poll::Ready(Err(io::Error::new(
+                io::ErrorKind::InvalidInput,
+                "connection parameters specified invalid server name",
+            ))),
+
+            Self::Future {
+                connect,
+                connection,
+            } => {
+                let stream = ready!(Pin::new(connect).poll(cx))?;
+                let connection = connection.take().unwrap();
+                tracing::trace!("TLS handshake success: {:?}", connection.hostname());
+                Poll::Ready(Ok(connection.replace_io(stream).1))
+            }
+        }
+    }
+}
diff --git a/actix-tls/tests/accept-openssl.rs b/actix-tls/tests/accept-openssl.rs
index ca57e17d..a3506b1b 100644
--- a/actix-tls/tests/accept-openssl.rs
+++ b/actix-tls/tests/accept-openssl.rs
@@ -3,19 +3,20 @@
 #![cfg(all(
     feature = "accept",
     feature = "connect",
-    feature = "rustls-0_21",
+    feature = "rustls-0_22",
     feature = "openssl"
 ))]
 
-use std::{convert::TryFrom, io::Write, sync::Arc};
+use std::{io::Write as _, sync::Arc};
 
 use actix_rt::net::TcpStream;
 use actix_server::TestServer;
 use actix_service::ServiceFactoryExt as _;
 use actix_tls::accept::openssl::{Acceptor, TlsStream};
 use actix_utils::future::ok;
-use tokio_rustls::rustls::{Certificate, ClientConfig, RootCertStore, ServerName};
-use tokio_rustls_024 as tokio_rustls;
+use rustls_pki_types_1::ServerName;
+use tokio_rustls::rustls::{ClientConfig, RootCertStore};
+use tokio_rustls_025 as tokio_rustls;
 
 fn new_cert_and_key() -> (String, String) {
     let cert =
@@ -48,28 +49,45 @@ fn openssl_acceptor(cert: String, key: String) -> tls_openssl::ssl::SslAcceptor
 
 #[allow(dead_code)]
 mod danger {
-    use std::time::SystemTime;
-
-    use tokio_rustls_024::rustls::{
-        self,
-        client::{ServerCertVerified, ServerCertVerifier},
-    };
-
-    use super::*;
+    use tokio_rustls_025::rustls;
 
+    #[derive(Debug)]
     pub struct NoCertificateVerification;
 
-    impl ServerCertVerifier for NoCertificateVerification {
+    impl rustls::client::danger::ServerCertVerifier for NoCertificateVerification {
         fn verify_server_cert(
             &self,
-            _end_entity: &Certificate,
-            _intermediates: &[Certificate],
-            _server_name: &ServerName,
-            _scts: &mut dyn Iterator<Item = &[u8]>,
-            _ocsp_response: &[u8],
-            _now: SystemTime,
-        ) -> Result<ServerCertVerified, rustls::Error> {
-            Ok(ServerCertVerified::assertion())
+            end_entity: &rustls_pki_types_1::CertificateDer::CertificateDer<'_>,
+            intermediates: &[rustls_pki_types_1::CertificateDer::CertificateDer<'_>],
+            server_name: &rustls_pki_types_1::CertificateDer::ServerName<'_>,
+            ocsp_response: &[u8],
+            now: rustls_pki_types_1::CertificateDer::UnixTime,
+        ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
+            Ok(rustls::client::danger::ServerCertVerified::assertion())
+        }
+
+        fn verify_tls12_signature(
+            &self,
+            message: &[u8],
+            cert: &rustls_pki_types_1::CertificateDer<'_>,
+            dss: &rustls::DigitallySignedStruct,
+        ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
+            Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
+        }
+
+        fn verify_tls13_signature(
+            &self,
+            message: &[u8],
+            cert: &rustls_pki_types_1::CertificateDer<'_>,
+            dss: &rustls::DigitallySignedStruct,
+        ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
+            Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
+        }
+
+        fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
+            rustls::crypto::ring::default_provider()
+                .signature_verification_algorithms
+                .supported_schemes()
         }
     }
 }
@@ -77,7 +95,6 @@ mod danger {
 #[allow(dead_code)]
 fn rustls_connector(_cert: String, _key: String) -> ClientConfig {
     let mut config = ClientConfig::builder()
-        .with_safe_defaults()
         .with_root_certificates(RootCertStore::empty())
         .with_no_client_auth();
 
diff --git a/actix-tls/tests/accept-rustls.rs b/actix-tls/tests/accept-rustls.rs
index 40d38b7d..554f0fc1 100644
--- a/actix-tls/tests/accept-rustls.rs
+++ b/actix-tls/tests/accept-rustls.rs
@@ -3,7 +3,7 @@
 #![cfg(all(
     feature = "accept",
     feature = "connect",
-    feature = "rustls-0_21",
+    feature = "rustls-0_22",
     feature = "openssl"
 ))]
 
@@ -41,8 +41,10 @@ fn rustls_server_config(cert: String, key: String) -> rustls::ServerConfig {
     let cert = &mut BufReader::new(cert.as_bytes());
     let key = &mut BufReader::new(key.as_bytes());
 
-    let cert_chain = certs(cert).unwrap().into_iter().map(Certificate).collect();
-    let mut keys = pkcs8_private_keys(key).unwrap();
+    let cert_chain = certs(cert).collect::<Result<Vec<_>, _>>().unwrap();
+    let mut keys = pkcs8_private_keys(key)
+        .collect::<Result<Vec<_>, _>>()
+        .unwrap();
 
     let mut config = ServerConfig::builder()
         .with_safe_defaults()
diff --git a/actix-tls/tests/test_connect.rs b/actix-tls/tests/test_connect.rs
index 8cf8d614..58ffa4db 100644
--- a/actix-tls/tests/test_connect.rs
+++ b/actix-tls/tests/test_connect.rs
@@ -11,7 +11,7 @@ use actix_server::TestServer;
 use actix_service::{fn_service, Service, ServiceFactory};
 use actix_tls::connect::{ConnectError, ConnectInfo, Connection, Connector, Host};
 use bytes::Bytes;
-use futures_util::sink::SinkExt;
+use futures_util::sink::SinkExt as _;
 
 #[cfg(feature = "openssl")]
 #[actix_rt::test]
@@ -30,7 +30,7 @@ async fn test_string() {
     assert_eq!(con.peer_addr().unwrap(), srv.addr());
 }
 
-#[cfg(feature = "rustls-0_21")]
+#[cfg(feature = "rustls-0_22")]
 #[actix_rt::test]
 async fn test_rustls_string() {
     let srv = TestServer::start(|| {
@@ -114,7 +114,7 @@ async fn test_openssl_uri() {
     assert_eq!(con.peer_addr().unwrap(), srv.addr());
 }
 
-#[cfg(all(feature = "rustls-0_21", feature = "uri"))]
+#[cfg(all(feature = "rustls-0_22", feature = "uri"))]
 #[actix_rt::test]
 async fn test_rustls_uri_http1() {
     let srv = TestServer::start(|| {
@@ -131,7 +131,7 @@ async fn test_rustls_uri_http1() {
     assert_eq!(con.peer_addr().unwrap(), srv.addr());
 }
 
-#[cfg(all(feature = "rustls-0_21", feature = "uri"))]
+#[cfg(all(feature = "rustls-0_22", feature = "uri"))]
 #[actix_rt::test]
 async fn test_rustls_uri() {
     use std::convert::TryFrom;