From eb999e9e1d55b0d57bd79d6f5ef740a7770e7096 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <daniel@yesql.se>
Date: Wed, 2 Feb 2022 11:59:26 +0100
Subject: [PATCH] Refactor SSL tests

The SSL support in libpq, pg_cryptohash and other relevant parts of the
codebase, has been abstracted such that support for alternative SSL
libraries can be implemented. The tests were however quite heavily tied
to OpenSSL, and how the OpenSSL implementation sets up the server. This
refactors the SSLServer.pm module into SSL::Server which abstracts SSL
library specific setup, which in turn is implemented in SSL::Backend::X
for each SSL library. Rudimentary POD documentation is also added.

The main difference for testcode is that switch_server_cert() now takes
a set of parameters where those which are library specific are passed
to the implementing module. Resolving the path to the sslkey is also
moved to the backend module, and is now a function call rather than a
hash lookup.

While not strictly related to the refactoring, the preplanning of test
cases is removed in favor of calling done_testing(), as the number is
quite hard to calculate by hand given that connection tests perform
subtests.

This was extracted from a larger patchset on SSL library support, as of
now only OpenSSL is supported however.

Discussion: https://postgr.es/m/<tbd>
Discussion: https://postgr.es/m/FAB21FC8-0F62-434F-AA78-6BD9336D630A@yesql.se
---
 src/test/ssl/t/001_ssltests.pl                | 134 ++++------
 src/test/ssl/t/002_scram.pl                   |   8 +-
 src/test/ssl/t/003_sslinfo.pl                 |  23 +-
 src/test/ssl/t/SSL/Backend/OpenSSL.pm         | 241 +++++++++++++++++
 .../ssl/t/{SSLServer.pm => SSL/Server.pm}     | 246 +++++++++++++-----
 5 files changed, 475 insertions(+), 177 deletions(-)
 create mode 100644 src/test/ssl/t/SSL/Backend/OpenSSL.pm
 rename src/test/ssl/t/{SSLServer.pm => SSL/Server.pm} (51%)

diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index b1fb15ce80..4a965fb69a 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -8,21 +8,15 @@ use PostgreSQL::Test::Cluster;
 use PostgreSQL::Test::Utils;
 use Test::More;
 
-use File::Copy;
-
 use FindBin;
 use lib $FindBin::RealBin;
 
-use SSLServer;
+use SSL::Server;
 
 if ($ENV{with_ssl} ne 'openssl')
 {
 	plan skip_all => 'OpenSSL not supported by this build';
 }
-else
-{
-	plan tests => 110;
-}
 
 #### Some configuration
 
@@ -36,39 +30,6 @@ my $SERVERHOSTCIDR = '127.0.0.1/32';
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
 
-# The client's private key must not be world-readable, so take a copy
-# of the key stored in the code tree and update its permissions.
-#
-# This changes to using keys stored in a temporary path for the rest of
-# the tests. To get the full path for inclusion in connection strings, the
-# %key hash can be interrogated.
-my $cert_tempdir = PostgreSQL::Test::Utils::tempdir();
-my %key;
-my @keys = (
-	"client.key",               "client-revoked.key",
-	"client-der.key",           "client-encrypted-pem.key",
-	"client-encrypted-der.key", "client-dn.key");
-foreach my $keyfile (@keys)
-{
-	copy("ssl/$keyfile", "$cert_tempdir/$keyfile")
-	  or die
-	  "couldn't copy ssl/$keyfile to $cert_tempdir/$keyfile for permissions change: $!";
-	chmod 0600, "$cert_tempdir/$keyfile"
-	  or die "failed to change permissions on $cert_tempdir/$keyfile: $!";
-	$key{$keyfile} = PostgreSQL::Test::Utils::perl2host("$cert_tempdir/$keyfile");
-	$key{$keyfile} =~ s!\\!/!g if $PostgreSQL::Test::Utils::windows_os;
-}
-
-# Also make a copy of that explicitly world-readable.  We can't
-# necessarily rely on the file in the source tree having those
-# permissions.
-copy("ssl/client.key", "$cert_tempdir/client_wrongperms.key")
-  or die
-  "couldn't copy ssl/client_key to $cert_tempdir/client_wrongperms.key for permission change: $!";
-chmod 0644, "$cert_tempdir/client_wrongperms.key"
-  or die "failed to change permissions on $cert_tempdir/client_wrongperms.key: $!";
-$key{'client_wrongperms.key'} = PostgreSQL::Test::Utils::perl2host("$cert_tempdir/client_wrongperms.key");
-$key{'client_wrongperms.key'} =~ s!\\!/!g if $PostgreSQL::Test::Utils::windows_os;
 #### Set up the server.
 
 note "setting up data directory";
@@ -83,31 +44,31 @@ $node->start;
 
 # Run this before we lock down access below.
 my $result = $node->safe_psql('postgres', "SHOW ssl_library");
-is($result, 'OpenSSL', 'ssl_library parameter');
+is($result, SSL::Server::ssl_library(), 'ssl_library parameter');
 
 configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
 	'trust');
 
 note "testing password-protected keys";
 
