Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,23 @@
and wired it into `build_pool`. Proved the bug and the fix with a live
test against a real non-SSL PostgreSQL instance: `ssl_mode=require`
connected successfully before this fix, and now correctly fails.
- `ssl_mode=require` validated the server certificate against the platform
trust store instead of skipping validation entirely. `build_tls_connector`'s
own doc comment already said `require` should force TLS "without
certificate validation," but `needs_cert_validation` only matched
`verify-ca`/`verify-full` — `require` fell through to the final
`with_platform_verifier()` fallback, which does validate against the OS
trust store, defeating the entire point of `require` vs. `verify-full`
(the standard `require` use case is self-signed certs / private CAs the
user hasn't configured `ssl_ca` for). Added `NoCertVerifier`, ported from
the builtin driver's `src-tauri/src/pool_manager.rs::NoCertVerifier`
(accepts any certificate unconditionally — no chain, hostname, or even
TLS 1.2/1.3 signature verification), and routed `require` mode to it.
Proved the bug and the fix live against a real self-signed-cert
SSL-enabled PostgreSQL instance: `require` failed the TLS handshake
before this fix, and now connects successfully; confirmed no regression
in `verify-ca`/`verify-full` (still correctly validate) or `require`
against a non-SSL server (still correctly fails, per the previous entry).

## [1.0.0-beta.7] - 2026-08-17

Expand Down
93 changes: 88 additions & 5 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -441,11 +441,12 @@ fn resolve_ssl_mode(ssl_mode: Option<&str>) -> Option<SslMode> {
/// `verify-ca` deliberately skips hostname verification (that's the entire
/// distinction from `verify-full` — matches libpq `sslmode=verify-ca`
/// semantics, see `VerifyCaCertVerifier` below). `require` forces TLS
/// without certificate validation (matches the builtin driver's `require`
/// behavior — see `src-tauri/src/pool_manager.rs`). When `ssl_cert`/
/// `ssl_key` are both supplied, presents them as a client certificate for
/// servers requiring mTLS (e.g. Google Cloud SQL) — matches the builtin
/// driver's `build_postgres_tls_connector` client-auth handling.
/// without any certificate validation at all (matches the builtin driver's
/// `require` behavior — see `src-tauri/src/pool_manager.rs::NoCertVerifier`).
/// When `ssl_cert`/`ssl_key` are both supplied, presents them as a client
/// certificate for servers requiring mTLS (e.g. Google Cloud SQL) —
/// matches the builtin driver's `build_postgres_tls_connector` client-auth
/// handling.
fn build_tls_connector(params: &ConnectionParams) -> Result<rustls::ClientConfig, String> {
use rustls_platform_verifier::BuilderVerifierExt;

Expand Down Expand Up @@ -506,6 +507,19 @@ fn build_tls_connector(params: &ConnectionParams) -> Result<rustls::ClientConfig
}
}

if params.ssl_mode.as_deref() == Some("require") {
let verifier = NoCertVerifier::new();
let builder = rustls::ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(std::sync::Arc::new(verifier));
return match client_auth {
Some((certs, key)) => builder
.with_client_auth_cert(certs, key)
.map_err(|e| format!("Failed to configure client certificate: {e}")),
None => Ok(builder.with_no_client_auth()),
};
}

let builder = rustls::ClientConfig::builder()
.with_platform_verifier()
.map_err(|e| format!("Failed to build platform TLS verifier: {e}"))?;
Expand Down Expand Up @@ -598,6 +612,75 @@ impl rustls::client::danger::ServerCertVerifier for VerifyCaCertVerifier {
}
}

/// Accepts any server certificate unconditionally — no chain validation,
/// no hostname check, not even TLS 1.2/1.3 signature verification. Used
/// for `require` mode, which forces TLS (encryption) without validating
/// who's on the other end — the standard use case is self-signed certs or
/// private CAs the user hasn't configured `ssl_ca` for. Matches the
/// builtin driver's `src-tauri/src/pool_manager.rs::NoCertVerifier`
/// exactly, including its signature-check bypass (more permissive than
/// `VerifyCaCertVerifier` above, which still validates the chain) — this
/// is the builtin's own deliberate choice for this mode, not something to
/// improve on silently.
#[derive(Debug)]
struct NoCertVerifier {
supported: rustls::crypto::WebPkiSupportedAlgorithms,
}

