From 9a8e6d11d6fdbfed907de2c999129ba8105d6aff Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Date: Wed, 8 Feb 2023 17:39:25 +0200
Subject: [PATCH 1/1] Don't advertise channel binding to client when we cannot
 support it.

With certain kinds of TLS server certificates, we cannot do SCRAM
channel binding (tls-server-end-point), because the hash algorithm to
use is not well defined, or is not supported by OpenSSL. If you try to
use channel binding with such a certificate, you get an obscure error
in the client during the authentication:

    could not find digest for NID UNDEF

To fix, don't advertise SCRAM-SHA-256-PLUS if we cannot calculate the
hash. Print a LOG message once on server startup, and at config
reload, if the server is using such a certificate. To do that,
calculate the certificate hash when the certificate is loaded, instead
of when a connection is established.

The availability of channel binding now depends on the installed
certificate, so this removes HAVE_BE_TLS_GET_CERTIFICATE_HASH and
replaces it with a new function, scram_channel_binding_possible(). If
the server is compiled with a version of OpenSSL that doesn't have the
X509_get_signature_nid() function, and hence cannot support our
channel binding at all, it will just always return false.

The libqq client code has a similar issue. It will choose
SCRAM-SHA-256-PLUS if the server advertises it, even if it doesn't
support the server certificate's signature algorithm, only to fail
later in the authentication. Unfortunately, there isn't much we can do
in the client: it cannot safely fall back to not using channel binding
if it doesn't support the server's certificate's hashing algorithm,
because that could lead to a downgrade attack. So in the client, just
make the error message more clear.

We could try harder to determine a hash algorithm if the certificate
doesn't specify one unambiguously. For example, we could fall back to
SHA-256. However, I'm not comfortable inventing new rules especially
in back-branches. OpenSSL also has a function X509_digest_sig() that
contains some fallback rules like that, but AFAICS it wasn't written
with channel binding in mind, so I'm not confident switching to that
either. In the future, we probably should switch to the tls-exporter
channel binding type as specified in RFC 9266.

Fixes bug #17760. We still don't support channel binding with RSA-PSS
certificates, but at least we fall back more gracefully now.

Reported-by: Gunnar "Nick" Bluth
Discussion: https://www.postgresql.org/message-id/17760-b6c61e752ec07060@postgresql.org
Backpatch: 11- (all supported versions)
---
 src/backend/libpq/auth-scram.c           |  54 +++++++-----
 src/backend/libpq/be-secure-openssl.c    | 102 ++++++++++++++++++++---
 src/backend/libpq/be-secure.c            |   1 +
 src/include/libpq/libpq-be.h             |   9 +-
 src/interfaces/libpq/fe-auth-scram.c     |  17 +++-
 src/interfaces/libpq/fe-secure-openssl.c |   5 +-
 src/test/ssl/conf/server-rsapss.config   |  14 ++++
 src/test/ssl/ssl/server-rsapss.crt       |  21 +++++
 src/test/ssl/ssl/server-rsapss.key       |  28 +++++++
 src/test/ssl/sslfiles.mk                 |  22 ++++-
 src/test/ssl/t/002_scram.pl              |  14 ++++
 11 files changed, 242 insertions(+), 45 deletions(-)
 create mode 100644 src/test/ssl/conf/server-rsapss.config
 create mode 100644 src/test/ssl/ssl/server-rsapss.crt
 create mode 100644 src/test/ssl/ssl/server-rsapss.key

diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index 4441e0d7745..fb39051bfa6 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -191,6 +191,36 @@ static char *scram_mock_salt(const char *username,
 							 pg_cryptohash_type hash_type,
 							 int key_length);
 
