From 6c365125c33ecf7bac7b4fc3f795380c8a0ea782 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <daniel@yesql.se>
Date: Sat, 30 Nov 2019 01:32:04 +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                  |  65 ++++++++
 src/interfaces/libpq/fe-connect.c        |   8 +
 src/interfaces/libpq/fe-secure-openssl.c | 189 +++++++++++++++++++++++
 src/interfaces/libpq/libpq-int.h         |   2 +
 4 files changed, 264 insertions(+)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 258b09cf8e..bad0d26aa3 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1635,6 +1635,33 @@ 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
+        doesn't support the modern protocol versions.
+       </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 doesn't support newer protocol versions.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-krbsrvname" xreflabel="krbsrvname">
       <term><literal>krbsrvname</literal></term>
       <listitem>
@@ -7021,6 +7048,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>
@@ -7674,6 +7721,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 dcd86ee804..b47498449b 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -316,6 +316,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("tlvs1.x") */ 7,
+	offsetof(struct pg_conn, sslmaxprotocolversion)},
+
 	/*
 	 * Expose gssencmode similarly to sslmode - we can still handle "disable"
 	 * and "prefer".
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index c8dddfb5fd..9d29a9e014 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -93,6 +93,11 @@ static long win32_ssl_create_mutex = 0;
 #endif
 #endif							/* ENABLE_THREAD_SAFETY */
 
+static int ssl_protocol_version_to_openssl(const char *protocol);
+#ifndef SSL_CTX_set_min_proto_version
+static int	SSL_CTX_set_min_proto_version(SSL_CTX *ctx, int version);
+static int	SSL_CTX_set_max_proto_version(SSL_CTX *ctx, int version);
+#endif
 
 /* ------------------------------------------------------------ */
 /*			 Procedures common to all secure sessions			*/
@@ -785,6 +790,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
@@ -821,6 +828,63 @@ 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)
+	{
+		ssl_min_ver = ssl_protocol_version_to_openssl(conn->sslminprotocolversion);
+
+		if (ssl_min_ver == -1)
+		{
+			printfPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("invalid minimum SSL 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 SSL version specified: %s\n"),
+							  err);
+			return -1;
+		}
+	}
+	else
+		ssl_min_ver = INT_MIN;
+
+	if (conn->sslmaxprotocolversion)
+	{
+		ssl_max_ver = ssl_protocol_version_to_openssl(conn->sslmaxprotocolversion);
+
+		if (ssl_max_ver < ssl_min_ver)
+		{
+			printfPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("invalid maximum SSL version specified, must be higher than minimum SSL version: %s\n"),
+							  conn->sslmaxprotocolversion);
+			return -1;
+		}
+
+		if (ssl_max_ver == -1)
+		{
+			printfPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("invalid maximum SSL 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.
@@ -1580,3 +1644,128 @@ my_SSL_set_fd(PGconn *conn, int fd)
 err:
 	return ret;
 }
+
+/*
+ * Convert TLS protocol versionstring 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) || pg_strcasecmp("tlsv1.0", 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;
+}
+
+/*
+ * Replacements for APIs present in newer versions of OpenSSL. This is a copy
+ * of the routines that exist in the backend.
+ */
+#ifndef SSL_CTX_set_min_proto_version
+
+/*
+ * OpenSSL versions that support TLS 1.3 shouldn't get here because they
+ * already have these functions.  So we don't have to keep updating the below
+ * code for every new TLS version, and eventually it can go away.  But let's
+ * just check this to make sure ...
+ */
+#ifdef TLS1_3_VERSION
+#error OpenSSL version mismatch
+#endif
+
+static int
+SSL_CTX_set_min_proto_version(SSL_CTX *ctx, int version)
+{
+	int			ssl_options = SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3;
+
+	if (version > TLS1_VERSION)
+		ssl_options |= SSL_OP_NO_TLSv1;
+	/*
+	 * Some OpenSSL versions define TLS*_VERSION macros but not the
+	 * corresponding SSL_OP_NO_* macro, so in those cases we have to return
+	 * unsuccessfully here.
+	 */
+#ifdef TLS1_1_VERSION
+	if (version > TLS1_1_VERSION)
+	{
+#ifdef SSL_OP_NO_TLSv1_1
+		ssl_options |= SSL_OP_NO_TLSv1_1;
+#else
+		return 0;
+#endif
+	}
+#endif
+#ifdef TLS1_2_VERSION
+	if (version > TLS1_2_VERSION)
+	{
+#ifdef SSL_OP_NO_TLSv1_2
+		ssl_options |= SSL_OP_NO_TLSv1_2;
+#else
+		return 0;
+#endif
+	}
+#endif
+
+	SSL_CTX_set_options(ctx, ssl_options);
+
+	return 1;					/* success */
+}
+
+static int
+SSL_CTX_set_max_proto_version(SSL_CTX *ctx, int version)
+{
+	int			ssl_options = 0;
+
+	AssertArg(version != 0);
+
+	/*
+	 * Some OpenSSL versions define TLS*_VERSION macros but not the
+	 * corresponding SSL_OP_NO_* macro, so in those cases we have to return
+	 * unsuccessfully here.
+	 */
+#ifdef TLS1_1_VERSION
+	if (version < TLS1_1_VERSION)
+	{
+#ifdef SSL_OP_NO_TLSv1_1
+		ssl_options |= SSL_OP_NO_TLSv1_1;
+#else
+		return 0;
+#endif
+	}
+#endif
+#ifdef TLS1_2_VERSION
+	if (version < TLS1_2_VERSION)
+	{
+#ifdef SSL_OP_NO_TLSv1_2
+		ssl_options |= SSL_OP_NO_TLSv1_2;
+#else
+		return 0;
+#endif
+	}
+#endif
+
+	SSL_CTX_set_options(ctx, ssl_options);
+
+	return 1;					/* success */
+}
+
+#endif							/* !SSL_CTX_set_min_proto_version */
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 64468ab4da..cfe9e86471 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -362,6 +362,8 @@ struct pg_conn
 	char	   *sslrootcert;	/* root certificate filename */
 	char	   *sslcrl;			/* certificate revocation list filename */
 	char	   *requirepeer;	/* required peer credentials for local sockets */
+	char	   *sslminprotocolversion;	/* minimum TLS protocol version */
+	char	   *sslmaxprotocolversion;	/* maximum TLS protocol version */
 
 #if defined(ENABLE_GSS) || defined(ENABLE_SSPI)
 	char	   *krbsrvname;		/* Kerberos service name */
-- 
2.21.0 (Apple Git-122.2)

