Add connection parameters to control SSL protocol min/max in libpq

These two new parameters, named sslminprotocolversion and
sslmaxprotocolversion, allow to respectively control the minimum and the
maximum version of the SSL protocol used for the SSL connection attempt.
The default setting is to allow any version for both the minimum and the
maximum bounds, causing libpq to rely on the bounds set by the backend
when negotiating the protocol to use for an SSL connection.  The bounds
are checked when the values are set at the earliest stage possible as
this makes the checks independent of any SSL implementation.

Author: Daniel Gustafsson
Reviewed-by: Michael Paquier, Cary Huang
Discussion: https://postgr.es/m/4F246AE3-A7AE-471E-BD3D-C799D3748E03@yesql.se
This commit is contained in:
Michael Paquier 2020-01-28 10:40:48 +09:00
parent 6f38d4dac3
commit ff8ca5fadd
7 changed files with 284 additions and 2 deletions

View File

@ -8898,7 +8898,7 @@ DO $d$
END; END;
$d$; $d$;
ERROR: invalid option "password" ERROR: invalid option "password"
HINT: Valid options in this context are: service, passfile, channel_binding, connect_timeout, dbname, host, hostaddr, port, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, tcp_user_timeout, sslmode, sslcompression, sslcert, sslkey, sslrootcert, sslcrl, requirepeer, gssencmode, krbsrvname, gsslib, target_session_attrs, use_remote_estimate, fdw_startup_cost, fdw_tuple_cost, extensions, updatable, fetch_size HINT: Valid options in this context are: service, passfile, channel_binding, connect_timeout, dbname, host, hostaddr, port, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, tcp_user_timeout, sslmode, sslcompression, sslcert, sslkey, sslrootcert, sslcrl, requirepeer, sslminprotocolversion, sslmaxprotocolversion, gssencmode, krbsrvname, gsslib, target_session_attrs, use_remote_estimate, fdw_startup_cost, fdw_tuple_cost, extensions, updatable, fetch_size
CONTEXT: SQL statement "ALTER SERVER loopback_nopw OPTIONS (ADD password 'dummypw')" CONTEXT: SQL statement "ALTER SERVER loopback_nopw OPTIONS (ADD password 'dummypw')"
PL/pgSQL function inline_code_block line 3 at EXECUTE PL/pgSQL function inline_code_block line 3 at EXECUTE
-- If we add a password for our user mapping instead, we should get a different -- If we add a password for our user mapping instead, we should get a different

View File

@ -1732,6 +1732,40 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
</listitem> </listitem>
</varlistentry> </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
not supporting the most modern protocol versions. If not set, this
parameter is ignored and the connection will use the minimum bound
defined by the backend.
</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. 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
not supporting the most modern protocol versions. If not set, this
parameter is ignored and the connection will use the maximum bound
defined by the backend, if set. Setting the maximum protocol version
is mainly useful for testing or if some component has issues working
with a newer protocol.
</para>
</listitem>
</varlistentry>
<varlistentry id="libpq-connect-krbsrvname" xreflabel="krbsrvname"> <varlistentry id="libpq-connect-krbsrvname" xreflabel="krbsrvname">
<term><literal>krbsrvname</literal></term> <term><literal>krbsrvname</literal></term>
<listitem> <listitem>
@ -7120,6 +7154,26 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
</para> </para>
</listitem> </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> <listitem>
<para> <para>
<indexterm> <indexterm>

View File

@ -1274,6 +1274,9 @@ X509_NAME_to_cstring(X509_NAME *name)
* version, then we log with the given loglevel and return (if we return) -1. * version, then we log with the given loglevel and return (if we return) -1.
* If a nonnegative value is returned, subsequent code can assume it's working * If a nonnegative value is returned, subsequent code can assume it's working
* with a supported version. * with a supported version.
*
* Note: this is rather similar to libpq's routine in fe-secure-openssl.c,
* so make sure to update both routines if changing this one.
*/ */
static int static int
ssl_protocol_version_to_openssl(int v, const char *guc_name, int loglevel) ssl_protocol_version_to_openssl(int v, const char *guc_name, int loglevel)

View File

