use std::{path::PathBuf, sync::Arc};
use fraiseql_error::{FraiseQLError, Result};
use rustls::{
ClientConfig, DigitallySignedStruct, Error as RustlsError, RootCertStore, SignatureScheme,
client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
pki_types::{CertificateDer, ServerName, UnixTime, pem::PemObject},
};
use tokio_postgres_rustls::MakeRustlsConnect;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PostgresSslMode {
Disable,
#[default]
Prefer,
Require,
VerifyFull,
}
impl PostgresSslMode {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Disable => "disable",
Self::Prefer => "prefer",
Self::Require => "require",
Self::VerifyFull => "verify-full",
}
}
#[must_use]
pub const fn verifies_server(self) -> bool {
matches!(self, Self::VerifyFull)
}
#[must_use]
pub const fn requires_encryption(self) -> bool {
matches!(self, Self::Require | Self::VerifyFull)
}
}
impl std::fmt::Display for PostgresSslMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl std::str::FromStr for PostgresSslMode {
type Err = FraiseQLError;
fn from_str(s: &str) -> Result<Self> {
match s.trim().to_ascii_lowercase().as_str() {
"disable" => Ok(Self::Disable),
"prefer" => Ok(Self::Prefer),
"require" => Ok(Self::Require),
"verify-full" => Ok(Self::VerifyFull),
"allow" => Err(FraiseQLError::Configuration {
message: "postgres ssl mode \"allow\" (try cleartext first, retry with TLS) is \
not supported by the PostgreSQL driver FraiseQL uses. Use \"prefer\" \
for the same best-effort behaviour with TLS attempted first, or \
\"require\" to insist on encryption."
.to_string(),
}),
"verify-ca" => Err(FraiseQLError::Configuration {
message: "postgres ssl mode \"verify-ca\" (verify the certificate chain but not \
the hostname) is not supported. Use \"verify-full\", which also checks \
that the certificate was issued for the host being connected to — \
without it, any host holding any certificate from the same CA can \
impersonate the database."
.to_string(),
}),
other => Err(FraiseQLError::Configuration {
message: format!(
"unknown postgres ssl mode {other:?}. Supported: disable, prefer, require, \
verify-full."
),
}),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PostgresTlsConfig {
pub mode: Option<PostgresSslMode>,
pub ca_bundle_path: Option<PathBuf>,
}
impl PostgresTlsConfig {
#[must_use]
pub const fn new(mode: PostgresSslMode) -> Self {
Self {
mode: Some(mode),
ca_bundle_path: None,
}
}
#[must_use]
pub fn effective_mode(&self) -> PostgresSslMode {
self.mode.unwrap_or_default()
}
pub fn validate(&self) -> Result<()> {
if self.ca_bundle_path.is_some() && !self.effective_mode().verifies_server() {
return Err(FraiseQLError::Configuration {
message: format!(
"a CA bundle was configured for database TLS, but ssl mode {:?} never \
verifies the server certificate, so the bundle would have no effect. Use \
\"verify-full\" to check the server against it, or remove the bundle.",
self.effective_mode().as_str()
),
});
}
Ok(())
}
pub fn warn_if_unverified(&self) {
if self.mode == Some(PostgresSslMode::Require) {
tracing::warn!(
postgres_ssl_mode = PostgresSslMode::Require.as_str(),
"Database connections are encrypted but the server certificate is NOT verified; \
an active machine-in-the-middle can still intercept them. Use \
postgres_ssl_mode = \"verify-full\" (with ca_bundle_path if the CA is not in \
the platform trust store) to authenticate the database."
);
}
}
pub fn connector(&self) -> Result<PostgresConnector> {
self.validate()?;
let mode = self.effective_mode();
if mode == PostgresSslMode::Disable {
return Ok(PostgresConnector::Plaintext);
}
let provider = Arc::new(rustls::crypto::ring::default_provider());
let builder = ClientConfig::builder_with_provider(Arc::clone(&provider))
.with_safe_default_protocol_versions()
.map_err(|e| FraiseQLError::Configuration {
message: format!("failed to initialise the database TLS client: {e}"),
})?;
let config = if mode.verifies_server() {
builder.with_root_certificates(self.root_store()?).with_no_client_auth()
} else {
builder
.dangerous()
.with_custom_certificate_verifier(Arc::new(UnverifiedServer::new(&provider)))
.with_no_client_auth()
};
Ok(PostgresConnector::Tls(Box::new(MakeRustlsConnect::new(config))))
}
fn root_store(&self) -> Result<RootCertStore> {
let mut roots = RootCertStore::empty();
if let Some(path) = &self.ca_bundle_path {
let pem = std::fs::read(path).map_err(|e| FraiseQLError::Configuration {
message: format!(
"failed to read the database TLS CA bundle at {}: {e}",
path.display()
),
})?;
let certs = CertificateDer::pem_slice_iter(&pem)
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(|e| FraiseQLError::Configuration {
message: format!(
"failed to parse the database TLS CA bundle at {}: {e}",
path.display()
),
})?;
let (added, ignored) = roots.add_parsable_certificates(certs);
if added == 0 {
return Err(FraiseQLError::Configuration {
message: format!(
"the database TLS CA bundle at {} contains no usable certificate \
({ignored} item(s) present but unparseable as X.509). Database \
connections would fail verification against an empty trust store.",
path.display()
),
});
}
if ignored > 0 {
tracing::warn!(
ca_bundle_path = %path.display(),
added,
ignored,
"Some entries in the database TLS CA bundle were not usable certificates \
and were skipped"
);
}
return Ok(roots);
}
let native = rustls_native_certs::load_native_certs();
let (added, ignored) = roots.add_parsable_certificates(native.certs);
if added == 0 {
return Err(FraiseQLError::Configuration {
message: format!(
"database TLS is set to \"verify-full\" but no trust anchors are available: \
the platform certificate store yielded no usable certificate ({ignored} \
skipped, {} load error(s)). Set ca_bundle_path to the CA that issued the \
database's certificate.",
native.errors.len()
),
});
}
Ok(roots)
}
}
pub enum PostgresConnector {
Plaintext,
Tls(Box<MakeRustlsConnect>),
}
#[derive(Debug)]
struct UnverifiedServer {
supported: rustls::crypto::WebPkiSupportedAlgorithms,
}
impl UnverifiedServer {
fn new(provider: &Arc<rustls::crypto::CryptoProvider>) -> Self {
Self {
supported: provider.signature_verification_algorithms,
}
}
}
impl ServerCertVerifier for UnverifiedServer {
fn verify_server_cert(
&self,
_end_entity: &CertificateDer<'_>,
_intermediates: &[CertificateDer<'_>],
_server_name: &ServerName<'_>,
_ocsp_response: &[u8],
_now: UnixTime,
) -> std::result::Result<ServerCertVerified, RustlsError> {
Ok(ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> std::result::Result<HandshakeSignatureValid, RustlsError> {
rustls::crypto::verify_tls12_signature(message, cert, dss, &self.supported)
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> std::result::Result<HandshakeSignatureValid, RustlsError> {
rustls::crypto::verify_tls13_signature(message, cert, dss, &self.supported)
}
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
self.supported.supported_schemes()
}
}