Refactoring SSL tests

Started by Daniel Gustafssonalmost 4 years ago9 messages
#1Daniel Gustafsson
daniel@yesql.se
1 attachment(s)

As part of the NSS patchset (and Secure Transport before that), I had to
refactor the SSL tests to handle different SSL libraries. The current tests
and test module is quite tied to how OpenSSL works wrt setting up the server,
the attached refactors this and abstracts the OpenSSL specifics more like how
the rest of the codebase is set up.

The tight coupling is of course not a problem right now, but I think this patch
has more benefits making it a candidate for going in regardless of the fate of
the NSS patchset. This is essentially the 0002 patch from that patchset with
additional cleanup and documentation:

* switch_server_cert takes a set of named parameters rather than a fixed set
with defaults depending on each other, which made adding ssl_passphrase to it
cumbersome. It also adds readability IMO.

* SSLServer is renamed SSL::Server, which in turn use SSL::Backend::X where X
is the backend pointed to by with_ssl. Each backend will implement its own
module which is responsible for setting up keys/certs and to resolve sslkey
values to their full paths. The idea is that the namespace will also allow for
an SSL::Client in the future when we implment running client tests against
different servers etc.

* The modules are POD documented.

* While not related to the refactor per se, the hardcoded number of planned
tests is removed in favor of calling done_testing().

With this, adding a new SSL library is quite straightforward, I've done the
legwork to test that =)

I opted for avoiding too invasive changes leaving the tests somewhat easy to
compare to back branches.

Thoughts? I'm fairly sure there are many crimes against Perl in this patch,
I'm happy to take pointers on how to improve that.

--
Daniel Gustafsson https://vmware.com/

Attachments:

0001-Refactor-SSL-tests.patchapplication/octet-stream; name=0001-Refactor-SSL-tests.patch; x-unix-mode=0644Download
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)

