From df079495c20779a7294a8ced2b62641a3aeebb64 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 20 Jun 2017 12:51:10 +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    | 69 ++++++++++++++++++++++++++++++++
 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           | 18 ++++++++-
 src/interfaces/libpq/fe-auth.h           |  3 +-
 src/interfaces/libpq/fe-secure-openssl.c | 66 ++++++++++++++++++++++++++++++
 src/interfaces/libpq/libpq-int.h         |  1 +
 src/test/ssl/t/002_sasl.pl               |  6 ++-
 12 files changed, 210 insertions(+), 16 deletions(-)

diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 37ae7276e5..62a958ed1d 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1552,8 +1552,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 d5371a2708..89b659942b 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_finish_message;
 	int			tls_finish_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_finish_message,
-				 int tls_finish_len)
+				 int tls_finish_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_finish_message = tls_finish_message;
 	state->tls_finish_len = tls_finish_len;
+	state->certificate_hash = certificate_hash;
+	state->certificate_hash_len = certificate_hash_len;
 	state->channel_binding = NULL;
 
 	/*
@@ -847,11 +853,12 @@ read_client_first_message(scram_state *state, char *input)
 							 errmsg("client supports SCRAM channel binding, but server does not need it for non-SSL connections")));
 
 				/*
-				 * 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"))));
@@ -1120,6 +1127,12 @@ read_client_final_message(scram_state *state, char *input)
 			raw_data = state->tls_finish_message;
 			raw_data_len = state->tls_finish_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 ee9bce03a2..bbab65b64b 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_finish = NULL;
 	int			tls_finish_len = 0;
+	char	   *certificate_bash = NULL;
+	int			certificate_bash_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_finish = be_tls_get_peer_finish(port, &tls_finish_len);
+		certificate_bash =
+			be_tls_get_certificate_hash(port,
+										&certificate_bash_len);
 	}
 #endif
 
@@ -942,7 +947,9 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
 								  shadow_pass,
 								  port->ssl_in_use,
 								  tls_finish,
-								  tls_finish_len);
+								  tls_finish_len,
+								  certificate_bash,
+								  certificate_bash_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..cea60c6741 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -1239,6 +1239,75 @@ 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. If SHA-256 or 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 nothing certificates 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_sha512:
+				algo_type = EVP_sha512();
+				break;
+
+			case NID_sha384:
+				algo_type = EVP_sha384();
+				break;
+
+			/*
+			 * Fallback to SHA-256 for weaker hashes, and keep them listed
+			 * here for reference.
+			 */
+			case NID_md5:
+			case NID_sha1:
+			case NID_sha224:
+			case NID_sha256:
+			default:
+				algo_type = EVP_sha256();
+				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 91cebe9a9b..55d0568d43 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_finish_message,
-					 int tls_finish_len);
+					 int tls_finish_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 5b8522391d..10517d16e5 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -48,6 +48,8 @@ typedef struct
 	bool		ssl_in_use;
 	char	   *tls_finish_message;
 	int			tls_finish_len;
+	char	   *certificate_hash;
+	int			certificate_hash_len;
 	/* enforceable user parameters */
 	char	   *saslchannelbinding;	/* name of channel binding to use */
 