+/*
+ * Is channel binding possible?
+ *
+ * SCRAM-SHA-256-PLUS in PostgreSQL implies the tls-server-end-point channel
+ * binding type, which is based on the hash of the server's certificate.  Try
+ * to get the hash.  It might not be available, if the certificate uses an
+ * unsupported or ambiguous digest algorithm.
+ *
+ * XXX: By supporting only tls-server-end-point, we are violating RFC 5802,
+ * which states that the server MUST support tls-unique channel binding type
+ * if channel binding is supported at all.  We choose to violate the RFC
+ * because tls-unique was not supported by all versions of OpenSSL when this
+ * was implemented, and also requires Extended Master Key Extension [RFC7627]
+ * or TLS v1.3 to be secure.  In the future, we should implement the
+ * tls-exporter channel binding type as defined in RFC9266.
+ */
+static bool
+scram_channel_binding_possible(Port *port)
+{
+	size_t		dummy_len;
+
+	if (!port->ssl_in_use)
+		return false;
+
+	if (be_tls_get_certificate_hash(port, &dummy_len) != NULL)
+		return true;
+	else
+		return false;
+}
+
 /*
  * Get a list of SASL mechanisms that this module supports.
  *
@@ -203,17 +233,13 @@ scram_get_mechanisms(Port *port, StringInfo buf)
 {
 	/*
 	 * Advertise the mechanisms in decreasing order of importance.  So the
-	 * channel-binding variants go first, if they are supported.  Channel
-	 * binding is only supported with SSL, and only if the SSL implementation
-	 * has a function to get the certificate's hash.
+	 * channel-binding variants go first, if they are supported.
 	 */
-#ifdef HAVE_BE_TLS_GET_CERTIFICATE_HASH
-	if (port->ssl_in_use)
+	if (scram_channel_binding_possible(port))
 	{
 		appendStringInfoString(buf, SCRAM_SHA_256_PLUS_NAME);
 		appendStringInfoChar(buf, '\0');
 	}
-#endif
 	appendStringInfoString(buf, SCRAM_SHA_256_NAME);
 	appendStringInfoChar(buf, '\0');
 }
@@ -252,12 +278,9 @@ scram_init(Port *port, const char *selected_mech, const char *shadow_pass)
 	 * client nevertheless tries to select it, it's a protocol violation like
 	 * selecting any other SASL mechanism we don't support.
 	 */
-#ifdef HAVE_BE_TLS_GET_CERTIFICATE_HASH
-	if (strcmp(selected_mech, SCRAM_SHA_256_PLUS_NAME) == 0 && port->ssl_in_use)
+	if (strcmp(selected_mech, SCRAM_SHA_256_PLUS_NAME) == 0 && scram_channel_binding_possible(port))
 		state->channel_binding_in_use = true;
-	else
-#endif
-	if (strcmp(selected_mech, SCRAM_SHA_256_NAME) == 0)
+	else if (strcmp(selected_mech, SCRAM_SHA_256_NAME) == 0)
 		state->channel_binding_in_use = false;
 	else
 		ereport(ERROR,
@@ -1005,14 +1028,12 @@ read_client_first_message(scram_state *state, const char *input)
 						 errmsg("malformed SCRAM message"),
 						 errdetail("The client selected SCRAM-SHA-256-PLUS, but the SCRAM message does not include channel binding data.")));
 
-#ifdef HAVE_BE_TLS_GET_CERTIFICATE_HASH
-			if (state->port->ssl_in_use)
+			if (scram_channel_binding_possible(state->port))
 				ereport(ERROR,
 						(errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION),
 						 errmsg("SCRAM channel binding negotiation error"),
 						 errdetail("The client supports SCRAM channel binding but thinks the server does not.  "
 								   "However, this server does support channel binding.")));
-#endif
 			p++;
 			if (*p != ',')
 				ereport(ERROR,
@@ -1301,7 +1322,6 @@ read_client_final_message(scram_state *state, const char *input)
 	channel_binding = read_attr_value(&p, 'c');
 	if (state->channel_binding_in_use)
 	{
-#ifdef HAVE_BE_TLS_GET_CERTIFICATE_HASH
 		const char *cbind_data = NULL;
 		size_t		cbind_data_len = 0;
 		size_t		cbind_header_len;
@@ -1343,10 +1363,6 @@ read_client_final_message(scram_state *state, const char *input)
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION),
 					 errmsg("SCRAM channel binding check failed")));
