Skip to main content

laminar_connectors/postgres/
tls.rs

1//! Verified TLS policy and client construction shared by `PostgreSQL` connectors.
2
3use std::path::Path;
4use std::str::FromStr;
5
6use tokio_postgres_rustls::MakeRustlsConnect;
7use tokio_rustls::rustls::pki_types::{pem::PemObject, CertificateDer};
8use tokio_rustls::rustls::{ClientConfig, RootCertStore};
9
10use crate::error::ConnectorError;
11
12/// Connection security for `PostgreSQL` connectors.
13///
14/// Production connections either verify both the certificate chain and server
15/// hostname, or explicitly opt into plaintext for trusted test networks.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
17pub enum SslMode {
18    /// Disable TLS explicitly.
19    Disable,
20    /// Require TLS with certificate-chain and hostname verification.
21    #[default]
22    VerifyFull,
23}
24
25impl std::fmt::Display for SslMode {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        match self {
28            Self::Disable => write!(f, "disable"),
29            Self::VerifyFull => write!(f, "verify-full"),
30        }
31    }
32}
33
34impl FromStr for SslMode {
35    type Err = String;
36
37    fn from_str(s: &str) -> Result<Self, Self::Err> {
38        match s.to_ascii_lowercase().as_str() {
39            "disable" => Ok(Self::Disable),
40            "verify-full" => Ok(Self::VerifyFull),
41            other => Err(format!("unknown SSL mode: '{other}'")),
42        }
43    }
44}
45
46pub(crate) fn make_rustls_connector(
47    ca_cert_path: Option<&Path>,
48) -> Result<MakeRustlsConnect, ConnectorError> {
49    // The workspace standardizes on aws-lc-rs. Explicit installation also avoids
50    // rustls provider ambiguity when pgwire-replication enables its ring feature.
51    let _ = tokio_rustls::rustls::crypto::aws_lc_rs::default_provider().install_default();
52
53    let mut roots = RootCertStore::empty();
54    if let Some(path) = ca_cert_path {
55        let certificates = CertificateDer::pem_file_iter(path)
56            .map_err(|error| {
57                ConnectorError::ConfigurationError(format!(
58                    "read PostgreSQL CA certificate '{}': {error}",
59                    path.display()
60                ))
61            })?
62            .collect::<Result<Vec<_>, _>>()
63            .map_err(|error| {
64                ConnectorError::ConfigurationError(format!(
65                    "parse PostgreSQL CA certificate '{}': {error}",
66                    path.display()
67                ))
68            })?;
69        if certificates.is_empty() {
70            return Err(ConnectorError::ConfigurationError(format!(
71                "PostgreSQL CA certificate '{}' contains no certificates",
72                path.display()
73            )));
74        }
75        for certificate in certificates {
76            roots.add(certificate).map_err(|error| {
77                ConnectorError::ConfigurationError(format!(
78                    "invalid PostgreSQL CA certificate '{}': {error}",
79                    path.display()
80                ))
81            })?;
82        }
83    } else {
84        roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
85    }
86
87    let config = ClientConfig::builder()
88        .with_root_certificates(roots)
89        .with_no_client_auth();
90    Ok(MakeRustlsConnect::new(config))
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn ssl_mode_defaults_to_verified_tls() {
99        assert_eq!(SslMode::default(), SslMode::VerifyFull);
100    }
101
102    #[test]
103    fn ssl_mode_parses_only_supported_policies() {
104        assert_eq!("disable".parse::<SslMode>().unwrap(), SslMode::Disable);
105        assert_eq!(
106            "verify-full".parse::<SslMode>().unwrap(),
107            SslMode::VerifyFull
108        );
109        assert_eq!(
110            "VERIFY-FULL".parse::<SslMode>().unwrap(),
111            SslMode::VerifyFull
112        );
113
114        for rejected in [
115            "allow",
116            "off",
117            "prefer",
118            "require",
119            "verify-ca",
120            "verify_full",
121            "verifyfull",
122            "",
123        ] {
124            assert!(rejected.parse::<SslMode>().is_err(), "{rejected}");
125        }
126    }
127
128    #[test]
129    fn ssl_mode_has_canonical_display_values() {
130        assert_eq!(SslMode::Disable.to_string(), "disable");
131        assert_eq!(SslMode::VerifyFull.to_string(), "verify-full");
132    }
133
134    #[test]
135    fn webpki_roots_build_without_filesystem_configuration() {
136        make_rustls_connector(None).unwrap();
137    }
138
139    #[test]
140    fn malformed_custom_ca_fails_before_network_io() {
141        let directory = tempfile::tempdir().unwrap();
142        let path = directory.path().join("ca.pem");
143        std::fs::write(&path, "not a certificate").unwrap();
144
145        let error = make_rustls_connector(Some(&path))
146            .err()
147            .expect("malformed CA must fail");
148        assert!(error.to_string().contains("PostgreSQL CA certificate"));
149    }
150}