From 2dbffc90b29f1c7d87978d9f8d8177e70b6c9b1f Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 10 Oct 2017 22:04:22 +0900
Subject: [PATCH 4/4] Implement channel binding tls-server-end-point for SCRAM

As referenced in RFC 5929, this channel binding is not the default value
and uses a hash of the certificate as binding data. On the frontend, this
can be resumed in getting the data from SSL_get_peer_certificate() and
on the backend SSL_get_certificate().

The hashing algorithm needs also to switch to SHA-256 if the signature
algorithm is MD5 or SHA-1, so let's be careful about that.
---
 doc/src/sgml/protocol.sgml               |  4 +-
 src/backend/libpq/auth-scram.c           | 21 +++++++--
 src/backend/libpq/auth.c                 | 13 ++++--
 src/backend/libpq/be-secure-openssl.c    | 61 +++++++++++++++++++++++++
 src/include/libpq/libpq-be.h             |  1 +
 src/include/libpq/scram.h                |  4 +-
 src/interfaces/libpq/fe-auth-scram.c     | 20 +++++++-
 src/interfaces/libpq/fe-auth.c           | 12 ++++-
 src/interfaces/libpq/fe-auth.h           |  3 +-
 src/interfaces/libpq/fe-secure-openssl.c | 78 ++++++++++++++++++++++++++++++++
 src/interfaces/libpq/libpq-int.h         |  1 +
 src/test/ssl/t/002_sasl.pl               |  6 ++-
 12 files changed, 209 insertions(+), 15 deletions(-)

diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 3ba8575fba..d86d354397 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1550,8 +1550,8 @@ the password is in.
   <para>
 <firstterm>Channel binding</> is supported in builds with SSL support, and
 uses as mechanism name <literal>SCRAM-SHA-256-PLUS</> for this purpose as
-defined per IANA. The only channel binding type supported by the server
-is <literal>tls-unique</>.
+defined per IANA. The channel binding types supported by the server
+are <literal>tls-unique</>, the default, and <literal>tls-server-end-point</>.
   </para>
 
 <procedure>
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index a90abf4c3b..02fd3332ac 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -113,6 +113,8 @@ typedef struct
 	bool		ssl_in_use;
 	char	   *tls_finished_message;
 	int			tls_finished_len;
+	char	   *certificate_hash;
+	int			certificate_hash_len;
 	char	   *channel_binding;
 
 	int			iterations;