-#else
-		/* shouldn't happen, because we checked this earlier already */
-		elog(ERROR, "channel binding not supported by this build");
-#endif
 	}
 	else
 	{
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index e3c7c12aa0e..751189600f8 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -61,6 +61,7 @@ static int	my_sock_write(BIO *h, const char *buf, int size);
 static BIO_METHOD *my_BIO_s_socket(void);
 static int	my_SSL_set_fd(Port *port, int fd);
 
+static char *calculate_certificate_hash(X509 *cert, size_t *len, bool isServerStart);
 static DH  *load_dh_file(char *filename, bool isServerStart);
 static DH  *load_dh_buffer(const char *buffer, size_t len);
 static int	ssl_external_passwd_cb(char *buf, int size, int rwflag, void *userdata);
@@ -73,7 +74,11 @@ static const char *SSLerrmessage(unsigned long ecode);
 
 static char *X509_NAME_to_cstring(X509_NAME *name);
 
+/* current SSL context, and hash of the server certificate in use in the context */
 static SSL_CTX *SSL_context = NULL;
+static char *ssl_cert_hash = NULL;
+static int	ssl_cert_hash_len = 0;
+
 static bool SSL_initialized = false;
 static bool dummy_ssl_passwd_cb_called = false;
 static bool ssl_is_server_start;
@@ -94,6 +99,9 @@ be_tls_init(bool isServerStart)
 	SSL_CTX    *context;
 	int			ssl_ver_min = -1;
 	int			ssl_ver_max = -1;
+	X509	   *new_cert;
+	char	   *new_hash = NULL;
+	size_t		new_hash_len = 0;
 
 	/* This stuff need be done only once. */
 	if (!SSL_initialized)
@@ -156,6 +164,30 @@ be_tls_init(bool isServerStart)
 	if (!check_ssl_key_file_permissions(ssl_key_file, isServerStart))
 		goto error;
 
+	/*
+	 * Pre-calculate the hash of the certificate, for use in channel binding.
+	 */
+	new_cert = SSL_CTX_get0_certificate(context);
+	if (new_cert == NULL)
+	{
+		/*
+		 * The SSL_CTX_use_certificate_chain_file() call above succeeded, but
+		 * there is no certificate in the context.  Not clear if this can
+		 * happen, but we certainly can't do tls-server-end-point channel
+		 * binding without a certificate.
+		 */
+		new_hash = NULL;
+		new_hash_len = 0;
+	}
+	else
+	{
+		MemoryContext oldcxt;
+
+		oldcxt = MemoryContextSwitchTo(TopMemoryContext);
+		new_hash = calculate_certificate_hash(new_cert, &new_hash_len, isServerStart);
+		MemoryContextSwitchTo(oldcxt);
+	}
+
 	/*
 	 * OK, try to load the private key file.
 	 */
@@ -380,8 +412,12 @@ be_tls_init(bool isServerStart)
 	 */
 	if (SSL_context)
 		SSL_CTX_free(SSL_context);
+	if (ssl_cert_hash)
+		pfree(ssl_cert_hash);
 
 	SSL_context = context;
+	ssl_cert_hash = new_hash;
+	ssl_cert_hash_len = new_hash_len;
 
 	/*
 	 * Set flag to remember whether CA store has been loaded into SSL_context.
@@ -397,6 +433,8 @@ be_tls_init(bool isServerStart)
 error:
 	if (context)
 		SSL_CTX_free(context);
+	if (new_hash)
+		pfree(new_hash);
 	return -1;
 }
 
@@ -407,6 +445,9 @@ be_tls_destroy(void)
 		SSL_CTX_free(SSL_context);
 	SSL_context = NULL;
 	ssl_loaded_verify_locations = false;
+	if (ssl_cert_hash)
+		pfree(ssl_cert_hash);
+	ssl_cert_hash = NULL;
 }
 
 int
@@ -1429,11 +1470,29 @@ be_tls_get_peer_serial(Port *port, char *ptr, size_t len)
 		ptr[0] = '\0';
 }
 
-#ifdef HAVE_X509_GET_SIGNATURE_NID
 char *
 be_tls_get_certificate_hash(Port *port, size_t *len)
 {
-	X509	   *server_cert;
+	*len = ssl_cert_hash_len;
+	return ssl_cert_hash;
+}
+
+/*
+ * Calculate hash of the given certificate, for use in tls-server-end-point
+ * channel binding.
+ *
+ * The returned hash is palloc'd, and its length is returned in *len.
+ *
+ * If the hash could not be calculated because the hash algorithm specified in
+ * the certificate's signature is not supported, or it doesn't unambiguously
+ * specify an algorithm to use, returns NULL and logs the error.  On other
+ * less expected errors, ereports the error with FATAL or LOG depending on
+ * whether 'isServerStart' is true.
+ */
+static char *
+calculate_certificate_hash(X509 *cert, size_t *len, bool isServerStart)
+{
+#ifdef HAVE_X509_GET_SIGNATURE_NID
 	char	   *cert_hash;
 	const EVP_MD *algo_type = NULL;
 	unsigned char hash[EVP_MAX_MD_SIZE];	/* size for SHA-512 */
@@ -1441,17 +1500,18 @@ be_tls_get_certificate_hash(Port *port, size_t *len)
 	int			algo_nid;
 
 	*len = 0;
-	server_cert = SSL_get_certificate(port->ssl);
-	if (server_cert == NULL)
-		return NULL;
 
 	/*
 	 * Get the signature algorithm of the certificate to determine the hash
 	 * algorithm to use for the result.
 	 */
-	if (!OBJ_find_sigid_algs(X509_get_signature_nid(server_cert),
-							 &algo_nid, NULL))
-		elog(ERROR, "could not determine server certificate signature algorithm");
+	if (OBJ_find_sigid_algs(X509_get_signature_nid(cert),
+							&algo_nid, NULL) == 0)
+	{
+		ereport(LOG,
+				(errmsg("channel binding is disabled; server certificate has unknown signature algorithm")));
+		return NULL;
+	}
 
 	/*
 	 * The TLS server's certificate bytes need to be hashed with SHA-256 if
@@ -1461,6 +1521,10 @@ be_tls_get_certificate_hash(Port *port, size_t *len)
 	 */
 	switch (algo_nid)
 	{
+		case NID_undef:
+			ereport(LOG,
+					(errmsg("channel binding is disabled; server certificate does not unambiguously specify a signature algorithm")));
+			return NULL;
 		case NID_md5:
 		case NID_sha1:
 			algo_type = EVP_sha256();
@@ -1468,22 +1532,34 @@ be_tls_get_certificate_hash(Port *port, size_t *len)
 		default:
 			algo_type = EVP_get_digestbynid(algo_nid);
 			if (algo_type == NULL)
-				elog(ERROR, "could not find digest for NID %s",
-					 OBJ_nid2sn(algo_nid));
+			{
+				ereport(isServerStart ? FATAL : LOG,
+						errmsg("could not find digest for NID %s",
+							   OBJ_nid2sn(algo_nid)));
+				return NULL;
+			}
 			break;
 	}
 
 	/* generate and save the certificate hash */
-	if (!X509_digest(server_cert, algo_type, hash, &hash_size))
-		elog(ERROR, "could not generate server certificate hash");
+	if (!X509_digest(cert, algo_type, hash, &hash_size))
+	{
+		ereport(isServerStart ? FATAL : LOG,
+				(errmsg("channel binding is disabled; could not generate server certificate hash")));
+		return NULL;
+	}
+	Assert(hash_size <= sizeof(hash));
 
 	cert_hash = palloc(hash_size);
 	memcpy(cert_hash, hash, hash_size);
 	*len = hash_size;
 
 	return cert_hash;
-}
+#else
+	*len = 0;
+	return NULL;
 #endif
+}
 
 /*
  * Convert an X509 subject name to a cstring.
diff --git a/src/backend/libpq/be-secure.c b/src/backend/libpq/be-secure.c
index a0f70840183..8c991099784 100644
--- a/src/backend/libpq/be-secure.c
+++ b/src/backend/libpq/be-secure.c
@@ -47,6 +47,7 @@ bool		ssl_passphrase_command_supports_reload;
 
 #ifdef USE_SSL
 bool		ssl_loaded_verify_locations = false;
+char	   *ssl_cert_hash = NULL;
 #endif
 
 /* GUC variable controlling SSL cipher list */
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 8c70b2fd5be..dda299a241f 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -303,15 +303,10 @@ extern void be_tls_get_peer_serial(Port *port, char *ptr, size_t len);
  * tls-server-end-point.
  *
  * The result is a palloc'd hash of the server certificate with its
