From 952c1b39dfa8acad737f6cca4240e82d6e4f97af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phan=20Kochen?= Date: Tue, 14 Feb 2023 18:11:42 +0100 Subject: [PATCH] Add support for rustls-native-certs (#843) --- .github/workflows/test.yml | 4 +- Cargo.toml | 1 + src/transport/smtp/client/mod.rs | 2 +- src/transport/smtp/client/tls.rs | 133 +++++++++++++++++++++++++++---- 4 files changed, 123 insertions(+), 17 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4ac1727..81eeb73 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -122,10 +122,10 @@ jobs: run: cargo test - name: Test with all features (-native-tls) - run: cargo test --no-default-features --features async-std,async-std1,async-std1-rustls-tls,async-trait,base64,boring,boring-tls,builder,dkim,ed25519-dalek,email-encoding,fastrand,file-transport,file-transport-envelope,futures-io,futures-rustls,futures-util,hostname,httpdate,mime,mime03,nom,once_cell,pool,quoted_printable,rsa,rustls,rustls-pemfile,rustls-tls,sendmail-transport,serde,serde_json,sha2,smtp-transport,socket2,tokio1,tokio1-boring-tls,tokio1-rustls-tls,tokio1_boring,tokio1_crate,tokio1_rustls,tracing,uuid,webpki-roots + run: cargo test --no-default-features --features async-std,async-std1,async-std1-rustls-tls,async-trait,base64,boring,boring-tls,builder,dkim,ed25519-dalek,email-encoding,fastrand,file-transport,file-transport-envelope,futures-io,futures-rustls,futures-util,hostname,httpdate,mime,mime03,nom,once_cell,pool,quoted_printable,rsa,rustls,rustls-native-certs,rustls-pemfile,rustls-tls,sendmail-transport,serde,serde_json,sha2,smtp-transport,socket2,tokio1,tokio1-boring-tls,tokio1-rustls-tls,tokio1_boring,tokio1_crate,tokio1_rustls,tracing,uuid,webpki-roots - name: Test with all features (-boring-tls) - run: cargo test --no-default-features --features async-std,async-std1,async-std1-rustls-tls,async-trait,base64,builder,dkim,ed25519-dalek,email-encoding,fastrand,file-transport,file-transport-envelope,futures-io,futures-rustls,futures-util,hostname,httpdate,mime,mime03,native-tls,nom,once_cell,pool,quoted_printable,rsa,rustls,rustls-pemfile,rustls-tls,sendmail-transport,serde,serde_json,sha2,smtp-transport,socket2,tokio1,tokio1-native-tls,tokio1-rustls-tls,tokio1_crate,tokio1_native_tls_crate,tokio1_rustls,tracing,uuid,webpki-roots + run: cargo test --no-default-features --features async-std,async-std1,async-std1-rustls-tls,async-trait,base64,builder,dkim,ed25519-dalek,email-encoding,fastrand,file-transport,file-transport-envelope,futures-io,futures-rustls,futures-util,hostname,httpdate,mime,mime03,native-tls,nom,once_cell,pool,quoted_printable,rsa,rustls,rustls-native-certs,rustls-pemfile,rustls-tls,sendmail-transport,serde,serde_json,sha2,smtp-transport,socket2,tokio1,tokio1-native-tls,tokio1-rustls-tls,tokio1_crate,tokio1_native_tls_crate,tokio1_rustls,tracing,uuid,webpki-roots # coverage: # name: Coverage diff --git a/Cargo.toml b/Cargo.toml index af0bd99..bf0e9a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ socket2 = { version = "0.4.4", optional = true } native-tls = { version = "0.2", optional = true } # feature rustls = { version = "0.20", features = ["dangerous_configuration"], optional = true } rustls-pemfile = { version = "1", optional = true } +rustls-native-certs = { version = "0.6.2", optional = true } webpki-roots = { version = "0.22", optional = true } boring = { version = "2.0.0", optional = true } diff --git a/src/transport/smtp/client/mod.rs b/src/transport/smtp/client/mod.rs index 125b5a6..320ba5a 100644 --- a/src/transport/smtp/client/mod.rs +++ b/src/transport/smtp/client/mod.rs @@ -38,7 +38,7 @@ pub(super) use self::tls::InnerTlsParameters; pub use self::tls::TlsVersion; pub use self::{ connection::SmtpConnection, - tls::{Certificate, Tls, TlsParameters, TlsParametersBuilder}, + tls::{Certificate, CertificateStore, Tls, TlsParameters, TlsParametersBuilder}, }; #[cfg(any(feature = "tokio1", feature = "async-std1"))] diff --git a/src/transport/smtp/client/tls.rs b/src/transport/smtp/client/tls.rs index 3401d44..b15c793 100644 --- a/src/transport/smtp/client/tls.rs +++ b/src/transport/smtp/client/tls.rs @@ -3,13 +3,16 @@ use std::fmt::{self, Debug}; use std::{sync::Arc, time::SystemTime}; #[cfg(feature = "boring-tls")] -use boring::ssl::{SslConnector, SslVersion}; +use boring::{ + ssl::{SslConnector, SslVersion}, + x509::store::X509StoreBuilder, +}; #[cfg(feature = "native-tls")] use native_tls::{Protocol, TlsConnector}; #[cfg(feature = "rustls-tls")] use rustls::{ client::{ServerCertVerified, ServerCertVerifier, WebPkiVerifier}, - ClientConfig, Error as TlsError, OwnedTrustAnchor, RootCertStore, ServerName, + ClientConfig, Error as TlsError, RootCertStore, ServerName, }; #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))] @@ -84,16 +87,45 @@ impl Debug for Tls { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self { Self::None => f.pad("None"), - #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))] + #[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))] Self::Opportunistic(_) => f.pad("Opportunistic"), - #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))] + #[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))] Self::Required(_) => f.pad("Required"), - #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))] + #[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))] Self::Wrapper(_) => f.pad("Wrapper"), } } } +/// Source for the base set of root certificates to trust. +#[allow(missing_copy_implementations)] +#[derive(Clone, Debug)] +pub enum CertificateStore { + /// Use the default for the TLS backend. + /// + /// For native-tls, this will use the system certificate store on Windows, the keychain on + /// macOS, and OpenSSL directories on Linux (usually `/etc/ssl`). + /// + /// For rustls, this will also use the the system store if the `rustls-native-certs` feature is + /// enabled, or will fall back to `webpki-roots`. + /// + /// The boring-tls backend uses the same logic as OpenSSL on all platforms. + Default, + /// Use a hardcoded set of Mozilla roots via the `webpki-roots` crate. + /// + /// This option is only available in the rustls backend. + #[cfg(feature = "webpki-roots")] + WebpkiRoots, + /// Don't use any system certificates. + None, +} + +impl Default for CertificateStore { + fn default() -> Self { + CertificateStore::Default + } +} + /// Parameters to use for secure clients #[derive(Clone)] pub struct TlsParameters { @@ -108,6 +140,7 @@ pub struct TlsParameters { #[derive(Debug, Clone)] pub struct TlsParametersBuilder { domain: String, + cert_store: CertificateStore, root_certs: Vec, accept_invalid_hostnames: bool, accept_invalid_certs: bool, @@ -120,6 +153,7 @@ impl TlsParametersBuilder { pub fn new(domain: String) -> Self { Self { domain, + cert_store: CertificateStore::Default, root_certs: Vec::new(), accept_invalid_hostnames: false, accept_invalid_certs: false, @@ -128,6 +162,12 @@ impl TlsParametersBuilder { } } + /// Set the source for the base set of root certificates to trust. + pub fn certificate_store(mut self, cert_store: CertificateStore) -> Self { + self.cert_store = cert_store; + self + } + /// Add a custom root certificate /// /// Can be used to safely connect to a server using a self signed certificate, for example. @@ -208,6 +248,18 @@ impl TlsParametersBuilder { pub fn build_native(self) -> Result { let mut tls_builder = TlsConnector::builder(); + match self.cert_store { + CertificateStore::Default => {} + CertificateStore::None => { + tls_builder.disable_built_in_roots(true); + } + #[allow(unreachable_patterns)] + other => { + return Err(error::tls(format!( + "{other:?} is not supported in native tls" + ))) + } + } for cert in self.root_certs { tls_builder.add_root_certificate(cert.native_tls); } @@ -246,6 +298,21 @@ impl TlsParametersBuilder { if self.accept_invalid_certs { tls_builder.set_verify(SslVerifyMode::NONE); } else { + match self.cert_store { + CertificateStore::Default => {} + CertificateStore::None => { + // Replace the default store with an empty store. + tls_builder + .set_cert_store(X509StoreBuilder::new().map_err(error::tls)?.build()); + } + #[allow(unreachable_patterns)] + other => { + return Err(error::tls(format!( + "{other:?} is not supported in boring tls" + ))) + } + } + let cert_store = tls_builder.cert_store_mut(); for cert in self.root_certs { @@ -299,20 +366,58 @@ impl TlsParametersBuilder { tls.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 mut valid_count = 0; + let mut invalid_count = 0; + for cert in native_certs { + match store.add(&rustls::Certificate(cert.0)) { + Ok(_) => valid_count += 1, + Err(err) => { + #[cfg(feature = "tracing")] + tracing::debug!("certificate parsing failed: {:?}", err); + invalid_count += 1; + } + } + } + #[cfg(feature = "tracing")] + tracing::debug!( + "loaded platform certs with {valid_count} valid and {invalid_count} invalid certs" + ); + Ok(()) + } + + #[cfg(feature = "webpki-roots")] + fn load_webpki_roots(store: &mut RootCertStore) { + store.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.0.iter().map(|ta| { + rustls::OwnedTrustAnchor::from_subject_spki_name_constraints( + ta.subject, + ta.spki, + ta.name_constraints, + ) + })); + } + + match self.cert_store { + CertificateStore::Default => { + #[cfg(feature = "rustls-native-certs")] + load_native_roots(&mut root_cert_store)?; + #[cfg(all(not(feature = "rustls-native-certs"), feature = "webpki-roots"))] + load_webpki_roots(&mut root_cert_store); + } + #[cfg(feature = "webpki-roots")] + CertificateStore::WebpkiRoots => { + load_webpki_roots(&mut root_cert_store); + } + CertificateStore::None => {} + } for cert in self.root_certs { for rustls_cert in cert.rustls { root_cert_store.add(&rustls_cert).map_err(error::tls)?; } } - root_cert_store.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.0.iter().map( - |ta| { - OwnedTrustAnchor::from_subject_spki_name_constraints( - ta.subject, - ta.spki, - ta.name_constraints, - ) - }, - )); tls.with_custom_certificate_verifier(Arc::new(WebPkiVerifier::new( root_cert_store,