Repository: lettre/lettre (language: rust)

Issue:
Implement accept_invalid_hostnames for rustls
Given that our API is heavily inspired from `reqwest`, it looks like it's now possible to ignore invalid hostnames for rustls https://github.com/seanmonstar/reqwest/pull/2249


Discussion / hints:

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index 6fa3c665b..09122e6f5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1322,6 +1322,7 @@ dependencies = [
  "rustls",
  "rustls-native-certs",
  "rustls-pemfile",
+ "rustls-pki-types",
  "serde",
  "serde_json",
  "sha2",
@@ -2026,9 +2027,9 @@ dependencies = [
 
 [[package]]
 name = "rustls-pki-types"
-version = "1.3.0"
+version = "1.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "048a63e5b3ac996d78d402940b5fa47973d2d080c6c6fffa1d0f19c4445310b7"
+checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d"
 
 [[package]]
 name = "rustls-webpki"
diff --git a/Cargo.toml b/Cargo.toml
index 5f8f73f0e..a05abedaf 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -48,6 +48,7 @@ native-tls = { version = "0.2.5", optional = true } # feature
 rustls = { version = "0.23.5", default-features = false, features = ["ring", "logging", "std", "tls12"], optional = true }
 rustls-pemfile = { version = "2", optional = true }
 rustls-native-certs = { version = "0.7", optional = true }
+rustls-pki-types = { version = "1.7", optional = true }
 webpki-roots = { version = "0.26", optional = true }
 boring = { version = "4", optional = true }
 
@@ -108,7 +109,7 @@ smtp-transport = ["dep:base64", "dep:nom", "dep:socket2", "dep:url", "dep:percen
 
 pool = ["dep:futures-util"]
 
-rustls-tls = ["dep:webpki-roots", "dep:rustls", "dep:rustls-pemfile"]
+rustls-tls = ["dep:webpki-roots", "dep:rustls", "dep:rustls-pemfile", "dep:rustls-pki-types"]
 
 boring-tls = ["dep:boring"]
 
diff --git a/src/transport/smtp/client/tls.rs b/src/transport/smtp/client/tls.rs
index 89d397c68..92dca9315 100644
--- a/src/transport/smtp/client/tls.rs
+++ b/src/transport/smtp/client/tls.rs
@@ -13,8 +13,10 @@ use native_tls::{Protocol, TlsConnector};
 #[cfg(feature = "rustls-tls")]
 use rustls::{
     client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
+    crypto::WebPkiSupportedAlgorithms,
     crypto::{verify_tls12_signature, verify_tls13_signature},
     pki_types::{CertificateDer, PrivateKeyDer, ServerName, UnixTime},
+    server::ParsedCertificate,
     ClientConfig, DigitallySignedStruct, Error as TlsError, RootCertStore, SignatureScheme,
 };
 
@@ -195,10 +197,11 @@ impl TlsParametersBuilder {
     /// including those from other sites, are trusted.
     ///
     /// This method introduces significant vulnerabilities to man-in-the-middle attacks.
-    ///
-    /// Hostname verification can only be disabled with the `native-tls` TLS backend.
     #[cfg(any(feature = "native-tls", feature = "boring-tls"))]
-    #[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "boring-tls"))))]
+    #[cfg_attr(
+        docsrs,
+        doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
+    )]
     pub fn dangerous_accept_invalid_hostnames(mut self, accept_invalid_hostnames: bool) -> Self {
         self.accept_invalid_hostnames = accept_invalid_hostnames;
         self
@@ -376,50 +379,63 @@ impl TlsParametersBuilder {
         };
 
         let tls = ClientConfig::builder_with_protocol_versions(supported_versions);
+        let provider = rustls::crypto::CryptoProvider::get_default()
+            .map(|arc| arc.clone())
+            .unwrap_or_else(|| Arc::new(rustls::crypto::ring::default_provider()));
+
+        // Build TLS config
+        let signature_algorithms = provider.signature_verification_algorithms;
+
+        let mut root_cert_store = RootCertStore::empty();
+
+        #[cfg(feature = "rustls-native-certs")]
+        fn load_native_roots(store: &mut RootCertStore) -> Result<(), Error> {
+            let native_certs = rustls_native_certs::load_native_certs().map_err(error::tls)?;
+            let (added, ignored) = store.add_parsable_certificates(native_certs);
+            #[cfg(feature = "tracing")]
+            tracing::debug!(
+                "loaded platform certs with {added} valid and {ignored} ignored (invalid) certs"
+            );
+            Ok(())
+        }
 
-        let tls = if self.accept_invalid_certs {
-            tls.dangerous()
-                .with_custom_certificate_verifier(Arc::new(InvalidCertsVerifier {}))
-        } else {
-            let mut root_cert_store = RootCertStore::empty();
-
-            #[cfg(feature = "rustls-native-certs")]
-            fn load_native_roots(store: &mut RootCertStore) -> Result<(), Error> {
-                let native_certs = rustls_native_certs::load_native_certs().map_err(error::tls)?;
-                let (added, ignored) = store.add_parsable_certificates(native_certs);
-                #[cfg(feature = "tracing")]
-                tracing::debug!(
-                    "loaded platform certs with {added} valid and {ignored} ignored (invalid) certs"
-                );
-                Ok(())
-            }
+        #[cfg(feature = "rustls-tls")]
+        fn load_webpki_roots(store: &mut RootCertStore) {
+            store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
+        }
 
-            #[cfg(feature = "rustls-tls")]
-            fn load_webpki_roots(store: &mut RootCertStore) {
-                store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
+        match self.cert_store {
+            CertificateStore::Default => {
+                #[cfg(feature = "rustls-native-certs")]
+                load_native_roots(&mut root_cert_store)?;
+                #[cfg(not(feature = "rustls-native-certs"))]
+                load_webpki_roots(&mut root_cert_store);
             }
-
-            match self.cert_store {
-                CertificateStore::Default => {
-                    #[cfg(feature = "rustls-native-certs")]
-                    load_native_roots(&mut root_cert_store)?;
-                    #[cfg(not(feature = "rustls-native-certs"))]
-                    load_webpki_roots(&mut root_cert_store);
-                }
-                #[cfg(feature = "rustls-tls")]
-                CertificateStore::WebpkiRoots => {
-                    load_webpki_roots(&mut root_cert_store);
-                }
-                CertificateStore::None => {}
+            #[cfg(feature = "rustls-tls")]
+            CertificateStore::WebpkiRoots => {
+                load_webpki_roots(&mut root_cert_store);
             }
-            for cert in self.root_certs {
-                for rustls_cert in cert.rustls {
-                    root_cert_store.add(rustls_cert).map_err(error::tls)?;
-                }
+            CertificateStore::None => {}
+        }
+        for cert in self.root_certs {
+            for rustls_cert in cert.rustls {
+                root_cert_store.add(rustls_cert).map_err(error::tls)?;
             }
+        }
 
+        let tls = if self.accept_invalid_certs || self.accept_invalid_hostnames {
+            let verifier = InvalidCertsVerifier {
+                ignore_invalid_hostnames: self.accept_invalid_hostnames,
+                ignore_invalid_certs: self.accept_invalid_certs,
+                roots: root_cert_store,
+                signature_algorithms,
+            };
+            tls.dangerous()
+                .with_custom_certificate_verifier(Arc::new(verifier))
+        } else {
             tls.with_root_certificates(root_cert_store)
         };
+
         let tls = if let Some(identity) = self.identity {
             let (client_certificates, private_key) = identity.rustls_tls;
             tls.with_client_auth_cert(client_certificates, private_key)
@@ -629,18 +645,38 @@ impl Identity {
 
 #[cfg(feature = "rustls-tls")]
 #[derive(Debug)]
-struct InvalidCertsVerifier;
+struct InvalidCertsVerifier {
+    ignore_invalid_hostnames: bool,
+    ignore_invalid_certs: bool,
+    roots: RootCertStore,
+    signature_algorithms: WebPkiSupportedAlgorithms,
+}
 
 #[cfg(feature = "rustls-tls")]
 impl ServerCertVerifier for InvalidCertsVerifier {
     fn verify_server_cert(
         &self,
-        _end_entity: &CertificateDer<'_>,
-        _intermediates: &[CertificateDer<'_>],
-        _server_name: &ServerName<'_>,
-        _ocsp_response: &[u8],
-        _now: UnixTime,
+        end_entity: &CertificateDer<'_>,
+        intermediates: &[CertificateDer<'_>],
+        server_name: &ServerName<'_>,
+        ocsp_response: &[u8],
+        now: UnixTime,
     ) -> Result<ServerCertVerified, TlsError> {
+        let cert = ParsedCertificate::try_from(end_entity)?;
+
+        if !self.ignore_invalid_certs {
+            rustls::client::verify_server_cert_signed_by_trust_anchor(
+                &cert,
+                &self.roots,
+                intermediates,
+                now,
+                self.signature_algorithms.all,
+            )?;
+        }
+
+        if !self.ignore_invalid_hostnames {
+            rustls::client::verify_server_name(&cert, server_name)?;
+        }
         Ok(ServerCertVerified::assertion())
     }
 


Test patch: