Skip to content

fix: ssl_mode=require validates cert against platform trust store - #47

Merged
aesslinger merged 1 commit into
mainfrom
fix/ssl-mode-require-cert-validation
Aug 19, 2026
Merged

fix: ssl_mode=require validates cert against platform trust store#47
aesslinger merged 1 commit into
mainfrom
fix/ssl-mode-require-cert-validation

Conversation

@aesslinger

Copy link
Copy Markdown
Collaborator

Summary

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-fullrequire fell through to the final with_platform_verifier() fallback, which does validate against the OS trust store. This defeats the entire point of require vs. verify-full: the standard require use case is self-signed certs or private CAs the user hasn't configured ssl_ca for.

Fixes #44.

Discovery context

Found while manually verifying #43's fix against a real SSL-enabled PostgreSQL instance with a self-signed cert. Confirmed via git show 9986b1e that this predates every PR from this session — distinct from #43 (different root cause: cert validation logic, not protocol-level SslMode enforcement), even though both live in build_tls_connector.

Fix

Ported NoCertVerifier from the builtin driver's src-tauri/src/pool_manager.rs::NoCertVerifier — accepts any certificate unconditionally, including bypassing TLS 1.2/1.3 signature verification (not just chain/hostname checks, more permissive than VerifyCaCertVerifier's chain-only bypass, but that's the builtin's own deliberate choice for this mode — per CLAUDE.md's parity mandate, ported faithfully rather than "improved on"). Routed require mode to it, threading client_auth through the same pattern already used by the verify-ca/verify-full branches so mTLS + require still work together.

Testing — exact local setup and results against a real SSL-enabled server

Reused the same two local Podman instances set up for #43/#45's testing (non-SSL fixture on port 54320, real self-signed-cert TLS-enabled instance on port 55432).

Manual repro — the exact command from #44's own issue body, against the fix:

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
Scenario Before this fix After this fix
require against SSL-on (55432, self-signed cert) {"error":{...,"message":"...error performing TLS handshake"}} (the bug) {"id":1,"jsonrpc":"2.0","result":null}
require against non-SSL (54320) fails (correct, per #43/#45) still fails (correct, no regression) ✅
verify-full against SSL-on, no ssl_ca fails (correct — self-signed cert isn't in platform trust) still fails (correct, no regression) ✅
verify-ca against SSL-on, no ssl_ca fails (falls to platform verifier, same path as verify-full today — see #46) still fails, unchanged (out of scope here)
verify-ca/verify-full with a properly-generated CA cert unit tests (verify_ca_cert_verifier_*) still pass, confirming VerifyCaCertVerifier itself is untouched

Automated tests:

  • no_cert_verifier_accepts_a_cert_with_no_matching_hostname_or_chain — constructs NoCertVerifier directly, feeds it the existing FIXTURE_SERVER_CERT_PEM with a deliberately mismatched hostname (proving it does no checking at all, not even the hostname skip VerifyCaCertVerifier does).
  • build_tls_connector_require_builds_successfully_with_no_ssl_ca — sanity check the new branch wires up correctly.

Related finding, filed separately

While testing this fix, noticed verify-ca without an explicit ssl_ca file silently falls back to platform-trust validation instead of erroring like the builtin does ("verify-ca mode requires an explicit CA file..."). Distinct, smaller discrepancy — filed as #46, intentionally out of scope for this PR.

Test plan

  • cargo test --lib --bins — 113/113 pass (111 previous + 2 new)
  • Manual live verification against both a non-SSL and a real self-signed-cert SSL-enabled PostgreSQL instance (table above)
  • cargo clippy --all-targets -- -D warnings — clean
  • cargo fmt --all -- --check — clean
  • cargo build --release — clean
  • npx markdownlint CHANGELOG.md — clean

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 or
private CAs the user hasn't configured ssl_ca for).

Ported NoCertVerifier from the builtin driver's
src-tauri/src/pool_manager.rs::NoCertVerifier -- accepts any
certificate unconditionally, including bypassing TLS 1.2/1.3 signature
verification, not just chain/hostname checks (more permissive than
VerifyCaCertVerifier's chain-only bypass, but that's the builtin's own
deliberate choice for this mode, not something to improve on
silently). Routed require mode to it, threading client_auth through
the same pattern already used by the verify-ca/verify-full branches so
mTLS + require still work together.

Proved the bug and the fix live against a real self-signed-cert
SSL-enabled PostgreSQL instance (separate from the non-SSL fixture
used for #43): require failed the TLS handshake before this fix, and
now connects successfully. Confirmed no regression in verify-ca/
verify-full (still correctly validate -- existing VerifyCaCertVerifier
unit tests unaffected) or require against a non-SSL server (still
correctly fails, per #43/#45).

Separately found while testing this fix that verify-ca without an
explicit ssl_ca file silently falls back to platform-trust validation
instead of erroring like the builtin does -- a distinct, smaller
discrepancy, filed as #46 and left out of scope here.

Fixes #44.
@github-actions

Copy link
Copy Markdown

Version suggestion

Based on this PR's title (fix) and the prerelease:beta label:

Current 1.0.0-beta.7
Suggested next tag v1.0.0-beta.8

This is informational only — no tag or release is created automatically yet.

@aesslinger aesslinger self-assigned this Aug 19, 2026
@aesslinger
aesslinger merged commit 0c944e0 into main Aug 19, 2026
13 of 14 checks passed
@aesslinger
aesslinger deleted the fix/ssl-mode-require-cert-validation branch August 19, 2026 15:08
aesslinger added a commit that referenced this pull request Aug 19, 2026
verify-ca without an explicit ssl_ca silently fell through to
with_platform_verifier() -- full OS-trust-store validation -- instead
of erroring. The builtin driver's build_postgres_tls_connector
requires an explicit CA file for this mode (platform roots aren't
used, since macOS's strict EKU check rejects them) and errors clearly
otherwise ("verify-ca mode requires an explicit CA file...").

Added the same guard, adapted to this plugin's param-name-based error
convention (references ssl_ca by name, since these come from JSON
params rather than a UI form). verify-full without ssl_ca is
unaffected -- that mode is documented to fall back to platform trust
automatically, which is the actual distinction between the two modes.

Proved the fix live against the same self-signed-cert SSL-enabled
PostgreSQL instance used for #44/#47: verify-ca with no ssl_ca
previously failed with a confusing generic "error performing TLS
handshake"; now returns a clear, actionable error. Confirmed
verify-full and require remain unaffected.

Fixes #46.
aesslinger added a commit that referenced this pull request Aug 19, 2026
Continuation of the beta line after PRs #45/#47/#48 (ssl_mode=require/
verify-ca/verify-full silently allowing plaintext, ssl_mode=require
validating against the platform trust store instead of skipping
validation, and verify-ca silently falling back to platform trust
instead of requiring an explicit CA file).

Also corrected three README claims that had gone stale across this
session's TLS work: the ssl_mode value list was missing allow/prefer
in two places, ssl_ca's description didn't note it's now required
(not just optional) for verify-ca, and the pool-caching description
still said host:port:database:user, missing startup_script and every
TLS param folded in by #37.

Verified: .tabularium re-validated clean against the live registry
schema; cargo build/test (114/114)/clippy/fmt all pass; markdownlint
clean; manual live-TLS smoke tests against real non-SSL and
self-signed-cert SSL-enabled PostgreSQL instances covering every
ssl_mode value and error path all behave as documented.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

prerelease:beta Version suggestion targets a beta prerelease

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant