v9-0002-Add-tests-for-libpq-choosing-encryption-mode.patch

text/x-patch

Filename: v9-0002-Add-tests-for-libpq-choosing-encryption-mode.patch
Type: text/x-patch
Part: 1
Message: Re: Experiments with Postgres and SSL

Patch

Same data as JSON: GET /api/v1/attachments/:id/patch the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes. API reference →
Format: format-patch
Series: patch v9-0002
Subject: Add tests for libpq choosing encryption mode
File+
src/test/libpq_encryption/Makefile 25 0
src/test/libpq_encryption/meson.build 19 0
src/test/libpq_encryption/README 23 0
src/test/libpq_encryption/t/001_negotiate_encryption.pl 369 0
src/test/perl/PostgreSQL/Test/Kerberos.pm 3 3
From e52c701931f5c4b67d2014d6d27c925501f15464 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Date: Tue, 5 Mar 2024 15:41:09 +0200
Subject: [PATCH v9 2/6] Add tests for libpq choosing encryption mode

Author: Heikki Linnakangas, Matthias van de Meent
Discussion: XX
---
 src/test/libpq_encryption/Makefile            |  25 ++
 src/test/libpq_encryption/README              |  23 ++
 src/test/libpq_encryption/meson.build         |  19 +
 .../t/001_negotiate_encryption.pl             | 369 ++++++++++++++++++
 src/test/perl/PostgreSQL/Test/Kerberos.pm     |   6 +-
 5 files changed, 439 insertions(+), 3 deletions(-)
 create mode 100644 src/test/libpq_encryption/Makefile
 create mode 100644 src/test/libpq_encryption/README
 create mode 100644 src/test/libpq_encryption/meson.build
 create mode 100644 src/test/libpq_encryption/t/001_negotiate_encryption.pl