#2Andrew Dunstan
andrew@dunslane.net
In reply to: Daniel Gustafsson (#1)
Re: Refactoring SSL tests

On 2/2/22 08:26, Daniel Gustafsson wrote:

Thoughts? I'm fairly sure there are many crimes against Perl in this patch,
I'm happy to take pointers on how to improve that.

It feels a bit odd to me from a perl POV. I think it needs to more along
the lines of standard OO patterns. I'll take a stab at that based on
this, might be a few days.

cheers

andrew

--
Andrew Dunstan
EDB: https://www.enterprisedb.com

#3Daniel Gustafsson
daniel@yesql.se
In reply to: Andrew Dunstan (#2)
Re: Refactoring SSL tests

On 2 Feb 2022, at 17:09, Andrew Dunstan <andrew@dunslane.net> wrote:
On 2/2/22 08:26, Daniel Gustafsson wrote:

Thoughts? I'm fairly sure there are many crimes against Perl in this patch,
I'm happy to take pointers on how to improve that.

It feels a bit odd to me from a perl POV. I think it needs to more along
the lines of standard OO patterns. I'll take a stab at that based on
this, might be a few days.

That would be great, thanks!

--
Daniel Gustafsson https://vmware.com/

#4Andrew Dunstan
andrew@dunslane.net
In reply to: Daniel Gustafsson (#3)
1 attachment(s)
Re: Refactoring SSL tests

On 2/2/22 14:50, Daniel Gustafsson wrote:

On 2 Feb 2022, at 17:09, Andrew Dunstan <andrew@dunslane.net> wrote:
On 2/2/22 08:26, Daniel Gustafsson wrote:

Thoughts? I'm fairly sure there are many crimes against Perl in this patch,
I'm happy to take pointers on how to improve that.

It feels a bit odd to me from a perl POV. I think it needs to more along
the lines of standard OO patterns. I'll take a stab at that based on
this, might be a few days.

That would be great, thanks!

Here's the result of that surgery.  It's a little incomplete in that it
needs some POD adjustment, but I think the code is right - it passes
testing for me.

One of the advantages of this, apart from being more idiomatic, is that
by avoiding the use of package level variables you can have two
SSL::Server objects, one for OpenSSL and (eventually) one for NSS. This
was the original motivation for the recent install_path additions to
PostgreSQL::Test::Cluster, so it complements that work nicely.

cheers

andrew

--
Andrew Dunstan
EDB: https://www.enterprisedb.com

Attachments:

0001-ssl-test-refactoring.patchtext/x-patch; charset=UTF-8; name=0001-ssl-test-refactoring.patchDownload
From fa9bd034210aae4e11eed463e1a1365231c944f5 Mon Sep 17 00:00:00 2001
From: Andrew Dunstan <andrew@dunslane.net>
Date: Mon, 7 Feb 2022 11:04:53 -0500
Subject: [PATCH] ssl test refactoring

---
 src/test/ssl/t/001_ssltests.pl        | 144 +++++------
 src/test/ssl/t/002_scram.pl           |  21 +-
 src/test/ssl/t/003_sslinfo.pl         |  33 ++-
 src/test/ssl/t/SSL/Backend/OpenSSL.pm | 227 ++++++++++++++++++
 src/test/ssl/t/SSL/Server.pm          | 332 ++++++++++++++++++++++++++
 src/test/ssl/t/SSLServer.pm           | 219 -----------------
 6 files changed, 647 insertions(+), 329 deletions(-)
 create mode 100644 src/test/ssl/t/SSL/Backend/OpenSSL.pm
 create mode 100644 src/test/ssl/t/SSL/Server.pm
 delete mode 100644 src/test/ssl/t/SSLServer.pm

diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index b1fb15ce80..d5383a58ce 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -8,20 +8,24 @@ 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
+
+my $ssl_server = SSL::Server->new();
+sub sslkey
+{
+	return $ssl_server->sslkey(@_);
+}
+sub switch_server_cert
 {
-	plan tests => 110;
+	$ssl_server->switch_server_cert(@_);
 }
 
 #### Some configuration
@@ -36,39 +40,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 +54,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');
+$ssl_server->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 +111,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 +225,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 +254,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 +278,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 +296,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 +311,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 +377,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 +412,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 +422,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 +447,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 +501,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 +515,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 +534,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 +547,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 +563,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 +582,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..bd7a84cec2 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -14,13 +14,24 @@ 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';
 }
 
+my $ssl_server = SSL::Server->new();
+sub sslkey
+{
+	return $ssl_server->sslkey(@_);
+}
+sub switch_server_cert
+{
+	$ssl_server->switch_server_cert(@_);
+}
+
+
 # This is the hostname used to connect to the server.
 my $SERVERHOSTADDR = '127.0.0.1';
 # This is the pattern to use in pg_hba.conf to match incoming connections.
@@ -30,8 +41,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;
 
@@ -48,9 +57,9 @@ $ENV{PGPORT} = $node->port;
 $node->start;
 
 # Configure server for SSL connections, with password handling.
-configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
+$ssl_server->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 +127,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..bc555048da 100644
--- a/src/test/ssl/t/003_sslinfo.pl
+++ b/src/test/ssl/t/003_sslinfo.pl
@@ -12,18 +12,24 @@ 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
+
+#### Some configuration
+my $ssl_server = SSL::Server->new();
+sub sslkey
+{
+	return $ssl_server->sslkey(@_);
+}
+sub switch_server_cert
 {
-	plan tests => 13;
+	$ssl_server->switch_server_cert(@_);
 }
 
-#### Some configuration
 
 # This is the hostname used to connect to the server. This cannot be a
 # hostname, because the server certificate is always for the domain
@@ -35,17 +41,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";
@@ -58,17 +53,17 @@ $ENV{PGHOST} = $node->host;
 $ENV{PGPORT} = $node->port;
 $node->start;
 
-configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
+$ssl_server->configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
 	'trust', extensions => [ qw(sslinfo) ]);
 
 # 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 +130,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..01b8a0857e
--- /dev/null
+++ b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
@@ -0,0 +1,227 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+=pod
+
+=head1 NAME
+
+SSL::Backend::OpenSSL
+
+=head1 SYNOPSIS
+
+  use SSL::Backend::OpenSSL;
+
+  my $backend = SSL::backend::OpenSSL->new();
+
+  $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 File::Basename;
+use File::Copy;
+
+=pod
+
+=head1 METHODS
+
+=over
+
+=item SSL::Backend::OpenSSL::get_new_openssl_backend()
+
+Create a new instance of the OpenSSL backend.
+
+=cut
+
+sub new
+{
+	my ($class) = @_;
+
+	my $self = { _library => 'OpenSSL', key => {} };
+
+	bless $self, $class;
+
+	return $self;
+}
+
+=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: $!";
+		$self->{key}->{$keyfile} =
+		  PostgreSQL::Test::Utils::perl2host("$cert_tempdir/$keyfile");
+		$self->{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: $!";
+	$self->{key}->{'client_wrongperms.key'} =
+	  PostgreSQL::Test::Utils::perl2host("$cert_tempdir/client_wrongperms.key");
+	$self->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=$self->{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/SSL/Server.pm b/src/test/ssl/t/SSL/Server.pm
new file mode 100644
index 0000000000..fd6ce924f2
--- /dev/null
+++ b/src/test/ssl/t/SSL/Server.pm
@@ -0,0 +1,332 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+=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 Test::More;
+use SSL::Backend::OpenSSL qw(get_new_openssl_backend);
+
+sub new
+{
+	my $class = shift;
+	my $flavor = shift || $ENV{with_ssl};
+	die "SSL flavor not defined" unless $flavor;
+	my $self = {};
+	bless $self, $class;
+	if ($flavor =~ /\Aopenssl\z/i)
+	{
+		$self->{flavor} = 'openssl';
+		$self->{backend} = SSL::Backend::OpenSSL->new();
+	}
+	else
+	{
+		die "SSL flavor $flavor unknown";
+	}
+	return $self;
+}
+
+=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 $self = shift;
+	my $keyfile = shift;
+	my $backend = $self->{backend};
+
+	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
+{
+	my $self=shift;
+	my ($node, $serverhost, $servercidr, $authmethod, %params) = @_;
+	my $backend = $self->{backend};
+	my $pgdata = $node->data_dir;
+
+	my @databases = ( 'trustdb', 'certdb', 'certdb_dn', 'certdb_dn_re', 'certdb_cn', 'verifydb' );
+
+	# Create test users and databases
+	$node->psql('postgres', "CREATE USER ssltestuser");
+	$node->psql('postgres', "CREATE USER md5testuser");
+	$node->psql('postgres', "CREATE USER anotheruser");
+	$node->psql('postgres', "CREATE USER yetanotheruser");
+
+	foreach my $db (@databases)
+	{
+		$node->psql('postgres', "CREATE DATABASE $db");
+	}
+
+	# Update password of each user as needed.
+	if (defined($params{password}))
+	{
+		die "Password encryption must be specified when password is set"
+			unless defined($params{password_enc});
+
+		$node->psql('postgres',
+			"SET password_encryption='$params{password_enc}'; ALTER USER ssltestuser PASSWORD '$params{password}';"
+		);
+		# A special user that always has an md5-encrypted password
+		$node->psql('postgres',
+			"SET password_encryption='md5'; ALTER USER md5testuser PASSWORD '$params{password}';"
+		);
+		$node->psql('postgres',
+			"SET password_encryption='$params{password_enc}'; ALTER USER anotheruser PASSWORD '$params{password}';"
+		);
+	}
+
+	# Create any extensions requested in the setup
+	if (defined($params{extensions}))
+	{
+		foreach my $extension (@{$params{extensions}})
+		{
+			foreach my $db (@databases)
+			{
+				$node->psql($db, "CREATE EXTENSION $extension CASCADE;");
+			}
+		}
+	}
+
+	# enable logging etc.
+	open my $conf, '>>', "$pgdata/postgresql.conf";
+	print $conf "fsync=off\n";
+	print $conf "log_connections=on\n";
+	print $conf "log_hostname=on\n";
+	print $conf "listen_addresses='$serverhost'\n";
+	print $conf "log_statement=all\n";
+
+	# enable SSL and set up server key
+	print $conf "include 'sslconfig.conf'\n";
+
+	close $conf;
+
+	# SSL configuration will be placed here
+	open my $sslconf, '>', "$pgdata/sslconfig.conf";
+	close $sslconf;
+
+	# Perform backend specific configuration
+	$backend->init($pgdata);
+
+	# Stop and restart server to load new listen_addresses.
+	$node->restart;
+
+	# Change pg_hba after restart because hostssl requires ssl=on
+	_configure_hba_for_ssl($node, $servercidr, $authmethod);
+
+	return;
+}
+
+=pod
+
+=item SSL::Server::ssl_library()
+
+Get the name of the currently used SSL backend.
+
+=cut
+
+sub ssl_library
+{
+	my $self = shift;
+	my $backend = $self->{backend};
+
+	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 $self = shift;
+	my $node   = shift;
+	my $backend = $self->{backend};
+	my %params = @_;
+	my $pgdata = $node->data_dir;
+
+	open my $sslconf, '>', "$pgdata/sslconfig.conf";
+	print $sslconf "ssl=on\n";
+	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) = @_;
+	my $pgdata = $node->data_dir;
+
+	# Only accept SSL connections from $servercidr. Our tests don't depend on this
+	# but seems best to keep it as narrow as possible for security reasons.
+	#
+	# When connecting to certdb, also check the client certificate.
+	open my $hba, '>', "$pgdata/pg_hba.conf";
+	print $hba
+	  "# TYPE  DATABASE        USER            ADDRESS                 METHOD             OPTIONS\n";
+	print $hba
+	  "hostssl trustdb         md5testuser     $servercidr            md5\n";
+	print $hba
+	  "hostssl trustdb         all             $servercidr            $authmethod\n";
+	print $hba
+	  "hostssl verifydb        ssltestuser     $servercidr            $authmethod        clientcert=verify-full\n";
+	print $hba
+	  "hostssl verifydb        anotheruser     $servercidr            $authmethod        clientcert=verify-full\n";
+	print $hba
+	  "hostssl verifydb        yetanotheruser  $servercidr            $authmethod        clientcert=verify-ca\n";
+	print $hba
+	  "hostssl certdb          all             $servercidr            cert\n";
+	print $hba
+	  "hostssl certdb_dn       all             $servercidr            cert clientname=DN map=dn\n",
+	  "hostssl certdb_dn_re    all             $servercidr            cert clientname=DN map=dnre\n",
+	  "hostssl certdb_cn       all             $servercidr            cert clientname=CN map=cn\n";
+	close $hba;
+
+	# Also set the ident maps. Note: fields with commas must be quoted
+	open my $map, ">", "$pgdata/pg_ident.conf";
+	print $map
+	  "# MAPNAME       SYSTEM-USERNAME                           PG-USERNAME\n",
+	  "dn             \"CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG\"    ssltestuser\n",
+	  "dnre           \"/^.*OU=Testing,.*\$\"                    ssltestuser\n",
+	  "cn              ssltestuser-dn                            ssltestuser\n";
+
+	return;
+}
+
+=pod
+
+=back
+
+=cut
+
+1;
diff --git a/src/test/ssl/t/SSLServer.pm b/src/test/ssl/t/SSLServer.pm
deleted file mode 100644
index c85c6fd997..0000000000
--- a/src/test/ssl/t/SSLServer.pm
+++ /dev/null
@@ -1,219 +0,0 @@
-
-# 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;
-
-use strict;
-use warnings;
-use PostgreSQL::Test::Cluster;
-use PostgreSQL::Test::Utils;
-use File::Basename;
-use File::Copy;
-use Test::More;
-
-use Exporter 'import';
-our @EXPORT = qw(
-  configure_test_server_for_ssl
-  switch_server_cert
-);
-
-# Copy 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;
-}
-
-# 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
-{
-	my ($node, $serverhost, $servercidr, $authmethod, %params) = @_;
-	my $pgdata = $node->data_dir;
-
-	my @databases = ( 'trustdb', 'certdb', 'certdb_dn', 'certdb_dn_re', 'certdb_cn', 'verifydb' );
-
-	# Create test users and databases
-	$node->psql('postgres', "CREATE USER ssltestuser");
-	$node->psql('postgres', "CREATE USER md5testuser");
-	$node->psql('postgres', "CREATE USER anotheruser");
-	$node->psql('postgres', "CREATE USER yetanotheruser");
-
-	foreach my $db (@databases)
-	{
-		$node->psql('postgres', "CREATE DATABASE $db");
-	}
-
-	# Update password of each user as needed.
-	if (defined($params{password}))
-	{
-		die "Password encryption must be specified when password is set"
-			unless defined($params{password_enc});
-
-		$node->psql('postgres',
-			"SET password_encryption='$params{password_enc}'; ALTER USER ssltestuser PASSWORD '$params{password}';"
-		);
-		# A special user that always has an md5-encrypted password
-		$node->psql('postgres',
-			"SET password_encryption='md5'; ALTER USER md5testuser PASSWORD '$params{password}';"
-		);
-		$node->psql('postgres',
-			"SET password_encryption='$params{password_enc}'; ALTER USER anotheruser PASSWORD '$params{password}';"
-		);
-	}
-
-	# Create any extensions requested in the setup
-	if (defined($params{extensions}))
-	{
-		foreach my $extension (@{$params{extensions}})
-		{
-			foreach my $db (@databases)
-			{
-				$node->psql($db, "CREATE EXTENSION $extension CASCADE;");
-			}
-		}
-	}
-
-	# enable logging etc.
-	open my $conf, '>>', "$pgdata/postgresql.conf";
-	print $conf "fsync=off\n";
-	print $conf "log_connections=on\n";
-	print $conf "log_hostname=on\n";
-	print $conf "listen_addresses='$serverhost'\n";
-	print $conf "log_statement=all\n";
-
-	# enable SSL and set up server key
-	print $conf "include 'sslconfig.conf'\n";
-
-	close $conf;
-
-	# 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/");
-
-	# Stop and restart server to load new listen_addresses.
-	$node->restart;
-
-	# Change pg_hba after restart because hostssl requires ssl=on
-	configure_hba_for_ssl($node, $servercidr, $authmethod);
-
-	return;
-}
-
-# Change the configuration to use given server cert file, and reload
-# the server so that the configuration takes effect.
-sub switch_server_cert
-{
-	my $node     = $_[0];
-	my $certfile = $_[1];
-	my $cafile   = $_[2] || "root+client_ca";
-	my $crlfile  = "root+client.crl";
-	my $crldir;
-	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;
-	close $sslconf;
-
-	$node->restart;
-	return;
-}
-
-sub configure_hba_for_ssl
-{
-	my ($node, $servercidr, $authmethod) = @_;
-	my $pgdata = $node->data_dir;
-
-	# Only accept SSL connections from $servercidr. Our tests don't depend on this
-	# but seems best to keep it as narrow as possible for security reasons.
-	#
-	# When connecting to certdb, also check the client certificate.
-	open my $hba, '>', "$pgdata/pg_hba.conf";
-	print $hba
-	  "# TYPE  DATABASE        USER            ADDRESS                 METHOD             OPTIONS\n";
-	print $hba
-	  "hostssl trustdb         md5testuser     $servercidr            md5\n";
-	print $hba
-	  "hostssl trustdb         all             $servercidr            $authmethod\n";
-	print $hba
-	  "hostssl verifydb        ssltestuser     $servercidr            $authmethod        clientcert=verify-full\n";
-	print $hba
-	  "hostssl verifydb        anotheruser     $servercidr            $authmethod        clientcert=verify-full\n";
-	print $hba
-	  "hostssl verifydb        yetanotheruser  $servercidr            $authmethod        clientcert=verify-ca\n";
-	print $hba
-	  "hostssl certdb          all             $servercidr            cert\n";
-	print $hba
-	  "hostssl certdb_dn       all             $servercidr            cert clientname=DN map=dn\n",
-	  "hostssl certdb_dn_re    all             $servercidr            cert clientname=DN map=dnre\n",
-	  "hostssl certdb_cn       all             $servercidr            cert clientname=CN map=cn\n";
-	close $hba;
-
-	# Also set the ident maps. Note: fields with commas must be quoted
-	open my $map, ">", "$pgdata/pg_ident.conf";
-	print $map
-	  "# MAPNAME       SYSTEM-USERNAME                           PG-USERNAME\n",
-	  "dn             \"CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG\"    ssltestuser\n",
-	  "dnre           \"/^.*OU=Testing,.*\$\"                    ssltestuser\n",
-	  "cn              ssltestuser-dn                            ssltestuser\n";
-
-	return;
-}
-
-1;
-- 
2.34.1

#5Daniel Gustafsson
daniel@yesql.se
In reply to: Andrew Dunstan (#4)
1 attachment(s)
Re: Refactoring SSL tests

On 7 Feb 2022, at 17:29, Andrew Dunstan <andrew@dunslane.net> wrote:
On 2/2/22 14:50, Daniel Gustafsson wrote:

On 2 Feb 2022, at 17:09, Andrew Dunstan <andrew@dunslane.net> wrote:
On 2/2/22 08:26, Daniel Gustafsson wrote:

Thoughts? I'm fairly sure there are many crimes against Perl in this patch,
I'm happy to take pointers on how to improve that.

It feels a bit odd to me from a perl POV. I think it needs to more along
the lines of standard OO patterns. I'll take a stab at that based on
this, might be a few days.

That would be great, thanks!

Here's the result of that surgery. It's a little incomplete in that it
needs some POD adjustment, but I think the code is right - it passes
testing for me.

Confirmed, it passes all tests for me as well.

One of the advantages of this, apart from being more idiomatic, is that
by avoiding the use of package level variables you can have two
SSL::Server objects, one for OpenSSL and (eventually) one for NSS. This
was the original motivation for the recent install_path additions to
PostgreSQL::Test::Cluster, so it complements that work nicely.

Agreed, this version is a clear improvement over my attempt. Thanks!

The attached v2 takes a stab at fixing up the POD sections.

--
Daniel Gustafsson https://vmware.com/

Attachments:

v2-0001-ssl-test-refactoring.patchapplication/octet-stream; name=v2-0001-ssl-test-refactoring.patch; x-unix-mode=0644Download
From f44fa52fb64cc6c1e8fa652b697386198033b0b2 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <daniel@yesql.se>
Date: Thu, 3 Feb 2022 22:16:15 +0100
Subject: [PATCH v2] ssl test refactoring

---
 src/test/ssl/t/001_ssltests.pl        | 144 +++++------
 src/test/ssl/t/002_scram.pl           |  21 +-
 src/test/ssl/t/003_sslinfo.pl         |  33 ++-
 src/test/ssl/t/SSL/Backend/OpenSSL.pm | 228 +++++++++++++++++
 src/test/ssl/t/SSL/Server.pm          | 342 ++++++++++++++++++++++++++
 src/test/ssl/t/SSLServer.pm           | 219 -----------------
 6 files changed, 658 insertions(+), 329 deletions(-)
 create mode 100644 src/test/ssl/t/SSL/Backend/OpenSSL.pm
 create mode 100644 src/test/ssl/t/SSL/Server.pm
 delete mode 100644 src/test/ssl/t/SSLServer.pm

diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index b1fb15ce80..d5383a58ce 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -8,20 +8,24 @@ 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
+
+my $ssl_server = SSL::Server->new();
+sub sslkey
+{
+	return $ssl_server->sslkey(@_);
+}
+sub switch_server_cert
 {
-	plan tests => 110;
+	$ssl_server->switch_server_cert(@_);
 }
 
 #### Some configuration
@@ -36,39 +40,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 +54,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');
+$ssl_server->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 +111,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 +225,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 +254,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 +278,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 +296,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 +311,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 +377,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 +412,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 +422,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 +447,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 +501,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 +515,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 +534,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 +547,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 +563,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 +582,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..bd7a84cec2 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -14,13 +14,24 @@ 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';
 }
 
+my $ssl_server = SSL::Server->new();
+sub sslkey
+{
+	return $ssl_server->sslkey(@_);
+}
+sub switch_server_cert
+{
+	$ssl_server->switch_server_cert(@_);
+}
+
+
 # This is the hostname used to connect to the server.
 my $SERVERHOSTADDR = '127.0.0.1';
 # This is the pattern to use in pg_hba.conf to match incoming connections.
@@ -30,8 +41,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;
 
@@ -48,9 +57,9 @@ $ENV{PGPORT} = $node->port;
 $node->start;
 
 # Configure server for SSL connections, with password handling.
-configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
+$ssl_server->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 +127,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..bc555048da 100644
--- a/src/test/ssl/t/003_sslinfo.pl
+++ b/src/test/ssl/t/003_sslinfo.pl
@@ -12,18 +12,24 @@ 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
+
+#### Some configuration
+my $ssl_server = SSL::Server->new();
+sub sslkey
+{
+	return $ssl_server->sslkey(@_);
+}
+sub switch_server_cert
 {
-	plan tests => 13;
+	$ssl_server->switch_server_cert(@_);
 }
 
-#### Some configuration
 
 # This is the hostname used to connect to the server. This cannot be a
 # hostname, because the server certificate is always for the domain
@@ -35,17 +41,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";
@@ -58,17 +53,17 @@ $ENV{PGHOST} = $node->host;
 $ENV{PGPORT} = $node->port;
 $node->start;
 
-configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
+$ssl_server->configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
 	'trust', extensions => [ qw(sslinfo) ]);
 
 # 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 +130,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..f8dc4b7ad9
