From abb27a9205a0b111b879d5d0efd6d73a474c74bc Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
Date: Mon, 9 Sep 2024 13:04:58 +0200
Subject: [PATCH v5 2/3] Support configuring multiple ECDH curves

The ssl_ecdh_curve only GUC accepts a single value, but the TLS
handshake can list multiple curves in the groups extension (the
extension has been renamed to contain more than elliptic curves).
This changes the GUC to accept a colon-separated list of curves.
This commit also renames the GUC to ssl_groups to match the new
nomenclature for the TLS extension.

Original patch by Erica Zhang with additional hacking by me.

Author: Erica Zhang <ericazhangy2021@qq.com>
Author: Daniel Gustafsson <daniel@yesql.se>
Reviewed-by: Jacob Champion <jacob.champion@enterprisedb.com>
Reviewed-by: Andres Freund <andres@anarazel.de>
Reviewed-by: Peter Eisentraut <peter@eisentraut.org>
Reviewed-by: Jelte Fennema-Nio <postgres@jeltef.nl>
Discussion: https://postgr.es/m/tencent_063F89FA72CCF2E48A0DF5338841988E9809@qq.com
---
 doc/src/sgml/config.sgml                      | 20 ++++++----
 src/backend/libpq/be-secure-openssl.c         | 37 +++++++++----------
 src/backend/utils/misc/guc.c                  |  1 +
 src/backend/utils/misc/guc_tables.c           |  6 +--
 src/backend/utils/misc/postgresql.conf.sample |  2 +-
 src/test/ssl/t/SSL/Server.pm                  |  3 ++
 6 files changed, 38 insertions(+), 31 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 0aec11f443..25f60dfeef 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1452,20 +1452,20 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
-     <varlistentry id="guc-ssl-ecdh-curve" xreflabel="ssl_ecdh_curve">
-      <term><varname>ssl_ecdh_curve</varname> (<type>string</type>)
+     <varlistentry id="guc-ssl-groups" xreflabel="ssl_groups">
+      <term><varname>ssl_groups</varname> (<type>string</type>)
       <indexterm>
-       <primary><varname>ssl_ecdh_curve</varname> configuration parameter</primary>
+       <primary><varname>ssl_groups</varname> configuration parameter</primary>
       </indexterm>
       </term>
       <listitem>
        <para>
         Specifies the name of the curve to use in <acronym>ECDH</acronym> key
         exchange.  It needs to be supported by all clients that connect.
+        Multiple curves can be specified by using a colon-separated list.
         It does not need to be the same curve used by the server's Elliptic
-        Curve key.
-        This parameter can only be set in the <filename>postgresql.conf</filename>
-        file or on the server command line.
+        Curve key.  This parameter can only be set in the
+        <filename>postgresql.conf</filename> file or on the server command line.
         The default is <literal>prime256v1</literal>.
        </para>
 
@@ -1477,7 +1477,13 @@ include_dir 'conf.d'
         <literal>secp521r1</literal> (NIST P-521).
         The full list of available curves can be shown with the command
         <command>openssl ecparam -list_curves</command>.  Not all of them
