From ce450afe422080264c6b395f183f1330c828f404 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <daniel@yesql.se>
Date: Fri, 24 Jan 2020 12:12:55 +0100
Subject: [PATCH] Allow setting min/max TLS protocol version in libpq

In the backend there are GUCs to control the minimum and maximum TLS
versions to allow for a connection, but the clientside libpq lacked
this ability.  Disallowing servers which aren't providing secure TLS
protocols is of interest to clients, but we provide a maximum protocol
version setting by the same rationale as for the backend; to aid with
testing and to cope with misbehaving software.
---
 doc/src/sgml/libpq.sgml                  | 67 ++++++++++++++++++++
 src/interfaces/libpq/fe-connect.c        | 44 +++++++++++++
 src/interfaces/libpq/fe-secure-openssl.c | 81 ++++++++++++++++++++++++
 src/interfaces/libpq/fe-secure.c         | 50 +++++++++++++++
 src/interfaces/libpq/libpq-int.h         |  4 ++
 src/test/ssl/t/001_ssltests.pl           | 26 +++++++-
 6 files changed, 271 insertions(+), 1 deletion(-)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index fcbf7fafbd..a4f0c305ae 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1732,6 +1732,35 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-sslminprotocolversion" xreflabel="sslminprotocolversion">
+      <term><literal>sslminprotocolversion</literal></term>
+      <listitem>
+       <para>
+        This parameter specifies the minimum SSL/TLS protocol version to allow
+        for the connection.  Valid values are <literal>TLSv1</literal>,
+        <literal>TLSv1.1</literal>, <literal>TLSv1.2</literal> and
+        <literal>TLSv1.3</literal>.  The supported protocols depend on the
+        version of <productname>OpenSSL</productname> used, older versions
+        don't support the modern protocol versions. If not set, the system
+        wide default configuration is used.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="libpq-connect-sslmaxprotocolversion" xreflabel="sslmaxprotocolversion">
+      <term><literal>sslmaxprotocolversion</literal></term>
+      <listitem>
+       <para>
+        This parameter specifies the maximum SSL/TLS protocol version to allow
+        for the connection. The supported values are the same as for <literal>
+        sslminprotocolversion</literal>.  Setting a maximum protocol version is
+        generally only useful for testing, or in case there are software components
+        which don't support newer protocol versions. If not set, the system
+        wide default configuration is used.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-krbsrvname" xreflabel="krbsrvname">
       <term><literal>krbsrvname</literal></term>
       <listitem>
@@ -7120,6 +7149,26 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGSSLMINPROTOCOLVERSION</envar></primary>
+      </indexterm>
+      <envar>PGSSLMINPROTOCOLVERSION</envar> behaves the same as the <xref
+      linkend="libpq-connect-sslminprotocolversion"/> connection parameter.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGSSLMAXPROTOCOLVERSION</envar></primary>
+      </indexterm>
+      <envar>PGSSLMAXPROTOCOLVERSION</envar> behaves the same as the <xref
+      linkend="libpq-connect-sslminprotocolversion"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
@@ -7793,6 +7842,24 @@ ldap://ldap.acme.com/cn=dbserver,cn=hosts?pgconnectinfo?base?(objectclass=*)
 
  </sect2>
 
+ <sect2>
+  <title>Client Protocol Usage</title>
+
+  <para>
+   When connecting using SSL, the client and server negotiate which protocol
+   to use for the connection.  <productname>PostgreSQL</productname> supports
+   <literal>TLSv1</literal>, <literal>TLSv1.1</literal>, <literal>TLSv1.2</literal>
+   and <literal>TLSv1.3</literal>, but the protocols available depends on the
+   version of <productname>OpenSSL</productname> which the client is using.
+   The minimum requested version can be specified with <literal>sslminprotocolversion</literal>,
+   which will ensure that the connection use that version, or higher, or fails.
+   The maximum requested version can be specified with <literal>sslmaxprotocolversion</literal>,
+   but this is mainly only useful for testing, or in case a component doesn't
+   work with a newer protocol.
+  </para>
+   
+ </sect2>
+
  <sect2 id="libpq-ssl-fileusage">
   <title>SSL Client File Usage</title>
 
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 80b54bc92b..54a5609b9a 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -320,6 +320,14 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Require-Peer", "", 10,
 	offsetof(struct pg_conn, requirepeer)},
 
