Improve errors when setting incorrect bounds for SSL protocols

Michael Paquier <[email protected]> Tue, 14 Jan 2020 12:54:20 +0900
Newsgroups gmane.comp.db.postgresql.devel.general
Message-ID <[email protected]>
Hi all,
(Daniel G. in CC.)

As discussed on the thread to be able to set the min/max SSL protocols
with libpq, when mixing incorrect bounds the user experience is not
that good: 
https://www.postgresql.org/message-id/[email protected]

It happens that the error generated with incorrect combinations
depends solely on what OpenSSL thinks is fine, and that's the
following:
psql: error: could not connect to server: SSL error: tlsv1 alert
internal error

It is hard for users to understand what such an error means and how to
act on it. 

Please note that OpenSSL 1.1.0 has added two routines to be able to
get the min/max protocols set in a context, called
SSL_CTX_get_min/max_proto_version.  Thinking about older versions of
OpenSSL I think that it is better to use
ssl_protocol_version_to_openssl to do the parsing work.  I also found
that it is easier to check for compatible versions after setting both
bounds in the SSL context, so as there is no need to worry about
invalid values depending on the build of OpenSSL used.

So attached is a patch to improve the detection of incorrect
combinations.  Once applied, we get a complain about an incorrect
version at server startup (FATAL) or reload (LOG).  The patch includes
new regression tests.

Thoughts?
--
Michael
ssl-proto-context-v1.patch (text/x-diff, 5.2 KB)
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 62f1fcab2b..75fdb2e91b 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -67,8 +67,7 @@ static bool SSL_initialized = false;
 static bool dummy_ssl_passwd_cb_called = false;
 static bool ssl_is_server_start;
 
-static int	ssl_protocol_version_to_openssl(int v, const char *guc_name,
-											int loglevel);
+static int	ssl_protocol_version_to_openssl(int v);
 #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);