@@ -175,7 +177,9 @@ pg_be_scram_init(const char *username,
 				 const char *shadow_pass,
 				 bool ssl_in_use,
 				 char *tls_finished_message,
-				 int tls_finished_len)
+				 int tls_finished_len,
+				 char *certificate_hash,
+				 int certificate_hash_len)
 {
 	scram_state *state;
 	bool		got_verifier;
@@ -186,6 +190,8 @@ pg_be_scram_init(const char *username,
 	state->ssl_in_use = ssl_in_use;
 	state->tls_finished_message = tls_finished_message;
 	state->tls_finished_len = tls_finished_len;
+	state->certificate_hash = certificate_hash;
+	state->certificate_hash_len = certificate_hash_len;
 	state->channel_binding = NULL;
 
 	/*
@@ -835,11 +841,12 @@ read_client_first_message(scram_state *state, char *input)
 							 errmsg("client requires SCRAM channel binding, but it is not supported")));
 
 				/*
-				 * Read value provided by client, only tls-unique is supported
-				 * for now.
+				 * Read value provided by client, only tls-unique and
+				 * tls-server-end-point are supported for now.
 				 */
 				channel_name = read_attr_value(&input, 'p');
-				if (strcmp(channel_name, SCRAM_CHANNEL_TLS_UNIQUE) != 0)
+				if (strcmp(channel_name, SCRAM_CHANNEL_TLS_UNIQUE) != 0 &&
+					strcmp(channel_name, SCRAM_CHANNEL_TLS_ENDPOINT) != 0)
 					ereport(ERROR,
 						(errcode(ERRCODE_PROTOCOL_VIOLATION),
 						 (errmsg("unexpected SCRAM channel-binding type"))));
@@ -1111,6 +1118,12 @@ read_client_final_message(scram_state *state, char *input)
 			raw_data = state->tls_finished_message;
 			raw_data_len = state->tls_finished_len;
 		}
+		else if (strcmp(state->channel_binding,
+						SCRAM_CHANNEL_TLS_ENDPOINT) == 0)
+		{
+			raw_data = state->certificate_hash;
+			raw_data_len = state->certificate_hash_len;
+		}
 		else
 		{
 			/* should not happen */
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 5bceae2162..338e3d7a0d 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -867,6 +867,8 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
 	bool		initial;
 	char	   *tls_finished = NULL;
 	int			tls_finished_len = 0;
+	char	   *certificate_hash = NULL;
+	int			certificate_hash_len = 0;
 
 	/*
 	 * SASL auth is not supported for protocol versions before 3, because it
@@ -918,12 +920,15 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
 
 #ifdef USE_SSL
 	/*
-	 * Fetch the data related to the SSL finish message to be used in the
-	 * exchange.
+	 * Fetch the data related to the SSL finish message and the client
+	 * certificate (if any) to be used in the exchange.
 	 */
 	if (port->ssl_in_use)
 	{
 		tls_finished = be_tls_get_peer_finish(port, &tls_finished_len);
+		certificate_hash =
+			be_tls_get_certificate_hash(port,
+										&certificate_hash_len);
 	}
 #endif
 
@@ -942,7 +947,9 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
 								  shadow_pass,
 								  port->ssl_in_use,
 								  tls_finished,
-								  tls_finished_len);
+								  tls_finished_len,
+								  certificate_hash,
+								  certificate_hash_len);
 
 	/*
 	 * Loop through SASL message exchange.  This exchange can consist of
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index d063a587d0..bf9cef7f15 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -1239,6 +1239,67 @@ be_tls_get_peer_finish(Port *port, int *len)
 	return result;
 }
 
+/*
+ * Get the server certificate hash for authentication purposes. Per
+ * RFC 5929 and tls-server-end-point, the TLS server's certificate bytes
+ * need to be hashed with SHA-256 if its signature algorithm is MD5 or
+ * SHA-1 as per RFC 5929 (https://tools.ietf.org/html/rfc5929#section-4.1).
+ * If something else is used, the same hash as the signature algorithm is
+ * used. The result is a palloc'd hash of the server certificate with its
+ * size, and NULL if there is no certificate available.
+ */
+char *
+be_tls_get_certificate_hash(Port *port, int *len)
+{
+	char	*cert_hash = NULL;
+	X509	*server_cert;
+
+	*len = 0;
+	server_cert = SSL_get_certificate(port->ssl);
+
+	if (server_cert != NULL)
+	{
+		const EVP_MD   *algo_type = NULL;
+		char			hash[EVP_MAX_MD_SIZE];	/* size for SHA-512 */
+		unsigned int	hash_size;
+		int				algo_nid;
+
+		/*
+		 * 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 find signature algorithm");
+
+		switch (algo_nid)
+		{
+			case NID_md5:
+			case NID_sha1:
+				algo_type = EVP_sha256();
+				break;
+
+			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));
+				break;
+		}
+
+		/* generate and save the certificate hash */
+		if (!X509_digest(server_cert, algo_type, (unsigned char *) hash,
+						 &hash_size))
+			elog(ERROR, "could not generate server certificate hash");
+
+		cert_hash = (char *) palloc(hash_size);
+		memcpy(cert_hash, hash, hash_size);
+		*len = hash_size;
+	}
+
+	return cert_hash;
+}
+
 /*
  * Convert an X509 subject name to a cstring.
  *
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 5d59d79822..0392860a87 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -210,6 +210,7 @@ extern void be_tls_get_version(Port *port, char *ptr, size_t len);
 extern void be_tls_get_cipher(Port *port, char *ptr, size_t len);
 extern void be_tls_get_peerdn_name(Port *port, char *ptr, size_t len);
 extern char *be_tls_get_peer_finish(Port *port, int *len);
+extern char *be_tls_get_certificate_hash(Port *port, int *len);
 #endif
 
 extern ProtocolVersion FrontendProtocol;
diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h
index 43cde0e46e..6329e111ba 100644
--- a/src/include/libpq/scram.h
+++ b/src/include/libpq/scram.h
@@ -19,6 +19,7 @@
 
 /* Channel binding names */
 #define SCRAM_CHANNEL_TLS_UNIQUE	"tls-unique"
+#define SCRAM_CHANNEL_TLS_ENDPOINT	"tls-server-end-point"
 
 /* Status codes for message exchange */
 #define SASL_EXCHANGE_CONTINUE		0
@@ -28,7 +29,8 @@
 /* Routines dedicated to authentication */
 extern void *pg_be_scram_init(const char *username, const char *shadow_pass,
 					 bool ssl_in_use, char *tls_finished_message,
-					 int tls_finished_len);
+					 int tls_finished_len, char *certificate_hash,
+					 int certificate_hash_len);
 extern int pg_be_scram_exchange(void *opaq, char *input, int inputlen,
 					 char **output, int *outputlen, char **logdetail);
 
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index 9bce4d980a..307e58f586 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -49,6 +49,8 @@ typedef struct
 	bool		channel_binding_advertised;
 	char	   *tls_finished_message;
 	int			tls_finished_len;