+	{"sslminprotocolversion", "PGSSLMINPROTOCOLVERSION", NULL, NULL,
+		"SSL-Minimum-Protocol-Version", "",  /* sizeof("TLSv1.x") */ 7,
+	offsetof(struct pg_conn, sslminprotocolversion)},
+
+	{"sslmaxprotocolversion", "PGSSLMAXPROTOCOLVERSION", NULL, NULL,
+		"SSL-Maximum-Protocol-Version", "", /* sizeof("TLSv1.x") */ 7,
+	offsetof(struct pg_conn, sslmaxprotocolversion)},
+
 	/*
 	 * As with SSL, all GSS options are exposed even in builds that don't have
 	 * support.
@@ -1285,6 +1293,38 @@ connectOptions2(PGconn *conn)
 			goto oom_error;
 	}
 
+	/*
+	 * Validate TLS protocol options sslminprotocolversion and
+	 * sslmaxprotocolversion.
+	 */
+	if (conn->sslminprotocolversion
+		&& !pq_verify_ssl_protocol_option(conn->sslminprotocolversion))
+	{
+		printfPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("invalid sslminprotocolversion value: \"%s\"\n"),
+						  conn->sslminprotocolversion);
+		return false;
+	}
+	if (conn->sslmaxprotocolversion
+		&& !pq_verify_ssl_protocol_option(conn->sslmaxprotocolversion))
+	{
+		printfPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("invalid sslmaxprotocolversion value: \"%s\"\n"),
+						  conn->sslmaxprotocolversion);
+		return false;
+	}
+
+	if (conn->sslminprotocolversion && conn->sslmaxprotocolversion)
+	{
+		if (!pq_verify_ssl_protocol_range(conn->sslminprotocolversion,
+										  conn->sslmaxprotocolversion))
+		{
+			printfPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("invalid protocol version range"));
+			return false;
+		}
+	}
+
 	/*
 	 * validate gssencmode option
 	 */
@@ -4001,6 +4041,10 @@ freePGconn(PGconn *conn)
 		free(conn->sslcompression);
 	if (conn->requirepeer)
 		free(conn->requirepeer);
+	if (conn->sslminprotocolversion)
+		free(conn->sslminprotocolversion);
+	if (conn->sslmaxprotocolversion)
+		free(conn->sslmaxprotocolversion);
 	if (conn->gssencmode)
 		free(conn->gssencmode);
 	if (conn->krbsrvname)
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 0e84fc8ac6..8ee6572ce1 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -30,6 +30,7 @@
 #include "fe-auth.h"
 #include "fe-secure-common.h"
 #include "libpq-int.h"
+#include "common/openssl.h"
 
 #ifdef WIN32
 #include "win32.h"
@@ -95,6 +96,7 @@ static long win32_ssl_create_mutex = 0;
 #endif							/* ENABLE_THREAD_SAFETY */
 
 static PQsslKeyPassHook_type PQsslKeyPassHook = NULL;
+static int ssl_protocol_version_to_openssl(const char *protocol);
 
 /* ------------------------------------------------------------ */
 /*			 Procedures common to all secure sessions			*/
@@ -787,6 +789,8 @@ initialize_SSL(PGconn *conn)
 	bool		have_cert;
 	bool		have_rootcert;
 	EVP_PKEY   *pkey = NULL;
+	int			ssl_max_ver;
+	int			ssl_min_ver;
 
 	/*
 	 * We'll need the home directory if any of the relevant parameters are
@@ -843,6 +847,52 @@ initialize_SSL(PGconn *conn)
 	/* Disable old protocol versions */
 	SSL_CTX_set_options(SSL_context, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
 
+	if (conn->sslminprotocolversion && strlen(conn->sslminprotocolversion) > 0)
+	{
+		ssl_min_ver = ssl_protocol_version_to_openssl(conn->sslminprotocolversion);
+
+		if (ssl_min_ver == -1)
+		{
+			printfPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("invalid minimum protocol version specified: %s\n"),
+							  conn->sslminprotocolversion);
+			return -1;
+		}
+
+		if (!SSL_CTX_set_min_proto_version(SSL_context, ssl_min_ver))
+		{
+			char	   *err = SSLerrmessage(ERR_get_error());
+
+			printfPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("unable to set minimum protocol version specified: %s\n"),
+							  err);
+			return -1;
+		}
+	}
+
+	if (conn->sslmaxprotocolversion && strlen(conn->sslmaxprotocolversion) > 0)
+	{
+		ssl_max_ver = ssl_protocol_version_to_openssl(conn->sslmaxprotocolversion);
+
+		if (ssl_max_ver == -1)
+		{
+			printfPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("invalid or unsupported maximum protocol version specified: %s\n"),
+							  conn->sslmaxprotocolversion);
+			return -1;
+		}
+
+		if (!SSL_CTX_set_max_proto_version(SSL_context, ssl_max_ver))
+		{
+			char	   *err = SSLerrmessage(ERR_get_error());
+
+			printfPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("unable to set maximum SSL version specified: %s\n"),
+							  err);
+			return -1;
+		}
+	}
+
 	/*
 	 * Disable OpenSSL's moving-write-buffer sanity check, because it causes
 	 * unnecessary failures in nonblocking send cases.
@@ -1659,3 +1709,34 @@ PQssl_passwd_cb(char *buf, int size, int rwflag, void *userdata)
 	else
 		return PQdefaultSSLKeyPassHook(buf, size, conn);
 }
+
+/*
+ * Convert TLS protocol version string to OpenSSL values
+ *
+ * If a version is passed that is not supported by the current OpenSSL version,
+ * then we return -1. If a nonnegative value is returned, subsequent code can
+ * assume it's working with a supported version.
+ */
+static int
+ssl_protocol_version_to_openssl(const char *protocol)
+{
+	if (pg_strcasecmp("TLSv1", protocol) == 0)
+		return TLS1_VERSION;
+
+#ifdef TLS1_1_VERSION
+	if (pg_strcasecmp("TLSv1.1", protocol) == 0)
+		return TLS1_1_VERSION;
+#endif
+
+#ifdef TLS1_2_VERSION
+	if (pg_strcasecmp("TLSv1.2", protocol) == 0)
+		return TLS1_2_VERSION;
+#endif
+
+#ifdef TLS1_3_VERSION
+	if (pg_strcasecmp("TLSv1.3", protocol) == 0)
+		return TLS1_3_VERSION;
+#endif
+
+	return -1;
+}
diff --git a/src/interfaces/libpq/fe-secure.c b/src/interfaces/libpq/fe-secure.c
index 52f6e8790e..fbbdee4686 100644
--- a/src/interfaces/libpq/fe-secure.c
+++ b/src/interfaces/libpq/fe-secure.c
@@ -555,3 +555,53 @@ pq_reset_sigpipe(sigset_t *osigset, bool sigpipe_pending, bool got_epipe)
 }
 
 #endif							/* ENABLE_THREAD_SAFETY && !WIN32 */