diff --git a/src/test/libpq_encryption/Makefile b/src/test/libpq_encryption/Makefile
new file mode 100644
index 00000000000..710929c4cce
--- /dev/null
+++ b/src/test/libpq_encryption/Makefile
@@ -0,0 +1,25 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/libpq_encryption
+#
+# Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/ldap/libpq_encryption
+#
+#-------------------------------------------------------------------------
+
+subdir = src/test/libpq_encryption
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+export  OPENSSL with_ssl with_gssapi with_krb_srvnam 
+
+check:
+	$(prove_check)
+
+installcheck:
+	$(prove_installcheck)
+
+clean distclean:
+	rm -rf tmp_check
diff --git a/src/test/libpq_encryption/README b/src/test/libpq_encryption/README
new file mode 100644
index 00000000000..66430b54316
--- /dev/null
+++ b/src/test/libpq_encryption/README
@@ -0,0 +1,23 @@
+src/test/libpq_encryption/README
+
+Tests for negotiating network encryption method
+===============================================
+
+
+Running the tests
+=================
+
+NOTE: You must have given the --enable-tap-tests argument to configure.
+
+Run
+    make check PG_TEST_EXTRA=libpq_encryption
+
+XXX You can use "make installcheck" if you previously did "make install".
+In that case, the code in the installation tree is tested.  With
+"make check", a temporary installation tree is built from the current
+sources and then tested.
+
+XXX Either way, this test initializes, starts, and stops a test Postgres
+cluster, as well as a test LDAP server.
+
+See src/test/perl/README for more info about running these tests.
diff --git a/src/test/libpq_encryption/meson.build b/src/test/libpq_encryption/meson.build
new file mode 100644
index 00000000000..04f479e9fe7
--- /dev/null
+++ b/src/test/libpq_encryption/meson.build
@@ -0,0 +1,19 @@
+# Copyright (c) 2022-2024, PostgreSQL Global Development Group
+
+tests += {
+  'name': 'ldap',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'tests': [
+      't/001_auth.pl',
+      't/002_bindpasswd.pl',
+    ],
+    'env': {
+      'with_ssl': ssl_library,
+      'OPENSSL': openssl.found() ? openssl.path() : '',
+      'with_gssapi': gssapi.found() ? 'yes' : 'no',
+      'with_krb_srvnam': 'postgres',
+    },
+  },
+}
diff --git a/src/test/libpq_encryption/t/001_negotiate_encryption.pl b/src/test/libpq_encryption/t/001_negotiate_encryption.pl
new file mode 100644
index 00000000000..b8646c5bc97
--- /dev/null
+++ b/src/test/libpq_encryption/t/001_negotiate_encryption.pl
@@ -0,0 +1,369 @@
+
+# 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;
+
+sub connect_test
+{
+	local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+	my ($node, $connstr, $expected_enc, @expect_log_msgs)
+	  = @_;
+
+	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;
+
+	my $log_location = -s $node->logfile;
+
+	my ($ret, $stdout, $stderr) = $node->psql(
+		'postgres',
+		'SELECT current_enc()',
+		extra_params => ['-w'],
+		connstr => "$connstr_full",
+		on_error_stop => 0);
+
+	my $result = $ret == 0 ? $stdout : 'fail';
+
+	is($result, $expected_enc, $test_name);
+
+	if (@expect_log_msgs)
+	{
+		# Match every message literally.
+		my @regexes = map { qr/\Q$_\E/ } @expect_log_msgs;
+		my %params = ();
+		$params{log_like} = \@regexes;
+		$node->log_check($test_name, $log_location, %params);
+	}
+}
+
+# Return the encryption mode that we expect to be chosen by libpq,
+# when connecting with given the user, gssmode, sslmode settings.
+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';
+
+	# If gssmode=require, drop all non-GSS modes.
+	if ($gssmode eq 'require')
+	{
+		@conntypes = grep {/gss/} @conntypes;
+	}
+
+	# If sslmode=require, drop plain mode.
+	#
+	# NOTE: GSS is also allowed with sslmode=require.
+	if ($sslmode eq 'require')
+	{
+		@conntypes = grep {!/plain/} @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, and gssencmode
+sub test_modes
+{
+	local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+	my ($pg_node, $node_conf,
+		$test_users, $ssl_modes, $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);
+				connect_test($pg_node, "user=$test_user sslmode=$client_mode gssencmode=$gssencmode", $res);
+			}
+		}
+	}
+}
+
+my $sslmodes = ['disable', 'allow', 'prefer', 'require'];
+my $gssencmodes = ['disable', 'prefer', 'require'];
+
+my $server_config = {
+	server_ssl => 0,
+	server_gss => 0,
+};
+
+note("Running tests with SSL and GSS disabled in server");
+test_modes($node, $server_config,
+		   ['testuser'],
+		   $sslmodes, $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;
+
+	note("Running tests with SSL enabled in server");
+	test_modes($node, $server_config,
+			   ['testuser', 'ssluser', 'nossluser'],
+			   $sslmodes, ['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_principal('gssuser', $gssuser_password);
+	$krb->create_ticket('gssuser', $gssuser_password);
+	$server_config->{server_gss} = 1;
+
+	note("Running tests with GSS enabled in server");
+	test_modes($node, $server_config,
+			   ['testuser', 'gssuser', 'nogssuser'],
+			   $sslmodes, $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');
+}
+
+# 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');
+
+	# Enable SSL
+	$node->adjust_conf('postgresql.conf', 'ssl', 'on');
+	$node->reload;
+	$server_config->{server_ssl} = 1;
+
+	note("Running tests with both GSS and SSL enabled in server");
+	test_modes($node, $server_config,
+			   ['testuser', 'gssuser', 'ssluser', 'nogssuser', 'nossluser'],
+			   $sslmodes, $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();
diff --git a/src/test/perl/PostgreSQL/Test/Kerberos.pm b/src/test/perl/PostgreSQL/Test/Kerberos.pm
index 5bb00c76f14..e6063703c3a 100644
--- a/src/test/perl/PostgreSQL/Test/Kerberos.pm
+++ b/src/test/perl/PostgreSQL/Test/Kerberos.pm
@@ -103,10 +103,10 @@ sub new
 
 	my ($stdout, $krb5_version);
 	run_log [ $krb5_config, '--version' ], '>', \$stdout
-	  or BAIL_OUT("could not execute krb5-config");
-	BAIL_OUT("Heimdal is not supported") if $stdout =~ m/heimdal/;
+	  or die("could not execute krb5-config");
+	die("Heimdal is not supported") if $stdout =~ m/heimdal/;
 	$stdout =~ m/Kerberos 5 release ([0-9]+\.[0-9]+)/
-	  or BAIL_OUT("could not get Kerberos version");
+	  or die("could not get Kerberos version");
 	$krb5_version = $1;
 
 	# Build the krb5.conf to use.
-- 
2.39.2