From ce450afe422080264c6b395f183f1330c828f404 Mon Sep 17 00:00:00 2001 From: Daniel Gustafsson 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 + + sslminprotocolversion + + + This parameter specifies the minimum SSL/TLS protocol version to allow + for the connection. Valid values are TLSv1, + TLSv1.1, TLSv1.2 and + TLSv1.3. The supported protocols depend on the + version of OpenSSL used, older versions + don't support the modern protocol versions. If not set, the system + wide default configuration is used. + + + + + + sslmaxprotocolversion + + + This parameter specifies the maximum SSL/TLS protocol version to allow + for the connection. The supported values are the same as for + sslminprotocolversion. 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. + + + + krbsrvname @@ -7120,6 +7149,26 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough) + + + + PGSSLMINPROTOCOLVERSION + + PGSSLMINPROTOCOLVERSION behaves the same as the connection parameter. + + + + + + + PGSSLMAXPROTOCOLVERSION + + PGSSLMAXPROTOCOLVERSION behaves the same as the connection parameter. + + + @@ -7793,6 +7842,24 @@ ldap://ldap.acme.com/cn=dbserver,cn=hosts?pgconnectinfo?base?(objectclass=*) + + Client Protocol Usage + + + When connecting using SSL, the client and server negotiate which protocol + to use for the connection. PostgreSQL supports + TLSv1, TLSv1.1, TLSv1.2 + and TLSv1.3, but the protocols available depends on the + version of OpenSSL which the client is using. + The minimum requested version can be specified with sslminprotocolversion, + which will ensure that the connection use that version, or higher, or fails. + The maximum requested version can be specified with sslmaxprotocolversion, + but this is mainly only useful for testing, or in case a component doesn't + work with a newer protocol. + + + + SSL Client File Usage 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)