Skip to main content

mz_tls_util/
lib.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! A tiny utility library for making TLS connectors.
11
12use mz_ore::secure::{Zeroize, Zeroizing};
13use openssl::pkcs12::Pkcs12;
14use openssl::pkey::PKey;
15use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode};
16use openssl::stack::Stack;
17use openssl::x509::X509;
18use postgres_openssl::MakeTlsConnector;
19use tokio_postgres::config::SslMode;
20
21macro_rules! bail_generic {
22    ($err:expr $(,)?) => {
23        return Err(TlsError::Generic(anyhow::anyhow!($err)))
24    };
25}
26
27/// An error representing tls failures.
28#[derive(Debug, thiserror::Error)]
29pub enum TlsError {
30    /// Any other error we bail on.
31    #[error(transparent)]
32    Generic(#[from] anyhow::Error),
33    /// Error setting up postgres ssl.
34    #[error(transparent)]
35    OpenSsl(#[from] openssl::error::ErrorStack),
36}
37
38/// Creates a TLS connector for the given [`Config`](tokio_postgres::Config).
39pub fn make_tls(config: &tokio_postgres::Config) -> Result<MakeTlsConnector, TlsError> {
40    let mut builder = SslConnector::builder(SslMethod::tls_client())?;
41    // The mode dictates whether we verify peer certs and hostnames. By default, Postgres is
42    // pretty relaxed and recommends SslMode::VerifyCa or SslMode::VerifyFull for security.
43    //
44    // For more details, check out Table 33.1. SSL Mode Descriptions in
45    // https://postgresql.org/docs/current/libpq-ssl.html#LIBPQ-SSL-PROTECTION.
46    let (verify_mode, verify_hostname) = match config.get_ssl_mode() {
47        SslMode::Disable | SslMode::Prefer => (SslVerifyMode::NONE, false),
48        SslMode::Require => match config.get_ssl_root_cert() {
49            // If a root CA file exists, the behavior of sslmode=require will be the same as
50            // that of verify-ca, meaning the server certificate is validated against the CA.
51            //
52            // For more details, check out the note about backwards compatibility in
53            // https://postgresql.org/docs/current/libpq-ssl.html#LIBQ-SSL-CERTIFICATES.
54            Some(_) => (SslVerifyMode::PEER, false),
55            None => (SslVerifyMode::NONE, false),
56        },
57        SslMode::VerifyCa => (SslVerifyMode::PEER, false),
58        SslMode::VerifyFull => (SslVerifyMode::PEER, true),
59        _ => panic!("unexpected sslmode {:?}", config.get_ssl_mode()),
60    };
61
62    // Configure peer verification
63    builder.set_verify(verify_mode);
64
65    // Configure certificates
66    match (config.get_ssl_cert(), config.get_ssl_key()) {
67        (Some(ssl_cert), Some(ssl_key)) => {
68            builder.set_certificate(&*X509::from_pem(ssl_cert)?)?;
69            builder.set_private_key(&*PKey::private_key_from_pem(ssl_key)?)?;
70        }
71        (None, Some(_)) => {
72            bail_generic!("must provide both sslcert and sslkey, but only provided sslkey")
73        }
74        (Some(_), None) => {
75            bail_generic!("must provide both sslcert and sslkey, but only provided sslcert")
76        }
77        _ => {}
78    }
79    if let Some(ssl_root_cert) = config.get_ssl_root_cert() {
80        for cert in X509::stack_from_pem(ssl_root_cert)? {
81            builder.cert_store_mut().add_cert(cert)?;
82        }
83    }
84
85    let mut tls_connector = MakeTlsConnector::new(builder.build());
86
87    // Configure hostname verification
88    match (verify_mode, verify_hostname) {
89        (SslVerifyMode::PEER, false) => tls_connector.set_callback(|connect, _| {
90            connect.set_verify_hostname(false);
91            Ok(())
92        }),
93        _ => {}
94    }
95
96    Ok(tls_connector)
97}
98
99pub struct Pkcs12Archive {
100    pub der: Vec<u8>,
101    pub pass: String,
102}
103
104impl Zeroize for Pkcs12Archive {
105    fn zeroize(&mut self) {
106        self.der.zeroize();
107        self.pass.zeroize();
108    }
109}
110
111impl Drop for Pkcs12Archive {
112    fn drop(&mut self) {
113        self.zeroize();
114    }
115}
116
117impl Pkcs12Archive {
118    pub fn into_parts(self) -> (Vec<u8>, String) {
119        let mut md = std::mem::ManuallyDrop::new(self);
120        let der = std::mem::take(&mut md.der);
121        let pass = std::mem::take(&mut md.pass);
122        (der, pass)
123    }
124}
125
126/// Constructs an identity from a PEM-formatted key and certificate using OpenSSL.
127pub fn pkcs12der_from_pem(
128    key: &[u8],
129    cert: &[u8],
130) -> Result<Pkcs12Archive, openssl::error::ErrorStack> {
131    let mut buf = Zeroizing::new(Vec::new());
132    buf.extend(key);
133    buf.push(b'\n');
134    buf.extend(cert);
135    let pem = buf.as_slice();
136    let pkey = PKey::private_key_from_pem(pem)?;
137    let mut certs = Stack::new()?;
138
139    // `X509::stack_from_pem` in openssl as of at least versions <= 0.10.48
140    // does not guarantee that it will either error or return at least 1
141    // element; in fact, it doesn't if the `pem` is not a well-formed
142    // representation of a PEM file. For example, if the represented file
143    // contains a well-formed key but a malformed certificate.
144    //
145    // To circumvent this issue, if `X509::stack_from_pem` returns no
146    // certificates, rely on getting the error message from
147    // `X509::from_pem`.
148    let mut cert_iter = X509::stack_from_pem(pem)?.into_iter();
149    let cert = match cert_iter.next() {
150        Some(cert) => cert,
151        None => X509::from_pem(pem)?,
152    };
153    for cert in cert_iter {
154        certs.push(cert)?;
155    }
156    // We build a PKCS #12 archive solely to have something to pass to
157    // `reqwest::Identity::from_pkcs12_der`, so the password and friendly
158    // name don't matter.
159    let pass = String::new();
160    let friendly_name = "";
161    let der = Pkcs12::builder()
162        .name(friendly_name)
163        .pkey(&pkey)
164        .cert(&cert)
165        .ca(certs)
166        .build2(&pass)?
167        .to_der()?;
168    Ok(Pkcs12Archive { der, pass })
169}
170
171#[cfg(test)]
172mod tests {
173    use std::io::Write;
174    use std::net::{SocketAddr, TcpListener};
175
176    use openssl::asn1::{Asn1Integer, Asn1Time};
177    use openssl::bn::{BigNum, MsbOption};
178    use openssl::hash::MessageDigest;
179    use openssl::pkey::Private;
180    use openssl::rsa::Rsa;
181    use openssl::ssl::SslAcceptor;
182    use openssl::x509::X509NameBuilder;
183    use openssl::x509::extension::{BasicConstraints, SubjectAlternativeName};
184    use tokio::io::{AsyncRead, AsyncReadExt};
185    use tokio::net::TcpStream;
186    use tokio_postgres::tls::{MakeTlsConnect, TlsConnect};
187
188    use super::*;
189
190    /// A throwaway CA that can issue leaf certificates.
191    struct TestCa {
192        cert: X509,
193        key: PKey<Private>,
194    }
195
196    fn random_serial() -> Asn1Integer {
197        let mut bn = BigNum::new().unwrap();
198        bn.rand(64, MsbOption::MAYBE_ZERO, false).unwrap();
199        bn.to_asn1_integer().unwrap()
200    }
201
202    impl TestCa {
203        fn new() -> TestCa {
204            let key = PKey::from_rsa(Rsa::generate(2048).unwrap()).unwrap();
205            let mut name = X509NameBuilder::new().unwrap();
206            name.append_entry_by_text("CN", "mz-tls-util test CA")
207                .unwrap();
208            let name = name.build();
209            let mut builder = X509::builder().unwrap();
210            builder.set_version(2).unwrap();
211            builder.set_serial_number(&random_serial()).unwrap();
212            builder.set_subject_name(&name).unwrap();
213            builder.set_issuer_name(&name).unwrap();
214            builder.set_pubkey(&key).unwrap();
215            builder
216                .set_not_before(&Asn1Time::days_from_now(0).unwrap())
217                .unwrap();
218            builder
219                .set_not_after(&Asn1Time::days_from_now(1).unwrap())
220                .unwrap();
221            builder
222                .append_extension(BasicConstraints::new().critical().ca().build().unwrap())
223                .unwrap();
224            builder.sign(&key, MessageDigest::sha256()).unwrap();
225            TestCa {
226                cert: builder.build(),
227                key,
228            }
229        }
230
231        /// Issues a leaf certificate, returning its PEM cert and PKCS#8 key.
232        fn issue(&self, cn: &str, dns_san: Option<&str>) -> (Vec<u8>, Vec<u8>) {
233            let key = PKey::from_rsa(Rsa::generate(2048).unwrap()).unwrap();
234            let mut name = X509NameBuilder::new().unwrap();
235            name.append_entry_by_text("CN", cn).unwrap();
236            let name = name.build();
237            let mut builder = X509::builder().unwrap();
238            builder.set_version(2).unwrap();
239            builder.set_serial_number(&random_serial()).unwrap();
240            builder.set_subject_name(&name).unwrap();
241            builder.set_issuer_name(self.cert.subject_name()).unwrap();
242            builder.set_pubkey(&key).unwrap();
243            builder
244                .set_not_before(&Asn1Time::days_from_now(0).unwrap())
245                .unwrap();
246            builder
247                .set_not_after(&Asn1Time::days_from_now(1).unwrap())
248                .unwrap();
249            if let Some(dns) = dns_san {
250                let san = SubjectAlternativeName::new()
251                    .dns(dns)
252                    .build(&builder.x509v3_context(Some(&self.cert), None))
253                    .unwrap();
254                builder.append_extension(san).unwrap();
255            }
256            builder.sign(&self.key, MessageDigest::sha256()).unwrap();
257            (
258                builder.build().to_pem().unwrap(),
259                key.private_key_to_pem_pkcs8().unwrap(),
260            )
261        }
262    }
263
264    /// Starts a blocking OpenSSL TLS server on an ephemeral port, serving
265    /// connections one at a time on a background thread. Each connection
266    /// receives "ok" once the server side of the handshake has completed. When
267    /// `client_ca_pem` is given the server requires a client certificate
268    /// signed by that CA, so the "ok" also confirms the client presented one.
269    fn run_tls_server(cert_pem: &[u8], key_pem: &[u8], client_ca_pem: Option<&[u8]>) -> SocketAddr {
270        let mut acceptor = SslAcceptor::mozilla_intermediate_v5(SslMethod::tls_server()).unwrap();
271        acceptor
272            .set_certificate(&X509::from_pem(cert_pem).unwrap())
273            .unwrap();
274        acceptor
275            .set_private_key(&PKey::private_key_from_pem(key_pem).unwrap())
276            .unwrap();
277        if let Some(ca_pem) = client_ca_pem {
278            for cert in X509::stack_from_pem(ca_pem).unwrap() {
279                acceptor.cert_store_mut().add_cert(cert).unwrap();
280            }
281            acceptor.set_verify(SslVerifyMode::PEER | SslVerifyMode::FAIL_IF_NO_PEER_CERT);
282        }
283        let acceptor = acceptor.build();
284        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
285        let addr = listener.local_addr().unwrap();
286        std::thread::spawn(move || {
287            for stream in listener.incoming() {
288                // A failed handshake drops the connection, which the client
289                // observes as a read error in `assert_server_ok`.
290                if let Ok(mut tls) = acceptor.accept(stream.unwrap()) {
291                    tls.write_all(b"ok").unwrap();
292                    let _ = tls.shutdown();
293                }
294            }
295        });
296        addr
297    }
298
299    /// Completes a TLS handshake for `host` via `make_tls`, returning the
300    /// established stream. Panics if certificate validation fails.
301    async fn openssl_connect(
302        config: &tokio_postgres::Config,
303        host: &str,
304        addr: SocketAddr,
305    ) -> postgres_openssl::TlsStream<TcpStream> {
306        let mut make = make_tls(config).unwrap();
307        let connect = MakeTlsConnect::<TcpStream>::make_tls_connect(&mut make, host).unwrap();
308        let tcp = TcpStream::connect(addr).await.unwrap();
309        TlsConnect::<TcpStream>::connect(connect, tcp)
310            .await
311            .unwrap()
312    }
313
314    async fn assert_server_ok<S: AsyncRead + Unpin>(mut tls: S) {
315        let mut buf = [0u8; 2];
316        tls.read_exact(&mut buf)
317            .await
318            .expect("server did not complete the handshake");
319        assert_eq!(&buf, b"ok");
320    }
321
322    #[mz_ore::test(tokio::test)]
323    #[cfg_attr(miri, ignore)] // uses the network and openssl FFI
324    async fn verify_full_trusts_default_store() {
325        let ca = TestCa::new();
326        let (server_cert, server_key) = ca.issue("server", Some("localhost"));
327
328        let mut ca_file = tempfile::NamedTempFile::new().unwrap();
329        ca_file.write_all(&ca.cert.to_pem().unwrap()).unwrap();
330        // SAFETY: the environment is process global. nextest, which CI uses,
331        // runs each test in its own process. Under plain `cargo test` this can
332        // race with other tests reading the environment.
333        unsafe { std::env::set_var("SSL_CERT_FILE", ca_file.path()) };
334
335        let addr = run_tls_server(&server_cert, &server_key, None);
336        let mut config = tokio_postgres::Config::new();
337        config.ssl_mode(SslMode::VerifyFull);
338
339        assert_server_ok(openssl_connect(&config, "localhost", addr).await).await;
340    }
341
342    #[mz_ore::test(tokio::test)]
343    #[cfg_attr(miri, ignore)] // uses the network and openssl FFI
344    async fn client_certs_accepted() {
345        let ca = TestCa::new();
346        let (server_cert, server_key) = ca.issue("server", Some("localhost"));
347        let (client_cert, client_key) = ca.issue("client", None);
348        let ca_pem = ca.cert.to_pem().unwrap();
349
350        let addr = run_tls_server(&server_cert, &server_key, Some(&ca_pem));
351        let mut config = tokio_postgres::Config::new();
352        config.ssl_mode(SslMode::VerifyFull);
353        config.ssl_root_cert(&ca_pem);
354        config.ssl_cert(&client_cert);
355        config.ssl_key(&client_key);
356
357        assert_server_ok(openssl_connect(&config, "localhost", addr).await).await;
358    }
359
360    #[mz_ore::test]
361    fn pkcs12_archive_needs_drop() {
362        assert!(std::mem::needs_drop::<Pkcs12Archive>());
363    }
364
365    #[mz_ore::test]
366    fn pkcs12_archive_zeroize_clears_fields() {
367        let mut archive = Pkcs12Archive {
368            der: vec![0xDE, 0xAD, 0xBE, 0xEF],
369            pass: String::from("hunter2"),
370        };
371
372        archive.zeroize();
373
374        assert!(archive.der.is_empty(), "der was not zeroed");
375        assert!(archive.pass.is_empty(), "pass was not zeroed");
376    }
377
378    #[mz_ore::test]
379    fn pkcs12_archive_implements_zeroize() {
380        fn assert_zeroize<T: mz_ore::secure::Zeroize>() {}
381        assert_zeroize::<Pkcs12Archive>();
382    }
383}