impl NoCertVerifier {
fn new() -> Self {
let provider = match rustls::crypto::CryptoProvider::get_default() {
Some(provider) => provider.clone(),
None => {
let provider = rustls::crypto::ring::default_provider();
let supported = provider.signature_verification_algorithms;
// Ignore the error from losing an install race — another
// caller's install still leaves a usable default installed.
let _ = provider.install_default();
return Self { supported };
}
};
Self {
supported: provider.signature_verification_algorithms,
}
}
}

impl rustls::client::danger::ServerCertVerifier for NoCertVerifier {
fn verify_server_cert(
&self,
_end_entity: &rustls::pki_types::CertificateDer<'_>,
_intermediates: &[rustls::pki_types::CertificateDer<'_>],
_server_name: &rustls::pki_types::ServerName<'_>,
_ocsp_response: &[u8],
_now: rustls::pki_types::UnixTime,
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
Ok(rustls::client::danger::ServerCertVerified::assertion())
}

fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &rustls::pki_types::CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
}

fn verify_tls13_signature(
&self,
_message: &[u8],
_cert: &rustls::pki_types::CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
}

fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
self.supported.supported_schemes()
}
}

/// Load root certificates from a PEM file (used for `ssl_ca`-pinned
/// `verify-ca`/`verify-full` connections).
fn load_roots_from_pem(path: &str) -> Result<rustls::RootCertStore, String> {
Expand Down
44 changes: 43 additions & 1 deletion src/client_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ use tokio::sync::Mutex;

use super::{
build_tls_connector, cleanup_idle_pools, connection_key, get_or_create_pool,
load_client_cert_from_pem, load_roots_from_pem, resolve_ssl_mode, VerifyCaCertVerifier, POOLS,
load_client_cert_from_pem, load_roots_from_pem, resolve_ssl_mode, NoCertVerifier,
VerifyCaCertVerifier, POOLS,
};
use crate::models::ConnectionParams;
use deadpool_postgres::SslMode;
Expand Down Expand Up @@ -628,3 +629,44 @@ fn resolve_ssl_mode_leaves_unset_or_unknown_values_unmapped() {
assert_eq!(resolve_ssl_mode(None), None);
assert_eq!(resolve_ssl_mode(Some("bogus")), None);
}

// Coverage for #44: build_tls_connector's `require` branch fell through to
// with_platform_verifier(), which DOES validate the server cert against the
// OS trust store — contradicting the function's own doc comment ("require
// forces TLS without certificate validation") and the builtin's actual
// behavior (NoCertVerifier: no validation at all for this mode). Unlike
// VerifyCaCertVerifier (which still skips hostname but validates the
// chain), NoCertVerifier accepts anything — not even a hostname check —
// matching the builtin's own NoCertVerifier exactly.

#[test]
fn no_cert_verifier_accepts_a_cert_with_no_matching_hostname_or_chain() {
use rustls::client::danger::ServerCertVerifier;
use rustls::pki_types::{pem::PemObject, CertificateDer, ServerName, UnixTime};

let verifier = NoCertVerifier::new();

let end_entity: CertificateDer =
CertificateDer::pem_slice_iter(FIXTURE_SERVER_CERT_PEM.as_bytes())
.next()
.unwrap()
.unwrap();
// Deliberately mismatched hostname vs. the cert's CN/SAN
// (cert-hostname.example) — proves this verifier doesn't even do the
// hostname check VerifyCaCertVerifier skips deliberately; it does no
// checking of any kind.
let server_name = ServerName::try_from("totally-unrelated-hostname.internal").unwrap();

let result = verifier.verify_server_cert(&end_entity, &[], &server_name, &[], UnixTime::now());
assert!(
result.is_ok(),
"require mode must accept any certificate, matching the builtin's NoCertVerifier"
);
}

#[test]
fn build_tls_connector_require_builds_successfully_with_no_ssl_ca() {
let params = params_with_ssl("require");
build_tls_connector(&params)
.expect("require mode must build a connector without needing ssl_ca set");
}
Loading