From bd086e2d8b34444151ca52d2e2cd43b8f4a9f522 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Mon, 29 Aug 2022 14:52:41 +0900
Subject: [PATCH] tls-exporter as channel binding for SCRAM/SSL

---
 src/include/libpq/libpq-be.h             |  6 +++
 src/backend/libpq/auth-scram.c           | 50 ++++++++++++++++++------
 src/backend/libpq/be-secure-openssl.c    | 18 +++++++++
 src/interfaces/libpq/fe-auth-scram.c     | 45 +++++++++++++++++----
 src/interfaces/libpq/fe-connect.c        | 27 +++++++++++++
 src/interfaces/libpq/fe-secure-openssl.c | 30 ++++++++++++++
 src/interfaces/libpq/libpq-int.h         | 14 +++++++
 src/test/ssl/t/002_scram.pl              | 15 +++++++
 doc/src/sgml/libpq.sgml                  | 28 +++++++++++++
 doc/src/sgml/protocol.sgml               |  3 +-
 10 files changed, 216 insertions(+), 20 deletions(-)

diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 6d452ec6d9..78ff51d053 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -298,6 +298,12 @@ extern void be_tls_get_peer_subject_name(Port *port, char *ptr, size_t len);
 extern void be_tls_get_peer_issuer_name(Port *port, char *ptr, size_t len);
 extern void be_tls_get_peer_serial(Port *port, char *ptr, size_t len);
 
+extern unsigned char *be_tls_export_keying_material(Port *port,
+													const char *label,
+													const unsigned char *ctx,
+													size_t ctxlen,
+													size_t outlen);
+
 /*
  * Get the server certificate hash for SCRAM channel binding type
  * tls-server-end-point.
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index ee7f52218a..6029e33596 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -148,6 +148,7 @@ typedef struct
 
 	/* Fields of the first message from client */
 	char		cbind_flag;
+	char	   *channel_binding_type;
 	char	   *client_first_message_bare;
 	char	   *client_username;
 	char	   *client_nonce;
@@ -876,7 +877,6 @@ static void
 read_client_first_message(scram_state *state, const char *input)
 {
 	char	   *p = pstrdup(input);
-	char	   *channel_binding_type;
 
 
 	/*------
@@ -1009,17 +1009,17 @@ read_client_first_message(scram_state *state, const char *input)
 						 errmsg("malformed SCRAM message"),
 						 errdetail("The client selected SCRAM-SHA-256 without channel binding, but the SCRAM message includes channel binding data.")));
 
-			channel_binding_type = read_attr_value(&p, 'p');
+			state->channel_binding_type = read_attr_value(&p, 'p');
 
 			/*
-			 * The only channel binding type we support is
-			 * tls-server-end-point.
+			 * We support tls-server-end-point and tls-exporter.
 			 */
-			if (strcmp(channel_binding_type, "tls-server-end-point") != 0)
+			if (strcmp(state->channel_binding_type, "tls-server-end-point") != 0
+				&& strcmp(state->channel_binding_type, "tls-exporter") != 0)
 				ereport(ERROR,
 						(errcode(ERRCODE_PROTOCOL_VIOLATION),
 						 errmsg("unsupported SCRAM channel-binding type \"%s\"",
-								sanitize_str(channel_binding_type))));
+								sanitize_str(state->channel_binding_type))));
 			break;
 		default:
 			ereport(ERROR,
@@ -1286,18 +1286,46 @@ read_client_final_message(scram_state *state, const char *input)
 
 		Assert(state->cbind_flag == 'p');
 
-		/* Fetch hash data of server's SSL certificate */
-		cbind_data = be_tls_get_certificate_hash(state->port,
-												 &cbind_data_len);
+		if (strcmp(state->channel_binding_type, "tls-exporter") == 0)
+		{
+			/*------
+			 * From the specification (RFC 9266):
+			 *
+			 * The [tls-exporter] EKM is obtained using the keying material
+			 * exporters for TLS as defined in [RFC5705] and [RFC8446]
+			 * section 7.5 by supplying the following inputs:
+			 *
+			 * Label:  The ASCII string "EXPORTER-Channel-Binding" with no
+			 *         terminating NUL.
+			 *
+			 * Context value:  Zero-length string.
+			 *
+			 * Length:  32 bytes.
+			 *------
+			 */
+			cbind_data_len = 32;
+			cbind_data = (char *)
+				be_tls_export_keying_material(state->port,
+											  "EXPORTER-Channel-Binding",
+											  (unsigned char *) "", 0,
+											  cbind_data_len);
+		}
+		else /* tls-server-end-point */
+		{
+			/* Fetch hash data of server's SSL certificate */
+			cbind_data = be_tls_get_certificate_hash(state->port,
+													 &cbind_data_len);
+		}
 
 		/* should not happen */
 		if (cbind_data == NULL || cbind_data_len == 0)
 			elog(ERROR, "could not get server certificate hash");
 
-		cbind_header_len = strlen("p=tls-server-end-point,,");	/* p=type,, */
+		cbind_header_len = strlen(state->channel_binding_type) + 4;	/* p=type,, */
 		cbind_input_len = cbind_header_len + cbind_data_len;
 		cbind_input = palloc(cbind_input_len);
-		snprintf(cbind_input, cbind_input_len, "p=tls-server-end-point,,");
+		snprintf(cbind_input, cbind_input_len, "p=%s,,",
+				 state->channel_binding_type);
 		memcpy(cbind_input + cbind_header_len, cbind_data, cbind_data_len);
 
 		b64_message_len = pg_b64_enc_len(cbind_input_len);
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 55d4b29f7e..163567bf51 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -1424,6 +1424,24 @@ be_tls_get_peer_serial(Port *port, char *ptr, size_t len)
 		ptr[0] = '\0';
 }
 
