From 9d82cbad4e1bf1c3e159df6e7c8972c8fa2313ae Mon Sep 17 00:00:00 2001
From: Jeff Davis <jdavis@postgresql.org>
Date: Wed, 7 Aug 2019 20:17:44 -0700
Subject: [PATCH] Add "password_protocol" connection parameter to libpq.

Sets the least-secure password protocol allowable when using password
authentication. Options are: "plaintext", "md5", "scram-sha-256", or
"scram-sha-256-plus".

Without setting this option, it's possible that the server will use a
less-secure authentication method than the client expects.
---
 doc/src/sgml/libpq.sgml           | 34 +++++++++++++++++++++++++
 src/interfaces/libpq/fe-auth.c    | 28 ++++++++++++++++++++-
 src/interfaces/libpq/fe-connect.c | 42 +++++++++++++++++++++++++++++++
 src/interfaces/libpq/libpq-int.h  |  2 ++
 4 files changed, 105 insertions(+), 1 deletion(-)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index e7295abda28..b337a781560 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1118,6 +1118,40 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-password-protocol" xreflabel="password_protocol">
+      <term><literal>password_protocol</literal></term>
+      <listitem>
+      <para>
+       Specifies the least-secure password protocol allowable when
+       authenticating with a password:
+       <literal>plaintext</literal>, <literal>md5</literal>,
+       <literal>scram-sha-256</literal>, or
+       <literal>scram-sha-256-plus</literal>. The default
+       is <literal>plaintext</literal>, meaning that any password protocol is
+       acceptable.
+      </para>
+      <para>
+        Note that this setting is unrelated to the use of SSL. Use of the
+        <literal>plaintext</literal> password protocol over SSL will be
+        encrypted over the network, but the server will have access to the
+        plaintext password.
+      </para>
+      <para>
+        The <literal>scram-sha-256-plus</literal> password protocol uses
+        channel binding, supported when communicating
+        with <productname>PostgreSQL</productname> 11.0 or later
+        servers. Channel binding additionally requires an SSL connection.
+      </para>
+      <para>
+        The <literal>plaintext</literal> password protocol must be used when
+        the server is using one of the following authentication
+        methods: <literal>password</literal>,
+        <literal>ldap</literal>, <literal>radius</literal>,
+        <literal>pam</literal>, or <literal>bsd</literal>.
+      </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..f9b23b457ca 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -493,6 +493,16 @@ pg_SASL_init(PGconn *conn, int payloadlen)
 			selected_mechanism = SCRAM_SHA_256_NAME;
 	}
 
+	if (selected_mechanism &&
+		strcmp(selected_mechanism, SCRAM_SHA_256_NAME) == 0 &&
+		strcmp(conn->password_protocol, "scram-sha-256-plus") == 0)
+	{
+		printfPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext(
+							  "server doesn't support SCRAM_SHA_256_PLUS, but it is required\n"));
+		goto error;
+	}
+
 	if (!selected_mechanism)
 	{
 		printfPQExpBuffer(&conn->errorMessage,
@@ -914,11 +924,27 @@ pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn)
 							  libpq_gettext("Crypt authentication not supported\n"));
 			return STATUS_ERROR;
 
-		case AUTH_REQ_MD5:
 		case AUTH_REQ_PASSWORD:
+				if (conn->password_protocol[0] == 'm')
+				{
+					printfPQExpBuffer(&conn->errorMessage,
+									  "server not configured for MD5, but it was required\n");
+					return STATUS_ERROR;
+				}
+				/* FALL THROUGH */
+
+		case AUTH_REQ_MD5:
 			{
 				char	   *password;
 
+				if (conn->password_protocol[0] == 's')
+				{
+					printfPQExpBuffer(&conn->errorMessage,
+									  "server not configured for %s, but it was required\n",
+									  SCRAM_SHA_256_NAME);
+					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 d262b57021d..b42f08ebbdd 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -123,6 +123,7 @@ static int	ldapServiceLookup(const char *purl, PQconninfoOption *options,
 #define DefaultTty		""
 #define DefaultOption	""
 #define DefaultAuthtype		  ""
+#define DefaultPasswordProtocol "plaintext"
 #define DefaultTargetSessionAttrs	"any"
 #ifdef USE_SSL
 #define DefaultSSLMode "prefer"
@@ -210,6 +211,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Database-Password-File", "", 64,
 	offsetof(struct pg_conn, pgpassfile)},
 
+	{"password_protocol", "PGPASSWORDPROTOCOL", DefaultPasswordProtocol, NULL,
+		"Password-Protocol", "", 18,	/* sizeof("scram-sha-256-plus") == 18 */
+	offsetof(struct pg_conn, password_protocol)},
+
 	{"connect_timeout", "PGCONNECT_TIMEOUT", NULL, NULL,
 		"Connect-timeout", "", 10,	/* strlen(INT32_MAX) == 10 */
 	offsetof(struct pg_conn, connect_timeout)},
@@ -1196,6 +1201,30 @@ connectOptions2(PGconn *conn)
 		}
 	}
 
+	/*
+	 * validate passwordmode option
+	 */
+	if (conn->password_protocol)
+	{
+		if (strcmp(conn->password_protocol, "plaintext") != 0 &&
+			strcmp(conn->password_protocol, "md5") != 0 &&
+			strcmp(conn->password_protocol, "scram-sha-256") != 0 &&
+			strcmp(conn->password_protocol, "scram-sha-256-plus") != 0)
+		{
+			conn->status = CONNECTION_BAD;
+			printfPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("invalid password_protocol value: \"%s\"\n"),
+							  conn->password_protocol);
+			return false;
+		}
+	}
+	else
+	{
+		conn->password_protocol = strdup(DefaultPasswordProtocol);
+		if (!conn->password_protocol)
+			goto oom_error;
+	}
+
 	/*
 	 * validate sslmode option
 	 */
@@ -1215,6 +1244,19 @@ connectOptions2(PGconn *conn)
 			return false;
 		}
 
+		/* scram-sha-256-plus only works over SSL */
+		if (strcmp(conn->password_protocol, "scram-sha-256-plus") == 0 &&
+			(conn->sslmode[0] == 'd' ||
+			 conn->sslmode[0] == 'a' ||
+			 conn->sslmode[0] == 'p'))
+		{
+				conn->status = CONNECTION_BAD;
+				printfPQExpBuffer(&conn->errorMessage,
+								  libpq_gettext("sslmode value \"%s\" invalid when password mode is scram-sha-256-plus\n"),
+								  conn->sslmode);
+				return false;
+		}
+
 #ifndef USE_SSL
 		switch (conn->sslmode[0])
 		{
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index d37bb3ce404..005ff1e676c 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -347,6 +347,8 @@ struct pg_conn
 	char	   *pguser;			/* Postgres username and password, if any */
 	char	   *pgpass;
 	char	   *pgpassfile;		/* path to a file containing password(s) */
+	char	   *password_protocol;	/* password mode (plaintext, md5,
+									   scram-sha-256, scram-sha-256-plus) */
 	char	   *keepalives;		/* use TCP keepalives? */
 	char	   *keepalives_idle;	/* time between TCP keepalives */
 	char	   *keepalives_interval;	/* time between TCP keepalive
-- 
2.17.1

