use std::path::Path;
use std::sync::Arc;
use postgres_rustls::MakeTlsConnector;
use rustls::pki_types::CertificateDer;
use rustls::pki_types::pem::PemObject;
use rustls::{ClientConfig, RootCertStore};
use crate::error::{PgError, PgResult};
pub fn make_tls_connector() -> MakeTlsConnector {
connector_with_roots(webpki_root_store())
}
pub fn make_tls_connector_with_root_cert(root_cert: Option<&Path>) -> PgResult<MakeTlsConnector> {
let roots = match root_cert {
None => webpki_root_store(),
Some(path) => {
let certs: Vec<CertificateDer<'static>> = CertificateDer::pem_file_iter(path)
.map_err(|e| {
PgError::config(format!(
"sslrootcert: cannot read certificates from {}: {e}",
path.display()
))
})?
.collect::<Result<_, _>>()
.map_err(|e| {
PgError::config(format!(
"sslrootcert: invalid certificate in {}: {e}",
path.display()
))
})?;
if certs.is_empty() {
return Err(PgError::config(format!(
"sslrootcert: no certificates found in {}",
path.display()
)));
}
let mut roots = RootCertStore::empty();
for cert in certs {
roots.add(cert).map_err(|e| {
PgError::config(format!(
"sslrootcert: rustls rejected a certificate in {}: {e}",
path.display()
))
})?;
}
roots
}
};
Ok(connector_with_roots(roots))
}
fn webpki_root_store() -> RootCertStore {
let mut roots = RootCertStore::empty();
roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
roots
}
fn connector_with_roots(roots: RootCertStore) -> MakeTlsConnector {
let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider());
let client_config = ClientConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions()
.expect("safe default protocol versions are available")
.with_root_certificates(roots)
.with_no_client_auth();
MakeTlsConnector::new(tokio_rustls::TlsConnector::from(Arc::new(client_config)))
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tokio_postgres::tls::MakeTlsConnect;
const TEST_CA_PEM: &str = include_str!("../tests/data/test-ca.pem");
fn write_temp(contents: &str, name: &str) -> std::path::PathBuf {
let path = std::env::temp_dir().join(format!("prax-{}-{}.pem", std::process::id(), name));
let mut f = std::fs::File::create(&path).expect("create temp pem");
f.write_all(contents.as_bytes()).expect("write temp pem");
path
}
#[test]
fn connector_is_constructible_and_cloneable() {
let connector = make_tls_connector();
let mut cloned = connector.clone();
assert!(
<MakeTlsConnector as MakeTlsConnect<tokio::net::TcpStream>>::make_tls_connect(
&mut cloned,
"invalid..domain"
)
.is_err()
);
}
#[test]
fn none_root_cert_matches_the_default_connector() {
assert!(make_tls_connector_with_root_cert(None).is_ok());
}
#[test]
fn loads_a_pem_bundle() {
let path = write_temp(TEST_CA_PEM, "valid");
let loaded = make_tls_connector_with_root_cert(Some(&path)).is_ok();
let _ = std::fs::remove_file(&path);
assert!(loaded, "a well-formed PEM bundle must load");
}
#[test]
fn missing_file_names_the_path() {
let msg = match make_tls_connector_with_root_cert(Some(std::path::Path::new(
"/nonexistent/prax-no-such-ca.pem",
))) {
Ok(_) => panic!("a missing bundle must fail"),
Err(e) => e.to_string(),
};
assert!(msg.contains("sslrootcert"), "message was: {msg}");
assert!(msg.contains("prax-no-such-ca.pem"), "message was: {msg}");
}
#[test]
fn empty_bundle_is_rejected() {
let path = write_temp("# no certificates here\n", "empty");
let result = make_tls_connector_with_root_cert(Some(&path));
let _ = std::fs::remove_file(&path);
let msg = match result {
Ok(_) => panic!("an empty bundle must fail"),
Err(e) => e.to_string(),
};
assert!(msg.contains("no certificates found"), "message was: {msg}");
}
}