--- /dev/null
+++ b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
@@ -0,0 +1,228 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+=pod
+
+=head1 NAME
+
+SSL::Backend::OpenSSL
+
+=head1 SYNOPSIS
+
+  use SSL::Backend::OpenSSL;
+
+  my $backend = SSL::backend::OpenSSL->new();
+
+  $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 File::Basename;
+use File::Copy;
+
+=pod
+
+=head1 METHODS
+
+=over
+
+=item SSL::Backend::OpenSSL->new()
+
+Create a new instance of the OpenSSL backend.
+
+=cut
+
+sub new
+{
+	my ($class) = @_;
+
+	my $self = { _library => 'OpenSSL', key => {} };
+
+	bless $self, $class;
+
+	return $self;
+}
+
+=pod
+
+=item $backend->init(pgdata)
+
+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: $!";
+		$self->{key}->{$keyfile} =
+		  PostgreSQL::Test::Utils::perl2host("$cert_tempdir/$keyfile");
+		$self->{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: $!";
+	$self->{key}->{'client_wrongperms.key'} =
+	  PostgreSQL::Test::Utils::perl2host("$cert_tempdir/client_wrongperms.key");
+	$self->key->{'client_wrongperms.key'} =~ s!\\!/!g
+	  if $PostgreSQL::Test::Utils::windows_os;
+}
+
+=pod
+
+=item $backend->get_sslkey(key)
+
+Get an 'sslkey' connection string parameter for the specified B<key> which has
+the correct path for direct inclusion in a connection string.
+
+=cut
+
+sub get_sslkey
+{
+	my ($self, $keyfile) = @_;
+
+	return " sslkey=$self->{key}->{$keyfile}";
+}
+
+=pod
+
+=item $backend->set_server_cert(params)
+
+Change the configuration to use given server cert, key and crl file(s). The
+following paramters are supported:
+
+=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/SSL/Server.pm b/src/test/ssl/t/SSL/Server.pm
new file mode 100644
index 0000000000..479a872d6a
--- /dev/null
+++ b/src/test/ssl/t/SSL/Server.pm
@@ -0,0 +1,342 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+=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;
+
+  # Initialize SSL Server functionality for the cluster
+  my $ssl_server = SSL::Server->new();
+
+  # Configure SSL on the newly formed cluster
+  $server->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 Test::More;
+use SSL::Backend::OpenSSL;
+
+sub new
+{
+	my $class = shift;
+	my $flavor = shift || $ENV{with_ssl};
+	die "SSL flavor not defined" unless $flavor;
+	my $self = {};
+	bless $self, $class;
+	if ($flavor =~ /\Aopenssl\z/i)
+	{
+		$self->{flavor} = 'openssl';
+		$self->{backend} = SSL::Backend::OpenSSL->new();
+	}
+	else
+	{
+		die "SSL flavor $flavor unknown";
+	}
+	return $self;
+}
+
+=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 $self = shift;
+	my $keyfile = shift;
+	my $backend = $self->{backend};
+
+	return $backend->get_sslkey($keyfile);
+}
+
+=pod
+
+=item $server->configure_test_server_for_ssl(node, host, cidr, auth, params)
+
+Configure the cluster specified by B<node> or listening on SSL connections.
+The following databases will be created in the cluster: trustdb, certdb,
+certdb_dn, certdb_dn_re, certdb_cn, verifydb. The following users will be
+created in the cluster: ssltestuser, md5testuser, anotheruser, yetanotheruser.
+If B<< $params{password} >> is set, it will be used as password for all users
+with the password encoding B<< $params{password_enc} >> (except for md5testuser
+which always have MD5).  Extensions defined in B<< @{$params{extension}} >>
+will be created in all the above created databases. B<host> is used for 
+C<listen_addresses> and B<cidr> for configuring C<pg_hba.conf>.
+
+=cut
+
+sub configure_test_server_for_ssl
+{
+	my $self=shift;
+	my ($node, $serverhost, $servercidr, $authmethod, %params) = @_;
+	my $backend = $self->{backend};
+	my $pgdata = $node->data_dir;
+
+	my @databases = ( 'trustdb', 'certdb', 'certdb_dn', 'certdb_dn_re', 'certdb_cn', 'verifydb' );
+
+	# Create test users and databases
+	$node->psql('postgres', "CREATE USER ssltestuser");
+	$node->psql('postgres', "CREATE USER md5testuser");
+	$node->psql('postgres', "CREATE USER anotheruser");
+	$node->psql('postgres', "CREATE USER yetanotheruser");
+
+	foreach my $db (@databases)
+	{
+		$node->psql('postgres', "CREATE DATABASE $db");
+	}
+
+	# Update password of each user as needed.
+	if (defined($params{password}))
+	{
+		die "Password encryption must be specified when password is set"
+			unless defined($params{password_enc});
+
+		$node->psql('postgres',
+			"SET password_encryption='$params{password_enc}'; ALTER USER ssltestuser PASSWORD '$params{password}';"
+		);
+		# A special user that always has an md5-encrypted password
+		$node->psql('postgres',
+			"SET password_encryption='md5'; ALTER USER md5testuser PASSWORD '$params{password}';"
+		);
+		$node->psql('postgres',
+			"SET password_encryption='$params{password_enc}'; ALTER USER anotheruser PASSWORD '$params{password}';"
+		);
+	}
+
+	# Create any extensions requested in the setup
+	if (defined($params{extensions}))
+	{
+		foreach my $extension (@{$params{extensions}})
+		{
+			foreach my $db (@databases)
+			{
+				$node->psql($db, "CREATE EXTENSION $extension CASCADE;");
+			}
+		}
+	}
+
+	# enable logging etc.
+	open my $conf, '>>', "$pgdata/postgresql.conf";
+	print $conf "fsync=off\n";
+	print $conf "log_connections=on\n";
+	print $conf "log_hostname=on\n";
+	print $conf "listen_addresses='$serverhost'\n";
+	print $conf "log_statement=all\n";
+
+	# enable SSL and set up server key
+	print $conf "include 'sslconfig.conf'\n";
+
+	close $conf;
+
+	# SSL configuration will be placed here
+	open my $sslconf, '>', "$pgdata/sslconfig.conf";
+	close $sslconf;
+
+	# Perform backend specific configuration
+	$backend->init($pgdata);
+
+	# Stop and restart server to load new listen_addresses.
+	$node->restart;
+
+	# Change pg_hba after restart because hostssl requires ssl=on
+	_configure_hba_for_ssl($node, $servercidr, $authmethod);
+
+	return;
+}
+
+=pod
+
+=item $server->ssl_library()
+
+Get the name of the currently used SSL backend.
+
+=cut
+
+sub ssl_library
+{
+	my $self = shift;
+	my $backend = $self->{backend};
+
+	return $backend->get_library();
+}
+
+=pod
+
+=item switch_server_cert(params)
+
+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
+B<< $params{restart} >> => 'no' opts out of it leaving the server running.
+The following params are supported:
+
+=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 $self = shift;
+	my $node   = shift;
+	my $backend = $self->{backend};
+	my %params = @_;
+	my $pgdata = $node->data_dir;
+
+	open my $sslconf, '>', "$pgdata/sslconfig.conf";
+	print $sslconf "ssl=on\n";
+	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) = @_;
+	my $pgdata = $node->data_dir;
+
+	# Only accept SSL connections from $servercidr. Our tests don't depend on this
+	# but seems best to keep it as narrow as possible for security reasons.
+	#
+	# When connecting to certdb, also check the client certificate.
+	open my $hba, '>', "$pgdata/pg_hba.conf";
+	print $hba
+	  "# TYPE  DATABASE        USER            ADDRESS                 METHOD             OPTIONS\n";
+	print $hba
+	  "hostssl trustdb         md5testuser     $servercidr            md5\n";
+	print $hba
+	  "hostssl trustdb         all             $servercidr            $authmethod\n";
+	print $hba
+	  "hostssl verifydb        ssltestuser     $servercidr            $authmethod        clientcert=verify-full\n";
+	print $hba
+	  "hostssl verifydb        anotheruser     $servercidr            $authmethod        clientcert=verify-full\n";
+	print $hba
+	  "hostssl verifydb        yetanotheruser  $servercidr            $authmethod        clientcert=verify-ca\n";
+	print $hba
+	  "hostssl certdb          all             $servercidr            cert\n";
+	print $hba
+	  "hostssl certdb_dn       all             $servercidr            cert clientname=DN map=dn\n",
+	  "hostssl certdb_dn_re    all             $servercidr            cert clientname=DN map=dnre\n",
+	  "hostssl certdb_cn       all             $servercidr            cert clientname=CN map=cn\n";
+	close $hba;
+
+	# Also set the ident maps. Note: fields with commas must be quoted
+	open my $map, ">", "$pgdata/pg_ident.conf";
+	print $map
+	  "# MAPNAME       SYSTEM-USERNAME                           PG-USERNAME\n",
+	  "dn             \"CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG\"    ssltestuser\n",
+	  "dnre           \"/^.*OU=Testing,.*\$\"                    ssltestuser\n",
+	  "cn              ssltestuser-dn                            ssltestuser\n";
+
+	return;
+}
+
+=pod
+
+=back
+
+=cut
+
+1;
diff --git a/src/test/ssl/t/SSLServer.pm b/src/test/ssl/t/SSLServer.pm
deleted file mode 100644
index c85c6fd997..0000000000
--- a/src/test/ssl/t/SSLServer.pm
+++ /dev/null
@@ -1,219 +0,0 @@
-
-# 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;
-
-use strict;
-use warnings;
-use PostgreSQL::Test::Cluster;
-use PostgreSQL::Test::Utils;
-use File::Basename;
-use File::Copy;
-use Test::More;
-
-use Exporter 'import';
-our @EXPORT = qw(
-  configure_test_server_for_ssl
-  switch_server_cert
-);
-
-# Copy 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;
-}
-
-# 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
-{
-	my ($node, $serverhost, $servercidr, $authmethod, %params) = @_;
-	my $pgdata = $node->data_dir;
-
-	my @databases = ( 'trustdb', 'certdb', 'certdb_dn', 'certdb_dn_re', 'certdb_cn', 'verifydb' );
-
-	# Create test users and databases
-	$node->psql('postgres', "CREATE USER ssltestuser");
-	$node->psql('postgres', "CREATE USER md5testuser");
-	$node->psql('postgres', "CREATE USER anotheruser");
-	$node->psql('postgres', "CREATE USER yetanotheruser");
-
-	foreach my $db (@databases)
-	{
-		$node->psql('postgres', "CREATE DATABASE $db");
-	}
-
-	# Update password of each user as needed.
-	if (defined($params{password}))
-	{
-		die "Password encryption must be specified when password is set"
-			unless defined($params{password_enc});
-
-		$node->psql('postgres',
-			"SET password_encryption='$params{password_enc}'; ALTER USER ssltestuser PASSWORD '$params{password}';"
-		);
-		# A special user that always has an md5-encrypted password
-		$node->psql('postgres',
-			"SET password_encryption='md5'; ALTER USER md5testuser PASSWORD '$params{password}';"
-		);
-		$node->psql('postgres',
-			"SET password_encryption='$params{password_enc}'; ALTER USER anotheruser PASSWORD '$params{password}';"
-		);
-	}
-
-	# Create any extensions requested in the setup
-	if (defined($params{extensions}))
-	{
-		foreach my $extension (@{$params{extensions}})
-		{
-			foreach my $db (@databases)
-			{
-				$node->psql($db, "CREATE EXTENSION $extension CASCADE;");
-			}
-		}
-	}
-
-	# enable logging etc.
-	open my $conf, '>>', "$pgdata/postgresql.conf";
-	print $conf "fsync=off\n";
-	print $conf "log_connections=on\n";
-	print $conf "log_hostname=on\n";
-	print $conf "listen_addresses='$serverhost'\n";
-	print $conf "log_statement=all\n";
-
-	# enable SSL and set up server key
-	print $conf "include 'sslconfig.conf'\n";
-
-	close $conf;
-
-	# 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/");
-
-	# Stop and restart server to load new listen_addresses.
-	$node->restart;
-
-	# Change pg_hba after restart because hostssl requires ssl=on
-	configure_hba_for_ssl($node, $servercidr, $authmethod);
-
-	return;
-}
-
-# Change the configuration to use given server cert file, and reload
-# the server so that the configuration takes effect.
-sub switch_server_cert
-{
-	my $node     = $_[0];
-	my $certfile = $_[1];
-	my $cafile   = $_[2] || "root+client_ca";
-	my $crlfile  = "root+client.crl";
-	my $crldir;
-	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;
-	close $sslconf;
-
-	$node->restart;
-	return;
-}
-
-sub configure_hba_for_ssl
-{
-	my ($node, $servercidr, $authmethod) = @_;
-	my $pgdata = $node->data_dir;
-
-	# Only accept SSL connections from $servercidr. Our tests don't depend on this
-	# but seems best to keep it as narrow as possible for security reasons.
-	#
-	# When connecting to certdb, also check the client certificate.
-	open my $hba, '>', "$pgdata/pg_hba.conf";
-	print $hba
-	  "# TYPE  DATABASE        USER            ADDRESS                 METHOD             OPTIONS\n";
-	print $hba
-	  "hostssl trustdb         md5testuser     $servercidr            md5\n";
-	print $hba
-	  "hostssl trustdb         all             $servercidr            $authmethod\n";
-	print $hba
-	  "hostssl verifydb        ssltestuser     $servercidr            $authmethod        clientcert=verify-full\n";
-	print $hba
-	  "hostssl verifydb        anotheruser     $servercidr            $authmethod        clientcert=verify-full\n";
-	print $hba
-	  "hostssl verifydb        yetanotheruser  $servercidr            $authmethod        clientcert=verify-ca\n";
-	print $hba
-	  "hostssl certdb          all             $servercidr            cert\n";
-	print $hba
-	  "hostssl certdb_dn       all             $servercidr            cert clientname=DN map=dn\n",
-	  "hostssl certdb_dn_re    all             $servercidr            cert clientname=DN map=dnre\n",
-	  "hostssl certdb_cn       all             $servercidr            cert clientname=CN map=cn\n";
-	close $hba;
-
-	# Also set the ident maps. Note: fields with commas must be quoted
-	open my $map, ">", "$pgdata/pg_ident.conf";
-	print $map
-	  "# MAPNAME       SYSTEM-USERNAME                           PG-USERNAME\n",
-	  "dn             \"CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG\"    ssltestuser\n",
-	  "dnre           \"/^.*OU=Testing,.*\$\"                    ssltestuser\n",
-	  "cn              ssltestuser-dn                            ssltestuser\n";
-
-	return;
-}
-
-1;
-- 
2.24.3 (Apple Git-128)

