Skip to main content

grpc_core/
tls.rs

1//! Synchronous rustls transports with authenticated HTTP/2 ALPN negotiation.
2//!
3//! Constructors perform no I/O. Call `handshake` explicitly or let the first
4//! nonempty read/write complete it. Nonblocking callers retry after readiness;
5//! `handshake_until` bounds negotiation by an absolute deadline and restores
6//! the underlying transport timeouts before returning.
7
8use std::{
9    io::{self, Read, Write},
10    sync::Arc,
11    time::{Duration, Instant},
12};
13
14use crate::transport::SyncTransport;
15
16/// Trusted roots and HTTP/2 ALPN policy for synchronous TLS clients.
17#[derive(Clone)]
18pub struct TlsClientConfig {
19    pub(crate) config: Arc<rustls::ClientConfig>,
20}
21
22impl TlsClientConfig {
23    /// Builds a TLS 1.3 client with certificate and server-name verification.
24    ///
25    /// # Errors
26    /// Returns an error if the TLS provider cannot support TLS 1.3.
27    pub fn new(roots: rustls::RootCertStore) -> io::Result<Self> {
28        let config = rustls::ClientConfig::builder_with_provider(Arc::new(
29            rustls::crypto::ring::default_provider(),
30        ))
31        .with_protocol_versions(&[&rustls::version::TLS13])
32        .map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?
33        .with_root_certificates(roots)
34        .with_no_client_auth();
35        Ok(Self::from_rustls(config))
36    }
37
38    /// Uses a caller's certificate policy, requiring HTTP/2 and disabling 0-RTT.
39    ///
40    /// The caller remains responsible for the supplied configuration's verifier,
41    /// protocol versions, and cryptographic provider.
42    pub fn from_rustls(mut config: rustls::ClientConfig) -> Self {
43        config.alpn_protocols = vec![b"h2".to_vec()];
44        config.enable_early_data = false;
45        Self {
46            config: Arc::new(config),
47        }
48    }
49
50    /// Returns the immutable rustls configuration.
51    pub fn get_ref(&self) -> &rustls::ClientConfig {
52        &self.config
53    }
54}
55
56/// Server identity and HTTP/2 ALPN policy for synchronous TLS servers.
57#[derive(Clone)]
58pub struct TlsServerConfig {
59    pub(crate) config: Arc<rustls::ServerConfig>,
60}
61
62impl TlsServerConfig {
63    /// Builds a TLS 1.3 server from a DER certificate chain and private key.
64    ///
65    /// Clients authenticate the server; client certificates are not required.
66    ///
67    /// # Errors
68    /// Returns an error for an unsupported provider, invalid identity, or a
69    /// certificate and private key that do not match.
70    pub fn new(
71        cert_chain: Vec<rustls::pki_types::CertificateDer<'static>>,
72        private_key: rustls::pki_types::PrivateKeyDer<'static>,
73    ) -> io::Result<Self> {
74        let config = rustls::ServerConfig::builder_with_provider(Arc::new(
75            rustls::crypto::ring::default_provider(),
76        ))
77        .with_protocol_versions(&[&rustls::version::TLS13])
78        .map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?
79        .with_no_client_auth()
80        .with_single_cert(cert_chain, private_key)
81        .map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?;
82        Ok(Self::from_rustls(config))
83    }
84
85    /// Uses a caller's certificate policy, requiring HTTP/2 and disabling 0-RTT.
86    ///
87    /// The caller remains responsible for the supplied configuration's client
88    /// authentication, protocol versions, and cryptographic provider.
89    pub fn from_rustls(mut config: rustls::ServerConfig) -> Self {
90        config.alpn_protocols = vec![b"h2".to_vec()];
91        config.max_early_data_size = 0;
92        Self {
93            config: Arc::new(config),
94        }
95    }
96
97    /// Returns the immutable rustls configuration.
98    pub fn get_ref(&self) -> &rustls::ServerConfig {
99        &self.config
100    }
101}
102
103/// A synchronous TLS stream that permits application I/O only after HTTP/2 ALPN.
104///
105/// Construction performs no I/O. Call `handshake` or perform application
106/// I/O to drive negotiation. In nonblocking mode, retry `WouldBlock` operations
107/// when the socket becomes ready. Writes may buffer accepted plaintext: call
108/// `flush` to drain encrypted records before waiting for a peer response.
109/// Dropping the transport closes its socket without blocking or sending an alert.
110pub struct TlsTransport<C, T: SyncTransport> {
111    stream: rustls::StreamOwned<C, T>,
112    state: HandshakeState,
113    write_closed: bool,
114}
115
116#[derive(Clone, Copy)]
117enum HandshakeState {
118    Negotiating,
119    Ready,
120    Failed(io::ErrorKind),
121}
122
123impl<T: SyncTransport> TlsTransport<rustls::ClientConnection, T> {
124    /// Creates a client stream without performing network I/O.
125    ///
126    /// # Errors
127    /// Returns an error if rustls rejects the connection configuration.
128    pub fn client(
129        sock: T,
130        config: &TlsClientConfig,
131        server_name: rustls::pki_types::ServerName<'static>,
132    ) -> io::Result<Self> {
133        let conn = rustls::ClientConnection::new(Arc::clone(&config.config), server_name)
134            .map_err(io::Error::other)?;
135        Ok(Self {
136            stream: rustls::StreamOwned::new(conn, sock),
137            state: HandshakeState::Negotiating,
138            write_closed: false,
139        })
140    }
141}
142
143impl<T: SyncTransport> TlsTransport<rustls::ServerConnection, T> {
144    /// Creates a server stream without performing network I/O.
145    ///
146    /// # Errors
147    /// Returns an error if rustls rejects the connection configuration.
148    pub fn server(sock: T, config: &TlsServerConfig) -> io::Result<Self> {
149        let conn =
150            rustls::ServerConnection::new(Arc::clone(&config.config)).map_err(io::Error::other)?;
151        Ok(Self {
152            stream: rustls::StreamOwned::new(conn, sock),
153            state: HandshakeState::Negotiating,
154            write_closed: false,
155        })
156    }
157}
158
159impl<C, T: SyncTransport> TlsTransport<C, T> {
160    /// Borrows the underlying transport for inspection.
161    pub fn get_ref(&self) -> &T {
162        &self.stream.sock
163    }
164}
165
166fn remaining_handshake_time(deadline: Option<Instant>) -> io::Result<Option<Duration>> {
167    deadline
168        .map(|deadline| {
169            deadline
170                .checked_duration_since(Instant::now())
171                .filter(|left| !left.is_zero())
172                .ok_or_else(|| io::Error::new(io::ErrorKind::TimedOut, "tls handshake timed out"))
173        })
174        .transpose()
175}
176
177fn prepare_handshake_io<T: SyncTransport>(
178    transport: &mut T,
179    deadline: Option<Instant>,
180) -> io::Result<()> {
181    if let Some(remaining) = remaining_handshake_time(deadline)? {
182        transport.set_read_timeout(Some(remaining))?;
183        transport.set_write_timeout(Some(remaining))?;
184    }
185    Ok(())
186}
187
188macro_rules! impl_tls_transport {
189    ($connection:ty, $drain_final_output:expr) => {
190        impl<T: SyncTransport> TlsTransport<$connection, T> {
191            /// Completes negotiation and verifies that the peer selected HTTP/2.
192            ///
193            /// Retains progress when nonblocking I/O returns `WouldBlock`.
194            /// Configure socket timeouts to bound blocking negotiation.
195            ///
196            /// # Errors
197            /// Returns TLS, I/O, or ALPN errors. Fatal failures permanently disable
198            /// application I/O; `WouldBlock`, `Interrupted`, and `TimedOut` can be retried.
199            pub fn handshake(&mut self) -> io::Result<()> {
200                self.handshake_until(None)
201            }
202
203            /// Negotiates HTTP/2 TLS under one absolute deadline across all I/O.
204            ///
205            /// Every encrypted read, write and flush uses the remaining budget.
206            /// Both original transport timeouts are restored before returning,
207            /// including after WouldBlock, timeout, TLS failure or setup failure.
208            /// None preserves the transport's configured per-operation timeouts.
209            ///
210            /// # Errors
211            /// Returns TimedOut with "tls handshake timed out" when the deadline
212            /// expires. Also returns TLS, ALPN, transport and timeout-query errors.
213            /// Custom transports must support timeout queries for Some(deadline).
214            /// A timeout restoration failure permanently disables application I/O.
215            pub fn handshake_until(&mut self, deadline: Option<Instant>) -> io::Result<()> {
216                match self.state {
217                    HandshakeState::Ready => return Ok(()),
218                    HandshakeState::Failed(kind) => {
219                        return Err(io::Error::new(kind, "TLS handshake previously failed"));
220                    }
221                    HandshakeState::Negotiating => {}
222                }
223                let result = if deadline.is_some() {
224                    remaining_handshake_time(deadline)?;
225                    let read_timeout = self.stream.sock.read_timeout()?;
226                    let write_timeout = self.stream.sock.write_timeout()?;
227                    let result = self.drive_handshake(deadline);
228                    let restored_read = self.stream.sock.set_read_timeout(read_timeout);
229                    let restored_write = self.stream.sock.set_write_timeout(write_timeout);
230                    if restored_read.is_err() || restored_write.is_err() {
231                        let kind = restored_read
232                            .as_ref()
233                            .err()
234                            .or(restored_write.as_ref().err())
235                            .map_or(io::ErrorKind::Other, io::Error::kind);
236                        self.state = HandshakeState::Failed(kind);
237                        return Err(io::Error::new(
238                            kind,
239                            format!(
240                                "restore TLS transport timeouts: read={restored_read:?}, \
241                                 write={restored_write:?}; handshake={result:?}"
242                            ),
243                        ));
244                    }
245                    result
246                } else {
247                    self.drive_handshake(None)
248                };
249                if let Err(error) = result {
250                    let error = if error.kind() == io::ErrorKind::TimedOut && deadline.is_some() {
251                        remaining_handshake_time(deadline).err().unwrap_or(error)
252                    } else {
253                        error
254                    };
255                    if !matches!(
256                        error.kind(),
257                        io::ErrorKind::WouldBlock
258                            | io::ErrorKind::Interrupted
259                            | io::ErrorKind::TimedOut
260                    ) {
261                        self.state = HandshakeState::Failed(error.kind());
262                    }
263                    return Err(error);
264                }
265                self.state = HandshakeState::Ready;
266                Ok(())
267            }
268
269            fn drive_handshake(&mut self, deadline: Option<Instant>) -> io::Result<()> {
270                while self.stream.conn.is_handshaking() {
271                    self.drain_handshake_output(deadline)?;
272                    prepare_handshake_io(&mut self.stream.sock, deadline)?;
273                    if self.stream.conn.read_tls(&mut self.stream.sock)? == 0 {
274                        return Err(io::ErrorKind::UnexpectedEof.into());
275                    }
276                    if let Err(error) = self.stream.conn.process_new_packets() {
277                        // Preserve the protocol error if sending its alert fails.
278                        drop(self.drain_handshake_output(deadline));
279                        return Err(io::Error::new(io::ErrorKind::InvalidData, error));
280                    }
281                }
282                if self.stream.conn.alpn_protocol() != Some(b"h2".as_slice()) {
283                    return Err(io::Error::new(
284                        io::ErrorKind::InvalidData,
285                        "TLS peer did not negotiate HTTP/2 ALPN",
286                    ));
287                }
288                // Client Finished must reach the peer. Server session tickets can
289                // wait for application I/O without delaying handshake readiness.
290                if $drain_final_output {
291                    self.drain_handshake_output(deadline)?;
292                }
293                Ok(())
294            }
295
296            fn drain_handshake_output(&mut self, deadline: Option<Instant>) -> io::Result<()> {
297                while self.stream.conn.wants_write() {
298                    prepare_handshake_io(&mut self.stream.sock, deadline)?;
299                    if self.stream.conn.write_tls(&mut self.stream.sock)? == 0 {
300                        return Err(io::ErrorKind::WriteZero.into());
301                    }
302                }
303                prepare_handshake_io(&mut self.stream.sock, deadline)?;
304                self.stream.sock.flush()
305            }
306
307            /// Returns ALPN only after successful HTTP/2 negotiation.
308            pub fn negotiated_alpn(&self) -> Option<&[u8]> {
309                matches!(self.state, HandshakeState::Ready)
310                    .then(|| self.stream.conn.alpn_protocol())
311                    .flatten()
312            }
313
314            /// Returns the TLS version only after successful HTTP/2 negotiation.
315            pub fn protocol_version(&self) -> Option<rustls::ProtocolVersion> {
316                matches!(self.state, HandshakeState::Ready)
317                    .then(|| self.stream.conn.protocol_version())
318                    .flatten()
319            }
320
321            /// Sends a TLS close notification and flushes pending encrypted data.
322            ///
323            /// # Errors
324            /// Returns negotiation or socket errors. Retry `WouldBlock` to drain
325            /// pending records. Dropping the stream does not invoke this method.
326            pub fn close_notify(&mut self) -> io::Result<()> {
327                self.handshake()?;
328                self.write_closed = true;
329                self.stream.conn.send_close_notify();
330                self.stream.flush()
331            }
332        }
333
334        impl<T: SyncTransport> Read for TlsTransport<$connection, T> {
335            fn read(&mut self, destination: &mut [u8]) -> io::Result<usize> {
336                if destination.is_empty() {
337                    return Ok(0);
338                }
339                self.handshake()?;
340                self.stream.read(destination)
341            }
342        }
343
344        impl<T: SyncTransport> Write for TlsTransport<$connection, T> {
345            fn write(&mut self, source: &[u8]) -> io::Result<usize> {
346                if source.is_empty() {
347                    return Ok(0);
348                }
349                self.handshake()?;
350                if self.write_closed {
351                    return Err(io::Error::new(
352                        io::ErrorKind::BrokenPipe,
353                        "TLS write direction is closed",
354                    ));
355                }
356                // StreamOwned reports bytes already accepted even if its socket
357                // write blocks, so retrying cannot duplicate application bytes.
358                self.stream.write(source)
359            }
360
361            fn flush(&mut self) -> io::Result<()> {
362                self.handshake()?;
363                self.stream.flush()
364            }
365        }
366
367        impl<T: SyncTransport> SyncTransport for TlsTransport<$connection, T> {
368            fn set_nonblocking(&mut self, nonblocking: bool) -> io::Result<()> {
369                self.stream.sock.set_nonblocking(nonblocking)
370            }
371
372            fn set_read_timeout(&mut self, timeout: Option<Duration>) -> io::Result<()> {
373                self.stream.sock.set_read_timeout(timeout)
374            }
375
376            fn set_write_timeout(&mut self, timeout: Option<Duration>) -> io::Result<()> {
377                self.stream.sock.set_write_timeout(timeout)
378            }
379
380            fn read_timeout(&self) -> io::Result<Option<Duration>> {
381                self.stream.sock.read_timeout()
382            }
383
384            fn write_timeout(&self) -> io::Result<Option<Duration>> {
385                self.stream.sock.write_timeout()
386            }
387
388            fn peer_endpoint(&self) -> io::Result<String> {
389                self.stream.sock.peer_endpoint()
390            }
391        }
392    };
393}
394
395impl_tls_transport!(rustls::ClientConnection, true);
396impl_tls_transport!(rustls::ServerConnection, false);
397
398#[cfg(test)]
399#[path = "../tests/unit/tls.rs"]
400mod tests;