From 8b753cc78834825d7e10321da369127aadb7cfca Mon Sep 17 00:00:00 2001
From: Jeff Davis <jeff@j-davis.com>
Date: Tue, 20 Aug 2019 18:59:23 -0700
Subject: [PATCH] Add libpq parameter 'channel_binding'.

Allow clients to require channel binding to enhance security against
untrusted servers.

Author: Jeff Davis
Discussion: https://postgr.es/m/227015d8417f2b4fef03f8966dbfa5cbcc4f44da.camel%40j-davis.com
---
 doc/src/sgml/libpq.sgml                   | 32 +++++++++
 src/interfaces/libpq/fe-auth-scram.c      | 10 ++-
 src/interfaces/libpq/fe-auth.c            | 82 +++++++++++++++++++++--
 src/interfaces/libpq/fe-connect.c         | 36 ++++++++++
 src/interfaces/libpq/libpq-int.h          |  3 +
 src/test/authentication/t/002_saslprep.pl | 12 +++-
 src/test/ssl/t/002_scram.pl               | 15 ++++-
 src/test/ssl/t/SSLServer.pm               |  9 ++-
 8 files changed, 184 insertions(+), 15 deletions(-)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 0a187e256a4..be9cd4ade75 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1122,6 +1122,28 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-channel-binding" xreflabel="channel_binding">
+      <term><literal>channel_binding</literal></term>
+      <listitem>
+      <para>
+        This option controls the client's use of channel binding. A setting
+        of <literal>require</literal> means that the connection must employ
+        channel binding, <literal>prefer</literal> means that the client will
+        choose channel binding if available, and <literal>disable</literal>
+        prevents the use of channel binding. The default
+        is <literal>prefer</literal> if
+        <productname>PostgreSQL</productname> is compiled with SSL support;
+        otherwise the default is <literal>disable</literal>.
+      </para>
+      <para>
+        Channel binding is a method for the server to authenticate itself to
+        the client. It is only supported over SSL connections
+        with <productname>PostgreSQL</productname> 11 or later servers using
+        the <literal>SCRAM</literal> authentication method.
+      </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-connect-timeout" xreflabel="connect_timeout">
       <term><literal>connect_timeout</literal></term>
       <listitem>
@@ -6864,6 +6886,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGCHANNELBINDING</envar></primary>
+      </indexterm>
+      <envar>PGCHANNELBINDING</envar> behaves the same as the <xref
+      linkend="libpq-connect-channel-binding"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index 7a8335bf9f8..875cd07bc05 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -225,9 +225,7 @@ pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
 
 			/*
 			 * Verify server signature, to make sure we're talking to the
-			 * genuine server.  XXX: A fake server could simply not require
-			 * authentication, though.  There is currently no option in libpq
-			 * to reject a connection, if SCRAM authentication did not happen.
+			 * genuine server.
 			 */
 			if (verify_server_signature(state))
 				*success = true;
@@ -358,7 +356,7 @@ build_client_first_message(fe_scram_state *state)
 		appendPQExpBufferStr(&buf, "p=tls-server-end-point");
 	}
 #ifdef HAVE_PGTLS_GET_PEER_CERTIFICATE_HASH
-	else if (conn->ssl_in_use)
+	else if (conn->ssl_in_use && conn->channel_binding[0] != 'd')
 	{
 		/*
 		 * Client supports channel binding, but thinks the server does not.
@@ -369,7 +367,7 @@ build_client_first_message(fe_scram_state *state)
 	else
 	{
 		/*
-		 * Client does not support channel binding.
+		 * Client does not support channel binding, or has disabled it.
 		 */
 		appendPQExpBufferChar(&buf, 'n');
 	}
@@ -498,7 +496,7 @@ build_client_final_message(fe_scram_state *state)
 #endif							/* HAVE_PGTLS_GET_PEER_CERTIFICATE_HASH */
 	}
 #ifdef HAVE_PGTLS_GET_PEER_CERTIFICATE_HASH
-	else if (conn->ssl_in_use)
+	else if (conn->ssl_in_use && conn->channel_binding[0] != 'd')
 		appendPQExpBufferStr(&buf, "c=eSws");	/* base64 of "y,," */
 #endif
 	else
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index ab227421b3b..3394ce5a142 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -423,6 +423,14 @@ pg_SASL_init(PGconn *conn, int payloadlen)
 
 	initPQExpBuffer(&mechanism_buf);
 