+
+bool
+pq_verify_ssl_protocol_option(const char *protocolversion)
+{
+	if (!protocolversion || strlen(protocolversion) == 0)
+		return false;
+
+	if (pg_strcasecmp(protocolversion, "TLSv1") == 0
+		|| pg_strcasecmp(protocolversion, "TLSv1.1") == 0
+		|| pg_strcasecmp(protocolversion, "TLSv1.2") == 0
+		|| pg_strcasecmp(protocolversion, "TLSv1.3") == 0)
+		return true;
+
+	return false;
+}
+
+/*
+ *	Ensure that the protocol range is sane
+ *
+ * Make sure that the maximum version isn't lower than the minimum. The check
+ * is performed on the input string to keep it TLS backend agnostic. Input to
+ * this function is expected verified with pq_verify_ssl_protocol_option, as
+ * the code is not performing errorchecking on the input.
+ */
+bool
+pq_verify_ssl_protocol_range(const char *min, const char *max)
+{
+	/*
+	 * If the minimum version is the lowest one we accept, then all options
+	 * for max are valid.
+	 */
+	if (strlen(min) == strlen("TLSv1"))
+		return true;
+
+	/*
+	 * We know now that the minimum isn't TLSv1, so having that as a max is
+	 * not valid.
+	 */
+	if (strlen(max) == strlen("TLSv1"))
+		return false;
+
+	/*
+	 * At this point we know we have a mix of TLSv1.1 through 1.3 versions, so
+	 * we can work with the properties of the minor rev character.
+	 */
+	if (*(min + strlen("TLSv1.")) > *(max + strlen("TLSv1.")))
+		return false;
+
+	return true;
+}
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 79bc3780ff..82399c968d 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -367,6 +367,8 @@ struct pg_conn
 	char	   *krbsrvname;		/* Kerberos service name */
 	char	   *gsslib;			/* What GSS library to use ("gssapi" or
 								 * "sspi") */
+	char	   *sslminprotocolversion;	/* minimum TLS protocol version */
+	char	   *sslmaxprotocolversion;	/* maximum TLS protocol version */
 
 	/* Type of connection to make.  Possible values: any, read-write. */
 	char	   *target_session_attrs;
@@ -682,6 +684,8 @@ extern ssize_t pqsecure_read(PGconn *, void *ptr, size_t len);
 extern ssize_t pqsecure_write(PGconn *, const void *ptr, size_t len);
 extern ssize_t pqsecure_raw_read(PGconn *, void *ptr, size_t len);
 extern ssize_t pqsecure_raw_write(PGconn *, const void *ptr, size_t len);
+extern bool pq_verify_ssl_protocol_option(const char *protocolversion);
+extern bool pq_verify_ssl_protocol_range(const char *min, const char *max);
 
 #if defined(ENABLE_THREAD_SAFETY) && !defined(WIN32)
 extern int	pq_block_sigpipe(sigset_t *osigset, bool *sigpipe_pending);
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 7b18402cf6..2bd81d53cc 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -13,7 +13,7 @@ use SSLServer;
 
 if ($ENV{with_openssl} eq 'yes')
 {
-	plan tests => 86;
+	plan tests => 93;
 }
 else
 {
@@ -356,6 +356,30 @@ command_like(
 				^\d+,t,TLSv[\d.]+,[\w-]+,\d+,f,_null_,_null_,_null_\r?$}mx,
 	'pg_stat_ssl view without client certificate');
 
+# Test min/mix protocol versions
+test_connect_ok(
+	$common_connstr,
+	"sslrootcert=ssl/root+server_ca.crt sslmode=require sslminprotocolversion=tlsv1.2 sslmaxprotocolversion=TLSv1.3",
+	"connect with correct range of allowed TLS protocol versions");
+
+test_connect_fails(
+	$common_connstr,
+	"sslrootcert=ssl/root+server_ca.crt sslmode=require sslminprotocolversion=TLSv1.3 sslmaxprotocolversion=tlsv1.2",
+	qr/invalid protocol version range/,
+	"connect with an incorrect range of TLS protocol versions leaving no versions allowed");
+
+test_connect_fails(
+	$common_connstr,
+	"sslrootcert=ssl/root+server_ca.crt sslmode=require sslminprotocolversion=TLSv1.3 sslmaxprotocolversion=tlsv1",
+	qr/invalid protocol version range/,
+	"connect with an incorrect range of TLS protocol versions leaving no versions allowed");
+
+test_connect_fails(
+	$common_connstr,
+	"sslrootcert=ssl/root+server_ca.crt sslmode=require sslminprotocolversion=TLSv3.1",
+	qr/invalid sslminprotocolversion value/,
+	"connect with an incorrect TLS protocol version");
+
 ### Server-side tests.
 ###
 ### Test certificate authorization.
-- 
2.21.0 (Apple Git-122.2)

