Skip to main content

rustlavel_db/
tls.rs

1//! TLS for the drivers that negotiate it over a plain socket.
2//!
3//! PostgreSQL and MySQL both start a connection in the clear and ask to upgrade:
4//! PostgreSQL with an `SSLRequest` packet, MySQL by setting `CLIENT_SSL` in its
5//! handshake response. The *asking* is protocol-specific and lives in each
6//! driver; everything after the server says yes is the same, and lives here.
7//!
8//! SQL Server is deliberately not part of this. It tunnels its handshake inside
9//! TDS pre-login packets rather than over the raw socket, so it keeps its own
10//! stream type in [`crate::sqlserver`].
11
12use crate::config::DatabaseConfig;
13use rustlavel_core::{Error, Result};
14use std::sync::Arc;
15
16/// How hard to insist on TLS, and how much of the certificate to believe.
17///
18/// The names match PostgreSQL's `sslmode` and MySQL's `--ssl-mode`, because a
19/// person configuring this has almost certainly met them before and a third
20/// spelling of the same idea helps nobody.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
22pub enum TlsMode {
23    /// Never encrypt. Everything, the password included, is on the wire in the
24    /// clear.
25    Disable,
26    /// Encrypt if the server offers it, otherwise carry on in the clear.
27    ///
28    /// The default, and it is worth being blunt about what it is worth: it
29    /// **guarantees nothing**. An attacker positioned to read the connection is
30    /// also positioned to answer "no, I don't do TLS", and the client will
31    /// obligingly continue in plain text. It defends against a passive
32    /// eavesdropper on a well-behaved network and against nobody else. Anything
33    /// facing a real network wants [`TlsMode::VerifyFull`].
34    #[default]
35    Prefer,
36    /// Refuse to connect without encryption, but believe whatever certificate
37    /// is presented.
38    ///
39    /// Stops passive eavesdropping outright. Does not stop an active attacker,
40    /// who simply presents a certificate of their own.
41    Require,
42    /// Encrypt, and check the certificate chains to a trusted root — but do not
43    /// check the hostname.
44    ///
45    /// The mode for a managed database reached through a name that is not on
46    /// its certificate, which is common enough that PostgreSQL and MySQL both
47    /// name it.
48    VerifyCa,
49    /// Encrypt, and check both the chain and the hostname. The only mode that
50    /// is secure against an active attacker.
51    VerifyFull,
52}
53
54impl TlsMode {
55    pub fn parse(raw: &str) -> Result<TlsMode> {
56        Ok(match raw.trim().to_ascii_lowercase().replace('_', "-").as_str() {
57            "disable" | "disabled" | "off" | "false" => TlsMode::Disable,
58            "prefer" | "preferred" => TlsMode::Prefer,
59            "require" | "required" | "on" | "true" => TlsMode::Require,
60            "verify-ca" => TlsMode::VerifyCa,
61            "verify-full" | "verify-identity" => TlsMode::VerifyFull,
62            other => {
63                return Err(Error::msg(format!(
64                    "`{other}` is not an sslmode. Use disable, prefer, require, verify-ca or \
65                     verify-full — prefer is the default, and verify-full is the only one that \
66                     is safe against an active attacker."
67                )));
68            }
69        })
70    }
71
72    pub fn as_str(self) -> &'static str {
73        match self {
74            TlsMode::Disable => "disable",
75            TlsMode::Prefer => "prefer",
76            TlsMode::Require => "require",
77            TlsMode::VerifyCa => "verify-ca",
78            TlsMode::VerifyFull => "verify-full",
79        }
80    }
81
82    /// Whether the driver should ask the server to encrypt at all.
83    pub fn wants_tls(self) -> bool {
84        self != TlsMode::Disable
85    }
86
87    /// Whether a server that declines to encrypt is a failure.
88    pub fn demands_tls(self) -> bool {
89        matches!(self, TlsMode::Require | TlsMode::VerifyCa | TlsMode::VerifyFull)
90    }
91
92    /// Whether the certificate is checked against a trust anchor.
93    pub fn verifies_certificate(self) -> bool {
94        matches!(self, TlsMode::VerifyCa | TlsMode::VerifyFull)
95    }
96}
97
98impl std::fmt::Display for TlsMode {
99    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100        f.write_str(self.as_str())
101    }
102}
103
104/// A socket that may or may not have been upgraded.
105///
106/// An enum rather than a boxed trait object: there are exactly two cases, both
107/// known at compile time, and a virtual call per read on a database connection
108/// buys nothing. `Closed` exists for the instant during the handshake when the
109/// plain socket has been taken out and the encrypted one is not yet in — a real
110/// placeholder, so a bug there reports itself instead of hanging on a dead file
111/// descriptor.
112pub enum DbStream {
113    Plain(tokio::net::TcpStream),
114    Tls(Box<tokio_rustls::client::TlsStream<tokio::net::TcpStream>>),
115    Closed,
116}
117
118impl DbStream {
119    pub fn is_encrypted(&self) -> bool {
120        matches!(self, DbStream::Tls(_))
121    }
122
123    /// Take the plain socket out, leaving `Closed` behind.
124    ///
125    /// Only for the upgrade: a driver calls this, hands the socket to
126    /// [`upgrade`], and puts the result back.
127    pub fn take_plain(&mut self) -> Result<tokio::net::TcpStream> {
128        match std::mem::replace(self, DbStream::Closed) {
129            DbStream::Plain(stream) => Ok(stream),
130            DbStream::Tls(stream) => {
131                *self = DbStream::Tls(stream);
132                Err(Error::msg("this connection is already encrypted"))
133            }
134            DbStream::Closed => Err(closed()),
135        }
136    }
137
138    pub async fn write_all(&mut self, bytes: &[u8]) -> std::io::Result<()> {
139        use tokio::io::AsyncWriteExt;
140        match self {
141            DbStream::Plain(stream) => stream.write_all(bytes).await,
142            DbStream::Tls(stream) => stream.write_all(bytes).await,
143            DbStream::Closed => Err(closed_io()),
144        }
145    }
146
147    pub async fn flush(&mut self) -> std::io::Result<()> {
148        use tokio::io::AsyncWriteExt;
149        match self {
150            DbStream::Plain(stream) => stream.flush().await,
151            DbStream::Tls(stream) => stream.flush().await,
152            DbStream::Closed => Err(closed_io()),
153        }
154    }
155
156    pub async fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
157        use tokio::io::AsyncReadExt;
158        match self {
159            DbStream::Plain(stream) => stream.read(buffer).await,
160            DbStream::Tls(stream) => stream.read(buffer).await,
161            DbStream::Closed => Err(closed_io()),
162        }
163    }
164
165    pub async fn shutdown(&mut self) -> std::io::Result<()> {
166        use tokio::io::AsyncWriteExt;
167        match self {
168            DbStream::Plain(stream) => stream.shutdown().await,
169            DbStream::Tls(stream) => stream.shutdown().await,
170            DbStream::Closed => Ok(()),
171        }
172    }
173}
174
175fn closed() -> Error {
176    Error::msg("the connection was left mid-upgrade; this is a bug in the driver")
177}
178
179fn closed_io() -> std::io::Error {
180    std::io::Error::other("the connection was left mid-upgrade; this is a bug in the driver")
181}
182
183/// Run the TLS handshake on a socket the server has agreed to encrypt.
184pub async fn upgrade(
185    stream: tokio::net::TcpStream,
186    host: &str,
187    config: &DatabaseConfig,
188) -> Result<tokio_rustls::client::TlsStream<tokio::net::TcpStream>> {
189    let connector = tokio_rustls::TlsConnector::from(client_config(config)?);
190
191    // An IP address is a valid server name for rustls, and one that no public
192    // certificate carries — which is why connecting to 127.0.0.1 under
193    // verify-full fails, correctly, and the error below says so.
194    let name = rustls::pki_types::ServerName::try_from(host.to_string())
195        .map_err(|_| Error::msg(format!("`{host}` is not a valid TLS server name")))?;
196
197    connector.connect(name, stream).await.map_err(|error| {
198        Error::msg(format!(
199            "the TLS handshake with {host} failed: {error}. sslmode is `{}`; if this is a \
200             development server with a self-signed certificate, either point `sslrootcert` at \
201             its certificate or drop to sslmode=require.",
202            config.tls_mode
203        ))
204    })
205}
206
207/// Build (and cache) the rustls configuration a mode implies.
208fn client_config(config: &DatabaseConfig) -> Result<Arc<rustls::ClientConfig>> {
209    let roots = match &config.tls_root_certificate {
210        Some(path) => root_store_from_pem(path)?,
211        None => rustls::RootCertStore { roots: webpki_roots::TLS_SERVER_ROOTS.to_vec() },
212    };
213
214    let builder = rustls::ClientConfig::builder();
215
216    let config = match config.tls_mode {
217        // Encryption without authentication. Named `dangerous` by rustls, and
218        // the name is right: this stops a passive listener and nothing else.
219        TlsMode::Require | TlsMode::Disable | TlsMode::Prefer => builder
220            .dangerous()
221            .with_custom_certificate_verifier(Arc::new(TrustAnyCertificate::new()?))
222            .with_no_client_auth(),
223        TlsMode::VerifyCa => {
224            let verifier = rustls::client::WebPkiServerVerifier::builder(Arc::new(roots))
225                .build()
226                .map_err(|error| Error::msg(format!("could not build a certificate verifier: {error}")))?;
227            builder
228                .dangerous()
229                .with_custom_certificate_verifier(Arc::new(ChainOnly(verifier)))
230                .with_no_client_auth()
231        }
232        TlsMode::VerifyFull => builder.with_root_certificates(roots).with_no_client_auth(),
233    };
234
235    Ok(Arc::new(config))
236}
237
238/// Read a PEM bundle into a root store.
239///
240/// The framing is unwrapped here rather than pulled in as a dependency — PEM is
241/// a base64 body between two marker lines, not cryptography, and rule one
242/// applies. What the bytes then *mean* is still webpki's problem, not ours.
243fn root_store_from_pem(path: &str) -> Result<rustls::RootCertStore> {
244    let text = std::fs::read_to_string(path).map_err(|error| {
245        Error::msg(format!("could not read the certificate at `{path}`: {error}"))
246    })?;
247
248    let mut store = rustls::RootCertStore::empty();
249    let mut found = 0;
250
251    for block in text.split("-----BEGIN CERTIFICATE-----").skip(1) {
252        let body = block.split("-----END CERTIFICATE-----").next().ok_or_else(|| {
253            Error::msg(format!("`{path}` has a BEGIN CERTIFICATE line with no END"))
254        })?;
255
256        let der = crate::base64::decode(&body.replace([' ', '\t'], "")).ok_or_else(|| {
257            Error::msg(format!("`{path}` contains a certificate that is not valid base64"))
258        })?;
259
260        store.add(rustls::pki_types::CertificateDer::from(der)).map_err(|error| {
261            Error::msg(format!("`{path}` contains a certificate rustls will not accept: {error}"))
262        })?;
263        found += 1;
264    }
265
266    if found == 0 {
267        return Err(Error::msg(format!(
268            "`{path}` contains no `-----BEGIN CERTIFICATE-----` block. It should be a PEM \
269             file; a DER file has to be converted first."
270        )));
271    }
272
273    Ok(store)
274}
275
276/// Chain checked, hostname not — `verify-ca`.
277///
278/// It delegates to the real verifier and forgives exactly one error: the name
279/// mismatch. Doing it this way rather than validating the chain by hand means
280/// expiry, signatures, key usage and path length are all still webpki's
281/// answer, and only the single check the mode is defined to skip is skipped.
282#[derive(Debug)]
283struct ChainOnly(Arc<rustls::client::WebPkiServerVerifier>);
284
285impl rustls::client::danger::ServerCertVerifier for ChainOnly {
286    fn verify_server_cert(
287        &self,
288        end_entity: &rustls::pki_types::CertificateDer<'_>,
289        intermediates: &[rustls::pki_types::CertificateDer<'_>],
290        server_name: &rustls::pki_types::ServerName<'_>,
291        ocsp_response: &[u8],
292        now: rustls::pki_types::UnixTime,
293    ) -> std::result::Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
294        use rustls::CertificateError::{NotValidForName, NotValidForNameContext};
295
296        match self.0.verify_server_cert(end_entity, intermediates, server_name, ocsp_response, now)
297        {
298            Err(rustls::Error::InvalidCertificate(
299                NotValidForName | NotValidForNameContext { .. },
300            )) => Ok(rustls::client::danger::ServerCertVerified::assertion()),
301            other => other,
302        }
303    }
304
305    fn verify_tls12_signature(
306        &self,
307        message: &[u8],
308        cert: &rustls::pki_types::CertificateDer<'_>,
309        dss: &rustls::DigitallySignedStruct,
310    ) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
311        self.0.verify_tls12_signature(message, cert, dss)
312    }
313
314    fn verify_tls13_signature(
315        &self,
316        message: &[u8],
317        cert: &rustls::pki_types::CertificateDer<'_>,
318        dss: &rustls::DigitallySignedStruct,
319    ) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
320        self.0.verify_tls13_signature(message, cert, dss)
321    }
322
323    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
324        self.0.supported_verify_schemes()
325    }
326}
327
328/// Believes any certificate — `require` and `prefer`.
329#[derive(Debug)]
330struct TrustAnyCertificate(Arc<rustls::crypto::CryptoProvider>);
331
332impl TrustAnyCertificate {
333    fn new() -> Result<TrustAnyCertificate> {
334        let provider = rustls::crypto::CryptoProvider::get_default()
335            .cloned()
336            .unwrap_or_else(|| Arc::new(rustls::crypto::aws_lc_rs::default_provider()));
337        Ok(TrustAnyCertificate(provider))
338    }
339}
340
341impl rustls::client::danger::ServerCertVerifier for TrustAnyCertificate {
342    fn verify_server_cert(
343        &self,
344        _end_entity: &rustls::pki_types::CertificateDer<'_>,
345        _intermediates: &[rustls::pki_types::CertificateDer<'_>],
346        _server_name: &rustls::pki_types::ServerName<'_>,
347        _ocsp_response: &[u8],
348        _now: rustls::pki_types::UnixTime,
349    ) -> std::result::Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
350        Ok(rustls::client::danger::ServerCertVerified::assertion())
351    }
352
353    fn verify_tls12_signature(
354        &self,
355        message: &[u8],
356        cert: &rustls::pki_types::CertificateDer<'_>,
357        dss: &rustls::DigitallySignedStruct,
358    ) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
359        rustls::crypto::verify_tls12_signature(
360            message,
361            cert,
362            dss,
363            &self.0.signature_verification_algorithms,
364        )
365    }
366
367    fn verify_tls13_signature(
368        &self,
369        message: &[u8],
370        cert: &rustls::pki_types::CertificateDer<'_>,
371        dss: &rustls::DigitallySignedStruct,
372    ) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
373        rustls::crypto::verify_tls13_signature(
374            message,
375            cert,
376            dss,
377            &self.0.signature_verification_algorithms,
378        )
379    }
380
381    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
382        self.0.signature_verification_algorithms.supported_schemes()
383    }
384}
385
386#[cfg(test)]
387mod tests {
388    use super::*;
389
390    #[test]
391    fn modes_parse_under_the_spellings_people_actually_write() {
392        assert_eq!(TlsMode::parse("disable").unwrap(), TlsMode::Disable);
393        assert_eq!(TlsMode::parse("PREFER").unwrap(), TlsMode::Prefer);
394        assert_eq!(TlsMode::parse(" require ").unwrap(), TlsMode::Require);
395        assert_eq!(TlsMode::parse("verify_ca").unwrap(), TlsMode::VerifyCa);
396        assert_eq!(TlsMode::parse("verify-full").unwrap(), TlsMode::VerifyFull);
397        // MySQL's spelling of verify-full.
398        assert_eq!(TlsMode::parse("VERIFY_IDENTITY").unwrap(), TlsMode::VerifyFull);
399    }
400
401    #[test]
402    fn an_unknown_mode_lists_the_real_ones_and_says_which_is_safe() {
403        let error = TlsMode::parse("yes-please").unwrap_err().to_string();
404
405        assert!(error.contains("verify-full"), "got {error}");
406        assert!(error.contains("active attacker"), "the error should say what is at stake");
407    }
408
409    #[test]
410    fn the_default_is_prefer() {
411        assert_eq!(TlsMode::default(), TlsMode::Prefer);
412    }
413
414    #[test]
415    fn what_each_mode_insists_on() {
416        // prefer asks but does not insist — the property that makes it worth
417        // nothing against an active attacker.
418        assert!(TlsMode::Prefer.wants_tls());
419        assert!(!TlsMode::Prefer.demands_tls());
420        assert!(!TlsMode::Prefer.verifies_certificate());
421
422        assert!(!TlsMode::Disable.wants_tls());
423
424        assert!(TlsMode::Require.demands_tls());
425        assert!(!TlsMode::Require.verifies_certificate());
426
427        for mode in [TlsMode::VerifyCa, TlsMode::VerifyFull] {
428            assert!(mode.demands_tls());
429            assert!(mode.verifies_certificate());
430        }
431    }
432
433    #[test]
434    fn modes_round_trip_through_their_written_form() {
435        for mode in [
436            TlsMode::Disable,
437            TlsMode::Prefer,
438            TlsMode::Require,
439            TlsMode::VerifyCa,
440            TlsMode::VerifyFull,
441        ] {
442            assert_eq!(TlsMode::parse(mode.as_str()).unwrap(), mode);
443        }
444    }
445
446    #[test]
447    fn a_pem_file_that_is_not_a_certificate_says_so() {
448        let path = std::env::temp_dir().join(format!("rustlavel-tls-not-a-cert-{}.pem", std::process::id()));
449        std::fs::write(&path, "just some text\n").unwrap();
450
451        let error = root_store_from_pem(path.to_str().unwrap()).unwrap_err().to_string();
452        assert!(error.contains("BEGIN CERTIFICATE"), "got {error}");
453
454        let _ = std::fs::remove_file(&path);
455    }
456
457    #[test]
458    fn a_missing_certificate_file_names_the_path() {
459        let error = root_store_from_pem("/nope/does-not-exist.pem").unwrap_err().to_string();
460        assert!(error.contains("/nope/does-not-exist.pem"), "got {error}");
461    }
462
463    #[test]
464    fn a_stream_left_mid_upgrade_errors_rather_than_hanging() {
465        let mut stream = DbStream::Closed;
466        assert!(stream.take_plain().is_err());
467        assert!(!stream.is_encrypted());
468    }
469}