+	if (conn->channel_binding[0] == 'r' && /* require */
+		!conn->ssl_in_use)
+	{
+		printfPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("Channel binding required, but SSL not in use\n"));
+		goto error;
+	}
+
 	if (conn->sasl_state)
 	{
 		printfPQExpBuffer(&conn->errorMessage,
@@ -455,9 +463,9 @@ pg_SASL_init(PGconn *conn, int payloadlen)
 		/*
 		 * Select the mechanism to use.  Pick SCRAM-SHA-256-PLUS over anything
 		 * else if a channel binding type is set and if the client supports
-		 * it. Pick SCRAM-SHA-256 if nothing else has already been picked.  If
-		 * we add more mechanisms, a more refined priority mechanism might
-		 * become necessary.
+		 * it (and did not set channel_binding=disable). Pick SCRAM-SHA-256 if
+		 * nothing else has already been picked.  If we add more mechanisms, a
+		 * more refined priority mechanism might become necessary.
 		 */
 		if (strcmp(mechanism_buf.data, SCRAM_SHA_256_PLUS_NAME) == 0)
 		{
@@ -466,10 +474,11 @@ pg_SASL_init(PGconn *conn, int payloadlen)
 				/*
 				 * The server has offered SCRAM-SHA-256-PLUS, which is only
 				 * supported by the client if a hash of the peer certificate
-				 * can be created.
+				 * can be created, and if channel_binding is not disabled.
 				 */
 #ifdef HAVE_PGTLS_GET_PEER_CERTIFICATE_HASH
-				selected_mechanism = SCRAM_SHA_256_PLUS_NAME;
+				if (conn->channel_binding[0] != 'd') /* disable */
+					selected_mechanism = SCRAM_SHA_256_PLUS_NAME;
 #endif
 			}
 			else
@@ -493,6 +502,14 @@ pg_SASL_init(PGconn *conn, int payloadlen)
 			selected_mechanism = SCRAM_SHA_256_NAME;
 	}
 
+	if (conn->channel_binding[0] == 'r' && /* require */
+		strcmp(selected_mechanism, SCRAM_SHA_256_NAME) == 0)
+	{
+		printfPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("SASL authentication mechanism SCRAM-SHA-256 selected but channel binding is required\n"));
+		goto error;
+	}
+
 	if (!selected_mechanism)
 	{
 		printfPQExpBuffer(&conn->errorMessage,
@@ -565,6 +582,9 @@ pg_SASL_init(PGconn *conn, int payloadlen)
 	if (initialresponse)
 		free(initialresponse);
 
+	if (strcmp(selected_mechanism, SCRAM_SHA_256_PLUS_NAME) == 0)
+		conn->channel_bound = true;
+
 	return STATUS_OK;
 
 error:
@@ -774,6 +794,51 @@ pg_password_sendauth(PGconn *conn, const char *password, AuthRequest areq)
 	return ret;
 }
 
+/*
+ * Verify that the authentication request is expected, given the connection
+ * parameters. This is especially important when the client wishes to
+ * authenticate the server before any sensitive information is exchanged.
+ */
+static bool
+check_expected_areq(AuthRequest areq, PGconn *conn)
+{
+	bool result = true;
+
+	/*
+	 * When channel_binding=require, we must protect against two cases:
+	 *
+	 * 1. we must not respond to non-SASL authentication requests, which might
+	 *    leak information such as the client's password; and
+	 * 2. even if we receive AUTH_REQ_OK, we still must ensure that channel
+	 *    binding has happened in order to authenticate the server
+	 */
+	if (conn->channel_binding[0] == 'r' /* require */)
+	{
+		switch (areq)
+		{
+			case AUTH_REQ_SASL:
+			case AUTH_REQ_SASL_CONT:
+			case AUTH_REQ_SASL_FIN:
+				break;
+			case AUTH_REQ_OK:
+				if (!conn->channel_bound || !conn->sasl_finished)
+				{
+					printfPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("Channel binding required but not offered by server\n"));
+					result = false;
+				}
+				break;
+			default:
+				printfPQExpBuffer(&conn->errorMessage,
+								  libpq_gettext("Channel binding required but not supported by server's authentication request\n"));
+				result = false;
+				break;
+		}
+	}
+
+	return result;
+}
+
 /*
  * pg_fe_sendauth
  *		client demux routine for processing an authentication request
@@ -788,6 +853,9 @@ pg_password_sendauth(PGconn *conn, const char *password, AuthRequest areq)
 int
 pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn)
 {
+	if (!check_expected_areq(areq, conn))
+		return STATUS_ERROR;
+
 	switch (areq)
 	{
 		case AUTH_REQ_OK:
@@ -968,6 +1036,10 @@ pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn)
 									  "fe_sendauth: error in SASL authentication\n");
 				return STATUS_ERROR;
 			}
+
+			if (areq == AUTH_REQ_SASL_FIN)
+				conn->sasl_finished = true;
+
 			break;
 
 		case AUTH_REQ_SCM_CREDS:
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 8ba0159313c..7113a47bd34 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -124,6 +124,11 @@ static int	ldapServiceLookup(const char *purl, PQconninfoOption *options,
 #define DefaultTty		""
 #define DefaultOption	""
 #define DefaultAuthtype		  ""
+#ifdef USE_SSL
+#define DefaultChannelBinding	"prefer"
+#else
+#define DefaultChannelBinding	"disable"
+#endif
 #define DefaultTargetSessionAttrs	"any"
 #ifdef USE_SSL
 #define DefaultSSLMode "prefer"
@@ -211,6 +216,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Database-Password-File", "", 64,
 	offsetof(struct pg_conn, pgpassfile)},
 
+	{"channel_binding", "PGCHANNELBINDING", NULL, NULL,
+	 "Channel-Binding", "", 7, /* sizeof("require") */
+	offsetof(struct pg_conn, channel_binding)},
+
 	{"connect_timeout", "PGCONNECT_TIMEOUT", NULL, NULL,
 		"Connect-timeout", "", 10,	/* strlen(INT32_MAX) == 10 */
 	offsetof(struct pg_conn, connect_timeout)},
