[PoC] Let libpq reject unexpected authentication requests

Started by Jacob Championalmost 4 years ago64 messages
#1Jacob Champion
pchampion@vmware.com
1 attachment(s)

Hello,

TL;DR: this patch lets you specify exactly one authentication method in
the connection string, and libpq will fail the connection if the server
doesn't use that method.

(This is not intended for PG15. I'm generally anxious about posting
experimental work during a commitfest, but there's been enough
conversation about this topic recently that I felt like it'd be useful
to have code to point to.)

== Proposal and Alternatives ==

$subject keeps coming up in threads. I think my first introduction to
it was after the TLS injection CVE, and then it came up again in the
pluggable auth thread. It's hard for me to generalize based on "sound
bites", but among the proposals I've seen are

1. reject plaintext passwords
2. reject a configurable list of unacceptable methods
3. allow client and server to negotiate a method

All of them seem to have merit. I'm personally motivated by the case
brought up by the CVE: if I'm expecting client certificate
authentication, it's not acceptable for the server to extract _any_
information about passwords from my system, whether they're plaintext,
hashed, or SCRAM-protected. So I chose not to implement option 1. And
option 3 looked like a lot of work to take on in an experiment without
a clear consensus.

Here is my take on option 2, then: you get to choose exactly one method
that the client will accept. If you want to use client certificates,
use require_auth=cert. If you want to force SCRAM, use
require_auth=scram-sha-256. If the server asks for something different,
libpq will fail. If the server tries to get away without asking you for
authentication, libpq will fail. There is no negotiation.

== Why Force Authn? ==

I think my decision to fail if the server doesn't authenticate might be
controversial. It doesn't provide additional protection against active
attack unless you're using a mutual authentication method (SCRAM),
because you can't prove that the server actually did anything with its
side of the handshake. But this approach grew on me for a few reasons:

- When using SCRAM, it allows the client to force a server to
authenticate itself, even when channel bindings aren't being used. (I
really think it's weird that we let the server get away with that
today.)

- In cases where you want to ensure that your actions are logged for
later audit, you can be reasonably sure that you're connecting to a
database that hasn't been configured with a `trust` setting.

- For cert authentication, it ensures that the server asked for a cert
and that you actually sent one. This is more forward-looking; today, we
always ask for a certificate from the client even if we don't use it.
But if implicit TLS takes off, I'd expect to see more middleware, with
more potential for misconfiguration.

== General Thoughts ==

I like that this approach fits nicely into the existing code. The
majority of the patch just beefs up check_expected_areq(). The new flag
that tracks whether or not we've authenticated is scattered around more
than I would like, but I'm hopeful that some of the pluggable auth
conversations will lead to nice refactoring opportunities for those
internal helpers.

There's currently no way to prohibit client certificates from being
sent. If my use case is "servers shouldn't be able to extract password
info if the client expects certificates", then someone else may very
well say "servers shouldn't be able to extract my client certificate if
I wanted to use SCRAM". Likewise, this feature won't prevent a GSS
authenticated channel -- but we do have gssencmode=disable, so I'm less
concerned there.

I made the assumption that a GSS encrypted channel authenticates both
parties to each other, but I don't actually know what guarantees are
made there. I have not tested SSPI.

I'm not a fan of the multiple spellings of "password" ("ldap", "pam",
et al). My initial thought was that it'd be nice to match the client
setting to the HBA setting in the server. But I don't think it's really
all that helpful; worst-case, it pretends to do something it can't,
since the client can't determine what the plaintext password is
actually used for on the backend. And if someone devises (say) a SASL
scheme for proxied LDAP authentication, that spelling becomes obsolete.

Speaking of obsolete, the current implementation assumes that any SASL
exchange must be for SCRAM. That won't be anywhere close to future-
proof.

Thanks,
--Jacob

Attachments:

0001-libpq-let-client-reject-unexpected-auth-methods.patchtext/x-patch; name=0001-libpq-let-client-reject-unexpected-auth-methods.patchDownload
From 545a89aafacd0f997cd3e14cd20b192335eafadc Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Mon, 28 Feb 2022 09:40:43 -0800
Subject: [PATCH] libpq: let client reject unexpected auth methods

The require_auth connection option allows the client to choose one (and
only one) authentication type for use with the server. There is no
negotiation: if the server does not present the expected authentication
request, the connection fails.

Internally, the patch expands the role of check_expected_areq() to
ensure that the incoming request matches conn->require_auth. It also
introduces a new flag, conn->client_finished_auth, which is set by
various authentication routines when the client side of the handshake is
finished. This signals to check_expected_areq() that an OK message from
the server is expected, and allows the client to complain if the server
forgoes authentication entirely.

(Since the client can't generally prove that the server is actually
doing the work of authentication, this last part is mostly useful for
SCRAM without channel binding. But it could be used to diagnose
configuration problems with client certificate authentication. It could
also provide a client with a decent signal that, at the very least, it's
not connecting to a database with trust auth, and so the connection can
be tied to the client in a later audit.)

Certificate authentication poses an additional complication.
conn->ssl_cert_requested and conn->ssl_cert_sent have been added so that
check_expected_areq() can ensure that we sent a certificate during the
TLS handshake.

Deficiencies:
- This feature currently doesn't prevent unexpected certificate exchange
  or unexpected GSSAPI encryption.
- require_auth isn't validated against the supported list of methods.
- It's unclear whether allowing various spellings of "password" (like
  "ldap", "pam", etc.) is actually helpful.
- This is unlikely to be very forwards-compatible at the moment,
  especially with SASL/SCRAM.
- SSPI support is "implemented" but untested.
---
 src/interfaces/libpq/fe-auth-scram.c      |   1 +
 src/interfaces/libpq/fe-auth.c            | 146 ++++++++++++++++++++++
 src/interfaces/libpq/fe-connect.c         |   4 +
 src/interfaces/libpq/fe-secure-openssl.c  |  28 +++++
 src/interfaces/libpq/libpq-int.h          |   6 +
 src/test/authentication/t/001_password.pl |  59 +++++++++
 src/test/kerberos/t/001_auth.pl           |  18 +++
 src/test/ldap/t/001_auth.pl               |   9 ++
 src/test/ssl/t/001_ssltests.pl            |  11 ++
 9 files changed, 282 insertions(+)

diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index e616200704..d918e8b87f 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -289,6 +289,7 @@ scram_exchange(void *opaq, char *input, int inputlen,
 			}
 			*done = true;
 			state->state = FE_SCRAM_FINISHED;
+			state->conn->client_finished_auth = true;
 			break;
 
 		default:
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 6fceff561b..27ce4b8898 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -138,7 +138,10 @@ pg_GSS_continue(PGconn *conn, int payloadlen)
 	}
 
 	if (maj_stat == GSS_S_COMPLETE)
+	{
+		conn->client_finished_auth = true;
 		gss_release_name(&lmin_s, &conn->gtarg_nam);
+	}
 
 	return STATUS_OK;
 }
@@ -328,6 +331,9 @@ pg_SSPI_continue(PGconn *conn, int payloadlen)
 		FreeContextBuffer(outbuf.pBuffers[0].pvBuffer);
 	}
 
+	if (r == SEC_E_OK)
+		conn->client_finished_auth = true;
+
 	/* Cleanup is handled by the code in freePGconn() */
 	return STATUS_OK;
 }
@@ -836,6 +842,34 @@ pg_password_sendauth(PGconn *conn, const char *password, AuthRequest areq)
 	return ret;
 }
 
+/*
+ * Translate an AuthRequest into a human-readable description.
+ */
+static const char *
+auth_description(AuthRequest areq)
+{
+	switch (areq)
+	{
+		case AUTH_REQ_PASSWORD:
+			return libpq_gettext("a cleartext password");
+		case AUTH_REQ_MD5:
+			return libpq_gettext("a hashed password");
+		case AUTH_REQ_GSS:
+		case AUTH_REQ_GSS_CONT:
+			return libpq_gettext("GSSAPI authentication");
+		case AUTH_REQ_SSPI:
+			return libpq_gettext("SSPI authentication");
+		case AUTH_REQ_SCM_CREDS:
+			return libpq_gettext("UNIX socket credentials");
+		case AUTH_REQ_SASL:
+		case AUTH_REQ_SASL_CONT:
+		case AUTH_REQ_SASL_FIN:
+			return libpq_gettext("SASL authentication");
+	}
+
+	return libpq_gettext("an unknown authentication type");
+}
+
 /*
  * Verify that the authentication request is expected, given the connection
  * parameters. This is especially important when the client wishes to
@@ -845,6 +879,115 @@ static bool
 check_expected_areq(AuthRequest areq, PGconn *conn)
 {
 	bool		result = true;
+	char	   *reason = NULL;
+
+	/* If the user required a specific auth method, reject all others. */
+	if (conn->require_auth)
+	{
+		switch (areq)
+		{
+			case AUTH_REQ_OK:
+				/*
+				 * Check to make sure we've actually finished our exchange.
+				 */
+				if (strcmp(conn->require_auth, "cert") == 0)
+				{
+					if (!conn->ssl_cert_requested)
+					{
+						reason = libpq_gettext("server did not request a certificate");
+						result = false;
+					}
+					else if (!conn->ssl_cert_sent)
+					{
+						reason = libpq_gettext("server accepted connection without a valid certificate");
+						result = false;
+					}
+				}
+				else if (strcmp(conn->require_auth, "gss") == 0
+						 && conn->gssenc)
+				{
+					/*
+					 * Special case: if implicit GSS auth has already been
+					 * performed via GSS encryption, we don't need to have
+					 * performed an AUTH_REQ_GSS exchange.
+					 *
+					 * TODO: check this assumption. What mutual auth guarantees
+					 * are made in this case?
+					 */
+				}
+				else if (!conn->client_finished_auth)
+				{
+					reason = libpq_gettext("server did not complete authentication"),
+					result = false;
+				}
+				break;
+
+			case AUTH_REQ_PASSWORD:
+				if (strcmp(conn->require_auth, "bsd")
+					&& strcmp(conn->require_auth, "ldap")
+					&& strcmp(conn->require_auth, "pam")
+					&& strcmp(conn->require_auth, "password")
+					&& strcmp(conn->require_auth, "radius"))
+					result = false;
+				break;
+
+			case AUTH_REQ_MD5:
+				if (strcmp(conn->require_auth, "md5"))
+					result = false;
+				break;
+
+			case AUTH_REQ_GSS:
+				if (strcmp(conn->require_auth, "gss"))
+					result = false;
+				break;
+
+			case AUTH_REQ_SSPI:
+				if (strcmp(conn->require_auth, "sspi"))
+					result = false;
+				break;
+
+			case AUTH_REQ_GSS_CONT:
+				if (strcmp(conn->require_auth, "gss") &&
+					strcmp(conn->require_auth, "sspi"))
+					result = false;
+				break;
+
+			case AUTH_REQ_SASL:
+			case AUTH_REQ_SASL_CONT:
+			case AUTH_REQ_SASL_FIN:
+				/* This currently assumes that SCRAM is the only SASL method. */
+				if (strcmp(conn->require_auth, "scram-sha-256"))
+					result = false;
+				break;
+
+			default:
+				result = false;
+				break;
+		}
+	}
+
+	if (!result)
+	{
+		if (reason)
+		{
+			/*
+			 * XXX double call to libpq_gettext() is probably not easy to
+			 * translate from English
+			 */
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("auth method \"%s\" required, but %s\n"),
+							  conn->require_auth, reason);
+		}
+		else
+		{
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("auth method \"%s\" required, but server requested %s\n"),
+							  conn->require_auth,
+							  auth_description(areq));
+		}
+
+		return result;
+	}
 
 	/*
 	 * When channel_binding=require, we must protect against two cases: (1) we
@@ -1046,6 +1189,9 @@ pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn)
 										 "fe_sendauth: error sending password authentication\n");
 					return STATUS_ERROR;
 				}
+
+				/* We expect no further authentication requests. */
+				conn->client_finished_auth = true;
 				break;
 			}
 
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 1c5a2b43e9..9982ce6e4e 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -310,6 +310,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Require-Peer", "", 10,
 	offsetof(struct pg_conn, requirepeer)},
 
+	{"require_auth", NULL, NULL, NULL,
+		"Require-Auth", "", 14, /* sizeof("scram-sha-256") == 14 */
+	offsetof(struct pg_conn, require_auth)},
+
 	{"ssl_min_protocol_version", "PGSSLMINPROTOCOLVERSION", "TLSv1.2", NULL,
 		"SSL-Minimum-Protocol-Version", "", 8,	/* sizeof("TLSv1.x") == 8 */
 	offsetof(struct pg_conn, ssl_min_protocol_version)},
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index c6a80d30c2..0b8e500afb 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -477,6 +477,31 @@ verify_cb(int ok, X509_STORE_CTX *ctx)
 	return ok;
 }
 
+/*
+ * Certificate selection callback
+ *
+ * This callback lets us choose the client certificate we send to the server
+ * after seeing its CertificateRequest. We only support sending a single
+ * hard-coded certificate via sslcert, so we don't actually set any certificates
+ * here; we just it to record whether or not the server has actually asked for
+ * one and whether we have one to send.
+ */
+static int
+cert_cb(SSL *ssl, void *arg)
+{
+	PGconn *conn = arg;
+	conn->ssl_cert_requested = true;
+
+	/* Do we have a certificate loaded to send back? */
+	if (SSL_get_certificate(ssl))
+		conn->ssl_cert_sent = true;
+
+	/*
+	 * Tell OpenSSL that the callback succeeded; we're not required to actually
+	 * make any changes to the SSL handle.
+	 */
+	return 1;
+}
 
 /*
  * OpenSSL-specific wrapper around
@@ -960,6 +985,9 @@ initialize_SSL(PGconn *conn)
 		SSL_CTX_set_default_passwd_cb_userdata(SSL_context, conn);
 	}
 
+	/* Set up a certificate selection callback. */
+	SSL_CTX_set_cert_cb(SSL_context, cert_cb, conn);
+
 	/* Disable old protocol versions */
 	SSL_CTX_set_options(SSL_context, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
 
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index e0cee4b142..f6dc21cc24 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -393,6 +393,7 @@ struct pg_conn
 	char	   *ssl_min_protocol_version;	/* minimum TLS protocol version */
 	char	   *ssl_max_protocol_version;	/* maximum TLS protocol version */
 	char	   *target_session_attrs;	/* desired session properties */
+	char	   *require_auth;	/* name of the expected auth method */
 
 	/* Optional file to write trace info to */
 	FILE	   *Pfdebug;
@@ -509,12 +510,17 @@ struct pg_conn
 	bool		error_result;	/* do we need to make an ERROR result? */
 	PGresult   *next_result;	/* next result (used in single-row mode) */
 
+	bool		client_finished_auth; /* have we finished our half of the
+									   * authentication exchange? */
+
 	/* Assorted state for SASL, SSL, GSS, etc */
 	const pg_fe_sasl_mech *sasl;
 	void	   *sasl_state;
 
 	/* SSL structures */
 	bool		ssl_in_use;
+	bool		ssl_cert_requested;	/* Did the server ask us for a cert? */
+	bool		ssl_cert_sent;		/* Did we send one in reply? */
 
 #ifdef USE_SSL
 	bool		allow_ssl_try;	/* Allowed to try SSL negotiation */
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 3e3079c824..c7062ca357 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -82,6 +82,29 @@ test_role($node, 'scram_role', 'trust', 0,
 test_role($node, 'md5_role', 'trust', 0,
 	log_unlike => [qr/connection authenticated:/]);
 
+# All require_auth options should fail.
+$node->connect_fails("user=scram_role require_auth=cert",
+	"certificate authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not request a certificate/);
+$node->connect_fails("user=scram_role require_auth=gss",
+	"GSS authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=sspi",
+	"GSS authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=password",
+	"password authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=ldap",
+	"LDAP authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'password');
 test_role($node, 'scram_role', 'password', 0,
@@ -91,6 +114,18 @@ test_role($node, 'md5_role', 'password', 0,
 	log_like =>
 	  [qr/connection authenticated: identity="md5_role" method=password/]);
 
+# require_auth should succeed with a plaintext password...
+$node->connect_ok("user=scram_role require_auth=password",
+	"password authentication can be required: works with password auth");
+
+# ...and fail for other auth types.
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+
 # For "scram-sha-256" method, user "scram_role" should be able to connect.
 reset_pg_hba($node, 'scram-sha-256');
 test_role(
@@ -104,6 +139,18 @@ test_role(
 test_role($node, 'md5_role', 'scram-sha-256', 2,
 	log_unlike => [qr/connection authenticated:/]);
 
+# require_auth should succeed with SCRAM...
+$node->connect_ok("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: works with SCRAM auth");
+
+# ...and fail for other auth types.
+$node->connect_fails("user=scram_role require_auth=password",
+	"password authentication can be required: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+
 # Test that bad passwords are rejected.
 $ENV{"PGPASSWORD"} = 'badpass';
 test_role($node, 'scram_role', 'scram-sha-256', 2,
@@ -120,6 +167,18 @@ test_role($node, 'md5_role', 'md5', 0,
 	log_like =>
 	  [qr/connection authenticated: identity="md5_role" method=md5/]);
 
+# require_auth should succeed with MD5...
+$node->connect_ok("user=md5_role require_auth=md5",
+	"MD5 authentication can be required: works with MD5 auth");
+
+# ...and fail for other auth types.
+$node->connect_fails("user=md5_role require_auth=password",
+	"password authentication can be required: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+$node->connect_fails("user=md5_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+
 # Tests for channel binding without SSL.
 # Using the password authentication method; channel binding can't work
 reset_pg_hba($node, 'password');
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index 62e0542639..84e94e8e81 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -307,6 +307,24 @@ test_query(
 	'gssencmode=require',
 	'sending 100K lines works');
 
+# require_auth=gss should succeed...
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=disable require_auth=gss",
+	"GSS authentication can be requested: works with GSS auth without encryption");
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss",
+	"GSS authentication can be requested: works with GSS auth with encryption");
+
+# ...and require_auth=sspi should fail.
+$node->connect_fails(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=disable require_auth=sspi",
+	"SSPI authentication can be requested: fails with GSS auth without encryption",
+	expected_stderr => qr/server requested GSSAPI authentication/);
+$node->connect_fails(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=sspi",
+	"SSPI authentication can be requested: fails with GSS auth with encryption",
+	expected_stderr => qr/server did not complete authentication/);
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
 	qq{hostgssenc all all $hostaddr/32 gss map=mymap});
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 094270cb5d..8197dad87b 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -206,6 +206,15 @@ test_access(
 		qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/
 	],);
 
+# require_auth=ldap (and other plaintext password methods) should complete
+# successfully; other methods should fail.
+$node->connect_ok("user=test1 require_auth=ldap",
+	"LDAP authentication can be required: works with ldap auth");
+$node->connect_ok("user=test1 require_auth=password",
+	"password authentication can be required: works with ldap auth");
+$node->connect_fails("user=test1 require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with ldap auth");
+
 note "search+bind";
 
 unlink($node->data_dir . '/pg_hba.conf');
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 6c73c0f9ea..1c6ff81c41 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -491,6 +491,17 @@ note "running server tests";
 $common_connstr =
   "sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=certdb hostaddr=$SERVERHOSTADDR host=localhost";
 
+# require_auth=cert should succeed against the certdb...
+$node->connect_ok(
+	"$common_connstr require_auth=cert user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client.key'}",
+	"certificate authentication can be required: works with cert auth");
+
+# ...and fail against the trustdb, if no certificate is provided.
+$node->connect_fails(
+	"$common_connstr require_auth=cert dbname=trustdb user=ssltestuser",
+	"certificate authentication can be required: fails with trust auth and no cert",
+	expected_stderr => qr/server accepted connection without a valid certificate/);
+
 # no client cert
 $node->connect_fails(
 	"$common_connstr user=ssltestuser sslcert=invalid",
-- 
2.25.1

#2Tom Lane
tgl@sss.pgh.pa.us
In reply to: Jacob Champion (#1)
Re: [PoC] Let libpq reject unexpected authentication requests

Jacob Champion <pchampion@vmware.com> writes:

$subject keeps coming up in threads. I think my first introduction to
it was after the TLS injection CVE, and then it came up again in the
pluggable auth thread. It's hard for me to generalize based on "sound
bites", but among the proposals I've seen are

1. reject plaintext passwords
2. reject a configurable list of unacceptable methods
3. allow client and server to negotiate a method

All of them seem to have merit.

Agreed.

Here is my take on option 2, then: you get to choose exactly one method
that the client will accept. If you want to use client certificates,
use require_auth=cert. If you want to force SCRAM, use
require_auth=scram-sha-256. If the server asks for something different,
libpq will fail. If the server tries to get away without asking you for
authentication, libpq will fail. There is no negotiation.

Seems reasonable, but I bet that for very little more code you could
accept a comma-separated list of allowed methods; libpq already allows
comma-separated lists for some other connection options. That seems
like it'd be a useful increment of flexibility.

regards, tom lane

#3Michael Paquier
michael@paquier.xyz
In reply to: Tom Lane (#2)
Re: [PoC] Let libpq reject unexpected authentication requests

On Fri, Mar 04, 2022 at 08:19:26PM -0500, Tom Lane wrote:

Jacob Champion <pchampion@vmware.com> writes:

Here is my take on option 2, then: you get to choose exactly one method
that the client will accept. If you want to use client certificates,
use require_auth=cert. If you want to force SCRAM, use
require_auth=scram-sha-256. If the server asks for something different,
libpq will fail. If the server tries to get away without asking you for
authentication, libpq will fail. There is no negotiation.

Fine by me to put all the control on the client-side, that makes the
whole much simpler to reason about.

Seems reasonable, but I bet that for very little more code you could
accept a comma-separated list of allowed methods; libpq already allows
comma-separated lists for some other connection options. That seems
like it'd be a useful increment of flexibility.

Same impression here, so +1 for supporting a comma-separated list of
values here. This is already handled in parse_comma_separated_list(),
now used for multiple hosts and hostaddrs.
--
Michael

#4Andrew Dunstan
andrew@dunslane.net
In reply to: Tom Lane (#2)
Re: [PoC] Let libpq reject unexpected authentication requests

On 3/4/22 20:19, Tom Lane wrote:

Jacob Champion <pchampion@vmware.com> writes:

$subject keeps coming up in threads. I think my first introduction to
it was after the TLS injection CVE, and then it came up again in the
pluggable auth thread. It's hard for me to generalize based on "sound
bites", but among the proposals I've seen are
1. reject plaintext passwords
2. reject a configurable list of unacceptable methods
3. allow client and server to negotiate a method
All of them seem to have merit.

Agreed.

Here is my take on option 2, then: you get to choose exactly one method
that the client will accept. If you want to use client certificates,
use require_auth=cert. If you want to force SCRAM, use
require_auth=scram-sha-256. If the server asks for something different,
libpq will fail. If the server tries to get away without asking you for
authentication, libpq will fail. There is no negotiation.

Seems reasonable, but I bet that for very little more code you could
accept a comma-separated list of allowed methods; libpq already allows
comma-separated lists for some other connection options. That seems
like it'd be a useful increment of flexibility.

Just about necessary I guess, since you can specify that a client cert
is required in addition to some other auth method, so for such cases you
might want something like "required_auth=cert,scram-sha-256"? Or do we
need a way of specifying the combination?

cheers

andrew

--
Andrew Dunstan
EDB: https://www.enterprisedb.com

#5Tom Lane
tgl@sss.pgh.pa.us
In reply to: Andrew Dunstan (#4)
Re: [PoC] Let libpq reject unexpected authentication requests

Andrew Dunstan <andrew@dunslane.net> writes:

On 3/4/22 20:19, Tom Lane wrote:

Seems reasonable, but I bet that for very little more code you could
accept a comma-separated list of allowed methods; libpq already allows
comma-separated lists for some other connection options. That seems
like it'd be a useful increment of flexibility.

Just about necessary I guess, since you can specify that a client cert
is required in addition to some other auth method, so for such cases you
might want something like "required_auth=cert,scram-sha-256"? Or do we
need a way of specifying the combination?

I'd view the comma as strictly meaning OR, so that you might need some
notation like "required_auth=cert+scram-sha-256" if you want to demand
ANDed conditions. It might be better to handle TLS-specific
conditions orthogonally to the authentication exchange, though.

regards, tom lane

#6Laurenz Albe
laurenz.albe@cybertec.at
In reply to: Jacob Champion (#1)
Re: [PoC] Let libpq reject unexpected authentication requests

On Sat, 2022-03-05 at 01:04 +0000, Jacob Champion wrote:

TL;DR: this patch lets you specify exactly one authentication method in
the connection string, and libpq will fail the connection if the server
doesn't use that method.

(This is not intended for PG15. I'm generally anxious about posting
experimental work during a commitfest, but there's been enough
conversation about this topic recently that I felt like it'd be useful
to have code to point to.)

== Proposal and Alternatives ==

$subject keeps coming up in threads. I think my first introduction to
it was after the TLS injection CVE, and then it came up again in the
pluggable auth thread. It's hard for me to generalize based on "sound
bites", but among the proposals I've seen are

1. reject plaintext passwords
2. reject a configurable list of unacceptable methods
3. allow client and server to negotiate a method

All of them seem to have merit. I'm personally motivated by the case
brought up by the CVE: if I'm expecting client certificate
authentication, it's not acceptable for the server to extract _any_
information about passwords from my system, whether they're plaintext,
hashed, or SCRAM-protected. So I chose not to implement option 1. And
option 3 looked like a lot of work to take on in an experiment without
a clear consensus.

Here is my take on option 2, then: you get to choose exactly one method
that the client will accept.

I am all for the idea, but you implemented the reverse of proposal 2.

Wouldn't it be better to list the *rejected* authentication methods?
Then we could have "password" on there by default.

Yours,
Laurenz Albe

#7Jacob Champion
pchampion@vmware.com
In reply to: Laurenz Albe (#6)
Re: [PoC] Let libpq reject unexpected authentication requests

On Mon, 2022-03-07 at 11:44 +0100, Laurenz Albe wrote:

I am all for the idea, but you implemented the reverse of proposal 2.

(This email was caught in my spam filter; sorry for the delay.)

Wouldn't it be better to list the *rejected* authentication methods?
Then we could have "password" on there by default.

Specifying the allowed list rather than the denied list tends to have
better security properties.

In the case I'm pursuing (the attack vector from the CVE), the end user
expects certificates to be used. Any other authentication method --
plaintext, hashed, SCRAM, Kerberos -- is unacceptable; it shouldn't be
possible for the server to extract any information about the client
environment other than the cert. And I don't want to have to specify
the whole list of things that _aren't_ allowed, and keep that list
updated as we add new fancy auth methods, if I just want certs to be
used. So that's my argument for making the methods opt-in rather than
opt-out.

But that doesn't help your case; you want to choose a good default, and
I agree that's important. Since there are arguments already for
accepting a OR in the list, and -- if we couldn't find a good
orthogonal method for certs, like Tom suggested -- an AND, maybe it
wouldn't be so bad to accept a NOT as well?

require_auth=cert # certs only
require_auth=cert+scram-sha-256 # SCRAM wrapped by certs
require_auth=cert,scram-sha-256 # SCRAM or certs (or both)
require_auth=!password # anything but plaintext
require_auth=!password,!md5 # no plaintext or MD5

But it doesn't ever make sense to mix them:

require_auth=cert,!password # error: !password is useless
require_auth=!password,password # error: nonsense

--Jacob

#8Laurenz Albe
laurenz.albe@cybertec.at
In reply to: Jacob Champion (#7)
Re: [PoC] Let libpq reject unexpected authentication requests

On Wed, 2022-03-23 at 21:31 +0000, Jacob Champion wrote:

On Mon, 2022-03-07 at 11:44 +0100, Laurenz Albe wrote:

I am all for the idea, but you implemented the reverse of proposal 2.

Wouldn't it be better to list the *rejected* authentication methods?
Then we could have "password" on there by default.

Specifying the allowed list rather than the denied list tends to have
better security properties.

In the case I'm pursuing (the attack vector from the CVE), the end user
expects certificates to be used. Any other authentication method --
plaintext, hashed, SCRAM, Kerberos -- is unacceptable;

That makes sense.

But that doesn't help your case; you want to choose a good default, and
I agree that's important. Since there are arguments already for
accepting a OR in the list, and -- if we couldn't find a good
orthogonal method for certs, like Tom suggested -- an AND, maybe it
wouldn't be so bad to accept a NOT as well?

    require_auth=cert                # certs only
    require_auth=cert+scram-sha-256  # SCRAM wrapped by certs
    require_auth=cert,scram-sha-256  # SCRAM or certs (or both)
    require_auth=!password           # anything but plaintext
    require_auth=!password,!md5      # no plaintext or MD5

Great, if there is a !something syntax, then I have nothing left to wish.
It may not be the most secure way do do it, but it sure is convenient.

Yours,
Laurenz Albe

#9Michael Paquier
michael@paquier.xyz
In reply to: Jacob Champion (#1)
Re: [PoC] Let libpq reject unexpected authentication requests

On Sat, Mar 05, 2022 at 01:04:05AM +0000, Jacob Champion wrote:

the connection string, and libpq will fail the connection if the server
doesn't use that method.

(This is not intended for PG15. I'm generally anxious about posting
experimental work during a commitfest, but there's been enough
conversation about this topic recently that I felt like it'd be useful
to have code to point to.)

Jacob, do you still have plans to work on this patch?
--
Michael

#10Jacob Champion
jchampion@timescale.com
In reply to: Michael Paquier (#9)
Re: [PoC] Let libpq reject unexpected authentication requests

On Wed, Jun 1, 2022 at 12:55 AM Michael Paquier <michael@paquier.xyz> wrote:

Jacob, do you still have plans to work on this patch?

Yes, definitely. That said, the more the merrier if there are others
interested in taking a shot at it. There are a large number of
alternative implementation proposals.

Thanks,
--Jacob

#11Jacob Champion
jchampion@timescale.com
In reply to: Jacob Champion (#10)
2 attachment(s)
Re: [PoC] Let libpq reject unexpected authentication requests

v2 rebases over latest, removes the alternate spellings of "password",
and implements OR operations with a comma-separated list. For example:

- require_auth=cert means that the server must ask for, and the client
must provide, a client certificate.
- require_auth=password,md5 means that the server must ask for a
plaintext password or an MD5 hash.
- require_auth=scram-sha-256,gss means that one of SCRAM, Kerberos
authentication, or GSS transport encryption must be successfully
negotiated.
- require_auth=scram-sha-256,cert means that either a SCRAM handshake
must be completed, or the server must request a client certificate. It
has a potential pitfall in that it allows a partial SCRAM handshake,
as long as a certificate is requested and sent.

AND and NOT, discussed upthread, are not yet implemented. I tied
myself up in knots trying to make AND generic, so I think I"m going to
tackle NOT first, instead. The problem with AND is that it only makes
sense when one (and only one) of the options is a form of transport
authentication. (E.g. password+md5 never makes sense.) And although
cert+<something> and gss+<something> could be useful, the latter case
is already handled by gssencmode=require, and the gssencmode option is
more powerful since you can disable it (or set it to don't-care).

I'm not generally happy with how the "cert" option is working. With
the other methods, if you don't include a method in the list, then the
connection fails if the server tries to negotiate it. But if you don't
include the cert method in the list, we don't forbid the server from
asking for a cert, because the server always asks for a client
certificate via TLS whether it needs one or not. Behaving in the
intuitive way here would effectively break all use of TLS.

So I think Tom's recommendation that the cert method be handled by an
orthogonal option was a good one, and if that works then maybe we
don't need an AND syntax at all. Presumably I can just add an option
that parallels gssencmode, and then the current don't-care semantics
can be explicitly controlled. Skipping AND also means that I don't
have to create a syntax that can handle AND and NOT at the same time,
which I was dreading.

--Jacob

Attachments:

since-v1.diff.txttext/plain; charset=US-ASCII; name=since-v1.diff.txtDownload
commit 403d27e07922babcdf6fd5e8ad8524d92811294c
Author: Jacob Champion <jchampion@timescale.com>
Date:   Mon Jun 6 12:32:03 2022 -0700

    squash! libpq: let client reject unexpected auth methods
    
    - rebase over new sslkey() test function
    - remove alternate spellings of "password"
    - allow comma-separated methods (OR). At least one of the authentication
      methods in the list must complete for the connection to succeed.

diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index a883959756..04f9bf4831 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -870,6 +870,13 @@ auth_description(AuthRequest areq)
 	return libpq_gettext("an unknown authentication type");
 }
 
+/*
+ * Convenience macro for checking the allowed_auth_methods bitmask. Caller must
+ * ensure that type is not greater than 31 (high bit of the bitmask).
+ */
+#define auth_allowed(conn, type) \
+	(((conn)->allowed_auth_methods & (1 << (type))) != 0)
+
 /*
  * Verify that the authentication request is expected, given the connection
  * parameters. This is especially important when the client wishes to
@@ -881,7 +888,11 @@ check_expected_areq(AuthRequest areq, PGconn *conn)
 	bool		result = true;
 	char	   *reason = NULL;
 
-	/* If the user required a specific auth method, reject all others. */
+	/*
+	 * If the user required a specific auth method, or specified an allowed set,
+	 * then reject all others here, and make sure the server actually completes
+	 * an authentication exchange.
+	 */
 	if (conn->require_auth)
 	{
 		switch (areq)
@@ -890,21 +901,43 @@ check_expected_areq(AuthRequest areq, PGconn *conn)
 				/*
 				 * Check to make sure we've actually finished our exchange.
 				 */
-				if (strcmp(conn->require_auth, "cert") == 0)
+				if (conn->client_finished_auth)
+					break;
+
+				/*
+				 * No explicit authentication request was made by the server --
+				 * or perhaps it was made and not completed, in the case of
+				 * SCRAM -- but there are two special cases to check:
+				 *
+				 * 1) If the user allowed "cert", then as long as we sent a
+				 *    client certificate to the server in response to its
+				 *    TLS CertificateRequest, this check is satisfied.
+				 *
+				 * 2) If the user allowed "gss", then a GSS-encrypted channel
+				 *    also satisfies the check.
+				 */
+				if (conn->allowed_auth_method_cert)
 				{
+					/*
+					 * Trade off a little bit of complexity to try to get these
+					 * error messages as precise as possible.
+					 */
 					if (!conn->ssl_cert_requested)
 					{
-						reason = libpq_gettext("server did not request a certificate");
+						reason = conn->allowed_auth_methods
+							? libpq_gettext("server did not complete authentication or request a certificate")
+							: libpq_gettext("server did not request a certificate");
 						result = false;
 					}
 					else if (!conn->ssl_cert_sent)
 					{
-						reason = libpq_gettext("server accepted connection without a valid certificate");
+						reason = conn->allowed_auth_methods
+							? libpq_gettext("server did not complete authentication and accepted connection without a valid certificate")
+							: libpq_gettext("server accepted connection without a valid certificate");
 						result = false;
 					}
 				}
-				else if (strcmp(conn->require_auth, "gss") == 0
-						 && conn->gssenc)
+				else if (auth_allowed(conn, AUTH_REQ_GSS) && conn->gssenc)
 				{
 					/*
 					 * Special case: if implicit GSS auth has already been
@@ -915,49 +948,28 @@ check_expected_areq(AuthRequest areq, PGconn *conn)
 					 * are made in this case?
 					 */
 				}
-				else if (!conn->client_finished_auth)
+				else
 				{
 					reason = libpq_gettext("server did not complete authentication"),
 					result = false;
 				}
-				break;
 
-			case AUTH_REQ_PASSWORD:
-				if (strcmp(conn->require_auth, "bsd")
-					&& strcmp(conn->require_auth, "ldap")
-					&& strcmp(conn->require_auth, "pam")
-					&& strcmp(conn->require_auth, "password")
-					&& strcmp(conn->require_auth, "radius"))
-					result = false;
 				break;
 
+			case AUTH_REQ_PASSWORD:
 			case AUTH_REQ_MD5:
-				if (strcmp(conn->require_auth, "md5"))
-					result = false;
-				break;
-
 			case AUTH_REQ_GSS:
-				if (strcmp(conn->require_auth, "gss"))
-					result = false;
-				break;
-
 			case AUTH_REQ_SSPI:
-				if (strcmp(conn->require_auth, "sspi"))
-					result = false;
-				break;
-
 			case AUTH_REQ_GSS_CONT:
-				if (strcmp(conn->require_auth, "gss") &&
-					strcmp(conn->require_auth, "sspi"))
-					result = false;
-				break;
-
 			case AUTH_REQ_SASL:
 			case AUTH_REQ_SASL_CONT:
 			case AUTH_REQ_SASL_FIN:
-				/* This currently assumes that SCRAM is the only SASL method. */
-				if (strcmp(conn->require_auth, "scram-sha-256"))
-					result = false;
+				/*
+				 * We don't handle these with the default case, to avoid
+				 * bit-shifting past the end of the allowed_auth_methods mask if
+				 * the server sends an unexpected AuthRequest.
+				 */
+				result = auth_allowed(conn, areq);
 				break;
 
 			default:
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 682f363c2b..c650687c9b 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -1259,6 +1259,93 @@ connectOptions2(PGconn *conn)
 		}
 	}
 
+	/*
+	 * parse and validate require_auth option
+	 */
+	if (conn->require_auth)
+	{
+		char	   *s = conn->require_auth;
+		bool		more = true;
+
+		while (more)
+		{
+			char	   *method;
+			uint32		bits;
+
+			method = parse_comma_separated_list(&s, &more);
+			if (method == NULL)
+				goto oom_error;
+
+			if (strcmp(method, "password") == 0)
+			{
+				bits = (1 << AUTH_REQ_PASSWORD);
+			}
+			else if (strcmp(method, "md5") == 0)
+			{
+				bits = (1 << AUTH_REQ_MD5);
+			}
+			else if (strcmp(method, "gss") == 0)
+			{
+				bits = (1 << AUTH_REQ_GSS);
+				bits |= (1 << AUTH_REQ_GSS_CONT);
+			}
+			else if (strcmp(method, "sspi") == 0)
+			{
+				bits = (1 << AUTH_REQ_SSPI);
+				bits |= (1 << AUTH_REQ_GSS_CONT);
+			}
+			else if (strcmp(method, "scram-sha-256") == 0)
+			{
+				/* This currently assumes that SCRAM is the only SASL method. */
+				bits = (1 << AUTH_REQ_SASL);
+				bits |= (1 << AUTH_REQ_SASL_CONT);
+				bits |= (1 << AUTH_REQ_SASL_FIN);
+			}
+			else if (strcmp(method, "cert") == 0)
+			{
+				/*
+				 * Special case: there is no AUTH_REQ code for certificate
+				 * authentication since it's done as part of the TLS handshake.
+				 * Make a note in conn, for check_expected_areq() to see later.
+				 */
+				if (conn->allowed_auth_method_cert)
+				{
+					conn->status = CONNECTION_BAD;
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("require_auth method \"%s\" is specified more than once"),
+									  method);
+					return false;
+				}
+
+				conn->allowed_auth_method_cert = true;
+				continue; /* avoid the bitmask manipulation below */
+			}
+			else
+			{
+				conn->status = CONNECTION_BAD;
+				appendPQExpBuffer(&conn->errorMessage,
+								  libpq_gettext("invalid require_auth method: \"%s\""),
+								  method);
+				return false;
+			}
+
+			/*
+			 * Sanity check; a duplicated method probably indicates a typo in a
+			 * setting where typos are extremely risky.
+			 */
+			if ((conn->allowed_auth_methods & bits) == bits)
+			{
+				conn->status = CONNECTION_BAD;
+				appendPQExpBuffer(&conn->errorMessage,
+								  libpq_gettext("require_auth method \"%s\" is specified more than once"),
+								  method);
+				return false;
+			}
+
+			conn->allowed_auth_methods |= bits;
+		}
+	}
+
 	/*
 	 * validate channel_binding option
 	 */
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index b7701c3683..62554fedde 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -455,6 +455,9 @@ struct pg_conn
 	bool		write_failed;	/* have we had a write failure on sock? */
 	char	   *write_err_msg;	/* write error message, or NULL if OOM */
 
+	uint32		allowed_auth_methods;	/* bitmask of acceptable AuthRequest codes */
+	bool		allowed_auth_method_cert;	/* tracks "cert" which has no AuthRequest code */
+
 	/* Transient state needed while establishing connection */
 	PGTargetServerType target_server_type;	/* desired session properties */
 	bool		try_next_addr;	/* time to advance to next address/host? */
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index c7062ca357..dab50774c4 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -95,15 +95,15 @@ $node->connect_fails("user=scram_role require_auth=sspi",
 $node->connect_fails("user=scram_role require_auth=password",
 	"password authentication can be required: fails with trust auth",
 	expected_stderr => qr/server did not complete authentication/);
-$node->connect_fails("user=scram_role require_auth=ldap",
-	"LDAP authentication can be required: fails with trust auth",
-	expected_stderr => qr/server did not complete authentication/);
 $node->connect_fails("user=scram_role require_auth=md5",
 	"md5 authentication can be required: fails with trust auth",
 	expected_stderr => qr/server did not complete authentication/);
 $node->connect_fails("user=scram_role require_auth=scram-sha-256",
 	"SCRAM authentication can be required: fails with trust auth",
 	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=cert,scram-sha-256",
+	"multiple authentication types can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication or request a certificate/);
 
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'password');
@@ -117,6 +117,8 @@ test_role($node, 'md5_role', 'password', 0,
 # require_auth should succeed with a plaintext password...
 $node->connect_ok("user=scram_role require_auth=password",
 	"password authentication can be required: works with password auth");
+$node->connect_ok("user=scram_role require_auth=cert,password,md5",
+	"multiple authentication types can be required: works with password auth");
 
 # ...and fail for other auth types.
 $node->connect_fails("user=scram_role require_auth=md5",
@@ -142,6 +144,8 @@ test_role($node, 'md5_role', 'scram-sha-256', 2,
 # require_auth should succeed with SCRAM...
 $node->connect_ok("user=scram_role require_auth=scram-sha-256",
 	"SCRAM authentication can be required: works with SCRAM auth");
+$node->connect_ok("user=scram_role require_auth=password,scram-sha-256,cert",
+	"multiple authentication types can be required: works with SCRAM auth");
 
 # ...and fail for other auth types.
 $node->connect_fails("user=scram_role require_auth=password",
@@ -170,6 +174,8 @@ test_role($node, 'md5_role', 'md5', 0,
 # require_auth should succeed with MD5...
 $node->connect_ok("user=md5_role require_auth=md5",
 	"MD5 authentication can be required: works with MD5 auth");
+$node->connect_ok("user=md5_role require_auth=md5,scram-sha-256,cert",
+	"multiple authentication types can be required: works with MD5 auth");
 
 # ...and fail for other auth types.
 $node->connect_fails("user=md5_role require_auth=password",
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index 84e94e8e81..de4ddcbf69 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -353,6 +353,14 @@ test_access(
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable',
 	'fails with GSS encryption disabled and hostgssenc hba');
 
+# require_auth=gss should succeed.
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss",
+	"GSS authentication can be requested: works with GSS encryption");
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss,scram-sha-256",
+	"multiple authentication types can be requested: works with GSS encryption");
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
 	qq{hostnogssenc all all $hostaddr/32 gss map=mymap});
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index a6b07051fc..e7c67920a1 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -216,10 +216,7 @@ test_access(
 		qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/
 	],);
 
-# require_auth=ldap (and other plaintext password methods) should complete
-# successfully; other methods should fail.
-$node->connect_ok("user=test1 require_auth=ldap",
-	"LDAP authentication can be required: works with ldap auth");
+# require_auth=password should complete successfully; other methods should fail.
 $node->connect_ok("user=test1 require_auth=password",
 	"password authentication can be required: works with ldap auth");
 $node->connect_fails("user=test1 require_auth=scram-sha-256",
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 10d67eee17..68a888e11a 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -507,7 +507,8 @@ $common_connstr =
 
 # require_auth=cert should succeed against the certdb...
 $node->connect_ok(
-	"$common_connstr require_auth=cert user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client.key'}",
+	"$common_connstr require_auth=cert user=ssltestuser sslcert=ssl/client.crt "
+	. sslkey('client.key'),
 	"certificate authentication can be required: works with cert auth");
 
 # ...and fail against the trustdb, if no certificate is provided.
@@ -515,6 +516,10 @@ $node->connect_fails(
 	"$common_connstr require_auth=cert dbname=trustdb user=ssltestuser",
 	"certificate authentication can be required: fails with trust auth and no cert",
 	expected_stderr => qr/server accepted connection without a valid certificate/);
+$node->connect_fails(
+	"$common_connstr require_auth=scram-sha-256,cert dbname=trustdb user=ssltestuser",
+	"multiple authentication types can be required: fails with trust auth and no cert",
+	expected_stderr => qr/server did not complete authentication and accepted connection without a valid certificate/);
 
 # no client cert
 $node->connect_fails(
v2-0001-libpq-let-client-reject-unexpected-auth-methods.patchtext/x-patch; charset=US-ASCII; name=v2-0001-libpq-let-client-reject-unexpected-auth-methods.patchDownload
From 0a4f31c989f7524116e9ec97ec05db5a0d8ad791 Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Mon, 28 Feb 2022 09:40:43 -0800
Subject: [PATCH v2] libpq: let client reject unexpected auth methods

The require_auth connection option allows the client to choose a list of
acceptable authentication types for use with the server. There is no
negotiation: if the server does not present one of the allowed
authentication requests, the connection fails.

Internally, the patch expands the role of check_expected_areq() to
ensure that the incoming request is compatible with conn->require_auth.
It also introduces a new flag, conn->client_finished_auth, which is set
by various authentication routines when the client side of the handshake
is finished. This signals to check_expected_areq() that an OK message
from the server is expected, and allows the client to complain if the
server forgoes authentication entirely.

(Since the client can't generally prove that the server is actually
doing the work of authentication, this last part is mostly useful for
SCRAM without channel binding. But it could be used to diagnose
configuration problems with client certificate authentication. It could
also provide a client with a decent signal that, at the very least, it's
not connecting to a database with trust auth, and so the connection can
be tied to the client in a later audit.)

Certificate authentication poses an additional complication.
conn->ssl_cert_requested and conn->ssl_cert_sent have been added so that
check_expected_areq() can ensure that we sent a certificate during the
TLS handshake.

Deficiencies:
- This feature currently doesn't prevent unexpected certificate exchange
  or unexpected GSSAPI encryption.
- This is unlikely to be very forwards-compatible at the moment,
  especially with SASL/SCRAM.
- SSPI support is "implemented" but untested.
---
 src/interfaces/libpq/fe-auth-scram.c      |   1 +
 src/interfaces/libpq/fe-auth.c            | 158 ++++++++++++++++++++++
 src/interfaces/libpq/fe-connect.c         |  91 +++++++++++++
 src/interfaces/libpq/fe-secure-openssl.c  |  28 ++++
 src/interfaces/libpq/libpq-int.h          |   9 ++
 src/test/authentication/t/001_password.pl |  65 +++++++++
 src/test/kerberos/t/001_auth.pl           |  26 ++++
 src/test/ldap/t/001_auth.pl               |   6 +
 src/test/ssl/t/001_ssltests.pl            |  16 +++
 9 files changed, 400 insertions(+)

diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index e616200704..d918e8b87f 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -289,6 +289,7 @@ scram_exchange(void *opaq, char *input, int inputlen,
 			}
 			*done = true;
 			state->state = FE_SCRAM_FINISHED;
+			state->conn->client_finished_auth = true;
 			break;
 
 		default:
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 0a072a36dc..04f9bf4831 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -138,7 +138,10 @@ pg_GSS_continue(PGconn *conn, int payloadlen)
 	}
 
 	if (maj_stat == GSS_S_COMPLETE)
+	{
+		conn->client_finished_auth = true;
 		gss_release_name(&lmin_s, &conn->gtarg_nam);
+	}
 
 	return STATUS_OK;
 }
@@ -328,6 +331,9 @@ pg_SSPI_continue(PGconn *conn, int payloadlen)
 		FreeContextBuffer(outbuf.pBuffers[0].pvBuffer);
 	}
 
+	if (r == SEC_E_OK)
+		conn->client_finished_auth = true;
+
 	/* Cleanup is handled by the code in freePGconn() */
 	return STATUS_OK;
 }
@@ -836,6 +842,41 @@ pg_password_sendauth(PGconn *conn, const char *password, AuthRequest areq)
 	return ret;
 }
 
+/*
+ * Translate an AuthRequest into a human-readable description.
+ */
+static const char *
+auth_description(AuthRequest areq)
+{
+	switch (areq)
+	{
+		case AUTH_REQ_PASSWORD:
+			return libpq_gettext("a cleartext password");
+		case AUTH_REQ_MD5:
+			return libpq_gettext("a hashed password");
+		case AUTH_REQ_GSS:
+		case AUTH_REQ_GSS_CONT:
+			return libpq_gettext("GSSAPI authentication");
+		case AUTH_REQ_SSPI:
+			return libpq_gettext("SSPI authentication");
+		case AUTH_REQ_SCM_CREDS:
+			return libpq_gettext("UNIX socket credentials");
+		case AUTH_REQ_SASL:
+		case AUTH_REQ_SASL_CONT:
+		case AUTH_REQ_SASL_FIN:
+			return libpq_gettext("SASL authentication");
+	}
+
+	return libpq_gettext("an unknown authentication type");
+}
+
+/*
+ * Convenience macro for checking the allowed_auth_methods bitmask. Caller must
+ * ensure that type is not greater than 31 (high bit of the bitmask).
+ */
+#define auth_allowed(conn, type) \
+	(((conn)->allowed_auth_methods & (1 << (type))) != 0)
+
 /*
  * Verify that the authentication request is expected, given the connection
  * parameters. This is especially important when the client wishes to
@@ -845,6 +886,120 @@ static bool
 check_expected_areq(AuthRequest areq, PGconn *conn)
 {
 	bool		result = true;
+	char	   *reason = NULL;
+
+	/*
+	 * If the user required a specific auth method, or specified an allowed set,
+	 * then reject all others here, and make sure the server actually completes
+	 * an authentication exchange.
+	 */
+	if (conn->require_auth)
+	{
+		switch (areq)
+		{
+			case AUTH_REQ_OK:
+				/*
+				 * Check to make sure we've actually finished our exchange.
+				 */
+				if (conn->client_finished_auth)
+					break;
+
+				/*
+				 * No explicit authentication request was made by the server --
+				 * or perhaps it was made and not completed, in the case of
+				 * SCRAM -- but there are two special cases to check:
+				 *
+				 * 1) If the user allowed "cert", then as long as we sent a
+				 *    client certificate to the server in response to its
+				 *    TLS CertificateRequest, this check is satisfied.
+				 *
+				 * 2) If the user allowed "gss", then a GSS-encrypted channel
+				 *    also satisfies the check.
+				 */
+				if (conn->allowed_auth_method_cert)
+				{
+					/*
+					 * Trade off a little bit of complexity to try to get these
+					 * error messages as precise as possible.
+					 */
+					if (!conn->ssl_cert_requested)
+					{
+						reason = conn->allowed_auth_methods
+							? libpq_gettext("server did not complete authentication or request a certificate")
+							: libpq_gettext("server did not request a certificate");
+						result = false;
+					}
+					else if (!conn->ssl_cert_sent)
+					{
+						reason = conn->allowed_auth_methods
+							? libpq_gettext("server did not complete authentication and accepted connection without a valid certificate")
+							: libpq_gettext("server accepted connection without a valid certificate");
+						result = false;
+					}
+				}
+				else if (auth_allowed(conn, AUTH_REQ_GSS) && conn->gssenc)
+				{
+					/*
+					 * Special case: if implicit GSS auth has already been
+					 * performed via GSS encryption, we don't need to have
+					 * performed an AUTH_REQ_GSS exchange.
+					 *
+					 * TODO: check this assumption. What mutual auth guarantees
+					 * are made in this case?
+					 */
+				}
+				else
+				{
+					reason = libpq_gettext("server did not complete authentication"),
+					result = false;
+				}
+
+				break;
+
+			case AUTH_REQ_PASSWORD:
+			case AUTH_REQ_MD5:
+			case AUTH_REQ_GSS:
+			case AUTH_REQ_SSPI:
+			case AUTH_REQ_GSS_CONT:
+			case AUTH_REQ_SASL:
+			case AUTH_REQ_SASL_CONT:
+			case AUTH_REQ_SASL_FIN:
+				/*
+				 * We don't handle these with the default case, to avoid
+				 * bit-shifting past the end of the allowed_auth_methods mask if
+				 * the server sends an unexpected AuthRequest.
+				 */
+				result = auth_allowed(conn, areq);
+				break;
+
+			default:
+				result = false;
+				break;
+		}
+	}
+
+	if (!result)
+	{
+		if (reason)
+		{
+			/*
+			 * XXX double call to libpq_gettext() is probably not easy to
+			 * translate from English
+			 */
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("auth method \"%s\" required, but %s\n"),
+							  conn->require_auth, reason);
+		}
+		else
+		{
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("auth method \"%s\" required, but server requested %s\n"),
+							  conn->require_auth,
+							  auth_description(areq));
+		}
+
+		return result;
+	}
 
 	/*
 	 * When channel_binding=require, we must protect against two cases: (1) we
@@ -1046,6 +1201,9 @@ pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn)
 										 "fe_sendauth: error sending password authentication\n");
 					return STATUS_ERROR;
 				}
+
+				/* We expect no further authentication requests. */
+				conn->client_finished_auth = true;
 				break;
 			}
 
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 6e936bbff3..c650687c9b 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -310,6 +310,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Require-Peer", "", 10,
 	offsetof(struct pg_conn, requirepeer)},
 
+	{"require_auth", NULL, NULL, NULL,
+		"Require-Auth", "", 14, /* sizeof("scram-sha-256") == 14 */
+	offsetof(struct pg_conn, require_auth)},
+
 	{"ssl_min_protocol_version", "PGSSLMINPROTOCOLVERSION", "TLSv1.2", NULL,
 		"SSL-Minimum-Protocol-Version", "", 8,	/* sizeof("TLSv1.x") == 8 */
 	offsetof(struct pg_conn, ssl_min_protocol_version)},
@@ -1255,6 +1259,93 @@ connectOptions2(PGconn *conn)
 		}
 	}
 
+	/*
+	 * parse and validate require_auth option
+	 */
+	if (conn->require_auth)
+	{
+		char	   *s = conn->require_auth;
+		bool		more = true;
+
+		while (more)
+		{
+			char	   *method;
+			uint32		bits;
+
+			method = parse_comma_separated_list(&s, &more);
+			if (method == NULL)
+				goto oom_error;
+
+			if (strcmp(method, "password") == 0)
+			{
+				bits = (1 << AUTH_REQ_PASSWORD);
+			}
+			else if (strcmp(method, "md5") == 0)
+			{
+				bits = (1 << AUTH_REQ_MD5);
+			}
+			else if (strcmp(method, "gss") == 0)
+			{
+				bits = (1 << AUTH_REQ_GSS);
+				bits |= (1 << AUTH_REQ_GSS_CONT);
+			}
+			else if (strcmp(method, "sspi") == 0)
+			{
+				bits = (1 << AUTH_REQ_SSPI);
+				bits |= (1 << AUTH_REQ_GSS_CONT);
+			}
+			else if (strcmp(method, "scram-sha-256") == 0)
+			{
+				/* This currently assumes that SCRAM is the only SASL method. */
+				bits = (1 << AUTH_REQ_SASL);
+				bits |= (1 << AUTH_REQ_SASL_CONT);
+				bits |= (1 << AUTH_REQ_SASL_FIN);
+			}
+			else if (strcmp(method, "cert") == 0)
+			{
+				/*
+				 * Special case: there is no AUTH_REQ code for certificate
+				 * authentication since it's done as part of the TLS handshake.
+				 * Make a note in conn, for check_expected_areq() to see later.
+				 */
+				if (conn->allowed_auth_method_cert)
+				{
+					conn->status = CONNECTION_BAD;
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("require_auth method \"%s\" is specified more than once"),
+									  method);
+					return false;
+				}
+
+				conn->allowed_auth_method_cert = true;
+				continue; /* avoid the bitmask manipulation below */
+			}
+			else
+			{
+				conn->status = CONNECTION_BAD;
+				appendPQExpBuffer(&conn->errorMessage,
+								  libpq_gettext("invalid require_auth method: \"%s\""),
+								  method);
+				return false;
+			}
+
+			/*
+			 * Sanity check; a duplicated method probably indicates a typo in a
+			 * setting where typos are extremely risky.
+			 */
+			if ((conn->allowed_auth_methods & bits) == bits)
+			{
+				conn->status = CONNECTION_BAD;
+				appendPQExpBuffer(&conn->errorMessage,
+								  libpq_gettext("require_auth method \"%s\" is specified more than once"),
+								  method);
+				return false;
+			}
+
+			conn->allowed_auth_methods |= bits;
+		}
+	}
+
 	/*
 	 * validate channel_binding option
 	 */
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 8117cbd40f..6fb9d48896 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -477,6 +477,31 @@ verify_cb(int ok, X509_STORE_CTX *ctx)
 	return ok;
 }
 
+/*
+ * Certificate selection callback
+ *
+ * This callback lets us choose the client certificate we send to the server
+ * after seeing its CertificateRequest. We only support sending a single
+ * hard-coded certificate via sslcert, so we don't actually set any certificates
+ * here; we just it to record whether or not the server has actually asked for
+ * one and whether we have one to send.
+ */
+static int
+cert_cb(SSL *ssl, void *arg)
+{
+	PGconn *conn = arg;
+	conn->ssl_cert_requested = true;
+
+	/* Do we have a certificate loaded to send back? */
+	if (SSL_get_certificate(ssl))
+		conn->ssl_cert_sent = true;
+
+	/*
+	 * Tell OpenSSL that the callback succeeded; we're not required to actually
+	 * make any changes to the SSL handle.
+	 */
+	return 1;
+}
 
 /*
  * OpenSSL-specific wrapper around
@@ -972,6 +997,9 @@ initialize_SSL(PGconn *conn)
 		SSL_CTX_set_default_passwd_cb_userdata(SSL_context, conn);
 	}
 
+	/* Set up a certificate selection callback. */
+	SSL_CTX_set_cert_cb(SSL_context, cert_cb, conn);
+
 	/* Disable old protocol versions */
 	SSL_CTX_set_options(SSL_context, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
 
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 3db6a17db4..62554fedde 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -393,6 +393,7 @@ struct pg_conn
 	char	   *ssl_min_protocol_version;	/* minimum TLS protocol version */
 	char	   *ssl_max_protocol_version;	/* maximum TLS protocol version */
 	char	   *target_session_attrs;	/* desired session properties */
+	char	   *require_auth;	/* name of the expected auth method */
 
 	/* Optional file to write trace info to */
 	FILE	   *Pfdebug;
@@ -454,6 +455,9 @@ struct pg_conn
 	bool		write_failed;	/* have we had a write failure on sock? */
 	char	   *write_err_msg;	/* write error message, or NULL if OOM */
 
+	uint32		allowed_auth_methods;	/* bitmask of acceptable AuthRequest codes */
+	bool		allowed_auth_method_cert;	/* tracks "cert" which has no AuthRequest code */
+
 	/* Transient state needed while establishing connection */
 	PGTargetServerType target_server_type;	/* desired session properties */
 	bool		try_next_addr;	/* time to advance to next address/host? */
@@ -509,12 +513,17 @@ struct pg_conn
 	bool		error_result;	/* do we need to make an ERROR result? */
 	PGresult   *next_result;	/* next result (used in single-row mode) */
 
+	bool		client_finished_auth; /* have we finished our half of the
+									   * authentication exchange? */
+
 	/* Assorted state for SASL, SSL, GSS, etc */
 	const pg_fe_sasl_mech *sasl;
 	void	   *sasl_state;
 
 	/* SSL structures */
 	bool		ssl_in_use;
+	bool		ssl_cert_requested;	/* Did the server ask us for a cert? */
+	bool		ssl_cert_sent;		/* Did we send one in reply? */
 
 #ifdef USE_SSL
 	bool		allow_ssl_try;	/* Allowed to try SSL negotiation */
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 3e3079c824..dab50774c4 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -82,6 +82,29 @@ test_role($node, 'scram_role', 'trust', 0,
 test_role($node, 'md5_role', 'trust', 0,
 	log_unlike => [qr/connection authenticated:/]);
 
+# All require_auth options should fail.
+$node->connect_fails("user=scram_role require_auth=cert",
+	"certificate authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not request a certificate/);
+$node->connect_fails("user=scram_role require_auth=gss",
+	"GSS authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=sspi",
+	"GSS authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=password",
+	"password authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=cert,scram-sha-256",
+	"multiple authentication types can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication or request a certificate/);
+
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'password');
 test_role($node, 'scram_role', 'password', 0,
@@ -91,6 +114,20 @@ test_role($node, 'md5_role', 'password', 0,
 	log_like =>
 	  [qr/connection authenticated: identity="md5_role" method=password/]);
 
+# require_auth should succeed with a plaintext password...
+$node->connect_ok("user=scram_role require_auth=password",
+	"password authentication can be required: works with password auth");
+$node->connect_ok("user=scram_role require_auth=cert,password,md5",
+	"multiple authentication types can be required: works with password auth");
+
+# ...and fail for other auth types.
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+
 # For "scram-sha-256" method, user "scram_role" should be able to connect.
 reset_pg_hba($node, 'scram-sha-256');
 test_role(
@@ -104,6 +141,20 @@ test_role(
 test_role($node, 'md5_role', 'scram-sha-256', 2,
 	log_unlike => [qr/connection authenticated:/]);
 
+# require_auth should succeed with SCRAM...
+$node->connect_ok("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: works with SCRAM auth");
+$node->connect_ok("user=scram_role require_auth=password,scram-sha-256,cert",
+	"multiple authentication types can be required: works with SCRAM auth");
+
+# ...and fail for other auth types.
+$node->connect_fails("user=scram_role require_auth=password",
+	"password authentication can be required: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+
 # Test that bad passwords are rejected.
 $ENV{"PGPASSWORD"} = 'badpass';
 test_role($node, 'scram_role', 'scram-sha-256', 2,
@@ -120,6 +171,20 @@ test_role($node, 'md5_role', 'md5', 0,
 	log_like =>
 	  [qr/connection authenticated: identity="md5_role" method=md5/]);
 
+# require_auth should succeed with MD5...
+$node->connect_ok("user=md5_role require_auth=md5",
+	"MD5 authentication can be required: works with MD5 auth");
+$node->connect_ok("user=md5_role require_auth=md5,scram-sha-256,cert",
+	"multiple authentication types can be required: works with MD5 auth");
+
+# ...and fail for other auth types.
+$node->connect_fails("user=md5_role require_auth=password",
+	"password authentication can be required: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+$node->connect_fails("user=md5_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+
 # Tests for channel binding without SSL.
 # Using the password authentication method; channel binding can't work
 reset_pg_hba($node, 'password');
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index 62e0542639..de4ddcbf69 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -307,6 +307,24 @@ test_query(
 	'gssencmode=require',
 	'sending 100K lines works');
 
+# require_auth=gss should succeed...
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=disable require_auth=gss",
+	"GSS authentication can be requested: works with GSS auth without encryption");
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss",
+	"GSS authentication can be requested: works with GSS auth with encryption");
+
+# ...and require_auth=sspi should fail.
+$node->connect_fails(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=disable require_auth=sspi",
+	"SSPI authentication can be requested: fails with GSS auth without encryption",
+	expected_stderr => qr/server requested GSSAPI authentication/);
+$node->connect_fails(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=sspi",
+	"SSPI authentication can be requested: fails with GSS auth with encryption",
+	expected_stderr => qr/server did not complete authentication/);
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
 	qq{hostgssenc all all $hostaddr/32 gss map=mymap});
@@ -335,6 +353,14 @@ test_access(
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable',
 	'fails with GSS encryption disabled and hostgssenc hba');
 
+# require_auth=gss should succeed.
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss",
+	"GSS authentication can be requested: works with GSS encryption");
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss,scram-sha-256",
+	"multiple authentication types can be requested: works with GSS encryption");
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
 	qq{hostnogssenc all all $hostaddr/32 gss map=mymap});
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 86dff8bd1f..e7c67920a1 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -216,6 +216,12 @@ test_access(
 		qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/
 	],);
 
+# require_auth=password should complete successfully; other methods should fail.
+$node->connect_ok("user=test1 require_auth=password",
+	"password authentication can be required: works with ldap auth");
+$node->connect_fails("user=test1 require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with ldap auth");
+
 note "search+bind";
 
 unlink($node->data_dir . '/pg_hba.conf');
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index c0b4a5739c..68a888e11a 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -505,6 +505,22 @@ note "running server tests";
 $common_connstr =
   "$default_ssl_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=certdb hostaddr=$SERVERHOSTADDR host=localhost";
 
+# require_auth=cert should succeed against the certdb...
+$node->connect_ok(
+	"$common_connstr require_auth=cert user=ssltestuser sslcert=ssl/client.crt "
+	. sslkey('client.key'),
+	"certificate authentication can be required: works with cert auth");
+
+# ...and fail against the trustdb, if no certificate is provided.
+$node->connect_fails(
+	"$common_connstr require_auth=cert dbname=trustdb user=ssltestuser",
+	"certificate authentication can be required: fails with trust auth and no cert",
+	expected_stderr => qr/server accepted connection without a valid certificate/);
+$node->connect_fails(
+	"$common_connstr require_auth=scram-sha-256,cert dbname=trustdb user=ssltestuser",
+	"multiple authentication types can be required: fails with trust auth and no cert",
+	expected_stderr => qr/server did not complete authentication and accepted connection without a valid certificate/);
+
 # no client cert
 $node->connect_fails(
 	"$common_connstr user=ssltestuser sslcert=invalid",
-- 
2.25.1

#12Michael Paquier
michael@paquier.xyz
In reply to: Jacob Champion (#11)
Re: [PoC] Let libpq reject unexpected authentication requests

On Tue, Jun 07, 2022 at 02:22:28PM -0700, Jacob Champion wrote:

v2 rebases over latest, removes the alternate spellings of "password",
and implements OR operations with a comma-separated list. For example:

- require_auth=cert means that the server must ask for, and the client
must provide, a client certificate.

Hmm.. Nya.

- require_auth=password,md5 means that the server must ask for a
plaintext password or an MD5 hash.
- require_auth=scram-sha-256,gss means that one of SCRAM, Kerberos
authentication, or GSS transport encryption must be successfully
negotiated.

Makes sense.

- require_auth=scram-sha-256,cert means that either a SCRAM handshake
must be completed, or the server must request a client certificate. It
has a potential pitfall in that it allows a partial SCRAM handshake,
as long as a certificate is requested and sent.

Er, this one could be a problem protocol-wise for SASL, because that
would mean that the authentication gets completed but that the
exchange has begun and is not finished?

AND and NOT, discussed upthread, are not yet implemented. I tied
myself up in knots trying to make AND generic, so I think I"m going to
tackle NOT first, instead. The problem with AND is that it only makes
sense when one (and only one) of the options is a form of transport
authentication. (E.g. password+md5 never makes sense.) And although
cert+<something> and gss+<something> could be useful, the latter case
is already handled by gssencmode=require, and the gssencmode option is
more powerful since you can disable it (or set it to don't-care).

I am on the edge regarding NOT as well, and I am unsure of the actual
benefits we could get from it as long as one can provide a white list
of auth methods. If we don't see an immediate benefit in that, I'd
rather choose a minimal, still useful, design. As a whole, I would
vote with adding only support for OR and a comma-separated list like
your proposal.

I'm not generally happy with how the "cert" option is working. With
the other methods, if you don't include a method in the list, then the
connection fails if the server tries to negotiate it. But if you don't
include the cert method in the list, we don't forbid the server from
asking for a cert, because the server always asks for a client
certificate via TLS whether it needs one or not. Behaving in the
intuitive way here would effectively break all use of TLS.

Agreed. Looking at what you are doing with allowed_auth_method_cert,
this makes the code harder to follow, which is risky for any
security-related feature, and that's different than the other methods
where we have the AUTH_REQ_* codes. This leads to extra complications
in the shape of ssl_cert_requested and ssl_cert_sent, as well. This
strongly looks like what we do for channel binding as it has
requirements separated from the actual auth methods but has dependency
with them, so a different parameter, as suggested, would make sense.
If we are not sure about this part, we could discard it in the first
instance of the patch.

So I think Tom's recommendation that the cert method be handled by an
orthogonal option was a good one, and if that works then maybe we
don't need an AND syntax at all. Presumably I can just add an option
that parallels gssencmode, and then the current don't-care semantics
can be explicitly controlled. Skipping AND also means that I don't
have to create a syntax that can handle AND and NOT at the same time,
which I was dreading.

I am not convinced that we have any need for the AND grammar within
one parameter, as that's basically the same thing as defining multiple
connection parameters, isn't it? This is somewhat a bit similar to
the interactions of channel binding with this new parameter and what
you have implemented. For example, channel_binding=require
require_auth=md5 would imply both and should fail, even if it makes
little sense because MD5 has no idea of channel binding. One
interesting case comes down to stuff like channel_binding=require
require_auth="md5,scram-sha-256", where I think that we should still
fail even if the server asks for MD5 and enforce an equivalent of an
AND grammar through the parameters. This reasoning limits the
dependencies between each parameter and treats the areas where these
are checked independently, which is what check_expected_areq() does
for channel binding. So that's more robust at the end.

Speaking of which, I would add tests to check some combinations of
channel_binding and require_auth.

+           appendPQExpBuffer(&conn->errorMessage,
+                             libpq_gettext("auth method \"%s\" required, but %s\n"),
+                             conn->require_auth, reason);
This one is going to make translation impossible.  One way to tackle
this issue is to use "auth method \"%s\" required: %s".
+   {"require_auth", NULL, NULL, NULL,
+       "Require-Auth", "", 14, /* sizeof("scram-sha-256") == 14 */
+   offsetof(struct pg_conn, require_auth)},
We could have an environment variable for that.
+/*
+ * Convenience macro for checking the allowed_auth_methods bitmask. Caller must
+ * ensure that type is not greater than 31 (high bit of the bitmask).
+ */
+#define auth_allowed(conn, type) \
+   (((conn)->allowed_auth_methods & (1 << (type))) != 0)
Better to add a compile-time check with StaticAssertDecl() then?  Or
add a note about that in pqcomm.h?
+      else if (auth_allowed(conn, AUTH_REQ_GSS) && conn->gssenc)
+      {
This field is only available under ENABLE_GSS, so this would fail to
compile when building without it?
+           method = parse_comma_separated_list(&s, &more);
+           if (method == NULL)
+               goto oom_error;
This should free the malloc'd copy of the element parsed, no?  That
means a free at the end of the while loop processing the options.
+           /*
+            * Sanity check; a duplicated method probably indicates a typo in a
+            * setting where typos are extremely risky.
+            */
Not sure why this is a problem.  Fine by me to check that, but this is
an OR list, so specifying one element twice means the same as once.

I get that it is not the priority yet as long as the design is not
completely clear, but having some docs would be nice :)
--
Michael

#13Jacob Champion
jchampion@timescale.com
In reply to: Michael Paquier (#12)
Re: [PoC] Let libpq reject unexpected authentication requests

On Wed, Jun 8, 2022 at 9:58 PM Michael Paquier <michael@paquier.xyz> wrote:

- require_auth=scram-sha-256,cert means that either a SCRAM handshake
must be completed, or the server must request a client certificate. It
has a potential pitfall in that it allows a partial SCRAM handshake,
as long as a certificate is requested and sent.

Er, this one could be a problem protocol-wise for SASL, because that
would mean that the authentication gets completed but that the
exchange has begun and is not finished?

I think it's already a problem, if you're not using channel_binding.
The cert behavior here makes it even less intuitive.

AND and NOT, discussed upthread, are not yet implemented. I tied
myself up in knots trying to make AND generic, so I think I"m going to
tackle NOT first, instead. The problem with AND is that it only makes
sense when one (and only one) of the options is a form of transport
authentication. (E.g. password+md5 never makes sense.) And although
cert+<something> and gss+<something> could be useful, the latter case
is already handled by gssencmode=require, and the gssencmode option is
more powerful since you can disable it (or set it to don't-care).

I am on the edge regarding NOT as well, and I am unsure of the actual
benefits we could get from it as long as one can provide a white list
of auth methods. If we don't see an immediate benefit in that, I'd
rather choose a minimal, still useful, design. As a whole, I would
vote with adding only support for OR and a comma-separated list like
your proposal.

Personally I think the ability to provide a default of `!password` is
very compelling. Any allowlist we hardcode won't be future-proof (see
also my response to Laurenz upthread [1]/messages/by-id/a14b1f89dcde75fb20afa7a1ffd2c2587b8d1a08.camel@vmware.com).

I'm not generally happy with how the "cert" option is working. With
the other methods, if you don't include a method in the list, then the
connection fails if the server tries to negotiate it. But if you don't
include the cert method in the list, we don't forbid the server from
asking for a cert, because the server always asks for a client
certificate via TLS whether it needs one or not. Behaving in the
intuitive way here would effectively break all use of TLS.

Agreed. Looking at what you are doing with allowed_auth_method_cert,
this makes the code harder to follow, which is risky for any
security-related feature, and that's different than the other methods
where we have the AUTH_REQ_* codes. This leads to extra complications
in the shape of ssl_cert_requested and ssl_cert_sent, as well. This
strongly looks like what we do for channel binding as it has
requirements separated from the actual auth methods but has dependency
with them, so a different parameter, as suggested, would make sense.
If we are not sure about this part, we could discard it in the first
instance of the patch.

I'm pretty motivated to provide the ability to say "I want cert auth
only, nothing else." Using a separate parameter would mean we'd need
something like `require_auth=none`, but I think that makes a certain
amount of sense.

I am not convinced that we have any need for the AND grammar within
one parameter, as that's basically the same thing as defining multiple
connection parameters, isn't it? This is somewhat a bit similar to
the interactions of channel binding with this new parameter and what
you have implemented. For example, channel_binding=require
require_auth=md5 would imply both and should fail, even if it makes
little sense because MD5 has no idea of channel binding. One
interesting case comes down to stuff like channel_binding=require
require_auth="md5,scram-sha-256", where I think that we should still
fail even if the server asks for MD5 and enforce an equivalent of an
AND grammar through the parameters. This reasoning limits the
dependencies between each parameter and treats the areas where these
are checked independently, which is what check_expected_areq() does
for channel binding. So that's more robust at the end.

Agreed.

Speaking of which, I would add tests to check some combinations of
channel_binding and require_auth.

Sounds good.

+           appendPQExpBuffer(&conn->errorMessage,
+                             libpq_gettext("auth method \"%s\" required, but %s\n"),
+                             conn->require_auth, reason);
This one is going to make translation impossible.  One way to tackle
this issue is to use "auth method \"%s\" required: %s".

Yeah, I think I have a TODO somewhere about that somewhere. I'm
confused about your suggested fix though; can you elaborate?

+   {"require_auth", NULL, NULL, NULL,
+       "Require-Auth", "", 14, /* sizeof("scram-sha-256") == 14 */
+   offsetof(struct pg_conn, require_auth)},
We could have an environment variable for that.

I think that'd be a good idea. It'd be nice to have the option of
forcing a particular auth type across a process tree.

+/*
+ * Convenience macro for checking the allowed_auth_methods bitmask. Caller must
+ * ensure that type is not greater than 31 (high bit of the bitmask).
+ */
+#define auth_allowed(conn, type) \
+   (((conn)->allowed_auth_methods & (1 << (type))) != 0)
Better to add a compile-time check with StaticAssertDecl() then?  Or
add a note about that in pqcomm.h?

If we only passed constants, that would work, but we also determine
the type at runtime, from the server message. Or am I missing
something?

+      else if (auth_allowed(conn, AUTH_REQ_GSS) && conn->gssenc)
+      {
This field is only available under ENABLE_GSS, so this would fail to
compile when building without it?

Yes, thank you for the catch. Will fix.

+           method = parse_comma_separated_list(&s, &more);
+           if (method == NULL)
+               goto oom_error;
This should free the malloc'd copy of the element parsed, no?  That
means a free at the end of the while loop processing the options.

Good catch again, thanks!

+           /*
+            * Sanity check; a duplicated method probably indicates a typo in a
+            * setting where typos are extremely risky.
+            */
Not sure why this is a problem.  Fine by me to check that, but this is
an OR list, so specifying one element twice means the same as once.

Since this is likely to be a set-and-forget sort of option, and it
needs to behave correctly across server upgrades, I'd personally
prefer that the client tell me immediately if I've made a silly
mistake. Even for something relatively benign like this (but arguably,
it's more important for the NOT case).

I get that it is not the priority yet as long as the design is not
completely clear, but having some docs would be nice :)

Agreed; I will tackle that soon.

Thanks!
--Jacob

[1]: /messages/by-id/a14b1f89dcde75fb20afa7a1ffd2c2587b8d1a08.camel@vmware.com

#14Michael Paquier
michael@paquier.xyz
In reply to: Jacob Champion (#13)
Re: [PoC] Let libpq reject unexpected authentication requests

On Thu, Jun 09, 2022 at 04:29:38PM -0700, Jacob Champion wrote:

On Wed, Jun 8, 2022 at 9:58 PM Michael Paquier <michael@paquier.xyz> wrote:

Er, this one could be a problem protocol-wise for SASL, because that
would mean that the authentication gets completed but that the
exchange has begun and is not finished?

I think it's already a problem, if you're not using channel_binding.
The cert behavior here makes it even less intuitive.

Ah right. I forgot about the part where we need to have the backend
publish the set of supported machanisms. If we don't get back
SCRAM-SHA-256-PLUS we are currently complaining after the exchange has
been initialized, true. Maybe I should look at the RFC more closely.
The backend is very strict regarding that and needs to return an error
back to the client only when the exchange is done, but I don't recall
all the bits about the client handling.

Personally I think the ability to provide a default of `!password` is
very compelling. Any allowlist we hardcode won't be future-proof (see
also my response to Laurenz upthread [1]).

Hm, perhaps. You could use that as a default at application level,
but the default set in libpq should be backward-compatible (aka allow
everything, even trust where the backend just sends AUTH_REQ_OK).
This is unfortunate but there is a point in not breaking any user's
application, as well, particularly with ldap, and note that we have to
keep a certain amount of things backward-compatible as a very common
practice of packaging with Postgres is to have libpq link to binaries
across *multiple* major versions (Debian is one if I recall), with the
newest version of libpq used for all. One argument in favor of
!password would be to control whether one does not want to use ldap,
but I'd still see most users just specify one or more methods in line
with their HBA policy as an approved list.

I'm pretty motivated to provide the ability to say "I want cert auth
only, nothing else." Using a separate parameter would mean we'd need
something like `require_auth=none`, but I think that makes a certain
amount of sense.

If the default of require_auth is backward-compatible and allows
everything, using a different parameter for the certs won't matter
anyway?

+           appendPQExpBuffer(&conn->errorMessage,
+                             libpq_gettext("auth method \"%s\" required, but %s\n"),
+                             conn->require_auth, reason);
This one is going to make translation impossible.  One way to tackle
this issue is to use "auth method \"%s\" required: %s".

Yeah, I think I have a TODO somewhere about that somewhere. I'm
confused about your suggested fix though; can you elaborate?

My suggestion is to reword the error message so as the reason and the
main error message can be treated as two independent things. You are
right to apply two times libpq_gettext(), once to "reason" and once to
the main string.

+/*
+ * Convenience macro for checking the allowed_auth_methods bitmask. Caller must
+ * ensure that type is not greater than 31 (high bit of the bitmask).
+ */
+#define auth_allowed(conn, type) \
+   (((conn)->allowed_auth_methods & (1 << (type))) != 0)
Better to add a compile-time check with StaticAssertDecl() then?  Or
add a note about that in pqcomm.h?

If we only passed constants, that would work, but we also determine
the type at runtime, from the server message. Or am I missing
something?

My point would be to either register a max flag in the set of
AUTH_REQ_* in pqcomm.h so as we never go above 32 with an assertion to
make sure that this would never overflow, but I would add a comment in
pqcomm.h telling that we also do bitwise operations, relying on the
number of AUTH_REQ_* flags, and that we'd better be careful once the
number of flags gets higher than 32. There is some margin, still that
could be easily forgotten.

+           /*
+            * Sanity check; a duplicated method probably indicates a typo in a
+            * setting where typos are extremely risky.
+            */
Not sure why this is a problem.  Fine by me to check that, but this is
an OR list, so specifying one element twice means the same as once.

Since this is likely to be a set-and-forget sort of option, and it
needs to behave correctly across server upgrades, I'd personally
prefer that the client tell me immediately if I've made a silly
mistake. Even for something relatively benign like this (but arguably,
it's more important for the NOT case).

This is just a couple of lines. Fine by me to keep it if you feel
that's better in the long run.
--
Michael

#15Jacob Champion
jchampion@timescale.com
In reply to: Michael Paquier (#14)
2 attachment(s)
Re: [PoC] Let libpq reject unexpected authentication requests

On Mon, Jun 13, 2022 at 10:00 PM Michael Paquier <michael@paquier.xyz> wrote:

Personally I think the ability to provide a default of `!password` is
very compelling. Any allowlist we hardcode won't be future-proof (see
also my response to Laurenz upthread [1]).

Hm, perhaps. You could use that as a default at application level,
but the default set in libpq should be backward-compatible (aka allow
everything, even trust where the backend just sends AUTH_REQ_OK).
This is unfortunate but there is a point in not breaking any user's
application, as well, particularly with ldap, and note that we have to
keep a certain amount of things backward-compatible as a very common
practice of packaging with Postgres is to have libpq link to binaries
across *multiple* major versions (Debian is one if I recall), with the
newest version of libpq used for all.

I am 100% with you on this, but there's been a lot of chatter around
removing plaintext password support from libpq. I would much rather
see them rejected by default than removed entirely. !password would
provide an easy path for that.

I'm pretty motivated to provide the ability to say "I want cert auth
only, nothing else." Using a separate parameter would mean we'd need
something like `require_auth=none`, but I think that makes a certain
amount of sense.

If the default of require_auth is backward-compatible and allows
everything, using a different parameter for the certs won't matter
anyway?

If you want cert authentication only, allowing "everything" will let
the server extract a password and then you're back at square one.
There needs to be a way to prohibit all explicit authentication
requests.

My suggestion is to reword the error message so as the reason and the
main error message can be treated as two independent things. You are
right to apply two times libpq_gettext(), once to "reason" and once to
the main string.

Ah, thanks for the clarification. Done that way in v3.

My point would be to either register a max flag in the set of
AUTH_REQ_* in pqcomm.h so as we never go above 32 with an assertion to
make sure that this would never overflow, but I would add a comment in
pqcomm.h telling that we also do bitwise operations, relying on the
number of AUTH_REQ_* flags, and that we'd better be careful once the
number of flags gets higher than 32. There is some margin, still that
could be easily forgotten.

Makes sense; done.

v3 also removes "cert" from require_auth while I work on a replacement
connection option, fixes the bugs/suggestions pointed out upthread,
and adds a documentation first draft. I tried combining this with the
NOT work but it was too much to juggle, so that'll wait for a v4+,
along with require_auth=none and "cert mode".

Thanks for the detailed review!
--Jacob

Attachments:

since-v2.diff.txttext/plain; charset=US-ASCII; name=since-v2.diff.txtDownload
commit c299b1abf0ffc477371cdd0d0d789e824da56328
Author: Jacob Champion <jchampion@timescale.com>
Date:   Fri Jun 10 11:20:08 2022 -0700

    squash! libpq: let client reject unexpected auth methods
    
    Per review suggestions:
    
    - Fix a memory leak when parsing require_auth.
    - Add PGREQUIREAUTH to the envvars.
    - Remove the "cert" method from require_auth; this will be handled with
      a separate conninfo option later.
    - Fix builds without GSSAPI.
    - Test combinations of require_auth and channel_binding.
    - Statically assert when AUTH_REQ_* grows too large to hold safely in
      our allowed_auth_methods bitmask.
    - Improve ease of translation.
    - Add a first draft of documentation.

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 37ec3cb4e5..00990ce47d 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1221,6 +1221,76 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-require-auth" xreflabel="require_auth">
+      <term><literal>require_auth</literal></term>
+      <listitem>
+      <para>
+        Specifies the authentication method that the client requires from the
+        server. If the server does not use the required method to authenticate
+        the client, or if the authentication handshake is not fully completed by
+        the server, the connection will fail. A comma-separated list of methods
+        may also be provided, of which the server must use exactly one in order
+        for the connection to succeed. By default, any authentication method is
+        accepted, and the server is free to skip authentication altogether.
+      </para>
+      <para>
+        The following methods may be specified:
+
+        <variablelist>
+         <varlistentry>
+          <term><literal>password</literal></term>
+          <listitem>
+           <para>
+            The server must request plaintext password authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>md5</literal></term>
+          <listitem>
+           <para>
+            The server must request MD5 hashed password authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>gss</literal></term>
+          <listitem>
+           <para>
+            The server must either request a Kerberos handshake via
+            <acronym>GSSAPI</acronym> or establish a
+            <acronym>GSS</acronym>-encrypted channel (see also
+            <xref linkend="libpq-connect-gssencmode" />).
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>sspi</literal></term>
+          <listitem>
+           <para>
+            The server must request Windows <acronym>SSPI</acronym>
+            authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>scram-sha-256</literal></term>
+          <listitem>
+           <para>
+            The server must successfully complete a SCRAM-SHA-256 authentication
+            exchange with the client.
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+      </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-channel-binding" xreflabel="channel_binding">
       <term><literal>channel_binding</literal></term>
       <listitem>
@@ -7761,6 +7831,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGREQUIREAUTH</envar></primary>
+      </indexterm>
+      <envar>PGREQUIREAUTH</envar> behaves the same as the <xref
+      linkend="libpq-connect-require-auth"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h
index b418283d5f..a47f7b2d91 100644
--- a/src/include/libpq/pqcomm.h
+++ b/src/include/libpq/pqcomm.h
@@ -161,6 +161,7 @@ extern PGDLLIMPORT bool Db_user_namespace;
 #define AUTH_REQ_SASL	   10	/* Begin SASL authentication */
 #define AUTH_REQ_SASL_CONT 11	/* Continue SASL authentication */
 #define AUTH_REQ_SASL_FIN  12	/* Final SASL message */
+#define AUTH_REQ_MAX	   AUTH_REQ_SASL_FIN	/* maximum AUTH_REQ_* value */
 
 typedef uint32 AuthRequest;
 
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 04f9bf4831..59c3575cc3 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -877,6 +877,9 @@ auth_description(AuthRequest areq)
 #define auth_allowed(conn, type) \
 	(((conn)->allowed_auth_methods & (1 << (type))) != 0)
 
+StaticAssertDecl(AUTH_REQ_MAX < CHAR_BIT * sizeof(((PGconn){0}).allowed_auth_methods),
+				 "AUTH_REQ_MAX overflows the allowed_auth_methods bitmask");
+
 /*
  * Verify that the authentication request is expected, given the connection
  * parameters. This is especially important when the client wishes to
@@ -907,48 +910,24 @@ check_expected_areq(AuthRequest areq, PGconn *conn)
 				/*
 				 * No explicit authentication request was made by the server --
 				 * or perhaps it was made and not completed, in the case of
-				 * SCRAM -- but there are two special cases to check:
-				 *
-				 * 1) If the user allowed "cert", then as long as we sent a
-				 *    client certificate to the server in response to its
-				 *    TLS CertificateRequest, this check is satisfied.
-				 *
-				 * 2) If the user allowed "gss", then a GSS-encrypted channel
-				 *    also satisfies the check.
+				 * SCRAM -- but there is one special case to check. If the user
+				 * allowed "gss", then a GSS-encrypted channel also satisfies
+				 * the check.
 				 */
-				if (conn->allowed_auth_method_cert)
-				{
-					/*
-					 * Trade off a little bit of complexity to try to get these
-					 * error messages as precise as possible.
-					 */
-					if (!conn->ssl_cert_requested)
-					{
-						reason = conn->allowed_auth_methods
-							? libpq_gettext("server did not complete authentication or request a certificate")
-							: libpq_gettext("server did not request a certificate");
-						result = false;
-					}
-					else if (!conn->ssl_cert_sent)
-					{
-						reason = conn->allowed_auth_methods
-							? libpq_gettext("server did not complete authentication and accepted connection without a valid certificate")
-							: libpq_gettext("server accepted connection without a valid certificate");
-						result = false;
-					}
-				}
-				else if (auth_allowed(conn, AUTH_REQ_GSS) && conn->gssenc)
+#ifdef ENABLE_GSS
+				if (auth_allowed(conn, AUTH_REQ_GSS) && conn->gssenc)
 				{
 					/*
-					 * Special case: if implicit GSS auth has already been
-					 * performed via GSS encryption, we don't need to have
-					 * performed an AUTH_REQ_GSS exchange.
+					 * If implicit GSS auth has already been performed via GSS
+					 * encryption, we don't need to have performed an
+					 * AUTH_REQ_GSS exchange.
 					 *
 					 * TODO: check this assumption. What mutual auth guarantees
 					 * are made in this case?
 					 */
 				}
 				else
+#endif
 				{
 					reason = libpq_gettext("server did not complete authentication"),
 					result = false;
@@ -982,12 +961,8 @@ check_expected_areq(AuthRequest areq, PGconn *conn)
 	{
 		if (reason)
 		{
-			/*
-			 * XXX double call to libpq_gettext() is probably not easy to
-			 * translate from English
-			 */
 			appendPQExpBuffer(&conn->errorMessage,
-							  libpq_gettext("auth method \"%s\" required, but %s\n"),
+							  libpq_gettext("auth method \"%s\" requirement failed: %s\n"),
 							  conn->require_auth, reason);
 		}
 		else
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index c650687c9b..f2579ff7ee 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -310,7 +310,7 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Require-Peer", "", 10,
 	offsetof(struct pg_conn, requirepeer)},
 
-	{"require_auth", NULL, NULL, NULL,
+	{"require_auth", "PGREQUIREAUTH", NULL, NULL,
 		"Require-Auth", "", 14, /* sizeof("scram-sha-256") == 14 */
 	offsetof(struct pg_conn, require_auth)},
 
@@ -1301,31 +1301,14 @@ connectOptions2(PGconn *conn)
 				bits |= (1 << AUTH_REQ_SASL_CONT);
 				bits |= (1 << AUTH_REQ_SASL_FIN);
 			}
-			else if (strcmp(method, "cert") == 0)
-			{
-				/*
-				 * Special case: there is no AUTH_REQ code for certificate
-				 * authentication since it's done as part of the TLS handshake.
-				 * Make a note in conn, for check_expected_areq() to see later.
-				 */
-				if (conn->allowed_auth_method_cert)
-				{
-					conn->status = CONNECTION_BAD;
-					appendPQExpBuffer(&conn->errorMessage,
-									  libpq_gettext("require_auth method \"%s\" is specified more than once"),
-									  method);
-					return false;
-				}
-
-				conn->allowed_auth_method_cert = true;
-				continue; /* avoid the bitmask manipulation below */
-			}
 			else
 			{
 				conn->status = CONNECTION_BAD;
 				appendPQExpBuffer(&conn->errorMessage,
 								  libpq_gettext("invalid require_auth method: \"%s\""),
 								  method);
+
+				free(method);
 				return false;
 			}
 
@@ -1339,10 +1322,13 @@ connectOptions2(PGconn *conn)
 				appendPQExpBuffer(&conn->errorMessage,
 								  libpq_gettext("require_auth method \"%s\" is specified more than once"),
 								  method);
+
+				free(method);
 				return false;
 			}
 
 			conn->allowed_auth_methods |= bits;
+			free(method);
 		}
 	}
 
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 6fb9d48896..fc91cae7a2 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -477,32 +477,6 @@ verify_cb(int ok, X509_STORE_CTX *ctx)
 	return ok;
 }
 
-/*
- * Certificate selection callback
- *
- * This callback lets us choose the client certificate we send to the server
- * after seeing its CertificateRequest. We only support sending a single
- * hard-coded certificate via sslcert, so we don't actually set any certificates
- * here; we just it to record whether or not the server has actually asked for
- * one and whether we have one to send.
- */
-static int
-cert_cb(SSL *ssl, void *arg)
-{
-	PGconn *conn = arg;
-	conn->ssl_cert_requested = true;
-
-	/* Do we have a certificate loaded to send back? */
-	if (SSL_get_certificate(ssl))
-		conn->ssl_cert_sent = true;
-
-	/*
-	 * Tell OpenSSL that the callback succeeded; we're not required to actually
-	 * make any changes to the SSL handle.
-	 */
-	return 1;
-}
-
 /*
  * OpenSSL-specific wrapper around
  * pq_verify_peer_name_matches_certificate_name(), converting the ASN1_STRING
@@ -997,9 +971,6 @@ initialize_SSL(PGconn *conn)
 		SSL_CTX_set_default_passwd_cb_userdata(SSL_context, conn);
 	}
 
-	/* Set up a certificate selection callback. */
-	SSL_CTX_set_cert_cb(SSL_context, cert_cb, conn);
-
 	/* Disable old protocol versions */
 	SSL_CTX_set_options(SSL_context, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
 
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 62554fedde..6216af7f87 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -456,7 +456,6 @@ struct pg_conn
 	char	   *write_err_msg;	/* write error message, or NULL if OOM */
 
 	uint32		allowed_auth_methods;	/* bitmask of acceptable AuthRequest codes */
-	bool		allowed_auth_method_cert;	/* tracks "cert" which has no AuthRequest code */
 
 	/* Transient state needed while establishing connection */
 	PGTargetServerType target_server_type;	/* desired session properties */
@@ -522,8 +521,6 @@ struct pg_conn
 
 	/* SSL structures */
 	bool		ssl_in_use;
-	bool		ssl_cert_requested;	/* Did the server ask us for a cert? */
-	bool		ssl_cert_sent;		/* Did we send one in reply? */
 
 #ifdef USE_SSL
 	bool		allow_ssl_try;	/* Allowed to try SSL negotiation */
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index dab50774c4..dd27092134 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -83,9 +83,6 @@ test_role($node, 'md5_role', 'trust', 0,
 	log_unlike => [qr/connection authenticated:/]);
 
 # All require_auth options should fail.
-$node->connect_fails("user=scram_role require_auth=cert",
-	"certificate authentication can be required: fails with trust auth",
-	expected_stderr => qr/server did not request a certificate/);
 $node->connect_fails("user=scram_role require_auth=gss",
 	"GSS authentication can be required: fails with trust auth",
 	expected_stderr => qr/server did not complete authentication/);
@@ -101,9 +98,9 @@ $node->connect_fails("user=scram_role require_auth=md5",
 $node->connect_fails("user=scram_role require_auth=scram-sha-256",
 	"SCRAM authentication can be required: fails with trust auth",
 	expected_stderr => qr/server did not complete authentication/);
-$node->connect_fails("user=scram_role require_auth=cert,scram-sha-256",
+$node->connect_fails("user=scram_role require_auth=password,scram-sha-256",
 	"multiple authentication types can be required: fails with trust auth",
-	expected_stderr => qr/server did not complete authentication or request a certificate/);
+	expected_stderr => qr/server did not complete authentication/);
 
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'password');
@@ -117,7 +114,7 @@ test_role($node, 'md5_role', 'password', 0,
 # require_auth should succeed with a plaintext password...
 $node->connect_ok("user=scram_role require_auth=password",
 	"password authentication can be required: works with password auth");
-$node->connect_ok("user=scram_role require_auth=cert,password,md5",
+$node->connect_ok("user=scram_role require_auth=scram-sha-256,password,md5",
 	"multiple authentication types can be required: works with password auth");
 
 # ...and fail for other auth types.
@@ -144,7 +141,7 @@ test_role($node, 'md5_role', 'scram-sha-256', 2,
 # require_auth should succeed with SCRAM...
 $node->connect_ok("user=scram_role require_auth=scram-sha-256",
 	"SCRAM authentication can be required: works with SCRAM auth");
-$node->connect_ok("user=scram_role require_auth=password,scram-sha-256,cert",
+$node->connect_ok("user=scram_role require_auth=password,scram-sha-256,md5",
 	"multiple authentication types can be required: works with SCRAM auth");
 
 # ...and fail for other auth types.
@@ -174,7 +171,7 @@ test_role($node, 'md5_role', 'md5', 0,
 # require_auth should succeed with MD5...
 $node->connect_ok("user=md5_role require_auth=md5",
 	"MD5 authentication can be required: works with MD5 auth");
-$node->connect_ok("user=md5_role require_auth=md5,scram-sha-256,cert",
+$node->connect_ok("user=md5_role require_auth=md5,scram-sha-256,password",
 	"multiple authentication types can be required: works with MD5 auth");
 
 # ...and fail for other auth types.
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 68a888e11a..c0b4a5739c 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -505,22 +505,6 @@ note "running server tests";
 $common_connstr =
   "$default_ssl_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=certdb hostaddr=$SERVERHOSTADDR host=localhost";
 
-# require_auth=cert should succeed against the certdb...
-$node->connect_ok(
-	"$common_connstr require_auth=cert user=ssltestuser sslcert=ssl/client.crt "
-	. sslkey('client.key'),
-	"certificate authentication can be required: works with cert auth");
-
-# ...and fail against the trustdb, if no certificate is provided.
-$node->connect_fails(
-	"$common_connstr require_auth=cert dbname=trustdb user=ssltestuser",
-	"certificate authentication can be required: fails with trust auth and no cert",
-	expected_stderr => qr/server accepted connection without a valid certificate/);
-$node->connect_fails(
-	"$common_connstr require_auth=scram-sha-256,cert dbname=trustdb user=ssltestuser",
-	"multiple authentication types can be required: fails with trust auth and no cert",
-	expected_stderr => qr/server did not complete authentication and accepted connection without a valid certificate/);
-
 # no client cert
 $node->connect_fails(
 	"$common_connstr user=ssltestuser sslcert=invalid",
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 588f47a39b..9957a96e69 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -132,4 +132,29 @@ $node->connect_ok(
 		qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/
 	]);
 
+# channel_binding should continue to function independently of require_auth.
+$node->connect_ok("$common_connstr user=ssltestuser channel_binding=disable require_auth=scram-sha-256",
+	"SCRAM with SSL, channel_binding=disable, and require_auth=scram-sha-256");
+$node->connect_fails(
+	"$common_connstr user=md5testuser require_auth=md5 channel_binding=require",
+	"channel_binding can fail even when require_auth succeeds",
+	expected_stderr =>
+	  qr/channel binding required but not supported by server's authentication request/
+);
+if ($supports_tls_server_end_point)
+{
+	$node->connect_ok(
+		"$common_connstr user=ssltestuser channel_binding=require require_auth=scram-sha-256",
+		"SCRAM with SSL, channel_binding=require, and require_auth=scram-sha-256");
+}
+else
+{
+	$node->connect_fails(
+		"$common_connstr user=ssltestuser channel_binding=require require_auth=scram-sha-256",
+		"SCRAM with SSL, channel_binding=require, and require_auth=scram-sha-256",
+		expected_stderr =>
+		  qr/channel binding is required, but server did not offer an authentication method that supports channel binding/
+	);
+}
+
 done_testing();
v3-0001-libpq-let-client-reject-unexpected-auth-methods.patchtext/x-patch; charset=US-ASCII; name=v3-0001-libpq-let-client-reject-unexpected-auth-methods.patchDownload
From 731dbc040a0efa908af978d743de1d1e14fd55c6 Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Mon, 28 Feb 2022 09:40:43 -0800
Subject: [PATCH v3] libpq: let client reject unexpected auth methods

The require_auth connection option allows the client to choose a list of
acceptable authentication types for use with the server. There is no
negotiation: if the server does not present one of the allowed
authentication requests, the connection fails.

Internally, the patch expands the role of check_expected_areq() to
ensure that the incoming request is compatible with conn->require_auth.
It also introduces a new flag, conn->client_finished_auth, which is set
by various authentication routines when the client side of the handshake
is finished. This signals to check_expected_areq() that an OK message
from the server is expected, and allows the client to complain if the
server forgoes authentication entirely.

(Since the client can't generally prove that the server is actually
doing the work of authentication, this last part is mostly useful for
SCRAM without channel binding. It could also provide a client with a
decent signal that, at the very least, it's not connecting to a database
with trust auth, and so the connection can be tied to the client in a
later audit.)

Deficiencies:
- This feature currently doesn't prevent unexpected certificate exchange
  or unexpected GSSAPI encryption.
- This is unlikely to be very forwards-compatible at the moment,
  especially with SASL/SCRAM.
- SSPI support is "implemented" but untested.
---
 doc/src/sgml/libpq.sgml                   |  80 +++++++++++++
 src/include/libpq/pqcomm.h                |   1 +
 src/interfaces/libpq/fe-auth-scram.c      |   1 +
 src/interfaces/libpq/fe-auth.c            | 133 ++++++++++++++++++++++
 src/interfaces/libpq/fe-connect.c         |  77 +++++++++++++
 src/interfaces/libpq/fe-secure-openssl.c  |   1 -
 src/interfaces/libpq/libpq-int.h          |   6 +
 src/test/authentication/t/001_password.pl |  62 ++++++++++
 src/test/kerberos/t/001_auth.pl           |  26 +++++
 src/test/ldap/t/001_auth.pl               |   6 +
 src/test/ssl/t/002_scram.pl               |  25 ++++
 11 files changed, 417 insertions(+), 1 deletion(-)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 37ec3cb4e5..00990ce47d 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1221,6 +1221,76 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-require-auth" xreflabel="require_auth">
+      <term><literal>require_auth</literal></term>
+      <listitem>
+      <para>
+        Specifies the authentication method that the client requires from the
+        server. If the server does not use the required method to authenticate
+        the client, or if the authentication handshake is not fully completed by
+        the server, the connection will fail. A comma-separated list of methods
+        may also be provided, of which the server must use exactly one in order
+        for the connection to succeed. By default, any authentication method is
+        accepted, and the server is free to skip authentication altogether.
+      </para>
+      <para>
+        The following methods may be specified:
+
+        <variablelist>
+         <varlistentry>
+          <term><literal>password</literal></term>
+          <listitem>
+           <para>
+            The server must request plaintext password authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>md5</literal></term>
+          <listitem>
+           <para>
+            The server must request MD5 hashed password authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>gss</literal></term>
+          <listitem>
+           <para>
+            The server must either request a Kerberos handshake via
+            <acronym>GSSAPI</acronym> or establish a
+            <acronym>GSS</acronym>-encrypted channel (see also
+            <xref linkend="libpq-connect-gssencmode" />).
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>sspi</literal></term>
+          <listitem>
+           <para>
+            The server must request Windows <acronym>SSPI</acronym>
+            authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>scram-sha-256</literal></term>
+          <listitem>
+           <para>
+            The server must successfully complete a SCRAM-SHA-256 authentication
+            exchange with the client.
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+      </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-channel-binding" xreflabel="channel_binding">
       <term><literal>channel_binding</literal></term>
       <listitem>
@@ -7761,6 +7831,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGREQUIREAUTH</envar></primary>
+      </indexterm>
+      <envar>PGREQUIREAUTH</envar> behaves the same as the <xref
+      linkend="libpq-connect-require-auth"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h
index b418283d5f..a47f7b2d91 100644
--- a/src/include/libpq/pqcomm.h
+++ b/src/include/libpq/pqcomm.h
@@ -161,6 +161,7 @@ extern PGDLLIMPORT bool Db_user_namespace;
 #define AUTH_REQ_SASL	   10	/* Begin SASL authentication */
 #define AUTH_REQ_SASL_CONT 11	/* Continue SASL authentication */
 #define AUTH_REQ_SASL_FIN  12	/* Final SASL message */
+#define AUTH_REQ_MAX	   AUTH_REQ_SASL_FIN	/* maximum AUTH_REQ_* value */
 
 typedef uint32 AuthRequest;
 
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index e616200704..d918e8b87f 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -289,6 +289,7 @@ scram_exchange(void *opaq, char *input, int inputlen,
 			}
 			*done = true;
 			state->state = FE_SCRAM_FINISHED;
+			state->conn->client_finished_auth = true;
 			break;
 
 		default:
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 0a072a36dc..59c3575cc3 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -138,7 +138,10 @@ pg_GSS_continue(PGconn *conn, int payloadlen)
 	}
 
 	if (maj_stat == GSS_S_COMPLETE)
+	{
+		conn->client_finished_auth = true;
 		gss_release_name(&lmin_s, &conn->gtarg_nam);
+	}
 
 	return STATUS_OK;
 }
@@ -328,6 +331,9 @@ pg_SSPI_continue(PGconn *conn, int payloadlen)
 		FreeContextBuffer(outbuf.pBuffers[0].pvBuffer);
 	}
 
+	if (r == SEC_E_OK)
+		conn->client_finished_auth = true;
+
 	/* Cleanup is handled by the code in freePGconn() */
 	return STATUS_OK;
 }
@@ -836,6 +842,44 @@ pg_password_sendauth(PGconn *conn, const char *password, AuthRequest areq)
 	return ret;
 }
 
+/*
+ * Translate an AuthRequest into a human-readable description.
+ */
+static const char *
+auth_description(AuthRequest areq)
+{
+	switch (areq)
+	{
+		case AUTH_REQ_PASSWORD:
+			return libpq_gettext("a cleartext password");
+		case AUTH_REQ_MD5:
+			return libpq_gettext("a hashed password");
+		case AUTH_REQ_GSS:
+		case AUTH_REQ_GSS_CONT:
+			return libpq_gettext("GSSAPI authentication");
+		case AUTH_REQ_SSPI:
+			return libpq_gettext("SSPI authentication");
+		case AUTH_REQ_SCM_CREDS:
+			return libpq_gettext("UNIX socket credentials");
+		case AUTH_REQ_SASL:
+		case AUTH_REQ_SASL_CONT:
+		case AUTH_REQ_SASL_FIN:
+			return libpq_gettext("SASL authentication");
+	}
+
+	return libpq_gettext("an unknown authentication type");
+}
+
+/*
+ * Convenience macro for checking the allowed_auth_methods bitmask. Caller must
+ * ensure that type is not greater than 31 (high bit of the bitmask).
+ */
+#define auth_allowed(conn, type) \
+	(((conn)->allowed_auth_methods & (1 << (type))) != 0)
+
+StaticAssertDecl(AUTH_REQ_MAX < CHAR_BIT * sizeof(((PGconn){0}).allowed_auth_methods),
+				 "AUTH_REQ_MAX overflows the allowed_auth_methods bitmask");
+
 /*
  * Verify that the authentication request is expected, given the connection
  * parameters. This is especially important when the client wishes to
@@ -845,6 +889,92 @@ static bool
 check_expected_areq(AuthRequest areq, PGconn *conn)
 {
 	bool		result = true;
+	char	   *reason = NULL;
+
+	/*
+	 * If the user required a specific auth method, or specified an allowed set,
+	 * then reject all others here, and make sure the server actually completes
+	 * an authentication exchange.
+	 */
+	if (conn->require_auth)
+	{
+		switch (areq)
+		{
+			case AUTH_REQ_OK:
+				/*
+				 * Check to make sure we've actually finished our exchange.
+				 */
+				if (conn->client_finished_auth)
+					break;
+
+				/*
+				 * No explicit authentication request was made by the server --
+				 * or perhaps it was made and not completed, in the case of
+				 * SCRAM -- but there is one special case to check. If the user
+				 * allowed "gss", then a GSS-encrypted channel also satisfies
+				 * the check.
+				 */
+#ifdef ENABLE_GSS
+				if (auth_allowed(conn, AUTH_REQ_GSS) && conn->gssenc)
+				{
+					/*
+					 * If implicit GSS auth has already been performed via GSS
+					 * encryption, we don't need to have performed an
+					 * AUTH_REQ_GSS exchange.
+					 *
+					 * TODO: check this assumption. What mutual auth guarantees
+					 * are made in this case?
+					 */
+				}
+				else
+#endif
+				{
+					reason = libpq_gettext("server did not complete authentication"),
+					result = false;
+				}
+
+				break;
+
+			case AUTH_REQ_PASSWORD:
+			case AUTH_REQ_MD5:
+			case AUTH_REQ_GSS:
+			case AUTH_REQ_SSPI:
+			case AUTH_REQ_GSS_CONT:
+			case AUTH_REQ_SASL:
+			case AUTH_REQ_SASL_CONT:
+			case AUTH_REQ_SASL_FIN:
+				/*
+				 * We don't handle these with the default case, to avoid
+				 * bit-shifting past the end of the allowed_auth_methods mask if
+				 * the server sends an unexpected AuthRequest.
+				 */
+				result = auth_allowed(conn, areq);
+				break;
+
+			default:
+				result = false;
+				break;
+		}
+	}
+
+	if (!result)
+	{
+		if (reason)
+		{
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("auth method \"%s\" requirement failed: %s\n"),
+							  conn->require_auth, reason);
+		}
+		else
+		{
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("auth method \"%s\" required, but server requested %s\n"),
+							  conn->require_auth,
+							  auth_description(areq));
+		}
+
+		return result;
+	}
 
 	/*
 	 * When channel_binding=require, we must protect against two cases: (1) we
@@ -1046,6 +1176,9 @@ pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn)
 										 "fe_sendauth: error sending password authentication\n");
 					return STATUS_ERROR;
 				}
+
+				/* We expect no further authentication requests. */
+				conn->client_finished_auth = true;
 				break;
 			}
 
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 6e936bbff3..f2579ff7ee 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -310,6 +310,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Require-Peer", "", 10,
 	offsetof(struct pg_conn, requirepeer)},
 
+	{"require_auth", "PGREQUIREAUTH", NULL, NULL,
+		"Require-Auth", "", 14, /* sizeof("scram-sha-256") == 14 */
+	offsetof(struct pg_conn, require_auth)},
+
 	{"ssl_min_protocol_version", "PGSSLMINPROTOCOLVERSION", "TLSv1.2", NULL,
 		"SSL-Minimum-Protocol-Version", "", 8,	/* sizeof("TLSv1.x") == 8 */
 	offsetof(struct pg_conn, ssl_min_protocol_version)},
@@ -1255,6 +1259,79 @@ connectOptions2(PGconn *conn)
 		}
 	}
 
+	/*
+	 * parse and validate require_auth option
+	 */
+	if (conn->require_auth)
+	{
+		char	   *s = conn->require_auth;
+		bool		more = true;
+
+		while (more)
+		{
+			char	   *method;
+			uint32		bits;
+
+			method = parse_comma_separated_list(&s, &more);
+			if (method == NULL)
+				goto oom_error;
+
+			if (strcmp(method, "password") == 0)
+			{
+				bits = (1 << AUTH_REQ_PASSWORD);
+			}
+			else if (strcmp(method, "md5") == 0)
+			{
+				bits = (1 << AUTH_REQ_MD5);
+			}
+			else if (strcmp(method, "gss") == 0)
+			{
+				bits = (1 << AUTH_REQ_GSS);
+				bits |= (1 << AUTH_REQ_GSS_CONT);
+			}
+			else if (strcmp(method, "sspi") == 0)
+			{
+				bits = (1 << AUTH_REQ_SSPI);
+				bits |= (1 << AUTH_REQ_GSS_CONT);
+			}
+			else if (strcmp(method, "scram-sha-256") == 0)
+			{
+				/* This currently assumes that SCRAM is the only SASL method. */
+				bits = (1 << AUTH_REQ_SASL);
+				bits |= (1 << AUTH_REQ_SASL_CONT);
+				bits |= (1 << AUTH_REQ_SASL_FIN);
+			}
+			else
+			{
+				conn->status = CONNECTION_BAD;
+				appendPQExpBuffer(&conn->errorMessage,
+								  libpq_gettext("invalid require_auth method: \"%s\""),
+								  method);
+
+				free(method);
+				return false;
+			}
+
+			/*
+			 * Sanity check; a duplicated method probably indicates a typo in a
+			 * setting where typos are extremely risky.
+			 */
+			if ((conn->allowed_auth_methods & bits) == bits)
+			{
+				conn->status = CONNECTION_BAD;
+				appendPQExpBuffer(&conn->errorMessage,
+								  libpq_gettext("require_auth method \"%s\" is specified more than once"),
+								  method);
+
+				free(method);
+				return false;
+			}
+
+			conn->allowed_auth_methods |= bits;
+			free(method);
+		}
+	}
+
 	/*
 	 * validate channel_binding option
 	 */
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 8117cbd40f..fc91cae7a2 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -477,7 +477,6 @@ verify_cb(int ok, X509_STORE_CTX *ctx)
 	return ok;
 }
 
-
 /*
  * OpenSSL-specific wrapper around
  * pq_verify_peer_name_matches_certificate_name(), converting the ASN1_STRING
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 3db6a17db4..6216af7f87 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -393,6 +393,7 @@ struct pg_conn
 	char	   *ssl_min_protocol_version;	/* minimum TLS protocol version */
 	char	   *ssl_max_protocol_version;	/* maximum TLS protocol version */
 	char	   *target_session_attrs;	/* desired session properties */
+	char	   *require_auth;	/* name of the expected auth method */
 
 	/* Optional file to write trace info to */
 	FILE	   *Pfdebug;
@@ -454,6 +455,8 @@ struct pg_conn
 	bool		write_failed;	/* have we had a write failure on sock? */
 	char	   *write_err_msg;	/* write error message, or NULL if OOM */
 
+	uint32		allowed_auth_methods;	/* bitmask of acceptable AuthRequest codes */
+
 	/* Transient state needed while establishing connection */
 	PGTargetServerType target_server_type;	/* desired session properties */
 	bool		try_next_addr;	/* time to advance to next address/host? */
@@ -509,6 +512,9 @@ struct pg_conn
 	bool		error_result;	/* do we need to make an ERROR result? */
 	PGresult   *next_result;	/* next result (used in single-row mode) */
 
+	bool		client_finished_auth; /* have we finished our half of the
+									   * authentication exchange? */
+
 	/* Assorted state for SASL, SSL, GSS, etc */
 	const pg_fe_sasl_mech *sasl;
 	void	   *sasl_state;
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 3e3079c824..dd27092134 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -82,6 +82,26 @@ test_role($node, 'scram_role', 'trust', 0,
 test_role($node, 'md5_role', 'trust', 0,
 	log_unlike => [qr/connection authenticated:/]);
 
+# All require_auth options should fail.
+$node->connect_fails("user=scram_role require_auth=gss",
+	"GSS authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=sspi",
+	"GSS authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=password",
+	"password authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=password,scram-sha-256",
+	"multiple authentication types can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'password');
 test_role($node, 'scram_role', 'password', 0,
@@ -91,6 +111,20 @@ test_role($node, 'md5_role', 'password', 0,
 	log_like =>
 	  [qr/connection authenticated: identity="md5_role" method=password/]);
 
+# require_auth should succeed with a plaintext password...
+$node->connect_ok("user=scram_role require_auth=password",
+	"password authentication can be required: works with password auth");
+$node->connect_ok("user=scram_role require_auth=scram-sha-256,password,md5",
+	"multiple authentication types can be required: works with password auth");
+
+# ...and fail for other auth types.
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+
 # For "scram-sha-256" method, user "scram_role" should be able to connect.
 reset_pg_hba($node, 'scram-sha-256');
 test_role(
@@ -104,6 +138,20 @@ test_role(
 test_role($node, 'md5_role', 'scram-sha-256', 2,
 	log_unlike => [qr/connection authenticated:/]);
 
+# require_auth should succeed with SCRAM...
+$node->connect_ok("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: works with SCRAM auth");
+$node->connect_ok("user=scram_role require_auth=password,scram-sha-256,md5",
+	"multiple authentication types can be required: works with SCRAM auth");
+
+# ...and fail for other auth types.
+$node->connect_fails("user=scram_role require_auth=password",
+	"password authentication can be required: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+
 # Test that bad passwords are rejected.
 $ENV{"PGPASSWORD"} = 'badpass';
 test_role($node, 'scram_role', 'scram-sha-256', 2,
@@ -120,6 +168,20 @@ test_role($node, 'md5_role', 'md5', 0,
 	log_like =>
 	  [qr/connection authenticated: identity="md5_role" method=md5/]);
 
+# require_auth should succeed with MD5...
+$node->connect_ok("user=md5_role require_auth=md5",
+	"MD5 authentication can be required: works with MD5 auth");
+$node->connect_ok("user=md5_role require_auth=md5,scram-sha-256,password",
+	"multiple authentication types can be required: works with MD5 auth");
+
+# ...and fail for other auth types.
+$node->connect_fails("user=md5_role require_auth=password",
+	"password authentication can be required: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+$node->connect_fails("user=md5_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+
 # Tests for channel binding without SSL.
 # Using the password authentication method; channel binding can't work
 reset_pg_hba($node, 'password');
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index 62e0542639..de4ddcbf69 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -307,6 +307,24 @@ test_query(
 	'gssencmode=require',
 	'sending 100K lines works');
 
+# require_auth=gss should succeed...
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=disable require_auth=gss",
+	"GSS authentication can be requested: works with GSS auth without encryption");
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss",
+	"GSS authentication can be requested: works with GSS auth with encryption");
+
+# ...and require_auth=sspi should fail.
+$node->connect_fails(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=disable require_auth=sspi",
+	"SSPI authentication can be requested: fails with GSS auth without encryption",
+	expected_stderr => qr/server requested GSSAPI authentication/);
+$node->connect_fails(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=sspi",
+	"SSPI authentication can be requested: fails with GSS auth with encryption",
+	expected_stderr => qr/server did not complete authentication/);
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
 	qq{hostgssenc all all $hostaddr/32 gss map=mymap});
@@ -335,6 +353,14 @@ test_access(
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable',
 	'fails with GSS encryption disabled and hostgssenc hba');
 
+# require_auth=gss should succeed.
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss",
+	"GSS authentication can be requested: works with GSS encryption");
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss,scram-sha-256",
+	"multiple authentication types can be requested: works with GSS encryption");
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
 	qq{hostnogssenc all all $hostaddr/32 gss map=mymap});
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 86dff8bd1f..e7c67920a1 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -216,6 +216,12 @@ test_access(
 		qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/
 	],);
 
+# require_auth=password should complete successfully; other methods should fail.
+$node->connect_ok("user=test1 require_auth=password",
+	"password authentication can be required: works with ldap auth");
+$node->connect_fails("user=test1 require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with ldap auth");
+
 note "search+bind";
 
 unlink($node->data_dir . '/pg_hba.conf');
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 588f47a39b..9957a96e69 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -132,4 +132,29 @@ $node->connect_ok(
 		qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/
 	]);
 
+# channel_binding should continue to function independently of require_auth.
+$node->connect_ok("$common_connstr user=ssltestuser channel_binding=disable require_auth=scram-sha-256",
+	"SCRAM with SSL, channel_binding=disable, and require_auth=scram-sha-256");
+$node->connect_fails(
+	"$common_connstr user=md5testuser require_auth=md5 channel_binding=require",
+	"channel_binding can fail even when require_auth succeeds",
+	expected_stderr =>
+	  qr/channel binding required but not supported by server's authentication request/
+);
+if ($supports_tls_server_end_point)
+{
+	$node->connect_ok(
+		"$common_connstr user=ssltestuser channel_binding=require require_auth=scram-sha-256",
+		"SCRAM with SSL, channel_binding=require, and require_auth=scram-sha-256");
+}
+else
+{
+	$node->connect_fails(
+		"$common_connstr user=ssltestuser channel_binding=require require_auth=scram-sha-256",
+		"SCRAM with SSL, channel_binding=require, and require_auth=scram-sha-256",
+		expected_stderr =>
+		  qr/channel binding is required, but server did not offer an authentication method that supports channel binding/
+	);
+}
+
 done_testing();
-- 
2.25.1

#16David G. Johnston
david.g.johnston@gmail.com
In reply to: Jacob Champion (#13)
Re: [PoC] Let libpq reject unexpected authentication requests

On Thu, Jun 9, 2022 at 4:30 PM Jacob Champion <jchampion@timescale.com>
wrote:

On Wed, Jun 8, 2022 at 9:58 PM Michael Paquier <michael@paquier.xyz>
wrote:

One
interesting case comes down to stuff like channel_binding=require
require_auth="md5,scram-sha-256", where I think that we should still
fail even if the server asks for MD5 and enforce an equivalent of an
AND grammar through the parameters. This reasoning limits the
dependencies between each parameter and treats the areas where these
are checked independently, which is what check_expected_areq() does
for channel binding. So that's more robust at the end.

Agreed.

That just makes me want to not implement OR'ing...

The existing set of individual parameters doesn't work as an API for
try-and-fallback.

Something like would be less problematic when it comes to setting multiple
related options:

--auth-try
"1;sslmode=require;channel_binding=require;method=scram-sha-256;sslcert=/tmp/machine.cert;sslkey=/tmp/machine.key"
--auth-try
"2;sslmode=require;method=cert;sslcert=/tmp/me.cert;sslkey=/tmp/me.key"
--auth-try "3;sslmode=prefer;method=md5"

Absent that radical idea, require_auth probably shouldn't change any
behavior that exists today without having specified require_auth and having
the chosen method happen anyway. So whatever happens today with an md5
password prompt while channel_binding is set to require (not in the mood
right now to figure out how to test that on a compiled against HEAD
instance).

David J.

#17Jacob Champion
jchampion@timescale.com
In reply to: David G. Johnston (#16)
Re: [PoC] Let libpq reject unexpected authentication requests

On Wed, Jun 22, 2022 at 5:56 PM David G. Johnston
<david.g.johnston@gmail.com> wrote:

That just makes me want to not implement OR'ing...

The existing set of individual parameters doesn't work as an API for try-and-fallback.

Something like would be less problematic when it comes to setting multiple related options:

--auth-try "1;sslmode=require;channel_binding=require;method=scram-sha-256;sslcert=/tmp/machine.cert;sslkey=/tmp/machine.key"
--auth-try "2;sslmode=require;method=cert;sslcert=/tmp/me.cert;sslkey=/tmp/me.key"
--auth-try "3;sslmode=prefer;method=md5"

I think that's a fair point, and your --auth-try example definitely
illustrates why having require_auth try to do everything is probably
not a viable strategy. My arguments for keeping OR in spite of that
are

- the default is effectively an OR of all available methods (plus "none");
- I think NOT is a important case in practice, which is effectively a
negative OR ("anything but this/these"); and
- not providing an explicit, positive OR to complement the above seems
like it would be a frustrating user experience once you want to get
just a little bit more creative.

It's also low-hanging fruit that doesn't require multiple connections
to the server per attempt (which I think your --auth-try proposal
might, if I understand it correctly).

Absent that radical idea, require_auth probably shouldn't change any behavior that exists today without having specified require_auth and having the chosen method happen anyway. So whatever happens today with an md5 password prompt while channel_binding is set to require (not in the mood right now to figure out how to test that on a compiled against HEAD instance).

I think the newest tests in v3 should enforce that, but let me know if
I've missed something.

--Jacob

#18Jacob Champion
jchampion@timescale.com
In reply to: Jacob Champion (#17)
2 attachment(s)
Re: [PoC] Let libpq reject unexpected authentication requests

On Thu, Jun 23, 2022 at 10:33 AM Jacob Champion <jchampion@timescale.com> wrote:

- I think NOT is a important case in practice, which is effectively a
negative OR ("anything but this/these")

Both NOT (via ! negation) and "none" are implemented in v4.

Examples:

# The server must use SCRAM.
require_auth=scram-sha-256
# The server must use SCRAM or Kerberos.
require_auth=scram-sha-256,gss,sspi
# The server may optionally use SCRAM.
require_auth=none,scram-sha-256
# The server must not use any application-level authentication.
require_auth=none
# The server may optionally use authentication, except plaintext
# passwords.
require_auth=!password
# The server may optionally use authentication, except weaker password
# challenges.
require_auth=!password,!md5
# The server must use an authentication method.
require_auth=!none
# The server must use a non-plaintext authentication method.
require_auth=!none,!password

Note that `require_auth=none,scram-sha-256` allows the server to
abandon a SCRAM exchange early, same as it can today. That might be a
bit surprising.

--Jacob

Attachments:

since-v3.diff.txttext/plain; charset=US-ASCII; name=since-v3.diff.txtDownload
commit dcae9a7c1cef593055c932c85244852a119b0313
Author: Jacob Champion <jchampion@timescale.com>
Date:   Thu Jun 23 15:38:12 2022 -0700

    squash! libpq: let client reject unexpected auth methods
    
    - Add "none" and method negation with "!".
    - Test unknown methods to round out coverage.
    
    TODO: should "none,scram-sha-256" allow an incomplete SCRAM handshake?

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 00990ce47d..cc831158b7 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1233,6 +1233,20 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
         for the connection to succeed. By default, any authentication method is
         accepted, and the server is free to skip authentication altogether.
       </para>
+      <para>
+        Methods may be negated with the addition of a <literal>!</literal>
+        prefix, in which case the server must <emphasis>not</emphasis> attempt
+        the listed method; any other method is accepted, and the server is free
+        not to authenticate the client at all. If a comma-separated list is
+        provided, the server may not attempt <emphasis>any</emphasis> of the
+        listed negated methods. Negated and non-negated forms may not be
+        combined in the same setting.
+      </para>
+      <para>
+        As a final special case, the <literal>none</literal> method requires the
+        server not to use an authentication challenge. (It may also be negated,
+        to require some form of authentication.)
+      </para>
       <para>
         The following methods may be specified:
 
@@ -1286,6 +1300,17 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
            </para>
           </listitem>
          </varlistentry>
+
+         <varlistentry>
+          <term><literal>none</literal></term>
+          <listitem>
+           <para>
+            The server must not prompt the client for an authentication
+            exchange. (This does not prohibit client certificate authentication
+			via TLS, nor GSS authentication via its encrypted transport.)
+           </para>
+          </listitem>
+         </varlistentry>
         </variablelist>
       </para>
       </listitem>
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 59c3575cc3..f789bc7ec3 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -902,9 +902,14 @@ check_expected_areq(AuthRequest areq, PGconn *conn)
 		{
 			case AUTH_REQ_OK:
 				/*
-				 * Check to make sure we've actually finished our exchange.
+				 * Check to make sure we've actually finished our exchange (or
+				 * else that the user has allowed an authentication-less
+				 * connection).
+				 *
+				 * TODO: how should !auth_required interact with an incomplete
+				 * SCRAM exchange?
 				 */
-				if (conn->client_finished_auth)
+				if (!conn->auth_required || conn->client_finished_auth)
 					break;
 
 				/*
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index f2579ff7ee..7d5bf337f6 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -1265,17 +1265,65 @@ connectOptions2(PGconn *conn)
 	if (conn->require_auth)
 	{
 		char	   *s = conn->require_auth;
-		bool		more = true;
+		bool		first, more;
+		bool		negated = false;
+
+		/*
+		 * By default, start from an empty set of allowed options and add to it.
+		 */
+		conn->auth_required = true;
+		conn->allowed_auth_methods = 0;
 
-		while (more)
+		for (first = true, more = true; more; first = false)
 		{
-			char	   *method;
+			char	   *method, *part;
 			uint32		bits;
 
-			method = parse_comma_separated_list(&s, &more);
-			if (method == NULL)
+			part = parse_comma_separated_list(&s, &more);
+			if (part == NULL)
 				goto oom_error;
 
+			/*
+			 * Check for negation, e.g. '!password'. If one element is negated,
+			 * they all have to be.
+			 */
+			method = part;
+			if (*method == '!')
+			{
+				if (first)
+				{
+					/*
+					 * Switch to a permissive set of allowed options, and
+					 * subtract from it.
+					 */
+					conn->auth_required = false;
+					conn->allowed_auth_methods = -1;
+				}
+				else if (!negated)
+				{
+					conn->status = CONNECTION_BAD;
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("negative require_auth method \"%s\" cannot be mixed with non-negative methods"),
+									  method);
+
+					free(part);
+					return false;
+				}
+
+				negated = true;
+				method++;
+			}
+			else if (negated)
+			{
+				conn->status = CONNECTION_BAD;
+				appendPQExpBuffer(&conn->errorMessage,
+								  libpq_gettext("require_auth method \"%s\" cannot be mixed with negative methods"),
+								  method);
+
+				free(part);
+				return false;
+			}
+
 			if (strcmp(method, "password") == 0)
 			{
 				bits = (1 << AUTH_REQ_PASSWORD);
@@ -1301,6 +1349,31 @@ connectOptions2(PGconn *conn)
 				bits |= (1 << AUTH_REQ_SASL_CONT);
 				bits |= (1 << AUTH_REQ_SASL_FIN);
 			}
+			else if (strcmp(method, "none") == 0)
+			{
+				/*
+				 * Special case: let the user explicitly allow (or disallow)
+				 * connections where the server does not send an explicit
+				 * authentication challenge, such as "trust" and "cert" auth.
+				 */
+				if (negated) /* "!none" */
+				{
+					if (conn->auth_required)
+						goto duplicate;
+
+					conn->auth_required = true;
+				}
+				else /* "none" */
+				{
+					if (!conn->auth_required)
+						goto duplicate;
+
+					conn->auth_required = false;
+				}
+
+				free(part);
+				continue; /* avoid the bitmask manipulation below */
+			}
 			else
 			{
 				conn->status = CONNECTION_BAD;
@@ -1308,27 +1381,41 @@ connectOptions2(PGconn *conn)
 								  libpq_gettext("invalid require_auth method: \"%s\""),
 								  method);
 
-				free(method);
+				free(part);
 				return false;
 			}
 
-			/*
-			 * Sanity check; a duplicated method probably indicates a typo in a
-			 * setting where typos are extremely risky.
-			 */
-			if ((conn->allowed_auth_methods & bits) == bits)
+			/* Update the bitmask. */
+			if (negated)
 			{
-				conn->status = CONNECTION_BAD;
-				appendPQExpBuffer(&conn->errorMessage,
-								  libpq_gettext("require_auth method \"%s\" is specified more than once"),
-								  method);
+				if ((conn->allowed_auth_methods & bits) == 0)
+					goto duplicate;
 
-				free(method);
-				return false;
+				conn->allowed_auth_methods &= ~bits;
 			}
+			else
+			{
+				if ((conn->allowed_auth_methods & bits) == bits)
+					goto duplicate;
+
+				conn->allowed_auth_methods |= bits;
+			}
+
+			free(part);
+			continue;
 
-			conn->allowed_auth_methods |= bits;
-			free(method);
+duplicate:
+			/*
+			 * A duplicated method probably indicates a typo in a setting where
+			 * typos are extremely risky.
+			 */
+			conn->status = CONNECTION_BAD;
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("require_auth method \"%s\" is specified more than once"),
+							  part);
+
+			free(part);
+			return false;
 		}
 	}
 
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 6216af7f87..3278196eea 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -455,6 +455,7 @@ struct pg_conn
 	bool		write_failed;	/* have we had a write failure on sock? */
 	char	   *write_err_msg;	/* write error message, or NULL if OOM */
 
+	bool		auth_required;	/* require an authentication challenge from the server? */
 	uint32		allowed_auth_methods;	/* bitmask of acceptable AuthRequest codes */
 
 	/* Transient state needed while establishing connection */
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index dd27092134..473a93d6db 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -82,12 +82,12 @@ test_role($node, 'scram_role', 'trust', 0,
 test_role($node, 'md5_role', 'trust', 0,
 	log_unlike => [qr/connection authenticated:/]);
 
-# All require_auth options should fail.
+# All positive require_auth options should fail...
 $node->connect_fails("user=scram_role require_auth=gss",
 	"GSS authentication can be required: fails with trust auth",
 	expected_stderr => qr/server did not complete authentication/);
 $node->connect_fails("user=scram_role require_auth=sspi",
-	"GSS authentication can be required: fails with trust auth",
+	"SSPI authentication can be required: fails with trust auth",
 	expected_stderr => qr/server did not complete authentication/);
 $node->connect_fails("user=scram_role require_auth=password",
 	"password authentication can be required: fails with trust auth",
@@ -102,6 +102,54 @@ $node->connect_fails("user=scram_role require_auth=password,scram-sha-256",
 	"multiple authentication types can be required: fails with trust auth",
 	expected_stderr => qr/server did not complete authentication/);
 
+# ...and negative require_auth options should succeed.
+$node->connect_ok("user=scram_role require_auth=!gss",
+	"GSS authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!sspi",
+	"SSPI authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!password",
+	"password authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!md5",
+	"md5 authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!scram-sha-256",
+	"SCRAM authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!password,!scram-sha-256",
+	"multiple authentication types can be forbidden: succeeds with trust auth");
+
+# require_auth=[!]none should interact correctly with trust auth.
+$node->connect_ok("user=scram_role require_auth=none",
+	"all authentication can be forbidden: succeeds with trust auth");
+$node->connect_fails("user=scram_role require_auth=!none",
+	"any authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+
+# Negative and positive require_auth options can't be mixed.
+$node->connect_fails("user=scram_role require_auth=scram-sha-256,!md5",
+	"negative require_auth methods can't be mixed with positive",
+	expected_stderr => qr/negative require_auth method "!md5" cannot be mixed with non-negative methods/);
+$node->connect_fails("user=scram_role require_auth=!password,scram-sha-256",
+	"positive require_auth methods can't be mixed with negative",
+	expected_stderr => qr/require_auth method "scram-sha-256" cannot be mixed with negative methods/);
+
+# require_auth methods can't be duplicated.
+$node->connect_fails("user=scram_role require_auth=password,md5,password",
+	"require_auth methods can't be duplicated: positive case",
+	expected_stderr => qr/require_auth method "password" is specified more than once/);
+$node->connect_fails("user=scram_role require_auth=!password,!md5,!password",
+	"require_auth methods can't be duplicated: negative case",
+	expected_stderr => qr/require_auth method "!password" is specified more than once/);
+$node->connect_fails("user=scram_role require_auth=none,md5,none",
+	"require_auth methods can't be duplicated: none case",
+	expected_stderr => qr/require_auth method "none" is specified more than once/);
+$node->connect_fails("user=scram_role require_auth=!none,!md5,!none",
+	"require_auth methods can't be duplicated: !none case",
+	expected_stderr => qr/require_auth method "!none" is specified more than once/);
+
+# Unknown require_auth methods are caught.
+$node->connect_fails("user=scram_role require_auth=none,abcdefg",
+	"unknown require_auth methods are rejected",
+	expected_stderr => qr/invalid require_auth method: "abcdefg"/);
+
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'password');
 test_role($node, 'scram_role', 'password', 0,
@@ -114,16 +162,29 @@ test_role($node, 'md5_role', 'password', 0,
 # require_auth should succeed with a plaintext password...
 $node->connect_ok("user=scram_role require_auth=password",
 	"password authentication can be required: works with password auth");
+$node->connect_ok("user=scram_role require_auth=!none",
+	"any authentication can be required: works with password auth");
 $node->connect_ok("user=scram_role require_auth=scram-sha-256,password,md5",
 	"multiple authentication types can be required: works with password auth");
 
-# ...and fail for other auth types.
+# ...fail for other auth types...
 $node->connect_fails("user=scram_role require_auth=md5",
 	"md5 authentication can be required: fails with password auth",
 	expected_stderr => qr/server requested a cleartext password/);
 $node->connect_fails("user=scram_role require_auth=scram-sha-256",
 	"SCRAM authentication can be required: fails with password auth",
 	expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails("user=scram_role require_auth=none",
+	"all authentication can be forbidden: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+
+# ...and allow password authentication to be prohibited.
+$node->connect_fails("user=scram_role require_auth=!password",
+	"password authentication can be forbidden: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails("user=scram_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types can be forbidden: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
 
 # For "scram-sha-256" method, user "scram_role" should be able to connect.
 reset_pg_hba($node, 'scram-sha-256');
@@ -141,16 +202,29 @@ test_role($node, 'md5_role', 'scram-sha-256', 2,
 # require_auth should succeed with SCRAM...
 $node->connect_ok("user=scram_role require_auth=scram-sha-256",
 	"SCRAM authentication can be required: works with SCRAM auth");
+$node->connect_ok("user=scram_role require_auth=!none",
+	"any authentication can be required: works with SCRAM auth");
 $node->connect_ok("user=scram_role require_auth=password,scram-sha-256,md5",
 	"multiple authentication types can be required: works with SCRAM auth");
 
-# ...and fail for other auth types.
+# ...fail for other auth types...
 $node->connect_fails("user=scram_role require_auth=password",
 	"password authentication can be required: fails with SCRAM auth",
 	expected_stderr => qr/server requested SASL authentication/);
 $node->connect_fails("user=scram_role require_auth=md5",
 	"md5 authentication can be required: fails with SCRAM auth",
 	expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails("user=scram_role require_auth=none",
+	"all authentication can be forbidden: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+
+# ...and allow SCRAM authentication to be prohibited.
+$node->connect_fails("user=scram_role require_auth=!scram-sha-256",
+	"SCRAM authentication can be forbidden: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails("user=scram_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types can be forbidden: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
 
 # Test that bad passwords are rejected.
 $ENV{"PGPASSWORD"} = 'badpass';
@@ -171,16 +245,29 @@ test_role($node, 'md5_role', 'md5', 0,
 # require_auth should succeed with MD5...
 $node->connect_ok("user=md5_role require_auth=md5",
 	"MD5 authentication can be required: works with MD5 auth");
+$node->connect_ok("user=md5_role require_auth=!none",
+	"any authentication can be required: works with MD5 auth");
 $node->connect_ok("user=md5_role require_auth=md5,scram-sha-256,password",
 	"multiple authentication types can be required: works with MD5 auth");
 
-# ...and fail for other auth types.
+# ...fail for other auth types...
 $node->connect_fails("user=md5_role require_auth=password",
 	"password authentication can be required: fails with MD5 auth",
 	expected_stderr => qr/server requested a hashed password/);
 $node->connect_fails("user=md5_role require_auth=scram-sha-256",
 	"SCRAM authentication can be required: fails with MD5 auth",
 	expected_stderr => qr/server requested a hashed password/);
+$node->connect_fails("user=md5_role require_auth=none",
+	"all authentication can be forbidden: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+
+# ...and allow MD5 authentication to be prohibited.
+$node->connect_fails("user=md5_role require_auth=!md5",
+	"password authentication can be forbidden: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+$node->connect_fails("user=md5_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types can be forbidden: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
 
 # Tests for channel binding without SSL.
 # Using the password authentication method; channel binding can't work
v4-0001-libpq-let-client-reject-unexpected-auth-methods.patchtext/x-patch; charset=US-ASCII; name=v4-0001-libpq-let-client-reject-unexpected-auth-methods.patchDownload
From fe629cbd21df83ac0065e9eb4ae88bffff19c429 Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Mon, 28 Feb 2022 09:40:43 -0800
Subject: [PATCH v4] libpq: let client reject unexpected auth methods

The require_auth connection option allows the client to choose a list of
acceptable authentication types for use with the server. There is no
negotiation: if the server does not present one of the allowed
authentication requests, the connection fails. Additionally, all methods
in the list may be negated, e.g. '!password', in which case the server
must NOT use the listed authentication type. The special method "none"
allows/disallows the use of unauthenticated connections (but it does not
govern transport-level authentication via TLS or GSSAPI).

Internally, the patch expands the role of check_expected_areq() to
ensure that the incoming request is compatible with conn->require_auth.
It also introduces a new flag, conn->client_finished_auth, which is set
by various authentication routines when the client side of the handshake
is finished. This signals to check_expected_areq() that an OK message
from the server is expected, and allows the client to complain if the
server forgoes authentication entirely.

(Since the client can't generally prove that the server is actually
doing the work of authentication, this last part is mostly useful for
SCRAM without channel binding. It could also provide a client with a
decent signal that, at the very least, it's not connecting to a database
with trust auth, and so the connection can be tied to the client in a
later audit.)

Deficiencies:
- This feature currently doesn't prevent unexpected certificate exchange
  or unexpected GSSAPI encryption.
- This is unlikely to be very forwards-compatible at the moment,
  especially with SASL/SCRAM.
- SSPI support is "implemented" but untested.
- require_auth=none,scram-sha-256 currently allows the server to leave a
  SCRAM exchange unfinished. This is not net-new behavior but may be
  surprising.
---
 doc/src/sgml/libpq.sgml                   | 105 ++++++++++++++
 src/include/libpq/pqcomm.h                |   1 +
 src/interfaces/libpq/fe-auth-scram.c      |   1 +
 src/interfaces/libpq/fe-auth.c            | 138 ++++++++++++++++++
 src/interfaces/libpq/fe-connect.c         | 164 ++++++++++++++++++++++
 src/interfaces/libpq/fe-secure-openssl.c  |   1 -
 src/interfaces/libpq/libpq-int.h          |   7 +
 src/test/authentication/t/001_password.pl | 149 ++++++++++++++++++++
 src/test/kerberos/t/001_auth.pl           |  26 ++++
 src/test/ldap/t/001_auth.pl               |   6 +
 src/test/ssl/t/002_scram.pl               |  25 ++++
 11 files changed, 622 insertions(+), 1 deletion(-)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 37ec3cb4e5..cc831158b7 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1221,6 +1221,101 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-require-auth" xreflabel="require_auth">
+      <term><literal>require_auth</literal></term>
+      <listitem>
+      <para>
+        Specifies the authentication method that the client requires from the
+        server. If the server does not use the required method to authenticate
+        the client, or if the authentication handshake is not fully completed by
+        the server, the connection will fail. A comma-separated list of methods
+        may also be provided, of which the server must use exactly one in order
+        for the connection to succeed. By default, any authentication method is
+        accepted, and the server is free to skip authentication altogether.
+      </para>
+      <para>
+        Methods may be negated with the addition of a <literal>!</literal>
+        prefix, in which case the server must <emphasis>not</emphasis> attempt
+        the listed method; any other method is accepted, and the server is free
+        not to authenticate the client at all. If a comma-separated list is
+        provided, the server may not attempt <emphasis>any</emphasis> of the
+        listed negated methods. Negated and non-negated forms may not be
+        combined in the same setting.
+      </para>
+      <para>
+        As a final special case, the <literal>none</literal> method requires the
+        server not to use an authentication challenge. (It may also be negated,
+        to require some form of authentication.)
+      </para>
+      <para>
+        The following methods may be specified:
+
+        <variablelist>
+         <varlistentry>
+          <term><literal>password</literal></term>
+          <listitem>
+           <para>
+            The server must request plaintext password authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>md5</literal></term>
+          <listitem>
+           <para>
+            The server must request MD5 hashed password authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>gss</literal></term>
+          <listitem>
+           <para>
+            The server must either request a Kerberos handshake via
+            <acronym>GSSAPI</acronym> or establish a
+            <acronym>GSS</acronym>-encrypted channel (see also
+            <xref linkend="libpq-connect-gssencmode" />).
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>sspi</literal></term>
+          <listitem>
+           <para>
+            The server must request Windows <acronym>SSPI</acronym>
+            authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>scram-sha-256</literal></term>
+          <listitem>
+           <para>
+            The server must successfully complete a SCRAM-SHA-256 authentication
+            exchange with the client.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>none</literal></term>
+          <listitem>
+           <para>
+            The server must not prompt the client for an authentication
+            exchange. (This does not prohibit client certificate authentication
+			via TLS, nor GSS authentication via its encrypted transport.)
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+      </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-channel-binding" xreflabel="channel_binding">
       <term><literal>channel_binding</literal></term>
       <listitem>
@@ -7761,6 +7856,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGREQUIREAUTH</envar></primary>
+      </indexterm>
+      <envar>PGREQUIREAUTH</envar> behaves the same as the <xref
+      linkend="libpq-connect-require-auth"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h
index b418283d5f..a47f7b2d91 100644
--- a/src/include/libpq/pqcomm.h
+++ b/src/include/libpq/pqcomm.h
@@ -161,6 +161,7 @@ extern PGDLLIMPORT bool Db_user_namespace;
 #define AUTH_REQ_SASL	   10	/* Begin SASL authentication */
 #define AUTH_REQ_SASL_CONT 11	/* Continue SASL authentication */
 #define AUTH_REQ_SASL_FIN  12	/* Final SASL message */
+#define AUTH_REQ_MAX	   AUTH_REQ_SASL_FIN	/* maximum AUTH_REQ_* value */
 
 typedef uint32 AuthRequest;
 
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index e616200704..d918e8b87f 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -289,6 +289,7 @@ scram_exchange(void *opaq, char *input, int inputlen,
 			}
 			*done = true;
 			state->state = FE_SCRAM_FINISHED;
+			state->conn->client_finished_auth = true;
 			break;
 
 		default:
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 0a072a36dc..f789bc7ec3 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -138,7 +138,10 @@ pg_GSS_continue(PGconn *conn, int payloadlen)
 	}
 
 	if (maj_stat == GSS_S_COMPLETE)
+	{
+		conn->client_finished_auth = true;
 		gss_release_name(&lmin_s, &conn->gtarg_nam);
+	}
 
 	return STATUS_OK;
 }
@@ -328,6 +331,9 @@ pg_SSPI_continue(PGconn *conn, int payloadlen)
 		FreeContextBuffer(outbuf.pBuffers[0].pvBuffer);
 	}
 
+	if (r == SEC_E_OK)
+		conn->client_finished_auth = true;
+
 	/* Cleanup is handled by the code in freePGconn() */
 	return STATUS_OK;
 }
@@ -836,6 +842,44 @@ pg_password_sendauth(PGconn *conn, const char *password, AuthRequest areq)
 	return ret;
 }
 
+/*
+ * Translate an AuthRequest into a human-readable description.
+ */
+static const char *
+auth_description(AuthRequest areq)
+{
+	switch (areq)
+	{
+		case AUTH_REQ_PASSWORD:
+			return libpq_gettext("a cleartext password");
+		case AUTH_REQ_MD5:
+			return libpq_gettext("a hashed password");
+		case AUTH_REQ_GSS:
+		case AUTH_REQ_GSS_CONT:
+			return libpq_gettext("GSSAPI authentication");
+		case AUTH_REQ_SSPI:
+			return libpq_gettext("SSPI authentication");
+		case AUTH_REQ_SCM_CREDS:
+			return libpq_gettext("UNIX socket credentials");
+		case AUTH_REQ_SASL:
+		case AUTH_REQ_SASL_CONT:
+		case AUTH_REQ_SASL_FIN:
+			return libpq_gettext("SASL authentication");
+	}
+
+	return libpq_gettext("an unknown authentication type");
+}
+
+/*
+ * Convenience macro for checking the allowed_auth_methods bitmask. Caller must
+ * ensure that type is not greater than 31 (high bit of the bitmask).
+ */
+#define auth_allowed(conn, type) \
+	(((conn)->allowed_auth_methods & (1 << (type))) != 0)
+
+StaticAssertDecl(AUTH_REQ_MAX < CHAR_BIT * sizeof(((PGconn){0}).allowed_auth_methods),
+				 "AUTH_REQ_MAX overflows the allowed_auth_methods bitmask");
+
 /*
  * Verify that the authentication request is expected, given the connection
  * parameters. This is especially important when the client wishes to
@@ -845,6 +889,97 @@ static bool
 check_expected_areq(AuthRequest areq, PGconn *conn)
 {
 	bool		result = true;
+	char	   *reason = NULL;
+
+	/*
+	 * If the user required a specific auth method, or specified an allowed set,
+	 * then reject all others here, and make sure the server actually completes
+	 * an authentication exchange.
+	 */
+	if (conn->require_auth)
+	{
+		switch (areq)
+		{
+			case AUTH_REQ_OK:
+				/*
+				 * Check to make sure we've actually finished our exchange (or
+				 * else that the user has allowed an authentication-less
+				 * connection).
+				 *
+				 * TODO: how should !auth_required interact with an incomplete
+				 * SCRAM exchange?
+				 */
+				if (!conn->auth_required || conn->client_finished_auth)
+					break;
+
+				/*
+				 * No explicit authentication request was made by the server --
+				 * or perhaps it was made and not completed, in the case of
+				 * SCRAM -- but there is one special case to check. If the user
+				 * allowed "gss", then a GSS-encrypted channel also satisfies
+				 * the check.
+				 */
+#ifdef ENABLE_GSS
+				if (auth_allowed(conn, AUTH_REQ_GSS) && conn->gssenc)
+				{
+					/*
+					 * If implicit GSS auth has already been performed via GSS
+					 * encryption, we don't need to have performed an
+					 * AUTH_REQ_GSS exchange.
+					 *
+					 * TODO: check this assumption. What mutual auth guarantees
+					 * are made in this case?
+					 */
+				}
+				else
+#endif
+				{
+					reason = libpq_gettext("server did not complete authentication"),
+					result = false;
+				}
+
+				break;
+
+			case AUTH_REQ_PASSWORD:
+			case AUTH_REQ_MD5:
+			case AUTH_REQ_GSS:
+			case AUTH_REQ_SSPI:
+			case AUTH_REQ_GSS_CONT:
+			case AUTH_REQ_SASL:
+			case AUTH_REQ_SASL_CONT:
+			case AUTH_REQ_SASL_FIN:
+				/*
+				 * We don't handle these with the default case, to avoid
+				 * bit-shifting past the end of the allowed_auth_methods mask if
+				 * the server sends an unexpected AuthRequest.
+				 */
+				result = auth_allowed(conn, areq);
+				break;
+
+			default:
+				result = false;
+				break;
+		}
+	}
+
+	if (!result)
+	{
+		if (reason)
+		{
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("auth method \"%s\" requirement failed: %s\n"),
+							  conn->require_auth, reason);
+		}
+		else
+		{
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("auth method \"%s\" required, but server requested %s\n"),
+							  conn->require_auth,
+							  auth_description(areq));
+		}
+
+		return result;
+	}
 
 	/*
 	 * When channel_binding=require, we must protect against two cases: (1) we
@@ -1046,6 +1181,9 @@ pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn)
 										 "fe_sendauth: error sending password authentication\n");
 					return STATUS_ERROR;
 				}
+
+				/* We expect no further authentication requests. */
+				conn->client_finished_auth = true;
 				break;
 			}
 
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 6e936bbff3..7d5bf337f6 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -310,6 +310,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Require-Peer", "", 10,
 	offsetof(struct pg_conn, requirepeer)},
 
+	{"require_auth", "PGREQUIREAUTH", NULL, NULL,
+		"Require-Auth", "", 14, /* sizeof("scram-sha-256") == 14 */
+	offsetof(struct pg_conn, require_auth)},
+
 	{"ssl_min_protocol_version", "PGSSLMINPROTOCOLVERSION", "TLSv1.2", NULL,
 		"SSL-Minimum-Protocol-Version", "", 8,	/* sizeof("TLSv1.x") == 8 */
 	offsetof(struct pg_conn, ssl_min_protocol_version)},
@@ -1255,6 +1259,166 @@ connectOptions2(PGconn *conn)
 		}
 	}
 
+	/*
+	 * parse and validate require_auth option
+	 */
+	if (conn->require_auth)
+	{
+		char	   *s = conn->require_auth;
+		bool		first, more;
+		bool		negated = false;
+
+		/*
+		 * By default, start from an empty set of allowed options and add to it.
+		 */
+		conn->auth_required = true;
+		conn->allowed_auth_methods = 0;
+
+		for (first = true, more = true; more; first = false)
+		{
+			char	   *method, *part;
+			uint32		bits;
+
+			part = parse_comma_separated_list(&s, &more);
+			if (part == NULL)
+				goto oom_error;
+
+			/*
+			 * Check for negation, e.g. '!password'. If one element is negated,
+			 * they all have to be.
+			 */
+			method = part;
+			if (*method == '!')
+			{
+				if (first)
+				{
+					/*
+					 * Switch to a permissive set of allowed options, and
+					 * subtract from it.
+					 */
+					conn->auth_required = false;
+					conn->allowed_auth_methods = -1;
+				}
+				else if (!negated)
+				{
+					conn->status = CONNECTION_BAD;
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("negative require_auth method \"%s\" cannot be mixed with non-negative methods"),
+									  method);
+
+					free(part);
+					return false;
+				}
+
+				negated = true;
+				method++;
+			}
+			else if (negated)
+			{
+				conn->status = CONNECTION_BAD;
+				appendPQExpBuffer(&conn->errorMessage,
+								  libpq_gettext("require_auth method \"%s\" cannot be mixed with negative methods"),
+								  method);
+
+				free(part);
+				return false;
+			}
+
+			if (strcmp(method, "password") == 0)
+			{
+				bits = (1 << AUTH_REQ_PASSWORD);
+			}
+			else if (strcmp(method, "md5") == 0)
+			{
+				bits = (1 << AUTH_REQ_MD5);
+			}
+			else if (strcmp(method, "gss") == 0)
+			{
+				bits = (1 << AUTH_REQ_GSS);
+				bits |= (1 << AUTH_REQ_GSS_CONT);
+			}
+			else if (strcmp(method, "sspi") == 0)
+			{
+				bits = (1 << AUTH_REQ_SSPI);
+				bits |= (1 << AUTH_REQ_GSS_CONT);
+			}
+			else if (strcmp(method, "scram-sha-256") == 0)
+			{
+				/* This currently assumes that SCRAM is the only SASL method. */
+				bits = (1 << AUTH_REQ_SASL);
+				bits |= (1 << AUTH_REQ_SASL_CONT);
+				bits |= (1 << AUTH_REQ_SASL_FIN);
+			}
+			else if (strcmp(method, "none") == 0)
+			{
+				/*
+				 * Special case: let the user explicitly allow (or disallow)
+				 * connections where the server does not send an explicit
+				 * authentication challenge, such as "trust" and "cert" auth.
+				 */
+				if (negated) /* "!none" */
+				{
+					if (conn->auth_required)
+						goto duplicate;
+
+					conn->auth_required = true;
+				}
+				else /* "none" */
+				{
+					if (!conn->auth_required)
+						goto duplicate;
+
+					conn->auth_required = false;
+				}
+
+				free(part);
+				continue; /* avoid the bitmask manipulation below */
+			}
+			else
+			{
+				conn->status = CONNECTION_BAD;
+				appendPQExpBuffer(&conn->errorMessage,
+								  libpq_gettext("invalid require_auth method: \"%s\""),
+								  method);
+
+				free(part);
+				return false;
+			}
+
+			/* Update the bitmask. */
+			if (negated)
+			{
+				if ((conn->allowed_auth_methods & bits) == 0)
+					goto duplicate;
+
+				conn->allowed_auth_methods &= ~bits;
+			}
+			else
+			{
+				if ((conn->allowed_auth_methods & bits) == bits)
+					goto duplicate;
+
+				conn->allowed_auth_methods |= bits;
+			}
+
+			free(part);
+			continue;
+
+duplicate:
+			/*
+			 * A duplicated method probably indicates a typo in a setting where
+			 * typos are extremely risky.
+			 */
+			conn->status = CONNECTION_BAD;
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("require_auth method \"%s\" is specified more than once"),
+							  part);
+
+			free(part);
+			return false;
+		}
+	}
+
 	/*
 	 * validate channel_binding option
 	 */
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 8117cbd40f..fc91cae7a2 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -477,7 +477,6 @@ verify_cb(int ok, X509_STORE_CTX *ctx)
 	return ok;
 }
 
-
 /*
  * OpenSSL-specific wrapper around
  * pq_verify_peer_name_matches_certificate_name(), converting the ASN1_STRING
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 3db6a17db4..3278196eea 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -393,6 +393,7 @@ struct pg_conn
 	char	   *ssl_min_protocol_version;	/* minimum TLS protocol version */
 	char	   *ssl_max_protocol_version;	/* maximum TLS protocol version */
 	char	   *target_session_attrs;	/* desired session properties */
+	char	   *require_auth;	/* name of the expected auth method */
 
 	/* Optional file to write trace info to */
 	FILE	   *Pfdebug;
@@ -454,6 +455,9 @@ struct pg_conn
 	bool		write_failed;	/* have we had a write failure on sock? */
 	char	   *write_err_msg;	/* write error message, or NULL if OOM */
 
+	bool		auth_required;	/* require an authentication challenge from the server? */
+	uint32		allowed_auth_methods;	/* bitmask of acceptable AuthRequest codes */
+
 	/* Transient state needed while establishing connection */
 	PGTargetServerType target_server_type;	/* desired session properties */
 	bool		try_next_addr;	/* time to advance to next address/host? */
@@ -509,6 +513,9 @@ struct pg_conn
 	bool		error_result;	/* do we need to make an ERROR result? */
 	PGresult   *next_result;	/* next result (used in single-row mode) */
 
+	bool		client_finished_auth; /* have we finished our half of the
+									   * authentication exchange? */
+
 	/* Assorted state for SASL, SSL, GSS, etc */
 	const pg_fe_sasl_mech *sasl;
 	void	   *sasl_state;
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 3e3079c824..473a93d6db 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -82,6 +82,74 @@ test_role($node, 'scram_role', 'trust', 0,
 test_role($node, 'md5_role', 'trust', 0,
 	log_unlike => [qr/connection authenticated:/]);
 
+# All positive require_auth options should fail...
+$node->connect_fails("user=scram_role require_auth=gss",
+	"GSS authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=sspi",
+	"SSPI authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=password",
+	"password authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=password,scram-sha-256",
+	"multiple authentication types can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+
+# ...and negative require_auth options should succeed.
+$node->connect_ok("user=scram_role require_auth=!gss",
+	"GSS authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!sspi",
+	"SSPI authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!password",
+	"password authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!md5",
+	"md5 authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!scram-sha-256",
+	"SCRAM authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!password,!scram-sha-256",
+	"multiple authentication types can be forbidden: succeeds with trust auth");
+
+# require_auth=[!]none should interact correctly with trust auth.
+$node->connect_ok("user=scram_role require_auth=none",
+	"all authentication can be forbidden: succeeds with trust auth");
+$node->connect_fails("user=scram_role require_auth=!none",
+	"any authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+
+# Negative and positive require_auth options can't be mixed.
+$node->connect_fails("user=scram_role require_auth=scram-sha-256,!md5",
+	"negative require_auth methods can't be mixed with positive",
+	expected_stderr => qr/negative require_auth method "!md5" cannot be mixed with non-negative methods/);
+$node->connect_fails("user=scram_role require_auth=!password,scram-sha-256",
+	"positive require_auth methods can't be mixed with negative",
+	expected_stderr => qr/require_auth method "scram-sha-256" cannot be mixed with negative methods/);
+
+# require_auth methods can't be duplicated.
+$node->connect_fails("user=scram_role require_auth=password,md5,password",
+	"require_auth methods can't be duplicated: positive case",
+	expected_stderr => qr/require_auth method "password" is specified more than once/);
+$node->connect_fails("user=scram_role require_auth=!password,!md5,!password",
+	"require_auth methods can't be duplicated: negative case",
+	expected_stderr => qr/require_auth method "!password" is specified more than once/);
+$node->connect_fails("user=scram_role require_auth=none,md5,none",
+	"require_auth methods can't be duplicated: none case",
+	expected_stderr => qr/require_auth method "none" is specified more than once/);
+$node->connect_fails("user=scram_role require_auth=!none,!md5,!none",
+	"require_auth methods can't be duplicated: !none case",
+	expected_stderr => qr/require_auth method "!none" is specified more than once/);
+
+# Unknown require_auth methods are caught.
+$node->connect_fails("user=scram_role require_auth=none,abcdefg",
+	"unknown require_auth methods are rejected",
+	expected_stderr => qr/invalid require_auth method: "abcdefg"/);
+
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'password');
 test_role($node, 'scram_role', 'password', 0,
@@ -91,6 +159,33 @@ test_role($node, 'md5_role', 'password', 0,
 	log_like =>
 	  [qr/connection authenticated: identity="md5_role" method=password/]);
 
+# require_auth should succeed with a plaintext password...
+$node->connect_ok("user=scram_role require_auth=password",
+	"password authentication can be required: works with password auth");
+$node->connect_ok("user=scram_role require_auth=!none",
+	"any authentication can be required: works with password auth");
+$node->connect_ok("user=scram_role require_auth=scram-sha-256,password,md5",
+	"multiple authentication types can be required: works with password auth");
+
+# ...fail for other auth types...
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails("user=scram_role require_auth=none",
+	"all authentication can be forbidden: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+
+# ...and allow password authentication to be prohibited.
+$node->connect_fails("user=scram_role require_auth=!password",
+	"password authentication can be forbidden: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails("user=scram_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types can be forbidden: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+
 # For "scram-sha-256" method, user "scram_role" should be able to connect.
 reset_pg_hba($node, 'scram-sha-256');
 test_role(
@@ -104,6 +199,33 @@ test_role(
 test_role($node, 'md5_role', 'scram-sha-256', 2,
 	log_unlike => [qr/connection authenticated:/]);
 
+# require_auth should succeed with SCRAM...
+$node->connect_ok("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: works with SCRAM auth");
+$node->connect_ok("user=scram_role require_auth=!none",
+	"any authentication can be required: works with SCRAM auth");
+$node->connect_ok("user=scram_role require_auth=password,scram-sha-256,md5",
+	"multiple authentication types can be required: works with SCRAM auth");
+
+# ...fail for other auth types...
+$node->connect_fails("user=scram_role require_auth=password",
+	"password authentication can be required: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails("user=scram_role require_auth=none",
+	"all authentication can be forbidden: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+
+# ...and allow SCRAM authentication to be prohibited.
+$node->connect_fails("user=scram_role require_auth=!scram-sha-256",
+	"SCRAM authentication can be forbidden: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails("user=scram_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types can be forbidden: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+
 # Test that bad passwords are rejected.
 $ENV{"PGPASSWORD"} = 'badpass';
 test_role($node, 'scram_role', 'scram-sha-256', 2,
@@ -120,6 +242,33 @@ test_role($node, 'md5_role', 'md5', 0,
 	log_like =>
 	  [qr/connection authenticated: identity="md5_role" method=md5/]);
 
+# require_auth should succeed with MD5...
+$node->connect_ok("user=md5_role require_auth=md5",
+	"MD5 authentication can be required: works with MD5 auth");
+$node->connect_ok("user=md5_role require_auth=!none",
+	"any authentication can be required: works with MD5 auth");
+$node->connect_ok("user=md5_role require_auth=md5,scram-sha-256,password",
+	"multiple authentication types can be required: works with MD5 auth");
+
+# ...fail for other auth types...
+$node->connect_fails("user=md5_role require_auth=password",
+	"password authentication can be required: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+$node->connect_fails("user=md5_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+$node->connect_fails("user=md5_role require_auth=none",
+	"all authentication can be forbidden: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+
+# ...and allow MD5 authentication to be prohibited.
+$node->connect_fails("user=md5_role require_auth=!md5",
+	"password authentication can be forbidden: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+$node->connect_fails("user=md5_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types can be forbidden: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+
 # Tests for channel binding without SSL.
 # Using the password authentication method; channel binding can't work
 reset_pg_hba($node, 'password');
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index 62e0542639..de4ddcbf69 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -307,6 +307,24 @@ test_query(
 	'gssencmode=require',
 	'sending 100K lines works');
 
+# require_auth=gss should succeed...
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=disable require_auth=gss",
+	"GSS authentication can be requested: works with GSS auth without encryption");
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss",
+	"GSS authentication can be requested: works with GSS auth with encryption");
+
+# ...and require_auth=sspi should fail.
+$node->connect_fails(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=disable require_auth=sspi",
+	"SSPI authentication can be requested: fails with GSS auth without encryption",
+	expected_stderr => qr/server requested GSSAPI authentication/);
+$node->connect_fails(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=sspi",
+	"SSPI authentication can be requested: fails with GSS auth with encryption",
+	expected_stderr => qr/server did not complete authentication/);
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
 	qq{hostgssenc all all $hostaddr/32 gss map=mymap});
@@ -335,6 +353,14 @@ test_access(
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable',
 	'fails with GSS encryption disabled and hostgssenc hba');
 
+# require_auth=gss should succeed.
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss",
+	"GSS authentication can be requested: works with GSS encryption");
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss,scram-sha-256",
+	"multiple authentication types can be requested: works with GSS encryption");
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
 	qq{hostnogssenc all all $hostaddr/32 gss map=mymap});
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 86dff8bd1f..e7c67920a1 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -216,6 +216,12 @@ test_access(
 		qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/
 	],);
 
+# require_auth=password should complete successfully; other methods should fail.
+$node->connect_ok("user=test1 require_auth=password",
+	"password authentication can be required: works with ldap auth");
+$node->connect_fails("user=test1 require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with ldap auth");
+
 note "search+bind";
 
 unlink($node->data_dir . '/pg_hba.conf');
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 588f47a39b..9957a96e69 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -132,4 +132,29 @@ $node->connect_ok(
 		qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/
 	]);
 
+# channel_binding should continue to function independently of require_auth.
+$node->connect_ok("$common_connstr user=ssltestuser channel_binding=disable require_auth=scram-sha-256",
+	"SCRAM with SSL, channel_binding=disable, and require_auth=scram-sha-256");
+$node->connect_fails(
+	"$common_connstr user=md5testuser require_auth=md5 channel_binding=require",
+	"channel_binding can fail even when require_auth succeeds",
+	expected_stderr =>
+	  qr/channel binding required but not supported by server's authentication request/
+);
+if ($supports_tls_server_end_point)
+{
+	$node->connect_ok(
+		"$common_connstr user=ssltestuser channel_binding=require require_auth=scram-sha-256",
+		"SCRAM with SSL, channel_binding=require, and require_auth=scram-sha-256");
+}
+else
+{
+	$node->connect_fails(
+		"$common_connstr user=ssltestuser channel_binding=require require_auth=scram-sha-256",
+		"SCRAM with SSL, channel_binding=require, and require_auth=scram-sha-256",
+		expected_stderr =>
+		  qr/channel binding is required, but server did not offer an authentication method that supports channel binding/
+	);
+}
+
 done_testing();
-- 
2.25.1

#19Jacob Champion
jchampion@timescale.com
In reply to: Jacob Champion (#18)
2 attachment(s)
Re: [PoC] Let libpq reject unexpected authentication requests

On Fri, Jun 24, 2022 at 12:17 PM Jacob Champion <jchampion@timescale.com> wrote:

Both NOT (via ! negation) and "none" are implemented in v4.

v5 adds a second patch which implements a client-certificate analogue
to gssencmode; I've named it sslcertmode. This takes the place of the
require_auth=[!]cert setting implemented previously.

As I mentioned upthread, I think sslcertmode=require is the weakest
feature here, since the server always sends a certificate request if
you are using TLS. It would potentially be more useful if we start
expanding TLS setups and middlebox options, but I still only see it as
a troubleshooting feature for administrators. By contrast,
sslcertmode=disable lets you turn off the use of the certificate, no
matter what libpq is able to find in your environment or home
directory. That seems more immediately useful.

With this addition, I'm wondering if GSS encrypted transport should be
removed from the definition/scope of require_auth=gss. We already have
gssencmode to control that, and it would remove an ugly special case
from the patch.

I'll add this patchset to the commitfest.

--Jacob

Attachments:

v5-0001-libpq-let-client-reject-unexpected-auth-methods.patchtext/x-patch; charset=US-ASCII; name=v5-0001-libpq-let-client-reject-unexpected-auth-methods.patchDownload
From 30739bd7e4c96f06101e3f235363a97c66adfced Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Mon, 28 Feb 2022 09:40:43 -0800
Subject: [PATCH v5 1/2] libpq: let client reject unexpected auth methods

The require_auth connection option allows the client to choose a list of
acceptable authentication types for use with the server. There is no
negotiation: if the server does not present one of the allowed
authentication requests, the connection fails. Additionally, all methods
in the list may be negated, e.g. '!password', in which case the server
must NOT use the listed authentication type. The special method "none"
allows/disallows the use of unauthenticated connections (but it does not
govern transport-level authentication via TLS or GSSAPI).

Internally, the patch expands the role of check_expected_areq() to
ensure that the incoming request is compatible with conn->require_auth.
It also introduces a new flag, conn->client_finished_auth, which is set
by various authentication routines when the client side of the handshake
is finished. This signals to check_expected_areq() that an OK message
from the server is expected, and allows the client to complain if the
server forgoes authentication entirely.

(Since the client can't generally prove that the server is actually
doing the work of authentication, this last part is mostly useful for
SCRAM without channel binding. It could also provide a client with a
decent signal that, at the very least, it's not connecting to a database
with trust auth, and so the connection can be tied to the client in a
later audit.)

Deficiencies:
- This feature currently doesn't prevent unexpected certificate exchange
  or unexpected GSSAPI encryption.
- This is unlikely to be very forwards-compatible at the moment,
  especially with SASL/SCRAM.
- SSPI support is "implemented" but untested.
- require_auth=none,scram-sha-256 currently allows the server to leave a
  SCRAM exchange unfinished. This is not net-new behavior but may be
  surprising.
---
 doc/src/sgml/libpq.sgml                   | 105 ++++++++++++++
 src/include/libpq/pqcomm.h                |   1 +
 src/interfaces/libpq/fe-auth-scram.c      |   1 +
 src/interfaces/libpq/fe-auth.c            | 138 ++++++++++++++++++
 src/interfaces/libpq/fe-connect.c         | 164 ++++++++++++++++++++++
 src/interfaces/libpq/fe-secure-openssl.c  |   1 -
 src/interfaces/libpq/libpq-int.h          |   7 +
 src/test/authentication/t/001_password.pl | 149 ++++++++++++++++++++
 src/test/kerberos/t/001_auth.pl           |  26 ++++
 src/test/ldap/t/001_auth.pl               |   6 +
 src/test/ssl/t/002_scram.pl               |  25 ++++
 11 files changed, 622 insertions(+), 1 deletion(-)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 37ec3cb4e5..cc831158b7 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1221,6 +1221,101 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-require-auth" xreflabel="require_auth">
+      <term><literal>require_auth</literal></term>
+      <listitem>
+      <para>
+        Specifies the authentication method that the client requires from the
+        server. If the server does not use the required method to authenticate
+        the client, or if the authentication handshake is not fully completed by
+        the server, the connection will fail. A comma-separated list of methods
+        may also be provided, of which the server must use exactly one in order
+        for the connection to succeed. By default, any authentication method is
+        accepted, and the server is free to skip authentication altogether.
+      </para>
+      <para>
+        Methods may be negated with the addition of a <literal>!</literal>
+        prefix, in which case the server must <emphasis>not</emphasis> attempt
+        the listed method; any other method is accepted, and the server is free
+        not to authenticate the client at all. If a comma-separated list is
+        provided, the server may not attempt <emphasis>any</emphasis> of the
+        listed negated methods. Negated and non-negated forms may not be
+        combined in the same setting.
+      </para>
+      <para>
+        As a final special case, the <literal>none</literal> method requires the
+        server not to use an authentication challenge. (It may also be negated,
+        to require some form of authentication.)
+      </para>
+      <para>
+        The following methods may be specified:
+
+        <variablelist>
+         <varlistentry>
+          <term><literal>password</literal></term>
+          <listitem>
+           <para>
+            The server must request plaintext password authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>md5</literal></term>
+          <listitem>
+           <para>
+            The server must request MD5 hashed password authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>gss</literal></term>
+          <listitem>
+           <para>
+            The server must either request a Kerberos handshake via
+            <acronym>GSSAPI</acronym> or establish a
+            <acronym>GSS</acronym>-encrypted channel (see also
+            <xref linkend="libpq-connect-gssencmode" />).
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>sspi</literal></term>
+          <listitem>
+           <para>
+            The server must request Windows <acronym>SSPI</acronym>
+            authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>scram-sha-256</literal></term>
+          <listitem>
+           <para>
+            The server must successfully complete a SCRAM-SHA-256 authentication
+            exchange with the client.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>none</literal></term>
+          <listitem>
+           <para>
+            The server must not prompt the client for an authentication
+            exchange. (This does not prohibit client certificate authentication
+			via TLS, nor GSS authentication via its encrypted transport.)
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+      </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-channel-binding" xreflabel="channel_binding">
       <term><literal>channel_binding</literal></term>
       <listitem>
@@ -7761,6 +7856,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGREQUIREAUTH</envar></primary>
+      </indexterm>
+      <envar>PGREQUIREAUTH</envar> behaves the same as the <xref
+      linkend="libpq-connect-require-auth"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h
index b418283d5f..a47f7b2d91 100644
--- a/src/include/libpq/pqcomm.h
+++ b/src/include/libpq/pqcomm.h
@@ -161,6 +161,7 @@ extern PGDLLIMPORT bool Db_user_namespace;
 #define AUTH_REQ_SASL	   10	/* Begin SASL authentication */
 #define AUTH_REQ_SASL_CONT 11	/* Continue SASL authentication */
 #define AUTH_REQ_SASL_FIN  12	/* Final SASL message */
+#define AUTH_REQ_MAX	   AUTH_REQ_SASL_FIN	/* maximum AUTH_REQ_* value */
 
 typedef uint32 AuthRequest;
 
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index e616200704..d918e8b87f 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -289,6 +289,7 @@ scram_exchange(void *opaq, char *input, int inputlen,
 			}
 			*done = true;
 			state->state = FE_SCRAM_FINISHED;
+			state->conn->client_finished_auth = true;
 			break;
 
 		default:
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 0a072a36dc..f789bc7ec3 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -138,7 +138,10 @@ pg_GSS_continue(PGconn *conn, int payloadlen)
 	}
 
 	if (maj_stat == GSS_S_COMPLETE)
+	{
+		conn->client_finished_auth = true;
 		gss_release_name(&lmin_s, &conn->gtarg_nam);
+	}
 
 	return STATUS_OK;
 }
@@ -328,6 +331,9 @@ pg_SSPI_continue(PGconn *conn, int payloadlen)
 		FreeContextBuffer(outbuf.pBuffers[0].pvBuffer);
 	}
 
+	if (r == SEC_E_OK)
+		conn->client_finished_auth = true;
+
 	/* Cleanup is handled by the code in freePGconn() */
 	return STATUS_OK;
 }
@@ -836,6 +842,44 @@ pg_password_sendauth(PGconn *conn, const char *password, AuthRequest areq)
 	return ret;
 }
 
+/*
+ * Translate an AuthRequest into a human-readable description.
+ */
+static const char *
+auth_description(AuthRequest areq)
+{
+	switch (areq)
+	{
+		case AUTH_REQ_PASSWORD:
+			return libpq_gettext("a cleartext password");
+		case AUTH_REQ_MD5:
+			return libpq_gettext("a hashed password");
+		case AUTH_REQ_GSS:
+		case AUTH_REQ_GSS_CONT:
+			return libpq_gettext("GSSAPI authentication");
+		case AUTH_REQ_SSPI:
+			return libpq_gettext("SSPI authentication");
+		case AUTH_REQ_SCM_CREDS:
+			return libpq_gettext("UNIX socket credentials");
+		case AUTH_REQ_SASL:
+		case AUTH_REQ_SASL_CONT:
+		case AUTH_REQ_SASL_FIN:
+			return libpq_gettext("SASL authentication");
+	}
+
+	return libpq_gettext("an unknown authentication type");
+}
+
+/*
+ * Convenience macro for checking the allowed_auth_methods bitmask. Caller must
+ * ensure that type is not greater than 31 (high bit of the bitmask).
+ */
+#define auth_allowed(conn, type) \
+	(((conn)->allowed_auth_methods & (1 << (type))) != 0)
+
+StaticAssertDecl(AUTH_REQ_MAX < CHAR_BIT * sizeof(((PGconn){0}).allowed_auth_methods),
+				 "AUTH_REQ_MAX overflows the allowed_auth_methods bitmask");
+
 /*
  * Verify that the authentication request is expected, given the connection
  * parameters. This is especially important when the client wishes to
@@ -845,6 +889,97 @@ static bool
 check_expected_areq(AuthRequest areq, PGconn *conn)
 {
 	bool		result = true;
+	char	   *reason = NULL;
+
+	/*
+	 * If the user required a specific auth method, or specified an allowed set,
+	 * then reject all others here, and make sure the server actually completes
+	 * an authentication exchange.
+	 */
+	if (conn->require_auth)
+	{
+		switch (areq)
+		{
+			case AUTH_REQ_OK:
+				/*
+				 * Check to make sure we've actually finished our exchange (or
+				 * else that the user has allowed an authentication-less
+				 * connection).
+				 *
+				 * TODO: how should !auth_required interact with an incomplete
+				 * SCRAM exchange?
+				 */
+				if (!conn->auth_required || conn->client_finished_auth)
+					break;
+
+				/*
+				 * No explicit authentication request was made by the server --
+				 * or perhaps it was made and not completed, in the case of
+				 * SCRAM -- but there is one special case to check. If the user
+				 * allowed "gss", then a GSS-encrypted channel also satisfies
+				 * the check.
+				 */
+#ifdef ENABLE_GSS
+				if (auth_allowed(conn, AUTH_REQ_GSS) && conn->gssenc)
+				{
+					/*
+					 * If implicit GSS auth has already been performed via GSS
+					 * encryption, we don't need to have performed an
+					 * AUTH_REQ_GSS exchange.
+					 *
+					 * TODO: check this assumption. What mutual auth guarantees
+					 * are made in this case?
+					 */
+				}
+				else
+#endif
+				{
+					reason = libpq_gettext("server did not complete authentication"),
+					result = false;
+				}
+
+				break;
+
+			case AUTH_REQ_PASSWORD:
+			case AUTH_REQ_MD5:
+			case AUTH_REQ_GSS:
+			case AUTH_REQ_SSPI:
+			case AUTH_REQ_GSS_CONT:
+			case AUTH_REQ_SASL:
+			case AUTH_REQ_SASL_CONT:
+			case AUTH_REQ_SASL_FIN:
+				/*
+				 * We don't handle these with the default case, to avoid
+				 * bit-shifting past the end of the allowed_auth_methods mask if
+				 * the server sends an unexpected AuthRequest.
+				 */
+				result = auth_allowed(conn, areq);
+				break;
+
+			default:
+				result = false;
+				break;
+		}
+	}
+
+	if (!result)
+	{
+		if (reason)
+		{
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("auth method \"%s\" requirement failed: %s\n"),
+							  conn->require_auth, reason);
+		}
+		else
+		{
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("auth method \"%s\" required, but server requested %s\n"),
+							  conn->require_auth,
+							  auth_description(areq));
+		}
+
+		return result;
+	}
 
 	/*
 	 * When channel_binding=require, we must protect against two cases: (1) we
@@ -1046,6 +1181,9 @@ pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn)
 										 "fe_sendauth: error sending password authentication\n");
 					return STATUS_ERROR;
 				}
+
+				/* We expect no further authentication requests. */
+				conn->client_finished_auth = true;
 				break;
 			}
 
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 6e936bbff3..7d5bf337f6 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -310,6 +310,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Require-Peer", "", 10,
 	offsetof(struct pg_conn, requirepeer)},
 
+	{"require_auth", "PGREQUIREAUTH", NULL, NULL,
+		"Require-Auth", "", 14, /* sizeof("scram-sha-256") == 14 */
+	offsetof(struct pg_conn, require_auth)},
+
 	{"ssl_min_protocol_version", "PGSSLMINPROTOCOLVERSION", "TLSv1.2", NULL,
 		"SSL-Minimum-Protocol-Version", "", 8,	/* sizeof("TLSv1.x") == 8 */
 	offsetof(struct pg_conn, ssl_min_protocol_version)},
@@ -1255,6 +1259,166 @@ connectOptions2(PGconn *conn)
 		}
 	}
 
+	/*
+	 * parse and validate require_auth option
+	 */
+	if (conn->require_auth)
+	{
+		char	   *s = conn->require_auth;
+		bool		first, more;
+		bool		negated = false;
+
+		/*
+		 * By default, start from an empty set of allowed options and add to it.
+		 */
+		conn->auth_required = true;
+		conn->allowed_auth_methods = 0;
+
+		for (first = true, more = true; more; first = false)
+		{
+			char	   *method, *part;
+			uint32		bits;
+
+			part = parse_comma_separated_list(&s, &more);
+			if (part == NULL)
+				goto oom_error;
+
+			/*
+			 * Check for negation, e.g. '!password'. If one element is negated,
+			 * they all have to be.
+			 */
+			method = part;
+			if (*method == '!')
+			{
+				if (first)
+				{
+					/*
+					 * Switch to a permissive set of allowed options, and
+					 * subtract from it.
+					 */
+					conn->auth_required = false;
+					conn->allowed_auth_methods = -1;
+				}
+				else if (!negated)
+				{
+					conn->status = CONNECTION_BAD;
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("negative require_auth method \"%s\" cannot be mixed with non-negative methods"),
+									  method);
+
+					free(part);
+					return false;
+				}
+
+				negated = true;
+				method++;
+			}
+			else if (negated)
+			{
+				conn->status = CONNECTION_BAD;
+				appendPQExpBuffer(&conn->errorMessage,
+								  libpq_gettext("require_auth method \"%s\" cannot be mixed with negative methods"),
+								  method);
+
+				free(part);
+				return false;
+			}
+
+			if (strcmp(method, "password") == 0)
+			{
+				bits = (1 << AUTH_REQ_PASSWORD);
+			}
+			else if (strcmp(method, "md5") == 0)
+			{
+				bits = (1 << AUTH_REQ_MD5);
+			}
+			else if (strcmp(method, "gss") == 0)
+			{
+				bits = (1 << AUTH_REQ_GSS);
+				bits |= (1 << AUTH_REQ_GSS_CONT);
+			}
+			else if (strcmp(method, "sspi") == 0)
+			{
+				bits = (1 << AUTH_REQ_SSPI);
+				bits |= (1 << AUTH_REQ_GSS_CONT);
+			}
+			else if (strcmp(method, "scram-sha-256") == 0)
+			{
+				/* This currently assumes that SCRAM is the only SASL method. */
+				bits = (1 << AUTH_REQ_SASL);
+				bits |= (1 << AUTH_REQ_SASL_CONT);
+				bits |= (1 << AUTH_REQ_SASL_FIN);
+			}
+			else if (strcmp(method, "none") == 0)
+			{
+				/*
+				 * Special case: let the user explicitly allow (or disallow)
+				 * connections where the server does not send an explicit
+				 * authentication challenge, such as "trust" and "cert" auth.
+				 */
+				if (negated) /* "!none" */
+				{
+					if (conn->auth_required)
+						goto duplicate;
+
+					conn->auth_required = true;
+				}
+				else /* "none" */
+				{
+					if (!conn->auth_required)
+						goto duplicate;
+
+					conn->auth_required = false;
+				}
+
+				free(part);
+				continue; /* avoid the bitmask manipulation below */
+			}
+			else
+			{
+				conn->status = CONNECTION_BAD;
+				appendPQExpBuffer(&conn->errorMessage,
+								  libpq_gettext("invalid require_auth method: \"%s\""),
+								  method);
+
+				free(part);
+				return false;
+			}
+
+			/* Update the bitmask. */
+			if (negated)
+			{
+				if ((conn->allowed_auth_methods & bits) == 0)
+					goto duplicate;
+
+				conn->allowed_auth_methods &= ~bits;
+			}
+			else
+			{
+				if ((conn->allowed_auth_methods & bits) == bits)
+					goto duplicate;
+
+				conn->allowed_auth_methods |= bits;
+			}
+
+			free(part);
+			continue;
+
+duplicate:
+			/*
+			 * A duplicated method probably indicates a typo in a setting where
+			 * typos are extremely risky.
+			 */
+			conn->status = CONNECTION_BAD;
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("require_auth method \"%s\" is specified more than once"),
+							  part);
+
+			free(part);
+			return false;
+		}
+	}
+
 	/*
 	 * validate channel_binding option
 	 */
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 8117cbd40f..fc91cae7a2 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -477,7 +477,6 @@ verify_cb(int ok, X509_STORE_CTX *ctx)
 	return ok;
 }
 
-
 /*
  * OpenSSL-specific wrapper around
  * pq_verify_peer_name_matches_certificate_name(), converting the ASN1_STRING
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 3db6a17db4..3278196eea 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -393,6 +393,7 @@ struct pg_conn
 	char	   *ssl_min_protocol_version;	/* minimum TLS protocol version */
 	char	   *ssl_max_protocol_version;	/* maximum TLS protocol version */
 	char	   *target_session_attrs;	/* desired session properties */
+	char	   *require_auth;	/* name of the expected auth method */
 
 	/* Optional file to write trace info to */
 	FILE	   *Pfdebug;
@@ -454,6 +455,9 @@ struct pg_conn
 	bool		write_failed;	/* have we had a write failure on sock? */
 	char	   *write_err_msg;	/* write error message, or NULL if OOM */
 
+	bool		auth_required;	/* require an authentication challenge from the server? */
+	uint32		allowed_auth_methods;	/* bitmask of acceptable AuthRequest codes */
+
 	/* Transient state needed while establishing connection */
 	PGTargetServerType target_server_type;	/* desired session properties */
 	bool		try_next_addr;	/* time to advance to next address/host? */
@@ -509,6 +513,9 @@ struct pg_conn
 	bool		error_result;	/* do we need to make an ERROR result? */
 	PGresult   *next_result;	/* next result (used in single-row mode) */
 
+	bool		client_finished_auth; /* have we finished our half of the
+									   * authentication exchange? */
+
 	/* Assorted state for SASL, SSL, GSS, etc */
 	const pg_fe_sasl_mech *sasl;
 	void	   *sasl_state;
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 3e3079c824..473a93d6db 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -82,6 +82,74 @@ test_role($node, 'scram_role', 'trust', 0,
 test_role($node, 'md5_role', 'trust', 0,
 	log_unlike => [qr/connection authenticated:/]);
 
+# All positive require_auth options should fail...
+$node->connect_fails("user=scram_role require_auth=gss",
+	"GSS authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=sspi",
+	"SSPI authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=password",
+	"password authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=password,scram-sha-256",
+	"multiple authentication types can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+
+# ...and negative require_auth options should succeed.
+$node->connect_ok("user=scram_role require_auth=!gss",
+	"GSS authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!sspi",
+	"SSPI authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!password",
+	"password authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!md5",
+	"md5 authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!scram-sha-256",
+	"SCRAM authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!password,!scram-sha-256",
+	"multiple authentication types can be forbidden: succeeds with trust auth");
+
+# require_auth=[!]none should interact correctly with trust auth.
+$node->connect_ok("user=scram_role require_auth=none",
+	"all authentication can be forbidden: succeeds with trust auth");
+$node->connect_fails("user=scram_role require_auth=!none",
+	"any authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+
+# Negative and positive require_auth options can't be mixed.
+$node->connect_fails("user=scram_role require_auth=scram-sha-256,!md5",
+	"negative require_auth methods can't be mixed with positive",
+	expected_stderr => qr/negative require_auth method "!md5" cannot be mixed with non-negative methods/);
+$node->connect_fails("user=scram_role require_auth=!password,scram-sha-256",
+	"positive require_auth methods can't be mixed with negative",
+	expected_stderr => qr/require_auth method "scram-sha-256" cannot be mixed with negative methods/);
+
+# require_auth methods can't be duplicated.
+$node->connect_fails("user=scram_role require_auth=password,md5,password",
+	"require_auth methods can't be duplicated: positive case",
+	expected_stderr => qr/require_auth method "password" is specified more than once/);
+$node->connect_fails("user=scram_role require_auth=!password,!md5,!password",
+	"require_auth methods can't be duplicated: negative case",
+	expected_stderr => qr/require_auth method "!password" is specified more than once/);
+$node->connect_fails("user=scram_role require_auth=none,md5,none",
+	"require_auth methods can't be duplicated: none case",
+	expected_stderr => qr/require_auth method "none" is specified more than once/);
+$node->connect_fails("user=scram_role require_auth=!none,!md5,!none",
+	"require_auth methods can't be duplicated: !none case",
+	expected_stderr => qr/require_auth method "!none" is specified more than once/);
+
+# Unknown require_auth methods are caught.
+$node->connect_fails("user=scram_role require_auth=none,abcdefg",
+	"unknown require_auth methods are rejected",
+	expected_stderr => qr/invalid require_auth method: "abcdefg"/);
+
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'password');
 test_role($node, 'scram_role', 'password', 0,
@@ -91,6 +159,33 @@ test_role($node, 'md5_role', 'password', 0,
 	log_like =>
 	  [qr/connection authenticated: identity="md5_role" method=password/]);
 
+# require_auth should succeed with a plaintext password...
+$node->connect_ok("user=scram_role require_auth=password",
+	"password authentication can be required: works with password auth");
+$node->connect_ok("user=scram_role require_auth=!none",
+	"any authentication can be required: works with password auth");
+$node->connect_ok("user=scram_role require_auth=scram-sha-256,password,md5",
+	"multiple authentication types can be required: works with password auth");
+
+# ...fail for other auth types...
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails("user=scram_role require_auth=none",
+	"all authentication can be forbidden: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+
+# ...and allow password authentication to be prohibited.
+$node->connect_fails("user=scram_role require_auth=!password",
+	"password authentication can be forbidden: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails("user=scram_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types can be forbidden: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+
 # For "scram-sha-256" method, user "scram_role" should be able to connect.
 reset_pg_hba($node, 'scram-sha-256');
 test_role(
@@ -104,6 +199,33 @@ test_role(
 test_role($node, 'md5_role', 'scram-sha-256', 2,
 	log_unlike => [qr/connection authenticated:/]);
 
+# require_auth should succeed with SCRAM...
+$node->connect_ok("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: works with SCRAM auth");
+$node->connect_ok("user=scram_role require_auth=!none",
+	"any authentication can be required: works with SCRAM auth");
+$node->connect_ok("user=scram_role require_auth=password,scram-sha-256,md5",
+	"multiple authentication types can be required: works with SCRAM auth");
+
+# ...fail for other auth types...
+$node->connect_fails("user=scram_role require_auth=password",
+	"password authentication can be required: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails("user=scram_role require_auth=none",
+	"all authentication can be forbidden: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+
+# ...and allow SCRAM authentication to be prohibited.
+$node->connect_fails("user=scram_role require_auth=!scram-sha-256",
+	"SCRAM authentication can be forbidden: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails("user=scram_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types can be forbidden: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+
 # Test that bad passwords are rejected.
 $ENV{"PGPASSWORD"} = 'badpass';
 test_role($node, 'scram_role', 'scram-sha-256', 2,
@@ -120,6 +242,33 @@ test_role($node, 'md5_role', 'md5', 0,
 	log_like =>
 	  [qr/connection authenticated: identity="md5_role" method=md5/]);
 
+# require_auth should succeed with MD5...
+$node->connect_ok("user=md5_role require_auth=md5",
+	"MD5 authentication can be required: works with MD5 auth");
+$node->connect_ok("user=md5_role require_auth=!none",
+	"any authentication can be required: works with MD5 auth");
+$node->connect_ok("user=md5_role require_auth=md5,scram-sha-256,password",
+	"multiple authentication types can be required: works with MD5 auth");
+
+# ...fail for other auth types...
+$node->connect_fails("user=md5_role require_auth=password",
+	"password authentication can be required: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+$node->connect_fails("user=md5_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+$node->connect_fails("user=md5_role require_auth=none",
+	"all authentication can be forbidden: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+
+# ...and allow MD5 authentication to be prohibited.
+$node->connect_fails("user=md5_role require_auth=!md5",
+	"password authentication can be forbidden: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+$node->connect_fails("user=md5_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types can be forbidden: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+
 # Tests for channel binding without SSL.
 # Using the password authentication method; channel binding can't work
 reset_pg_hba($node, 'password');
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index 62e0542639..de4ddcbf69 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -307,6 +307,24 @@ test_query(
 	'gssencmode=require',
 	'sending 100K lines works');
 
+# require_auth=gss should succeed...
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=disable require_auth=gss",
+	"GSS authentication can be requested: works with GSS auth without encryption");
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss",
+	"GSS authentication can be requested: works with GSS auth with encryption");
+
+# ...and require_auth=sspi should fail.
+$node->connect_fails(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=disable require_auth=sspi",
+	"SSPI authentication can be requested: fails with GSS auth without encryption",
+	expected_stderr => qr/server requested GSSAPI authentication/);
+$node->connect_fails(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=sspi",
+	"SSPI authentication can be requested: fails with GSS auth with encryption",
+	expected_stderr => qr/server did not complete authentication/);
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
 	qq{hostgssenc all all $hostaddr/32 gss map=mymap});
@@ -335,6 +353,14 @@ test_access(
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable',
 	'fails with GSS encryption disabled and hostgssenc hba');
 
+# require_auth=gss should succeed.
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss",
+	"GSS authentication can be requested: works with GSS encryption");
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss,scram-sha-256",
+	"multiple authentication types can be requested: works with GSS encryption");
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
 	qq{hostnogssenc all all $hostaddr/32 gss map=mymap});
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 86dff8bd1f..e7c67920a1 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -216,6 +216,12 @@ test_access(
 		qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/
 	],);
 
+# require_auth=password should complete successfully; other methods should fail.
+$node->connect_ok("user=test1 require_auth=password",
+	"password authentication can be required: works with ldap auth");
+$node->connect_fails("user=test1 require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with ldap auth");
+
 note "search+bind";
 
 unlink($node->data_dir . '/pg_hba.conf');
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 588f47a39b..9957a96e69 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -132,4 +132,29 @@ $node->connect_ok(
 		qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/
 	]);
 
+# channel_binding should continue to function independently of require_auth.
+$node->connect_ok("$common_connstr user=ssltestuser channel_binding=disable require_auth=scram-sha-256",
+	"SCRAM with SSL, channel_binding=disable, and require_auth=scram-sha-256");
+$node->connect_fails(
+	"$common_connstr user=md5testuser require_auth=md5 channel_binding=require",
+	"channel_binding can fail even when require_auth succeeds",
+	expected_stderr =>
+	  qr/channel binding required but not supported by server's authentication request/
+);
+if ($supports_tls_server_end_point)
+{
+	$node->connect_ok(
+		"$common_connstr user=ssltestuser channel_binding=require require_auth=scram-sha-256",
+		"SCRAM with SSL, channel_binding=require, and require_auth=scram-sha-256");
+}
+else
+{
+	$node->connect_fails(
+		"$common_connstr user=ssltestuser channel_binding=require require_auth=scram-sha-256",
+		"SCRAM with SSL, channel_binding=require, and require_auth=scram-sha-256",
+		expected_stderr =>
+		  qr/channel binding is required, but server did not offer an authentication method that supports channel binding/
+	);
+}
+
 done_testing();
-- 
2.25.1

v5-0002-Add-sslcertmode-option-for-client-certificates.patchtext/x-patch; charset=US-ASCII; name=v5-0002-Add-sslcertmode-option-for-client-certificates.patchDownload
From b56fae1e88517149039947e46927a6e85e48debd Mon Sep 17 00:00:00 2001
From: Jacob Champion <jchampion@timescale.com>
Date: Fri, 24 Jun 2022 15:40:42 -0700
Subject: [PATCH v5 2/2] Add sslcertmode option for client certificates

The sslcertmode option controls whether the server is allowed and/or
required to request a certificate from the client. There are three
modes:

- "allow" is the default and follows the current behavior -- a
  configured  sslcert is sent if the server requests one (which, with
  the current implementation, will happen whenever TLS is negotiated).

- "disable" causes the client to refuse to send a client certificate
  even if an sslcert is configured.

- "require" causes the client to fail if a client certificate is never
  sent and the server opens a connection anyway. This doesn't add any
  additional security, since there is no guarantee that the server is
  validating the certificate correctly, but it may help troubleshoot
  more complicated TLS setups.

sslcertmode=require needs the OpenSSL implementation to support
SSL_CTX_set_cert_cb(). Notably, LibreSSL does not.
---
 configure                                | 11 ++---
 configure.ac                             |  4 +-
 doc/src/sgml/libpq.sgml                  | 54 +++++++++++++++++++++++
 src/include/pg_config.h.in               |  3 ++
 src/interfaces/libpq/fe-auth.c           | 21 +++++++++
 src/interfaces/libpq/fe-connect.c        | 55 ++++++++++++++++++++++++
 src/interfaces/libpq/fe-secure-openssl.c | 40 ++++++++++++++++-
 src/interfaces/libpq/libpq-int.h         |  3 ++
 src/test/ssl/t/001_ssltests.pl           | 43 ++++++++++++++++++
 9 files changed, 226 insertions(+), 8 deletions(-)

diff --git a/configure b/configure
index 7dec6b7bf9..4deb9006ee 100755
--- a/configure
+++ b/configure
@@ -13044,13 +13044,14 @@ else
 fi
 
   fi
-  # Function introduced in OpenSSL 1.0.2.
-  for ac_func in X509_get_signature_nid
+  # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all of these.
+  for ac_func in X509_get_signature_nid SSL_CTX_set_cert_cb
 do :
-  ac_fn_c_check_func "$LINENO" "X509_get_signature_nid" "ac_cv_func_X509_get_signature_nid"
-if test "x$ac_cv_func_X509_get_signature_nid" = xyes; then :
+  as_ac_var=`$as_echo "ac_cv_func_$ac_func" | $as_tr_sh`
+ac_fn_c_check_func "$LINENO" "$ac_func" "$as_ac_var"
+if eval test \"x\$"$as_ac_var"\" = x"yes"; then :
   cat >>confdefs.h <<_ACEOF
-#define HAVE_X509_GET_SIGNATURE_NID 1
+#define `$as_echo "HAVE_$ac_func" | $as_tr_cpp` 1
 _ACEOF
 
 fi
diff --git a/configure.ac b/configure.ac
index d093fb88dd..e1d7409cbd 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1325,8 +1325,8 @@ if test "$with_ssl" = openssl ; then
      AC_SEARCH_LIBS(CRYPTO_new_ex_data, [eay32 crypto], [], [AC_MSG_ERROR([library 'eay32' or 'crypto' is required for OpenSSL])])
      AC_SEARCH_LIBS(SSL_new, [ssleay32 ssl], [], [AC_MSG_ERROR([library 'ssleay32' or 'ssl' is required for OpenSSL])])
   fi
-  # Function introduced in OpenSSL 1.0.2.
-  AC_CHECK_FUNCS([X509_get_signature_nid])
+  # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all of these.
+  AC_CHECK_FUNCS([X509_get_signature_nid SSL_CTX_set_cert_cb])
   # Functions introduced in OpenSSL 1.1.0. We used to check for
   # OPENSSL_VERSION_NUMBER, but that didn't work with 1.1.0, because LibreSSL
   # defines OPENSSL_VERSION_NUMBER to claim version 2.0.0, even though it
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index cc831158b7..b4c5dedccb 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1820,6 +1820,50 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-sslcertmode" xreflabel="sslcertmode">
+      <term><literal>sslcertmode</literal></term>
+      <listitem>
+       <para>
+        This option determines whether a client certificate may be sent to the
+        server, and whether the server is required to request one. There are
+        three modes:
+
+        <variablelist>
+         <varlistentry>
+          <term><literal>disable</literal></term>
+          <listitem>
+           <para>
+            a client certificate is never sent, even if one is provided via
+            <xref linkend="libpq-connect-sslcert" />
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>allow</literal> (default)</term>
+          <listitem>
+           <para>
+            a certificate may be sent, if the server requests one and it has
+            been provided via <literal>sslcert</literal>
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>require</literal></term>
+          <listitem>
+           <para>
+            the server <emphasis>must</emphasis> request a certificate. The
+            connection will fail if the server authenticates the client despite
+            not requesting or receiving one.
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-sslrootcert" xreflabel="sslrootcert">
       <term><literal>sslrootcert</literal></term>
       <listitem>
@@ -7973,6 +8017,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGSSLCERTMODE</envar></primary>
+      </indexterm>
+      <envar>PGSSLCERTMODE</envar> behaves the same as the <xref
+      linkend="libpq-connect-sslcertmode"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index cdd742cb55..0ecf40c6b3 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -514,6 +514,9 @@
 /* Define to 1 if you have spinlocks. */
 #undef HAVE_SPINLOCKS
 
+/* Define to 1 if you have the `SSL_CTX_set_cert_cb' function. */
+#undef HAVE_SSL_CTX_SET_CERT_CB
+
 /* Define to 1 if stdbool.h conforms to C99. */
 #undef HAVE_STDBOOL_H
 
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index f789bc7ec3..406440a201 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -891,6 +891,27 @@ check_expected_areq(AuthRequest areq, PGconn *conn)
 	bool		result = true;
 	char	   *reason = NULL;
 
+	if (conn->sslcertmode[0] == 'r' /* require */
+		&& areq == AUTH_REQ_OK)
+	{
+		/*
+		 * Trade off a little bit of complexity to try to get these error
+		 * messages as precise as possible.
+		 */
+		if (!conn->ssl_cert_requested)
+		{
+			appendPQExpBufferStr(&conn->errorMessage,
+								 libpq_gettext("server did not request a certificate"));
+			return false;
+		}
+		else if (!conn->ssl_cert_sent)
+		{
+			appendPQExpBufferStr(&conn->errorMessage,
+								 libpq_gettext("server accepted connection without a valid certificate"));
+			return false;
+		}
+	}
+
 	/*
 	 * If the user required a specific auth method, or specified an allowed set,
 	 * then reject all others here, and make sure the server actually completes
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 7d5bf337f6..92c5516abc 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -128,8 +128,10 @@ static int	ldapServiceLookup(const char *purl, PQconninfoOption *options,
 #define DefaultTargetSessionAttrs	"any"
 #ifdef USE_SSL
 #define DefaultSSLMode "prefer"
+#define DefaultSSLCertMode "allow"
 #else
 #define DefaultSSLMode	"disable"
+#define DefaultSSLCertMode "disable"
 #endif
 #ifdef ENABLE_GSS
 #include "fe-gssapi-common.h"
@@ -286,6 +288,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"SSL-Client-Key", "", 64,
 	offsetof(struct pg_conn, sslkey)},
 
+	{"sslcertmode", "PGSSLCERTMODE", NULL, NULL,
+		"SSL-Client-Cert-Mode", "", 8, /* sizeof("disable") == 8 */
+	offsetof(struct pg_conn, sslcertmode)},
+
 	{"sslpassword", NULL, NULL, NULL,
 		"SSL-Client-Key-Password", "*", 20,
 	offsetof(struct pg_conn, sslpassword)},
@@ -1529,6 +1535,55 @@ duplicate:
 		return false;
 	}
 
+	/*
+	 * validate sslcertmode option
+	 */
+	if (conn->sslcertmode)
+	{
+		if (strcmp(conn->sslcertmode, "disable") != 0 &&
+			strcmp(conn->sslcertmode, "allow") != 0 &&
+			strcmp(conn->sslcertmode, "require") != 0)
+		{
+			conn->status = CONNECTION_BAD;
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("invalid %s value: \"%s\"\n"),
+							  "sslcertmode",
+							  conn->sslcertmode);
+			return false;
+		}
+#ifndef USE_SSL
+		if (strcmp(conn->sslcertmode, "require") == 0)
+		{
+			conn->status = CONNECTION_BAD;
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("sslcertmode value \"%s\" invalid when SSL support is not compiled in\n"),
+							  conn->sslcertmode);
+			return false;
+		}
+#endif
+#ifndef HAVE_SSL_CTX_SET_CERT_CB
+		/*
+		 * Without a certificate callback, the current implementation can't
+		 * figure out if a certficate was actually requested, so "require" is
+		 * useless.
+		 */
+		if (strcmp(conn->sslcertmode, "require") == 0)
+		{
+			conn->status = CONNECTION_BAD;
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("sslcertmode value \"%s\" is not supported (check OpenSSL version)\n"),
+							  conn->sslcertmode);
+			return false;
+		}
+#endif
+	}
+	else
+	{
+		conn->sslcertmode = strdup(DefaultSSLCertMode);
+		if (!conn->sslcertmode)
+			goto oom_error;
+	}
+
 	/*
 	 * validate gssencmode option
 	 */
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index fc91cae7a2..13f6a28605 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -477,6 +477,34 @@ verify_cb(int ok, X509_STORE_CTX *ctx)
 	return ok;
 }
 
+#ifdef HAVE_SSL_CTX_SET_CERT_CB
+/*
+ * Certificate selection callback
+ *
+ * This callback lets us choose the client certificate we send to the server
+ * after seeing its CertificateRequest. We only support sending a single
+ * hard-coded certificate via sslcert, so we don't actually set any certificates
+ * here; we just it to record whether or not the server has actually asked for
+ * one and whether we have one to send.
+ */
+static int
+cert_cb(SSL *ssl, void *arg)
+{
+	PGconn *conn = arg;
+	conn->ssl_cert_requested = true;
+
+	/* Do we have a certificate loaded to send back? */
+	if (SSL_get_certificate(ssl))
+		conn->ssl_cert_sent = true;
+
+	/*
+	 * Tell OpenSSL that the callback succeeded; we're not required to actually
+	 * make any changes to the SSL handle.
+	 */
+	return 1;
+}
+#endif
+
 /*
  * OpenSSL-specific wrapper around
  * pq_verify_peer_name_matches_certificate_name(), converting the ASN1_STRING
@@ -971,6 +999,11 @@ initialize_SSL(PGconn *conn)
 		SSL_CTX_set_default_passwd_cb_userdata(SSL_context, conn);
 	}
 
+#ifdef HAVE_SSL_CTX_SET_CERT_CB
+	/* Set up a certificate selection callback. */
+	SSL_CTX_set_cert_cb(SSL_context, cert_cb, conn);
+#endif
+
 	/* Disable old protocol versions */
 	SSL_CTX_set_options(SSL_context, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
 
@@ -1134,7 +1167,12 @@ initialize_SSL(PGconn *conn)
 	else
 		fnbuf[0] = '\0';
 
-	if (fnbuf[0] == '\0')
+	if (conn->sslcertmode[0] == 'd') /* disable */
+	{
+		/* don't send a client cert even if we have one */
+		have_cert = false;
+	}
+	else if (fnbuf[0] == '\0')
 	{
 		/* no home directory, proceed without a client cert */
 		have_cert = false;
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 3278196eea..7c5989457a 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -381,6 +381,7 @@ struct pg_conn
 	char	   *sslkey;			/* client key filename */
 	char	   *sslcert;		/* client certificate filename */
 	char	   *sslpassword;	/* client key file password */
+	char	   *sslcertmode;	/* client cert mode (require,allow,disable) */
 	char	   *sslrootcert;	/* root certificate filename */
 	char	   *sslcrl;			/* certificate revocation list filename */
 	char	   *sslcrldir;		/* certificate revocation list directory name */
@@ -522,6 +523,8 @@ struct pg_conn
 
 	/* SSL structures */
 	bool		ssl_in_use;
+	bool		ssl_cert_requested;	/* Did the server ask us for a cert? */
+	bool		ssl_cert_sent;		/* Did we send one in reply? */
 
 #ifdef USE_SSL
 	bool		allow_ssl_try;	/* Allowed to try SSL negotiation */
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 707f4005af..357ac08110 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -38,6 +38,10 @@ my $SERVERHOSTADDR = '127.0.0.1';
 # This is the pattern to use in pg_hba.conf to match incoming connections.
 my $SERVERHOSTCIDR = '127.0.0.1/32';
 
+# Determine whether build supports sslcertmode=require.
+my $supports_sslcertmode_require =
+  check_pg_config("#define HAVE_SSL_CTX_SET_CERT_CB 1");
+
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
 
@@ -187,6 +191,22 @@ $node->connect_ok(
 	"$common_connstr sslrootcert=ssl/both-cas-2.crt sslmode=verify-ca",
 	"cert root file that contains two certificates, order 2");
 
+# sslcertmode=allow and =disable should both work without a client certificate.
+$node->connect_ok(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=disable",
+	"connect with sslcertmode=disable");
+$node->connect_ok(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=allow",
+	"connect with sslcertmode=allow");
+
+# sslcertmode=require, however, should fail.
+$node->connect_fails(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=require",
+	"connect with sslcertmode=require fails without a client certificate",
+	expected_stderr => $supports_sslcertmode_require
+		? qr/server accepted connection without a valid certificate/
+		: qr/sslcertmode value "require" is not supported/);
+
 # CRL tests
 
 # Invalid CRL filename is the same as no CRL, succeeds
@@ -534,6 +554,29 @@ $node->connect_ok(
 	"certificate authorization succeeds with correct client cert in encrypted DER format"
 );
 
+# correct client cert with required/allowed certificate authentication
+if ($supports_sslcertmode_require)
+{
+	$node->connect_ok(
+		"$common_connstr user=ssltestuser sslcertmode=require sslcert=ssl/client.crt "
+		  . sslkey('client.key'),
+		"certificate authorization succeeds with sslcertmode=require"
+	);
+}
+$node->connect_ok(
+	"$common_connstr user=ssltestuser sslcertmode=allow sslcert=ssl/client.crt "
+	  . sslkey('client.key'),
+	"certificate authorization succeeds with sslcertmode=allow"
+);
+
+# client cert isn't sent if certificate authentication is disabled
+$node->connect_fails(
+	"$common_connstr user=ssltestuser sslcertmode=disable sslcert=ssl/client.crt "
+	  . sslkey('client.key'),
+	"certificate authorization fails with sslcertmode=disable",
+	expected_stderr => qr/connection requires a valid client certificate/
+);
+
 # correct client cert in encrypted PEM with wrong password
 $node->connect_fails(
 	"$common_connstr user=ssltestuser sslcert=ssl/client.crt "
-- 
2.25.1

#20Jacob Champion
jchampion@timescale.com
In reply to: Jacob Champion (#19)
3 attachment(s)
Re: [PoC] Let libpq reject unexpected authentication requests

On Mon, Jun 27, 2022 at 12:05 PM Jacob Champion <jchampion@timescale.com> wrote:

v5 adds a second patch which implements a client-certificate analogue
to gssencmode; I've named it sslcertmode.

...and v6 fixes check-world, because I always forget about postgres_fdw.

--Jacob

Attachments:

since-v5.diff.txttext/plain; charset=US-ASCII; name=since-v5.diff.txtDownload
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 44457f930c..47df10119e 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -9529,7 +9529,7 @@ DO $d$
     END;
 $d$;
 ERROR:  invalid option "password"
-HINT:  Valid options in this context are: service, passfile, channel_binding, connect_timeout, dbname, host, hostaddr, port, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, tcp_user_timeout, sslmode, sslcompression, sslcert, sslkey, sslrootcert, sslcrl, sslcrldir, sslsni, requirepeer, ssl_min_protocol_version, ssl_max_protocol_version, gssencmode, krbsrvname, gsslib, target_session_attrs, use_remote_estimate, fdw_startup_cost, fdw_tuple_cost, extensions, updatable, truncatable, fetch_size, batch_size, async_capable, parallel_commit, keep_connections
+HINT:  Valid options in this context are: service, passfile, channel_binding, connect_timeout, dbname, host, hostaddr, port, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, tcp_user_timeout, sslmode, sslcompression, sslcert, sslkey, sslcertmode, sslrootcert, sslcrl, sslcrldir, sslsni, requirepeer, require_auth, ssl_min_protocol_version, ssl_max_protocol_version, gssencmode, krbsrvname, gsslib, target_session_attrs, use_remote_estimate, fdw_startup_cost, fdw_tuple_cost, extensions, updatable, truncatable, fetch_size, batch_size, async_capable, parallel_commit, keep_connections
 CONTEXT:  SQL statement "ALTER SERVER loopback_nopw OPTIONS (ADD password 'dummypw')"
 PL/pgSQL function inline_code_block line 3 at EXECUTE
 -- If we add a password for our user mapping instead, we should get a different
v6-0001-libpq-let-client-reject-unexpected-auth-methods.patchtext/x-patch; charset=US-ASCII; name=v6-0001-libpq-let-client-reject-unexpected-auth-methods.patchDownload
From 3973c6d89874c265d0c2187374baf72f968c294f Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Mon, 28 Feb 2022 09:40:43 -0800
Subject: [PATCH v6 1/2] libpq: let client reject unexpected auth methods

The require_auth connection option allows the client to choose a list of
acceptable authentication types for use with the server. There is no
negotiation: if the server does not present one of the allowed
authentication requests, the connection fails. Additionally, all methods
in the list may be negated, e.g. '!password', in which case the server
must NOT use the listed authentication type. The special method "none"
allows/disallows the use of unauthenticated connections (but it does not
govern transport-level authentication via TLS or GSSAPI).

Internally, the patch expands the role of check_expected_areq() to
ensure that the incoming request is compatible with conn->require_auth.
It also introduces a new flag, conn->client_finished_auth, which is set
by various authentication routines when the client side of the handshake
is finished. This signals to check_expected_areq() that an OK message
from the server is expected, and allows the client to complain if the
server forgoes authentication entirely.

(Since the client can't generally prove that the server is actually
doing the work of authentication, this last part is mostly useful for
SCRAM without channel binding. It could also provide a client with a
decent signal that, at the very least, it's not connecting to a database
with trust auth, and so the connection can be tied to the client in a
later audit.)

Deficiencies:
- This feature currently doesn't prevent unexpected certificate exchange
  or unexpected GSSAPI encryption.
- This is unlikely to be very forwards-compatible at the moment,
  especially with SASL/SCRAM.
- SSPI support is "implemented" but untested.
- require_auth=none,scram-sha-256 currently allows the server to leave a
  SCRAM exchange unfinished. This is not net-new behavior but may be
  surprising.
---
 .../postgres_fdw/expected/postgres_fdw.out    |   2 +-
 doc/src/sgml/libpq.sgml                       | 105 +++++++++++
 src/include/libpq/pqcomm.h                    |   1 +
 src/interfaces/libpq/fe-auth-scram.c          |   1 +
 src/interfaces/libpq/fe-auth.c                | 138 +++++++++++++++
 src/interfaces/libpq/fe-connect.c             | 164 ++++++++++++++++++
 src/interfaces/libpq/fe-secure-openssl.c      |   1 -
 src/interfaces/libpq/libpq-int.h              |   7 +
 src/test/authentication/t/001_password.pl     | 149 ++++++++++++++++
 src/test/kerberos/t/001_auth.pl               |  26 +++
 src/test/ldap/t/001_auth.pl                   |   6 +
 src/test/ssl/t/002_scram.pl                   |  25 +++
 12 files changed, 623 insertions(+), 2 deletions(-)

diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 44457f930c..493931e003 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -9529,7 +9529,7 @@ DO $d$
     END;
 $d$;
 ERROR:  invalid option "password"
-HINT:  Valid options in this context are: service, passfile, channel_binding, connect_timeout, dbname, host, hostaddr, port, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, tcp_user_timeout, sslmode, sslcompression, sslcert, sslkey, sslrootcert, sslcrl, sslcrldir, sslsni, requirepeer, ssl_min_protocol_version, ssl_max_protocol_version, gssencmode, krbsrvname, gsslib, target_session_attrs, use_remote_estimate, fdw_startup_cost, fdw_tuple_cost, extensions, updatable, truncatable, fetch_size, batch_size, async_capable, parallel_commit, keep_connections
+HINT:  Valid options in this context are: service, passfile, channel_binding, connect_timeout, dbname, host, hostaddr, port, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, tcp_user_timeout, sslmode, sslcompression, sslcert, sslkey, sslrootcert, sslcrl, sslcrldir, sslsni, requirepeer, require_auth, ssl_min_protocol_version, ssl_max_protocol_version, gssencmode, krbsrvname, gsslib, target_session_attrs, use_remote_estimate, fdw_startup_cost, fdw_tuple_cost, extensions, updatable, truncatable, fetch_size, batch_size, async_capable, parallel_commit, keep_connections
 CONTEXT:  SQL statement "ALTER SERVER loopback_nopw OPTIONS (ADD password 'dummypw')"
 PL/pgSQL function inline_code_block line 3 at EXECUTE
 -- If we add a password for our user mapping instead, we should get a different
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 37ec3cb4e5..cc831158b7 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1221,6 +1221,101 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-require-auth" xreflabel="require_auth">
+      <term><literal>require_auth</literal></term>
+      <listitem>
+      <para>
+        Specifies the authentication method that the client requires from the
+        server. If the server does not use the required method to authenticate
+        the client, or if the authentication handshake is not fully completed by
+        the server, the connection will fail. A comma-separated list of methods
+        may also be provided, of which the server must use exactly one in order
+        for the connection to succeed. By default, any authentication method is
+        accepted, and the server is free to skip authentication altogether.
+      </para>
+      <para>
+        Methods may be negated with the addition of a <literal>!</literal>
+        prefix, in which case the server must <emphasis>not</emphasis> attempt
+        the listed method; any other method is accepted, and the server is free
+        not to authenticate the client at all. If a comma-separated list is
+        provided, the server may not attempt <emphasis>any</emphasis> of the
+        listed negated methods. Negated and non-negated forms may not be
+        combined in the same setting.
+      </para>
+      <para>
+        As a final special case, the <literal>none</literal> method requires the
+        server not to use an authentication challenge. (It may also be negated,
+        to require some form of authentication.)
+      </para>
+      <para>
+        The following methods may be specified:
+
+        <variablelist>
+         <varlistentry>
+          <term><literal>password</literal></term>
+          <listitem>
+           <para>
+            The server must request plaintext password authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>md5</literal></term>
+          <listitem>
+           <para>
+            The server must request MD5 hashed password authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>gss</literal></term>
+          <listitem>
+           <para>
+            The server must either request a Kerberos handshake via
+            <acronym>GSSAPI</acronym> or establish a
+            <acronym>GSS</acronym>-encrypted channel (see also
+            <xref linkend="libpq-connect-gssencmode" />).
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>sspi</literal></term>
+          <listitem>
+           <para>
+            The server must request Windows <acronym>SSPI</acronym>
+            authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>scram-sha-256</literal></term>
+          <listitem>
+           <para>
+            The server must successfully complete a SCRAM-SHA-256 authentication
+            exchange with the client.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>none</literal></term>
+          <listitem>
+           <para>
+            The server must not prompt the client for an authentication
+            exchange. (This does not prohibit client certificate authentication
+			via TLS, nor GSS authentication via its encrypted transport.)
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+      </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-channel-binding" xreflabel="channel_binding">
       <term><literal>channel_binding</literal></term>
       <listitem>
@@ -7761,6 +7856,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGREQUIREAUTH</envar></primary>
+      </indexterm>
+      <envar>PGREQUIREAUTH</envar> behaves the same as the <xref
+      linkend="libpq-connect-require-auth"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h
index b418283d5f..a47f7b2d91 100644
--- a/src/include/libpq/pqcomm.h
+++ b/src/include/libpq/pqcomm.h
@@ -161,6 +161,7 @@ extern PGDLLIMPORT bool Db_user_namespace;
 #define AUTH_REQ_SASL	   10	/* Begin SASL authentication */
 #define AUTH_REQ_SASL_CONT 11	/* Continue SASL authentication */
 #define AUTH_REQ_SASL_FIN  12	/* Final SASL message */
+#define AUTH_REQ_MAX	   AUTH_REQ_SASL_FIN	/* maximum AUTH_REQ_* value */
 
 typedef uint32 AuthRequest;
 
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index e616200704..d918e8b87f 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -289,6 +289,7 @@ scram_exchange(void *opaq, char *input, int inputlen,
 			}
 			*done = true;
 			state->state = FE_SCRAM_FINISHED;
+			state->conn->client_finished_auth = true;
 			break;
 
 		default:
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 0a072a36dc..f789bc7ec3 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -138,7 +138,10 @@ pg_GSS_continue(PGconn *conn, int payloadlen)
 	}
 
 	if (maj_stat == GSS_S_COMPLETE)
+	{
+		conn->client_finished_auth = true;
 		gss_release_name(&lmin_s, &conn->gtarg_nam);
+	}
 
 	return STATUS_OK;
 }
@@ -328,6 +331,9 @@ pg_SSPI_continue(PGconn *conn, int payloadlen)
 		FreeContextBuffer(outbuf.pBuffers[0].pvBuffer);
 	}
 
+	if (r == SEC_E_OK)
+		conn->client_finished_auth = true;
+
 	/* Cleanup is handled by the code in freePGconn() */
 	return STATUS_OK;
 }
@@ -836,6 +842,44 @@ pg_password_sendauth(PGconn *conn, const char *password, AuthRequest areq)
 	return ret;
 }
 
+/*
+ * Translate an AuthRequest into a human-readable description.
+ */
+static const char *
+auth_description(AuthRequest areq)
+{
+	switch (areq)
+	{
+		case AUTH_REQ_PASSWORD:
+			return libpq_gettext("a cleartext password");
+		case AUTH_REQ_MD5:
+			return libpq_gettext("a hashed password");
+		case AUTH_REQ_GSS:
+		case AUTH_REQ_GSS_CONT:
+			return libpq_gettext("GSSAPI authentication");
+		case AUTH_REQ_SSPI:
+			return libpq_gettext("SSPI authentication");
+		case AUTH_REQ_SCM_CREDS:
+			return libpq_gettext("UNIX socket credentials");
+		case AUTH_REQ_SASL:
+		case AUTH_REQ_SASL_CONT:
+		case AUTH_REQ_SASL_FIN:
+			return libpq_gettext("SASL authentication");
+	}
+
+	return libpq_gettext("an unknown authentication type");
+}
+
+/*
+ * Convenience macro for checking the allowed_auth_methods bitmask. Caller must
+ * ensure that type is not greater than 31 (high bit of the bitmask).
+ */
+#define auth_allowed(conn, type) \
+	(((conn)->allowed_auth_methods & (1 << (type))) != 0)
+
+StaticAssertDecl(AUTH_REQ_MAX < CHAR_BIT * sizeof(((PGconn){0}).allowed_auth_methods),
+				 "AUTH_REQ_MAX overflows the allowed_auth_methods bitmask");
+
 /*
  * Verify that the authentication request is expected, given the connection
  * parameters. This is especially important when the client wishes to
@@ -845,6 +889,97 @@ static bool
 check_expected_areq(AuthRequest areq, PGconn *conn)
 {
 	bool		result = true;
+	char	   *reason = NULL;
+
+	/*
+	 * If the user required a specific auth method, or specified an allowed set,
+	 * then reject all others here, and make sure the server actually completes
+	 * an authentication exchange.
+	 */
+	if (conn->require_auth)
+	{
+		switch (areq)
+		{
+			case AUTH_REQ_OK:
+				/*
+				 * Check to make sure we've actually finished our exchange (or
+				 * else that the user has allowed an authentication-less
+				 * connection).
+				 *
+				 * TODO: how should !auth_required interact with an incomplete
+				 * SCRAM exchange?
+				 */
+				if (!conn->auth_required || conn->client_finished_auth)
+					break;
+
+				/*
+				 * No explicit authentication request was made by the server --
+				 * or perhaps it was made and not completed, in the case of
+				 * SCRAM -- but there is one special case to check. If the user
+				 * allowed "gss", then a GSS-encrypted channel also satisfies
+				 * the check.
+				 */
+#ifdef ENABLE_GSS
+				if (auth_allowed(conn, AUTH_REQ_GSS) && conn->gssenc)
+				{
+					/*
+					 * If implicit GSS auth has already been performed via GSS
+					 * encryption, we don't need to have performed an
+					 * AUTH_REQ_GSS exchange.
+					 *
+					 * TODO: check this assumption. What mutual auth guarantees
+					 * are made in this case?
+					 */
+				}
+				else
+#endif
+				{
+					reason = libpq_gettext("server did not complete authentication"),
+					result = false;
+				}
+
+				break;
+
+			case AUTH_REQ_PASSWORD:
+			case AUTH_REQ_MD5:
+			case AUTH_REQ_GSS:
+			case AUTH_REQ_SSPI:
+			case AUTH_REQ_GSS_CONT:
+			case AUTH_REQ_SASL:
+			case AUTH_REQ_SASL_CONT:
+			case AUTH_REQ_SASL_FIN:
+				/*
+				 * We don't handle these with the default case, to avoid
+				 * bit-shifting past the end of the allowed_auth_methods mask if
+				 * the server sends an unexpected AuthRequest.
+				 */
+				result = auth_allowed(conn, areq);
+				break;
+
+			default:
+				result = false;
+				break;
+		}
+	}
+
+	if (!result)
+	{
+		if (reason)
+		{
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("auth method \"%s\" requirement failed: %s\n"),
+							  conn->require_auth, reason);
+		}
+		else
+		{
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("auth method \"%s\" required, but server requested %s\n"),
+							  conn->require_auth,
+							  auth_description(areq));
+		}
+
+		return result;
+	}
 
 	/*
 	 * When channel_binding=require, we must protect against two cases: (1) we
@@ -1046,6 +1181,9 @@ pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn)
 										 "fe_sendauth: error sending password authentication\n");
 					return STATUS_ERROR;
 				}
+
+				/* We expect no further authentication requests. */
+				conn->client_finished_auth = true;
 				break;
 			}
 
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 6e936bbff3..7d5bf337f6 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -310,6 +310,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Require-Peer", "", 10,
 	offsetof(struct pg_conn, requirepeer)},
 
+	{"require_auth", "PGREQUIREAUTH", NULL, NULL,
+		"Require-Auth", "", 14, /* sizeof("scram-sha-256") == 14 */
+	offsetof(struct pg_conn, require_auth)},
+
 	{"ssl_min_protocol_version", "PGSSLMINPROTOCOLVERSION", "TLSv1.2", NULL,
 		"SSL-Minimum-Protocol-Version", "", 8,	/* sizeof("TLSv1.x") == 8 */
 	offsetof(struct pg_conn, ssl_min_protocol_version)},
@@ -1255,6 +1259,166 @@ connectOptions2(PGconn *conn)
 		}
 	}
 
+	/*
+	 * parse and validate require_auth option
+	 */
+	if (conn->require_auth)
+	{
+		char	   *s = conn->require_auth;
+		bool		first, more;
+		bool		negated = false;
+
+		/*
+		 * By default, start from an empty set of allowed options and add to it.
+		 */
+		conn->auth_required = true;
+		conn->allowed_auth_methods = 0;
+
+		for (first = true, more = true; more; first = false)
+		{
+			char	   *method, *part;
+			uint32		bits;
+
+			part = parse_comma_separated_list(&s, &more);
+			if (part == NULL)
+				goto oom_error;
+
+			/*
+			 * Check for negation, e.g. '!password'. If one element is negated,
+			 * they all have to be.
+			 */
+			method = part;
+			if (*method == '!')
+			{
+				if (first)
+				{
+					/*
+					 * Switch to a permissive set of allowed options, and
+					 * subtract from it.
+					 */
+					conn->auth_required = false;
+					conn->allowed_auth_methods = -1;
+				}
+				else if (!negated)
+				{
+					conn->status = CONNECTION_BAD;
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("negative require_auth method \"%s\" cannot be mixed with non-negative methods"),
+									  method);
+
+					free(part);
+					return false;
+				}
+
+				negated = true;
+				method++;
+			}
+			else if (negated)
+			{
+				conn->status = CONNECTION_BAD;
+				appendPQExpBuffer(&conn->errorMessage,
+								  libpq_gettext("require_auth method \"%s\" cannot be mixed with negative methods"),
+								  method);
+
+				free(part);
+				return false;
+			}
+
+			if (strcmp(method, "password") == 0)
+			{
+				bits = (1 << AUTH_REQ_PASSWORD);
+			}
+			else if (strcmp(method, "md5") == 0)
+			{
+				bits = (1 << AUTH_REQ_MD5);
+			}
+			else if (strcmp(method, "gss") == 0)
+			{
+				bits = (1 << AUTH_REQ_GSS);
+				bits |= (1 << AUTH_REQ_GSS_CONT);
+			}
+			else if (strcmp(method, "sspi") == 0)
+			{
+				bits = (1 << AUTH_REQ_SSPI);
+				bits |= (1 << AUTH_REQ_GSS_CONT);
+			}
+			else if (strcmp(method, "scram-sha-256") == 0)
+			{
+				/* This currently assumes that SCRAM is the only SASL method. */
+				bits = (1 << AUTH_REQ_SASL);
+				bits |= (1 << AUTH_REQ_SASL_CONT);
+				bits |= (1 << AUTH_REQ_SASL_FIN);
+			}
+			else if (strcmp(method, "none") == 0)
+			{
+				/*
+				 * Special case: let the user explicitly allow (or disallow)
+				 * connections where the server does not send an explicit
+				 * authentication challenge, such as "trust" and "cert" auth.
+				 */
+				if (negated) /* "!none" */
+				{
+					if (conn->auth_required)
+						goto duplicate;
+
+					conn->auth_required = true;
+				}
+				else /* "none" */
+				{
+					if (!conn->auth_required)
+						goto duplicate;
+
+					conn->auth_required = false;
+				}
+
+				free(part);
+				continue; /* avoid the bitmask manipulation below */
+			}
+			else
+			{
+				conn->status = CONNECTION_BAD;
+				appendPQExpBuffer(&conn->errorMessage,
+								  libpq_gettext("invalid require_auth method: \"%s\""),
+								  method);
+
+				free(part);
+				return false;
+			}
+
+			/* Update the bitmask. */
+			if (negated)
+			{
+				if ((conn->allowed_auth_methods & bits) == 0)
+					goto duplicate;
+
+				conn->allowed_auth_methods &= ~bits;
+			}
+			else
+			{
+				if ((conn->allowed_auth_methods & bits) == bits)
+					goto duplicate;
+
+				conn->allowed_auth_methods |= bits;
+			}
+
+			free(part);
+			continue;
+
+duplicate:
+			/*
+			 * A duplicated method probably indicates a typo in a setting where
+			 * typos are extremely risky.
+			 */
+			conn->status = CONNECTION_BAD;
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("require_auth method \"%s\" is specified more than once"),
+							  part);
+
+			free(part);
+			return false;
+		}
+	}
+
 	/*
 	 * validate channel_binding option
 	 */
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 8117cbd40f..fc91cae7a2 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -477,7 +477,6 @@ verify_cb(int ok, X509_STORE_CTX *ctx)
 	return ok;
 }
 
-
 /*
  * OpenSSL-specific wrapper around
  * pq_verify_peer_name_matches_certificate_name(), converting the ASN1_STRING
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 3db6a17db4..3278196eea 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -393,6 +393,7 @@ struct pg_conn
 	char	   *ssl_min_protocol_version;	/* minimum TLS protocol version */
 	char	   *ssl_max_protocol_version;	/* maximum TLS protocol version */
 	char	   *target_session_attrs;	/* desired session properties */
+	char	   *require_auth;	/* name of the expected auth method */
 
 	/* Optional file to write trace info to */
 	FILE	   *Pfdebug;
@@ -454,6 +455,9 @@ struct pg_conn
 	bool		write_failed;	/* have we had a write failure on sock? */
 	char	   *write_err_msg;	/* write error message, or NULL if OOM */
 
+	bool		auth_required;	/* require an authentication challenge from the server? */
+	uint32		allowed_auth_methods;	/* bitmask of acceptable AuthRequest codes */
+
 	/* Transient state needed while establishing connection */
 	PGTargetServerType target_server_type;	/* desired session properties */
 	bool		try_next_addr;	/* time to advance to next address/host? */
@@ -509,6 +513,9 @@ struct pg_conn
 	bool		error_result;	/* do we need to make an ERROR result? */
 	PGresult   *next_result;	/* next result (used in single-row mode) */
 
+	bool		client_finished_auth; /* have we finished our half of the
+									   * authentication exchange? */
+
 	/* Assorted state for SASL, SSL, GSS, etc */
 	const pg_fe_sasl_mech *sasl;
 	void	   *sasl_state;
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 3e3079c824..473a93d6db 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -82,6 +82,74 @@ test_role($node, 'scram_role', 'trust', 0,
 test_role($node, 'md5_role', 'trust', 0,
 	log_unlike => [qr/connection authenticated:/]);
 
+# All positive require_auth options should fail...
+$node->connect_fails("user=scram_role require_auth=gss",
+	"GSS authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=sspi",
+	"SSPI authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=password",
+	"password authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=password,scram-sha-256",
+	"multiple authentication types can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+
+# ...and negative require_auth options should succeed.
+$node->connect_ok("user=scram_role require_auth=!gss",
+	"GSS authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!sspi",
+	"SSPI authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!password",
+	"password authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!md5",
+	"md5 authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!scram-sha-256",
+	"SCRAM authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!password,!scram-sha-256",
+	"multiple authentication types can be forbidden: succeeds with trust auth");
+
+# require_auth=[!]none should interact correctly with trust auth.
+$node->connect_ok("user=scram_role require_auth=none",
+	"all authentication can be forbidden: succeeds with trust auth");
+$node->connect_fails("user=scram_role require_auth=!none",
+	"any authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+
+# Negative and positive require_auth options can't be mixed.
+$node->connect_fails("user=scram_role require_auth=scram-sha-256,!md5",
+	"negative require_auth methods can't be mixed with positive",
+	expected_stderr => qr/negative require_auth method "!md5" cannot be mixed with non-negative methods/);
+$node->connect_fails("user=scram_role require_auth=!password,scram-sha-256",
+	"positive require_auth methods can't be mixed with negative",
+	expected_stderr => qr/require_auth method "scram-sha-256" cannot be mixed with negative methods/);
+
+# require_auth methods can't be duplicated.
+$node->connect_fails("user=scram_role require_auth=password,md5,password",
+	"require_auth methods can't be duplicated: positive case",
+	expected_stderr => qr/require_auth method "password" is specified more than once/);
+$node->connect_fails("user=scram_role require_auth=!password,!md5,!password",
+	"require_auth methods can't be duplicated: negative case",
+	expected_stderr => qr/require_auth method "!password" is specified more than once/);
+$node->connect_fails("user=scram_role require_auth=none,md5,none",
+	"require_auth methods can't be duplicated: none case",
+	expected_stderr => qr/require_auth method "none" is specified more than once/);
+$node->connect_fails("user=scram_role require_auth=!none,!md5,!none",
+	"require_auth methods can't be duplicated: !none case",
+	expected_stderr => qr/require_auth method "!none" is specified more than once/);
+
+# Unknown require_auth methods are caught.
+$node->connect_fails("user=scram_role require_auth=none,abcdefg",
+	"unknown require_auth methods are rejected",
+	expected_stderr => qr/invalid require_auth method: "abcdefg"/);
+
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'password');
 test_role($node, 'scram_role', 'password', 0,
@@ -91,6 +159,33 @@ test_role($node, 'md5_role', 'password', 0,
 	log_like =>
 	  [qr/connection authenticated: identity="md5_role" method=password/]);
 
+# require_auth should succeed with a plaintext password...
+$node->connect_ok("user=scram_role require_auth=password",
+	"password authentication can be required: works with password auth");
+$node->connect_ok("user=scram_role require_auth=!none",
+	"any authentication can be required: works with password auth");
+$node->connect_ok("user=scram_role require_auth=scram-sha-256,password,md5",
+	"multiple authentication types can be required: works with password auth");
+
+# ...fail for other auth types...
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails("user=scram_role require_auth=none",
+	"all authentication can be forbidden: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+
+# ...and allow password authentication to be prohibited.
+$node->connect_fails("user=scram_role require_auth=!password",
+	"password authentication can be forbidden: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails("user=scram_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types can be forbidden: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+
 # For "scram-sha-256" method, user "scram_role" should be able to connect.
 reset_pg_hba($node, 'scram-sha-256');
 test_role(
@@ -104,6 +199,33 @@ test_role(
 test_role($node, 'md5_role', 'scram-sha-256', 2,
 	log_unlike => [qr/connection authenticated:/]);
 
+# require_auth should succeed with SCRAM...
+$node->connect_ok("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: works with SCRAM auth");
+$node->connect_ok("user=scram_role require_auth=!none",
+	"any authentication can be required: works with SCRAM auth");
+$node->connect_ok("user=scram_role require_auth=password,scram-sha-256,md5",
+	"multiple authentication types can be required: works with SCRAM auth");
+
+# ...fail for other auth types...
+$node->connect_fails("user=scram_role require_auth=password",
+	"password authentication can be required: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails("user=scram_role require_auth=none",
+	"all authentication can be forbidden: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+
+# ...and allow SCRAM authentication to be prohibited.
+$node->connect_fails("user=scram_role require_auth=!scram-sha-256",
+	"SCRAM authentication can be forbidden: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails("user=scram_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types can be forbidden: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+
 # Test that bad passwords are rejected.
 $ENV{"PGPASSWORD"} = 'badpass';
 test_role($node, 'scram_role', 'scram-sha-256', 2,
@@ -120,6 +242,33 @@ test_role($node, 'md5_role', 'md5', 0,
 	log_like =>
 	  [qr/connection authenticated: identity="md5_role" method=md5/]);
 
+# require_auth should succeed with MD5...
+$node->connect_ok("user=md5_role require_auth=md5",
+	"MD5 authentication can be required: works with MD5 auth");
+$node->connect_ok("user=md5_role require_auth=!none",
+	"any authentication can be required: works with MD5 auth");
+$node->connect_ok("user=md5_role require_auth=md5,scram-sha-256,password",
+	"multiple authentication types can be required: works with MD5 auth");
+
+# ...fail for other auth types...
+$node->connect_fails("user=md5_role require_auth=password",
+	"password authentication can be required: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+$node->connect_fails("user=md5_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+$node->connect_fails("user=md5_role require_auth=none",
+	"all authentication can be forbidden: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+
+# ...and allow MD5 authentication to be prohibited.
+$node->connect_fails("user=md5_role require_auth=!md5",
+	"password authentication can be forbidden: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+$node->connect_fails("user=md5_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types can be forbidden: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+
 # Tests for channel binding without SSL.
 # Using the password authentication method; channel binding can't work
 reset_pg_hba($node, 'password');
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index 62e0542639..de4ddcbf69 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -307,6 +307,24 @@ test_query(
 	'gssencmode=require',
 	'sending 100K lines works');
 
+# require_auth=gss should succeed...
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=disable require_auth=gss",
+	"GSS authentication can be requested: works with GSS auth without encryption");
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss",
+	"GSS authentication can be requested: works with GSS auth with encryption");
+
+# ...and require_auth=sspi should fail.
+$node->connect_fails(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=disable require_auth=sspi",
+	"SSPI authentication can be requested: fails with GSS auth without encryption",
+	expected_stderr => qr/server requested GSSAPI authentication/);
+$node->connect_fails(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=sspi",
+	"SSPI authentication can be requested: fails with GSS auth with encryption",
+	expected_stderr => qr/server did not complete authentication/);
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
 	qq{hostgssenc all all $hostaddr/32 gss map=mymap});
@@ -335,6 +353,14 @@ test_access(
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable',
 	'fails with GSS encryption disabled and hostgssenc hba');
 
+# require_auth=gss should succeed.
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss",
+	"GSS authentication can be requested: works with GSS encryption");
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss,scram-sha-256",
+	"multiple authentication types can be requested: works with GSS encryption");
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
 	qq{hostnogssenc all all $hostaddr/32 gss map=mymap});
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 86dff8bd1f..e7c67920a1 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -216,6 +216,12 @@ test_access(
 		qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/
 	],);
 
+# require_auth=password should complete successfully; other methods should fail.
+$node->connect_ok("user=test1 require_auth=password",
+	"password authentication can be required: works with ldap auth");
+$node->connect_fails("user=test1 require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with ldap auth");
+
 note "search+bind";
 
 unlink($node->data_dir . '/pg_hba.conf');
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 588f47a39b..9957a96e69 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -132,4 +132,29 @@ $node->connect_ok(
 		qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/
 	]);
 
+# channel_binding should continue to function independently of require_auth.
+$node->connect_ok("$common_connstr user=ssltestuser channel_binding=disable require_auth=scram-sha-256",
+	"SCRAM with SSL, channel_binding=disable, and require_auth=scram-sha-256");
+$node->connect_fails(
+	"$common_connstr user=md5testuser require_auth=md5 channel_binding=require",
+	"channel_binding can fail even when require_auth succeeds",
+	expected_stderr =>
+	  qr/channel binding required but not supported by server's authentication request/
+);
+if ($supports_tls_server_end_point)
+{
+	$node->connect_ok(
+		"$common_connstr user=ssltestuser channel_binding=require require_auth=scram-sha-256",
+		"SCRAM with SSL, channel_binding=require, and require_auth=scram-sha-256");
+}
+else
+{
+	$node->connect_fails(
+		"$common_connstr user=ssltestuser channel_binding=require require_auth=scram-sha-256",
+		"SCRAM with SSL, channel_binding=require, and require_auth=scram-sha-256",
+		expected_stderr =>
+		  qr/channel binding is required, but server did not offer an authentication method that supports channel binding/
+	);
+}
+
 done_testing();
-- 
2.25.1

v6-0002-Add-sslcertmode-option-for-client-certificates.patchtext/x-patch; charset=US-ASCII; name=v6-0002-Add-sslcertmode-option-for-client-certificates.patchDownload
From 1a10df8b8f24a93e3e720ded626174805887ce63 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jchampion@timescale.com>
Date: Fri, 24 Jun 2022 15:40:42 -0700
Subject: [PATCH v6 2/2] Add sslcertmode option for client certificates

The sslcertmode option controls whether the server is allowed and/or
required to request a certificate from the client. There are three
modes:

- "allow" is the default and follows the current behavior -- a
  configured  sslcert is sent if the server requests one (which, with
  the current implementation, will happen whenever TLS is negotiated).

- "disable" causes the client to refuse to send a client certificate
  even if an sslcert is configured.

- "require" causes the client to fail if a client certificate is never
  sent and the server opens a connection anyway. This doesn't add any
  additional security, since there is no guarantee that the server is
  validating the certificate correctly, but it may help troubleshoot
  more complicated TLS setups.

sslcertmode=require needs the OpenSSL implementation to support
SSL_CTX_set_cert_cb(). Notably, LibreSSL does not.
---
 configure                                     | 11 ++--
 configure.ac                                  |  4 +-
 .../postgres_fdw/expected/postgres_fdw.out    |  2 +-
 doc/src/sgml/libpq.sgml                       | 54 ++++++++++++++++++
 src/include/pg_config.h.in                    |  3 +
 src/interfaces/libpq/fe-auth.c                | 21 +++++++
 src/interfaces/libpq/fe-connect.c             | 55 +++++++++++++++++++
 src/interfaces/libpq/fe-secure-openssl.c      | 40 +++++++++++++-
 src/interfaces/libpq/libpq-int.h              |  3 +
 src/test/ssl/t/001_ssltests.pl                | 43 +++++++++++++++
 10 files changed, 227 insertions(+), 9 deletions(-)

diff --git a/configure b/configure
index 7dec6b7bf9..4deb9006ee 100755
--- a/configure
+++ b/configure
@@ -13044,13 +13044,14 @@ else
 fi
 
   fi
-  # Function introduced in OpenSSL 1.0.2.
-  for ac_func in X509_get_signature_nid
+  # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all of these.
+  for ac_func in X509_get_signature_nid SSL_CTX_set_cert_cb
 do :
-  ac_fn_c_check_func "$LINENO" "X509_get_signature_nid" "ac_cv_func_X509_get_signature_nid"
-if test "x$ac_cv_func_X509_get_signature_nid" = xyes; then :
+  as_ac_var=`$as_echo "ac_cv_func_$ac_func" | $as_tr_sh`
+ac_fn_c_check_func "$LINENO" "$ac_func" "$as_ac_var"
+if eval test \"x\$"$as_ac_var"\" = x"yes"; then :
   cat >>confdefs.h <<_ACEOF
-#define HAVE_X509_GET_SIGNATURE_NID 1
+#define `$as_echo "HAVE_$ac_func" | $as_tr_cpp` 1
 _ACEOF
 
 fi
diff --git a/configure.ac b/configure.ac
index d093fb88dd..e1d7409cbd 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1325,8 +1325,8 @@ if test "$with_ssl" = openssl ; then
      AC_SEARCH_LIBS(CRYPTO_new_ex_data, [eay32 crypto], [], [AC_MSG_ERROR([library 'eay32' or 'crypto' is required for OpenSSL])])
      AC_SEARCH_LIBS(SSL_new, [ssleay32 ssl], [], [AC_MSG_ERROR([library 'ssleay32' or 'ssl' is required for OpenSSL])])
   fi
-  # Function introduced in OpenSSL 1.0.2.
-  AC_CHECK_FUNCS([X509_get_signature_nid])
+  # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all of these.
+  AC_CHECK_FUNCS([X509_get_signature_nid SSL_CTX_set_cert_cb])
   # Functions introduced in OpenSSL 1.1.0. We used to check for
   # OPENSSL_VERSION_NUMBER, but that didn't work with 1.1.0, because LibreSSL
   # defines OPENSSL_VERSION_NUMBER to claim version 2.0.0, even though it
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 493931e003..47df10119e 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -9529,7 +9529,7 @@ DO $d$
     END;
 $d$;
 ERROR:  invalid option "password"
-HINT:  Valid options in this context are: service, passfile, channel_binding, connect_timeout, dbname, host, hostaddr, port, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, tcp_user_timeout, sslmode, sslcompression, sslcert, sslkey, sslrootcert, sslcrl, sslcrldir, sslsni, requirepeer, require_auth, ssl_min_protocol_version, ssl_max_protocol_version, gssencmode, krbsrvname, gsslib, target_session_attrs, use_remote_estimate, fdw_startup_cost, fdw_tuple_cost, extensions, updatable, truncatable, fetch_size, batch_size, async_capable, parallel_commit, keep_connections
+HINT:  Valid options in this context are: service, passfile, channel_binding, connect_timeout, dbname, host, hostaddr, port, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, tcp_user_timeout, sslmode, sslcompression, sslcert, sslkey, sslcertmode, sslrootcert, sslcrl, sslcrldir, sslsni, requirepeer, require_auth, ssl_min_protocol_version, ssl_max_protocol_version, gssencmode, krbsrvname, gsslib, target_session_attrs, use_remote_estimate, fdw_startup_cost, fdw_tuple_cost, extensions, updatable, truncatable, fetch_size, batch_size, async_capable, parallel_commit, keep_connections
 CONTEXT:  SQL statement "ALTER SERVER loopback_nopw OPTIONS (ADD password 'dummypw')"
 PL/pgSQL function inline_code_block line 3 at EXECUTE
 -- If we add a password for our user mapping instead, we should get a different
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index cc831158b7..b4c5dedccb 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1820,6 +1820,50 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-sslcertmode" xreflabel="sslcertmode">
+      <term><literal>sslcertmode</literal></term>
+      <listitem>
+       <para>
+        This option determines whether a client certificate may be sent to the
+        server, and whether the server is required to request one. There are
+        three modes:
+
+        <variablelist>
+         <varlistentry>
+          <term><literal>disable</literal></term>
+          <listitem>
+           <para>
+            a client certificate is never sent, even if one is provided via
+            <xref linkend="libpq-connect-sslcert" />
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>allow</literal> (default)</term>
+          <listitem>
+           <para>
+            a certificate may be sent, if the server requests one and it has
+            been provided via <literal>sslcert</literal>
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>require</literal></term>
+          <listitem>
+           <para>
+            the server <emphasis>must</emphasis> request a certificate. The
+            connection will fail if the server authenticates the client despite
+            not requesting or receiving one.
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-sslrootcert" xreflabel="sslrootcert">
       <term><literal>sslrootcert</literal></term>
       <listitem>
@@ -7973,6 +8017,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGSSLCERTMODE</envar></primary>
+      </indexterm>
+      <envar>PGSSLCERTMODE</envar> behaves the same as the <xref
+      linkend="libpq-connect-sslcertmode"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index cdd742cb55..0ecf40c6b3 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -514,6 +514,9 @@
 /* Define to 1 if you have spinlocks. */
 #undef HAVE_SPINLOCKS
 
+/* Define to 1 if you have the `SSL_CTX_set_cert_cb' function. */
+#undef HAVE_SSL_CTX_SET_CERT_CB
+
 /* Define to 1 if stdbool.h conforms to C99. */
 #undef HAVE_STDBOOL_H
 
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index f789bc7ec3..406440a201 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -891,6 +891,27 @@ check_expected_areq(AuthRequest areq, PGconn *conn)
 	bool		result = true;
 	char	   *reason = NULL;
 
+	if (conn->sslcertmode[0] == 'r' /* require */
+		&& areq == AUTH_REQ_OK)
+	{
+		/*
+		 * Trade off a little bit of complexity to try to get these error
+		 * messages as precise as possible.
+		 */
+		if (!conn->ssl_cert_requested)
+		{
+			appendPQExpBufferStr(&conn->errorMessage,
+								 libpq_gettext("server did not request a certificate"));
+			return false;
+		}
+		else if (!conn->ssl_cert_sent)
+		{
+			appendPQExpBufferStr(&conn->errorMessage,
+								 libpq_gettext("server accepted connection without a valid certificate"));
+			return false;
+		}
+	}
+
 	/*
 	 * If the user required a specific auth method, or specified an allowed set,
 	 * then reject all others here, and make sure the server actually completes
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 7d5bf337f6..92c5516abc 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -128,8 +128,10 @@ static int	ldapServiceLookup(const char *purl, PQconninfoOption *options,
 #define DefaultTargetSessionAttrs	"any"
 #ifdef USE_SSL
 #define DefaultSSLMode "prefer"
+#define DefaultSSLCertMode "allow"
 #else
 #define DefaultSSLMode	"disable"
+#define DefaultSSLCertMode "disable"
 #endif
 #ifdef ENABLE_GSS
 #include "fe-gssapi-common.h"
@@ -286,6 +288,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"SSL-Client-Key", "", 64,
 	offsetof(struct pg_conn, sslkey)},
 
+	{"sslcertmode", "PGSSLCERTMODE", NULL, NULL,
+		"SSL-Client-Cert-Mode", "", 8, /* sizeof("disable") == 8 */
+	offsetof(struct pg_conn, sslcertmode)},
+
 	{"sslpassword", NULL, NULL, NULL,
 		"SSL-Client-Key-Password", "*", 20,
 	offsetof(struct pg_conn, sslpassword)},
@@ -1529,6 +1535,55 @@ duplicate:
 		return false;
 	}
 
+	/*
+	 * validate sslcertmode option
+	 */
+	if (conn->sslcertmode)
+	{
+		if (strcmp(conn->sslcertmode, "disable") != 0 &&
+			strcmp(conn->sslcertmode, "allow") != 0 &&
+			strcmp(conn->sslcertmode, "require") != 0)
+		{
+			conn->status = CONNECTION_BAD;
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("invalid %s value: \"%s\"\n"),
+							  "sslcertmode",
+							  conn->sslcertmode);
+			return false;
+		}
+#ifndef USE_SSL
+		if (strcmp(conn->sslcertmode, "require") == 0)
+		{
+			conn->status = CONNECTION_BAD;
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("sslcertmode value \"%s\" invalid when SSL support is not compiled in\n"),
+							  conn->sslcertmode);
+			return false;
+		}
+#endif
+#ifndef HAVE_SSL_CTX_SET_CERT_CB
+		/*
+		 * Without a certificate callback, the current implementation can't
+		 * figure out if a certficate was actually requested, so "require" is
+		 * useless.
+		 */
+		if (strcmp(conn->sslcertmode, "require") == 0)
+		{
+			conn->status = CONNECTION_BAD;
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("sslcertmode value \"%s\" is not supported (check OpenSSL version)\n"),
+							  conn->sslcertmode);
+			return false;
+		}
+#endif
+	}
+	else
+	{
+		conn->sslcertmode = strdup(DefaultSSLCertMode);
+		if (!conn->sslcertmode)
+			goto oom_error;
+	}
+
 	/*
 	 * validate gssencmode option
 	 */
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index fc91cae7a2..13f6a28605 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -477,6 +477,34 @@ verify_cb(int ok, X509_STORE_CTX *ctx)
 	return ok;
 }
 
+#ifdef HAVE_SSL_CTX_SET_CERT_CB
+/*
+ * Certificate selection callback
+ *
+ * This callback lets us choose the client certificate we send to the server
+ * after seeing its CertificateRequest. We only support sending a single
+ * hard-coded certificate via sslcert, so we don't actually set any certificates
+ * here; we just it to record whether or not the server has actually asked for
+ * one and whether we have one to send.
+ */
+static int
+cert_cb(SSL *ssl, void *arg)
+{
+	PGconn *conn = arg;
+	conn->ssl_cert_requested = true;
+
+	/* Do we have a certificate loaded to send back? */
+	if (SSL_get_certificate(ssl))
+		conn->ssl_cert_sent = true;
+
+	/*
+	 * Tell OpenSSL that the callback succeeded; we're not required to actually
+	 * make any changes to the SSL handle.
+	 */
+	return 1;
+}
+#endif
+
 /*
  * OpenSSL-specific wrapper around
  * pq_verify_peer_name_matches_certificate_name(), converting the ASN1_STRING
@@ -971,6 +999,11 @@ initialize_SSL(PGconn *conn)
 		SSL_CTX_set_default_passwd_cb_userdata(SSL_context, conn);
 	}
 
+#ifdef HAVE_SSL_CTX_SET_CERT_CB
+	/* Set up a certificate selection callback. */
+	SSL_CTX_set_cert_cb(SSL_context, cert_cb, conn);
+#endif
+
 	/* Disable old protocol versions */
 	SSL_CTX_set_options(SSL_context, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
 
@@ -1134,7 +1167,12 @@ initialize_SSL(PGconn *conn)
 	else
 		fnbuf[0] = '\0';
 
-	if (fnbuf[0] == '\0')
+	if (conn->sslcertmode[0] == 'd') /* disable */
+	{
+		/* don't send a client cert even if we have one */
+		have_cert = false;
+	}
+	else if (fnbuf[0] == '\0')
 	{
 		/* no home directory, proceed without a client cert */
 		have_cert = false;
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 3278196eea..7c5989457a 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -381,6 +381,7 @@ struct pg_conn
 	char	   *sslkey;			/* client key filename */
 	char	   *sslcert;		/* client certificate filename */
 	char	   *sslpassword;	/* client key file password */
+	char	   *sslcertmode;	/* client cert mode (require,allow,disable) */
 	char	   *sslrootcert;	/* root certificate filename */
 	char	   *sslcrl;			/* certificate revocation list filename */
 	char	   *sslcrldir;		/* certificate revocation list directory name */
@@ -522,6 +523,8 @@ struct pg_conn
 
 	/* SSL structures */
 	bool		ssl_in_use;
+	bool		ssl_cert_requested;	/* Did the server ask us for a cert? */
+	bool		ssl_cert_sent;		/* Did we send one in reply? */
 
 #ifdef USE_SSL
 	bool		allow_ssl_try;	/* Allowed to try SSL negotiation */
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 707f4005af..357ac08110 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -38,6 +38,10 @@ my $SERVERHOSTADDR = '127.0.0.1';
 # This is the pattern to use in pg_hba.conf to match incoming connections.
 my $SERVERHOSTCIDR = '127.0.0.1/32';
 
+# Determine whether build supports sslcertmode=require.
+my $supports_sslcertmode_require =
+  check_pg_config("#define HAVE_SSL_CTX_SET_CERT_CB 1");
+
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
 
@@ -187,6 +191,22 @@ $node->connect_ok(
 	"$common_connstr sslrootcert=ssl/both-cas-2.crt sslmode=verify-ca",
 	"cert root file that contains two certificates, order 2");
 
+# sslcertmode=allow and =disable should both work without a client certificate.
+$node->connect_ok(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=disable",
+	"connect with sslcertmode=disable");
+$node->connect_ok(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=allow",
+	"connect with sslcertmode=allow");
+
+# sslcertmode=require, however, should fail.
+$node->connect_fails(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=require",
+	"connect with sslcertmode=require fails without a client certificate",
+	expected_stderr => $supports_sslcertmode_require
+		? qr/server accepted connection without a valid certificate/
+		: qr/sslcertmode value "require" is not supported/);
+
 # CRL tests
 
 # Invalid CRL filename is the same as no CRL, succeeds
@@ -534,6 +554,29 @@ $node->connect_ok(
 	"certificate authorization succeeds with correct client cert in encrypted DER format"
 );
 
+# correct client cert with required/allowed certificate authentication
+if ($supports_sslcertmode_require)
+{
+	$node->connect_ok(
+		"$common_connstr user=ssltestuser sslcertmode=require sslcert=ssl/client.crt "
+		  . sslkey('client.key'),
+		"certificate authorization succeeds with sslcertmode=require"
+	);
+}
+$node->connect_ok(
+	"$common_connstr user=ssltestuser sslcertmode=allow sslcert=ssl/client.crt "
+	  . sslkey('client.key'),
+	"certificate authorization succeeds with sslcertmode=allow"
+);
+
+# client cert isn't sent if certificate authentication is disabled
+$node->connect_fails(
+	"$common_connstr user=ssltestuser sslcertmode=disable sslcert=ssl/client.crt "
+	  . sslkey('client.key'),
+	"certificate authorization fails with sslcertmode=disable",
+	expected_stderr => qr/connection requires a valid client certificate/
+);
+
 # correct client cert in encrypted PEM with wrong password
 $node->connect_fails(
 	"$common_connstr user=ssltestuser sslcert=ssl/client.crt "
-- 
2.25.1

#21Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Jacob Champion (#20)
Re: [PoC] Let libpq reject unexpected authentication requests

On 27.06.22 23:40, Jacob Champion wrote:

-HINT:  Valid options in this context are: service, passfile, channel_binding, connect_timeout, dbname, host, hostaddr, port, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, tcp_user_timeout, sslmode, sslcompression, sslcert, sslkey, sslrootcert, sslcrl, sslcrldir, sslsni, requirepeer, ssl_min_protocol_version, ssl_max_protocol_version, gssencmode, krbsrvname, gsslib, target_session_attrs, use_remote_estimate, fdw_startup_cost, fdw_tuple_cost, extensions, updatable, truncatable, fetch_size, batch_size, async_capable, parallel_commit, keep_connections
+HINT:  Valid options in this context are: service, passfile, channel_binding, connect_timeout, dbname, host, hostaddr, port, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, tcp_user_timeout, sslmode, sslcompression, sslcert, sslkey, sslcertmode, sslrootcert, sslcrl, sslcrldir, sslsni, requirepeer, require_auth, ssl_min_protocol_version, ssl_max_protocol_version, gssencmode, krbsrvname, gsslib, target_session_attrs, use_remote_estimate, fdw_startup_cost, fdw_tuple_cost, extensions, updatable, truncatable, fetch_size, batch_size, async_capable, parallel_commit, keep_connections

It's not strictly related to your patch, but maybe this hint has
outlived its usefulness? I mean, we don't list all available tables
when you try to reference a table that doesn't exist. And unordered on
top of that.

#22Jacob Champion
jchampion@timescale.com
In reply to: Peter Eisentraut (#21)
3 attachment(s)
Re: [PoC] Let libpq reject unexpected authentication requests

On Wed, Jun 29, 2022 at 6:36 AM Peter Eisentraut
<peter.eisentraut@enterprisedb.com> wrote:

It's not strictly related to your patch, but maybe this hint has
outlived its usefulness? I mean, we don't list all available tables
when you try to reference a table that doesn't exist. And unordered on
top of that.

Yeah, maybe it'd be better to tell the user the correct context for an
otherwise-valid option ("the 'sslcert' option may only be applied to
USER MAPPING"), and avoid the option dump entirely?

--

v7, attached, fixes configuration on Windows.

--Jacob

Attachments:

since-v6.diff.txttext/plain; charset=US-ASCII; name=since-v6.diff.txtDownload
commit 326912a24350d10e509bf911b4452d48708021ab
Author: Jacob Champion <jchampion@timescale.com>
Date:   Thu Jun 30 14:22:02 2022 -0700

    squash! Add sslcertmode option for client certificates
    
    Fix Windows configuration.

diff --git a/src/tools/msvc/Solution.pm b/src/tools/msvc/Solution.pm
index d30e8fcb11..d7e7a897b2 100644
--- a/src/tools/msvc/Solution.pm
+++ b/src/tools/msvc/Solution.pm
@@ -364,6 +364,7 @@ sub GenerateFiles
 		HAVE_SHM_OPEN                            => undef,
 		HAVE_SOCKLEN_T                           => 1,
 		HAVE_SPINLOCKS                           => 1,
+		HAVE_SSL_CTX_SET_CERT_CB                 => undef,
 		HAVE_STDBOOL_H                           => 1,
 		HAVE_STDINT_H                            => 1,
 		HAVE_STDLIB_H                            => 1,
@@ -553,6 +554,13 @@ sub GenerateFiles
 
 		my ($digit1, $digit2, $digit3) = $self->GetOpenSSLVersion();
 
+		if (   ($digit1 >= '3' && $digit2 >= '0' && $digit3 >= '0')
+			|| ($digit1 >= '1' && $digit2 >= '1' && $digit3 >= '0')
+			|| ($digit1 >= '1' && $digit2 >= '0' && $digit3 >= '2'))
+		{
+			$define{HAVE_SSL_CTX_SET_CERT_CB} = 1;
+		}
+
 		# More symbols are needed with OpenSSL 1.1.0 and above.
 		if (   ($digit1 >= '3' && $digit2 >= '0' && $digit3 >= '0')
 			|| ($digit1 >= '1' && $digit2 >= '1' && $digit3 >= '0'))
v7-0002-Add-sslcertmode-option-for-client-certificates.patchtext/x-patch; charset=US-ASCII; name=v7-0002-Add-sslcertmode-option-for-client-certificates.patchDownload
From e16a23b9cd2a2600f45636ca399d6239329f23c8 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jchampion@timescale.com>
Date: Fri, 24 Jun 2022 15:40:42 -0700
Subject: [PATCH v7 2/2] Add sslcertmode option for client certificates

The sslcertmode option controls whether the server is allowed and/or
required to request a certificate from the client. There are three
modes:

- "allow" is the default and follows the current behavior -- a
  configured  sslcert is sent if the server requests one (which, with
  the current implementation, will happen whenever TLS is negotiated).

- "disable" causes the client to refuse to send a client certificate
  even if an sslcert is configured.

- "require" causes the client to fail if a client certificate is never
  sent and the server opens a connection anyway. This doesn't add any
  additional security, since there is no guarantee that the server is
  validating the certificate correctly, but it may help troubleshoot
  more complicated TLS setups.

sslcertmode=require needs the OpenSSL implementation to support
SSL_CTX_set_cert_cb(). Notably, LibreSSL does not.
---
 configure                                     | 11 ++--
 configure.ac                                  |  4 +-
 .../postgres_fdw/expected/postgres_fdw.out    |  2 +-
 doc/src/sgml/libpq.sgml                       | 54 ++++++++++++++++++
 src/include/pg_config.h.in                    |  3 +
 src/interfaces/libpq/fe-auth.c                | 21 +++++++
 src/interfaces/libpq/fe-connect.c             | 55 +++++++++++++++++++
 src/interfaces/libpq/fe-secure-openssl.c      | 40 +++++++++++++-
 src/interfaces/libpq/libpq-int.h              |  3 +
 src/test/ssl/t/001_ssltests.pl                | 43 +++++++++++++++
 src/tools/msvc/Solution.pm                    |  8 +++
 11 files changed, 235 insertions(+), 9 deletions(-)

diff --git a/configure b/configure
index fb07cd27d9..9d5db2879f 100755
--- a/configure
+++ b/configure
@@ -13044,13 +13044,14 @@ else
 fi
 
   fi
-  # Function introduced in OpenSSL 1.0.2.
-  for ac_func in X509_get_signature_nid
+  # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all of these.
+  for ac_func in X509_get_signature_nid SSL_CTX_set_cert_cb
 do :
-  ac_fn_c_check_func "$LINENO" "X509_get_signature_nid" "ac_cv_func_X509_get_signature_nid"
-if test "x$ac_cv_func_X509_get_signature_nid" = xyes; then :
+  as_ac_var=`$as_echo "ac_cv_func_$ac_func" | $as_tr_sh`
+ac_fn_c_check_func "$LINENO" "$ac_func" "$as_ac_var"
+if eval test \"x\$"$as_ac_var"\" = x"yes"; then :
   cat >>confdefs.h <<_ACEOF
-#define HAVE_X509_GET_SIGNATURE_NID 1
+#define `$as_echo "HAVE_$ac_func" | $as_tr_cpp` 1
 _ACEOF
 
 fi
diff --git a/configure.ac b/configure.ac
index 6c6f997ee3..8110bcfc38 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1325,8 +1325,8 @@ if test "$with_ssl" = openssl ; then
      AC_SEARCH_LIBS(CRYPTO_new_ex_data, [eay32 crypto], [], [AC_MSG_ERROR([library 'eay32' or 'crypto' is required for OpenSSL])])
      AC_SEARCH_LIBS(SSL_new, [ssleay32 ssl], [], [AC_MSG_ERROR([library 'ssleay32' or 'ssl' is required for OpenSSL])])
   fi
-  # Function introduced in OpenSSL 1.0.2.
-  AC_CHECK_FUNCS([X509_get_signature_nid])
+  # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all of these.
+  AC_CHECK_FUNCS([X509_get_signature_nid SSL_CTX_set_cert_cb])
   # Functions introduced in OpenSSL 1.1.0. We used to check for
   # OPENSSL_VERSION_NUMBER, but that didn't work with 1.1.0, because LibreSSL
   # defines OPENSSL_VERSION_NUMBER to claim version 2.0.0, even though it
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 493931e003..47df10119e 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -9529,7 +9529,7 @@ DO $d$
     END;
 $d$;
 ERROR:  invalid option "password"
-HINT:  Valid options in this context are: service, passfile, channel_binding, connect_timeout, dbname, host, hostaddr, port, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, tcp_user_timeout, sslmode, sslcompression, sslcert, sslkey, sslrootcert, sslcrl, sslcrldir, sslsni, requirepeer, require_auth, ssl_min_protocol_version, ssl_max_protocol_version, gssencmode, krbsrvname, gsslib, target_session_attrs, use_remote_estimate, fdw_startup_cost, fdw_tuple_cost, extensions, updatable, truncatable, fetch_size, batch_size, async_capable, parallel_commit, keep_connections
+HINT:  Valid options in this context are: service, passfile, channel_binding, connect_timeout, dbname, host, hostaddr, port, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, tcp_user_timeout, sslmode, sslcompression, sslcert, sslkey, sslcertmode, sslrootcert, sslcrl, sslcrldir, sslsni, requirepeer, require_auth, ssl_min_protocol_version, ssl_max_protocol_version, gssencmode, krbsrvname, gsslib, target_session_attrs, use_remote_estimate, fdw_startup_cost, fdw_tuple_cost, extensions, updatable, truncatable, fetch_size, batch_size, async_capable, parallel_commit, keep_connections
 CONTEXT:  SQL statement "ALTER SERVER loopback_nopw OPTIONS (ADD password 'dummypw')"
 PL/pgSQL function inline_code_block line 3 at EXECUTE
 -- If we add a password for our user mapping instead, we should get a different
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index cc831158b7..b4c5dedccb 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1820,6 +1820,50 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-sslcertmode" xreflabel="sslcertmode">
+      <term><literal>sslcertmode</literal></term>
+      <listitem>
+       <para>
+        This option determines whether a client certificate may be sent to the
+        server, and whether the server is required to request one. There are
+        three modes:
+
+        <variablelist>
+         <varlistentry>
+          <term><literal>disable</literal></term>
+          <listitem>
+           <para>
+            a client certificate is never sent, even if one is provided via
+            <xref linkend="libpq-connect-sslcert" />
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>allow</literal> (default)</term>
+          <listitem>
+           <para>
+            a certificate may be sent, if the server requests one and it has
+            been provided via <literal>sslcert</literal>
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>require</literal></term>
+          <listitem>
+           <para>
+            the server <emphasis>must</emphasis> request a certificate. The
+            connection will fail if the server authenticates the client despite
+            not requesting or receiving one.
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-sslrootcert" xreflabel="sslrootcert">
       <term><literal>sslrootcert</literal></term>
       <listitem>
@@ -7973,6 +8017,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGSSLCERTMODE</envar></primary>
+      </indexterm>
+      <envar>PGSSLCERTMODE</envar> behaves the same as the <xref
+      linkend="libpq-connect-sslcertmode"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index cdd742cb55..0ecf40c6b3 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -514,6 +514,9 @@
 /* Define to 1 if you have spinlocks. */
 #undef HAVE_SPINLOCKS
 
+/* Define to 1 if you have the `SSL_CTX_set_cert_cb' function. */
+#undef HAVE_SSL_CTX_SET_CERT_CB
+
 /* Define to 1 if stdbool.h conforms to C99. */
 #undef HAVE_STDBOOL_H
 
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index f789bc7ec3..406440a201 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -891,6 +891,27 @@ check_expected_areq(AuthRequest areq, PGconn *conn)
 	bool		result = true;
 	char	   *reason = NULL;
 
+	if (conn->sslcertmode[0] == 'r' /* require */
+		&& areq == AUTH_REQ_OK)
+	{
+		/*
+		 * Trade off a little bit of complexity to try to get these error
+		 * messages as precise as possible.
+		 */
+		if (!conn->ssl_cert_requested)
+		{
+			appendPQExpBufferStr(&conn->errorMessage,
+								 libpq_gettext("server did not request a certificate"));
+			return false;
+		}
+		else if (!conn->ssl_cert_sent)
+		{
+			appendPQExpBufferStr(&conn->errorMessage,
+								 libpq_gettext("server accepted connection without a valid certificate"));
+			return false;
+		}
+	}
+
 	/*
 	 * If the user required a specific auth method, or specified an allowed set,
 	 * then reject all others here, and make sure the server actually completes
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 7d5bf337f6..92c5516abc 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -128,8 +128,10 @@ static int	ldapServiceLookup(const char *purl, PQconninfoOption *options,
 #define DefaultTargetSessionAttrs	"any"
 #ifdef USE_SSL
 #define DefaultSSLMode "prefer"
+#define DefaultSSLCertMode "allow"
 #else
 #define DefaultSSLMode	"disable"
+#define DefaultSSLCertMode "disable"
 #endif
 #ifdef ENABLE_GSS
 #include "fe-gssapi-common.h"
@@ -286,6 +288,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"SSL-Client-Key", "", 64,
 	offsetof(struct pg_conn, sslkey)},
 
+	{"sslcertmode", "PGSSLCERTMODE", NULL, NULL,
+		"SSL-Client-Cert-Mode", "", 8, /* sizeof("disable") == 8 */
+	offsetof(struct pg_conn, sslcertmode)},
+
 	{"sslpassword", NULL, NULL, NULL,
 		"SSL-Client-Key-Password", "*", 20,
 	offsetof(struct pg_conn, sslpassword)},
@@ -1529,6 +1535,55 @@ duplicate:
 		return false;
 	}
 
+	/*
+	 * validate sslcertmode option
+	 */
+	if (conn->sslcertmode)
+	{
+		if (strcmp(conn->sslcertmode, "disable") != 0 &&
+			strcmp(conn->sslcertmode, "allow") != 0 &&
+			strcmp(conn->sslcertmode, "require") != 0)
+		{
+			conn->status = CONNECTION_BAD;
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("invalid %s value: \"%s\"\n"),
+							  "sslcertmode",
+							  conn->sslcertmode);
+			return false;
+		}
+#ifndef USE_SSL
+		if (strcmp(conn->sslcertmode, "require") == 0)
+		{
+			conn->status = CONNECTION_BAD;
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("sslcertmode value \"%s\" invalid when SSL support is not compiled in\n"),
+							  conn->sslcertmode);
+			return false;
+		}
+#endif
+#ifndef HAVE_SSL_CTX_SET_CERT_CB
+		/*
+		 * Without a certificate callback, the current implementation can't
+		 * figure out if a certficate was actually requested, so "require" is
+		 * useless.
+		 */
+		if (strcmp(conn->sslcertmode, "require") == 0)
+		{
+			conn->status = CONNECTION_BAD;
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("sslcertmode value \"%s\" is not supported (check OpenSSL version)\n"),
+							  conn->sslcertmode);
+			return false;
+		}
+#endif
+	}
+	else
+	{
+		conn->sslcertmode = strdup(DefaultSSLCertMode);
+		if (!conn->sslcertmode)
+			goto oom_error;
+	}
+
 	/*
 	 * validate gssencmode option
 	 */
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index fc91cae7a2..13f6a28605 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -477,6 +477,34 @@ verify_cb(int ok, X509_STORE_CTX *ctx)
 	return ok;
 }
 
+#ifdef HAVE_SSL_CTX_SET_CERT_CB
+/*
+ * Certificate selection callback
+ *
+ * This callback lets us choose the client certificate we send to the server
+ * after seeing its CertificateRequest. We only support sending a single
+ * hard-coded certificate via sslcert, so we don't actually set any certificates
+ * here; we just it to record whether or not the server has actually asked for
+ * one and whether we have one to send.
+ */
+static int
+cert_cb(SSL *ssl, void *arg)
+{
+	PGconn *conn = arg;
+	conn->ssl_cert_requested = true;
+
+	/* Do we have a certificate loaded to send back? */
+	if (SSL_get_certificate(ssl))
+		conn->ssl_cert_sent = true;
+
+	/*
+	 * Tell OpenSSL that the callback succeeded; we're not required to actually
+	 * make any changes to the SSL handle.
+	 */
+	return 1;
+}
+#endif
+
 /*
  * OpenSSL-specific wrapper around
  * pq_verify_peer_name_matches_certificate_name(), converting the ASN1_STRING
@@ -971,6 +999,11 @@ initialize_SSL(PGconn *conn)
 		SSL_CTX_set_default_passwd_cb_userdata(SSL_context, conn);
 	}
 
+#ifdef HAVE_SSL_CTX_SET_CERT_CB
+	/* Set up a certificate selection callback. */
+	SSL_CTX_set_cert_cb(SSL_context, cert_cb, conn);
+#endif
+
 	/* Disable old protocol versions */
 	SSL_CTX_set_options(SSL_context, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
 
@@ -1134,7 +1167,12 @@ initialize_SSL(PGconn *conn)
 	else
 		fnbuf[0] = '\0';
 
-	if (fnbuf[0] == '\0')
+	if (conn->sslcertmode[0] == 'd') /* disable */
+	{
+		/* don't send a client cert even if we have one */
+		have_cert = false;
+	}
+	else if (fnbuf[0] == '\0')
 	{
 		/* no home directory, proceed without a client cert */
 		have_cert = false;
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 3278196eea..7c5989457a 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -381,6 +381,7 @@ struct pg_conn
 	char	   *sslkey;			/* client key filename */
 	char	   *sslcert;		/* client certificate filename */
 	char	   *sslpassword;	/* client key file password */
+	char	   *sslcertmode;	/* client cert mode (require,allow,disable) */
 	char	   *sslrootcert;	/* root certificate filename */
 	char	   *sslcrl;			/* certificate revocation list filename */
 	char	   *sslcrldir;		/* certificate revocation list directory name */
@@ -522,6 +523,8 @@ struct pg_conn
 
 	/* SSL structures */
 	bool		ssl_in_use;
+	bool		ssl_cert_requested;	/* Did the server ask us for a cert? */
+	bool		ssl_cert_sent;		/* Did we send one in reply? */
 
 #ifdef USE_SSL
 	bool		allow_ssl_try;	/* Allowed to try SSL negotiation */
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 707f4005af..357ac08110 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -38,6 +38,10 @@ my $SERVERHOSTADDR = '127.0.0.1';
 # This is the pattern to use in pg_hba.conf to match incoming connections.
 my $SERVERHOSTCIDR = '127.0.0.1/32';
 
+# Determine whether build supports sslcertmode=require.
+my $supports_sslcertmode_require =
+  check_pg_config("#define HAVE_SSL_CTX_SET_CERT_CB 1");
+
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
 
@@ -187,6 +191,22 @@ $node->connect_ok(
 	"$common_connstr sslrootcert=ssl/both-cas-2.crt sslmode=verify-ca",
 	"cert root file that contains two certificates, order 2");
 
+# sslcertmode=allow and =disable should both work without a client certificate.
+$node->connect_ok(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=disable",
+	"connect with sslcertmode=disable");
+$node->connect_ok(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=allow",
+	"connect with sslcertmode=allow");
+
+# sslcertmode=require, however, should fail.
+$node->connect_fails(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=require",
+	"connect with sslcertmode=require fails without a client certificate",
+	expected_stderr => $supports_sslcertmode_require
+		? qr/server accepted connection without a valid certificate/
+		: qr/sslcertmode value "require" is not supported/);
+
 # CRL tests
 
 # Invalid CRL filename is the same as no CRL, succeeds
@@ -534,6 +554,29 @@ $node->connect_ok(
 	"certificate authorization succeeds with correct client cert in encrypted DER format"
 );
 
+# correct client cert with required/allowed certificate authentication
+if ($supports_sslcertmode_require)
+{
+	$node->connect_ok(
+		"$common_connstr user=ssltestuser sslcertmode=require sslcert=ssl/client.crt "
+		  . sslkey('client.key'),
+		"certificate authorization succeeds with sslcertmode=require"
+	);
+}
+$node->connect_ok(
+	"$common_connstr user=ssltestuser sslcertmode=allow sslcert=ssl/client.crt "
+	  . sslkey('client.key'),
+	"certificate authorization succeeds with sslcertmode=allow"
+);
+
+# client cert isn't sent if certificate authentication is disabled
+$node->connect_fails(
+	"$common_connstr user=ssltestuser sslcertmode=disable sslcert=ssl/client.crt "
+	  . sslkey('client.key'),
+	"certificate authorization fails with sslcertmode=disable",
+	expected_stderr => qr/connection requires a valid client certificate/
+);
+
 # correct client cert in encrypted PEM with wrong password
 $node->connect_fails(
 	"$common_connstr user=ssltestuser sslcert=ssl/client.crt "
diff --git a/src/tools/msvc/Solution.pm b/src/tools/msvc/Solution.pm
index d30e8fcb11..d7e7a897b2 100644
--- a/src/tools/msvc/Solution.pm
+++ b/src/tools/msvc/Solution.pm
@@ -364,6 +364,7 @@ sub GenerateFiles
 		HAVE_SHM_OPEN                            => undef,
 		HAVE_SOCKLEN_T                           => 1,
 		HAVE_SPINLOCKS                           => 1,
+		HAVE_SSL_CTX_SET_CERT_CB                 => undef,
 		HAVE_STDBOOL_H                           => 1,
 		HAVE_STDINT_H                            => 1,
 		HAVE_STDLIB_H                            => 1,
@@ -553,6 +554,13 @@ sub GenerateFiles
 
 		my ($digit1, $digit2, $digit3) = $self->GetOpenSSLVersion();
 
+		if (   ($digit1 >= '3' && $digit2 >= '0' && $digit3 >= '0')
+			|| ($digit1 >= '1' && $digit2 >= '1' && $digit3 >= '0')
+			|| ($digit1 >= '1' && $digit2 >= '0' && $digit3 >= '2'))
+		{
+			$define{HAVE_SSL_CTX_SET_CERT_CB} = 1;
+		}
+
 		# More symbols are needed with OpenSSL 1.1.0 and above.
 		if (   ($digit1 >= '3' && $digit2 >= '0' && $digit3 >= '0')
 			|| ($digit1 >= '1' && $digit2 >= '1' && $digit3 >= '0'))
-- 
2.25.1

v7-0001-libpq-let-client-reject-unexpected-auth-methods.patchtext/x-patch; charset=US-ASCII; name=v7-0001-libpq-let-client-reject-unexpected-auth-methods.patchDownload
From 423d7f5d7628b7f26921d04afe9f768d35676e25 Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Mon, 28 Feb 2022 09:40:43 -0800
Subject: [PATCH v7 1/2] libpq: let client reject unexpected auth methods

The require_auth connection option allows the client to choose a list of
acceptable authentication types for use with the server. There is no
negotiation: if the server does not present one of the allowed
authentication requests, the connection fails. Additionally, all methods
in the list may be negated, e.g. '!password', in which case the server
must NOT use the listed authentication type. The special method "none"
allows/disallows the use of unauthenticated connections (but it does not
govern transport-level authentication via TLS or GSSAPI).

Internally, the patch expands the role of check_expected_areq() to
ensure that the incoming request is compatible with conn->require_auth.
It also introduces a new flag, conn->client_finished_auth, which is set
by various authentication routines when the client side of the handshake
is finished. This signals to check_expected_areq() that an OK message
from the server is expected, and allows the client to complain if the
server forgoes authentication entirely.

(Since the client can't generally prove that the server is actually
doing the work of authentication, this last part is mostly useful for
SCRAM without channel binding. It could also provide a client with a
decent signal that, at the very least, it's not connecting to a database
with trust auth, and so the connection can be tied to the client in a
later audit.)

Deficiencies:
- This feature currently doesn't prevent unexpected certificate exchange
  or unexpected GSSAPI encryption.
- This is unlikely to be very forwards-compatible at the moment,
  especially with SASL/SCRAM.
- SSPI support is "implemented" but untested.
- require_auth=none,scram-sha-256 currently allows the server to leave a
  SCRAM exchange unfinished. This is not net-new behavior but may be
  surprising.
---
 .../postgres_fdw/expected/postgres_fdw.out    |   2 +-
 doc/src/sgml/libpq.sgml                       | 105 +++++++++++
 src/include/libpq/pqcomm.h                    |   1 +
 src/interfaces/libpq/fe-auth-scram.c          |   1 +
 src/interfaces/libpq/fe-auth.c                | 138 +++++++++++++++
 src/interfaces/libpq/fe-connect.c             | 164 ++++++++++++++++++
 src/interfaces/libpq/fe-secure-openssl.c      |   1 -
 src/interfaces/libpq/libpq-int.h              |   7 +
 src/test/authentication/t/001_password.pl     | 149 ++++++++++++++++
 src/test/kerberos/t/001_auth.pl               |  26 +++
 src/test/ldap/t/001_auth.pl                   |   6 +
 src/test/ssl/t/002_scram.pl                   |  25 +++
 12 files changed, 623 insertions(+), 2 deletions(-)

diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 44457f930c..493931e003 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -9529,7 +9529,7 @@ DO $d$
     END;
 $d$;
 ERROR:  invalid option "password"
-HINT:  Valid options in this context are: service, passfile, channel_binding, connect_timeout, dbname, host, hostaddr, port, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, tcp_user_timeout, sslmode, sslcompression, sslcert, sslkey, sslrootcert, sslcrl, sslcrldir, sslsni, requirepeer, ssl_min_protocol_version, ssl_max_protocol_version, gssencmode, krbsrvname, gsslib, target_session_attrs, use_remote_estimate, fdw_startup_cost, fdw_tuple_cost, extensions, updatable, truncatable, fetch_size, batch_size, async_capable, parallel_commit, keep_connections
+HINT:  Valid options in this context are: service, passfile, channel_binding, connect_timeout, dbname, host, hostaddr, port, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, tcp_user_timeout, sslmode, sslcompression, sslcert, sslkey, sslrootcert, sslcrl, sslcrldir, sslsni, requirepeer, require_auth, ssl_min_protocol_version, ssl_max_protocol_version, gssencmode, krbsrvname, gsslib, target_session_attrs, use_remote_estimate, fdw_startup_cost, fdw_tuple_cost, extensions, updatable, truncatable, fetch_size, batch_size, async_capable, parallel_commit, keep_connections
 CONTEXT:  SQL statement "ALTER SERVER loopback_nopw OPTIONS (ADD password 'dummypw')"
 PL/pgSQL function inline_code_block line 3 at EXECUTE
 -- If we add a password for our user mapping instead, we should get a different
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 37ec3cb4e5..cc831158b7 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1221,6 +1221,101 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-require-auth" xreflabel="require_auth">
+      <term><literal>require_auth</literal></term>
+      <listitem>
+      <para>
+        Specifies the authentication method that the client requires from the
+        server. If the server does not use the required method to authenticate
+        the client, or if the authentication handshake is not fully completed by
+        the server, the connection will fail. A comma-separated list of methods
+        may also be provided, of which the server must use exactly one in order
+        for the connection to succeed. By default, any authentication method is
+        accepted, and the server is free to skip authentication altogether.
+      </para>
+      <para>
+        Methods may be negated with the addition of a <literal>!</literal>
+        prefix, in which case the server must <emphasis>not</emphasis> attempt
+        the listed method; any other method is accepted, and the server is free
+        not to authenticate the client at all. If a comma-separated list is
+        provided, the server may not attempt <emphasis>any</emphasis> of the
+        listed negated methods. Negated and non-negated forms may not be
+        combined in the same setting.
+      </para>
+      <para>
+        As a final special case, the <literal>none</literal> method requires the
+        server not to use an authentication challenge. (It may also be negated,
+        to require some form of authentication.)
+      </para>
+      <para>
+        The following methods may be specified:
+
+        <variablelist>
+         <varlistentry>
+          <term><literal>password</literal></term>
+          <listitem>
+           <para>
+            The server must request plaintext password authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>md5</literal></term>
+          <listitem>
+           <para>
+            The server must request MD5 hashed password authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>gss</literal></term>
+          <listitem>
+           <para>
+            The server must either request a Kerberos handshake via
+            <acronym>GSSAPI</acronym> or establish a
+            <acronym>GSS</acronym>-encrypted channel (see also
+            <xref linkend="libpq-connect-gssencmode" />).
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>sspi</literal></term>
+          <listitem>
+           <para>
+            The server must request Windows <acronym>SSPI</acronym>
+            authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>scram-sha-256</literal></term>
+          <listitem>
+           <para>
+            The server must successfully complete a SCRAM-SHA-256 authentication
+            exchange with the client.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>none</literal></term>
+          <listitem>
+           <para>
+            The server must not prompt the client for an authentication
+            exchange. (This does not prohibit client certificate authentication
+			via TLS, nor GSS authentication via its encrypted transport.)
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+      </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-channel-binding" xreflabel="channel_binding">
       <term><literal>channel_binding</literal></term>
       <listitem>
@@ -7761,6 +7856,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGREQUIREAUTH</envar></primary>
+      </indexterm>
+      <envar>PGREQUIREAUTH</envar> behaves the same as the <xref
+      linkend="libpq-connect-require-auth"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h
index b418283d5f..a47f7b2d91 100644
--- a/src/include/libpq/pqcomm.h
+++ b/src/include/libpq/pqcomm.h
@@ -161,6 +161,7 @@ extern PGDLLIMPORT bool Db_user_namespace;
 #define AUTH_REQ_SASL	   10	/* Begin SASL authentication */
 #define AUTH_REQ_SASL_CONT 11	/* Continue SASL authentication */
 #define AUTH_REQ_SASL_FIN  12	/* Final SASL message */
+#define AUTH_REQ_MAX	   AUTH_REQ_SASL_FIN	/* maximum AUTH_REQ_* value */
 
 typedef uint32 AuthRequest;
 
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index e616200704..d918e8b87f 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -289,6 +289,7 @@ scram_exchange(void *opaq, char *input, int inputlen,
 			}
 			*done = true;
 			state->state = FE_SCRAM_FINISHED;
+			state->conn->client_finished_auth = true;
 			break;
 
 		default:
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 0a072a36dc..f789bc7ec3 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -138,7 +138,10 @@ pg_GSS_continue(PGconn *conn, int payloadlen)
 	}
 
 	if (maj_stat == GSS_S_COMPLETE)
+	{
+		conn->client_finished_auth = true;
 		gss_release_name(&lmin_s, &conn->gtarg_nam);
+	}
 
 	return STATUS_OK;
 }
@@ -328,6 +331,9 @@ pg_SSPI_continue(PGconn *conn, int payloadlen)
 		FreeContextBuffer(outbuf.pBuffers[0].pvBuffer);
 	}
 
+	if (r == SEC_E_OK)
+		conn->client_finished_auth = true;
+
 	/* Cleanup is handled by the code in freePGconn() */
 	return STATUS_OK;
 }
@@ -836,6 +842,44 @@ pg_password_sendauth(PGconn *conn, const char *password, AuthRequest areq)
 	return ret;
 }
 
+/*
+ * Translate an AuthRequest into a human-readable description.
+ */
+static const char *
+auth_description(AuthRequest areq)
+{
+	switch (areq)
+	{
+		case AUTH_REQ_PASSWORD:
+			return libpq_gettext("a cleartext password");
+		case AUTH_REQ_MD5:
+			return libpq_gettext("a hashed password");
+		case AUTH_REQ_GSS:
+		case AUTH_REQ_GSS_CONT:
+			return libpq_gettext("GSSAPI authentication");
+		case AUTH_REQ_SSPI:
+			return libpq_gettext("SSPI authentication");
+		case AUTH_REQ_SCM_CREDS:
+			return libpq_gettext("UNIX socket credentials");
+		case AUTH_REQ_SASL:
+		case AUTH_REQ_SASL_CONT:
+		case AUTH_REQ_SASL_FIN:
+			return libpq_gettext("SASL authentication");
+	}
+
+	return libpq_gettext("an unknown authentication type");
+}
+
+/*
+ * Convenience macro for checking the allowed_auth_methods bitmask. Caller must
+ * ensure that type is not greater than 31 (high bit of the bitmask).
+ */
+#define auth_allowed(conn, type) \
+	(((conn)->allowed_auth_methods & (1 << (type))) != 0)
+
+StaticAssertDecl(AUTH_REQ_MAX < CHAR_BIT * sizeof(((PGconn){0}).allowed_auth_methods),
+				 "AUTH_REQ_MAX overflows the allowed_auth_methods bitmask");
+
 /*
  * Verify that the authentication request is expected, given the connection
  * parameters. This is especially important when the client wishes to
@@ -845,6 +889,97 @@ static bool
 check_expected_areq(AuthRequest areq, PGconn *conn)
 {
 	bool		result = true;
+	char	   *reason = NULL;
+
+	/*
+	 * If the user required a specific auth method, or specified an allowed set,
+	 * then reject all others here, and make sure the server actually completes
+	 * an authentication exchange.
+	 */
+	if (conn->require_auth)
+	{
+		switch (areq)
+		{
+			case AUTH_REQ_OK:
+				/*
+				 * Check to make sure we've actually finished our exchange (or
+				 * else that the user has allowed an authentication-less
+				 * connection).
+				 *
+				 * TODO: how should !auth_required interact with an incomplete
+				 * SCRAM exchange?
+				 */
+				if (!conn->auth_required || conn->client_finished_auth)
+					break;
+
+				/*
+				 * No explicit authentication request was made by the server --
+				 * or perhaps it was made and not completed, in the case of
+				 * SCRAM -- but there is one special case to check. If the user
+				 * allowed "gss", then a GSS-encrypted channel also satisfies
+				 * the check.
+				 */
+#ifdef ENABLE_GSS
+				if (auth_allowed(conn, AUTH_REQ_GSS) && conn->gssenc)
+				{
+					/*
+					 * If implicit GSS auth has already been performed via GSS
+					 * encryption, we don't need to have performed an
+					 * AUTH_REQ_GSS exchange.
+					 *
+					 * TODO: check this assumption. What mutual auth guarantees
+					 * are made in this case?
+					 */
+				}
+				else
+#endif
+				{
+					reason = libpq_gettext("server did not complete authentication"),
+					result = false;
+				}
+
+				break;
+
+			case AUTH_REQ_PASSWORD:
+			case AUTH_REQ_MD5:
+			case AUTH_REQ_GSS:
+			case AUTH_REQ_SSPI:
+			case AUTH_REQ_GSS_CONT:
+			case AUTH_REQ_SASL:
+			case AUTH_REQ_SASL_CONT:
+			case AUTH_REQ_SASL_FIN:
+				/*
+				 * We don't handle these with the default case, to avoid
+				 * bit-shifting past the end of the allowed_auth_methods mask if
+				 * the server sends an unexpected AuthRequest.
+				 */
+				result = auth_allowed(conn, areq);
+				break;
+
+			default:
+				result = false;
+				break;
+		}
+	}
+
+	if (!result)
+	{
+		if (reason)
+		{
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("auth method \"%s\" requirement failed: %s\n"),
+							  conn->require_auth, reason);
+		}
+		else
+		{
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("auth method \"%s\" required, but server requested %s\n"),
+							  conn->require_auth,
+							  auth_description(areq));
+		}
+
+		return result;
+	}
 
 	/*
 	 * When channel_binding=require, we must protect against two cases: (1) we
@@ -1046,6 +1181,9 @@ pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn)
 										 "fe_sendauth: error sending password authentication\n");
 					return STATUS_ERROR;
 				}
+
+				/* We expect no further authentication requests. */
+				conn->client_finished_auth = true;
 				break;
 			}
 
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 6e936bbff3..7d5bf337f6 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -310,6 +310,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Require-Peer", "", 10,
 	offsetof(struct pg_conn, requirepeer)},
 
+	{"require_auth", "PGREQUIREAUTH", NULL, NULL,
+		"Require-Auth", "", 14, /* sizeof("scram-sha-256") == 14 */
+	offsetof(struct pg_conn, require_auth)},
+
 	{"ssl_min_protocol_version", "PGSSLMINPROTOCOLVERSION", "TLSv1.2", NULL,
 		"SSL-Minimum-Protocol-Version", "", 8,	/* sizeof("TLSv1.x") == 8 */
 	offsetof(struct pg_conn, ssl_min_protocol_version)},
@@ -1255,6 +1259,166 @@ connectOptions2(PGconn *conn)
 		}
 	}
 
+	/*
+	 * parse and validate require_auth option
+	 */
+	if (conn->require_auth)
+	{
+		char	   *s = conn->require_auth;
+		bool		first, more;
+		bool		negated = false;
+
+		/*
+		 * By default, start from an empty set of allowed options and add to it.
+		 */
+		conn->auth_required = true;
+		conn->allowed_auth_methods = 0;
+
+		for (first = true, more = true; more; first = false)
+		{
+			char	   *method, *part;
+			uint32		bits;
+
+			part = parse_comma_separated_list(&s, &more);
+			if (part == NULL)
+				goto oom_error;
+
+			/*
+			 * Check for negation, e.g. '!password'. If one element is negated,
+			 * they all have to be.
+			 */
+			method = part;
+			if (*method == '!')
+			{
+				if (first)
+				{
+					/*
+					 * Switch to a permissive set of allowed options, and
+					 * subtract from it.
+					 */
+					conn->auth_required = false;
+					conn->allowed_auth_methods = -1;
+				}
+				else if (!negated)
+				{
+					conn->status = CONNECTION_BAD;
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("negative require_auth method \"%s\" cannot be mixed with non-negative methods"),
+									  method);
+
+					free(part);
+					return false;
+				}
+
+				negated = true;
+				method++;
+			}
+			else if (negated)
+			{
+				conn->status = CONNECTION_BAD;
+				appendPQExpBuffer(&conn->errorMessage,
+								  libpq_gettext("require_auth method \"%s\" cannot be mixed with negative methods"),
+								  method);
+
+				free(part);
+				return false;
+			}
+
+			if (strcmp(method, "password") == 0)
+			{
+				bits = (1 << AUTH_REQ_PASSWORD);
+			}
+			else if (strcmp(method, "md5") == 0)
+			{
+				bits = (1 << AUTH_REQ_MD5);
+			}
+			else if (strcmp(method, "gss") == 0)
+			{
+				bits = (1 << AUTH_REQ_GSS);
+				bits |= (1 << AUTH_REQ_GSS_CONT);
+			}
+			else if (strcmp(method, "sspi") == 0)
+			{
+				bits = (1 << AUTH_REQ_SSPI);
+				bits |= (1 << AUTH_REQ_GSS_CONT);
+			}
+			else if (strcmp(method, "scram-sha-256") == 0)
+			{
+				/* This currently assumes that SCRAM is the only SASL method. */
+				bits = (1 << AUTH_REQ_SASL);
+				bits |= (1 << AUTH_REQ_SASL_CONT);
+				bits |= (1 << AUTH_REQ_SASL_FIN);
+			}
+			else if (strcmp(method, "none") == 0)
+			{
+				/*
+				 * Special case: let the user explicitly allow (or disallow)
+				 * connections where the server does not send an explicit
+				 * authentication challenge, such as "trust" and "cert" auth.
+				 */
+				if (negated) /* "!none" */
+				{
+					if (conn->auth_required)
+						goto duplicate;
+
+					conn->auth_required = true;
+				}
+				else /* "none" */
+				{
+					if (!conn->auth_required)
+						goto duplicate;
+
+					conn->auth_required = false;
+				}
+
+				free(part);
+				continue; /* avoid the bitmask manipulation below */
+			}
+			else
+			{
+				conn->status = CONNECTION_BAD;
+				appendPQExpBuffer(&conn->errorMessage,
+								  libpq_gettext("invalid require_auth method: \"%s\""),
+								  method);
+
+				free(part);
+				return false;
+			}
+
+			/* Update the bitmask. */
+			if (negated)
+			{
+				if ((conn->allowed_auth_methods & bits) == 0)
+					goto duplicate;
+
+				conn->allowed_auth_methods &= ~bits;
+			}
+			else
+			{
+				if ((conn->allowed_auth_methods & bits) == bits)
+					goto duplicate;
+
+				conn->allowed_auth_methods |= bits;
+			}
+
+			free(part);
+			continue;
+
+duplicate:
+			/*
+			 * A duplicated method probably indicates a typo in a setting where
+			 * typos are extremely risky.
+			 */
+			conn->status = CONNECTION_BAD;
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("require_auth method \"%s\" is specified more than once"),
+							  part);
+
+			free(part);
+			return false;
+		}
+	}
+
 	/*
 	 * validate channel_binding option
 	 */
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 8117cbd40f..fc91cae7a2 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -477,7 +477,6 @@ verify_cb(int ok, X509_STORE_CTX *ctx)
 	return ok;
 }
 
-
 /*
  * OpenSSL-specific wrapper around
  * pq_verify_peer_name_matches_certificate_name(), converting the ASN1_STRING
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 3db6a17db4..3278196eea 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -393,6 +393,7 @@ struct pg_conn
 	char	   *ssl_min_protocol_version;	/* minimum TLS protocol version */
 	char	   *ssl_max_protocol_version;	/* maximum TLS protocol version */
 	char	   *target_session_attrs;	/* desired session properties */
+	char	   *require_auth;	/* name of the expected auth method */
 
 	/* Optional file to write trace info to */
 	FILE	   *Pfdebug;
@@ -454,6 +455,9 @@ struct pg_conn
 	bool		write_failed;	/* have we had a write failure on sock? */
 	char	   *write_err_msg;	/* write error message, or NULL if OOM */
 
+	bool		auth_required;	/* require an authentication challenge from the server? */
+	uint32		allowed_auth_methods;	/* bitmask of acceptable AuthRequest codes */
+
 	/* Transient state needed while establishing connection */
 	PGTargetServerType target_server_type;	/* desired session properties */
 	bool		try_next_addr;	/* time to advance to next address/host? */
@@ -509,6 +513,9 @@ struct pg_conn
 	bool		error_result;	/* do we need to make an ERROR result? */
 	PGresult   *next_result;	/* next result (used in single-row mode) */
 
+	bool		client_finished_auth; /* have we finished our half of the
+									   * authentication exchange? */
+
 	/* Assorted state for SASL, SSL, GSS, etc */
 	const pg_fe_sasl_mech *sasl;
 	void	   *sasl_state;
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 3e3079c824..473a93d6db 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -82,6 +82,74 @@ test_role($node, 'scram_role', 'trust', 0,
 test_role($node, 'md5_role', 'trust', 0,
 	log_unlike => [qr/connection authenticated:/]);
 
+# All positive require_auth options should fail...
+$node->connect_fails("user=scram_role require_auth=gss",
+	"GSS authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=sspi",
+	"SSPI authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=password",
+	"password authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=password,scram-sha-256",
+	"multiple authentication types can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+
+# ...and negative require_auth options should succeed.
+$node->connect_ok("user=scram_role require_auth=!gss",
+	"GSS authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!sspi",
+	"SSPI authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!password",
+	"password authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!md5",
+	"md5 authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!scram-sha-256",
+	"SCRAM authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!password,!scram-sha-256",
+	"multiple authentication types can be forbidden: succeeds with trust auth");
+
+# require_auth=[!]none should interact correctly with trust auth.
+$node->connect_ok("user=scram_role require_auth=none",
+	"all authentication can be forbidden: succeeds with trust auth");
+$node->connect_fails("user=scram_role require_auth=!none",
+	"any authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+
+# Negative and positive require_auth options can't be mixed.
+$node->connect_fails("user=scram_role require_auth=scram-sha-256,!md5",
+	"negative require_auth methods can't be mixed with positive",
+	expected_stderr => qr/negative require_auth method "!md5" cannot be mixed with non-negative methods/);
+$node->connect_fails("user=scram_role require_auth=!password,scram-sha-256",
+	"positive require_auth methods can't be mixed with negative",
+	expected_stderr => qr/require_auth method "scram-sha-256" cannot be mixed with negative methods/);
+
+# require_auth methods can't be duplicated.
+$node->connect_fails("user=scram_role require_auth=password,md5,password",
+	"require_auth methods can't be duplicated: positive case",
+	expected_stderr => qr/require_auth method "password" is specified more than once/);
+$node->connect_fails("user=scram_role require_auth=!password,!md5,!password",
+	"require_auth methods can't be duplicated: negative case",
+	expected_stderr => qr/require_auth method "!password" is specified more than once/);
+$node->connect_fails("user=scram_role require_auth=none,md5,none",
+	"require_auth methods can't be duplicated: none case",
+	expected_stderr => qr/require_auth method "none" is specified more than once/);
+$node->connect_fails("user=scram_role require_auth=!none,!md5,!none",
+	"require_auth methods can't be duplicated: !none case",
+	expected_stderr => qr/require_auth method "!none" is specified more than once/);
+
+# Unknown require_auth methods are caught.
+$node->connect_fails("user=scram_role require_auth=none,abcdefg",
+	"unknown require_auth methods are rejected",
+	expected_stderr => qr/invalid require_auth method: "abcdefg"/);
+
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'password');
 test_role($node, 'scram_role', 'password', 0,
@@ -91,6 +159,33 @@ test_role($node, 'md5_role', 'password', 0,
 	log_like =>
 	  [qr/connection authenticated: identity="md5_role" method=password/]);
 
+# require_auth should succeed with a plaintext password...
+$node->connect_ok("user=scram_role require_auth=password",
+	"password authentication can be required: works with password auth");
+$node->connect_ok("user=scram_role require_auth=!none",
+	"any authentication can be required: works with password auth");
+$node->connect_ok("user=scram_role require_auth=scram-sha-256,password,md5",
+	"multiple authentication types can be required: works with password auth");
+
+# ...fail for other auth types...
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails("user=scram_role require_auth=none",
+	"all authentication can be forbidden: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+
+# ...and allow password authentication to be prohibited.
+$node->connect_fails("user=scram_role require_auth=!password",
+	"password authentication can be forbidden: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails("user=scram_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types can be forbidden: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+
 # For "scram-sha-256" method, user "scram_role" should be able to connect.
 reset_pg_hba($node, 'scram-sha-256');
 test_role(
@@ -104,6 +199,33 @@ test_role(
 test_role($node, 'md5_role', 'scram-sha-256', 2,
 	log_unlike => [qr/connection authenticated:/]);
 
+# require_auth should succeed with SCRAM...
+$node->connect_ok("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: works with SCRAM auth");
+$node->connect_ok("user=scram_role require_auth=!none",
+	"any authentication can be required: works with SCRAM auth");
+$node->connect_ok("user=scram_role require_auth=password,scram-sha-256,md5",
+	"multiple authentication types can be required: works with SCRAM auth");
+
+# ...fail for other auth types...
+$node->connect_fails("user=scram_role require_auth=password",
+	"password authentication can be required: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails("user=scram_role require_auth=none",
+	"all authentication can be forbidden: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+
+# ...and allow SCRAM authentication to be prohibited.
+$node->connect_fails("user=scram_role require_auth=!scram-sha-256",
+	"SCRAM authentication can be forbidden: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails("user=scram_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types can be forbidden: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+
 # Test that bad passwords are rejected.
 $ENV{"PGPASSWORD"} = 'badpass';
 test_role($node, 'scram_role', 'scram-sha-256', 2,
@@ -120,6 +242,33 @@ test_role($node, 'md5_role', 'md5', 0,
 	log_like =>
 	  [qr/connection authenticated: identity="md5_role" method=md5/]);
 
+# require_auth should succeed with MD5...
+$node->connect_ok("user=md5_role require_auth=md5",
+	"MD5 authentication can be required: works with MD5 auth");
+$node->connect_ok("user=md5_role require_auth=!none",
+	"any authentication can be required: works with MD5 auth");
+$node->connect_ok("user=md5_role require_auth=md5,scram-sha-256,password",
+	"multiple authentication types can be required: works with MD5 auth");
+
+# ...fail for other auth types...
+$node->connect_fails("user=md5_role require_auth=password",
+	"password authentication can be required: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+$node->connect_fails("user=md5_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+$node->connect_fails("user=md5_role require_auth=none",
+	"all authentication can be forbidden: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+
+# ...and allow MD5 authentication to be prohibited.
+$node->connect_fails("user=md5_role require_auth=!md5",
+	"password authentication can be forbidden: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+$node->connect_fails("user=md5_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types can be forbidden: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+
 # Tests for channel binding without SSL.
 # Using the password authentication method; channel binding can't work
 reset_pg_hba($node, 'password');
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index 62e0542639..de4ddcbf69 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -307,6 +307,24 @@ test_query(
 	'gssencmode=require',
 	'sending 100K lines works');
 
+# require_auth=gss should succeed...
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=disable require_auth=gss",
+	"GSS authentication can be requested: works with GSS auth without encryption");
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss",
+	"GSS authentication can be requested: works with GSS auth with encryption");
+
+# ...and require_auth=sspi should fail.
+$node->connect_fails(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=disable require_auth=sspi",
+	"SSPI authentication can be requested: fails with GSS auth without encryption",
+	expected_stderr => qr/server requested GSSAPI authentication/);
+$node->connect_fails(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=sspi",
+	"SSPI authentication can be requested: fails with GSS auth with encryption",
+	expected_stderr => qr/server did not complete authentication/);
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
 	qq{hostgssenc all all $hostaddr/32 gss map=mymap});
@@ -335,6 +353,14 @@ test_access(
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable',
 	'fails with GSS encryption disabled and hostgssenc hba');
 
+# require_auth=gss should succeed.
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss",
+	"GSS authentication can be requested: works with GSS encryption");
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss,scram-sha-256",
+	"multiple authentication types can be requested: works with GSS encryption");
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
 	qq{hostnogssenc all all $hostaddr/32 gss map=mymap});
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 86dff8bd1f..e7c67920a1 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -216,6 +216,12 @@ test_access(
 		qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/
 	],);
 
+# require_auth=password should complete successfully; other methods should fail.
+$node->connect_ok("user=test1 require_auth=password",
+	"password authentication can be required: works with ldap auth");
+$node->connect_fails("user=test1 require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with ldap auth");
+
 note "search+bind";
 
 unlink($node->data_dir . '/pg_hba.conf');
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 588f47a39b..9957a96e69 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -132,4 +132,29 @@ $node->connect_ok(
 		qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/
 	]);
 
+# channel_binding should continue to function independently of require_auth.
+$node->connect_ok("$common_connstr user=ssltestuser channel_binding=disable require_auth=scram-sha-256",
+	"SCRAM with SSL, channel_binding=disable, and require_auth=scram-sha-256");
+$node->connect_fails(
+	"$common_connstr user=md5testuser require_auth=md5 channel_binding=require",
+	"channel_binding can fail even when require_auth succeeds",
+	expected_stderr =>
+	  qr/channel binding required but not supported by server's authentication request/
+);
+if ($supports_tls_server_end_point)
+{
+	$node->connect_ok(
+		"$common_connstr user=ssltestuser channel_binding=require require_auth=scram-sha-256",
+		"SCRAM with SSL, channel_binding=require, and require_auth=scram-sha-256");
+}
+else
+{
+	$node->connect_fails(
+		"$common_connstr user=ssltestuser channel_binding=require require_auth=scram-sha-256",
+		"SCRAM with SSL, channel_binding=require, and require_auth=scram-sha-256",
+		expected_stderr =>
+		  qr/channel binding is required, but server did not offer an authentication method that supports channel binding/
+	);
+}
+
 done_testing();
-- 
2.25.1

#23Michael Paquier
michael@paquier.xyz
In reply to: Jacob Champion (#22)
Re: [PoC] Let libpq reject unexpected authentication requests

On Thu, Jun 30, 2022 at 04:26:54PM -0700, Jacob Champion wrote:

Yeah, maybe it'd be better to tell the user the correct context for an
otherwise-valid option ("the 'sslcert' option may only be applied to
USER MAPPING"), and avoid the option dump entirely?

Yes, that would be nice. Now, this HINT has been an annoyance in the
context of the regression tests when adding features entirely
unrelated to postgres_fdw, at least for me. I would be more tempted
to get rid of it entirely, FWIW.
--
Michael

#24Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Jacob Champion (#22)
Re: [PoC] Let libpq reject unexpected authentication requests

I'm wondering about making the list of things you can specify in
require_auth less confusing and future proof.

For example, before long someone is going to try putting "ldap" into
require_auth. The fact that the methods in pg_hba.conf are not what
libpq sees is not something that was really exposed to users until now.
"none" vs. "trust" takes advantage of that. But then I think we could
also make "password" clearer, which surely sounds like any kind of
password, encrypted or not, and that's also how pg_hba.conf behaves.
The protocol specification calls that "AuthenticationCleartextPassword";
maybe we could pick a name based on that.

And then, what if we add a new method in the future, and someone puts
that into their connection string. Old clients will just refuse to
parse that. Ok, that effectively gives you the same behavior as
rejecting the server's authentication offer. But what about the negated
version? Also, what if we add new SASL methods. How would we modify
this code to be able to pick and choose and also have backward and
forward compatible behavior?

In general, I like this. We just need to think about the above things a
bit more.

#25Jacob Champion
jchampion@timescale.com
In reply to: Peter Eisentraut (#24)
Re: [PoC] Let libpq reject unexpected authentication requests

On Thu, Sep 8, 2022 at 6:25 AM Peter Eisentraut
<peter.eisentraut@enterprisedb.com> wrote:

For example, before long someone is going to try putting "ldap" into
require_auth. The fact that the methods in pg_hba.conf are not what
libpq sees is not something that was really exposed to users until now.
"none" vs. "trust" takes advantage of that. But then I think we could
also make "password" clearer, which surely sounds like any kind of
password, encrypted or not, and that's also how pg_hba.conf behaves.
The protocol specification calls that "AuthenticationCleartextPassword";
maybe we could pick a name based on that.

Sounds fair. "cleartext"? "plaintext"? "plain" (like SASL's PLAIN)?

And then, what if we add a new method in the future, and someone puts
that into their connection string. Old clients will just refuse to
parse that. Ok, that effectively gives you the same behavior as
rejecting the server's authentication offer. But what about the negated
version?

I assume the alternative behavior you're thinking of is to ignore
negated "future methods"? I think the danger with that (for a feature
that's supposed to be locking communication down) is that it's not
possible to differentiate between a maybe-future method and a typo. If
I want "!password" because my intention is to disallow a plaintext
exchange, I really don't want "!pasword" to silently allow anything.

Also, what if we add new SASL methods. How would we modify
this code to be able to pick and choose and also have backward and
forward compatible behavior?

On the SASL front: In the back of my head I'd been considering adding
a "sasl:" prefix to "scram-sha-256", so that we have a namespace for
new SASL methods. That would also give us a jumping-off point in the
future if we decide to add SASL method negotiation to the protocol.
What do you think about that?

Backwards compatibility will, I think, be handled trivially by a newer
client. The only way to break backwards compatibility would be to
remove support for a method, which I assume would be independent of
this feature.

Forwards compatibility doesn't seem like something this feature can
add by itself (old clients can't speak new methods). Though we could
backport new method names to allow them to be used in negations, if
maintaining that aspect of compatibility is worth the effort.

In general, I like this. We just need to think about the above things a
bit more.

Thanks!

--Jacob

#26Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Jacob Champion (#25)
Re: [PoC] Let libpq reject unexpected authentication requests

On 08.09.22 20:18, Jacob Champion wrote:

Sounds fair. "cleartext"? "plaintext"? "plain" (like SASL's PLAIN)?

On the SASL front: In the back of my head I'd been considering adding
a "sasl:" prefix to "scram-sha-256", so that we have a namespace for
new SASL methods. That would also give us a jumping-off point in the
future if we decide to add SASL method negotiation to the protocol.
What do you think about that?

After thinking about this a bit more, I think it would be best if the
words used here match exactly with what is used in pg_hba.conf. That's
the only thing the user cares about: reject "password", reject "trust",
require "scram-sha-256", etc. How this maps to the protocol and that
some things are SASL or not is not something they have needed to care
about and don't really need to know for this. So I would suggest to
organize it that way.

Another idea: Maybe instead of the "!" syntax, use two settings,
require_auth and reject_auth? Might be simpler?

#27Jacob Champion
jchampion@timescale.com
In reply to: Peter Eisentraut (#26)
Re: [PoC] Let libpq reject unexpected authentication requests

On Fri, Sep 16, 2022 at 7:56 AM Peter Eisentraut
<peter.eisentraut@enterprisedb.com> wrote:

On 08.09.22 20:18, Jacob Champion wrote:
After thinking about this a bit more, I think it would be best if the
words used here match exactly with what is used in pg_hba.conf. That's
the only thing the user cares about: reject "password", reject "trust",
require "scram-sha-256", etc. How this maps to the protocol and that
some things are SASL or not is not something they have needed to care
about and don't really need to know for this. So I would suggest to
organize it that way.

I tried that in v1, if you'd like to see what that ends up looking
like. As a counterexample, I believe `cert` auth looks identical to
`trust` on the client side. (The server always asks for a client
certificate even if it doesn't use it. Otherwise, this proposal would
probably have looked different.) And `ldap` auth is indistinguishable
from `password`, etc. In my opinion, how it maps to the protocol is
more honest to the user than how it maps to HBA, because the auth
request sent by the protocol determines your level of risk.

I also like `none` over `trust` because you don't have to administer a
server to understand what it means. That's why I was on board with
your proposal to change the name of `password`. And you don't have to
ignore the natural meaning of client-side "trust", which IMO means
"trust the server." There's opportunity for confusion either way,
unfortunately, but naming them differently may help make it clear that
they _are_ different.

This problem overlaps a bit with the last remaining TODO in the code.
I treat gssenc tunnels as satisfying require_auth=gss. Maybe that's
useful, because it kind of maps to how HBA treats it? But it's not
consistent with the TLS side of things, and it overlaps with
gssencmode=require, complicating the relationship with the new
sslcertmode.

Another idea: Maybe instead of the "!" syntax, use two settings,
require_auth and reject_auth? Might be simpler?

Might be. If that means we have to handle the case where both are set
to something, though, it might make things harder.

We can error out if they conflict, which adds a decent but not huge
amount of complication. Or we can require that only one is set, which
is both easy and overly restrictive. But either choice makes it harder
to adopt a `reject password` default, as many people seem to be
interested in doing. Because if you want to override that default,
then you have to first unset reject_auth and then set require_auth, as
opposed to just saying require_auth=something and being done with it.
I'm not sure that's worth it. Thoughts?

I'm happy to implement proofs of concept for that, or any other ideas,
given the importance of getting this "right enough" the first time.
Just let me know.

Thanks,
--Jacob

#28Jacob Champion
jchampion@timescale.com
In reply to: Jacob Champion (#27)
2 attachment(s)
Re: [PoC] Let libpq reject unexpected authentication requests

On Fri, Sep 16, 2022 at 1:29 PM Jacob Champion <jchampion@timescale.com> wrote:

I'm happy to implement proofs of concept for that, or any other ideas,
given the importance of getting this "right enough" the first time.
Just let me know.

v8 rebases over the postgres_fdw HINT changes; there are no functional
differences.

--Jacob

Attachments:

v8-0002-Add-sslcertmode-option-for-client-certificates.patchtext/x-patch; charset=US-ASCII; name=v8-0002-Add-sslcertmode-option-for-client-certificates.patchDownload
From 2343bc37ebdbb6847d6a537f094a5de660274cc3 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jchampion@timescale.com>
Date: Fri, 24 Jun 2022 15:40:42 -0700
Subject: [PATCH v8 2/2] Add sslcertmode option for client certificates

The sslcertmode option controls whether the server is allowed and/or
required to request a certificate from the client. There are three
modes:

- "allow" is the default and follows the current behavior -- a
  configured  sslcert is sent if the server requests one (which, with
  the current implementation, will happen whenever TLS is negotiated).

- "disable" causes the client to refuse to send a client certificate
  even if an sslcert is configured.

- "require" causes the client to fail if a client certificate is never
  sent and the server opens a connection anyway. This doesn't add any
  additional security, since there is no guarantee that the server is
  validating the certificate correctly, but it may help troubleshoot
  more complicated TLS setups.

sslcertmode=require needs the OpenSSL implementation to support
SSL_CTX_set_cert_cb(). Notably, LibreSSL does not.
---
 configure                                | 11 ++---
 configure.ac                             |  4 +-
 doc/src/sgml/libpq.sgml                  | 54 +++++++++++++++++++++++
 src/include/pg_config.h.in               |  3 ++
 src/interfaces/libpq/fe-auth.c           | 21 +++++++++
 src/interfaces/libpq/fe-connect.c        | 55 ++++++++++++++++++++++++
 src/interfaces/libpq/fe-secure-openssl.c | 40 ++++++++++++++++-
 src/interfaces/libpq/libpq-int.h         |  3 ++
 src/test/ssl/t/001_ssltests.pl           | 43 ++++++++++++++++++
 src/tools/msvc/Solution.pm               |  8 ++++
 10 files changed, 234 insertions(+), 8 deletions(-)

diff --git a/configure b/configure
index 4efed743a1..86cbf5d68a 100755
--- a/configure
+++ b/configure
@@ -12904,13 +12904,14 @@ else
 fi
 
   fi
-  # Function introduced in OpenSSL 1.0.2.
-  for ac_func in X509_get_signature_nid
+  # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all of these.
+  for ac_func in X509_get_signature_nid SSL_CTX_set_cert_cb
 do :
-  ac_fn_c_check_func "$LINENO" "X509_get_signature_nid" "ac_cv_func_X509_get_signature_nid"
-if test "x$ac_cv_func_X509_get_signature_nid" = xyes; then :
+  as_ac_var=`$as_echo "ac_cv_func_$ac_func" | $as_tr_sh`
+ac_fn_c_check_func "$LINENO" "$ac_func" "$as_ac_var"
+if eval test \"x\$"$as_ac_var"\" = x"yes"; then :
   cat >>confdefs.h <<_ACEOF
-#define HAVE_X509_GET_SIGNATURE_NID 1
+#define `$as_echo "HAVE_$ac_func" | $as_tr_cpp` 1
 _ACEOF
 
 fi
diff --git a/configure.ac b/configure.ac
index 967f7e7209..936c4f4d07 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1350,8 +1350,8 @@ if test "$with_ssl" = openssl ; then
      AC_SEARCH_LIBS(CRYPTO_new_ex_data, [eay32 crypto], [], [AC_MSG_ERROR([library 'eay32' or 'crypto' is required for OpenSSL])])
      AC_SEARCH_LIBS(SSL_new, [ssleay32 ssl], [], [AC_MSG_ERROR([library 'ssleay32' or 'ssl' is required for OpenSSL])])
   fi
-  # Function introduced in OpenSSL 1.0.2.
-  AC_CHECK_FUNCS([X509_get_signature_nid])
+  # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all of these.
+  AC_CHECK_FUNCS([X509_get_signature_nid SSL_CTX_set_cert_cb])
   # Functions introduced in OpenSSL 1.1.0. We used to check for
   # OPENSSL_VERSION_NUMBER, but that didn't work with 1.1.0, because LibreSSL
   # defines OPENSSL_VERSION_NUMBER to claim version 2.0.0, even though it
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 1d6c02058e..821078ea6a 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1810,6 +1810,50 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-sslcertmode" xreflabel="sslcertmode">
+      <term><literal>sslcertmode</literal></term>
+      <listitem>
+       <para>
+        This option determines whether a client certificate may be sent to the
+        server, and whether the server is required to request one. There are
+        three modes:
+
+        <variablelist>
+         <varlistentry>
+          <term><literal>disable</literal></term>
+          <listitem>
+           <para>
+            a client certificate is never sent, even if one is provided via
+            <xref linkend="libpq-connect-sslcert" />
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>allow</literal> (default)</term>
+          <listitem>
+           <para>
+            a certificate may be sent, if the server requests one and it has
+            been provided via <literal>sslcert</literal>
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>require</literal></term>
+          <listitem>
+           <para>
+            the server <emphasis>must</emphasis> request a certificate. The
+            connection will fail if the server authenticates the client despite
+            not requesting or receiving one.
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-sslrootcert" xreflabel="sslrootcert">
       <term><literal>sslrootcert</literal></term>
       <listitem>
@@ -7968,6 +8012,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGSSLCERTMODE</envar></primary>
+      </indexterm>
+      <envar>PGSSLCERTMODE</envar> behaves the same as the <xref
+      linkend="libpq-connect-sslcertmode"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index c5a80b829e..08382e868b 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -397,6 +397,9 @@
 /* Define to 1 if you have spinlocks. */
 #undef HAVE_SPINLOCKS
 
+/* Define to 1 if you have the `SSL_CTX_set_cert_cb' function. */
+#undef HAVE_SSL_CTX_SET_CERT_CB
+
 /* Define to 1 if stdbool.h conforms to C99. */
 #undef HAVE_STDBOOL_H
 
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index dce2838126..565e44fcf6 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -885,6 +885,27 @@ check_expected_areq(AuthRequest areq, PGconn *conn)
 	bool		result = true;
 	char	   *reason = NULL;
 
+	if (conn->sslcertmode[0] == 'r' /* require */
+		&& areq == AUTH_REQ_OK)
+	{
+		/*
+		 * Trade off a little bit of complexity to try to get these error
+		 * messages as precise as possible.
+		 */
+		if (!conn->ssl_cert_requested)
+		{
+			appendPQExpBufferStr(&conn->errorMessage,
+								 libpq_gettext("server did not request a certificate"));
+			return false;
+		}
+		else if (!conn->ssl_cert_sent)
+		{
+			appendPQExpBufferStr(&conn->errorMessage,
+								 libpq_gettext("server accepted connection without a valid certificate"));
+			return false;
+		}
+	}
+
 	/*
 	 * If the user required a specific auth method, or specified an allowed set,
 	 * then reject all others here, and make sure the server actually completes
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 40f571fadb..eaca8cee1f 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -125,8 +125,10 @@ static int	ldapServiceLookup(const char *purl, PQconninfoOption *options,
 #define DefaultTargetSessionAttrs	"any"
 #ifdef USE_SSL
 #define DefaultSSLMode "prefer"
+#define DefaultSSLCertMode "allow"
 #else
 #define DefaultSSLMode	"disable"
+#define DefaultSSLCertMode "disable"
 #endif
 #ifdef ENABLE_GSS
 #include "fe-gssapi-common.h"
@@ -283,6 +285,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"SSL-Client-Key", "", 64,
 	offsetof(struct pg_conn, sslkey)},
 
+	{"sslcertmode", "PGSSLCERTMODE", NULL, NULL,
+		"SSL-Client-Cert-Mode", "", 8, /* sizeof("disable") == 8 */
+	offsetof(struct pg_conn, sslcertmode)},
+
 	{"sslpassword", NULL, NULL, NULL,
 		"SSL-Client-Key-Password", "*", 20,
 	offsetof(struct pg_conn, sslpassword)},
@@ -1514,6 +1520,55 @@ duplicate:
 		return false;
 	}
 
+	/*
+	 * validate sslcertmode option
+	 */
+	if (conn->sslcertmode)
+	{
+		if (strcmp(conn->sslcertmode, "disable") != 0 &&
+			strcmp(conn->sslcertmode, "allow") != 0 &&
+			strcmp(conn->sslcertmode, "require") != 0)
+		{
+			conn->status = CONNECTION_BAD;
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("invalid %s value: \"%s\"\n"),
+							  "sslcertmode",
+							  conn->sslcertmode);
+			return false;
+		}
+#ifndef USE_SSL
+		if (strcmp(conn->sslcertmode, "require") == 0)
+		{
+			conn->status = CONNECTION_BAD;
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("sslcertmode value \"%s\" invalid when SSL support is not compiled in\n"),
+							  conn->sslcertmode);
+			return false;
+		}
+#endif
+#ifndef HAVE_SSL_CTX_SET_CERT_CB
+		/*
+		 * Without a certificate callback, the current implementation can't
+		 * figure out if a certficate was actually requested, so "require" is
+		 * useless.
+		 */
+		if (strcmp(conn->sslcertmode, "require") == 0)
+		{
+			conn->status = CONNECTION_BAD;
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("sslcertmode value \"%s\" is not supported (check OpenSSL version)\n"),
+							  conn->sslcertmode);
+			return false;
+		}
+#endif
+	}
+	else
+	{
+		conn->sslcertmode = strdup(DefaultSSLCertMode);
+		if (!conn->sslcertmode)
+			goto oom_error;
+	}
+
 	/*
 	 * validate gssencmode option
 	 */
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 957c080356..10d65c966f 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -475,6 +475,34 @@ verify_cb(int ok, X509_STORE_CTX *ctx)
 	return ok;
 }
 
+#ifdef HAVE_SSL_CTX_SET_CERT_CB
+/*
+ * Certificate selection callback
+ *
+ * This callback lets us choose the client certificate we send to the server
+ * after seeing its CertificateRequest. We only support sending a single
+ * hard-coded certificate via sslcert, so we don't actually set any certificates
+ * here; we just it to record whether or not the server has actually asked for
+ * one and whether we have one to send.
+ */
+static int
+cert_cb(SSL *ssl, void *arg)
+{
+	PGconn *conn = arg;
+	conn->ssl_cert_requested = true;
+
+	/* Do we have a certificate loaded to send back? */
+	if (SSL_get_certificate(ssl))
+		conn->ssl_cert_sent = true;
+
+	/*
+	 * Tell OpenSSL that the callback succeeded; we're not required to actually
+	 * make any changes to the SSL handle.
+	 */
+	return 1;
+}
+#endif
+
 /*
  * OpenSSL-specific wrapper around
  * pq_verify_peer_name_matches_certificate_name(), converting the ASN1_STRING
@@ -969,6 +997,11 @@ initialize_SSL(PGconn *conn)
 		SSL_CTX_set_default_passwd_cb_userdata(SSL_context, conn);
 	}
 
+#ifdef HAVE_SSL_CTX_SET_CERT_CB
+	/* Set up a certificate selection callback. */
+	SSL_CTX_set_cert_cb(SSL_context, cert_cb, conn);
+#endif
+
 	/* Disable old protocol versions */
 	SSL_CTX_set_options(SSL_context, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
 
@@ -1132,7 +1165,12 @@ initialize_SSL(PGconn *conn)
 	else
 		fnbuf[0] = '\0';
 
-	if (fnbuf[0] == '\0')
+	if (conn->sslcertmode[0] == 'd') /* disable */
+	{
+		/* don't send a client cert even if we have one */
+		have_cert = false;
+	}
+	else if (fnbuf[0] == '\0')
 	{
 		/* no home directory, proceed without a client cert */
 		have_cert = false;
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index e4d88e22a1..ad6f8b78c6 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -384,6 +384,7 @@ struct pg_conn
 	char	   *sslkey;			/* client key filename */
 	char	   *sslcert;		/* client certificate filename */
 	char	   *sslpassword;	/* client key file password */
+	char	   *sslcertmode;	/* client cert mode (require,allow,disable) */
 	char	   *sslrootcert;	/* root certificate filename */
 	char	   *sslcrl;			/* certificate revocation list filename */
 	char	   *sslcrldir;		/* certificate revocation list directory name */
@@ -525,6 +526,8 @@ struct pg_conn
 
 	/* SSL structures */
 	bool		ssl_in_use;
+	bool		ssl_cert_requested;	/* Did the server ask us for a cert? */
+	bool		ssl_cert_sent;		/* Did we send one in reply? */
 
 #ifdef USE_SSL
 	bool		allow_ssl_try;	/* Allowed to try SSL negotiation */
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index efe5634fff..49003ba1b7 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -42,6 +42,10 @@ my $SERVERHOSTADDR = '127.0.0.1';
 # This is the pattern to use in pg_hba.conf to match incoming connections.
 my $SERVERHOSTCIDR = '127.0.0.1/32';
 
+# Determine whether build supports sslcertmode=require.
+my $supports_sslcertmode_require =
+  check_pg_config("#define HAVE_SSL_CTX_SET_CERT_CB 1");
+
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
 
@@ -191,6 +195,22 @@ $node->connect_ok(
 	"$common_connstr sslrootcert=ssl/both-cas-2.crt sslmode=verify-ca",
 	"cert root file that contains two certificates, order 2");
 
+# sslcertmode=allow and =disable should both work without a client certificate.
+$node->connect_ok(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=disable",
+	"connect with sslcertmode=disable");
+$node->connect_ok(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=allow",
+	"connect with sslcertmode=allow");
+
+# sslcertmode=require, however, should fail.
+$node->connect_fails(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=require",
+	"connect with sslcertmode=require fails without a client certificate",
+	expected_stderr => $supports_sslcertmode_require
+		? qr/server accepted connection without a valid certificate/
+		: qr/sslcertmode value "require" is not supported/);
+
 # CRL tests
 
 # Invalid CRL filename is the same as no CRL, succeeds
@@ -538,6 +558,29 @@ $node->connect_ok(
 	"certificate authorization succeeds with correct client cert in encrypted DER format"
 );
 
+# correct client cert with required/allowed certificate authentication
+if ($supports_sslcertmode_require)
+{
+	$node->connect_ok(
+		"$common_connstr user=ssltestuser sslcertmode=require sslcert=ssl/client.crt "
+		  . sslkey('client.key'),
+		"certificate authorization succeeds with sslcertmode=require"
+	);
+}
+$node->connect_ok(
+	"$common_connstr user=ssltestuser sslcertmode=allow sslcert=ssl/client.crt "
+	  . sslkey('client.key'),
+	"certificate authorization succeeds with sslcertmode=allow"
+);
+
+# client cert isn't sent if certificate authentication is disabled
+$node->connect_fails(
+	"$common_connstr user=ssltestuser sslcertmode=disable sslcert=ssl/client.crt "
+	  . sslkey('client.key'),
+	"certificate authorization fails with sslcertmode=disable",
+	expected_stderr => qr/connection requires a valid client certificate/
+);
+
 # correct client cert in encrypted PEM with wrong password
 $node->connect_fails(
 	"$common_connstr user=ssltestuser sslcert=ssl/client.crt "
diff --git a/src/tools/msvc/Solution.pm b/src/tools/msvc/Solution.pm
index c2acb58df0..ff6d6f9e35 100644
--- a/src/tools/msvc/Solution.pm
+++ b/src/tools/msvc/Solution.pm
@@ -328,6 +328,7 @@ sub GenerateFiles
 		HAVE_SETPROCTITLE_FAST                   => undef,
 		HAVE_SOCKLEN_T                           => 1,
 		HAVE_SPINLOCKS                           => 1,
+		HAVE_SSL_CTX_SET_CERT_CB                 => undef,
 		HAVE_STDBOOL_H                           => 1,
 		HAVE_STDINT_H                            => 1,
 		HAVE_STDLIB_H                            => 1,
@@ -489,6 +490,13 @@ sub GenerateFiles
 
 		my ($digit1, $digit2, $digit3) = $self->GetOpenSSLVersion();
 
+		if (   ($digit1 >= '3' && $digit2 >= '0' && $digit3 >= '0')
+			|| ($digit1 >= '1' && $digit2 >= '1' && $digit3 >= '0')
+			|| ($digit1 >= '1' && $digit2 >= '0' && $digit3 >= '2'))
+		{
+			$define{HAVE_SSL_CTX_SET_CERT_CB} = 1;
+		}
+
 		# More symbols are needed with OpenSSL 1.1.0 and above.
 		if (   ($digit1 >= '3' && $digit2 >= '0' && $digit3 >= '0')
 			|| ($digit1 >= '1' && $digit2 >= '1' && $digit3 >= '0'))
-- 
2.25.1

v8-0001-libpq-let-client-reject-unexpected-auth-methods.patchtext/x-patch; charset=US-ASCII; name=v8-0001-libpq-let-client-reject-unexpected-auth-methods.patchDownload
From 3f5825d656f4a6bfd8d622e0781432f9b714775c Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Mon, 28 Feb 2022 09:40:43 -0800
Subject: [PATCH v8 1/2] libpq: let client reject unexpected auth methods

The require_auth connection option allows the client to choose a list of
acceptable authentication types for use with the server. There is no
negotiation: if the server does not present one of the allowed
authentication requests, the connection fails. Additionally, all methods
in the list may be negated, e.g. '!password', in which case the server
must NOT use the listed authentication type. The special method "none"
allows/disallows the use of unauthenticated connections (but it does not
govern transport-level authentication via TLS or GSSAPI).

Internally, the patch expands the role of check_expected_areq() to
ensure that the incoming request is compatible with conn->require_auth.
It also introduces a new flag, conn->client_finished_auth, which is set
by various authentication routines when the client side of the handshake
is finished. This signals to check_expected_areq() that an OK message
from the server is expected, and allows the client to complain if the
server forgoes authentication entirely.

(Since the client can't generally prove that the server is actually
doing the work of authentication, this last part is mostly useful for
SCRAM without channel binding. It could also provide a client with a
decent signal that, at the very least, it's not connecting to a database
with trust auth, and so the connection can be tied to the client in a
later audit.)

Deficiencies:
- This feature currently doesn't prevent unexpected certificate exchange
  or unexpected GSSAPI encryption.
- This is unlikely to be very forwards-compatible at the moment,
  especially with SASL/SCRAM.
- SSPI support is "implemented" but untested.
- require_auth=none,scram-sha-256 currently allows the server to leave a
  SCRAM exchange unfinished. This is not net-new behavior but may be
  surprising.
---
 doc/src/sgml/libpq.sgml                   | 105 ++++++++++++++
 src/include/libpq/pqcomm.h                |   1 +
 src/interfaces/libpq/fe-auth-scram.c      |   1 +
 src/interfaces/libpq/fe-auth.c            | 138 ++++++++++++++++++
 src/interfaces/libpq/fe-connect.c         | 164 ++++++++++++++++++++++
 src/interfaces/libpq/fe-secure-openssl.c  |   1 -
 src/interfaces/libpq/libpq-int.h          |   7 +
 src/test/authentication/t/001_password.pl | 149 ++++++++++++++++++++
 src/test/kerberos/t/001_auth.pl           |  26 ++++
 src/test/ldap/t/001_auth.pl               |   6 +
 src/test/ssl/t/002_scram.pl               |  25 ++++
 11 files changed, 622 insertions(+), 1 deletion(-)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 8a1a9e9932..1d6c02058e 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1220,6 +1220,101 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-require-auth" xreflabel="require_auth">
+      <term><literal>require_auth</literal></term>
+      <listitem>
+      <para>
+        Specifies the authentication method that the client requires from the
+        server. If the server does not use the required method to authenticate
+        the client, or if the authentication handshake is not fully completed by
+        the server, the connection will fail. A comma-separated list of methods
+        may also be provided, of which the server must use exactly one in order
+        for the connection to succeed. By default, any authentication method is
+        accepted, and the server is free to skip authentication altogether.
+      </para>
+      <para>
+        Methods may be negated with the addition of a <literal>!</literal>
+        prefix, in which case the server must <emphasis>not</emphasis> attempt
+        the listed method; any other method is accepted, and the server is free
+        not to authenticate the client at all. If a comma-separated list is
+        provided, the server may not attempt <emphasis>any</emphasis> of the
+        listed negated methods. Negated and non-negated forms may not be
+        combined in the same setting.
+      </para>
+      <para>
+        As a final special case, the <literal>none</literal> method requires the
+        server not to use an authentication challenge. (It may also be negated,
+        to require some form of authentication.)
+      </para>
+      <para>
+        The following methods may be specified:
+
+        <variablelist>
+         <varlistentry>
+          <term><literal>password</literal></term>
+          <listitem>
+           <para>
+            The server must request plaintext password authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>md5</literal></term>
+          <listitem>
+           <para>
+            The server must request MD5 hashed password authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>gss</literal></term>
+          <listitem>
+           <para>
+            The server must either request a Kerberos handshake via
+            <acronym>GSSAPI</acronym> or establish a
+            <acronym>GSS</acronym>-encrypted channel (see also
+            <xref linkend="libpq-connect-gssencmode" />).
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>sspi</literal></term>
+          <listitem>
+           <para>
+            The server must request Windows <acronym>SSPI</acronym>
+            authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>scram-sha-256</literal></term>
+          <listitem>
+           <para>
+            The server must successfully complete a SCRAM-SHA-256 authentication
+            exchange with the client.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>none</literal></term>
+          <listitem>
+           <para>
+            The server must not prompt the client for an authentication
+            exchange. (This does not prohibit client certificate authentication
+			via TLS, nor GSS authentication via its encrypted transport.)
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+      </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-channel-binding" xreflabel="channel_binding">
       <term><literal>channel_binding</literal></term>
       <listitem>
@@ -7756,6 +7851,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGREQUIREAUTH</envar></primary>
+      </indexterm>
+      <envar>PGREQUIREAUTH</envar> behaves the same as the <xref
+      linkend="libpq-connect-require-auth"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h
index fcf68df39b..912451c913 100644
--- a/src/include/libpq/pqcomm.h
+++ b/src/include/libpq/pqcomm.h
@@ -123,6 +123,7 @@ extern PGDLLIMPORT bool Db_user_namespace;
 #define AUTH_REQ_SASL	   10	/* Begin SASL authentication */
 #define AUTH_REQ_SASL_CONT 11	/* Continue SASL authentication */
 #define AUTH_REQ_SASL_FIN  12	/* Final SASL message */
+#define AUTH_REQ_MAX	   AUTH_REQ_SASL_FIN	/* maximum AUTH_REQ_* value */
 
 typedef uint32 AuthRequest;
 
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index 35cfd9987d..ad57df88b6 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -280,6 +280,7 @@ scram_exchange(void *opaq, char *input, int inputlen,
 			}
 			*done = true;
 			state->state = FE_SCRAM_FINISHED;
+			state->conn->client_finished_auth = true;
 			break;
 
 		default:
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 49a1c626f6..dce2838126 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -137,7 +137,10 @@ pg_GSS_continue(PGconn *conn, int payloadlen)
 	}
 
 	if (maj_stat == GSS_S_COMPLETE)
+	{
+		conn->client_finished_auth = true;
 		gss_release_name(&lmin_s, &conn->gtarg_nam);
+	}
 
 	return STATUS_OK;
 }
@@ -326,6 +329,9 @@ pg_SSPI_continue(PGconn *conn, int payloadlen)
 		FreeContextBuffer(outbuf.pBuffers[0].pvBuffer);
 	}
 
+	if (r == SEC_E_OK)
+		conn->client_finished_auth = true;
+
 	/* Cleanup is handled by the code in freePGconn() */
 	return STATUS_OK;
 }
@@ -830,6 +836,44 @@ pg_password_sendauth(PGconn *conn, const char *password, AuthRequest areq)
 	return ret;
 }
 
+/*
+ * Translate an AuthRequest into a human-readable description.
+ */
+static const char *
+auth_description(AuthRequest areq)
+{
+	switch (areq)
+	{
+		case AUTH_REQ_PASSWORD:
+			return libpq_gettext("a cleartext password");
+		case AUTH_REQ_MD5:
+			return libpq_gettext("a hashed password");
+		case AUTH_REQ_GSS:
+		case AUTH_REQ_GSS_CONT:
+			return libpq_gettext("GSSAPI authentication");
+		case AUTH_REQ_SSPI:
+			return libpq_gettext("SSPI authentication");
+		case AUTH_REQ_SCM_CREDS:
+			return libpq_gettext("UNIX socket credentials");
+		case AUTH_REQ_SASL:
+		case AUTH_REQ_SASL_CONT:
+		case AUTH_REQ_SASL_FIN:
+			return libpq_gettext("SASL authentication");
+	}
+
+	return libpq_gettext("an unknown authentication type");
+}
+
+/*
+ * Convenience macro for checking the allowed_auth_methods bitmask. Caller must
+ * ensure that type is not greater than 31 (high bit of the bitmask).
+ */
+#define auth_allowed(conn, type) \
+	(((conn)->allowed_auth_methods & (1 << (type))) != 0)
+
+StaticAssertDecl(AUTH_REQ_MAX < CHAR_BIT * sizeof(((PGconn){0}).allowed_auth_methods),
+				 "AUTH_REQ_MAX overflows the allowed_auth_methods bitmask");
+
 /*
  * Verify that the authentication request is expected, given the connection
  * parameters. This is especially important when the client wishes to
@@ -839,6 +883,97 @@ static bool
 check_expected_areq(AuthRequest areq, PGconn *conn)
 {
 	bool		result = true;
+	char	   *reason = NULL;
+
+	/*
+	 * If the user required a specific auth method, or specified an allowed set,
+	 * then reject all others here, and make sure the server actually completes
+	 * an authentication exchange.
+	 */
+	if (conn->require_auth)
+	{
+		switch (areq)
+		{
+			case AUTH_REQ_OK:
+				/*
+				 * Check to make sure we've actually finished our exchange (or
+				 * else that the user has allowed an authentication-less
+				 * connection).
+				 *
+				 * TODO: how should !auth_required interact with an incomplete
+				 * SCRAM exchange?
+				 */
+				if (!conn->auth_required || conn->client_finished_auth)
+					break;
+
+				/*
+				 * No explicit authentication request was made by the server --
+				 * or perhaps it was made and not completed, in the case of
+				 * SCRAM -- but there is one special case to check. If the user
+				 * allowed "gss", then a GSS-encrypted channel also satisfies
+				 * the check.
+				 */
+#ifdef ENABLE_GSS
+				if (auth_allowed(conn, AUTH_REQ_GSS) && conn->gssenc)
+				{
+					/*
+					 * If implicit GSS auth has already been performed via GSS
+					 * encryption, we don't need to have performed an
+					 * AUTH_REQ_GSS exchange.
+					 *
+					 * TODO: check this assumption. What mutual auth guarantees
+					 * are made in this case?
+					 */
+				}
+				else
+#endif
+				{
+					reason = libpq_gettext("server did not complete authentication"),
+					result = false;
+				}
+
+				break;
+
+			case AUTH_REQ_PASSWORD:
+			case AUTH_REQ_MD5:
+			case AUTH_REQ_GSS:
+			case AUTH_REQ_SSPI:
+			case AUTH_REQ_GSS_CONT:
+			case AUTH_REQ_SASL:
+			case AUTH_REQ_SASL_CONT:
+			case AUTH_REQ_SASL_FIN:
+				/*
+				 * We don't handle these with the default case, to avoid
+				 * bit-shifting past the end of the allowed_auth_methods mask if
+				 * the server sends an unexpected AuthRequest.
+				 */
+				result = auth_allowed(conn, areq);
+				break;
+
+			default:
+				result = false;
+				break;
+		}
+	}
+
+	if (!result)
+	{
+		if (reason)
+		{
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("auth method \"%s\" requirement failed: %s\n"),
+							  conn->require_auth, reason);
+		}
+		else
+		{
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("auth method \"%s\" required, but server requested %s\n"),
+							  conn->require_auth,
+							  auth_description(areq));
+		}
+
+		return result;
+	}
 
 	/*
 	 * When channel_binding=require, we must protect against two cases: (1) we
@@ -1040,6 +1175,9 @@ pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn)
 										 "fe_sendauth: error sending password authentication\n");
 					return STATUS_ERROR;
 				}
+
+				/* We expect no further authentication requests. */
+				conn->client_finished_auth = true;
 				break;
 			}
 
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 746e9b4f1e..40f571fadb 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -307,6 +307,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Require-Peer", "", 10,
 	offsetof(struct pg_conn, requirepeer)},
 
+	{"require_auth", "PGREQUIREAUTH", NULL, NULL,
+		"Require-Auth", "", 14, /* sizeof("scram-sha-256") == 14 */
+	offsetof(struct pg_conn, require_auth)},
+
 	{"ssl_min_protocol_version", "PGSSLMINPROTOCOLVERSION", "TLSv1.2", NULL,
 		"SSL-Minimum-Protocol-Version", "", 8,	/* sizeof("TLSv1.x") == 8 */
 	offsetof(struct pg_conn, ssl_min_protocol_version)},
@@ -1240,6 +1244,166 @@ connectOptions2(PGconn *conn)
 		}
 	}
 
+	/*
+	 * parse and validate require_auth option
+	 */
+	if (conn->require_auth)
+	{
+		char	   *s = conn->require_auth;
+		bool		first, more;
+		bool		negated = false;
+
+		/*
+		 * By default, start from an empty set of allowed options and add to it.
+		 */
+		conn->auth_required = true;
+		conn->allowed_auth_methods = 0;
+
+		for (first = true, more = true; more; first = false)
+		{
+			char	   *method, *part;
+			uint32		bits;
+
+			part = parse_comma_separated_list(&s, &more);
+			if (part == NULL)
+				goto oom_error;
+
+			/*
+			 * Check for negation, e.g. '!password'. If one element is negated,
+			 * they all have to be.
+			 */
+			method = part;
+			if (*method == '!')
+			{
+				if (first)
+				{
+					/*
+					 * Switch to a permissive set of allowed options, and
+					 * subtract from it.
+					 */
+					conn->auth_required = false;
+					conn->allowed_auth_methods = -1;
+				}
+				else if (!negated)
+				{
+					conn->status = CONNECTION_BAD;
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("negative require_auth method \"%s\" cannot be mixed with non-negative methods"),
+									  method);
+
+					free(part);
+					return false;
+				}
+
+				negated = true;
+				method++;
+			}
+			else if (negated)
+			{
+				conn->status = CONNECTION_BAD;
+				appendPQExpBuffer(&conn->errorMessage,
+								  libpq_gettext("require_auth method \"%s\" cannot be mixed with negative methods"),
+								  method);
+
+				free(part);
+				return false;
+			}
+
+			if (strcmp(method, "password") == 0)
+			{
+				bits = (1 << AUTH_REQ_PASSWORD);
+			}
+			else if (strcmp(method, "md5") == 0)
+			{
+				bits = (1 << AUTH_REQ_MD5);
+			}
+			else if (strcmp(method, "gss") == 0)
+			{
+				bits = (1 << AUTH_REQ_GSS);
+				bits |= (1 << AUTH_REQ_GSS_CONT);
+			}
+			else if (strcmp(method, "sspi") == 0)
+			{
+				bits = (1 << AUTH_REQ_SSPI);
+				bits |= (1 << AUTH_REQ_GSS_CONT);
+			}
+			else if (strcmp(method, "scram-sha-256") == 0)
+			{
+				/* This currently assumes that SCRAM is the only SASL method. */
+				bits = (1 << AUTH_REQ_SASL);
+				bits |= (1 << AUTH_REQ_SASL_CONT);
+				bits |= (1 << AUTH_REQ_SASL_FIN);
+			}
+			else if (strcmp(method, "none") == 0)
+			{
+				/*
+				 * Special case: let the user explicitly allow (or disallow)
+				 * connections where the server does not send an explicit
+				 * authentication challenge, such as "trust" and "cert" auth.
+				 */
+				if (negated) /* "!none" */
+				{
+					if (conn->auth_required)
+						goto duplicate;
+
+					conn->auth_required = true;
+				}
+				else /* "none" */
+				{
+					if (!conn->auth_required)
+						goto duplicate;
+
+					conn->auth_required = false;
+				}
+
+				free(part);
+				continue; /* avoid the bitmask manipulation below */
+			}
+			else
+			{
+				conn->status = CONNECTION_BAD;
+				appendPQExpBuffer(&conn->errorMessage,
+								  libpq_gettext("invalid require_auth method: \"%s\""),
+								  method);
+
+				free(part);
+				return false;
+			}
+
+			/* Update the bitmask. */
+			if (negated)
+			{
+				if ((conn->allowed_auth_methods & bits) == 0)
+					goto duplicate;
+
+				conn->allowed_auth_methods &= ~bits;
+			}
+			else
+			{
+				if ((conn->allowed_auth_methods & bits) == bits)
+					goto duplicate;
+
+				conn->allowed_auth_methods |= bits;
+			}
+
+			free(part);
+			continue;
+
+duplicate:
+			/*
+			 * A duplicated method probably indicates a typo in a setting where
+			 * typos are extremely risky.
+			 */
+			conn->status = CONNECTION_BAD;
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("require_auth method \"%s\" is specified more than once"),
+							  part);
+
+			free(part);
+			return false;
+		}
+	}
+
 	/*
 	 * validate channel_binding option
 	 */
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index aea4661736..957c080356 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -475,7 +475,6 @@ verify_cb(int ok, X509_STORE_CTX *ctx)
 	return ok;
 }
 
-
 /*
  * OpenSSL-specific wrapper around
  * pq_verify_peer_name_matches_certificate_name(), converting the ASN1_STRING
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index c75ed63a2c..e4d88e22a1 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -396,6 +396,7 @@ struct pg_conn
 	char	   *ssl_min_protocol_version;	/* minimum TLS protocol version */
 	char	   *ssl_max_protocol_version;	/* maximum TLS protocol version */
 	char	   *target_session_attrs;	/* desired session properties */
+	char	   *require_auth;	/* name of the expected auth method */
 
 	/* Optional file to write trace info to */
 	FILE	   *Pfdebug;
@@ -457,6 +458,9 @@ struct pg_conn
 	bool		write_failed;	/* have we had a write failure on sock? */
 	char	   *write_err_msg;	/* write error message, or NULL if OOM */
 
+	bool		auth_required;	/* require an authentication challenge from the server? */
+	uint32		allowed_auth_methods;	/* bitmask of acceptable AuthRequest codes */
+
 	/* Transient state needed while establishing connection */
 	PGTargetServerType target_server_type;	/* desired session properties */
 	bool		try_next_addr;	/* time to advance to next address/host? */
@@ -512,6 +516,9 @@ struct pg_conn
 	bool		error_result;	/* do we need to make an ERROR result? */
 	PGresult   *next_result;	/* next result (used in single-row mode) */
 
+	bool		client_finished_auth; /* have we finished our half of the
+									   * authentication exchange? */
+
 	/* Assorted state for SASL, SSL, GSS, etc */
 	const pg_fe_sasl_mech *sasl;
 	void	   *sasl_state;
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 3e3079c824..473a93d6db 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -82,6 +82,74 @@ test_role($node, 'scram_role', 'trust', 0,
 test_role($node, 'md5_role', 'trust', 0,
 	log_unlike => [qr/connection authenticated:/]);
 
+# All positive require_auth options should fail...
+$node->connect_fails("user=scram_role require_auth=gss",
+	"GSS authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=sspi",
+	"SSPI authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=password",
+	"password authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=password,scram-sha-256",
+	"multiple authentication types can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+
+# ...and negative require_auth options should succeed.
+$node->connect_ok("user=scram_role require_auth=!gss",
+	"GSS authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!sspi",
+	"SSPI authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!password",
+	"password authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!md5",
+	"md5 authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!scram-sha-256",
+	"SCRAM authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!password,!scram-sha-256",
+	"multiple authentication types can be forbidden: succeeds with trust auth");
+
+# require_auth=[!]none should interact correctly with trust auth.
+$node->connect_ok("user=scram_role require_auth=none",
+	"all authentication can be forbidden: succeeds with trust auth");
+$node->connect_fails("user=scram_role require_auth=!none",
+	"any authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+
+# Negative and positive require_auth options can't be mixed.
+$node->connect_fails("user=scram_role require_auth=scram-sha-256,!md5",
+	"negative require_auth methods can't be mixed with positive",
+	expected_stderr => qr/negative require_auth method "!md5" cannot be mixed with non-negative methods/);
+$node->connect_fails("user=scram_role require_auth=!password,scram-sha-256",
+	"positive require_auth methods can't be mixed with negative",
+	expected_stderr => qr/require_auth method "scram-sha-256" cannot be mixed with negative methods/);
+
+# require_auth methods can't be duplicated.
+$node->connect_fails("user=scram_role require_auth=password,md5,password",
+	"require_auth methods can't be duplicated: positive case",
+	expected_stderr => qr/require_auth method "password" is specified more than once/);
+$node->connect_fails("user=scram_role require_auth=!password,!md5,!password",
+	"require_auth methods can't be duplicated: negative case",
+	expected_stderr => qr/require_auth method "!password" is specified more than once/);
+$node->connect_fails("user=scram_role require_auth=none,md5,none",
+	"require_auth methods can't be duplicated: none case",
+	expected_stderr => qr/require_auth method "none" is specified more than once/);
+$node->connect_fails("user=scram_role require_auth=!none,!md5,!none",
+	"require_auth methods can't be duplicated: !none case",
+	expected_stderr => qr/require_auth method "!none" is specified more than once/);
+
+# Unknown require_auth methods are caught.
+$node->connect_fails("user=scram_role require_auth=none,abcdefg",
+	"unknown require_auth methods are rejected",
+	expected_stderr => qr/invalid require_auth method: "abcdefg"/);
+
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'password');
 test_role($node, 'scram_role', 'password', 0,
@@ -91,6 +159,33 @@ test_role($node, 'md5_role', 'password', 0,
 	log_like =>
 	  [qr/connection authenticated: identity="md5_role" method=password/]);
 
+# require_auth should succeed with a plaintext password...
+$node->connect_ok("user=scram_role require_auth=password",
+	"password authentication can be required: works with password auth");
+$node->connect_ok("user=scram_role require_auth=!none",
+	"any authentication can be required: works with password auth");
+$node->connect_ok("user=scram_role require_auth=scram-sha-256,password,md5",
+	"multiple authentication types can be required: works with password auth");
+
+# ...fail for other auth types...
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails("user=scram_role require_auth=none",
+	"all authentication can be forbidden: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+
+# ...and allow password authentication to be prohibited.
+$node->connect_fails("user=scram_role require_auth=!password",
+	"password authentication can be forbidden: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails("user=scram_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types can be forbidden: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+
 # For "scram-sha-256" method, user "scram_role" should be able to connect.
 reset_pg_hba($node, 'scram-sha-256');
 test_role(
@@ -104,6 +199,33 @@ test_role(
 test_role($node, 'md5_role', 'scram-sha-256', 2,
 	log_unlike => [qr/connection authenticated:/]);
 
+# require_auth should succeed with SCRAM...
+$node->connect_ok("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: works with SCRAM auth");
+$node->connect_ok("user=scram_role require_auth=!none",
+	"any authentication can be required: works with SCRAM auth");
+$node->connect_ok("user=scram_role require_auth=password,scram-sha-256,md5",
+	"multiple authentication types can be required: works with SCRAM auth");
+
+# ...fail for other auth types...
+$node->connect_fails("user=scram_role require_auth=password",
+	"password authentication can be required: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails("user=scram_role require_auth=none",
+	"all authentication can be forbidden: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+
+# ...and allow SCRAM authentication to be prohibited.
+$node->connect_fails("user=scram_role require_auth=!scram-sha-256",
+	"SCRAM authentication can be forbidden: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails("user=scram_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types can be forbidden: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+
 # Test that bad passwords are rejected.
 $ENV{"PGPASSWORD"} = 'badpass';
 test_role($node, 'scram_role', 'scram-sha-256', 2,
@@ -120,6 +242,33 @@ test_role($node, 'md5_role', 'md5', 0,
 	log_like =>
 	  [qr/connection authenticated: identity="md5_role" method=md5/]);
 
+# require_auth should succeed with MD5...
+$node->connect_ok("user=md5_role require_auth=md5",
+	"MD5 authentication can be required: works with MD5 auth");
+$node->connect_ok("user=md5_role require_auth=!none",
+	"any authentication can be required: works with MD5 auth");
+$node->connect_ok("user=md5_role require_auth=md5,scram-sha-256,password",
+	"multiple authentication types can be required: works with MD5 auth");
+
+# ...fail for other auth types...
+$node->connect_fails("user=md5_role require_auth=password",
+	"password authentication can be required: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+$node->connect_fails("user=md5_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+$node->connect_fails("user=md5_role require_auth=none",
+	"all authentication can be forbidden: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+
+# ...and allow MD5 authentication to be prohibited.
+$node->connect_fails("user=md5_role require_auth=!md5",
+	"password authentication can be forbidden: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+$node->connect_fails("user=md5_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types can be forbidden: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+
 # Tests for channel binding without SSL.
 # Using the password authentication method; channel binding can't work
 reset_pg_hba($node, 'password');
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index 47169a1d1e..054df71541 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -311,6 +311,24 @@ test_query(
 	'gssencmode=require',
 	'sending 100K lines works');
 
+# require_auth=gss should succeed...
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=disable require_auth=gss",
+	"GSS authentication can be requested: works with GSS auth without encryption");
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss",
+	"GSS authentication can be requested: works with GSS auth with encryption");
+
+# ...and require_auth=sspi should fail.
+$node->connect_fails(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=disable require_auth=sspi",
+	"SSPI authentication can be requested: fails with GSS auth without encryption",
+	expected_stderr => qr/server requested GSSAPI authentication/);
+$node->connect_fails(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=sspi",
+	"SSPI authentication can be requested: fails with GSS auth with encryption",
+	expected_stderr => qr/server did not complete authentication/);
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
 	qq{hostgssenc all all $hostaddr/32 gss map=mymap});
@@ -339,6 +357,14 @@ test_access(
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable',
 	'fails with GSS encryption disabled and hostgssenc hba');
 
+# require_auth=gss should succeed.
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss",
+	"GSS authentication can be requested: works with GSS encryption");
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss,scram-sha-256",
+	"multiple authentication types can be requested: works with GSS encryption");
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
 	qq{hostnogssenc all all $hostaddr/32 gss map=mymap});
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 2f064f6944..25483e50cc 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -220,6 +220,12 @@ test_access(
 		qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/
 	],);
 
+# require_auth=password should complete successfully; other methods should fail.
+$node->connect_ok("user=test1 require_auth=password",
+	"password authentication can be required: works with ldap auth");
+$node->connect_fails("user=test1 require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with ldap auth");
+
 note "search+bind";
 
 unlink($node->data_dir . '/pg_hba.conf');
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index deaa4aa086..cf61bc7d0d 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -136,4 +136,29 @@ $node->connect_ok(
 		qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/
 	]);
 
+# channel_binding should continue to function independently of require_auth.
+$node->connect_ok("$common_connstr user=ssltestuser channel_binding=disable require_auth=scram-sha-256",
+	"SCRAM with SSL, channel_binding=disable, and require_auth=scram-sha-256");
+$node->connect_fails(
+	"$common_connstr user=md5testuser require_auth=md5 channel_binding=require",
+	"channel_binding can fail even when require_auth succeeds",
+	expected_stderr =>
+	  qr/channel binding required but not supported by server's authentication request/
+);
+if ($supports_tls_server_end_point)
+{
+	$node->connect_ok(
+		"$common_connstr user=ssltestuser channel_binding=require require_auth=scram-sha-256",
+		"SCRAM with SSL, channel_binding=require, and require_auth=scram-sha-256");
+}
+else
+{
+	$node->connect_fails(
+		"$common_connstr user=ssltestuser channel_binding=require require_auth=scram-sha-256",
+		"SCRAM with SSL, channel_binding=require, and require_auth=scram-sha-256",
+		expected_stderr =>
+		  qr/channel binding is required, but server did not offer an authentication method that supports channel binding/
+	);
+}
+
 done_testing();
-- 
2.25.1

#29Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Jacob Champion (#28)
Re: [PoC] Let libpq reject unexpected authentication requests

On 21.09.22 17:33, Jacob Champion wrote:

On Fri, Sep 16, 2022 at 1:29 PM Jacob Champion <jchampion@timescale.com> wrote:

I'm happy to implement proofs of concept for that, or any other ideas,
given the importance of getting this "right enough" the first time.
Just let me know.

v8 rebases over the postgres_fdw HINT changes; there are no functional
differences.

So let's look at the two TODO comments you have:

* TODO: how should !auth_required interact with an incomplete
* SCRAM exchange?

What specific combination of events are you thinking of here?

/*
* If implicit GSS auth has already been performed via GSS
* encryption, we don't need to have performed an
* AUTH_REQ_GSS exchange.
*
* TODO: check this assumption. What mutual auth guarantees
* are made in this case?
*/

I don't understand the details involved here, but I would be surprised
if this assumption is true. For example, does GSS encryption deal with
user names and a user name map? I don't see how these can be
equivalent. In any case, it seems to me that it would be safer to *not*
make this assumption at first and then have someone more knowledgeable
make the argument that it would be safe.

#30Jacob Champion
jchampion@timescale.com
In reply to: Peter Eisentraut (#29)
Re: [PoC] Let libpq reject unexpected authentication requests

On Wed, Sep 21, 2022 at 3:36 PM Peter Eisentraut
<peter.eisentraut@enterprisedb.com> wrote:

So let's look at the two TODO comments you have:

* TODO: how should !auth_required interact with an incomplete
* SCRAM exchange?

What specific combination of events are you thinking of here?

Let's say the client is using `require_auth=!password`. If the server
starts a SCRAM exchange. but doesn't finish it, the connection will
still succeed with the current implementation (because it satisfies
the "none" case). This is also true for a client setting of
`require_auth=scram-sha-256,none`. I think this is potentially
dangerous, but it mirrors the current behavior of libpq and I'm not
sure that we should change it as part of this patch.

/*
* If implicit GSS auth has already been performed via GSS
* encryption, we don't need to have performed an
* AUTH_REQ_GSS exchange.
*
* TODO: check this assumption. What mutual auth guarantees
* are made in this case?
*/

I don't understand the details involved here, but I would be surprised
if this assumption is true. For example, does GSS encryption deal with
user names and a user name map?

To my understanding, yes. There are explicit tests for it.

In any case, it seems to me that it would be safer to *not*
make this assumption at first and then have someone more knowledgeable
make the argument that it would be safe.

I think I'm okay with that, regardless. Here's one of the wrinkles:
right now, both of the following connstrings work:

require_auth=gss gssencmode=require
require_auth=gss gssencmode=prefer

If we don't treat gssencmode as providing GSS auth, then the first
case will always fail, because there will be no GSS authentication
packet over an encrypted connection. Likewise, the second case will
almost always fail, unless the server doesn't support gssencmode at
all (so why are you using prefer?).

If you're okay with those limitations, I will rip out the code. The
reason I'm not too worried about it is, I don't think it makes much
sense to be strict about your authentication requirements while at the
same time leaving the choice of transport encryption up to the server.

Thanks,
--Jacob

#31Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Jacob Champion (#30)
Re: [PoC] Let libpq reject unexpected authentication requests

On 22.09.22 01:37, Jacob Champion wrote:

On Wed, Sep 21, 2022 at 3:36 PM Peter Eisentraut
<peter.eisentraut@enterprisedb.com> wrote:

So let's look at the two TODO comments you have:

* TODO: how should !auth_required interact with an incomplete
* SCRAM exchange?

What specific combination of events are you thinking of here?

Let's say the client is using `require_auth=!password`. If the server
starts a SCRAM exchange. but doesn't finish it, the connection will
still succeed with the current implementation (because it satisfies
the "none" case). This is also true for a client setting of
`require_auth=scram-sha-256,none`. I think this is potentially
dangerous, but it mirrors the current behavior of libpq and I'm not
sure that we should change it as part of this patch.

It might be worth reviewing that behavior for other reasons, but I think
semantics of your patch are correct.

In any case, it seems to me that it would be safer to *not*
make this assumption at first and then have someone more knowledgeable
make the argument that it would be safe.

I think I'm okay with that, regardless. Here's one of the wrinkles:
right now, both of the following connstrings work:

require_auth=gss gssencmode=require
require_auth=gss gssencmode=prefer

If we don't treat gssencmode as providing GSS auth, then the first
case will always fail, because there will be no GSS authentication
packet over an encrypted connection. Likewise, the second case will
almost always fail, unless the server doesn't support gssencmode at
all (so why are you using prefer?).

If you're okay with those limitations, I will rip out the code. The
reason I'm not too worried about it is, I don't think it makes much
sense to be strict about your authentication requirements while at the
same time leaving the choice of transport encryption up to the server.

The way I understand what you explained here is that it would be more
sensible to leave that code in. I would be okay with that.

#32Jacob Champion
jchampion@timescale.com
In reply to: Peter Eisentraut (#31)
3 attachment(s)
Re: [PoC] Let libpq reject unexpected authentication requests

On Thu, Sep 22, 2022 at 4:52 AM Peter Eisentraut
<peter.eisentraut@enterprisedb.com> wrote:

On 22.09.22 01:37, Jacob Champion wrote:

I think this is potentially
dangerous, but it mirrors the current behavior of libpq and I'm not
sure that we should change it as part of this patch.

It might be worth reviewing that behavior for other reasons, but I think
semantics of your patch are correct.

Sounds good. v9 removes the TODO and adds a better explanation.

If you're okay with those [GSS] limitations, I will rip out the code. The
reason I'm not too worried about it is, I don't think it makes much
sense to be strict about your authentication requirements while at the
same time leaving the choice of transport encryption up to the server.

The way I understand what you explained here is that it would be more
sensible to leave that code in. I would be okay with that.

I've added a comment there explaining the gssencmode interaction. That
leaves no TODOs inside the code itself.

I removed the commit message note about not being able to prevent
unexpected client cert requests or GSS encryption, since we've decided
to handle those cases outside of require_auth.

I'm not able to test SSPI easily at the moment; if anyone is able to
try it out, that'd be really helpful. There's also the question of
SASL forwards compatibility -- if someone adds a new SASL mechanism,
the code will treat it like scram-sha-256 until it's changed, and
there will be no test to catch it. Should we leave that to the future
mechanism implementer to fix, or add a mechanism check now so the
client is safe even if they forget?

Thanks!
--Jacob

Attachments:

since-v8.diff.txttext/plain; charset=US-ASCII; name=since-v8.diff.txtDownload
commit e13f21d596bc5670156a441dc6eec5228864b4b0
Author: Jacob Champion <jchampion@timescale.com>
Date:   Thu Sep 22 16:39:34 2022 -0700

    squash! libpq: let client reject unexpected auth methods
    
    Remove TODOs, and document why the code remains as-is.

diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 565e44fcf6..793888d30f 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -921,8 +921,14 @@ check_expected_areq(AuthRequest areq, PGconn *conn)
 				 * else that the user has allowed an authentication-less
 				 * connection).
 				 *
-				 * TODO: how should !auth_required interact with an incomplete
-				 * SCRAM exchange?
+				 * If the user has allowed both SCRAM and unauthenticated
+				 * (trust) connections, then this check will silently accept
+				 * partial SCRAM exchanges, where a misbehaving server does not
+				 * provide its verifier before sending an OK. This is consistent
+				 * with historical behavior, but it may be a point to revisit in
+				 * the future, since it could allow a server that doesn't know
+				 * the user's password to silently harvest material for a brute
+				 * force attack.
 				 */
 				if (!conn->auth_required || conn->client_finished_auth)
 					break;
@@ -940,10 +946,9 @@ check_expected_areq(AuthRequest areq, PGconn *conn)
 					/*
 					 * If implicit GSS auth has already been performed via GSS
 					 * encryption, we don't need to have performed an
-					 * AUTH_REQ_GSS exchange.
-					 *
-					 * TODO: check this assumption. What mutual auth guarantees
-					 * are made in this case?
+					 * AUTH_REQ_GSS exchange. This allows require_auth=gss to be
+					 * combined with gssencmode, since there won't be an
+					 * explicit authentication request in that case.
 					 */
 				}
 				else
v9-0001-libpq-let-client-reject-unexpected-auth-methods.patchtext/x-patch; charset=US-ASCII; name=v9-0001-libpq-let-client-reject-unexpected-auth-methods.patchDownload
From f88abcb4b22499466758e566b956c487877118bb Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Mon, 28 Feb 2022 09:40:43 -0800
Subject: [PATCH v9 1/2] libpq: let client reject unexpected auth methods

The require_auth connection option allows the client to choose a list of
acceptable authentication types for use with the server. There is no
negotiation: if the server does not present one of the allowed
authentication requests, the connection fails. Additionally, all methods
in the list may be negated, e.g. '!password', in which case the server
must NOT use the listed authentication type. The special method "none"
allows/disallows the use of unauthenticated connections (but it does not
govern transport-level authentication via TLS or GSSAPI).

Internally, the patch expands the role of check_expected_areq() to
ensure that the incoming request is compatible with conn->require_auth.
It also introduces a new flag, conn->client_finished_auth, which is set
by various authentication routines when the client side of the handshake
is finished. This signals to check_expected_areq() that an OK message
from the server is expected, and allows the client to complain if the
server forgoes authentication entirely.

(Since the client can't generally prove that the server is actually
doing the work of authentication, this last part is mostly useful for
SCRAM without channel binding. It could also provide a client with a
decent signal that, at the very least, it's not connecting to a database
with trust auth, and so the connection can be tied to the client in a
later audit.)

Deficiencies:
- This is unlikely to be very forwards-compatible at the moment,
  especially with SASL/SCRAM.
- SSPI support is "implemented" but untested.
- require_auth=none,scram-sha-256 currently allows the server to leave a
  SCRAM exchange unfinished. This is not net-new behavior but may be
  surprising.
---
 doc/src/sgml/libpq.sgml                   | 105 ++++++++++++++
 src/include/libpq/pqcomm.h                |   1 +
 src/interfaces/libpq/fe-auth-scram.c      |   1 +
 src/interfaces/libpq/fe-auth.c            | 143 +++++++++++++++++++
 src/interfaces/libpq/fe-connect.c         | 164 ++++++++++++++++++++++
 src/interfaces/libpq/fe-secure-openssl.c  |   1 -
 src/interfaces/libpq/libpq-int.h          |   7 +
 src/test/authentication/t/001_password.pl | 149 ++++++++++++++++++++
 src/test/kerberos/t/001_auth.pl           |  26 ++++
 src/test/ldap/t/001_auth.pl               |   6 +
 src/test/ssl/t/002_scram.pl               |  25 ++++
 11 files changed, 627 insertions(+), 1 deletion(-)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 8a1a9e9932..1d6c02058e 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1220,6 +1220,101 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-require-auth" xreflabel="require_auth">
+      <term><literal>require_auth</literal></term>
+      <listitem>
+      <para>
+        Specifies the authentication method that the client requires from the
+        server. If the server does not use the required method to authenticate
+        the client, or if the authentication handshake is not fully completed by
+        the server, the connection will fail. A comma-separated list of methods
+        may also be provided, of which the server must use exactly one in order
+        for the connection to succeed. By default, any authentication method is
+        accepted, and the server is free to skip authentication altogether.
+      </para>
+      <para>
+        Methods may be negated with the addition of a <literal>!</literal>
+        prefix, in which case the server must <emphasis>not</emphasis> attempt
+        the listed method; any other method is accepted, and the server is free
+        not to authenticate the client at all. If a comma-separated list is
+        provided, the server may not attempt <emphasis>any</emphasis> of the
+        listed negated methods. Negated and non-negated forms may not be
+        combined in the same setting.
+      </para>
+      <para>
+        As a final special case, the <literal>none</literal> method requires the
+        server not to use an authentication challenge. (It may also be negated,
+        to require some form of authentication.)
+      </para>
+      <para>
+        The following methods may be specified:
+
+        <variablelist>
+         <varlistentry>
+          <term><literal>password</literal></term>
+          <listitem>
+           <para>
+            The server must request plaintext password authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>md5</literal></term>
+          <listitem>
+           <para>
+            The server must request MD5 hashed password authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>gss</literal></term>
+          <listitem>
+           <para>
+            The server must either request a Kerberos handshake via
+            <acronym>GSSAPI</acronym> or establish a
+            <acronym>GSS</acronym>-encrypted channel (see also
+            <xref linkend="libpq-connect-gssencmode" />).
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>sspi</literal></term>
+          <listitem>
+           <para>
+            The server must request Windows <acronym>SSPI</acronym>
+            authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>scram-sha-256</literal></term>
+          <listitem>
+           <para>
+            The server must successfully complete a SCRAM-SHA-256 authentication
+            exchange with the client.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>none</literal></term>
+          <listitem>
+           <para>
+            The server must not prompt the client for an authentication
+            exchange. (This does not prohibit client certificate authentication
+			via TLS, nor GSS authentication via its encrypted transport.)
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+      </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-channel-binding" xreflabel="channel_binding">
       <term><literal>channel_binding</literal></term>
       <listitem>
@@ -7756,6 +7851,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGREQUIREAUTH</envar></primary>
+      </indexterm>
+      <envar>PGREQUIREAUTH</envar> behaves the same as the <xref
+      linkend="libpq-connect-require-auth"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h
index fcf68df39b..912451c913 100644
--- a/src/include/libpq/pqcomm.h
+++ b/src/include/libpq/pqcomm.h
@@ -123,6 +123,7 @@ extern PGDLLIMPORT bool Db_user_namespace;
 #define AUTH_REQ_SASL	   10	/* Begin SASL authentication */
 #define AUTH_REQ_SASL_CONT 11	/* Continue SASL authentication */
 #define AUTH_REQ_SASL_FIN  12	/* Final SASL message */
+#define AUTH_REQ_MAX	   AUTH_REQ_SASL_FIN	/* maximum AUTH_REQ_* value */
 
 typedef uint32 AuthRequest;
 
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index 35cfd9987d..ad57df88b6 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -280,6 +280,7 @@ scram_exchange(void *opaq, char *input, int inputlen,
 			}
 			*done = true;
 			state->state = FE_SCRAM_FINISHED;
+			state->conn->client_finished_auth = true;
 			break;
 
 		default:
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 49a1c626f6..c95deffd2b 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -137,7 +137,10 @@ pg_GSS_continue(PGconn *conn, int payloadlen)
 	}
 
 	if (maj_stat == GSS_S_COMPLETE)
+	{
+		conn->client_finished_auth = true;
 		gss_release_name(&lmin_s, &conn->gtarg_nam);
+	}
 
 	return STATUS_OK;
 }
@@ -326,6 +329,9 @@ pg_SSPI_continue(PGconn *conn, int payloadlen)
 		FreeContextBuffer(outbuf.pBuffers[0].pvBuffer);
 	}
 
+	if (r == SEC_E_OK)
+		conn->client_finished_auth = true;
+
 	/* Cleanup is handled by the code in freePGconn() */
 	return STATUS_OK;
 }
@@ -830,6 +836,44 @@ pg_password_sendauth(PGconn *conn, const char *password, AuthRequest areq)
 	return ret;
 }
 
+/*
+ * Translate an AuthRequest into a human-readable description.
+ */
+static const char *
+auth_description(AuthRequest areq)
+{
+	switch (areq)
+	{
+		case AUTH_REQ_PASSWORD:
+			return libpq_gettext("a cleartext password");
+		case AUTH_REQ_MD5:
+			return libpq_gettext("a hashed password");
+		case AUTH_REQ_GSS:
+		case AUTH_REQ_GSS_CONT:
+			return libpq_gettext("GSSAPI authentication");
+		case AUTH_REQ_SSPI:
+			return libpq_gettext("SSPI authentication");
+		case AUTH_REQ_SCM_CREDS:
+			return libpq_gettext("UNIX socket credentials");
+		case AUTH_REQ_SASL:
+		case AUTH_REQ_SASL_CONT:
+		case AUTH_REQ_SASL_FIN:
+			return libpq_gettext("SASL authentication");
+	}
+
+	return libpq_gettext("an unknown authentication type");
+}
+
+/*
+ * Convenience macro for checking the allowed_auth_methods bitmask. Caller must
+ * ensure that type is not greater than 31 (high bit of the bitmask).
+ */
+#define auth_allowed(conn, type) \
+	(((conn)->allowed_auth_methods & (1 << (type))) != 0)
+
+StaticAssertDecl(AUTH_REQ_MAX < CHAR_BIT * sizeof(((PGconn){0}).allowed_auth_methods),
+				 "AUTH_REQ_MAX overflows the allowed_auth_methods bitmask");
+
 /*
  * Verify that the authentication request is expected, given the connection
  * parameters. This is especially important when the client wishes to
@@ -839,6 +883,102 @@ static bool
 check_expected_areq(AuthRequest areq, PGconn *conn)
 {
 	bool		result = true;
+	char	   *reason = NULL;
+
+	/*
+	 * If the user required a specific auth method, or specified an allowed set,
+	 * then reject all others here, and make sure the server actually completes
+	 * an authentication exchange.
+	 */
+	if (conn->require_auth)
+	{
+		switch (areq)
+		{
+			case AUTH_REQ_OK:
+				/*
+				 * Check to make sure we've actually finished our exchange (or
+				 * else that the user has allowed an authentication-less
+				 * connection).
+				 *
+				 * If the user has allowed both SCRAM and unauthenticated
+				 * (trust) connections, then this check will silently accept
+				 * partial SCRAM exchanges, where a misbehaving server does not
+				 * provide its verifier before sending an OK. This is consistent
+				 * with historical behavior, but it may be a point to revisit in
+				 * the future, since it could allow a server that doesn't know
+				 * the user's password to silently harvest material for a brute
+				 * force attack.
+				 */
+				if (!conn->auth_required || conn->client_finished_auth)
+					break;
+
+				/*
+				 * No explicit authentication request was made by the server --
+				 * or perhaps it was made and not completed, in the case of
+				 * SCRAM -- but there is one special case to check. If the user
+				 * allowed "gss", then a GSS-encrypted channel also satisfies
+				 * the check.
+				 */
+#ifdef ENABLE_GSS
+				if (auth_allowed(conn, AUTH_REQ_GSS) && conn->gssenc)
+				{
+					/*
+					 * If implicit GSS auth has already been performed via GSS
+					 * encryption, we don't need to have performed an
+					 * AUTH_REQ_GSS exchange. This allows require_auth=gss to be
+					 * combined with gssencmode, since there won't be an
+					 * explicit authentication request in that case.
+					 */
+				}
+				else
+#endif
+				{
+					reason = libpq_gettext("server did not complete authentication"),
+					result = false;
+				}
+
+				break;
+
+			case AUTH_REQ_PASSWORD:
+			case AUTH_REQ_MD5:
+			case AUTH_REQ_GSS:
+			case AUTH_REQ_SSPI:
+			case AUTH_REQ_GSS_CONT:
+			case AUTH_REQ_SASL:
+			case AUTH_REQ_SASL_CONT:
+			case AUTH_REQ_SASL_FIN:
+				/*
+				 * We don't handle these with the default case, to avoid
+				 * bit-shifting past the end of the allowed_auth_methods mask if
+				 * the server sends an unexpected AuthRequest.
+				 */
+				result = auth_allowed(conn, areq);
+				break;
+
+			default:
+				result = false;
+				break;
+		}
+	}
+
+	if (!result)
+	{
+		if (reason)
+		{
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("auth method \"%s\" requirement failed: %s\n"),
+							  conn->require_auth, reason);
+		}
+		else
+		{
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("auth method \"%s\" required, but server requested %s\n"),
+							  conn->require_auth,
+							  auth_description(areq));
+		}
+
+		return result;
+	}
 
 	/*
 	 * When channel_binding=require, we must protect against two cases: (1) we
@@ -1040,6 +1180,9 @@ pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn)
 										 "fe_sendauth: error sending password authentication\n");
 					return STATUS_ERROR;
 				}
+
+				/* We expect no further authentication requests. */
+				conn->client_finished_auth = true;
 				break;
 			}
 
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 746e9b4f1e..40f571fadb 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -307,6 +307,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Require-Peer", "", 10,
 	offsetof(struct pg_conn, requirepeer)},
 
+	{"require_auth", "PGREQUIREAUTH", NULL, NULL,
+		"Require-Auth", "", 14, /* sizeof("scram-sha-256") == 14 */
+	offsetof(struct pg_conn, require_auth)},
+
 	{"ssl_min_protocol_version", "PGSSLMINPROTOCOLVERSION", "TLSv1.2", NULL,
 		"SSL-Minimum-Protocol-Version", "", 8,	/* sizeof("TLSv1.x") == 8 */
 	offsetof(struct pg_conn, ssl_min_protocol_version)},
@@ -1240,6 +1244,166 @@ connectOptions2(PGconn *conn)
 		}
 	}
 
+	/*
+	 * parse and validate require_auth option
+	 */
+	if (conn->require_auth)
+	{
+		char	   *s = conn->require_auth;
+		bool		first, more;
+		bool		negated = false;
+
+		/*
+		 * By default, start from an empty set of allowed options and add to it.
+		 */
+		conn->auth_required = true;
+		conn->allowed_auth_methods = 0;
+
+		for (first = true, more = true; more; first = false)
+		{
+			char	   *method, *part;
+			uint32		bits;
+
+			part = parse_comma_separated_list(&s, &more);
+			if (part == NULL)
+				goto oom_error;
+
+			/*
+			 * Check for negation, e.g. '!password'. If one element is negated,
+			 * they all have to be.
+			 */
+			method = part;
+			if (*method == '!')
+			{
+				if (first)
+				{
+					/*
+					 * Switch to a permissive set of allowed options, and
+					 * subtract from it.
+					 */
+					conn->auth_required = false;
+					conn->allowed_auth_methods = -1;
+				}
+				else if (!negated)
+				{
+					conn->status = CONNECTION_BAD;
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("negative require_auth method \"%s\" cannot be mixed with non-negative methods"),
+									  method);
+
+					free(part);
+					return false;
+				}
+
+				negated = true;
+				method++;
+			}
+			else if (negated)
+			{
+				conn->status = CONNECTION_BAD;
+				appendPQExpBuffer(&conn->errorMessage,
+								  libpq_gettext("require_auth method \"%s\" cannot be mixed with negative methods"),
+								  method);
+
+				free(part);
+				return false;
+			}
+
+			if (strcmp(method, "password") == 0)
+			{
+				bits = (1 << AUTH_REQ_PASSWORD);
+			}
+			else if (strcmp(method, "md5") == 0)
+			{
+				bits = (1 << AUTH_REQ_MD5);
+			}
+			else if (strcmp(method, "gss") == 0)
+			{
+				bits = (1 << AUTH_REQ_GSS);
+				bits |= (1 << AUTH_REQ_GSS_CONT);
+			}
+			else if (strcmp(method, "sspi") == 0)
+			{
+				bits = (1 << AUTH_REQ_SSPI);
+				bits |= (1 << AUTH_REQ_GSS_CONT);
+			}
+			else if (strcmp(method, "scram-sha-256") == 0)
+			{
+				/* This currently assumes that SCRAM is the only SASL method. */
+				bits = (1 << AUTH_REQ_SASL);
+				bits |= (1 << AUTH_REQ_SASL_CONT);
+				bits |= (1 << AUTH_REQ_SASL_FIN);
+			}
+			else if (strcmp(method, "none") == 0)
+			{
+				/*
+				 * Special case: let the user explicitly allow (or disallow)
+				 * connections where the server does not send an explicit
+				 * authentication challenge, such as "trust" and "cert" auth.
+				 */
+				if (negated) /* "!none" */
+				{
+					if (conn->auth_required)
+						goto duplicate;
+
+					conn->auth_required = true;
+				}
+				else /* "none" */
+				{
+					if (!conn->auth_required)
+						goto duplicate;
+
+					conn->auth_required = false;
+				}
+
+				free(part);
+				continue; /* avoid the bitmask manipulation below */
+			}
+			else
+			{
+				conn->status = CONNECTION_BAD;
+				appendPQExpBuffer(&conn->errorMessage,
+								  libpq_gettext("invalid require_auth method: \"%s\""),
+								  method);
+
+				free(part);
+				return false;
+			}
+
+			/* Update the bitmask. */
+			if (negated)
+			{
+				if ((conn->allowed_auth_methods & bits) == 0)
+					goto duplicate;
+
+				conn->allowed_auth_methods &= ~bits;
+			}
+			else
+			{
+				if ((conn->allowed_auth_methods & bits) == bits)
+					goto duplicate;
+
+				conn->allowed_auth_methods |= bits;
+			}
+
+			free(part);
+			continue;
+
+duplicate:
+			/*
+			 * A duplicated method probably indicates a typo in a setting where
+			 * typos are extremely risky.
+			 */
+			conn->status = CONNECTION_BAD;
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("require_auth method \"%s\" is specified more than once"),
+							  part);
+
+			free(part);
+			return false;
+		}
+	}
+
 	/*
 	 * validate channel_binding option
 	 */
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index aea4661736..957c080356 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -475,7 +475,6 @@ verify_cb(int ok, X509_STORE_CTX *ctx)
 	return ok;
 }
 
-
 /*
  * OpenSSL-specific wrapper around
  * pq_verify_peer_name_matches_certificate_name(), converting the ASN1_STRING
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index c75ed63a2c..e4d88e22a1 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -396,6 +396,7 @@ struct pg_conn
 	char	   *ssl_min_protocol_version;	/* minimum TLS protocol version */
 	char	   *ssl_max_protocol_version;	/* maximum TLS protocol version */
 	char	   *target_session_attrs;	/* desired session properties */
+	char	   *require_auth;	/* name of the expected auth method */
 
 	/* Optional file to write trace info to */
 	FILE	   *Pfdebug;
@@ -457,6 +458,9 @@ struct pg_conn
 	bool		write_failed;	/* have we had a write failure on sock? */
 	char	   *write_err_msg;	/* write error message, or NULL if OOM */
 
+	bool		auth_required;	/* require an authentication challenge from the server? */
+	uint32		allowed_auth_methods;	/* bitmask of acceptable AuthRequest codes */
+
 	/* Transient state needed while establishing connection */
 	PGTargetServerType target_server_type;	/* desired session properties */
 	bool		try_next_addr;	/* time to advance to next address/host? */
@@ -512,6 +516,9 @@ struct pg_conn
 	bool		error_result;	/* do we need to make an ERROR result? */
 	PGresult   *next_result;	/* next result (used in single-row mode) */
 
+	bool		client_finished_auth; /* have we finished our half of the
+									   * authentication exchange? */
+
 	/* Assorted state for SASL, SSL, GSS, etc */
 	const pg_fe_sasl_mech *sasl;
 	void	   *sasl_state;
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 3e3079c824..473a93d6db 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -82,6 +82,74 @@ test_role($node, 'scram_role', 'trust', 0,
 test_role($node, 'md5_role', 'trust', 0,
 	log_unlike => [qr/connection authenticated:/]);
 
+# All positive require_auth options should fail...
+$node->connect_fails("user=scram_role require_auth=gss",
+	"GSS authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=sspi",
+	"SSPI authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=password",
+	"password authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=password,scram-sha-256",
+	"multiple authentication types can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+
+# ...and negative require_auth options should succeed.
+$node->connect_ok("user=scram_role require_auth=!gss",
+	"GSS authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!sspi",
+	"SSPI authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!password",
+	"password authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!md5",
+	"md5 authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!scram-sha-256",
+	"SCRAM authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!password,!scram-sha-256",
+	"multiple authentication types can be forbidden: succeeds with trust auth");
+
+# require_auth=[!]none should interact correctly with trust auth.
+$node->connect_ok("user=scram_role require_auth=none",
+	"all authentication can be forbidden: succeeds with trust auth");
+$node->connect_fails("user=scram_role require_auth=!none",
+	"any authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+
+# Negative and positive require_auth options can't be mixed.
+$node->connect_fails("user=scram_role require_auth=scram-sha-256,!md5",
+	"negative require_auth methods can't be mixed with positive",
+	expected_stderr => qr/negative require_auth method "!md5" cannot be mixed with non-negative methods/);
+$node->connect_fails("user=scram_role require_auth=!password,scram-sha-256",
+	"positive require_auth methods can't be mixed with negative",
+	expected_stderr => qr/require_auth method "scram-sha-256" cannot be mixed with negative methods/);
+
+# require_auth methods can't be duplicated.
+$node->connect_fails("user=scram_role require_auth=password,md5,password",
+	"require_auth methods can't be duplicated: positive case",
+	expected_stderr => qr/require_auth method "password" is specified more than once/);
+$node->connect_fails("user=scram_role require_auth=!password,!md5,!password",
+	"require_auth methods can't be duplicated: negative case",
+	expected_stderr => qr/require_auth method "!password" is specified more than once/);
+$node->connect_fails("user=scram_role require_auth=none,md5,none",
+	"require_auth methods can't be duplicated: none case",
+	expected_stderr => qr/require_auth method "none" is specified more than once/);
+$node->connect_fails("user=scram_role require_auth=!none,!md5,!none",
+	"require_auth methods can't be duplicated: !none case",
+	expected_stderr => qr/require_auth method "!none" is specified more than once/);
+
+# Unknown require_auth methods are caught.
+$node->connect_fails("user=scram_role require_auth=none,abcdefg",
+	"unknown require_auth methods are rejected",
+	expected_stderr => qr/invalid require_auth method: "abcdefg"/);
+
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'password');
 test_role($node, 'scram_role', 'password', 0,
@@ -91,6 +159,33 @@ test_role($node, 'md5_role', 'password', 0,
 	log_like =>
 	  [qr/connection authenticated: identity="md5_role" method=password/]);
 
+# require_auth should succeed with a plaintext password...
+$node->connect_ok("user=scram_role require_auth=password",
+	"password authentication can be required: works with password auth");
+$node->connect_ok("user=scram_role require_auth=!none",
+	"any authentication can be required: works with password auth");
+$node->connect_ok("user=scram_role require_auth=scram-sha-256,password,md5",
+	"multiple authentication types can be required: works with password auth");
+
+# ...fail for other auth types...
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails("user=scram_role require_auth=none",
+	"all authentication can be forbidden: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+
+# ...and allow password authentication to be prohibited.
+$node->connect_fails("user=scram_role require_auth=!password",
+	"password authentication can be forbidden: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails("user=scram_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types can be forbidden: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+
 # For "scram-sha-256" method, user "scram_role" should be able to connect.
 reset_pg_hba($node, 'scram-sha-256');
 test_role(
@@ -104,6 +199,33 @@ test_role(
 test_role($node, 'md5_role', 'scram-sha-256', 2,
 	log_unlike => [qr/connection authenticated:/]);
 
+# require_auth should succeed with SCRAM...
+$node->connect_ok("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: works with SCRAM auth");
+$node->connect_ok("user=scram_role require_auth=!none",
+	"any authentication can be required: works with SCRAM auth");
+$node->connect_ok("user=scram_role require_auth=password,scram-sha-256,md5",
+	"multiple authentication types can be required: works with SCRAM auth");
+
+# ...fail for other auth types...
+$node->connect_fails("user=scram_role require_auth=password",
+	"password authentication can be required: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails("user=scram_role require_auth=none",
+	"all authentication can be forbidden: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+
+# ...and allow SCRAM authentication to be prohibited.
+$node->connect_fails("user=scram_role require_auth=!scram-sha-256",
+	"SCRAM authentication can be forbidden: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails("user=scram_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types can be forbidden: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+
 # Test that bad passwords are rejected.
 $ENV{"PGPASSWORD"} = 'badpass';
 test_role($node, 'scram_role', 'scram-sha-256', 2,
@@ -120,6 +242,33 @@ test_role($node, 'md5_role', 'md5', 0,
 	log_like =>
 	  [qr/connection authenticated: identity="md5_role" method=md5/]);
 
+# require_auth should succeed with MD5...
+$node->connect_ok("user=md5_role require_auth=md5",
+	"MD5 authentication can be required: works with MD5 auth");
+$node->connect_ok("user=md5_role require_auth=!none",
+	"any authentication can be required: works with MD5 auth");
+$node->connect_ok("user=md5_role require_auth=md5,scram-sha-256,password",
+	"multiple authentication types can be required: works with MD5 auth");
+
+# ...fail for other auth types...
+$node->connect_fails("user=md5_role require_auth=password",
+	"password authentication can be required: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+$node->connect_fails("user=md5_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+$node->connect_fails("user=md5_role require_auth=none",
+	"all authentication can be forbidden: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+
+# ...and allow MD5 authentication to be prohibited.
+$node->connect_fails("user=md5_role require_auth=!md5",
+	"password authentication can be forbidden: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+$node->connect_fails("user=md5_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types can be forbidden: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+
 # Tests for channel binding without SSL.
 # Using the password authentication method; channel binding can't work
 reset_pg_hba($node, 'password');
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index 47169a1d1e..054df71541 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -311,6 +311,24 @@ test_query(
 	'gssencmode=require',
 	'sending 100K lines works');
 
+# require_auth=gss should succeed...
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=disable require_auth=gss",
+	"GSS authentication can be requested: works with GSS auth without encryption");
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss",
+	"GSS authentication can be requested: works with GSS auth with encryption");
+
+# ...and require_auth=sspi should fail.
+$node->connect_fails(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=disable require_auth=sspi",
+	"SSPI authentication can be requested: fails with GSS auth without encryption",
+	expected_stderr => qr/server requested GSSAPI authentication/);
+$node->connect_fails(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=sspi",
+	"SSPI authentication can be requested: fails with GSS auth with encryption",
+	expected_stderr => qr/server did not complete authentication/);
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
 	qq{hostgssenc all all $hostaddr/32 gss map=mymap});
@@ -339,6 +357,14 @@ test_access(
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable',
 	'fails with GSS encryption disabled and hostgssenc hba');
 
+# require_auth=gss should succeed.
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss",
+	"GSS authentication can be requested: works with GSS encryption");
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss,scram-sha-256",
+	"multiple authentication types can be requested: works with GSS encryption");
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
 	qq{hostnogssenc all all $hostaddr/32 gss map=mymap});
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 2f064f6944..25483e50cc 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -220,6 +220,12 @@ test_access(
 		qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/
 	],);
 
+# require_auth=password should complete successfully; other methods should fail.
+$node->connect_ok("user=test1 require_auth=password",
+	"password authentication can be required: works with ldap auth");
+$node->connect_fails("user=test1 require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with ldap auth");
+
 note "search+bind";
 
 unlink($node->data_dir . '/pg_hba.conf');
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index deaa4aa086..cf61bc7d0d 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -136,4 +136,29 @@ $node->connect_ok(
 		qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/
 	]);
 
+# channel_binding should continue to function independently of require_auth.
+$node->connect_ok("$common_connstr user=ssltestuser channel_binding=disable require_auth=scram-sha-256",
+	"SCRAM with SSL, channel_binding=disable, and require_auth=scram-sha-256");
+$node->connect_fails(
+	"$common_connstr user=md5testuser require_auth=md5 channel_binding=require",
+	"channel_binding can fail even when require_auth succeeds",
+	expected_stderr =>
+	  qr/channel binding required but not supported by server's authentication request/
+);
+if ($supports_tls_server_end_point)
+{
+	$node->connect_ok(
+		"$common_connstr user=ssltestuser channel_binding=require require_auth=scram-sha-256",
+		"SCRAM with SSL, channel_binding=require, and require_auth=scram-sha-256");
+}
+else
+{
+	$node->connect_fails(
+		"$common_connstr user=ssltestuser channel_binding=require require_auth=scram-sha-256",
+		"SCRAM with SSL, channel_binding=require, and require_auth=scram-sha-256",
+		expected_stderr =>
+		  qr/channel binding is required, but server did not offer an authentication method that supports channel binding/
+	);
+}
+
 done_testing();
-- 
2.25.1

v9-0002-Add-sslcertmode-option-for-client-certificates.patchtext/x-patch; charset=US-ASCII; name=v9-0002-Add-sslcertmode-option-for-client-certificates.patchDownload
From ff3a153fa31bd98e19c5f6c77aaa54b0f7173d8e Mon Sep 17 00:00:00 2001
From: Jacob Champion <jchampion@timescale.com>
Date: Fri, 24 Jun 2022 15:40:42 -0700
Subject: [PATCH v9 2/2] Add sslcertmode option for client certificates

The sslcertmode option controls whether the server is allowed and/or
required to request a certificate from the client. There are three
modes:

- "allow" is the default and follows the current behavior -- a
  configured  sslcert is sent if the server requests one (which, with
  the current implementation, will happen whenever TLS is negotiated).

- "disable" causes the client to refuse to send a client certificate
  even if an sslcert is configured.

- "require" causes the client to fail if a client certificate is never
  sent and the server opens a connection anyway. This doesn't add any
  additional security, since there is no guarantee that the server is
  validating the certificate correctly, but it may help troubleshoot
  more complicated TLS setups.

sslcertmode=require needs the OpenSSL implementation to support
SSL_CTX_set_cert_cb(). Notably, LibreSSL does not.
---
 configure                                | 11 ++---
 configure.ac                             |  4 +-
 doc/src/sgml/libpq.sgml                  | 54 +++++++++++++++++++++++
 src/include/pg_config.h.in               |  3 ++
 src/interfaces/libpq/fe-auth.c           | 21 +++++++++
 src/interfaces/libpq/fe-connect.c        | 55 ++++++++++++++++++++++++
 src/interfaces/libpq/fe-secure-openssl.c | 40 ++++++++++++++++-
 src/interfaces/libpq/libpq-int.h         |  3 ++
 src/test/ssl/t/001_ssltests.pl           | 43 ++++++++++++++++++
 src/tools/msvc/Solution.pm               |  8 ++++
 10 files changed, 234 insertions(+), 8 deletions(-)

diff --git a/configure b/configure
index 1caca21b62..94171d4f54 100755
--- a/configure
+++ b/configure
@@ -12904,13 +12904,14 @@ else
 fi
 
   fi
-  # Function introduced in OpenSSL 1.0.2.
-  for ac_func in X509_get_signature_nid
+  # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all of these.
+  for ac_func in X509_get_signature_nid SSL_CTX_set_cert_cb
 do :
-  ac_fn_c_check_func "$LINENO" "X509_get_signature_nid" "ac_cv_func_X509_get_signature_nid"
-if test "x$ac_cv_func_X509_get_signature_nid" = xyes; then :
+  as_ac_var=`$as_echo "ac_cv_func_$ac_func" | $as_tr_sh`
+ac_fn_c_check_func "$LINENO" "$ac_func" "$as_ac_var"
+if eval test \"x\$"$as_ac_var"\" = x"yes"; then :
   cat >>confdefs.h <<_ACEOF
-#define HAVE_X509_GET_SIGNATURE_NID 1
+#define `$as_echo "HAVE_$ac_func" | $as_tr_cpp` 1
 _ACEOF
 
 fi
diff --git a/configure.ac b/configure.ac
index 10fa55dd15..1150a21b1f 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1350,8 +1350,8 @@ if test "$with_ssl" = openssl ; then
      AC_SEARCH_LIBS(CRYPTO_new_ex_data, [eay32 crypto], [], [AC_MSG_ERROR([library 'eay32' or 'crypto' is required for OpenSSL])])
      AC_SEARCH_LIBS(SSL_new, [ssleay32 ssl], [], [AC_MSG_ERROR([library 'ssleay32' or 'ssl' is required for OpenSSL])])
   fi
-  # Function introduced in OpenSSL 1.0.2.
-  AC_CHECK_FUNCS([X509_get_signature_nid])
+  # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all of these.
+  AC_CHECK_FUNCS([X509_get_signature_nid SSL_CTX_set_cert_cb])
   # Functions introduced in OpenSSL 1.1.0. We used to check for
   # OPENSSL_VERSION_NUMBER, but that didn't work with 1.1.0, because LibreSSL
   # defines OPENSSL_VERSION_NUMBER to claim version 2.0.0, even though it
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 1d6c02058e..821078ea6a 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1810,6 +1810,50 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-sslcertmode" xreflabel="sslcertmode">
+      <term><literal>sslcertmode</literal></term>
+      <listitem>
+       <para>
+        This option determines whether a client certificate may be sent to the
+        server, and whether the server is required to request one. There are
+        three modes:
+
+        <variablelist>
+         <varlistentry>
+          <term><literal>disable</literal></term>
+          <listitem>
+           <para>
+            a client certificate is never sent, even if one is provided via
+            <xref linkend="libpq-connect-sslcert" />
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>allow</literal> (default)</term>
+          <listitem>
+           <para>
+            a certificate may be sent, if the server requests one and it has
+            been provided via <literal>sslcert</literal>
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>require</literal></term>
+          <listitem>
+           <para>
+            the server <emphasis>must</emphasis> request a certificate. The
+            connection will fail if the server authenticates the client despite
+            not requesting or receiving one.
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-sslrootcert" xreflabel="sslrootcert">
       <term><literal>sslrootcert</literal></term>
       <listitem>
@@ -7968,6 +8012,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGSSLCERTMODE</envar></primary>
+      </indexterm>
+      <envar>PGSSLCERTMODE</envar> behaves the same as the <xref
+      linkend="libpq-connect-sslcertmode"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index c5a80b829e..08382e868b 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -397,6 +397,9 @@
 /* Define to 1 if you have spinlocks. */
 #undef HAVE_SPINLOCKS
 
+/* Define to 1 if you have the `SSL_CTX_set_cert_cb' function. */
+#undef HAVE_SSL_CTX_SET_CERT_CB
+
 /* Define to 1 if stdbool.h conforms to C99. */
 #undef HAVE_STDBOOL_H
 
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index c95deffd2b..793888d30f 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -885,6 +885,27 @@ check_expected_areq(AuthRequest areq, PGconn *conn)
 	bool		result = true;
 	char	   *reason = NULL;
 
+	if (conn->sslcertmode[0] == 'r' /* require */
+		&& areq == AUTH_REQ_OK)
+	{
+		/*
+		 * Trade off a little bit of complexity to try to get these error
+		 * messages as precise as possible.
+		 */
+		if (!conn->ssl_cert_requested)
+		{
+			appendPQExpBufferStr(&conn->errorMessage,
+								 libpq_gettext("server did not request a certificate"));
+			return false;
+		}
+		else if (!conn->ssl_cert_sent)
+		{
+			appendPQExpBufferStr(&conn->errorMessage,
+								 libpq_gettext("server accepted connection without a valid certificate"));
+			return false;
+		}
+	}
+
 	/*
 	 * If the user required a specific auth method, or specified an allowed set,
 	 * then reject all others here, and make sure the server actually completes
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 40f571fadb..eaca8cee1f 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -125,8 +125,10 @@ static int	ldapServiceLookup(const char *purl, PQconninfoOption *options,
 #define DefaultTargetSessionAttrs	"any"
 #ifdef USE_SSL
 #define DefaultSSLMode "prefer"
+#define DefaultSSLCertMode "allow"
 #else
 #define DefaultSSLMode	"disable"
+#define DefaultSSLCertMode "disable"
 #endif
 #ifdef ENABLE_GSS
 #include "fe-gssapi-common.h"
@@ -283,6 +285,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"SSL-Client-Key", "", 64,
 	offsetof(struct pg_conn, sslkey)},
 
+	{"sslcertmode", "PGSSLCERTMODE", NULL, NULL,
+		"SSL-Client-Cert-Mode", "", 8, /* sizeof("disable") == 8 */
+	offsetof(struct pg_conn, sslcertmode)},
+
 	{"sslpassword", NULL, NULL, NULL,
 		"SSL-Client-Key-Password", "*", 20,
 	offsetof(struct pg_conn, sslpassword)},
@@ -1514,6 +1520,55 @@ duplicate:
 		return false;
 	}
 
+	/*
+	 * validate sslcertmode option
+	 */
+	if (conn->sslcertmode)
+	{
+		if (strcmp(conn->sslcertmode, "disable") != 0 &&
+			strcmp(conn->sslcertmode, "allow") != 0 &&
+			strcmp(conn->sslcertmode, "require") != 0)
+		{
+			conn->status = CONNECTION_BAD;
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("invalid %s value: \"%s\"\n"),
+							  "sslcertmode",
+							  conn->sslcertmode);
+			return false;
+		}
+#ifndef USE_SSL
+		if (strcmp(conn->sslcertmode, "require") == 0)
+		{
+			conn->status = CONNECTION_BAD;
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("sslcertmode value \"%s\" invalid when SSL support is not compiled in\n"),
+							  conn->sslcertmode);
+			return false;
+		}
+#endif
+#ifndef HAVE_SSL_CTX_SET_CERT_CB
+		/*
+		 * Without a certificate callback, the current implementation can't
+		 * figure out if a certficate was actually requested, so "require" is
+		 * useless.
+		 */
+		if (strcmp(conn->sslcertmode, "require") == 0)
+		{
+			conn->status = CONNECTION_BAD;
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("sslcertmode value \"%s\" is not supported (check OpenSSL version)\n"),
+							  conn->sslcertmode);
+			return false;
+		}
+#endif
+	}
+	else
+	{
+		conn->sslcertmode = strdup(DefaultSSLCertMode);
+		if (!conn->sslcertmode)
+			goto oom_error;
+	}
+
 	/*
 	 * validate gssencmode option
 	 */
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 957c080356..10d65c966f 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -475,6 +475,34 @@ verify_cb(int ok, X509_STORE_CTX *ctx)
 	return ok;
 }
 
+#ifdef HAVE_SSL_CTX_SET_CERT_CB
+/*
+ * Certificate selection callback
+ *
+ * This callback lets us choose the client certificate we send to the server
+ * after seeing its CertificateRequest. We only support sending a single
+ * hard-coded certificate via sslcert, so we don't actually set any certificates
+ * here; we just it to record whether or not the server has actually asked for
+ * one and whether we have one to send.
+ */
+static int
+cert_cb(SSL *ssl, void *arg)
+{
+	PGconn *conn = arg;
+	conn->ssl_cert_requested = true;
+
+	/* Do we have a certificate loaded to send back? */
+	if (SSL_get_certificate(ssl))
+		conn->ssl_cert_sent = true;
+
+	/*
+	 * Tell OpenSSL that the callback succeeded; we're not required to actually
+	 * make any changes to the SSL handle.
+	 */
+	return 1;
+}
+#endif
+
 /*
  * OpenSSL-specific wrapper around
  * pq_verify_peer_name_matches_certificate_name(), converting the ASN1_STRING
@@ -969,6 +997,11 @@ initialize_SSL(PGconn *conn)
 		SSL_CTX_set_default_passwd_cb_userdata(SSL_context, conn);
 	}
 
+#ifdef HAVE_SSL_CTX_SET_CERT_CB
+	/* Set up a certificate selection callback. */
+	SSL_CTX_set_cert_cb(SSL_context, cert_cb, conn);
+#endif
+
 	/* Disable old protocol versions */
 	SSL_CTX_set_options(SSL_context, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
 
@@ -1132,7 +1165,12 @@ initialize_SSL(PGconn *conn)
 	else
 		fnbuf[0] = '\0';
 
-	if (fnbuf[0] == '\0')
+	if (conn->sslcertmode[0] == 'd') /* disable */
+	{
+		/* don't send a client cert even if we have one */
+		have_cert = false;
+	}
+	else if (fnbuf[0] == '\0')
 	{
 		/* no home directory, proceed without a client cert */
 		have_cert = false;
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index e4d88e22a1..ad6f8b78c6 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -384,6 +384,7 @@ struct pg_conn
 	char	   *sslkey;			/* client key filename */
 	char	   *sslcert;		/* client certificate filename */
 	char	   *sslpassword;	/* client key file password */
+	char	   *sslcertmode;	/* client cert mode (require,allow,disable) */
 	char	   *sslrootcert;	/* root certificate filename */
 	char	   *sslcrl;			/* certificate revocation list filename */
 	char	   *sslcrldir;		/* certificate revocation list directory name */
@@ -525,6 +526,8 @@ struct pg_conn
 
 	/* SSL structures */
 	bool		ssl_in_use;
+	bool		ssl_cert_requested;	/* Did the server ask us for a cert? */
+	bool		ssl_cert_sent;		/* Did we send one in reply? */
 
 #ifdef USE_SSL
 	bool		allow_ssl_try;	/* Allowed to try SSL negotiation */
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index efe5634fff..49003ba1b7 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -42,6 +42,10 @@ my $SERVERHOSTADDR = '127.0.0.1';
 # This is the pattern to use in pg_hba.conf to match incoming connections.
 my $SERVERHOSTCIDR = '127.0.0.1/32';
 
+# Determine whether build supports sslcertmode=require.
+my $supports_sslcertmode_require =
+  check_pg_config("#define HAVE_SSL_CTX_SET_CERT_CB 1");
+
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
 
@@ -191,6 +195,22 @@ $node->connect_ok(
 	"$common_connstr sslrootcert=ssl/both-cas-2.crt sslmode=verify-ca",
 	"cert root file that contains two certificates, order 2");
 
+# sslcertmode=allow and =disable should both work without a client certificate.
+$node->connect_ok(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=disable",
+	"connect with sslcertmode=disable");
+$node->connect_ok(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=allow",
+	"connect with sslcertmode=allow");
+
+# sslcertmode=require, however, should fail.
+$node->connect_fails(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=require",
+	"connect with sslcertmode=require fails without a client certificate",
+	expected_stderr => $supports_sslcertmode_require
+		? qr/server accepted connection without a valid certificate/
+		: qr/sslcertmode value "require" is not supported/);
+
 # CRL tests
 
 # Invalid CRL filename is the same as no CRL, succeeds
@@ -538,6 +558,29 @@ $node->connect_ok(
 	"certificate authorization succeeds with correct client cert in encrypted DER format"
 );
 
+# correct client cert with required/allowed certificate authentication
+if ($supports_sslcertmode_require)
+{
+	$node->connect_ok(
+		"$common_connstr user=ssltestuser sslcertmode=require sslcert=ssl/client.crt "
+		  . sslkey('client.key'),
+		"certificate authorization succeeds with sslcertmode=require"
+	);
+}
+$node->connect_ok(
+	"$common_connstr user=ssltestuser sslcertmode=allow sslcert=ssl/client.crt "
+	  . sslkey('client.key'),
+	"certificate authorization succeeds with sslcertmode=allow"
+);
+
+# client cert isn't sent if certificate authentication is disabled
+$node->connect_fails(
+	"$common_connstr user=ssltestuser sslcertmode=disable sslcert=ssl/client.crt "
+	  . sslkey('client.key'),
+	"certificate authorization fails with sslcertmode=disable",
+	expected_stderr => qr/connection requires a valid client certificate/
+);
+
 # correct client cert in encrypted PEM with wrong password
 $node->connect_fails(
 	"$common_connstr user=ssltestuser sslcert=ssl/client.crt "
diff --git a/src/tools/msvc/Solution.pm b/src/tools/msvc/Solution.pm
index c2acb58df0..ff6d6f9e35 100644
--- a/src/tools/msvc/Solution.pm
+++ b/src/tools/msvc/Solution.pm
@@ -328,6 +328,7 @@ sub GenerateFiles
 		HAVE_SETPROCTITLE_FAST                   => undef,
 		HAVE_SOCKLEN_T                           => 1,
 		HAVE_SPINLOCKS                           => 1,
+		HAVE_SSL_CTX_SET_CERT_CB                 => undef,
 		HAVE_STDBOOL_H                           => 1,
 		HAVE_STDINT_H                            => 1,
 		HAVE_STDLIB_H                            => 1,
@@ -489,6 +490,13 @@ sub GenerateFiles
 
 		my ($digit1, $digit2, $digit3) = $self->GetOpenSSLVersion();
 
+		if (   ($digit1 >= '3' && $digit2 >= '0' && $digit3 >= '0')
+			|| ($digit1 >= '1' && $digit2 >= '1' && $digit3 >= '0')
+			|| ($digit1 >= '1' && $digit2 >= '0' && $digit3 >= '2'))
+		{
+			$define{HAVE_SSL_CTX_SET_CERT_CB} = 1;
+		}
+
 		# More symbols are needed with OpenSSL 1.1.0 and above.
 		if (   ($digit1 >= '3' && $digit2 >= '0' && $digit3 >= '0')
 			|| ($digit1 >= '1' && $digit2 >= '1' && $digit3 >= '0'))
-- 
2.25.1

#33Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Jacob Champion (#32)
Re: [PoC] Let libpq reject unexpected authentication requests

On 23.09.22 02:02, Jacob Champion wrote:

On Thu, Sep 22, 2022 at 4:52 AM Peter Eisentraut
<peter.eisentraut@enterprisedb.com> wrote:

On 22.09.22 01:37, Jacob Champion wrote:

I think this is potentially
dangerous, but it mirrors the current behavior of libpq and I'm not
sure that we should change it as part of this patch.

It might be worth reviewing that behavior for other reasons, but I think
semantics of your patch are correct.

Sounds good. v9 removes the TODO and adds a better explanation.

I'm generally okay with these patches now.

I'm not able to test SSPI easily at the moment; if anyone is able to
try it out, that'd be really helpful. There's also the question of
SASL forwards compatibility -- if someone adds a new SASL mechanism,
the code will treat it like scram-sha-256 until it's changed, and
there will be no test to catch it. Should we leave that to the future
mechanism implementer to fix, or add a mechanism check now so the
client is safe even if they forget?

I think it would be good to put some provisions in place here, even if
they are elementary. Otherwise, there will be a significant burden on
the person who implements the next SASL method (i.e., you ;-) ) to
figure that out then.

I think you could just stick a string list of allowed SASL methods into
PGconn.

By the way, I'm not sure all the bit fiddling is really worth it. An
array of integers (or unsigned char or whatever) would work just as
well. Especially if you are going to have a string list for SASL
anyway. You're not really saving any bits or bytes either way in the
normal case.

Minor comments:

Pasting together error messages like with auth_description() isn't going
to work. You either need to expand the whole message in
check_expected_areq(), or perhaps rephrase the message like

libpq_gettext("auth method \"%s\" required, but server requested \"%s\"\n"),
conn->require_auth,
auth_description(areq)

and make auth_description() just return a single word not subject to
translation.

spurious whitespace change in fe-secure-openssl.c

whitespace error in patch:

.git/rebase-apply/patch:109: tab in indent.
via TLS, nor GSS authentication via its
encrypted transport.)

In the 0002 patch, the configure test needs to be added to meson.build.

#34Jacob Champion
jchampion@timescale.com
In reply to: Peter Eisentraut (#33)
3 attachment(s)
Re: [PoC] Let libpq reject unexpected authentication requests

On 10/5/22 06:33, Peter Eisentraut wrote:

I think it would be good to put some provisions in place here, even if
they are elementary. Otherwise, there will be a significant burden on
the person who implements the next SASL method (i.e., you ;-) ) to
figure that out then.

Sounds good, I'll work on that. v10 does not yet make changes in this area.

I think you could just stick a string list of allowed SASL methods into
PGconn.

By the way, I'm not sure all the bit fiddling is really worth it. An
array of integers (or unsigned char or whatever) would work just as
well. Especially if you are going to have a string list for SASL
anyway. You're not really saving any bits or bytes either way in the
normal case.

Yeah, with the SASL case added in, the bitmasks might not be long for
this world. It is nice to be able to invert the whole thing, but a
separate boolean saying "invert the list" could accomplish the same goal
and I think we'll need to have that for the SASL mechanism list anyway.

Minor comments:

Pasting together error messages like with auth_description() isn't going
to work. You either need to expand the whole message in
check_expected_areq(), or perhaps rephrase the message like

libpq_gettext("auth method \"%s\" required, but server requested \"%s\"\n"),
conn->require_auth,
auth_description(areq)

and make auth_description() just return a single word not subject to
translation.

Right. Michael tried to warn me about that upthread, but I only ended up
fixing one of the two error cases for some reason. I've merged the two
into one code path for v10.

Quick error messaging bikeshed: do you prefer

auth method "!password,!md5" requirement failed: ...

or

auth method requirement "!password,!md5" failed: ...

?

spurious whitespace change in fe-secure-openssl.c

Fixed.

whitespace error in patch:

.git/rebase-apply/patch:109: tab in indent.
via TLS, nor GSS authentication via its
encrypted transport.)

Fixed.

In the 0002 patch, the configure test needs to be added to meson.build.

Added.

Thanks,
--Jacob

Attachments:

since-v9.diff.txttext/plain; charset=UTF-8; name=since-v9.diff.txtDownload
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 45c5228cfe..c06b0718cf 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1306,7 +1306,7 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
            <para>
             The server must not prompt the client for an authentication
             exchange. (This does not prohibit client certificate authentication
-			via TLS, nor GSS authentication via its encrypted transport.)
+            via TLS, nor GSS authentication via its encrypted transport.)
            </para>
           </listitem>
          </varlistentry>
diff --git a/meson.build b/meson.build
index 925db70c9d..bad035c8e3 100644
--- a/meson.build
+++ b/meson.build
@@ -1173,8 +1173,9 @@ if get_option('ssl') == 'openssl'
     ['CRYPTO_new_ex_data', {'required': true}],
     ['SSL_new', {'required': true}],
 
-    # Function introduced in OpenSSL 1.0.2.
+    # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all of these.
     ['X509_get_signature_nid'],
+    ['SSL_CTX_set_cert_cb'],
 
     # Functions introduced in OpenSSL 1.1.0. We used to check for
     # OPENSSL_VERSION_NUMBER, but that didn't work with 1.1.0, because LibreSSL
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 793888d30f..295b978525 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -837,7 +837,7 @@ pg_password_sendauth(PGconn *conn, const char *password, AuthRequest areq)
 }
 
 /*
- * Translate an AuthRequest into a human-readable description.
+ * Translate a disallowed AuthRequest code into an error message.
  */
 static const char *
 auth_description(AuthRequest areq)
@@ -845,23 +845,23 @@ auth_description(AuthRequest areq)
 	switch (areq)
 	{
 		case AUTH_REQ_PASSWORD:
-			return libpq_gettext("a cleartext password");
+			return libpq_gettext("server requested a cleartext password");
 		case AUTH_REQ_MD5:
-			return libpq_gettext("a hashed password");
+			return libpq_gettext("server requested a hashed password");
 		case AUTH_REQ_GSS:
 		case AUTH_REQ_GSS_CONT:
-			return libpq_gettext("GSSAPI authentication");
+			return libpq_gettext("server requested GSSAPI authentication");
 		case AUTH_REQ_SSPI:
-			return libpq_gettext("SSPI authentication");
+			return libpq_gettext("server requested SSPI authentication");
 		case AUTH_REQ_SCM_CREDS:
-			return libpq_gettext("UNIX socket credentials");
+			return libpq_gettext("server requested UNIX socket credentials");
 		case AUTH_REQ_SASL:
 		case AUTH_REQ_SASL_CONT:
 		case AUTH_REQ_SASL_FIN:
-			return libpq_gettext("SASL authentication");
+			return libpq_gettext("server requested SASL authentication");
 	}
 
-	return libpq_gettext("an unknown authentication type");
+	return libpq_gettext("server requested an unknown authentication type");
 }
 
 /*
@@ -883,7 +883,7 @@ static bool
 check_expected_areq(AuthRequest areq, PGconn *conn)
 {
 	bool		result = true;
-	char	   *reason = NULL;
+	const char *reason = NULL;
 
 	if (conn->sslcertmode[0] == 'r' /* require */
 		&& areq == AUTH_REQ_OK)
@@ -984,19 +984,12 @@ check_expected_areq(AuthRequest areq, PGconn *conn)
 
 	if (!result)
 	{
-		if (reason)
-		{
-			appendPQExpBuffer(&conn->errorMessage,
-							  libpq_gettext("auth method \"%s\" requirement failed: %s\n"),
-							  conn->require_auth, reason);
-		}
-		else
-		{
-			appendPQExpBuffer(&conn->errorMessage,
-							  libpq_gettext("auth method \"%s\" required, but server requested %s\n"),
-							  conn->require_auth,
-							  auth_description(areq));
-		}
+		if (!reason)
+			reason = auth_description(areq);
+
+		appendPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("auth method \"%s\" requirement failed: %s\n"),
+						  conn->require_auth, reason);
 
 		return result;
 	}
v10-0001-libpq-let-client-reject-unexpected-auth-methods.patchtext/x-patch; charset=UTF-8; name=v10-0001-libpq-let-client-reject-unexpected-auth-methods.patchDownload
From bf33dc2d7ac42ff4c14a149b707a3ee47aa3d76f Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Mon, 28 Feb 2022 09:40:43 -0800
Subject: [PATCH v10 1/2] libpq: let client reject unexpected auth methods

The require_auth connection option allows the client to choose a list of
acceptable authentication types for use with the server. There is no
negotiation: if the server does not present one of the allowed
authentication requests, the connection fails. Additionally, all methods
in the list may be negated, e.g. '!password', in which case the server
must NOT use the listed authentication type. The special method "none"
allows/disallows the use of unauthenticated connections (but it does not
govern transport-level authentication via TLS or GSSAPI).

Internally, the patch expands the role of check_expected_areq() to
ensure that the incoming request is compatible with conn->require_auth.
It also introduces a new flag, conn->client_finished_auth, which is set
by various authentication routines when the client side of the handshake
is finished. This signals to check_expected_areq() that an OK message
from the server is expected, and allows the client to complain if the
server forgoes authentication entirely.

(Since the client can't generally prove that the server is actually
doing the work of authentication, this last part is mostly useful for
SCRAM without channel binding. It could also provide a client with a
decent signal that, at the very least, it's not connecting to a database
with trust auth, and so the connection can be tied to the client in a
later audit.)

Deficiencies:
- This is unlikely to be very forwards-compatible at the moment,
  especially with SASL/SCRAM.
- SSPI support is "implemented" but untested.
- require_auth=none,scram-sha-256 currently allows the server to leave a
  SCRAM exchange unfinished. This is not net-new behavior but may be
  surprising.
---
 doc/src/sgml/libpq.sgml                   | 105 ++++++++++++++
 src/include/libpq/pqcomm.h                |   1 +
 src/interfaces/libpq/fe-auth-scram.c      |   1 +
 src/interfaces/libpq/fe-auth.c            | 136 ++++++++++++++++++
 src/interfaces/libpq/fe-connect.c         | 164 ++++++++++++++++++++++
 src/interfaces/libpq/libpq-int.h          |   7 +
 src/test/authentication/t/001_password.pl | 149 ++++++++++++++++++++
 src/test/kerberos/t/001_auth.pl           |  26 ++++
 src/test/ldap/t/001_auth.pl               |   6 +
 src/test/ssl/t/002_scram.pl               |  25 ++++
 10 files changed, 620 insertions(+)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 3c9bd3d673..05d9645f40 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1220,6 +1220,101 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-require-auth" xreflabel="require_auth">
+      <term><literal>require_auth</literal></term>
+      <listitem>
+      <para>
+        Specifies the authentication method that the client requires from the
+        server. If the server does not use the required method to authenticate
+        the client, or if the authentication handshake is not fully completed by
+        the server, the connection will fail. A comma-separated list of methods
+        may also be provided, of which the server must use exactly one in order
+        for the connection to succeed. By default, any authentication method is
+        accepted, and the server is free to skip authentication altogether.
+      </para>
+      <para>
+        Methods may be negated with the addition of a <literal>!</literal>
+        prefix, in which case the server must <emphasis>not</emphasis> attempt
+        the listed method; any other method is accepted, and the server is free
+        not to authenticate the client at all. If a comma-separated list is
+        provided, the server may not attempt <emphasis>any</emphasis> of the
+        listed negated methods. Negated and non-negated forms may not be
+        combined in the same setting.
+      </para>
+      <para>
+        As a final special case, the <literal>none</literal> method requires the
+        server not to use an authentication challenge. (It may also be negated,
+        to require some form of authentication.)
+      </para>
+      <para>
+        The following methods may be specified:
+
+        <variablelist>
+         <varlistentry>
+          <term><literal>password</literal></term>
+          <listitem>
+           <para>
+            The server must request plaintext password authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>md5</literal></term>
+          <listitem>
+           <para>
+            The server must request MD5 hashed password authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>gss</literal></term>
+          <listitem>
+           <para>
+            The server must either request a Kerberos handshake via
+            <acronym>GSSAPI</acronym> or establish a
+            <acronym>GSS</acronym>-encrypted channel (see also
+            <xref linkend="libpq-connect-gssencmode" />).
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>sspi</literal></term>
+          <listitem>
+           <para>
+            The server must request Windows <acronym>SSPI</acronym>
+            authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>scram-sha-256</literal></term>
+          <listitem>
+           <para>
+            The server must successfully complete a SCRAM-SHA-256 authentication
+            exchange with the client.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>none</literal></term>
+          <listitem>
+           <para>
+            The server must not prompt the client for an authentication
+            exchange. (This does not prohibit client certificate authentication
+            via TLS, nor GSS authentication via its encrypted transport.)
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+      </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-channel-binding" xreflabel="channel_binding">
       <term><literal>channel_binding</literal></term>
       <listitem>
@@ -7773,6 +7868,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGREQUIREAUTH</envar></primary>
+      </indexterm>
+      <envar>PGREQUIREAUTH</envar> behaves the same as the <xref
+      linkend="libpq-connect-require-auth"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h
index fcf68df39b..912451c913 100644
--- a/src/include/libpq/pqcomm.h
+++ b/src/include/libpq/pqcomm.h
@@ -123,6 +123,7 @@ extern PGDLLIMPORT bool Db_user_namespace;
 #define AUTH_REQ_SASL	   10	/* Begin SASL authentication */
 #define AUTH_REQ_SASL_CONT 11	/* Continue SASL authentication */
 #define AUTH_REQ_SASL_FIN  12	/* Final SASL message */
+#define AUTH_REQ_MAX	   AUTH_REQ_SASL_FIN	/* maximum AUTH_REQ_* value */
 
 typedef uint32 AuthRequest;
 
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index 35cfd9987d..ad57df88b6 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -280,6 +280,7 @@ scram_exchange(void *opaq, char *input, int inputlen,
 			}
 			*done = true;
 			state->state = FE_SCRAM_FINISHED;
+			state->conn->client_finished_auth = true;
 			break;
 
 		default:
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 49a1c626f6..5a46cf0ee9 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -137,7 +137,10 @@ pg_GSS_continue(PGconn *conn, int payloadlen)
 	}
 
 	if (maj_stat == GSS_S_COMPLETE)
+	{
+		conn->client_finished_auth = true;
 		gss_release_name(&lmin_s, &conn->gtarg_nam);
+	}
 
 	return STATUS_OK;
 }
@@ -326,6 +329,9 @@ pg_SSPI_continue(PGconn *conn, int payloadlen)
 		FreeContextBuffer(outbuf.pBuffers[0].pvBuffer);
 	}
 
+	if (r == SEC_E_OK)
+		conn->client_finished_auth = true;
+
 	/* Cleanup is handled by the code in freePGconn() */
 	return STATUS_OK;
 }
@@ -830,6 +836,44 @@ pg_password_sendauth(PGconn *conn, const char *password, AuthRequest areq)
 	return ret;
 }
 
+/*
+ * Translate a disallowed AuthRequest code into an error message.
+ */
+static const char *
+auth_description(AuthRequest areq)
+{
+	switch (areq)
+	{
+		case AUTH_REQ_PASSWORD:
+			return libpq_gettext("server requested a cleartext password");
+		case AUTH_REQ_MD5:
+			return libpq_gettext("server requested a hashed password");
+		case AUTH_REQ_GSS:
+		case AUTH_REQ_GSS_CONT:
+			return libpq_gettext("server requested GSSAPI authentication");
+		case AUTH_REQ_SSPI:
+			return libpq_gettext("server requested SSPI authentication");
+		case AUTH_REQ_SCM_CREDS:
+			return libpq_gettext("server requested UNIX socket credentials");
+		case AUTH_REQ_SASL:
+		case AUTH_REQ_SASL_CONT:
+		case AUTH_REQ_SASL_FIN:
+			return libpq_gettext("server requested SASL authentication");
+	}
+
+	return libpq_gettext("server requested an unknown authentication type");
+}
+
+/*
+ * Convenience macro for checking the allowed_auth_methods bitmask. Caller must
+ * ensure that type is not greater than 31 (high bit of the bitmask).
+ */
+#define auth_allowed(conn, type) \
+	(((conn)->allowed_auth_methods & (1 << (type))) != 0)
+
+StaticAssertDecl(AUTH_REQ_MAX < CHAR_BIT * sizeof(((PGconn){0}).allowed_auth_methods),
+				 "AUTH_REQ_MAX overflows the allowed_auth_methods bitmask");
+
 /*
  * Verify that the authentication request is expected, given the connection
  * parameters. This is especially important when the client wishes to
@@ -839,6 +883,95 @@ static bool
 check_expected_areq(AuthRequest areq, PGconn *conn)
 {
 	bool		result = true;
+	const char *reason = NULL;
+
+	/*
+	 * If the user required a specific auth method, or specified an allowed set,
+	 * then reject all others here, and make sure the server actually completes
+	 * an authentication exchange.
+	 */
+	if (conn->require_auth)
+	{
+		switch (areq)
+		{
+			case AUTH_REQ_OK:
+				/*
+				 * Check to make sure we've actually finished our exchange (or
+				 * else that the user has allowed an authentication-less
+				 * connection).
+				 *
+				 * If the user has allowed both SCRAM and unauthenticated
+				 * (trust) connections, then this check will silently accept
+				 * partial SCRAM exchanges, where a misbehaving server does not
+				 * provide its verifier before sending an OK. This is consistent
+				 * with historical behavior, but it may be a point to revisit in
+				 * the future, since it could allow a server that doesn't know
+				 * the user's password to silently harvest material for a brute
+				 * force attack.
+				 */
+				if (!conn->auth_required || conn->client_finished_auth)
+					break;
+
+				/*
+				 * No explicit authentication request was made by the server --
+				 * or perhaps it was made and not completed, in the case of
+				 * SCRAM -- but there is one special case to check. If the user
+				 * allowed "gss", then a GSS-encrypted channel also satisfies
+				 * the check.
+				 */
+#ifdef ENABLE_GSS
+				if (auth_allowed(conn, AUTH_REQ_GSS) && conn->gssenc)
+				{
+					/*
+					 * If implicit GSS auth has already been performed via GSS
+					 * encryption, we don't need to have performed an
+					 * AUTH_REQ_GSS exchange. This allows require_auth=gss to be
+					 * combined with gssencmode, since there won't be an
+					 * explicit authentication request in that case.
+					 */
+				}
+				else
+#endif
+				{
+					reason = libpq_gettext("server did not complete authentication"),
+					result = false;
+				}
+
+				break;
+
+			case AUTH_REQ_PASSWORD:
+			case AUTH_REQ_MD5:
+			case AUTH_REQ_GSS:
+			case AUTH_REQ_SSPI:
+			case AUTH_REQ_GSS_CONT:
+			case AUTH_REQ_SASL:
+			case AUTH_REQ_SASL_CONT:
+			case AUTH_REQ_SASL_FIN:
+				/*
+				 * We don't handle these with the default case, to avoid
+				 * bit-shifting past the end of the allowed_auth_methods mask if
+				 * the server sends an unexpected AuthRequest.
+				 */
+				result = auth_allowed(conn, areq);
+				break;
+
+			default:
+				result = false;
+				break;
+		}
+	}
+
+	if (!result)
+	{
+		if (!reason)
+			reason = auth_description(areq);
+
+		appendPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("auth method \"%s\" requirement failed: %s\n"),
+						  conn->require_auth, reason);
+
+		return result;
+	}
 
 	/*
 	 * When channel_binding=require, we must protect against two cases: (1) we
@@ -1040,6 +1173,9 @@ pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn)
 										 "fe_sendauth: error sending password authentication\n");
 					return STATUS_ERROR;
 				}
+
+				/* We expect no further authentication requests. */
+				conn->client_finished_auth = true;
 				break;
 			}
 
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 746e9b4f1e..40f571fadb 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -307,6 +307,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Require-Peer", "", 10,
 	offsetof(struct pg_conn, requirepeer)},
 
+	{"require_auth", "PGREQUIREAUTH", NULL, NULL,
+		"Require-Auth", "", 14, /* sizeof("scram-sha-256") == 14 */
+	offsetof(struct pg_conn, require_auth)},
+
 	{"ssl_min_protocol_version", "PGSSLMINPROTOCOLVERSION", "TLSv1.2", NULL,
 		"SSL-Minimum-Protocol-Version", "", 8,	/* sizeof("TLSv1.x") == 8 */
 	offsetof(struct pg_conn, ssl_min_protocol_version)},
@@ -1240,6 +1244,166 @@ connectOptions2(PGconn *conn)
 		}
 	}
 
+	/*
+	 * parse and validate require_auth option
+	 */
+	if (conn->require_auth)
+	{
+		char	   *s = conn->require_auth;
+		bool		first, more;
+		bool		negated = false;
+
+		/*
+		 * By default, start from an empty set of allowed options and add to it.
+		 */
+		conn->auth_required = true;
+		conn->allowed_auth_methods = 0;
+
+		for (first = true, more = true; more; first = false)
+		{
+			char	   *method, *part;
+			uint32		bits;
+
+			part = parse_comma_separated_list(&s, &more);
+			if (part == NULL)
+				goto oom_error;
+
+			/*
+			 * Check for negation, e.g. '!password'. If one element is negated,
+			 * they all have to be.
+			 */
+			method = part;
+			if (*method == '!')
+			{
+				if (first)
+				{
+					/*
+					 * Switch to a permissive set of allowed options, and
+					 * subtract from it.
+					 */
+					conn->auth_required = false;
+					conn->allowed_auth_methods = -1;
+				}
+				else if (!negated)
+				{
+					conn->status = CONNECTION_BAD;
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("negative require_auth method \"%s\" cannot be mixed with non-negative methods"),
+									  method);
+
+					free(part);
+					return false;
+				}
+
+				negated = true;
+				method++;
+			}
+			else if (negated)
+			{
+				conn->status = CONNECTION_BAD;
+				appendPQExpBuffer(&conn->errorMessage,
+								  libpq_gettext("require_auth method \"%s\" cannot be mixed with negative methods"),
+								  method);
+
+				free(part);
+				return false;
+			}
+
+			if (strcmp(method, "password") == 0)
+			{
+				bits = (1 << AUTH_REQ_PASSWORD);
+			}
+			else if (strcmp(method, "md5") == 0)
+			{
+				bits = (1 << AUTH_REQ_MD5);
+			}
+			else if (strcmp(method, "gss") == 0)
+			{
+				bits = (1 << AUTH_REQ_GSS);
+				bits |= (1 << AUTH_REQ_GSS_CONT);
+			}
+			else if (strcmp(method, "sspi") == 0)
+			{
+				bits = (1 << AUTH_REQ_SSPI);
+				bits |= (1 << AUTH_REQ_GSS_CONT);
+			}
+			else if (strcmp(method, "scram-sha-256") == 0)
+			{
+				/* This currently assumes that SCRAM is the only SASL method. */
+				bits = (1 << AUTH_REQ_SASL);
+				bits |= (1 << AUTH_REQ_SASL_CONT);
+				bits |= (1 << AUTH_REQ_SASL_FIN);
+			}
+			else if (strcmp(method, "none") == 0)
+			{
+				/*
+				 * Special case: let the user explicitly allow (or disallow)
+				 * connections where the server does not send an explicit
+				 * authentication challenge, such as "trust" and "cert" auth.
+				 */
+				if (negated) /* "!none" */
+				{
+					if (conn->auth_required)
+						goto duplicate;
+
+					conn->auth_required = true;
+				}
+				else /* "none" */
+				{
+					if (!conn->auth_required)
+						goto duplicate;
+
+					conn->auth_required = false;
+				}
+
+				free(part);
+				continue; /* avoid the bitmask manipulation below */
+			}
+			else
+			{
+				conn->status = CONNECTION_BAD;
+				appendPQExpBuffer(&conn->errorMessage,
+								  libpq_gettext("invalid require_auth method: \"%s\""),
+								  method);
+
+				free(part);
+				return false;
+			}
+
+			/* Update the bitmask. */
+			if (negated)
+			{
+				if ((conn->allowed_auth_methods & bits) == 0)
+					goto duplicate;
+
+				conn->allowed_auth_methods &= ~bits;
+			}
+			else
+			{
+				if ((conn->allowed_auth_methods & bits) == bits)
+					goto duplicate;
+
+				conn->allowed_auth_methods |= bits;
+			}
+
+			free(part);
+			continue;
+
+duplicate:
+			/*
+			 * A duplicated method probably indicates a typo in a setting where
+			 * typos are extremely risky.
+			 */
+			conn->status = CONNECTION_BAD;
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("require_auth method \"%s\" is specified more than once"),
+							  part);
+
+			free(part);
+			return false;
+		}
+	}
+
 	/*
 	 * validate channel_binding option
 	 */
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index c75ed63a2c..e4d88e22a1 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -396,6 +396,7 @@ struct pg_conn
 	char	   *ssl_min_protocol_version;	/* minimum TLS protocol version */
 	char	   *ssl_max_protocol_version;	/* maximum TLS protocol version */
 	char	   *target_session_attrs;	/* desired session properties */
+	char	   *require_auth;	/* name of the expected auth method */
 
 	/* Optional file to write trace info to */
 	FILE	   *Pfdebug;
@@ -457,6 +458,9 @@ struct pg_conn
 	bool		write_failed;	/* have we had a write failure on sock? */
 	char	   *write_err_msg;	/* write error message, or NULL if OOM */
 
+	bool		auth_required;	/* require an authentication challenge from the server? */
+	uint32		allowed_auth_methods;	/* bitmask of acceptable AuthRequest codes */
+
 	/* Transient state needed while establishing connection */
 	PGTargetServerType target_server_type;	/* desired session properties */
 	bool		try_next_addr;	/* time to advance to next address/host? */
@@ -512,6 +516,9 @@ struct pg_conn
 	bool		error_result;	/* do we need to make an ERROR result? */
 	PGresult   *next_result;	/* next result (used in single-row mode) */
 
+	bool		client_finished_auth; /* have we finished our half of the
+									   * authentication exchange? */
+
 	/* Assorted state for SASL, SSL, GSS, etc */
 	const pg_fe_sasl_mech *sasl;
 	void	   *sasl_state;
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 93df77aa4e..ca8731d379 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -107,6 +107,74 @@ is($res, 't',
 	"users with trust authentication use SYSTEM_USER = NULL in parallel workers"
 );
 
+# All positive require_auth options should fail...
+$node->connect_fails("user=scram_role require_auth=gss",
+	"GSS authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=sspi",
+	"SSPI authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=password",
+	"password authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=password,scram-sha-256",
+	"multiple authentication types can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+
+# ...and negative require_auth options should succeed.
+$node->connect_ok("user=scram_role require_auth=!gss",
+	"GSS authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!sspi",
+	"SSPI authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!password",
+	"password authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!md5",
+	"md5 authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!scram-sha-256",
+	"SCRAM authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!password,!scram-sha-256",
+	"multiple authentication types can be forbidden: succeeds with trust auth");
+
+# require_auth=[!]none should interact correctly with trust auth.
+$node->connect_ok("user=scram_role require_auth=none",
+	"all authentication can be forbidden: succeeds with trust auth");
+$node->connect_fails("user=scram_role require_auth=!none",
+	"any authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+
+# Negative and positive require_auth options can't be mixed.
+$node->connect_fails("user=scram_role require_auth=scram-sha-256,!md5",
+	"negative require_auth methods can't be mixed with positive",
+	expected_stderr => qr/negative require_auth method "!md5" cannot be mixed with non-negative methods/);
+$node->connect_fails("user=scram_role require_auth=!password,scram-sha-256",
+	"positive require_auth methods can't be mixed with negative",
+	expected_stderr => qr/require_auth method "scram-sha-256" cannot be mixed with negative methods/);
+
+# require_auth methods can't be duplicated.
+$node->connect_fails("user=scram_role require_auth=password,md5,password",
+	"require_auth methods can't be duplicated: positive case",
+	expected_stderr => qr/require_auth method "password" is specified more than once/);
+$node->connect_fails("user=scram_role require_auth=!password,!md5,!password",
+	"require_auth methods can't be duplicated: negative case",
+	expected_stderr => qr/require_auth method "!password" is specified more than once/);
+$node->connect_fails("user=scram_role require_auth=none,md5,none",
+	"require_auth methods can't be duplicated: none case",
+	expected_stderr => qr/require_auth method "none" is specified more than once/);
+$node->connect_fails("user=scram_role require_auth=!none,!md5,!none",
+	"require_auth methods can't be duplicated: !none case",
+	expected_stderr => qr/require_auth method "!none" is specified more than once/);
+
+# Unknown require_auth methods are caught.
+$node->connect_fails("user=scram_role require_auth=none,abcdefg",
+	"unknown require_auth methods are rejected",
+	expected_stderr => qr/invalid require_auth method: "abcdefg"/);
+
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'all', 'all', 'password');
 test_conn($node, 'user=scram_role', 'password', 0,
@@ -116,6 +184,33 @@ test_conn($node, 'user=md5_role', 'password', 0,
 	log_like =>
 	  [qr/connection authenticated: identity="md5_role" method=password/]);
 
+# require_auth should succeed with a plaintext password...
+$node->connect_ok("user=scram_role require_auth=password",
+	"password authentication can be required: works with password auth");
+$node->connect_ok("user=scram_role require_auth=!none",
+	"any authentication can be required: works with password auth");
+$node->connect_ok("user=scram_role require_auth=scram-sha-256,password,md5",
+	"multiple authentication types can be required: works with password auth");
+
+# ...fail for other auth types...
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails("user=scram_role require_auth=none",
+	"all authentication can be forbidden: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+
+# ...and allow password authentication to be prohibited.
+$node->connect_fails("user=scram_role require_auth=!password",
+	"password authentication can be forbidden: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails("user=scram_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types can be forbidden: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+
 # For "scram-sha-256" method, user "scram_role" should be able to connect.
 reset_pg_hba($node, 'all', 'all', 'scram-sha-256');
 test_conn(
@@ -129,6 +224,33 @@ test_conn(
 test_conn($node, 'user=md5_role', 'scram-sha-256', 2,
 	log_unlike => [qr/connection authenticated:/]);
 
+# require_auth should succeed with SCRAM...
+$node->connect_ok("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: works with SCRAM auth");
+$node->connect_ok("user=scram_role require_auth=!none",
+	"any authentication can be required: works with SCRAM auth");
+$node->connect_ok("user=scram_role require_auth=password,scram-sha-256,md5",
+	"multiple authentication types can be required: works with SCRAM auth");
+
+# ...fail for other auth types...
+$node->connect_fails("user=scram_role require_auth=password",
+	"password authentication can be required: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails("user=scram_role require_auth=none",
+	"all authentication can be forbidden: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+
+# ...and allow SCRAM authentication to be prohibited.
+$node->connect_fails("user=scram_role require_auth=!scram-sha-256",
+	"SCRAM authentication can be forbidden: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails("user=scram_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types can be forbidden: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+
 # Test that bad passwords are rejected.
 $ENV{"PGPASSWORD"} = 'badpass';
 test_conn($node, 'user=scram_role', 'scram-sha-256', 2,
@@ -145,6 +267,33 @@ test_conn($node, 'user=md5_role', 'md5', 0,
 	log_like =>
 	  [qr/connection authenticated: identity="md5_role" method=md5/]);
 
+# require_auth should succeed with MD5...
+$node->connect_ok("user=md5_role require_auth=md5",
+	"MD5 authentication can be required: works with MD5 auth");
+$node->connect_ok("user=md5_role require_auth=!none",
+	"any authentication can be required: works with MD5 auth");
+$node->connect_ok("user=md5_role require_auth=md5,scram-sha-256,password",
+	"multiple authentication types can be required: works with MD5 auth");
+
+# ...fail for other auth types...
+$node->connect_fails("user=md5_role require_auth=password",
+	"password authentication can be required: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+$node->connect_fails("user=md5_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+$node->connect_fails("user=md5_role require_auth=none",
+	"all authentication can be forbidden: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+
+# ...and allow MD5 authentication to be prohibited.
+$node->connect_fails("user=md5_role require_auth=!md5",
+	"password authentication can be forbidden: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+$node->connect_fails("user=md5_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types can be forbidden: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+
 # Test SYSTEM_USER <> NULL with parallel workers.
 $node->safe_psql(
 	'postgres',
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index a2bc8a5351..61aede12d1 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -318,6 +318,24 @@ test_query(
 	'gssencmode=require',
 	'sending 100K lines works');
 
+# require_auth=gss should succeed...
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=disable require_auth=gss",
+	"GSS authentication can be requested: works with GSS auth without encryption");
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss",
+	"GSS authentication can be requested: works with GSS auth with encryption");
+
+# ...and require_auth=sspi should fail.
+$node->connect_fails(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=disable require_auth=sspi",
+	"SSPI authentication can be requested: fails with GSS auth without encryption",
+	expected_stderr => qr/server requested GSSAPI authentication/);
+$node->connect_fails(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=sspi",
+	"SSPI authentication can be requested: fails with GSS auth with encryption",
+	expected_stderr => qr/server did not complete authentication/);
+
 # Test that SYSTEM_USER works.
 test_query($node, 'test1', 'SELECT SYSTEM_USER;',
 	qr/^gss:test1\@$realm$/s, 'gssencmode=require', 'testing system_user');
@@ -363,6 +381,14 @@ test_access(
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable',
 	'fails with GSS encryption disabled and hostgssenc hba');
 
+# require_auth=gss should succeed.
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss",
+	"GSS authentication can be requested: works with GSS encryption");
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss,scram-sha-256",
+	"multiple authentication types can be requested: works with GSS encryption");
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
 	qq{hostnogssenc all all $hostaddr/32 gss map=mymap});
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 2f064f6944..25483e50cc 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -220,6 +220,12 @@ test_access(
 		qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/
 	],);
 
+# require_auth=password should complete successfully; other methods should fail.
+$node->connect_ok("user=test1 require_auth=password",
+	"password authentication can be required: works with ldap auth");
+$node->connect_fails("user=test1 require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with ldap auth");
+
 note "search+bind";
 
 unlink($node->data_dir . '/pg_hba.conf');
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index deaa4aa086..cf61bc7d0d 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -136,4 +136,29 @@ $node->connect_ok(
 		qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/
 	]);
 
+# channel_binding should continue to function independently of require_auth.
+$node->connect_ok("$common_connstr user=ssltestuser channel_binding=disable require_auth=scram-sha-256",
+	"SCRAM with SSL, channel_binding=disable, and require_auth=scram-sha-256");
+$node->connect_fails(
+	"$common_connstr user=md5testuser require_auth=md5 channel_binding=require",
+	"channel_binding can fail even when require_auth succeeds",
+	expected_stderr =>
+	  qr/channel binding required but not supported by server's authentication request/
+);
+if ($supports_tls_server_end_point)
+{
+	$node->connect_ok(
+		"$common_connstr user=ssltestuser channel_binding=require require_auth=scram-sha-256",
+		"SCRAM with SSL, channel_binding=require, and require_auth=scram-sha-256");
+}
+else
+{
+	$node->connect_fails(
+		"$common_connstr user=ssltestuser channel_binding=require require_auth=scram-sha-256",
+		"SCRAM with SSL, channel_binding=require, and require_auth=scram-sha-256",
+		expected_stderr =>
+		  qr/channel binding is required, but server did not offer an authentication method that supports channel binding/
+	);
+}
+
 done_testing();
-- 
2.25.1

v10-0002-Add-sslcertmode-option-for-client-certificates.patchtext/x-patch; charset=UTF-8; name=v10-0002-Add-sslcertmode-option-for-client-certificates.patchDownload
From def670a45b7963f1f537b83c264937e14c279fd6 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jchampion@timescale.com>
Date: Fri, 24 Jun 2022 15:40:42 -0700
Subject: [PATCH v10 2/2] Add sslcertmode option for client certificates

The sslcertmode option controls whether the server is allowed and/or
required to request a certificate from the client. There are three
modes:

- "allow" is the default and follows the current behavior -- a
  configured  sslcert is sent if the server requests one (which, with
  the current implementation, will happen whenever TLS is negotiated).

- "disable" causes the client to refuse to send a client certificate
  even if an sslcert is configured.

- "require" causes the client to fail if a client certificate is never
  sent and the server opens a connection anyway. This doesn't add any
  additional security, since there is no guarantee that the server is
  validating the certificate correctly, but it may help troubleshoot
  more complicated TLS setups.

sslcertmode=require needs the OpenSSL implementation to support
SSL_CTX_set_cert_cb(). Notably, LibreSSL does not.
---
 configure                                | 11 ++---
 configure.ac                             |  4 +-
 doc/src/sgml/libpq.sgml                  | 54 +++++++++++++++++++++++
 meson.build                              |  3 +-
 src/include/pg_config.h.in               |  3 ++
 src/interfaces/libpq/fe-auth.c           | 21 +++++++++
 src/interfaces/libpq/fe-connect.c        | 55 ++++++++++++++++++++++++
 src/interfaces/libpq/fe-secure-openssl.c | 39 ++++++++++++++++-
 src/interfaces/libpq/libpq-int.h         |  3 ++
 src/test/ssl/t/001_ssltests.pl           | 43 ++++++++++++++++++
 src/tools/msvc/Solution.pm               |  8 ++++
 11 files changed, 235 insertions(+), 9 deletions(-)

diff --git a/configure b/configure
index e04ee9fb41..d46896e44e 100755
--- a/configure
+++ b/configure
@@ -12902,13 +12902,14 @@ else
 fi
 
   fi
-  # Function introduced in OpenSSL 1.0.2.
-  for ac_func in X509_get_signature_nid
+  # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all of these.
+  for ac_func in X509_get_signature_nid SSL_CTX_set_cert_cb
 do :
-  ac_fn_c_check_func "$LINENO" "X509_get_signature_nid" "ac_cv_func_X509_get_signature_nid"
-if test "x$ac_cv_func_X509_get_signature_nid" = xyes; then :
+  as_ac_var=`$as_echo "ac_cv_func_$ac_func" | $as_tr_sh`
+ac_fn_c_check_func "$LINENO" "$ac_func" "$as_ac_var"
+if eval test \"x\$"$as_ac_var"\" = x"yes"; then :
   cat >>confdefs.h <<_ACEOF
-#define HAVE_X509_GET_SIGNATURE_NID 1
+#define `$as_echo "HAVE_$ac_func" | $as_tr_cpp` 1
 _ACEOF
 
 fi
diff --git a/configure.ac b/configure.ac
index f146c8301a..8458e3793d 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1351,8 +1351,8 @@ if test "$with_ssl" = openssl ; then
      AC_SEARCH_LIBS(CRYPTO_new_ex_data, [eay32 crypto], [], [AC_MSG_ERROR([library 'eay32' or 'crypto' is required for OpenSSL])])
      AC_SEARCH_LIBS(SSL_new, [ssleay32 ssl], [], [AC_MSG_ERROR([library 'ssleay32' or 'ssl' is required for OpenSSL])])
   fi
-  # Function introduced in OpenSSL 1.0.2.
-  AC_CHECK_FUNCS([X509_get_signature_nid])
+  # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all of these.
+  AC_CHECK_FUNCS([X509_get_signature_nid SSL_CTX_set_cert_cb])
   # Functions introduced in OpenSSL 1.1.0. We used to check for
   # OPENSSL_VERSION_NUMBER, but that didn't work with 1.1.0, because LibreSSL
   # defines OPENSSL_VERSION_NUMBER to claim version 2.0.0, even though it
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 05d9645f40..c06b0718cf 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1810,6 +1810,50 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-sslcertmode" xreflabel="sslcertmode">
+      <term><literal>sslcertmode</literal></term>
+      <listitem>
+       <para>
+        This option determines whether a client certificate may be sent to the
+        server, and whether the server is required to request one. There are
+        three modes:
+
+        <variablelist>
+         <varlistentry>
+          <term><literal>disable</literal></term>
+          <listitem>
+           <para>
+            a client certificate is never sent, even if one is provided via
+            <xref linkend="libpq-connect-sslcert" />
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>allow</literal> (default)</term>
+          <listitem>
+           <para>
+            a certificate may be sent, if the server requests one and it has
+            been provided via <literal>sslcert</literal>
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>require</literal></term>
+          <listitem>
+           <para>
+            the server <emphasis>must</emphasis> request a certificate. The
+            connection will fail if the server authenticates the client despite
+            not requesting or receiving one.
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-sslrootcert" xreflabel="sslrootcert">
       <term><literal>sslrootcert</literal></term>
       <listitem>
@@ -7985,6 +8029,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGSSLCERTMODE</envar></primary>
+      </indexterm>
+      <envar>PGSSLCERTMODE</envar> behaves the same as the <xref
+      linkend="libpq-connect-sslcertmode"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/meson.build b/meson.build
index 925db70c9d..bad035c8e3 100644
--- a/meson.build
+++ b/meson.build
@@ -1173,8 +1173,9 @@ if get_option('ssl') == 'openssl'
     ['CRYPTO_new_ex_data', {'required': true}],
     ['SSL_new', {'required': true}],
 
-    # Function introduced in OpenSSL 1.0.2.
+    # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all of these.
     ['X509_get_signature_nid'],
+    ['SSL_CTX_set_cert_cb'],
 
     # Functions introduced in OpenSSL 1.1.0. We used to check for
     # OPENSSL_VERSION_NUMBER, but that didn't work with 1.1.0, because LibreSSL
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index c5a80b829e..08382e868b 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -397,6 +397,9 @@
 /* Define to 1 if you have spinlocks. */
 #undef HAVE_SPINLOCKS
 
+/* Define to 1 if you have the `SSL_CTX_set_cert_cb' function. */
+#undef HAVE_SSL_CTX_SET_CERT_CB
+
 /* Define to 1 if stdbool.h conforms to C99. */
 #undef HAVE_STDBOOL_H
 
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 5a46cf0ee9..295b978525 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -885,6 +885,27 @@ check_expected_areq(AuthRequest areq, PGconn *conn)
 	bool		result = true;
 	const char *reason = NULL;
 
+	if (conn->sslcertmode[0] == 'r' /* require */
+		&& areq == AUTH_REQ_OK)
+	{
+		/*
+		 * Trade off a little bit of complexity to try to get these error
+		 * messages as precise as possible.
+		 */
+		if (!conn->ssl_cert_requested)
+		{
+			appendPQExpBufferStr(&conn->errorMessage,
+								 libpq_gettext("server did not request a certificate"));
+			return false;
+		}
+		else if (!conn->ssl_cert_sent)
+		{
+			appendPQExpBufferStr(&conn->errorMessage,
+								 libpq_gettext("server accepted connection without a valid certificate"));
+			return false;
+		}
+	}
+
 	/*
 	 * If the user required a specific auth method, or specified an allowed set,
 	 * then reject all others here, and make sure the server actually completes
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 40f571fadb..eaca8cee1f 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -125,8 +125,10 @@ static int	ldapServiceLookup(const char *purl, PQconninfoOption *options,
 #define DefaultTargetSessionAttrs	"any"
 #ifdef USE_SSL
 #define DefaultSSLMode "prefer"
+#define DefaultSSLCertMode "allow"
 #else
 #define DefaultSSLMode	"disable"
+#define DefaultSSLCertMode "disable"
 #endif
 #ifdef ENABLE_GSS
 #include "fe-gssapi-common.h"
@@ -283,6 +285,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"SSL-Client-Key", "", 64,
 	offsetof(struct pg_conn, sslkey)},
 
+	{"sslcertmode", "PGSSLCERTMODE", NULL, NULL,
+		"SSL-Client-Cert-Mode", "", 8, /* sizeof("disable") == 8 */
+	offsetof(struct pg_conn, sslcertmode)},
+
 	{"sslpassword", NULL, NULL, NULL,
 		"SSL-Client-Key-Password", "*", 20,
 	offsetof(struct pg_conn, sslpassword)},
@@ -1514,6 +1520,55 @@ duplicate:
 		return false;
 	}
 
+	/*
+	 * validate sslcertmode option
+	 */
+	if (conn->sslcertmode)
+	{
+		if (strcmp(conn->sslcertmode, "disable") != 0 &&
+			strcmp(conn->sslcertmode, "allow") != 0 &&
+			strcmp(conn->sslcertmode, "require") != 0)
+		{
+			conn->status = CONNECTION_BAD;
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("invalid %s value: \"%s\"\n"),
+							  "sslcertmode",
+							  conn->sslcertmode);
+			return false;
+		}
+#ifndef USE_SSL
+		if (strcmp(conn->sslcertmode, "require") == 0)
+		{
+			conn->status = CONNECTION_BAD;
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("sslcertmode value \"%s\" invalid when SSL support is not compiled in\n"),
+							  conn->sslcertmode);
+			return false;
+		}
+#endif
+#ifndef HAVE_SSL_CTX_SET_CERT_CB
+		/*
+		 * Without a certificate callback, the current implementation can't
+		 * figure out if a certficate was actually requested, so "require" is
+		 * useless.
+		 */
+		if (strcmp(conn->sslcertmode, "require") == 0)
+		{
+			conn->status = CONNECTION_BAD;
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("sslcertmode value \"%s\" is not supported (check OpenSSL version)\n"),
+							  conn->sslcertmode);
+			return false;
+		}
+#endif
+	}
+	else
+	{
+		conn->sslcertmode = strdup(DefaultSSLCertMode);
+		if (!conn->sslcertmode)
+			goto oom_error;
+	}
+
 	/*
 	 * validate gssencmode option
 	 */
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index b42a908733..04fa02af94 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -475,6 +475,33 @@ verify_cb(int ok, X509_STORE_CTX *ctx)
 	return ok;
 }
 
+#ifdef HAVE_SSL_CTX_SET_CERT_CB
+/*
+ * Certificate selection callback
+ *
+ * This callback lets us choose the client certificate we send to the server
+ * after seeing its CertificateRequest. We only support sending a single
+ * hard-coded certificate via sslcert, so we don't actually set any certificates
+ * here; we just it to record whether or not the server has actually asked for
+ * one and whether we have one to send.
+ */
+static int
+cert_cb(SSL *ssl, void *arg)
+{
+	PGconn *conn = arg;
+	conn->ssl_cert_requested = true;
+
+	/* Do we have a certificate loaded to send back? */
+	if (SSL_get_certificate(ssl))
+		conn->ssl_cert_sent = true;
+
+	/*
+	 * Tell OpenSSL that the callback succeeded; we're not required to actually
+	 * make any changes to the SSL handle.
+	 */
+	return 1;
+}
+#endif
 
 /*
  * OpenSSL-specific wrapper around
@@ -970,6 +997,11 @@ initialize_SSL(PGconn *conn)
 		SSL_CTX_set_default_passwd_cb_userdata(SSL_context, conn);
 	}
 
+#ifdef HAVE_SSL_CTX_SET_CERT_CB
+	/* Set up a certificate selection callback. */
+	SSL_CTX_set_cert_cb(SSL_context, cert_cb, conn);
+#endif
+
 	/* Disable old protocol versions */
 	SSL_CTX_set_options(SSL_context, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
 
@@ -1133,7 +1165,12 @@ initialize_SSL(PGconn *conn)
 	else
 		fnbuf[0] = '\0';
 
-	if (fnbuf[0] == '\0')
+	if (conn->sslcertmode[0] == 'd') /* disable */
+	{
+		/* don't send a client cert even if we have one */
+		have_cert = false;
+	}
+	else if (fnbuf[0] == '\0')
 	{
 		/* no home directory, proceed without a client cert */
 		have_cert = false;
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index e4d88e22a1..ad6f8b78c6 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -384,6 +384,7 @@ struct pg_conn
 	char	   *sslkey;			/* client key filename */
 	char	   *sslcert;		/* client certificate filename */
 	char	   *sslpassword;	/* client key file password */
+	char	   *sslcertmode;	/* client cert mode (require,allow,disable) */
 	char	   *sslrootcert;	/* root certificate filename */
 	char	   *sslcrl;			/* certificate revocation list filename */
 	char	   *sslcrldir;		/* certificate revocation list directory name */
@@ -525,6 +526,8 @@ struct pg_conn
 
 	/* SSL structures */
 	bool		ssl_in_use;
+	bool		ssl_cert_requested;	/* Did the server ask us for a cert? */
+	bool		ssl_cert_sent;		/* Did we send one in reply? */
 
 #ifdef USE_SSL
 	bool		allow_ssl_try;	/* Allowed to try SSL negotiation */
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index efe5634fff..49003ba1b7 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -42,6 +42,10 @@ my $SERVERHOSTADDR = '127.0.0.1';
 # This is the pattern to use in pg_hba.conf to match incoming connections.
 my $SERVERHOSTCIDR = '127.0.0.1/32';
 
+# Determine whether build supports sslcertmode=require.
+my $supports_sslcertmode_require =
+  check_pg_config("#define HAVE_SSL_CTX_SET_CERT_CB 1");
+
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
 
@@ -191,6 +195,22 @@ $node->connect_ok(
 	"$common_connstr sslrootcert=ssl/both-cas-2.crt sslmode=verify-ca",
 	"cert root file that contains two certificates, order 2");
 
+# sslcertmode=allow and =disable should both work without a client certificate.
+$node->connect_ok(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=disable",
+	"connect with sslcertmode=disable");
+$node->connect_ok(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=allow",
+	"connect with sslcertmode=allow");
+
+# sslcertmode=require, however, should fail.
+$node->connect_fails(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=require",
+	"connect with sslcertmode=require fails without a client certificate",
+	expected_stderr => $supports_sslcertmode_require
+		? qr/server accepted connection without a valid certificate/
+		: qr/sslcertmode value "require" is not supported/);
+
 # CRL tests
 
 # Invalid CRL filename is the same as no CRL, succeeds
@@ -538,6 +558,29 @@ $node->connect_ok(
 	"certificate authorization succeeds with correct client cert in encrypted DER format"
 );
 
+# correct client cert with required/allowed certificate authentication
+if ($supports_sslcertmode_require)
+{
+	$node->connect_ok(
+		"$common_connstr user=ssltestuser sslcertmode=require sslcert=ssl/client.crt "
+		  . sslkey('client.key'),
+		"certificate authorization succeeds with sslcertmode=require"
+	);
+}
+$node->connect_ok(
+	"$common_connstr user=ssltestuser sslcertmode=allow sslcert=ssl/client.crt "
+	  . sslkey('client.key'),
+	"certificate authorization succeeds with sslcertmode=allow"
+);
+
+# client cert isn't sent if certificate authentication is disabled
+$node->connect_fails(
+	"$common_connstr user=ssltestuser sslcertmode=disable sslcert=ssl/client.crt "
+	  . sslkey('client.key'),
+	"certificate authorization fails with sslcertmode=disable",
+	expected_stderr => qr/connection requires a valid client certificate/
+);
+
 # correct client cert in encrypted PEM with wrong password
 $node->connect_fails(
 	"$common_connstr user=ssltestuser sslcert=ssl/client.crt "
diff --git a/src/tools/msvc/Solution.pm b/src/tools/msvc/Solution.pm
index c2acb58df0..ff6d6f9e35 100644
--- a/src/tools/msvc/Solution.pm
+++ b/src/tools/msvc/Solution.pm
@@ -328,6 +328,7 @@ sub GenerateFiles
 		HAVE_SETPROCTITLE_FAST                   => undef,
 		HAVE_SOCKLEN_T                           => 1,
 		HAVE_SPINLOCKS                           => 1,
+		HAVE_SSL_CTX_SET_CERT_CB                 => undef,
 		HAVE_STDBOOL_H                           => 1,
 		HAVE_STDINT_H                            => 1,
 		HAVE_STDLIB_H                            => 1,
@@ -489,6 +490,13 @@ sub GenerateFiles
 
 		my ($digit1, $digit2, $digit3) = $self->GetOpenSSLVersion();
 
+		if (   ($digit1 >= '3' && $digit2 >= '0' && $digit3 >= '0')
+			|| ($digit1 >= '1' && $digit2 >= '1' && $digit3 >= '0')
+			|| ($digit1 >= '1' && $digit2 >= '0' && $digit3 >= '2'))
+		{
+			$define{HAVE_SSL_CTX_SET_CERT_CB} = 1;
+		}
+
 		# More symbols are needed with OpenSSL 1.1.0 and above.
 		if (   ($digit1 >= '3' && $digit2 >= '0' && $digit3 >= '0')
 			|| ($digit1 >= '1' && $digit2 >= '1' && $digit3 >= '0'))
-- 
2.25.1

#35Jacob Champion
jchampion@timescale.com
In reply to: Jacob Champion (#34)
3 attachment(s)
Re: [PoC] Let libpq reject unexpected authentication requests

On Wed, Oct 12, 2022 at 9:40 AM Jacob Champion <jchampion@timescale.com> wrote:

On 10/5/22 06:33, Peter Eisentraut wrote:

I think it would be good to put some provisions in place here, even if
they are elementary. Otherwise, there will be a significant burden on
the person who implements the next SASL method (i.e., you ;-) ) to
figure that out then.

Sounds good, I'll work on that. v10 does not yet make changes in this area.

v11 makes an attempt at this (see 0003), using the proposed string list.

Personally I'm not happy with the amount of complexity it adds in
exchange for flexibility we can't use yet. Maybe there's a way to
simplify it, but I think the two-tiered approach of the patch has to
remain, unless we find a way to move SASL mechanism selection to a
different part of the code. I'm not sure that'd be helpful.

Maybe I should just add a basic Assert here, to trip if someone adds a
new SASL mechanism, and point that lucky person to this thread with a
comment?

--Jacob

Attachments:

v11-0003-require_auth-decouple-SASL-and-SCRAM.patchtext/x-patch; charset=US-ASCII; name=v11-0003-require_auth-decouple-SASL-and-SCRAM.patchDownload
From d747093ac9fe9875ae1f9cc5273e6133627f9691 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jchampion@timescale.com>
Date: Tue, 18 Oct 2022 16:55:36 -0700
Subject: [PATCH v11 3/3] require_auth: decouple SASL and SCRAM

Rather than assume that an AUTH_REQ_SASL* code refers to SCRAM-SHA-256,
future-proof by separating the single allowlist into a list of allowed
authentication request codes and a list of allowed SASL mechanisms.

The require_auth check is now separated into two tiers. The
AUTH_REQ_SASL* codes are always allowed. If the server sends one,
responsibility for the check then falls to pg_SASL_init(), which
compares the selected mechanism against the list of allowed mechanisms.
(Other SCRAM code is already responsible for rejecting unexpected or
out-of-order AUTH_REQ_SASL_* codes, so that's not explicitly handled
with this addition.)

Since there's only one recognized SASL mechanism, conn->sasl_mechs
currently only points at static hardcoded lists. Whenever a second
mechanism is added, the list will need to be managed dynamically.
---
 src/interfaces/libpq/fe-auth.c            | 35 +++++++++++++++++++
 src/interfaces/libpq/fe-connect.c         | 42 +++++++++++++++++++----
 src/interfaces/libpq/libpq-int.h          |  2 ++
 src/test/authentication/t/001_password.pl | 10 +++---
 src/test/ssl/t/002_scram.pl               |  6 ++++
 5 files changed, 84 insertions(+), 11 deletions(-)

diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 295b978525..24c31ae624 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -536,6 +536,41 @@ pg_SASL_init(PGconn *conn, int payloadlen)
 		goto error;
 	}
 
+	/*
+	 * Before going ahead with any SASL exchange, ensure that the user has
+	 * allowed (or, alternatively, has not forbidden) this particular mechanism.
+	 *
+	 * In a hypothetical future where a server responds with multiple SASL
+	 * mechanism families, we would need to instead consult this list up above,
+	 * during mechanism negotiation. We don't live in that world yet. The server
+	 * presents one auth method and we decide whether that's acceptable or not.
+	 */
+	if (conn->sasl_mechs)
+	{
+		const char **mech;
+		bool		found = false;
+
+		Assert(conn->require_auth);
+
+		for (mech = conn->sasl_mechs; *mech; mech++)
+		{
+			if (strcmp(*mech, selected_mechanism) == 0)
+			{
+				found = true;
+				break;
+			}
+		}
+
+		if ((conn->sasl_mechs_denied && found)
+			|| (!conn->sasl_mechs_denied && !found))
+		{
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("auth method \"%s\" requirement failed: server requested unacceptable SASL mechanism \"%s\"\n"),
+							  conn->require_auth, selected_mechanism);
+			goto error;
+		}
+	}
+
 	if (conn->channel_binding[0] == 'r' &&	/* require */
 		strcmp(selected_mechanism, SCRAM_SHA_256_PLUS_NAME) != 0)
 	{
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index eaca8cee1f..be002b4fda 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -1259,11 +1259,25 @@ connectOptions2(PGconn *conn)
 		bool		first, more;
 		bool		negated = false;
 
+		static const uint32 default_methods = (
+			1 << AUTH_REQ_SASL
+			| 1 << AUTH_REQ_SASL_CONT
+			| 1 << AUTH_REQ_SASL_FIN
+		);
+		static const char *no_mechs[] = { NULL };
+
 		/*
-		 * By default, start from an empty set of allowed options and add to it.
+		 * By default, start from a minimum set of allowed options and add to
+		 * it.
+		 *
+		 * NB: The SASL method codes are always "allowed" here. If the server
+		 * requests SASL auth, pg_SASL_init() will enforce adherence to the
+		 * sasl_mechs list, which by default is empty.
 		 */
 		conn->auth_required = true;
-		conn->allowed_auth_methods = 0;
+		conn->allowed_auth_methods = default_methods;
+		conn->sasl_mechs = no_mechs;
+		conn->sasl_mechs_denied = false;
 
 		for (first = true, more = true; more; first = false)
 		{
@@ -1289,6 +1303,9 @@ connectOptions2(PGconn *conn)
 					 */
 					conn->auth_required = false;
 					conn->allowed_auth_methods = -1;
+
+					/* conn->sasl_mechs is now a list of denied mechanisms. */
+					conn->sasl_mechs_denied = true;
 				}
 				else if (!negated)
 				{
@@ -1335,10 +1352,23 @@ connectOptions2(PGconn *conn)
 			}
 			else if (strcmp(method, "scram-sha-256") == 0)
 			{
-				/* This currently assumes that SCRAM is the only SASL method. */
-				bits = (1 << AUTH_REQ_SASL);
-				bits |= (1 << AUTH_REQ_SASL_CONT);
-				bits |= (1 << AUTH_REQ_SASL_FIN);
+				static const char *scram_mechs[] = {
+					SCRAM_SHA_256_NAME,
+					SCRAM_SHA_256_PLUS_NAME,
+					NULL /* list terminator */
+				};
+
+				/*
+				 * This currently assumes that SCRAM is the only SASL method.
+				 * Once a second mechanism is added, this code will need to add
+				 * to the list instead of replacing it wholesale.
+				 */
+				if (conn->sasl_mechs[0])
+					goto duplicate;
+				conn->sasl_mechs = scram_mechs;
+
+				free(part);
+				continue; /* avoid the bitmask manipulation below */
 			}
 			else if (strcmp(method, "none") == 0)
 			{
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index ad6f8b78c6..6fdba960da 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -461,6 +461,8 @@ struct pg_conn
 
 	bool		auth_required;	/* require an authentication challenge from the server? */
 	uint32		allowed_auth_methods;	/* bitmask of acceptable AuthRequest codes */
+	const char **sasl_mechs;	/* list of allowed/denied SASL mechanisms */
+	bool		sasl_mechs_denied;	/* is the sasl_mechs list forbidden? */
 
 	/* Transient state needed while establishing connection */
 	PGTargetServerType target_server_type;	/* desired session properties */
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index fb738886a3..23d934892e 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -235,21 +235,21 @@ $node->connect_ok("user=scram_role require_auth=password,scram-sha-256,md5",
 # ...fail for other auth types...
 $node->connect_fails("user=scram_role require_auth=password",
 	"password authentication can be required: fails with SCRAM auth",
-	expected_stderr => qr/server requested SASL authentication/);
+	expected_stderr => qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256"/);
 $node->connect_fails("user=scram_role require_auth=md5",
 	"md5 authentication can be required: fails with SCRAM auth",
-	expected_stderr => qr/server requested SASL authentication/);
+	expected_stderr => qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256"/);
 $node->connect_fails("user=scram_role require_auth=none",
 	"all authentication can be forbidden: fails with SCRAM auth",
-	expected_stderr => qr/server requested SASL authentication/);
+	expected_stderr => qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256"/);
 
 # ...and allow SCRAM authentication to be prohibited.
 $node->connect_fails("user=scram_role require_auth=!scram-sha-256",
 	"SCRAM authentication can be forbidden: fails with SCRAM auth",
-	expected_stderr => qr/server requested SASL authentication/);
+	expected_stderr => qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256"/);
 $node->connect_fails("user=scram_role require_auth=!password,!md5,!scram-sha-256",
 	"multiple authentication types can be forbidden: fails with SCRAM auth",
-	expected_stderr => qr/server requested SASL authentication/);
+	expected_stderr => qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256"/);
 
 # Test that bad passwords are rejected.
 $ENV{"PGPASSWORD"} = 'badpass';
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index cf61bc7d0d..472b2efdf1 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -150,6 +150,12 @@ if ($supports_tls_server_end_point)
 	$node->connect_ok(
 		"$common_connstr user=ssltestuser channel_binding=require require_auth=scram-sha-256",
 		"SCRAM with SSL, channel_binding=require, and require_auth=scram-sha-256");
+	$node->connect_fails(
+		"$common_connstr user=ssltestuser channel_binding=require require_auth=password",
+		"SCRAM with SSL, channel_binding=require, and require_auth=password",
+		expected_stderr =>
+		  qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256-PLUS"/
+	);
 }
 else
 {
-- 
2.25.1

v11-0002-Add-sslcertmode-option-for-client-certificates.patchtext/x-patch; charset=US-ASCII; name=v11-0002-Add-sslcertmode-option-for-client-certificates.patchDownload
From 6a28a6c9c7861ff1cadf4157bcddfefd1091a500 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jchampion@timescale.com>
Date: Fri, 24 Jun 2022 15:40:42 -0700
Subject: [PATCH v11 2/3] Add sslcertmode option for client certificates

The sslcertmode option controls whether the server is allowed and/or
required to request a certificate from the client. There are three
modes:

- "allow" is the default and follows the current behavior -- a
  configured  sslcert is sent if the server requests one (which, with
  the current implementation, will happen whenever TLS is negotiated).

- "disable" causes the client to refuse to send a client certificate
  even if an sslcert is configured.

- "require" causes the client to fail if a client certificate is never
  sent and the server opens a connection anyway. This doesn't add any
  additional security, since there is no guarantee that the server is
  validating the certificate correctly, but it may help troubleshoot
  more complicated TLS setups.

sslcertmode=require needs the OpenSSL implementation to support
SSL_CTX_set_cert_cb(). Notably, LibreSSL does not.
---
 configure                                | 11 ++---
 configure.ac                             |  4 +-
 doc/src/sgml/libpq.sgml                  | 54 +++++++++++++++++++++++
 meson.build                              |  3 +-
 src/include/pg_config.h.in               |  3 ++
 src/interfaces/libpq/fe-auth.c           | 21 +++++++++
 src/interfaces/libpq/fe-connect.c        | 55 ++++++++++++++++++++++++
 src/interfaces/libpq/fe-secure-openssl.c | 39 ++++++++++++++++-
 src/interfaces/libpq/libpq-int.h         |  3 ++
 src/test/ssl/t/001_ssltests.pl           | 43 ++++++++++++++++++
 src/tools/msvc/Solution.pm               |  8 ++++
 11 files changed, 235 insertions(+), 9 deletions(-)

diff --git a/configure b/configure
index 5ea790d638..83deb7fdfa 100755
--- a/configure
+++ b/configure
@@ -12991,13 +12991,14 @@ else
 fi
 
   fi
-  # Function introduced in OpenSSL 1.0.2.
-  for ac_func in X509_get_signature_nid
+  # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all of these.
+  for ac_func in X509_get_signature_nid SSL_CTX_set_cert_cb
 do :
-  ac_fn_c_check_func "$LINENO" "X509_get_signature_nid" "ac_cv_func_X509_get_signature_nid"
-if test "x$ac_cv_func_X509_get_signature_nid" = xyes; then :
+  as_ac_var=`$as_echo "ac_cv_func_$ac_func" | $as_tr_sh`
+ac_fn_c_check_func "$LINENO" "$ac_func" "$as_ac_var"
+if eval test \"x\$"$as_ac_var"\" = x"yes"; then :
   cat >>confdefs.h <<_ACEOF
-#define HAVE_X509_GET_SIGNATURE_NID 1
+#define `$as_echo "HAVE_$ac_func" | $as_tr_cpp` 1
 _ACEOF
 
 fi
diff --git a/configure.ac b/configure.ac
index d80cdb5ca2..f0e1eb0d1a 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1354,8 +1354,8 @@ if test "$with_ssl" = openssl ; then
      AC_SEARCH_LIBS(CRYPTO_new_ex_data, [eay32 crypto], [], [AC_MSG_ERROR([library 'eay32' or 'crypto' is required for OpenSSL])])
      AC_SEARCH_LIBS(SSL_new, [ssleay32 ssl], [], [AC_MSG_ERROR([library 'ssleay32' or 'ssl' is required for OpenSSL])])
   fi
-  # Function introduced in OpenSSL 1.0.2.
-  AC_CHECK_FUNCS([X509_get_signature_nid])
+  # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all of these.
+  AC_CHECK_FUNCS([X509_get_signature_nid SSL_CTX_set_cert_cb])
   # Functions introduced in OpenSSL 1.1.0. We used to check for
   # OPENSSL_VERSION_NUMBER, but that didn't work with 1.1.0, because LibreSSL
   # defines OPENSSL_VERSION_NUMBER to claim version 2.0.0, even though it
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 05d9645f40..c06b0718cf 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1810,6 +1810,50 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-sslcertmode" xreflabel="sslcertmode">
+      <term><literal>sslcertmode</literal></term>
+      <listitem>
+       <para>
+        This option determines whether a client certificate may be sent to the
+        server, and whether the server is required to request one. There are
+        three modes:
+
+        <variablelist>
+         <varlistentry>
+          <term><literal>disable</literal></term>
+          <listitem>
+           <para>
+            a client certificate is never sent, even if one is provided via
+            <xref linkend="libpq-connect-sslcert" />
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>allow</literal> (default)</term>
+          <listitem>
+           <para>
+            a certificate may be sent, if the server requests one and it has
+            been provided via <literal>sslcert</literal>
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>require</literal></term>
+          <listitem>
+           <para>
+            the server <emphasis>must</emphasis> request a certificate. The
+            connection will fail if the server authenticates the client despite
+            not requesting or receiving one.
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-sslrootcert" xreflabel="sslrootcert">
       <term><literal>sslrootcert</literal></term>
       <listitem>
@@ -7985,6 +8029,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGSSLCERTMODE</envar></primary>
+      </indexterm>
+      <envar>PGSSLCERTMODE</envar> behaves the same as the <xref
+      linkend="libpq-connect-sslcertmode"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/meson.build b/meson.build
index 2d225f706d..ffbb43dfa1 100644
--- a/meson.build
+++ b/meson.build
@@ -1180,8 +1180,9 @@ if get_option('ssl') == 'openssl'
     ['CRYPTO_new_ex_data', {'required': true}],
     ['SSL_new', {'required': true}],
 
-    # Function introduced in OpenSSL 1.0.2.
+    # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all of these.
     ['X509_get_signature_nid'],
+    ['SSL_CTX_set_cert_cb'],
 
     # Functions introduced in OpenSSL 1.1.0. We used to check for
     # OPENSSL_VERSION_NUMBER, but that didn't work with 1.1.0, because LibreSSL
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index c5a80b829e..08382e868b 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -397,6 +397,9 @@
 /* Define to 1 if you have spinlocks. */
 #undef HAVE_SPINLOCKS
 
+/* Define to 1 if you have the `SSL_CTX_set_cert_cb' function. */
+#undef HAVE_SSL_CTX_SET_CERT_CB
+
 /* Define to 1 if stdbool.h conforms to C99. */
 #undef HAVE_STDBOOL_H
 
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 5a46cf0ee9..295b978525 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -885,6 +885,27 @@ check_expected_areq(AuthRequest areq, PGconn *conn)
 	bool		result = true;
 	const char *reason = NULL;
 
+	if (conn->sslcertmode[0] == 'r' /* require */
+		&& areq == AUTH_REQ_OK)
+	{
+		/*
+		 * Trade off a little bit of complexity to try to get these error
+		 * messages as precise as possible.
+		 */
+		if (!conn->ssl_cert_requested)
+		{
+			appendPQExpBufferStr(&conn->errorMessage,
+								 libpq_gettext("server did not request a certificate"));
+			return false;
+		}
+		else if (!conn->ssl_cert_sent)
+		{
+			appendPQExpBufferStr(&conn->errorMessage,
+								 libpq_gettext("server accepted connection without a valid certificate"));
+			return false;
+		}
+	}
+
 	/*
 	 * If the user required a specific auth method, or specified an allowed set,
 	 * then reject all others here, and make sure the server actually completes
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 40f571fadb..eaca8cee1f 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -125,8 +125,10 @@ static int	ldapServiceLookup(const char *purl, PQconninfoOption *options,
 #define DefaultTargetSessionAttrs	"any"
 #ifdef USE_SSL
 #define DefaultSSLMode "prefer"
+#define DefaultSSLCertMode "allow"
 #else
 #define DefaultSSLMode	"disable"
+#define DefaultSSLCertMode "disable"
 #endif
 #ifdef ENABLE_GSS
 #include "fe-gssapi-common.h"
@@ -283,6 +285,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"SSL-Client-Key", "", 64,
 	offsetof(struct pg_conn, sslkey)},
 
+	{"sslcertmode", "PGSSLCERTMODE", NULL, NULL,
+		"SSL-Client-Cert-Mode", "", 8, /* sizeof("disable") == 8 */
+	offsetof(struct pg_conn, sslcertmode)},
+
 	{"sslpassword", NULL, NULL, NULL,
 		"SSL-Client-Key-Password", "*", 20,
 	offsetof(struct pg_conn, sslpassword)},
@@ -1514,6 +1520,55 @@ duplicate:
 		return false;
 	}
 
+	/*
+	 * validate sslcertmode option
+	 */
+	if (conn->sslcertmode)
+	{
+		if (strcmp(conn->sslcertmode, "disable") != 0 &&
+			strcmp(conn->sslcertmode, "allow") != 0 &&
+			strcmp(conn->sslcertmode, "require") != 0)
+		{
+			conn->status = CONNECTION_BAD;
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("invalid %s value: \"%s\"\n"),
+							  "sslcertmode",
+							  conn->sslcertmode);
+			return false;
+		}
+#ifndef USE_SSL
+		if (strcmp(conn->sslcertmode, "require") == 0)
+		{
+			conn->status = CONNECTION_BAD;
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("sslcertmode value \"%s\" invalid when SSL support is not compiled in\n"),
+							  conn->sslcertmode);
+			return false;
+		}
+#endif
+#ifndef HAVE_SSL_CTX_SET_CERT_CB
+		/*
+		 * Without a certificate callback, the current implementation can't
+		 * figure out if a certficate was actually requested, so "require" is
+		 * useless.
+		 */
+		if (strcmp(conn->sslcertmode, "require") == 0)
+		{
+			conn->status = CONNECTION_BAD;
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("sslcertmode value \"%s\" is not supported (check OpenSSL version)\n"),
+							  conn->sslcertmode);
+			return false;
+		}
+#endif
+	}
+	else
+	{
+		conn->sslcertmode = strdup(DefaultSSLCertMode);
+		if (!conn->sslcertmode)
+			goto oom_error;
+	}
+
 	/*
 	 * validate gssencmode option
 	 */
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index b42a908733..04fa02af94 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -475,6 +475,33 @@ verify_cb(int ok, X509_STORE_CTX *ctx)
 	return ok;
 }
 
+#ifdef HAVE_SSL_CTX_SET_CERT_CB
+/*
+ * Certificate selection callback
+ *
+ * This callback lets us choose the client certificate we send to the server
+ * after seeing its CertificateRequest. We only support sending a single
+ * hard-coded certificate via sslcert, so we don't actually set any certificates
+ * here; we just it to record whether or not the server has actually asked for
+ * one and whether we have one to send.
+ */
+static int
+cert_cb(SSL *ssl, void *arg)
+{
+	PGconn *conn = arg;
+	conn->ssl_cert_requested = true;
+
+	/* Do we have a certificate loaded to send back? */
+	if (SSL_get_certificate(ssl))
+		conn->ssl_cert_sent = true;
+
+	/*
+	 * Tell OpenSSL that the callback succeeded; we're not required to actually
+	 * make any changes to the SSL handle.
+	 */
+	return 1;
+}
+#endif
 
 /*
  * OpenSSL-specific wrapper around
@@ -970,6 +997,11 @@ initialize_SSL(PGconn *conn)
 		SSL_CTX_set_default_passwd_cb_userdata(SSL_context, conn);
 	}
 
+#ifdef HAVE_SSL_CTX_SET_CERT_CB
+	/* Set up a certificate selection callback. */
+	SSL_CTX_set_cert_cb(SSL_context, cert_cb, conn);
+#endif
+
 	/* Disable old protocol versions */
 	SSL_CTX_set_options(SSL_context, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
 
@@ -1133,7 +1165,12 @@ initialize_SSL(PGconn *conn)
 	else
 		fnbuf[0] = '\0';
 
-	if (fnbuf[0] == '\0')
+	if (conn->sslcertmode[0] == 'd') /* disable */
+	{
+		/* don't send a client cert even if we have one */
+		have_cert = false;
+	}
+	else if (fnbuf[0] == '\0')
 	{
 		/* no home directory, proceed without a client cert */
 		have_cert = false;
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index e4d88e22a1..ad6f8b78c6 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -384,6 +384,7 @@ struct pg_conn
 	char	   *sslkey;			/* client key filename */
 	char	   *sslcert;		/* client certificate filename */
 	char	   *sslpassword;	/* client key file password */
+	char	   *sslcertmode;	/* client cert mode (require,allow,disable) */
 	char	   *sslrootcert;	/* root certificate filename */
 	char	   *sslcrl;			/* certificate revocation list filename */
 	char	   *sslcrldir;		/* certificate revocation list directory name */
@@ -525,6 +526,8 @@ struct pg_conn
 
 	/* SSL structures */
 	bool		ssl_in_use;
+	bool		ssl_cert_requested;	/* Did the server ask us for a cert? */
+	bool		ssl_cert_sent;		/* Did we send one in reply? */
 
 #ifdef USE_SSL
 	bool		allow_ssl_try;	/* Allowed to try SSL negotiation */
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index efe5634fff..49003ba1b7 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -42,6 +42,10 @@ my $SERVERHOSTADDR = '127.0.0.1';
 # This is the pattern to use in pg_hba.conf to match incoming connections.
 my $SERVERHOSTCIDR = '127.0.0.1/32';
 
+# Determine whether build supports sslcertmode=require.
+my $supports_sslcertmode_require =
+  check_pg_config("#define HAVE_SSL_CTX_SET_CERT_CB 1");
+
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
 
@@ -191,6 +195,22 @@ $node->connect_ok(
 	"$common_connstr sslrootcert=ssl/both-cas-2.crt sslmode=verify-ca",
 	"cert root file that contains two certificates, order 2");
 
+# sslcertmode=allow and =disable should both work without a client certificate.
+$node->connect_ok(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=disable",
+	"connect with sslcertmode=disable");
+$node->connect_ok(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=allow",
+	"connect with sslcertmode=allow");
+
+# sslcertmode=require, however, should fail.
+$node->connect_fails(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=require",
+	"connect with sslcertmode=require fails without a client certificate",
+	expected_stderr => $supports_sslcertmode_require
+		? qr/server accepted connection without a valid certificate/
+		: qr/sslcertmode value "require" is not supported/);
+
 # CRL tests
 
 # Invalid CRL filename is the same as no CRL, succeeds
@@ -538,6 +558,29 @@ $node->connect_ok(
 	"certificate authorization succeeds with correct client cert in encrypted DER format"
 );
 
+# correct client cert with required/allowed certificate authentication
+if ($supports_sslcertmode_require)
+{
+	$node->connect_ok(
+		"$common_connstr user=ssltestuser sslcertmode=require sslcert=ssl/client.crt "
+		  . sslkey('client.key'),
+		"certificate authorization succeeds with sslcertmode=require"
+	);
+}
+$node->connect_ok(
+	"$common_connstr user=ssltestuser sslcertmode=allow sslcert=ssl/client.crt "
+	  . sslkey('client.key'),
+	"certificate authorization succeeds with sslcertmode=allow"
+);
+
+# client cert isn't sent if certificate authentication is disabled
+$node->connect_fails(
+	"$common_connstr user=ssltestuser sslcertmode=disable sslcert=ssl/client.crt "
+	  . sslkey('client.key'),
+	"certificate authorization fails with sslcertmode=disable",
+	expected_stderr => qr/connection requires a valid client certificate/
+);
+
 # correct client cert in encrypted PEM with wrong password
 $node->connect_fails(
 	"$common_connstr user=ssltestuser sslcert=ssl/client.crt "
diff --git a/src/tools/msvc/Solution.pm b/src/tools/msvc/Solution.pm
index c2acb58df0..ff6d6f9e35 100644
--- a/src/tools/msvc/Solution.pm
+++ b/src/tools/msvc/Solution.pm
@@ -328,6 +328,7 @@ sub GenerateFiles
 		HAVE_SETPROCTITLE_FAST                   => undef,
 		HAVE_SOCKLEN_T                           => 1,
 		HAVE_SPINLOCKS                           => 1,
+		HAVE_SSL_CTX_SET_CERT_CB                 => undef,
 		HAVE_STDBOOL_H                           => 1,
 		HAVE_STDINT_H                            => 1,
 		HAVE_STDLIB_H                            => 1,
@@ -489,6 +490,13 @@ sub GenerateFiles
 
 		my ($digit1, $digit2, $digit3) = $self->GetOpenSSLVersion();
 
+		if (   ($digit1 >= '3' && $digit2 >= '0' && $digit3 >= '0')
+			|| ($digit1 >= '1' && $digit2 >= '1' && $digit3 >= '0')
+			|| ($digit1 >= '1' && $digit2 >= '0' && $digit3 >= '2'))
+		{
+			$define{HAVE_SSL_CTX_SET_CERT_CB} = 1;
+		}
+
 		# More symbols are needed with OpenSSL 1.1.0 and above.
 		if (   ($digit1 >= '3' && $digit2 >= '0' && $digit3 >= '0')
 			|| ($digit1 >= '1' && $digit2 >= '1' && $digit3 >= '0'))
-- 
2.25.1

v11-0001-libpq-let-client-reject-unexpected-auth-methods.patchtext/x-patch; charset=US-ASCII; name=v11-0001-libpq-let-client-reject-unexpected-auth-methods.patchDownload
From feba856c9f1e91568a5168fecdd68d70723f4026 Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Mon, 28 Feb 2022 09:40:43 -0800
Subject: [PATCH v11 1/3] libpq: let client reject unexpected auth methods

The require_auth connection option allows the client to choose a list of
acceptable authentication types for use with the server. There is no
negotiation: if the server does not present one of the allowed
authentication requests, the connection fails. Additionally, all methods
in the list may be negated, e.g. '!password', in which case the server
must NOT use the listed authentication type. The special method "none"
allows/disallows the use of unauthenticated connections (but it does not
govern transport-level authentication via TLS or GSSAPI).

Internally, the patch expands the role of check_expected_areq() to
ensure that the incoming request is compatible with conn->require_auth.
It also introduces a new flag, conn->client_finished_auth, which is set
by various authentication routines when the client side of the handshake
is finished. This signals to check_expected_areq() that an OK message
from the server is expected, and allows the client to complain if the
server forgoes authentication entirely.

(Since the client can't generally prove that the server is actually
doing the work of authentication, this last part is mostly useful for
SCRAM without channel binding. It could also provide a client with a
decent signal that, at the very least, it's not connecting to a database
with trust auth, and so the connection can be tied to the client in a
later audit.)

Deficiencies:
- This is unlikely to be very forwards-compatible at the moment,
  especially with SASL/SCRAM.
- SSPI support is "implemented" but untested.
- require_auth=none,scram-sha-256 currently allows the server to leave a
  SCRAM exchange unfinished. This is not net-new behavior but may be
  surprising.
---
 doc/src/sgml/libpq.sgml                   | 105 ++++++++++++++
 src/include/libpq/pqcomm.h                |   1 +
 src/interfaces/libpq/fe-auth-scram.c      |   1 +
 src/interfaces/libpq/fe-auth.c            | 136 ++++++++++++++++++
 src/interfaces/libpq/fe-connect.c         | 164 ++++++++++++++++++++++
 src/interfaces/libpq/libpq-int.h          |   7 +
 src/test/authentication/t/001_password.pl | 149 ++++++++++++++++++++
 src/test/kerberos/t/001_auth.pl           |  26 ++++
 src/test/ldap/t/001_auth.pl               |   6 +
 src/test/ssl/t/002_scram.pl               |  25 ++++
 10 files changed, 620 insertions(+)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 3c9bd3d673..05d9645f40 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1220,6 +1220,101 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-require-auth" xreflabel="require_auth">
+      <term><literal>require_auth</literal></term>
+      <listitem>
+      <para>
+        Specifies the authentication method that the client requires from the
+        server. If the server does not use the required method to authenticate
+        the client, or if the authentication handshake is not fully completed by
+        the server, the connection will fail. A comma-separated list of methods
+        may also be provided, of which the server must use exactly one in order
+        for the connection to succeed. By default, any authentication method is
+        accepted, and the server is free to skip authentication altogether.
+      </para>
+      <para>
+        Methods may be negated with the addition of a <literal>!</literal>
+        prefix, in which case the server must <emphasis>not</emphasis> attempt
+        the listed method; any other method is accepted, and the server is free
+        not to authenticate the client at all. If a comma-separated list is
+        provided, the server may not attempt <emphasis>any</emphasis> of the
+        listed negated methods. Negated and non-negated forms may not be
+        combined in the same setting.
+      </para>
+      <para>
+        As a final special case, the <literal>none</literal> method requires the
+        server not to use an authentication challenge. (It may also be negated,
+        to require some form of authentication.)
+      </para>
+      <para>
+        The following methods may be specified:
+
+        <variablelist>
+         <varlistentry>
+          <term><literal>password</literal></term>
+          <listitem>
+           <para>
+            The server must request plaintext password authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>md5</literal></term>
+          <listitem>
+           <para>
+            The server must request MD5 hashed password authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>gss</literal></term>
+          <listitem>
+           <para>
+            The server must either request a Kerberos handshake via
+            <acronym>GSSAPI</acronym> or establish a
+            <acronym>GSS</acronym>-encrypted channel (see also
+            <xref linkend="libpq-connect-gssencmode" />).
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>sspi</literal></term>
+          <listitem>
+           <para>
+            The server must request Windows <acronym>SSPI</acronym>
+            authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>scram-sha-256</literal></term>
+          <listitem>
+           <para>
+            The server must successfully complete a SCRAM-SHA-256 authentication
+            exchange with the client.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>none</literal></term>
+          <listitem>
+           <para>
+            The server must not prompt the client for an authentication
+            exchange. (This does not prohibit client certificate authentication
+            via TLS, nor GSS authentication via its encrypted transport.)
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+      </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-channel-binding" xreflabel="channel_binding">
       <term><literal>channel_binding</literal></term>
       <listitem>
@@ -7773,6 +7868,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGREQUIREAUTH</envar></primary>
+      </indexterm>
+      <envar>PGREQUIREAUTH</envar> behaves the same as the <xref
+      linkend="libpq-connect-require-auth"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h
index fcf68df39b..912451c913 100644
--- a/src/include/libpq/pqcomm.h
+++ b/src/include/libpq/pqcomm.h
@@ -123,6 +123,7 @@ extern PGDLLIMPORT bool Db_user_namespace;
 #define AUTH_REQ_SASL	   10	/* Begin SASL authentication */
 #define AUTH_REQ_SASL_CONT 11	/* Continue SASL authentication */
 #define AUTH_REQ_SASL_FIN  12	/* Final SASL message */
+#define AUTH_REQ_MAX	   AUTH_REQ_SASL_FIN	/* maximum AUTH_REQ_* value */
 
 typedef uint32 AuthRequest;
 
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index 35cfd9987d..ad57df88b6 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -280,6 +280,7 @@ scram_exchange(void *opaq, char *input, int inputlen,
 			}
 			*done = true;
 			state->state = FE_SCRAM_FINISHED;
+			state->conn->client_finished_auth = true;
 			break;
 
 		default:
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 49a1c626f6..5a46cf0ee9 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -137,7 +137,10 @@ pg_GSS_continue(PGconn *conn, int payloadlen)
 	}
 
 	if (maj_stat == GSS_S_COMPLETE)
+	{
+		conn->client_finished_auth = true;
 		gss_release_name(&lmin_s, &conn->gtarg_nam);
+	}
 
 	return STATUS_OK;
 }
@@ -326,6 +329,9 @@ pg_SSPI_continue(PGconn *conn, int payloadlen)
 		FreeContextBuffer(outbuf.pBuffers[0].pvBuffer);
 	}
 
+	if (r == SEC_E_OK)
+		conn->client_finished_auth = true;
+
 	/* Cleanup is handled by the code in freePGconn() */
 	return STATUS_OK;
 }
@@ -830,6 +836,44 @@ pg_password_sendauth(PGconn *conn, const char *password, AuthRequest areq)
 	return ret;
 }
 
+/*
+ * Translate a disallowed AuthRequest code into an error message.
+ */
+static const char *
+auth_description(AuthRequest areq)
+{
+	switch (areq)
+	{
+		case AUTH_REQ_PASSWORD:
+			return libpq_gettext("server requested a cleartext password");
+		case AUTH_REQ_MD5:
+			return libpq_gettext("server requested a hashed password");
+		case AUTH_REQ_GSS:
+		case AUTH_REQ_GSS_CONT:
+			return libpq_gettext("server requested GSSAPI authentication");
+		case AUTH_REQ_SSPI:
+			return libpq_gettext("server requested SSPI authentication");
+		case AUTH_REQ_SCM_CREDS:
+			return libpq_gettext("server requested UNIX socket credentials");
+		case AUTH_REQ_SASL:
+		case AUTH_REQ_SASL_CONT:
+		case AUTH_REQ_SASL_FIN:
+			return libpq_gettext("server requested SASL authentication");
+	}
+
+	return libpq_gettext("server requested an unknown authentication type");
+}
+
+/*
+ * Convenience macro for checking the allowed_auth_methods bitmask. Caller must
+ * ensure that type is not greater than 31 (high bit of the bitmask).
+ */
+#define auth_allowed(conn, type) \
+	(((conn)->allowed_auth_methods & (1 << (type))) != 0)
+
+StaticAssertDecl(AUTH_REQ_MAX < CHAR_BIT * sizeof(((PGconn){0}).allowed_auth_methods),
+				 "AUTH_REQ_MAX overflows the allowed_auth_methods bitmask");
+
 /*
  * Verify that the authentication request is expected, given the connection
  * parameters. This is especially important when the client wishes to
@@ -839,6 +883,95 @@ static bool
 check_expected_areq(AuthRequest areq, PGconn *conn)
 {
 	bool		result = true;
+	const char *reason = NULL;
+
+	/*
+	 * If the user required a specific auth method, or specified an allowed set,
+	 * then reject all others here, and make sure the server actually completes
+	 * an authentication exchange.
+	 */
+	if (conn->require_auth)
+	{
+		switch (areq)
+		{
+			case AUTH_REQ_OK:
+				/*
+				 * Check to make sure we've actually finished our exchange (or
+				 * else that the user has allowed an authentication-less
+				 * connection).
+				 *
+				 * If the user has allowed both SCRAM and unauthenticated
+				 * (trust) connections, then this check will silently accept
+				 * partial SCRAM exchanges, where a misbehaving server does not
+				 * provide its verifier before sending an OK. This is consistent
+				 * with historical behavior, but it may be a point to revisit in
+				 * the future, since it could allow a server that doesn't know
+				 * the user's password to silently harvest material for a brute
+				 * force attack.
+				 */
+				if (!conn->auth_required || conn->client_finished_auth)
+					break;
+
+				/*
+				 * No explicit authentication request was made by the server --
+				 * or perhaps it was made and not completed, in the case of
+				 * SCRAM -- but there is one special case to check. If the user
+				 * allowed "gss", then a GSS-encrypted channel also satisfies
+				 * the check.
+				 */
+#ifdef ENABLE_GSS
+				if (auth_allowed(conn, AUTH_REQ_GSS) && conn->gssenc)
+				{
+					/*
+					 * If implicit GSS auth has already been performed via GSS
+					 * encryption, we don't need to have performed an
+					 * AUTH_REQ_GSS exchange. This allows require_auth=gss to be
+					 * combined with gssencmode, since there won't be an
+					 * explicit authentication request in that case.
+					 */
+				}
+				else
+#endif
+				{
+					reason = libpq_gettext("server did not complete authentication"),
+					result = false;
+				}
+
+				break;
+
+			case AUTH_REQ_PASSWORD:
+			case AUTH_REQ_MD5:
+			case AUTH_REQ_GSS:
+			case AUTH_REQ_SSPI:
+			case AUTH_REQ_GSS_CONT:
+			case AUTH_REQ_SASL:
+			case AUTH_REQ_SASL_CONT:
+			case AUTH_REQ_SASL_FIN:
+				/*
+				 * We don't handle these with the default case, to avoid
+				 * bit-shifting past the end of the allowed_auth_methods mask if
+				 * the server sends an unexpected AuthRequest.
+				 */
+				result = auth_allowed(conn, areq);
+				break;
+
+			default:
+				result = false;
+				break;
+		}
+	}
+
+	if (!result)
+	{
+		if (!reason)
+			reason = auth_description(areq);
+
+		appendPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("auth method \"%s\" requirement failed: %s\n"),
+						  conn->require_auth, reason);
+
+		return result;
+	}
 
 	/*
 	 * When channel_binding=require, we must protect against two cases: (1) we
@@ -1040,6 +1173,9 @@ pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn)
 										 "fe_sendauth: error sending password authentication\n");
 					return STATUS_ERROR;
 				}
+
+				/* We expect no further authentication requests. */
+				conn->client_finished_auth = true;
 				break;
 			}
 
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 746e9b4f1e..40f571fadb 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -307,6 +307,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Require-Peer", "", 10,
 	offsetof(struct pg_conn, requirepeer)},
 
+	{"require_auth", "PGREQUIREAUTH", NULL, NULL,
+		"Require-Auth", "", 14, /* sizeof("scram-sha-256") == 14 */
+	offsetof(struct pg_conn, require_auth)},
+
 	{"ssl_min_protocol_version", "PGSSLMINPROTOCOLVERSION", "TLSv1.2", NULL,
 		"SSL-Minimum-Protocol-Version", "", 8,	/* sizeof("TLSv1.x") == 8 */
 	offsetof(struct pg_conn, ssl_min_protocol_version)},
@@ -1240,6 +1244,166 @@ connectOptions2(PGconn *conn)
 		}
 	}
 
+	/*
+	 * parse and validate require_auth option
+	 */
+	if (conn->require_auth)
+	{
+		char	   *s = conn->require_auth;
+		bool		first, more;
+		bool		negated = false;
+
+		/*
+		 * By default, start from an empty set of allowed options and add to it.
+		 */
+		conn->auth_required = true;
+		conn->allowed_auth_methods = 0;
+
+		for (first = true, more = true; more; first = false)
+		{
+			char	   *method, *part;
+			uint32		bits;
+
+			part = parse_comma_separated_list(&s, &more);
+			if (part == NULL)
+				goto oom_error;
+
+			/*
+			 * Check for negation, e.g. '!password'. If one element is negated,
+			 * they all have to be.
+			 */
+			method = part;
+			if (*method == '!')
+			{
+				if (first)
+				{
+					/*
+					 * Switch to a permissive set of allowed options, and
+					 * subtract from it.
+					 */
+					conn->auth_required = false;
+					conn->allowed_auth_methods = -1;
+				}
+				else if (!negated)
+				{
+					conn->status = CONNECTION_BAD;
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("negative require_auth method \"%s\" cannot be mixed with non-negative methods"),
+									  method);
+
+					free(part);
+					return false;
+				}
+
+				negated = true;
+				method++;
+			}
+			else if (negated)
+			{
+				conn->status = CONNECTION_BAD;
+				appendPQExpBuffer(&conn->errorMessage,
+								  libpq_gettext("require_auth method \"%s\" cannot be mixed with negative methods"),
+								  method);
+
+				free(part);
+				return false;
+			}
+
+			if (strcmp(method, "password") == 0)
+			{
+				bits = (1 << AUTH_REQ_PASSWORD);
+			}
+			else if (strcmp(method, "md5") == 0)
+			{
+				bits = (1 << AUTH_REQ_MD5);
+			}
+			else if (strcmp(method, "gss") == 0)
+			{
+				bits = (1 << AUTH_REQ_GSS);
+				bits |= (1 << AUTH_REQ_GSS_CONT);
+			}
+			else if (strcmp(method, "sspi") == 0)
+			{
+				bits = (1 << AUTH_REQ_SSPI);
+				bits |= (1 << AUTH_REQ_GSS_CONT);
+			}
+			else if (strcmp(method, "scram-sha-256") == 0)
+			{
+				/* This currently assumes that SCRAM is the only SASL method. */
+				bits = (1 << AUTH_REQ_SASL);
+				bits |= (1 << AUTH_REQ_SASL_CONT);
+				bits |= (1 << AUTH_REQ_SASL_FIN);
+			}
+			else if (strcmp(method, "none") == 0)
+			{
+				/*
+				 * Special case: let the user explicitly allow (or disallow)
+				 * connections where the server does not send an explicit
+				 * authentication challenge, such as "trust" and "cert" auth.
+				 */
+				if (negated) /* "!none" */
+				{
+					if (conn->auth_required)
+						goto duplicate;
+
+					conn->auth_required = true;
+				}
+				else /* "none" */
+				{
+					if (!conn->auth_required)
+						goto duplicate;
+
+					conn->auth_required = false;
+				}
+
+				free(part);
+				continue; /* avoid the bitmask manipulation below */
+			}
+			else
+			{
+				conn->status = CONNECTION_BAD;
+				appendPQExpBuffer(&conn->errorMessage,
+								  libpq_gettext("invalid require_auth method: \"%s\""),
+								  method);
+
+				free(part);
+				return false;
+			}
+
+			/* Update the bitmask. */
+			if (negated)
+			{
+				if ((conn->allowed_auth_methods & bits) == 0)
+					goto duplicate;
+
+				conn->allowed_auth_methods &= ~bits;
+			}
+			else
+			{
+				if ((conn->allowed_auth_methods & bits) == bits)
+					goto duplicate;
+
+				conn->allowed_auth_methods |= bits;
+			}
+
+			free(part);
+			continue;
+
+duplicate:
+			/*
+			 * A duplicated method probably indicates a typo in a setting where
+			 * typos are extremely risky.
+			 */
+			conn->status = CONNECTION_BAD;
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("require_auth method \"%s\" is specified more than once"),
+							  part);
+
+			free(part);
+			return false;
+		}
+	}
+
 	/*
 	 * validate channel_binding option
 	 */
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index c75ed63a2c..e4d88e22a1 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -396,6 +396,7 @@ struct pg_conn
 	char	   *ssl_min_protocol_version;	/* minimum TLS protocol version */
 	char	   *ssl_max_protocol_version;	/* maximum TLS protocol version */
 	char	   *target_session_attrs;	/* desired session properties */
+	char	   *require_auth;	/* name of the expected auth method */
 
 	/* Optional file to write trace info to */
 	FILE	   *Pfdebug;
@@ -457,6 +458,9 @@ struct pg_conn
 	bool		write_failed;	/* have we had a write failure on sock? */
 	char	   *write_err_msg;	/* write error message, or NULL if OOM */
 
+	bool		auth_required;	/* require an authentication challenge from the server? */
+	uint32		allowed_auth_methods;	/* bitmask of acceptable AuthRequest codes */
+
 	/* Transient state needed while establishing connection */
 	PGTargetServerType target_server_type;	/* desired session properties */
 	bool		try_next_addr;	/* time to advance to next address/host? */
@@ -512,6 +516,9 @@ struct pg_conn
 	bool		error_result;	/* do we need to make an ERROR result? */
 	PGresult   *next_result;	/* next result (used in single-row mode) */
 
+	bool		client_finished_auth; /* have we finished our half of the
+									   * authentication exchange? */
+
 	/* Assorted state for SASL, SSL, GSS, etc */
 	const pg_fe_sasl_mech *sasl;
 	void	   *sasl_state;
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index ea664d18f5..fb738886a3 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -107,6 +107,74 @@ is($res, 't',
 	"users with trust authentication use SYSTEM_USER = NULL in parallel workers"
 );
 
+# All positive require_auth options should fail...
+$node->connect_fails("user=scram_role require_auth=gss",
+	"GSS authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=sspi",
+	"SSPI authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=password",
+	"password authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=password,scram-sha-256",
+	"multiple authentication types can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+
+# ...and negative require_auth options should succeed.
+$node->connect_ok("user=scram_role require_auth=!gss",
+	"GSS authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!sspi",
+	"SSPI authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!password",
+	"password authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!md5",
+	"md5 authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!scram-sha-256",
+	"SCRAM authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!password,!scram-sha-256",
+	"multiple authentication types can be forbidden: succeeds with trust auth");
+
+# require_auth=[!]none should interact correctly with trust auth.
+$node->connect_ok("user=scram_role require_auth=none",
+	"all authentication can be forbidden: succeeds with trust auth");
+$node->connect_fails("user=scram_role require_auth=!none",
+	"any authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+
+# Negative and positive require_auth options can't be mixed.
+$node->connect_fails("user=scram_role require_auth=scram-sha-256,!md5",
+	"negative require_auth methods can't be mixed with positive",
+	expected_stderr => qr/negative require_auth method "!md5" cannot be mixed with non-negative methods/);
+$node->connect_fails("user=scram_role require_auth=!password,scram-sha-256",
+	"positive require_auth methods can't be mixed with negative",
+	expected_stderr => qr/require_auth method "scram-sha-256" cannot be mixed with negative methods/);
+
+# require_auth methods can't be duplicated.
+$node->connect_fails("user=scram_role require_auth=password,md5,password",
+	"require_auth methods can't be duplicated: positive case",
+	expected_stderr => qr/require_auth method "password" is specified more than once/);
+$node->connect_fails("user=scram_role require_auth=!password,!md5,!password",
+	"require_auth methods can't be duplicated: negative case",
+	expected_stderr => qr/require_auth method "!password" is specified more than once/);
+$node->connect_fails("user=scram_role require_auth=none,md5,none",
+	"require_auth methods can't be duplicated: none case",
+	expected_stderr => qr/require_auth method "none" is specified more than once/);
+$node->connect_fails("user=scram_role require_auth=!none,!md5,!none",
+	"require_auth methods can't be duplicated: !none case",
+	expected_stderr => qr/require_auth method "!none" is specified more than once/);
+
+# Unknown require_auth methods are caught.
+$node->connect_fails("user=scram_role require_auth=none,abcdefg",
+	"unknown require_auth methods are rejected",
+	expected_stderr => qr/invalid require_auth method: "abcdefg"/);
+
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'all', 'all', 'password');
 test_conn($node, 'user=scram_role', 'password', 0,
@@ -116,6 +184,33 @@ test_conn($node, 'user=md5_role', 'password', 0,
 	log_like =>
 	  [qr/connection authenticated: identity="md5_role" method=password/]);
 
+# require_auth should succeed with a plaintext password...
+$node->connect_ok("user=scram_role require_auth=password",
+	"password authentication can be required: works with password auth");
+$node->connect_ok("user=scram_role require_auth=!none",
+	"any authentication can be required: works with password auth");
+$node->connect_ok("user=scram_role require_auth=scram-sha-256,password,md5",
+	"multiple authentication types can be required: works with password auth");
+
+# ...fail for other auth types...
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails("user=scram_role require_auth=none",
+	"all authentication can be forbidden: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+
+# ...and allow password authentication to be prohibited.
+$node->connect_fails("user=scram_role require_auth=!password",
+	"password authentication can be forbidden: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails("user=scram_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types can be forbidden: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+
 # For "scram-sha-256" method, user "scram_role" should be able to connect.
 reset_pg_hba($node, 'all', 'all', 'scram-sha-256');
 test_conn(
@@ -129,6 +224,33 @@ test_conn(
 test_conn($node, 'user=md5_role', 'scram-sha-256', 2,
 	log_unlike => [qr/connection authenticated:/]);
 
+# require_auth should succeed with SCRAM...
+$node->connect_ok("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: works with SCRAM auth");
+$node->connect_ok("user=scram_role require_auth=!none",
+	"any authentication can be required: works with SCRAM auth");
+$node->connect_ok("user=scram_role require_auth=password,scram-sha-256,md5",
+	"multiple authentication types can be required: works with SCRAM auth");
+
+# ...fail for other auth types...
+$node->connect_fails("user=scram_role require_auth=password",
+	"password authentication can be required: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails("user=scram_role require_auth=none",
+	"all authentication can be forbidden: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+
+# ...and allow SCRAM authentication to be prohibited.
+$node->connect_fails("user=scram_role require_auth=!scram-sha-256",
+	"SCRAM authentication can be forbidden: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails("user=scram_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types can be forbidden: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+
 # Test that bad passwords are rejected.
 $ENV{"PGPASSWORD"} = 'badpass';
 test_conn($node, 'user=scram_role', 'scram-sha-256', 2,
@@ -145,6 +267,33 @@ test_conn($node, 'user=md5_role', 'md5', 0,
 	log_like =>
 	  [qr/connection authenticated: identity="md5_role" method=md5/]);
 
+# require_auth should succeed with MD5...
+$node->connect_ok("user=md5_role require_auth=md5",
+	"MD5 authentication can be required: works with MD5 auth");
+$node->connect_ok("user=md5_role require_auth=!none",
+	"any authentication can be required: works with MD5 auth");
+$node->connect_ok("user=md5_role require_auth=md5,scram-sha-256,password",
+	"multiple authentication types can be required: works with MD5 auth");
+
+# ...fail for other auth types...
+$node->connect_fails("user=md5_role require_auth=password",
+	"password authentication can be required: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+$node->connect_fails("user=md5_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+$node->connect_fails("user=md5_role require_auth=none",
+	"all authentication can be forbidden: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+
+# ...and allow MD5 authentication to be prohibited.
+$node->connect_fails("user=md5_role require_auth=!md5",
+	"password authentication can be forbidden: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+$node->connect_fails("user=md5_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types can be forbidden: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+
 # Test SYSTEM_USER <> NULL with parallel workers.
 $node->safe_psql(
 	'postgres',
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index a2bc8a5351..61aede12d1 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -318,6 +318,24 @@ test_query(
 	'gssencmode=require',
 	'sending 100K lines works');
 
+# require_auth=gss should succeed...
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=disable require_auth=gss",
+	"GSS authentication can be requested: works with GSS auth without encryption");
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss",
+	"GSS authentication can be requested: works with GSS auth with encryption");
+
+# ...and require_auth=sspi should fail.
+$node->connect_fails(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=disable require_auth=sspi",
+	"SSPI authentication can be requested: fails with GSS auth without encryption",
+	expected_stderr => qr/server requested GSSAPI authentication/);
+$node->connect_fails(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=sspi",
+	"SSPI authentication can be requested: fails with GSS auth with encryption",
+	expected_stderr => qr/server did not complete authentication/);
+
 # Test that SYSTEM_USER works.
 test_query($node, 'test1', 'SELECT SYSTEM_USER;',
 	qr/^gss:test1\@$realm$/s, 'gssencmode=require', 'testing system_user');
@@ -363,6 +381,14 @@ test_access(
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable',
 	'fails with GSS encryption disabled and hostgssenc hba');
 
+# require_auth=gss should succeed.
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss",
+	"GSS authentication can be requested: works with GSS encryption");
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss,scram-sha-256",
+	"multiple authentication types can be requested: works with GSS encryption");
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
 	qq{hostnogssenc all all $hostaddr/32 gss map=mymap});
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 2f064f6944..25483e50cc 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -220,6 +220,12 @@ test_access(
 		qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/
 	],);
 
+# require_auth=password should complete successfully; other methods should fail.
+$node->connect_ok("user=test1 require_auth=password",
+	"password authentication can be required: works with ldap auth");
+$node->connect_fails("user=test1 require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with ldap auth");
+
 note "search+bind";
 
 unlink($node->data_dir . '/pg_hba.conf');
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index deaa4aa086..cf61bc7d0d 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -136,4 +136,29 @@ $node->connect_ok(
 		qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/
 	]);
 
+# channel_binding should continue to function independently of require_auth.
+$node->connect_ok("$common_connstr user=ssltestuser channel_binding=disable require_auth=scram-sha-256",
+	"SCRAM with SSL, channel_binding=disable, and require_auth=scram-sha-256");
+$node->connect_fails(
+	"$common_connstr user=md5testuser require_auth=md5 channel_binding=require",
+	"channel_binding can fail even when require_auth succeeds",
+	expected_stderr =>
+	  qr/channel binding required but not supported by server's authentication request/
+);
+if ($supports_tls_server_end_point)
+{
+	$node->connect_ok(
+		"$common_connstr user=ssltestuser channel_binding=require require_auth=scram-sha-256",
+		"SCRAM with SSL, channel_binding=require, and require_auth=scram-sha-256");
+}
+else
+{
+	$node->connect_fails(
+		"$common_connstr user=ssltestuser channel_binding=require require_auth=scram-sha-256",
+		"SCRAM with SSL, channel_binding=require, and require_auth=scram-sha-256",
+		expected_stderr =>
+		  qr/channel binding is required, but server did not offer an authentication method that supports channel binding/
+	);
+}
+
 done_testing();
-- 
2.25.1

#36Aleksander Alekseev
aleksander@timescale.com
In reply to: Jacob Champion (#35)
Re: [PoC] Let libpq reject unexpected authentication requests

Hi Jacob,

v11 makes an attempt at this (see 0003), using the proposed string list.

I noticed that this patchset stuck a bit so I decided to take a look.

In 0001:

```
+                    conn->auth_required = false;
+                    conn->allowed_auth_methods = -1;
...
+    uint32        allowed_auth_methods;    /* bitmask of acceptable
AuthRequest codes */
```

Assigning a negative number to uint32 doesn't necessarily work on all
platforms. I suggest using PG_UINT32_MAX.

In 0002:

```
+          <term><literal>require</literal></term>
+          <listitem>
+           <para>
+            the server <emphasis>must</emphasis> request a certificate. The
+            connection will fail if the server authenticates the client despite
+            not requesting or receiving one.
```

The commit message IMO has a better description of "require". I
suggest adding the part about "This doesn't add any additional
security ..." to the documentation.

```
+ * hard-coded certificate via sslcert, so we don't actually set any
certificates
+ * here; we just it to record whether or not the server has actually asked for
```

Something is off with the wording here in the "we just it to ..." part.

The patchset seems to be in very good shape except for these few
nitpicks. I'm inclined to change its status to "Ready for Committer"
as soon as the new version will pass cfbot unless there are going to
be any objections from the community.

--
Best regards,
Aleksander Alekseev

#37Jacob Champion
jchampion@timescale.com
In reply to: Aleksander Alekseev (#36)
4 attachment(s)
Re: [PoC] Let libpq reject unexpected authentication requests

On 11/11/22 05:52, Aleksander Alekseev wrote:

I noticed that this patchset stuck a bit so I decided to take a look.

Thanks!

Assigning a negative number to uint32 doesn't necessarily work on all
platforms. I suggest using PG_UINT32_MAX.

Hmm -- on which platforms is "-1 converted to unsigned" not equivalent
to the maximum value? Are they C-compliant?

The commit message IMO has a better description of "require". I
suggest adding the part about "This doesn't add any additional
security ..." to the documentation.

Sounds good; see what you think of v12.

```
+ * hard-coded certificate via sslcert, so we don't actually set any
certificates
+ * here; we just it to record whether or not the server has actually asked for
```

Something is off with the wording here in the "we just it to ..." part.

Fixed.

The patchset seems to be in very good shape except for these few
nitpicks. I'm inclined to change its status to "Ready for Committer"
as soon as the new version will pass cfbot unless there are going to
be any objections from the community.

Thank you! I expect a maintainer will need to weigh in on the
cost/benefit of 0003 either way.

--Jacob

Attachments:

since-v11.diff.txttext/plain; charset=UTF-8; name=since-v11.diff.txtDownload
commit e71ea0d0356f5ef2fb4214fc978f835d9fa815f8
Author: Jacob Champion <jchampion@timescale.com>
Date:   Fri Nov 11 15:55:23 2022 -0800

    squash! Add sslcertmode option for client certificates
    
    Improve docs, fix comment.

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index c06b0718cf..32c0872eed 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1844,13 +1844,23 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
           <listitem>
            <para>
             the server <emphasis>must</emphasis> request a certificate. The
-            connection will fail if the server authenticates the client despite
-            not requesting or receiving one.
+            connection will fail if the client does not send a certificate and
+            the server successfully authenticates the client anyway.
            </para>
           </listitem>
          </varlistentry>
         </variablelist>
        </para>
+
+       <note>
+        <para>
+         <literal>sslcertmode=require</literal> doesn't add any additional
+         security, since there is no guarantee that the server is validating the
+         certificate correctly; PostgreSQL servers generally request TLS
+         certificates from clients whether they validate them or not. The option
+         may be useful when troubleshooting more complicated TLS setups.
+        </para>
+       </note>
       </listitem>
      </varlistentry>
 
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 04fa02af94..241a28a32d 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -482,8 +482,8 @@ verify_cb(int ok, X509_STORE_CTX *ctx)
  * This callback lets us choose the client certificate we send to the server
  * after seeing its CertificateRequest. We only support sending a single
  * hard-coded certificate via sslcert, so we don't actually set any certificates
- * here; we just it to record whether or not the server has actually asked for
- * one and whether we have one to send.
+ * here; we just use it to record whether or not the server has actually asked
+ * for one and whether we have one to send.
  */
 static int
 cert_cb(SSL *ssl, void *arg)
v12-0001-libpq-let-client-reject-unexpected-auth-methods.patchtext/x-patch; charset=UTF-8; name=v12-0001-libpq-let-client-reject-unexpected-auth-methods.patchDownload
From ec251d78c8488ee578d8c876464d3730c1310939 Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Mon, 28 Feb 2022 09:40:43 -0800
Subject: [PATCH v12 1/3] libpq: let client reject unexpected auth methods

The require_auth connection option allows the client to choose a list of
acceptable authentication types for use with the server. There is no
negotiation: if the server does not present one of the allowed
authentication requests, the connection fails. Additionally, all methods
in the list may be negated, e.g. '!password', in which case the server
must NOT use the listed authentication type. The special method "none"
allows/disallows the use of unauthenticated connections (but it does not
govern transport-level authentication via TLS or GSSAPI).

Internally, the patch expands the role of check_expected_areq() to
ensure that the incoming request is compatible with conn->require_auth.
It also introduces a new flag, conn->client_finished_auth, which is set
by various authentication routines when the client side of the handshake
is finished. This signals to check_expected_areq() that an OK message
from the server is expected, and allows the client to complain if the
server forgoes authentication entirely.

(Since the client can't generally prove that the server is actually
doing the work of authentication, this last part is mostly useful for
SCRAM without channel binding. It could also provide a client with a
decent signal that, at the very least, it's not connecting to a database
with trust auth, and so the connection can be tied to the client in a
later audit.)

Deficiencies:
- This is unlikely to be very forwards-compatible at the moment,
  especially with SASL/SCRAM.
- SSPI support is "implemented" but untested.
- require_auth=none,scram-sha-256 currently allows the server to leave a
  SCRAM exchange unfinished. This is not net-new behavior but may be
  surprising.
---
 doc/src/sgml/libpq.sgml                   | 105 ++++++++++++++
 src/include/libpq/pqcomm.h                |   1 +
 src/interfaces/libpq/fe-auth-scram.c      |   1 +
 src/interfaces/libpq/fe-auth.c            | 136 ++++++++++++++++++
 src/interfaces/libpq/fe-connect.c         | 164 ++++++++++++++++++++++
 src/interfaces/libpq/libpq-int.h          |   7 +
 src/test/authentication/t/001_password.pl | 149 ++++++++++++++++++++
 src/test/kerberos/t/001_auth.pl           |  26 ++++
 src/test/ldap/t/001_auth.pl               |   6 +
 src/test/ssl/t/002_scram.pl               |  25 ++++
 10 files changed, 620 insertions(+)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 3c9bd3d673..05d9645f40 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1220,6 +1220,101 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-require-auth" xreflabel="require_auth">
+      <term><literal>require_auth</literal></term>
+      <listitem>
+      <para>
+        Specifies the authentication method that the client requires from the
+        server. If the server does not use the required method to authenticate
+        the client, or if the authentication handshake is not fully completed by
+        the server, the connection will fail. A comma-separated list of methods
+        may also be provided, of which the server must use exactly one in order
+        for the connection to succeed. By default, any authentication method is
+        accepted, and the server is free to skip authentication altogether.
+      </para>
+      <para>
+        Methods may be negated with the addition of a <literal>!</literal>
+        prefix, in which case the server must <emphasis>not</emphasis> attempt
+        the listed method; any other method is accepted, and the server is free
+        not to authenticate the client at all. If a comma-separated list is
+        provided, the server may not attempt <emphasis>any</emphasis> of the
+        listed negated methods. Negated and non-negated forms may not be
+        combined in the same setting.
+      </para>
+      <para>
+        As a final special case, the <literal>none</literal> method requires the
+        server not to use an authentication challenge. (It may also be negated,
+        to require some form of authentication.)
+      </para>
+      <para>
+        The following methods may be specified:
+
+        <variablelist>
+         <varlistentry>
+          <term><literal>password</literal></term>
+          <listitem>
+           <para>
+            The server must request plaintext password authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>md5</literal></term>
+          <listitem>
+           <para>
+            The server must request MD5 hashed password authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>gss</literal></term>
+          <listitem>
+           <para>
+            The server must either request a Kerberos handshake via
+            <acronym>GSSAPI</acronym> or establish a
+            <acronym>GSS</acronym>-encrypted channel (see also
+            <xref linkend="libpq-connect-gssencmode" />).
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>sspi</literal></term>
+          <listitem>
+           <para>
+            The server must request Windows <acronym>SSPI</acronym>
+            authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>scram-sha-256</literal></term>
+          <listitem>
+           <para>
+            The server must successfully complete a SCRAM-SHA-256 authentication
+            exchange with the client.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>none</literal></term>
+          <listitem>
+           <para>
+            The server must not prompt the client for an authentication
+            exchange. (This does not prohibit client certificate authentication
+            via TLS, nor GSS authentication via its encrypted transport.)
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+      </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-channel-binding" xreflabel="channel_binding">
       <term><literal>channel_binding</literal></term>
       <listitem>
@@ -7773,6 +7868,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGREQUIREAUTH</envar></primary>
+      </indexterm>
+      <envar>PGREQUIREAUTH</envar> behaves the same as the <xref
+      linkend="libpq-connect-require-auth"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h
index fcf68df39b..912451c913 100644
--- a/src/include/libpq/pqcomm.h
+++ b/src/include/libpq/pqcomm.h
@@ -123,6 +123,7 @@ extern PGDLLIMPORT bool Db_user_namespace;
 #define AUTH_REQ_SASL	   10	/* Begin SASL authentication */
 #define AUTH_REQ_SASL_CONT 11	/* Continue SASL authentication */
 #define AUTH_REQ_SASL_FIN  12	/* Final SASL message */
+#define AUTH_REQ_MAX	   AUTH_REQ_SASL_FIN	/* maximum AUTH_REQ_* value */
 
 typedef uint32 AuthRequest;
 
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index 35cfd9987d..ad57df88b6 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -280,6 +280,7 @@ scram_exchange(void *opaq, char *input, int inputlen,
 			}
 			*done = true;
 			state->state = FE_SCRAM_FINISHED;
+			state->conn->client_finished_auth = true;
 			break;
 
 		default:
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 49a1c626f6..5a46cf0ee9 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -137,7 +137,10 @@ pg_GSS_continue(PGconn *conn, int payloadlen)
 	}
 
 	if (maj_stat == GSS_S_COMPLETE)
+	{
+		conn->client_finished_auth = true;
 		gss_release_name(&lmin_s, &conn->gtarg_nam);
+	}
 
 	return STATUS_OK;
 }
@@ -326,6 +329,9 @@ pg_SSPI_continue(PGconn *conn, int payloadlen)
 		FreeContextBuffer(outbuf.pBuffers[0].pvBuffer);
 	}
 
+	if (r == SEC_E_OK)
+		conn->client_finished_auth = true;
+
 	/* Cleanup is handled by the code in freePGconn() */
 	return STATUS_OK;
 }
@@ -830,6 +836,44 @@ pg_password_sendauth(PGconn *conn, const char *password, AuthRequest areq)
 	return ret;
 }
 
+/*
+ * Translate a disallowed AuthRequest code into an error message.
+ */
+static const char *
+auth_description(AuthRequest areq)
+{
+	switch (areq)
+	{
+		case AUTH_REQ_PASSWORD:
+			return libpq_gettext("server requested a cleartext password");
+		case AUTH_REQ_MD5:
+			return libpq_gettext("server requested a hashed password");
+		case AUTH_REQ_GSS:
+		case AUTH_REQ_GSS_CONT:
+			return libpq_gettext("server requested GSSAPI authentication");
+		case AUTH_REQ_SSPI:
+			return libpq_gettext("server requested SSPI authentication");
+		case AUTH_REQ_SCM_CREDS:
+			return libpq_gettext("server requested UNIX socket credentials");
+		case AUTH_REQ_SASL:
+		case AUTH_REQ_SASL_CONT:
+		case AUTH_REQ_SASL_FIN:
+			return libpq_gettext("server requested SASL authentication");
+	}
+
+	return libpq_gettext("server requested an unknown authentication type");
+}
+
+/*
+ * Convenience macro for checking the allowed_auth_methods bitmask. Caller must
+ * ensure that type is not greater than 31 (high bit of the bitmask).
+ */
+#define auth_allowed(conn, type) \
+	(((conn)->allowed_auth_methods & (1 << (type))) != 0)
+
+StaticAssertDecl(AUTH_REQ_MAX < CHAR_BIT * sizeof(((PGconn){0}).allowed_auth_methods),
+				 "AUTH_REQ_MAX overflows the allowed_auth_methods bitmask");
+
 /*
  * Verify that the authentication request is expected, given the connection
  * parameters. This is especially important when the client wishes to
@@ -839,6 +883,95 @@ static bool
 check_expected_areq(AuthRequest areq, PGconn *conn)
 {
 	bool		result = true;
+	const char *reason = NULL;
+
+	/*
+	 * If the user required a specific auth method, or specified an allowed set,
+	 * then reject all others here, and make sure the server actually completes
+	 * an authentication exchange.
+	 */
+	if (conn->require_auth)
+	{
+		switch (areq)
+		{
+			case AUTH_REQ_OK:
+				/*
+				 * Check to make sure we've actually finished our exchange (or
+				 * else that the user has allowed an authentication-less
+				 * connection).
+				 *
+				 * If the user has allowed both SCRAM and unauthenticated
+				 * (trust) connections, then this check will silently accept
+				 * partial SCRAM exchanges, where a misbehaving server does not
+				 * provide its verifier before sending an OK. This is consistent
+				 * with historical behavior, but it may be a point to revisit in
+				 * the future, since it could allow a server that doesn't know
+				 * the user's password to silently harvest material for a brute
+				 * force attack.
+				 */
+				if (!conn->auth_required || conn->client_finished_auth)
+					break;
+
+				/*
+				 * No explicit authentication request was made by the server --
+				 * or perhaps it was made and not completed, in the case of
+				 * SCRAM -- but there is one special case to check. If the user
+				 * allowed "gss", then a GSS-encrypted channel also satisfies
+				 * the check.
+				 */
+#ifdef ENABLE_GSS
+				if (auth_allowed(conn, AUTH_REQ_GSS) && conn->gssenc)
+				{
+					/*
+					 * If implicit GSS auth has already been performed via GSS
+					 * encryption, we don't need to have performed an
+					 * AUTH_REQ_GSS exchange. This allows require_auth=gss to be
+					 * combined with gssencmode, since there won't be an
+					 * explicit authentication request in that case.
+					 */
+				}
+				else
+#endif
+				{
+					reason = libpq_gettext("server did not complete authentication"),
+					result = false;
+				}
+
+				break;
+
+			case AUTH_REQ_PASSWORD:
+			case AUTH_REQ_MD5:
+			case AUTH_REQ_GSS:
+			case AUTH_REQ_SSPI:
+			case AUTH_REQ_GSS_CONT:
+			case AUTH_REQ_SASL:
+			case AUTH_REQ_SASL_CONT:
+			case AUTH_REQ_SASL_FIN:
+				/*
+				 * We don't handle these with the default case, to avoid
+				 * bit-shifting past the end of the allowed_auth_methods mask if
+				 * the server sends an unexpected AuthRequest.
+				 */
+				result = auth_allowed(conn, areq);
+				break;
+
+			default:
+				result = false;
+				break;
+		}
+	}
+
+	if (!result)
+	{
+		if (!reason)
+			reason = auth_description(areq);
+
+		appendPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("auth method \"%s\" requirement failed: %s\n"),
+						  conn->require_auth, reason);
+
+		return result;
+	}
 
 	/*
 	 * When channel_binding=require, we must protect against two cases: (1) we
@@ -1040,6 +1173,9 @@ pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn)
 										 "fe_sendauth: error sending password authentication\n");
 					return STATUS_ERROR;
 				}
+
+				/* We expect no further authentication requests. */
+				conn->client_finished_auth = true;
 				break;
 			}
 
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 746e9b4f1e..40f571fadb 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -307,6 +307,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Require-Peer", "", 10,
 	offsetof(struct pg_conn, requirepeer)},
 
+	{"require_auth", "PGREQUIREAUTH", NULL, NULL,
+		"Require-Auth", "", 14, /* sizeof("scram-sha-256") == 14 */
+	offsetof(struct pg_conn, require_auth)},
+
 	{"ssl_min_protocol_version", "PGSSLMINPROTOCOLVERSION", "TLSv1.2", NULL,
 		"SSL-Minimum-Protocol-Version", "", 8,	/* sizeof("TLSv1.x") == 8 */
 	offsetof(struct pg_conn, ssl_min_protocol_version)},
@@ -1240,6 +1244,166 @@ connectOptions2(PGconn *conn)
 		}
 	}
 
+	/*
+	 * parse and validate require_auth option
+	 */
+	if (conn->require_auth)
+	{
+		char	   *s = conn->require_auth;
+		bool		first, more;
+		bool		negated = false;
+
+		/*
+		 * By default, start from an empty set of allowed options and add to it.
+		 */
+		conn->auth_required = true;
+		conn->allowed_auth_methods = 0;
+
+		for (first = true, more = true; more; first = false)
+		{
+			char	   *method, *part;
+			uint32		bits;
+
+			part = parse_comma_separated_list(&s, &more);
+			if (part == NULL)
+				goto oom_error;
+
+			/*
+			 * Check for negation, e.g. '!password'. If one element is negated,
+			 * they all have to be.
+			 */
+			method = part;
+			if (*method == '!')
+			{
+				if (first)
+				{
+					/*
+					 * Switch to a permissive set of allowed options, and
+					 * subtract from it.
+					 */
+					conn->auth_required = false;
+					conn->allowed_auth_methods = -1;
+				}
+				else if (!negated)
+				{
+					conn->status = CONNECTION_BAD;
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("negative require_auth method \"%s\" cannot be mixed with non-negative methods"),
+									  method);
+
+					free(part);
+					return false;
+				}
+
+				negated = true;
+				method++;
+			}
+			else if (negated)
+			{
+				conn->status = CONNECTION_BAD;
+				appendPQExpBuffer(&conn->errorMessage,
+								  libpq_gettext("require_auth method \"%s\" cannot be mixed with negative methods"),
+								  method);
+
+				free(part);
+				return false;
+			}
+
+			if (strcmp(method, "password") == 0)
+			{
+				bits = (1 << AUTH_REQ_PASSWORD);
+			}
+			else if (strcmp(method, "md5") == 0)
+			{
+				bits = (1 << AUTH_REQ_MD5);
+			}
+			else if (strcmp(method, "gss") == 0)
+			{
+				bits = (1 << AUTH_REQ_GSS);
+				bits |= (1 << AUTH_REQ_GSS_CONT);
+			}
+			else if (strcmp(method, "sspi") == 0)
+			{
+				bits = (1 << AUTH_REQ_SSPI);
+				bits |= (1 << AUTH_REQ_GSS_CONT);
+			}
+			else if (strcmp(method, "scram-sha-256") == 0)
+			{
+				/* This currently assumes that SCRAM is the only SASL method. */
+				bits = (1 << AUTH_REQ_SASL);
+				bits |= (1 << AUTH_REQ_SASL_CONT);
+				bits |= (1 << AUTH_REQ_SASL_FIN);
+			}
+			else if (strcmp(method, "none") == 0)
+			{
+				/*
+				 * Special case: let the user explicitly allow (or disallow)
+				 * connections where the server does not send an explicit
+				 * authentication challenge, such as "trust" and "cert" auth.
+				 */
+				if (negated) /* "!none" */
+				{
+					if (conn->auth_required)
+						goto duplicate;
+
+					conn->auth_required = true;
+				}
+				else /* "none" */
+				{
+					if (!conn->auth_required)
+						goto duplicate;
+
+					conn->auth_required = false;
+				}
+
+				free(part);
+				continue; /* avoid the bitmask manipulation below */
+			}
+			else
+			{
+				conn->status = CONNECTION_BAD;
+				appendPQExpBuffer(&conn->errorMessage,
+								  libpq_gettext("invalid require_auth method: \"%s\""),
+								  method);
+
+				free(part);
+				return false;
+			}
+
+			/* Update the bitmask. */
+			if (negated)
+			{
+				if ((conn->allowed_auth_methods & bits) == 0)
+					goto duplicate;
+
+				conn->allowed_auth_methods &= ~bits;
+			}
+			else
+			{
+				if ((conn->allowed_auth_methods & bits) == bits)
+					goto duplicate;
+
+				conn->allowed_auth_methods |= bits;
+			}
+
+			free(part);
+			continue;
+
+duplicate:
+			/*
+			 * A duplicated method probably indicates a typo in a setting where
+			 * typos are extremely risky.
+			 */
+			conn->status = CONNECTION_BAD;
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("require_auth method \"%s\" is specified more than once"),
+							  part);
+
+			free(part);
+			return false;
+		}
+	}
+
 	/*
 	 * validate channel_binding option
 	 */
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index c75ed63a2c..e4d88e22a1 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -396,6 +396,7 @@ struct pg_conn
 	char	   *ssl_min_protocol_version;	/* minimum TLS protocol version */
 	char	   *ssl_max_protocol_version;	/* maximum TLS protocol version */
 	char	   *target_session_attrs;	/* desired session properties */
+	char	   *require_auth;	/* name of the expected auth method */
 
 	/* Optional file to write trace info to */
 	FILE	   *Pfdebug;
@@ -457,6 +458,9 @@ struct pg_conn
 	bool		write_failed;	/* have we had a write failure on sock? */
 	char	   *write_err_msg;	/* write error message, or NULL if OOM */
 
+	bool		auth_required;	/* require an authentication challenge from the server? */
+	uint32		allowed_auth_methods;	/* bitmask of acceptable AuthRequest codes */
+
 	/* Transient state needed while establishing connection */
 	PGTargetServerType target_server_type;	/* desired session properties */
 	bool		try_next_addr;	/* time to advance to next address/host? */
@@ -512,6 +516,9 @@ struct pg_conn
 	bool		error_result;	/* do we need to make an ERROR result? */
 	PGresult   *next_result;	/* next result (used in single-row mode) */
 
+	bool		client_finished_auth; /* have we finished our half of the
+									   * authentication exchange? */
+
 	/* Assorted state for SASL, SSL, GSS, etc */
 	const pg_fe_sasl_mech *sasl;
 	void	   *sasl_state;
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 42d3d4c79b..01f256bbbc 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -115,6 +115,74 @@ is($res, 't',
 	"users with trust authentication use SYSTEM_USER = NULL in parallel workers"
 );
 
+# All positive require_auth options should fail...
+$node->connect_fails("user=scram_role require_auth=gss",
+	"GSS authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=sspi",
+	"SSPI authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=password",
+	"password authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=password,scram-sha-256",
+	"multiple authentication types can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+
+# ...and negative require_auth options should succeed.
+$node->connect_ok("user=scram_role require_auth=!gss",
+	"GSS authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!sspi",
+	"SSPI authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!password",
+	"password authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!md5",
+	"md5 authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!scram-sha-256",
+	"SCRAM authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!password,!scram-sha-256",
+	"multiple authentication types can be forbidden: succeeds with trust auth");
+
+# require_auth=[!]none should interact correctly with trust auth.
+$node->connect_ok("user=scram_role require_auth=none",
+	"all authentication can be forbidden: succeeds with trust auth");
+$node->connect_fails("user=scram_role require_auth=!none",
+	"any authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+
+# Negative and positive require_auth options can't be mixed.
+$node->connect_fails("user=scram_role require_auth=scram-sha-256,!md5",
+	"negative require_auth methods can't be mixed with positive",
+	expected_stderr => qr/negative require_auth method "!md5" cannot be mixed with non-negative methods/);
+$node->connect_fails("user=scram_role require_auth=!password,scram-sha-256",
+	"positive require_auth methods can't be mixed with negative",
+	expected_stderr => qr/require_auth method "scram-sha-256" cannot be mixed with negative methods/);
+
+# require_auth methods can't be duplicated.
+$node->connect_fails("user=scram_role require_auth=password,md5,password",
+	"require_auth methods can't be duplicated: positive case",
+	expected_stderr => qr/require_auth method "password" is specified more than once/);
+$node->connect_fails("user=scram_role require_auth=!password,!md5,!password",
+	"require_auth methods can't be duplicated: negative case",
+	expected_stderr => qr/require_auth method "!password" is specified more than once/);
+$node->connect_fails("user=scram_role require_auth=none,md5,none",
+	"require_auth methods can't be duplicated: none case",
+	expected_stderr => qr/require_auth method "none" is specified more than once/);
+$node->connect_fails("user=scram_role require_auth=!none,!md5,!none",
+	"require_auth methods can't be duplicated: !none case",
+	expected_stderr => qr/require_auth method "!none" is specified more than once/);
+
+# Unknown require_auth methods are caught.
+$node->connect_fails("user=scram_role require_auth=none,abcdefg",
+	"unknown require_auth methods are rejected",
+	expected_stderr => qr/invalid require_auth method: "abcdefg"/);
+
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'all', 'all', 'password');
 test_conn($node, 'user=scram_role', 'password', 0,
@@ -124,6 +192,33 @@ test_conn($node, 'user=md5_role', 'password', 0,
 	log_like =>
 	  [qr/connection authenticated: identity="md5_role" method=password/]);
 
+# require_auth should succeed with a plaintext password...
+$node->connect_ok("user=scram_role require_auth=password",
+	"password authentication can be required: works with password auth");
+$node->connect_ok("user=scram_role require_auth=!none",
+	"any authentication can be required: works with password auth");
+$node->connect_ok("user=scram_role require_auth=scram-sha-256,password,md5",
+	"multiple authentication types can be required: works with password auth");
+
+# ...fail for other auth types...
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails("user=scram_role require_auth=none",
+	"all authentication can be forbidden: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+
+# ...and allow password authentication to be prohibited.
+$node->connect_fails("user=scram_role require_auth=!password",
+	"password authentication can be forbidden: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails("user=scram_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types can be forbidden: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+
 # For "scram-sha-256" method, user "scram_role" should be able to connect.
 reset_pg_hba($node, 'all', 'all', 'scram-sha-256');
 test_conn(
@@ -137,6 +232,33 @@ test_conn(
 test_conn($node, 'user=md5_role', 'scram-sha-256', 2,
 	log_unlike => [qr/connection authenticated:/]);
 
+# require_auth should succeed with SCRAM...
+$node->connect_ok("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: works with SCRAM auth");
+$node->connect_ok("user=scram_role require_auth=!none",
+	"any authentication can be required: works with SCRAM auth");
+$node->connect_ok("user=scram_role require_auth=password,scram-sha-256,md5",
+	"multiple authentication types can be required: works with SCRAM auth");
+
+# ...fail for other auth types...
+$node->connect_fails("user=scram_role require_auth=password",
+	"password authentication can be required: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails("user=scram_role require_auth=none",
+	"all authentication can be forbidden: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+
+# ...and allow SCRAM authentication to be prohibited.
+$node->connect_fails("user=scram_role require_auth=!scram-sha-256",
+	"SCRAM authentication can be forbidden: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails("user=scram_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types can be forbidden: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+
 # Test that bad passwords are rejected.
 $ENV{"PGPASSWORD"} = 'badpass';
 test_conn($node, 'user=scram_role', 'scram-sha-256', 2,
@@ -153,6 +275,33 @@ test_conn($node, 'user=md5_role', 'md5', 0,
 	log_like =>
 	  [qr/connection authenticated: identity="md5_role" method=md5/]);
 
+# require_auth should succeed with MD5...
+$node->connect_ok("user=md5_role require_auth=md5",
+	"MD5 authentication can be required: works with MD5 auth");
+$node->connect_ok("user=md5_role require_auth=!none",
+	"any authentication can be required: works with MD5 auth");
+$node->connect_ok("user=md5_role require_auth=md5,scram-sha-256,password",
+	"multiple authentication types can be required: works with MD5 auth");
+
+# ...fail for other auth types...
+$node->connect_fails("user=md5_role require_auth=password",
+	"password authentication can be required: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+$node->connect_fails("user=md5_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+$node->connect_fails("user=md5_role require_auth=none",
+	"all authentication can be forbidden: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+
+# ...and allow MD5 authentication to be prohibited.
+$node->connect_fails("user=md5_role require_auth=!md5",
+	"password authentication can be forbidden: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+$node->connect_fails("user=md5_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types can be forbidden: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+
 # Test SYSTEM_USER <> NULL with parallel workers.
 $node->safe_psql(
 	'postgres',
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index a2bc8a5351..61aede12d1 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -318,6 +318,24 @@ test_query(
 	'gssencmode=require',
 	'sending 100K lines works');
 
+# require_auth=gss should succeed...
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=disable require_auth=gss",
+	"GSS authentication can be requested: works with GSS auth without encryption");
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss",
+	"GSS authentication can be requested: works with GSS auth with encryption");
+
+# ...and require_auth=sspi should fail.
+$node->connect_fails(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=disable require_auth=sspi",
+	"SSPI authentication can be requested: fails with GSS auth without encryption",
+	expected_stderr => qr/server requested GSSAPI authentication/);
+$node->connect_fails(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=sspi",
+	"SSPI authentication can be requested: fails with GSS auth with encryption",
+	expected_stderr => qr/server did not complete authentication/);
+
 # Test that SYSTEM_USER works.
 test_query($node, 'test1', 'SELECT SYSTEM_USER;',
 	qr/^gss:test1\@$realm$/s, 'gssencmode=require', 'testing system_user');
@@ -363,6 +381,14 @@ test_access(
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable',
 	'fails with GSS encryption disabled and hostgssenc hba');
 
+# require_auth=gss should succeed.
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss",
+	"GSS authentication can be requested: works with GSS encryption");
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss,scram-sha-256",
+	"multiple authentication types can be requested: works with GSS encryption");
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
 	qq{hostnogssenc all all $hostaddr/32 gss map=mymap});
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index fd90832b75..0319f2ee74 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -222,6 +222,12 @@ test_access(
 		qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/
 	],);
 
+# require_auth=password should complete successfully; other methods should fail.
+$node->connect_ok("user=test1 require_auth=password",
+	"password authentication can be required: works with ldap auth");
+$node->connect_fails("user=test1 require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with ldap auth");
+
 note "search+bind";
 
 unlink($node->data_dir . '/pg_hba.conf');
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index deaa4aa086..cf61bc7d0d 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -136,4 +136,29 @@ $node->connect_ok(
 		qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/
 	]);
 
+# channel_binding should continue to function independently of require_auth.
+$node->connect_ok("$common_connstr user=ssltestuser channel_binding=disable require_auth=scram-sha-256",
+	"SCRAM with SSL, channel_binding=disable, and require_auth=scram-sha-256");
+$node->connect_fails(
+	"$common_connstr user=md5testuser require_auth=md5 channel_binding=require",
+	"channel_binding can fail even when require_auth succeeds",
+	expected_stderr =>
+	  qr/channel binding required but not supported by server's authentication request/
+);
+if ($supports_tls_server_end_point)
+{
+	$node->connect_ok(
+		"$common_connstr user=ssltestuser channel_binding=require require_auth=scram-sha-256",
+		"SCRAM with SSL, channel_binding=require, and require_auth=scram-sha-256");
+}
+else
+{
+	$node->connect_fails(
+		"$common_connstr user=ssltestuser channel_binding=require require_auth=scram-sha-256",
+		"SCRAM with SSL, channel_binding=require, and require_auth=scram-sha-256",
+		expected_stderr =>
+		  qr/channel binding is required, but server did not offer an authentication method that supports channel binding/
+	);
+}
+
 done_testing();
-- 
2.25.1

v12-0002-Add-sslcertmode-option-for-client-certificates.patchtext/x-patch; charset=UTF-8; name=v12-0002-Add-sslcertmode-option-for-client-certificates.patchDownload
From db296eacef97e40a6471bb58419f36f44aab052b Mon Sep 17 00:00:00 2001
From: Jacob Champion <jchampion@timescale.com>
Date: Fri, 24 Jun 2022 15:40:42 -0700
Subject: [PATCH v12 2/3] Add sslcertmode option for client certificates

The sslcertmode option controls whether the server is allowed and/or
required to request a certificate from the client. There are three
modes:

- "allow" is the default and follows the current behavior -- a
  configured  sslcert is sent if the server requests one (which, with
  the current implementation, will happen whenever TLS is negotiated).

- "disable" causes the client to refuse to send a client certificate
  even if an sslcert is configured.

- "require" causes the client to fail if a client certificate is never
  sent and the server opens a connection anyway. This doesn't add any
  additional security, since there is no guarantee that the server is
  validating the certificate correctly, but it may help troubleshoot
  more complicated TLS setups.

sslcertmode=require needs the OpenSSL implementation to support
SSL_CTX_set_cert_cb(). Notably, LibreSSL does not.
---
 configure                                | 11 ++--
 configure.ac                             |  4 +-
 doc/src/sgml/libpq.sgml                  | 64 ++++++++++++++++++++++++
 meson.build                              |  3 +-
 src/include/pg_config.h.in               |  3 ++
 src/interfaces/libpq/fe-auth.c           | 21 ++++++++
 src/interfaces/libpq/fe-connect.c        | 55 ++++++++++++++++++++
 src/interfaces/libpq/fe-secure-openssl.c | 39 ++++++++++++++-
 src/interfaces/libpq/libpq-int.h         |  3 ++
 src/test/ssl/t/001_ssltests.pl           | 43 ++++++++++++++++
 src/tools/msvc/Solution.pm               |  8 +++
 11 files changed, 245 insertions(+), 9 deletions(-)

diff --git a/configure b/configure
index 3966368b8d..368588e98d 100755
--- a/configure
+++ b/configure
@@ -12992,13 +12992,14 @@ else
 fi
 
   fi
-  # Function introduced in OpenSSL 1.0.2.
-  for ac_func in X509_get_signature_nid
+  # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all of these.
+  for ac_func in X509_get_signature_nid SSL_CTX_set_cert_cb
 do :
-  ac_fn_c_check_func "$LINENO" "X509_get_signature_nid" "ac_cv_func_X509_get_signature_nid"
-if test "x$ac_cv_func_X509_get_signature_nid" = xyes; then :
+  as_ac_var=`$as_echo "ac_cv_func_$ac_func" | $as_tr_sh`
+ac_fn_c_check_func "$LINENO" "$ac_func" "$as_ac_var"
+if eval test \"x\$"$as_ac_var"\" = x"yes"; then :
   cat >>confdefs.h <<_ACEOF
-#define HAVE_X509_GET_SIGNATURE_NID 1
+#define `$as_echo "HAVE_$ac_func" | $as_tr_cpp` 1
 _ACEOF
 
 fi
diff --git a/configure.ac b/configure.ac
index f76b7ee31f..431594b921 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1354,8 +1354,8 @@ if test "$with_ssl" = openssl ; then
      AC_SEARCH_LIBS(CRYPTO_new_ex_data, [eay32 crypto], [], [AC_MSG_ERROR([library 'eay32' or 'crypto' is required for OpenSSL])])
      AC_SEARCH_LIBS(SSL_new, [ssleay32 ssl], [], [AC_MSG_ERROR([library 'ssleay32' or 'ssl' is required for OpenSSL])])
   fi
-  # Function introduced in OpenSSL 1.0.2.
-  AC_CHECK_FUNCS([X509_get_signature_nid])
+  # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all of these.
+  AC_CHECK_FUNCS([X509_get_signature_nid SSL_CTX_set_cert_cb])
   # Functions introduced in OpenSSL 1.1.0. We used to check for
   # OPENSSL_VERSION_NUMBER, but that didn't work with 1.1.0, because LibreSSL
   # defines OPENSSL_VERSION_NUMBER to claim version 2.0.0, even though it
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 05d9645f40..32c0872eed 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1810,6 +1810,60 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-sslcertmode" xreflabel="sslcertmode">
+      <term><literal>sslcertmode</literal></term>
+      <listitem>
+       <para>
+        This option determines whether a client certificate may be sent to the
+        server, and whether the server is required to request one. There are
+        three modes:
+
+        <variablelist>
+         <varlistentry>
+          <term><literal>disable</literal></term>
+          <listitem>
+           <para>
+            a client certificate is never sent, even if one is provided via
+            <xref linkend="libpq-connect-sslcert" />
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>allow</literal> (default)</term>
+          <listitem>
+           <para>
+            a certificate may be sent, if the server requests one and it has
+            been provided via <literal>sslcert</literal>
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>require</literal></term>
+          <listitem>
+           <para>
+            the server <emphasis>must</emphasis> request a certificate. The
+            connection will fail if the client does not send a certificate and
+            the server successfully authenticates the client anyway.
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+       </para>
+
+       <note>
+        <para>
+         <literal>sslcertmode=require</literal> doesn't add any additional
+         security, since there is no guarantee that the server is validating the
+         certificate correctly; PostgreSQL servers generally request TLS
+         certificates from clients whether they validate them or not. The option
+         may be useful when troubleshooting more complicated TLS setups.
+        </para>
+       </note>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-sslrootcert" xreflabel="sslrootcert">
       <term><literal>sslrootcert</literal></term>
       <listitem>
@@ -7985,6 +8039,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGSSLCERTMODE</envar></primary>
+      </indexterm>
+      <envar>PGSSLCERTMODE</envar> behaves the same as the <xref
+      linkend="libpq-connect-sslcertmode"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/meson.build b/meson.build
index ce2f223a40..df1cfe111f 100644
--- a/meson.build
+++ b/meson.build
@@ -1181,8 +1181,9 @@ if get_option('ssl') == 'openssl'
     ['CRYPTO_new_ex_data', {'required': true}],
     ['SSL_new', {'required': true}],
 
-    # Function introduced in OpenSSL 1.0.2.
+    # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all of these.
     ['X509_get_signature_nid'],
+    ['SSL_CTX_set_cert_cb'],
 
     # Functions introduced in OpenSSL 1.1.0. We used to check for
     # OPENSSL_VERSION_NUMBER, but that didn't work with 1.1.0, because LibreSSL
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index c5a80b829e..08382e868b 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -397,6 +397,9 @@
 /* Define to 1 if you have spinlocks. */
 #undef HAVE_SPINLOCKS
 
+/* Define to 1 if you have the `SSL_CTX_set_cert_cb' function. */
+#undef HAVE_SSL_CTX_SET_CERT_CB
+
 /* Define to 1 if stdbool.h conforms to C99. */
 #undef HAVE_STDBOOL_H
 
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 5a46cf0ee9..295b978525 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -885,6 +885,27 @@ check_expected_areq(AuthRequest areq, PGconn *conn)
 	bool		result = true;
 	const char *reason = NULL;
 
+	if (conn->sslcertmode[0] == 'r' /* require */
+		&& areq == AUTH_REQ_OK)
+	{
+		/*
+		 * Trade off a little bit of complexity to try to get these error
+		 * messages as precise as possible.
+		 */
+		if (!conn->ssl_cert_requested)
+		{
+			appendPQExpBufferStr(&conn->errorMessage,
+								 libpq_gettext("server did not request a certificate"));
+			return false;
+		}
+		else if (!conn->ssl_cert_sent)
+		{
+			appendPQExpBufferStr(&conn->errorMessage,
+								 libpq_gettext("server accepted connection without a valid certificate"));
+			return false;
+		}
+	}
+
 	/*
 	 * If the user required a specific auth method, or specified an allowed set,
 	 * then reject all others here, and make sure the server actually completes
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 40f571fadb..eaca8cee1f 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -125,8 +125,10 @@ static int	ldapServiceLookup(const char *purl, PQconninfoOption *options,
 #define DefaultTargetSessionAttrs	"any"
 #ifdef USE_SSL
 #define DefaultSSLMode "prefer"
+#define DefaultSSLCertMode "allow"
 #else
 #define DefaultSSLMode	"disable"
+#define DefaultSSLCertMode "disable"
 #endif
 #ifdef ENABLE_GSS
 #include "fe-gssapi-common.h"
@@ -283,6 +285,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"SSL-Client-Key", "", 64,
 	offsetof(struct pg_conn, sslkey)},
 
+	{"sslcertmode", "PGSSLCERTMODE", NULL, NULL,
+		"SSL-Client-Cert-Mode", "", 8, /* sizeof("disable") == 8 */
+	offsetof(struct pg_conn, sslcertmode)},
+
 	{"sslpassword", NULL, NULL, NULL,
 		"SSL-Client-Key-Password", "*", 20,
 	offsetof(struct pg_conn, sslpassword)},
@@ -1514,6 +1520,55 @@ duplicate:
 		return false;
 	}
 
+	/*
+	 * validate sslcertmode option
+	 */
+	if (conn->sslcertmode)
+	{
+		if (strcmp(conn->sslcertmode, "disable") != 0 &&
+			strcmp(conn->sslcertmode, "allow") != 0 &&
+			strcmp(conn->sslcertmode, "require") != 0)
+		{
+			conn->status = CONNECTION_BAD;
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("invalid %s value: \"%s\"\n"),
+							  "sslcertmode",
+							  conn->sslcertmode);
+			return false;
+		}
+#ifndef USE_SSL
+		if (strcmp(conn->sslcertmode, "require") == 0)
+		{
+			conn->status = CONNECTION_BAD;
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("sslcertmode value \"%s\" invalid when SSL support is not compiled in\n"),
+							  conn->sslcertmode);
+			return false;
+		}
+#endif
+#ifndef HAVE_SSL_CTX_SET_CERT_CB
+		/*
+		 * Without a certificate callback, the current implementation can't
+		 * figure out if a certficate was actually requested, so "require" is
+		 * useless.
+		 */
+		if (strcmp(conn->sslcertmode, "require") == 0)
+		{
+			conn->status = CONNECTION_BAD;
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("sslcertmode value \"%s\" is not supported (check OpenSSL version)\n"),
+							  conn->sslcertmode);
+			return false;
+		}
+#endif
+	}
+	else
+	{
+		conn->sslcertmode = strdup(DefaultSSLCertMode);
+		if (!conn->sslcertmode)
+			goto oom_error;
+	}
+
 	/*
 	 * validate gssencmode option
 	 */
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index b42a908733..241a28a32d 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -475,6 +475,33 @@ verify_cb(int ok, X509_STORE_CTX *ctx)
 	return ok;
 }
 
+#ifdef HAVE_SSL_CTX_SET_CERT_CB
+/*
+ * Certificate selection callback
+ *
+ * This callback lets us choose the client certificate we send to the server
+ * after seeing its CertificateRequest. We only support sending a single
+ * hard-coded certificate via sslcert, so we don't actually set any certificates
+ * here; we just use it to record whether or not the server has actually asked
+ * for one and whether we have one to send.
+ */
+static int
+cert_cb(SSL *ssl, void *arg)
+{
+	PGconn *conn = arg;
+	conn->ssl_cert_requested = true;
+
+	/* Do we have a certificate loaded to send back? */
+	if (SSL_get_certificate(ssl))
+		conn->ssl_cert_sent = true;
+
+	/*
+	 * Tell OpenSSL that the callback succeeded; we're not required to actually
+	 * make any changes to the SSL handle.
+	 */
+	return 1;
+}
+#endif
 
 /*
  * OpenSSL-specific wrapper around
@@ -970,6 +997,11 @@ initialize_SSL(PGconn *conn)
 		SSL_CTX_set_default_passwd_cb_userdata(SSL_context, conn);
 	}
 
+#ifdef HAVE_SSL_CTX_SET_CERT_CB
+	/* Set up a certificate selection callback. */
+	SSL_CTX_set_cert_cb(SSL_context, cert_cb, conn);
+#endif
+
 	/* Disable old protocol versions */
 	SSL_CTX_set_options(SSL_context, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
 
@@ -1133,7 +1165,12 @@ initialize_SSL(PGconn *conn)
 	else
 		fnbuf[0] = '\0';
 
-	if (fnbuf[0] == '\0')
+	if (conn->sslcertmode[0] == 'd') /* disable */
+	{
+		/* don't send a client cert even if we have one */
+		have_cert = false;
+	}
+	else if (fnbuf[0] == '\0')
 	{
 		/* no home directory, proceed without a client cert */
 		have_cert = false;
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index e4d88e22a1..ad6f8b78c6 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -384,6 +384,7 @@ struct pg_conn
 	char	   *sslkey;			/* client key filename */
 	char	   *sslcert;		/* client certificate filename */
 	char	   *sslpassword;	/* client key file password */
+	char	   *sslcertmode;	/* client cert mode (require,allow,disable) */
 	char	   *sslrootcert;	/* root certificate filename */
 	char	   *sslcrl;			/* certificate revocation list filename */
 	char	   *sslcrldir;		/* certificate revocation list directory name */
@@ -525,6 +526,8 @@ struct pg_conn
 
 	/* SSL structures */
 	bool		ssl_in_use;
+	bool		ssl_cert_requested;	/* Did the server ask us for a cert? */
+	bool		ssl_cert_sent;		/* Did we send one in reply? */
 
 #ifdef USE_SSL
 	bool		allow_ssl_try;	/* Allowed to try SSL negotiation */
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index fe42161a0f..03f0a3eaf4 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -42,6 +42,10 @@ my $SERVERHOSTADDR = '127.0.0.1';
 # This is the pattern to use in pg_hba.conf to match incoming connections.
 my $SERVERHOSTCIDR = '127.0.0.1/32';
 
+# Determine whether build supports sslcertmode=require.
+my $supports_sslcertmode_require =
+  check_pg_config("#define HAVE_SSL_CTX_SET_CERT_CB 1");
+
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
 
@@ -191,6 +195,22 @@ $node->connect_ok(
 	"$common_connstr sslrootcert=ssl/both-cas-2.crt sslmode=verify-ca",
 	"cert root file that contains two certificates, order 2");
 
+# sslcertmode=allow and =disable should both work without a client certificate.
+$node->connect_ok(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=disable",
+	"connect with sslcertmode=disable");
+$node->connect_ok(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=allow",
+	"connect with sslcertmode=allow");
+
+# sslcertmode=require, however, should fail.
+$node->connect_fails(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=require",
+	"connect with sslcertmode=require fails without a client certificate",
+	expected_stderr => $supports_sslcertmode_require
+		? qr/server accepted connection without a valid certificate/
+		: qr/sslcertmode value "require" is not supported/);
+
 # CRL tests
 
 # Invalid CRL filename is the same as no CRL, succeeds
@@ -538,6 +558,29 @@ $node->connect_ok(
 	"certificate authorization succeeds with correct client cert in encrypted DER format"
 );
 
+# correct client cert with required/allowed certificate authentication
+if ($supports_sslcertmode_require)
+{
+	$node->connect_ok(
+		"$common_connstr user=ssltestuser sslcertmode=require sslcert=ssl/client.crt "
+		  . sslkey('client.key'),
+		"certificate authorization succeeds with sslcertmode=require"
+	);
+}
+$node->connect_ok(
+	"$common_connstr user=ssltestuser sslcertmode=allow sslcert=ssl/client.crt "
+	  . sslkey('client.key'),
+	"certificate authorization succeeds with sslcertmode=allow"
+);
+
+# client cert isn't sent if certificate authentication is disabled
+$node->connect_fails(
+	"$common_connstr user=ssltestuser sslcertmode=disable sslcert=ssl/client.crt "
+	  . sslkey('client.key'),
+	"certificate authorization fails with sslcertmode=disable",
+	expected_stderr => qr/connection requires a valid client certificate/
+);
+
 # correct client cert in encrypted PEM with wrong password
 $node->connect_fails(
 	"$common_connstr user=ssltestuser sslcert=ssl/client.crt "
diff --git a/src/tools/msvc/Solution.pm b/src/tools/msvc/Solution.pm
index c2acb58df0..ff6d6f9e35 100644
--- a/src/tools/msvc/Solution.pm
+++ b/src/tools/msvc/Solution.pm
@@ -328,6 +328,7 @@ sub GenerateFiles
 		HAVE_SETPROCTITLE_FAST                   => undef,
 		HAVE_SOCKLEN_T                           => 1,
 		HAVE_SPINLOCKS                           => 1,
+		HAVE_SSL_CTX_SET_CERT_CB                 => undef,
 		HAVE_STDBOOL_H                           => 1,
 		HAVE_STDINT_H                            => 1,
 		HAVE_STDLIB_H                            => 1,
@@ -489,6 +490,13 @@ sub GenerateFiles
 
 		my ($digit1, $digit2, $digit3) = $self->GetOpenSSLVersion();
 
+		if (   ($digit1 >= '3' && $digit2 >= '0' && $digit3 >= '0')
+			|| ($digit1 >= '1' && $digit2 >= '1' && $digit3 >= '0')
+			|| ($digit1 >= '1' && $digit2 >= '0' && $digit3 >= '2'))
+		{
+			$define{HAVE_SSL_CTX_SET_CERT_CB} = 1;
+		}
+
 		# More symbols are needed with OpenSSL 1.1.0 and above.
 		if (   ($digit1 >= '3' && $digit2 >= '0' && $digit3 >= '0')
 			|| ($digit1 >= '1' && $digit2 >= '1' && $digit3 >= '0'))
-- 
2.25.1

v12-0003-require_auth-decouple-SASL-and-SCRAM.patchtext/x-patch; charset=UTF-8; name=v12-0003-require_auth-decouple-SASL-and-SCRAM.patchDownload
From 858d96736dfebba7ceaf0e5821e2c222b60c6c8f Mon Sep 17 00:00:00 2001
From: Jacob Champion <jchampion@timescale.com>
Date: Tue, 18 Oct 2022 16:55:36 -0700
Subject: [PATCH v12 3/3] require_auth: decouple SASL and SCRAM

Rather than assume that an AUTH_REQ_SASL* code refers to SCRAM-SHA-256,
future-proof by separating the single allowlist into a list of allowed
authentication request codes and a list of allowed SASL mechanisms.

The require_auth check is now separated into two tiers. The
AUTH_REQ_SASL* codes are always allowed. If the server sends one,
responsibility for the check then falls to pg_SASL_init(), which
compares the selected mechanism against the list of allowed mechanisms.
(Other SCRAM code is already responsible for rejecting unexpected or
out-of-order AUTH_REQ_SASL_* codes, so that's not explicitly handled
with this addition.)

Since there's only one recognized SASL mechanism, conn->sasl_mechs
currently only points at static hardcoded lists. Whenever a second
mechanism is added, the list will need to be managed dynamically.
---
 src/interfaces/libpq/fe-auth.c            | 35 +++++++++++++++++++
 src/interfaces/libpq/fe-connect.c         | 42 +++++++++++++++++++----
 src/interfaces/libpq/libpq-int.h          |  2 ++
 src/test/authentication/t/001_password.pl | 10 +++---
 src/test/ssl/t/002_scram.pl               |  6 ++++
 5 files changed, 84 insertions(+), 11 deletions(-)

diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 295b978525..24c31ae624 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -536,6 +536,41 @@ pg_SASL_init(PGconn *conn, int payloadlen)
 		goto error;
 	}
 
+	/*
+	 * Before going ahead with any SASL exchange, ensure that the user has
+	 * allowed (or, alternatively, has not forbidden) this particular mechanism.
+	 *
+	 * In a hypothetical future where a server responds with multiple SASL
+	 * mechanism families, we would need to instead consult this list up above,
+	 * during mechanism negotiation. We don't live in that world yet. The server
+	 * presents one auth method and we decide whether that's acceptable or not.
+	 */
+	if (conn->sasl_mechs)
+	{
+		const char **mech;
+		bool		found = false;
+
+		Assert(conn->require_auth);
+
+		for (mech = conn->sasl_mechs; *mech; mech++)
+		{
+			if (strcmp(*mech, selected_mechanism) == 0)
+			{
+				found = true;
+				break;
+			}
+		}
+
+		if ((conn->sasl_mechs_denied && found)
+			|| (!conn->sasl_mechs_denied && !found))
+		{
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("auth method \"%s\" requirement failed: server requested unacceptable SASL mechanism \"%s\"\n"),
+							  conn->require_auth, selected_mechanism);
+			goto error;
+		}
+	}
+
 	if (conn->channel_binding[0] == 'r' &&	/* require */
 		strcmp(selected_mechanism, SCRAM_SHA_256_PLUS_NAME) != 0)
 	{
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index eaca8cee1f..be002b4fda 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -1259,11 +1259,25 @@ connectOptions2(PGconn *conn)
 		bool		first, more;
 		bool		negated = false;
 
+		static const uint32 default_methods = (
+			1 << AUTH_REQ_SASL
+			| 1 << AUTH_REQ_SASL_CONT
+			| 1 << AUTH_REQ_SASL_FIN
+		);
+		static const char *no_mechs[] = { NULL };
+
 		/*
-		 * By default, start from an empty set of allowed options and add to it.
+		 * By default, start from a minimum set of allowed options and add to
+		 * it.
+		 *
+		 * NB: The SASL method codes are always "allowed" here. If the server
+		 * requests SASL auth, pg_SASL_init() will enforce adherence to the
+		 * sasl_mechs list, which by default is empty.
 		 */
 		conn->auth_required = true;
-		conn->allowed_auth_methods = 0;
+		conn->allowed_auth_methods = default_methods;
+		conn->sasl_mechs = no_mechs;
+		conn->sasl_mechs_denied = false;
 
 		for (first = true, more = true; more; first = false)
 		{
@@ -1289,6 +1303,9 @@ connectOptions2(PGconn *conn)
 					 */
 					conn->auth_required = false;
 					conn->allowed_auth_methods = -1;
+
+					/* conn->sasl_mechs is now a list of denied mechanisms. */
+					conn->sasl_mechs_denied = true;
 				}
 				else if (!negated)
 				{
@@ -1335,10 +1352,23 @@ connectOptions2(PGconn *conn)
 			}
 			else if (strcmp(method, "scram-sha-256") == 0)
 			{
-				/* This currently assumes that SCRAM is the only SASL method. */
-				bits = (1 << AUTH_REQ_SASL);
-				bits |= (1 << AUTH_REQ_SASL_CONT);
-				bits |= (1 << AUTH_REQ_SASL_FIN);
+				static const char *scram_mechs[] = {
+					SCRAM_SHA_256_NAME,
+					SCRAM_SHA_256_PLUS_NAME,
+					NULL /* list terminator */
+				};
+
+				/*
+				 * This currently assumes that SCRAM is the only SASL method.
+				 * Once a second mechanism is added, this code will need to add
+				 * to the list instead of replacing it wholesale.
+				 */
+				if (conn->sasl_mechs[0])
+					goto duplicate;
+				conn->sasl_mechs = scram_mechs;
+
+				free(part);
+				continue; /* avoid the bitmask manipulation below */
 			}
 			else if (strcmp(method, "none") == 0)
 			{
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index ad6f8b78c6..6fdba960da 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -461,6 +461,8 @@ struct pg_conn
 
 	bool		auth_required;	/* require an authentication challenge from the server? */
 	uint32		allowed_auth_methods;	/* bitmask of acceptable AuthRequest codes */
+	const char **sasl_mechs;	/* list of allowed/denied SASL mechanisms */
+	bool		sasl_mechs_denied;	/* is the sasl_mechs list forbidden? */
 
 	/* Transient state needed while establishing connection */
 	PGTargetServerType target_server_type;	/* desired session properties */
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 01f256bbbc..9ef4088dce 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -243,21 +243,21 @@ $node->connect_ok("user=scram_role require_auth=password,scram-sha-256,md5",
 # ...fail for other auth types...
 $node->connect_fails("user=scram_role require_auth=password",
 	"password authentication can be required: fails with SCRAM auth",
-	expected_stderr => qr/server requested SASL authentication/);
+	expected_stderr => qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256"/);
 $node->connect_fails("user=scram_role require_auth=md5",
 	"md5 authentication can be required: fails with SCRAM auth",
-	expected_stderr => qr/server requested SASL authentication/);
+	expected_stderr => qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256"/);
 $node->connect_fails("user=scram_role require_auth=none",
 	"all authentication can be forbidden: fails with SCRAM auth",
-	expected_stderr => qr/server requested SASL authentication/);
+	expected_stderr => qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256"/);
 
 # ...and allow SCRAM authentication to be prohibited.
 $node->connect_fails("user=scram_role require_auth=!scram-sha-256",
 	"SCRAM authentication can be forbidden: fails with SCRAM auth",
-	expected_stderr => qr/server requested SASL authentication/);
+	expected_stderr => qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256"/);
 $node->connect_fails("user=scram_role require_auth=!password,!md5,!scram-sha-256",
 	"multiple authentication types can be forbidden: fails with SCRAM auth",
-	expected_stderr => qr/server requested SASL authentication/);
+	expected_stderr => qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256"/);
 
 # Test that bad passwords are rejected.
 $ENV{"PGPASSWORD"} = 'badpass';
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index cf61bc7d0d..472b2efdf1 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -150,6 +150,12 @@ if ($supports_tls_server_end_point)
 	$node->connect_ok(
 		"$common_connstr user=ssltestuser channel_binding=require require_auth=scram-sha-256",
 		"SCRAM with SSL, channel_binding=require, and require_auth=scram-sha-256");
+	$node->connect_fails(
+		"$common_connstr user=ssltestuser channel_binding=require require_auth=password",
+		"SCRAM with SSL, channel_binding=require, and require_auth=password",
+		expected_stderr =>
+		  qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256-PLUS"/
+	);
 }
 else
 {
-- 
2.25.1

#38Aleksander Alekseev
aleksander@timescale.com
In reply to: Jacob Champion (#37)
Re: [PoC] Let libpq reject unexpected authentication requests

Hi Jacob,

Assigning a negative number to uint32 doesn't necessarily work on all
platforms. I suggest using PG_UINT32_MAX.

Hmm -- on which platforms is "-1 converted to unsigned" not equivalent
to the maximum value? Are they C-compliant?

I did a little more research and I think you are right. What happens
according to the C standard:

"""
the value is converted to unsigned by adding to it one greater than the largest
number that can be represented in the unsigned integer type
"""

so this is effectively -1 + (PG_UINT32_MAX + 1).

--
Best regards,
Aleksander Alekseev

#39Jacob Champion
jchampion@timescale.com
In reply to: Aleksander Alekseev (#38)
Re: [PoC] Let libpq reject unexpected authentication requests

On 11/11/22 22:57, Aleksander Alekseev wrote:

I did a little more research and I think you are right. What happens
according to the C standard:

Thanks for confirming! (I personally prefer -1 to a *MAX macro, because
it works regardless of the length of the type.)

--Jacob

#40Michael Paquier
michael@paquier.xyz
In reply to: Jacob Champion (#35)
Re: [PoC] Let libpq reject unexpected authentication requests

On Thu, Oct 20, 2022 at 11:36:34AM -0700, Jacob Champion wrote:

Maybe I should just add a basic Assert here, to trip if someone adds a
new SASL mechanism, and point that lucky person to this thread with a
comment?

I am beginning to look at the last version proposed, which has been
marked as RfC. Does this patch need a refresh in light of a9e9a9f and
0873b2d? The changes for libpq_append_conn_error() should be
straight-forward.

The CF bot is still happy.
--
Michael

#41Jacob Champion
jchampion@timescale.com
In reply to: Michael Paquier (#40)
4 attachment(s)
Re: [PoC] Let libpq reject unexpected authentication requests

On Tue, Nov 15, 2022 at 11:07 PM Michael Paquier <michael@paquier.xyz> wrote:

I am beginning to look at the last version proposed, which has been
marked as RfC. Does this patch need a refresh in light of a9e9a9f and
0873b2d? The changes for libpq_append_conn_error() should be
straight-forward.

Updated in v13, thanks!

--Jacob

Attachments:

since-v12.diff.txttext/plain; charset=US-ASCII; name=since-v12.diff.txtDownload
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 85e3b9d913..52b1ba927e 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -550,9 +550,8 @@ pg_SASL_init(PGconn *conn, int payloadlen)
 		if ((conn->sasl_mechs_denied && found)
 			|| (!conn->sasl_mechs_denied && !found))
 		{
-			appendPQExpBuffer(&conn->errorMessage,
-							  libpq_gettext("auth method \"%s\" requirement failed: server requested unacceptable SASL mechanism \"%s\"\n"),
-							  conn->require_auth, selected_mechanism);
+			libpq_append_conn_error(conn, "auth method \"%s\" requirement failed: server requested unacceptable SASL mechanism \"%s\"",
+									conn->require_auth, selected_mechanism);
 			goto error;
 		}
 	}
@@ -904,14 +903,12 @@ check_expected_areq(AuthRequest areq, PGconn *conn)
 		 */
 		if (!conn->ssl_cert_requested)
 		{
-			appendPQExpBufferStr(&conn->errorMessage,
-								 libpq_gettext("server did not request a certificate"));
+			libpq_append_conn_error(conn, "server did not request a certificate");
 			return false;
 		}
 		else if (!conn->ssl_cert_sent)
 		{
-			appendPQExpBufferStr(&conn->errorMessage,
-								 libpq_gettext("server accepted connection without a valid certificate"));
+			libpq_append_conn_error(conn, "server accepted connection without a valid certificate");
 			return false;
 		}
 	}
@@ -997,10 +994,8 @@ check_expected_areq(AuthRequest areq, PGconn *conn)
 		if (!reason)
 			reason = auth_description(areq);
 
-		appendPQExpBuffer(&conn->errorMessage,
-						  libpq_gettext("auth method \"%s\" requirement failed: %s\n"),
-						  conn->require_auth, reason);
-
+		libpq_append_conn_error(conn, "auth method \"%s\" requirement failed: %s",
+								conn->require_auth, reason);
 		return result;
 	}
 
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index f40af2a4f9..46fc8e4940 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -1307,9 +1307,8 @@ connectOptions2(PGconn *conn)
 				else if (!negated)
 				{
 					conn->status = CONNECTION_BAD;
-					appendPQExpBuffer(&conn->errorMessage,
-									  libpq_gettext("negative require_auth method \"%s\" cannot be mixed with non-negative methods"),
-									  method);
+					libpq_append_conn_error(conn, "negative require_auth method \"%s\" cannot be mixed with non-negative methods",
+											method);
 
 					free(part);
 					return false;
@@ -1321,9 +1320,8 @@ connectOptions2(PGconn *conn)
 			else if (negated)
 			{
 				conn->status = CONNECTION_BAD;
-				appendPQExpBuffer(&conn->errorMessage,
-								  libpq_gettext("require_auth method \"%s\" cannot be mixed with negative methods"),
-								  method);
+				libpq_append_conn_error(conn, "require_auth method \"%s\" cannot be mixed with negative methods",
+										method);
 
 				free(part);
 				return false;
@@ -1395,9 +1393,8 @@ connectOptions2(PGconn *conn)
 			else
 			{
 				conn->status = CONNECTION_BAD;
-				appendPQExpBuffer(&conn->errorMessage,
-								  libpq_gettext("invalid require_auth method: \"%s\""),
-								  method);
+				libpq_append_conn_error(conn, "invalid require_auth method: \"%s\"",
+										method);
 
 				free(part);
 				return false;
@@ -1428,9 +1425,8 @@ duplicate:
 			 * typos are extremely risky.
 			 */
 			conn->status = CONNECTION_BAD;
-			appendPQExpBuffer(&conn->errorMessage,
-							  libpq_gettext("require_auth method \"%s\" is specified more than once"),
-							  part);
+			libpq_append_conn_error(conn, "require_auth method \"%s\" is specified more than once",
+									part);
 
 			free(part);
 			return false;
@@ -1551,19 +1547,16 @@ duplicate:
 			strcmp(conn->sslcertmode, "require") != 0)
 		{
 			conn->status = CONNECTION_BAD;
-			appendPQExpBuffer(&conn->errorMessage,
-							  libpq_gettext("invalid %s value: \"%s\"\n"),
-							  "sslcertmode",
-							  conn->sslcertmode);
+			libpq_append_conn_error(conn, "invalid %s value: \"%s\"",
+									"sslcertmode", conn->sslcertmode);
 			return false;
 		}
 #ifndef USE_SSL
 		if (strcmp(conn->sslcertmode, "require") == 0)
 		{
 			conn->status = CONNECTION_BAD;
-			appendPQExpBuffer(&conn->errorMessage,
-							  libpq_gettext("sslcertmode value \"%s\" invalid when SSL support is not compiled in\n"),
-							  conn->sslcertmode);
+			libpq_append_conn_error(conn, "sslcertmode value \"%s\" invalid when SSL support is not compiled in",
+									conn->sslcertmode);
 			return false;
 		}
 #endif
@@ -1576,9 +1569,8 @@ duplicate:
 		if (strcmp(conn->sslcertmode, "require") == 0)
 		{
 			conn->status = CONNECTION_BAD;
-			appendPQExpBuffer(&conn->errorMessage,
-							  libpq_gettext("sslcertmode value \"%s\" is not supported (check OpenSSL version)\n"),
-							  conn->sslcertmode);
+			libpq_append_conn_error(conn, "sslcertmode value \"%s\" is not supported (check OpenSSL version)",
+									conn->sslcertmode);
 			return false;
 		}
 #endif
v13-0001-libpq-let-client-reject-unexpected-auth-methods.patchtext/x-patch; charset=US-ASCII; name=v13-0001-libpq-let-client-reject-unexpected-auth-methods.patchDownload
From 542d330310633b7d51ce1d6a92bb72e70f1ebc48 Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Mon, 28 Feb 2022 09:40:43 -0800
Subject: [PATCH v13 1/3] libpq: let client reject unexpected auth methods

The require_auth connection option allows the client to choose a list of
acceptable authentication types for use with the server. There is no
negotiation: if the server does not present one of the allowed
authentication requests, the connection fails. Additionally, all methods
in the list may be negated, e.g. '!password', in which case the server
must NOT use the listed authentication type. The special method "none"
allows/disallows the use of unauthenticated connections (but it does not
govern transport-level authentication via TLS or GSSAPI).

Internally, the patch expands the role of check_expected_areq() to
ensure that the incoming request is compatible with conn->require_auth.
It also introduces a new flag, conn->client_finished_auth, which is set
by various authentication routines when the client side of the handshake
is finished. This signals to check_expected_areq() that an OK message
from the server is expected, and allows the client to complain if the
server forgoes authentication entirely.

(Since the client can't generally prove that the server is actually
doing the work of authentication, this last part is mostly useful for
SCRAM without channel binding. It could also provide a client with a
decent signal that, at the very least, it's not connecting to a database
with trust auth, and so the connection can be tied to the client in a
later audit.)

Deficiencies:
- This is unlikely to be very forwards-compatible at the moment,
  especially with SASL/SCRAM.
- SSPI support is "implemented" but untested.
- require_auth=none,scram-sha-256 currently allows the server to leave a
  SCRAM exchange unfinished. This is not net-new behavior but may be
  surprising.
---
 doc/src/sgml/libpq.sgml                   | 105 ++++++++++++++
 src/include/libpq/pqcomm.h                |   1 +
 src/interfaces/libpq/fe-auth-scram.c      |   1 +
 src/interfaces/libpq/fe-auth.c            | 134 ++++++++++++++++++
 src/interfaces/libpq/fe-connect.c         | 160 ++++++++++++++++++++++
 src/interfaces/libpq/libpq-int.h          |   7 +
 src/test/authentication/t/001_password.pl | 149 ++++++++++++++++++++
 src/test/kerberos/t/001_auth.pl           |  26 ++++
 src/test/ldap/t/001_auth.pl               |   6 +
 src/test/ssl/t/002_scram.pl               |  25 ++++
 10 files changed, 614 insertions(+)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 3c9bd3d673..05d9645f40 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1220,6 +1220,101 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-require-auth" xreflabel="require_auth">
+      <term><literal>require_auth</literal></term>
+      <listitem>
+      <para>
+        Specifies the authentication method that the client requires from the
+        server. If the server does not use the required method to authenticate
+        the client, or if the authentication handshake is not fully completed by
+        the server, the connection will fail. A comma-separated list of methods
+        may also be provided, of which the server must use exactly one in order
+        for the connection to succeed. By default, any authentication method is
+        accepted, and the server is free to skip authentication altogether.
+      </para>
+      <para>
+        Methods may be negated with the addition of a <literal>!</literal>
+        prefix, in which case the server must <emphasis>not</emphasis> attempt
+        the listed method; any other method is accepted, and the server is free
+        not to authenticate the client at all. If a comma-separated list is
+        provided, the server may not attempt <emphasis>any</emphasis> of the
+        listed negated methods. Negated and non-negated forms may not be
+        combined in the same setting.
+      </para>
+      <para>
+        As a final special case, the <literal>none</literal> method requires the
+        server not to use an authentication challenge. (It may also be negated,
+        to require some form of authentication.)
+      </para>
+      <para>
+        The following methods may be specified:
+
+        <variablelist>
+         <varlistentry>
+          <term><literal>password</literal></term>
+          <listitem>
+           <para>
+            The server must request plaintext password authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>md5</literal></term>
+          <listitem>
+           <para>
+            The server must request MD5 hashed password authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>gss</literal></term>
+          <listitem>
+           <para>
+            The server must either request a Kerberos handshake via
+            <acronym>GSSAPI</acronym> or establish a
+            <acronym>GSS</acronym>-encrypted channel (see also
+            <xref linkend="libpq-connect-gssencmode" />).
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>sspi</literal></term>
+          <listitem>
+           <para>
+            The server must request Windows <acronym>SSPI</acronym>
+            authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>scram-sha-256</literal></term>
+          <listitem>
+           <para>
+            The server must successfully complete a SCRAM-SHA-256 authentication
+            exchange with the client.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>none</literal></term>
+          <listitem>
+           <para>
+            The server must not prompt the client for an authentication
+            exchange. (This does not prohibit client certificate authentication
+            via TLS, nor GSS authentication via its encrypted transport.)
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+      </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-channel-binding" xreflabel="channel_binding">
       <term><literal>channel_binding</literal></term>
       <listitem>
@@ -7773,6 +7868,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGREQUIREAUTH</envar></primary>
+      </indexterm>
+      <envar>PGREQUIREAUTH</envar> behaves the same as the <xref
+      linkend="libpq-connect-require-auth"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h
index fcf68df39b..912451c913 100644
--- a/src/include/libpq/pqcomm.h
+++ b/src/include/libpq/pqcomm.h
@@ -123,6 +123,7 @@ extern PGDLLIMPORT bool Db_user_namespace;
 #define AUTH_REQ_SASL	   10	/* Begin SASL authentication */
 #define AUTH_REQ_SASL_CONT 11	/* Continue SASL authentication */
 #define AUTH_REQ_SASL_FIN  12	/* Final SASL message */
+#define AUTH_REQ_MAX	   AUTH_REQ_SASL_FIN	/* maximum AUTH_REQ_* value */
 
 typedef uint32 AuthRequest;
 
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index e71626580a..85a01b44a1 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -276,6 +276,7 @@ scram_exchange(void *opaq, char *input, int inputlen,
 			}
 			*done = true;
 			state->state = FE_SCRAM_FINISHED;
+			state->conn->client_finished_auth = true;
 			break;
 
 		default:
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 4a6c358bb6..6d9ec468db 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -136,7 +136,10 @@ pg_GSS_continue(PGconn *conn, int payloadlen)
 	}
 
 	if (maj_stat == GSS_S_COMPLETE)
+	{
+		conn->client_finished_auth = true;
 		gss_release_name(&lmin_s, &conn->gtarg_nam);
+	}
 
 	return STATUS_OK;
 }
@@ -321,6 +324,9 @@ pg_SSPI_continue(PGconn *conn, int payloadlen)
 		FreeContextBuffer(outbuf.pBuffers[0].pvBuffer);
 	}
 
+	if (r == SEC_E_OK)
+		conn->client_finished_auth = true;
+
 	/* Cleanup is handled by the code in freePGconn() */
 	return STATUS_OK;
 }
@@ -805,6 +811,44 @@ pg_password_sendauth(PGconn *conn, const char *password, AuthRequest areq)
 	return ret;
 }
 
+/*
+ * Translate a disallowed AuthRequest code into an error message.
+ */
+static const char *
+auth_description(AuthRequest areq)
+{
+	switch (areq)
+	{
+		case AUTH_REQ_PASSWORD:
+			return libpq_gettext("server requested a cleartext password");
+		case AUTH_REQ_MD5:
+			return libpq_gettext("server requested a hashed password");
+		case AUTH_REQ_GSS:
+		case AUTH_REQ_GSS_CONT:
+			return libpq_gettext("server requested GSSAPI authentication");
+		case AUTH_REQ_SSPI:
+			return libpq_gettext("server requested SSPI authentication");
+		case AUTH_REQ_SCM_CREDS:
+			return libpq_gettext("server requested UNIX socket credentials");
+		case AUTH_REQ_SASL:
+		case AUTH_REQ_SASL_CONT:
+		case AUTH_REQ_SASL_FIN:
+			return libpq_gettext("server requested SASL authentication");
+	}
+
+	return libpq_gettext("server requested an unknown authentication type");
+}
+
+/*
+ * Convenience macro for checking the allowed_auth_methods bitmask. Caller must
+ * ensure that type is not greater than 31 (high bit of the bitmask).
+ */
+#define auth_allowed(conn, type) \
+	(((conn)->allowed_auth_methods & (1 << (type))) != 0)
+
+StaticAssertDecl(AUTH_REQ_MAX < CHAR_BIT * sizeof(((PGconn){0}).allowed_auth_methods),
+				 "AUTH_REQ_MAX overflows the allowed_auth_methods bitmask");
+
 /*
  * Verify that the authentication request is expected, given the connection
  * parameters. This is especially important when the client wishes to
@@ -814,6 +858,93 @@ static bool
 check_expected_areq(AuthRequest areq, PGconn *conn)
 {
 	bool		result = true;
+	const char *reason = NULL;
+
+	/*
+	 * If the user required a specific auth method, or specified an allowed set,
+	 * then reject all others here, and make sure the server actually completes
+	 * an authentication exchange.
+	 */
+	if (conn->require_auth)
+	{
+		switch (areq)
+		{
+			case AUTH_REQ_OK:
+				/*
+				 * Check to make sure we've actually finished our exchange (or
+				 * else that the user has allowed an authentication-less
+				 * connection).
+				 *
+				 * If the user has allowed both SCRAM and unauthenticated
+				 * (trust) connections, then this check will silently accept
+				 * partial SCRAM exchanges, where a misbehaving server does not
+				 * provide its verifier before sending an OK. This is consistent
+				 * with historical behavior, but it may be a point to revisit in
+				 * the future, since it could allow a server that doesn't know
+				 * the user's password to silently harvest material for a brute
+				 * force attack.
+				 */
+				if (!conn->auth_required || conn->client_finished_auth)
+					break;
+
+				/*
+				 * No explicit authentication request was made by the server --
+				 * or perhaps it was made and not completed, in the case of
+				 * SCRAM -- but there is one special case to check. If the user
+				 * allowed "gss", then a GSS-encrypted channel also satisfies
+				 * the check.
+				 */
+#ifdef ENABLE_GSS
+				if (auth_allowed(conn, AUTH_REQ_GSS) && conn->gssenc)
+				{
+					/*
+					 * If implicit GSS auth has already been performed via GSS
+					 * encryption, we don't need to have performed an
+					 * AUTH_REQ_GSS exchange. This allows require_auth=gss to be
+					 * combined with gssencmode, since there won't be an
+					 * explicit authentication request in that case.
+					 */
+				}
+				else
+#endif
+				{
+					reason = libpq_gettext("server did not complete authentication"),
+					result = false;
+				}
+
+				break;
+
+			case AUTH_REQ_PASSWORD:
+			case AUTH_REQ_MD5:
+			case AUTH_REQ_GSS:
+			case AUTH_REQ_SSPI:
+			case AUTH_REQ_GSS_CONT:
+			case AUTH_REQ_SASL:
+			case AUTH_REQ_SASL_CONT:
+			case AUTH_REQ_SASL_FIN:
+				/*
+				 * We don't handle these with the default case, to avoid
+				 * bit-shifting past the end of the allowed_auth_methods mask if
+				 * the server sends an unexpected AuthRequest.
+				 */
+				result = auth_allowed(conn, areq);
+				break;
+
+			default:
+				result = false;
+				break;
+		}
+	}
+
+	if (!result)
+	{
+		if (!reason)
+			reason = auth_description(areq);
+
+		libpq_append_conn_error(conn, "auth method \"%s\" requirement failed: %s",
+								conn->require_auth, reason);
+		return result;
+	}
 
 	/*
 	 * When channel_binding=require, we must protect against two cases: (1) we
@@ -1008,6 +1139,9 @@ pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn)
 										 "fe_sendauth: error sending password authentication\n");
 					return STATUS_ERROR;
 				}
+
+				/* We expect no further authentication requests. */
+				conn->client_finished_auth = true;
 				break;
 			}
 
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index a6120bf58b..9eb6e4aef7 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -307,6 +307,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Require-Peer", "", 10,
 	offsetof(struct pg_conn, requirepeer)},
 
+	{"require_auth", "PGREQUIREAUTH", NULL, NULL,
+		"Require-Auth", "", 14, /* sizeof("scram-sha-256") == 14 */
+	offsetof(struct pg_conn, require_auth)},
+
 	{"ssl_min_protocol_version", "PGSSLMINPROTOCOLVERSION", "TLSv1.2", NULL,
 		"SSL-Minimum-Protocol-Version", "", 8,	/* sizeof("TLSv1.x") == 8 */
 	offsetof(struct pg_conn, ssl_min_protocol_version)},
@@ -1237,6 +1241,162 @@ connectOptions2(PGconn *conn)
 		}
 	}
 
+	/*
+	 * parse and validate require_auth option
+	 */
+	if (conn->require_auth)
+	{
+		char	   *s = conn->require_auth;
+		bool		first, more;
+		bool		negated = false;
+
+		/*
+		 * By default, start from an empty set of allowed options and add to it.
+		 */
+		conn->auth_required = true;
+		conn->allowed_auth_methods = 0;
+
+		for (first = true, more = true; more; first = false)
+		{
+			char	   *method, *part;
+			uint32		bits;
+
+			part = parse_comma_separated_list(&s, &more);
+			if (part == NULL)
+				goto oom_error;
+
+			/*
+			 * Check for negation, e.g. '!password'. If one element is negated,
+			 * they all have to be.
+			 */
+			method = part;
+			if (*method == '!')
+			{
+				if (first)
+				{
+					/*
+					 * Switch to a permissive set of allowed options, and
+					 * subtract from it.
+					 */
+					conn->auth_required = false;
+					conn->allowed_auth_methods = -1;
+				}
+				else if (!negated)
+				{
+					conn->status = CONNECTION_BAD;
+					libpq_append_conn_error(conn, "negative require_auth method \"%s\" cannot be mixed with non-negative methods",
+											method);
+
+					free(part);
+					return false;
+				}
+
+				negated = true;
+				method++;
+			}
+			else if (negated)
+			{
+				conn->status = CONNECTION_BAD;
+				libpq_append_conn_error(conn, "require_auth method \"%s\" cannot be mixed with negative methods",
+										method);
+
+				free(part);
+				return false;
+			}
+
+			if (strcmp(method, "password") == 0)
+			{
+				bits = (1 << AUTH_REQ_PASSWORD);
+			}
+			else if (strcmp(method, "md5") == 0)
+			{
+				bits = (1 << AUTH_REQ_MD5);
+			}
+			else if (strcmp(method, "gss") == 0)
+			{
+				bits = (1 << AUTH_REQ_GSS);
+				bits |= (1 << AUTH_REQ_GSS_CONT);
+			}
+			else if (strcmp(method, "sspi") == 0)
+			{
+				bits = (1 << AUTH_REQ_SSPI);
+				bits |= (1 << AUTH_REQ_GSS_CONT);
+			}
+			else if (strcmp(method, "scram-sha-256") == 0)
+			{
+				/* This currently assumes that SCRAM is the only SASL method. */
+				bits = (1 << AUTH_REQ_SASL);
+				bits |= (1 << AUTH_REQ_SASL_CONT);
+				bits |= (1 << AUTH_REQ_SASL_FIN);
+			}
+			else if (strcmp(method, "none") == 0)
+			{
+				/*
+				 * Special case: let the user explicitly allow (or disallow)
+				 * connections where the server does not send an explicit
+				 * authentication challenge, such as "trust" and "cert" auth.
+				 */
+				if (negated) /* "!none" */
+				{
+					if (conn->auth_required)
+						goto duplicate;
+
+					conn->auth_required = true;
+				}
+				else /* "none" */
+				{
+					if (!conn->auth_required)
+						goto duplicate;
+
+					conn->auth_required = false;
+				}
+
+				free(part);
+				continue; /* avoid the bitmask manipulation below */
+			}
+			else
+			{
+				conn->status = CONNECTION_BAD;
+				libpq_append_conn_error(conn, "invalid require_auth method: \"%s\"",
+										method);
+
+				free(part);
+				return false;
+			}
+
+			/* Update the bitmask. */
+			if (negated)
+			{
+				if ((conn->allowed_auth_methods & bits) == 0)
+					goto duplicate;
+
+				conn->allowed_auth_methods &= ~bits;
+			}
+			else
+			{
+				if ((conn->allowed_auth_methods & bits) == bits)
+					goto duplicate;
+
+				conn->allowed_auth_methods |= bits;
+			}
+
+			free(part);
+			continue;
+
+duplicate:
+			/*
+			 * A duplicated method probably indicates a typo in a setting where
+			 * typos are extremely risky.
+			 */
+			conn->status = CONNECTION_BAD;
+			libpq_append_conn_error(conn, "require_auth method \"%s\" is specified more than once",
+									part);
+
+			free(part);
+			return false;
+		}
+	}
+
 	/*
 	 * validate channel_binding option
 	 */
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index c24645b469..f30933f1b2 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -396,6 +396,7 @@ struct pg_conn
 	char	   *ssl_min_protocol_version;	/* minimum TLS protocol version */
 	char	   *ssl_max_protocol_version;	/* maximum TLS protocol version */
 	char	   *target_session_attrs;	/* desired session properties */
+	char	   *require_auth;	/* name of the expected auth method */
 
 	/* Optional file to write trace info to */
 	FILE	   *Pfdebug;
@@ -457,6 +458,9 @@ struct pg_conn
 	bool		write_failed;	/* have we had a write failure on sock? */
 	char	   *write_err_msg;	/* write error message, or NULL if OOM */
 
+	bool		auth_required;	/* require an authentication challenge from the server? */
+	uint32		allowed_auth_methods;	/* bitmask of acceptable AuthRequest codes */
+
 	/* Transient state needed while establishing connection */
 	PGTargetServerType target_server_type;	/* desired session properties */
 	bool		try_next_addr;	/* time to advance to next address/host? */
@@ -512,6 +516,9 @@ struct pg_conn
 	bool		error_result;	/* do we need to make an ERROR result? */
 	PGresult   *next_result;	/* next result (used in single-row mode) */
 
+	bool		client_finished_auth; /* have we finished our half of the
+									   * authentication exchange? */
+
 	/* Assorted state for SASL, SSL, GSS, etc */
 	const pg_fe_sasl_mech *sasl;
 	void	   *sasl_state;
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 42d3d4c79b..01f256bbbc 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -115,6 +115,74 @@ is($res, 't',
 	"users with trust authentication use SYSTEM_USER = NULL in parallel workers"
 );
 
+# All positive require_auth options should fail...
+$node->connect_fails("user=scram_role require_auth=gss",
+	"GSS authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=sspi",
+	"SSPI authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=password",
+	"password authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=password,scram-sha-256",
+	"multiple authentication types can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+
+# ...and negative require_auth options should succeed.
+$node->connect_ok("user=scram_role require_auth=!gss",
+	"GSS authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!sspi",
+	"SSPI authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!password",
+	"password authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!md5",
+	"md5 authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!scram-sha-256",
+	"SCRAM authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!password,!scram-sha-256",
+	"multiple authentication types can be forbidden: succeeds with trust auth");
+
+# require_auth=[!]none should interact correctly with trust auth.
+$node->connect_ok("user=scram_role require_auth=none",
+	"all authentication can be forbidden: succeeds with trust auth");
+$node->connect_fails("user=scram_role require_auth=!none",
+	"any authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+
+# Negative and positive require_auth options can't be mixed.
+$node->connect_fails("user=scram_role require_auth=scram-sha-256,!md5",
+	"negative require_auth methods can't be mixed with positive",
+	expected_stderr => qr/negative require_auth method "!md5" cannot be mixed with non-negative methods/);
+$node->connect_fails("user=scram_role require_auth=!password,scram-sha-256",
+	"positive require_auth methods can't be mixed with negative",
+	expected_stderr => qr/require_auth method "scram-sha-256" cannot be mixed with negative methods/);
+
+# require_auth methods can't be duplicated.
+$node->connect_fails("user=scram_role require_auth=password,md5,password",
+	"require_auth methods can't be duplicated: positive case",
+	expected_stderr => qr/require_auth method "password" is specified more than once/);
+$node->connect_fails("user=scram_role require_auth=!password,!md5,!password",
+	"require_auth methods can't be duplicated: negative case",
+	expected_stderr => qr/require_auth method "!password" is specified more than once/);
+$node->connect_fails("user=scram_role require_auth=none,md5,none",
+	"require_auth methods can't be duplicated: none case",
+	expected_stderr => qr/require_auth method "none" is specified more than once/);
+$node->connect_fails("user=scram_role require_auth=!none,!md5,!none",
+	"require_auth methods can't be duplicated: !none case",
+	expected_stderr => qr/require_auth method "!none" is specified more than once/);
+
+# Unknown require_auth methods are caught.
+$node->connect_fails("user=scram_role require_auth=none,abcdefg",
+	"unknown require_auth methods are rejected",
+	expected_stderr => qr/invalid require_auth method: "abcdefg"/);
+
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'all', 'all', 'password');
 test_conn($node, 'user=scram_role', 'password', 0,
@@ -124,6 +192,33 @@ test_conn($node, 'user=md5_role', 'password', 0,
 	log_like =>
 	  [qr/connection authenticated: identity="md5_role" method=password/]);
 
+# require_auth should succeed with a plaintext password...
+$node->connect_ok("user=scram_role require_auth=password",
+	"password authentication can be required: works with password auth");
+$node->connect_ok("user=scram_role require_auth=!none",
+	"any authentication can be required: works with password auth");
+$node->connect_ok("user=scram_role require_auth=scram-sha-256,password,md5",
+	"multiple authentication types can be required: works with password auth");
+
+# ...fail for other auth types...
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails("user=scram_role require_auth=none",
+	"all authentication can be forbidden: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+
+# ...and allow password authentication to be prohibited.
+$node->connect_fails("user=scram_role require_auth=!password",
+	"password authentication can be forbidden: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails("user=scram_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types can be forbidden: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+
 # For "scram-sha-256" method, user "scram_role" should be able to connect.
 reset_pg_hba($node, 'all', 'all', 'scram-sha-256');
 test_conn(
@@ -137,6 +232,33 @@ test_conn(
 test_conn($node, 'user=md5_role', 'scram-sha-256', 2,
 	log_unlike => [qr/connection authenticated:/]);
 
+# require_auth should succeed with SCRAM...
+$node->connect_ok("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: works with SCRAM auth");
+$node->connect_ok("user=scram_role require_auth=!none",
+	"any authentication can be required: works with SCRAM auth");
+$node->connect_ok("user=scram_role require_auth=password,scram-sha-256,md5",
+	"multiple authentication types can be required: works with SCRAM auth");
+
+# ...fail for other auth types...
+$node->connect_fails("user=scram_role require_auth=password",
+	"password authentication can be required: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails("user=scram_role require_auth=none",
+	"all authentication can be forbidden: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+
+# ...and allow SCRAM authentication to be prohibited.
+$node->connect_fails("user=scram_role require_auth=!scram-sha-256",
+	"SCRAM authentication can be forbidden: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails("user=scram_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types can be forbidden: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+
 # Test that bad passwords are rejected.
 $ENV{"PGPASSWORD"} = 'badpass';
 test_conn($node, 'user=scram_role', 'scram-sha-256', 2,
@@ -153,6 +275,33 @@ test_conn($node, 'user=md5_role', 'md5', 0,
 	log_like =>
 	  [qr/connection authenticated: identity="md5_role" method=md5/]);
 
+# require_auth should succeed with MD5...
+$node->connect_ok("user=md5_role require_auth=md5",
+	"MD5 authentication can be required: works with MD5 auth");
+$node->connect_ok("user=md5_role require_auth=!none",
+	"any authentication can be required: works with MD5 auth");
+$node->connect_ok("user=md5_role require_auth=md5,scram-sha-256,password",
+	"multiple authentication types can be required: works with MD5 auth");
+
+# ...fail for other auth types...
+$node->connect_fails("user=md5_role require_auth=password",
+	"password authentication can be required: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+$node->connect_fails("user=md5_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+$node->connect_fails("user=md5_role require_auth=none",
+	"all authentication can be forbidden: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+
+# ...and allow MD5 authentication to be prohibited.
+$node->connect_fails("user=md5_role require_auth=!md5",
+	"password authentication can be forbidden: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+$node->connect_fails("user=md5_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types can be forbidden: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+
 # Test SYSTEM_USER <> NULL with parallel workers.
 $node->safe_psql(
 	'postgres',
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index a2bc8a5351..61aede12d1 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -318,6 +318,24 @@ test_query(
 	'gssencmode=require',
 	'sending 100K lines works');
 
+# require_auth=gss should succeed...
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=disable require_auth=gss",
+	"GSS authentication can be requested: works with GSS auth without encryption");
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss",
+	"GSS authentication can be requested: works with GSS auth with encryption");
+
+# ...and require_auth=sspi should fail.
+$node->connect_fails(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=disable require_auth=sspi",
+	"SSPI authentication can be requested: fails with GSS auth without encryption",
+	expected_stderr => qr/server requested GSSAPI authentication/);
+$node->connect_fails(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=sspi",
+	"SSPI authentication can be requested: fails with GSS auth with encryption",
+	expected_stderr => qr/server did not complete authentication/);
+
 # Test that SYSTEM_USER works.
 test_query($node, 'test1', 'SELECT SYSTEM_USER;',
 	qr/^gss:test1\@$realm$/s, 'gssencmode=require', 'testing system_user');
@@ -363,6 +381,14 @@ test_access(
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable',
 	'fails with GSS encryption disabled and hostgssenc hba');
 
+# require_auth=gss should succeed.
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss",
+	"GSS authentication can be requested: works with GSS encryption");
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss,scram-sha-256",
+	"multiple authentication types can be requested: works with GSS encryption");
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
 	qq{hostnogssenc all all $hostaddr/32 gss map=mymap});
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index fd90832b75..0319f2ee74 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -222,6 +222,12 @@ test_access(
 		qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/
 	],);
 
+# require_auth=password should complete successfully; other methods should fail.
+$node->connect_ok("user=test1 require_auth=password",
+	"password authentication can be required: works with ldap auth");
+$node->connect_fails("user=test1 require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with ldap auth");
+
 note "search+bind";
 
 unlink($node->data_dir . '/pg_hba.conf');
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index deaa4aa086..cf61bc7d0d 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -136,4 +136,29 @@ $node->connect_ok(
 		qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/
 	]);
 
+# channel_binding should continue to function independently of require_auth.
+$node->connect_ok("$common_connstr user=ssltestuser channel_binding=disable require_auth=scram-sha-256",
+	"SCRAM with SSL, channel_binding=disable, and require_auth=scram-sha-256");
+$node->connect_fails(
+	"$common_connstr user=md5testuser require_auth=md5 channel_binding=require",
+	"channel_binding can fail even when require_auth succeeds",
+	expected_stderr =>
+	  qr/channel binding required but not supported by server's authentication request/
+);
+if ($supports_tls_server_end_point)
+{
+	$node->connect_ok(
+		"$common_connstr user=ssltestuser channel_binding=require require_auth=scram-sha-256",
+		"SCRAM with SSL, channel_binding=require, and require_auth=scram-sha-256");
+}
+else
+{
+	$node->connect_fails(
+		"$common_connstr user=ssltestuser channel_binding=require require_auth=scram-sha-256",
+		"SCRAM with SSL, channel_binding=require, and require_auth=scram-sha-256",
+		expected_stderr =>
+		  qr/channel binding is required, but server did not offer an authentication method that supports channel binding/
+	);
+}
+
 done_testing();
-- 
2.25.1

v13-0002-Add-sslcertmode-option-for-client-certificates.patchtext/x-patch; charset=US-ASCII; name=v13-0002-Add-sslcertmode-option-for-client-certificates.patchDownload
From 815fdc4393e6b6691ee64bd88ca22a11805ad857 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jchampion@timescale.com>
Date: Fri, 24 Jun 2022 15:40:42 -0700
Subject: [PATCH v13 2/3] Add sslcertmode option for client certificates

The sslcertmode option controls whether the server is allowed and/or
required to request a certificate from the client. There are three
modes:

- "allow" is the default and follows the current behavior -- a
  configured  sslcert is sent if the server requests one (which, with
  the current implementation, will happen whenever TLS is negotiated).

- "disable" causes the client to refuse to send a client certificate
  even if an sslcert is configured.

- "require" causes the client to fail if a client certificate is never
  sent and the server opens a connection anyway. This doesn't add any
  additional security, since there is no guarantee that the server is
  validating the certificate correctly, but it may help troubleshoot
  more complicated TLS setups.

sslcertmode=require needs the OpenSSL implementation to support
SSL_CTX_set_cert_cb(). Notably, LibreSSL does not.
---
 configure                                | 11 ++--
 configure.ac                             |  4 +-
 doc/src/sgml/libpq.sgml                  | 64 ++++++++++++++++++++++++
 meson.build                              |  3 +-
 src/include/pg_config.h.in               |  3 ++
 src/interfaces/libpq/fe-auth.c           | 19 +++++++
 src/interfaces/libpq/fe-connect.c        | 51 +++++++++++++++++++
 src/interfaces/libpq/fe-secure-openssl.c | 39 ++++++++++++++-
 src/interfaces/libpq/libpq-int.h         |  3 ++
 src/test/ssl/t/001_ssltests.pl           | 43 ++++++++++++++++
 src/tools/msvc/Solution.pm               |  8 +++
 11 files changed, 239 insertions(+), 9 deletions(-)

diff --git a/configure b/configure
index 3966368b8d..368588e98d 100755
--- a/configure
+++ b/configure
@@ -12992,13 +12992,14 @@ else
 fi
 
   fi
-  # Function introduced in OpenSSL 1.0.2.
-  for ac_func in X509_get_signature_nid
+  # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all of these.
+  for ac_func in X509_get_signature_nid SSL_CTX_set_cert_cb
 do :
-  ac_fn_c_check_func "$LINENO" "X509_get_signature_nid" "ac_cv_func_X509_get_signature_nid"
-if test "x$ac_cv_func_X509_get_signature_nid" = xyes; then :
+  as_ac_var=`$as_echo "ac_cv_func_$ac_func" | $as_tr_sh`
+ac_fn_c_check_func "$LINENO" "$ac_func" "$as_ac_var"
+if eval test \"x\$"$as_ac_var"\" = x"yes"; then :
   cat >>confdefs.h <<_ACEOF
-#define HAVE_X509_GET_SIGNATURE_NID 1
+#define `$as_echo "HAVE_$ac_func" | $as_tr_cpp` 1
 _ACEOF
 
 fi
diff --git a/configure.ac b/configure.ac
index f76b7ee31f..431594b921 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1354,8 +1354,8 @@ if test "$with_ssl" = openssl ; then
      AC_SEARCH_LIBS(CRYPTO_new_ex_data, [eay32 crypto], [], [AC_MSG_ERROR([library 'eay32' or 'crypto' is required for OpenSSL])])
      AC_SEARCH_LIBS(SSL_new, [ssleay32 ssl], [], [AC_MSG_ERROR([library 'ssleay32' or 'ssl' is required for OpenSSL])])
   fi
-  # Function introduced in OpenSSL 1.0.2.
-  AC_CHECK_FUNCS([X509_get_signature_nid])
+  # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all of these.
+  AC_CHECK_FUNCS([X509_get_signature_nid SSL_CTX_set_cert_cb])
   # Functions introduced in OpenSSL 1.1.0. We used to check for
   # OPENSSL_VERSION_NUMBER, but that didn't work with 1.1.0, because LibreSSL
   # defines OPENSSL_VERSION_NUMBER to claim version 2.0.0, even though it
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 05d9645f40..32c0872eed 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1810,6 +1810,60 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-sslcertmode" xreflabel="sslcertmode">
+      <term><literal>sslcertmode</literal></term>
+      <listitem>
+       <para>
+        This option determines whether a client certificate may be sent to the
+        server, and whether the server is required to request one. There are
+        three modes:
+
+        <variablelist>
+         <varlistentry>
+          <term><literal>disable</literal></term>
+          <listitem>
+           <para>
+            a client certificate is never sent, even if one is provided via
+            <xref linkend="libpq-connect-sslcert" />
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>allow</literal> (default)</term>
+          <listitem>
+           <para>
+            a certificate may be sent, if the server requests one and it has
+            been provided via <literal>sslcert</literal>
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>require</literal></term>
+          <listitem>
+           <para>
+            the server <emphasis>must</emphasis> request a certificate. The
+            connection will fail if the client does not send a certificate and
+            the server successfully authenticates the client anyway.
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+       </para>
+
+       <note>
+        <para>
+         <literal>sslcertmode=require</literal> doesn't add any additional
+         security, since there is no guarantee that the server is validating the
+         certificate correctly; PostgreSQL servers generally request TLS
+         certificates from clients whether they validate them or not. The option
+         may be useful when troubleshooting more complicated TLS setups.
+        </para>
+       </note>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-sslrootcert" xreflabel="sslrootcert">
       <term><literal>sslrootcert</literal></term>
       <listitem>
@@ -7985,6 +8039,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGSSLCERTMODE</envar></primary>
+      </indexterm>
+      <envar>PGSSLCERTMODE</envar> behaves the same as the <xref
+      linkend="libpq-connect-sslcertmode"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/meson.build b/meson.build
index 058382046e..31ab349037 100644
--- a/meson.build
+++ b/meson.build
@@ -1181,8 +1181,9 @@ if get_option('ssl') == 'openssl'
     ['CRYPTO_new_ex_data', {'required': true}],
     ['SSL_new', {'required': true}],
 
-    # Function introduced in OpenSSL 1.0.2.
+    # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all of these.
     ['X509_get_signature_nid'],
+    ['SSL_CTX_set_cert_cb'],
 
     # Functions introduced in OpenSSL 1.1.0. We used to check for
     # OPENSSL_VERSION_NUMBER, but that didn't work with 1.1.0, because LibreSSL
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index c5a80b829e..08382e868b 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -397,6 +397,9 @@
 /* Define to 1 if you have spinlocks. */
 #undef HAVE_SPINLOCKS
 
+/* Define to 1 if you have the `SSL_CTX_set_cert_cb' function. */
+#undef HAVE_SSL_CTX_SET_CERT_CB
+
 /* Define to 1 if stdbool.h conforms to C99. */
 #undef HAVE_STDBOOL_H
 
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 6d9ec468db..c5302462c6 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -860,6 +860,25 @@ check_expected_areq(AuthRequest areq, PGconn *conn)
 	bool		result = true;
 	const char *reason = NULL;
 
+	if (conn->sslcertmode[0] == 'r' /* require */
+		&& areq == AUTH_REQ_OK)
+	{
+		/*
+		 * Trade off a little bit of complexity to try to get these error
+		 * messages as precise as possible.
+		 */
+		if (!conn->ssl_cert_requested)
+		{
+			libpq_append_conn_error(conn, "server did not request a certificate");
+			return false;
+		}
+		else if (!conn->ssl_cert_sent)
+		{
+			libpq_append_conn_error(conn, "server accepted connection without a valid certificate");
+			return false;
+		}
+	}
+
 	/*
 	 * If the user required a specific auth method, or specified an allowed set,
 	 * then reject all others here, and make sure the server actually completes
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 9eb6e4aef7..3744d95955 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -125,8 +125,10 @@ static int	ldapServiceLookup(const char *purl, PQconninfoOption *options,
 #define DefaultTargetSessionAttrs	"any"
 #ifdef USE_SSL
 #define DefaultSSLMode "prefer"
+#define DefaultSSLCertMode "allow"
 #else
 #define DefaultSSLMode	"disable"
+#define DefaultSSLCertMode "disable"
 #endif
 #ifdef ENABLE_GSS
 #include "fe-gssapi-common.h"
@@ -283,6 +285,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"SSL-Client-Key", "", 64,
 	offsetof(struct pg_conn, sslkey)},
 
+	{"sslcertmode", "PGSSLCERTMODE", NULL, NULL,
+		"SSL-Client-Cert-Mode", "", 8, /* sizeof("disable") == 8 */
+	offsetof(struct pg_conn, sslcertmode)},
+
 	{"sslpassword", NULL, NULL, NULL,
 		"SSL-Client-Key-Password", "*", 20,
 	offsetof(struct pg_conn, sslpassword)},
@@ -1501,6 +1507,51 @@ duplicate:
 		return false;
 	}
 
+	/*
+	 * validate sslcertmode option
+	 */
+	if (conn->sslcertmode)
+	{
+		if (strcmp(conn->sslcertmode, "disable") != 0 &&
+			strcmp(conn->sslcertmode, "allow") != 0 &&
+			strcmp(conn->sslcertmode, "require") != 0)
+		{
+			conn->status = CONNECTION_BAD;
+			libpq_append_conn_error(conn, "invalid %s value: \"%s\"",
+									"sslcertmode", conn->sslcertmode);
+			return false;
+		}
+#ifndef USE_SSL
+		if (strcmp(conn->sslcertmode, "require") == 0)
+		{
+			conn->status = CONNECTION_BAD;
+			libpq_append_conn_error(conn, "sslcertmode value \"%s\" invalid when SSL support is not compiled in",
+									conn->sslcertmode);
+			return false;
+		}
+#endif
+#ifndef HAVE_SSL_CTX_SET_CERT_CB
+		/*
+		 * Without a certificate callback, the current implementation can't
+		 * figure out if a certficate was actually requested, so "require" is
+		 * useless.
+		 */
+		if (strcmp(conn->sslcertmode, "require") == 0)
+		{
+			conn->status = CONNECTION_BAD;
+			libpq_append_conn_error(conn, "sslcertmode value \"%s\" is not supported (check OpenSSL version)",
+									conn->sslcertmode);
+			return false;
+		}
+#endif
+	}
+	else
+	{
+		conn->sslcertmode = strdup(DefaultSSLCertMode);
+		if (!conn->sslcertmode)
+			goto oom_error;
+	}
+
 	/*
 	 * validate gssencmode option
 	 */
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index bad85359b6..74d48c6d14 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -457,6 +457,33 @@ verify_cb(int ok, X509_STORE_CTX *ctx)
 	return ok;
 }
 
+#ifdef HAVE_SSL_CTX_SET_CERT_CB
+/*
+ * Certificate selection callback
+ *
+ * This callback lets us choose the client certificate we send to the server
+ * after seeing its CertificateRequest. We only support sending a single
+ * hard-coded certificate via sslcert, so we don't actually set any certificates
+ * here; we just use it to record whether or not the server has actually asked
+ * for one and whether we have one to send.
+ */
+static int
+cert_cb(SSL *ssl, void *arg)
+{
+	PGconn *conn = arg;
+	conn->ssl_cert_requested = true;
+
+	/* Do we have a certificate loaded to send back? */
+	if (SSL_get_certificate(ssl))
+		conn->ssl_cert_sent = true;
+
+	/*
+	 * Tell OpenSSL that the callback succeeded; we're not required to actually
+	 * make any changes to the SSL handle.
+	 */
+	return 1;
+}
+#endif
 
 /*
  * OpenSSL-specific wrapper around
@@ -948,6 +975,11 @@ initialize_SSL(PGconn *conn)
 		SSL_CTX_set_default_passwd_cb_userdata(SSL_context, conn);
 	}
 
+#ifdef HAVE_SSL_CTX_SET_CERT_CB
+	/* Set up a certificate selection callback. */
+	SSL_CTX_set_cert_cb(SSL_context, cert_cb, conn);
+#endif
+
 	/* Disable old protocol versions */
 	SSL_CTX_set_options(SSL_context, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
 
@@ -1102,7 +1134,12 @@ initialize_SSL(PGconn *conn)
 	else
 		fnbuf[0] = '\0';
 
-	if (fnbuf[0] == '\0')
+	if (conn->sslcertmode[0] == 'd') /* disable */
+	{
+		/* don't send a client cert even if we have one */
+		have_cert = false;
+	}
+	else if (fnbuf[0] == '\0')
 	{
 		/* no home directory, proceed without a client cert */
 		have_cert = false;
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index f30933f1b2..edce16143c 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -384,6 +384,7 @@ struct pg_conn
 	char	   *sslkey;			/* client key filename */
 	char	   *sslcert;		/* client certificate filename */
 	char	   *sslpassword;	/* client key file password */
+	char	   *sslcertmode;	/* client cert mode (require,allow,disable) */
 	char	   *sslrootcert;	/* root certificate filename */
 	char	   *sslcrl;			/* certificate revocation list filename */
 	char	   *sslcrldir;		/* certificate revocation list directory name */
@@ -525,6 +526,8 @@ struct pg_conn
 
 	/* SSL structures */
 	bool		ssl_in_use;
+	bool		ssl_cert_requested;	/* Did the server ask us for a cert? */
+	bool		ssl_cert_sent;		/* Did we send one in reply? */
 
 #ifdef USE_SSL
 	bool		allow_ssl_try;	/* Allowed to try SSL negotiation */
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index fe42161a0f..03f0a3eaf4 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -42,6 +42,10 @@ my $SERVERHOSTADDR = '127.0.0.1';
 # This is the pattern to use in pg_hba.conf to match incoming connections.
 my $SERVERHOSTCIDR = '127.0.0.1/32';
 
+# Determine whether build supports sslcertmode=require.
+my $supports_sslcertmode_require =
+  check_pg_config("#define HAVE_SSL_CTX_SET_CERT_CB 1");
+
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
 
@@ -191,6 +195,22 @@ $node->connect_ok(
 	"$common_connstr sslrootcert=ssl/both-cas-2.crt sslmode=verify-ca",
 	"cert root file that contains two certificates, order 2");
 
+# sslcertmode=allow and =disable should both work without a client certificate.
+$node->connect_ok(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=disable",
+	"connect with sslcertmode=disable");
+$node->connect_ok(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=allow",
+	"connect with sslcertmode=allow");
+
+# sslcertmode=require, however, should fail.
+$node->connect_fails(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=require",
+	"connect with sslcertmode=require fails without a client certificate",
+	expected_stderr => $supports_sslcertmode_require
+		? qr/server accepted connection without a valid certificate/
+		: qr/sslcertmode value "require" is not supported/);
+
 # CRL tests
 
 # Invalid CRL filename is the same as no CRL, succeeds
@@ -538,6 +558,29 @@ $node->connect_ok(
 	"certificate authorization succeeds with correct client cert in encrypted DER format"
 );
 
+# correct client cert with required/allowed certificate authentication
+if ($supports_sslcertmode_require)
+{
+	$node->connect_ok(
+		"$common_connstr user=ssltestuser sslcertmode=require sslcert=ssl/client.crt "
+		  . sslkey('client.key'),
+		"certificate authorization succeeds with sslcertmode=require"
+	);
+}
+$node->connect_ok(
+	"$common_connstr user=ssltestuser sslcertmode=allow sslcert=ssl/client.crt "
+	  . sslkey('client.key'),
+	"certificate authorization succeeds with sslcertmode=allow"
+);
+
+# client cert isn't sent if certificate authentication is disabled
+$node->connect_fails(
+	"$common_connstr user=ssltestuser sslcertmode=disable sslcert=ssl/client.crt "
+	  . sslkey('client.key'),
+	"certificate authorization fails with sslcertmode=disable",
+	expected_stderr => qr/connection requires a valid client certificate/
+);
+
 # correct client cert in encrypted PEM with wrong password
 $node->connect_fails(
 	"$common_connstr user=ssltestuser sslcert=ssl/client.crt "
diff --git a/src/tools/msvc/Solution.pm b/src/tools/msvc/Solution.pm
index c2acb58df0..ff6d6f9e35 100644
--- a/src/tools/msvc/Solution.pm
+++ b/src/tools/msvc/Solution.pm
@@ -328,6 +328,7 @@ sub GenerateFiles
 		HAVE_SETPROCTITLE_FAST                   => undef,
 		HAVE_SOCKLEN_T                           => 1,
 		HAVE_SPINLOCKS                           => 1,
+		HAVE_SSL_CTX_SET_CERT_CB                 => undef,
 		HAVE_STDBOOL_H                           => 1,
 		HAVE_STDINT_H                            => 1,
 		HAVE_STDLIB_H                            => 1,
@@ -489,6 +490,13 @@ sub GenerateFiles
 
 		my ($digit1, $digit2, $digit3) = $self->GetOpenSSLVersion();
 
+		if (   ($digit1 >= '3' && $digit2 >= '0' && $digit3 >= '0')
+			|| ($digit1 >= '1' && $digit2 >= '1' && $digit3 >= '0')
+			|| ($digit1 >= '1' && $digit2 >= '0' && $digit3 >= '2'))
+		{
+			$define{HAVE_SSL_CTX_SET_CERT_CB} = 1;
+		}
+
 		# More symbols are needed with OpenSSL 1.1.0 and above.
 		if (   ($digit1 >= '3' && $digit2 >= '0' && $digit3 >= '0')
 			|| ($digit1 >= '1' && $digit2 >= '1' && $digit3 >= '0'))
-- 
2.25.1

v13-0003-require_auth-decouple-SASL-and-SCRAM.patchtext/x-patch; charset=US-ASCII; name=v13-0003-require_auth-decouple-SASL-and-SCRAM.patchDownload
From 6c3b1f28bcfd70772b4037786a9eca639d96c2c0 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jchampion@timescale.com>
Date: Tue, 18 Oct 2022 16:55:36 -0700
Subject: [PATCH v13 3/3] require_auth: decouple SASL and SCRAM

Rather than assume that an AUTH_REQ_SASL* code refers to SCRAM-SHA-256,
future-proof by separating the single allowlist into a list of allowed
authentication request codes and a list of allowed SASL mechanisms.

The require_auth check is now separated into two tiers. The
AUTH_REQ_SASL* codes are always allowed. If the server sends one,
responsibility for the check then falls to pg_SASL_init(), which
compares the selected mechanism against the list of allowed mechanisms.
(Other SCRAM code is already responsible for rejecting unexpected or
out-of-order AUTH_REQ_SASL_* codes, so that's not explicitly handled
with this addition.)

Since there's only one recognized SASL mechanism, conn->sasl_mechs
currently only points at static hardcoded lists. Whenever a second
mechanism is added, the list will need to be managed dynamically.
---
 src/interfaces/libpq/fe-auth.c            | 34 ++++++++++++++++++
 src/interfaces/libpq/fe-connect.c         | 42 +++++++++++++++++++----
 src/interfaces/libpq/libpq-int.h          |  2 ++
 src/test/authentication/t/001_password.pl | 10 +++---
 src/test/ssl/t/002_scram.pl               |  6 ++++
 5 files changed, 83 insertions(+), 11 deletions(-)

diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index c5302462c6..52b1ba927e 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -522,6 +522,40 @@ pg_SASL_init(PGconn *conn, int payloadlen)
 		goto error;
 	}
 
+	/*
+	 * Before going ahead with any SASL exchange, ensure that the user has
+	 * allowed (or, alternatively, has not forbidden) this particular mechanism.
+	 *
+	 * In a hypothetical future where a server responds with multiple SASL
+	 * mechanism families, we would need to instead consult this list up above,
+	 * during mechanism negotiation. We don't live in that world yet. The server
+	 * presents one auth method and we decide whether that's acceptable or not.
+	 */
+	if (conn->sasl_mechs)
+	{
+		const char **mech;
+		bool		found = false;
+
+		Assert(conn->require_auth);
+
+		for (mech = conn->sasl_mechs; *mech; mech++)
+		{
+			if (strcmp(*mech, selected_mechanism) == 0)
+			{
+				found = true;
+				break;
+			}
+		}
+
+		if ((conn->sasl_mechs_denied && found)
+			|| (!conn->sasl_mechs_denied && !found))
+		{
+			libpq_append_conn_error(conn, "auth method \"%s\" requirement failed: server requested unacceptable SASL mechanism \"%s\"",
+									conn->require_auth, selected_mechanism);
+			goto error;
+		}
+	}
+
 	if (conn->channel_binding[0] == 'r' &&	/* require */
 		strcmp(selected_mechanism, SCRAM_SHA_256_PLUS_NAME) != 0)
 	{
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 3744d95955..46fc8e4940 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -1256,11 +1256,25 @@ connectOptions2(PGconn *conn)
 		bool		first, more;
 		bool		negated = false;
 
+		static const uint32 default_methods = (
+			1 << AUTH_REQ_SASL
+			| 1 << AUTH_REQ_SASL_CONT
+			| 1 << AUTH_REQ_SASL_FIN
+		);
+		static const char *no_mechs[] = { NULL };
+
 		/*
-		 * By default, start from an empty set of allowed options and add to it.
+		 * By default, start from a minimum set of allowed options and add to
+		 * it.
+		 *
+		 * NB: The SASL method codes are always "allowed" here. If the server
+		 * requests SASL auth, pg_SASL_init() will enforce adherence to the
+		 * sasl_mechs list, which by default is empty.
 		 */
 		conn->auth_required = true;
-		conn->allowed_auth_methods = 0;
+		conn->allowed_auth_methods = default_methods;
+		conn->sasl_mechs = no_mechs;
+		conn->sasl_mechs_denied = false;
 
 		for (first = true, more = true; more; first = false)
 		{
@@ -1286,6 +1300,9 @@ connectOptions2(PGconn *conn)
 					 */
 					conn->auth_required = false;
 					conn->allowed_auth_methods = -1;
+
+					/* conn->sasl_mechs is now a list of denied mechanisms. */
+					conn->sasl_mechs_denied = true;
 				}
 				else if (!negated)
 				{
@@ -1330,10 +1347,23 @@ connectOptions2(PGconn *conn)
 			}
 			else if (strcmp(method, "scram-sha-256") == 0)
 			{
-				/* This currently assumes that SCRAM is the only SASL method. */
-				bits = (1 << AUTH_REQ_SASL);
-				bits |= (1 << AUTH_REQ_SASL_CONT);
-				bits |= (1 << AUTH_REQ_SASL_FIN);
+				static const char *scram_mechs[] = {
+					SCRAM_SHA_256_NAME,
+					SCRAM_SHA_256_PLUS_NAME,
+					NULL /* list terminator */
+				};
+
+				/*
+				 * This currently assumes that SCRAM is the only SASL method.
+				 * Once a second mechanism is added, this code will need to add
+				 * to the list instead of replacing it wholesale.
+				 */
+				if (conn->sasl_mechs[0])
+					goto duplicate;
+				conn->sasl_mechs = scram_mechs;
+
+				free(part);
+				continue; /* avoid the bitmask manipulation below */
 			}
 			else if (strcmp(method, "none") == 0)
 			{
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index edce16143c..b7c91cb0d7 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -461,6 +461,8 @@ struct pg_conn
 
 	bool		auth_required;	/* require an authentication challenge from the server? */
 	uint32		allowed_auth_methods;	/* bitmask of acceptable AuthRequest codes */
+	const char **sasl_mechs;	/* list of allowed/denied SASL mechanisms */
+	bool		sasl_mechs_denied;	/* is the sasl_mechs list forbidden? */
 
 	/* Transient state needed while establishing connection */
 	PGTargetServerType target_server_type;	/* desired session properties */
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 01f256bbbc..9ef4088dce 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -243,21 +243,21 @@ $node->connect_ok("user=scram_role require_auth=password,scram-sha-256,md5",
 # ...fail for other auth types...
 $node->connect_fails("user=scram_role require_auth=password",
 	"password authentication can be required: fails with SCRAM auth",
-	expected_stderr => qr/server requested SASL authentication/);
+	expected_stderr => qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256"/);
 $node->connect_fails("user=scram_role require_auth=md5",
 	"md5 authentication can be required: fails with SCRAM auth",
-	expected_stderr => qr/server requested SASL authentication/);
+	expected_stderr => qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256"/);
 $node->connect_fails("user=scram_role require_auth=none",
 	"all authentication can be forbidden: fails with SCRAM auth",
-	expected_stderr => qr/server requested SASL authentication/);
+	expected_stderr => qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256"/);
 
 # ...and allow SCRAM authentication to be prohibited.
 $node->connect_fails("user=scram_role require_auth=!scram-sha-256",
 	"SCRAM authentication can be forbidden: fails with SCRAM auth",
-	expected_stderr => qr/server requested SASL authentication/);
+	expected_stderr => qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256"/);
 $node->connect_fails("user=scram_role require_auth=!password,!md5,!scram-sha-256",
 	"multiple authentication types can be forbidden: fails with SCRAM auth",
-	expected_stderr => qr/server requested SASL authentication/);
+	expected_stderr => qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256"/);
 
 # Test that bad passwords are rejected.
 $ENV{"PGPASSWORD"} = 'badpass';
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index cf61bc7d0d..472b2efdf1 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -150,6 +150,12 @@ if ($supports_tls_server_end_point)
 	$node->connect_ok(
 		"$common_connstr user=ssltestuser channel_binding=require require_auth=scram-sha-256",
 		"SCRAM with SSL, channel_binding=require, and require_auth=scram-sha-256");
+	$node->connect_fails(
+		"$common_connstr user=ssltestuser channel_binding=require require_auth=password",
+		"SCRAM with SSL, channel_binding=require, and require_auth=password",
+		expected_stderr =>
+		  qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256-PLUS"/
+	);
 }
 else
 {
-- 
2.25.1

#42Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Jacob Champion (#41)
Re: [PoC] Let libpq reject unexpected authentication requests

On 16.11.22 18:26, Jacob Champion wrote:

On Tue, Nov 15, 2022 at 11:07 PM Michael Paquier <michael@paquier.xyz> wrote:

I am beginning to look at the last version proposed, which has been
marked as RfC. Does this patch need a refresh in light of a9e9a9f and
0873b2d? The changes for libpq_append_conn_error() should be
straight-forward.

Updated in v13, thanks!

What is the status of this patch set? Michael had registered himself as
committer and then removed himself again. So I hadn't been paying much
attention myself. Was there anything left to discuss?

#43Aleksander Alekseev
aleksander@timescale.com
In reply to: Peter Eisentraut (#42)
Re: [PoC] Let libpq reject unexpected authentication requests

Hi Peter,

Updated in v13, thanks!

What is the status of this patch set? Michael had registered himself as
committer and then removed himself again. So I hadn't been paying much
attention myself. Was there anything left to discuss?

Previously I marked the patch as RfC. Although it's been a few months
ago and I don't recall all the details, it should have been in good
shape (in my personal opinion at least). The commits a9e9a9f and
0873b2d Michael referred to are message refactorings so I doubt Jacob
had serious problems with them.

Of course, I'll take another fresh look and let you know my findings in a bit.

--
Best regards,
Aleksander Alekseev

#44Aleksander Alekseev
aleksander@timescale.com
In reply to: Aleksander Alekseev (#43)
Re: [PoC] Let libpq reject unexpected authentication requests

Hi Peter,

What is the status of this patch set? Michael had registered himself as
committer and then removed himself again. So I hadn't been paying much
attention myself. Was there anything left to discuss?

Previously I marked the patch as RfC. Although it's been a few months
ago and I don't recall all the details, it should have been in good
shape (in my personal opinion at least). The commits a9e9a9f and
0873b2d Michael referred to are message refactorings so I doubt Jacob
had serious problems with them.

Of course, I'll take another fresh look and let you know my findings in a bit.

The code is well written, documented and test-covered. All the tests
pass. To my knowledge there are no open questions left. I think the
patch is as good as it will ever get.

--
Best regards,
Aleksander Alekseev

#45Jacob Champion
jchampion@timescale.com
In reply to: Aleksander Alekseev (#44)
Re: [PoC] Let libpq reject unexpected authentication requests

On Tue, Jan 31, 2023 at 5:20 AM Aleksander Alekseev
<aleksander@timescale.com> wrote:

To my knowledge there are no open questions left. I think the
patch is as good as it will ever get.

A committer will need to decide whether they're willing to maintain
0003 or not, as mentioned with the v11 post. Which I suppose is the
last open question, but not one I can answer from here.

Thanks!
--Jacob

#46Michael Paquier
michael@paquier.xyz
In reply to: Aleksander Alekseev (#43)
Re: [PoC] Let libpq reject unexpected authentication requests

On Tue, Jan 31, 2023 at 02:03:54PM +0300, Aleksander Alekseev wrote:

What is the status of this patch set? Michael had registered himself as
committer and then removed himself again. So I hadn't been paying much
attention myself. Was there anything left to discuss?

Yes, sorry about not following up on that. I was registered as such
for a few weeks, but I have not been able to follow up. It did not
seem fair for this patch to wait on only me, which is why I have
removed my name, at least temporarily, so as somebody may be able to
come back to it before me. I am not completely sure whether I will be
able to come back and dive deeply into this thread soon, TBH :/

Previously I marked the patch as RfC. Although it's been a few months
ago and I don't recall all the details, it should have been in good
shape (in my personal opinion at least). The commits a9e9a9f and
0873b2d Michael referred to are message refactorings so I doubt Jacob
had serious problems with them.

Of course, I'll take another fresh look and let you know my findings in a bit.

(There were a few things around certificate handling that need careful
consideration, at least that was my impression.)
--
Michael

#47Jacob Champion
jchampion@timescale.com
In reply to: Michael Paquier (#46)
4 attachment(s)
Re: [PoC] Let libpq reject unexpected authentication requests

v14 rebases over the test and solution conflicts from 9244c11afe2.

Thanks,
--Jacob

Attachments:

since-v13.diff.txttext/plain; charset=US-ASCII; name=since-v13.diff.txtDownload
1:  542d330310 ! 1:  eec891c519 libpq: let client reject unexpected auth methods
    @@ src/test/ssl/t/002_scram.pl: $node->connect_ok(
     +		  qr/channel binding is required, but server did not offer an authentication method that supports channel binding/
     +	);
     +}
    ++
    + # Now test with a server certificate that uses the RSA-PSS algorithm.
    + # This checks that the certificate can be loaded and that channel binding
    + # works. (see bug #17760)
    +@@ src/test/ssl/t/002_scram.pl: if ($supports_rsapss_certs)
    + 			qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/
    + 		]);
    + }
     +
      done_testing();
2:  815fdc4393 ! 2:  dc7f94f24c Add sslcertmode option for client certificates
    @@ src/tools/msvc/Solution.pm: sub GenerateFiles
      		HAVE_STDINT_H                            => 1,
      		HAVE_STDLIB_H                            => 1,
     @@ src/tools/msvc/Solution.pm: sub GenerateFiles
    - 
    - 		my ($digit1, $digit2, $digit3) = $self->GetOpenSSLVersion();
    - 
    + 			$define{HAVE_HMAC_CTX_NEW}          = 1;
    + 			$define{HAVE_OPENSSL_INIT_SSL}      = 1;
    + 		}
    ++
    ++		# Symbols needed with OpenSSL 1.0.2 and above.
     +		if (   ($digit1 >= '3' && $digit2 >= '0' && $digit3 >= '0')
     +			|| ($digit1 >= '1' && $digit2 >= '1' && $digit3 >= '0')
     +			|| ($digit1 >= '1' && $digit2 >= '0' && $digit3 >= '2'))
     +		{
     +			$define{HAVE_SSL_CTX_SET_CERT_CB} = 1;
     +		}
    -+
    - 		# More symbols are needed with OpenSSL 1.1.0 and above.
    - 		if (   ($digit1 >= '3' && $digit2 >= '0' && $digit3 >= '0')
    - 			|| ($digit1 >= '1' && $digit2 >= '1' && $digit3 >= '0'))
    + 	}
    + 
    + 	$self->GenerateConfigHeader('src/include/pg_config.h',     \%define, 1);
3:  6c3b1f28bc = 3:  9a84af5936 require_auth: decouple SASL and SCRAM
v14-0003-require_auth-decouple-SASL-and-SCRAM.patchtext/x-patch; charset=US-ASCII; name=v14-0003-require_auth-decouple-SASL-and-SCRAM.patchDownload
From 9a84af59361e09446d087cf0c79f1a9a3b61e39d Mon Sep 17 00:00:00 2001
From: Jacob Champion <jchampion@timescale.com>
Date: Tue, 18 Oct 2022 16:55:36 -0700
Subject: [PATCH v14 3/3] require_auth: decouple SASL and SCRAM

Rather than assume that an AUTH_REQ_SASL* code refers to SCRAM-SHA-256,
future-proof by separating the single allowlist into a list of allowed
authentication request codes and a list of allowed SASL mechanisms.

The require_auth check is now separated into two tiers. The
AUTH_REQ_SASL* codes are always allowed. If the server sends one,
responsibility for the check then falls to pg_SASL_init(), which
compares the selected mechanism against the list of allowed mechanisms.
(Other SCRAM code is already responsible for rejecting unexpected or
out-of-order AUTH_REQ_SASL_* codes, so that's not explicitly handled
with this addition.)

Since there's only one recognized SASL mechanism, conn->sasl_mechs
currently only points at static hardcoded lists. Whenever a second
mechanism is added, the list will need to be managed dynamically.
---
 src/interfaces/libpq/fe-auth.c            | 34 ++++++++++++++++++
 src/interfaces/libpq/fe-connect.c         | 42 +++++++++++++++++++----
 src/interfaces/libpq/libpq-int.h          |  2 ++
 src/test/authentication/t/001_password.pl | 10 +++---
 src/test/ssl/t/002_scram.pl               |  6 ++++
 5 files changed, 83 insertions(+), 11 deletions(-)

diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index a474e17fb2..34b0573a98 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -522,6 +522,40 @@ pg_SASL_init(PGconn *conn, int payloadlen)
 		goto error;
 	}
 
+	/*
+	 * Before going ahead with any SASL exchange, ensure that the user has
+	 * allowed (or, alternatively, has not forbidden) this particular mechanism.
+	 *
+	 * In a hypothetical future where a server responds with multiple SASL
+	 * mechanism families, we would need to instead consult this list up above,
+	 * during mechanism negotiation. We don't live in that world yet. The server
+	 * presents one auth method and we decide whether that's acceptable or not.
+	 */
+	if (conn->sasl_mechs)
+	{
+		const char **mech;
+		bool		found = false;
+
+		Assert(conn->require_auth);
+
+		for (mech = conn->sasl_mechs; *mech; mech++)
+		{
+			if (strcmp(*mech, selected_mechanism) == 0)
+			{
+				found = true;
+				break;
+			}
+		}
+
+		if ((conn->sasl_mechs_denied && found)
+			|| (!conn->sasl_mechs_denied && !found))
+		{
+			libpq_append_conn_error(conn, "auth method \"%s\" requirement failed: server requested unacceptable SASL mechanism \"%s\"",
+									conn->require_auth, selected_mechanism);
+			goto error;
+		}
+	}
+
 	if (conn->channel_binding[0] == 'r' &&	/* require */
 		strcmp(selected_mechanism, SCRAM_SHA_256_PLUS_NAME) != 0)
 	{
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index a7cb836916..d1da63323f 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -1256,11 +1256,25 @@ connectOptions2(PGconn *conn)
 		bool		first, more;
 		bool		negated = false;
 
+		static const uint32 default_methods = (
+			1 << AUTH_REQ_SASL
+			| 1 << AUTH_REQ_SASL_CONT
+			| 1 << AUTH_REQ_SASL_FIN
+		);
+		static const char *no_mechs[] = { NULL };
+
 		/*
-		 * By default, start from an empty set of allowed options and add to it.
+		 * By default, start from a minimum set of allowed options and add to
+		 * it.
+		 *
+		 * NB: The SASL method codes are always "allowed" here. If the server
+		 * requests SASL auth, pg_SASL_init() will enforce adherence to the
+		 * sasl_mechs list, which by default is empty.
 		 */
 		conn->auth_required = true;
-		conn->allowed_auth_methods = 0;
+		conn->allowed_auth_methods = default_methods;
+		conn->sasl_mechs = no_mechs;
+		conn->sasl_mechs_denied = false;
 
 		for (first = true, more = true; more; first = false)
 		{
@@ -1286,6 +1300,9 @@ connectOptions2(PGconn *conn)
 					 */
 					conn->auth_required = false;
 					conn->allowed_auth_methods = -1;
+
+					/* conn->sasl_mechs is now a list of denied mechanisms. */
+					conn->sasl_mechs_denied = true;
 				}
 				else if (!negated)
 				{
@@ -1330,10 +1347,23 @@ connectOptions2(PGconn *conn)
 			}
 			else if (strcmp(method, "scram-sha-256") == 0)
 			{
-				/* This currently assumes that SCRAM is the only SASL method. */
-				bits = (1 << AUTH_REQ_SASL);
-				bits |= (1 << AUTH_REQ_SASL_CONT);
-				bits |= (1 << AUTH_REQ_SASL_FIN);
+				static const char *scram_mechs[] = {
+					SCRAM_SHA_256_NAME,
+					SCRAM_SHA_256_PLUS_NAME,
+					NULL /* list terminator */
+				};
+
+				/*
+				 * This currently assumes that SCRAM is the only SASL method.
+				 * Once a second mechanism is added, this code will need to add
+				 * to the list instead of replacing it wholesale.
+				 */
+				if (conn->sasl_mechs[0])
+					goto duplicate;
+				conn->sasl_mechs = scram_mechs;
+
+				free(part);
+				continue; /* avoid the bitmask manipulation below */
 			}
 			else if (strcmp(method, "none") == 0)
 			{
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index bab74d00a2..be44b1bfb1 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -461,6 +461,8 @@ struct pg_conn
 
 	bool		auth_required;	/* require an authentication challenge from the server? */
 	uint32		allowed_auth_methods;	/* bitmask of acceptable AuthRequest codes */
+	const char **sasl_mechs;	/* list of allowed/denied SASL mechanisms */
+	bool		sasl_mechs_denied;	/* is the sasl_mechs list forbidden? */
 
 	/* Transient state needed while establishing connection */
 	PGTargetServerType target_server_type;	/* desired session properties */
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index bba98eac79..fdd3afab7b 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -243,21 +243,21 @@ $node->connect_ok("user=scram_role require_auth=password,scram-sha-256,md5",
 # ...fail for other auth types...
 $node->connect_fails("user=scram_role require_auth=password",
 	"password authentication can be required: fails with SCRAM auth",
-	expected_stderr => qr/server requested SASL authentication/);
+	expected_stderr => qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256"/);
 $node->connect_fails("user=scram_role require_auth=md5",
 	"md5 authentication can be required: fails with SCRAM auth",
-	expected_stderr => qr/server requested SASL authentication/);
+	expected_stderr => qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256"/);
 $node->connect_fails("user=scram_role require_auth=none",
 	"all authentication can be forbidden: fails with SCRAM auth",
-	expected_stderr => qr/server requested SASL authentication/);
+	expected_stderr => qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256"/);
 
 # ...and allow SCRAM authentication to be prohibited.
 $node->connect_fails("user=scram_role require_auth=!scram-sha-256",
 	"SCRAM authentication can be forbidden: fails with SCRAM auth",
-	expected_stderr => qr/server requested SASL authentication/);
+	expected_stderr => qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256"/);
 $node->connect_fails("user=scram_role require_auth=!password,!md5,!scram-sha-256",
 	"multiple authentication types can be forbidden: fails with SCRAM auth",
-	expected_stderr => qr/server requested SASL authentication/);
+	expected_stderr => qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256"/);
 
 # Test that bad passwords are rejected.
 $ENV{"PGPASSWORD"} = 'badpass';
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 8f645b9553..9b43581290 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -154,6 +154,12 @@ if ($supports_tls_server_end_point)
 	$node->connect_ok(
 		"$common_connstr user=ssltestuser channel_binding=require require_auth=scram-sha-256",
 		"SCRAM with SSL, channel_binding=require, and require_auth=scram-sha-256");
+	$node->connect_fails(
+		"$common_connstr user=ssltestuser channel_binding=require require_auth=password",
+		"SCRAM with SSL, channel_binding=require, and require_auth=password",
+		expected_stderr =>
+		  qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256-PLUS"/
+	);
 }
 else
 {
-- 
2.25.1

v14-0002-Add-sslcertmode-option-for-client-certificates.patchtext/x-patch; charset=US-ASCII; name=v14-0002-Add-sslcertmode-option-for-client-certificates.patchDownload
From dc7f94f24cd00ab156d32dcf6a8f6a844f27250d Mon Sep 17 00:00:00 2001
From: Jacob Champion <jchampion@timescale.com>
Date: Fri, 24 Jun 2022 15:40:42 -0700
Subject: [PATCH v14 2/3] Add sslcertmode option for client certificates

The sslcertmode option controls whether the server is allowed and/or
required to request a certificate from the client. There are three
modes:

- "allow" is the default and follows the current behavior -- a
  configured  sslcert is sent if the server requests one (which, with
  the current implementation, will happen whenever TLS is negotiated).

- "disable" causes the client to refuse to send a client certificate
  even if an sslcert is configured.

- "require" causes the client to fail if a client certificate is never
  sent and the server opens a connection anyway. This doesn't add any
  additional security, since there is no guarantee that the server is
  validating the certificate correctly, but it may help troubleshoot
  more complicated TLS setups.

sslcertmode=require needs the OpenSSL implementation to support
SSL_CTX_set_cert_cb(). Notably, LibreSSL does not.
---
 configure                                | 11 ++--
 configure.ac                             |  4 +-
 doc/src/sgml/libpq.sgml                  | 64 ++++++++++++++++++++++++
 meson.build                              |  3 +-
 src/include/pg_config.h.in               |  3 ++
 src/interfaces/libpq/fe-auth.c           | 19 +++++++
 src/interfaces/libpq/fe-connect.c        | 51 +++++++++++++++++++
 src/interfaces/libpq/fe-secure-openssl.c | 39 ++++++++++++++-
 src/interfaces/libpq/libpq-int.h         |  3 ++
 src/test/ssl/t/001_ssltests.pl           | 43 ++++++++++++++++
 src/tools/msvc/Solution.pm               |  9 ++++
 11 files changed, 240 insertions(+), 9 deletions(-)

diff --git a/configure b/configure
index 7bb829ddf4..8737f52c82 100755
--- a/configure
+++ b/configure
@@ -12973,13 +12973,14 @@ else
 fi
 
   fi
-  # Function introduced in OpenSSL 1.0.2.
-  for ac_func in X509_get_signature_nid
+  # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all of these.
+  for ac_func in X509_get_signature_nid SSL_CTX_set_cert_cb
 do :
-  ac_fn_c_check_func "$LINENO" "X509_get_signature_nid" "ac_cv_func_X509_get_signature_nid"
-if test "x$ac_cv_func_X509_get_signature_nid" = xyes; then :
+  as_ac_var=`$as_echo "ac_cv_func_$ac_func" | $as_tr_sh`
+ac_fn_c_check_func "$LINENO" "$ac_func" "$as_ac_var"
+if eval test \"x\$"$as_ac_var"\" = x"yes"; then :
   cat >>confdefs.h <<_ACEOF
-#define HAVE_X509_GET_SIGNATURE_NID 1
+#define `$as_echo "HAVE_$ac_func" | $as_tr_cpp` 1
 _ACEOF
 
 fi
diff --git a/configure.ac b/configure.ac
index 137a40a942..eefaac1f5a 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1373,8 +1373,8 @@ if test "$with_ssl" = openssl ; then
      AC_SEARCH_LIBS(CRYPTO_new_ex_data, [eay32 crypto], [], [AC_MSG_ERROR([library 'eay32' or 'crypto' is required for OpenSSL])])
      AC_SEARCH_LIBS(SSL_new, [ssleay32 ssl], [], [AC_MSG_ERROR([library 'ssleay32' or 'ssl' is required for OpenSSL])])
   fi
-  # Function introduced in OpenSSL 1.0.2.
-  AC_CHECK_FUNCS([X509_get_signature_nid])
+  # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all of these.
+  AC_CHECK_FUNCS([X509_get_signature_nid SSL_CTX_set_cert_cb])
   # Functions introduced in OpenSSL 1.1.0. We used to check for
   # OPENSSL_VERSION_NUMBER, but that didn't work with 1.1.0, because LibreSSL
   # defines OPENSSL_VERSION_NUMBER to claim version 2.0.0, even though it
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 93bf5d2afb..2a6ab0288a 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1810,6 +1810,60 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-sslcertmode" xreflabel="sslcertmode">
+      <term><literal>sslcertmode</literal></term>
+      <listitem>
+       <para>
+        This option determines whether a client certificate may be sent to the
+        server, and whether the server is required to request one. There are
+        three modes:
+
+        <variablelist>
+         <varlistentry>
+          <term><literal>disable</literal></term>
+          <listitem>
+           <para>
+            a client certificate is never sent, even if one is provided via
+            <xref linkend="libpq-connect-sslcert" />
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>allow</literal> (default)</term>
+          <listitem>
+           <para>
+            a certificate may be sent, if the server requests one and it has
+            been provided via <literal>sslcert</literal>
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>require</literal></term>
+          <listitem>
+           <para>
+            the server <emphasis>must</emphasis> request a certificate. The
+            connection will fail if the client does not send a certificate and
+            the server successfully authenticates the client anyway.
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+       </para>
+
+       <note>
+        <para>
+         <literal>sslcertmode=require</literal> doesn't add any additional
+         security, since there is no guarantee that the server is validating the
+         certificate correctly; PostgreSQL servers generally request TLS
+         certificates from clients whether they validate them or not. The option
+         may be useful when troubleshooting more complicated TLS setups.
+        </para>
+       </note>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-sslrootcert" xreflabel="sslrootcert">
       <term><literal>sslrootcert</literal></term>
       <listitem>
@@ -7986,6 +8040,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGSSLCERTMODE</envar></primary>
+      </indexterm>
+      <envar>PGSSLCERTMODE</envar> behaves the same as the <xref
+      linkend="libpq-connect-sslcertmode"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/meson.build b/meson.build
index b5daed9f38..a029454918 100644
--- a/meson.build
+++ b/meson.build
@@ -1204,8 +1204,9 @@ if get_option('ssl') == 'openssl'
     ['CRYPTO_new_ex_data', {'required': true}],
     ['SSL_new', {'required': true}],
 
-    # Function introduced in OpenSSL 1.0.2.
+    # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all of these.
     ['X509_get_signature_nid'],
+    ['SSL_CTX_set_cert_cb'],
 
     # Functions introduced in OpenSSL 1.1.0. We used to check for
     # OPENSSL_VERSION_NUMBER, but that didn't work with 1.1.0, because LibreSSL
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 2490bf8ace..9fdabb6b1c 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -397,6 +397,9 @@
 /* Define to 1 if you have spinlocks. */
 #undef HAVE_SPINLOCKS
 
+/* Define to 1 if you have the `SSL_CTX_set_cert_cb' function. */
+#undef HAVE_SSL_CTX_SET_CERT_CB
+
 /* Define to 1 if stdbool.h conforms to C99. */
 #undef HAVE_STDBOOL_H
 
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 0858c5df12..a474e17fb2 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -860,6 +860,25 @@ check_expected_areq(AuthRequest areq, PGconn *conn)
 	bool		result = true;
 	const char *reason = NULL;
 
+	if (conn->sslcertmode[0] == 'r' /* require */
+		&& areq == AUTH_REQ_OK)
+	{
+		/*
+		 * Trade off a little bit of complexity to try to get these error
+		 * messages as precise as possible.
+		 */
+		if (!conn->ssl_cert_requested)
+		{
+			libpq_append_conn_error(conn, "server did not request a certificate");
+			return false;
+		}
+		else if (!conn->ssl_cert_sent)
+		{
+			libpq_append_conn_error(conn, "server accepted connection without a valid certificate");
+			return false;
+		}
+	}
+
 	/*
 	 * If the user required a specific auth method, or specified an allowed set,
 	 * then reject all others here, and make sure the server actually completes
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index abc367b713..a7cb836916 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -125,8 +125,10 @@ static int	ldapServiceLookup(const char *purl, PQconninfoOption *options,
 #define DefaultTargetSessionAttrs	"any"
 #ifdef USE_SSL
 #define DefaultSSLMode "prefer"
+#define DefaultSSLCertMode "allow"
 #else
 #define DefaultSSLMode	"disable"
+#define DefaultSSLCertMode "disable"
 #endif
 #ifdef ENABLE_GSS
 #include "fe-gssapi-common.h"
@@ -283,6 +285,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"SSL-Client-Key", "", 64,
 	offsetof(struct pg_conn, sslkey)},
 
+	{"sslcertmode", "PGSSLCERTMODE", NULL, NULL,
+		"SSL-Client-Cert-Mode", "", 8, /* sizeof("disable") == 8 */
+	offsetof(struct pg_conn, sslcertmode)},
+
 	{"sslpassword", NULL, NULL, NULL,
 		"SSL-Client-Key-Password", "*", 20,
 	offsetof(struct pg_conn, sslpassword)},
@@ -1501,6 +1507,51 @@ duplicate:
 		return false;
 	}
 
+	/*
+	 * validate sslcertmode option
+	 */
+	if (conn->sslcertmode)
+	{
+		if (strcmp(conn->sslcertmode, "disable") != 0 &&
+			strcmp(conn->sslcertmode, "allow") != 0 &&
+			strcmp(conn->sslcertmode, "require") != 0)
+		{
+			conn->status = CONNECTION_BAD;
+			libpq_append_conn_error(conn, "invalid %s value: \"%s\"",
+									"sslcertmode", conn->sslcertmode);
+			return false;
+		}
+#ifndef USE_SSL
+		if (strcmp(conn->sslcertmode, "require") == 0)
+		{
+			conn->status = CONNECTION_BAD;
+			libpq_append_conn_error(conn, "sslcertmode value \"%s\" invalid when SSL support is not compiled in",
+									conn->sslcertmode);
+			return false;
+		}
+#endif
+#ifndef HAVE_SSL_CTX_SET_CERT_CB
+		/*
+		 * Without a certificate callback, the current implementation can't
+		 * figure out if a certficate was actually requested, so "require" is
+		 * useless.
+		 */
+		if (strcmp(conn->sslcertmode, "require") == 0)
+		{
+			conn->status = CONNECTION_BAD;
+			libpq_append_conn_error(conn, "sslcertmode value \"%s\" is not supported (check OpenSSL version)",
+									conn->sslcertmode);
+			return false;
+		}
+#endif
+	}
+	else
+	{
+		conn->sslcertmode = strdup(DefaultSSLCertMode);
+		if (!conn->sslcertmode)
+			goto oom_error;
+	}
+
 	/*
 	 * validate gssencmode option
 	 */
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 6a4431ddfe..b88d9da3e2 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -462,6 +462,33 @@ verify_cb(int ok, X509_STORE_CTX *ctx)
 	return ok;
 }
 
+#ifdef HAVE_SSL_CTX_SET_CERT_CB
+/*
+ * Certificate selection callback
+ *
+ * This callback lets us choose the client certificate we send to the server
+ * after seeing its CertificateRequest. We only support sending a single
+ * hard-coded certificate via sslcert, so we don't actually set any certificates
+ * here; we just use it to record whether or not the server has actually asked
+ * for one and whether we have one to send.
+ */
+static int
+cert_cb(SSL *ssl, void *arg)
+{
+	PGconn *conn = arg;
+	conn->ssl_cert_requested = true;
+
+	/* Do we have a certificate loaded to send back? */
+	if (SSL_get_certificate(ssl))
+		conn->ssl_cert_sent = true;
+
+	/*
+	 * Tell OpenSSL that the callback succeeded; we're not required to actually
+	 * make any changes to the SSL handle.
+	 */
+	return 1;
+}
+#endif
 
 /*
  * OpenSSL-specific wrapper around
@@ -953,6 +980,11 @@ initialize_SSL(PGconn *conn)
 		SSL_CTX_set_default_passwd_cb_userdata(SSL_context, conn);
 	}
 
+#ifdef HAVE_SSL_CTX_SET_CERT_CB
+	/* Set up a certificate selection callback. */
+	SSL_CTX_set_cert_cb(SSL_context, cert_cb, conn);
+#endif
+
 	/* Disable old protocol versions */
 	SSL_CTX_set_options(SSL_context, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
 
@@ -1107,7 +1139,12 @@ initialize_SSL(PGconn *conn)
 	else
 		fnbuf[0] = '\0';
 
-	if (fnbuf[0] == '\0')
+	if (conn->sslcertmode[0] == 'd') /* disable */
+	{
+		/* don't send a client cert even if we have one */
+		have_cert = false;
+	}
+	else if (fnbuf[0] == '\0')
 	{
 		/* no home directory, proceed without a client cert */
 		have_cert = false;
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index f465a36b78..bab74d00a2 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -384,6 +384,7 @@ struct pg_conn
 	char	   *sslkey;			/* client key filename */
 	char	   *sslcert;		/* client certificate filename */
 	char	   *sslpassword;	/* client key file password */
+	char	   *sslcertmode;	/* client cert mode (require,allow,disable) */
 	char	   *sslrootcert;	/* root certificate filename */
 	char	   *sslcrl;			/* certificate revocation list filename */
 	char	   *sslcrldir;		/* certificate revocation list directory name */
@@ -525,6 +526,8 @@ struct pg_conn
 
 	/* SSL structures */
 	bool		ssl_in_use;
+	bool		ssl_cert_requested;	/* Did the server ask us for a cert? */
+	bool		ssl_cert_sent;		/* Did we send one in reply? */
 
 #ifdef USE_SSL
 	bool		allow_ssl_try;	/* Allowed to try SSL negotiation */
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 3094e27af3..4617f06f86 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -42,6 +42,10 @@ my $SERVERHOSTADDR = '127.0.0.1';
 # This is the pattern to use in pg_hba.conf to match incoming connections.
 my $SERVERHOSTCIDR = '127.0.0.1/32';
 
+# Determine whether build supports sslcertmode=require.
+my $supports_sslcertmode_require =
+  check_pg_config("#define HAVE_SSL_CTX_SET_CERT_CB 1");
+
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
 
@@ -191,6 +195,22 @@ $node->connect_ok(
 	"$common_connstr sslrootcert=ssl/both-cas-2.crt sslmode=verify-ca",
 	"cert root file that contains two certificates, order 2");
 
+# sslcertmode=allow and =disable should both work without a client certificate.
+$node->connect_ok(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=disable",
+	"connect with sslcertmode=disable");
+$node->connect_ok(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=allow",
+	"connect with sslcertmode=allow");
+
+# sslcertmode=require, however, should fail.
+$node->connect_fails(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=require",
+	"connect with sslcertmode=require fails without a client certificate",
+	expected_stderr => $supports_sslcertmode_require
+		? qr/server accepted connection without a valid certificate/
+		: qr/sslcertmode value "require" is not supported/);
+
 # CRL tests
 
 # Invalid CRL filename is the same as no CRL, succeeds
@@ -538,6 +558,29 @@ $node->connect_ok(
 	"certificate authorization succeeds with correct client cert in encrypted DER format"
 );
 
+# correct client cert with required/allowed certificate authentication
+if ($supports_sslcertmode_require)
+{
+	$node->connect_ok(
+		"$common_connstr user=ssltestuser sslcertmode=require sslcert=ssl/client.crt "
+		  . sslkey('client.key'),
+		"certificate authorization succeeds with sslcertmode=require"
+	);
+}
+$node->connect_ok(
+	"$common_connstr user=ssltestuser sslcertmode=allow sslcert=ssl/client.crt "
+	  . sslkey('client.key'),
+	"certificate authorization succeeds with sslcertmode=allow"
+);
+
+# client cert isn't sent if certificate authentication is disabled
+$node->connect_fails(
+	"$common_connstr user=ssltestuser sslcertmode=disable sslcert=ssl/client.crt "
+	  . sslkey('client.key'),
+	"certificate authorization fails with sslcertmode=disable",
+	expected_stderr => qr/connection requires a valid client certificate/
+);
+
 # correct client cert in encrypted PEM with wrong password
 $node->connect_fails(
 	"$common_connstr user=ssltestuser sslcert=ssl/client.crt "
diff --git a/src/tools/msvc/Solution.pm b/src/tools/msvc/Solution.pm
index 5399050492..da31052db2 100644
--- a/src/tools/msvc/Solution.pm
+++ b/src/tools/msvc/Solution.pm
@@ -328,6 +328,7 @@ sub GenerateFiles
 		HAVE_SETPROCTITLE_FAST                   => undef,
 		HAVE_SOCKLEN_T                           => 1,
 		HAVE_SPINLOCKS                           => 1,
+		HAVE_SSL_CTX_SET_CERT_CB                 => undef,
 		HAVE_STDBOOL_H                           => 1,
 		HAVE_STDINT_H                            => 1,
 		HAVE_STDLIB_H                            => 1,
@@ -508,6 +509,14 @@ sub GenerateFiles
 			$define{HAVE_HMAC_CTX_NEW}          = 1;
 			$define{HAVE_OPENSSL_INIT_SSL}      = 1;
 		}
+
+		# Symbols needed with OpenSSL 1.0.2 and above.
+		if (   ($digit1 >= '3' && $digit2 >= '0' && $digit3 >= '0')
+			|| ($digit1 >= '1' && $digit2 >= '1' && $digit3 >= '0')
+			|| ($digit1 >= '1' && $digit2 >= '0' && $digit3 >= '2'))
+		{
+			$define{HAVE_SSL_CTX_SET_CERT_CB} = 1;
+		}
 	}
 
 	$self->GenerateConfigHeader('src/include/pg_config.h',     \%define, 1);
-- 
2.25.1

v14-0001-libpq-let-client-reject-unexpected-auth-methods.patchtext/x-patch; charset=US-ASCII; name=v14-0001-libpq-let-client-reject-unexpected-auth-methods.patchDownload
From eec891c519d86821f0d7c5606456afd69b8ea27c Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Mon, 28 Feb 2022 09:40:43 -0800
Subject: [PATCH v14 1/3] libpq: let client reject unexpected auth methods

The require_auth connection option allows the client to choose a list of
acceptable authentication types for use with the server. There is no
negotiation: if the server does not present one of the allowed
authentication requests, the connection fails. Additionally, all methods
in the list may be negated, e.g. '!password', in which case the server
must NOT use the listed authentication type. The special method "none"
allows/disallows the use of unauthenticated connections (but it does not
govern transport-level authentication via TLS or GSSAPI).

Internally, the patch expands the role of check_expected_areq() to
ensure that the incoming request is compatible with conn->require_auth.
It also introduces a new flag, conn->client_finished_auth, which is set
by various authentication routines when the client side of the handshake
is finished. This signals to check_expected_areq() that an OK message
from the server is expected, and allows the client to complain if the
server forgoes authentication entirely.

(Since the client can't generally prove that the server is actually
doing the work of authentication, this last part is mostly useful for
SCRAM without channel binding. It could also provide a client with a
decent signal that, at the very least, it's not connecting to a database
with trust auth, and so the connection can be tied to the client in a
later audit.)

Deficiencies:
- This is unlikely to be very forwards-compatible at the moment,
  especially with SASL/SCRAM.
- SSPI support is "implemented" but untested.
- require_auth=none,scram-sha-256 currently allows the server to leave a
  SCRAM exchange unfinished. This is not net-new behavior but may be
  surprising.
---
 doc/src/sgml/libpq.sgml                   | 105 ++++++++++++++
 src/include/libpq/pqcomm.h                |   1 +
 src/interfaces/libpq/fe-auth-scram.c      |   1 +
 src/interfaces/libpq/fe-auth.c            | 134 ++++++++++++++++++
 src/interfaces/libpq/fe-connect.c         | 160 ++++++++++++++++++++++
 src/interfaces/libpq/libpq-int.h          |   7 +
 src/test/authentication/t/001_password.pl | 149 ++++++++++++++++++++
 src/test/kerberos/t/001_auth.pl           |  26 ++++
 src/test/ldap/t/001_auth.pl               |   6 +
 src/test/ssl/t/002_scram.pl               |  26 ++++
 10 files changed, 615 insertions(+)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 0e7ae70c70..93bf5d2afb 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1220,6 +1220,101 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-require-auth" xreflabel="require_auth">
+      <term><literal>require_auth</literal></term>
+      <listitem>
+      <para>
+        Specifies the authentication method that the client requires from the
+        server. If the server does not use the required method to authenticate
+        the client, or if the authentication handshake is not fully completed by
+        the server, the connection will fail. A comma-separated list of methods
+        may also be provided, of which the server must use exactly one in order
+        for the connection to succeed. By default, any authentication method is
+        accepted, and the server is free to skip authentication altogether.
+      </para>
+      <para>
+        Methods may be negated with the addition of a <literal>!</literal>
+        prefix, in which case the server must <emphasis>not</emphasis> attempt
+        the listed method; any other method is accepted, and the server is free
+        not to authenticate the client at all. If a comma-separated list is
+        provided, the server may not attempt <emphasis>any</emphasis> of the
+        listed negated methods. Negated and non-negated forms may not be
+        combined in the same setting.
+      </para>
+      <para>
+        As a final special case, the <literal>none</literal> method requires the
+        server not to use an authentication challenge. (It may also be negated,
+        to require some form of authentication.)
+      </para>
+      <para>
+        The following methods may be specified:
+
+        <variablelist>
+         <varlistentry>
+          <term><literal>password</literal></term>
+          <listitem>
+           <para>
+            The server must request plaintext password authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>md5</literal></term>
+          <listitem>
+           <para>
+            The server must request MD5 hashed password authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>gss</literal></term>
+          <listitem>
+           <para>
+            The server must either request a Kerberos handshake via
+            <acronym>GSSAPI</acronym> or establish a
+            <acronym>GSS</acronym>-encrypted channel (see also
+            <xref linkend="libpq-connect-gssencmode" />).
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>sspi</literal></term>
+          <listitem>
+           <para>
+            The server must request Windows <acronym>SSPI</acronym>
+            authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>scram-sha-256</literal></term>
+          <listitem>
+           <para>
+            The server must successfully complete a SCRAM-SHA-256 authentication
+            exchange with the client.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>none</literal></term>
+          <listitem>
+           <para>
+            The server must not prompt the client for an authentication
+            exchange. (This does not prohibit client certificate authentication
+            via TLS, nor GSS authentication via its encrypted transport.)
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+      </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-channel-binding" xreflabel="channel_binding">
       <term><literal>channel_binding</literal></term>
       <listitem>
@@ -7774,6 +7869,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGREQUIREAUTH</envar></primary>
+      </indexterm>
+      <envar>PGREQUIREAUTH</envar> behaves the same as the <xref
+      linkend="libpq-connect-require-auth"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h
index 66ba359390..5268d442ab 100644
--- a/src/include/libpq/pqcomm.h
+++ b/src/include/libpq/pqcomm.h
@@ -123,6 +123,7 @@ extern PGDLLIMPORT bool Db_user_namespace;
 #define AUTH_REQ_SASL	   10	/* Begin SASL authentication */
 #define AUTH_REQ_SASL_CONT 11	/* Continue SASL authentication */
 #define AUTH_REQ_SASL_FIN  12	/* Final SASL message */
+#define AUTH_REQ_MAX	   AUTH_REQ_SASL_FIN	/* maximum AUTH_REQ_* value */
 
 typedef uint32 AuthRequest;
 
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index 9c42ea4f81..c0abb18368 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -282,6 +282,7 @@ scram_exchange(void *opaq, char *input, int inputlen,
 			}
 			*done = true;
 			state->state = FE_SCRAM_FINISHED;
+			state->conn->client_finished_auth = true;
 			break;
 
 		default:
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 9afc6f19b9..0858c5df12 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -136,7 +136,10 @@ pg_GSS_continue(PGconn *conn, int payloadlen)
 	}
 
 	if (maj_stat == GSS_S_COMPLETE)
+	{
+		conn->client_finished_auth = true;
 		gss_release_name(&lmin_s, &conn->gtarg_nam);
+	}
 
 	return STATUS_OK;
 }
@@ -321,6 +324,9 @@ pg_SSPI_continue(PGconn *conn, int payloadlen)
 		FreeContextBuffer(outbuf.pBuffers[0].pvBuffer);
 	}
 
+	if (r == SEC_E_OK)
+		conn->client_finished_auth = true;
+
 	/* Cleanup is handled by the code in freePGconn() */
 	return STATUS_OK;
 }
@@ -805,6 +811,44 @@ pg_password_sendauth(PGconn *conn, const char *password, AuthRequest areq)
 	return ret;
 }
 
+/*
+ * Translate a disallowed AuthRequest code into an error message.
+ */
+static const char *
+auth_description(AuthRequest areq)
+{
+	switch (areq)
+	{
+		case AUTH_REQ_PASSWORD:
+			return libpq_gettext("server requested a cleartext password");
+		case AUTH_REQ_MD5:
+			return libpq_gettext("server requested a hashed password");
+		case AUTH_REQ_GSS:
+		case AUTH_REQ_GSS_CONT:
+			return libpq_gettext("server requested GSSAPI authentication");
+		case AUTH_REQ_SSPI:
+			return libpq_gettext("server requested SSPI authentication");
+		case AUTH_REQ_SCM_CREDS:
+			return libpq_gettext("server requested UNIX socket credentials");
+		case AUTH_REQ_SASL:
+		case AUTH_REQ_SASL_CONT:
+		case AUTH_REQ_SASL_FIN:
+			return libpq_gettext("server requested SASL authentication");
+	}
+
+	return libpq_gettext("server requested an unknown authentication type");
+}
+
+/*
+ * Convenience macro for checking the allowed_auth_methods bitmask. Caller must
+ * ensure that type is not greater than 31 (high bit of the bitmask).
+ */
+#define auth_allowed(conn, type) \
+	(((conn)->allowed_auth_methods & (1 << (type))) != 0)
+
+StaticAssertDecl(AUTH_REQ_MAX < CHAR_BIT * sizeof(((PGconn){0}).allowed_auth_methods),
+				 "AUTH_REQ_MAX overflows the allowed_auth_methods bitmask");
+
 /*
  * Verify that the authentication request is expected, given the connection
  * parameters. This is especially important when the client wishes to
@@ -814,6 +858,93 @@ static bool
 check_expected_areq(AuthRequest areq, PGconn *conn)
 {
 	bool		result = true;
+	const char *reason = NULL;
+
+	/*
+	 * If the user required a specific auth method, or specified an allowed set,
+	 * then reject all others here, and make sure the server actually completes
+	 * an authentication exchange.
+	 */
+	if (conn->require_auth)
+	{
+		switch (areq)
+		{
+			case AUTH_REQ_OK:
+				/*
+				 * Check to make sure we've actually finished our exchange (or
+				 * else that the user has allowed an authentication-less
+				 * connection).
+				 *
+				 * If the user has allowed both SCRAM and unauthenticated
+				 * (trust) connections, then this check will silently accept
+				 * partial SCRAM exchanges, where a misbehaving server does not
+				 * provide its verifier before sending an OK. This is consistent
+				 * with historical behavior, but it may be a point to revisit in
+				 * the future, since it could allow a server that doesn't know
+				 * the user's password to silently harvest material for a brute
+				 * force attack.
+				 */
+				if (!conn->auth_required || conn->client_finished_auth)
+					break;
+
+				/*
+				 * No explicit authentication request was made by the server --
+				 * or perhaps it was made and not completed, in the case of
+				 * SCRAM -- but there is one special case to check. If the user
+				 * allowed "gss", then a GSS-encrypted channel also satisfies
+				 * the check.
+				 */
+#ifdef ENABLE_GSS
+				if (auth_allowed(conn, AUTH_REQ_GSS) && conn->gssenc)
+				{
+					/*
+					 * If implicit GSS auth has already been performed via GSS
+					 * encryption, we don't need to have performed an
+					 * AUTH_REQ_GSS exchange. This allows require_auth=gss to be
+					 * combined with gssencmode, since there won't be an
+					 * explicit authentication request in that case.
+					 */
+				}
+				else
+#endif
+				{
+					reason = libpq_gettext("server did not complete authentication"),
+					result = false;
+				}
+
+				break;
+
+			case AUTH_REQ_PASSWORD:
+			case AUTH_REQ_MD5:
+			case AUTH_REQ_GSS:
+			case AUTH_REQ_SSPI:
+			case AUTH_REQ_GSS_CONT:
+			case AUTH_REQ_SASL:
+			case AUTH_REQ_SASL_CONT:
+			case AUTH_REQ_SASL_FIN:
+				/*
+				 * We don't handle these with the default case, to avoid
+				 * bit-shifting past the end of the allowed_auth_methods mask if
+				 * the server sends an unexpected AuthRequest.
+				 */
+				result = auth_allowed(conn, areq);
+				break;
+
+			default:
+				result = false;
+				break;
+		}
+	}
+
+	if (!result)
+	{
+		if (!reason)
+			reason = auth_description(areq);
+
+		libpq_append_conn_error(conn, "auth method \"%s\" requirement failed: %s",
+								conn->require_auth, reason);
+		return result;
+	}
 
 	/*
 	 * When channel_binding=require, we must protect against two cases: (1) we
@@ -1008,6 +1139,9 @@ pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn)
 										 "fe_sendauth: error sending password authentication\n");
 					return STATUS_ERROR;
 				}
+
+				/* We expect no further authentication requests. */
+				conn->client_finished_auth = true;
 				break;
 			}
 
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 50b5df3490..abc367b713 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -307,6 +307,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Require-Peer", "", 10,
 	offsetof(struct pg_conn, requirepeer)},
 
+	{"require_auth", "PGREQUIREAUTH", NULL, NULL,
+		"Require-Auth", "", 14, /* sizeof("scram-sha-256") == 14 */
+	offsetof(struct pg_conn, require_auth)},
+
 	{"ssl_min_protocol_version", "PGSSLMINPROTOCOLVERSION", "TLSv1.2", NULL,
 		"SSL-Minimum-Protocol-Version", "", 8,	/* sizeof("TLSv1.x") == 8 */
 	offsetof(struct pg_conn, ssl_min_protocol_version)},
@@ -1237,6 +1241,162 @@ connectOptions2(PGconn *conn)
 		}
 	}
 
+	/*
+	 * parse and validate require_auth option
+	 */
+	if (conn->require_auth)
+	{
+		char	   *s = conn->require_auth;
+		bool		first, more;
+		bool		negated = false;
+
+		/*
+		 * By default, start from an empty set of allowed options and add to it.
+		 */
+		conn->auth_required = true;
+		conn->allowed_auth_methods = 0;
+
+		for (first = true, more = true; more; first = false)
+		{
+			char	   *method, *part;
+			uint32		bits;
+
+			part = parse_comma_separated_list(&s, &more);
+			if (part == NULL)
+				goto oom_error;
+
+			/*
+			 * Check for negation, e.g. '!password'. If one element is negated,
+			 * they all have to be.
+			 */
+			method = part;
+			if (*method == '!')
+			{
+				if (first)
+				{
+					/*
+					 * Switch to a permissive set of allowed options, and
+					 * subtract from it.
+					 */
+					conn->auth_required = false;
+					conn->allowed_auth_methods = -1;
+				}
+				else if (!negated)
+				{
+					conn->status = CONNECTION_BAD;
+					libpq_append_conn_error(conn, "negative require_auth method \"%s\" cannot be mixed with non-negative methods",
+											method);
+
+					free(part);
+					return false;
+				}
+
+				negated = true;
+				method++;
+			}
+			else if (negated)
+			{
+				conn->status = CONNECTION_BAD;
+				libpq_append_conn_error(conn, "require_auth method \"%s\" cannot be mixed with negative methods",
+										method);
+
+				free(part);
+				return false;
+			}
+
+			if (strcmp(method, "password") == 0)
+			{
+				bits = (1 << AUTH_REQ_PASSWORD);
+			}
+			else if (strcmp(method, "md5") == 0)
+			{
+				bits = (1 << AUTH_REQ_MD5);
+			}
+			else if (strcmp(method, "gss") == 0)
+			{
+				bits = (1 << AUTH_REQ_GSS);
+				bits |= (1 << AUTH_REQ_GSS_CONT);
+			}
+			else if (strcmp(method, "sspi") == 0)
+			{
+				bits = (1 << AUTH_REQ_SSPI);
+				bits |= (1 << AUTH_REQ_GSS_CONT);
+			}
+			else if (strcmp(method, "scram-sha-256") == 0)
+			{
+				/* This currently assumes that SCRAM is the only SASL method. */
+				bits = (1 << AUTH_REQ_SASL);
+				bits |= (1 << AUTH_REQ_SASL_CONT);
+				bits |= (1 << AUTH_REQ_SASL_FIN);
+			}
+			else if (strcmp(method, "none") == 0)
+			{
+				/*
+				 * Special case: let the user explicitly allow (or disallow)
+				 * connections where the server does not send an explicit
+				 * authentication challenge, such as "trust" and "cert" auth.
+				 */
+				if (negated) /* "!none" */
+				{
+					if (conn->auth_required)
+						goto duplicate;
+
+					conn->auth_required = true;
+				}
+				else /* "none" */
+				{
+					if (!conn->auth_required)
+						goto duplicate;
+
+					conn->auth_required = false;
+				}
+
+				free(part);
+				continue; /* avoid the bitmask manipulation below */
+			}
+			else
+			{
+				conn->status = CONNECTION_BAD;
+				libpq_append_conn_error(conn, "invalid require_auth method: \"%s\"",
+										method);
+
+				free(part);
+				return false;
+			}
+
+			/* Update the bitmask. */
+			if (negated)
+			{
+				if ((conn->allowed_auth_methods & bits) == 0)
+					goto duplicate;
+
+				conn->allowed_auth_methods &= ~bits;
+			}
+			else
+			{
+				if ((conn->allowed_auth_methods & bits) == bits)
+					goto duplicate;
+
+				conn->allowed_auth_methods |= bits;
+			}
+
+			free(part);
+			continue;
+
+duplicate:
+			/*
+			 * A duplicated method probably indicates a typo in a setting where
+			 * typos are extremely risky.
+			 */
+			conn->status = CONNECTION_BAD;
+			libpq_append_conn_error(conn, "require_auth method \"%s\" is specified more than once",
+									part);
+
+			free(part);
+			return false;
+		}
+	}
+
 	/*
 	 * validate channel_binding option
 	 */
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index d7ec5ed429..f465a36b78 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -396,6 +396,7 @@ struct pg_conn
 	char	   *ssl_min_protocol_version;	/* minimum TLS protocol version */
 	char	   *ssl_max_protocol_version;	/* maximum TLS protocol version */
 	char	   *target_session_attrs;	/* desired session properties */
+	char	   *require_auth;	/* name of the expected auth method */
 
 	/* Optional file to write trace info to */
 	FILE	   *Pfdebug;
@@ -457,6 +458,9 @@ struct pg_conn
 	bool		write_failed;	/* have we had a write failure on sock? */
 	char	   *write_err_msg;	/* write error message, or NULL if OOM */
 
+	bool		auth_required;	/* require an authentication challenge from the server? */
+	uint32		allowed_auth_methods;	/* bitmask of acceptable AuthRequest codes */
+
 	/* Transient state needed while establishing connection */
 	PGTargetServerType target_server_type;	/* desired session properties */
 	bool		try_next_addr;	/* time to advance to next address/host? */
@@ -512,6 +516,9 @@ struct pg_conn
 	bool		error_result;	/* do we need to make an ERROR result? */
 	PGresult   *next_result;	/* next result (used in single-row mode) */
 
+	bool		client_finished_auth; /* have we finished our half of the
+									   * authentication exchange? */
+
 	/* Assorted state for SASL, SSL, GSS, etc */
 	const pg_fe_sasl_mech *sasl;
 	void	   *sasl_state;
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index a2fde1408b..bba98eac79 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -115,6 +115,74 @@ is($res, 't',
 	"users with trust authentication use SYSTEM_USER = NULL in parallel workers"
 );
 
+# All positive require_auth options should fail...
+$node->connect_fails("user=scram_role require_auth=gss",
+	"GSS authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=sspi",
+	"SSPI authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=password",
+	"password authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+$node->connect_fails("user=scram_role require_auth=password,scram-sha-256",
+	"multiple authentication types can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+
+# ...and negative require_auth options should succeed.
+$node->connect_ok("user=scram_role require_auth=!gss",
+	"GSS authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!sspi",
+	"SSPI authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!password",
+	"password authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!md5",
+	"md5 authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!scram-sha-256",
+	"SCRAM authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!password,!scram-sha-256",
+	"multiple authentication types can be forbidden: succeeds with trust auth");
+
+# require_auth=[!]none should interact correctly with trust auth.
+$node->connect_ok("user=scram_role require_auth=none",
+	"all authentication can be forbidden: succeeds with trust auth");
+$node->connect_fails("user=scram_role require_auth=!none",
+	"any authentication can be required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+
+# Negative and positive require_auth options can't be mixed.
+$node->connect_fails("user=scram_role require_auth=scram-sha-256,!md5",
+	"negative require_auth methods can't be mixed with positive",
+	expected_stderr => qr/negative require_auth method "!md5" cannot be mixed with non-negative methods/);
+$node->connect_fails("user=scram_role require_auth=!password,scram-sha-256",
+	"positive require_auth methods can't be mixed with negative",
+	expected_stderr => qr/require_auth method "scram-sha-256" cannot be mixed with negative methods/);
+
+# require_auth methods can't be duplicated.
+$node->connect_fails("user=scram_role require_auth=password,md5,password",
+	"require_auth methods can't be duplicated: positive case",
+	expected_stderr => qr/require_auth method "password" is specified more than once/);
+$node->connect_fails("user=scram_role require_auth=!password,!md5,!password",
+	"require_auth methods can't be duplicated: negative case",
+	expected_stderr => qr/require_auth method "!password" is specified more than once/);
+$node->connect_fails("user=scram_role require_auth=none,md5,none",
+	"require_auth methods can't be duplicated: none case",
+	expected_stderr => qr/require_auth method "none" is specified more than once/);
+$node->connect_fails("user=scram_role require_auth=!none,!md5,!none",
+	"require_auth methods can't be duplicated: !none case",
+	expected_stderr => qr/require_auth method "!none" is specified more than once/);
+
+# Unknown require_auth methods are caught.
+$node->connect_fails("user=scram_role require_auth=none,abcdefg",
+	"unknown require_auth methods are rejected",
+	expected_stderr => qr/invalid require_auth method: "abcdefg"/);
+
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'all', 'all', 'password');
 test_conn($node, 'user=scram_role', 'password', 0,
@@ -124,6 +192,33 @@ test_conn($node, 'user=md5_role', 'password', 0,
 	log_like =>
 	  [qr/connection authenticated: identity="md5_role" method=password/]);
 
+# require_auth should succeed with a plaintext password...
+$node->connect_ok("user=scram_role require_auth=password",
+	"password authentication can be required: works with password auth");
+$node->connect_ok("user=scram_role require_auth=!none",
+	"any authentication can be required: works with password auth");
+$node->connect_ok("user=scram_role require_auth=scram-sha-256,password,md5",
+	"multiple authentication types can be required: works with password auth");
+
+# ...fail for other auth types...
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails("user=scram_role require_auth=none",
+	"all authentication can be forbidden: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+
+# ...and allow password authentication to be prohibited.
+$node->connect_fails("user=scram_role require_auth=!password",
+	"password authentication can be forbidden: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails("user=scram_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types can be forbidden: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+
 # For "scram-sha-256" method, user "scram_role" should be able to connect.
 reset_pg_hba($node, 'all', 'all', 'scram-sha-256');
 test_conn(
@@ -137,6 +232,33 @@ test_conn(
 test_conn($node, 'user=md5_role', 'scram-sha-256', 2,
 	log_unlike => [qr/connection authenticated:/]);
 
+# require_auth should succeed with SCRAM...
+$node->connect_ok("user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: works with SCRAM auth");
+$node->connect_ok("user=scram_role require_auth=!none",
+	"any authentication can be required: works with SCRAM auth");
+$node->connect_ok("user=scram_role require_auth=password,scram-sha-256,md5",
+	"multiple authentication types can be required: works with SCRAM auth");
+
+# ...fail for other auth types...
+$node->connect_fails("user=scram_role require_auth=password",
+	"password authentication can be required: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails("user=scram_role require_auth=md5",
+	"md5 authentication can be required: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails("user=scram_role require_auth=none",
+	"all authentication can be forbidden: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+
+# ...and allow SCRAM authentication to be prohibited.
+$node->connect_fails("user=scram_role require_auth=!scram-sha-256",
+	"SCRAM authentication can be forbidden: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails("user=scram_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types can be forbidden: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+
 # Test that bad passwords are rejected.
 $ENV{"PGPASSWORD"} = 'badpass';
 test_conn($node, 'user=scram_role', 'scram-sha-256', 2,
@@ -153,6 +275,33 @@ test_conn($node, 'user=md5_role', 'md5', 0,
 	log_like =>
 	  [qr/connection authenticated: identity="md5_role" method=md5/]);
 
+# require_auth should succeed with MD5...
+$node->connect_ok("user=md5_role require_auth=md5",
+	"MD5 authentication can be required: works with MD5 auth");
+$node->connect_ok("user=md5_role require_auth=!none",
+	"any authentication can be required: works with MD5 auth");
+$node->connect_ok("user=md5_role require_auth=md5,scram-sha-256,password",
+	"multiple authentication types can be required: works with MD5 auth");
+
+# ...fail for other auth types...
+$node->connect_fails("user=md5_role require_auth=password",
+	"password authentication can be required: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+$node->connect_fails("user=md5_role require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+$node->connect_fails("user=md5_role require_auth=none",
+	"all authentication can be forbidden: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+
+# ...and allow MD5 authentication to be prohibited.
+$node->connect_fails("user=md5_role require_auth=!md5",
+	"password authentication can be forbidden: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+$node->connect_fails("user=md5_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types can be forbidden: fails with MD5 auth",
+	expected_stderr => qr/server requested a hashed password/);
+
 # Test SYSTEM_USER <> NULL with parallel workers.
 $node->safe_psql(
 	'postgres',
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index d610ce63ab..a6d931c9e0 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -325,6 +325,24 @@ test_query(
 	'gssencmode=require',
 	'sending 100K lines works');
 
+# require_auth=gss should succeed...
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=disable require_auth=gss",
+	"GSS authentication can be requested: works with GSS auth without encryption");
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss",
+	"GSS authentication can be requested: works with GSS auth with encryption");
+
+# ...and require_auth=sspi should fail.
+$node->connect_fails(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=disable require_auth=sspi",
+	"SSPI authentication can be requested: fails with GSS auth without encryption",
+	expected_stderr => qr/server requested GSSAPI authentication/);
+$node->connect_fails(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=sspi",
+	"SSPI authentication can be requested: fails with GSS auth with encryption",
+	expected_stderr => qr/server did not complete authentication/);
+
 # Test that SYSTEM_USER works.
 test_query($node, 'test1', 'SELECT SYSTEM_USER;',
 	qr/^gss:test1\@$realm$/s, 'gssencmode=require', 'testing system_user');
@@ -370,6 +388,14 @@ test_access(
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable',
 	'fails with GSS encryption disabled and hostgssenc hba');
 
+# require_auth=gss should succeed.
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss",
+	"GSS authentication can be requested: works with GSS encryption");
+$node->connect_ok(
+	$node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss,scram-sha-256",
+	"multiple authentication types can be requested: works with GSS encryption");
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
 	qq{hostnogssenc all all $hostaddr/32 gss map=mymap});
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index f3ed806ec2..8f5a7dd2b8 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -101,6 +101,12 @@ test_access(
 		qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/
 	],);
 
+# require_auth=password should complete successfully; other methods should fail.
+$node->connect_ok("user=test1 require_auth=password",
+	"password authentication can be required: works with ldap auth");
+$node->connect_fails("user=test1 require_auth=scram-sha-256",
+	"SCRAM authentication can be required: fails with ldap auth");
+
 note "search+bind";
 
 unlink($node->data_dir . '/pg_hba.conf');
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 1d3905d3a1..8f645b9553 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -140,6 +140,31 @@ $node->connect_ok(
 		qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/
 	]);
 
+# channel_binding should continue to function independently of require_auth.
+$node->connect_ok("$common_connstr user=ssltestuser channel_binding=disable require_auth=scram-sha-256",
+	"SCRAM with SSL, channel_binding=disable, and require_auth=scram-sha-256");
+$node->connect_fails(
+	"$common_connstr user=md5testuser require_auth=md5 channel_binding=require",
+	"channel_binding can fail even when require_auth succeeds",
+	expected_stderr =>
+	  qr/channel binding required but not supported by server's authentication request/
+);
+if ($supports_tls_server_end_point)
+{
+	$node->connect_ok(
+		"$common_connstr user=ssltestuser channel_binding=require require_auth=scram-sha-256",
+		"SCRAM with SSL, channel_binding=require, and require_auth=scram-sha-256");
+}
+else
+{
+	$node->connect_fails(
+		"$common_connstr user=ssltestuser channel_binding=require require_auth=scram-sha-256",
+		"SCRAM with SSL, channel_binding=require, and require_auth=scram-sha-256",
+		expected_stderr =>
+		  qr/channel binding is required, but server did not offer an authentication method that supports channel binding/
+	);
+}
+
 # Now test with a server certificate that uses the RSA-PSS algorithm.
 # This checks that the certificate can be loaded and that channel binding
 # works. (see bug #17760)
@@ -153,4 +178,5 @@ if ($supports_rsapss_certs)
 			qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/
 		]);
 }
+
 done_testing();
-- 
2.25.1

#48Jacob Champion
jchampion@timescale.com
In reply to: Jacob Champion (#47)
Re: [PoC] Let libpq reject unexpected authentication requests

On 2/16/23 10:57, Jacob Champion wrote:

v14 rebases over the test and solution conflicts from 9244c11afe2.

Since we're to the final CF for PG16, here's a rough summary.

This patchset provides two related features: 1) the ability for a client
to explicitly allow or deny particular methods of in-band authentication
(that is, things like password exchange), and 2) the ability to withhold
a client certificate from a server that asks for it.

Feature 1 was originally proposed to mitigate abuse where a successful
MITM attack can then be used to fish for client credentials [1]/messages/by-id/fcc3ebeb7f05775b63f3207ed52a54ea5d17fb42.camel@vmware.com. It also
lets users disable undesirable authentication types (like plaintext) by
default, which seems to be a common interest. Both features came up
again in the context of proxies such as postgres_fdw, where it's
sometimes important that users authenticate using only their credentials
and not piggyback on the authority of the proxy host [2]/messages/by-id/20230123015255.h3jro3yyitlsqykp@awork3.anarazel.de. And another
use case for feature 2 just came up independently [3]/messages/by-id/CAAWbhmh_QqCnRVV8ct3gJULReQjWxLTaTBqs+fV7c7FpH0zbew@mail.gmail.com, to fix
connections where the default client certificate isn't valid for a
particular server.

Since this is all client-side, it's compatible with existing servers.
Also since it's client-side, it can't prevent connections from being
established by an eager server; it can only drop the connection once it
sees that its requirement was not met, similar to how we handle
target_session_attrs. That means it can't prevent a login trigger from
being processed on behalf of a confused proxy. (I think that would
require server-side support.)

0001 and 0002 are the core features. 0003 is a more future-looking
refactoring of the internals, to make it easier to handle more SASL
mechanisms, but it's not required and contains some unexercised code.

Thanks,
--Jacob

[1]: /messages/by-id/fcc3ebeb7f05775b63f3207ed52a54ea5d17fb42.camel@vmware.com
/messages/by-id/fcc3ebeb7f05775b63f3207ed52a54ea5d17fb42.camel@vmware.com
[2]: /messages/by-id/20230123015255.h3jro3yyitlsqykp@awork3.anarazel.de
/messages/by-id/20230123015255.h3jro3yyitlsqykp@awork3.anarazel.de
[3]: /messages/by-id/CAAWbhmh_QqCnRVV8ct3gJULReQjWxLTaTBqs+fV7c7FpH0zbew@mail.gmail.com
/messages/by-id/CAAWbhmh_QqCnRVV8ct3gJULReQjWxLTaTBqs+fV7c7FpH0zbew@mail.gmail.com

#49Michael Paquier
michael@paquier.xyz
In reply to: Jacob Champion (#48)
Re: [PoC] Let libpq reject unexpected authentication requests

On Tue, Feb 28, 2023 at 03:38:21PM -0800, Jacob Champion wrote:

0001 and 0002 are the core features. 0003 is a more future-looking
refactoring of the internals, to make it easier to handle more SASL
mechanisms, but it's not required and contains some unexercised code.

I was refreshing my mind with 0001 yesterday, and except for the two
parts where we need to worry about AUTH_REQ_OK being sent too early
and the business with gssenc, this is a rather straight-forward. It
also looks like the the participants of the thread are OK with the
design you are proposing (list of keywords, potentially negative
patterns). I think that I can get this part merged for this CF, at
least, not sure about the rest :p
--
Michael

#50Jacob Champion
jchampion@timescale.com
In reply to: Michael Paquier (#49)
Re: [PoC] Let libpq reject unexpected authentication requests

On Fri, Mar 3, 2023 at 6:35 PM Michael Paquier <michael@paquier.xyz> wrote:

I was refreshing my mind with 0001 yesterday, and except for the two
parts where we need to worry about AUTH_REQ_OK being sent too early
and the business with gssenc, this is a rather straight-forward. It
also looks like the the participants of the thread are OK with the
design you are proposing (list of keywords, potentially negative
patterns). I think that I can get this part merged for this CF, at
least, not sure about the rest :p

Thanks! Is there anything that would make the sslcertmode patch more
palatable? Or any particular areas of concern?

--Jacob

#51Michael Paquier
michael@paquier.xyz
In reply to: Jacob Champion (#50)
1 attachment(s)
Re: [PoC] Let libpq reject unexpected authentication requests

On Mon, Mar 06, 2023 at 04:02:25PM -0800, Jacob Champion wrote:

On Fri, Mar 3, 2023 at 6:35 PM Michael Paquier <michael@paquier.xyz> wrote:

I was refreshing my mind with 0001 yesterday, and except for the two
parts where we need to worry about AUTH_REQ_OK being sent too early
and the business with gssenc, this is a rather straight-forward. It
also looks like the the participants of the thread are OK with the
design you are proposing (list of keywords, potentially negative
patterns). I think that I can get this part merged for this CF, at
least, not sure about the rest :p

Thanks! Is there anything that would make the sslcertmode patch more
palatable? Or any particular areas of concern?

I have been reviewing 0001, finishing with the attached, and that's
nice work. My notes are below.

pqDropServerData() is in charge of cleaning up the transient data of a
connection between different attempts. Shouldn't client_finished_auth
be reset to false there? No parameters related to the connection
parameters should be reset in this code path, but this state is
different. It does not seem possible that we could reach
pqDropServerData() after client_finished_auth has been set to true,
but that feels safer. I was tempted first to do that as well in
makeEmptyPGconn(), but we do a memset(0) there, so there is no point
in doing that anyway ;)

require_auth needs a cleanup in freePGconn().

+       case AUTH_REQ_SCM_CREDS:
+           return libpq_gettext("server requested UNIX socket credentials");
I am not really cool with the fact that this would fail and that we
offer no options to control that.  Now, this involves servers up to
9.1, which is also a very good to rip of this code entirely.  For now,
I think that we'd better allow this option, and discuss the removal of
that in a separate thread.

pgindent has been complaining on the StaticAssertDecl() in fe-auth.c:
src/interfaces/libpq/fe-auth.c: Error@847: Unbalanced parens
Warning@847: Extra )
Warning@847: Extra )
Warning@848: Extra )

From what I can see, this comes from the use of {0} within the
expression itself. I don't really want to dig into why pg_bsd_indent
thinks this is a bad idea, so let's just move the StaticAssertDecl() a
bit, like in the attached. The result is the same.

As of the "sensitive" cases of the patch:
- I don't really think that we have to care much of the cases like
"none,scram" meaning that a SASL exchange hastily downgraded to
AUTH_REQ_OK by the server would be a success, as "none" means that the
client is basically OK with trust-level. This said, "none" could be a
dangerous option in some cases, while useful in others.
- SSPI is the default connection setup for the TAP tests on Windows.
We could stick a small test somewhere, perhaps, certainly not in
src/test/authentication/.
- SASL/SCRAM is indeed a problem on its own. My guess is that we
should let channel_binding do the job for SASL, or introduce a new
option to decide which sasl mechanisms are authorized. At the end,
using "scram-sha-256" as the keyword is fine by me as we use that even
for HBA files, so that's quite known now, I hope.
--
Michael

Attachments:

v15-0001-libpq-let-client-reject-unexpected-auth-methods.patchtext/x-diff; charset=us-asciiDownload
From 4c4eaf6989e60676df4a597e7831abf35133cfc5 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 9 Mar 2023 15:26:39 +0900
Subject: [PATCH v15] libpq: let client reject unexpected auth methods

The require_auth connection option allows the client to choose a list of
acceptable authentication types for use with the server. There is no
negotiation: if the server does not present one of the allowed
authentication requests, the connection fails. Additionally, all methods
in the list may be negated, e.g. '!password', in which case the server
must NOT use the listed authentication type. The special method "none"
allows/disallows the use of unauthenticated connections (but it does not
govern transport-level authentication via TLS or GSSAPI).

Internally, the patch expands the role of check_expected_areq() to
ensure that the incoming request is compatible with conn->require_auth.
It also introduces a new flag, conn->client_finished_auth, which is set
by various authentication routines when the client side of the handshake
is finished. This signals to check_expected_areq() that an OK message
from the server is expected, and allows the client to complain if the
server forgoes authentication entirely.

(Since the client can't generally prove that the server is actually
doing the work of authentication, this last part is mostly useful for
SCRAM without channel binding. It could also provide a client with a
decent signal that, at the very least, it's not connecting to a database
with trust auth, and so the connection can be tied to the client in a
later audit.)

Deficiencies:
- This is unlikely to be very forwards-compatible at the moment,
  especially with SASL/SCRAM.
- SSPI support is "implemented" but untested.
- require_auth=none,scram-sha-256 currently allows the server to leave a
  SCRAM exchange unfinished. This is not net-new behavior but may be
  surprising.
---
 src/include/libpq/pqcomm.h                |   1 +
 src/interfaces/libpq/fe-auth-scram.c      |   1 +
 src/interfaces/libpq/fe-auth.c            | 139 +++++++++++++
 src/interfaces/libpq/fe-connect.c         | 170 ++++++++++++++++
 src/interfaces/libpq/libpq-int.h          |   9 +
 src/test/authentication/t/001_password.pl | 227 ++++++++++++++++++++++
 src/test/kerberos/t/001_auth.pl           |  36 ++++
 src/test/ldap/t/001_auth.pl               |   6 +
 src/test/ssl/t/002_scram.pl               |  28 +++
 doc/src/sgml/libpq.sgml                   | 115 +++++++++++
 10 files changed, 732 insertions(+)

diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h
index 66ba359390..5268d442ab 100644
--- a/src/include/libpq/pqcomm.h
+++ b/src/include/libpq/pqcomm.h
@@ -123,6 +123,7 @@ extern PGDLLIMPORT bool Db_user_namespace;
 #define AUTH_REQ_SASL	   10	/* Begin SASL authentication */
 #define AUTH_REQ_SASL_CONT 11	/* Continue SASL authentication */
 #define AUTH_REQ_SASL_FIN  12	/* Final SASL message */
+#define AUTH_REQ_MAX	   AUTH_REQ_SASL_FIN	/* maximum AUTH_REQ_* value */
 
 typedef uint32 AuthRequest;
 
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index 12c3d0bc33..277f72b280 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -282,6 +282,7 @@ scram_exchange(void *opaq, char *input, int inputlen,
 			}
 			*done = true;
 			state->state = FE_SCRAM_FINISHED;
+			state->conn->client_finished_auth = true;
 			break;
 
 		default:
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index ab454e6cd0..1bcdc07625 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -136,7 +136,10 @@ pg_GSS_continue(PGconn *conn, int payloadlen)
 	}
 
 	if (maj_stat == GSS_S_COMPLETE)
+	{
+		conn->client_finished_auth = true;
 		gss_release_name(&lmin_s, &conn->gtarg_nam);
+	}
 
 	return STATUS_OK;
 }
@@ -321,6 +324,9 @@ pg_SSPI_continue(PGconn *conn, int payloadlen)
 		FreeContextBuffer(outbuf.pBuffers[0].pvBuffer);
 	}
 
+	if (r == SEC_E_OK)
+		conn->client_finished_auth = true;
+
 	/* Cleanup is handled by the code in freePGconn() */
 	return STATUS_OK;
 }
@@ -735,6 +741,8 @@ pg_local_sendauth(PGconn *conn)
 						  strerror_r(errno, sebuf, sizeof(sebuf)));
 		return STATUS_ERROR;
 	}
+
+	conn->client_finished_auth = true;
 	return STATUS_OK;
 #else
 	libpq_append_conn_error(conn, "SCM_CRED authentication method not supported");
@@ -805,6 +813,41 @@ pg_password_sendauth(PGconn *conn, const char *password, AuthRequest areq)
 	return ret;
 }
 
+/*
+ * Translate a disallowed AuthRequest code into an error message.
+ */
+static const char *
+auth_method_description(AuthRequest areq)
+{
+	switch (areq)
+	{
+		case AUTH_REQ_PASSWORD:
+			return libpq_gettext("server requested a cleartext password");
+		case AUTH_REQ_MD5:
+			return libpq_gettext("server requested a hashed password");
+		case AUTH_REQ_GSS:
+		case AUTH_REQ_GSS_CONT:
+			return libpq_gettext("server requested GSSAPI authentication");
+		case AUTH_REQ_SSPI:
+			return libpq_gettext("server requested SSPI authentication");
+		case AUTH_REQ_SCM_CREDS:
+			return libpq_gettext("server requested UNIX socket credentials");
+		case AUTH_REQ_SASL:
+		case AUTH_REQ_SASL_CONT:
+		case AUTH_REQ_SASL_FIN:
+			return libpq_gettext("server requested SASL authentication");
+	}
+
+	return libpq_gettext("server requested an unknown authentication type");
+}
+
+/*
+ * Convenience macro for checking the allowed_auth_methods bitmask. Caller must
+ * ensure that type is not greater than 31 (high bit of the bitmask).
+ */
+#define auth_method_allowed(conn, type) \
+	(((conn)->allowed_auth_methods & (1 << (type))) != 0)
+
 /*
  * Verify that the authentication request is expected, given the connection
  * parameters. This is especially important when the client wishes to
@@ -814,6 +857,99 @@ static bool
 check_expected_areq(AuthRequest areq, PGconn *conn)
 {
 	bool		result = true;
+	const char *reason = NULL;
+
+	StaticAssertDecl((sizeof(conn->allowed_auth_methods) * CHAR_BIT) > AUTH_REQ_MAX,
+					 "AUTH_REQ_MAX overflows the allowed_auth_methods bitmask");
+
+	/*
+	 * If the user required a specific auth method, or specified an allowed
+	 * set, then reject all others here, and make sure the server actually
+	 * completes an authentication exchange.
+	 */
+	if (conn->require_auth)
+	{
+		switch (areq)
+		{
+			case AUTH_REQ_OK:
+
+				/*
+				 * Check to make sure we've actually finished our exchange (or
+				 * else that the user has allowed an authentication-less
+				 * connection).
+				 *
+				 * If the user has allowed both SCRAM and unauthenticated
+				 * (trust) connections, then this check will silently accept
+				 * partial SCRAM exchanges, where a misbehaving server does
+				 * not provide its verifier before sending an OK.  This is
+				 * consistent with historical behavior, but it may be a point
+				 * to revisit in the future, since it could allow a server
+				 * that doesn't know the user's password to silently harvest
+				 * material for a brute force attack.
+				 */
+				if (!conn->auth_required || conn->client_finished_auth)
+					break;
+
+				/*
+				 * No explicit authentication request was made by the server
+				 * -- or perhaps it was made and not completed, in the case of
+				 * SCRAM -- but there is one special case to check.  If the
+				 * user allowed "gss", then a GSS-encrypted channel also
+				 * satisfies the check.
+				 */
+#ifdef ENABLE_GSS
+				if (auth_method_allowed(conn, AUTH_REQ_GSS) && conn->gssenc)
+				{
+					/*
+					 * If implicit GSS auth has already been performed via GSS
+					 * encryption, we don't need to have performed an
+					 * AUTH_REQ_GSS exchange.  This allows require_auth=gss to
+					 * be combined with gssencmode, since there won't be an
+					 * explicit authentication request in that case.
+					 */
+				}
+				else
+#endif
+				{
+					reason = libpq_gettext("server did not complete authentication"),
+						result = false;
+				}
+
+				break;
+
+			case AUTH_REQ_PASSWORD:
+			case AUTH_REQ_MD5:
+			case AUTH_REQ_GSS:
+			case AUTH_REQ_GSS_CONT:
+			case AUTH_REQ_SSPI:
+			case AUTH_REQ_SCM_CREDS:
+			case AUTH_REQ_SASL:
+			case AUTH_REQ_SASL_CONT:
+			case AUTH_REQ_SASL_FIN:
+
+				/*
+				 * We don't handle these with the default case, to avoid
+				 * bit-shifting past the end of the allowed_auth_methods mask
+				 * if the server sends an unexpected AuthRequest.
+				 */
+				result = auth_method_allowed(conn, areq);
+				break;
+
+			default:
+				result = false;
+				break;
+		}
+	}
+
+	if (!result)
+	{
+		if (!reason)
+			reason = auth_method_description(areq);
+
+		libpq_append_conn_error(conn, "auth method \"%s\" requirement failed: %s",
+								conn->require_auth, reason);
+		return result;
+	}
 
 	/*
 	 * When channel_binding=require, we must protect against two cases: (1) we
@@ -1008,6 +1144,9 @@ pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn)
 										 "fe_sendauth: error sending password authentication\n");
 					return STATUS_ERROR;
 				}
+
+				/* We expect no further authentication requests. */
+				conn->client_finished_auth = true;
 				break;
 			}
 
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 97e47f0585..8cd0d918bf 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -307,6 +307,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Require-Peer", "", 10,
 	offsetof(struct pg_conn, requirepeer)},
 
+	{"require_auth", "PGREQUIREAUTH", NULL, NULL,
+		"Require-Auth", "", 14, /* sizeof("scram-sha-256") == 14 */
+	offsetof(struct pg_conn, require_auth)},
+
 	{"ssl_min_protocol_version", "PGSSLMINPROTOCOLVERSION", "TLSv1.2", NULL,
 		"SSL-Minimum-Protocol-Version", "", 8,	/* sizeof("TLSv1.x") == 8 */
 	offsetof(struct pg_conn, ssl_min_protocol_version)},
@@ -595,6 +599,7 @@ pqDropServerData(PGconn *conn)
 	/* Reset assorted other per-connection state */
 	conn->last_sqlstate[0] = '\0';
 	conn->auth_req_received = false;
+	conn->client_finished_auth = false;
 	conn->password_needed = false;
 	conn->write_failed = false;
 	free(conn->write_err_msg);
@@ -1237,6 +1242,170 @@ connectOptions2(PGconn *conn)
 		}
 	}
 
+	/*
+	 * parse and validate require_auth option
+	 */
+	if (conn->require_auth && conn->require_auth[0])
+	{
+		char	   *s = conn->require_auth;
+		bool		first,
+					more;
+		bool		negated = false;
+
+		/*
+		 * By default, start from an empty set of allowed options and add to
+		 * it.
+		 */
+		conn->auth_required = true;
+		conn->allowed_auth_methods = 0;
+
+		for (first = true, more = true; more; first = false)
+		{
+			char	   *method,
+					   *part;
+			uint32		bits;
+
+			part = parse_comma_separated_list(&s, &more);
+			if (part == NULL)
+				goto oom_error;
+
+			/*
+			 * Check for negation, e.g. '!password'. If one element is
+			 * negated, they all have to be.
+			 */
+			method = part;
+			if (*method == '!')
+			{
+				if (first)
+				{
+					/*
+					 * Switch to a permissive set of allowed options, and
+					 * subtract from it.
+					 */
+					conn->auth_required = false;
+					conn->allowed_auth_methods = -1;
+				}
+				else if (!negated)
+				{
+					conn->status = CONNECTION_BAD;
+					libpq_append_conn_error(conn, "negative require_auth method \"%s\" cannot be mixed with non-negative methods",
+											method);
+
+					free(part);
+					return false;
+				}
+
+				negated = true;
+				method++;
+			}
+			else if (negated)
+			{
+				conn->status = CONNECTION_BAD;
+				libpq_append_conn_error(conn, "require_auth method \"%s\" cannot be mixed with negative methods",
+										method);
+
+				free(part);
+				return false;
+			}
+
+			if (strcmp(method, "password") == 0)
+			{
+				bits = (1 << AUTH_REQ_PASSWORD);
+			}
+			else if (strcmp(method, "md5") == 0)
+			{
+				bits = (1 << AUTH_REQ_MD5);
+			}
+			else if (strcmp(method, "gss") == 0)
+			{
+				bits = (1 << AUTH_REQ_GSS);
+				bits |= (1 << AUTH_REQ_GSS_CONT);
+			}
+			else if (strcmp(method, "sspi") == 0)
+			{
+				bits = (1 << AUTH_REQ_SSPI);
+				bits |= (1 << AUTH_REQ_GSS_CONT);
+			}
+			else if (strcmp(method, "scram-sha-256") == 0)
+			{
+				/* This currently assumes that SCRAM is the only SASL method. */
+				bits = (1 << AUTH_REQ_SASL);
+				bits |= (1 << AUTH_REQ_SASL_CONT);
+				bits |= (1 << AUTH_REQ_SASL_FIN);
+			}
+			else if (strcmp(method, "creds") == 0)
+			{
+				bits = (1 << AUTH_REQ_SCM_CREDS);
+			}
+			else if (strcmp(method, "none") == 0)
+			{
+				/*
+				 * Special case: let the user explicitly allow (or disallow)
+				 * connections where the server does not send an explicit
+				 * authentication challenge, such as "trust" and "cert" auth.
+				 */
+				if (negated)	/* "!none" */
+				{
+					if (conn->auth_required)
+						goto duplicate;
+
+					conn->auth_required = true;
+				}
+				else			/* "none" */
+				{
+					if (!conn->auth_required)
+						goto duplicate;
+
+					conn->auth_required = false;
+				}
+
+				free(part);
+				continue;		/* avoid the bitmask manipulation below */
+			}
+			else
+			{
+				conn->status = CONNECTION_BAD;
+				libpq_append_conn_error(conn, "invalid require_auth method: \"%s\"",
+										method);
+
+				free(part);
+				return false;
+			}
+
+			/* Update the bitmask. */
+			if (negated)
+			{
+				if ((conn->allowed_auth_methods & bits) == 0)
+					goto duplicate;
+
+				conn->allowed_auth_methods &= ~bits;
+			}
+			else
+			{
+				if ((conn->allowed_auth_methods & bits) == bits)
+					goto duplicate;
+
+				conn->allowed_auth_methods |= bits;
+			}
+
+			free(part);
+			continue;
+
+	duplicate:
+
+			/*
+			 * A duplicated method probably indicates a typo in a setting
+			 * where typos are extremely risky.
+			 */
+			conn->status = CONNECTION_BAD;
+			libpq_append_conn_error(conn, "require_auth method \"%s\" is specified more than once",
+									part);
+
+			free(part);
+			return false;
+		}
+	}
+
 	/*
 	 * validate channel_binding option
 	 */
@@ -4050,6 +4219,7 @@ freePGconn(PGconn *conn)
 	free(conn->sslcompression);
 	free(conn->sslsni);
 	free(conn->requirepeer);
+	free(conn->require_auth);
 	free(conn->ssl_min_protocol_version);
 	free(conn->ssl_max_protocol_version);
 	free(conn->gssencmode);
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index d7ec5ed429..1dc264fe54 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -396,6 +396,7 @@ struct pg_conn
 	char	   *ssl_min_protocol_version;	/* minimum TLS protocol version */
 	char	   *ssl_max_protocol_version;	/* maximum TLS protocol version */
 	char	   *target_session_attrs;	/* desired session properties */
+	char	   *require_auth;	/* name of the expected auth method */
 
 	/* Optional file to write trace info to */
 	FILE	   *Pfdebug;
@@ -457,6 +458,14 @@ struct pg_conn
 	bool		write_failed;	/* have we had a write failure on sock? */
 	char	   *write_err_msg;	/* write error message, or NULL if OOM */
 
+	bool		auth_required;	/* require an authentication challenge from
+								 * the server? */
+	uint32		allowed_auth_methods;	/* bitmask of acceptable AuthRequest
+										 * codes */
+	bool		client_finished_auth;	/* have we finished our half of the
+										 * authentication exchange? */
+
+
 	/* Transient state needed while establishing connection */
 	PGTargetServerType target_server_type;	/* desired session properties */
 	bool		try_next_addr;	/* time to advance to next address/host? */
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index a2fde1408b..75b862dd0f 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -115,6 +115,109 @@ is($res, 't',
 	"users with trust authentication use SYSTEM_USER = NULL in parallel workers"
 );
 
+# All these values of require_auth should fail, as trust is expected.
+$node->connect_fails(
+	"user=scram_role require_auth=gss",
+	"GSS authentication required, fails with trust auth",
+	expected_stderr =>
+	  qr/auth method "gss" requirement failed: server did not complete authentication/
+);
+$node->connect_fails(
+	"user=scram_role require_auth=sspi",
+	"SSPI authentication required, fails with trust auth",
+	expected_stderr =>
+	  qr/auth method "sspi" requirement failed: server did not complete authentication/
+);
+$node->connect_fails(
+	"user=scram_role require_auth=password",
+	"password authentication required: fails with trust auth",
+	expected_stderr =>
+	  qr/auth method "password" requirement failed: server did not complete authentication/
+);
+$node->connect_fails(
+	"user=scram_role require_auth=md5",
+	"MD5 authentication required: fails with trust auth",
+	expected_stderr =>
+	  qr/auth method "md5" requirement failed: server did not complete authentication/
+);
+$node->connect_fails(
+	"user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication required: fails with trust auth",
+	expected_stderr =>
+	  qr/auth method "scram-sha-256" requirement failed: server did not complete authentication/
+);
+$node->connect_fails(
+	"user=scram_role require_auth=password,scram-sha-256",
+	"password and SCRAM authentication required: fails with trust auth",
+	expected_stderr =>
+	  qr/auth method "password,scram-sha-256" requirement failed: server did not complete authentication/
+);
+
+# These negative patterns of require_auth should succeed.
+$node->connect_ok("user=scram_role require_auth=!gss",
+	"GSS authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!sspi",
+	"SSPI authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!password",
+	"password authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!md5",
+	"md5 authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!scram-sha-256",
+	"SCRAM authentication can be forbidden: succeeds with trust auth");
+$node->connect_ok(
+	"user=scram_role require_auth=!password,!scram-sha-256",
+	"multiple authentication types forbidden: succeeds with trust auth");
+
+# require_auth=[!]none should interact correctly with trust auth.
+$node->connect_ok("user=scram_role require_auth=none",
+	"all authentication types forbidden: succeeds with trust auth");
+$node->connect_fails(
+	"user=scram_role require_auth=!none",
+	"any authentication types required: fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+
+# Negative and positive require_auth methods can't be mixed.
+$node->connect_fails(
+	"user=scram_role require_auth=scram-sha-256,!md5",
+	"negative require_auth methods cannot be mixed with positive ones",
+	expected_stderr =>
+	  qr/negative require_auth method "!md5" cannot be mixed with non-negative methods/
+);
+$node->connect_fails(
+	"user=scram_role require_auth=!password,!none,scram-sha-256",
+	"positive require_auth methods cannot be mixed with negative one",
+	expected_stderr =>
+	  qr/require_auth method "scram-sha-256" cannot be mixed with negative methods/
+);
+
+# require_auth methods cannot have duplicated values.
+$node->connect_fails(
+	"user=scram_role require_auth=password,md5,password",
+	"require_auth methods cannot include duplicates: positive case",
+	expected_stderr =>
+	  qr/require_auth method "password" is specified more than once/);
+$node->connect_fails(
+	"user=scram_role require_auth=!password,!md5,!password",
+	"require_auth methods cannot be duplicated: negative case",
+	expected_stderr =>
+	  qr/require_auth method "!password" is specified more than once/);
+$node->connect_fails(
+	"user=scram_role require_auth=none,md5,none",
+	"require_auth methods cannot be duplicated: none case",
+	expected_stderr =>
+	  qr/require_auth method "none" is specified more than once/);
+$node->connect_fails(
+	"user=scram_role require_auth=!none,!md5,!none",
+	"require_auth methods cannot be duplicated: !none case",
+	expected_stderr =>
+	  qr/require_auth method "!none" is specified more than once/);
+
+# Unknown value defined in require_auth.
+$node->connect_fails(
+	"user=scram_role require_auth=none,abcdefg",
+	"unknown require_auth methods are rejected",
+	expected_stderr => qr/invalid require_auth method: "abcdefg"/);
+
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'all', 'all', 'password');
 test_conn($node, 'user=scram_role', 'password', 0,
@@ -124,6 +227,47 @@ test_conn($node, 'user=md5_role', 'password', 0,
 	log_like =>
 	  [qr/connection authenticated: identity="md5_role" method=password/]);
 
+# require_auth succeeds here with a plaintext password.
+$node->connect_ok("user=scram_role require_auth=password",
+	"password authentication required: works with password auth");
+$node->connect_ok("user=scram_role require_auth=!none",
+	"any authentication required: works with password auth");
+$node->connect_ok(
+	"user=scram_role require_auth=scram-sha-256,password,md5",
+	"multiple authentication types required: works with password auth");
+
+# require_auth fails for other authentication types.
+$node->connect_fails(
+	"user=scram_role require_auth=md5",
+	"md5 authentication required: fails with password auth",
+	expected_stderr =>
+	  qr/auth method "md5" requirement failed: server requested a cleartext password/
+);
+$node->connect_fails(
+	"user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication required: fails with password auth",
+	expected_stderr =>
+	  qr/auth method "scram-sha-256" requirement failed: server requested a cleartext password/
+);
+$node->connect_fails(
+	"user=scram_role require_auth=none",
+	"all authentication forbidden: fails with password auth",
+	expected_stderr =>
+	  qr/auth method "none" requirement failed: server requested a cleartext password/
+);
+
+# Disallowing password authentication fails, even if requested by server.
+$node->connect_fails(
+	"user=scram_role require_auth=!password",
+	"password authentication forbidden: fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails(
+	"user=scram_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types forbidden: fails with password auth",
+	expected_stderr =>
+	  qr/ method "!password,!md5,!scram-sha-256" requirement failed: server requested a cleartext password/
+);
+
 # For "scram-sha-256" method, user "scram_role" should be able to connect.
 reset_pg_hba($node, 'all', 'all', 'scram-sha-256');
 test_conn(
@@ -137,6 +281,46 @@ test_conn(
 test_conn($node, 'user=md5_role', 'scram-sha-256', 2,
 	log_unlike => [qr/connection authenticated:/]);
 
+# require_auth should succeeds with SCRAM when it is required.
+$node->connect_ok(
+	"user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication required: works with SCRAM auth");
+$node->connect_ok("user=scram_role require_auth=!none",
+	"any authentication required: works with SCRAM auth");
+$node->connect_ok(
+	"user=scram_role require_auth=password,scram-sha-256,md5",
+	"multiple authentication types required: works with SCRAM auth");
+
+# Authentication fails for other authentication types.
+$node->connect_fails(
+	"user=scram_role require_auth=password",
+	"password authentication required: fails with SCRAM auth",
+	expected_stderr =>
+	  qr/auth method "password" requirement failed: server requested SASL authentication/
+);
+$node->connect_fails(
+	"user=scram_role require_auth=md5",
+	"md5 authentication required: fails with SCRAM auth",
+	expected_stderr =>
+	  qr/auth method "md5" requirement failed: server requested SASL authentication/
+);
+$node->connect_fails(
+	"user=scram_role require_auth=none",
+	"all authentication forbidden: fails with SCRAM auth",
+	expected_stderr =>
+	  qr/auth method "none" requirement failed: server requested SASL authentication/
+);
+
+# Authentication fails if SCRAM authentication is forbidden.
+$node->connect_fails(
+	"user=scram_role require_auth=!scram-sha-256",
+	"SCRAM authentication forbidden: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails(
+	"user=scram_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types forbidden: fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+
 # Test that bad passwords are rejected.
 $ENV{"PGPASSWORD"} = 'badpass';
 test_conn($node, 'user=scram_role', 'scram-sha-256', 2,
@@ -153,6 +337,49 @@ test_conn($node, 'user=md5_role', 'md5', 0,
 	log_like =>
 	  [qr/connection authenticated: identity="md5_role" method=md5/]);
 
+# require_auth succeeds with MD5 required.
+$node->connect_ok("user=md5_role require_auth=md5",
+	"MD5 authentication required: works with MD5 auth");
+$node->connect_ok("user=md5_role require_auth=!none",
+	"any authentication required: works with MD5 auth");
+$node->connect_ok(
+	"user=md5_role require_auth=md5,scram-sha-256,password",
+	"multiple authentication types required: works with MD5 auth");
+
+# Authentication fails if other types are required.
+$node->connect_fails(
+	"user=md5_role require_auth=password",
+	"password authentication required: fails with MD5 auth",
+	expected_stderr =>
+	  qr/auth method "password" requirement failed: server requested a hashed password/
+);
+$node->connect_fails(
+	"user=md5_role require_auth=scram-sha-256",
+	"SCRAM authentication required: fails with MD5 auth",
+	expected_stderr =>
+	  qr/auth method "scram-sha-256" requirement failed: server requested a hashed password/
+);
+$node->connect_fails(
+	"user=md5_role require_auth=none",
+	"all authentication types forbidden: fails with MD5 auth",
+	expected_stderr =>
+	  qr/auth method "none" requirement failed: server requested a hashed password/
+);
+
+# Authentication fails if MD5 is forbidden.
+$node->connect_fails(
+	"user=md5_role require_auth=!md5",
+	"password authentication forbidden: fails with MD5 auth",
+	expected_stderr =>
+	  qr/auth method "!md5" requirement failed: server requested a hashed password/
+);
+$node->connect_fails(
+	"user=md5_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types forbidden: fails with MD5 auth",
+	expected_stderr =>
+	  qr/auth method "!password,!md5,!scram-sha-256" requirement failed: server requested a hashed password/
+);
+
 # Test SYSTEM_USER <> NULL with parallel workers.
 $node->safe_psql(
 	'postgres',
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index d610ce63ab..cfed2b10d4 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -325,6 +325,32 @@ test_query(
 	'gssencmode=require',
 	'sending 100K lines works');
 
+# require_auth=gss succeeds if required.
+$node->connect_ok(
+	$node->connstr('postgres')
+	  . " user=test1 host=$host hostaddr=$hostaddr gssencmode=disable require_auth=gss",
+	"GSS authentication requested: works with GSS auth without encryption");
+$node->connect_ok(
+	$node->connstr('postgres')
+	  . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss",
+	"GSS authentication requested: works with GSS auth with encryption");
+
+# require_auth=sspi fails if required.
+$node->connect_fails(
+	$node->connstr('postgres')
+	  . " user=test1 host=$host hostaddr=$hostaddr gssencmode=disable require_auth=sspi",
+	"SSPI authentication requested: fails with GSS auth without encryption",
+	expected_stderr =>
+	  qr/auth method "sspi" requirement failed: server requested GSSAPI authentication/
+);
+$node->connect_fails(
+	$node->connstr('postgres')
+	  . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=sspi",
+	"SSPI authentication requested: fails with GSS auth with encryption",
+	expected_stderr =>
+	  qr/auth method "sspi" requirement failed: server did not complete authentication/
+);
+
 # Test that SYSTEM_USER works.
 test_query($node, 'test1', 'SELECT SYSTEM_USER;',
 	qr/^gss:test1\@$realm$/s, 'gssencmode=require', 'testing system_user');
@@ -370,6 +396,16 @@ test_access(
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable',
 	'fails with GSS encryption disabled and hostgssenc hba');
 
+# require_auth=gss succeeds if required.
+$node->connect_ok(
+	$node->connstr('postgres')
+	  . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss",
+	"GSS authentication requested: works with GSS encryption");
+$node->connect_ok(
+	$node->connstr('postgres')
+	  . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss,scram-sha-256",
+	"multiple authentication types requested: works with GSS encryption");
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
 	qq{hostnogssenc all all $hostaddr/32 gss map=mymap});
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index f3ed806ec2..8b7c9d0ba8 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -101,6 +101,12 @@ test_access(
 		qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/
 	],);
 
+# require_auth=password should complete successfully; other methods should fail.
+$node->connect_ok("user=test1 require_auth=password",
+	"password authentication required: works with ldap auth");
+$node->connect_fails("user=test1 require_auth=scram-sha-256",
+	"SCRAM authentication required: fails with ldap auth");
+
 note "search+bind";
 
 unlink($node->data_dir . '/pg_hba.conf');
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 1d3905d3a1..8038135697 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -140,6 +140,34 @@ $node->connect_ok(
 		qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/
 	]);
 
+# channel_binding should continue to work independently of require_auth.
+$node->connect_ok(
+	"$common_connstr user=ssltestuser channel_binding=disable require_auth=scram-sha-256",
+	"SCRAM with SSL, channel_binding=disable, and require_auth=scram-sha-256"
+);
+$node->connect_fails(
+	"$common_connstr user=md5testuser require_auth=md5 channel_binding=require",
+	"channel_binding can fail even when require_auth succeeds",
+	expected_stderr =>
+	  qr/channel binding required but not supported by server's authentication request/
+);
+if ($supports_tls_server_end_point)
+{
+	$node->connect_ok(
+		"$common_connstr user=ssltestuser channel_binding=require require_auth=scram-sha-256",
+		"SCRAM with SSL, channel_binding=require, and require_auth=scram-sha-256"
+	);
+}
+else
+{
+	$node->connect_fails(
+		"$common_connstr user=ssltestuser channel_binding=require require_auth=scram-sha-256",
+		"SCRAM with SSL, channel_binding=require, and require_auth=scram-sha-256",
+		expected_stderr =>
+		  qr/channel binding is required, but server did not offer an authentication method that supports channel binding/
+	);
+}
+
 # Now test with a server certificate that uses the RSA-PSS algorithm.
 # This checks that the certificate can be loaded and that channel binding
 # works. (see bug #17760)
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 3ccd8ff942..3706d349ab 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1220,6 +1220,111 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-require-auth" xreflabel="require_auth">
+      <term><literal>require_auth</literal></term>
+      <listitem>
+      <para>
+        Specifies the authentication method that the client requires from the
+        server. If the server does not use the required method to authenticate
+        the client, or if the authentication handshake is not fully completed by
+        the server, the connection will fail. A comma-separated list of methods
+        may also be provided, of which the server must use exactly one in order
+        for the connection to succeed. By default, any authentication method is
+        accepted, and the server is free to skip authentication altogether.
+      </para>
+      <para>
+        Methods may be negated with the addition of a <literal>!</literal>
+        prefix, in which case the server must <emphasis>not</emphasis> attempt
+        the listed method; any other method is accepted, and the server is free
+        not to authenticate the client at all. If a comma-separated list is
+        provided, the server may not attempt <emphasis>any</emphasis> of the
+        listed negated methods. Negated and non-negated forms may not be
+        combined in the same setting.
+      </para>
+      <para>
+        As a final special case, the <literal>none</literal> method requires the
+        server not to use an authentication challenge. (It may also be negated,
+        to require some form of authentication.)
+      </para>
+      <para>
+        The following methods may be specified:
+
+        <variablelist>
+         <varlistentry>
+          <term><literal>password</literal></term>
+          <listitem>
+           <para>
+            The server must request plaintext password authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>md5</literal></term>
+          <listitem>
+           <para>
+            The server must request MD5 hashed password authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>gss</literal></term>
+          <listitem>
+           <para>
+            The server must either request a Kerberos handshake via
+            <acronym>GSSAPI</acronym> or establish a
+            <acronym>GSS</acronym>-encrypted channel (see also
+            <xref linkend="libpq-connect-gssencmode" />).
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>sspi</literal></term>
+          <listitem>
+           <para>
+            The server must request Windows <acronym>SSPI</acronym>
+            authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>scram-sha-256</literal></term>
+          <listitem>
+           <para>
+            The server must successfully complete a SCRAM-SHA-256 authentication
+            exchange with the client.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>creds</literal></term>
+          <listitem>
+           <para>
+            The server must request SCM credential authentication (deprecated
+            as of <productname>PostgreSQL</productname> 9.1).
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>none</literal></term>
+          <listitem>
+           <para>
+            The server must not prompt the client for an authentication
+            exchange. (This does not prohibit client certificate authentication
+            via TLS, nor GSS authentication via its encrypted transport.)
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+      </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-channel-binding" xreflabel="channel_binding">
       <term><literal>channel_binding</literal></term>
       <listitem>
@@ -7774,6 +7879,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGREQUIREAUTH</envar></primary>
+      </indexterm>
+      <envar>PGREQUIREAUTH</envar> behaves the same as the <xref
+      linkend="libpq-connect-require-auth"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
-- 
2.39.2

#52Jacob Champion
jchampion@timescale.com
In reply to: Michael Paquier (#51)
Re: [PoC] Let libpq reject unexpected authentication requests

On 3/8/23 22:35, Michael Paquier wrote:

I have been reviewing 0001, finishing with the attached, and that's
nice work. My notes are below.

Thanks!

pqDropServerData() is in charge of cleaning up the transient data of a
connection between different attempts. Shouldn't client_finished_auth
be reset to false there? No parameters related to the connection
parameters should be reset in this code path, but this state is
different. It does not seem possible that we could reach
pqDropServerData() after client_finished_auth has been set to true,
but that feels safer.

Yeah, that seems reasonable.

+       case AUTH_REQ_SCM_CREDS:
+           return libpq_gettext("server requested UNIX socket credentials");
I am not really cool with the fact that this would fail and that we
offer no options to control that.  Now, this involves servers up to
9.1, which is also a very good to rip of this code entirely.  For now,
I think that we'd better allow this option, and discuss the removal of
that in a separate thread.

Fair enough.

pgindent has been complaining on the StaticAssertDecl() in fe-auth.c:
src/interfaces/libpq/fe-auth.c: Error@847: Unbalanced parens
Warning@847: Extra )
Warning@847: Extra )
Warning@848: Extra )

From what I can see, this comes from the use of {0} within the
expression itself. I don't really want to dig into why pg_bsd_indent
thinks this is a bad idea, so let's just move the StaticAssertDecl() a
bit, like in the attached. The result is the same.

Works for me. I wonder if

sizeof(((PGconn*) 0)->allowed_auth_methods)

would make pgindent any happier? That'd let you keep the assertion local
to auth_method_allowed, but it looks scarier. :)

As of the "sensitive" cases of the patch:
- I don't really think that we have to care much of the cases like
"none,scram" meaning that a SASL exchange hastily downgraded to
AUTH_REQ_OK by the server would be a success, as "none" means that the
client is basically OK with trust-level. This said, "none" could be a
dangerous option in some cases, while useful in others.

Yeah. I think a server shouldn't be allowed to abandon a SCRAM exchange
partway through, but that's completely independent of this patchset.

- SSPI is the default connection setup for the TAP tests on Windows.

Oh, I don't think I ever noticed that.

We could stick a small test somewhere, perhaps, certainly not in
src/test/authentication/.

Where were you thinking? (Would it be so bad to have a tiny
t/005_sspi.pl that's just skipped on *nix?)

- SASL/SCRAM is indeed a problem on its own. My guess is that we
should let channel_binding do the job for SASL, or introduce a new
option to decide which sasl mechanisms are authorized. At the end,
using "scram-sha-256" as the keyword is fine by me as we use that even
for HBA files, so that's quite known now, I hope.

Did you have any thoughts about the 0003 generalization attempt?

-+  if (conn->require_auth)
++  if (conn->require_auth && conn->require_auth[0])

Thank you for that catch. I guess we should test somewhere that
`require_auth=` behaves normally?

+                  reason = libpq_gettext("server did not complete authentication"),
-+                  result = false;
++                      result = false;
+              }

This reindentation looks odd.

nit: some of the new TAP test names have been rewritten with commas,
others with colons.

Thanks,
--Jacob

#53Michael Paquier
michael@paquier.xyz
In reply to: Jacob Champion (#52)
Re: [PoC] Let libpq reject unexpected authentication requests

On Fri, Mar 10, 2023 at 02:32:17PM -0800, Jacob Champion wrote:

On 3/8/23 22:35, Michael Paquier wrote:
Works for me. I wonder if

sizeof(((PGconn*) 0)->allowed_auth_methods)

would make pgindent any happier? That'd let you keep the assertion local
to auth_method_allowed, but it looks scarier. :)

I can check that, now it's not bad to keep the assertion as it is,
either.

As of the "sensitive" cases of the patch:
- I don't really think that we have to care much of the cases like
"none,scram" meaning that a SASL exchange hastily downgraded to
AUTH_REQ_OK by the server would be a success, as "none" means that the
client is basically OK with trust-level. This said, "none" could be a
dangerous option in some cases, while useful in others.

Yeah. I think a server shouldn't be allowed to abandon a SCRAM exchange
partway through, but that's completely independent of this patchset.

Agreed.

We could stick a small test somewhere, perhaps, certainly not in
src/test/authentication/.

Where were you thinking? (Would it be so bad to have a tiny
t/005_sspi.pl that's just skipped on *nix?)

Hmm, OK. It may be worth having a 005_sspi.pl in
src/test/authentication/ specifically for Windows. This patch gives
at least one reason to do so. Looking at pg_regress.c, we have that:
if (config_auth_datadir)
{
#ifdef ENABLE_SSPI
if (!use_unix_sockets)
config_sspi_auth(config_auth_datadir, user);
#endif
exit(0);
}

So applying a check on $use_unix_sockets should be OK, I hope.

- SASL/SCRAM is indeed a problem on its own. My guess is that we
should let channel_binding do the job for SASL, or introduce a new
option to decide which sasl mechanisms are authorized. At the end,
using "scram-sha-256" as the keyword is fine by me as we use that even
for HBA files, so that's quite known now, I hope.

Did you have any thoughts about the 0003 generalization attempt?

Not yet, unfortunately.

-+  if (conn->require_auth)
++  if (conn->require_auth && conn->require_auth[0])

Thank you for that catch. I guess we should test somewhere that
`require_auth=` behaves normally?

Yeah, that seems like an idea. That would be cheap enough.

+                  reason = libpq_gettext("server did not complete authentication"),
-+                  result = false;
++                      result = false;
+              }

This reindentation looks odd.

That's because the previous line has a comma. So the reindent is
right, not the code.

nit: some of the new TAP test names have been rewritten with commas,
others with colons.

Indeed, I thought to have caught all of them, but you wrote a lot of
tests :)

Could you send a new patch with all these adjustments? That would
help a lot.
--
Michael

#54Jacob Champion
jchampion@timescale.com
In reply to: Michael Paquier (#53)
Re: [PoC] Let libpq reject unexpected authentication requests

On Fri, Mar 10, 2023 at 3:09 PM Michael Paquier <michael@paquier.xyz> wrote:

+                  reason = libpq_gettext("server did not complete authentication"),
-+                  result = false;
++                      result = false;
+              }

This reindentation looks odd.

That's because the previous line has a comma. So the reindent is
right, not the code.

Whoops. :(

Could you send a new patch with all these adjustments? That would
help a lot.

Will do!

Thanks,
--Jacob

#55Jacob Champion
jchampion@timescale.com
In reply to: Jacob Champion (#54)
3 attachment(s)
Re: [PoC] Let libpq reject unexpected authentication requests

On Fri, Mar 10, 2023 at 3:16 PM Jacob Champion <jchampion@timescale.com> wrote:

Could you send a new patch with all these adjustments? That would
help a lot.

Will do!

Here's a v16:
- updated 0001 patch message
- all test names should have commas rather than colons now
- new test for an empty require_auth
- new SSPI suite (note that it doesn't run by default on Cirrus, due
to the use of PG_TEST_USE_UNIX_SOCKETS)
- fixed errant comma at EOL

Thanks,
--Jacob

Attachments:

v16-0003-require_auth-decouple-SASL-and-SCRAM.patchtext/x-patch; charset=US-ASCII; name=v16-0003-require_auth-decouple-SASL-and-SCRAM.patchDownload
From 8cc020598c7c939e3a45088f18ca04ea801fc87e Mon Sep 17 00:00:00 2001
From: Jacob Champion <jchampion@timescale.com>
Date: Tue, 18 Oct 2022 16:55:36 -0700
Subject: [PATCH v16 3/3] require_auth: decouple SASL and SCRAM

Rather than assume that an AUTH_REQ_SASL* code refers to SCRAM-SHA-256,
future-proof by separating the single allowlist into a list of allowed
authentication request codes and a list of allowed SASL mechanisms.

The require_auth check is now separated into two tiers. The
AUTH_REQ_SASL* codes are always allowed. If the server sends one,
responsibility for the check then falls to pg_SASL_init(), which
compares the selected mechanism against the list of allowed mechanisms.
(Other SCRAM code is already responsible for rejecting unexpected or
out-of-order AUTH_REQ_SASL_* codes, so that's not explicitly handled
with this addition.)

Since there's only one recognized SASL mechanism, conn->sasl_mechs
currently only points at static hardcoded lists. Whenever a second
mechanism is added, the list will need to be managed dynamically.
---
 src/interfaces/libpq/fe-auth.c            | 34 +++++++++++++++++++
 src/interfaces/libpq/fe-connect.c         | 41 +++++++++++++++++++----
 src/interfaces/libpq/libpq-int.h          |  3 +-
 src/test/authentication/t/001_password.pl | 14 +++++---
 src/test/ssl/t/002_scram.pl               |  6 ++++
 5 files changed, 86 insertions(+), 12 deletions(-)

diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 53c7d30eff..7927aebed8 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -522,6 +522,40 @@ pg_SASL_init(PGconn *conn, int payloadlen)
 		goto error;
 	}
 
+	/*
+	 * Before going ahead with any SASL exchange, ensure that the user has
+	 * allowed (or, alternatively, has not forbidden) this particular mechanism.
+	 *
+	 * In a hypothetical future where a server responds with multiple SASL
+	 * mechanism families, we would need to instead consult this list up above,
+	 * during mechanism negotiation. We don't live in that world yet. The server
+	 * presents one auth method and we decide whether that's acceptable or not.
+	 */
+	if (conn->sasl_mechs)
+	{
+		const char **mech;
+		bool		found = false;
+
+		Assert(conn->require_auth);
+
+		for (mech = conn->sasl_mechs; *mech; mech++)
+		{
+			if (strcmp(*mech, selected_mechanism) == 0)
+			{
+				found = true;
+				break;
+			}
+		}
+
+		if ((conn->sasl_mechs_denied && found)
+			|| (!conn->sasl_mechs_denied && !found))
+		{
+			libpq_append_conn_error(conn, "auth method \"%s\" requirement failed: server requested unacceptable SASL mechanism \"%s\"",
+									conn->require_auth, selected_mechanism);
+			goto error;
+		}
+	}
+
 	if (conn->channel_binding[0] == 'r' &&	/* require */
 		strcmp(selected_mechanism, SCRAM_SHA_256_PLUS_NAME) != 0)
 	{
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index cbadb3f6af..a048793b46 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -1258,12 +1258,25 @@ connectOptions2(PGconn *conn)
 					more;
 		bool		negated = false;
 
+		static const uint32 default_methods = (
+			1 << AUTH_REQ_SASL
+			| 1 << AUTH_REQ_SASL_CONT
+			| 1 << AUTH_REQ_SASL_FIN
+		);
+		static const char *no_mechs[] = { NULL };
+
 		/*
-		 * By default, start from an empty set of allowed options and add to
+		 * By default, start from a minimum set of allowed options and add to
 		 * it.
+		 *
+		 * NB: The SASL method codes are always "allowed" here. If the server
+		 * requests SASL auth, pg_SASL_init() will enforce adherence to the
+		 * sasl_mechs list, which by default is empty.
 		 */
 		conn->auth_required = true;
-		conn->allowed_auth_methods = 0;
+		conn->allowed_auth_methods = default_methods;
+		conn->sasl_mechs = no_mechs;
+		conn->sasl_mechs_denied = false;
 
 		for (first = true, more = true; more; first = false)
 		{
@@ -1290,6 +1303,9 @@ connectOptions2(PGconn *conn)
 					 */
 					conn->auth_required = false;
 					conn->allowed_auth_methods = -1;
+
+					/* conn->sasl_mechs is now a list of denied mechanisms. */
+					conn->sasl_mechs_denied = true;
 				}
 				else if (!negated)
 				{
@@ -1334,10 +1350,23 @@ connectOptions2(PGconn *conn)
 			}
 			else if (strcmp(method, "scram-sha-256") == 0)
 			{
-				/* This currently assumes that SCRAM is the only SASL method. */
-				bits = (1 << AUTH_REQ_SASL);
-				bits |= (1 << AUTH_REQ_SASL_CONT);
-				bits |= (1 << AUTH_REQ_SASL_FIN);
+				static const char *scram_mechs[] = {
+					SCRAM_SHA_256_NAME,
+					SCRAM_SHA_256_PLUS_NAME,
+					NULL /* list terminator */
+				};
+
+				/*
+				 * This currently assumes that SCRAM is the only SASL method.
+				 * Once a second mechanism is added, this code will need to add
+				 * to the list instead of replacing it wholesale.
+				 */
+				if (conn->sasl_mechs[0])
+					goto duplicate;
+				conn->sasl_mechs = scram_mechs;
+
+				free(part);
+				continue; /* avoid the bitmask manipulation below */
 			}
 			else if (strcmp(method, "creds") == 0)
 			{
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index f1f1d973cc..ab26292586 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -465,7 +465,8 @@ struct pg_conn
 										 * codes */
 	bool		client_finished_auth;	/* have we finished our half of the
 										 * authentication exchange? */
-
+	const char **sasl_mechs;	/* list of allowed/denied SASL mechanisms */
+	bool		sasl_mechs_denied;	/* is the sasl_mechs list forbidden? */
 
 	/* Transient state needed while establishing connection */
 	PGTargetServerType target_server_type;	/* desired session properties */
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index cba5d7d648..015532893c 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -301,30 +301,34 @@ $node->connect_fails(
 	"user=scram_role require_auth=password",
 	"password authentication required, fails with SCRAM auth",
 	expected_stderr =>
-	  qr/auth method "password" requirement failed: server requested SASL authentication/
+	  qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256"/
 );
 $node->connect_fails(
 	"user=scram_role require_auth=md5",
 	"md5 authentication required, fails with SCRAM auth",
 	expected_stderr =>
-	  qr/auth method "md5" requirement failed: server requested SASL authentication/
+	  qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256"/
 );
 $node->connect_fails(
 	"user=scram_role require_auth=none",
 	"all authentication forbidden, fails with SCRAM auth",
 	expected_stderr =>
-	  qr/auth method "none" requirement failed: server requested SASL authentication/
+	  qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256"/
 );
 
 # Authentication fails if SCRAM authentication is forbidden.
 $node->connect_fails(
 	"user=scram_role require_auth=!scram-sha-256",
 	"SCRAM authentication forbidden, fails with SCRAM auth",
-	expected_stderr => qr/server requested SASL authentication/);
+	expected_stderr =>
+	  qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256"/
+);
 $node->connect_fails(
 	"user=scram_role require_auth=!password,!md5,!scram-sha-256",
 	"multiple authentication types forbidden, fails with SCRAM auth",
-	expected_stderr => qr/server requested SASL authentication/);
+	expected_stderr =>
+	  qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256"/
+);
 
 # Test that bad passwords are rejected.
 $ENV{"PGPASSWORD"} = 'badpass';
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 8038135697..173ac8d86b 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -157,6 +157,12 @@ if ($supports_tls_server_end_point)
 		"$common_connstr user=ssltestuser channel_binding=require require_auth=scram-sha-256",
 		"SCRAM with SSL, channel_binding=require, and require_auth=scram-sha-256"
 	);
+	$node->connect_fails(
+		"$common_connstr user=ssltestuser channel_binding=require require_auth=password",
+		"SCRAM with SSL, channel_binding=require, and require_auth=password",
+		expected_stderr =>
+		  qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256-PLUS"/
+	);
 }
 else
 {
-- 
2.25.1

v16-0001-libpq-let-client-reject-unexpected-auth-methods.patchtext/x-patch; charset=US-ASCII; name=v16-0001-libpq-let-client-reject-unexpected-auth-methods.patchDownload
From 437add51a7b617d720334011745619404fba3f9e Mon Sep 17 00:00:00 2001
From: Jacob Champion <jchampion@timescale.com>
Date: Mon, 13 Mar 2023 12:05:39 -0700
Subject: [PATCH v16 1/3] libpq: let client reject unexpected auth methods

The require_auth connection option allows the client to choose a list of
acceptable authentication types for use with the server. There is no
negotiation: if the server does not present one of the allowed
authentication requests, the connection fails. Additionally, all methods
in the list may be negated, e.g. '!password', in which case the server
must NOT use the listed authentication type. The special method "none"
allows/disallows the use of unauthenticated connections (but it does not
govern transport-level authentication via TLS or GSSAPI).

Internally, the patch expands the role of check_expected_areq() to
ensure that the incoming request is compatible with conn->require_auth.
It also introduces a new flag, conn->client_finished_auth, which is set
by various authentication routines when the client side of the handshake
is finished. This signals to check_expected_areq() that an OK message
from the server is expected, and allows the client to complain if the
server forgoes authentication entirely.

(Since the client can't generally prove that the server is actually
doing the work of authentication, this last part is mostly useful for
SCRAM without channel binding. It could also provide a client with a
decent signal that, at the very least, it's not connecting to a database
with trust auth; this allows proxies to ensure that their clients are
actually using their assigned credentials.)

Deficiencies:
- This is unlikely to be very forwards-compatible at the moment,
  especially with SASL/SCRAM.
---
 doc/src/sgml/libpq.sgml                   | 115 +++++++++++
 src/include/libpq/pqcomm.h                |   1 +
 src/interfaces/libpq/fe-auth-scram.c      |   1 +
 src/interfaces/libpq/fe-auth.c            | 139 +++++++++++++
 src/interfaces/libpq/fe-connect.c         | 170 ++++++++++++++++
 src/interfaces/libpq/libpq-int.h          |   9 +
 src/test/authentication/meson.build       |   1 +
 src/test/authentication/t/001_password.pl | 232 ++++++++++++++++++++++
 src/test/authentication/t/005_sspi.pl     |  40 ++++
 src/test/kerberos/t/001_auth.pl           |  36 ++++
 src/test/ldap/t/001_auth.pl               |   6 +
 src/test/ssl/t/002_scram.pl               |  28 +++
 12 files changed, 778 insertions(+)
 create mode 100644 src/test/authentication/t/005_sspi.pl

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 3ccd8ff942..3706d349ab 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1220,6 +1220,111 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-require-auth" xreflabel="require_auth">
+      <term><literal>require_auth</literal></term>
+      <listitem>
+      <para>
+        Specifies the authentication method that the client requires from the
+        server. If the server does not use the required method to authenticate
+        the client, or if the authentication handshake is not fully completed by
+        the server, the connection will fail. A comma-separated list of methods
+        may also be provided, of which the server must use exactly one in order
+        for the connection to succeed. By default, any authentication method is
+        accepted, and the server is free to skip authentication altogether.
+      </para>
+      <para>
+        Methods may be negated with the addition of a <literal>!</literal>
+        prefix, in which case the server must <emphasis>not</emphasis> attempt
+        the listed method; any other method is accepted, and the server is free
+        not to authenticate the client at all. If a comma-separated list is
+        provided, the server may not attempt <emphasis>any</emphasis> of the
+        listed negated methods. Negated and non-negated forms may not be
+        combined in the same setting.
+      </para>
+      <para>
+        As a final special case, the <literal>none</literal> method requires the
+        server not to use an authentication challenge. (It may also be negated,
+        to require some form of authentication.)
+      </para>
+      <para>
+        The following methods may be specified:
+
+        <variablelist>
+         <varlistentry>
+          <term><literal>password</literal></term>
+          <listitem>
+           <para>
+            The server must request plaintext password authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>md5</literal></term>
+          <listitem>
+           <para>
+            The server must request MD5 hashed password authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>gss</literal></term>
+          <listitem>
+           <para>
+            The server must either request a Kerberos handshake via
+            <acronym>GSSAPI</acronym> or establish a
+            <acronym>GSS</acronym>-encrypted channel (see also
+            <xref linkend="libpq-connect-gssencmode" />).
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>sspi</literal></term>
+          <listitem>
+           <para>
+            The server must request Windows <acronym>SSPI</acronym>
+            authentication.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>scram-sha-256</literal></term>
+          <listitem>
+           <para>
+            The server must successfully complete a SCRAM-SHA-256 authentication
+            exchange with the client.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>creds</literal></term>
+          <listitem>
+           <para>
+            The server must request SCM credential authentication (deprecated
+            as of <productname>PostgreSQL</productname> 9.1).
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>none</literal></term>
+          <listitem>
+           <para>
+            The server must not prompt the client for an authentication
+            exchange. (This does not prohibit client certificate authentication
+            via TLS, nor GSS authentication via its encrypted transport.)
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+      </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-channel-binding" xreflabel="channel_binding">
       <term><literal>channel_binding</literal></term>
       <listitem>
@@ -7774,6 +7879,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGREQUIREAUTH</envar></primary>
+      </indexterm>
+      <envar>PGREQUIREAUTH</envar> behaves the same as the <xref
+      linkend="libpq-connect-require-auth"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h
index 66ba359390..5268d442ab 100644
--- a/src/include/libpq/pqcomm.h
+++ b/src/include/libpq/pqcomm.h
@@ -123,6 +123,7 @@ extern PGDLLIMPORT bool Db_user_namespace;
 #define AUTH_REQ_SASL	   10	/* Begin SASL authentication */
 #define AUTH_REQ_SASL_CONT 11	/* Continue SASL authentication */
 #define AUTH_REQ_SASL_FIN  12	/* Final SASL message */
+#define AUTH_REQ_MAX	   AUTH_REQ_SASL_FIN	/* maximum AUTH_REQ_* value */
 
 typedef uint32 AuthRequest;
 
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index 12c3d0bc33..277f72b280 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -282,6 +282,7 @@ scram_exchange(void *opaq, char *input, int inputlen,
 			}
 			*done = true;
 			state->state = FE_SCRAM_FINISHED;
+			state->conn->client_finished_auth = true;
 			break;
 
 		default:
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index ab454e6cd0..8ce5b60a3d 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -136,7 +136,10 @@ pg_GSS_continue(PGconn *conn, int payloadlen)
 	}
 
 	if (maj_stat == GSS_S_COMPLETE)
+	{
+		conn->client_finished_auth = true;
 		gss_release_name(&lmin_s, &conn->gtarg_nam);
+	}
 
 	return STATUS_OK;
 }
@@ -321,6 +324,9 @@ pg_SSPI_continue(PGconn *conn, int payloadlen)
 		FreeContextBuffer(outbuf.pBuffers[0].pvBuffer);
 	}
 
+	if (r == SEC_E_OK)
+		conn->client_finished_auth = true;
+
 	/* Cleanup is handled by the code in freePGconn() */
 	return STATUS_OK;
 }
@@ -735,6 +741,8 @@ pg_local_sendauth(PGconn *conn)
 						  strerror_r(errno, sebuf, sizeof(sebuf)));
 		return STATUS_ERROR;
 	}
+
+	conn->client_finished_auth = true;
 	return STATUS_OK;
 #else
 	libpq_append_conn_error(conn, "SCM_CRED authentication method not supported");
@@ -805,6 +813,41 @@ pg_password_sendauth(PGconn *conn, const char *password, AuthRequest areq)
 	return ret;
 }
 
+/*
+ * Translate a disallowed AuthRequest code into an error message.
+ */
+static const char *
+auth_method_description(AuthRequest areq)
+{
+	switch (areq)
+	{
+		case AUTH_REQ_PASSWORD:
+			return libpq_gettext("server requested a cleartext password");
+		case AUTH_REQ_MD5:
+			return libpq_gettext("server requested a hashed password");
+		case AUTH_REQ_GSS:
+		case AUTH_REQ_GSS_CONT:
+			return libpq_gettext("server requested GSSAPI authentication");
+		case AUTH_REQ_SSPI:
+			return libpq_gettext("server requested SSPI authentication");
+		case AUTH_REQ_SCM_CREDS:
+			return libpq_gettext("server requested UNIX socket credentials");
+		case AUTH_REQ_SASL:
+		case AUTH_REQ_SASL_CONT:
+		case AUTH_REQ_SASL_FIN:
+			return libpq_gettext("server requested SASL authentication");
+	}
+
+	return libpq_gettext("server requested an unknown authentication type");
+}
+
+/*
+ * Convenience macro for checking the allowed_auth_methods bitmask. Caller must
+ * ensure that type is not greater than 31 (high bit of the bitmask).
+ */
+#define auth_method_allowed(conn, type) \
+	(((conn)->allowed_auth_methods & (1 << (type))) != 0)
+
 /*
  * Verify that the authentication request is expected, given the connection
  * parameters. This is especially important when the client wishes to
@@ -814,6 +857,99 @@ static bool
 check_expected_areq(AuthRequest areq, PGconn *conn)
 {
 	bool		result = true;
+	const char *reason = NULL;
+
+	StaticAssertDecl((sizeof(conn->allowed_auth_methods) * CHAR_BIT) > AUTH_REQ_MAX,
+					 "AUTH_REQ_MAX overflows the allowed_auth_methods bitmask");
+
+	/*
+	 * If the user required a specific auth method, or specified an allowed
+	 * set, then reject all others here, and make sure the server actually
+	 * completes an authentication exchange.
+	 */
+	if (conn->require_auth)
+	{
+		switch (areq)
+		{
+			case AUTH_REQ_OK:
+
+				/*
+				 * Check to make sure we've actually finished our exchange (or
+				 * else that the user has allowed an authentication-less
+				 * connection).
+				 *
+				 * If the user has allowed both SCRAM and unauthenticated
+				 * (trust) connections, then this check will silently accept
+				 * partial SCRAM exchanges, where a misbehaving server does
+				 * not provide its verifier before sending an OK.  This is
+				 * consistent with historical behavior, but it may be a point
+				 * to revisit in the future, since it could allow a server
+				 * that doesn't know the user's password to silently harvest
+				 * material for a brute force attack.
+				 */
+				if (!conn->auth_required || conn->client_finished_auth)
+					break;
+
+				/*
+				 * No explicit authentication request was made by the server
+				 * -- or perhaps it was made and not completed, in the case of
+				 * SCRAM -- but there is one special case to check.  If the
+				 * user allowed "gss", then a GSS-encrypted channel also
+				 * satisfies the check.
+				 */
+#ifdef ENABLE_GSS
+				if (auth_method_allowed(conn, AUTH_REQ_GSS) && conn->gssenc)
+				{
+					/*
+					 * If implicit GSS auth has already been performed via GSS
+					 * encryption, we don't need to have performed an
+					 * AUTH_REQ_GSS exchange.  This allows require_auth=gss to
+					 * be combined with gssencmode, since there won't be an
+					 * explicit authentication request in that case.
+					 */
+				}
+				else
+#endif
+				{
+					reason = libpq_gettext("server did not complete authentication");
+					result = false;
+				}
+
+				break;
+
+			case AUTH_REQ_PASSWORD:
+			case AUTH_REQ_MD5:
+			case AUTH_REQ_GSS:
+			case AUTH_REQ_GSS_CONT:
+			case AUTH_REQ_SSPI:
+			case AUTH_REQ_SCM_CREDS:
+			case AUTH_REQ_SASL:
+			case AUTH_REQ_SASL_CONT:
+			case AUTH_REQ_SASL_FIN:
+
+				/*
+				 * We don't handle these with the default case, to avoid
+				 * bit-shifting past the end of the allowed_auth_methods mask
+				 * if the server sends an unexpected AuthRequest.
+				 */
+				result = auth_method_allowed(conn, areq);
+				break;
+
+			default:
+				result = false;
+				break;
+		}
+	}
+
+	if (!result)
+	{
+		if (!reason)
+			reason = auth_method_description(areq);
+
+		libpq_append_conn_error(conn, "auth method \"%s\" requirement failed: %s",
+								conn->require_auth, reason);
+		return result;
+	}
 
 	/*
 	 * When channel_binding=require, we must protect against two cases: (1) we
@@ -1008,6 +1144,9 @@ pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn)
 										 "fe_sendauth: error sending password authentication\n");
 					return STATUS_ERROR;
 				}
+
+				/* We expect no further authentication requests. */
+				conn->client_finished_auth = true;
 				break;
 			}
 
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 5638b223cb..dd4b98e099 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -307,6 +307,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Require-Peer", "", 10,
 	offsetof(struct pg_conn, requirepeer)},
 
+	{"require_auth", "PGREQUIREAUTH", NULL, NULL,
+		"Require-Auth", "", 14, /* sizeof("scram-sha-256") == 14 */
+	offsetof(struct pg_conn, require_auth)},
+
 	{"ssl_min_protocol_version", "PGSSLMINPROTOCOLVERSION", "TLSv1.2", NULL,
 		"SSL-Minimum-Protocol-Version", "", 8,	/* sizeof("TLSv1.x") == 8 */
 	offsetof(struct pg_conn, ssl_min_protocol_version)},
@@ -595,6 +599,7 @@ pqDropServerData(PGconn *conn)
 	/* Reset assorted other per-connection state */
 	conn->last_sqlstate[0] = '\0';
 	conn->auth_req_received = false;
+	conn->client_finished_auth = false;
 	conn->password_needed = false;
 	conn->write_failed = false;
 	free(conn->write_err_msg);
@@ -1237,6 +1242,170 @@ connectOptions2(PGconn *conn)
 		}
 	}
 
+	/*
+	 * parse and validate require_auth option
+	 */
+	if (conn->require_auth && conn->require_auth[0])
+	{
+		char	   *s = conn->require_auth;
+		bool		first,
+					more;
+		bool		negated = false;
+
+		/*
+		 * By default, start from an empty set of allowed options and add to
+		 * it.
+		 */
+		conn->auth_required = true;
+		conn->allowed_auth_methods = 0;
+
+		for (first = true, more = true; more; first = false)
+		{
+			char	   *method,
+					   *part;
+			uint32		bits;
+
+			part = parse_comma_separated_list(&s, &more);
+			if (part == NULL)
+				goto oom_error;
+
+			/*
+			 * Check for negation, e.g. '!password'. If one element is
+			 * negated, they all have to be.
+			 */
+			method = part;
+			if (*method == '!')
+			{
+				if (first)
+				{
+					/*
+					 * Switch to a permissive set of allowed options, and
+					 * subtract from it.
+					 */
+					conn->auth_required = false;
+					conn->allowed_auth_methods = -1;
+				}
+				else if (!negated)
+				{
+					conn->status = CONNECTION_BAD;
+					libpq_append_conn_error(conn, "negative require_auth method \"%s\" cannot be mixed with non-negative methods",
+											method);
+
+					free(part);
+					return false;
+				}
+
+				negated = true;
+				method++;
+			}
+			else if (negated)
+			{
+				conn->status = CONNECTION_BAD;
+				libpq_append_conn_error(conn, "require_auth method \"%s\" cannot be mixed with negative methods",
+										method);
+
+				free(part);
+				return false;
+			}
+
+			if (strcmp(method, "password") == 0)
+			{
+				bits = (1 << AUTH_REQ_PASSWORD);
+			}
+			else if (strcmp(method, "md5") == 0)
+			{
+				bits = (1 << AUTH_REQ_MD5);
+			}
+			else if (strcmp(method, "gss") == 0)
+			{
+				bits = (1 << AUTH_REQ_GSS);
+				bits |= (1 << AUTH_REQ_GSS_CONT);
+			}
+			else if (strcmp(method, "sspi") == 0)
+			{
+				bits = (1 << AUTH_REQ_SSPI);
+				bits |= (1 << AUTH_REQ_GSS_CONT);
+			}
+			else if (strcmp(method, "scram-sha-256") == 0)
+			{
+				/* This currently assumes that SCRAM is the only SASL method. */
+				bits = (1 << AUTH_REQ_SASL);
+				bits |= (1 << AUTH_REQ_SASL_CONT);
+				bits |= (1 << AUTH_REQ_SASL_FIN);
+			}
+			else if (strcmp(method, "creds") == 0)
+			{
+				bits = (1 << AUTH_REQ_SCM_CREDS);
+			}
+			else if (strcmp(method, "none") == 0)
+			{
+				/*
+				 * Special case: let the user explicitly allow (or disallow)
+				 * connections where the server does not send an explicit
+				 * authentication challenge, such as "trust" and "cert" auth.
+				 */
+				if (negated)	/* "!none" */
+				{
+					if (conn->auth_required)
+						goto duplicate;
+
+					conn->auth_required = true;
+				}
+				else			/* "none" */
+				{
+					if (!conn->auth_required)
+						goto duplicate;
+
+					conn->auth_required = false;
+				}
+
+				free(part);
+				continue;		/* avoid the bitmask manipulation below */
+			}
+			else
+			{
+				conn->status = CONNECTION_BAD;
+				libpq_append_conn_error(conn, "invalid require_auth method: \"%s\"",
+										method);
+
+				free(part);
+				return false;
+			}
+
+			/* Update the bitmask. */
+			if (negated)
+			{
+				if ((conn->allowed_auth_methods & bits) == 0)
+					goto duplicate;
+
+				conn->allowed_auth_methods &= ~bits;
+			}
+			else
+			{
+				if ((conn->allowed_auth_methods & bits) == bits)
+					goto duplicate;
+
+				conn->allowed_auth_methods |= bits;
+			}
+
+			free(part);
+			continue;
+
+	duplicate:
+
+			/*
+			 * A duplicated method probably indicates a typo in a setting
+			 * where typos are extremely risky.
+			 */
+			conn->status = CONNECTION_BAD;
+			libpq_append_conn_error(conn, "require_auth method \"%s\" is specified more than once",
+									part);
+
+			free(part);
+			return false;
+		}
+	}
+
 	/*
 	 * validate channel_binding option
 	 */
@@ -4055,6 +4224,7 @@ freePGconn(PGconn *conn)
 	free(conn->sslcompression);
 	free(conn->sslsni);
 	free(conn->requirepeer);
+	free(conn->require_auth);
 	free(conn->ssl_min_protocol_version);
 	free(conn->ssl_max_protocol_version);
 	free(conn->gssencmode);
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index d7ec5ed429..1dc264fe54 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -396,6 +396,7 @@ struct pg_conn
 	char	   *ssl_min_protocol_version;	/* minimum TLS protocol version */
 	char	   *ssl_max_protocol_version;	/* maximum TLS protocol version */
 	char	   *target_session_attrs;	/* desired session properties */
+	char	   *require_auth;	/* name of the expected auth method */
 
 	/* Optional file to write trace info to */
 	FILE	   *Pfdebug;
@@ -457,6 +458,14 @@ struct pg_conn
 	bool		write_failed;	/* have we had a write failure on sock? */
 	char	   *write_err_msg;	/* write error message, or NULL if OOM */
 
+	bool		auth_required;	/* require an authentication challenge from
+								 * the server? */
+	uint32		allowed_auth_methods;	/* bitmask of acceptable AuthRequest
+										 * codes */
+	bool		client_finished_auth;	/* have we finished our half of the
+										 * authentication exchange? */
+
+
 	/* Transient state needed while establishing connection */
 	PGTargetServerType target_server_type;	/* desired session properties */
 	bool		try_next_addr;	/* time to advance to next address/host? */
diff --git a/src/test/authentication/meson.build b/src/test/authentication/meson.build
index 3fe279fc10..e501041785 100644
--- a/src/test/authentication/meson.build
+++ b/src/test/authentication/meson.build
@@ -10,6 +10,7 @@ tests += {
       't/002_saslprep.pl',
       't/003_peer.pl',
       't/004_file_inclusion.pl',
+      't/005_sspi.pl',
     ],
   },
 }
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index a2fde1408b..cba5d7d648 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -115,6 +115,114 @@ is($res, 't',
 	"users with trust authentication use SYSTEM_USER = NULL in parallel workers"
 );
 
+# Explicitly specifying an empty require_auth (the default) should always
+# succeed.
+$node->connect_ok("user=scram_role require_auth=",
+	"empty require_auth succeeds");
+
+# All these values of require_auth should fail, as trust is expected.
+$node->connect_fails(
+	"user=scram_role require_auth=gss",
+	"GSS authentication required, fails with trust auth",
+	expected_stderr =>
+	  qr/auth method "gss" requirement failed: server did not complete authentication/
+);
+$node->connect_fails(
+	"user=scram_role require_auth=sspi",
+	"SSPI authentication required, fails with trust auth",
+	expected_stderr =>
+	  qr/auth method "sspi" requirement failed: server did not complete authentication/
+);
+$node->connect_fails(
+	"user=scram_role require_auth=password",
+	"password authentication required, fails with trust auth",
+	expected_stderr =>
+	  qr/auth method "password" requirement failed: server did not complete authentication/
+);
+$node->connect_fails(
+	"user=scram_role require_auth=md5",
+	"MD5 authentication required, fails with trust auth",
+	expected_stderr =>
+	  qr/auth method "md5" requirement failed: server did not complete authentication/
+);
+$node->connect_fails(
+	"user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication required, fails with trust auth",
+	expected_stderr =>
+	  qr/auth method "scram-sha-256" requirement failed: server did not complete authentication/
+);
+$node->connect_fails(
+	"user=scram_role require_auth=password,scram-sha-256",
+	"password and SCRAM authentication required, fails with trust auth",
+	expected_stderr =>
+	  qr/auth method "password,scram-sha-256" requirement failed: server did not complete authentication/
+);
+
+# These negative patterns of require_auth should succeed.
+$node->connect_ok("user=scram_role require_auth=!gss",
+	"GSS authentication can be forbidden, succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!sspi",
+	"SSPI authentication can be forbidden, succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!password",
+	"password authentication can be forbidden, succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!md5",
+	"md5 authentication can be forbidden, succeeds with trust auth");
+$node->connect_ok("user=scram_role require_auth=!scram-sha-256",
+	"SCRAM authentication can be forbidden, succeeds with trust auth");
+$node->connect_ok(
+	"user=scram_role require_auth=!password,!scram-sha-256",
+	"multiple authentication types forbidden, succeeds with trust auth");
+
+# require_auth=[!]none should interact correctly with trust auth.
+$node->connect_ok("user=scram_role require_auth=none",
+	"all authentication types forbidden, succeeds with trust auth");
+$node->connect_fails(
+	"user=scram_role require_auth=!none",
+	"any authentication types required, fails with trust auth",
+	expected_stderr => qr/server did not complete authentication/);
+
+# Negative and positive require_auth methods can't be mixed.
+$node->connect_fails(
+	"user=scram_role require_auth=scram-sha-256,!md5",
+	"negative require_auth methods cannot be mixed with positive ones",
+	expected_stderr =>
+	  qr/negative require_auth method "!md5" cannot be mixed with non-negative methods/
+);
+$node->connect_fails(
+	"user=scram_role require_auth=!password,!none,scram-sha-256",
+	"positive require_auth methods cannot be mixed with negative one",
+	expected_stderr =>
+	  qr/require_auth method "scram-sha-256" cannot be mixed with negative methods/
+);
+
+# require_auth methods cannot have duplicated values.
+$node->connect_fails(
+	"user=scram_role require_auth=password,md5,password",
+	"require_auth methods cannot include duplicates, positive case",
+	expected_stderr =>
+	  qr/require_auth method "password" is specified more than once/);
+$node->connect_fails(
+	"user=scram_role require_auth=!password,!md5,!password",
+	"require_auth methods cannot be duplicated, negative case",
+	expected_stderr =>
+	  qr/require_auth method "!password" is specified more than once/);
+$node->connect_fails(
+	"user=scram_role require_auth=none,md5,none",
+	"require_auth methods cannot be duplicated, none case",
+	expected_stderr =>
+	  qr/require_auth method "none" is specified more than once/);
+$node->connect_fails(
+	"user=scram_role require_auth=!none,!md5,!none",
+	"require_auth methods cannot be duplicated, !none case",
+	expected_stderr =>
+	  qr/require_auth method "!none" is specified more than once/);
+
+# Unknown value defined in require_auth.
+$node->connect_fails(
+	"user=scram_role require_auth=none,abcdefg",
+	"unknown require_auth methods are rejected",
+	expected_stderr => qr/invalid require_auth method: "abcdefg"/);
+
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'all', 'all', 'password');
 test_conn($node, 'user=scram_role', 'password', 0,
@@ -124,6 +232,47 @@ test_conn($node, 'user=md5_role', 'password', 0,
 	log_like =>
 	  [qr/connection authenticated: identity="md5_role" method=password/]);
 
+# require_auth succeeds here with a plaintext password.
+$node->connect_ok("user=scram_role require_auth=password",
+	"password authentication required, works with password auth");
+$node->connect_ok("user=scram_role require_auth=!none",
+	"any authentication required, works with password auth");
+$node->connect_ok(
+	"user=scram_role require_auth=scram-sha-256,password,md5",
+	"multiple authentication types required, works with password auth");
+
+# require_auth fails for other authentication types.
+$node->connect_fails(
+	"user=scram_role require_auth=md5",
+	"md5 authentication required, fails with password auth",
+	expected_stderr =>
+	  qr/auth method "md5" requirement failed: server requested a cleartext password/
+);
+$node->connect_fails(
+	"user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication required, fails with password auth",
+	expected_stderr =>
+	  qr/auth method "scram-sha-256" requirement failed: server requested a cleartext password/
+);
+$node->connect_fails(
+	"user=scram_role require_auth=none",
+	"all authentication forbidden, fails with password auth",
+	expected_stderr =>
+	  qr/auth method "none" requirement failed: server requested a cleartext password/
+);
+
+# Disallowing password authentication fails, even if requested by server.
+$node->connect_fails(
+	"user=scram_role require_auth=!password",
+	"password authentication forbidden, fails with password auth",
+	expected_stderr => qr/server requested a cleartext password/);
+$node->connect_fails(
+	"user=scram_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types forbidden, fails with password auth",
+	expected_stderr =>
+	  qr/ method "!password,!md5,!scram-sha-256" requirement failed: server requested a cleartext password/
+);
+
 # For "scram-sha-256" method, user "scram_role" should be able to connect.
 reset_pg_hba($node, 'all', 'all', 'scram-sha-256');
 test_conn(
@@ -137,6 +286,46 @@ test_conn(
 test_conn($node, 'user=md5_role', 'scram-sha-256', 2,
 	log_unlike => [qr/connection authenticated:/]);
 
+# require_auth should succeeds with SCRAM when it is required.
+$node->connect_ok(
+	"user=scram_role require_auth=scram-sha-256",
+	"SCRAM authentication required, works with SCRAM auth");
+$node->connect_ok("user=scram_role require_auth=!none",
+	"any authentication required, works with SCRAM auth");
+$node->connect_ok(
+	"user=scram_role require_auth=password,scram-sha-256,md5",
+	"multiple authentication types required, works with SCRAM auth");
+
+# Authentication fails for other authentication types.
+$node->connect_fails(
+	"user=scram_role require_auth=password",
+	"password authentication required, fails with SCRAM auth",
+	expected_stderr =>
+	  qr/auth method "password" requirement failed: server requested SASL authentication/
+);
+$node->connect_fails(
+	"user=scram_role require_auth=md5",
+	"md5 authentication required, fails with SCRAM auth",
+	expected_stderr =>
+	  qr/auth method "md5" requirement failed: server requested SASL authentication/
+);
+$node->connect_fails(
+	"user=scram_role require_auth=none",
+	"all authentication forbidden, fails with SCRAM auth",
+	expected_stderr =>
+	  qr/auth method "none" requirement failed: server requested SASL authentication/
+);
+
+# Authentication fails if SCRAM authentication is forbidden.
+$node->connect_fails(
+	"user=scram_role require_auth=!scram-sha-256",
+	"SCRAM authentication forbidden, fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+$node->connect_fails(
+	"user=scram_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types forbidden, fails with SCRAM auth",
+	expected_stderr => qr/server requested SASL authentication/);
+
 # Test that bad passwords are rejected.
 $ENV{"PGPASSWORD"} = 'badpass';
 test_conn($node, 'user=scram_role', 'scram-sha-256', 2,
@@ -153,6 +342,49 @@ test_conn($node, 'user=md5_role', 'md5', 0,
 	log_like =>
 	  [qr/connection authenticated: identity="md5_role" method=md5/]);
 
+# require_auth succeeds with MD5 required.
+$node->connect_ok("user=md5_role require_auth=md5",
+	"MD5 authentication required, works with MD5 auth");
+$node->connect_ok("user=md5_role require_auth=!none",
+	"any authentication required, works with MD5 auth");
+$node->connect_ok(
+	"user=md5_role require_auth=md5,scram-sha-256,password",
+	"multiple authentication types required, works with MD5 auth");
+
+# Authentication fails if other types are required.
+$node->connect_fails(
+	"user=md5_role require_auth=password",
+	"password authentication required, fails with MD5 auth",
+	expected_stderr =>
+	  qr/auth method "password" requirement failed: server requested a hashed password/
+);
+$node->connect_fails(
+	"user=md5_role require_auth=scram-sha-256",
+	"SCRAM authentication required, fails with MD5 auth",
+	expected_stderr =>
+	  qr/auth method "scram-sha-256" requirement failed: server requested a hashed password/
+);
+$node->connect_fails(
+	"user=md5_role require_auth=none",
+	"all authentication types forbidden, fails with MD5 auth",
+	expected_stderr =>
+	  qr/auth method "none" requirement failed: server requested a hashed password/
+);
+
+# Authentication fails if MD5 is forbidden.
+$node->connect_fails(
+	"user=md5_role require_auth=!md5",
+	"password authentication forbidden, fails with MD5 auth",
+	expected_stderr =>
+	  qr/auth method "!md5" requirement failed: server requested a hashed password/
+);
+$node->connect_fails(
+	"user=md5_role require_auth=!password,!md5,!scram-sha-256",
+	"multiple authentication types forbidden, fails with MD5 auth",
+	expected_stderr =>
+	  qr/auth method "!password,!md5,!scram-sha-256" requirement failed: server requested a hashed password/
+);
+
 # Test SYSTEM_USER <> NULL with parallel workers.
 $node->safe_psql(
 	'postgres',
diff --git a/src/test/authentication/t/005_sspi.pl b/src/test/authentication/t/005_sspi.pl
new file mode 100644
index 0000000000..c6b52ff3c9
--- /dev/null
+++ b/src/test/authentication/t/005_sspi.pl
@@ -0,0 +1,40 @@
+
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+# Tests targeting SSPI on Windows.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+if (!$windows_os || $use_unix_sockets)
+{
+	plan skip_all =>
+	  "SSPI tests require Windows (without PG_TEST_USE_UNIX_SOCKETS)";
+}
+
+# Initialize primary node
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
+$node->start;
+
+# SSPI is set up by default. Make sure it interacts correctly with require_auth.
+$node->connect_ok("require_auth=sspi",
+	"SSPI authentication required, works with SSPI auth");
+$node->connect_fails(
+	"require_auth=!sspi",
+	"SSPI authentication forbidden, fails with SSPI auth",
+	expected_stderr =>
+	  qr/auth method "!sspi" requirement failed: server requested SSPI authentication/
+);
+$node->connect_fails(
+	"require_auth=scram-sha-256",
+	"SCRAM authentication required, fails with SSPI auth",
+	expected_stderr =>
+	  qr/auth method "scram-sha-256" requirement failed: server requested SSPI authentication/
+);
+
+done_testing();
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index 3bc4ad7dd3..ce7a323d0e 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -337,6 +337,32 @@ test_query(
 	'gssencmode=require',
 	'sending 100K lines works');
 
+# require_auth=gss succeeds if required.
+$node->connect_ok(
+	$node->connstr('postgres')
+	  . " user=test1 host=$host hostaddr=$hostaddr gssencmode=disable require_auth=gss",
+	"GSS authentication requested, works with GSS auth without encryption");
+$node->connect_ok(
+	$node->connstr('postgres')
+	  . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss",
+	"GSS authentication requested, works with GSS auth with encryption");
+
+# require_auth=sspi fails if required.
+$node->connect_fails(
+	$node->connstr('postgres')
+	  . " user=test1 host=$host hostaddr=$hostaddr gssencmode=disable require_auth=sspi",
+	"SSPI authentication requested, fails with GSS auth without encryption",
+	expected_stderr =>
+	  qr/auth method "sspi" requirement failed: server requested GSSAPI authentication/
+);
+$node->connect_fails(
+	$node->connstr('postgres')
+	  . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=sspi",
+	"SSPI authentication requested, fails with GSS auth with encryption",
+	expected_stderr =>
+	  qr/auth method "sspi" requirement failed: server did not complete authentication/
+);
+
 # Test that SYSTEM_USER works.
 test_query($node, 'test1', 'SELECT SYSTEM_USER;',
 	qr/^gss:test1\@$realm$/s, 'gssencmode=require', 'testing system_user');
@@ -382,6 +408,16 @@ test_access(
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable',
 	'fails with GSS encryption disabled and hostgssenc hba');
 
+# require_auth=gss succeeds if required.
+$node->connect_ok(
+	$node->connstr('postgres')
+	  . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss",
+	"GSS authentication requested, works with GSS encryption");
+$node->connect_ok(
+	$node->connstr('postgres')
+	  . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss,scram-sha-256",
+	"multiple authentication types requested, works with GSS encryption");
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
 	qq{hostnogssenc all all $hostaddr/32 gss map=mymap});
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index f3ed806ec2..1e027ced01 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -101,6 +101,12 @@ test_access(
 		qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/
 	],);
 
+# require_auth=password should complete successfully; other methods should fail.
+$node->connect_ok("user=test1 require_auth=password",
+	"password authentication required, works with ldap auth");
+$node->connect_fails("user=test1 require_auth=scram-sha-256",
+	"SCRAM authentication required, fails with ldap auth");
+
 note "search+bind";
 
 unlink($node->data_dir . '/pg_hba.conf');
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 1d3905d3a1..8038135697 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -140,6 +140,34 @@ $node->connect_ok(
 		qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/
 	]);
 
+# channel_binding should continue to work independently of require_auth.
+$node->connect_ok(
+	"$common_connstr user=ssltestuser channel_binding=disable require_auth=scram-sha-256",
+	"SCRAM with SSL, channel_binding=disable, and require_auth=scram-sha-256"
+);
+$node->connect_fails(
+	"$common_connstr user=md5testuser require_auth=md5 channel_binding=require",
+	"channel_binding can fail even when require_auth succeeds",
+	expected_stderr =>
+	  qr/channel binding required but not supported by server's authentication request/
+);
+if ($supports_tls_server_end_point)
+{
+	$node->connect_ok(
+		"$common_connstr user=ssltestuser channel_binding=require require_auth=scram-sha-256",
+		"SCRAM with SSL, channel_binding=require, and require_auth=scram-sha-256"
+	);
+}
+else
+{
+	$node->connect_fails(
+		"$common_connstr user=ssltestuser channel_binding=require require_auth=scram-sha-256",
+		"SCRAM with SSL, channel_binding=require, and require_auth=scram-sha-256",
+		expected_stderr =>
+		  qr/channel binding is required, but server did not offer an authentication method that supports channel binding/
+	);
+}
+
 # Now test with a server certificate that uses the RSA-PSS algorithm.
 # This checks that the certificate can be loaded and that channel binding
 # works. (see bug #17760)
-- 
2.25.1

v16-0002-Add-sslcertmode-option-for-client-certificates.patchtext/x-patch; charset=US-ASCII; name=v16-0002-Add-sslcertmode-option-for-client-certificates.patchDownload
From d621f5b3cb948b64678a67b2a70ee366d690b38d Mon Sep 17 00:00:00 2001
From: Jacob Champion <jchampion@timescale.com>
Date: Fri, 24 Jun 2022 15:40:42 -0700
Subject: [PATCH v16 2/3] Add sslcertmode option for client certificates

The sslcertmode option controls whether the server is allowed and/or
required to request a certificate from the client. There are three
modes:

- "allow" is the default and follows the current behavior -- a
  configured  sslcert is sent if the server requests one (which, with
  the current implementation, will happen whenever TLS is negotiated).

- "disable" causes the client to refuse to send a client certificate
  even if an sslcert is configured.

- "require" causes the client to fail if a client certificate is never
  sent and the server opens a connection anyway. This doesn't add any
  additional security, since there is no guarantee that the server is
  validating the certificate correctly, but it may help troubleshoot
  more complicated TLS setups.

sslcertmode=require needs the OpenSSL implementation to support
SSL_CTX_set_cert_cb(). Notably, LibreSSL does not.
---
 configure                                | 11 ++--
 configure.ac                             |  4 +-
 doc/src/sgml/libpq.sgml                  | 64 ++++++++++++++++++++++++
 meson.build                              |  3 +-
 src/include/pg_config.h.in               |  3 ++
 src/interfaces/libpq/fe-auth.c           | 19 +++++++
 src/interfaces/libpq/fe-connect.c        | 51 +++++++++++++++++++
 src/interfaces/libpq/fe-secure-openssl.c | 39 ++++++++++++++-
 src/interfaces/libpq/libpq-int.h         |  3 ++
 src/test/ssl/t/001_ssltests.pl           | 43 ++++++++++++++++
 src/tools/msvc/Solution.pm               |  9 ++++
 11 files changed, 240 insertions(+), 9 deletions(-)

diff --git a/configure b/configure
index e35769ea73..92896d2d83 100755
--- a/configure
+++ b/configure
@@ -12973,13 +12973,14 @@ else
 fi
 
   fi
-  # Function introduced in OpenSSL 1.0.2.
-  for ac_func in X509_get_signature_nid
+  # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all of these.
+  for ac_func in X509_get_signature_nid SSL_CTX_set_cert_cb
 do :
-  ac_fn_c_check_func "$LINENO" "X509_get_signature_nid" "ac_cv_func_X509_get_signature_nid"
-if test "x$ac_cv_func_X509_get_signature_nid" = xyes; then :
+  as_ac_var=`$as_echo "ac_cv_func_$ac_func" | $as_tr_sh`
+ac_fn_c_check_func "$LINENO" "$ac_func" "$as_ac_var"
+if eval test \"x\$"$as_ac_var"\" = x"yes"; then :
   cat >>confdefs.h <<_ACEOF
-#define HAVE_X509_GET_SIGNATURE_NID 1
+#define `$as_echo "HAVE_$ac_func" | $as_tr_cpp` 1
 _ACEOF
 
 fi
diff --git a/configure.ac b/configure.ac
index af23c15cb2..34af40f621 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1373,8 +1373,8 @@ if test "$with_ssl" = openssl ; then
      AC_SEARCH_LIBS(CRYPTO_new_ex_data, [eay32 crypto], [], [AC_MSG_ERROR([library 'eay32' or 'crypto' is required for OpenSSL])])
      AC_SEARCH_LIBS(SSL_new, [ssleay32 ssl], [], [AC_MSG_ERROR([library 'ssleay32' or 'ssl' is required for OpenSSL])])
   fi
-  # Function introduced in OpenSSL 1.0.2.
-  AC_CHECK_FUNCS([X509_get_signature_nid])
+  # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all of these.
+  AC_CHECK_FUNCS([X509_get_signature_nid SSL_CTX_set_cert_cb])
   # Functions introduced in OpenSSL 1.1.0. We used to check for
   # OPENSSL_VERSION_NUMBER, but that didn't work with 1.1.0, because LibreSSL
   # defines OPENSSL_VERSION_NUMBER to claim version 2.0.0, even though it
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 3706d349ab..ac1c379f2a 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1820,6 +1820,60 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-sslcertmode" xreflabel="sslcertmode">
+      <term><literal>sslcertmode</literal></term>
+      <listitem>
+       <para>
+        This option determines whether a client certificate may be sent to the
+        server, and whether the server is required to request one. There are
+        three modes:
+
+        <variablelist>
+         <varlistentry>
+          <term><literal>disable</literal></term>
+          <listitem>
+           <para>
+            a client certificate is never sent, even if one is provided via
+            <xref linkend="libpq-connect-sslcert" />
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>allow</literal> (default)</term>
+          <listitem>
+           <para>
+            a certificate may be sent, if the server requests one and it has
+            been provided via <literal>sslcert</literal>
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>require</literal></term>
+          <listitem>
+           <para>
+            the server <emphasis>must</emphasis> request a certificate. The
+            connection will fail if the client does not send a certificate and
+            the server successfully authenticates the client anyway.
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+       </para>
+
+       <note>
+        <para>
+         <literal>sslcertmode=require</literal> doesn't add any additional
+         security, since there is no guarantee that the server is validating the
+         certificate correctly; PostgreSQL servers generally request TLS
+         certificates from clients whether they validate them or not. The option
+         may be useful when troubleshooting more complicated TLS setups.
+        </para>
+       </note>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-sslrootcert" xreflabel="sslrootcert">
       <term><literal>sslrootcert</literal></term>
       <listitem>
@@ -7996,6 +8050,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGSSLCERTMODE</envar></primary>
+      </indexterm>
+      <envar>PGSSLCERTMODE</envar> behaves the same as the <xref
+      linkend="libpq-connect-sslcertmode"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/meson.build b/meson.build
index 8208815c96..70b64ea4b7 100644
--- a/meson.build
+++ b/meson.build
@@ -1213,8 +1213,9 @@ if sslopt in ['auto', 'openssl']
       ['CRYPTO_new_ex_data', {'required': true}],
       ['SSL_new', {'required': true}],
 
-      # Function introduced in OpenSSL 1.0.2.
+      # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all of these.
       ['X509_get_signature_nid'],
+      ['SSL_CTX_set_cert_cb'],
 
       # Functions introduced in OpenSSL 1.1.0. We used to check for
       # OPENSSL_VERSION_NUMBER, but that didn't work with 1.1.0, because LibreSSL
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 20c82f5979..c5a6762fc1 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -394,6 +394,9 @@
 /* Define to 1 if you have spinlocks. */
 #undef HAVE_SPINLOCKS
 
+/* Define to 1 if you have the `SSL_CTX_set_cert_cb' function. */
+#undef HAVE_SSL_CTX_SET_CERT_CB
+
 /* Define to 1 if stdbool.h conforms to C99. */
 #undef HAVE_STDBOOL_H
 
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 8ce5b60a3d..53c7d30eff 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -862,6 +862,25 @@ check_expected_areq(AuthRequest areq, PGconn *conn)
 	StaticAssertDecl((sizeof(conn->allowed_auth_methods) * CHAR_BIT) > AUTH_REQ_MAX,
 					 "AUTH_REQ_MAX overflows the allowed_auth_methods bitmask");
 
+	if (conn->sslcertmode[0] == 'r' /* require */
+		&& areq == AUTH_REQ_OK)
+	{
+		/*
+		 * Trade off a little bit of complexity to try to get these error
+		 * messages as precise as possible.
+		 */
+		if (!conn->ssl_cert_requested)
+		{
+			libpq_append_conn_error(conn, "server did not request a certificate");
+			return false;
+		}
+		else if (!conn->ssl_cert_sent)
+		{
+			libpq_append_conn_error(conn, "server accepted connection without a valid certificate");
+			return false;
+		}
+	}
+
 	/*
 	 * If the user required a specific auth method, or specified an allowed
 	 * set, then reject all others here, and make sure the server actually
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index dd4b98e099..cbadb3f6af 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -125,8 +125,10 @@ static int	ldapServiceLookup(const char *purl, PQconninfoOption *options,
 #define DefaultTargetSessionAttrs	"any"
 #ifdef USE_SSL
 #define DefaultSSLMode "prefer"
+#define DefaultSSLCertMode "allow"
 #else
 #define DefaultSSLMode	"disable"
+#define DefaultSSLCertMode "disable"
 #endif
 #ifdef ENABLE_GSS
 #include "fe-gssapi-common.h"
@@ -283,6 +285,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"SSL-Client-Key", "", 64,
 	offsetof(struct pg_conn, sslkey)},
 
+	{"sslcertmode", "PGSSLCERTMODE", NULL, NULL,
+		"SSL-Client-Cert-Mode", "", 8, /* sizeof("disable") == 8 */
+	offsetof(struct pg_conn, sslcertmode)},
+
 	{"sslpassword", NULL, NULL, NULL,
 		"SSL-Client-Key-Password", "*", 20,
 	offsetof(struct pg_conn, sslpassword)},
@@ -1510,6 +1516,51 @@ connectOptions2(PGconn *conn)
 		return false;
 	}
 
+	/*
+	 * validate sslcertmode option
+	 */
+	if (conn->sslcertmode)
+	{
+		if (strcmp(conn->sslcertmode, "disable") != 0 &&
+			strcmp(conn->sslcertmode, "allow") != 0 &&
+			strcmp(conn->sslcertmode, "require") != 0)
+		{
+			conn->status = CONNECTION_BAD;
+			libpq_append_conn_error(conn, "invalid %s value: \"%s\"",
+									"sslcertmode", conn->sslcertmode);
+			return false;
+		}
+#ifndef USE_SSL
+		if (strcmp(conn->sslcertmode, "require") == 0)
+		{
+			conn->status = CONNECTION_BAD;
+			libpq_append_conn_error(conn, "sslcertmode value \"%s\" invalid when SSL support is not compiled in",
+									conn->sslcertmode);
+			return false;
+		}
+#endif
+#ifndef HAVE_SSL_CTX_SET_CERT_CB
+		/*
+		 * Without a certificate callback, the current implementation can't
+		 * figure out if a certficate was actually requested, so "require" is
+		 * useless.
+		 */
+		if (strcmp(conn->sslcertmode, "require") == 0)
+		{
+			conn->status = CONNECTION_BAD;
+			libpq_append_conn_error(conn, "sslcertmode value \"%s\" is not supported (check OpenSSL version)",
+									conn->sslcertmode);
+			return false;
+		}
+#endif
+	}
+	else
+	{
+		conn->sslcertmode = strdup(DefaultSSLCertMode);
+		if (!conn->sslcertmode)
+			goto oom_error;
+	}
+
 	/*
 	 * validate gssencmode option
 	 */
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 6a4431ddfe..b88d9da3e2 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -462,6 +462,33 @@ verify_cb(int ok, X509_STORE_CTX *ctx)
 	return ok;
 }
 
+#ifdef HAVE_SSL_CTX_SET_CERT_CB
+/*
+ * Certificate selection callback
+ *
+ * This callback lets us choose the client certificate we send to the server
+ * after seeing its CertificateRequest. We only support sending a single
+ * hard-coded certificate via sslcert, so we don't actually set any certificates
+ * here; we just use it to record whether or not the server has actually asked
+ * for one and whether we have one to send.
+ */
+static int
+cert_cb(SSL *ssl, void *arg)
+{
+	PGconn *conn = arg;
+	conn->ssl_cert_requested = true;
+
+	/* Do we have a certificate loaded to send back? */
+	if (SSL_get_certificate(ssl))
+		conn->ssl_cert_sent = true;
+
+	/*
+	 * Tell OpenSSL that the callback succeeded; we're not required to actually
+	 * make any changes to the SSL handle.
+	 */
+	return 1;
+}
+#endif
 
 /*
  * OpenSSL-specific wrapper around
@@ -953,6 +980,11 @@ initialize_SSL(PGconn *conn)
 		SSL_CTX_set_default_passwd_cb_userdata(SSL_context, conn);
 	}
 
+#ifdef HAVE_SSL_CTX_SET_CERT_CB
+	/* Set up a certificate selection callback. */
+	SSL_CTX_set_cert_cb(SSL_context, cert_cb, conn);
+#endif
+
 	/* Disable old protocol versions */
 	SSL_CTX_set_options(SSL_context, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
 
@@ -1107,7 +1139,12 @@ initialize_SSL(PGconn *conn)
 	else
 		fnbuf[0] = '\0';
 
-	if (fnbuf[0] == '\0')
+	if (conn->sslcertmode[0] == 'd') /* disable */
+	{
+		/* don't send a client cert even if we have one */
+		have_cert = false;
+	}
+	else if (fnbuf[0] == '\0')
 	{
 		/* no home directory, proceed without a client cert */
 		have_cert = false;
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 1dc264fe54..f1f1d973cc 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -384,6 +384,7 @@ struct pg_conn
 	char	   *sslkey;			/* client key filename */
 	char	   *sslcert;		/* client certificate filename */
 	char	   *sslpassword;	/* client key file password */
+	char	   *sslcertmode;	/* client cert mode (require,allow,disable) */
 	char	   *sslrootcert;	/* root certificate filename */
 	char	   *sslcrl;			/* certificate revocation list filename */
 	char	   *sslcrldir;		/* certificate revocation list directory name */
@@ -527,6 +528,8 @@ struct pg_conn
 
 	/* SSL structures */
 	bool		ssl_in_use;
+	bool		ssl_cert_requested;	/* Did the server ask us for a cert? */
+	bool		ssl_cert_sent;		/* Did we send one in reply? */
 
 #ifdef USE_SSL
 	bool		allow_ssl_try;	/* Allowed to try SSL negotiation */
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 3094e27af3..4617f06f86 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -42,6 +42,10 @@ my $SERVERHOSTADDR = '127.0.0.1';
 # This is the pattern to use in pg_hba.conf to match incoming connections.
 my $SERVERHOSTCIDR = '127.0.0.1/32';
 
+# Determine whether build supports sslcertmode=require.
+my $supports_sslcertmode_require =
+  check_pg_config("#define HAVE_SSL_CTX_SET_CERT_CB 1");
+
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
 
@@ -191,6 +195,22 @@ $node->connect_ok(
 	"$common_connstr sslrootcert=ssl/both-cas-2.crt sslmode=verify-ca",
 	"cert root file that contains two certificates, order 2");
 
+# sslcertmode=allow and =disable should both work without a client certificate.
+$node->connect_ok(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=disable",
+	"connect with sslcertmode=disable");
+$node->connect_ok(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=allow",
+	"connect with sslcertmode=allow");
+
+# sslcertmode=require, however, should fail.
+$node->connect_fails(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=require",
+	"connect with sslcertmode=require fails without a client certificate",
+	expected_stderr => $supports_sslcertmode_require
+		? qr/server accepted connection without a valid certificate/
+		: qr/sslcertmode value "require" is not supported/);
+
 # CRL tests
 
 # Invalid CRL filename is the same as no CRL, succeeds
@@ -538,6 +558,29 @@ $node->connect_ok(
 	"certificate authorization succeeds with correct client cert in encrypted DER format"
 );
 
+# correct client cert with required/allowed certificate authentication
+if ($supports_sslcertmode_require)
+{
+	$node->connect_ok(
+		"$common_connstr user=ssltestuser sslcertmode=require sslcert=ssl/client.crt "
+		  . sslkey('client.key'),
+		"certificate authorization succeeds with sslcertmode=require"
+	);
+}
+$node->connect_ok(
+	"$common_connstr user=ssltestuser sslcertmode=allow sslcert=ssl/client.crt "
+	  . sslkey('client.key'),
+	"certificate authorization succeeds with sslcertmode=allow"
+);
+
+# client cert isn't sent if certificate authentication is disabled
+$node->connect_fails(
+	"$common_connstr user=ssltestuser sslcertmode=disable sslcert=ssl/client.crt "
+	  . sslkey('client.key'),
+	"certificate authorization fails with sslcertmode=disable",
+	expected_stderr => qr/connection requires a valid client certificate/
+);
+
 # correct client cert in encrypted PEM with wrong password
 $node->connect_fails(
 	"$common_connstr user=ssltestuser sslcert=ssl/client.crt "
diff --git a/src/tools/msvc/Solution.pm b/src/tools/msvc/Solution.pm
index 5eaea6355e..1ec1bac552 100644
--- a/src/tools/msvc/Solution.pm
+++ b/src/tools/msvc/Solution.pm
@@ -327,6 +327,7 @@ sub GenerateFiles
 		HAVE_SETPROCTITLE_FAST                   => undef,
 		HAVE_SOCKLEN_T                           => 1,
 		HAVE_SPINLOCKS                           => 1,
+		HAVE_SSL_CTX_SET_CERT_CB                 => undef,
 		HAVE_STDBOOL_H                           => 1,
 		HAVE_STDINT_H                            => 1,
 		HAVE_STDLIB_H                            => 1,
@@ -507,6 +508,14 @@ sub GenerateFiles
 			$define{HAVE_HMAC_CTX_NEW}          = 1;
 			$define{HAVE_OPENSSL_INIT_SSL}      = 1;
 		}
+
+		# Symbols needed with OpenSSL 1.0.2 and above.
+		if (   ($digit1 >= '3' && $digit2 >= '0' && $digit3 >= '0')
+			|| ($digit1 >= '1' && $digit2 >= '1' && $digit3 >= '0')
+			|| ($digit1 >= '1' && $digit2 >= '0' && $digit3 >= '2'))
+		{
+			$define{HAVE_SSL_CTX_SET_CERT_CB} = 1;
+		}
 	}
 
 	$self->GenerateConfigHeader('src/include/pg_config.h',     \%define, 1);
-- 
2.25.1

#56Michael Paquier
michael@paquier.xyz
In reply to: Jacob Champion (#55)
Re: [PoC] Let libpq reject unexpected authentication requests

On Mon, Mar 13, 2023 at 12:38:10PM -0700, Jacob Champion wrote:

Here's a v16:
- updated 0001 patch message
- all test names should have commas rather than colons now
- new test for an empty require_auth
- new SSPI suite (note that it doesn't run by default on Cirrus, due
to the use of PG_TEST_USE_UNIX_SOCKETS)
- fixed errant comma at EOL

0001 was looking fine enough seen from here, so applied it after
tweaking a few comments. That's enough to cover most of the needs of
this thread.

0002 looks pretty simple as well, I think that's worth a look for this
CF. I am not sure about 0003, to be honest, as I am wondering if
there could be a better solution than tying more the mechanism names
with the expected AUTH_REQ_* values..
--
Michael

#57Jacob Champion
jchampion@timescale.com
In reply to: Michael Paquier (#56)
2 attachment(s)
Re: [PoC] Let libpq reject unexpected authentication requests

On Mon, Mar 13, 2023 at 10:39 PM Michael Paquier <michael@paquier.xyz> wrote:

0001 was looking fine enough seen from here, so applied it after
tweaking a few comments. That's enough to cover most of the needs of
this thread.

Thank you very much!

0002 looks pretty simple as well, I think that's worth a look for this
CF.

Cool. v17 just rebases the set over HEAD, then, for cfbot.

I am not sure about 0003, to be honest, as I am wondering if
there could be a better solution than tying more the mechanism names
with the expected AUTH_REQ_* values..

Yeah, I'm not particularly excited about the approach I took. It'd be
easier if we had a second SASL method to verify the implementation...
I'd also proposed just adding an Assert, as a third option, to guide
the eventual SASL implementer back to this conversation?

--Jacob

Attachments:

v17-0002-require_auth-decouple-SASL-and-SCRAM.patchtext/x-patch; charset=US-ASCII; name=v17-0002-require_auth-decouple-SASL-and-SCRAM.patchDownload
From e2343c008916fe8dd9ecacfa71c60cc250fa208d Mon Sep 17 00:00:00 2001
From: Jacob Champion <jchampion@timescale.com>
Date: Tue, 18 Oct 2022 16:55:36 -0700
Subject: [PATCH v17 2/2] require_auth: decouple SASL and SCRAM

Rather than assume that an AUTH_REQ_SASL* code refers to SCRAM-SHA-256,
future-proof by separating the single allowlist into a list of allowed
authentication request codes and a list of allowed SASL mechanisms.

The require_auth check is now separated into two tiers. The
AUTH_REQ_SASL* codes are always allowed. If the server sends one,
responsibility for the check then falls to pg_SASL_init(), which
compares the selected mechanism against the list of allowed mechanisms.
(Other SCRAM code is already responsible for rejecting unexpected or
out-of-order AUTH_REQ_SASL_* codes, so that's not explicitly handled
with this addition.)

Since there's only one recognized SASL mechanism, conn->sasl_mechs
currently only points at static hardcoded lists. Whenever a second
mechanism is added, the list will need to be managed dynamically.
---
 src/interfaces/libpq/fe-auth.c            | 34 +++++++++++++++++++
 src/interfaces/libpq/fe-connect.c         | 41 +++++++++++++++++++----
 src/interfaces/libpq/libpq-int.h          |  3 +-
 src/test/authentication/t/001_password.pl | 14 +++++---
 src/test/ssl/t/002_scram.pl               |  6 ++++
 5 files changed, 86 insertions(+), 12 deletions(-)

diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index b2497acdad..4ff49d8207 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -522,6 +522,40 @@ pg_SASL_init(PGconn *conn, int payloadlen)
 		goto error;
 	}
 
+	/*
+	 * Before going ahead with any SASL exchange, ensure that the user has
+	 * allowed (or, alternatively, has not forbidden) this particular mechanism.
+	 *
+	 * In a hypothetical future where a server responds with multiple SASL
+	 * mechanism families, we would need to instead consult this list up above,
+	 * during mechanism negotiation. We don't live in that world yet. The server
+	 * presents one auth method and we decide whether that's acceptable or not.
+	 */
+	if (conn->sasl_mechs)
+	{
+		const char **mech;
+		bool		found = false;
+
+		Assert(conn->require_auth);
+
+		for (mech = conn->sasl_mechs; *mech; mech++)
+		{
+			if (strcmp(*mech, selected_mechanism) == 0)
+			{
+				found = true;
+				break;
+			}
+		}
+
+		if ((conn->sasl_mechs_denied && found)
+			|| (!conn->sasl_mechs_denied && !found))
+		{
+			libpq_append_conn_error(conn, "auth method \"%s\" requirement failed: server requested unacceptable SASL mechanism \"%s\"",
+									conn->require_auth, selected_mechanism);
+			goto error;
+		}
+	}
+
 	if (conn->channel_binding[0] == 'r' &&	/* require */
 		strcmp(selected_mechanism, SCRAM_SHA_256_PLUS_NAME) != 0)
 	{
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index cbadb3f6af..a048793b46 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -1258,12 +1258,25 @@ connectOptions2(PGconn *conn)
 					more;
 		bool		negated = false;
 
+		static const uint32 default_methods = (
+			1 << AUTH_REQ_SASL
+			| 1 << AUTH_REQ_SASL_CONT
+			| 1 << AUTH_REQ_SASL_FIN
+		);
+		static const char *no_mechs[] = { NULL };
+
 		/*
-		 * By default, start from an empty set of allowed options and add to
+		 * By default, start from a minimum set of allowed options and add to
 		 * it.
+		 *
+		 * NB: The SASL method codes are always "allowed" here. If the server
+		 * requests SASL auth, pg_SASL_init() will enforce adherence to the
+		 * sasl_mechs list, which by default is empty.
 		 */
 		conn->auth_required = true;
-		conn->allowed_auth_methods = 0;
+		conn->allowed_auth_methods = default_methods;
+		conn->sasl_mechs = no_mechs;
+		conn->sasl_mechs_denied = false;
 
 		for (first = true, more = true; more; first = false)
 		{
@@ -1290,6 +1303,9 @@ connectOptions2(PGconn *conn)
 					 */
 					conn->auth_required = false;
 					conn->allowed_auth_methods = -1;
+
+					/* conn->sasl_mechs is now a list of denied mechanisms. */
+					conn->sasl_mechs_denied = true;
 				}
 				else if (!negated)
 				{
@@ -1334,10 +1350,23 @@ connectOptions2(PGconn *conn)
 			}
 			else if (strcmp(method, "scram-sha-256") == 0)
 			{
-				/* This currently assumes that SCRAM is the only SASL method. */
-				bits = (1 << AUTH_REQ_SASL);
-				bits |= (1 << AUTH_REQ_SASL_CONT);
-				bits |= (1 << AUTH_REQ_SASL_FIN);
+				static const char *scram_mechs[] = {
+					SCRAM_SHA_256_NAME,
+					SCRAM_SHA_256_PLUS_NAME,
+					NULL /* list terminator */
+				};
+
+				/*
+				 * This currently assumes that SCRAM is the only SASL method.
+				 * Once a second mechanism is added, this code will need to add
+				 * to the list instead of replacing it wholesale.
+				 */
+				if (conn->sasl_mechs[0])
+					goto duplicate;
+				conn->sasl_mechs = scram_mechs;
+
+				free(part);
+				continue; /* avoid the bitmask manipulation below */
 			}
 			else if (strcmp(method, "creds") == 0)
 			{
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index f1f1d973cc..ab26292586 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -465,7 +465,8 @@ struct pg_conn
 										 * codes */
 	bool		client_finished_auth;	/* have we finished our half of the
 										 * authentication exchange? */
-
+	const char **sasl_mechs;	/* list of allowed/denied SASL mechanisms */
+	bool		sasl_mechs_denied;	/* is the sasl_mechs list forbidden? */
 
 	/* Transient state needed while establishing connection */
 	PGTargetServerType target_server_type;	/* desired session properties */
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index cba5d7d648..015532893c 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -301,30 +301,34 @@ $node->connect_fails(
 	"user=scram_role require_auth=password",
 	"password authentication required, fails with SCRAM auth",
 	expected_stderr =>
-	  qr/auth method "password" requirement failed: server requested SASL authentication/
+	  qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256"/
 );
 $node->connect_fails(
 	"user=scram_role require_auth=md5",
 	"md5 authentication required, fails with SCRAM auth",
 	expected_stderr =>
-	  qr/auth method "md5" requirement failed: server requested SASL authentication/
+	  qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256"/
 );
 $node->connect_fails(
 	"user=scram_role require_auth=none",
 	"all authentication forbidden, fails with SCRAM auth",
 	expected_stderr =>
-	  qr/auth method "none" requirement failed: server requested SASL authentication/
+	  qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256"/
 );
 
 # Authentication fails if SCRAM authentication is forbidden.
 $node->connect_fails(
 	"user=scram_role require_auth=!scram-sha-256",
 	"SCRAM authentication forbidden, fails with SCRAM auth",
-	expected_stderr => qr/server requested SASL authentication/);
+	expected_stderr =>
+	  qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256"/
+);
 $node->connect_fails(
 	"user=scram_role require_auth=!password,!md5,!scram-sha-256",
 	"multiple authentication types forbidden, fails with SCRAM auth",
-	expected_stderr => qr/server requested SASL authentication/);
+	expected_stderr =>
+	  qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256"/
+);
 
 # Test that bad passwords are rejected.
 $ENV{"PGPASSWORD"} = 'badpass';
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 8038135697..173ac8d86b 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -157,6 +157,12 @@ if ($supports_tls_server_end_point)
 		"$common_connstr user=ssltestuser channel_binding=require require_auth=scram-sha-256",
 		"SCRAM with SSL, channel_binding=require, and require_auth=scram-sha-256"
 	);
+	$node->connect_fails(
+		"$common_connstr user=ssltestuser channel_binding=require require_auth=password",
+		"SCRAM with SSL, channel_binding=require, and require_auth=password",
+		expected_stderr =>
+		  qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256-PLUS"/
+	);
 }
 else
 {
-- 
2.25.1

v17-0001-Add-sslcertmode-option-for-client-certificates.patchtext/x-patch; charset=US-ASCII; name=v17-0001-Add-sslcertmode-option-for-client-certificates.patchDownload
From 8d93ca3792bd886f7ba50f21f9f6d8836b557a2b Mon Sep 17 00:00:00 2001
From: Jacob Champion <jchampion@timescale.com>
Date: Fri, 24 Jun 2022 15:40:42 -0700
Subject: [PATCH v17 1/2] Add sslcertmode option for client certificates

The sslcertmode option controls whether the server is allowed and/or
required to request a certificate from the client. There are three
modes:

- "allow" is the default and follows the current behavior -- a
  configured  sslcert is sent if the server requests one (which, with
  the current implementation, will happen whenever TLS is negotiated).

- "disable" causes the client to refuse to send a client certificate
  even if an sslcert is configured.

- "require" causes the client to fail if a client certificate is never
  sent and the server opens a connection anyway. This doesn't add any
  additional security, since there is no guarantee that the server is
  validating the certificate correctly, but it may help troubleshoot
  more complicated TLS setups.

sslcertmode=require needs the OpenSSL implementation to support
SSL_CTX_set_cert_cb(). Notably, LibreSSL does not.
---
 configure                                | 11 ++--
 configure.ac                             |  4 +-
 doc/src/sgml/libpq.sgml                  | 64 ++++++++++++++++++++++++
 meson.build                              |  3 +-
 src/include/pg_config.h.in               |  3 ++
 src/interfaces/libpq/fe-auth.c           | 19 +++++++
 src/interfaces/libpq/fe-connect.c        | 51 +++++++++++++++++++
 src/interfaces/libpq/fe-secure-openssl.c | 39 ++++++++++++++-
 src/interfaces/libpq/libpq-int.h         |  3 ++
 src/test/ssl/t/001_ssltests.pl           | 43 ++++++++++++++++
 src/tools/msvc/Solution.pm               |  9 ++++
 11 files changed, 240 insertions(+), 9 deletions(-)

diff --git a/configure b/configure
index e35769ea73..92896d2d83 100755
--- a/configure
+++ b/configure
@@ -12973,13 +12973,14 @@ else
 fi
 
   fi
-  # Function introduced in OpenSSL 1.0.2.
-  for ac_func in X509_get_signature_nid
+  # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all of these.
+  for ac_func in X509_get_signature_nid SSL_CTX_set_cert_cb
 do :
-  ac_fn_c_check_func "$LINENO" "X509_get_signature_nid" "ac_cv_func_X509_get_signature_nid"
-if test "x$ac_cv_func_X509_get_signature_nid" = xyes; then :
+  as_ac_var=`$as_echo "ac_cv_func_$ac_func" | $as_tr_sh`
+ac_fn_c_check_func "$LINENO" "$ac_func" "$as_ac_var"
+if eval test \"x\$"$as_ac_var"\" = x"yes"; then :
   cat >>confdefs.h <<_ACEOF
-#define HAVE_X509_GET_SIGNATURE_NID 1
+#define `$as_echo "HAVE_$ac_func" | $as_tr_cpp` 1
 _ACEOF
 
 fi
diff --git a/configure.ac b/configure.ac
index af23c15cb2..34af40f621 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1373,8 +1373,8 @@ if test "$with_ssl" = openssl ; then
      AC_SEARCH_LIBS(CRYPTO_new_ex_data, [eay32 crypto], [], [AC_MSG_ERROR([library 'eay32' or 'crypto' is required for OpenSSL])])
      AC_SEARCH_LIBS(SSL_new, [ssleay32 ssl], [], [AC_MSG_ERROR([library 'ssleay32' or 'ssl' is required for OpenSSL])])
   fi
-  # Function introduced in OpenSSL 1.0.2.
-  AC_CHECK_FUNCS([X509_get_signature_nid])
+  # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all of these.
+  AC_CHECK_FUNCS([X509_get_signature_nid SSL_CTX_set_cert_cb])
   # Functions introduced in OpenSSL 1.1.0. We used to check for
   # OPENSSL_VERSION_NUMBER, but that didn't work with 1.1.0, because LibreSSL
   # defines OPENSSL_VERSION_NUMBER to claim version 2.0.0, even though it
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 3706d349ab..ac1c379f2a 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1820,6 +1820,60 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-sslcertmode" xreflabel="sslcertmode">
+      <term><literal>sslcertmode</literal></term>
+      <listitem>
+       <para>
+        This option determines whether a client certificate may be sent to the
+        server, and whether the server is required to request one. There are
+        three modes:
+
+        <variablelist>
+         <varlistentry>
+          <term><literal>disable</literal></term>
+          <listitem>
+           <para>
+            a client certificate is never sent, even if one is provided via
+            <xref linkend="libpq-connect-sslcert" />
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>allow</literal> (default)</term>
+          <listitem>
+           <para>
+            a certificate may be sent, if the server requests one and it has
+            been provided via <literal>sslcert</literal>
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>require</literal></term>
+          <listitem>
+           <para>
+            the server <emphasis>must</emphasis> request a certificate. The
+            connection will fail if the client does not send a certificate and
+            the server successfully authenticates the client anyway.
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+       </para>
+
+       <note>
+        <para>
+         <literal>sslcertmode=require</literal> doesn't add any additional
+         security, since there is no guarantee that the server is validating the
+         certificate correctly; PostgreSQL servers generally request TLS
+         certificates from clients whether they validate them or not. The option
+         may be useful when troubleshooting more complicated TLS setups.
+        </para>
+       </note>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-sslrootcert" xreflabel="sslrootcert">
       <term><literal>sslrootcert</literal></term>
       <listitem>
@@ -7996,6 +8050,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGSSLCERTMODE</envar></primary>
+      </indexterm>
+      <envar>PGSSLCERTMODE</envar> behaves the same as the <xref
+      linkend="libpq-connect-sslcertmode"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/meson.build b/meson.build
index 2ebdf914c1..6a5e76af94 100644
--- a/meson.build
+++ b/meson.build
@@ -1219,8 +1219,9 @@ if sslopt in ['auto', 'openssl']
       ['CRYPTO_new_ex_data', {'required': true}],
       ['SSL_new', {'required': true}],
 
-      # Function introduced in OpenSSL 1.0.2.
+      # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all of these.
       ['X509_get_signature_nid'],
+      ['SSL_CTX_set_cert_cb'],
 
       # Functions introduced in OpenSSL 1.1.0. We used to check for
       # OPENSSL_VERSION_NUMBER, but that didn't work with 1.1.0, because LibreSSL
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 20c82f5979..c5a6762fc1 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -394,6 +394,9 @@
 /* Define to 1 if you have spinlocks. */
 #undef HAVE_SPINLOCKS
 
+/* Define to 1 if you have the `SSL_CTX_set_cert_cb' function. */
+#undef HAVE_SSL_CTX_SET_CERT_CB
+
 /* Define to 1 if stdbool.h conforms to C99. */
 #undef HAVE_STDBOOL_H
 
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index a3b80dc550..b2497acdad 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -862,6 +862,25 @@ check_expected_areq(AuthRequest areq, PGconn *conn)
 	StaticAssertDecl((sizeof(conn->allowed_auth_methods) * CHAR_BIT) > AUTH_REQ_MAX,
 					 "AUTH_REQ_MAX overflows the allowed_auth_methods bitmask");
 
+	if (conn->sslcertmode[0] == 'r' /* require */
+		&& areq == AUTH_REQ_OK)
+	{
+		/*
+		 * Trade off a little bit of complexity to try to get these error
+		 * messages as precise as possible.
+		 */
+		if (!conn->ssl_cert_requested)
+		{
+			libpq_append_conn_error(conn, "server did not request a certificate");
+			return false;
+		}
+		else if (!conn->ssl_cert_sent)
+		{
+			libpq_append_conn_error(conn, "server accepted connection without a valid certificate");
+			return false;
+		}
+	}
+
 	/*
 	 * If the user required a specific auth method, or specified an allowed
 	 * set, then reject all others here, and make sure the server actually
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index dd4b98e099..cbadb3f6af 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -125,8 +125,10 @@ static int	ldapServiceLookup(const char *purl, PQconninfoOption *options,
 #define DefaultTargetSessionAttrs	"any"
 #ifdef USE_SSL
 #define DefaultSSLMode "prefer"
+#define DefaultSSLCertMode "allow"
 #else
 #define DefaultSSLMode	"disable"
+#define DefaultSSLCertMode "disable"
 #endif
 #ifdef ENABLE_GSS
 #include "fe-gssapi-common.h"
@@ -283,6 +285,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"SSL-Client-Key", "", 64,
 	offsetof(struct pg_conn, sslkey)},
 
+	{"sslcertmode", "PGSSLCERTMODE", NULL, NULL,
+		"SSL-Client-Cert-Mode", "", 8, /* sizeof("disable") == 8 */
+	offsetof(struct pg_conn, sslcertmode)},
+
 	{"sslpassword", NULL, NULL, NULL,
 		"SSL-Client-Key-Password", "*", 20,
 	offsetof(struct pg_conn, sslpassword)},
@@ -1510,6 +1516,51 @@ connectOptions2(PGconn *conn)
 		return false;
 	}
 
+	/*
+	 * validate sslcertmode option
+	 */
+	if (conn->sslcertmode)
+	{
+		if (strcmp(conn->sslcertmode, "disable") != 0 &&
+			strcmp(conn->sslcertmode, "allow") != 0 &&
+			strcmp(conn->sslcertmode, "require") != 0)
+		{
+			conn->status = CONNECTION_BAD;
+			libpq_append_conn_error(conn, "invalid %s value: \"%s\"",
+									"sslcertmode", conn->sslcertmode);
+			return false;
+		}
+#ifndef USE_SSL
+		if (strcmp(conn->sslcertmode, "require") == 0)
+		{
+			conn->status = CONNECTION_BAD;
+			libpq_append_conn_error(conn, "sslcertmode value \"%s\" invalid when SSL support is not compiled in",
+									conn->sslcertmode);
+			return false;
+		}
+#endif
+#ifndef HAVE_SSL_CTX_SET_CERT_CB
+		/*
+		 * Without a certificate callback, the current implementation can't
+		 * figure out if a certficate was actually requested, so "require" is
+		 * useless.
+		 */
+		if (strcmp(conn->sslcertmode, "require") == 0)
+		{
+			conn->status = CONNECTION_BAD;
+			libpq_append_conn_error(conn, "sslcertmode value \"%s\" is not supported (check OpenSSL version)",
+									conn->sslcertmode);
+			return false;
+		}
+#endif
+	}
+	else
+	{
+		conn->sslcertmode = strdup(DefaultSSLCertMode);
+		if (!conn->sslcertmode)
+			goto oom_error;
+	}
+
 	/*
 	 * validate gssencmode option
 	 */
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 6a4431ddfe..b88d9da3e2 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -462,6 +462,33 @@ verify_cb(int ok, X509_STORE_CTX *ctx)
 	return ok;
 }
 
+#ifdef HAVE_SSL_CTX_SET_CERT_CB
+/*
+ * Certificate selection callback
+ *
+ * This callback lets us choose the client certificate we send to the server
+ * after seeing its CertificateRequest. We only support sending a single
+ * hard-coded certificate via sslcert, so we don't actually set any certificates
+ * here; we just use it to record whether or not the server has actually asked
+ * for one and whether we have one to send.
+ */
+static int
+cert_cb(SSL *ssl, void *arg)
+{
+	PGconn *conn = arg;
+	conn->ssl_cert_requested = true;
+
+	/* Do we have a certificate loaded to send back? */
+	if (SSL_get_certificate(ssl))
+		conn->ssl_cert_sent = true;
+
+	/*
+	 * Tell OpenSSL that the callback succeeded; we're not required to actually
+	 * make any changes to the SSL handle.
+	 */
+	return 1;
+}
+#endif
 
 /*
  * OpenSSL-specific wrapper around
@@ -953,6 +980,11 @@ initialize_SSL(PGconn *conn)
 		SSL_CTX_set_default_passwd_cb_userdata(SSL_context, conn);
 	}
 
+#ifdef HAVE_SSL_CTX_SET_CERT_CB
+	/* Set up a certificate selection callback. */
+	SSL_CTX_set_cert_cb(SSL_context, cert_cb, conn);
+#endif
+
 	/* Disable old protocol versions */
 	SSL_CTX_set_options(SSL_context, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
 
@@ -1107,7 +1139,12 @@ initialize_SSL(PGconn *conn)
 	else
 		fnbuf[0] = '\0';
 
-	if (fnbuf[0] == '\0')
+	if (conn->sslcertmode[0] == 'd') /* disable */
+	{
+		/* don't send a client cert even if we have one */
+		have_cert = false;
+	}
+	else if (fnbuf[0] == '\0')
 	{
 		/* no home directory, proceed without a client cert */
 		have_cert = false;
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 1dc264fe54..f1f1d973cc 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -384,6 +384,7 @@ struct pg_conn
 	char	   *sslkey;			/* client key filename */
 	char	   *sslcert;		/* client certificate filename */
 	char	   *sslpassword;	/* client key file password */
+	char	   *sslcertmode;	/* client cert mode (require,allow,disable) */
 	char	   *sslrootcert;	/* root certificate filename */
 	char	   *sslcrl;			/* certificate revocation list filename */
 	char	   *sslcrldir;		/* certificate revocation list directory name */
@@ -527,6 +528,8 @@ struct pg_conn
 
 	/* SSL structures */
 	bool		ssl_in_use;
+	bool		ssl_cert_requested;	/* Did the server ask us for a cert? */
+	bool		ssl_cert_sent;		/* Did we send one in reply? */
 
 #ifdef USE_SSL
 	bool		allow_ssl_try;	/* Allowed to try SSL negotiation */
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 3094e27af3..4617f06f86 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -42,6 +42,10 @@ my $SERVERHOSTADDR = '127.0.0.1';
 # This is the pattern to use in pg_hba.conf to match incoming connections.
 my $SERVERHOSTCIDR = '127.0.0.1/32';
 
+# Determine whether build supports sslcertmode=require.
+my $supports_sslcertmode_require =
+  check_pg_config("#define HAVE_SSL_CTX_SET_CERT_CB 1");
+
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
 
@@ -191,6 +195,22 @@ $node->connect_ok(
 	"$common_connstr sslrootcert=ssl/both-cas-2.crt sslmode=verify-ca",
 	"cert root file that contains two certificates, order 2");
 
+# sslcertmode=allow and =disable should both work without a client certificate.
+$node->connect_ok(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=disable",
+	"connect with sslcertmode=disable");
+$node->connect_ok(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=allow",
+	"connect with sslcertmode=allow");
+
+# sslcertmode=require, however, should fail.
+$node->connect_fails(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=require",
+	"connect with sslcertmode=require fails without a client certificate",
+	expected_stderr => $supports_sslcertmode_require
+		? qr/server accepted connection without a valid certificate/
+		: qr/sslcertmode value "require" is not supported/);
+
 # CRL tests
 
 # Invalid CRL filename is the same as no CRL, succeeds
@@ -538,6 +558,29 @@ $node->connect_ok(
 	"certificate authorization succeeds with correct client cert in encrypted DER format"
 );
 
+# correct client cert with required/allowed certificate authentication
+if ($supports_sslcertmode_require)
+{
+	$node->connect_ok(
+		"$common_connstr user=ssltestuser sslcertmode=require sslcert=ssl/client.crt "
+		  . sslkey('client.key'),
+		"certificate authorization succeeds with sslcertmode=require"
+	);
+}
+$node->connect_ok(
+	"$common_connstr user=ssltestuser sslcertmode=allow sslcert=ssl/client.crt "
+	  . sslkey('client.key'),
+	"certificate authorization succeeds with sslcertmode=allow"
+);
+
+# client cert isn't sent if certificate authentication is disabled
+$node->connect_fails(
+	"$common_connstr user=ssltestuser sslcertmode=disable sslcert=ssl/client.crt "
+	  . sslkey('client.key'),
+	"certificate authorization fails with sslcertmode=disable",
+	expected_stderr => qr/connection requires a valid client certificate/
+);
+
 # correct client cert in encrypted PEM with wrong password
 $node->connect_fails(
 	"$common_connstr user=ssltestuser sslcert=ssl/client.crt "
diff --git a/src/tools/msvc/Solution.pm b/src/tools/msvc/Solution.pm
index 5eaea6355e..1ec1bac552 100644
--- a/src/tools/msvc/Solution.pm
+++ b/src/tools/msvc/Solution.pm
@@ -327,6 +327,7 @@ sub GenerateFiles
 		HAVE_SETPROCTITLE_FAST                   => undef,
 		HAVE_SOCKLEN_T                           => 1,
 		HAVE_SPINLOCKS                           => 1,
+		HAVE_SSL_CTX_SET_CERT_CB                 => undef,
 		HAVE_STDBOOL_H                           => 1,
 		HAVE_STDINT_H                            => 1,
 		HAVE_STDLIB_H                            => 1,
@@ -507,6 +508,14 @@ sub GenerateFiles
 			$define{HAVE_HMAC_CTX_NEW}          = 1;
 			$define{HAVE_OPENSSL_INIT_SSL}      = 1;
 		}
+
+		# Symbols needed with OpenSSL 1.0.2 and above.
+		if (   ($digit1 >= '3' && $digit2 >= '0' && $digit3 >= '0')
+			|| ($digit1 >= '1' && $digit2 >= '1' && $digit3 >= '0')
+			|| ($digit1 >= '1' && $digit2 >= '0' && $digit3 >= '2'))
+		{
+			$define{HAVE_SSL_CTX_SET_CERT_CB} = 1;
+		}
 	}
 
 	$self->GenerateConfigHeader('src/include/pg_config.h',     \%define, 1);
-- 
2.25.1

#58Michael Paquier
michael@paquier.xyz
In reply to: Jacob Champion (#57)
Re: [PoC] Let libpq reject unexpected authentication requests

On Tue, Mar 14, 2023 at 12:14:40PM -0700, Jacob Champion wrote:

On Mon, Mar 13, 2023 at 10:39 PM Michael Paquier <michael@paquier.xyz> wrote:

0002 looks pretty simple as well, I think that's worth a look for this
CF.

Cool. v17 just rebases the set over HEAD, then, for cfbot.

I have looked at 0002, and I am on board with using a separate
connection parameter for this case, orthogonal to require_auth, with
the three value "allow", "disable" and "require". So that's one thing
:)

-      # Function introduced in OpenSSL 1.0.2.
+      # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all of these.
       ['X509_get_signature_nid'],
+      ['SSL_CTX_set_cert_cb'],

From what I can see, X509_get_signature_nid() is in LibreSSL, but not
SSL_CTX_set_cert_cb(). Perhaps that's worth having two different
comments?

+           <para>
+            a certificate may be sent, if the server requests one and it has
+            been provided via <literal>sslcert</literal>
+           </para>

It seems to me that this description is not completely exact. The
default is to look at ~/.postgresql/postgresql.crt, so sslcert is not
mandatory. There could be a certificate even without sslcert set.

+           libpq_append_conn_error(conn, "sslcertmode value \"%s\" invalid when SSL support is not compiled in",
+                                   conn->sslcertmode);

This string could be combined with the same one used for sslmode,
saving a bit in translation effortm by making the connection parameter
name a value of the string ("%s value \"%s\" invalid .."). The second
string where HAVE_SSL_CTX_SET_CERT_CB is not set could be refactored
the same way, I guess.

+ * figure out if a certficate was actually requested, so "require" is
s/certficate/certificate/.

contrib/sslinfo/ has ssl_client_cert_present(), that we could use in
the tests to make sure that the client has actually sent a
certificate? How about adding some of these tests to 003_sslinfo.pl
for the "allow" and "require" cases? Even for "disable", we could
check check that ssl_client_cert_present() returns false? That would
make four tests if everything is covered:
- "allow" without a certificate sent.
- "allow" with a certificate sent.
- "disable".
- "require"

+       if (!conn->ssl_cert_requested)
+       {
+           libpq_append_conn_error(conn, "server did not request a certificate");
+           return false;
+       }
+       else if (!conn->ssl_cert_sent)
+       {
+           libpq_append_conn_error(conn, "server accepted connection without a valid certificate");
+           return false;
+       }
Perhaps useless question: should this say "SSL certificate"?

freePGconn() is missing a free(sslcertmode).
--
Michael

#59Jacob Champion
jchampion@timescale.com
In reply to: Michael Paquier (#58)
3 attachment(s)
Re: [PoC] Let libpq reject unexpected authentication requests

On Tue, Mar 21, 2023 at 11:01 PM Michael Paquier <michael@paquier.xyz> wrote:

-      # Function introduced in OpenSSL 1.0.2.
+      # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all of these.
['X509_get_signature_nid'],
+      ['SSL_CTX_set_cert_cb'],

From what I can see, X509_get_signature_nid() is in LibreSSL, but not
SSL_CTX_set_cert_cb(). Perhaps that's worth having two different
comments?

I took a stab at that in v18. I diverged a bit between Meson and
Autoconf, which you may not care for.

+           <para>
+            a certificate may be sent, if the server requests one and it has
+            been provided via <literal>sslcert</literal>
+           </para>

It seems to me that this description is not completely exact. The
default is to look at ~/.postgresql/postgresql.crt, so sslcert is not
mandatory. There could be a certificate even without sslcert set.

Reworded.

+           libpq_append_conn_error(conn, "sslcertmode value \"%s\" invalid when SSL support is not compiled in",
+                                   conn->sslcertmode);

This string could be combined with the same one used for sslmode,
saving a bit in translation effortm by making the connection parameter
name a value of the string ("%s value \"%s\" invalid ..").

Done.

+ * figure out if a certficate was actually requested, so "require" is
s/certficate/certificate/.

Heh, fixed. I need new glasses, clearly.

contrib/sslinfo/ has ssl_client_cert_present(), that we could use in
the tests to make sure that the client has actually sent a
certificate? How about adding some of these tests to 003_sslinfo.pl
for the "allow" and "require" cases?

Added; see what you think.

+       if (!conn->ssl_cert_requested)
+       {
+           libpq_append_conn_error(conn, "server did not request a certificate");
+           return false;
+       }
+       else if (!conn->ssl_cert_sent)
+       {
+           libpq_append_conn_error(conn, "server accepted connection without a valid certificate");
+           return false;
+       }
Perhaps useless question: should this say "SSL certificate"?

I have no objection, so done that way.

freePGconn() is missing a free(sslcertmode).

Argh, I keep forgetting that. Fixed, thanks!

--Jacob

Attachments:

since-v17.diff.txttext/plain; charset=US-ASCII; name=since-v17.diff.txtDownload
1:  8d93ca3792 ! 1:  ad71dc8b72 Add sslcertmode option for client certificates
    @@ configure: else
        fi
     -  # Function introduced in OpenSSL 1.0.2.
     -  for ac_func in X509_get_signature_nid
    -+  # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all of these.
    ++  # Functions introduced in OpenSSL 1.0.2. Note that LibreSSL doesn't have
    ++  # SSL_CTX_set_cert_cb().
     +  for ac_func in X509_get_signature_nid SSL_CTX_set_cert_cb
      do :
     -  ac_fn_c_check_func "$LINENO" "X509_get_signature_nid" "ac_cv_func_X509_get_signature_nid"
    @@ configure.ac: if test "$with_ssl" = openssl ; then
        fi
     -  # Function introduced in OpenSSL 1.0.2.
     -  AC_CHECK_FUNCS([X509_get_signature_nid])
    -+  # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all of these.
    ++  # Functions introduced in OpenSSL 1.0.2. Note that LibreSSL doesn't have
    ++  # SSL_CTX_set_cert_cb().
     +  AC_CHECK_FUNCS([X509_get_signature_nid SSL_CTX_set_cert_cb])
        # Functions introduced in OpenSSL 1.1.0. We used to check for
        # OPENSSL_VERSION_NUMBER, but that didn't work with 1.1.0, because LibreSSL
    @@ doc/src/sgml/libpq.sgml: postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
     +          <term><literal>allow</literal> (default)</term>
     +          <listitem>
     +           <para>
    -+            a certificate may be sent, if the server requests one and it has
    -+            been provided via <literal>sslcert</literal>
    ++            a certificate may be sent, if the server requests one and the client
    ++            has one to send
     +           </para>
     +          </listitem>
     +         </varlistentry>
    @@ meson.build: if sslopt in ['auto', 'openssl']
            ['SSL_new', {'required': true}],
      
     -      # Function introduced in OpenSSL 1.0.2.
    -+      # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all of these.
    ++      # Functions introduced in OpenSSL 1.0.2.
            ['X509_get_signature_nid'],
    -+      ['SSL_CTX_set_cert_cb'],
    ++      ['SSL_CTX_set_cert_cb'], # not in LibreSSL
      
            # Functions introduced in OpenSSL 1.1.0. We used to check for
            # OPENSSL_VERSION_NUMBER, but that didn't work with 1.1.0, because LibreSSL
    @@ src/interfaces/libpq/fe-auth.c: check_expected_areq(AuthRequest areq, PGconn *co
     +		 */
     +		if (!conn->ssl_cert_requested)
     +		{
    -+			libpq_append_conn_error(conn, "server did not request a certificate");
    ++			libpq_append_conn_error(conn, "server did not request an SSL certificate");
     +			return false;
     +		}
     +		else if (!conn->ssl_cert_sent)
     +		{
    -+			libpq_append_conn_error(conn, "server accepted connection without a valid certificate");
    ++			libpq_append_conn_error(conn, "server accepted connection without a valid SSL certificate");
     +			return false;
     +		}
     +	}
    @@ src/interfaces/libpq/fe-connect.c: static const internalPQconninfoOption PQconni
      	{"sslpassword", NULL, NULL, NULL,
      		"SSL-Client-Key-Password", "*", 20,
      	offsetof(struct pg_conn, sslpassword)},
    +@@ src/interfaces/libpq/fe-connect.c: connectOptions2(PGconn *conn)
    + 			case 'r':			/* "require" */
    + 			case 'v':			/* "verify-ca" or "verify-full" */
    + 				conn->status = CONNECTION_BAD;
    +-				libpq_append_conn_error(conn, "sslmode value \"%s\" invalid when SSL support is not compiled in",
    +-										conn->sslmode);
    ++				libpq_append_conn_error(conn, "%s value \"%s\" invalid when SSL support is not compiled in",
    ++										"sslmode", conn->sslmode);
    + 				return false;
    + 		}
    + #endif
     @@ src/interfaces/libpq/fe-connect.c: connectOptions2(PGconn *conn)
      		return false;
      	}
    @@ src/interfaces/libpq/fe-connect.c: connectOptions2(PGconn *conn)
     +		if (strcmp(conn->sslcertmode, "require") == 0)
     +		{
     +			conn->status = CONNECTION_BAD;
    -+			libpq_append_conn_error(conn, "sslcertmode value \"%s\" invalid when SSL support is not compiled in",
    -+									conn->sslcertmode);
    ++			libpq_append_conn_error(conn, "%s value \"%s\" invalid when SSL support is not compiled in",
    ++									"sslcertmode", conn->sslcertmode);
     +			return false;
     +		}
     +#endif
     +#ifndef HAVE_SSL_CTX_SET_CERT_CB
     +		/*
     +		 * Without a certificate callback, the current implementation can't
    -+		 * figure out if a certficate was actually requested, so "require" is
    ++		 * figure out if a certificate was actually requested, so "require" is
     +		 * useless.
     +		 */
     +		if (strcmp(conn->sslcertmode, "require") == 0)
    @@ src/interfaces/libpq/fe-connect.c: connectOptions2(PGconn *conn)
      	/*
      	 * validate gssencmode option
      	 */
    +@@ src/interfaces/libpq/fe-connect.c: freePGconn(PGconn *conn)
    + 		explicit_bzero(conn->sslpassword, strlen(conn->sslpassword));
    + 		free(conn->sslpassword);
    + 	}
    ++	free(conn->sslcertmode);
    + 	free(conn->sslrootcert);
    + 	free(conn->sslcrl);
    + 	free(conn->sslcrldir);
     
      ## src/interfaces/libpq/fe-secure-openssl.c ##
     @@ src/interfaces/libpq/fe-secure-openssl.c: verify_cb(int ok, X509_STORE_CTX *ctx)
    @@ src/test/ssl/t/001_ssltests.pl: $node->connect_ok(
     +	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=require",
     +	"connect with sslcertmode=require fails without a client certificate",
     +	expected_stderr => $supports_sslcertmode_require
    -+		? qr/server accepted connection without a valid certificate/
    ++		? qr/server accepted connection without a valid SSL certificate/
     +		: qr/sslcertmode value "require" is not supported/);
     +
      # CRL tests
    @@ src/test/ssl/t/001_ssltests.pl: $node->connect_ok(
      $node->connect_fails(
      	"$common_connstr user=ssltestuser sslcert=ssl/client.crt "
     
    + ## src/test/ssl/t/003_sslinfo.pl ##
    +@@ src/test/ssl/t/003_sslinfo.pl: my $SERVERHOSTADDR = '127.0.0.1';
    + # This is the pattern to use in pg_hba.conf to match incoming connections.
    + my $SERVERHOSTCIDR = '127.0.0.1/32';
    + 
    ++# Determine whether build supports sslcertmode=require.
    ++my $supports_sslcertmode_require =
    ++  check_pg_config("#define HAVE_SSL_CTX_SET_CERT_CB 1");
    ++
    + # Allocation of base connection string shared among multiple tests.
    + my $common_connstr;
    + 
    +@@ src/test/ssl/t/003_sslinfo.pl: $result = $node->safe_psql(
    + 	connstr => $common_connstr);
    + is($result, 'CA:FALSE|t', 'extract extension from cert');
    + 
    ++# Sanity tests for sslcertmode, using ssl_client_cert_present()
    ++my @cases = (
    ++	{ opts => "sslcertmode=allow",					present => 't' },
    ++	{ opts => "sslcertmode=allow sslcert=invalid",	present => 'f' },
    ++	{ opts => "sslcertmode=disable",				present => 'f' },
    ++);
    ++if ($supports_sslcertmode_require)
    ++{
    ++	push(@cases, { opts => "sslcertmode=require",	present => 't' });
    ++}
    ++
    ++foreach my $c (@cases) {
    ++	$result = $node->safe_psql(
    ++		"trustdb",
    ++		"SELECT ssl_client_cert_present();",
    ++		connstr => "$common_connstr dbname=trustdb $c->{'opts'}"
    ++	);
    ++	is($result, $c->{'present'}, "ssl_client_cert_present() for $c->{'opts'}");
    ++}
    ++
    + done_testing();
    +
      ## src/tools/msvc/Solution.pm ##
     @@ src/tools/msvc/Solution.pm: sub GenerateFiles
      		HAVE_SETPROCTITLE_FAST                   => undef,
2:  e2343c0089 ! 2:  c9ecdd54ea require_auth: decouple SASL and SCRAM
    @@ src/interfaces/libpq/fe-connect.c: connectOptions2(PGconn *conn)
     +				free(part);
     +				continue; /* avoid the bitmask manipulation below */
      			}
    - 			else if (strcmp(method, "creds") == 0)
    + 			else if (strcmp(method, "none") == 0)
      			{
     
      ## src/interfaces/libpq/libpq-int.h ##
v18-0001-Add-sslcertmode-option-for-client-certificates.patchtext/x-patch; charset=US-ASCII; name=v18-0001-Add-sslcertmode-option-for-client-certificates.patchDownload
From ad71dc8b727fc57548693946904de53c2be7defc Mon Sep 17 00:00:00 2001
From: Jacob Champion <jchampion@timescale.com>
Date: Fri, 24 Jun 2022 15:40:42 -0700
Subject: [PATCH v18 1/2] Add sslcertmode option for client certificates

The sslcertmode option controls whether the server is allowed and/or
required to request a certificate from the client. There are three
modes:

- "allow" is the default and follows the current behavior -- a
  configured  sslcert is sent if the server requests one (which, with
  the current implementation, will happen whenever TLS is negotiated).

- "disable" causes the client to refuse to send a client certificate
  even if an sslcert is configured.

- "require" causes the client to fail if a client certificate is never
  sent and the server opens a connection anyway. This doesn't add any
  additional security, since there is no guarantee that the server is
  validating the certificate correctly, but it may help troubleshoot
  more complicated TLS setups.

sslcertmode=require needs the OpenSSL implementation to support
SSL_CTX_set_cert_cb(). Notably, LibreSSL does not.
---
 configure                                | 12 +++--
 configure.ac                             |  5 +-
 doc/src/sgml/libpq.sgml                  | 64 ++++++++++++++++++++++++
 meson.build                              |  3 +-
 src/include/pg_config.h.in               |  3 ++
 src/interfaces/libpq/fe-auth.c           | 19 +++++++
 src/interfaces/libpq/fe-connect.c        | 56 ++++++++++++++++++++-
 src/interfaces/libpq/fe-secure-openssl.c | 39 ++++++++++++++-
 src/interfaces/libpq/libpq-int.h         |  3 ++
 src/test/ssl/t/001_ssltests.pl           | 43 ++++++++++++++++
 src/test/ssl/t/003_sslinfo.pl            | 24 +++++++++
 src/tools/msvc/Solution.pm               |  9 ++++
 12 files changed, 269 insertions(+), 11 deletions(-)

diff --git a/configure b/configure
index e221dd5b0f..037e8ace54 100755
--- a/configure
+++ b/configure
@@ -12973,13 +12973,15 @@ else
 fi
 
   fi
-  # Function introduced in OpenSSL 1.0.2.
-  for ac_func in X509_get_signature_nid
+  # Functions introduced in OpenSSL 1.0.2. Note that LibreSSL doesn't have
+  # SSL_CTX_set_cert_cb().
+  for ac_func in X509_get_signature_nid SSL_CTX_set_cert_cb
 do :
-  ac_fn_c_check_func "$LINENO" "X509_get_signature_nid" "ac_cv_func_X509_get_signature_nid"
-if test "x$ac_cv_func_X509_get_signature_nid" = xyes; then :
+  as_ac_var=`$as_echo "ac_cv_func_$ac_func" | $as_tr_sh`
+ac_fn_c_check_func "$LINENO" "$ac_func" "$as_ac_var"
+if eval test \"x\$"$as_ac_var"\" = x"yes"; then :
   cat >>confdefs.h <<_ACEOF
-#define HAVE_X509_GET_SIGNATURE_NID 1
+#define `$as_echo "HAVE_$ac_func" | $as_tr_cpp` 1
 _ACEOF
 
 fi
diff --git a/configure.ac b/configure.ac
index 3aa6c15c13..f3ae7569d8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1373,8 +1373,9 @@ if test "$with_ssl" = openssl ; then
      AC_SEARCH_LIBS(CRYPTO_new_ex_data, [eay32 crypto], [], [AC_MSG_ERROR([library 'eay32' or 'crypto' is required for OpenSSL])])
      AC_SEARCH_LIBS(SSL_new, [ssleay32 ssl], [], [AC_MSG_ERROR([library 'ssleay32' or 'ssl' is required for OpenSSL])])
   fi
-  # Function introduced in OpenSSL 1.0.2.
-  AC_CHECK_FUNCS([X509_get_signature_nid])
+  # Functions introduced in OpenSSL 1.0.2. Note that LibreSSL doesn't have
+  # SSL_CTX_set_cert_cb().
+  AC_CHECK_FUNCS([X509_get_signature_nid SSL_CTX_set_cert_cb])
   # Functions introduced in OpenSSL 1.1.0. We used to check for
   # OPENSSL_VERSION_NUMBER, but that didn't work with 1.1.0, because LibreSSL
   # defines OPENSSL_VERSION_NUMBER to claim version 2.0.0, even though it
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 9ee5532c07..d4cc470be7 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1810,6 +1810,60 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-sslcertmode" xreflabel="sslcertmode">
+      <term><literal>sslcertmode</literal></term>
+      <listitem>
+       <para>
+        This option determines whether a client certificate may be sent to the
+        server, and whether the server is required to request one. There are
+        three modes:
+
+        <variablelist>
+         <varlistentry>
+          <term><literal>disable</literal></term>
+          <listitem>
+           <para>
+            a client certificate is never sent, even if one is provided via
+            <xref linkend="libpq-connect-sslcert" />
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>allow</literal> (default)</term>
+          <listitem>
+           <para>
+            a certificate may be sent, if the server requests one and the client
+            has one to send
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>require</literal></term>
+          <listitem>
+           <para>
+            the server <emphasis>must</emphasis> request a certificate. The
+            connection will fail if the client does not send a certificate and
+            the server successfully authenticates the client anyway.
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+       </para>
+
+       <note>
+        <para>
+         <literal>sslcertmode=require</literal> doesn't add any additional
+         security, since there is no guarantee that the server is validating the
+         certificate correctly; PostgreSQL servers generally request TLS
+         certificates from clients whether they validate them or not. The option
+         may be useful when troubleshooting more complicated TLS setups.
+        </para>
+       </note>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-sslrootcert" xreflabel="sslrootcert">
       <term><literal>sslrootcert</literal></term>
       <listitem>
@@ -7986,6 +8040,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGSSLCERTMODE</envar></primary>
+      </indexterm>
+      <envar>PGSSLCERTMODE</envar> behaves the same as the <xref
+      linkend="libpq-connect-sslcertmode"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/meson.build b/meson.build
index 7f76a101ec..b0bdc17b1d 100644
--- a/meson.build
+++ b/meson.build
@@ -1219,8 +1219,9 @@ if sslopt in ['auto', 'openssl']
       ['CRYPTO_new_ex_data', {'required': true}],
       ['SSL_new', {'required': true}],
 
-      # Function introduced in OpenSSL 1.0.2.
+      # Functions introduced in OpenSSL 1.0.2.
       ['X509_get_signature_nid'],
+      ['SSL_CTX_set_cert_cb'], # not in LibreSSL
 
       # Functions introduced in OpenSSL 1.1.0. We used to check for
       # OPENSSL_VERSION_NUMBER, but that didn't work with 1.1.0, because LibreSSL
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 4882c70559..3665e799e7 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -394,6 +394,9 @@
 /* Define to 1 if you have spinlocks. */
 #undef HAVE_SPINLOCKS
 
+/* Define to 1 if you have the `SSL_CTX_set_cert_cb' function. */
+#undef HAVE_SSL_CTX_SET_CERT_CB
+
 /* Define to 1 if stdbool.h conforms to C99. */
 #undef HAVE_STDBOOL_H
 
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index fa95f8e6e9..934e3f4f7c 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -798,6 +798,25 @@ check_expected_areq(AuthRequest areq, PGconn *conn)
 	StaticAssertDecl((sizeof(conn->allowed_auth_methods) * CHAR_BIT) > AUTH_REQ_MAX,
 					 "AUTH_REQ_MAX overflows the allowed_auth_methods bitmask");
 
+	if (conn->sslcertmode[0] == 'r' /* require */
+		&& areq == AUTH_REQ_OK)
+	{
+		/*
+		 * Trade off a little bit of complexity to try to get these error
+		 * messages as precise as possible.
+		 */
+		if (!conn->ssl_cert_requested)
+		{
+			libpq_append_conn_error(conn, "server did not request an SSL certificate");
+			return false;
+		}
+		else if (!conn->ssl_cert_sent)
+		{
+			libpq_append_conn_error(conn, "server accepted connection without a valid SSL certificate");
+			return false;
+		}
+	}
+
 	/*
 	 * If the user required a specific auth method, or specified an allowed
 	 * set, then reject all others here, and make sure the server actually
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index b9f899c552..15515fbd45 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -125,8 +125,10 @@ static int	ldapServiceLookup(const char *purl, PQconninfoOption *options,
 #define DefaultTargetSessionAttrs	"any"
 #ifdef USE_SSL
 #define DefaultSSLMode "prefer"
+#define DefaultSSLCertMode "allow"
 #else
 #define DefaultSSLMode	"disable"
+#define DefaultSSLCertMode "disable"
 #endif
 #ifdef ENABLE_GSS
 #include "fe-gssapi-common.h"
@@ -283,6 +285,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"SSL-Client-Key", "", 64,
 	offsetof(struct pg_conn, sslkey)},
 
+	{"sslcertmode", "PGSSLCERTMODE", NULL, NULL,
+		"SSL-Client-Cert-Mode", "", 8, /* sizeof("disable") == 8 */
+	offsetof(struct pg_conn, sslcertmode)},
+
 	{"sslpassword", NULL, NULL, NULL,
 		"SSL-Client-Key-Password", "*", 20,
 	offsetof(struct pg_conn, sslpassword)},
@@ -1457,8 +1463,8 @@ connectOptions2(PGconn *conn)
 			case 'r':			/* "require" */
 			case 'v':			/* "verify-ca" or "verify-full" */
 				conn->status = CONNECTION_BAD;
-				libpq_append_conn_error(conn, "sslmode value \"%s\" invalid when SSL support is not compiled in",
-										conn->sslmode);
+				libpq_append_conn_error(conn, "%s value \"%s\" invalid when SSL support is not compiled in",
+										"sslmode", conn->sslmode);
 				return false;
 		}
 #endif
@@ -1506,6 +1512,51 @@ connectOptions2(PGconn *conn)
 		return false;
 	}
 
+	/*
+	 * validate sslcertmode option
+	 */
+	if (conn->sslcertmode)
+	{
+		if (strcmp(conn->sslcertmode, "disable") != 0 &&
+			strcmp(conn->sslcertmode, "allow") != 0 &&
+			strcmp(conn->sslcertmode, "require") != 0)
+		{
+			conn->status = CONNECTION_BAD;
+			libpq_append_conn_error(conn, "invalid %s value: \"%s\"",
+									"sslcertmode", conn->sslcertmode);
+			return false;
+		}
+#ifndef USE_SSL
+		if (strcmp(conn->sslcertmode, "require") == 0)
+		{
+			conn->status = CONNECTION_BAD;
+			libpq_append_conn_error(conn, "%s value \"%s\" invalid when SSL support is not compiled in",
+									"sslcertmode", conn->sslcertmode);
+			return false;
+		}
+#endif
+#ifndef HAVE_SSL_CTX_SET_CERT_CB
+		/*
+		 * Without a certificate callback, the current implementation can't
+		 * figure out if a certificate was actually requested, so "require" is
+		 * useless.
+		 */
+		if (strcmp(conn->sslcertmode, "require") == 0)
+		{
+			conn->status = CONNECTION_BAD;
+			libpq_append_conn_error(conn, "sslcertmode value \"%s\" is not supported (check OpenSSL version)",
+									conn->sslcertmode);
+			return false;
+		}
+#endif
+	}
+	else
+	{
+		conn->sslcertmode = strdup(DefaultSSLCertMode);
+		if (!conn->sslcertmode)
+			goto oom_error;
+	}
+
 	/*
 	 * validate gssencmode option
 	 */
@@ -4238,6 +4289,7 @@ freePGconn(PGconn *conn)
 		explicit_bzero(conn->sslpassword, strlen(conn->sslpassword));
 		free(conn->sslpassword);
 	}
+	free(conn->sslcertmode);
 	free(conn->sslrootcert);
 	free(conn->sslcrl);
 	free(conn->sslcrldir);
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 6a4431ddfe..b88d9da3e2 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -462,6 +462,33 @@ verify_cb(int ok, X509_STORE_CTX *ctx)
 	return ok;
 }
 
+#ifdef HAVE_SSL_CTX_SET_CERT_CB
+/*
+ * Certificate selection callback
+ *
+ * This callback lets us choose the client certificate we send to the server
+ * after seeing its CertificateRequest. We only support sending a single
+ * hard-coded certificate via sslcert, so we don't actually set any certificates
+ * here; we just use it to record whether or not the server has actually asked
+ * for one and whether we have one to send.
+ */
+static int
+cert_cb(SSL *ssl, void *arg)
+{
+	PGconn *conn = arg;
+	conn->ssl_cert_requested = true;
+
+	/* Do we have a certificate loaded to send back? */
+	if (SSL_get_certificate(ssl))
+		conn->ssl_cert_sent = true;
+
+	/*
+	 * Tell OpenSSL that the callback succeeded; we're not required to actually
+	 * make any changes to the SSL handle.
+	 */
+	return 1;
+}
+#endif
 
 /*
  * OpenSSL-specific wrapper around
@@ -953,6 +980,11 @@ initialize_SSL(PGconn *conn)
 		SSL_CTX_set_default_passwd_cb_userdata(SSL_context, conn);
 	}
 
+#ifdef HAVE_SSL_CTX_SET_CERT_CB
+	/* Set up a certificate selection callback. */
+	SSL_CTX_set_cert_cb(SSL_context, cert_cb, conn);
+#endif
+
 	/* Disable old protocol versions */
 	SSL_CTX_set_options(SSL_context, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
 
@@ -1107,7 +1139,12 @@ initialize_SSL(PGconn *conn)
 	else
 		fnbuf[0] = '\0';
 
-	if (fnbuf[0] == '\0')
+	if (conn->sslcertmode[0] == 'd') /* disable */
+	{
+		/* don't send a client cert even if we have one */
+		have_cert = false;
+	}
+	else if (fnbuf[0] == '\0')
 	{
 		/* no home directory, proceed without a client cert */
 		have_cert = false;
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 1dc264fe54..f1f1d973cc 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -384,6 +384,7 @@ struct pg_conn
 	char	   *sslkey;			/* client key filename */
 	char	   *sslcert;		/* client certificate filename */
 	char	   *sslpassword;	/* client key file password */
+	char	   *sslcertmode;	/* client cert mode (require,allow,disable) */
 	char	   *sslrootcert;	/* root certificate filename */
 	char	   *sslcrl;			/* certificate revocation list filename */
 	char	   *sslcrldir;		/* certificate revocation list directory name */
@@ -527,6 +528,8 @@ struct pg_conn
 
 	/* SSL structures */
 	bool		ssl_in_use;
+	bool		ssl_cert_requested;	/* Did the server ask us for a cert? */
+	bool		ssl_cert_sent;		/* Did we send one in reply? */
 
 #ifdef USE_SSL
 	bool		allow_ssl_try;	/* Allowed to try SSL negotiation */
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 3094e27af3..3e5d30c37d 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -42,6 +42,10 @@ my $SERVERHOSTADDR = '127.0.0.1';
 # This is the pattern to use in pg_hba.conf to match incoming connections.
 my $SERVERHOSTCIDR = '127.0.0.1/32';
 
+# Determine whether build supports sslcertmode=require.
+my $supports_sslcertmode_require =
+  check_pg_config("#define HAVE_SSL_CTX_SET_CERT_CB 1");
+
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
 
@@ -191,6 +195,22 @@ $node->connect_ok(
 	"$common_connstr sslrootcert=ssl/both-cas-2.crt sslmode=verify-ca",
 	"cert root file that contains two certificates, order 2");
 
+# sslcertmode=allow and =disable should both work without a client certificate.
+$node->connect_ok(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=disable",
+	"connect with sslcertmode=disable");
+$node->connect_ok(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=allow",
+	"connect with sslcertmode=allow");
+
+# sslcertmode=require, however, should fail.
+$node->connect_fails(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=require",
+	"connect with sslcertmode=require fails without a client certificate",
+	expected_stderr => $supports_sslcertmode_require
+		? qr/server accepted connection without a valid SSL certificate/
+		: qr/sslcertmode value "require" is not supported/);
+
 # CRL tests
 
 # Invalid CRL filename is the same as no CRL, succeeds
@@ -538,6 +558,29 @@ $node->connect_ok(
 	"certificate authorization succeeds with correct client cert in encrypted DER format"
 );
 
+# correct client cert with required/allowed certificate authentication
+if ($supports_sslcertmode_require)
+{
+	$node->connect_ok(
+		"$common_connstr user=ssltestuser sslcertmode=require sslcert=ssl/client.crt "
+		  . sslkey('client.key'),
+		"certificate authorization succeeds with sslcertmode=require"
+	);
+}
+$node->connect_ok(
+	"$common_connstr user=ssltestuser sslcertmode=allow sslcert=ssl/client.crt "
+	  . sslkey('client.key'),
+	"certificate authorization succeeds with sslcertmode=allow"
+);
+
+# client cert isn't sent if certificate authentication is disabled
+$node->connect_fails(
+	"$common_connstr user=ssltestuser sslcertmode=disable sslcert=ssl/client.crt "
+	  . sslkey('client.key'),
+	"certificate authorization fails with sslcertmode=disable",
+	expected_stderr => qr/connection requires a valid client certificate/
+);
+
 # correct client cert in encrypted PEM with wrong password
 $node->connect_fails(
 	"$common_connstr user=ssltestuser sslcert=ssl/client.crt "
diff --git a/src/test/ssl/t/003_sslinfo.pl b/src/test/ssl/t/003_sslinfo.pl
index 3f498fff70..e63256a3b8 100644
--- a/src/test/ssl/t/003_sslinfo.pl
+++ b/src/test/ssl/t/003_sslinfo.pl
@@ -43,6 +43,10 @@ my $SERVERHOSTADDR = '127.0.0.1';
 # This is the pattern to use in pg_hba.conf to match incoming connections.
 my $SERVERHOSTCIDR = '127.0.0.1/32';
 
+# Determine whether build supports sslcertmode=require.
+my $supports_sslcertmode_require =
+  check_pg_config("#define HAVE_SSL_CTX_SET_CERT_CB 1");
+
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
 
@@ -166,4 +170,24 @@ $result = $node->safe_psql(
 	connstr => $common_connstr);
 is($result, 'CA:FALSE|t', 'extract extension from cert');
 
+# Sanity tests for sslcertmode, using ssl_client_cert_present()
+my @cases = (
+	{ opts => "sslcertmode=allow",					present => 't' },
+	{ opts => "sslcertmode=allow sslcert=invalid",	present => 'f' },
+	{ opts => "sslcertmode=disable",				present => 'f' },
+);
+if ($supports_sslcertmode_require)
+{
+	push(@cases, { opts => "sslcertmode=require",	present => 't' });
+}
+
+foreach my $c (@cases) {
+	$result = $node->safe_psql(
+		"trustdb",
+		"SELECT ssl_client_cert_present();",
+		connstr => "$common_connstr dbname=trustdb $c->{'opts'}"
+	);
+	is($result, $c->{'present'}, "ssl_client_cert_present() for $c->{'opts'}");
+}
+
 done_testing();
diff --git a/src/tools/msvc/Solution.pm b/src/tools/msvc/Solution.pm
index b59953e5b5..153be7be11 100644
--- a/src/tools/msvc/Solution.pm
+++ b/src/tools/msvc/Solution.pm
@@ -327,6 +327,7 @@ sub GenerateFiles
 		HAVE_SETPROCTITLE_FAST                   => undef,
 		HAVE_SOCKLEN_T                           => 1,
 		HAVE_SPINLOCKS                           => 1,
+		HAVE_SSL_CTX_SET_CERT_CB                 => undef,
 		HAVE_STDBOOL_H                           => 1,
 		HAVE_STDINT_H                            => 1,
 		HAVE_STDLIB_H                            => 1,
@@ -506,6 +507,14 @@ sub GenerateFiles
 			$define{HAVE_HMAC_CTX_NEW}          = 1;
 			$define{HAVE_OPENSSL_INIT_SSL}      = 1;
 		}
+
+		# Symbols needed with OpenSSL 1.0.2 and above.
+		if (   ($digit1 >= '3' && $digit2 >= '0' && $digit3 >= '0')
+			|| ($digit1 >= '1' && $digit2 >= '1' && $digit3 >= '0')
+			|| ($digit1 >= '1' && $digit2 >= '0' && $digit3 >= '2'))
+		{
+			$define{HAVE_SSL_CTX_SET_CERT_CB} = 1;
+		}
 	}
 
 	$self->GenerateConfigHeader('src/include/pg_config.h',     \%define, 1);
-- 
2.25.1

v18-0002-require_auth-decouple-SASL-and-SCRAM.patchtext/x-patch; charset=US-ASCII; name=v18-0002-require_auth-decouple-SASL-and-SCRAM.patchDownload
From c9ecdd54eaa7a9dd9e9eb34ea1fbafcd22a7c2bc Mon Sep 17 00:00:00 2001
From: Jacob Champion <jchampion@timescale.com>
Date: Tue, 18 Oct 2022 16:55:36 -0700
Subject: [PATCH v18 2/2] require_auth: decouple SASL and SCRAM

Rather than assume that an AUTH_REQ_SASL* code refers to SCRAM-SHA-256,
future-proof by separating the single allowlist into a list of allowed
authentication request codes and a list of allowed SASL mechanisms.

The require_auth check is now separated into two tiers. The
AUTH_REQ_SASL* codes are always allowed. If the server sends one,
responsibility for the check then falls to pg_SASL_init(), which
compares the selected mechanism against the list of allowed mechanisms.
(Other SCRAM code is already responsible for rejecting unexpected or
out-of-order AUTH_REQ_SASL_* codes, so that's not explicitly handled
with this addition.)

Since there's only one recognized SASL mechanism, conn->sasl_mechs
currently only points at static hardcoded lists. Whenever a second
mechanism is added, the list will need to be managed dynamically.
---
 src/interfaces/libpq/fe-auth.c            | 34 +++++++++++++++++++
 src/interfaces/libpq/fe-connect.c         | 41 +++++++++++++++++++----
 src/interfaces/libpq/libpq-int.h          |  3 +-
 src/test/authentication/t/001_password.pl | 14 +++++---
 src/test/ssl/t/002_scram.pl               |  6 ++++
 5 files changed, 86 insertions(+), 12 deletions(-)

diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 934e3f4f7c..7ce0e16b18 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -522,6 +522,40 @@ pg_SASL_init(PGconn *conn, int payloadlen)
 		goto error;
 	}
 
+	/*
+	 * Before going ahead with any SASL exchange, ensure that the user has
+	 * allowed (or, alternatively, has not forbidden) this particular mechanism.
+	 *
+	 * In a hypothetical future where a server responds with multiple SASL
+	 * mechanism families, we would need to instead consult this list up above,
+	 * during mechanism negotiation. We don't live in that world yet. The server
+	 * presents one auth method and we decide whether that's acceptable or not.
+	 */
+	if (conn->sasl_mechs)
+	{
+		const char **mech;
+		bool		found = false;
+
+		Assert(conn->require_auth);
+
+		for (mech = conn->sasl_mechs; *mech; mech++)
+		{
+			if (strcmp(*mech, selected_mechanism) == 0)
+			{
+				found = true;
+				break;
+			}
+		}
+
+		if ((conn->sasl_mechs_denied && found)
+			|| (!conn->sasl_mechs_denied && !found))
+		{
+			libpq_append_conn_error(conn, "auth method \"%s\" requirement failed: server requested unacceptable SASL mechanism \"%s\"",
+									conn->require_auth, selected_mechanism);
+			goto error;
+		}
+	}
+
 	if (conn->channel_binding[0] == 'r' &&	/* require */
 		strcmp(selected_mechanism, SCRAM_SHA_256_PLUS_NAME) != 0)
 	{
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 15515fbd45..c90a23fd2a 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -1258,12 +1258,25 @@ connectOptions2(PGconn *conn)
 					more;
 		bool		negated = false;
 
+		static const uint32 default_methods = (
+			1 << AUTH_REQ_SASL
+			| 1 << AUTH_REQ_SASL_CONT
+			| 1 << AUTH_REQ_SASL_FIN
+		);
+		static const char *no_mechs[] = { NULL };
+
 		/*
-		 * By default, start from an empty set of allowed options and add to
+		 * By default, start from a minimum set of allowed options and add to
 		 * it.
+		 *
+		 * NB: The SASL method codes are always "allowed" here. If the server
+		 * requests SASL auth, pg_SASL_init() will enforce adherence to the
+		 * sasl_mechs list, which by default is empty.
 		 */
 		conn->auth_required = true;
-		conn->allowed_auth_methods = 0;
+		conn->allowed_auth_methods = default_methods;
+		conn->sasl_mechs = no_mechs;
+		conn->sasl_mechs_denied = false;
 
 		for (first = true, more = true; more; first = false)
 		{
@@ -1290,6 +1303,9 @@ connectOptions2(PGconn *conn)
 					 */
 					conn->auth_required = false;
 					conn->allowed_auth_methods = -1;
+
+					/* conn->sasl_mechs is now a list of denied mechanisms. */
+					conn->sasl_mechs_denied = true;
 				}
 				else if (!negated)
 				{
@@ -1334,10 +1350,23 @@ connectOptions2(PGconn *conn)
 			}
 			else if (strcmp(method, "scram-sha-256") == 0)
 			{
-				/* This currently assumes that SCRAM is the only SASL method. */
-				bits = (1 << AUTH_REQ_SASL);
-				bits |= (1 << AUTH_REQ_SASL_CONT);
-				bits |= (1 << AUTH_REQ_SASL_FIN);
+				static const char *scram_mechs[] = {
+					SCRAM_SHA_256_NAME,
+					SCRAM_SHA_256_PLUS_NAME,
+					NULL /* list terminator */
+				};
+
+				/*
+				 * This currently assumes that SCRAM is the only SASL method.
+				 * Once a second mechanism is added, this code will need to add
+				 * to the list instead of replacing it wholesale.
+				 */
+				if (conn->sasl_mechs[0])
+					goto duplicate;
+				conn->sasl_mechs = scram_mechs;
+
+				free(part);
+				continue; /* avoid the bitmask manipulation below */
 			}
 			else if (strcmp(method, "none") == 0)
 			{
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index f1f1d973cc..ab26292586 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -465,7 +465,8 @@ struct pg_conn
 										 * codes */
 	bool		client_finished_auth;	/* have we finished our half of the
 										 * authentication exchange? */
-
+	const char **sasl_mechs;	/* list of allowed/denied SASL mechanisms */
+	bool		sasl_mechs_denied;	/* is the sasl_mechs list forbidden? */
 
 	/* Transient state needed while establishing connection */
 	PGTargetServerType target_server_type;	/* desired session properties */
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index cba5d7d648..015532893c 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -301,30 +301,34 @@ $node->connect_fails(
 	"user=scram_role require_auth=password",
 	"password authentication required, fails with SCRAM auth",
 	expected_stderr =>
-	  qr/auth method "password" requirement failed: server requested SASL authentication/
+	  qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256"/
 );
 $node->connect_fails(
 	"user=scram_role require_auth=md5",
 	"md5 authentication required, fails with SCRAM auth",
 	expected_stderr =>
-	  qr/auth method "md5" requirement failed: server requested SASL authentication/
+	  qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256"/
 );
 $node->connect_fails(
 	"user=scram_role require_auth=none",
 	"all authentication forbidden, fails with SCRAM auth",
 	expected_stderr =>
-	  qr/auth method "none" requirement failed: server requested SASL authentication/
+	  qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256"/
 );
 
 # Authentication fails if SCRAM authentication is forbidden.
 $node->connect_fails(
 	"user=scram_role require_auth=!scram-sha-256",
 	"SCRAM authentication forbidden, fails with SCRAM auth",
-	expected_stderr => qr/server requested SASL authentication/);
+	expected_stderr =>
+	  qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256"/
+);
 $node->connect_fails(
 	"user=scram_role require_auth=!password,!md5,!scram-sha-256",
 	"multiple authentication types forbidden, fails with SCRAM auth",
-	expected_stderr => qr/server requested SASL authentication/);
+	expected_stderr =>
+	  qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256"/
+);
 
 # Test that bad passwords are rejected.
 $ENV{"PGPASSWORD"} = 'badpass';
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 8038135697..173ac8d86b 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -157,6 +157,12 @@ if ($supports_tls_server_end_point)
 		"$common_connstr user=ssltestuser channel_binding=require require_auth=scram-sha-256",
 		"SCRAM with SSL, channel_binding=require, and require_auth=scram-sha-256"
 	);
+	$node->connect_fails(
+		"$common_connstr user=ssltestuser channel_binding=require require_auth=password",
+		"SCRAM with SSL, channel_binding=require, and require_auth=password",
+		expected_stderr =>
+		  qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256-PLUS"/
+	);
 }
 else
 {
-- 
2.25.1

#60Michael Paquier
michael@paquier.xyz
In reply to: Jacob Champion (#59)
Re: [PoC] Let libpq reject unexpected authentication requests

On Thu, Mar 23, 2023 at 03:40:55PM -0700, Jacob Champion wrote:

On Tue, Mar 21, 2023 at 11:01 PM Michael Paquier <michael@paquier.xyz> wrote:

contrib/sslinfo/ has ssl_client_cert_present(), that we could use in
the tests to make sure that the client has actually sent a
certificate? How about adding some of these tests to 003_sslinfo.pl
for the "allow" and "require" cases?

Added; see what you think.

That's a pretty good test design, covering all 4 cases. Nice.

freePGconn() is missing a free(sslcertmode).

Argh, I keep forgetting that. Fixed, thanks!

I have spent a couple of hours looking at the whole again today,
testing that with OpenSSL to make sure that everything was OK. Apart
from a few tweaks, that seemed pretty good. So, applied.
--
Michael

#61Jacob Champion
jchampion@timescale.com
In reply to: Michael Paquier (#60)
Re: [PoC] Let libpq reject unexpected authentication requests

On Thu, Mar 23, 2023 at 10:18 PM Michael Paquier <michael@paquier.xyz> wrote:

I have spent a couple of hours looking at the whole again today,
testing that with OpenSSL to make sure that everything was OK. Apart
from a few tweaks, that seemed pretty good. So, applied.

Thank you!

--Jacob

#62Michael Paquier
michael@paquier.xyz
In reply to: Jacob Champion (#61)
Re: [PoC] Let libpq reject unexpected authentication requests

On Fri, Mar 24, 2023 at 09:30:06AM -0700, Jacob Champion wrote:

On Thu, Mar 23, 2023 at 10:18 PM Michael Paquier <michael@paquier.xyz> wrote:

I have spent a couple of hours looking at the whole again today,
testing that with OpenSSL to make sure that everything was OK. Apart
from a few tweaks, that seemed pretty good. So, applied.

Thank you!

Please note that the CF entry has been marked as committed. We should
really do something about having a cleaner separation between SASL,
the mechanisms and the AUTH_REQ_* codes, in the long term, though
honestly I don't know yet what would be the most elegant and the least
error-prone approach. And for anything that touches authentication,
simpler means better.
--
Michael

#63Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Michael Paquier (#62)
Re: [PoC] Let libpq reject unexpected authentication requests

Michael Paquier <michael@paquier.xyz> wrote:

Please note that the CF entry has been marked as committed. We should
really do something about having a cleaner separation between SASL,
the mechanisms and the AUTH_REQ_* codes, in the long term, though
honestly I don't know yet what would be the most elegant and the least
error-prone approach. And for anything that touches authentication,
simpler means better.

I've taken another shot at this over on the OAuth thread [1]/messages/by-id/CAOYmi+=FzVg+C-pQHCwjW0qU-POHmzZaD2z3CdsACj==14H8kQ@mail.gmail.com, for
those who are still interested; see v40-0002. It's more code than my
previous attempt, but I think it does a clearer job of separating the
two concerns.

Thanks,
--Jacob

[1]: /messages/by-id/CAOYmi+=FzVg+C-pQHCwjW0qU-POHmzZaD2z3CdsACj==14H8kQ@mail.gmail.com

#64Michael Paquier
michael@paquier.xyz
In reply to: Jacob Champion (#63)
Re: [PoC] Let libpq reject unexpected authentication requests

On Thu, Dec 19, 2024 at 05:02:19PM -0800, Jacob Champion wrote:

I've taken another shot at this over on the OAuth thread [1], for
those who are still interested; see v40-0002. It's more code than my
previous attempt, but I think it does a clearer job of separating the
two concerns.

[1] /messages/by-id/CAOYmi+=FzVg+C-pQHCwjW0qU-POHmzZaD2z3CdsACj==14H8kQ@mail.gmail.com

Ah, thanks for the poke on this one. I'm wondering if v40-0002 and
v40-0001 should be in reverse order, but that's a discussion to keep
on the other thread, of course.
--
Michael