From f3743a9a7c59b628249986d8b7252dfb13e1952d 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                   | 22 +++++++
 src/interfaces/libpq/fe-auth-scram.c      |  6 +-
 src/interfaces/libpq/fe-auth.c            | 71 +++++++++++++++++++++--
 src/interfaces/libpq/fe-connect.c         | 33 +++++++++++
 src/interfaces/libpq/libpq-int.h          |  2 +
 src/test/authentication/t/002_saslprep.pl | 12 +++-
 src/test/ssl/t/002_scram.pl               |  8 ++-
 7 files changed, 144 insertions(+), 10 deletions(-)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index b7c3d96b01f..bc03a171f53 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1118,6 +1118,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-sha-256</literal> authentication method.
+      </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-connect-timeout" xreflabel="connect_timeout">
       <term><literal>connect_timeout</literal></term>
       <listitem>
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index 7a8335bf9f8..1232aa8c6d2 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -358,7 +358,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 +369,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 +498,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..dd883b6d8e9 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,11 +463,12 @@ 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)
+		if (conn->channel_binding[0] != 'd' &&
+			strcmp(mechanism_buf.data, SCRAM_SHA_256_PLUS_NAME) == 0)
 		{
 			if (conn->ssl_in_use)
 			{
@@ -488,7 +497,8 @@ pg_SASL_init(PGconn *conn, int payloadlen)
 				goto error;
 			}
 		}
-		else if (strcmp(mechanism_buf.data, SCRAM_SHA_256_NAME) == 0 &&
+		else if (conn->channel_binding[0] != 'r' && /* require */
+				 strcmp(mechanism_buf.data, SCRAM_SHA_256_NAME) == 0 &&
 				 !selected_mechanism)
 			selected_mechanism = SCRAM_SHA_256_NAME;
 	}
@@ -565,6 +575,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 +787,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)
+				{
+					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 +846,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:
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 7f1fd2f45eb..3f33dcdbfd6 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)},
@@ -556,6 +565,7 @@ pqDropServerData(PGconn *conn)
 	conn->last_sqlstate[0] = '\0';
 	conn->auth_req_received = false;
 	conn->password_needed = false;
+	conn->channel_bound = false;
 	conn->write_failed = false;
 	if (conn->write_err_msg)
 		free(conn->write_err_msg);
@@ -1197,6 +1207,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
 	 */
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index d37bb3ce404..70a5b445d72 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? */
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..2b834c11ed4 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 = 3;
 
 # This is the hostname used to connect to the server.
 my $SERVERHOSTADDR = '127.0.0.1';
@@ -49,4 +49,10 @@ $common_connstr =
 # 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");
+
 done_testing($number_of_tests);
-- 
2.17.1