+unsigned char *
+be_tls_export_keying_material(Port *port, const char *label,
+							  const unsigned char *ctx, size_t ctxlen,
+							  size_t outlen)
+{
+	int				rc;
+	unsigned char  *out = palloc(outlen);
+
+	rc = SSL_export_keying_material(port->ssl, out, outlen,
+									label, strlen(label),
+									ctx, ctxlen,
+									1 /* use the context */);
+	if (rc < 1)
+		elog(ERROR, "could not export keying material");
+
+	return out;
+}
+
 #ifdef HAVE_X509_GET_SIGNATURE_NID
 char *
 be_tls_get_certificate_hash(Port *port, size_t *len)
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index 35cfd9987d..ca1a583783 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -400,7 +400,7 @@ build_client_first_message(fe_scram_state *state)
 	if (strcmp(state->sasl_mechanism, SCRAM_SHA_256_PLUS_NAME) == 0)
 	{
 		Assert(conn->ssl_in_use);
-		appendPQExpBufferStr(&buf, "p=tls-server-end-point");
+		appendPQExpBuffer(&buf, "p=%s", conn->channel_binding_type);
 	}
 #ifdef HAVE_PGTLS_GET_PEER_CERTIFICATE_HASH
 	else if (conn->channel_binding[0] != 'd' && /* disable */
@@ -484,10 +484,38 @@ build_client_final_message(fe_scram_state *state)
 		size_t		cbind_input_len;
 		int			encoded_cbind_len;
 
-		/* Fetch hash data of server's SSL certificate */
-		cbind_data =
-			pgtls_get_peer_certificate_hash(state->conn,
-											&cbind_data_len);
+		if (strcmp(state->conn->channel_binding_type, "tls-exporter") == 0)
+		{
+			/*------
+			 * From the spec:
+			 *
+			 * The [tls-exporter] EKM is obtained using the keying material
+			 * exporters for TLS as defined in [RFC5705] and [RFC8446] section
+			 * 7.5 by supplying the following inputs:
+			 *
+			 * Label:  The ASCII string "EXPORTER-Channel-Binding" with no
+			 *         terminating NUL.
+			 *
+			 * Context value:  Zero-length string.
+			 *
+			 * Length:  32 bytes.
+			 *------
+			 */
+			cbind_data_len = 32;
+			cbind_data = (char *)
+				pgtls_export_keying_material(state->conn,
+											 "EXPORTER-Channel-Binding",
+											 (unsigned char *) "", 0,
+											 cbind_data_len);
+		}
+		else /* tls-server-end-point */
+		{
+			/* Fetch hash data of server's SSL certificate */
+			cbind_data =
+				pgtls_get_peer_certificate_hash(state->conn,
+												&cbind_data_len);
+		}
+
 		if (cbind_data == NULL)
 		{
 			/* error message is already set on error */
@@ -498,7 +526,7 @@ build_client_final_message(fe_scram_state *state)
 		appendPQExpBufferStr(&buf, "c=");
 
 		/* p=type,, */
-		cbind_header_len = strlen("p=tls-server-end-point,,");
+		cbind_header_len = strlen(state->conn->channel_binding_type) + 4;
 		cbind_input_len = cbind_header_len + cbind_data_len;
 		cbind_input = malloc(cbind_input_len);
 		if (!cbind_input)
@@ -506,8 +534,9 @@ build_client_final_message(fe_scram_state *state)
 			free(cbind_data);
 			goto oom_error;
 		}
-		memcpy(cbind_input, "p=tls-server-end-point,,", cbind_header_len);
-		memcpy(cbind_input + cbind_header_len, cbind_data, cbind_data_len);
+		snprintf(cbind_input, cbind_input_len, "p=%s,,",
+				 state->conn->channel_binding_type);
+		memcpy(cbind_input + cbind_header_len , cbind_data, cbind_data_len);
 
 		encoded_cbind_len = pg_b64_enc_len(cbind_input_len);
 		if (!enlargePQExpBuffer(&buf, encoded_cbind_len))
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 917b19e0e9..53ff024f1d 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -122,6 +122,7 @@ static int	ldapServiceLookup(const char *purl, PQconninfoOption *options,
 #else
 #define DefaultChannelBinding	"disable"
 #endif
+#define DefaultChannelBindingType	"tls-server-end-point"
 #define DefaultTargetSessionAttrs	"any"
 #ifdef USE_SSL
 #define DefaultSSLMode "prefer"
@@ -205,6 +206,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Channel-Binding", "", 8,	/* sizeof("require") == 8 */
 	offsetof(struct pg_conn, channel_binding)},
 
+	{"channel_binding_type", "PGCHANNELBINDINGTYPE", NULL, NULL,
+		"Channel-Binding-Type", "", 20,	/* sizeof("tls-server-end-point") == 20 */
+	offsetof(struct pg_conn, channel_binding_type)},
+
 	{"connect_timeout", "PGCONNECT_TIMEOUT", NULL, NULL,
 		"Connect-timeout", "", 10,	/* strlen(INT32_MAX) == 10 */
 	offsetof(struct pg_conn, connect_timeout)},
@@ -1263,6 +1268,28 @@ connectOptions2(PGconn *conn)
 			goto oom_error;
 	}
 
