Hi all,

RFC9266, that has been released not so long ago, has added
tls-exporter as a new channel binding type:
https://www.rfc-editor.org/rfc/rfc5929.html

An advantage over tls-server-end-point, AFAIK, is that this prevents
man-in-the-middle attacks even if the attacker holds the server's
private key, which was the kind of job that tls-unique does for
TLSv1.2, though we've decided at the end to drop it during the PG11
dev cycle because it does things poorly.

This patch provides an implementation, tests and documentation for the
so-said feature.  An environment variable called PGCHANNELBINDINGTYPE
is added, as well as new connection parameter called
channel_binding_type.  The key point of the implementation is
SSL_export_keying_material(), that is available down to 1.0.1 (oldest
version supported on HEAD), so this should not require a ./configure
check.

Perhaps the part about the new libpq parameter could be refactored as
of its own patch, with the addition of channel_binding_type in the
SCRAM status structures.  Note also that tls-exporter is aimed for
TLSv1.3 and newer protocols, but OpenSSL allows the thing to work with
older protocols (testable with ssl_max_protocol_version, for example),
and I don't see a need to prevent this scenario.  An extra thing is
that attempting to use tls-exporter with a backend <= 15 and a client
>= 16 causes a failure during the SASL exchange, where the backend
complains about tls-exporter being unsupported.

Jacob Champion should be considered as the primary author of the
patch, even if I have spent some time on this patch before sending it
here.  I am adding that to the next commit fest.

Thanks,
--
Michael
From bd086e2d8b34444151ca52d2e2cd43b8f4a9f522 Mon Sep 17 00:00:00 2001
From: Michael Paquier <mich...@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

Attachment: signature.asc
Description: PGP signature

Reply via email to