@ -320,6 +320,14 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
"Require-Peer", "", 10, "Require-Peer", "", 10,
offsetof(struct pg_conn, requirepeer)}, offsetof(struct pg_conn, requirepeer)},
{"sslminprotocolversion", "PGSSLMINPROTOCOLVERSION", NULL, NULL,
"SSL-Minimum-Protocol-Version", "", 8, /* sizeof("TLSv1.x") == 8 */
offsetof(struct pg_conn, sslminprotocolversion)},
{"sslmaxprotocolversion", "PGSSLMAXPROTOCOLVERSION", NULL, NULL,
"SSL-Maximum-Protocol-Version", "", 8, /* sizeof("TLSv1.x") == 8 */
offsetof(struct pg_conn, sslmaxprotocolversion)},
/* /*
* As with SSL, all GSS options are exposed even in builds that don't have * As with SSL, all GSS options are exposed even in builds that don't have
* support. * support.
@ -426,6 +434,8 @@ static char *passwordFromFile(const char *hostname, const char *port, const char
const char *username, const char *pgpassfile); const char *username, const char *pgpassfile);
static void pgpassfileWarning(PGconn *conn); static void pgpassfileWarning(PGconn *conn);
static void default_threadlock(int acquire); static void default_threadlock(int acquire);
static bool sslVerifyProtocolVersion(const char *version);
static bool sslVerifyProtocolRange(const char *min, const char *max);
/* global variable because fe-auth.c needs to access it */ /* global variable because fe-auth.c needs to access it */
@ -1285,6 +1295,40 @@ connectOptions2(PGconn *conn)
goto oom_error; goto oom_error;
} }
/*
* Validate TLS protocol versions for sslminprotocolversion and
* sslmaxprotocolversion.
*/
if (!sslVerifyProtocolVersion(conn->sslminprotocolversion))
{
printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("invalid sslminprotocolversion value: \"%s\"\n"),
conn->sslminprotocolversion);
return false;
}
if (!sslVerifyProtocolVersion(conn->sslmaxprotocolversion))
{
printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("invalid sslmaxprotocolversion value: \"%s\"\n"),
conn->sslmaxprotocolversion);
return false;
}
/*
* Check if the range of SSL protocols defined is correct. This is done
* at this early step because this is independent of the SSL
* implementation used, and this avoids unnecessary cycles with an
* already-built SSL context when the connection is being established, as
* it would be doomed anyway.
*/
if (!sslVerifyProtocolRange(conn->sslminprotocolversion,
conn->sslmaxprotocolversion))
{
printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("invalid SSL protocol version range"));
return false;
}
/* /*
* validate gssencmode option * validate gssencmode option
*/ */
@ -4001,6 +4045,10 @@ freePGconn(PGconn *conn)
free(conn->sslcompression); free(conn->sslcompression);
if (conn->requirepeer) if (conn->requirepeer)
free(conn->requirepeer); free(conn->requirepeer);
if (conn->sslminprotocolversion)
free(conn->sslminprotocolversion);
if (conn->sslmaxprotocolversion)
free(conn->sslmaxprotocolversion);
if (conn->gssencmode) if (conn->gssencmode)
free(conn->gssencmode); free(conn->gssencmode);
if (conn->krbsrvname) if (conn->krbsrvname)
@ -7031,6 +7079,71 @@ pgpassfileWarning(PGconn *conn)
} }
} }
/*
* Check if the SSL procotol value given in input is valid or not.
* This is used as a sanity check routine for the connection parameters
* sslminprotocolversion and sslmaxprotocolversion.
*/
static bool
sslVerifyProtocolVersion(const char *version)
{
/*
* An empty string and a NULL value are considered valid as it is
* equivalent to ignoring the parameter.
*/
if (!version || strlen(version) == 0)
return true;
if (pg_strcasecmp(version, "TLSv1") == 0 ||
pg_strcasecmp(version, "TLSv1.1") == 0 ||
pg_strcasecmp(version, "TLSv1.2") == 0 ||
pg_strcasecmp(version, "TLSv1.3") == 0)
return true;
/* anything else is wrong */
return false;
}
/*
* Ensure that the SSL protocol range given in input is correct. The check
* is performed on the input string to keep it TLS backend agnostic. Input
* to this function is expected verified with sslVerifyProtocolVersion().
*/
static bool
sslVerifyProtocolRange(const char *min, const char *max)
{
Assert(sslVerifyProtocolVersion(min) &&
sslVerifyProtocolVersion(max));
/* If at least one of the bounds is not set, the range is valid */
if (min == NULL || max == NULL || strlen(min) == 0 || strlen(max) == 0)
return true;
/*
* If the minimum version is the lowest one we accept, then all options
* for the maximum are valid.
*/
if (pg_strcasecmp(min, "TLSv1") == 0)
return true;
/*
* The minimum bound is valid, and cannot be TLSv1, so using TLSv1 for the
* maximum is incorrect.
*/
if (pg_strcasecmp(max, "TLSv1") == 0)
return false;
/*
* At this point we know that we have a mix of TLSv1.1 through 1.3
* versions.
*/
if (pg_strcasecmp(min, max) > 0)
return false;
return true;
}
/* /*
* Obtain user's home directory, return in given buffer * Obtain user's home directory, return in given buffer

View File

@ -30,6 +30,7 @@
#include "fe-auth.h" #include "fe-auth.h"
#include "fe-secure-common.h" #include "fe-secure-common.h"
#include "libpq-int.h" #include "libpq-int.h"
#include "common/openssl.h"
#ifdef WIN32 #ifdef WIN32
#include "win32.h" #include "win32.h"
@ -95,6 +96,7 @@ static long win32_ssl_create_mutex = 0;
#endif /* ENABLE_THREAD_SAFETY */ #endif /* ENABLE_THREAD_SAFETY */
static PQsslKeyPassHook_type PQsslKeyPassHook = NULL; static PQsslKeyPassHook_type PQsslKeyPassHook = NULL;
static int ssl_protocol_version_to_openssl(const char *protocol);
/* ------------------------------------------------------------ */ /* ------------------------------------------------------------ */
/* Procedures common to all secure sessions */ /* Procedures common to all secure sessions */
@ -843,6 +845,59 @@ initialize_SSL(PGconn *conn)
/* Disable old protocol versions */ /* Disable old protocol versions */
SSL_CTX_set_options(SSL_context, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3); SSL_CTX_set_options(SSL_context, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
/* Set the minimum and maximum protocol versions if necessary */
if (conn->sslminprotocolversion &&
strlen(conn->sslminprotocolversion) != 0)
{
int ssl_min_ver;
ssl_min_ver = ssl_protocol_version_to_openssl(conn->sslminprotocolversion);
if (ssl_min_ver == -1)
{
printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("invalid value \"%s\" for minimum version of SSL protocol\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("could not set minimum version of SSL protocol: %s\n"),
err);
return -1;
}
}
if (conn->sslmaxprotocolversion &&
strlen(conn->sslmaxprotocolversion) != 0)
{
int ssl_max_ver;
ssl_max_ver = ssl_protocol_version_to_openssl(conn->sslmaxprotocolversion);
if (ssl_max_ver == -1)
{
printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("invalid value \"%s\" for maximum version of SSL protocol\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("could not set maximum version of SSL protocol: %s\n"),
err);
return -1;
}
}
/* /*
* Disable OpenSSL's moving-write-buffer sanity check, because it causes * Disable OpenSSL's moving-write-buffer sanity check, because it causes
* unnecessary failures in nonblocking send cases. * unnecessary failures in nonblocking send cases.
@ -1659,3 +1714,37 @@ PQssl_passwd_cb(char *buf, int size, int rwflag, void *userdata)
else else
return PQdefaultSSLKeyPassHook(buf, size, conn); 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 non-negative value is returned, subsequent code can
* assume it is working with a supported version.
*
* Note: this is rather similar to the backend routine in be-secure-openssl.c,
* so make sure to update both routines if changing this one.
*/
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;
}

