SSL TAP test backend library independence refactoring

The SSL TAP tests were tightly coupled to the OpenSSL implementation,
making it hard to add support for additional SSL/TLS backends.  This
refactoring makes the test avoid depending on specific implementations

The SSLServer Perl module 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 responsible for setting up keys,
certs and to resolve sslkey values to their implementation specific
value (file paths or vault nicknames etc). Further, switch_server_cert
now takes a set of named parameters rather than a fixed set which used
defaults. The modules also come with POD documentation.

There are a few testcases which still use OpenSSL specifics, but it's
not entirely clear how to abstract those until we have another library
implemented.

Original patch by me, with lots of rework by Andrew Dunstan to turn it
into better Perl.

Discussion: https://postgr.es/m/AA18A362-CA65-4F9A-AF61-76AE318FE97C@yesql.se
This commit is contained in:
Daniel Gustafsson 2022-03-26 22:00:39 +01:00
parent e07d4ddc55
commit 4a7e964fc6
6 changed files with 664 additions and 322 deletions

View File

@ -8,18 +8,25 @@ 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';
}
my $ssl_server = SSL::Server->new();
sub sslkey
{
return $ssl_server->sslkey(@_);
}
sub switch_server_cert
{
$ssl_server->switch_server_cert(@_);
}
#### Some configuration
# This is the hostname used to connect to the server. This cannot be a
@ -32,39 +39,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} = "$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'} = "$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";
@ -79,31 +53,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' ],
@ -136,7 +110,7 @@ command_ok(
note "running client tests";
switch_server_cert($node, 'server-cn-only');
switch_server_cert($node, certfile => 'server-cn-only');
# Set of default settings for SSL parameters in connection string. This
# makes the tests protected against any defaults the environment may have
@ -256,7 +230,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 =
"$default_ssl_connstr user=ssltestuser dbname=trustdb sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
@ -285,7 +259,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 =
"$default_ssl_connstr user=ssltestuser dbname=trustdb sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
@ -309,7 +283,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 =
"$default_ssl_connstr user=ssltestuser dbname=trustdb sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
@ -327,7 +301,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 =
"$default_ssl_connstr user=ssltestuser dbname=trustdb sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR";
@ -342,7 +316,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 =
"$default_ssl_connstr user=ssltestuser dbname=trustdb hostaddr=$SERVERHOSTADDR host=common-name.pg-ssltest.test";
@ -410,34 +384,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!,
);
@ -445,7 +419,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/
@ -455,14 +429,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 => [
@ -480,18 +454,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!
);
}
@ -534,12 +508,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
@ -548,16 +522,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"/,
@ -567,7 +541,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
@ -580,31 +554,31 @@ $common_connstr =
"$default_ssl_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=verifydb hostaddr=$SERVERHOSTADDR host=localhost";
$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"/,
# verify-full does not provide authentication
log_unlike => [qr/connection authenticated:/],);
# Check that connecting with auth-optionverify-ca in pg_hba :
# Check that connecting with auth-option verify-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 =
"$default_ssl_connstr user=ssltestuser dbname=certdb sslkey=$key{'client.key'} sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR host=localhost";
"$default_ssl_connstr user=ssltestuser dbname=certdb " . sslkey('client.key') . " sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR host=localhost";
$node->connect_ok(
"$common_connstr sslmode=require sslcert=ssl/client+client_ca.crt",
@ -615,12 +589,11 @@ $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/);

View File

@ -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.
@ -46,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 host=localhost";

View File

@ -12,7 +12,7 @@ use File::Copy;
use FindBin;
use lib $FindBin::RealBin;
use SSLServer;
use SSL::Server;
if ($ENV{with_ssl} ne 'openssl')
{
@ -20,6 +20,15 @@ if ($ENV{with_ssl} ne 'openssl')
}
#### Some configuration
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. This cannot be a
# hostname, because the server certificate is always for the domain
@ -31,17 +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.
my $cert_tempdir = PostgreSQL::Test::Utils::tempdir();
my $client_tmp_key = "$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";
@ -54,13 +52,13 @@ $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');
# Set of default settings for SSL parameters in connection string. This
# makes the tests protected against any defaults the environment may have
@ -69,7 +67,7 @@ my $default_ssl_connstr = "sslkey=invalid sslcert=invalid sslrootcert=invalid ss
$common_connstr =
"$default_ssl_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=certdb hostaddr=$SERVERHOSTADDR host=localhost " .
"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(

View File

@ -0,0 +1,226 @@
# 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} = "$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'} = "$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;

View File

@ -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;

View File

@ -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;