laminar_connectors/postgres/
tls.rs1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
17pub enum SslMode {
18 Disable,
20 #[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 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}