+	char	   *certificate_hash;
+	int			certificate_hash_len;
 	/* enforce-able user parameters */
 	char	   *saslmechanism;		/* name of mechanism used */
 	char	   *saslchannelbinding;	/* name of channel binding to use */
@@ -96,7 +98,9 @@ pg_fe_scram_init(const char *username,
 				 const char *saslmechanism,
 				 char *saslchannelbinding,
 				 char *tls_finished_message,
-				 int tls_finished_len)
+				 int tls_finished_len,
+				 char *certificate_hash,
+				 int certificate_hash_len)
 {
 	fe_scram_state *state;
 	char	   *prep_password;
@@ -115,6 +119,8 @@ pg_fe_scram_init(const char *username,
 	state->tls_finished_message = tls_finished_message;
 	state->tls_finished_len = tls_finished_len;
 	state->saslmechanism = strdup(saslmechanism);
+	state->certificate_hash = certificate_hash;
+	state->certificate_hash_len = certificate_hash_len;
 
 	/*
 	 * If user has specified a channel binding to use, enforce the
@@ -158,6 +164,8 @@ pg_fe_scram_free(void *opaq)
 		free(state->password);
 	if (state->tls_finished_message)
 		free(state->tls_finished_message);
+	if (state->certificate_hash)
+		free(state->certificate_hash);
 	if (state->saslmechanism)
 		free(state->saslmechanism);
 	if (state->saslchannelbinding)
@@ -466,7 +474,9 @@ build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage)
 	 * Construct client-final-message-without-proof.  We need to remember it
 	 * for verifying the server proof in the final step of authentication.
 	 * Client needs to provide a b64 encoded string of the TLS finish message
-	 * only if a SSL connection is attempted.
+	 * only if a SSL connection is attempted using "tls-unique" as channel
+	 * binding. For "tls-server-end-point", a hash of the client certificate
+	 * is sent instead.
 	 */
 #ifdef USE_SSL
 	if (strcmp(state->saslmechanism, SCRAM_SHA256_PLUS_NAME) == 0)
@@ -479,6 +489,12 @@ build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage)
 			raw_data = state->tls_finished_message;
 			raw_data_len = state->tls_finished_len;
 		}
+		else if (strcmp(state->saslchannelbinding,
+						SCRAM_CHANNEL_TLS_ENDPOINT) == 0)
+		{
+			raw_data = state->certificate_hash;
+			raw_data_len = state->certificate_hash_len;
+		}
 		else
 		{
 			/* should not happen */
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 97de9a0a30..56dbe2be28 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -494,6 +494,8 @@ pg_SASL_init(PGconn *conn, int payloadlen)
 	bool		channel_binding_advertised = false;
 	char	   *tls_finished = NULL;
 	int			tls_finished_len = 0;
+	char	   *certificate_hash = NULL;
+	int			certificate_hash_len = 0;
 	char	   *password;
 
 	initPQExpBuffer(&mechanism_buf);
@@ -610,6 +612,12 @@ pg_SASL_init(PGconn *conn, int payloadlen)
 		tls_finished = pgtls_get_finish(conn, &tls_finished_len);
 		if (tls_finished == NULL)
 			goto oom_error;
+
+		certificate_hash =
+			pgtls_get_peer_certificate_hash(conn,
+											&certificate_hash_len);
+		if (certificate_hash == NULL)
+			goto error;		/* error message is set */
 	}
 #endif
 
@@ -626,7 +634,9 @@ pg_SASL_init(PGconn *conn, int payloadlen)
 										selected_mechanism,
 										conn->saslchannelbinding,
 										tls_finished,
-										tls_finished_len);
+										tls_finished_len,
+										certificate_hash,
+										certificate_hash_len);
 	if (!conn->sasl_state)
 		goto oom_error;
 
diff --git a/src/interfaces/libpq/fe-auth.h b/src/interfaces/libpq/fe-auth.h
index 81378d0008..ed574d39e5 100644
--- a/src/interfaces/libpq/fe-auth.h
+++ b/src/interfaces/libpq/fe-auth.h
@@ -26,7 +26,8 @@ extern char *pg_fe_getauthname(PQExpBuffer errorMessage);
 extern void *pg_fe_scram_init(const char *username, const char *password,
 					 bool ssl_in_use, bool channel_binding_advertised,
 					 const char *saslmechanism,char *saslchannelbinding,
-					 char *tls_finished_message, int tls_finished_len);
+					 char *tls_finished_message, int tls_finished_len,
+					 char *certificate_hash, int certificate_hash_len);
 extern void pg_fe_scram_free(void *opaq);
 extern void pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
 					 char **output, int *outputlen,
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 84a6e3c322..301bde0799 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -418,6 +418,84 @@ pgtls_get_finish(PGconn *conn, int *len)
 	return result;
 }
 
