Enhance libpq encryption negotiation tests with new GUC
[pgsql.git] / src / test / libpq_encryption / t / 001_negotiate_encryption.pl
blobf277edda82554bfa67be55bc3481b2e723732580
2 # Copyright (c) 2021-2024, PostgreSQL Global Development Group
4 # OVERVIEW
5 # --------
7 # Test negotiation of SSL and GSSAPI encryption
9 # We test all combinations of:
11 # - all the libpq client options that affect the protocol negotiations
12 # (gssencmode, sslmode)
13 # - server accepting or rejecting the authentication due to
14 # pg_hba.conf entries
15 # - SSL and GSS enabled/disabled in the server
17 # That's a lot of combinations, so we use a table-driven approach.
18 # Each combination is represented by a line in a table. The line lists
19 # the options specifying the test case, and an expected outcome. The
20 # expected outcome includes whether the connection succeeds or fails,
21 # and whether it uses SSL, GSS or no encryption. It also includes a
22 # condensed trace of what steps were taken during the negotiation.
23 # That can catch cases like useless retries, or if the encryption
24 # methods are attempted in wrong order, even when it doesn't affect
25 # the final outcome.
27 # TEST TABLE FORMAT
28 # -----------------
30 # Example of the test table format:
32 # # USER GSSENCMODE SSLMODE EVENTS -> OUTCOME
33 # testuser disable allow connect, authok -> plain
34 # . . prefer connect, sslaccept, authok -> ssl
35 # testuser require * connect, gssreject -> fail
37 # USER, GSSENCMODE and SSLMODE fields are the libpq 'user',
38 # 'gssencmode' and 'sslmode' options used in the test. As a shorthand,
39 # a single dot ('.') can be used in the USER, GSSENCMODE, and SSLMODE
40 # fields, to indicate "same as on previous line". A '*' can be used
41 # as a wildcard; it is expanded to mean all possible values of that
42 # field.
44 # The EVENTS field is a condensed trace of expected steps during the
45 # negotiation:
47 # connect: a TCP connection was established
48 # reconnect: TCP connection was disconnected, and a new one was established
49 # sslaccept: client requested SSL encryption and server accepted it
50 # sslreject: client requested SSL encryption but server rejected it
51 # gssaccept: client requested GSSAPI encryption and server accepted it
52 # gssreject: client requested GSSAPI encryption but server rejected it
53 # authok: client sent startup packet and authentication was performed successfully
54 # authfail: client sent startup packet but server rejected the authentication
56 # The event trace can be used to verify that the client negotiated the
57 # connection properly in more detail than just by looking at the
58 # outcome. For example, if the client opens spurious extra TCP
59 # connections, that would show up in the EVENTS.
61 # The OUTCOME field indicates the expected result of the test:
63 # plain: an unencrypted connection was established
64 # ssl: SSL connection was established
65 # gss: GSSAPI encrypted connection was established
66 # fail: the connection attempt failed
68 # Empty lines are ignored. '#' can be used to mark the rest of the
69 # line as a comment.
71 use strict;
72 use warnings FATAL => 'all';
73 use PostgreSQL::Test::Utils;
74 use PostgreSQL::Test::Cluster;
75 use PostgreSQL::Test::Kerberos;
76 use File::Basename;
77 use File::Copy;
78 use Test::More;
80 if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\blibpq_encryption\b/)
82 plan skip_all =>
83 'Potentially unsafe test libpq_encryption not enabled in PG_TEST_EXTRA';
86 my $ssl_supported = $ENV{with_ssl} eq 'openssl';
87 my $gss_supported = $ENV{with_gssapi} eq 'yes';
89 ###
90 ### Prepare test server for GSSAPI and SSL authentication, with a few
91 ### different test users and helper functions. We don't actually
92 ### enable SSL and kerberos in the server yet, we will do that later.
93 ###
95 my $host = 'enc-test-localhost.postgresql.example.com';
96 my $hostaddr = '127.0.0.1';
97 my $servercidr = '127.0.0.1/32';
99 my $node = PostgreSQL::Test::Cluster->new('node');
100 $node->init;
101 $node->append_conf(
102 'postgresql.conf', qq{
103 listen_addresses = '$hostaddr'
105 # Capturing the EVENTS that occur during tests requires these settings
106 log_connections = on
107 log_disconnections = on
108 trace_connection_negotiation = on
109 lc_messages = 'C'
111 my $pgdata = $node->data_dir;
113 my $dbname = 'postgres';
114 my $username = 'enctest';
115 my $application = '001_negotiate_encryption.pl';
117 my $gssuser_password = 'secret1';
119 my $krb;
121 if ($gss_supported != 0)
123 note "setting up Kerberos";
125 my $realm = 'EXAMPLE.COM';
126 $krb = PostgreSQL::Test::Kerberos->new($host, $hostaddr, $realm);
127 $node->append_conf('postgresql.conf', "krb_server_keyfile = '$krb->{keytab}'\n");
130 if ($ssl_supported != 0)
132 my $certdir = dirname(__FILE__) . "/../../ssl/ssl";
134 copy "$certdir/server-cn-only.crt", "$pgdata/server.crt"
135 || die "copying server.crt: $!";
136 copy "$certdir/server-cn-only.key", "$pgdata/server.key"
137 || die "copying server.key: $!";
138 chmod(0600, "$pgdata/server.key");
140 # Start with SSL disabled.
141 $node->append_conf('postgresql.conf', "ssl = off\n");
144 $node->start;
146 $node->safe_psql('postgres', 'CREATE USER localuser;');
147 $node->safe_psql('postgres', 'CREATE USER testuser;');
148 $node->safe_psql('postgres', 'CREATE USER ssluser;');
149 $node->safe_psql('postgres', 'CREATE USER nossluser;');
150 $node->safe_psql('postgres', 'CREATE USER gssuser;');
151 $node->safe_psql('postgres', 'CREATE USER nogssuser;');
153 my $unixdir = $node->safe_psql('postgres', 'SHOW unix_socket_directories;');
154 chomp($unixdir);
156 # Helper function that returns the encryption method in use in the
157 # connection.
158 $node->safe_psql('postgres', q{
159 CREATE FUNCTION current_enc() RETURNS text LANGUAGE plpgsql AS $$
160 DECLARE
161 ssl_in_use bool;
162 gss_in_use bool;
163 BEGIN
164 ssl_in_use = (SELECT ssl FROM pg_stat_ssl WHERE pid = pg_backend_pid());
165 gss_in_use = (SELECT encrypted FROM pg_stat_gssapi WHERE pid = pg_backend_pid());
167 raise log 'ssl % gss %', ssl_in_use, gss_in_use;
169 IF ssl_in_use AND gss_in_use THEN
170 RETURN 'ssl+gss'; -- shouldn't happen
171 ELSIF ssl_in_use THEN
172 RETURN 'ssl';
173 ELSIF gss_in_use THEN
174 RETURN 'gss';
175 ELSE
176 RETURN 'plain';
177 END IF;
178 END;
182 # Only accept SSL connections from $servercidr. Our tests don't depend on this
183 # but seems best to keep it as narrow as possible for security reasons.
184 open my $hba, '>', "$pgdata/pg_hba.conf";
185 print $hba qq{
186 # TYPE DATABASE USER ADDRESS METHOD OPTIONS
187 local postgres localuser trust
188 host postgres testuser $servercidr trust
189 hostnossl postgres nossluser $servercidr trust
190 hostnogssenc postgres nogssuser $servercidr trust
193 print $hba qq{
194 hostssl postgres ssluser $servercidr trust
195 } if ($ssl_supported != 0);
197 print $hba qq{
198 hostgssenc postgres gssuser $servercidr trust
199 } if ($gss_supported != 0);
200 close $hba;
201 $node->reload;
203 # Ok, all prepared. Run the tests.
205 my @all_test_users = ('testuser', 'ssluser', 'nossluser', 'gssuser', 'nogssuser');
206 my @all_gssencmodes = ('disable', 'prefer', 'require');
207 my @all_sslmodes = ('disable', 'allow', 'prefer', 'require');
209 my $server_config = {
210 server_ssl => 0,
211 server_gss => 0,
215 ### Run tests with GSS and SSL disabled in the server
217 my $test_table = q{
218 # USER GSSENCMODE SSLMODE EVENTS -> OUTCOME
219 testuser disable disable connect, authok -> plain
220 . . allow connect, authok -> plain
221 . . prefer connect, sslreject, authok -> plain
222 . . require connect, sslreject -> fail
223 . prefer disable connect, authok -> plain
224 . . allow connect, authok -> plain
225 . . prefer connect, sslreject, authok -> plain
226 . . require connect, sslreject -> fail
228 # All attempts with gssencmode=require fail without connecting because
229 # no credential cache has been configured in the client
230 . require * - -> fail
232 note("Running tests with SSL and GSS disabled in the server");
233 test_matrix($node, $server_config,
234 ['testuser'],
235 \@all_sslmodes, \@all_gssencmodes, parse_table($test_table));
238 ### Run tests with GSS disabled and SSL enabled in the server
240 SKIP:
242 skip "SSL not supported by this build" if $ssl_supported == 0;
244 $test_table = q{
245 # USER GSSENCMODE SSLMODE EVENTS -> OUTCOME
246 testuser disable disable connect, authok -> plain
247 . . allow connect, authok -> plain
248 . . prefer connect, sslaccept, authok -> ssl
249 . . require connect, sslaccept, authok -> ssl
250 ssluser . disable connect, authfail -> fail
251 . . allow connect, authfail, reconnect, sslaccept, authok -> ssl
252 . . prefer connect, sslaccept, authok -> ssl
253 . . require connect, sslaccept, authok -> ssl
254 nossluser . disable connect, authok -> plain
255 . . allow connect, authok -> plain
256 . . prefer connect, sslaccept, authfail, reconnect, authok -> plain
257 . . require connect, sslaccept, authfail -> fail
260 # Enable SSL in the server
261 $node->adjust_conf('postgresql.conf', 'ssl', 'on');
262 $node->reload;
263 $server_config->{server_ssl} = 1;
265 note("Running tests with SSL enabled in server");
266 test_matrix($node, $server_config,
267 ['testuser', 'ssluser', 'nossluser'],
268 \@all_sslmodes, ['disable'], parse_table($test_table));
270 # Disable SSL again
271 $node->adjust_conf('postgresql.conf', 'ssl', 'off');
272 $node->reload;
273 $server_config->{server_ssl} = 0;
277 ### Run tests with GSS enabled, SSL disabled in the server
279 SKIP:
281 skip "GSSAPI/Kerberos not supported by this build" if $gss_supported == 0;
282 $test_table = q{
283 # USER GSSENCMODE SSLMODE EVENTS -> OUTCOME
284 testuser disable disable connect, authok -> plain
285 . . allow connect, authok -> plain
286 . . prefer connect, sslreject, authok -> plain
287 . . require connect, sslreject -> fail
288 . prefer * connect, gssaccept, authok -> gss
289 . require disable connect, gssaccept, authok -> gss
290 . . allow connect, gssaccept, authok -> gss
291 . . prefer connect, gssaccept, authok -> gss
292 . . require connect, gssaccept, authok -> gss # If both GSS and SSL is possible, GSS is chosen over SSL, even if sslmode=require
294 gssuser disable disable connect, authfail -> fail
296 # XXX: after the reconnection and SSL negotiation failure, libpq tries
297 # again to authenticate in plaintext. That's unnecessariy and doomed
298 # to fail. We already know the server doesn't accept that because of
299 # the first authentication failure.
300 . . allow connect, authfail, reconnect, sslreject, authfail -> fail
302 . . prefer connect, sslreject, authfail -> fail
303 . . require connect, sslreject -> fail
304 . prefer * connect, gssaccept, authok -> gss
305 . require * connect, gssaccept, authok -> gss
307 nogssuser disable disable connect, authok -> plain
308 . . allow connect, authok -> plain
309 . . prefer connect, sslreject, authok -> plain
310 . . require connect, sslreject, -> fail
311 . prefer disable connect, gssaccept, authfail, reconnect, authok -> plain
312 . . allow connect, gssaccept, authfail, reconnect, authok -> plain
313 . . prefer connect, gssaccept, authfail, reconnect, sslreject, authok -> plain
314 . . require connect, gssaccept, authfail, reconnect, sslreject -> fail
315 . require disable connect, gssaccept, authfail -> fail
317 # XXX: libpq retries the connection unnecessarily in this case:
318 . . allow connect, gssaccept, authfail, reconnect, gssaccept, authfail -> fail
320 . . prefer connect, gssaccept, authfail -> fail
321 . . require connect, gssaccept, authfail -> fail
324 # Sanity check that the connection fails when no kerberos ticket
325 # is present in the client
326 connect_test($node, 'user=testuser gssencmode=require sslmode=disable', '- -> fail');
328 $krb->create_principal('gssuser', $gssuser_password);
329 $krb->create_ticket('gssuser', $gssuser_password);
330 $server_config->{server_gss} = 1;
332 note("Running tests with GSS enabled in server");
333 test_matrix($node, $server_config,
334 ['testuser', 'gssuser', 'nogssuser'],
335 \@all_sslmodes, \@all_gssencmodes, parse_table($test_table));
339 ### Tests with both GSS and SSL enabled in the server
341 SKIP:
343 skip "GSSAPI/Kerberos or SSL not supported by this build" unless ($ssl_supported && $gss_supported);
345 $test_table = q{
346 # USER GSSENCMODE SSLMODE EVENTS -> OUTCOME
347 testuser disable disable connect, authok -> plain
348 . . allow connect, authok -> plain
349 . . prefer connect, sslaccept, authok -> ssl
350 . . require connect, sslaccept, authok -> ssl
351 . prefer disable connect, gssaccept, authok -> gss
352 . . allow connect, gssaccept, authok -> gss
353 . . prefer connect, gssaccept, authok -> gss
354 . . require connect, gssaccept, authok -> gss # If both GSS and SSL is possible, GSS is chosen over SSL, even if sslmode=require
355 . require disable connect, gssaccept, authok -> gss
356 . . allow connect, gssaccept, authok -> gss
357 . . prefer connect, gssaccept, authok -> gss
358 . . require connect, gssaccept, authok -> gss # If both GSS and SSL is possible, GSS is chosen over SSL, even if sslmode=require
360 gssuser disable disable connect, authfail -> fail
361 . . allow connect, authfail, reconnect, sslaccept, authfail -> fail
362 . . prefer connect, sslaccept, authfail, reconnect, authfail -> fail
363 . . require connect, sslaccept, authfail -> fail
364 . prefer * connect, gssaccept, authok -> gss
365 . require disable connect, gssaccept, authok -> gss
366 . . allow connect, gssaccept, authok -> gss
367 . . prefer connect, gssaccept, authok -> gss
368 . . require connect, gssaccept, authok -> gss # If both GSS and SSL is possible, GSS is chosen over SSL, even if sslmode=require
370 ssluser disable disable connect, authfail -> fail
371 . . allow connect, authfail, reconnect, sslaccept, authok -> ssl
372 . . prefer connect, sslaccept, authok -> ssl
373 . . require connect, sslaccept, authok -> ssl
374 . prefer disable connect, gssaccept, authfail, reconnect, authfail -> fail
375 . . allow connect, gssaccept, authfail, reconnect, authfail, reconnect, sslaccept, authok -> ssl
376 . . prefer connect, gssaccept, authfail, reconnect, sslaccept, authok -> ssl
377 . . require connect, gssaccept, authfail, reconnect, sslaccept, authok -> ssl
378 . require disable connect, gssaccept, authfail -> fail
380 # XXX: libpq retries the connection unnecessarily in this case:
381 . . allow connect, gssaccept, authfail, reconnect, gssaccept, authfail -> fail
383 . . prefer connect, gssaccept, authfail -> fail
384 . . require connect, gssaccept, authfail -> fail # If both GSS and SSL are required, the sslmode=require is effectively ignored and GSS is required
386 nogssuser disable disable connect, authok -> plain
387 . . allow connect, authok -> plain
388 . . prefer connect, sslaccept, authok -> ssl
389 . . require connect, sslaccept, authok -> ssl
390 . prefer disable connect, gssaccept, authfail, reconnect, authok -> plain
391 . . allow connect, gssaccept, authfail, reconnect, authok -> plain
392 . . prefer connect, gssaccept, authfail, reconnect, sslaccept, authok -> ssl
393 . . require connect, gssaccept, authfail, reconnect, sslaccept, authok -> ssl
394 . require disable connect, gssaccept, authfail -> fail
396 # XXX: libpq retries the connection unnecessarily in this case:
397 . . allow connect, gssaccept, authfail, reconnect, gssaccept, authfail -> fail
399 . . prefer connect, gssaccept, authfail -> fail
400 . . require connect, gssaccept, authfail -> fail
402 nossluser disable disable connect, authok -> plain
403 . . allow connect, authok -> plain
404 . . prefer connect, sslaccept, authfail, reconnect, authok -> plain
405 . . require connect, sslaccept, authfail -> fail
406 . prefer * connect, gssaccept, authok -> gss
407 . require * connect, gssaccept, authok -> gss
410 # Sanity check that GSSAPI is still enabled from previous test.
411 connect_test($node, 'user=testuser gssencmode=prefer sslmode=prefer', 'connect, gssaccept, authok -> gss');
413 # Enable SSL
414 $node->adjust_conf('postgresql.conf', 'ssl', 'on');
415 $node->reload;
416 $server_config->{server_ssl} = 1;
418 note("Running tests with both GSS and SSL enabled in server");
419 test_matrix($node, $server_config,
420 ['testuser', 'gssuser', 'ssluser', 'nogssuser', 'nossluser'],
421 \@all_sslmodes, \@all_gssencmodes, parse_table($test_table));
425 ### Test negotiation over unix domain sockets.
427 SKIP:
429 skip "Unix domain sockets not supported" unless ($unixdir ne "");
431 connect_test($node, "user=localuser gssencmode=prefer sslmode=require host=$unixdir", 'connect, authok -> plain');
432 connect_test($node, "user=localuser gssencmode=require sslmode=prefer host=$unixdir", '- -> fail');
435 done_testing();
438 ### Helper functions
440 # Test the cube of parameters: user, sslmode, and gssencmode
441 sub test_matrix
443 local $Test::Builder::Level = $Test::Builder::Level + 1;
445 my ($pg_node, $node_conf,
446 $test_users, $ssl_modes, $gss_modes, %expected) = @_;
448 foreach my $test_user (@{$test_users})
450 foreach my $gssencmode (@{$gss_modes})
452 foreach my $client_mode (@{$ssl_modes})
454 my %params = (
455 server_ssl=>$node_conf->{server_ssl},
456 server_gss=>$node_conf->{server_gss},
457 user=>$test_user,
458 gssencmode=>$gssencmode,
459 sslmode=>$client_mode,
461 my $key = "$test_user $gssencmode $client_mode";
462 my $expected_events = $expected{$key};
463 if (!defined($expected_events)) {
464 $expected_events = "<line missing from expected output table>";
466 connect_test($pg_node, "user=$test_user gssencmode=$gssencmode sslmode=$client_mode", $expected_events);
472 # Try to establish a connection to the server using libpq. Verify the
473 # negotiation events and outcome.
474 sub connect_test
476 local $Test::Builder::Level = $Test::Builder::Level + 1;
478 my ($node, $connstr, $expected_events_and_outcome) = @_;
480 my $test_name = " '$connstr' -> $expected_events_and_outcome";
482 my $connstr_full = "";
483 $connstr_full .= "dbname=postgres " unless $connstr =~ m/dbname=/;
484 $connstr_full .= "host=$host hostaddr=$hostaddr " unless $connstr =~ m/host=/;
485 $connstr_full .= $connstr;
487 # Get the current size of the logfile before running the test.
488 # After the test, we can then check just the new lines that have
489 # appeared. (This is the same approach that the $node->log_contains
490 # function uses).
491 my $log_location = -s $node->logfile;
493 # XXX: Pass command with -c, because I saw intermittent test
494 # failures like this:
496 # ack Broken pipe: write( 13, 'SELECT current_enc()' ) at /usr/local/lib/perl5/site_perl/IPC/Run/IO.pm line 550.
498 # I think that happens if the connection fails before we write the
499 # query to its stdin. This test gets a lot of connection failures
500 # on purpose.
501 my ($ret, $stdout, $stderr) = $node->psql(
502 'postgres',
504 extra_params => ['-w', '-c', 'SELECT current_enc()'],
505 connstr => "$connstr_full",
506 on_error_stop => 0);
508 my $outcome = $ret == 0 ? $stdout : 'fail';
510 # Parse the EVENTS from the log file.
511 my $log_contents =
512 PostgreSQL::Test::Utils::slurp_file($node->logfile, $log_location);
513 my @events = parse_log_events($log_contents);
515 # Check that the events and outcome match the expected events and
516 # outcome
517 my $events_and_outcome = join(', ', @events) . " -> $outcome";
518 is($events_and_outcome, $expected_events_and_outcome, $test_name) or diag("$stderr");
521 # Parse a test table. See comment at top of the file for the format.
522 sub parse_table
524 my ($table) = @_;
525 my @lines = split /\n/, $table;
527 my %expected;
529 my ($user, $gssencmode, $sslmode);
530 foreach my $line (@lines) {
532 # Trim comments
533 $line =~ s/#.*$//;
535 # Trim whitespace at beginning and end
536 $line =~ s/^\s+//;
537 $line =~ s/\s+$//;
539 # Ignore empty lines (includes comment-only lines)
540 next if $line eq '';
542 $line =~ m/^(\S+)\s+(\S+)\s+(\S+)\s+(\S.*)\s*->\s*(\S+)\s*$/ or die "could not parse line \"$line\"";
543 $user = $1 unless $1 eq ".";
544 $gssencmode = $2 unless $2 eq ".";
545 $sslmode = $3 unless $3 eq ".";
547 # Normalize the whitespace in the "EVENTS -> OUTCOME" part
548 my @events = split /,\s*/, $4;
549 my $outcome = $5;
550 my $events_str = join(', ', @events);
551 $events_str =~ s/\s+$//; # trim whitespace
552 my $events_and_outcome = "$events_str -> $outcome";
554 my %expanded = expand_expected_line($user, $gssencmode, $sslmode, $events_and_outcome);
555 %expected = (%expected, %expanded);
557 return %expected;
560 # Expand wildcards on a test table line
561 sub expand_expected_line
563 my ($user, $gssencmode, $sslmode, $expected) = @_;
565 my %result;
566 if ($user eq '*') {
567 foreach my $x (@all_test_users) {
568 %result = (%result, expand_expected_line($x, $gssencmode, $sslmode, $expected));
570 } elsif ($gssencmode eq '*') {
571 foreach my $x (@all_gssencmodes) {
572 %result = (%result, expand_expected_line($user, $x, $sslmode, $expected));
574 } elsif ($sslmode eq '*') {
575 foreach my $x (@all_sslmodes) {
576 %result = (%result, expand_expected_line($user, $gssencmode, $x, $expected));
578 } else {
579 $result{"$user $gssencmode $sslmode"} = $expected;
581 return %result;
584 # Scrape the server log for the negotiation events that match the
585 # EVENTS field of the test tables.
586 sub parse_log_events
588 my ($log_contents) = (@_);
590 my @events = ();
592 my @lines = split /\n/, $log_contents;
593 foreach my $line (@lines) {
594 push @events, "reconnect" if $line =~ /connection received/ && scalar(@events) > 0;
595 push @events, "connect" if $line =~ /connection received/ && scalar(@events) == 0;
596 push @events, "sslaccept" if $line =~ /SSLRequest accepted/;
597 push @events, "sslreject" if $line =~ /SSLRequest rejected/;
598 push @events, "gssaccept" if $line =~ /GSSENCRequest accepted/;
599 push @events, "gssreject" if $line =~ /GSSENCRequest rejected/;
600 push @events, "authfail" if $line =~ /no pg_hba.conf entry/;
601 push @events, "authok" if $line =~ /connection authenticated/;
604 # No events at all is represented by "-"
605 if (scalar @events == 0) {
606 push @events, "-"
609 return @events;