libpq_minmaxproto_v5.patch
application/octet-stream
Filename: libpq_minmaxproto_v5.patch
Type: application/octet-stream
Part: 0
Patch
Format: format-patch
Series: patch v5
Subject: Allow setting min/max TLS protocol version in libpq
| File | + | − |
|---|---|---|
| doc/src/sgml/libpq.sgml | 67 | 0 |
| src/interfaces/libpq/fe-connect.c | 44 | 0 |
| src/interfaces/libpq/fe-secure.c | 50 | 0 |
| src/interfaces/libpq/fe-secure-openssl.c | 81 | 0 |
| src/interfaces/libpq/libpq-int.h | 4 | 0 |
| src/test/ssl/t/001_ssltests.pl | 25 | 1 |
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)