View File

@ -367,6 +367,8 @@ struct pg_conn
char *krbsrvname; /* Kerberos service name */ char *krbsrvname; /* Kerberos service name */
char *gsslib; /* What GSS library to use ("gssapi" or char *gsslib; /* What GSS library to use ("gssapi" or
* "sspi") */ * "sspi") */
char *sslminprotocolversion; /* minimum TLS protocol version */
char *sslmaxprotocolversion; /* maximum TLS protocol version */
/* Type of connection to make. Possible values: any, read-write. */ /* Type of connection to make. Possible values: any, read-write. */
char *target_session_attrs; char *target_session_attrs;

View File

@ -13,7 +13,7 @@ use SSLServer;
if ($ENV{with_openssl} eq 'yes') if ($ENV{with_openssl} eq 'yes')
{ {
plan tests => 86; plan tests => 93;
} }
else else
{ {
@ -356,6 +356,27 @@ command_like(
^\d+,t,TLSv[\d.]+,[\w-]+,\d+,f,_null_,_null_,_null_\r?$}mx, ^\d+,t,TLSv[\d.]+,[\w-]+,\d+,f,_null_,_null_,_null_\r?$}mx,
'pg_stat_ssl view without client certificate'); 'pg_stat_ssl view without client certificate');
# Test min/max SSL protocol versions.
test_connect_ok(
$common_connstr,
"sslrootcert=ssl/root+server_ca.crt sslmode=require sslminprotocolversion=TLSv1.2 sslmaxprotocolversion=TLSv1.2",
"connection success with correct range of TLS protocol versions");
test_connect_fails(
$common_connstr,
"sslrootcert=ssl/root+server_ca.crt sslmode=require sslminprotocolversion=TLSv1.2 sslmaxprotocolversion=TLSv1.1",
qr/invalid SSL protocol version range/,
"connection failure with incorrect range of TLS protocol versions");
test_connect_fails(
$common_connstr,
"sslrootcert=ssl/root+server_ca.crt sslmode=require sslminprotocolversion=incorrect_tls",
qr/invalid sslminprotocolversion value/,
"connection failure with an incorrect SSL protocol minimum bound");
test_connect_fails(
$common_connstr,
"sslrootcert=ssl/root+server_ca.crt sslmode=require sslmaxprotocolversion=incorrect_tls",
qr/invalid sslmaxprotocolversion value/,
"connection failure with an incorrect SSL protocol maximum bound");
### Server-side tests. ### Server-side tests.
### ###
### Test certificate authorization. ### Test certificate authorization.