+	/*
+	 * validate channel_binding_type option
+	 */
+	if (conn->channel_binding_type)
+	{
+		if (strcmp(conn->channel_binding_type, "tls-server-end-point") != 0
+			&& strcmp(conn->channel_binding_type, "tls-exporter") != 0)
+		{
+			conn->status = CONNECTION_BAD;
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("invalid %s value: \"%s\"\n"),
+							  "channel_binding_type", conn->channel_binding_type);
+			return false;
+		}
+	}
+	else
+	{
+		conn->channel_binding_type = strdup(DefaultChannelBindingType);
+		if (!conn->channel_binding_type)
+			goto oom_error;
+	}
+
 	/*
 	 * validate sslmode option
 	 */
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 3798bb3f11..89fa4513c4 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -378,6 +378,36 @@ pgtls_write(PGconn *conn, const void *ptr, size_t len)
 	return n;
 }
 
+unsigned char *
+pgtls_export_keying_material(PGconn *conn, const char *label,
+							 const unsigned char *ctx, size_t ctxlen,
+							 size_t outlen)
+{
+	int				rc;
+	unsigned char  *out = malloc(outlen);
+
+	if (out == NULL)
+	{
+		appendPQExpBufferStr(&conn->errorMessage,
+							 libpq_gettext("out of memory\n"));
+		return NULL;
+	}
+
+	rc = SSL_export_keying_material(conn->ssl, out, outlen,
+									label, strlen(label),
+									ctx, ctxlen,
+									1 /* use the context */);
+	if (rc < 1)
+	{
+		appendPQExpBufferStr(&conn->errorMessage,
+							 libpq_gettext("could not export keying material\n"));
+		free(out);
+		return NULL;
+	}
+
+	return out;
+}
+
 #ifdef HAVE_X509_GET_SIGNATURE_NID
 char *
 pgtls_get_peer_certificate_hash(PGconn *conn, size_t *len)
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index c75ed63a2c..f412e0c2ce 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -373,6 +373,8 @@ struct pg_conn
 	char	   *pgpassfile;		/* path to a file containing password(s) */
 	char	   *channel_binding;	/* channel binding mode
 									 * (require,prefer,disable) */