-        are usable in <acronym>TLS</acronym> though.
+        are usable with <acronym>TLS</acronym> though.
+       </para>
+
+       <para>
+        In <productname>PostgreSQL</productname> versions before 18.0 this
+        setting was named <literal>ssl_ecdh_curve</literal> and only accepted
+        a single value.
        </para>
       </listitem>
      </varlistentry>
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 1ebd3f2e6d..72da8f72dc 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -1392,30 +1392,27 @@ static bool
 initialize_ecdh(SSL_CTX *context, bool isServerStart)
 {
 #ifndef OPENSSL_NO_ECDH
-	EC_KEY	   *ecdh;
-	int			nid;
-
-	nid = OBJ_sn2nid(SSLECDHCurve);
-	if (!nid)
-	{
-		ereport(isServerStart ? FATAL : LOG,
-				(errcode(ERRCODE_CONFIG_FILE_ERROR),
-				 errmsg("ECDH: unrecognized curve name: %s", SSLECDHCurve)));
-		return false;
-	}
-
-	ecdh = EC_KEY_new_by_curve_name(nid);
-	if (!ecdh)
+	if (SSL_CTX_set1_groups_list(context, SSLECDHCurve) != 1)
 	{
 		ereport(isServerStart ? FATAL : LOG,
-				(errcode(ERRCODE_CONFIG_FILE_ERROR),
-				 errmsg("ECDH: could not create key")));
+				errcode(ERRCODE_CONFIG_FILE_ERROR),
+#if (OPENSSL_VERSION_NUMBER <= 0x30200000L)
+				/*
+				 * OpenSSL 3.3.0 introduced proper error messages for group
+				 * parsing errors, earlier versions returns "no SSL error
+				 * reported" which is far from helpful. For older versions, we
+				 * manually set a better error message. Injecting the error
+				 * into the OpenSSL error queue need APIs from OpenSSL 3.0.
+				 */
+				errmsg("ECDH: failed to set curve names: No valid groups in '%s'",
+					   SSLECDHCurve),
+#else
+				errmsg("ECDH: failed to set curve names: %s",
+					   SSLerrmessage(ERR_get_error())),
+#endif
+				errhint("Ensure that each group name is spelled correctly and supported by the installed version of OpenSSL"));
 		return false;
 	}
-
-	SSL_CTX_set_options(context, SSL_OP_SINGLE_ECDH_USE);
-	SSL_CTX_set_tmp_ecdh(context, ecdh);
-	EC_KEY_free(ecdh);
 #endif
 
 	return true;
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 13527fc258..c4271a0951 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -190,6 +190,7 @@ static const unit_conversion time_unit_conversion_table[] =
 static const char *const map_old_guc_names[] = {
 	"sort_mem", "work_mem",
 	"vacuum_mem", "maintenance_work_mem",
+	"ssl_ecdh_curve", "ssl_groups",
 	NULL
 };
 
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 686309db58..620e318e94 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -4656,9 +4656,9 @@ struct config_string ConfigureNamesString[] =
 	},
 
 	{
-		{"ssl_ecdh_curve", PGC_SIGHUP, CONN_AUTH_SSL,
-			gettext_noop("Sets the curve to use for ECDH."),
-			NULL,
+		{"ssl_groups", PGC_SIGHUP, CONN_AUTH_SSL,
+			gettext_noop("Sets the curve(s) to use for ECDH."),
+			gettext_noop("Multiple curves can be specified using colon-separated list."),
 			GUC_SUPERUSER_ONLY
 		},
 		&SSLECDHCurve,
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 667e0dc40a..204eab5f1a 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -112,7 +112,7 @@
 #ssl_key_file = 'server.key'
 #ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL'	# allowed SSL ciphers
 #ssl_prefer_server_ciphers = on
-#ssl_ecdh_curve = 'prime256v1'
+#ssl_groups = 'prime256v1'
 #ssl_min_protocol_version = 'TLSv1.2'
 #ssl_max_protocol_version = ''
 #ssl_dh_params_file = ''
diff --git a/src/test/ssl/t/SSL/Server.pm b/src/test/ssl/t/SSL/Server.pm
index de06f6f242..6575f71a80 100644
--- a/src/test/ssl/t/SSL/Server.pm
+++ b/src/test/ssl/t/SSL/Server.pm
@@ -300,6 +300,9 @@ sub switch_server_cert
 	ok(unlink($node->data_dir . '/sslconfig.conf'));
 	$node->append_conf('sslconfig.conf', "ssl=on");
 	$node->append_conf('sslconfig.conf', $backend->set_server_cert(\%params));
+	# use lists of ECDH curves for syntax testing
+	$node->append_conf('sslconfig.conf', 'ssl_ecdh_curve=prime256v1:secp521r1');
+
 	$node->append_conf('sslconfig.conf',
 		"ssl_passphrase_command='" . $params{passphrase_cmd} . "'")
 	  if defined $params{passphrase_cmd};
-- 
2.39.3 (Apple Git-146)