@@ -84,6 +83,8 @@ be_tls_init(bool isServerStart)
 {
 	STACK_OF(X509_NAME) *root_cert_list = NULL;
 	SSL_CTX    *context;
+	int			ssl_ver_min = -1;
+	int			ssl_ver_max = -1;
 
 	/* This stuff need be done only once. */
 	if (!SSL_initialized)
@@ -192,13 +193,19 @@ be_tls_init(bool isServerStart)
 
 	if (ssl_min_protocol_version)
 	{
-		int			ssl_ver = ssl_protocol_version_to_openssl(ssl_min_protocol_version,
-															  "ssl_min_protocol_version",
-															  isServerStart ? FATAL : LOG);
+		ssl_ver_min = ssl_protocol_version_to_openssl(ssl_min_protocol_version);
 
-		if (ssl_ver == -1)
+		if (ssl_ver_min == -1)
+		{
+			ereport(isServerStart ? FATAL : LOG,
+					(errmsg("%s setting %s not supported by this build",
+							"ssl_min_protocol_version",
+							GetConfigOption("ssl_min_protocol_version",
+											false, false))));
 			goto error;
-		if (!SSL_CTX_set_min_proto_version(context, ssl_ver))
+		}
+
+		if (!SSL_CTX_set_min_proto_version(context, ssl_ver_min))
 		{
 			ereport(isServerStart ? FATAL : LOG,
 					(errmsg("could not set minimum SSL protocol version")));
@@ -208,13 +215,19 @@ be_tls_init(bool isServerStart)
 
 	if (ssl_max_protocol_version)
 	{
-		int			ssl_ver = ssl_protocol_version_to_openssl(ssl_max_protocol_version,
-															  "ssl_max_protocol_version",
-															  isServerStart ? FATAL : LOG);
+		ssl_ver_max = ssl_protocol_version_to_openssl(ssl_max_protocol_version);
 
-		if (ssl_ver == -1)
+		if (ssl_ver_max == -1)
+		{
+			ereport(isServerStart ? FATAL : LOG,
+					(errmsg("%s setting %s not supported by this build",
+							"ssl_max_protocol_version",
+							GetConfigOption("ssl_max_protocol_version",
+											false, false))));
 			goto error;
-		if (!SSL_CTX_set_max_proto_version(context, ssl_ver))
+		}
+
+		if (!SSL_CTX_set_max_proto_version(context, ssl_ver_max))
 		{
 			ereport(isServerStart ? FATAL : LOG,
 					(errmsg("could not set maximum SSL protocol version")));
@@ -222,6 +235,20 @@ be_tls_init(bool isServerStart)
 		}
 	}
 
+	/* Check compatibility of min/max protocols */
+	if (ssl_min_protocol_version &&
+		ssl_max_protocol_version)
+	{
+		/*
+		 * No need to check for invalid values (-1) for each protocol number
+		 * as the code above would have already generated an error.
+		 */
+		if (ssl_ver_min > ssl_ver_max)
+			ereport(isServerStart ? FATAL : LOG,
+					(errmsg("incompatible min/max SSL protocol versions set")));
+		goto error;
+	}
+
 	/* disallow SSL session tickets */
 	SSL_CTX_set_options(context, SSL_OP_NO_TICKET);
 
@@ -1275,12 +1302,11 @@ X509_NAME_to_cstring(X509_NAME *name)
  * guc.c independent of OpenSSL availability and version.
  *
  * If a version is passed that is not supported by the current OpenSSL
- * 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
- * with a supported version.
+ * 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(int v, const char *guc_name, int loglevel)
+ssl_protocol_version_to_openssl(int v)
 {
 	switch (v)
 	{
@@ -1308,10 +1334,6 @@ ssl_protocol_version_to_openssl(int v, const char *guc_name, int loglevel)
 #endif
 	}
 
-	ereport(loglevel,
-			(errmsg("%s setting %s not supported by this build",
-					guc_name,
-					GetConfigOption(guc_name, false, false))));
 	return -1;
 }
 
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 83fcd5e839..7931ebc067 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 => 84;
+	plan tests => 86;
 }
 else
 {
@@ -97,6 +97,22 @@ command_ok(
 	'restart succeeds with password-protected key file');
 $node->_update_pid(1);
 
+# Test compatibility of SSL protocols.
+# TLSv1.1 is lower than TLSv1.2, so it won't work.
+$node->append_conf('postgresql.conf',
+		   qq{ssl_min_protocol_version='TLSv1.2'
+ssl_max_protocol_version='TLSv1.1'});
+command_fails(
+	[ 'pg_ctl', '-D', $node->data_dir, '-l', $node->logfile, 'restart' ],
+	'restart fails with incorrect SSL protocol bounds');
+# Go back to the defaults, this works.
+$node->append_conf('postgresql.conf',
+		   qq{ssl_min_protocol_version='TLSv1.2'
+ssl_max_protocol_version=''});
+command_ok(
+	[ 'pg_ctl', '-D', $node->data_dir, '-l', $node->logfile, 'restart' ],
+	'restart succeeds with correct SSL protocol bounds');
+
 ### Run client-side tests.
 ###
 ### Test that libpq accepts/rejects the connection correctly, depending
signature.asc (application/pgp-signature, 833 B)
-----BEGIN PGP SIGNATURE-----

iQIzBAABCgAdFiEEG72nH6vTowiyblFKnvQgOdbyQH0FAl4dO2wACgkQnvQgOdby
QH3zKA//f4//WFrO/luVIsOcwh32WmM4RMr6qqVIERwmMNHXs4Et3fKdlL09889S
FcAyI5pex9HjU+YehYLoK5VSYzSZxny2p9XuiBbHBS0B8Uw7bWFK8d0efa2WZeKs
cpxlo8FgMzjuBtuMuZNkvhXOHhQZFdnHTLkYjbFTG5PwhiqpoH/dgtj9H2e5ScDq
vAXhkWZxAgDBgTUl8617qQxiBnpPWk7C0h5lLFh9oWLkNIejNijOtyv/B7slutZU
wl102x46Xy511tb4lm0HFBQO9C0YgZkwkZpPVPVbFaE/Y0GMjRokQZMNoAieBxxE
rUE+zhlMXoEJ5udFX5M6clc88nioYOMsGsHkvJ3Wm8AA29LGLFx7xDizEtNj9YBg
dQ9nPKUDUiaa2XPIqqKZArVxPk80ZWULFEIcX1mPfZ8ZBSKV+u7B3qbNs0UGl+rt
kGdHMj6isXfP5osSOoVAnpXqDlaLi97eAFO3rgX0G6fi6EVqgCgFyyrzciydtOLU
OhQcOY2kDXv+HvRuO/4kYDAqZ44Esd86InEAYpZ7Ge+qQfuH5NjzyDUk9WITKXQk
Z6wGm/yEw2ZTUKN/UkqVKAqa1NeWvo2khqR8fh9tmjm8v8bS9N+tdAoF0vjDCkDQ
N5k7NME96/j5aXRYQT/N8+A3cKDVSaNZKsXgk6BDwscqSJGlxH4=
=s/gM
-----END PGP SIGNATURE-----