aboutsummaryrefslogtreecommitdiff
path: root/src/interfaces/libpq/t/005_negotiate_encryption.pl
diff options
context:
space:
mode:
Diffstat (limited to 'src/interfaces/libpq/t/005_negotiate_encryption.pl')
-rw-r--r--src/interfaces/libpq/t/005_negotiate_encryption.pl724
1 files changed, 724 insertions, 0 deletions
diff --git a/src/interfaces/libpq/t/005_negotiate_encryption.pl b/src/interfaces/libpq/t/005_negotiate_encryption.pl
new file mode 100644
index 00000000000..b369289ef1d
--- /dev/null
+++ b/src/interfaces/libpq/t/005_negotiate_encryption.pl
@@ -0,0 +1,724 @@
+
+# Copyright (c) 2021-2024, PostgreSQL Global Development Group
+
+# OVERVIEW
+# --------
+#
+# Test negotiation of SSL and GSSAPI encryption
+#
+# We test all combinations of:
+#
+# - all the libpq client options that affect the protocol negotiations
+# (gssencmode, sslmode, sslnegotiation)
+# - server accepting or rejecting the authentication due to
+# pg_hba.conf entries
+# - SSL and GSS enabled/disabled in the server
+#
+# That's a lot of combinations, so we use a table-driven approach.
+# Each combination is represented by a line in a table. The line lists
+# the options specifying the test case, and an expected outcome. The
+# expected outcome includes whether the connection succeeds or fails,
+# and whether it uses SSL, GSS or no encryption. It also includes a
+# condensed trace of what steps were taken during the negotiation.
+# That can catch cases like useless retries, or if the encryption
+# methods are attempted in wrong order, even when it doesn't affect
+# the final outcome.
+#
+# TEST TABLE FORMAT
+# -----------------
+#
+# Example of the test table format:
+#
+# # USER GSSENCMODE SSLMODE EVENTS -> OUTCOME
+# testuser disable allow connect, authok -> plain
+# . . prefer connect, sslaccept, authok -> ssl
+# testuser require * connect, gssreject -> fail
+#
+# USER, GSSENCMODE and SSLMODE fields are the libpq 'user',
+# 'gssencmode' and 'sslmode' options used in the test. As a shorthand,
+# a single dot ('.') can be used in the USER, GSSENCMODE, and SSLMODE
+# fields, to indicate "same as on previous line". A '*' can be used
+# as a wildcard; it is expanded to mean all possible values of that
+# field.
+#
+# The EVENTS field is a condensed trace of expected steps during the
+# negotiation:
+#
+# connect: a TCP connection was established
+# reconnect: TCP connection was disconnected, and a new one was established
+# sslaccept: client requested SSL encryption and server accepted it
+# sslreject: client requested SSL encryption but server rejected it
+# gssaccept: client requested GSSAPI encryption and server accepted it
+# gssreject: client requested GSSAPI encryption but server rejected it
+# authok: client sent startup packet and authentication was performed successfully
+# authfail: client sent startup packet but server rejected the authentication
+#
+# The event trace can be used to verify that the client negotiated the
+# connection properly in more detail than just by looking at the
+# outcome. For example, if the client opens spurious extra TCP
+# connections, that would show up in the EVENTS.
+#
+# The OUTCOME field indicates the expected result of the test:
+#
+# plain: an unencrypted connection was established
+# ssl: SSL connection was established
+# gss: GSSAPI encrypted connection was established
+# fail: the connection attempt failed
+#
+# Empty lines are ignored. '#' can be used to mark the rest of the
+# line as a comment.
+
+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 $ssl_supported = $ENV{with_ssl} eq 'openssl';
+my $gss_supported = $ENV{with_gssapi} eq 'yes';
+
+###
+### Prepare test server for GSSAPI and SSL authentication, with a few
+### different test users and helper functions. We don't actually
+### enable SSL and kerberos in the server yet, we will do that later.
+###
+
+my $host = 'enc-test-localhost.postgresql.example.com';
+my $hostaddr = '127.0.0.1';
+my $servercidr = '127.0.0.1/32';
+
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init;
+$node->append_conf(
+ 'postgresql.conf', qq{
+listen_addresses = '$hostaddr'
+
+# Capturing the EVENTS that occur during tests requires these settings
+log_connections = on
+log_disconnections = on
+trace_connection_negotiation = 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;
+
+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__) . "/../../../test/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")
+ or die "failed to change permissions on server keys: $!";
+
+ # 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);
+
+# Helper function that returns the encryption method in use in the
+# connection.
+$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.
+open my $hba, '>', "$pgdata/pg_hba.conf" or die $!;
+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;
+
+# Ok, all prepared. Run the tests.
+
+my @all_test_users = ('testuser', 'ssluser', 'nossluser', 'gssuser', 'nogssuser');
+my @all_gssencmodes = ('disable', 'prefer', 'require');
+my @all_sslmodes = ('disable', 'allow', 'prefer', 'require');
+my @all_sslnegotiations = ('postgres', 'direct', 'requiredirect');
+
+my $server_config = {
+ server_ssl => 0,
+ server_gss => 0,
+};
+
+###
+### Run tests with GSS and SSL disabled in the server
+###
+my $test_table;
+if ($ssl_supported) {
+ $test_table = q{
+# USER GSSENCMODE SSLMODE SSLNEGOTIATION EVENTS -> OUTCOME
+testuser disable disable * connect, authok -> plain
+. . allow * connect, authok -> plain
+. . prefer postgres connect, sslreject, authok -> plain
+. . . direct connect, directsslreject, reconnect, sslreject, authok -> plain
+. . . requiredirect connect, directsslreject, reconnect, authok -> plain
+. . require postgres connect, sslreject -> fail
+. . . direct connect, directsslreject, reconnect, sslreject -> fail
+. . . requiredirect connect, directsslreject -> fail
+. prefer disable * connect, authok -> plain
+. . allow * connect, authok -> plain
+. . prefer postgres connect, sslreject, authok -> plain
+. . . direct connect, directsslreject, reconnect, sslreject, authok -> plain
+. . . requiredirect connect, directsslreject, reconnect, authok -> plain
+. . require postgres connect, sslreject -> fail
+. . . direct connect, directsslreject, reconnect, sslreject -> fail
+. . . requiredirect connect, directsslreject -> fail
+ };
+} else {
+ # Compiled without SSL support
+ $test_table = q{
+# USER GSSENCMODE SSLMODE SSLNEGOTIATION EVENTS -> OUTCOME
+testuser disable disable postgres connect, authok -> plain
+. . allow postgres connect, authok -> plain
+. . prefer postgres connect, authok -> plain
+. prefer disable postgres connect, authok -> plain
+. . allow postgres connect, authok -> plain
+. . prefer postgres connect, authok -> plain
+
+# Without SSL support, sslmode=require and sslnegotiation=direct/requiredirect
+# are not accepted at all.
+. * require * - -> fail
+. * * direct - -> fail
+. * * requiredirect - -> fail
+ };
+}
+
+# All attempts with gssencmode=require fail without connecting because
+# no credential cache has been configured in the client. (Or if GSS
+# support is not compiled in, they will fail because of that.)
+$test_table .= q{
+testuser require * * - -> fail
+};
+
+note("Running tests with SSL and GSS disabled in the server");
+test_matrix($node, $server_config,
+ ['testuser'], \@all_gssencmodes, \@all_sslmodes, \@all_sslnegotiations,
+ parse_table($test_table));
+
+
+###
+### Run tests with GSS disabled and SSL enabled in the server
+###
+SKIP:
+{
+ skip "SSL not supported by this build" if $ssl_supported == 0;
+
+ $test_table = q{
+# USER GSSENCMODE SSLMODE SSLNEGOTIATION EVENTS -> OUTCOME
+testuser disable disable * connect, authok -> plain
+. . allow * connect, authok -> plain
+. . prefer postgres connect, sslaccept, authok -> ssl
+. . . direct connect, directsslaccept, authok -> ssl
+. . . requiredirect connect, directsslaccept, authok -> ssl
+. . require postgres connect, sslaccept, authok -> ssl
+. . . direct connect, directsslaccept, authok -> ssl
+. . . requiredirect connect, directsslaccept, authok -> ssl
+ssluser . disable * connect, authfail -> fail
+. . allow postgres connect, authfail, reconnect, sslaccept, authok -> ssl
+. . . direct connect, authfail, reconnect, directsslaccept, authok -> ssl
+. . . requiredirect connect, authfail, reconnect, directsslaccept, authok -> ssl
+. . prefer postgres connect, sslaccept, authok -> ssl
+. . . direct connect, directsslaccept, authok -> ssl
+. . . requiredirect connect, directsslaccept, authok -> ssl
+. . require postgres connect, sslaccept, authok -> ssl
+. . . direct connect, directsslaccept, authok -> ssl
+. . . requiredirect connect, directsslaccept, authok -> ssl
+nossluser . disable * connect, authok -> plain
+. . allow postgres connect, authok -> plain
+. . . direct connect, authok -> plain
+. . . requiredirect connect, authok -> plain
+. . prefer postgres connect, sslaccept, authfail, reconnect, authok -> plain
+. . . direct connect, directsslaccept, authfail, reconnect, authok -> plain
+. . . requiredirect connect, directsslaccept, authfail, reconnect, authok -> plain
+. . require postgres connect, sslaccept, authfail -> fail
+. . require direct connect, directsslaccept, authfail -> fail
+. . require requiredirect connect, directsslaccept, authfail -> fail
+};
+
+ # Enable SSL in the server
+ $node->adjust_conf('postgresql.conf', 'ssl', 'on');
+ $node->reload;
+ $server_config->{server_ssl} = 1;
+
+ note("Running tests with SSL enabled in server");
+ test_matrix($node, $server_config,
+ ['testuser', 'ssluser', 'nossluser'],
+ ['disable'], \@all_sslmodes, \@all_sslnegotiations,
+ parse_table($test_table));
+
+ # Disable SSL again
+ $node->adjust_conf('postgresql.conf', 'ssl', 'off');
+ $node->reload;
+ $server_config->{server_ssl} = 0;
+}
+
+###
+### Run tests with GSS enabled, SSL disabled in the server
+###
+SKIP:
+{
+ skip "GSSAPI/Kerberos not supported by this build" if $gss_supported == 0;
+
+ $krb->create_principal('gssuser', $gssuser_password);
+ $krb->create_ticket('gssuser', $gssuser_password);
+ $server_config->{server_gss} = 1;
+
+ $test_table = q{
+# USER GSSENCMODE SSLMODE SSLNEGOTIATION EVENTS -> OUTCOME
+testuser disable disable * connect, authok -> plain
+. . allow * connect, authok -> plain
+. . prefer postgres connect, sslreject, authok -> plain
+. . . direct connect, directsslreject, reconnect, sslreject, authok -> plain
+. . . requiredirect connect, directsslreject, reconnect, authok -> plain
+. . require postgres connect, sslreject -> fail
+. . . direct connect, directsslreject, reconnect, sslreject -> fail
+. . . requiredirect connect, directsslreject -> fail
+. prefer * * connect, gssaccept, authok -> gss
+. require * * connect, gssaccept, authok -> gss
+
+gssuser disable disable * connect, authfail -> fail
+. . allow postgres connect, authfail, reconnect, sslreject -> fail
+. . . direct connect, authfail, reconnect, directsslreject, reconnect, sslreject -> fail
+. . . requiredirect connect, authfail, reconnect, directsslreject -> fail
+. . prefer postgres connect, sslreject, authfail -> fail
+. . . direct connect, directsslreject, reconnect, sslreject, authfail -> fail
+. . . requiredirect connect, directsslreject, reconnect, authfail -> fail
+. . require postgres connect, sslreject -> fail
+. . . direct connect, directsslreject, reconnect, sslreject -> fail
+. . . requiredirect connect, directsslreject -> fail
+. prefer * * connect, gssaccept, authok -> gss
+. require * * connect, gssaccept, authok -> gss
+
+nogssuser disable disable * connect, authok -> plain
+. . allow postgres connect, authok -> plain
+. . . direct connect, authok -> plain
+. . . requiredirect connect, authok -> plain
+. . prefer postgres connect, sslreject, authok -> plain
+. . . direct connect, directsslreject, reconnect, sslreject, authok -> plain
+. . . requiredirect connect, directsslreject, reconnect, authok -> plain
+. . require postgres connect, sslreject -> fail
+. . . direct connect, directsslreject, reconnect, sslreject -> fail
+. . . requiredirect connect, directsslreject -> fail
+. prefer disable * connect, gssaccept, authfail, reconnect, authok -> plain
+. . allow postgres connect, gssaccept, authfail, reconnect, authok -> plain
+. . . direct connect, gssaccept, authfail, reconnect, authok -> plain
+. . . requiredirect connect, gssaccept, authfail, reconnect, authok -> plain
+. . prefer postgres connect, gssaccept, authfail, reconnect, sslreject, authok -> plain
+. . . direct connect, gssaccept, authfail, reconnect, directsslreject, reconnect, sslreject, authok -> plain
+. . . requiredirect connect, gssaccept, authfail, reconnect, directsslreject, reconnect, authok -> plain
+. . require postgres connect, gssaccept, authfail, reconnect, sslreject -> fail
+. . . direct connect, gssaccept, authfail, reconnect, directsslreject, reconnect, sslreject -> fail
+. . . requiredirect connect, gssaccept, authfail, reconnect, directsslreject -> fail
+. require disable * connect, gssaccept, authfail -> fail
+. . allow * connect, gssaccept, authfail -> fail
+. . prefer * connect, gssaccept, authfail -> fail
+. . require * connect, gssaccept, authfail -> fail # If both GSSAPI and sslmode are required, and GSS is not available -> fail
+ };
+
+ # The expected events and outcomes above assume that SSL support
+ # is enabled. When libpq is compiled without SSL support, all
+ # attempts to connect with sslmode=require or
+ # sslnegotition=direct/requiredirectwould fail immediately without
+ # even connecting to the server. Skip those, because we tested
+ # them earlier already.
+ my ($sslmodes, $sslnegotiations);
+ if ($ssl_supported != 0) {
+ ($sslmodes, $sslnegotiations) = (\@all_sslmodes, \@all_sslnegotiations);
+ } else {
+ ($sslmodes, $sslnegotiations) = (['disable'], ['postgres']);
+ }
+
+ note("Running tests with GSS enabled in server");
+ test_matrix($node, $server_config,
+ ['testuser', 'gssuser', 'nogssuser'],
+ \@all_gssencmodes, $sslmodes, $sslnegotiations,
+ parse_table($test_table));
+}
+
+###
+### Tests with both GSS and SSL enabled in the server
+###
+SKIP:
+{
+ skip "GSSAPI/Kerberos or SSL not supported by this build" unless ($ssl_supported && $gss_supported);
+
+ # Sanity check that GSSAPI is still enabled from previous test.
+ connect_test($node, 'user=testuser gssencmode=prefer sslmode=prefer', 'connect, gssaccept, authok -> gss');
+
+ # Enable SSL
+ $node->adjust_conf('postgresql.conf', 'ssl', 'on');
+ $node->reload;
+ $server_config->{server_ssl} = 1;
+
+ $test_table = q{
+# USER GSSENCMODE SSLMODE SSLNEGOTIATION EVENTS -> OUTCOME
+testuser disable disable * connect, authok -> plain
+. . allow * connect, authok -> plain
+. . prefer postgres connect, sslaccept, authok -> ssl
+. . . direct connect, directsslaccept, authok -> ssl
+. . . requiredirect connect, directsslaccept, authok -> ssl
+. . require postgres connect, sslaccept, authok -> ssl
+. . . direct connect, directsslaccept, authok -> ssl
+. . . requiredirect connect, directsslaccept, authok -> ssl
+. prefer disable * connect, gssaccept, authok -> gss
+. . allow * connect, gssaccept, authok -> gss
+. . prefer * connect, gssaccept, authok -> gss
+. . require * connect, gssaccept, authok -> gss # If both GSS and SSL is possible, GSS is chosen over SSL, even if sslmode=require
+. require disable * connect, gssaccept, authok -> gss
+. . allow * connect, gssaccept, authok -> gss
+. . prefer * connect, gssaccept, authok -> gss
+. . require * connect, gssaccept, authok -> gss # If both GSS and SSL is possible, GSS is chosen over SSL, even if sslmode=require
+
+gssuser disable disable * connect, authfail -> fail
+. . allow postgres connect, authfail, reconnect, sslaccept, authfail -> fail
+. . . direct connect, authfail, reconnect, directsslaccept, authfail -> fail
+. . . requiredirect connect, authfail, reconnect, directsslaccept, authfail -> fail
+. . prefer postgres connect, sslaccept, authfail, reconnect, authfail -> fail
+. . . direct connect, directsslaccept, authfail, reconnect, authfail -> fail
+. . . requiredirect connect, directsslaccept, authfail, reconnect, authfail -> fail
+. . require postgres connect, sslaccept, authfail -> fail
+. . . direct connect, directsslaccept, authfail -> fail
+. . . requiredirect connect, directsslaccept, authfail -> fail
+. prefer disable * connect, gssaccept, authok -> gss
+. . allow * connect, gssaccept, authok -> gss
+. . prefer * connect, gssaccept, authok -> gss
+. . require * connect, gssaccept, authok -> gss # GSS is chosen over SSL, even though sslmode=require
+. require disable * connect, gssaccept, authok -> gss
+. . allow * connect, gssaccept, authok -> gss
+. . prefer * connect, gssaccept, authok -> gss
+. . require * connect, gssaccept, authok -> gss # If both GSS and SSL is possible, GSS is chosen over SSL, even if sslmode=require
+
+ssluser disable disable * connect, authfail -> fail
+. . allow postgres connect, authfail, reconnect, sslaccept, authok -> ssl
+. . . direct connect, authfail, reconnect, directsslaccept, authok -> ssl
+. . . requiredirect connect, authfail, reconnect, directsslaccept, authok -> ssl
+. . prefer postgres connect, sslaccept, authok -> ssl
+. . . direct connect, directsslaccept, authok -> ssl
+. . . requiredirect connect, directsslaccept, authok -> ssl
+. . require postgres connect, sslaccept, authok -> ssl
+. . . direct connect, directsslaccept, authok -> ssl
+. . . requiredirect connect, directsslaccept, authok -> ssl
+. prefer disable * connect, gssaccept, authfail, reconnect, authfail -> fail
+. . allow postgres connect, gssaccept, authfail, reconnect, authfail, reconnect, sslaccept, authok -> ssl
+. . . direct connect, gssaccept, authfail, reconnect, authfail, reconnect, directsslaccept, authok -> ssl
+. . . requiredirect connect, gssaccept, authfail, reconnect, authfail, reconnect, directsslaccept, authok -> ssl
+. . prefer postgres connect, gssaccept, authfail, reconnect, sslaccept, authok -> ssl
+. . . direct connect, gssaccept, authfail, reconnect, directsslaccept, authok -> ssl
+. . . requiredirect connect, gssaccept, authfail, reconnect, directsslaccept, authok -> ssl
+. . require postgres connect, gssaccept, authfail, reconnect, sslaccept, authok -> ssl
+. . . direct connect, gssaccept, authfail, reconnect, directsslaccept, authok -> ssl
+. . . requiredirect connect, gssaccept, authfail, reconnect, directsslaccept, authok -> ssl
+. require disable * connect, gssaccept, authfail -> fail
+. . allow * connect, gssaccept, authfail -> fail
+. . prefer * connect, gssaccept, authfail -> fail
+. . require * connect, gssaccept, authfail -> fail # If both GSS and SSL are required, the sslmode=require is effectively ignored and GSS is required
+
+nogssuser disable disable * connect, authok -> plain
+. . allow postgres connect, authok -> plain
+. . . direct connect, authok -> plain
+. . . requiredirect connect, authok -> plain
+. . prefer postgres connect, sslaccept, authok -> ssl
+. . . direct connect, directsslaccept, authok -> ssl
+. . . requiredirect connect, directsslaccept, authok -> ssl
+. . require postgres connect, sslaccept, authok -> ssl
+. . . direct connect, directsslaccept, authok -> ssl
+. . . requiredirect connect, directsslaccept, authok -> ssl
+. prefer disable * connect, gssaccept, authfail, reconnect, authok -> plain
+. . allow * connect, gssaccept, authfail, reconnect, authok -> plain
+. . prefer postgres connect, gssaccept, authfail, reconnect, sslaccept, authok -> ssl
+. . . direct connect, gssaccept, authfail, reconnect, directsslaccept, authok -> ssl
+. . . requiredirect connect, gssaccept, authfail, reconnect, directsslaccept, authok -> ssl
+. . require postgres connect, gssaccept, authfail, reconnect, sslaccept, authok -> ssl
+. . . direct connect, gssaccept, authfail, reconnect, directsslaccept, authok -> ssl
+. . . requiredirect connect, gssaccept, authfail, reconnect, directsslaccept, authok -> ssl
+. require disable * connect, gssaccept, authfail -> fail
+. . allow * connect, gssaccept, authfail -> fail
+. . prefer * connect, gssaccept, authfail -> fail
+. . require * connect, gssaccept, authfail -> fail # If both GSS and SSL are required, the sslmode=require is effectively ignored and GSS is required
+
+nossluser disable disable * connect, authok -> plain
+. . allow * connect, authok -> plain
+. . prefer postgres connect, sslaccept, authfail, reconnect, authok -> plain
+. . . direct connect, directsslaccept, authfail, reconnect, authok -> plain
+. . . requiredirect connect, directsslaccept, authfail, reconnect, authok -> plain
+. . require postgres connect, sslaccept, authfail -> fail
+. . . direct connect, directsslaccept, authfail -> fail
+. . . requiredirect connect, directsslaccept, authfail -> fail
+. prefer * * connect, gssaccept, authok -> gss
+. require * * connect, gssaccept, authok -> gss
+ };
+
+ note("Running tests with both GSS and SSL enabled in server");
+ test_matrix($node, $server_config,
+ ['testuser', 'gssuser', 'ssluser', 'nogssuser', 'nossluser'],
+ \@all_gssencmodes, \@all_sslmodes, \@all_sslnegotiations,
+ parse_table($test_table));
+}
+
+###
+### Test negotiation over unix domain sockets.
+###
+SKIP:
+{
+ skip "Unix domain sockets not supported" unless ($unixdir ne "");
+
+ # libpq doesn't attempt SSL or GSSAPI over Unix domain
+ # sockets. The server would reject them too.
+ connect_test($node, "user=localuser gssencmode=prefer sslmode=prefer host=$unixdir", 'connect, authok -> plain');
+ connect_test($node, "user=localuser gssencmode=require sslmode=prefer host=$unixdir", '- -> fail');
+}
+
+done_testing();
+
+
+### Helper functions
+
+# Test the cube of parameters: user, gssencmode, sslmode, and sslnegotitation
+sub test_matrix
+{
+ local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+ my ($pg_node, $node_conf,
+ $test_users, $gssencmodes, $sslmodes, $sslnegotiations, %expected) = @_;
+
+ foreach my $test_user (@{$test_users})
+ {
+ foreach my $gssencmode (@{$gssencmodes})
+ {
+ foreach my $client_mode (@{$sslmodes})
+ {
+ # sslnegotiation only makes a difference if SSL is enabled. This saves a few combinations.
+ my ($key, $expected_events);
+ foreach my $negotiation (@{$sslnegotiations})
+ {
+ $key = "$test_user $gssencmode $client_mode $negotiation";
+ $expected_events = $expected{$key};
+ if (!defined($expected_events)) {
+ $expected_events = "<line missing from expected output table>";
+ }
+ connect_test($pg_node, "user=$test_user gssencmode=$gssencmode sslmode=$client_mode sslnegotiation=$negotiation", $expected_events);
+ }
+ }
+ }
+ }
+}
+
+# Try to establish a connection to the server using libpq. Verify the
+# negotiation events and outcome.
+sub connect_test
+{
+ local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+ my ($node, $connstr, $expected_events_and_outcome) = @_;
+
+ my $test_name = " '$connstr' -> $expected_events_and_outcome";
+
+ my $connstr_full = "";
+ $connstr_full .= "dbname=postgres " unless $connstr =~ m/dbname=/;
+ $connstr_full .= "host=$host hostaddr=$hostaddr " unless $connstr =~ m/host=/;
+ $connstr_full .= $connstr;
+
+ # Get the current size of the logfile before running the test.
+ # After the test, we can then check just the new lines that have
+ # appeared. (This is the same approach that the $node->log_contains
+ # function uses).
+ my $log_location = -s $node->logfile;
+
+ # XXX: Pass command with -c, because I saw intermittent test
+ # failures like this:
+ #
+ # ack Broken pipe: write( 13, 'SELECT current_enc()' ) at /usr/local/lib/perl5/site_perl/IPC/Run/IO.pm line 550.
+ #
+ # I think that happens if the connection fails before we write the
+ # query to its stdin. This test gets a lot of connection failures
+ # on purpose.
+ my ($ret, $stdout, $stderr) = $node->psql(
+ 'postgres',
+ '',
+ extra_params => ['-w', '-c', 'SELECT current_enc()'],
+ connstr => "$connstr_full",
+ on_error_stop => 0);
+
+ my $outcome = $ret == 0 ? $stdout : 'fail';
+
+ # Parse the EVENTS from the log file.
+ my $log_contents =
+ PostgreSQL::Test::Utils::slurp_file($node->logfile, $log_location);
+ my @events = parse_log_events($log_contents);
+
+ # Check that the events and outcome match the expected events and
+ # outcome
+ my $events_and_outcome = join(', ', @events) . " -> $outcome";
+ is($events_and_outcome, $expected_events_and_outcome, $test_name) or diag("$stderr");
+}
+
+# Parse a test table. See comment at top of the file for the format.
+sub parse_table
+{
+ my ($table) = @_;
+ my @lines = split /\n/, $table;
+
+ my %expected;
+
+ my ($user, $gssencmode, $sslmode, $sslnegotiation);
+ foreach my $line (@lines) {
+
+ # Trim comments
+ $line =~ s/#.*$//;
+
+ # Trim whitespace at beginning and end
+ $line =~ s/^\s+//;
+ $line =~ s/\s+$//;
+
+ # Ignore empty lines (includes comment-only lines)
+ next if $line eq '';
+
+ $line =~ m/^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S.*)\s*->\s*(\S+)\s*$/ or die "could not parse line \"$line\"";
+ $user = $1 unless $1 eq ".";
+ $gssencmode = $2 unless $2 eq ".";
+ $sslmode = $3 unless $3 eq ".";
+ $sslnegotiation = $4 unless $4 eq ".";
+
+ # Normalize the whitespace in the "EVENTS -> OUTCOME" part
+ my @events = split /,\s*/, $5;
+ my $outcome = $6;
+ my $events_str = join(', ', @events);
+ $events_str =~ s/\s+$//; # trim whitespace
+ my $events_and_outcome = "$events_str -> $outcome";
+
+ my %expanded = expand_expected_line($user, $gssencmode, $sslmode, $sslnegotiation, $events_and_outcome);
+ %expected = (%expected, %expanded);
+ }
+ return %expected;
+}
+
+# Expand wildcards on a test table line
+sub expand_expected_line
+{
+ my ($user, $gssencmode, $sslmode, $sslnegotiation, $expected) = @_;
+
+ my %result;
+ if ($user eq '*') {
+ foreach my $x (@all_test_users) {
+ %result = (%result, expand_expected_line($x, $gssencmode, $sslmode, $sslnegotiation, $expected));
+ }
+ } elsif ($gssencmode eq '*') {
+ foreach my $x (@all_gssencmodes) {
+ %result = (%result, expand_expected_line($user, $x, $sslmode, $sslnegotiation, $expected));
+ }
+ } elsif ($sslmode eq '*') {
+ foreach my $x (@all_sslmodes) {
+ %result = (%result, expand_expected_line($user, $gssencmode, $x, $sslnegotiation, $expected));
+ }
+ } elsif ($sslnegotiation eq '*') {
+ foreach my $x (@all_sslnegotiations) {
+ %result = (%result, expand_expected_line($user, $gssencmode, $sslmode, $x, $expected));
+ }
+ } else {
+ $result{"$user $gssencmode $sslmode $sslnegotiation"} = $expected;
+ }
+ return %result;
+}
+
+# Scrape the server log for the negotiation events that match the
+# EVENTS field of the test tables.
+sub parse_log_events
+{
+ my ($log_contents) = (@_);
+
+ my @events = ();
+
+ my @lines = split /\n/, $log_contents;
+ foreach my $line (@lines) {
+ push @events, "reconnect" if $line =~ /connection received/ && scalar(@events) > 0;
+ push @events, "connect" if $line =~ /connection received/ && scalar(@events) == 0;
+ push @events, "sslaccept" if $line =~ /SSLRequest accepted/;
+ push @events, "sslreject" if $line =~ /SSLRequest rejected/;
+ push @events, "directsslaccept" if $line =~ /direct SSL connection accepted/;
+ push @events, "directsslreject" if $line =~ /direct SSL connection rejected/;
+ push @events, "gssaccept" if $line =~ /GSSENCRequest accepted/;
+ push @events, "gssreject" if $line =~ /GSSENCRequest rejected/;
+ push @events, "authfail" if $line =~ /no pg_hba.conf entry/;
+ push @events, "authok" if $line =~ /connection authenticated/;
+ }
+
+ # No events at all is represented by "-"
+ if (scalar @events == 0) {
+ push @events, "-"
+ }
+
+ return @events;
+}