#6Andrew Dunstan
andrew@dunslane.net
In reply to: Daniel Gustafsson (#5)
Re: Refactoring SSL tests

On 2/8/22 09:24, Daniel Gustafsson wrote:

The attached v2 takes a stab at fixing up the POD sections.

There a capitalization typo in SSL/Backend/OpenSSL.pm - looks like
that's my fault:

+  my $backend = SSL::backend::OpenSSL->new();

Also, I think we should document that SSL::Server::new() takes an
optional flavor parameter, in the absence of which it uses
$ENV{with_ssl}, and that 'openssl' is the only currently supported value.

cheers

andrew

--

Andrew Dunstan
EDB: https://www.enterprisedb.com

#7Daniel Gustafsson
daniel@yesql.se
In reply to: Andrew Dunstan (#6)
1 attachment(s)
Re: Refactoring SSL tests

On 8 Feb 2022, at 16:46, Andrew Dunstan <andrew@dunslane.net> wrote:

There a capitalization typo in SSL/Backend/OpenSSL.pm - looks like
that's my fault:

+ my $backend = SSL::backend::OpenSSL->new();

Fixed.

Also, I think we should document that SSL::Server::new() takes an
optional flavor parameter, in the absence of which it uses
$ENV{with_ssl}, and that 'openssl' is the only currently supported value.

Good point, done in the attached.

--
Daniel Gustafsson https://vmware.com/

Attachments:

v3-0001-ssl-test-refactoring.patchapplication/octet-stream; name=v3-0001-ssl-test-refactoring.patch; x-unix-mode=0644Download
From 5527d4801eabe5ea3a95727cabd3ad22bedbaa55 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <daniel@yesql.se>
Date: Thu, 3 Feb 2022 22:16:15 +0100
Subject: [PATCH v3] ssl test refactoring

---
 src/test/ssl/t/001_ssltests.pl        | 144 +++++------
 src/test/ssl/t/002_scram.pl           |  21 +-
 src/test/ssl/t/003_sslinfo.pl         |  33 ++-
 src/test/ssl/t/SSL/Backend/OpenSSL.pm | 228 +++++++++++++++++
 src/test/ssl/t/SSL/Server.pm          | 353 ++++++++++++++++++++++++++
 src/test/ssl/t/SSLServer.pm           | 219 ----------------
 6 files changed, 669 insertions(+), 329 deletions(-)
 create mode 100644 src/test/ssl/t/SSL/Backend/OpenSSL.pm
 create mode 100644 src/test/ssl/t/SSL/Server.pm
 delete mode 100644 src/test/ssl/t/SSLServer.pm

diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index b1fb15ce80..d5383a58ce 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -8,20 +8,24 @@ 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
+
+my $ssl_server = SSL::Server->new();
+sub sslkey
+{
+	return $ssl_server->sslkey(@_);
+}
+sub switch_server_cert
 {
-	plan tests => 110;
+	$ssl_server->switch_server_cert(@_);
 }
 
 #### Some configuration
@@ -36,39 +40,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 +54,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');
+$ssl_server->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 +111,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 +225,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 +254,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 +278,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 +296,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 +311,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 +377,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 +412,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 +422,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 +447,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 +501,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 +515,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 +534,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 +547,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 +563,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 +582,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..bd7a84cec2 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -14,13 +14,24 @@ 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';
 }
 
+my $ssl_server = SSL::Server->new();
+sub sslkey
+{
+	return $ssl_server->sslkey(@_);
+}
+sub switch_server_cert
+{
+	$ssl_server->switch_server_cert(@_);
+}
+
+
 # This is the hostname used to connect to the server.
 my $SERVERHOSTADDR = '127.0.0.1';
 # This is the pattern to use in pg_hba.conf to match incoming connections.
@@ -30,8 +41,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;
 
@@ -48,9 +57,9 @@ $ENV{PGPORT} = $node->port;
 $node->start;
 
 # Configure server for SSL connections, with password handling.
-configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
+$ssl_server->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 +127,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..bc555048da 100644
--- a/src/test/ssl/t/003_sslinfo.pl
+++ b/src/test/ssl/t/003_sslinfo.pl
@@ -12,18 +12,24 @@ 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
+
+#### Some configuration
+my $ssl_server = SSL::Server->new();
+sub sslkey
+{
+	return $ssl_server->sslkey(@_);
+}
+sub switch_server_cert
 {
-	plan tests => 13;
+	$ssl_server->switch_server_cert(@_);
 }
 
-#### Some configuration
 
 # This is the hostname used to connect to the server. This cannot be a
 # hostname, because the server certificate is always for the domain
@@ -35,17 +41,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";
@@ -58,17 +53,17 @@ $ENV{PGHOST} = $node->host;
 $ENV{PGPORT} = $node->port;
 $node->start;
 
-configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
+$ssl_server->configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
 	'trust', extensions => [ qw(sslinfo) ]);
 
 # 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 +130,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..4ca7fdba4a