@@ -92,7 +94,9 @@ pg_fe_scram_init(const char *username,
 				 bool ssl_in_use,
 				 char *saslchannelbinding,
 				 char *tls_finish_message,
-				 int tls_finish_len)
+				 int tls_finish_len,
+				 char *certificate_hash,
+				 int certificate_hash_len)
 {
 	fe_scram_state *state;
 	char	   *prep_password;
@@ -107,6 +111,8 @@ pg_fe_scram_init(const char *username,
 	state->ssl_in_use = ssl_in_use;
 	state->tls_finish_message = tls_finish_message;
 	state->tls_finish_len = tls_finish_len;
+	state->certificate_hash = certificate_hash;
+	state->certificate_hash_len = certificate_hash_len;
 
 	/*
 	 * If user has specified a channel binding to use, enforce the
@@ -150,6 +156,8 @@ pg_fe_scram_free(void *opaq)
 		free(state->password);
 	if (state->tls_finish_message)
 		free(state->tls_finish_message);
+	if (state->certificate_hash)
+		free(state->certificate_hash);
 	if (state->saslchannelbinding)
 		free(state->saslchannelbinding);
 
@@ -423,7 +431,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 (state->ssl_in_use)
@@ -436,6 +446,12 @@ build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage)
 			raw_data = state->tls_finish_message;
 			raw_data_len = state->tls_finish_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 4b26018f65..d72d35670f 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -539,6 +539,8 @@ pg_SASL_init(PGconn *conn, int payloadlen)
 			char	   *password;
 			char	   *tls_finish = NULL;
 			int			tls_finish_len = 0;
+			char	   *certificate_hash = NULL;
+			int			certificate_hash_len = 0;
 
 			conn->password_needed = true;
 			password = conn->connhost[conn->whichhost].password;
@@ -552,12 +554,21 @@ pg_SASL_init(PGconn *conn, int payloadlen)
 			}
 
 #ifdef USE_SSL
-			/* Fetch information about the TLS finish message */
+			/*
+			 * Fetch information about the TLS finish message and client
+			 * certificate if any.
+			 */
 			if (conn->ssl_in_use)
 			{
 				tls_finish = pgtls_get_finish(conn, &tls_finish_len);
 				if (tls_finish == NULL)
 					goto oom_error;
+
+				certificate_hash =
+					pgtls_get_peer_certificate_hash(conn,
+													&certificate_hash_len);
+				if (certificate_hash == NULL)
+					goto oom_error;
 			}
 #endif
 
@@ -566,7 +577,10 @@ pg_SASL_init(PGconn *conn, int payloadlen)
 												conn->ssl_in_use,
 												conn->saslchannelbinding,
 												tls_finish,
-												tls_finish_len);
+												tls_finish_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 3c699959e0..8db86f8bcc 100644
--- a/src/interfaces/libpq/fe-auth.h
+++ b/src/interfaces/libpq/fe-auth.h
@@ -25,7 +25,8 @@ extern char *pg_fe_getauthname(PQExpBuffer errorMessage);
 /* Prototypes for functions in fe-auth-scram.c */
 extern void *pg_fe_scram_init(const char *username, const char *password,
 					 bool ssl_in_use, char *saslchannelbinding,
-					 char *tls_finish_message, int tls_finish_len);
+					 char *tls_finish_message, int tls_finish_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..00a14289e4 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -418,6 +418,72 @@ 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.
+ */
+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))
+			return NULL;
+
+		switch (algo_nid)
+		{
+			case NID_sha512:
+				algo_type = EVP_sha512();
+				break;
+
+			case NID_sha384:
+				algo_type = EVP_sha384();
+				break;
+
+			/*
+			 * Fallback to SHA-256 for weaker hashes, and keep them listed
+			 * here for reference.
+			 */
+			case NID_md5:
+			case NID_sha1:
+			case NID_sha224:
+			case NID_sha256:
+			default:
+				algo_type = EVP_sha256();
+				break;
+		}
+
+		if (!X509_digest(peer_cert, algo_type, (unsigned char *) hash,
+						 &hash_size))
+			return NULL;
+
+		/* save result */
+		cert_hash = (char *) malloc(hash_size);
+		if (cert_hash == NULL)
+			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 a625f0d473..a4e6dfac3d 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;
 
@@ -44,9 +44,13 @@ test_connect_fails($common_connstr, "saslname=not-exists");
 test_connect_fails($common_connstr, "saslname=SCRAM-SHA-256");
 test_connect_fails($common_connstr,
 		"saslname=SCRAM-SHA-256 saslchannelbinding=tls-unique");
+test_connect_fails($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.1

