This commit is contained in:
Paolo Barbolini
2025-05-01 21:23:31 +02:00
parent 610b72e93b
commit 2f4e36ac61
5 changed files with 355 additions and 7 deletions

View File

@@ -1,7 +1,65 @@
use std::fmt::{self, Debug};
use boring::{
ssl::{SslConnector, SslMethod, SslVerifyMode, SslVersion},
x509::store::X509StoreBuilder,
};
use crate::transport::smtp::error::{self, Error};
pub(super) fn build_connector(
builder: super::TlsParametersBuilder<super::BoringTls>,
) -> Result<SslConnector, Error> {
let mut tls_builder = SslConnector::builder(SslMethod::tls_client()).map_err(error::tls)?;
if builder.accept_invalid_certs {
tls_builder.set_verify(SslVerifyMode::NONE);
} else {
match builder.cert_store {
CertificateStore::System => {}
CertificateStore::None => {
// Replace the default store with an empty store.
tls_builder.set_cert_store(X509StoreBuilder::new().map_err(error::tls)?.build());
}
}
let cert_store = tls_builder.cert_store_mut();
for cert in builder.root_certs {
cert_store.add_cert(cert.0).map_err(error::tls)?;
}
}
if let Some(identity) = builder.identity {
tls_builder
.set_certificate(identity.chain.as_ref())
.map_err(error::tls)?;
tls_builder
.set_private_key(identity.key.as_ref())
.map_err(error::tls)?;
}
let min_tls_version = match builder.min_tls_version {
MinTlsVersion::Tlsv10 => SslVersion::TLS1,
MinTlsVersion::Tlsv11 => SslVersion::TLS1_1,
MinTlsVersion::Tlsv12 => SslVersion::TLS1_2,
MinTlsVersion::Tlsv13 => SslVersion::TLS1_3,
};
tls_builder
.set_min_proto_version(Some(min_tls_version))
.map_err(error::tls)?;
Ok(tls_builder.build())
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub enum CertificateStore {
#[default]
System,
None,
}
#[derive(Clone)]
pub struct Certificate(pub(super) boring::x509::X509);
@@ -40,3 +98,13 @@ impl Debug for Identity {
f.debug_struct("Identity").finish_non_exhaustive()
}
}
#[derive(Debug, Copy, Clone, Default)]
#[non_exhaustive]
pub enum MinTlsVersion {
Tlsv10,
Tlsv11,
#[default]
Tlsv12,
Tlsv13,
}

View File