+	char	   *channel_binding_type;	/* from the IANA Channel-Binding Types
+										 * registry */
 	char	   *keepalives;		/* use TCP keepalives? */
 	char	   *keepalives_idle;	/* time between TCP keepalives */
 	char	   *keepalives_interval;	/* time between TCP keepalive
@@ -795,6 +797,18 @@ extern bool pgtls_read_pending(PGconn *conn);
  */
 extern ssize_t pgtls_write(PGconn *conn, const void *ptr, size_t len);
 
+/*
+ * Export keying material, for SCRAM channel binding type tls-exporter.
+ *
+ * NULL is sent back to the caller in the evenf of an error, with an
+ * error message for the caller to consume.
+ */
+extern unsigned char *pgtls_export_keying_material(PGconn *conn,
+												   const char *label,
+												   const unsigned char *ctx,
+												   size_t ctxlen,
+												   size_t outlen);
+
 /*
  * Get the hash of the server certificate, for SCRAM channel binding type
  * tls-server-end-point.
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 588f47a39b..f4c175424f 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -105,6 +105,21 @@ $node->connect_fails(
 	  qr/channel binding required but not supported by server's authentication request/
 );
 
+# Tests for channel_binding_type
+$node->connect_fails(
+	"$common_connstr user=ssltestuser channel_binding=require channel_binding_type=invalid_value",
+	"SCRAM with SSL and channel_binding_type=invalid_value",
+	expected_stderr => qr/invalid channel_binding_type value: "invalid_value"/);
+if ($supports_tls_server_end_point)
+{
+	$node->connect_ok(
+		"$common_connstr user=ssltestuser channel_binding=require channel_binding_type=tls-server-end-point",
+		"SCRAM with SSL and channel_binding_type=tls-server-end-point");
+}
+$node->connect_ok(
+	"$common_connstr user=ssltestuser channel_binding=require channel_binding_type=tls-exporter",
+	"SCRAM with SSL and channel_binding_type=tls-exporter");
+
 # Now test with auth method 'cert' by connecting to 'certdb'. Should fail,
 # because channel binding is not performed.  Note that ssl/client.key may
 # be used in a different test, so the name of this temporary client key
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 8a1a9e9932..b29dcd69d0 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1242,6 +1242,24 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+
+     <varlistentry id="libpq-connect-channel-binding-type" xreflabel="channel_binding_type">
+      <term><literal>channel_binding_type</literal></term>
+      <listitem>
+      <para>
+        This option controls the type of channel binding used by the client
+        when <literal>channel_binding</literal> is enabled. Supported
+        values are <literal>tls-server-end-point</literal>
+        (<ulink url="https://tools.ietf.org/html/rfc5929">RFC 5929</ulink>)
+        and <literal>tls-exporter</literal>
+        (<ulink url="https://tools.ietf.org/html/rfc9266">RFC 9266</ulink>
+        channel binding for <literal>TLSv1.3</literal> that has the advantage
+        to prevent man-in-the-middle attacks when the attacker has the
+        server's private key).
+      </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-connect-timeout" xreflabel="connect_timeout">
       <term><literal>connect_timeout</literal></term>
       <listitem>
@@ -7766,6 +7784,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGCHANNELBINDINGTYPE</envar></primary>
+      </indexterm>
+      <envar>PGCHANNELBINDINGTYPE</envar> behaves the same as the <xref
+      linkend="libpq-connect-channel-binding-type"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 87870c5b10..a66abb1f29 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1752,7 +1752,8 @@ SELCT 1/0;<!-- this typo is intentional -->
     <firstterm>Channel binding</firstterm> is supported in PostgreSQL builds with
     SSL support. The SASL mechanism name for SCRAM with channel binding is
     <literal>SCRAM-SHA-256-PLUS</literal>.  The channel binding type used by
-    PostgreSQL is <literal>tls-server-end-point</literal>.
+    PostgreSQL are <literal>tls-server-end-point</literal> and
+    <literal>tls-exporter</literal>.
    </para>
 
    <para>
-- 
2.37.2

