Skip to main content

rustls_connector/
lib.rs

1#![deny(missing_docs, missing_debug_implementations, unsafe_code)]
2#![warn(unreachable_pub, unused_qualifications, unused_lifetimes)]
3#![warn(
4    clippy::must_use_candidate,
5    clippy::unwrap_in_result,
6    clippy::panic_in_result_fn
7)]
8
9//! A TLS connector for rustls modelled after the `openssl` and `native-tls` APIs.
10//!
11//! Wraps [`rustls`] with a high-level [`RustlsConnector`] type that mirrors the
12//! ergonomics of `native_tls::TlsConnector`, making it straightforward to swap
13//! TLS backends in existing code.
14//!
15//! # Feature flags
16//!
17//! ## Certificate store (pick at least one)
18//!
19//! | Flag | Notes |
20//! |------|-------|
21//! | `platform-verifier` *(default)* | Platform trust store via rustls-platform-verifier |
22//! | `native-certs` | Native root certificates via rustls-native-certs |
23//! | `webpki-root-certs` | Bundled Mozilla root certificate set |
24//!
25//! ## Rustls crypto provider (at least one must be enabled)
26//!
27//! | Flag | Notes |
28//! |------|-------|
29//! | `rustls--aws_lc_rs` *(default)* | Uses aws-lc-rs |
30//! | `rustls--ring` | Uses ring (more portable) |
31//!
32//! Enabling *both* providers (which cargo feature unification can do behind your
33//! back) leaves rustls unable to pick one on its own. In that case install a
34//! process-level default with
35//! [`CryptoProvider::install_default`](rustls::crypto::CryptoProvider::install_default)
36//! before building a connector, otherwise every constructor below panics.
37//!
38//! ## Miscellaneous
39//!
40//! | Flag | Notes |
41//! |------|-------|
42//! | `futures` | Async connect via `futures-rustls` |
43//! | `logging` | Enable rustls TLS logging |
44//!
45//! # Example
46//!
47// The example needs the `platform-verifier` feature to compile; keep it visible in the rendered
48// docs either way, but don't let `cargo test --no-default-features` trip over it.
49#![cfg_attr(feature = "platform-verifier", doc = "```rust, no_run")]
50#![cfg_attr(not(feature = "platform-verifier"), doc = "```rust, ignore")]
51//! use rustls_connector::RustlsConnector;
52//!
53//! use std::{
54//!     io::{Read, Write},
55//!     net::TcpStream,
56//! };
57//!
58//! let connector = RustlsConnector::new_with_platform_verifier().unwrap();
59//! let stream = TcpStream::connect("google.com:443").unwrap();
60//! let mut stream = connector.connect("google.com", stream).unwrap();
61//!
62//! stream.write_all(b"GET / HTTP/1.0\r\n\r\n").unwrap();
63//! let mut res = vec![];
64//! stream.read_to_end(&mut res).unwrap();
65//! println!("{}", String::from_utf8_lossy(&res));
66//! ```
67
68/// Reexport of the [`rustls`](https://docs.rs/rustls) crate.
69pub use rustls;
70#[cfg(feature = "native-certs")]
71/// Reexport of the [`rustls_native_certs`](https://docs.rs/rustls-native-certs) crate.
72pub use rustls_native_certs;
73/// Reexport of the [`rustls_pki_types`](https://docs.rs/rustls-pki-types) crate.
74pub use rustls_pki_types;
75#[cfg(feature = "platform-verifier")]
76/// Reexport of the [`rustls_platform_verifier`](https://docs.rs/rustls-platform-verifier) crate.
77pub use rustls_platform_verifier;
78/// Reexport of the [`rustls_webpki`](https://docs.rs/rustls-webpki) crate.
79pub use webpki;
80#[cfg(feature = "webpki-root-certs")]
81/// Reexport of the [`webpki_root_certs`](https://docs.rs/webpki-root-certs) crate.
82pub use webpki_root_certs;
83
84#[cfg(feature = "futures")]
85use futures_io::{AsyncRead, AsyncWrite};
86use rustls::{
87    ClientConfig, ClientConnection, ConfigBuilder, RootCertStore, StreamOwned,
88    client::WantsClientCert,
89};
90use rustls_pki_types::{CertificateDer, PrivateKeyDer, ServerName};
91
92use std::{
93    error::Error,
94    fmt,
95    io::{self, Read, Write},
96    sync::Arc,
97};
98
99/// A rustls client TLS stream wrapping an underlying synchronous I/O stream `S`.
100pub type TlsStream<S> = StreamOwned<ClientConnection, S>;
101
102#[cfg(feature = "futures")]
103/// A rustls client TLS stream wrapping an underlying async I/O stream `S`.
104pub type AsyncTlsStream<S> = futures_rustls::client::TlsStream<S>;
105
106/// Configuration helper for [`RustlsConnector`]
107#[derive(Clone, Default, Debug)]
108pub struct RustlsConnectorConfig {
109    store: Vec<CertificateDer<'static>>,
110    #[cfg(feature = "platform-verifier")]
111    platform_verifier: bool,
112}
113
114impl RustlsConnectorConfig {
115    #[cfg(feature = "webpki-root-certs")]
116    /// Create a new [`RustlsConnectorConfig`] using the webpki-root-certs (requires webpki-root-certs feature enabled)
117    #[must_use]
118    pub fn new_with_webpki_root_certs() -> Self {
119        Self::default().with_webpki_root_certs()
120    }
121
122    #[cfg(feature = "platform-verifier")]
123    /// Create a new [`RustlsConnectorConfig`] using the rustls-platform-verifier mechanism (requires platform-verifier feature enabled)
124    #[must_use]
125    pub fn new_with_platform_verifier() -> Self {
126        Self::default().with_platform_verifier()
127    }
128
129    #[cfg(feature = "native-certs")]
130    /// Create a new [`RustlsConnectorConfig`] using the system certs (requires native-certs feature enabled)
131    ///
132    /// # Errors
133    ///
134    /// Returns an error if we fail to load the native certs.
135    pub fn new_with_native_certs() -> io::Result<Self> {
136        Self::default().with_native_certs()
137    }
138
139    /// Queue the given DER-encoded certificates as additional roots.
140    ///
141    /// Parsing is deferred until the connector is built. Certificates that fail to parse are then
142    /// skipped in a best-effort fashion, because large collections of root certificates often
143    /// include ancient or syntactically invalid certificates.
144    ///
145    /// The one exception is [`with_platform_verifier`](Self::with_platform_verifier): the platform
146    /// verifier rejects unparsable extra roots outright, so a single bad certificate makes
147    /// building the connector fail.
148    pub fn add_parsable_certificates(&mut self, mut der_certs: Vec<CertificateDer<'static>>) {
149        self.store.append(&mut der_certs)
150    }
151
152    /// Queue the given DER-encoded certificates as additional roots.
153    ///
154    /// Chainable variant of [`add_parsable_certificates`](Self::add_parsable_certificates); see it
155    /// for the parsing semantics.
156    #[must_use]
157    pub fn with_parsable_certificates(mut self, der_certs: Vec<CertificateDer<'static>>) -> Self {
158        self.add_parsable_certificates(der_certs);
159        self
160    }
161
162    #[cfg(feature = "webpki-root-certs")]
163    /// Add certs from webpki-root-certs (requires webpki-root-certs feature enabled)
164    #[must_use]
165    pub fn with_webpki_root_certs(mut self) -> Self {
166        self.add_parsable_certificates(webpki_root_certs::TLS_SERVER_ROOT_CERTS.to_vec());
167        self
168    }
169
170    #[cfg(feature = "platform-verifier")]
171    /// Use the rustls-platform-verifier mechanism (requires platform-verifier feature enabled)
172    #[must_use]
173    pub fn with_platform_verifier(mut self) -> Self {
174        self.platform_verifier = true;
175        self
176    }
177
178    #[cfg(feature = "native-certs")]
179    /// Add the system certs (requires native-certs feature enabled)
180    ///
181    /// # Errors
182    ///
183    /// Returns an error if we fail to load the native certs.
184    pub fn with_native_certs(mut self) -> io::Result<Self> {
185        let certs_result = rustls_native_certs::load_native_certs();
186        for err in certs_result.errors {
187            log::warn!("Got error while loading some native certificates: {err:?}");
188        }
189        if certs_result.certs.is_empty() {
190            return Err(io::Error::other(
191                "Could not load any valid native certificates",
192            ));
193        }
194        self.add_parsable_certificates(certs_result.certs);
195        Ok(self)
196    }
197
198    fn builder(self) -> io::Result<ConfigBuilder<ClientConfig, WantsClientCert>> {
199        let builder = ClientConfig::builder();
200        #[cfg(feature = "platform-verifier")]
201        {
202            if self.platform_verifier {
203                let provider = builder.crypto_provider().clone();
204                // `rustls-platform-verifier` has no `new_with_extra_roots` on Android: its trust
205                // decisions are delegated to the platform and cannot be augmented from Rust.
206                // Refuse rather than silently trusting fewer roots than the caller asked for.
207                #[cfg(target_os = "android")]
208                let verifier = {
209                    if !self.store.is_empty() {
210                        return Err(io::Error::other(
211                            "extra root certificates cannot be combined with the platform verifier on Android",
212                        ));
213                    }
214                    rustls_platform_verifier::Verifier::new(provider)
215                };
216                #[cfg(not(target_os = "android"))]
217                let verifier =
218                    rustls_platform_verifier::Verifier::new_with_extra_roots(self.store, provider);
219                let verifier =
220                    verifier.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
221                // `.dangerous()` is the rustls API for supplying a custom verifier;
222                // it does not bypass verification — `Verifier` delegates to the OS store.
223                return Ok(builder
224                    .dangerous()
225                    .with_custom_certificate_verifier(Arc::new(verifier)));
226            }
227        }
228        let mut store = RootCertStore::empty();
229        let (_, ignored) = store.add_parsable_certificates(self.store);
230        if ignored > 0 {
231            log::warn!("{ignored} CA root certificates were ignored due to errors");
232        }
233        if store.is_empty() {
234            return Err(io::Error::other("Could not load any valid certificates"));
235        }
236        Ok(builder.with_root_certificates(store))
237    }
238
239    /// Create a new [`RustlsConnector`] from this config and no client certificate
240    ///
241    /// # Errors
242    ///
243    /// Returns an error if we fail to init our verifier or if no valid root certificate could be
244    /// loaded
245    ///
246    /// # Panics
247    ///
248    /// Panics if rustls cannot determine a crypto provider, i.e. if no process-level default has
249    /// been installed and the enabled crate features select zero or more than one provider.
250    pub fn connector_with_no_client_auth(self) -> io::Result<RustlsConnector> {
251        Ok(self.builder()?.with_no_client_auth().into())
252    }
253
254    /// Create a new [`RustlsConnector`] from this config and the given client certificate
255    ///
256    /// cert_chain is a vector of DER-encoded certificates. key_der is a DER-encoded RSA, ECDSA, or
257    /// Ed25519 private key.
258    ///
259    /// # Errors
260    ///
261    /// Returns an error if we fail to init our verifier, if no valid root certificate could be
262    /// loaded, or if key_der is invalid.
263    ///
264    /// # Panics
265    ///
266    /// Panics if rustls cannot determine a crypto provider, i.e. if no process-level default has
267    /// been installed and the enabled crate features select zero or more than one provider.
268    pub fn connector_with_single_cert(
269        self,
270        cert_chain: Vec<CertificateDer<'static>>,
271        key_der: PrivateKeyDer<'static>,
272    ) -> io::Result<RustlsConnector> {
273        Ok(self
274            .builder()?
275            .with_client_auth_cert(cert_chain, key_der)
276            .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?
277            .into())
278    }
279}
280
281/// A rustls TLS connector ready to perform TLS handshakes.
282///
283/// Wraps an [`Arc<ClientConfig>`] and can be built from a [`RustlsConnectorConfig`] via
284/// [`connector_with_no_client_auth`](RustlsConnectorConfig::connector_with_no_client_auth) or
285/// [`connector_with_single_cert`](RustlsConnectorConfig::connector_with_single_cert), or
286/// directly from a `ClientConfig` via the [`From`] impl.
287#[derive(Clone, Debug)]
288pub struct RustlsConnector(Arc<ClientConfig>);
289
290impl From<ClientConfig> for RustlsConnector {
291    fn from(config: ClientConfig) -> Self {
292        Arc::new(config).into()
293    }
294}
295
296impl From<Arc<ClientConfig>> for RustlsConnector {
297    fn from(config: Arc<ClientConfig>) -> Self {
298        Self(config)
299    }
300}
301
302impl RustlsConnector {
303    #[cfg(feature = "webpki-root-certs")]
304    /// Create a new RustlsConnector using the webpki-root certs (requires webpki-root-certs feature enabled)
305    ///
306    /// # Errors
307    ///
308    /// Returns an error if we fail to init our verifier
309    ///
310    /// # Panics
311    ///
312    /// See [`connector_with_no_client_auth`](RustlsConnectorConfig::connector_with_no_client_auth).
313    pub fn new_with_webpki_root_certs() -> io::Result<Self> {
314        RustlsConnectorConfig::new_with_webpki_root_certs().connector_with_no_client_auth()
315    }
316
317    #[cfg(feature = "platform-verifier")]
318    /// Create a new [`RustlsConnector`] using the rustls-platform-verifier mechanism (requires platform-verifier feature enabled)
319    ///
320    /// # Errors
321    ///
322    /// Returns an error if we fail to init our verifier
323    ///
324    /// # Panics
325    ///
326    /// See [`connector_with_no_client_auth`](RustlsConnectorConfig::connector_with_no_client_auth).
327    pub fn new_with_platform_verifier() -> io::Result<Self> {
328        RustlsConnectorConfig::new_with_platform_verifier().connector_with_no_client_auth()
329    }
330
331    #[cfg(feature = "native-certs")]
332    /// Create a new [`RustlsConnector`] using the system certs (requires native-certs feature enabled)
333    ///
334    /// # Errors
335    ///
336    /// Returns an error if we fail to load the native certs or to init our verifier.
337    ///
338    /// # Panics
339    ///
340    /// See [`connector_with_no_client_auth`](RustlsConnectorConfig::connector_with_no_client_auth).
341    pub fn new_with_native_certs() -> io::Result<Self> {
342        RustlsConnectorConfig::new_with_native_certs()?.connector_with_no_client_auth()
343    }
344
345    /// Connect to the given host
346    ///
347    /// # Errors
348    ///
349    /// Returns a [`HandshakeError`] containing either the current state of the handshake or the
350    /// failure when we couldn't complete the handshake
351    #[allow(clippy::result_large_err)]
352    pub fn connect<S: Read + Write + Send + 'static>(
353        &self,
354        domain: &str,
355        stream: S,
356    ) -> Result<TlsStream<S>, HandshakeError<S>> {
357        let session = ClientConnection::new(
358            self.0.clone(),
359            server_name(domain).map_err(HandshakeError::Failure)?,
360        )
361        .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
362        MidHandshakeTlsStream { session, stream }.handshake()
363    }
364
365    #[cfg(feature = "futures")]
366    /// Connect to the given host asynchronously
367    ///
368    /// # Errors
369    ///
370    /// Returns a [`io::Error`] containing the failure when we couldn't complete the TLS handshake
371    pub async fn connect_async<S: AsyncRead + AsyncWrite + Send + Unpin + 'static>(
372        &self,
373        domain: &str,
374        stream: S,
375    ) -> io::Result<AsyncTlsStream<S>> {
376        futures_rustls::TlsConnector::from(self.0.clone())
377            .connect(server_name(domain)?, stream)
378            .await
379    }
380}
381
382fn server_name(domain: &str) -> io::Result<ServerName<'static>> {
383    Ok(ServerName::try_from(domain)
384        .map_err(|err| {
385            io::Error::new(
386                io::ErrorKind::InvalidData,
387                format!("Invalid domain name: {err:?}"),
388            )
389        })?
390        .to_owned())
391}
392
393/// A TLS stream which has been interrupted during the handshake
394#[derive(Debug)]
395pub struct MidHandshakeTlsStream<S: Read + Write> {
396    session: ClientConnection,
397    stream: S,
398}
399
400impl<S: Read + Write> MidHandshakeTlsStream<S> {
401    /// Get a reference to the inner stream
402    pub fn get_ref(&self) -> &S {
403        &self.stream
404    }
405
406    /// Get a mutable reference to the inner stream
407    pub fn get_mut(&mut self) -> &mut S {
408        &mut self.stream
409    }
410}
411
412impl<S: Read + Write + Send + 'static> MidHandshakeTlsStream<S> {
413    /// Retry the handshake
414    ///
415    /// # Errors
416    ///
417    /// Returns a [`HandshakeError`] containing either the current state of the handshake or the
418    /// failure when we couldn't complete the handshake
419    #[allow(clippy::result_large_err)]
420    pub fn handshake(mut self) -> Result<TlsStream<S>, HandshakeError<S>> {
421        if let Err(e) = self.session.complete_io(&mut self.stream) {
422            if e.kind() == io::ErrorKind::WouldBlock {
423                if self.session.is_handshaking() {
424                    return Err(HandshakeError::WouldBlock(self));
425                }
426            } else {
427                return Err(e.into());
428            }
429        }
430        Ok(TlsStream::new(self.session, self.stream))
431    }
432}
433
434impl<S: Read + Write> fmt::Display for MidHandshakeTlsStream<S> {
435    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
436        f.write_str("MidHandshakeTlsStream")
437    }
438}
439
440/// An error returned while performing the handshake
441#[allow(clippy::large_enum_variant)]
442pub enum HandshakeError<S: Read + Write + Send + 'static> {
443    /// We hit WouldBlock during handshake.
444    /// Note that this is not a critical failure, you should be able to call handshake again once the stream is ready to perform I/O.
445    WouldBlock(MidHandshakeTlsStream<S>),
446    /// We hit a critical failure.
447    Failure(io::Error),
448}
449
450impl<S: Read + Write + Send + 'static> fmt::Display for HandshakeError<S> {
451    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
452        match self {
453            HandshakeError::WouldBlock(_) => f.write_str("WouldBlock hit during handshake"),
454            HandshakeError::Failure(err) => f.write_fmt(format_args!("IO error: {err}")),
455        }
456    }
457}
458
459impl<S: Read + Write + Send + 'static> fmt::Debug for HandshakeError<S> {
460    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
461        let mut d = f.debug_tuple("HandshakeError");
462        match self {
463            HandshakeError::WouldBlock(_) => d.field(&"WouldBlock"),
464            HandshakeError::Failure(err) => d.field(&err),
465        }
466        .finish()
467    }
468}
469
470impl<S: Read + Write + Send + 'static> Error for HandshakeError<S> {
471    fn source(&self) -> Option<&(dyn Error + 'static)> {
472        match self {
473            HandshakeError::Failure(err) => Some(err),
474            _ => None,
475        }
476    }
477}
478
479impl<S: Read + Send + Write + 'static> From<io::Error> for HandshakeError<S> {
480    fn from(err: io::Error) -> Self {
481        HandshakeError::Failure(err)
482    }
483}
484
485#[cfg(test)]
486mod tests {
487    use super::*;
488
489    #[test]
490    fn empty_config_fails() {
491        assert!(
492            RustlsConnectorConfig::default()
493                .connector_with_no_client_auth()
494                .is_err()
495        );
496    }
497
498    #[test]
499    #[cfg(feature = "webpki-root-certs")]
500    fn webpki_root_certs_connector_builds() {
501        RustlsConnector::new_with_webpki_root_certs().unwrap();
502    }
503
504    #[test]
505    #[cfg(feature = "platform-verifier")]
506    fn platform_verifier_connector_builds() {
507        RustlsConnector::new_with_platform_verifier().unwrap();
508    }
509
510    #[test]
511    #[cfg(feature = "webpki-root-certs")]
512    fn invalid_certificates_are_skipped() {
513        let mut certs = vec![CertificateDer::from(vec![0x00, 0x01, 0x02])];
514        certs.extend(webpki_root_certs::TLS_SERVER_ROOT_CERTS.iter().cloned());
515        RustlsConnectorConfig::default()
516            .with_parsable_certificates(certs)
517            .connector_with_no_client_auth()
518            .unwrap();
519    }
520
521    #[test]
522    #[cfg(feature = "platform-verifier")]
523    fn platform_verifier_rejects_invalid_extra_roots() {
524        assert!(
525            RustlsConnectorConfig::new_with_platform_verifier()
526                .with_parsable_certificates(vec![CertificateDer::from(vec![0x00, 0x01, 0x02])])
527                .connector_with_no_client_auth()
528                .is_err()
529        );
530    }
531
532    #[test]
533    fn handshake_error_failure_display() {
534        let err: HandshakeError<std::net::TcpStream> =
535            HandshakeError::Failure(io::Error::other("test error"));
536        assert!(err.to_string().contains("test error"));
537        assert!(format!("{err:?}").contains("test error"));
538        assert!(err.source().is_some());
539    }
540}