@@ -500,6 +509,8 @@ pqDropConnection(PGconn *conn, bool flushInput)
 		pg_fe_scram_free(conn->sasl_state);
 		conn->sasl_state = NULL;
 	}
+	conn->sasl_finished = false;
+	conn->channel_bound = false;
 }
 
 
@@ -1197,6 +1208,29 @@ connectOptions2(PGconn *conn)
 		}
 	}
 
+	/*
+	 * validate channel_binding option
+	 */
+	if (conn->channel_binding)
+	{
+		if (strcmp(conn->channel_binding, "disable") != 0
+			&& strcmp(conn->channel_binding, "prefer") != 0
+			&& strcmp(conn->channel_binding, "require") != 0)
+		{
+			conn->status = CONNECTION_BAD;
+			printfPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("invalid channel_binding value: \"%s\"\n"),
+							  conn->channel_binding);
+			return false;
+		}
+	}
+	else
+	{
+		conn->channel_binding = strdup(DefaultChannelBinding);
+		if (!conn->channel_binding)
+			goto oom_error;
+	}
+
 	/*
 	 * validate sslmode option
 	 */
@@ -3905,6 +3939,8 @@ freePGconn(PGconn *conn)
 	}
 	if (conn->pgpassfile)
 		free(conn->pgpassfile);
+	if (conn->channel_binding)
+		free(conn->channel_binding);
 	if (conn->keepalives)
 		free(conn->keepalives);
 	if (conn->keepalives_idle)
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index d37bb3ce404..d6b7d3db013 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -347,6 +347,7 @@ struct pg_conn
 	char	   *pguser;			/* Postgres username and password, if any */
 	char	   *pgpass;
 	char	   *pgpassfile;		/* path to a file containing password(s) */
+	char	   *channel_binding;	/* channel binding mode (require,prefer) */
 	char	   *keepalives;		/* use TCP keepalives? */
 	char	   *keepalives_idle;	/* time between TCP keepalives */
 	char	   *keepalives_interval;	/* time between TCP keepalive
@@ -410,6 +411,7 @@ struct pg_conn
 	int			sversion;		/* server version, e.g. 70401 for 7.4.1 */
 	bool		auth_req_received;	/* true if any type of auth req received */
 	bool		password_needed;	/* true if server demanded a password */
+	bool		channel_bound;	/* true if channel_binding happened */
 	bool		sigpipe_so;		/* have we masked SIGPIPE via SO_NOSIGPIPE? */
 	bool		sigpipe_flag;	/* can we mask SIGPIPE via MSG_NOSIGNAL? */
 	bool		write_failed;	/* have we had a write failure on sock? */
@@ -462,6 +464,7 @@ struct pg_conn
 
 	/* Assorted state for SASL, SSL, GSS, etc */
 	void	   *sasl_state;
+	bool		sasl_finished;
 
 	/* SSL structures */
 	bool		ssl_in_use;
diff --git a/src/test/authentication/t/002_saslprep.pl b/src/test/authentication/t/002_saslprep.pl
index c4b335c45fe..37f6d19c3a1 100644
--- a/src/test/authentication/t/002_saslprep.pl
+++ b/src/test/authentication/t/002_saslprep.pl
@@ -14,7 +14,7 @@ if ($windows_os)
 }
 else
 {
-	plan tests => 12;
+	plan tests => 14;
 }
 
 # Delete pg_hba.conf from the given node, add a new entry to it