- * size, and NULL if there is no certificate available.
- *
- * This is not supported with old versions of OpenSSL that don't have
- * the X509_get_signature_nid() function.
+ * size, or NULL if there is no certificate available or we could not
+ * unambiguously calculate a hash for it.
  */
-#if defined(USE_OPENSSL) && defined(HAVE_X509_GET_SIGNATURE_NID)
-#define HAVE_BE_TLS_GET_CERTIFICATE_HASH
 extern char *be_tls_get_certificate_hash(Port *port, size_t *len);
-#endif
 
 /* init hook for SSL, the default sets the password callback if appropriate */
 #ifdef USE_OPENSSL
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index 9c42ea4f819..fc0a5cd264a 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -487,7 +487,22 @@ build_client_final_message(fe_scram_state *state)
 											&cbind_data_len);
 		if (cbind_data == NULL)
 		{
-			/* error message is already set on error */
+			/*
+			 * Could not calculate hash of the server's certificate.  Error
+			 * message is already set by pgtls_get_peer_certificate_hash().
+			 *
+			 * This is known to happen with certificates using 'rsassaPss',
+			 * 'ED25519' and 'ED448' signatures, because they don't specify an
+			 * unambiguous digest algorithm to use.  The server should not
+			 * offer PLUS-variants with such certificates, but older versions
+			 * incorrectly did.  If channel binding is required, we naturally
+			 * must fail, but even if channel binding is merely preferred, we
+			 * cannot safely fall back to disabling channel binding, because
+			 * that could lead to a downgrade attack: A man-in-the-middle
+			 * could intentionally offer us a certificate with an unrecognized
+			 * signature algorithm, to force the client to disable channel
+			 * binding.
+			 */
 			termPQExpBuffer(&buf);
 			return NULL;
 		}
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 983536de251..96168a8bd7e 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -389,7 +389,7 @@ pgtls_get_peer_certificate_hash(PGconn *conn, size_t *len)
 	if (!OBJ_find_sigid_algs(X509_get_signature_nid(peer_cert),
 							 &algo_nid, NULL))
 	{
-		libpq_append_conn_error(conn, "could not determine server certificate signature algorithm");
+		libpq_append_conn_error(conn, "could not determine signature algorithm to use with server's certificate; channel binding not possible");
 		return NULL;
 	}
 