--- /dev/null
+++ b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
@@ -0,0 +1,228 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+=pod
+
+=head1 NAME
+
+SSL::Backend::OpenSSL
+
+=head1 SYNOPSIS
+
+  use SSL::Backend::OpenSSL;
+
+  my $backend = SSL::Backend::OpenSSL->new();
+
+  $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 File::Basename;
+use File::Copy;
+
+=pod
+
+=head1 METHODS
+
+=over
+
+=item SSL::Backend::OpenSSL->new()
+
+Create a new instance of the OpenSSL backend.
+
+=cut
+
+sub new
+{
+	my ($class) = @_;
+
+	my $self = { _library => 'OpenSSL', key => {} };
+
+	bless $self, $class;
+
+	return $self;
+}
+
+=pod
+
+=item $backend->init(pgdata)
+
+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: $!";
+		$self->{key}->{$keyfile} =
+		  PostgreSQL::Test::Utils::perl2host("$cert_tempdir/$keyfile");
+		$self->{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: $!";
+	$self->{key}->{'client_wrongperms.key'} =
+	  PostgreSQL::Test::Utils::perl2host("$cert_tempdir/client_wrongperms.key");
+	$self->key->{'client_wrongperms.key'} =~ s!\\!/!g
+	  if $PostgreSQL::Test::Utils::windows_os;
+}
+
+=pod
+
+=item $backend->get_sslkey(key)
+
+Get an 'sslkey' connection string parameter for the specified B<key> which has
+the correct path for direct inclusion in a connection string.
+
+=cut
+
+sub get_sslkey
+{
+	my ($self, $keyfile) = @_;
+
+	return " sslkey=$self->{key}->{$keyfile}";
+}
+
+=pod
+
+=item $backend->set_server_cert(params)
+
+Change the configuration to use given server cert, key and crl file(s). The
+following paramters are supported:
+
+=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/SSL/Server.pm b/src/test/ssl/t/SSL/Server.pm
new file mode 100644
index 0000000000..af25ac3b29
--- /dev/null
+++ b/src/test/ssl/t/SSL/Server.pm
@@ -0,0 +1,353 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+=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;
+
+  # Initialize SSL Server functionality for the cluster
+  my $ssl_server = SSL::Server->new();
+
+  # Configure SSL on the newly formed cluster
+  $server->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 Test::More;
+use SSL::Backend::OpenSSL;
+
+=pod
+
+=head1 METHODS
+
+=over
+
+=item SSL::Server->new(flavor)
+
+Create a new SSL Server object for configuring a PostgreSQL test cluster
+node for accepting SSL connections using the with B<flavor> selected SSL
+backend. If B<flavor> isn't set, the C<with_ssl> environment variable will
+be used for selecting backend. Currently only C<openssl> is supported.
+
+=cut
+
+sub new
+{
+	my $class = shift;
+	my $flavor = shift || $ENV{with_ssl};
+	die "SSL flavor not defined" unless $flavor;
+	my $self = {};
+	bless $self, $class;
+	if ($flavor =~ /\Aopenssl\z/i)
+	{
+		$self->{flavor} = 'openssl';
+		$self->{backend} = SSL::Backend::OpenSSL->new();
+	}
+	else
+	{
+		die "SSL flavor $flavor unknown";
+	}
+	return $self;
+}
+
+=pod
+
+=item sslkey(filename)
+
+Return a C<sslkey> construct for the specified key for use in a connection
+string.
+
+=cut
+
+sub sslkey
+{
+	my $self = shift;
+	my $keyfile = shift;
+	my $backend = $self->{backend};
+
+	return $backend->get_sslkey($keyfile);
+}
+
+=pod
+
+=item $server->configure_test_server_for_ssl(node, host, cidr, auth, params)
+
+Configure the cluster specified by B<node> or listening on SSL connections.
+The following databases will be created in the cluster: trustdb, certdb,
+certdb_dn, certdb_dn_re, certdb_cn, verifydb. The following users will be
+created in the cluster: ssltestuser, md5testuser, anotheruser, yetanotheruser.
+If B<< $params{password} >> is set, it will be used as password for all users
+with the password encoding B<< $params{password_enc} >> (except for md5testuser
+which always have MD5).  Extensions defined in B<< @{$params{extension}} >>
+will be created in all the above created databases. B<host> is used for 
+C<listen_addresses> and B<cidr> for configuring C<pg_hba.conf>.
+
+=cut
+
+sub configure_test_server_for_ssl
+{
+	my $self=shift;
+	my ($node, $serverhost, $servercidr, $authmethod, %params) = @_;
+	my $backend = $self->{backend};
+	my $pgdata = $node->data_dir;
+
+	my @databases = ( 'trustdb', 'certdb', 'certdb_dn', 'certdb_dn_re', 'certdb_cn', 'verifydb' );
+
+	# Create test users and databases
+	$node->psql('postgres', "CREATE USER ssltestuser");
+	$node->psql('postgres', "CREATE USER md5testuser");
+	$node->psql('postgres', "CREATE USER anotheruser");
+	$node->psql('postgres', "CREATE USER yetanotheruser");
+
+	foreach my $db (@databases)
+	{
+		$node->psql('postgres', "CREATE DATABASE $db");
+	}
+
+	# Update password of each user as needed.
+	if (defined($params{password}))
+	{
+		die "Password encryption must be specified when password is set"
+			unless defined($params{password_enc});
+
+		$node->psql('postgres',
+			"SET password_encryption='$params{password_enc}'; ALTER USER ssltestuser PASSWORD '$params{password}';"
+		);
+		# A special user that always has an md5-encrypted password
+		$node->psql('postgres',
+			"SET password_encryption='md5'; ALTER USER md5testuser PASSWORD '$params{password}';"
+		);
+		$node->psql('postgres',
+			"SET password_encryption='$params{password_enc}'; ALTER USER anotheruser PASSWORD '$params{password}';"
+		);
+	}
+
+	# Create any extensions requested in the setup
+	if (defined($params{extensions}))
+	{
+		foreach my $extension (@{$params{extensions}})
+		{
+			foreach my $db (@databases)
+			{
+				$node->psql($db, "CREATE EXTENSION $extension CASCADE;");
+			}
+		}
+	}
+
+	# enable logging etc.
+	open my $conf, '>>', "$pgdata/postgresql.conf";
+	print $conf "fsync=off\n";
+	print $conf "log_connections=on\n";
+	print $conf "log_hostname=on\n";
+	print $conf "listen_addresses='$serverhost'\n";
+	print $conf "log_statement=all\n";
+
+	# enable SSL and set up server key
+	print $conf "include 'sslconfig.conf'\n";
+
+	close $conf;
+
+	# SSL configuration will be placed here
+	open my $sslconf, '>', "$pgdata/sslconfig.conf";
+	close $sslconf;
+
+	# Perform backend specific configuration
+	$backend->init($pgdata);
+
+	# Stop and restart server to load new listen_addresses.
+	$node->restart;
+
+	# Change pg_hba after restart because hostssl requires ssl=on
+	_configure_hba_for_ssl($node, $servercidr, $authmethod);
+
+	return;
+}
+
+=pod
+
+=item $server->ssl_library()
+
+Get the name of the currently used SSL backend.
+
+=cut
+
+sub ssl_library
+{
+	my $self = shift;
+	my $backend = $self->{backend};
+
+	return $backend->get_library();
+}
+
+=pod
+
+=item switch_server_cert(params)
+
+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
+B<< $params{restart} >> => 'no' opts out of it leaving the server running.
+The following params are supported:
+
+=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 $self = shift;
+	my $node   = shift;
+	my $backend = $self->{backend};
+	my %params = @_;
+	my $pgdata = $node->data_dir;
+
+	open my $sslconf, '>', "$pgdata/sslconfig.conf";
+	print $sslconf "ssl=on\n";
+	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) = @_;
+	my $pgdata = $node->data_dir;
+
+	# Only accept SSL connections from $servercidr. Our tests don't depend on this
+	# but seems best to keep it as narrow as possible for security reasons.
+	#
+	# When connecting to certdb, also check the client certificate.
+	open my $hba, '>', "$pgdata/pg_hba.conf";
+	print $hba
+	  "# TYPE  DATABASE        USER            ADDRESS                 METHOD             OPTIONS\n";
+	print $hba
+	  "hostssl trustdb         md5testuser     $servercidr            md5\n";
+	print $hba
+	  "hostssl trustdb         all             $servercidr            $authmethod\n";
+	print $hba
+	  "hostssl verifydb        ssltestuser     $servercidr            $authmethod        clientcert=verify-full\n";
+	print $hba
+	  "hostssl verifydb        anotheruser     $servercidr            $authmethod        clientcert=verify-full\n";
+	print $hba
+	  "hostssl verifydb        yetanotheruser  $servercidr            $authmethod        clientcert=verify-ca\n";
+	print $hba
+	  "hostssl certdb          all             $servercidr            cert\n";
+	print $hba
+	  "hostssl certdb_dn       all             $servercidr            cert clientname=DN map=dn\n",
+	  "hostssl certdb_dn_re    all             $servercidr            cert clientname=DN map=dnre\n",
+	  "hostssl certdb_cn       all             $servercidr            cert clientname=CN map=cn\n";
+	close $hba;
+
+	# Also set the ident maps. Note: fields with commas must be quoted
+	open my $map, ">", "$pgdata/pg_ident.conf";
+	print $map
+	  "# MAPNAME       SYSTEM-USERNAME                           PG-USERNAME\n",
+	  "dn             \"CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG\"    ssltestuser\n",
+	  "dnre           \"/^.*OU=Testing,.*\$\"                    ssltestuser\n",
+	  "cn              ssltestuser-dn                            ssltestuser\n";
+
+	return;
+}
+
+=pod
+
+=back
+
+=cut
+
+1;
diff --git a/src/test/ssl/t/SSLServer.pm b/src/test/ssl/t/SSLServer.pm
deleted file mode 100644
index c85c6fd997..0000000000
--- a/src/test/ssl/t/SSLServer.pm
+++ /dev/null
@@ -1,219 +0,0 @@
-
-# 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;
-
-use strict;
-use warnings;
-use PostgreSQL::Test::Cluster;
-use PostgreSQL::Test::Utils;
-use File::Basename;
-use File::Copy;
-use Test::More;
-
-use Exporter 'import';
-our @EXPORT = qw(
-  configure_test_server_for_ssl
-  switch_server_cert
-);
-
-# Copy 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;
-}
-
-# 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
-{
-	my ($node, $serverhost, $servercidr, $authmethod, %params) = @_;
-	my $pgdata = $node->data_dir;
-
-	my @databases = ( 'trustdb', 'certdb', 'certdb_dn', 'certdb_dn_re', 'certdb_cn', 'verifydb' );
-
-	# Create test users and databases
-	$node->psql('postgres', "CREATE USER ssltestuser");
-	$node->psql('postgres', "CREATE USER md5testuser");
-	$node->psql('postgres', "CREATE USER anotheruser");
-	$node->psql('postgres', "CREATE USER yetanotheruser");
-
-	foreach my $db (@databases)
-	{
-		$node->psql('postgres', "CREATE DATABASE $db");
-	}
-
-	# Update password of each user as needed.
-	if (defined($params{password}))
-	{
-		die "Password encryption must be specified when password is set"
-			unless defined($params{password_enc});
-
-		$node->psql('postgres',
-			"SET password_encryption='$params{password_enc}'; ALTER USER ssltestuser PASSWORD '$params{password}';"
-		);
-		# A special user that always has an md5-encrypted password
-		$node->psql('postgres',
-			"SET password_encryption='md5'; ALTER USER md5testuser PASSWORD '$params{password}';"
-		);
-		$node->psql('postgres',
-			"SET password_encryption='$params{password_enc}'; ALTER USER anotheruser PASSWORD '$params{password}';"
-		);
-	}
-
-	# Create any extensions requested in the setup
-	if (defined($params{extensions}))
-	{
-		foreach my $extension (@{$params{extensions}})
-		{
-			foreach my $db (@databases)
-			{
-				$node->psql($db, "CREATE EXTENSION $extension CASCADE;");
-			}
-		}
-	}
-
-	# enable logging etc.
-	open my $conf, '>>', "$pgdata/postgresql.conf";
-	print $conf "fsync=off\n";
-	print $conf "log_connections=on\n";
-	print $conf "log_hostname=on\n";
-	print $conf "listen_addresses='$serverhost'\n";
-	print $conf "log_statement=all\n";
-
-	# enable SSL and set up server key
-	print $conf "include 'sslconfig.conf'\n";
-
-	close $conf;
-
-	# 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/");
-
-	# Stop and restart server to load new listen_addresses.
-	$node->restart;
-
-	# Change pg_hba after restart because hostssl requires ssl=on
-	configure_hba_for_ssl($node, $servercidr, $authmethod);
-
-	return;
-}
-
-# Change the configuration to use given server cert file, and reload
-# the server so that the configuration takes effect.
-sub switch_server_cert
-{
-	my $node     = $_[0];
-	my $certfile = $_[1];
-	my $cafile   = $_[2] || "root+client_ca";
-	my $crlfile  = "root+client.crl";
-	my $crldir;
-	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;
-	close $sslconf;
-
-	$node->restart;
-	return;
-}
-
-sub configure_hba_for_ssl
-{
-	my ($node, $servercidr, $authmethod) = @_;
-	my $pgdata = $node->data_dir;
-
-	# Only accept SSL connections from $servercidr. Our tests don't depend on this
-	# but seems best to keep it as narrow as possible for security reasons.
-	#
-	# When connecting to certdb, also check the client certificate.
-	open my $hba, '>', "$pgdata/pg_hba.conf";
-	print $hba
-	  "# TYPE  DATABASE        USER            ADDRESS                 METHOD             OPTIONS\n";
-	print $hba
-	  "hostssl trustdb         md5testuser     $servercidr            md5\n";
-	print $hba
-	  "hostssl trustdb         all             $servercidr            $authmethod\n";
-	print $hba
-	  "hostssl verifydb        ssltestuser     $servercidr            $authmethod        clientcert=verify-full\n";
-	print $hba
-	  "hostssl verifydb        anotheruser     $servercidr            $authmethod        clientcert=verify-full\n";
-	print $hba
-	  "hostssl verifydb        yetanotheruser  $servercidr            $authmethod        clientcert=verify-ca\n";
-	print $hba
-	  "hostssl certdb          all             $servercidr            cert\n";
-	print $hba
-	  "hostssl certdb_dn       all             $servercidr            cert clientname=DN map=dn\n",
-	  "hostssl certdb_dn_re    all             $servercidr            cert clientname=DN map=dnre\n",
-	  "hostssl certdb_cn       all             $servercidr            cert clientname=CN map=cn\n";
-	close $hba;
-
-	# Also set the ident maps. Note: fields with commas must be quoted
-	open my $map, ">", "$pgdata/pg_ident.conf";
-	print $map
-	  "# MAPNAME       SYSTEM-USERNAME                           PG-USERNAME\n",
-	  "dn             \"CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG\"    ssltestuser\n",
-	  "dnre           \"/^.*OU=Testing,.*\$\"                    ssltestuser\n",
-	  "cn              ssltestuser-dn                            ssltestuser\n";
-
-	return;
-}
-
-1;
-- 
2.24.3 (Apple Git-128)

