001_negotiate_encryption.pl
application/octet-stream
Filename: 001_negotiate_encryption.pl
Type: application/octet-stream
Part: 0
# Copyright (c) 2021-2024, PostgreSQL Global Development Group
# Test negotiation of SSL and GSSAPI encryption
use strict;
use warnings FATAL => 'all';
use PostgreSQL::Test::Utils;
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Kerberos;
use File::Basename;
use File::Copy;
use Test::More;
if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\blibpq_encryption\b/)
{
plan skip_all =>
'Potentially unsafe test libpq_encryption not enabled in PG_TEST_EXTRA';
}
my $host = 'enc-test-localhost.postgresql.example.com';
my $hostaddr = '127.0.0.1';
my $servercidr = '127.0.0.1/32';
note "setting up PostgreSQL instance";
my $node = PostgreSQL::Test::Cluster->new('node');
$node->init;
$node->append_conf(
'postgresql.conf', qq{
listen_addresses = '$hostaddr'
log_connections = on
lc_messages = 'C'
});
my $pgdata = $node->data_dir;
my $dbname = 'postgres';
my $username = 'enctest';
my $application = '001_negotiate_encryption.pl';
my $gssuser_password = 'secret1';
my $krb;
my $ssl_supported = $ENV{with_ssl} eq 'openssl';
my $gss_supported = $ENV{with_gssapi} eq 'yes';
if ($gss_supported != 0)
{
note "setting up Kerberos";
my $realm = 'EXAMPLE.COM';
$krb = PostgreSQL::Test::Kerberos->new($host, $hostaddr, $realm);
$node->append_conf('postgresql.conf', "krb_server_keyfile = '$krb->{keytab}'\n");
}
if ($ssl_supported != 0)
{
my $certdir = dirname(__FILE__) . "/../../ssl/ssl";
copy "$certdir/server-cn-only.crt", "$pgdata/server.crt"
|| die "copying server.crt: $!";
copy "$certdir/server-cn-only.key", "$pgdata/server.key"
|| die "copying server.key: $!";
chmod(0600, "$pgdata/server.key");
# Start with SSL disabled.
$node->append_conf('postgresql.conf', "ssl = off\n");
}
$node->start;
$node->safe_psql('postgres', 'CREATE USER localuser;');
$node->safe_psql('postgres', 'CREATE USER testuser;');
$node->safe_psql('postgres', 'CREATE USER ssluser;');
$node->safe_psql('postgres', 'CREATE USER nossluser;');
$node->safe_psql('postgres', 'CREATE USER gssuser;');
$node->safe_psql('postgres', 'CREATE USER nogssuser;');
my $unixdir = $node->safe_psql('postgres', 'SHOW unix_socket_directories;');
chomp($unixdir);
$node->safe_psql('postgres', q{
CREATE FUNCTION current_enc() RETURNS text LANGUAGE plpgsql AS $$
DECLARE
ssl_in_use bool;
gss_in_use bool;
BEGIN
ssl_in_use = (SELECT ssl FROM pg_stat_ssl WHERE pid = pg_backend_pid());
gss_in_use = (SELECT encrypted FROM pg_stat_gssapi WHERE pid = pg_backend_pid());
raise log 'ssl % gss %', ssl_in_use, gss_in_use;
IF ssl_in_use AND gss_in_use THEN
RETURN 'ssl+gss'; -- shouldn't happen
ELSIF ssl_in_use THEN
RETURN 'ssl';
ELSIF gss_in_use THEN
RETURN 'gss';
ELSE
RETURN 'plain';
END IF;
END;
$$;
});
# 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 qq{
# TYPE DATABASE USER ADDRESS METHOD OPTIONS
local postgres localuser trust
host postgres testuser $servercidr trust
hostnossl postgres nossluser $servercidr trust
hostnogssenc postgres nogssuser $servercidr trust
};
print $hba qq{
hostssl postgres ssluser $servercidr trust
} if ($ssl_supported != 0);
print $hba qq{
hostgssenc postgres gssuser $servercidr trust
} if ($gss_supported != 0);
close $hba;
$node->reload;
note "running tests";
sub connect_test
{
local $Test::Builder::Level = $Test::Builder::Level + 1;
my ($node, $connstr, $expected_enc, @expect_log_msgs)
= @_;
my %params = ();
if (@expect_log_msgs)
{
# Match every message literally.
my @regexes = map { qr/\Q$_\E/ } @expect_log_msgs;
$params{log_like} = \@regexes;
}
my $test_name = "'$connstr' -> $expected_enc";
my $connstr_full = "";
$connstr_full .= "dbname=postgres " unless $connstr =~ m/dbname=/;
$connstr_full .= "host=$host hostaddr=$hostaddr " unless $connstr =~ m/host=/;
$connstr_full .= $connstr;
if ($expected_enc eq "fail")
{
$node->connect_fails($connstr_full, $test_name, %params);
}
else
{
$params{sql} = "SELECT current_enc()";
$params{expected_stdout} = qr/^$expected_enc$/;;
$node->connect_ok($connstr_full, $test_name, %params);
}
}
sub resolve_connection_type
{
my ($config) = @_;
my $user = $config->{user};
my $gssmode = $config->{gssmode};
my $sslmode = $config->{sslmode};
my @conntypes = qw(plain);
# Add connection types supported by the server to the pool
push(@conntypes, "ssl") if $config->{server_ssl} == 1;
push(@conntypes, "gss") if $config->{server_gss} == 1;
# User configurations:
# gssuser/ssluser require the relevant connection type,
@conntypes = grep {/gss/} @conntypes if $user eq 'gssuser';
@conntypes = grep {/ssl/} @conntypes if $user eq 'ssluser';
# nogssuser/nossluser require anything but the relevant connection type.
@conntypes = grep {!/gss/} @conntypes if $user eq 'nogssuser';
@conntypes = grep {!/ssl/} @conntypes if $user eq 'nossluser';
print STDOUT "After user filter: @conntypes\n";
# remove disabled connection modes
@conntypes = grep {!/gss/} @conntypes if $gssmode eq 'disable';
@conntypes = grep {!/ssl/} @conntypes if $sslmode eq 'disable';
# Drop non-required connection types.
# Note that if GSS is required, we ignore that SSL might be required.
if ($gssmode eq 'require')
{
@conntypes = grep {/gss/} @conntypes;
}
# This is not current behavior, but arguably the correct behavior.
# See also just below.
elsif ($sslmode eq 'require')
{
@conntypes = grep {/ssl/} @conntypes;
}
# In current versions of PostgreSQL, we ignore the "require" flag for SSL
# if we can connect with an available GSSAPI.
# However, I argue that is a bug, so I keep the code below commented out.
#
# if ($sslmode eq 'require' && $gssmode ne 'disable')
# {
# @conntypes = grep {/ssl/} @conntypes;
# }
print STDOUT "After mode require filter: @conntypes\n";
# Handle priorities of the various types.
# Note that this doesn't need to care about require/disable/etc, those
# filters were applied before we get here.
# Also note that preference is 1 > 2 > 3 > 4 > 5, so first preference
# without ssl or gss 'prefer/require' is plain connections.
my %order = (plain=>3, gss=>4, ssl=>5);
$order{ssl} = 2 if $sslmode eq "prefer";
$order{gss} = 1 if $gssmode eq "prefer";
@conntypes = sort { $order{$a} cmp $order{$b} } @conntypes;
# If there are no connection types available after filtering requirements,
# the connection fails.
return "fail" if @conntypes == 0;
# Else, we get to connect using the connection type with the highest
# priority.
return $conntypes[0];
}
# First test with SSL disabled in the server
# Test the cube of parameters: user, sslmode, sslnegotiation and gssencmode
sub test_modes
{
local $Test::Builder::Level = $Test::Builder::Level + 1;
my ($pg_node, $node_conf,
$test_users, $ssl_modes, $ssl_negotiations, $gss_modes) = @_;
foreach my $test_user (@{$test_users})
{
foreach my $client_mode (@{$ssl_modes})
{
foreach my $gssencmode (@{$gss_modes})
{
my %params = (
server_ssl=>$node_conf->{server_ssl},
server_gss=>$node_conf->{server_gss},
user=>$test_user,
sslmode=>$client_mode,
gssmode=>$gssencmode,
);
my $res = resolve_connection_type(\%params);
# Negotiation type doesn't matter for supported connection types
foreach my $negotiation (@{$ssl_negotiations})
{
connect_test($pg_node, "user=$test_user sslmode=$client_mode sslnegotiation=$negotiation gssencmode=$gssencmode", $res);
}
}
}
}
}
my $sslmodes = ['disable', 'allow', 'prefer', 'require'];
my $sslnegotiations = ['postgres', 'direct', 'requiredirect'];
my $gssencmodes = ['disable', 'prefer', 'require'];
my $server_config = {
server_ssl => 0,
server_gss => 0,
};
test_modes($node, $server_config,
['testuser'],
$sslmodes, $sslnegotiations, $gssencmodes);
# Enable SSL in the server
SKIP:
{
skip "SSL not supported by this build" if $ssl_supported == 0;
$node->adjust_conf('postgresql.conf', 'ssl', 'on');
$node->restart;
$server_config->{server_ssl} = 1;
test_modes($node, $server_config,
['testuser', 'ssluser', 'nossluser'],
$sslmodes, $sslnegotiations, ['disable']);
$node->adjust_conf('postgresql.conf', 'ssl', 'off');
$node->reload;
$server_config->{server_ssl} = 0;
}
# Test GSSAPI
SKIP:
{
skip "GSSAPI/Kerberos not supported by this build" if $gss_supported == 0;
# No ticket
connect_test($node, 'user=testuser sslmode=disable gssencmode=require', 'fail');
$krb->create_principle('gssuser', $gssuser_password);
$krb->create_ticket('gssuser', $gssuser_password);
$server_config->{server_gss} = 1;
test_modes($node, $server_config,
['testuser', 'gssuser', 'nogssuser'],
$sslmodes, $sslnegotiations, $gssencmodes);
# Check that logs match the expected 'no pg_hba.conf entry' line, too, as
# that is not tested by test_modes.
connect_test($node, 'user=nogssuser sslmode=prefer gssencmode=require', 'fail',
'no pg_hba.conf entry for host "127.0.0.1", user "nogssuser", database "postgres", GSS encryption');
# With 'gssencmode=prefer', libpq will first negotiate GSSAPI
# encryption, but the connection will fail because pg_hba.conf
# forbids GSSAPI encryption for this user. It will then reconnect
# with SSL, but the server doesn't support it, so it will continue
# with no encryption.
connect_test($node, 'user=nogssuser sslmode=prefer gssencmode=prefer', 'plain',
'no pg_hba.conf entry for host "127.0.0.1", user "nogssuser", database "postgres", GSS encryption');
connect_test($node, 'user=nogssuser sslmode=prefer gssencmode=prefer sslnegotiation=direct', 'plain',
'no pg_hba.conf entry for host "127.0.0.1", user "nogssuser", database "postgres", GSS encryption');
connect_test($node, 'user=nogssuser sslmode=prefer gssencmode=prefer sslnegotiation=requiredirect', 'plain',
'no pg_hba.conf entry for host "127.0.0.1", user "nogssuser", database "postgres", GSS encryption');
}
# Server supports both SSL and GSSAPI
SKIP:
{
skip "GSSAPI/Kerberos or SSL not supported by this build" unless ($ssl_supported && $gss_supported);
# SSL is still disabled
connect_test($node, 'user=testuser sslmode=prefer gssencmode=prefer', 'gss');
connect_test($node, 'user=testuser sslmode=prefer gssencmode=prefer sslnegotiation=direct', 'gss');
connect_test($node, 'user=testuser sslmode=prefer gssencmode=prefer sslnegotiation=requiredirect', 'gss');
# Enable SSL
$node->adjust_conf('postgresql.conf', 'ssl', 'on');
$node->reload;
$server_config->{server_ssl} = 1;
test_modes($node, $server_config,
['testuser', 'gssuser', 'ssluser', 'nogssuser', 'nossluser'],
$sslmodes, $sslnegotiations, $gssencmodes);
# Test case that server supports GSSAPI, but it's not allowed for
# this user. Special cased because we check output
connect_test($node, 'user=nogssuser sslmode=prefer gssencmode=require', 'fail',
'no pg_hba.conf entry for host "127.0.0.1", user "nogssuser", database "postgres", GSS encryption');
# with 'gssencmode=prefer', libpq will first negotiate GSSAPI
# encryption, but the connection will fail because pg_hba.conf
# forbids GSSAPI encryption for this user. It will then reconnect
# with SSL.
connect_test($node, 'user=nogssuser sslmode=prefer gssencmode=prefer', 'ssl',
'no pg_hba.conf entry for host "127.0.0.1", user "nogssuser", database "postgres", GSS encryption');
# Setting both sslmode=require and gssencmode=require fails if GSSAPI is not
# available.
connect_test($node, 'user=nogssuser sslmode=require gssencmode=require', 'fail');
}
# Test negotiation over unix domain sockets.
SKIP:
{
skip "Unix domain sockets not supported" unless ($unixdir ne "");
connect_test($node, "user=localuser sslmode=require gssencmode=prefer host=$unixdir", 'plain');
connect_test($node, "user=localuser sslmode=prefer gssencmode=require host=$unixdir", 'fail');
}
done_testing();