postgres/src/test/ssl/t/SSL/Server.pm
Joe Conway 0ebd242555 Run pgperltidy
This is required before the creation of a new branch.  pgindent is
clean, as well as is reformat-dat-files.

perltidy version is v20230309, as documented in pgindent's README.
2025-06-29 21:14:21 -04:00

383 lines
10 KiB
Perl

# Copyright (c) 2021-2025, 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 FATAL => 'all';
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;
use SSL::Backend::OpenSSL;
# Force SSL tests nodes to begin in TCP mode. They won't work in Unix Socket
# mode and this way they will find a port to run on in a more robust way.
# Use an INIT block so it runs after the BEGIN block in Utils.pm.
INIT { $PostgreSQL::Test::Utils::use_unix_sockets = 0; }
=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.
$node->append_conf(
'postgresql.conf', <<EOF
fsync=off
log_connections=all
log_hostname=on
listen_addresses='$serverhost'
log_statement=all
EOF
);
# enable SSL and set up server key
$node->append_conf('postgresql.conf', "include 'sslconfig.conf'");
# SSL configuration will be placed here
open my $sslconf, '>', "$pgdata/sslconfig.conf" or die $!;
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 $server->is_libressl()
Detect whether the currently used SSL backend is LibreSSL.
(Ideally we'd not need this hack, but presently we do.)
=cut
sub is_libressl
{
my $self = shift;
my $backend = $self->{backend};
return $backend->library_is_libressl();
}
=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 file 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;
ok(unlink($node->data_dir . '/sslconfig.conf'));
$node->append_conf('sslconfig.conf', "ssl=on");
$node->append_conf('sslconfig.conf', $backend->set_server_cert(\%params));
# use lists of ECDH curves and cipher suites for syntax testing
$node->append_conf('sslconfig.conf',
'ssl_groups=X25519:prime256v1:secp521r1');
$node->append_conf('sslconfig.conf',
'ssl_tls13_ciphers=TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256');
$node->append_conf('sslconfig.conf',
"ssl_passphrase_command='" . $params{passphrase_cmd} . "'")
if defined $params{passphrase_cmd};
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.
ok(unlink($node->data_dir . '/pg_hba.conf'));
$node->append_conf(
'pg_hba.conf', <<EOF
# TYPE DATABASE USER ADDRESS METHOD OPTIONS
hostssl trustdb md5testuser $servercidr md5
hostssl trustdb all $servercidr $authmethod
hostssl verifydb ssltestuser $servercidr $authmethod clientcert=verify-full
hostssl verifydb anotheruser $servercidr $authmethod clientcert=verify-full
hostssl verifydb yetanotheruser $servercidr $authmethod clientcert=verify-ca
hostssl certdb all $servercidr cert
hostssl certdb_dn all $servercidr cert clientname=DN map=dn
hostssl certdb_dn_re all $servercidr cert clientname=DN map=dnre
hostssl certdb_cn all $servercidr cert clientname=CN map=cn
EOF
);
# Also set the ident maps. Note: fields with commas must be quoted
ok(unlink($node->data_dir . '/pg_ident.conf'));
$node->append_conf(
'pg_ident.conf', <<EOF
# MAPNAME SYSTEM-USERNAME PG-USERNAME
dn "CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG" ssltestuser
dnre "/^.*OU=Testing,.*\$" ssltestuser
cn ssltestuser-dn ssltestuser
EOF
);
return;
}
=pod
=back
=cut
1;