@@ -104,3 +104,13 @@ test_login($node, 'saslpreptest6_role', "foobar",     2);
 test_login($node, 'saslpreptest7_role', "foo\xd8\xa71bar", 0);
 test_login($node, 'saslpreptest7_role', "foo1\xd8\xa7bar", 2);
 test_login($node, 'saslpreptest7_role', "foobar",          2);
+
+# using the password authentication method; channel binding can't work
+reset_pg_hba($node, 'password');
+$ENV{"PGCHANNELBINDING"} = 'require';
+test_login($node, 'saslpreptest4a_role', "a", 2);
+
+# SSL not in use; channel binding still can't work
+reset_pg_hba($node, 'scram-sha-256');
+$ENV{"PGCHANNELBINDING"} = 'require';
+test_login($node, 'saslpreptest4a_role', "a", 2);
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 7c4b821cb78..af508e74f5c 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -18,7 +18,7 @@ if ($ENV{with_openssl} ne 'yes')
 	plan skip_all => 'SSL not supported by this build';
 }
 
-my $number_of_tests = 1;
+my $number_of_tests = 5;
 
 # This is the hostname used to connect to the server.
 my $SERVERHOSTADDR = '127.0.0.1';
@@ -42,11 +42,22 @@ $node->start;
 configure_test_server_for_ssl($node, $SERVERHOSTADDR, "scram-sha-256",
 	"pass", "scram-sha-256");
 switch_server_cert($node, 'server-cn-only');
+$ENV{PGUSER} = "ssltestuser";
 $ENV{PGPASSWORD} = "pass";
 $common_connstr =
-  "user=ssltestuser dbname=trustdb sslmode=require sslcert=invalid sslrootcert=invalid hostaddr=$SERVERHOSTADDR";
+  "dbname=trustdb sslmode=require sslcert=invalid sslrootcert=invalid hostaddr=$SERVERHOSTADDR";
 
 # Default settings
 test_connect_ok($common_connstr, '', "Basic SCRAM authentication with SSL");
 
+# Test channel_binding
+$ENV{PGCHANNELBINDING} = "disable";
+test_connect_ok($common_connstr, '', "SCRAM with SSL and channel_binding=disable");
+$ENV{PGCHANNELBINDING} = "require";
+test_connect_ok($common_connstr, '', "SCRAM with SSL and channel_binding=require");
+
+# Now test when the user has an MD5-encrypted password; should fail
+$ENV{PGUSER} = "md5testuser";
+test_connect_fails($common_connstr, '', qr/Channel binding required but not supported by server's authentication request/, "MD5 with SSL and channel_binding=require");
+
 done_testing($number_of_tests);
diff --git a/src/test/ssl/t/SSLServer.pm b/src/test/ssl/t/SSLServer.pm
index d25c38dbbc7..9288b428115 100644
--- a/src/test/ssl/t/SSLServer.pm
+++ b/src/test/ssl/t/SSLServer.pm
@@ -102,6 +102,7 @@ sub configure_test_server_for_ssl
 
 	# 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");
 	$node->psql('postgres', "CREATE DATABASE trustdb");
@@ -113,6 +114,10 @@ sub configure_test_server_for_ssl
 	{
 		$node->psql('postgres',
 			"SET password_encryption='$password_enc'; ALTER USER ssltestuser PASSWORD '$password';"
+		  );
+		# A special user that always has an md5-encrypted password
+		$node->psql('postgres',
+			"SET password_encryption='md5'; ALTER USER md5testuser PASSWORD '$password';"
 		);
 		$node->psql('postgres',
 			"SET password_encryption='$password_enc'; ALTER USER anotheruser PASSWORD '$password';"
@@ -128,7 +133,7 @@ sub configure_test_server_for_ssl
 	print $conf "log_statement=all\n";
 
 	# enable SSL and set up server key
-	print $conf "include 'sslconfig.conf'";
+	print $conf "include 'sslconfig.conf'\n";
 
 	close $conf;
 
@@ -186,6 +191,8 @@ sub configure_hba_for_ssl
 	open my $hba, '>', "$pgdata/pg_hba.conf";
 	print $hba
 	  "# TYPE  DATABASE        USER            ADDRESS                 METHOD             OPTIONS\n";
+	print $hba
+	  "hostssl trustdb         md5testuser     $serverhost/32            md5\n";
 	print $hba
 	  "hostssl trustdb         all             $serverhost/32            $authmethod\n";
 	print $hba
-- 
2.17.1

