From 971b29c33d0ac2f81f4dc90190cc2fc89f4c7e90 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
Date: Fri, 7 Nov 2025 13:48:07 +0100
Subject: [PATCH v1 2/2] ssl: Add connection and reload tests for key
 passphrases

ssl_passphrase_command_supports_reload was not covered by the SSL
testsuite,  and connection tests after unlocking secrets with the
passphrase was also missing.  This adds test coverage for reloads
of passphrase commands as well as connection attempts which tests
the different codepaths for Windows and non-EXEC_BACKEND builds.

Author: Daniel Gustafsson <daniel@yesql.se>
Reviewed-by: ...
Discussion: https://postgr.es/m/...
---
 src/test/ssl/t/001_ssltests.pl | 87 +++++++++++++++++++++++++++++-----
 src/test/ssl/t/SSL/Server.pm   | 14 +++++-
 2 files changed, 87 insertions(+), 14 deletions(-)

diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 310d70a4c08..1bbe086501b 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -51,8 +51,15 @@ my $SERVERHOSTCIDR = '127.0.0.1/32';
 my $supports_sslcertmode_require =
   check_pg_config("#define HAVE_SSL_CTX_SET_CERT_CB 1");
 
+# Set of default settings for SSL parameters in connection string.  This
+# makes the tests protected against any defaults the environment may have
+# in ~/.postgresql/.
+my $default_ssl_connstr =
+  "sslkey=invalid sslcert=invalid sslrootcert=invalid sslcrl=invalid sslcrldir=invalid";
+
 # Allocation of base connection string shared among multiple tests.
-my $common_connstr;
+my $common_connstr =
+  "$default_ssl_connstr user=ssltestuser dbname=trustdb hostaddr=$SERVERHOSTADDR host=common-name.pg-ssltest.test";
 
 #### Set up the server.
 
