Libpq doesn't have a way to control which password protocols are used.
For example, the client might expect the server to be using SCRAM, but
it actually ends up using plain password authentication instead.

This patch adds:

  password_protocol = {plaintext|md5|scram-sha-256|scram-sha-256-plus}

as a connection parameter. Libpq will then reject any authentication
request from the server that is less secure than this setting. Setting
it to "plaintext" (default) will answer to any kind of authentication
request.

I'm not 100% happy with the name "password_protocol", but other names I
could think of seemed likely to cause confusion.

Regards,
        Jeff Davis

From 9d82cbad4e1bf1c3e159df6e7c8972c8fa2313ae Mon Sep 17 00:00:00 2001
From: Jeff Davis <jda...@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

Reply via email to