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 openssl::pkcs12::Pkcs12;
13use openssl::pkey::PKey;
14use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode};
15use openssl::stack::Stack;
16use openssl::x509::X509;
17use postgres_openssl::MakeTlsConnector;
18use tokio_postgres::config::SslMode;
19
20macro_rules! bail_generic {
21    ($err:expr $(,)?) => {
22        return Err(TlsError::Generic(anyhow::anyhow!($err)))
23    };
24}
25
26/// An error representing tls failures.
27#[derive(Debug, thiserror::Error)]
28pub enum TlsError {
29    /// Any other error we bail on.
30    #[error(transparent)]
31    Generic(#[from] anyhow::Error),
32    /// Error setting up postgres ssl.
33    #[error(transparent)]
34    OpenSsl(#[from] openssl::error::ErrorStack),
35}
36
37/// Creates a TLS connector for the given [`Config`](tokio_postgres::Config).
38pub fn make_tls(config: &tokio_postgres::Config) -> Result<MakeTlsConnector, TlsError> {
39    let mut builder = SslConnector::builder(SslMethod::tls_client())?;
40    // The mode dictates whether we verify peer certs and hostnames. By default, Postgres is
41    // pretty relaxed and recommends SslMode::VerifyCa or SslMode::VerifyFull for security.
42    //
43    // For more details, check out Table 33.1. SSL Mode Descriptions in
44    // https://postgresql.org/docs/current/libpq-ssl.html#LIBPQ-SSL-PROTECTION.
45    let (verify_mode, verify_hostname) = match config.get_ssl_mode() {
46        SslMode::Disable | SslMode::Prefer => (SslVerifyMode::NONE, false),
47        SslMode::Require => match config.get_ssl_root_cert() {
48            // If a root CA file exists, the behavior of sslmode=require will be the same as
49            // that of verify-ca, meaning the server certificate is validated against the CA.
50            //
51            // For more details, check out the note about backwards compatibility in
52            // https://postgresql.org/docs/current/libpq-ssl.html#LIBQ-SSL-CERTIFICATES.
53            Some(_) => (SslVerifyMode::PEER, false),
54            None => (SslVerifyMode::NONE, false),
55        },
56        SslMode::VerifyCa => (SslVerifyMode::PEER, false),
57        SslMode::VerifyFull => (SslVerifyMode::PEER, true),
58        _ => panic!("unexpected sslmode {:?}", config.get_ssl_mode()),
59    };
60
61    // Configure peer verification
62    builder.set_verify(verify_mode);
63
64    // Configure certificates
65    match (config.get_ssl_cert(), config.get_ssl_key()) {
66        (Some(ssl_cert), Some(ssl_key)) => {
67            builder.set_certificate(&*X509::from_pem(ssl_cert)?)?;
68            builder.set_private_key(&*PKey::private_key_from_pem(ssl_key)?)?;
69        }
70        (None, Some(_)) => {
71            bail_generic!("must provide both sslcert and sslkey, but only provided sslkey")
72        }
73        (Some(_), None) => {
74            bail_generic!("must provide both sslcert and sslkey, but only provided sslcert")
75        }
76        _ => {}
77    }
78    if let Some(ssl_root_cert) = config.get_ssl_root_cert() {
79        for cert in X509::stack_from_pem(ssl_root_cert)? {
80            builder.cert_store_mut().add_cert(cert)?;
81        }
82    }
83
84    let mut tls_connector = MakeTlsConnector::new(builder.build());
85
86    // Configure hostname verification
87    match (verify_mode, verify_hostname) {
88        (SslVerifyMode::PEER, false) => tls_connector.set_callback(|connect, _| {
89            connect.set_verify_hostname(false);
90            Ok(())
91        }),
92        _ => {}
93    }
94
95    Ok(tls_connector)
96}
97
98pub struct Pkcs12Archive {
99    pub der: Vec<u8>,
100    pub pass: String,
101}
102
103/// Constructs an identity from a PEM-formatted key and certificate using OpenSSL.
104pub fn pkcs12der_from_pem(
105    key: &[u8],
106    cert: &[u8],
107) -> Result<Pkcs12Archive, openssl::error::ErrorStack> {
108    let mut buf = Vec::new();
109    buf.extend(key);
110    buf.push(b'\n');
111    buf.extend(cert);
112    let pem = buf.as_slice();
113    let pkey = PKey::private_key_from_pem(pem)?;
114    let mut certs = Stack::new()?;
115
116    // `X509::stack_from_pem` in openssl as of at least versions <= 0.10.48
117    // does not guarantee that it will either error or return at least 1
118    // element; in fact, it doesn't if the `pem` is not a well-formed
119    // representation of a PEM file. For example, if the represented file
120    // contains a well-formed key but a malformed certificate.
121    //
122    // To circumvent this issue, if `X509::stack_from_pem` returns no
123    // certificates, rely on getting the error message from
124    // `X509::from_pem`.
125    let mut cert_iter = X509::stack_from_pem(pem)?.into_iter();
126    let cert = match cert_iter.next() {
127        Some(cert) => cert,
128        None => X509::from_pem(pem)?,
129    };
130    for cert in cert_iter {
131        certs.push(cert)?;
132    }
133    // We build a PKCS #12 archive solely to have something to pass to
134    // `reqwest::Identity::from_pkcs12_der`, so the password and friendly
135    // name don't matter.
136    let pass = String::new();
137    let friendly_name = "";
138    let der = Pkcs12::builder()
139        .name(friendly_name)
140        .pkey(&pkey)
141        .cert(&cert)
142        .ca(certs)
143        .build2(&pass)?
144        .to_der()?;
145    Ok(Pkcs12Archive { der, pass })
146}