+/*
+ *	Get the hash of the server certificate
+ *
+ * This information is useful for end-point channel binding, where
+ * the client certificate hash is used as a link, per RFC 5929. If
+ * the signature hash algorithm is MD5 or SHA-1, fall back to SHA-256,
+ * as per RFC 5929 (https://tools.ietf.org/html/rfc5929#section-4.1).
+ * NULL is sent back to the caller in the event of an error, with an
+ * error message for the caller to consume.
+ */
+char *
+pgtls_get_peer_certificate_hash(PGconn *conn, int *len)
+{
+	char	   *cert_hash = NULL;
+
+	*len = 0;
+
+	if (conn->peer)
+	{
+		X509		   *peer_cert = conn->peer;
+		const EVP_MD   *algo_type = NULL;
+		char			hash[EVP_MAX_MD_SIZE];	/* size for SHA-512 */
+		unsigned int	hash_size;
+		int				algo_nid;
+
+		/*
+		 * 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(peer_cert),
+								 &algo_nid, NULL))
+		{
+			printfPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("could not find signature algorithm\n"));
+			return NULL;
+		}
+
+		switch (algo_nid)
+		{
+			case NID_md5:
+			case NID_sha1:
+				algo_type = EVP_sha256();
+				break;
+
+			default:
+				algo_type = EVP_get_digestbynid(algo_nid);
+				if (algo_type == NULL)
+				{
+					printfPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("could not find digest for NID %s\n"),
+									  OBJ_nid2sn(algo_nid));
+					return NULL;
+				}
+				break;
+		}
+
+		if (!X509_digest(peer_cert, algo_type, (unsigned char *) hash,
+						 &hash_size))
+		{
+			printfPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("could not generate peer certificate hash\n"));
+			return NULL;
+		}
+
+		/* save result */
+		cert_hash = (char *) malloc(hash_size);
+		if (cert_hash == NULL)
+		{
+			printfPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("out of memory\n"));
+			return NULL;
+		}
+		memcpy(cert_hash, hash, hash_size);
+		*len = hash_size;
+	}
+
+	return cert_hash;
+}
 
 /* ------------------------------------------------------------ */
 /*						OpenSSL specific code					*/
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 6d500aa5db..f1e2c0bb3c 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -673,6 +673,7 @@ extern ssize_t pgtls_read(PGconn *conn, void *ptr, size_t len);
 extern bool pgtls_read_pending(PGconn *conn);
 extern ssize_t pgtls_write(PGconn *conn, const void *ptr, size_t len);
 extern char *pgtls_get_finish(PGconn *conn, int *len);
+extern char *pgtls_get_peer_certificate_hash(PGconn *conn, int *len);
 
 /*
  * this is so that we can check if a connection is non-blocking internally
diff --git a/src/test/ssl/t/002_sasl.pl b/src/test/ssl/t/002_sasl.pl
index e9226903ca..fa3cca24a2 100644
--- a/src/test/ssl/t/002_sasl.pl
+++ b/src/test/ssl/t/002_sasl.pl
@@ -2,7 +2,7 @@ use strict;
 use warnings;
 use PostgresNode;
 use TestLib;
-use Test::More tests => 6;
+use Test::More tests => 8;
 use ServerSetup;
 use File::Copy;
 
@@ -45,9 +45,13 @@ test_connect_fails($common_connstr, "saslname=not-exists");
 test_connect_ok($common_connstr, "saslname=SCRAM-SHA-256");
 test_connect_ok($common_connstr,
 		"saslname=SCRAM-SHA-256 saslchannelbinding=tls-unique");
+test_connect_ok($common_connstr,
+		"saslname=SCRAM-SHA-256 saslchannelbinding=tls-server-end-point");
 
 # Channel bindings
 test_connect_ok($common_connstr,
 		"saslname=SCRAM-SHA-256-PLUS saslchannelbinding=tls-unique");
+test_connect_ok($common_connstr,
+		"saslname=SCRAM-SHA-256-PLUS saslchannelbinding=tls-server-end-point");
 test_connect_fails($common_connstr,
 		"saslname=SCRAM-SHA-256-PLUS saslchannelbinding=not-exists");
-- 
2.14.2

