From 55190cd28ed1498378b6709f927dd694bf86eb6f Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
Date: Fri, 6 Mar 2026 22:43:06 +0100
Subject: [PATCH v15 1/2] ssl: Add tests for client CA

These tests were originally written to test the SSL SNI patchset
but they have merit on their own since we lack coverage for these
scenarios in the non SNI case as well.

Author: Jacob Champion <jacob.champion@enterprisedb.com>
Co-authored-by: Daniel Gustafsson <daniel@yesql.se>
Discussion: https://postgr.es/m/1C81CD0D-407E-44F9-833A-DD0331C202E5@yesql.se
---
 src/test/ssl/t/001_ssltests.pl        | 39 +++++++++++++++++++++++++++
 src/test/ssl/t/SSL/Backend/OpenSSL.pm | 16 ++++++++---
 2 files changed, 52 insertions(+), 3 deletions(-)

diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 2b9b3dfd663..a86e8ff0e86 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -1004,4 +1004,43 @@ $node->connect_fails(
 		qr{Failed certificate data \(unverified\): subject "/CN=\\xce\\x9f\\xce\\xb4\\xcf\\x85\\xcf\\x83\\xcf\\x83\\xce\\xad\\xce\\xb1\\xcf\\x82", serial number \d+, issuer "/CN=Test CA for PostgreSQL SSL regression test client certs"},
 	]);
 
+# Test client CAs
+my $connstr =
+  "user=ssltestuser dbname=certdb hostaddr=$SERVERHOSTADDR sslmode=require sslsni=1";
+
+switch_server_cert($node, certfile => 'server-cn-only', cafile => '');
+# example.org is unconfigured and should fail.
+$node->connect_fails(
+	"$connstr host=example.org sslcertmode=require sslcert=ssl/client.crt"
+	  . sslkey('client.key'),
+	"host: 'example.org', ca: '': connect with sslcert, no client CA configured",
+	expected_stderr => qr/client certificates can only be checked if a root certificate store is available/);
+
+# example.com uses the client CA.
+switch_server_cert($node, certfile => 'server-cn-only', cafile => 'root+client_ca');
+# example.com is configured and should require a valid client cert.
+$node->connect_fails(
+	"$connstr host=example.com sslcertmode=disable",
+	"host: 'example.com', ca: 'root+client_ca.crt': connect fails if no client certificate sent",
+	expected_stderr => qr/connection requires a valid client certificate/);
+$node->connect_ok(
+	"$connstr host=example.com sslcertmode=require sslcert=ssl/client.crt " . sslkey('client.key'),
+	"host: 'example.com', ca: 'root+client_ca.crt': connect with sslcert, client certificate sent"
+);
+
+# example.net uses the server CA (which is wrong).
+switch_server_cert($node, certfile => 'server-cn-only', cafile => 'root+server_ca');
+# example.net is configured and should require a client cert, but will
+# always fail verification.
+$node->connect_fails(
+	"$connstr host=example.net sslcertmode=disable",
+	"host: 'example.net', ca: 'root+server_ca.crt': connect fails if no client certificate sent",
+	expected_stderr => qr/connection requires a valid client certificate/);
+
+$node->connect_fails(
+	"$connstr host=example.net sslcertmode=require sslcert=ssl/client.crt "
+	  . sslkey('client.key'),
+	"host: 'example.net', ca: 'root+server_ca.crt': connect with sslcert, client certificate sent",
+	expected_stderr => qr/unknown ca/);
+
 done_testing();
diff --git a/src/test/ssl/t/SSL/Backend/OpenSSL.pm b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
index 7ea05572a8d..6060771c1a8 100644
--- a/src/test/ssl/t/SSL/Backend/OpenSSL.pm
+++ b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
@@ -72,6 +72,7 @@ sub init
 	chmod(0600, glob "$pgdata/server-*.key")
 	  or die "failed to change permissions on server keys: $!";
 	_copy_files("ssl/root+client_ca.crt", $pgdata);
+	_copy_files("ssl/root+server_ca.crt", $pgdata);
 	_copy_files("ssl/root_ca.crt", $pgdata);
 	_copy_files("ssl/root+client.crl", $pgdata);
 	mkdir("$pgdata/root+client-crldir")
@@ -146,7 +147,8 @@ following parameters are supported:
 =item cafile => B<value>
 
 The CA certificate file to use for the C<ssl_ca_file> GUC. If omitted it will
-default to 'root+client_ca.crt'.
+default to 'root+client_ca.crt'. If empty, no C<ssl_ca_file> configuration
+parameter will be set.
 
 =item certfile => B<value>
 
@@ -181,10 +183,18 @@ sub set_server_cert
 	  unless defined $params->{keyfile};
 
 	my $sslconf =
-		"ssl_ca_file='$params->{cafile}.crt'\n"
-	  . "ssl_cert_file='$params->{certfile}.crt'\n"
+		"ssl_cert_file='$params->{certfile}.crt'\n"
 	  . "ssl_key_file='$params->{keyfile}.key'\n"
 	  . "ssl_crl_file='$params->{crlfile}'\n";
+	if ($params->{cafile} ne "")
+	{
+		$sslconf .= "ssl_ca_file='$params->{cafile}.crt'\n";
+	}
+	else
+	{
+		$sslconf .= "ssl_ca_file=''\n";
+	}
+
 	$sslconf .= "ssl_crl_dir='$params->{crldir}'\n"
 	  if defined $params->{crldir};
 
-- 
2.39.3 (Apple Git-146)