-open my $sslconf, '>', $node->data_dir . "/sslconfig.conf";
-print $sslconf "ssl=on\n";
-print $sslconf "ssl_cert_file='server-cn-only.crt'\n";
-print $sslconf "ssl_key_file='server-password.key'\n";
-print $sslconf "ssl_passphrase_command='echo wrongpassword'\n";
-close $sslconf;
+switch_server_cert($node,
+	certfile => 'server-cn-only',
+	cafile => 'root+client_ca',
+	keyfile => 'server-password',
+	passphrase_cmd => 'echo wrongpassword',
+	restart => 'no' );
 
 command_fails(
 	[ 'pg_ctl', '-D', $node->data_dir, '-l', $node->logfile, 'restart' ],
 	'restart fails with password-protected key file with wrong password');
 $node->_update_pid(0);
 
-open $sslconf, '>', $node->data_dir . "/sslconfig.conf";
-print $sslconf "ssl=on\n";
-print $sslconf "ssl_cert_file='server-cn-only.crt'\n";
-print $sslconf "ssl_key_file='server-password.key'\n";
-print $sslconf "ssl_passphrase_command='echo secret1'\n";
-close $sslconf;
+switch_server_cert($node,
+	certfile => 'server-cn-only',
+	cafile => 'root+client_ca',
+	keyfile => 'server-password',
+	passphrase_cmd => 'echo secret1',
+	restart => 'no');
 
 command_ok(
 	[ 'pg_ctl', '-D', $node->data_dir, '-l', $node->logfile, 'restart' ],
@@ -140,7 +101,7 @@ command_ok(
 
 note "running client tests";
 
-switch_server_cert($node, 'server-cn-only');
+switch_server_cert($node, certfile => 'server-cn-only');
 
 $common_connstr =
   "user=ssltestuser dbname=trustdb sslcert=invalid hostaddr=$SERVERHOSTADDR host=common-name.pg-ssltest.test";
@@ -254,7 +215,7 @@ $node->connect_fails(
 );
 
 # Test Subject Alternative Names.
-switch_server_cert($node, 'server-multiple-alt-names');
+switch_server_cert($node, certfile => 'server-multiple-alt-names');
 
 $common_connstr =
   "user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
@@ -283,7 +244,7 @@ $node->connect_fails(
 
 # Test certificate with a single Subject Alternative Name. (this gives a
 # slightly different error message, that's all)
-switch_server_cert($node, 'server-single-alt-name');
+switch_server_cert($node, certfile => 'server-single-alt-name');
 
 $common_connstr =
   "user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
@@ -307,7 +268,7 @@ $node->connect_fails(
 
 # Test server certificate with a CN and SANs. Per RFCs 2818 and 6125, the CN
 # should be ignored when the certificate has both.
-switch_server_cert($node, 'server-cn-and-alt-names');
+switch_server_cert($node, certfile => 'server-cn-and-alt-names');
 
 $common_connstr =
   "user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
@@ -325,7 +286,7 @@ $node->connect_fails(
 
 # Finally, test a server certificate that has no CN or SANs. Of course, that's
 # not a very sensible certificate, but libpq should handle it gracefully.
-switch_server_cert($node, 'server-no-names');
+switch_server_cert($node, certfile => 'server-no-names');
 $common_connstr =
   "user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR";
 
@@ -340,7 +301,7 @@ $node->connect_fails(
 	  qr/could not get server's host name from server certificate/);
 
 # Test that the CRL works
-switch_server_cert($node, 'server-revoked');
+switch_server_cert($node, certfile => 'server-revoked');
 
 $common_connstr =
   "user=ssltestuser dbname=trustdb sslcert=invalid hostaddr=$SERVERHOSTADDR host=common-name.pg-ssltest.test";
@@ -406,34 +367,34 @@ $node->connect_fails(
 
 # correct client cert in unencrypted PEM
 $node->connect_ok(
-	"$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client.key'}",
+	"$common_connstr user=ssltestuser sslcert=ssl/client.crt " . sslkey('client.key'),
 	"certificate authorization succeeds with correct client cert in PEM format"
 );
 
 # correct client cert in unencrypted DER
 $node->connect_ok(
-	"$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client-der.key'}",
+	"$common_connstr user=ssltestuser sslcert=ssl/client.crt " . sslkey('client-der.key'),
 	"certificate authorization succeeds with correct client cert in DER format"
 );
 
 # correct client cert in encrypted PEM
 $node->connect_ok(
-	"$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client-encrypted-pem.key'} sslpassword='dUmmyP^#+'",
+	"$common_connstr user=ssltestuser sslcert=ssl/client.crt " . sslkey('client-encrypted-pem.key') . " sslpassword='dUmmyP^#+'",
 	"certificate authorization succeeds with correct client cert in encrypted PEM format"
 );
 
 # correct client cert in encrypted DER
 $node->connect_ok(
-	"$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client-encrypted-der.key'} sslpassword='dUmmyP^#+'",
+	"$common_connstr user=ssltestuser sslcert=ssl/client.crt " . sslkey('client-encrypted-der.key') . " sslpassword='dUmmyP^#+'",
 	"certificate authorization succeeds with correct client cert in encrypted DER format"
 );
 
 # correct client cert in encrypted PEM with wrong password
 $node->connect_fails(
-	"$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client-encrypted-pem.key'} sslpassword='wrong'",
+	"$common_connstr user=ssltestuser sslcert=ssl/client.crt " . sslkey('client-encrypted-pem.key') . " sslpassword='wrong'",
 	"certificate authorization fails with correct client cert and wrong password in encrypted PEM format",
 	expected_stderr =>
-	  qr!\Qprivate key file "$key{'client-encrypted-pem.key'}": bad decrypt\E!
+	  qr!private key file \".*client-encrypted-pem\.key\": bad decrypt!,
 );
 
 
@@ -441,7 +402,7 @@ $node->connect_fails(
 my $dn_connstr = "$common_connstr dbname=certdb_dn";
 
 $node->connect_ok(
-	"$dn_connstr user=ssltestuser sslcert=ssl/client-dn.crt sslkey=$key{'client-dn.key'}",
+	"$dn_connstr user=ssltestuser sslcert=ssl/client-dn.crt " . sslkey('client-dn.key'),
 	"certificate authorization succeeds with DN mapping",
 	log_like => [
 		qr/connection authenticated: identity="CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG" method=cert/
@@ -451,14 +412,14 @@ $node->connect_ok(
 $dn_connstr = "$common_connstr dbname=certdb_dn_re";
 
 $node->connect_ok(
-	"$dn_connstr user=ssltestuser sslcert=ssl/client-dn.crt sslkey=$key{'client-dn.key'}",
+	"$dn_connstr user=ssltestuser sslcert=ssl/client-dn.crt " . sslkey('client-dn.key'),
 	"certificate authorization succeeds with DN regex mapping");
 
 # same thing but using explicit CN
 $dn_connstr = "$common_connstr dbname=certdb_cn";
 
 $node->connect_ok(
-	"$dn_connstr user=ssltestuser sslcert=ssl/client-dn.crt sslkey=$key{'client-dn.key'}",
+	"$dn_connstr user=ssltestuser sslcert=ssl/client-dn.crt " . sslkey('client-dn.key'),
 	"certificate authorization succeeds with CN mapping",
 	# the full DN should still be used as the authenticated identity
 	log_like => [
@@ -476,18 +437,18 @@ TODO:
 
 	# correct client cert in encrypted PEM with empty password
 	$node->connect_fails(
-		"$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client-encrypted-pem.key'} sslpassword=''",
+		"$common_connstr user=ssltestuser sslcert=ssl/client.crt " . sslkey('client-encrypted-pem.key') . " sslpassword=''",
 		"certificate authorization fails with correct client cert and empty password in encrypted PEM format",
 		expected_stderr =>
-		  qr!\Qprivate key file "$key{'client-encrypted-pem.key'}": processing error\E!
+		  qr!private key file \".*client-encrypted-pem\.key\": processing error!
 	);
 
 	# correct client cert in encrypted PEM with no password
 	$node->connect_fails(
-		"$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client-encrypted-pem.key'}",
+		"$common_connstr user=ssltestuser sslcert=ssl/client.crt " . sslkey('client-encrypted-pem.key'),
 		"certificate authorization fails with correct client cert and no password in encrypted PEM format",
 		expected_stderr =>
-		  qr!\Qprivate key file "$key{'client-encrypted-pem.key'}": processing error\E!
+		  qr!private key file \".*client-encrypted-pem\.key\": processing error!
 	);
 
 }
@@ -530,12 +491,12 @@ command_like(
 		'-P',
 		'null=_null_',
 		'-d',
-		"$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client.key'}",
+		"$common_connstr user=ssltestuser sslcert=ssl/client.crt " . sslkey('client.key'),
 		'-c',
 		"SELECT * FROM pg_stat_ssl WHERE pid = pg_backend_pid()"
 	],
 	qr{^pid,ssl,version,cipher,bits,client_dn,client_serial,issuer_dn\r?\n
-				^\d+,t,TLSv[\d.]+,[\w-]+,\d+,/CN=ssltestuser,$serialno,\Q/CN=Test CA for PostgreSQL SSL regression test client certs\E\r?$}mx,
+				^\d+,t,TLSv[\d.]+,[\w-]+,\d+,/?CN=ssltestuser,$serialno,/?\QCN=Test CA for PostgreSQL SSL regression test client certs\E\r?$}mx,
 	'pg_stat_ssl with client certificate');
 
 # client key with wrong permissions
@@ -544,16 +505,16 @@ SKIP:
 	skip "Permissions check not enforced on Windows", 2 if ($windows_os);
 
 	$node->connect_fails(
-		"$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client_wrongperms.key'}",
+		"$common_connstr user=ssltestuser sslcert=ssl/client.crt " . sslkey('client_wrongperms.key'),
 		"certificate authorization fails because of file permissions",
 		expected_stderr =>
-		  qr!\Qprivate key file "$key{'client_wrongperms.key'}" has group or world access\E!
+		  qr!private key file \".*client_wrongperms\.key\" has group or world access!
 	);
 }
 
 # client cert belonging to another user
 $node->connect_fails(
-	"$common_connstr user=anotheruser sslcert=ssl/client.crt sslkey=$key{'client.key'}",
+	"$common_connstr user=anotheruser sslcert=ssl/client.crt " . sslkey('client.key'),
 	"certificate authorization fails with client cert belonging to another user",
 	expected_stderr =>
 	  qr/certificate authentication failed for user "anotheruser"/,
@@ -563,7 +524,7 @@ $node->connect_fails(
 
 # revoked client cert
 $node->connect_fails(
-	"$common_connstr user=ssltestuser sslcert=ssl/client-revoked.crt sslkey=$key{'client-revoked.key'}",
+	"$common_connstr user=ssltestuser sslcert=ssl/client-revoked.crt " . sslkey('client-revoked.key'),
 	"certificate authorization fails with revoked client cert",
 	expected_stderr => qr/SSL error: sslv3 alert certificate revoked/,
 	# revoked certificates should not authenticate the user
@@ -576,13 +537,13 @@ $common_connstr =
   "sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=verifydb hostaddr=$SERVERHOSTADDR";
 
 $node->connect_ok(
-	"$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client.key'}",
+	"$common_connstr user=ssltestuser sslcert=ssl/client.crt " . sslkey('client.key'),
 	"auth_option clientcert=verify-full succeeds with matching username and Common Name",
 	# verify-full does not provide authentication
 	log_unlike => [qr/connection authenticated:/],);
 
 $node->connect_fails(
-	"$common_connstr user=anotheruser sslcert=ssl/client.crt sslkey=$key{'client.key'}",
+	"$common_connstr user=anotheruser sslcert=ssl/client.crt " . sslkey('client.key'),
 	"auth_option clientcert=verify-full fails with mismatching username and Common Name",
 	expected_stderr =>
 	  qr/FATAL: .* "trust" authentication failed for user "anotheruser"/,
@@ -592,15 +553,15 @@ $node->connect_fails(
 # Check that connecting with auth-optionverify-ca in pg_hba :
 # works, when username doesn't match Common Name
 $node->connect_ok(
-	"$common_connstr user=yetanotheruser sslcert=ssl/client.crt sslkey=$key{'client.key'}",
+	"$common_connstr user=yetanotheruser sslcert=ssl/client.crt " . sslkey('client.key'),
 	"auth_option clientcert=verify-ca succeeds with mismatching username and Common Name",
 	# verify-full does not provide authentication
 	log_unlike => [qr/connection authenticated:/],);
 
 # intermediate client_ca.crt is provided by client, and isn't in server's ssl_ca_file
-switch_server_cert($node, 'server-cn-only', 'root_ca');
+switch_server_cert($node, certfile => 'server-cn-only', cafile => 'root_ca');
 $common_connstr =
-  "user=ssltestuser dbname=certdb sslkey=$key{'client.key'} sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR";
+  "user=ssltestuser dbname=certdb " . sslkey('client.key') . " sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR";
 
 $node->connect_ok(
 	"$common_connstr sslmode=require sslcert=ssl/client+client_ca.crt",
@@ -611,11 +572,12 @@ $node->connect_fails(
 	expected_stderr => qr/SSL error: tlsv1 alert unknown ca/);
 
 # test server-side CRL directory
-switch_server_cert($node, 'server-cn-only', undef, undef,
-	'root+client-crldir');
+switch_server_cert($node, certfile => 'server-cn-only', crldir => 'root+client-crldir');
 
 # revoked client cert
 $node->connect_fails(
-	"$common_connstr user=ssltestuser sslcert=ssl/client-revoked.crt sslkey=$key{'client-revoked.key'}",
+	"$common_connstr user=ssltestuser sslcert=ssl/client-revoked.crt " . sslkey('client-revoked.key'),
 	"certificate authorization fails with revoked client cert with server-side CRL directory",
 	expected_stderr => qr/SSL error: sslv3 alert certificate revoked/);
+
+done_testing();
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 86312be88c..7728ffcaa9 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -14,7 +14,7 @@ use File::Copy;
 use FindBin;
 use lib $FindBin::RealBin;
 
-use SSLServer;
+use SSL::Server;
 
 if ($ENV{with_ssl} ne 'openssl')
 {
@@ -30,8 +30,6 @@ my $SERVERHOSTCIDR = '127.0.0.1/32';
 my $supports_tls_server_end_point =
   check_pg_config("#define HAVE_X509_GET_SIGNATURE_NID 1");
 
-my $number_of_tests = $supports_tls_server_end_point ? 11 : 12;
-
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
 
@@ -50,7 +48,7 @@ $node->start;
 # Configure server for SSL connections, with password handling.
 configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
 	"scram-sha-256", 'password' => "pass", 'password_enc' => "scram-sha-256");
-switch_server_cert($node, 'server-cn-only');
+switch_server_cert($node, certfile => 'server-cn-only');
 $ENV{PGPASSWORD} = "pass";
 $common_connstr =
   "dbname=trustdb sslmode=require sslcert=invalid sslrootcert=invalid hostaddr=$SERVERHOSTADDR";
@@ -118,4 +116,4 @@ $node->connect_ok(
 		qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/
 	]);
 
-done_testing($number_of_tests);
+done_testing();
diff --git a/src/test/ssl/t/003_sslinfo.pl b/src/test/ssl/t/003_sslinfo.pl
index 8c760b39db..af7ffc5b04 100644
--- a/src/test/ssl/t/003_sslinfo.pl
+++ b/src/test/ssl/t/003_sslinfo.pl
@@ -12,16 +12,12 @@ use File::Copy;
 use FindBin;
 use lib $FindBin::RealBin;
 
-use SSLServer;
+use SSL::Server;
 
 if ($ENV{with_ssl} ne 'openssl')
 {
 	plan skip_all => 'OpenSSL not supported by this build';
 }
-else
-{
-	plan tests => 13;
-}
 
 #### Some configuration
 
@@ -35,17 +31,6 @@ my $SERVERHOSTCIDR = '127.0.0.1/32';
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
 
-# The client's private key must not be world-readable, so take a copy
-# of the key stored in the code tree and update its permissions.
-my $cert_tempdir = PostgreSQL::Test::Utils::tempdir();
-my $client_tmp_key = PostgreSQL::Test::Utils::perl2host("$cert_tempdir/client_ext.key");
-copy("ssl/client_ext.key", "$cert_tempdir/client_ext.key")
-  or die
-  "couldn't copy ssl/client_ext.key to $cert_tempdir/client_ext.key for permissions change: $!";
-chmod 0600, "$cert_tempdir/client_ext.key"
-  or die "failed to change permissions on $cert_tempdir/client_ext.key: $!";
-$client_tmp_key =~ s!\\!/!g if $PostgreSQL::Test::Utils::windows_os;
-
 #### Set up the server.
 
 note "setting up data directory";
@@ -64,11 +49,11 @@ configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
 # We aren't using any CRL's in this suite so we can keep using server-revoked
 # as server certificate for simple client.crt connection much like how the
 # 001 test does.
-switch_server_cert($node, 'server-revoked');
+switch_server_cert($node, certfile => 'server-revoked');
 
 $common_connstr =
   "sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=certdb hostaddr=$SERVERHOSTADDR " .
-  "user=ssltestuser sslcert=ssl/client_ext.crt sslkey=$client_tmp_key";
+  "user=ssltestuser sslcert=ssl/client_ext.crt " . sslkey('client_ext.key');
 
 # Make sure we can connect even though previous test suites have established this
 $node->connect_ok(
@@ -135,3 +120,5 @@ $result = $node->safe_psql("certdb",
   "SELECT value, critical FROM ssl_extension_info() WHERE name = 'basicConstraints';",
   connstr => $common_connstr);
 is($result, 'CA:FALSE|t', 'extract extension from cert');
+
+done_testing();
diff --git a/src/test/ssl/t/SSL/Backend/OpenSSL.pm b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
new file mode 100644
index 0000000000..156a560453
--- /dev/null
+++ b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
@@ -0,0 +1,241 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+=pod
+
+=head1 NAME
+
+SSL::Backend::OpenSSL
+
+=head1 SYNOPSIS
+
+  use SSL::Backend::OpenSSL qw(get_new_openssl_backend);
+
+  my $backend = get_new_openssl_backend();
+
+  $backend->init($pgdata);
+
+=head1 DESCRIPTION
+
+SSL::Backend::OpenSSL implements the library specific parts in SSL::Server
+for a PostgreSQL cluster compiled against OpenSSL.
+
+=cut
+
+package SSL::Backend::OpenSSL;
+
+use strict;
+use warnings;
+use Exporter;
+use File::Basename;
+use File::Copy;
+
+our @ISA       = qw(Exporter);
+our @EXPORT_OK = qw(get_new_openssl_backend);
+
+our (%key);
+
+INIT
+{
+}
+
+sub new
+{
+	my ($class) = @_;
+
+	my $self = { _library => 'OpenSSL' };
+
+	bless $self, $class;
+
+	return $self;
+}
+
+=pod
+
+=head1 METHODS
+
+=over
+
+=item SSL::Backend::OpenSSL::get_new_openssl_backend()
+
+Create a new instance of the OpenSSL backend.
+
+=cut
+
+sub get_new_openssl_backend
+{
+	my $class = 'SSL::Backend::OpenSSL';
+
+	my $backend = $class->new();
+
+	return $backend;
+}
+
+=pod
+
+=item $backend->init()
+
+Install certificates, keys and CRL files required to run the tests against an
+OpenSSL backend.
+
+=cut
+
+sub init
+{
+	my ($self, $pgdata) = @_;
+
+	# Install server certificates and keys into the cluster data directory.
+	copy_files("ssl/server-*.crt", $pgdata);
+	copy_files("ssl/server-*.key", $pgdata);
+	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_ca.crt",        $pgdata);
+	copy_files("ssl/root+client.crl",    $pgdata);
+	mkdir("$pgdata/root+client-crldir")
+	  or die "unable to create server CRL dir $pgdata/root+client-crldir: $!";
+	copy_files("ssl/root+client-crldir/*", "$pgdata/root+client-crldir/");
+
+	# The client's private key must not be world-readable, so take a copy
+	# of the key stored in the code tree and update its permissions.
+	#
+	# This changes to using keys stored in a temporary path for the rest of
+	# the tests. To get the full path for inclusion in connection strings, the
+	# %key hash can be interrogated.
+	my $cert_tempdir = PostgreSQL::Test::Utils::tempdir();
+	my @keys = (
+		"client.key",               "client-revoked.key",
+		"client-der.key",           "client-encrypted-pem.key",
+		"client-encrypted-der.key", "client-dn.key",
+		"client_ext.key");
+	foreach my $keyfile (@keys)
+	{
+		copy("ssl/$keyfile", "$cert_tempdir/$keyfile")
+		  or die
+		  "couldn't copy ssl/$keyfile to $cert_tempdir/$keyfile for permissions change: $!";
+		chmod 0600, "$cert_tempdir/$keyfile"
+		  or die "failed to change permissions on $cert_tempdir/$keyfile: $!";
+		$key{$keyfile} = PostgreSQL::Test::Utils::perl2host("$cert_tempdir/$keyfile");
+		$key{$keyfile} =~ s!\\!/!g if $PostgreSQL::Test::Utils::windows_os;
+	}
+
+	# Also make a copy of client.key explicitly world-readable in order to be
+	# able to test incorrect permissions.  We can't necessarily rely on the
+	# file in the source tree having those permissions.
+	copy("ssl/client.key", "$cert_tempdir/client_wrongperms.key")
+	  or die
+	  "couldn't copy ssl/client_key to $cert_tempdir/client_wrongperms.key for permission change: $!";
+	chmod 0644, "$cert_tempdir/client_wrongperms.key"
+	  or die "failed to change permissions on $cert_tempdir/client_wrongperms.key: $!";
+	$key{'client_wrongperms.key'} = PostgreSQL::Test::Utils::perl2host("$cert_tempdir/client_wrongperms.key");
+	$key{'client_wrongperms.key'} =~ s!\\!/!g if $PostgreSQL::Test::Utils::windows_os;
+}
+
+=pod
+
+=item $backend->get_sslkey()
+
+Get an 'sslkey' connection string parameter for the specified key which has
+the correct path for direct inclusion in a connection string.
+
+=cut
+
+sub get_sslkey
+{
+	my ($self, $keyfile) = @_;
+
+	return " sslkey=$key{$keyfile}";
+}
+
+=pod
+
+=item $backend->set_server_cert(params)
+
+Change the configuration to use given server cert, key and crl file(s).
+
+=over
+
+=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'.
+
+=item certfile => B<value>
+
+The server certificate file to use for the C<ssl_cert_file> GUC.
+
+=item keyfile => B<value>
+
+The private key file to use for the C<ssl_key_file GUC>. If omitted it will
+default to the B<certfile>.key.
+
+=item crlfile => B<value>
+
+The CRL file to use for the C<ssl_crl_file> GUC. If omitted it will default to
+'root+client.crl'.
+
+=item crldir => B<value>
+
+The CRL directory to use for the C<ssl_crl_dir> GUC. If omitted,
+C<no ssl_crl_dir> configuration parameter will be set.
+
+=back
+
+=cut
+
+sub set_server_cert
+{
+	my ($self, $params) = @_;
+
+	$params->{cafile} = 'root+client_ca' unless defined $params->{cafile};
+	$params->{crlfile} = 'root+client.crl' unless defined $params->{crlfile};
+	$params->{keyfile} = $params->{certfile} unless defined $params->{keyfile};
+
+	my $sslconf =
+	    "ssl_ca_file='$params->{cafile}.crt'\n"
+	  . "ssl_cert_file='$params->{certfile}.crt'\n"
+	  . "ssl_key_file='$params->{keyfile}.key'\n"
+	  . "ssl_crl_file='$params->{crlfile}'\n";
+	$sslconf .= "ssl_crl_dir='$params->{crldir}'\n" if defined $params->{crldir};
+
+	return $sslconf;
+}
+
+=pod
+
+=item $backend->get_library()
+
+Returns the name of the SSL library, in this case "OpenSSL".
+
+=cut
+
+sub get_library
+{
+	my ($self) = @_;
+
+	return $self->{_library};
+}
+
+# Internal method for copying a set of files, taking into account wildcards
+sub copy_files
+{
+	my $orig = shift;
+	my $dest = shift;
+
+	my @orig_files = glob $orig;
+	foreach my $orig_file (@orig_files)
+	{
+		my $base_file = basename($orig_file);
+		copy($orig_file, "$dest/$base_file")
+		  or die "Could not copy $orig_file to $dest";
+	}
+	return;
+}
+
+=pod
+
+=back
+
+=cut
+
+1;
diff --git a/src/test/ssl/t/SSLServer.pm b/src/test/ssl/t/SSL/Server.pm
similarity index 51%
rename from src/test/ssl/t/SSLServer.pm
rename to src/test/ssl/t/SSL/Server.pm
index c85c6fd997..4be338647d 100644
--- a/src/test/ssl/t/SSLServer.pm
+++ b/src/test/ssl/t/SSL/Server.pm
@@ -1,63 +1,125 @@
 
 # Copyright (c) 2021-2022, PostgreSQL Global Development Group
 
-# This module sets up a test server, for the SSL regression tests.
-#
-# The server is configured as follows:
-#
-# - SSL enabled, with the server certificate specified by argument to
-#   switch_server_cert function.
-# - ssl/root+client_ca.crt as the CA root for validating client certs.
-# - reject non-SSL connections
-# - a database called trustdb that lets anyone in
-# - another database called certdb that uses certificate authentication, ie.
-#   the client must present a valid certificate signed by the client CA
-#
-# The server is configured to only accept connections from localhost. If you
-# want to run the client from another host, you'll have to configure that
-# manually.
-#
-# Note: Someone running these test could have key or certificate files
-# in their ~/.postgresql/, which would interfere with the tests.  The
-# way to override that is to specify sslcert=invalid and/or
-# sslrootcert=invalid if no actual certificate is used for a
-# particular test.  libpq will ignore specifications that name
-# nonexisting files.  (sslkey and sslcrl do not need to specified
-# explicitly because an invalid sslcert or sslrootcert, respectively,
-# causes those to be ignored.)
-
-package SSLServer;
+=pod
+
+=head1 NAME
+
+SSL::Server - Class for setting up SSL in a PostgreSQL Cluster
+
+=head1 SYNOPSIS
+
+  use PostgreSQL::Test::Cluster;
+  use SSL::Server;
+
+  # Create a new cluster
+  my $node = PostgreSQL::Test::Cluster->new('primary');
+
+  # Initialize and start the new cluster
+  $node->init;
+  $node->start;
+
+  # Configure SSL on the newly formed cluster
+  configure_test_server_for_ssl($node, '127.0.0.1', '127.0.0.1/32', 'trust');
+
+=head1 DESCRIPTION
+
+SSL::Server configures an existing test cluster, for the SSL regression tests.
+
+The server is configured as follows:
+
+=over
+
+=item * SSL enabled, with the server certificate specified by arguments to switch_server_cert function.
+
+=item * reject non-SSL connections
+
+=item * a database called trustdb that lets anyone in
+
+=item * another database called certdb that uses certificate authentication, ie.  the client must present a valid certificate signed by the client CA
+
+=back
+
+The server is configured to only accept connections from localhost. If you
+want to run the client from another host, you'll have to configure that
+manually.
+
+Note: Someone running these test could have key or certificate files in their
+~/.postgresql/, which would interfere with the tests.  The way to override that
+is to specify sslcert=invalid and/or sslrootcert=invalid if no actual
+certificate is used for a particular test.  libpq will ignore specifications
+that name nonexisting files.  (sslkey and sslcrl do not need to specified
+explicitly because an invalid sslcert or sslrootcert, respectively, causes
+those to be ignored.)
+
+The SSL::Server module presents a SSL library abstraction to the test writer,
+which in turn use modules in SSL::Backend which implements the SSL library
+specific infrastructure. Currently only OpenSSL is supported.
+
+=cut
+
+package SSL::Server;
 
 use strict;
 use warnings;
 use PostgreSQL::Test::Cluster;
 use PostgreSQL::Test::Utils;
-use File::Basename;
-use File::Copy;
 use Test::More;
+use SSL::Backend::OpenSSL qw(get_new_openssl_backend);
+
+our ($openssl, $backend);
 
 use Exporter 'import';
 our @EXPORT = qw(
   configure_test_server_for_ssl
   switch_server_cert
+  sslkey
 );
 
-# Copy a set of files, taking into account wildcards
-sub copy_files
+INIT
 {
-	my $orig = shift;
-	my $dest = shift;
-
-	my @orig_files = glob $orig;
-	foreach my $orig_file (@orig_files)
+	# The TLS backend which the server is using should be mostly transparent
+	# for the user, apart from individual configuration settings, so keep the
+	# backend specific things abstracted behind SSL::Server.
+	if ($ENV{with_ssl} eq 'openssl')
 	{
-		my $base_file = basename($orig_file);
-		copy($orig_file, "$dest/$base_file")
-		  or die "Could not copy $orig_file to $dest";
+		$backend = get_new_openssl_backend();
+		$openssl = 1;
+	}
+	else
+	{
+		die 'No SSL backend defined';
 	}
-	return;
 }
 
+=pod
+
+=head1 METHODS
+
+=over
+
+=item sslkey(filename)
+
+Return a C<sslkey> construct for the specified key for use in a connection
+string.
+
+=cut
+
+sub sslkey
+{
+	my $keyfile = shift;
+
+	return $backend->get_sslkey($keyfile);
+}
+
+=pod
+
+=item configure_test_server_for_ssl()
+
+Configure the cluster for listening on SSL connections.
+
+=cut
+
 # serverhost: what to put in listen_addresses, e.g. '127.0.0.1'
 # servercidr: what to put in pg_hba.conf, e.g. '127.0.0.1/32'
 sub configure_test_server_for_ssl
@@ -121,19 +183,12 @@ sub configure_test_server_for_ssl
 
 	close $conf;
 
-	# ssl configuration will be placed here
+	# SSL configuration will be placed here
 	open my $sslconf, '>', "$pgdata/sslconfig.conf";
 	close $sslconf;
 
-	# Copy all server certificates and keys, and client root cert, to the data dir
-	copy_files("ssl/server-*.crt", $pgdata);
-	copy_files("ssl/server-*.key", $pgdata);
-	chmod(0600, glob "$pgdata/server-*.key") or die $!;
-	copy_files("ssl/root+client_ca.crt", $pgdata);
-	copy_files("ssl/root_ca.crt",        $pgdata);
-	copy_files("ssl/root+client.crl",    $pgdata);
-	mkdir("$pgdata/root+client-crldir");
-	copy_files("ssl/root+client-crldir/*", "$pgdata/root+client-crldir/");
+	# Perform backend specific configuration
+	$backend->init($pgdata);
 
 	# Stop and restart server to load new listen_addresses.
 	$node->restart;
@@ -144,37 +199,86 @@ sub configure_test_server_for_ssl
 	return;
 }
 
-# Change the configuration to use given server cert file, and reload
-# the server so that the configuration takes effect.
+=pod
+
+=item SSL::Server::ssl_library()
+
+Get the name of the currently used SSL backend.
+
+=cut
+
+sub ssl_library
+{
+	return $backend->get_library();
+}
+
+=pod
+
+=item switch_server_cert()
+
+Change the configuration to use the given set of certificate, key, ca and
+CRL, and potentially reload the configuration by restarting the server so
+that the configuration takes effect.  Restarting is the default, passing
+restart => 'no' opts out of it leaving the server running.
+
+=over
+
+=item cafile => B<value>
+
+The CA certificate to use. Implementation is SSL backend specific.
+
+=item certfile => B<value>
+
+The certificate file to use. Implementation is SSL backend specific.
+
+=item keyfile => B<value>
+
+The private key to to use. Implementation is SSL backend specific.
+
+=item crlfile => B<value>
+
+The CRL file to use. Implementation is SSL backend specific.
+
+=item crldir => B<value>
+
+The CRL directory to use. Implementation is SSL backend specific.
+
+=item passphrase_cmd => B<value>
+
+The passphrase command to use. If not set, an empty passphrase command will
+be set.
+
+=item restart => B<value>
+
+If set to 'no', the server won't be restarted after updating the settings.
+If omitted, or any other value is passed, the server will be restarted before
+returning.
+
+=back
+
+=cut
+
 sub switch_server_cert
 {
-	my $node     = $_[0];
-	my $certfile = $_[1];
-	my $cafile   = $_[2] || "root+client_ca";
-	my $crlfile  = "root+client.crl";
-	my $crldir;
+	my $node   = shift;
+	my %params = @_;
 	my $pgdata = $node->data_dir;
 
-	# defaults to use crl file
-	if (defined $_[3] || defined $_[4])
-	{
-		$crlfile = $_[3];
-		$crldir  = $_[4];
-	}
-
 	open my $sslconf, '>', "$pgdata/sslconfig.conf";
 	print $sslconf "ssl=on\n";
-	print $sslconf "ssl_ca_file='$cafile.crt'\n";
-	print $sslconf "ssl_cert_file='$certfile.crt'\n";
-	print $sslconf "ssl_key_file='$certfile.key'\n";
-	print $sslconf "ssl_crl_file='$crlfile'\n" if defined $crlfile;
-	print $sslconf "ssl_crl_dir='$crldir'\n"   if defined $crldir;
+	print $sslconf $backend->set_server_cert(\%params);
+	print $sslconf "ssl_passphrase_command='" . $params{passphrase_cmd} . "'\n"
+	  if defined $params{passphrase_cmd};
 	close $sslconf;
 
+	return if (defined($params{restart}) && $params{restart} eq 'no');
+
 	$node->restart;
 	return;
 }
 
+
+# Internal function for configuring pg_hba.conf for SSL connections.
 sub configure_hba_for_ssl
 {
 	my ($node, $servercidr, $authmethod) = @_;
@@ -216,4 +320,10 @@ sub configure_hba_for_ssl
 	return;
 }
 
+=pod
+
+=back
+
+=cut
+
 1;
-- 
2.24.3 (Apple Git-128)

