diff --git a/Cargo.toml b/Cargo.toml index 8156ca4..6233323 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -108,7 +108,7 @@ ipnet = "2.3" ## default-tls hyper-tls = { version = "0.5", optional = true } -native-tls-crate = { version = "0.2.7", optional = true, package = "native-tls" } +native-tls-crate = { version = "0.2.8", optional = true, package = "native-tls" } tokio-native-tls = { version = "0.3.0", optional = true } # rustls-tls diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs index 2e88c0e..1da6961 100644 --- a/src/async_impl/client.rs +++ b/src/async_impl/client.rs @@ -37,7 +37,7 @@ use crate::error; use crate::into_url::{expect_uri, try_uri}; use crate::redirect::{self, remove_sensitive_headers}; #[cfg(feature = "__tls")] -use crate::tls::TlsBackend; +use crate::tls::{self, TlsBackend}; #[cfg(feature = "__tls")] use crate::Certificate; #[cfg(any(feature = "native-tls", feature = "__rustls"))] @@ -99,6 +99,10 @@ struct Config { #[cfg(feature = "__tls")] tls_built_in_root_certs: bool, #[cfg(feature = "__tls")] + min_tls_version: Option, + #[cfg(feature = "__tls")] + max_tls_version: Option, + #[cfg(feature = "__tls")] tls: TlsBackend, http_version_pref: HttpVersionPref, http1_title_case_headers: bool, @@ -158,6 +162,10 @@ impl ClientBuilder { #[cfg(any(feature = "native-tls", feature = "__rustls"))] identity: None, #[cfg(feature = "__tls")] + min_tls_version: None, + #[cfg(feature = "__tls")] + max_tls_version: None, + #[cfg(feature = "__tls")] tls: TlsBackend::default(), http_version_pref: HttpVersionPref::All, http1_title_case_headers: false, @@ -262,6 +270,27 @@ impl ClientBuilder { } } + if let Some(min_tls_version) = config.min_tls_version { + let protocol = min_tls_version.to_native_tls().ok_or_else(|| { + // TLS v1.3. This would be entirely reasonable, + // native-tls just doesn't support it. + // https://github.com/sfackler/rust-native-tls/issues/140 + crate::error::builder("invalid minimum TLS version for backend") + })?; + tls.min_protocol_version(Some(protocol)); + } + + if let Some(max_tls_version) = config.max_tls_version { + let protocol = max_tls_version.to_native_tls().ok_or_else(|| { + // TLS v1.3. + // We could arguably do max_protocol_version(None), given + // that 1.4 does not exist yet, but that'd get messy in the + // future. + crate::error::builder("invalid maximum TLS version for backend") + })?; + tls.max_protocol_version(Some(protocol)); + } + Connector::new_default_tls( http, tls, @@ -329,6 +358,34 @@ impl ClientBuilder { id.add_to_rustls(&mut tls)?; } + // rustls does not support TLS versions <1.2 and this is unlikely to change. + // https://github.com/rustls/rustls/issues/33 + + // As of writing, TLS 1.2 and 1.3 are the only implemented versions and are both + // enabled by default. + // rustls 0.20 will add ALL_VERSIONS and DEFAULT_VERSIONS. That will enable a more + // sophisticated approach. + // For now we assume the default tls.versions matches the future ALL_VERSIONS and + // act based on that. + + if let Some(min_tls_version) = config.min_tls_version { + tls.versions + .retain(|&version| match tls::Version::from_rustls(version) { + Some(version) => version >= min_tls_version, + // Assume it's so new we don't know about it, allow it + // (as of writing this is unreachable) + None => true, + }); + } + + if let Some(max_tls_version) = config.max_tls_version { + tls.versions + .retain(|&version| match tls::Version::from_rustls(version) { + Some(version) => version <= max_tls_version, + None => false, + }); + } + Connector::new_rustls_tls( http, tls, @@ -957,6 +1014,64 @@ impl ClientBuilder { self } + /// Set the minimum required TLS version for connections. + /// + /// By default the TLS backend's own default is used. + /// + /// # Errors + /// + /// A value of `tls::Version::TLS_1_3` will cause an error with the + /// `native-tls`/`default-tls` backend. This does not mean the version + /// isn't supported, just that it can't be set as a minimum due to + /// technical limitations. + /// + /// # Optional + /// + /// This requires the optional `default-tls`, `native-tls`, or `rustls-tls(-...)` + /// feature to be enabled. + #[cfg(feature = "__tls")] + #[cfg_attr( + docsrs, + doc(cfg(any( + feature = "default-tls", + feature = "native-tls", + feature = "rustls-tls" + ))) + )] + pub fn min_tls_version(mut self, version: tls::Version) -> ClientBuilder { + self.config.min_tls_version = Some(version); + self + } + + /// Set the maximum allowed TLS version for connections. + /// + /// By default there's no maximum. + /// + /// # Errors + /// + /// A value of `tls::Version::TLS_1_3` will cause an error with the + /// `native-tls`/`default-tls` backend. This does not mean the version + /// isn't supported, just that it can't be set as a maximum due to + /// technical limitations. + /// + /// # Optional + /// + /// This requires the optional `default-tls`, `native-tls`, or `rustls-tls(-...)` + /// feature to be enabled. + #[cfg(feature = "__tls")] + #[cfg_attr( + docsrs, + doc(cfg(any( + feature = "default-tls", + feature = "native-tls", + feature = "rustls-tls" + ))) + )] + pub fn max_tls_version(mut self, version: tls::Version) -> ClientBuilder { + self.config.max_tls_version = Some(version); + self + } + /// Force using the native TLS backend. /// /// Since multiple TLS backends can be optionally enabled, this option will @@ -1399,6 +1514,14 @@ impl Config { if !self.certs_verification { f.field("danger_accept_invalid_certs", &true); } + + if let Some(ref min_tls_version) = self.min_tls_version { + f.field("min_tls_version", min_tls_version); + } + + if let Some(ref max_tls_version) = self.max_tls_version { + f.field("max_tls_version", max_tls_version); + } } #[cfg(all(feature = "native-tls-crate", feature = "__rustls"))] diff --git a/src/blocking/client.rs b/src/blocking/client.rs index a3b4e89..f512725 100644 --- a/src/blocking/client.rs +++ b/src/blocking/client.rs @@ -16,6 +16,8 @@ use super::request::{Request, RequestBuilder}; use super::response::Response; use super::wait; #[cfg(feature = "__tls")] +use crate::tls; +#[cfg(feature = "__tls")] use crate::Certificate; #[cfg(any(feature = "native-tls", feature = "__rustls"))] use crate::Identity; @@ -603,6 +605,62 @@ impl ClientBuilder { self.with_inner(|inner| inner.danger_accept_invalid_certs(accept_invalid_certs)) } + /// Set the minimum required TLS version for connections. + /// + /// By default the TLS backend's own default is used. + /// + /// # Errors + /// + /// A value of `tls::Version::TLS_1_3` will cause an error with the + /// `native-tls`/`default-tls` backend. This does not mean the version + /// isn't supported, just that it can't be set as a minimum due to + /// technical limitations. + /// + /// # Optional + /// + /// This requires the optional `default-tls`, `native-tls`, or `rustls-tls(-...)` + /// feature to be enabled. + #[cfg(feature = "__tls")] + #[cfg_attr( + docsrs, + doc(cfg(any( + feature = "default-tls", + feature = "native-tls", + feature = "rustls-tls" + ))) + )] + pub fn min_tls_version(self, version: tls::Version) -> ClientBuilder { + self.with_inner(|inner| inner.min_tls_version(version)) + } + + /// Set the maximum allowed TLS version for connections. + /// + /// By default there's no maximum. + /// + /// # Errors + /// + /// A value of `tls::Version::TLS_1_3` will cause an error with the + /// `native-tls`/`default-tls` backend. This does not mean the version + /// isn't supported, just that it can't be set as a maximum due to + /// technical limitations. + /// + /// # Optional + /// + /// This requires the optional `default-tls`, `native-tls`, or `rustls-tls(-...)` + /// feature to be enabled. + #[cfg(feature = "__tls")] + #[cfg_attr( + docsrs, + doc(cfg(any( + feature = "default-tls", + feature = "native-tls", + feature = "rustls-tls" + ))) + )] + pub fn max_tls_version(self, version: tls::Version) -> ClientBuilder { + self.with_inner(|inner| inner.max_tls_version(version)) + } + /// Force using the native TLS backend. /// /// Since multiple TLS backends can be optionally enabled, this option will diff --git a/src/lib.rs b/src/lib.rs index b5ddac7..2b154b6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -298,7 +298,8 @@ if_hyper! { }; pub use self::proxy::Proxy; #[cfg(feature = "__tls")] - pub use self::tls::{Certificate, Identity}; + // Re-exports, to be removed in a future release + pub use tls::{Certificate, Identity}; #[cfg(feature = "multipart")] pub use self::async_impl::multipart; @@ -314,7 +315,7 @@ if_hyper! { mod proxy; pub mod redirect; #[cfg(feature = "__tls")] - mod tls; + pub mod tls; mod util; } diff --git a/src/tls.rs b/src/tls.rs index 3c3696b..32ffa62 100644 --- a/src/tls.rs +++ b/src/tls.rs @@ -1,3 +1,16 @@ +//! TLS configuration +//! +//! By default, a `Client` will make use of system-native transport layer +//! security to connect to HTTPS destinations. This means schannel on Windows, +//! Security-Framework on macOS, and OpenSSL on Linux. +//! +//! - Additional X509 certificates can be configured on a `ClientBuilder` with the +//! [`Certificate`](Certificate) type. +//! - Client certificates can be add to a `ClientBuilder` with the +//! [`Identity`][Identity] type. +//! - Various parts of TLS can also be configured or even disabled on the +//! `ClientBuilder`. + #[cfg(feature = "__rustls")] use rustls::{ internal::msgs::handshake::DigitallySignedStruct, HandshakeSignatureValid, RootCertStore, @@ -268,6 +281,55 @@ impl fmt::Debug for Identity { } } +/// A TLS protocol version. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct Version(InnerVersion); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[non_exhaustive] +enum InnerVersion { + Tls1_0, + Tls1_1, + Tls1_2, + Tls1_3, +} + +// These could perhaps be From/TryFrom implementations, but those would be +// part of the public API so let's be careful +impl Version { + /// Version 1.0 of the TLS protocol. + pub const TLS_1_0: Version = Version(InnerVersion::Tls1_0); + /// Version 1.1 of the TLS protocol. + pub const TLS_1_1: Version = Version(InnerVersion::Tls1_1); + /// Version 1.2 of the TLS protocol. + pub const TLS_1_2: Version = Version(InnerVersion::Tls1_2); + /// Version 1.3 of the TLS protocol. + pub const TLS_1_3: Version = Version(InnerVersion::Tls1_3); + + #[cfg(feature = "default-tls")] + pub(crate) fn to_native_tls(self) -> Option { + match self.0 { + InnerVersion::Tls1_0 => Some(native_tls_crate::Protocol::Tlsv10), + InnerVersion::Tls1_1 => Some(native_tls_crate::Protocol::Tlsv11), + InnerVersion::Tls1_2 => Some(native_tls_crate::Protocol::Tlsv12), + InnerVersion::Tls1_3 => None, + } + } + + #[cfg(feature = "__rustls")] + pub(crate) fn from_rustls(version: rustls::ProtocolVersion) -> Option { + match version { + rustls::ProtocolVersion::SSLv2 => None, + rustls::ProtocolVersion::SSLv3 => None, + rustls::ProtocolVersion::TLSv1_0 => Some(Self(InnerVersion::Tls1_0)), + rustls::ProtocolVersion::TLSv1_1 => Some(Self(InnerVersion::Tls1_1)), + rustls::ProtocolVersion::TLSv1_2 => Some(Self(InnerVersion::Tls1_2)), + rustls::ProtocolVersion::TLSv1_3 => Some(Self(InnerVersion::Tls1_3)), + _ => None, + } + } +} + pub(crate) enum TlsBackend { #[cfg(feature = "default-tls")] Default,