@@ -174,7 +174,10 @@ pub enum NativeTlsCertificateStore {
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
pub enum RustlsCertificateStore {
#[cfg(all(feature = "rustls", feature = "rustls-native-certs"))]
#[cfg_attr(docsrs, doc(cfg(all(feature = "rustls", feature = "rustls-native-certs"))))]
#[cfg_attr(
docsrs,
doc(cfg(all(feature = "rustls", feature = "rustls-native-certs")))
)]
NativeCerts,
#[cfg(all(feature = "rustls", feature = "webpki-roots"))]
#[cfg_attr(docsrs, doc(cfg(all(feature = "rustls", feature = "webpki-roots"))))]
@@ -204,6 +207,7 @@ pub struct TlsParameters {
/// Builder for `TlsParameters`
#[derive(Debug, Clone)]
#[deprecated]
pub struct TlsParametersBuilder {
domain: String,
#[allow(deprecated)]
@@ -216,6 +220,7 @@ pub struct TlsParametersBuilder {
min_tls_version: TlsVersion,
}
#[allow(deprecated)]
impl TlsParametersBuilder {
/// Creates a new builder for `TlsParameters`
pub fn new(domain: String) -> Self {

View File

@@ -1,3 +1,5 @@
use crate::transport::smtp::Error;
#[cfg(feature = "boring-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "boring-tls")))]
pub mod boring_tls;
@@ -9,9 +11,81 @@ pub mod native_tls;
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
pub mod rustls;
pub trait TlsBackend: private::Sealed {
#[derive(Debug)]
pub struct TlsParametersBuilder<B: TlsBackend> {
domain: String,
cert_store: B::CertificateStore,
root_certs: Vec<B::Certificate>,
identity: Option<B::Identity>,
accept_invalid_certs: bool,
accept_invalid_hostnames: bool,
min_tls_version: B::MinTlsVersion,
}
impl<B: TlsBackend> TlsParametersBuilder<B> {
pub(super) fn new(domain: String) -> Self {
Self {
domain,
cert_store: Default::default(),
root_certs: Vec::new(),
identity: None,
accept_invalid_certs: false,
accept_invalid_hostnames: false,
min_tls_version: Default::default(),
}
}
pub fn certificate_store(mut self, cert_store: B::CertificateStore) -> Self {
self.cert_store = cert_store;
self
}
pub fn add_root_certificate(mut self, cert: B::Certificate) -> Self {
self.root_certs.push(cert);
self
}
pub fn identify_with(mut self, identity: B::Identity) -> Self {
self.identity = Some(identity);
self
}
pub fn min_tls_version(mut self, min_tls_version: B::MinTlsVersion) -> Self {
self.min_tls_version = min_tls_version;
self
}
pub fn dangerous_accept_invalid_certs(mut self, accept_invalid_certs: bool) -> Self {
self.accept_invalid_certs = accept_invalid_certs;
self
}
pub fn dangerous_accept_invalid_hostnames(mut self, accept_invalid_hostnames: bool) -> Self {
self.accept_invalid_hostnames = accept_invalid_hostnames;
self
}
pub fn build_legacy(self) -> Result<self::deprecated::TlsParameters, Error> {
let domain = self.domain.clone();
let connector = B::__build_legacy_connector(self)?;
Ok(self::deprecated::TlsParameters { connector, domain })
}
}
pub trait TlsBackend: private::SealedTlsBackend {
type CertificateStore: Default;
type Certificate;
type Identity;
type MinTlsVersion: Default;
#[doc(hidden)]
fn __build_connector(builder: TlsParametersBuilder<Self>) -> Result<Self::Connector, Error>;
#[doc(hidden)]
#[allow(private_interfaces)]
fn __build_legacy_connector(
builder: TlsParametersBuilder<Self>,
) -> Result<self::deprecated::InnerTlsParameters, Error>;
}
#[cfg(feature = "boring-tls")]
@@ -22,8 +96,27 @@ pub struct BoringTls;
#[cfg(feature = "boring-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "boring-tls")))]
impl TlsBackend for BoringTls {
type CertificateStore = self::boring_tls::CertificateStore;
type Certificate = self::boring_tls::Certificate;
type Identity = self::boring_tls::Identity;
type MinTlsVersion = self::boring_tls::MinTlsVersion;
fn __build_connector(builder: TlsParametersBuilder<Self>) -> Result<Self::Connector, Error> {
self::boring_tls::build_connector(builder)
}
#[allow(private_interfaces)]
fn __build_legacy_connector(
builder: TlsParametersBuilder<Self>,
) -> Result<self::deprecated::InnerTlsParameters, Error> {
let accept_invalid_hostnames = builder.accept_invalid_hostnames;
Self::__build_connector(builder).map(|connector| {
self::deprecated::InnerTlsParameters::BoringTls {
connector,
accept_invalid_hostnames,
}
})
}
}
#[cfg(feature = "native-tls")]
@@ -34,8 +127,22 @@ pub struct NativeTls;
#[cfg(feature = "native-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
impl TlsBackend for NativeTls {
type CertificateStore = self::native_tls::CertificateStore;
type Certificate = self::native_tls::Certificate;
type Identity = self::native_tls::Identity;
type MinTlsVersion = self::native_tls::MinTlsVersion;
fn __build_connector(builder: TlsParametersBuilder<Self>) -> Result<Self::Connector, Error> {
self::native_tls::build_connector(builder)
}
#[allow(private_interfaces)]
fn __build_legacy_connector(
builder: TlsParametersBuilder<Self>,
) -> Result<self::deprecated::InnerTlsParameters, Error> {
Self::__build_connector(builder)
.map(|connector| self::deprecated::InnerTlsParameters::NativeTls { connector })
}
}
#[cfg(feature = "rustls")]
@@ -46,20 +153,42 @@ pub struct Rustls;
#[cfg(feature = "rustls")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
impl TlsBackend for Rustls {
type CertificateStore = self::rustls::CertificateStore;
type Certificate = self::rustls::Certificate;
type Identity = self::rustls::Identity;
type MinTlsVersion = self::rustls::MinTlsVersion;
fn __build_connector(builder: TlsParametersBuilder<Self>) -> Result<Self::Connector, Error> {
self::rustls::build_connector(builder)
}
#[allow(private_interfaces)]
fn __build_legacy_connector(
builder: TlsParametersBuilder<Self>,
) -> Result<self::deprecated::InnerTlsParameters, Error> {
Self::__build_connector(builder)
.map(|config| self::deprecated::InnerTlsParameters::Rustls { config })
}
}
mod private {
// FIXME: this should be `pub(super)` but the `private_bounds` lint doesn't like it
pub trait Sealed {}
pub trait SealedTlsBackend: Sized {
type Connector;
}
#[cfg(feature = "boring-tls")]
impl Sealed for super::BoringTls {}
impl SealedTlsBackend for super::BoringTls {
type Connector = boring::ssl::SslConnector;
}
#[cfg(feature = "native-tls")]
impl Sealed for super::NativeTls {}
impl SealedTlsBackend for super::NativeTls {
type Connector = native_tls::TlsConnector;
}
#[cfg(feature = "rustls")]
impl Sealed for super::Rustls {}
impl SealedTlsBackend for super::Rustls {
type Connector = std::sync::Arc<rustls::client::ClientConfig>;
}
}

View File

@@ -1,7 +1,48 @@
use std::fmt::{self, Debug};
use native_tls::TlsConnector;
use crate::transport::smtp::error::{self, Error};
pub(super) fn build_connector(
builder: super::TlsParametersBuilder<super::NativeTls>,
) -> Result<TlsConnector, Error> {
let mut tls_builder = TlsConnector::builder();
match builder.cert_store {
CertificateStore::System => {}
CertificateStore::None => {
tls_builder.disable_built_in_roots(true);
}
}
for cert in builder.root_certs {
tls_builder.add_root_certificate(cert.0);
}
tls_builder.danger_accept_invalid_hostnames(builder.accept_invalid_hostnames);
tls_builder.danger_accept_invalid_certs(builder.accept_invalid_certs);
let min_tls_version = match builder.min_tls_version {
MinTlsVersion::Tlsv10 => native_tls::Protocol::Tlsv10,
MinTlsVersion::Tlsv11 => native_tls::Protocol::Tlsv11,
MinTlsVersion::Tlsv12 => native_tls::Protocol::Tlsv12,
};
tls_builder.min_protocol_version(Some(min_tls_version));
if let Some(identity) = builder.identity {
tls_builder.identity(identity.0);
}
tls_builder.build().map_err(error::tls)
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub enum CertificateStore {
#[default]
System,
None,
}
#[derive(Clone)]
pub struct Certificate(pub(super) native_tls::Certificate);
@@ -41,3 +82,12 @@ impl Debug for Identity {
f.debug_struct("Identity").finish_non_exhaustive()
}
}
#[derive(Debug, Copy, Clone, Default)]
#[non_exhaustive]
pub enum MinTlsVersion {
Tlsv10,
Tlsv11,
#[default]
Tlsv12,
}

View File

@@ -4,7 +4,10 @@ use std::{
};
use rustls::{
client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
client::{
danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
ClientConfig,
},
crypto::{verify_tls12_signature, verify_tls13_signature, CryptoProvider},
pki_types::{self, ServerName, UnixTime},
server::ParsedCertificate,
@@ -13,6 +16,91 @@ use rustls::{
use crate::transport::smtp::error::{self, Error};
pub(super) fn build_connector(
builder: super::TlsParametersBuilder<super::Rustls>,
) -> Result<Arc<ClientConfig>, Error> {
let just_version3 = &[&rustls::version::TLS13];
let supported_versions = match builder.min_tls_version {
MinTlsVersion::Tlsv12 => rustls::ALL_VERSIONS,
MinTlsVersion::Tlsv13 => just_version3,
};
let crypto_provider = crate::rustls_crypto::crypto_provider();
let tls = ClientConfig::builder_with_provider(Arc::clone(&crypto_provider))
.with_protocol_versions(supported_versions)
.map_err(error::tls)?;
// Build TLS config
let mut root_cert_store = RootCertStore::empty();
match builder.cert_store {
#[cfg(feature = "rustls-native-certs")]
CertificateStore::NativeCerts => {
let rustls_native_certs::CertificateResult { certs, errors, .. } =
rustls_native_certs::load_native_certs();
let errors_len = errors.len();
let (added, ignored) = store.add_parsable_certificates(certs);
#[cfg(feature = "tracing")]
tracing::debug!(
"loaded platform certs with {errors_len} failing to load, {added} valid and {ignored} ignored (invalid) certs"
);
#[cfg(not(feature = "tracing"))]
let _ = (errors_len, added, ignored);
}
#[cfg(feature = "webpki-roots")]
CertificateStore::WebpkiRoots => {
root_cert_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
}
CertificateStore::None => {}
}
for cert in builder.root_certs {
root_cert_store.add(cert.0).map_err(error::tls)?;
}
let tls = if builder.accept_invalid_certs || builder.accept_invalid_hostnames {
let verifier = InvalidCertsVerifier {
ignore_invalid_hostnames: builder.accept_invalid_hostnames,
ignore_invalid_certs: builder.accept_invalid_certs,
roots: root_cert_store,
crypto_provider,
};
tls.dangerous()
.with_custom_certificate_verifier(Arc::new(verifier))
} else {
tls.with_root_certificates(root_cert_store)
};
let tls = if let Some(identity) = builder.identity {
tls.with_client_auth_cert(identity.chain, identity.key)
.map_err(error::tls)?
} else {
tls.with_no_client_auth()
};
Ok(Arc::new(tls))
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub enum CertificateStore {
#[cfg(feature = "rustls-native-certs")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-native-certs")))]
#[cfg_attr(feature = "rustls-native-certs", default)]
NativeCerts,
#[cfg(feature = "webpki-roots")]
#[cfg_attr(docsrs, doc(cfg(feature = "webpki-roots")))]
#[cfg_attr(
all(feature = "webpki-roots", not(feature = "rustls-native-certs")),
default
)]
WebpkiRoots,
#[cfg_attr(
all(not(feature = "webpki-roots"), not(feature = "rustls-native-certs")),
default
)]
None,
}
#[derive(Clone)]
pub struct Certificate(pub(super) pki_types::CertificateDer<'static>);
@@ -76,6 +164,14 @@ impl Debug for Identity {
}
}
#[derive(Debug, Copy, Clone, Default)]
#[non_exhaustive]
pub enum MinTlsVersion {
#[default]
Tlsv12,
Tlsv13,
}
// FIXME: remove `pub(super)`
#[derive(Debug)]
pub(super) struct InvalidCertsVerifier {