On Mon, 2019-08-19 at 14:51 +0900, Michael Paquier wrote:
> On Fri, Aug 16, 2019 at 02:11:57PM -0400, Jonathan S. Katz wrote:
> > To be pedantic, +1 on the channel_binding param.
> 
> Seems like we are moving in this direction then.  I don't object to
> the introduction of this parameter.

OK, new patch attached. Seems like everyone is in agreement that we
need a channel_binding param.

Regards,
        Jeff Davis

From 0eb8e76d08a64979c3070761efab95d61c4ff887 Mon Sep 17 00:00:00 2001
From: Jeff Davis <j...@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           | 19 +++++++++++++++++++
 src/interfaces/libpq/fe-auth.c    | 30 +++++++++++++++++++++++++++++-
 src/interfaces/libpq/fe-connect.c | 28 ++++++++++++++++++++++++++++
 src/interfaces/libpq/libpq-int.h  |  2 ++
 4 files changed, 78 insertions(+), 1 deletion(-)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index e7295abda28..1785cb1bcc5 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1118,6 +1118,25 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-channel-binding" xreflabel="channel_binding">
+      <term><literal>channel_binding</literal></term>
+      <listitem>
+      <para>
+        A setting of <literal>require</literal> means that the connection must
+        employ channel binding; and that the client will not respond to
+        requests from the server for cleartext password or MD5
+        authentication. The default setting is <literal>prefer</literal>,
+        which employs channel binding if available.
+      </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.0 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.c b/src/interfaces/libpq/fe-auth.c
index ab227421b3b..5df30913337 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -423,6 +423,13 @@ pg_SASL_init(PGconn *conn, int payloadlen)
 
 	initPQExpBuffer(&mechanism_buf);
 
+	if (conn->channel_binding[0] == 'r' && !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,
@@ -488,7 +495,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' &&
+				 strcmp(mechanism_buf.data, SCRAM_SHA_256_NAME) == 0 &&
 				 !selected_mechanism)
 			selected_mechanism = SCRAM_SHA_256_NAME;
 	}
@@ -565,6 +573,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:
@@ -791,6 +802,12 @@ pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn)
 	switch (areq)
 	{
 		case AUTH_REQ_OK:
+			if (conn->channel_binding[0] == 'r' && !conn->channel_bound)
+			{
+				printfPQExpBuffer(&conn->errorMessage,
+								  libpq_gettext("Channel binding required but not offered by server\n"));
+				return STATUS_ERROR;
+			}
 			break;
 
 		case AUTH_REQ_KRB4:
@@ -919,6 +936,17 @@ pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn)
 			{
 				char	   *password;
 
+				if (conn->channel_binding[0] == 'r')
+				{
+					if (areq == AUTH_REQ_PASSWORD)
+						printfPQExpBuffer(&conn->errorMessage,
+										  libpq_gettext("Channel binding required but server requested cleartext password authentication\n"));
+					else
+						printfPQExpBuffer(&conn->errorMessage,
+										  libpq_gettext("Channel binding required but server requested MD5 authentication\n"));
+					return STATUS_ERROR;
+				}
+
 				conn->password_needed = true;
 				password = conn->connhost[conn->whichhost].password;
 				if (password == NULL)
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index fa5af18ffac..e9627c06ec1 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -124,6 +124,7 @@ static int	ldapServiceLookup(const char *purl, PQconninfoOption *options,
 #define DefaultTty		""
 #define DefaultOption	""
 #define DefaultAuthtype		  ""
+#define DefaultChannelBinding	"prefer"
 #define DefaultTargetSessionAttrs	"any"
 #ifdef USE_SSL
 #define DefaultSSLMode "prefer"
@@ -211,6 +212,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 +561,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 +1203,28 @@ connectOptions2(PGconn *conn)
 		}
 	}
 
+	/*
+	 * validate channel_binding option
+	 */
+	if (conn->channel_binding)
+	{
+		if (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? */
-- 
2.17.1

Reply via email to