From 3e9b1876d90818fb4f162d8341baa30169ea95d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Cruz?= Date: Mon, 17 Feb 2025 08:51:45 +0000 Subject: [PATCH] feat: add method to obtain TLS result (#1039) Some TLS toolkits export a result that can be checked afterwards even if the TLS negotation returned successfully. This can be used for example if you disabled certificate checks by default, but then want to check the outcome. Currently this is only supported on boring TLS. --- src/transport/smtp/client/async_connection.rs | 16 +++++++++++++ src/transport/smtp/client/async_net.rs | 24 +++++++++++++++++++ src/transport/smtp/client/connection.rs | 16 +++++++++++++ src/transport/smtp/client/net.rs | 16 +++++++++++++ 4 files changed, 72 insertions(+) diff --git a/src/transport/smtp/client/async_connection.rs b/src/transport/smtp/client/async_connection.rs index e170b52..194c638 100644 --- a/src/transport/smtp/client/async_connection.rs +++ b/src/transport/smtp/client/async_connection.rs @@ -373,6 +373,22 @@ impl AsyncSmtpConnection { self.stream.get_ref().peer_certificate() } + /// Currently this is only avaialable when using Boring TLS and + /// returns the result of the verification of the TLS certificate + /// presented by the peer, if any. Only the last error encountered + /// during verification is presented. + /// It can be useful when you don't want to fail outright the TLS + /// negotiation, for example when a self-signed certificate is + /// encountered, but still want to record metrics or log the fact. + /// When using DANE verification, the PKI root of trust moves from + /// the CAs to DNS, so self-signed certificates are permitted as long + /// as the TLSA records match the leaf or issuer certificates. + /// It cannot be called on non Boring TLS streams. + #[cfg(feature = "boring-tls")] + pub fn tls_verify_result(&self) -> Result<(), Error> { + self.stream.get_ref().tls_verify_result() + } + /// All the X509 certificates of the chain (DER encoded) #[cfg(any(feature = "rustls-tls", feature = "boring-tls"))] pub fn certificate_chain(&self) -> Result>, Error> { diff --git a/src/transport/smtp/client/async_net.rs b/src/transport/smtp/client/async_net.rs index 7343387..ed5c984 100644 --- a/src/transport/smtp/client/async_net.rs +++ b/src/transport/smtp/client/async_net.rs @@ -429,6 +429,30 @@ impl AsyncNetworkStream { } } + #[cfg(feature = "boring-tls")] + pub fn tls_verify_result(&self) -> Result<(), Error> { + match &self.inner { + #[cfg(feature = "tokio1")] + InnerAsyncNetworkStream::Tokio1Tcp(_) => { + Err(error::client("Connection is not encrypted")) + } + #[cfg(feature = "tokio1-native-tls")] + InnerAsyncNetworkStream::Tokio1NativeTls(_) => panic!("Unsupported"), + #[cfg(feature = "tokio1-rustls-tls")] + InnerAsyncNetworkStream::Tokio1RustlsTls(_) => panic!("Unsupported"), + #[cfg(feature = "tokio1-boring-tls")] + InnerAsyncNetworkStream::Tokio1BoringTls(stream) => { + stream.ssl().verify_result().map_err(error::tls) + } + #[cfg(feature = "async-std1")] + InnerAsyncNetworkStream::AsyncStd1Tcp(_) => { + Err(error::client("Connection is not encrypted")) + } + #[cfg(feature = "async-std1-rustls-tls")] + InnerAsyncNetworkStream::AsyncStd1RustlsTls(_) => panic!("Unsupported"), + InnerAsyncNetworkStream::None => panic!("InnerNetworkStream::None must never be built"), + } + } pub fn certificate_chain(&self) -> Result>, Error> { match &self.inner { #[cfg(feature = "tokio1")] diff --git a/src/transport/smtp/client/connection.rs b/src/transport/smtp/client/connection.rs index 0e6ebdb..76f778f 100644 --- a/src/transport/smtp/client/connection.rs +++ b/src/transport/smtp/client/connection.rs @@ -308,6 +308,22 @@ impl SmtpConnection { self.stream.get_ref().peer_certificate() } + /// Currently this is only avaialable when using Boring TLS and + /// returns the result of the verification of the TLS certificate + /// presented by the peer, if any. Only the last error encountered + /// during verification is presented. + /// It can be useful when you don't want to fail outright the TLS + /// negotiation, for example when a self-signed certificate is + /// encountered, but still want to record metrics or log the fact. + /// When using DANE verification, the PKI root of trust moves from + /// the CAs to DNS, so self-signed certificates are permitted as long + /// as the TLSA records match the leaf or issuer certificates. + /// It cannot be called on non Boring TLS streams. + #[cfg(feature = "boring-tls")] + pub fn tls_verify_result(&self) -> Result<(), Error> { + self.stream.get_ref().tls_verify_result() + } + /// All the X509 certificates of the chain (DER encoded) #[cfg(any(feature = "rustls-tls", feature = "boring-tls"))] pub fn certificate_chain(&self) -> Result>, Error> { diff --git a/src/transport/smtp/client/net.rs b/src/transport/smtp/client/net.rs index 4e7fae3..6be0a99 100644 --- a/src/transport/smtp/client/net.rs +++ b/src/transport/smtp/client/net.rs @@ -222,6 +222,22 @@ impl NetworkStream { } } + #[cfg(feature = "boring-tls")] + pub fn tls_verify_result(&self) -> Result<(), Error> { + match &self.inner { + InnerNetworkStream::Tcp(_) => Err(error::client("Connection is not encrypted")), + #[cfg(feature = "native-tls")] + InnerNetworkStream::NativeTls(_) => panic!("Unsupported"), + #[cfg(feature = "rustls-tls")] + InnerNetworkStream::RustlsTls(_) => panic!("Unsupported"), + #[cfg(feature = "boring-tls")] + InnerNetworkStream::BoringTls(stream) => { + stream.ssl().verify_result().map_err(error::tls) + } + InnerNetworkStream::None => panic!("InnerNetworkStream::None must never be built"), + } + } + #[cfg(any(feature = "rustls-tls", feature = "boring-tls"))] pub fn certificate_chain(&self) -> Result>, Error> { match &self.inner {