@@ -401,6 +401,9 @@ pgtls_get_peer_certificate_hash(PGconn *conn, size_t *len)
 	 */
 	switch (algo_nid)
 	{
+		case NID_undef:
+			libpq_append_conn_error(conn, "could not determine signature algorithm to use with server's certificate; channel binding not possible");
+			return NULL;
 		case NID_md5:
 		case NID_sha1:
 			algo_type = EVP_sha256();
diff --git a/src/test/ssl/conf/server-rsapss.config b/src/test/ssl/conf/server-rsapss.config
new file mode 100644
index 00000000000..d4701bdb36b
--- /dev/null
+++ b/src/test/ssl/conf/server-rsapss.config
@@ -0,0 +1,14 @@
+# An OpenSSL format CSR config file for creating a server certificate.
+#
+# This is identical to server-cn-only certificate, but we specify
+# RSA-PSS as the algorithm on the command line.
+
+[ req ]
+distinguished_name     = req_distinguished_name
+prompt                 = no
+
+[ req_distinguished_name ]
+CN = common-name.pg-ssltest.test
+OU = PostgreSQL test suite
+
+# No Subject Alternative Names
diff --git a/src/test/ssl/ssl/server-rsapss.crt b/src/test/ssl/ssl/server-rsapss.crt
new file mode 100644
index 00000000000..bef29adb1c3
--- /dev/null
+++ b/src/test/ssl/ssl/server-rsapss.crt
@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDczCCAioCFB6p3YqR83DVMn6wTP1AI9E3akswMD4GCSqGSIb3DQEBCjAxoA0w
+CwYJYIZIAWUDBAIBoRowGAYJKoZIhvcNAQEIMAsGCWCGSAFlAwQCAaIEAgIA3jBG
+MSQwIgYDVQQDDBtjb21tb24tbmFtZS5wZy1zc2x0ZXN0LnRlc3QxHjAcBgNVBAsM
+FVBvc3RncmVTUUwgdGVzdCBzdWl0ZTAeFw0yMzAyMDgxNDIzMzRaFw0yMzAzMTAx
+NDIzMzRaMEYxJDAiBgNVBAMMG2NvbW1vbi1uYW1lLnBnLXNzbHRlc3QudGVzdDEe
+MBwGA1UECwwVUG9zdGdyZVNRTCB0ZXN0IHN1aXRlMIIBIDALBgkqhkiG9w0BAQoD
+ggEPADCCAQoCggEBALaoRpce0lzkRKERq4cUANEoiVtOPfKnG4n4cifMJYto+DlE
+TIB5PfeNeuLoLdKnohYlICQDLuV7W3r93MoimZ8tbYt78OQ06G/6qtyc6FjlGTKP
+6+ULV8AVBbhcyQPtKhpzq+WbX/6Lz4hVtms/aeElB60ZbAs6wzSt8XBTmfjnOrty
+5w/k2/+/X0DzRE1aYTnVlFTlVyuF1Th7Psm1EOfvAdjbVLCso4Ttpsixq0ILarAm
+xQfc5cO6/dXRDsjCnly+5CVNWYCE1+AB4G9NvklORLApkdc4UBjxFpDOoh5pNHZa
+ofExUthbGwyUfYOwGDPTPNYeYVshUZlAvgTJIrMCAwEAATA+BgkqhkiG9w0BAQow
+MaANMAsGCWCGSAFlAwQCAaEaMBgGCSqGSIb3DQEBCDALBglghkgBZQMEAgGiBAIC
+AN4DggEBAIyTGMj5Z+Rl/lhvPkLM5TAtl/pGP23wHa6hsf+SpgwPGFQe5qC5Tbrd
++w/5KK5mh40+QMWGNo+kaDGJY3N+WV69Vk4aMvKvxVIGgH5itwvBE/emYFThbMmw
+sGlW9f33+Edb2pEAEGEeb/AcesCYCF0tptyD1FphGVJaVFxo9uwwI0R3t/Ogok5H
+576WHz6L56/Bqx5qpF+6h3cSDom3uGS0BYJlKBuDG3F89gNSBsixhfPHaIZxy+Jc
+x5Tedp1sAqvxOQ1lez9LMPxdYAo3Qubw7c8H7bzyauiZnTt0xuTwOPJGFxwp5LMN
+7+KYwyAzsHaglC/1ySPHAfsg0Xkq4Q0=
+-----END CERTIFICATE-----
diff --git a/src/test/ssl/ssl/server-rsapss.key b/src/test/ssl/ssl/server-rsapss.key
new file mode 100644
index 00000000000..71a1dcebd33
--- /dev/null
+++ b/src/test/ssl/ssl/server-rsapss.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADALBgkqhkiG9w0BAQoEggSqMIIEpgIBAAKCAQEAtqhGlx7SXOREoRGr
+hxQA0SiJW0498qcbifhyJ8wli2j4OURMgHk994164ugt0qeiFiUgJAMu5Xtbev3c
+yiKZny1ti3vw5DTob/qq3JzoWOUZMo/r5QtXwBUFuFzJA+0qGnOr5Ztf/ovPiFW2
+az9p4SUHrRlsCzrDNK3xcFOZ+Oc6u3LnD+Tb/79fQPNETVphOdWUVOVXK4XVOHs+
+ybUQ5+8B2NtUsKyjhO2myLGrQgtqsCbFB9zlw7r91dEOyMKeXL7kJU1ZgITX4AHg
+b02+SU5EsCmR1zhQGPEWkM6iHmk0dlqh8TFS2FsbDJR9g7AYM9M81h5hWyFRmUC+
+BMkiswIDAQABAoIBAQCA4h3lFf9zQjJWwKQajPfCnerFvWCirl0VLnRGNpsrUmcL
+6XBdmq8p1O+n2hIzOMt6+rlCD+jwTemP0D8RdfaVxbUtHcgtDlQQZ3xWbHTZ/NWZ
+wsm9dRJ2256MpxPDrH3v6jyZp9fFQZuvZGNyK22nSryu3GhLSCxjKHVbP/I+GKE3
+XUqN8coC9w0IznlURzxOCtN1MQff7ZAriXRtAMX0aWSzdkw/h2MkWsWGyB6ZpevA
+iWKqGSzBuQirYcwvk3Ne6IqOU83QRbLO4O7y2IBiwEz+BDB1l0/oIV3mwx2+4qLJ
+8HlFeAhPoCYFD/U3w+ZeQHevU8Y6+lFt6YqcGXfZAoGBANwP6+4gQkO+fRg13ncd
+7plWuebeEi/GZNpIk36/+U14Z88sZ0apB8szmkYrebDw1aDAsMdg313LlRQdbjzS
+9q3Xwx6Zso0CGE+qZ53BByImIcpcOqyuy6XSjd6buAK1mIGtUOvMEQAhCKaKzGZH
+mASN6aQazEPP+bLL43GhTU/9AoGBANR8lFHxMFf0uUX0F9Q0yxcmUGkW2bmHQXQl
+jnSCGdvMZzi21VLN4MjoLnKUsfo63THFZ0IshNeZbcMfA8wts26cPwJPE/EUppwJ
+Okdatp4zb0Pm5rRudhnpMvNELNjIxKSQ1Gpf3eXvpegE97nsApAvO7NzEA0yfNJ9
+hO7dqYRvAoGBAM2adpUqQJ8op5nqIqrqJVXQyKniC93lH68uJdhDprpx55OR6gAD
+x0tcMCSlU/I0YXPq9H+ji64HmoaLpMZhWKY9s4iwkzjZnzs3e83RXkfSlrmJiCx0
+t8J7QZ+dn8OwRQfLlZJpO+0B8CIiBiz9cRO4P3Xar8Qf2Szq/9rXmPNFAoGBALlu
+HmGEAZRAVI7ffQwLJlCFdxa+gjKN/mjFfZPfFYHi1xF/PJqOX+Pz+tSzPr4IMJAG
+nR36i9M6abclkcLU/wlAARyateRlCSCSTPGIEGXurOKs1hgPbbEe+P+iNyDX1ANp
+AHX0Q0kt4bKg4y6072e0UH1BpHhf3t4x+5gYiJ6xAoGBAJdD0GxLpe3AyG577Oo6
+x30W0/l4A4fPPkdRA+9zukt9Ybgs/KnoHzydBAqbuNBlj9DzqRuXlZA+zktJdyY1
+HC2oQ8qem7HbzbD10wLMN56VZWEohhEgw5A4EWdIcwYRKmIAliGTwyuJ+TQ4wl0x
++AVn5Ma2QzxTvU1Rrw0ok0w5
+-----END PRIVATE KEY-----
diff --git a/src/test/ssl/sslfiles.mk b/src/test/ssl/sslfiles.mk
index 5d9dc09a4b0..a565e90e4f1 100644
--- a/src/test/ssl/sslfiles.mk
+++ b/src/test/ssl/sslfiles.mk
@@ -37,13 +37,17 @@ CLIENTS := client client-dn client-revoked client_ext client-long \
 	client-revoked-utf8
 
 #
-# To add a new non-standard key, add it to SPECIAL_KEYS and then add a recipe
-# for creating it to the "Special-case keys" section below.
+# To add a new non-standard certificate, add it to SPECIAL_CERTS and then add
+# a recipe for creating it to the "Special-case certificates" section below.
 #
+SPECIAL_CERTS := ssl/server-rsapss.crt
+
+# Likewise for non-standard keys
 SPECIAL_KEYS := ssl/server-password.key \
 	ssl/client-der.key \
 	ssl/client-encrypted-pem.key \
-	ssl/client-encrypted-der.key
+	ssl/client-encrypted-der.key \
+	ssl/server-rsapss.key
 
 #
 # These files are just concatenations of other files. You can add new ones to
@@ -66,7 +70,9 @@ CRLS := ssl/root.crl \
 	ssl/client.crl \
 	ssl/server.crl
 
-SSLFILES := $(STANDARD_CERTS) $(STANDARD_KEYS) $(SPECIAL_KEYS) $(COMBINATIONS) $(CRLS)
+SSLFILES := $(STANDARD_CERTS) $(STANDARD_KEYS) \
+            $(SPECIAL_CERTS) $(SPECIAL_KEYS) \
+            $(COMBINATIONS) $(CRLS)
 SSLDIRS := ssl/client-crldir \
 	ssl/server-crldir \
 	ssl/root+client-crldir \
@@ -86,6 +92,10 @@ sslfiles: $(SSLFILES) $(SSLDIRS)
 ssl/root_ca.crt: ssl/root_ca.key conf/root_ca.config
 	$(OPENSSL) req -new -x509 -config conf/root_ca.config -days 10000 -key $< -out $@
 
+# Certificate using RSA-PSS algorithm. Also self-signed.
+ssl/server-rsapss.crt: ssl/server-rsapss.key conf/server-rsapss.config
+	$(OPENSSL) req -new -x509 -config conf/server-rsapss.config -key $< -out $@
+
 #
 # Special-case keys
 #
@@ -96,6 +106,10 @@ ssl/root_ca.crt: ssl/root_ca.key conf/root_ca.config
 ssl/server-password.key: ssl/server-cn-only.key
 	$(OPENSSL) rsa -aes256 -in $< -out $@ -passout 'pass:secret1'
 
+# Key that uses the RSA-PSS algorithm
+ssl/server-rsapss.key:
+	$(OPENSSL) genpkey -algorithm rsa-pss -out $@
+
 # DER-encoded version of client.key
 ssl/client-der.key: ssl/client.key
 	$(OPENSSL) rsa -in $< -outform DER -out $@
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 0f3d180cfa9..a14fa3cb09b 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -136,4 +136,18 @@ $node->connect_ok(
 		qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/
 	]);
 
+# Now test with a server certificate that uses the RSA-PSS algorithm.  It
+# cannot be used for channel binding currently, but check that connecting
+# without channel binding is still possible. (see bug #17760)
+switch_server_cert($node, certfile => 'server-rsapss');
+$node->connect_fails(
+	"$common_connstr user=ssltestuser channel_binding=require",
+	"SCRAM with SSL and channel_binding=require, server certificate uses 'rsassaPss'",
+	expected_stderr =>
+	  qr/channel binding is required, but server did not offer an authentication method that supports channel binding/
+  );
+$node->connect_ok(
+	"$common_connstr user=ssltestuser channel_binding=prefer",
+	"SCRAM with SSL and channel_binding=prefer, server certificate uses 'rsassaPss'");
+
 done_testing();
-- 
2.30.2

