Summary
ssl_mode=verify-ca is supposed to validate the server's certificate chain against a CA but skip hostname verification — that's the entire distinction between verify-ca and verify-full (matches libpq semantics, and the builtin driver's own doc comment: "validates the certificate chain against a custom root store but skips hostname verification. Matches libpq sslmode=verify-ca behavior").
This plugin's verify-ca path (src/client.rs, build_tls_connector) instead wraps rustls::client::WebPkiServerVerifier:
if needs_cert_validation {
if let Some(ca_path) = user_ca {
let roots = load_roots_from_pem(ca_path)?;
let verifier =
rustls::client::WebPkiServerVerifier::builder(std::sync::Arc::new(roots))
.build()
.map_err(|e| format!("Failed to build certificate verifier: {e}"))?;
return Ok(rustls::ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(verifier)
.with_no_client_auth());
}
}
WebPkiServerVerifier::verify_server_cert unconditionally calls verify_server_name(&cert, server_name) after chain validation — there's no way to opt out. So verify-ca here actually behaves identically to verify-full: hostname mismatches are rejected even though verify-ca should accept them as long as the chain validates.
Proof
Confirmed via a standalone probe (not part of the test suite, scratch-only): generated a CA + server cert whose CN/SAN (totally-different-hostname.example) deliberately mismatches the hostname passed to verification (my-postgres-host.internal). Calling WebPkiServerVerifier::verify_server_cert directly with that mismatch rejects with InvalidCertificate(NotValidForNameContext { expected: DnsName("my-postgres-host.internal"), presented: ["DnsName(\"totally-different-hostname.example\")"] }) — i.e. exactly the behavior verify-ca should NOT have.
Builtin's approach (for reference)
src-tauri/src/pool_manager.rs's VerifyCaCertVerifier deliberately avoids WebPkiServerVerifier for this exact reason (its own doc comment, pool_manager.rs ~line 583):
Uses verify_server_cert_signed_by_trust_anchor directly rather than wrapping WebPkiServerVerifier — this makes the "skip hostname check" intent explicit, avoids double-verifying the chain, and prevents the fragile .or(Ok(...)) error-recovery pattern.
Its verify_server_cert impl:
fn verify_server_cert(
&self,
end_entity: &CertificateDer<'_>,
intermediates: &[CertificateDer<'_>],
_server_name: &ServerName<'_>, // <-- deliberately unused
_ocsp_response: &[u8],
now: UnixTime,
) -> Result<ServerCertVerified, TlsError> {
let cert = ParsedCertificate::try_from(end_entity)?;
verify_server_cert_signed_by_trust_anchor(
&cert, &self.roots, intermediates, now, self.supported.all,
)?;
Ok(ServerCertVerified::assertion())
}
rustls::client::verify_server_cert_signed_by_trust_anchor is a public, stable function in rustls 0.23 (this plugin already pins rustls = "0.23"), and doesn't take/check server_name at all — it validates the chain only.
Proposed fix
Port a VerifyCaCertVerifier-equivalent struct into src/client.rs, implementing rustls::client::danger::ServerCertVerifier and using verify_server_cert_signed_by_trust_anchor directly (ignoring server_name), instead of WebPkiServerVerifier, for the verify-ca branch of build_tls_connector. verify-full should keep using WebPkiServerVerifier (or the platform verifier) since it should check hostname.
Add a unit test proving verify-ca accepts a chain-valid cert whose hostname doesn't match the connection target — mirroring the probe used to confirm this bug — and that verify-full still rejects the same mismatch.
Discovery context
Found while auditing this plugin for other builtin-driver parity gaps (following #34/#36), which are also TLS/pool-config issues. Not related to those two — this one is specific to verify-ca certificate verification logic.
Summary
ssl_mode=verify-cais supposed to validate the server's certificate chain against a CA but skip hostname verification — that's the entire distinction betweenverify-caandverify-full(matches libpq semantics, and the builtin driver's own doc comment: "validates the certificate chain against a custom root store but skips hostname verification. Matches libpqsslmode=verify-cabehavior").This plugin's
verify-capath (src/client.rs,build_tls_connector) instead wrapsrustls::client::WebPkiServerVerifier:WebPkiServerVerifier::verify_server_certunconditionally callsverify_server_name(&cert, server_name)after chain validation — there's no way to opt out. Soverify-cahere actually behaves identically toverify-full: hostname mismatches are rejected even thoughverify-cashould accept them as long as the chain validates.Proof
Confirmed via a standalone probe (not part of the test suite, scratch-only): generated a CA + server cert whose CN/SAN (
totally-different-hostname.example) deliberately mismatches the hostname passed to verification (my-postgres-host.internal). CallingWebPkiServerVerifier::verify_server_certdirectly with that mismatch rejects withInvalidCertificate(NotValidForNameContext { expected: DnsName("my-postgres-host.internal"), presented: ["DnsName(\"totally-different-hostname.example\")"] })— i.e. exactly the behaviorverify-cashould NOT have.Builtin's approach (for reference)
src-tauri/src/pool_manager.rs'sVerifyCaCertVerifierdeliberately avoidsWebPkiServerVerifierfor this exact reason (its own doc comment,pool_manager.rs~line 583):Its
verify_server_certimpl:rustls::client::verify_server_cert_signed_by_trust_anchoris a public, stable function inrustls0.23 (this plugin already pinsrustls = "0.23"), and doesn't take/checkserver_nameat all — it validates the chain only.Proposed fix
Port a
VerifyCaCertVerifier-equivalent struct intosrc/client.rs, implementingrustls::client::danger::ServerCertVerifierand usingverify_server_cert_signed_by_trust_anchordirectly (ignoringserver_name), instead ofWebPkiServerVerifier, for theverify-cabranch ofbuild_tls_connector.verify-fullshould keep usingWebPkiServerVerifier(or the platform verifier) since it should check hostname.Add a unit test proving
verify-caaccepts a chain-valid cert whose hostname doesn't match the connection target — mirroring the probe used to confirm this bug — and thatverify-fullstill rejects the same mismatch.Discovery context
Found while auditing this plugin for other builtin-driver parity gaps (following #34/#36), which are also TLS/pool-config issues. Not related to those two — this one is specific to
verify-cacertificate verification logic.