Allow configuring a custom root certificate in TlsParametersBuilder

This commit is contained in:
Paolo Barbolini
2020-09-28 18:23:15 +02:00
parent e00eff8b2a
commit 6526eff5b2
5 changed files with 127 additions and 9 deletions

View File

@@ -103,6 +103,10 @@ required-features = ["smtp-transport", "native-tls"]
name = "smtp_starttls"
required-features = ["smtp-transport", "native-tls"]
[[example]]
name = "smtp_selfsigned"
required-features = ["smtp-transport", "native-tls"]
[[example]]
name = "tokio02_smtp_tls"
required-features = ["smtp-transport", "tokio02", "tokio02-native-tls"]

View File

@@ -0,0 +1,40 @@
use std::fs;
use lettre::{
transport::smtp::authentication::Credentials,
transport::smtp::client::{Certificate, Tls, TlsParameters},
Message, SmtpTransport, Transport,
};
fn main() {
tracing_subscriber::fmt::init();
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.body("Be happy!")
.unwrap();
let pem_cert = fs::read("certificate.pem").unwrap();
let cert = Certificate::from_pem(&pem_cert).unwrap();
let mut tls = TlsParameters::builder("smtp.server.com".into());
tls.add_root_certificate(cert);
let tls = tls.build().unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
// Open a remote connection to gmail
let mailer = SmtpTransport::builder_dangerous("smtp.server.com")
.port(465)
.tls(Tls::Wrapper(tls))
.credentials(creds)
.build();
// Send the email
match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e),
}
}

View File

@@ -34,7 +34,7 @@ pub(super) use self::tls::InnerTlsParameters;
pub use self::{
connection::SmtpConnection,
mock::MockStream,
tls::{Tls, TlsParameters, TlsParametersBuilder},
tls::{Certificate, Tls, TlsParameters, TlsParametersBuilder},
};
#[cfg(feature = "tokio02")]

View File

@@ -1,18 +1,15 @@
#[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::{
Certificate, ClientConfig, RootCertStore, ServerCertVerified, ServerCertVerifier, TLSError,
};
use rustls::{ClientConfig, RootCertStore, ServerCertVerified, ServerCertVerifier, TLSError};
#[cfg(feature = "rustls-tls")]
use webpki::DNSNameRef;
use crate::transport::smtp::error::Error;
/// Accepted protocols by default.
/// This removes TLS 1.0 and 1.1 compared to tls-native defaults.
// This is also rustls' default behavior
@@ -46,9 +43,10 @@ pub struct TlsParameters {
}
/// Builder for `TlsParameters`
#[derive(Debug, Clone)]
#[derive(Clone)]
pub struct TlsParametersBuilder {
domain: String,
root_certs: Vec<Certificate>,
accept_invalid_hostnames: bool,
accept_invalid_certs: bool,
}
@@ -58,11 +56,20 @@ impl TlsParametersBuilder {
pub fn new(domain: String) -> Self {
Self {
domain,
root_certs: Vec::new(),
accept_invalid_hostnames: false,
accept_invalid_certs: false,
}
}
/// Add a custom root certificate
///
/// Can be used to safely connect to a server using a self signed certificate, for example.
pub fn add_root_certificate(&mut self, cert: Certificate) -> &mut Self {
self.root_certs.push(cert);
self
}
/// Controls whether certificates with an invalid hostname are accepted
///
/// Defaults to `false`.
@@ -130,8 +137,13 @@ impl TlsParametersBuilder {
#[cfg(feature = "native-tls")]
pub fn build_native(self) -> Result<TlsParameters, Error> {
let mut tls_builder = TlsConnector::builder();
for cert in self.root_certs {
tls_builder.add_root_certificate(cert.native_tls);
}
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 {
@@ -146,10 +158,19 @@ impl TlsParametersBuilder {
use webpki_roots::TLS_SERVER_ROOTS;
let mut tls = ClientConfig::new();
for cert in self.root_certs {
for rustls_cert in cert.rustls {
tls.root_store
.add(&rustls_cert)
.map_err(|_| Error::InvalidCertificate)?;
}
}
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),
@@ -195,6 +216,55 @@ impl TlsParameters {
}
}
/// A client certificate that can be used with [`TlsParametersBuilder::add_root_certificate`]
#[derive(Clone)]
#[allow(missing_copy_implementations)]
pub struct Certificate {
#[cfg(feature = "native-tls")]
native_tls: native_tls::Certificate,
#[cfg(feature = "rustls-tls")]
rustls: Vec<rustls::Certificate>,
}
impl Certificate {
/// Create a `Certificate` from a DER encoded certificate
pub fn from_der(der: Vec<u8>) -> Result<Self, Error> {
#[cfg(feature = "native-tls")]
let native_tls_cert =
native_tls::Certificate::from_der(&der).map_err(|_| Error::InvalidCertificate)?;
Ok(Self {
#[cfg(feature = "native-tls")]
native_tls: native_tls_cert,
#[cfg(feature = "rustls-tls")]
rustls: vec![rustls::Certificate(der)],
})
}
/// Create a `Certificate` from a PEM encoded certificate
pub fn from_pem(pem: &[u8]) -> Result<Self, Error> {
#[cfg(feature = "native-tls")]
let native_tls_cert =
native_tls::Certificate::from_pem(pem).map_err(|_| Error::InvalidCertificate)?;
#[cfg(feature = "rustls-tls")]
let rustls_cert = {
use rustls::internal::pemfile;
use std::io::Cursor;
let mut pem = Cursor::new(pem);
pemfile::certs(&mut pem).map_err(|_| Error::InvalidCertificate)?
};
Ok(Self {
#[cfg(feature = "native-tls")]
native_tls: native_tls_cert,
#[cfg(feature = "rustls-tls")]
rustls: rustls_cert,
})
}
}
#[cfg(feature = "rustls-tls")]
struct InvalidCertsVerifier;
@@ -203,7 +273,7 @@ impl ServerCertVerifier for InvalidCertsVerifier {
fn verify_server_cert(
&self,
_roots: &RootCertStore,
_presented_certs: &[Certificate],
_presented_certs: &[rustls::Certificate],
_dns_name: DNSNameRef<'_>,
_ocsp_response: &[u8],
) -> Result<ServerCertVerified, TLSError> {

View File

@@ -41,6 +41,8 @@ pub enum Error {
/// Invalid hostname
#[cfg(feature = "rustls-tls")]
InvalidDNSName(webpki::InvalidDNSNameError),
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
InvalidCertificate,
#[cfg(feature = "r2d2")]
Pool(r2d2::Error),
}
@@ -69,6 +71,8 @@ impl Display for Error {
Parsing(ref err) => fmt.write_str(err.description()),
#[cfg(feature = "rustls-tls")]
InvalidDNSName(ref err) => err.fmt(fmt),
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
InvalidCertificate => fmt.write_str("invalid certificate"),
#[cfg(feature = "r2d2")]
Pool(ref err) => err.fmt(fmt),
}