Skip to content

ssl_mode=require validates the server certificate against the platform trust store (should skip validation entirely) #44

Description

@aesslinger

Summary

build_tls_connector's own doc comment states: "require forces TLS without certificate validation (matches the builtin driver's require behavior — see src-tauri/src/pool_manager.rs)." The code doesn't do this.

needs_cert_validation only matches "verify-ca" | "verify-full"require is excluded. So for ssl_mode=require, execution falls through the if needs_cert_validation { ... } block entirely and lands on the final fallback:

let builder = rustls::ClientConfig::builder()
    .with_platform_verifier()
    .map_err(|e| format!("Failed to build platform TLS verifier: {e}"))?;

with_platform_verifier() does validate the server's certificate against the OS trust store — the opposite of "without certificate validation." So require currently behaves like verify-full (full platform-trust validation) instead of like disable/allow/prefer (no validation, just opportunistic/forced TLS).

Builtin's actual behavior (for reference)

src-tauri/src/pool_manager.rs's build_postgres_tls_connector:

"require" => {
    // Force TLS, skip cert validation.
    let verifier = Arc::new(NoCertVerifier::new());
    ClientConfig::builder()
        .dangerous()
        .with_custom_certificate_verifier(verifier)
        .with_no_client_auth()
}

require uses the same NoCertVerifier (accepts any certificate, no validation at all) as disable/allow/prefer — the only thing that changes between those modes and require is whether TLS is attempted at all (handled by SslMode, see #43), not whether the cert is checked.

Impact / reproduction

Found while manually verifying the #43 fix against a real SSL-enabled PostgreSQL instance with a self-signed certificate:

echo '{"jsonrpc":"2.0","id":1,"method":"test_connection","params":{"params":{"host":"127.0.0.1","port":55432,"username":"postgres","password":"password","database":"testdb","ssl_mode":"require"}}}' \
  | ./target/debug/postgresql-plugin

Result: {"error":{"code":-32603,"message":"Connection failed: Error occurred while creating a new object: error performing TLS handshake"}}

Expected (per the doc comment and the builtin's behavior): require should connect successfully — the whole point of require mode (vs. verify-full) is "encrypt the connection, but don't bother validating who's on the other end" — the standard use case being self-signed certs, private CAs the user hasn't configured ssl_ca for, or any server the user trusts by network topology rather than PKI.

Discovery context

Confirmed via git log/git show 9986b1e that this bug has been present since client.rs was first staged (#3) — not a regression from #35/#37/#40/#41/#42, and distinct from #43 (different root cause: cert validation logic, not protocol-level SslMode enforcement), even though both live in build_tls_connector/TLS setup and were found back-to-back while testing #43.

Proposed fix

Add a no-op/accept-all certificate verifier (mirroring the builtin's NoCertVerifier) and route require mode to it instead of falling through to with_platform_verifier(). Something like:

let needs_cert_validation = matches!(
    params.ssl_mode.as_deref(),
    Some("verify-ca" | "verify-full")
);

if needs_cert_validation {
    // ...existing verify-ca / verify-full logic...
}

if params.ssl_mode.as_deref() == Some("require") {
    // no validation, matches builtin's NoCertVerifier
    ...
}

// remaining fallback (disable/allow/prefer/unset) — platform verifier is
// arguably also wrong here per the builtin's NoCertVerifier for these
// modes; worth checking during the fix whether this fallback branch needs
// the same treatment, or whether it's intentionally different since those
// modes don't force TLS to begin with.

Needs a rustls::client::danger::ServerCertVerifier impl that unconditionally accepts (or a Debug-safe wrapper around rustls::client::WebPkiServerVerifier-style dangerous-but-empty checks) — check if rustls's ecosystem already has one exposed (e.g. behind a feature flag) before hand-rolling one.

Add a unit test constructing the verifier directly and asserting it accepts an arbitrary self-signed cert (mirroring the pattern used for VerifyCaCertVerifier's tests in src/client_tests.rs), plus a live-DB test against an SSL-enabled fixture with a self-signed cert (the repo doesn't currently have one in CI — .github/workflows/ci.yml's Postgres fixture runs without SSL at all).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions