diff --git a/Cargo.toml b/Cargo.toml index 7aba32c..d0da2e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,7 @@ hostname = { version = "0.3", optional = true } # feature ## tls native-tls = { version = "0.2", optional = true } # feature -rustls = { version = "0.18", optional = true } +rustls = { version = "0.18", features = ["dangerous_configuration"], optional = true } webpki = { version = "0.21", optional = true } webpki-roots = { version = "0.20", optional = true } diff --git a/src/transport/smtp/async_transport.rs b/src/transport/smtp/async_transport.rs index 2e9ae51..ec90045 100644 --- a/src/transport/smtp/async_transport.rs +++ b/src/transport/smtp/async_transport.rs @@ -45,7 +45,7 @@ where pub fn relay(relay: &str) -> Result { use super::{TlsParameters, SUBMISSIONS_PORT}; - let tls_parameters = TlsParameters::new_tokio02(relay.into())?; + let tls_parameters = TlsParameters::builder(relay.into()).build_tokio02()?; Ok(Self::builder_dangerous(relay) .port(SUBMISSIONS_PORT) diff --git a/src/transport/smtp/client/mod.rs b/src/transport/smtp/client/mod.rs index 50f71ef..54c403f 100644 --- a/src/transport/smtp/client/mod.rs +++ b/src/transport/smtp/client/mod.rs @@ -34,7 +34,7 @@ pub(super) use self::tls::InnerTlsParameters; pub use self::{ connection::SmtpConnection, mock::MockStream, - tls::{Tls, TlsParameters}, + tls::{Tls, TlsParameters, TlsParametersBuilder}, }; #[cfg(feature = "tokio02")] diff --git a/src/transport/smtp/client/tls.rs b/src/transport/smtp/client/tls.rs index e9c6958..8e90087 100644 --- a/src/transport/smtp/client/tls.rs +++ b/src/transport/smtp/client/tls.rs @@ -1,10 +1,17 @@ +#[cfg(feature = "rustls-tls")] +use std::sync::Arc; + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] use crate::transport::smtp::error::Error; #[cfg(feature = "native-tls")] use native_tls::{Protocol, TlsConnector}; #[cfg(feature = "rustls-tls")] -use rustls::ClientConfig; +use rustls::{ + Certificate, ClientConfig, RootCertStore, ServerCertVerified, ServerCertVerifier, TLSError, +}; +#[cfg(feature = "rustls-tls")] +use webpki::DNSNameRef; /// Accepted protocols by default. /// This removes TLS 1.0 and 1.1 compared to tls-native defaults. @@ -38,6 +45,119 @@ pub struct TlsParameters { pub(super) domain: String, } +/// Builder for `TlsParameters` +#[derive(Debug, Clone)] +pub struct TlsParametersBuilder { + domain: String, + accept_invalid_hostnames: bool, + accept_invalid_certs: bool, +} + +impl TlsParametersBuilder { + /// Creates a new builder for `TlsParameters` + pub fn new(domain: String) -> Self { + Self { + domain, + accept_invalid_hostnames: false, + accept_invalid_certs: false, + } + } + + /// Controls whether certificates with an invalid hostname are accepted + /// + /// Defaults to `false`. + /// + /// # Warning + /// + /// You should think very carefully before using this method. + /// If hostname verification is disabled *any* valid certificate, + /// 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(feature = "native-tls")] + pub fn dangerous_accept_invalid_hostnames( + &mut self, + accept_invalid_hostnames: bool, + ) -> &mut Self { + self.accept_invalid_hostnames = accept_invalid_hostnames; + self + } + + /// Controls whether invalid certificates are accepted + /// + /// Defaults to `false`. + /// + /// # Warning + /// + /// You should think very carefully before using this method. + /// If certificate verification is disabled, *any* certificate + /// is trusted for use, including: + /// + /// * Self signed certificates + /// * Certificates from different hostnames + /// * Expired certificates + /// + /// This method should only be used as a last resort, as it introduces + /// significant vulnerabilities to man-in-the-middle attacks. + pub fn dangerous_accept_invalid_certs(&mut self, accept_invalid_certs: bool) -> &mut Self { + self.accept_invalid_certs = accept_invalid_certs; + self + } + + /// Creates a new `TlsParameters` using native-tls or rustls + /// depending on which one is available + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] + pub fn build(self) -> Result { + #[cfg(feature = "native-tls")] + return self.build_native(); + + #[cfg(not(feature = "native-tls"))] + return self.build_rustls(); + } + + #[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))] + pub(crate) fn build_tokio02(self) -> Result { + #[cfg(feature = "tokio02-native-tls")] + return self.build_native(); + + #[cfg(not(feature = "tokio02-native-tls"))] + return self.build_rustls(); + } + + /// Creates a new `TlsParameters` using native-tls with the provided configuration + #[cfg(feature = "native-tls")] + pub fn build_native(self) -> Result { + let mut tls_builder = TlsConnector::builder(); + tls_builder.danger_accept_invalid_hostnames(self.accept_invalid_hostnames); + tls_builder.danger_accept_invalid_certs(self.accept_invalid_certs); + tls_builder.min_protocol_version(Some(DEFAULT_TLS_MIN_PROTOCOL)); + let connector = tls_builder.build()?; + Ok(TlsParameters { + connector: InnerTlsParameters::NativeTls(connector), + domain: self.domain, + }) + } + + /// Creates a new `TlsParameters` using rustls with the provided configuration + #[cfg(feature = "rustls-tls")] + pub fn build_rustls(self) -> Result { + use webpki_roots::TLS_SERVER_ROOTS; + + let mut tls = ClientConfig::new(); + if self.accept_invalid_certs { + tls.dangerous() + .set_certificate_verifier(Arc::new(InvalidCertsVerifier {})); + } + tls.root_store.add_server_trust_anchors(&TLS_SERVER_ROOTS); + Ok(TlsParameters { + connector: InnerTlsParameters::RustlsTls(tls), + domain: self.domain, + }) + } +} + #[derive(Clone)] pub enum InnerTlsParameters { #[cfg(feature = "native-tls")] @@ -51,48 +171,42 @@ impl TlsParameters { /// depending on which one is available #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] pub fn new(domain: String) -> Result { - #[cfg(feature = "native-tls")] - return Self::new_native(domain); - - #[cfg(not(feature = "native-tls"))] - return Self::new_rustls(domain); + TlsParametersBuilder::new(domain).build() } - #[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))] - pub(crate) fn new_tokio02(domain: String) -> Result { - #[cfg(feature = "tokio02-native-tls")] - return Self::new_native(domain); - - #[cfg(not(feature = "tokio02-native-tls"))] - return Self::new_rustls(domain); + pub fn builder(domain: String) -> TlsParametersBuilder { + TlsParametersBuilder::new(domain) } /// Creates a new `TlsParameters` using native-tls #[cfg(feature = "native-tls")] pub fn new_native(domain: String) -> Result { - let mut tls_builder = TlsConnector::builder(); - tls_builder.min_protocol_version(Some(DEFAULT_TLS_MIN_PROTOCOL)); - let connector = tls_builder.build()?; - Ok(Self { - connector: InnerTlsParameters::NativeTls(connector), - domain, - }) + TlsParametersBuilder::new(domain).build_native() } /// Creates a new `TlsParameters` using rustls #[cfg(feature = "rustls-tls")] pub fn new_rustls(domain: String) -> Result { - use webpki_roots::TLS_SERVER_ROOTS; - - let mut tls = ClientConfig::new(); - tls.root_store.add_server_trust_anchors(&TLS_SERVER_ROOTS); - Ok(Self { - connector: InnerTlsParameters::RustlsTls(tls), - domain, - }) + TlsParametersBuilder::new(domain).build_rustls() } pub fn domain(&self) -> &str { &self.domain } } + +#[cfg(feature = "rustls-tls")] +struct InvalidCertsVerifier; + +#[cfg(feature = "rustls-tls")] +impl ServerCertVerifier for InvalidCertsVerifier { + fn verify_server_cert( + &self, + _roots: &RootCertStore, + _presented_certs: &[Certificate], + _dns_name: DNSNameRef<'_>, + _ocsp_response: &[u8], + ) -> Result { + Ok(ServerCertVerified::assertion()) + } +}