#8Andrew Dunstan
andrew@dunslane.net
In reply to: Daniel Gustafsson (#7)
Re: Refactoring SSL tests

On 2/9/22 08:11, Daniel Gustafsson wrote:

On 8 Feb 2022, at 16:46, Andrew Dunstan <andrew@dunslane.net> wrote:
There a capitalization typo in SSL/Backend/OpenSSL.pm - looks like
that's my fault:

+ my $backend = SSL::backend::OpenSSL->new();

Fixed.

Also, I think we should document that SSL::Server::new() takes an
optional flavor parameter, in the absence of which it uses
$ENV{with_ssl}, and that 'openssl' is the only currently supported value.

Good point, done in the attached.

LGTM

cheers

andrew

--
Andrew Dunstan
EDB: https://www.enterprisedb.com

#9Daniel Gustafsson
daniel@yesql.se
In reply to: Andrew Dunstan (#8)
Re: Refactoring SSL tests

On 9 Feb 2022, at 14:28, Andrew Dunstan <andrew@dunslane.net> wrote:
On 2/9/22 08:11, Daniel Gustafsson wrote:

Good point, done in the attached.

LGTM

Now that the recent changes to TAP and SSL tests have settled, I took another
pass at this. After rebasing and fixing and polishing and taking it for
multiple spins on the CI I've pushed this today to master.

--
Daniel Gustafsson https://vmware.com/