@@ -77,6 +84,8 @@ $ssl_server->configure_test_server_for_ssl($node, $SERVERHOSTADDR,
 
 note "testing password-protected keys";
 
+# Test a passphrase command which fails to unlock the private key, the server
+# should not start at all.
 switch_server_cert(
 	$node,
 	certfile => 'server-cn-only',
@@ -85,20 +94,83 @@ switch_server_cert(
 	passphrase_cmd => 'echo wrongpassword',
 	restart => 'no');
 
-$result = $node->restart(fail_ok => 1);
+$result = $node->restart(
+	fail_ok => 1,
+	log_like => qr/could not load private key file/);
 is($result, 0,
 	'restart fails with password-protected key file with wrong password');
 
+# Test a passphrase command which successfully unlocks the private key but
+# which doesn't support reloading.  Unlocking the private key will fail when
+# reloading and the already existing SSL context will remain in place, with
+# connections still accepted.  On Windows connections will however fail since
+# EXEC_BACKEND builds will reload the SSL context on each backend startup, so
+# command reloading must be enabled.
 switch_server_cert(
 	$node,
 	certfile => 'server-cn-only',
 	cafile => 'root+client_ca',
 	keyfile => 'server-password',
 	passphrase_cmd => 'echo secret1',
+	passphrase_cmd_reload => 'off',
 	restart => 'no');
 
-$result = $node->restart(fail_ok => 1);
+$result = $node->restart(
+	fail_ok => 1,
+	log_unlike => qr/could not load private key file/);
+is($result, 1, 'restart succeeds with password-protected key file');
+
+if ($windows_os)
+{
+	$node->connect_fails(
+		"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+		"connect with correct server CA cert file sslmode=require",
+		expected_stderr => qr/\Qthe expected err\E/);
+}
+else
+{
+	$node->connect_ok(
+		"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+		"connect with correct server CA cert file sslmode=require");
+}
+
+# Reloading should fail since we cannot execute the passphrase command
+$node->reload();
+my $log_start = $node->wait_for_log(
+	qr/cannot be reloaded because it requires a passphrase/);
+
+# Test a passphrase command which successfully unlocks the private key, and
+# which can be reloaded.  The server should start and connections be accepted.
+switch_server_cert(
+	$node,
+	certfile => 'server-cn-only',
+	cafile => 'root+client_ca',
+	keyfile => 'server-password',
+	passphrase_cmd => 'echo secret1',
+	passphrase_cmd_reload => 'on',
+	restart => 'no');
+
+$result = $node->restart(
+	fail_ok => 1,
+	log_unlike => qr/could not load private key file/);
 is($result, 1, 'restart succeeds with password-protected key file');
+$node->connect_ok(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+	"connect with correct server CA cert file sslmode=require");
+
+# Reloading the config should execute the passphrase reload command and
+# successfully reload the private key.
+$node->reload();
+$log_start =
+  $node->wait_for_log(qr/reloading configuration files/, $log_start);
+$node->log_check(
+	"passhprase could reload private key",
+	$log_start,
+	log_unlike => [ qr/cannot be reloaded because it requires a passphrase/, ]
+);
+$node->connect_ok(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+	"connect with correct server CA cert file sslmode=require");
 
 # Test compatibility of SSL protocols.
 # TLSv1.1 is lower than TLSv1.2, so it won't work.
@@ -139,15 +211,6 @@ note "running client tests";
 
 switch_server_cert($node, certfile => 'server-cn-only');
 
-# Set of default settings for SSL parameters in connection string.  This
-# makes the tests protected against any defaults the environment may have
-# in ~/.postgresql/.
-my $default_ssl_connstr =
-  "sslkey=invalid sslcert=invalid sslrootcert=invalid sslcrl=invalid sslcrldir=invalid";
-
-$common_connstr =
-  "$default_ssl_connstr user=ssltestuser dbname=trustdb hostaddr=$SERVERHOSTADDR host=common-name.pg-ssltest.test";
-
 SKIP:
 {
 	skip "Keylogging is not supported with LibreSSL", 5 if $libressl;
diff --git a/src/test/ssl/t/SSL/Server.pm b/src/test/ssl/t/SSL/Server.pm
index efbd0dafaf6..a0a786c2ef2 100644
--- a/src/test/ssl/t/SSL/Server.pm
+++ b/src/test/ssl/t/SSL/Server.pm
@@ -296,6 +296,11 @@ The CRL directory to use. Implementation is SSL backend specific.
 The passphrase command to use. If not set, an empty passphrase command will
 be set.
 
+=item passphrase_cmd_reload => B<value>
+
+Whether or not to allow passphrase command reloading. If set the passphrase
+command reload configuration setting will be set to the value.
+
 =item restart => B<value>
 
 If set to 'no', the server won't be restarted after updating the settings.
@@ -315,7 +320,7 @@ sub switch_server_cert
 	my $pgdata = $node->data_dir;
 
 	ok(unlink($node->data_dir . '/sslconfig.conf'));
-	$node->append_conf('sslconfig.conf', "ssl=on");
+	$node->append_conf('sslconfig.conf', 'ssl=on');
 	$node->append_conf('sslconfig.conf', $backend->set_server_cert(\%params));
 	# use lists of ECDH curves and cipher suites for syntax testing
 	$node->append_conf('sslconfig.conf',
@@ -324,9 +329,14 @@ sub switch_server_cert
 		'ssl_tls13_ciphers=TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256');
 
 	$node->append_conf('sslconfig.conf',
-		"ssl_passphrase_command='" . $params{passphrase_cmd} . "'")
+		'ssl_passphrase_command=\'' . $params{passphrase_cmd} . '\'')
 	  if defined $params{passphrase_cmd};
 
+	$node->append_conf('sslconfig.conf',
+		'ssl_passphrase_command_supports_reload=\''
+		  . $params{passphrase_cmd_reload} . '\'')
+	  if defined $params{passphrase_cmd_reload};
+
 	return if (defined($params{restart}) && $params{restart} eq 'no');
 
 	$node->restart;
-- 
2.39.3 (Apple Git-146)

