To: [email protected]
Subject: Re: [PATCH v2] Add ssl_alt_cert_file/ssl_alt_key_file for dual RSA+ECDSA certificate support
In-Reply-To: <[email protected]>

Hi Zsolt,

Thanks for the thorough review.  Here is v2 addressing all six issues
you raised.  The fixes are in separate commits so each one can be
reviewed independently:

v2-0001: original patch (unchanged from v1)
v2-0002: reject alternate certificate with same key type as primary (#5)
v2-0003: load full certificate chain for alternate certificate (#4)
v2-0004: load alternate certificates only for the default host context (#2)
v2-0005: add compatibility guards for LibreSSL and older OpenSSL (#6)
v2-0006: document ssl_alt_cert_file limitations (#3)
v2-0007: fix TLS 1.3 HelloRetryRequest with multiple certificate types (#1)
v2-0008: expand test coverage (same-type rejection, SIGHUP reload,
         single-cert regression, TLS 1.3 actually exercised)

Specific answers:

1) TLS 1.3 HelloRetryRequest: the root cause was using override=0 for
   the second certificate.  During HRR the ClientHello callback fires
   twice; on the second invocation the key type was already loaded from
   the first, so SSL_use_cert_and_key() refused to replace it. Fixed
   by always using override=1 — since we load a complete set from the
   SSL_CTX, replacing is always correct.  Added a TLS 1.3 connectivity
   test and removed the TLS 1.2 protocol restriction.

2) Global GUCs leaking into SNI contexts: added an is_default parameter
   to init_host_context().  Alt certs are now loaded only for the
   default host context (postgresql.conf), not for per-host entries from
   pg_hosts.conf.

3) Per-host SNI support: documented that ssl_alt_cert_file applies only
   to the default SSL configuration from postgresql.conf.

4) Full chain loading: switched from SSL_CTX_use_certificate_file() to
   SSL_CTX_use_certificate_chain_file(), matching how the primary
   certificate is loaded.

5) Same-type detection: after loading the alt cert, we iterate with
   SSL_CTX_set_current_cert(FIRST/NEXT) and compare
   EVP_PKEY_get_base_id() on both.  Produces a FATAL error if they
   match.

6) LibreSSL/older OpenSSL: guarded SSL_CTX_set_current_cert() and
   SSL_CERT_SET_FIRST/NEXT with #ifdef SSL_CERT_SET_FIRST.  When not
   available, ssl_alt_cert_file produces a clear error, ssl_update_ssl()
   falls back to the original single-cert copy, and cert types caching
   is skipped.  Verified by building with #undef SSL_CERT_SET_FIRST to
   exercise the fallback code paths.

Test results: 452/452 SSL tests pass (29 alt cert subtests including
TLS 1.3, same-type rejection, SIGHUP reload add/remove, single-cert
regression, plus the existing 423), no regressions.  RHEL 9.8 /
OpenSSL 3.5.5.

---

Looking ahead, I want to flag an alternative design direction worth
considering.  The ssl_alt_cert_file approach is inherently limited to
two certificates (primary + one alternate), but OpenSSL supports up to
three key types (RSA, ECDSA, EdDSA).  It also introduces new GUC names
that don't generalize well.

An approach that other projects have converged on is to make
ssl_cert_file and ssl_key_file multi-valued — httpd and nginx already
work this way, and I am doing the same for MariaDB [1] where we went
through a similar evolution: we started with --ssl-alt-cert, then
redesigned to allow repeated --ssl-cert/--ssl-key options and let
OpenSSL handle cert/key matching and type verification internally.

For PostgreSQL, since GUCs are strings, this could take the form of
comma-separated paths:

    ssl_cert_file = 'server-rsa.crt, server-ecdsa.crt'
    ssl_key_file  = 'server-rsa.key, server-ecdsa.key'

The server would load each pair via
SSL_CTX_use_certificate_chain_file() / SSL_CTX_use_PrivateKey_file()
and let OpenSSL sort out the type matching — no same-type detection
needed, no "alt" naming, and natural support for all three key types.
The ssl_update_ssl() fix from this patch (iterating all cert types)
would still be needed regardless.

I'm happy to go either direction — the current v2 is functional and
complete, but if the community prefers the multi-valued approach, I
can rework it.

[1] https://github.com/MariaDB/server/pull/5178

Thanks,
Renaud Métrich
Red Hat

Le 12/06/2026 à 10:39 PM, Zsolt Parragi a écrit :
Hello!

The problem the patch tries to solve is real, but I see several
gaps/problems with the current implementation with some testing:

1. it seems to break TLS 1.3 HelloRetryRequest as it tries to add the
second certificate with override=0. Connection then fails with "SSL
error: tlsv1 alert internal error", server log shows "could not update
certificate chain: not replacing certificate" / "failed to switch to
SSL configuration for host, terminating connection"

2. The global ssl_alt_* GUCs are loaded into every pg_hosts context.
If the SNI cert is a different type, it loads the alternative
certificates as alternatives, if it's the same type, it replaces the
hosts entry.

3. pg_hosts/SNI has no support for the new GUCs, there's no way to
configure per host versions of the feature. Shouldn't the patch
include proper support for SNI?

4. Shouldn't alternative certificates load the entire chain, not just
the first block?

5. If both have the same type, the alternate certificate silently
replaces the primary one. Shouldn't that result in a startup error
instead?

6. Won't this cause build failure with LibreSSL, or older OpenSSL?


From a230d54069b1615520ce2c5c78731041076ff1c1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Renaud=20M=C3=A9trich?= <[email protected]>
Date: Mon, 8 Jun 2026 17:38:32 +0200
Subject: [PATCH v2 1/8] Add ssl_alt_cert_file/ssl_alt_key_file for dual
 RSA+ECDSA certificate support
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Add two new GUC parameters (ssl_alt_cert_file, ssl_alt_key_file) that allow
loading an alternate SSL certificate alongside the primary one.  OpenSSL
selects the appropriate certificate during the TLS handshake based on the
negotiated cipher suite.

Fix ssl_update_ssl() to iterate all certificate types in the SSL_CTX
using SSL_CTX_set_current_cert(FIRST/NEXT) and copy each to the
per-connection SSL object, preserving multi-cert state across the SNI
architecture.

Add ssl_server_cert_type()/ssl_server_cert_types() to the sslinfo
extension (v1.3) for observability of which certificate types are loaded
and which was selected for the current connection.

Author: Renaud Métrich <[email protected]>
---
 contrib/sslinfo/Makefile                      |   2 +-
 contrib/sslinfo/meson.build                   |   2 +
 contrib/sslinfo/sslinfo--1.2--1.3.sql         |  12 ++
 contrib/sslinfo/sslinfo--1.3.sql              |  56 +++++
 contrib/sslinfo/sslinfo.c                     |  42 ++++
 contrib/sslinfo/sslinfo.control               |   2 +-
 doc/src/sgml/config.sgml                      |  43 +++-
 doc/src/sgml/runtime.sgml                     |  13 ++
 doc/src/sgml/sslinfo.sgml                     |  34 +++
 src/backend/libpq/be-secure-openssl.c         | 199 ++++++++++++++++--
 src/backend/libpq/be-secure.c                 |   2 +
 src/backend/utils/misc/guc_parameters.dat     |  12 ++
 src/backend/utils/misc/postgresql.conf.sample |   2 +
 src/include/libpq/libpq-be.h                  |   2 +
 src/include/libpq/libpq.h                     |   2 +
 src/test/ssl/t/004_ssl_alt_cert.pl            | 188 +++++++++++++++++
 16 files changed, 591 insertions(+), 22 deletions(-)
 create mode 100644 contrib/sslinfo/sslinfo--1.2--1.3.sql
 create mode 100644 contrib/sslinfo/sslinfo--1.3.sql
 create mode 100644 src/test/ssl/t/004_ssl_alt_cert.pl

diff --git a/contrib/sslinfo/Makefile b/contrib/sslinfo/Makefile
index 14305594e2d..2d3ee10bf1e 100644
--- a/contrib/sslinfo/Makefile
+++ b/contrib/sslinfo/Makefile
@@ -6,7 +6,7 @@ OBJS = \
 	sslinfo.o
 
 EXTENSION = sslinfo
-DATA = sslinfo--1.2.sql sslinfo--1.1--1.2.sql sslinfo--1.0--1.1.sql
+DATA = sslinfo--1.3.sql sslinfo--1.2--1.3.sql sslinfo--1.2.sql sslinfo--1.1--1.2.sql sslinfo--1.0--1.1.sql
 PGFILEDESC = "sslinfo - information about client SSL certificate"
 
 ifdef USE_PGXS
diff --git a/contrib/sslinfo/meson.build b/contrib/sslinfo/meson.build
index 6e9cb96430a..e3c578415de 100644
--- a/contrib/sslinfo/meson.build
+++ b/contrib/sslinfo/meson.build
@@ -25,6 +25,8 @@ contrib_targets += sslinfo
 install_data(
   'sslinfo--1.0--1.1.sql',
   'sslinfo--1.1--1.2.sql',
+  'sslinfo--1.3.sql',
+  'sslinfo--1.2--1.3.sql',
   'sslinfo--1.2.sql',
   'sslinfo.control',
   kwargs: contrib_data_args,
diff --git a/contrib/sslinfo/sslinfo--1.2--1.3.sql b/contrib/sslinfo/sslinfo--1.2--1.3.sql
new file mode 100644
index 00000000000..87af04b34de
--- /dev/null
+++ b/contrib/sslinfo/sslinfo--1.2--1.3.sql
@@ -0,0 +1,12 @@
+/* contrib/sslinfo/sslinfo--1.2--1.3.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION sslinfo UPDATE TO '1.3'" to load this file. \quit
+
+CREATE FUNCTION ssl_server_cert_type() RETURNS text
+AS 'MODULE_PATHNAME', 'ssl_server_cert_type'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+
+CREATE FUNCTION ssl_server_cert_types() RETURNS text
+AS 'MODULE_PATHNAME', 'ssl_server_cert_types'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
diff --git a/contrib/sslinfo/sslinfo--1.3.sql b/contrib/sslinfo/sslinfo--1.3.sql
new file mode 100644
index 00000000000..55c53137ff0
--- /dev/null
+++ b/contrib/sslinfo/sslinfo--1.3.sql
@@ -0,0 +1,56 @@
+/* contrib/sslinfo/sslinfo--1.3.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION sslinfo" to load this file. \quit
+
+CREATE FUNCTION ssl_client_serial() RETURNS numeric
+AS 'MODULE_PATHNAME', 'ssl_client_serial'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+
+CREATE FUNCTION ssl_is_used() RETURNS boolean
+AS 'MODULE_PATHNAME', 'ssl_is_used'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+
+CREATE FUNCTION ssl_version() RETURNS text
+AS 'MODULE_PATHNAME', 'ssl_version'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+
+CREATE FUNCTION ssl_cipher() RETURNS text
+AS 'MODULE_PATHNAME', 'ssl_cipher'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+
+CREATE FUNCTION ssl_client_cert_present() RETURNS boolean
+AS 'MODULE_PATHNAME', 'ssl_client_cert_present'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+
+CREATE FUNCTION ssl_client_dn_field(text) RETURNS text
+AS 'MODULE_PATHNAME', 'ssl_client_dn_field'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+
+CREATE FUNCTION ssl_issuer_field(text) RETURNS text
+AS 'MODULE_PATHNAME', 'ssl_issuer_field'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+
+CREATE FUNCTION ssl_client_dn() RETURNS text
+AS 'MODULE_PATHNAME', 'ssl_client_dn'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+
+CREATE FUNCTION ssl_issuer_dn() RETURNS text
+AS 'MODULE_PATHNAME', 'ssl_issuer_dn'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+
+CREATE FUNCTION
+ssl_extension_info(OUT name text,
+    OUT value text,
+    OUT critical boolean
+) RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'ssl_extension_info'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+
+CREATE FUNCTION ssl_server_cert_type() RETURNS text
+AS 'MODULE_PATHNAME', 'ssl_server_cert_type'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
+
+CREATE FUNCTION ssl_server_cert_types() RETURNS text
+AS 'MODULE_PATHNAME', 'ssl_server_cert_types'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
diff --git a/contrib/sslinfo/sslinfo.c b/contrib/sslinfo/sslinfo.c
index c4ae847880d..96b2907312a 100644
--- a/contrib/sslinfo/sslinfo.c
+++ b/contrib/sslinfo/sslinfo.c
@@ -474,3 +474,45 @@ ssl_extension_info(PG_FUNCTION_ARGS)
 	/* All done */
 	SRF_RETURN_DONE(funcctx);
 }
+
+
+/*
+ * Returns the key type of the server certificate used in the current
+ * SSL connection (e.g. "RSA", "ECDSA", "EdDSA").
+ */
+PG_FUNCTION_INFO_V1(ssl_server_cert_type);
+Datum
+ssl_server_cert_type(PG_FUNCTION_ARGS)
+{
+	const char *cert_type;
+
+	if (!MyProcPort->ssl_in_use)
+		PG_RETURN_NULL();
+
+	cert_type = be_tls_get_server_cert_type(MyProcPort);
+	if (cert_type == NULL)
+		PG_RETURN_NULL();
+
+	PG_RETURN_TEXT_P(cstring_to_text(cert_type));
+}
+
+
+/*
+ * Returns a comma-separated list of all certificate key types loaded
+ * by the server (e.g. "RSA, ECDSA").
+ */
+PG_FUNCTION_INFO_V1(ssl_server_cert_types);
+Datum
+ssl_server_cert_types(PG_FUNCTION_ARGS)
+{
+	const char *cert_types;
+
+	if (!MyProcPort->ssl_in_use)
+		PG_RETURN_NULL();
+
+	cert_types = be_tls_get_server_cert_types();
+	if (cert_types == NULL || cert_types[0] == '\0')
+		PG_RETURN_NULL();
+
+	PG_RETURN_TEXT_P(cstring_to_text(cert_types));
+}
diff --git a/contrib/sslinfo/sslinfo.control b/contrib/sslinfo/sslinfo.control
index c7754f924cf..b53e95b7da8 100644
--- a/contrib/sslinfo/sslinfo.control
+++ b/contrib/sslinfo/sslinfo.control
@@ -1,5 +1,5 @@
 # sslinfo extension
 comment = 'information about SSL certificates'
-default_version = '1.2'
+default_version = '1.3'
 module_pathname = '$libdir/sslinfo'
 relocatable = true
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index fa566c9e553..4d712ed852b 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1327,7 +1327,48 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
-     <varlistentry id="guc-ssl-ca-file" xreflabel="ssl_ca_file">
+     <varlistentry id="guc-ssl-alt-cert-file" xreflabel="ssl_alt_cert_file">
+      <term><varname>ssl_alt_cert_file</varname> (<type>string</type>)
+      <indexterm>
+       <primary><varname>ssl_alt_cert_file</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Specifies the name of the file containing an alternate SSL server
+        certificate of a different key type (e.g., ECDSA if the primary is
+        RSA).  <productname>OpenSSL</productname> selects the appropriate
+        certificate during the TLS handshake based on the negotiated cipher
+        suite.  Relative paths are relative to the data directory.
+        This parameter can only be set in the <filename>postgresql.conf</filename>
+        file or on the server command line.
+        The default is empty, meaning no alternate certificate is loaded.
+        Both <varname>ssl_alt_cert_file</varname> and
+        <xref linkend="guc-ssl-alt-key-file"/> must be set together.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="guc-ssl-alt-key-file" xreflabel="ssl_alt_key_file">
+      <term><varname>ssl_alt_key_file</varname> (<type>string</type>)
+      <indexterm>
+       <primary><varname>ssl_alt_key_file</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Specifies the name of the file containing the private key for the
+        alternate SSL server certificate specified by
+        <xref linkend="guc-ssl-alt-cert-file"/>.
+        Relative paths are relative to the data directory.
+        This parameter can only be set in the <filename>postgresql.conf</filename>
+        file or on the server command line.
+        The default is empty.
+       </para>
+      </listitem>
+     </varlistentry>
+
+          <varlistentry id="guc-ssl-ca-file" xreflabel="ssl_ca_file">
       <term><varname>ssl_ca_file</varname> (<type>string</type>)
       <indexterm>
        <primary><varname>ssl_ca_file</varname> configuration parameter</primary>
diff --git a/doc/src/sgml/runtime.sgml b/doc/src/sgml/runtime.sgml
index dfa292c2c3a..cc2ab2168d3 100644
--- a/doc/src/sgml/runtime.sgml
+++ b/doc/src/sgml/runtime.sgml
@@ -2451,6 +2451,19 @@ pg_dumpall -p 5432 | psql -d postgres -p 5433
       certificate owner is trustworthy</entry>
      </row>
 
+     <row>
+      <entry><xref linkend="guc-ssl-alt-cert-file"/></entry>
+      <entry>alternate server certificate</entry>
+      <entry>loaded alongside the primary certificate for dual key-type
+      support (e.g., ECDSA alongside RSA)</entry>
+     </row>
+
+     <row>
+      <entry><xref linkend="guc-ssl-alt-key-file"/></entry>
+      <entry>alternate server private key</entry>
+      <entry>proves alternate server certificate was sent by the owner</entry>
+     </row>
+
      <row>
       <entry><xref linkend="guc-ssl-ca-file"/></entry>
       <entry>trusted certificate authorities</entry>
diff --git a/doc/src/sgml/sslinfo.sgml b/doc/src/sgml/sslinfo.sgml
index 85d49f66537..4ad3fecbf02 100644
--- a/doc/src/sgml/sslinfo.sgml
+++ b/doc/src/sgml/sslinfo.sgml
@@ -240,6 +240,40 @@ emailAddress
     </para>
     </listitem>
    </varlistentry>
+   <varlistentry>
+    <term>
+     <function>ssl_server_cert_type() returns text</function>
+     <indexterm>
+      <primary>ssl_server_cert_type</primary>
+     </indexterm>
+    </term>
+    <listitem>
+    <para>
+     Returns the key type of the server certificate selected for the current
+     SSL connection (e.g., <literal>RSA</literal>,
+     <literal>ECDSA</literal>, or <literal>EdDSA</literal>).
+    </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term>
+     <function>ssl_server_cert_types() returns text</function>
+     <indexterm>
+      <primary>ssl_server_cert_types</primary>
+     </indexterm>
+    </term>
+    <listitem>
+    <para>
+     Returns a comma-separated list of all certificate key types loaded by
+     the server (e.g., <literal>RSA, ECDSA</literal>).  This reflects all
+     certificates configured via
+     <xref linkend="guc-ssl-cert-file"/> and
+     <xref linkend="guc-ssl-alt-cert-file"/>.
+    </para>
+    </listitem>
+   </varlistentry>
+
   </variablelist>
  </sect2>
 
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 7890e6c2de2..cf7d981b697 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -125,7 +125,9 @@ static struct hosts
 	 */
 	HostsLine  *default_host;
 }		   *SSL_hosts;
+static char ssl_cert_types_cached[64];
 
+static const char *evp_pkey_type_name(int type);
 static bool dummy_ssl_passwd_cb_called = false;
 static bool ssl_is_server_start;
 
@@ -548,6 +550,50 @@ be_tls_init(bool isServerStart)
 	if (SSLPreferServerCiphers)
 		SSL_CTX_set_options(context, SSL_OP_CIPHER_SERVER_PREFERENCE);
 
+
+	/*
+	 * Cache the loaded certificate types for be_tls_get_server_cert_types().
+	 * This must be done before the host memory context is replaced, since
+	 * child backends will inherit this cached value via fork() and the host
+	 * contexts will be freed when PostmasterContext is deleted.
+	 */
+	ssl_cert_types_cached[0] = '\0';
+	if (new_hosts->default_host && new_hosts->default_host->ssl_ctx)
+	{
+		SSL_CTX    *cache_ctx = new_hosts->default_host->ssl_ctx;
+		int			cpos = 0;
+
+		SSL_CTX_set_current_cert(cache_ctx, SSL_CERT_SET_FIRST);
+		do
+		{
+			X509	   *cert = SSL_CTX_get0_certificate(cache_ctx);
+
+			if (cert)
+			{
+				EVP_PKEY   *pkey = X509_get0_pubkey(cert);
+
+				if (pkey)
+				{
+					const char *name = evp_pkey_type_name(EVP_PKEY_get_base_id(pkey));
+					size_t		nlen = strlen(name);
+
+					if (cpos > 0 && cpos + 2 < (int) sizeof(ssl_cert_types_cached))
+					{
+						ssl_cert_types_cached[cpos++] = ',';
+						ssl_cert_types_cached[cpos++] = ' ';
+					}
+					if (cpos + nlen < sizeof(ssl_cert_types_cached))
+					{
+						memcpy(ssl_cert_types_cached + cpos, name, nlen);
+						cpos += (int) nlen;
+					}
+				}
+			}
+		} while (SSL_CTX_set_current_cert(cache_ctx, SSL_CERT_SET_NEXT));
+
+		ssl_cert_types_cached[cpos] = '\0';
+	}
+
 	/*
 	 * Success!  Replace any existing SSL_context and host configurations.
 	 */
@@ -735,6 +781,60 @@ init_host_context(HostsLine *host, bool isServerStart)
 		goto error;
 	}
 
+	/*
+	 * Load alternate certificate (e.g. ECDSA alongside RSA) into the same
+	 * context.  OpenSSL supports one certificate per key type and selects
+	 * the appropriate one during the TLS handshake.
+	 */
+	if (ssl_alt_cert_file[0] && !ssl_alt_key_file[0])
+	{
+		ereport(isServerStart ? FATAL : LOG,
+				(errcode(ERRCODE_CONFIG_FILE_ERROR),
+				 errmsg("ssl_alt_cert_file is set but ssl_alt_key_file is not")));
+		goto error;
+	}
+	if (!ssl_alt_cert_file[0] && ssl_alt_key_file[0])
+	{
+		ereport(isServerStart ? FATAL : LOG,
+				(errcode(ERRCODE_CONFIG_FILE_ERROR),
+				 errmsg("ssl_alt_key_file is set but ssl_alt_cert_file is not")));
+		goto error;
+	}
+	if (ssl_alt_cert_file[0] && ssl_alt_key_file[0])
+	{
+		if (SSL_CTX_use_certificate_file(ctx, ssl_alt_cert_file,
+										 SSL_FILETYPE_PEM) != 1)
+		{
+			ereport(isServerStart ? FATAL : LOG,
+					(errcode(ERRCODE_CONFIG_FILE_ERROR),
+					 errmsg("could not load alternate server certificate file \"%s\": %s",
+							ssl_alt_cert_file, SSLerrmessage(ERR_get_error()))));
+			goto error;
+		}
+
+		if (!check_ssl_key_file_permissions(ssl_alt_key_file, isServerStart))
+			goto error;
+
+		if (SSL_CTX_use_PrivateKey_file(ctx, ssl_alt_key_file,
+										   SSL_FILETYPE_PEM) != 1)
+		{
+			ereport(isServerStart ? FATAL : LOG,
+					(errcode(ERRCODE_CONFIG_FILE_ERROR),
+					 errmsg("could not load alternate private key file \"%s\": %s",
+							ssl_alt_key_file, SSLerrmessage(ERR_get_error()))));
+			goto error;
+		}
+
+		if (SSL_CTX_check_private_key(ctx) != 1)
+		{
+			ereport(isServerStart ? FATAL : LOG,
+					(errcode(ERRCODE_CONFIG_FILE_ERROR),
+					 errmsg("check of alternate private key failed: %s",
+							SSLerrmessage(ERR_get_error()))));
+			goto error;
+		}
+	}
+
 	/*
 	 * Load CA store, so we can verify client certificates if needed.
 	 */
@@ -1826,33 +1926,43 @@ ssl_update_ssl(SSL *ssl, HostsLine *host_config)
 
 	X509	   *cert;
 	EVP_PKEY   *key;
-
-	STACK_OF(X509) * chain;
+	STACK_OF(X509) *chain;
+	bool		first = true;
 
 	Assert(ctx != NULL);
-	/*-
-	 * Make use of the already-loaded certificate chain and key. At first
-	 * glance, SSL_set_SSL_CTX() looks like the easiest way to do this, but
-	 * beware -- it has very odd behavior:
-	 *
-	 *     https://github.com/openssl/openssl/issues/6109
+
+	/*
+	 * Iterate over all certificate types loaded in the SSL_CTX (e.g. RSA
+	 * and ECDSA) and copy each onto the per-connection SSL object.
+	 * SSL_use_cert_and_key with override=1 replaces; override=0 adds a
+	 * cert for a different key type without wiping existing ones.
 	 */
-	cert = SSL_CTX_get0_certificate(ctx);
-	key = SSL_CTX_get0_privatekey(ctx);
+	SSL_CTX_set_current_cert(ctx, SSL_CERT_SET_FIRST);
+	do
+	{
+		cert = SSL_CTX_get0_certificate(ctx);
+		key = SSL_CTX_get0_privatekey(ctx);
+
+		if (!cert || !key)
+			continue;
 
-	Assert(cert && key);
+		if (!SSL_CTX_get0_chain_certs(ctx, &chain)
+			|| !SSL_use_cert_and_key(ssl, cert, key, chain, first ? 1 : 0))
+		{
+			ereport(COMMERROR,
+					errcode(ERRCODE_INTERNAL_ERROR),
+					errmsg_internal("could not update certificate chain: %s",
+									SSLerrmessage(ERR_get_error())));
+			return false;
+		}
+		first = false;
+	} while (SSL_CTX_set_current_cert(ctx, SSL_CERT_SET_NEXT));
 
-	if (!SSL_CTX_get0_chain_certs(ctx, &chain)
-		|| !SSL_use_cert_and_key(ssl, cert, key, chain, 1 /* override */ )
-		|| !SSL_check_private_key(ssl))
+	if (!SSL_check_private_key(ssl))
 	{
-		/*
-		 * This shouldn't really be possible, since the inputs came from a
-		 * SSL_CTX that was already populated by OpenSSL.
-		 */
 		ereport(COMMERROR,
 				errcode(ERRCODE_INTERNAL_ERROR),
-				errmsg_internal("could not update certificate chain: %s",
+				errmsg_internal("could not verify private key: %s",
 								SSLerrmessage(ERR_get_error())));
 		return false;
 	}
@@ -2194,6 +2304,29 @@ SSLerrmessage(unsigned long ecode)
 	return errbuf;
 }
 
+
+static const char *
+evp_pkey_type_name(int type)
+{
+	switch (type)
+	{
+		case EVP_PKEY_RSA:
+			return "RSA";
+		case EVP_PKEY_EC:
+			return "ECDSA";
+#ifdef EVP_PKEY_ED25519
+		case EVP_PKEY_ED25519:
+			return "EdDSA";
+#endif
+#ifdef EVP_PKEY_ED448
+		case EVP_PKEY_ED448:
+			return "EdDSA";
+#endif
+		default:
+			return "unknown";
+	}
+}
+
 int
 be_tls_get_cipher_bits(Port *port)
 {
@@ -2226,6 +2359,34 @@ be_tls_get_cipher(Port *port)
 		return NULL;
 }
 
+const char *
+be_tls_get_server_cert_type(Port *port)
+{
+	if (port->ssl)
+	{
+		X509	   *cert = SSL_get_certificate(port->ssl);
+
+		if (cert)
+		{
+			EVP_PKEY   *pkey = X509_get0_pubkey(cert);
+
+			if (pkey)
+				return evp_pkey_type_name(EVP_PKEY_get_base_id(pkey));
+		}
+	}
+	return NULL;
+}
+
+const char *
+be_tls_get_server_cert_types(void)
+{
+	if (ssl_cert_types_cached[0] == '\0')
+		return NULL;
+
+	return ssl_cert_types_cached;
+}
+
+
 void
 be_tls_get_peer_subject_name(Port *port, char *ptr, size_t len)
 {
diff --git a/src/backend/libpq/be-secure.c b/src/backend/libpq/be-secure.c
index 86ceea72e64..51e479c463d 100644
--- a/src/backend/libpq/be-secure.c
+++ b/src/backend/libpq/be-secure.c
@@ -37,6 +37,8 @@
 char	   *ssl_library;
 char	   *ssl_cert_file;
 char	   *ssl_key_file;
+char	   *ssl_alt_cert_file;
+char	   *ssl_alt_key_file;
 char	   *ssl_ca_file;
 char	   *ssl_crl_file;
 char	   *ssl_crl_dir;
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index afaa058b046..2cfed2d42a4 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -2750,6 +2750,18 @@
   check_hook => 'check_ssl',
 },
 
+{ name => 'ssl_alt_cert_file', type => 'string', context => 'PGC_SIGHUP', group => 'CONN_AUTH_SSL',
+  short_desc => 'Location of the alternate SSL server certificate file.',
+  variable => 'ssl_alt_cert_file',
+  boot_val => '""',
+},
+
+{ name => 'ssl_alt_key_file', type => 'string', context => 'PGC_SIGHUP', group => 'CONN_AUTH_SSL',
+  short_desc => 'Location of the alternate SSL server private key file.',
+  variable => 'ssl_alt_key_file',
+  boot_val => '""',
+},
+
 { name => 'ssl_ca_file', type => 'string', context => 'PGC_SIGHUP', group => 'CONN_AUTH_SSL',
   short_desc => 'Location of the SSL certificate authority file.',
   variable => 'ssl_ca_file',
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index ac38cddaaf9..d367a096079 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -115,6 +115,8 @@
 #ssl_crl_file = ''
 #ssl_crl_dir = ''
 #ssl_key_file = 'server.key'
+#ssl_alt_cert_file = ''
+#ssl_alt_key_file = ''
 #ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL'       # allowed TLSv1.2 ciphers
 #ssl_tls13_ciphers = '' # allowed TLSv1.3 cipher suites, blank for default
 #ssl_prefer_server_ciphers = on
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 921b2daa4ff..cf4fd1d282f 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -320,6 +320,8 @@ extern const char *be_tls_get_cipher(Port *port);
 extern void be_tls_get_peer_subject_name(Port *port, char *ptr, size_t len);
 extern void be_tls_get_peer_issuer_name(Port *port, char *ptr, size_t len);
 extern void be_tls_get_peer_serial(Port *port, char *ptr, size_t len);
+extern const char *be_tls_get_server_cert_type(Port *port);
+extern const char *be_tls_get_server_cert_types(void);
 
 /*
  * Get the server certificate hash for SCRAM channel binding type
diff --git a/src/include/libpq/libpq.h b/src/include/libpq/libpq.h
index d15073a0a93..5e7705adba7 100644
--- a/src/include/libpq/libpq.h
+++ b/src/include/libpq/libpq.h
@@ -108,6 +108,8 @@ extern PGDLLIMPORT char *ssl_cert_file;
 extern PGDLLIMPORT char *ssl_crl_file;
 extern PGDLLIMPORT char *ssl_crl_dir;
 extern PGDLLIMPORT char *ssl_key_file;
+extern PGDLLIMPORT char *ssl_alt_cert_file;
+extern PGDLLIMPORT char *ssl_alt_key_file;
 extern PGDLLIMPORT int ssl_min_protocol_version;
 extern PGDLLIMPORT int ssl_max_protocol_version;
 extern PGDLLIMPORT char *ssl_passphrase_command;
diff --git a/src/test/ssl/t/004_ssl_alt_cert.pl b/src/test/ssl/t/004_ssl_alt_cert.pl
new file mode 100644
index 00000000000..dca4da34f19
--- /dev/null
+++ b/src/test/ssl/t/004_ssl_alt_cert.pl
@@ -0,0 +1,188 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test dual RSA + ECDSA certificate support via ssl_alt_cert_file/ssl_alt_key_file
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use SSL::Server;
+
+if ($ENV{with_ssl} ne 'openssl')
+{
+	plan skip_all => 'OpenSSL not supported by this build';
+}
+if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bssl\b/)
+{
+	plan skip_all =>
+	  'Potentially unsafe test SSL not enabled in PG_TEST_EXTRA';
+}
+
+my $ssl_server = SSL::Server->new();
+my $SERVERHOSTADDR = '127.0.0.1';
+my $SERVERHOSTCIDR = '127.0.0.1/32';
+
+#### Set up the server.
+
+note "setting up data directory";
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+
+$ENV{PGHOST} = $node->host;
+$ENV{PGPORT} = $node->port;
+$node->start;
+
+$ssl_server->configure_test_server_for_ssl($node, $SERVERHOSTADDR,
+	$SERVERHOSTCIDR, 'trust', extensions => [qw(sslinfo)]);
+
+my $pgdata = $node->data_dir;
+
+#### Generate ECDSA cert signed by the test server CA.
+
+my $ssl_dir = "$FindBin::RealBin/../ssl";
+my $ecdsa_key = "$pgdata/server-ecdsa.key";
+my $ecdsa_csr = "$pgdata/server-ecdsa.csr";
+my $ecdsa_crt = "$pgdata/server-ecdsa.crt";
+
+note "generating ECDSA server certificate";
+
+system("openssl ecparam -genkey -name prime256v1 -out $ecdsa_key 2>/dev/null") == 0
+	or die "failed to generate ECDSA key";
+system("openssl req -new -key $ecdsa_key -out $ecdsa_csr -subj '/CN=localhost' -batch 2>/dev/null") == 0
+	or die "failed to generate ECDSA CSR";
+system("openssl x509 -req -in $ecdsa_csr -CA $ssl_dir/server_ca.crt -CAkey $ssl_dir/server_ca.key "
+	. "-CAcreateserial -out $ecdsa_crt -days 3650 2>/dev/null") == 0
+	or die "failed to sign ECDSA cert";
+chmod 0600, $ecdsa_key;
+unlink $ecdsa_csr;
+
+#### Configure server with dual certs (RSA primary + ECDSA alternate).
+
+note "configuring server with dual RSA + ECDSA certificates";
+
+# Use the standard RSA cert as primary via switch_server_cert
+$ssl_server->switch_server_cert($node,
+	certfile => 'server-cn-only',
+	cafile => 'root+client_ca',
+	restart => 'no');
+
+# Add alt cert configuration
+$node->append_conf('sslconfig.conf',
+	"ssl_alt_cert_file='$ecdsa_crt'");
+$node->append_conf('sslconfig.conf',
+	"ssl_alt_key_file='$ecdsa_key'");
+
+# Force TLS 1.2 so we can control cipher selection
+$node->append_conf('sslconfig.conf',
+	"ssl_max_protocol_version='TLSv1.2'");
+
+$node->restart;
+
+#### Tests.
+
+my $common_connstr = "sslrootcert=invalid hostaddr=$SERVERHOSTADDR host=localhost "
+	. "user=ssltestuser dbname=trustdb sslmode=require";
+
+# Test 1: Connect with RSA cipher
+note "testing RSA cipher connection";
+$node->connect_ok(
+	"$common_connstr sslcert=invalid",
+	"connect with RSA cipher via default negotiation",
+	sql => "SELECT 1");
+
+# Test 2: Verify the GUC parameters are set
+my $result = $node->safe_psql('trustdb',
+	"SHOW ssl_alt_cert_file",
+	connstr => "$common_connstr sslcert=invalid");
+like($result, qr/server-ecdsa\.crt/, 'ssl_alt_cert_file is set');
+
+$result = $node->safe_psql('trustdb',
+	"SHOW ssl_alt_key_file",
+	connstr => "$common_connstr sslcert=invalid");
+like($result, qr/server-ecdsa\.key/, 'ssl_alt_key_file is set');
+
+# Test 3: Verify both cipher types work via openssl s_client
+note "testing RSA cipher via openssl s_client";
+my $openssl_rsa = `echo "QUIT" | openssl s_client -connect $SERVERHOSTADDR:${\$node->port} -starttls postgres -tls1_2 -cipher ECDHE-RSA-AES256-GCM-SHA384 2>&1`;
+like($openssl_rsa, qr/ECDHE-RSA-AES256-GCM-SHA384/, 'RSA cipher negotiates successfully');
+
+note "testing ECDSA cipher via openssl s_client";
+my $openssl_ecdsa = `echo "QUIT" | openssl s_client -connect $SERVERHOSTADDR:${\$node->port} -starttls postgres -tls1_2 -cipher ECDHE-ECDSA-AES256-GCM-SHA384 2>&1`;
+like($openssl_ecdsa, qr/ECDHE-ECDSA-AES256-GCM-SHA384/, 'ECDSA cipher negotiates successfully');
+
+# Test 4: Verify correct cert type is served for each cipher
+note "verifying RSA cert served for RSA cipher";
+my $rsa_cert = `echo "QUIT" | openssl s_client -connect $SERVERHOSTADDR:${\$node->port} -starttls postgres -tls1_2 -cipher ECDHE-RSA-AES256-GCM-SHA384 2>&1 | openssl x509 -noout -text 2>/dev/null`;
+like($rsa_cert, qr/rsaEncryption/, 'RSA cert served for RSA cipher');
+
+note "verifying ECDSA cert served for ECDSA cipher";
+my $ecdsa_cert = `echo "QUIT" | openssl s_client -connect $SERVERHOSTADDR:${\$node->port} -starttls postgres -tls1_2 -cipher ECDHE-ECDSA-AES256-GCM-SHA384 2>&1 | openssl x509 -noout -text 2>/dev/null`;
+like($ecdsa_cert, qr/id-ecPublicKey/, 'ECDSA cert served for ECDSA cipher');
+
+# Test 5: Verify ssl_server_cert_type() returns correct type per connection
+note "testing ssl_server_cert_type() via default negotiation";
+$result = $node->safe_psql('trustdb',
+	"SELECT ssl_server_cert_type()",
+	connstr => "$common_connstr sslcert=invalid");
+like($result, qr/^(RSA|ECDSA)$/, 'ssl_server_cert_type() returns valid cert type');
+
+# Test 6: Verify ssl_server_cert_types() shows all loaded types
+note "testing ssl_server_cert_types() shows all loaded cert types";
+$result = $node->safe_psql('trustdb',
+	"SELECT ssl_server_cert_types()",
+	connstr => "$common_connstr sslcert=invalid");
+like($result, qr/RSA/, 'ssl_server_cert_types() includes RSA');
+like($result, qr/ECDSA/, 'ssl_server_cert_types() includes ECDSA');
+
+# Test 7: Verify server rejects mismatched alt cert/key configuration
+note "testing server rejects ssl_alt_cert_file without ssl_alt_key_file";
+
+$node->append_conf('sslconfig.conf',
+	"ssl_alt_key_file=''");
+
+$result = $node->restart(fail_ok => 1);
+is($result, 0, 'restart fails with ssl_alt_cert_file set but ssl_alt_key_file empty');
+
+# Check the log for the expected error message
+my $log = slurp_file($node->logfile);
+like($log, qr/ssl_alt_cert_file is set but ssl_alt_key_file is not/,
+	'log contains expected error for missing ssl_alt_key_file');
+
+# Test 8: Verify the reverse mismatch (key without cert)
+note "testing server rejects ssl_alt_key_file without ssl_alt_cert_file";
+
+ok(unlink($node->data_dir . '/sslconfig.conf'));
+$node->append_conf('sslconfig.conf', "ssl=on");
+$node->append_conf('sslconfig.conf',
+	"ssl_ca_file='root+client_ca.crt'");
+$node->append_conf('sslconfig.conf',
+	"ssl_cert_file='server-cn-only.crt'");
+$node->append_conf('sslconfig.conf',
+	"ssl_key_file='server-cn-only.key'");
+$node->append_conf('sslconfig.conf',
+	"ssl_alt_key_file='$ecdsa_key'");
+
+$result = $node->restart(fail_ok => 1);
+is($result, 0, 'restart fails with ssl_alt_key_file set but ssl_alt_cert_file empty');
+
+$log = slurp_file($node->logfile);
+like($log, qr/ssl_alt_key_file is set but ssl_alt_cert_file is not/,
+	'log contains expected error for missing ssl_alt_cert_file');
+
+# Restore valid config so the node can be stopped cleanly
+ok(unlink($node->data_dir . '/sslconfig.conf'));
+$node->append_conf('sslconfig.conf', "ssl=on");
+$node->append_conf('sslconfig.conf',
+	"ssl_ca_file='root+client_ca.crt'");
+$node->append_conf('sslconfig.conf',
+	"ssl_cert_file='server-cn-only.crt'");
+$node->append_conf('sslconfig.conf',
+	"ssl_key_file='server-cn-only.key'");
+$node->start;
+
+done_testing();
-- 
2.52.0

From e613fb1d5b6d09661004ec0f65627b2549af5634 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Renaud=20M=C3=A9trich?= <[email protected]>
Date: Mon, 15 Jun 2026 08:14:09 +0200
Subject: [PATCH v2 2/8] Reject alternate certificate with same key type as
 primary
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

If ssl_alt_cert_file specifies a certificate of the same key type as the
primary ssl_cert_file (e.g., both RSA), the alternate silently replaces
the primary, which is not useful.  Detect this by comparing
EVP_PKEY_get_base_id() on both certificates after loading, and report
a configuration error at startup or reload.

Author: Renaud Métrich <[email protected]>
---
 src/backend/libpq/be-secure-openssl.c | 33 +++++++++++++++++++++++++++
 1 file changed, 33 insertions(+)

diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index cf7d981b697..bfbce535ac8 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -833,6 +833,39 @@ init_host_context(HostsLine *host, bool isServerStart)
 							SSLerrmessage(ERR_get_error()))));
 			goto error;
 		}
+
+		/*
+		 * Verify that the alternate certificate uses a different key type
+		 * than the primary.  If both are the same type (e.g. both RSA),
+		 * the alternate silently replaces the primary, which is not useful.
+		 */
+		{
+			X509	   *primary_cert;
+			X509	   *alt_cert;
+			int			primary_type;
+			int			alt_type;
+
+			SSL_CTX_set_current_cert(ctx, SSL_CERT_SET_FIRST);
+			primary_cert = SSL_CTX_get0_certificate(ctx);
+
+			SSL_CTX_set_current_cert(ctx, SSL_CERT_SET_NEXT);
+			alt_cert = SSL_CTX_get0_certificate(ctx);
+
+			if (primary_cert && alt_cert)
+			{
+				primary_type = EVP_PKEY_get_base_id(X509_get0_pubkey(primary_cert));
+				alt_type = EVP_PKEY_get_base_id(X509_get0_pubkey(alt_cert));
+
+				if (primary_type == alt_type)
+				{
+					ereport(isServerStart ? FATAL : LOG,
+							(errcode(ERRCODE_CONFIG_FILE_ERROR),
+							 errmsg("alternate certificate has the same key type (%s) as the primary certificate",
+									evp_pkey_type_name(alt_type))));
+					goto error;
+				}
+			}
+		}
 	}
 
 	/*
-- 
2.52.0

From bff0d5d0bb295824a0c7136903f07cabfe1d8668 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Renaud=20M=C3=A9trich?= <[email protected]>
Date: Mon, 15 Jun 2026 08:14:56 +0200
Subject: [PATCH v2 3/8] Load full certificate chain for alternate certificate
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Use SSL_CTX_use_certificate_chain_file() instead of
SSL_CTX_use_certificate_file() for the alternate certificate, matching
how the primary certificate is loaded.  This ensures that intermediate
CA certificates included in the PEM file are loaded as the chain for
the alternate certificate.

Author: Renaud Métrich <[email protected]>
---
 src/backend/libpq/be-secure-openssl.c | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index bfbce535ac8..fa9f551e22a 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -802,8 +802,7 @@ init_host_context(HostsLine *host, bool isServerStart)
 	}
 	if (ssl_alt_cert_file[0] && ssl_alt_key_file[0])
 	{
-		if (SSL_CTX_use_certificate_file(ctx, ssl_alt_cert_file,
-										 SSL_FILETYPE_PEM) != 1)
+		if (SSL_CTX_use_certificate_chain_file(ctx, ssl_alt_cert_file) != 1)
 		{
 			ereport(isServerStart ? FATAL : LOG,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
-- 
2.52.0

From c0966084b91818bdf3d35e87666399c7e59298d5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Renaud=20M=C3=A9trich?= <[email protected]>
Date: Mon, 15 Jun 2026 08:15:50 +0200
Subject: [PATCH v2 4/8] Load alternate certificates only for the default host
 context
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The global ssl_alt_cert_file/ssl_alt_key_file GUCs were being loaded
into every host context, including per-host SNI entries from
pg_hosts.conf.  This caused unintended behavior: if an SNI host used
a different key type, the alt cert was added as an alternative; if the
same type, it silently replaced the host cert.

Add an is_default parameter to init_host_context() and only load the
alternate certificate for the default host context (postgresql.conf),
not for per-host SNI entries.

Author: Renaud Métrich <[email protected]>
---
 src/backend/libpq/be-secure-openssl.c | 18 ++++++++++--------
 1 file changed, 10 insertions(+), 8 deletions(-)

diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index fa9f551e22a..6964830c5af 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -98,7 +98,7 @@ static bool initialize_dh(SSL_CTX *context, bool isServerStart);
 static bool initialize_ecdh(SSL_CTX *context, bool isServerStart);
 static const char *SSLerrmessageExt(unsigned long ecode, const char *replacement);
 static const char *SSLerrmessage(unsigned long ecode);
-static bool init_host_context(HostsLine *host, bool isServerStart);
+static bool init_host_context(HostsLine *host, bool isServerStart, bool is_default);
 static void host_context_cleanup_cb(void *arg);
 #ifdef HAVE_SSL_CTX_SET_CLIENT_HELLO_CB
 static int	sni_clienthello_cb(SSL *ssl, int *al, void *arg);
@@ -251,7 +251,7 @@ be_tls_init(bool isServerStart)
 		{
 			HostsLine  *host = lfirst(line);
 
-			if (!init_host_context(host, isServerStart))
+			if (!init_host_context(host, isServerStart, false))
 				goto error;
 
 			/*
@@ -346,7 +346,7 @@ be_tls_init(bool isServerStart)
 		pgconf->ssl_passphrase_cmd = ssl_passphrase_command;
 		pgconf->ssl_passphrase_reload = ssl_passphrase_command_supports_reload;
 
-		if (!init_host_context(pgconf, isServerStart))
+		if (!init_host_context(pgconf, isServerStart, true))
 			goto error;
 
 		/*
@@ -655,7 +655,7 @@ host_context_cleanup_cb(void *arg)
 }
 
 static bool
-init_host_context(HostsLine *host, bool isServerStart)
+init_host_context(HostsLine *host, bool isServerStart, bool is_default)
 {
 	SSL_CTX    *ctx = SSL_CTX_new(SSLv23_method());
 	static bool init_warned = false;
@@ -784,23 +784,25 @@ init_host_context(HostsLine *host, bool isServerStart)
 	/*
 	 * Load alternate certificate (e.g. ECDSA alongside RSA) into the same
 	 * context.  OpenSSL supports one certificate per key type and selects
-	 * the appropriate one during the TLS handshake.
+	 * the appropriate one during the TLS handshake.  Only load for the
+	 * default host context (postgresql.conf), not for per-host SNI entries
+	 * from pg_hosts.conf.
 	 */
-	if (ssl_alt_cert_file[0] && !ssl_alt_key_file[0])
+	if (is_default && ssl_alt_cert_file[0] && !ssl_alt_key_file[0])
 	{
 		ereport(isServerStart ? FATAL : LOG,
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("ssl_alt_cert_file is set but ssl_alt_key_file is not")));
 		goto error;
 	}
-	if (!ssl_alt_cert_file[0] && ssl_alt_key_file[0])
+	if (is_default && !ssl_alt_cert_file[0] && ssl_alt_key_file[0])
 	{
 		ereport(isServerStart ? FATAL : LOG,
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("ssl_alt_key_file is set but ssl_alt_cert_file is not")));
 		goto error;
 	}
-	if (ssl_alt_cert_file[0] && ssl_alt_key_file[0])
+	if (is_default && ssl_alt_cert_file[0] && ssl_alt_key_file[0])
 	{
 		if (SSL_CTX_use_certificate_chain_file(ctx, ssl_alt_cert_file) != 1)
 		{
-- 
2.52.0

From 6662750b8e6bc2c9c3fcd5ae727559912a64757d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Renaud=20M=C3=A9trich?= <[email protected]>
Date: Mon, 15 Jun 2026 08:17:29 +0200
Subject: [PATCH v2 5/8] Add compatibility guards for LibreSSL and older
 OpenSSL
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Guard SSL_CTX_set_current_cert() and SSL_CERT_SET_FIRST/NEXT usage
with #ifdef SSL_CERT_SET_FIRST, since these are available in OpenSSL
1.0.2+ but not in LibreSSL.

When not available:
- ssl_alt_cert_file produces a clear configuration error
- ssl_update_ssl() falls back to the original single-cert copy
- cert types caching is skipped (ssl_server_cert_types() returns NULL)

Author: Renaud Métrich <[email protected]>
---
 src/backend/libpq/be-secure-openssl.c | 36 +++++++++++++++++++++++++++
 1 file changed, 36 insertions(+)

diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 6964830c5af..2b69be6b46b 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -558,6 +558,7 @@ be_tls_init(bool isServerStart)
 	 * contexts will be freed when PostmasterContext is deleted.
 	 */
 	ssl_cert_types_cached[0] = '\0';
+#ifdef SSL_CERT_SET_FIRST
 	if (new_hosts->default_host && new_hosts->default_host->ssl_ctx)
 	{
 		SSL_CTX    *cache_ctx = new_hosts->default_host->ssl_ctx;
@@ -593,6 +594,7 @@ be_tls_init(bool isServerStart)
 
 		ssl_cert_types_cached[cpos] = '\0';
 	}
+#endif								/* SSL_CERT_SET_FIRST */
 
 	/*
 	 * Success!  Replace any existing SSL_context and host configurations.
@@ -787,7 +789,11 @@ init_host_context(HostsLine *host, bool isServerStart, bool is_default)
 	 * the appropriate one during the TLS handshake.  Only load for the
 	 * default host context (postgresql.conf), not for per-host SNI entries
 	 * from pg_hosts.conf.
+	 *
+	 * Requires SSL_CTX_set_current_cert() for same-type detection, which
+	 * is available in OpenSSL 1.0.2+ but not in LibreSSL.
 	 */
+#ifdef SSL_CERT_SET_FIRST
 	if (is_default && ssl_alt_cert_file[0] && !ssl_alt_key_file[0])
 	{
 		ereport(isServerStart ? FATAL : LOG,
@@ -868,6 +874,16 @@ init_host_context(HostsLine *host, bool isServerStart, bool is_default)
 			}
 		}
 	}
+#else
+	if (is_default && ssl_alt_cert_file[0])
+	{
+		ereport(isServerStart ? FATAL : LOG,
+				(errcode(ERRCODE_CONFIG_FILE_ERROR),
+				 errmsg("ssl_alt_cert_file is not supported by this build"),
+				 errhint("Alternate certificate support requires OpenSSL 1.0.2 or later.")));
+		goto error;
+	}
+#endif								/* SSL_CERT_SET_FIRST */
 
 	/*
 	 * Load CA store, so we can verify client certificates if needed.
@@ -1970,7 +1986,11 @@ ssl_update_ssl(SSL *ssl, HostsLine *host_config)
 	 * and ECDSA) and copy each onto the per-connection SSL object.
 	 * SSL_use_cert_and_key with override=1 replaces; override=0 adds a
 	 * cert for a different key type without wiping existing ones.
+	 *
+	 * Fall back to single-cert copy when SSL_CTX_set_current_cert() is
+	 * not available (LibreSSL).
 	 */
+#ifdef SSL_CERT_SET_FIRST
 	SSL_CTX_set_current_cert(ctx, SSL_CERT_SET_FIRST);
 	do
 	{
@@ -1991,6 +2011,22 @@ ssl_update_ssl(SSL *ssl, HostsLine *host_config)
 		}
 		first = false;
 	} while (SSL_CTX_set_current_cert(ctx, SSL_CERT_SET_NEXT));
+#else
+	cert = SSL_CTX_get0_certificate(ctx);
+	key = SSL_CTX_get0_privatekey(ctx);
+
+	Assert(cert && key);
+
+	if (!SSL_CTX_get0_chain_certs(ctx, &chain)
+		|| !SSL_use_cert_and_key(ssl, cert, key, chain, 1))
+	{
+		ereport(COMMERROR,
+				errcode(ERRCODE_INTERNAL_ERROR),
+				errmsg_internal("could not update certificate chain: %s",
+								SSLerrmessage(ERR_get_error())));
+		return false;
+	}
+#endif
 
 	if (!SSL_check_private_key(ssl))
 	{
-- 
2.52.0

From 0fab5ad14f25f2458c0847880cca4d8dc175db2f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Renaud=20M=C3=A9trich?= <[email protected]>
Date: Mon, 15 Jun 2026 08:18:02 +0200
Subject: [PATCH v2 6/8] Document ssl_alt_cert_file limitations
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Note that the alternate certificate must use a different key type than
the primary, and that this setting applies only to the default SSL
configuration from postgresql.conf, not to per-host entries in
pg_hosts.conf.

Author: Renaud Métrich <[email protected]>
---
 doc/src/sgml/config.sgml | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 4d712ed852b..a684523aeb4 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1345,6 +1345,14 @@ include_dir 'conf.d'
         The default is empty, meaning no alternate certificate is loaded.
         Both <varname>ssl_alt_cert_file</varname> and
         <xref linkend="guc-ssl-alt-key-file"/> must be set together.
+        The alternate certificate must use a different key type than the
+        primary certificate; specifying the same type is an error.
+       </para>
+       <para>
+        This setting applies only to the default SSL configuration from
+        <filename>postgresql.conf</filename>.  Per-host certificate
+        configuration via <filename>pg_hosts.conf</filename> is not affected
+        by this parameter.
        </para>
       </listitem>
      </varlistentry>
-- 
2.52.0

From 9619bf7020f592d954d1a85e7c0c2133dc1230bc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Renaud=20M=C3=A9trich?= <[email protected]>
Date: Mon, 15 Jun 2026 09:03:04 +0200
Subject: [PATCH v2 7/8] Fix TLS 1.3 HelloRetryRequest with multiple
 certificate types
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

During a TLS 1.3 HelloRetryRequest, the ClientHello callback fires
twice.  On the second invocation, ssl_update_ssl() was using
SSL_use_cert_and_key() with override=0 for the second certificate type,
which failed because that key type was already loaded from the first
invocation, producing "could not update certificate chain: not
replacing certificate".

Fix by always using override=1 when loading certificates from the
SSL_CTX onto the per-connection SSL object.  Since we are loading a
complete set of certificates from a known-good context, replacing is
always the correct behavior.

Remove the TLS 1.2 protocol restriction from the test and add a TLS 1.3
connectivity test to exercise this code path.

Author: Renaud Métrich <[email protected]>
---
 src/backend/libpq/be-secure-openssl.c | 10 +++++-----
 src/test/ssl/t/004_ssl_alt_cert.pl    | 15 +++++++++++----
 2 files changed, 16 insertions(+), 9 deletions(-)

diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 2b69be6b46b..0290c9b61e5 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -1977,15 +1977,16 @@ ssl_update_ssl(SSL *ssl, HostsLine *host_config)
 	X509	   *cert;
 	EVP_PKEY   *key;
 	STACK_OF(X509) *chain;
-	bool		first = true;
 
 	Assert(ctx != NULL);
 
 	/*
 	 * Iterate over all certificate types loaded in the SSL_CTX (e.g. RSA
 	 * and ECDSA) and copy each onto the per-connection SSL object.
-	 * SSL_use_cert_and_key with override=1 replaces; override=0 adds a
-	 * cert for a different key type without wiping existing ones.
+	 * Always use override=1 because this callback may fire more than
+	 * once per handshake (e.g. TLS 1.3 HelloRetryRequest).  Using
+	 * override=0 for subsequent certs would fail on the second
+	 * invocation since those key types are already set.
 	 *
 	 * Fall back to single-cert copy when SSL_CTX_set_current_cert() is
 	 * not available (LibreSSL).
@@ -2001,7 +2002,7 @@ ssl_update_ssl(SSL *ssl, HostsLine *host_config)
 			continue;
 
 		if (!SSL_CTX_get0_chain_certs(ctx, &chain)
-			|| !SSL_use_cert_and_key(ssl, cert, key, chain, first ? 1 : 0))
+			|| !SSL_use_cert_and_key(ssl, cert, key, chain, 1))
 		{
 			ereport(COMMERROR,
 					errcode(ERRCODE_INTERNAL_ERROR),
@@ -2009,7 +2010,6 @@ ssl_update_ssl(SSL *ssl, HostsLine *host_config)
 									SSLerrmessage(ERR_get_error())));
 			return false;
 		}
-		first = false;
 	} while (SSL_CTX_set_current_cert(ctx, SSL_CERT_SET_NEXT));
 #else
 	cert = SSL_CTX_get0_certificate(ctx);
diff --git a/src/test/ssl/t/004_ssl_alt_cert.pl b/src/test/ssl/t/004_ssl_alt_cert.pl
index dca4da34f19..a148e0a7fb1 100644
--- a/src/test/ssl/t/004_ssl_alt_cert.pl
+++ b/src/test/ssl/t/004_ssl_alt_cert.pl
@@ -124,14 +124,21 @@ note "verifying ECDSA cert served for ECDSA cipher";
 my $ecdsa_cert = `echo "QUIT" | openssl s_client -connect $SERVERHOSTADDR:${\$node->port} -starttls postgres -tls1_2 -cipher ECDHE-ECDSA-AES256-GCM-SHA384 2>&1 | openssl x509 -noout -text 2>/dev/null`;
 like($ecdsa_cert, qr/id-ecPublicKey/, 'ECDSA cert served for ECDSA cipher');
 
-# Test 5: Verify ssl_server_cert_type() returns correct type per connection
+# Test 5: Verify TLS 1.3 connection works (exercises HelloRetryRequest path)
+note "testing TLS 1.3 connection with dual certs";
+$node->connect_ok(
+	"$common_connstr sslcert=invalid",
+	"connect via TLS 1.3 with dual certs (default negotiation)",
+	sql => "SELECT 1");
+
+# Test 6: Verify ssl_server_cert_type() returns correct type per connection
 note "testing ssl_server_cert_type() via default negotiation";
 $result = $node->safe_psql('trustdb',
 	"SELECT ssl_server_cert_type()",
 	connstr => "$common_connstr sslcert=invalid");
 like($result, qr/^(RSA|ECDSA)$/, 'ssl_server_cert_type() returns valid cert type');
 
-# Test 6: Verify ssl_server_cert_types() shows all loaded types
+# Test 7: Verify ssl_server_cert_types() shows all loaded types
 note "testing ssl_server_cert_types() shows all loaded cert types";
 $result = $node->safe_psql('trustdb',
 	"SELECT ssl_server_cert_types()",
@@ -139,7 +146,7 @@ $result = $node->safe_psql('trustdb',
 like($result, qr/RSA/, 'ssl_server_cert_types() includes RSA');
 like($result, qr/ECDSA/, 'ssl_server_cert_types() includes ECDSA');
 
-# Test 7: Verify server rejects mismatched alt cert/key configuration
+# Test 8: Verify server rejects mismatched alt cert/key configuration
 note "testing server rejects ssl_alt_cert_file without ssl_alt_key_file";
 
 $node->append_conf('sslconfig.conf',
@@ -153,7 +160,7 @@ my $log = slurp_file($node->logfile);
 like($log, qr/ssl_alt_cert_file is set but ssl_alt_key_file is not/,
 	'log contains expected error for missing ssl_alt_key_file');
 
-# Test 8: Verify the reverse mismatch (key without cert)
+# Test 9: Verify the reverse mismatch (key without cert)
 note "testing server rejects ssl_alt_key_file without ssl_alt_cert_file";
 
 ok(unlink($node->data_dir . '/sslconfig.conf'));
-- 
2.52.0

From 724917cd546d24ac99f3d07c87c2c29a36884616 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Renaud=20M=C3=A9trich?= <[email protected]>
Date: Mon, 15 Jun 2026 09:41:58 +0200
Subject: [PATCH v2 8/8] Expand test coverage for dual certificate support
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Add tests for:
- Same-type alt cert rejection (both RSA -> startup error)
- Single cert mode regression (no alt cert still works)
- SIGHUP reload adds alt cert (ECDSA becomes available)
- SIGHUP reload removes alt cert (ECDSA no longer available)

Remove the global ssl_max_protocol_version=TLSv1.2 restriction so
TLS 1.3 connections are actually exercised (the openssl s_client
tests already force -tls1_2 explicitly).

Refactor config rewriting into a write_sslconfig() helper.

29 subtests total, up from 22.

Author: Renaud Métrich <[email protected]>
---
 src/test/ssl/t/004_ssl_alt_cert.pl | 111 ++++++++++++++++++++++-------
 1 file changed, 86 insertions(+), 25 deletions(-)

diff --git a/src/test/ssl/t/004_ssl_alt_cert.pl b/src/test/ssl/t/004_ssl_alt_cert.pl
index a148e0a7fb1..b4973bfc996 100644
--- a/src/test/ssl/t/004_ssl_alt_cert.pl
+++ b/src/test/ssl/t/004_ssl_alt_cert.pl
@@ -77,10 +77,6 @@ $node->append_conf('sslconfig.conf',
 $node->append_conf('sslconfig.conf',
 	"ssl_alt_key_file='$ecdsa_key'");
 
-# Force TLS 1.2 so we can control cipher selection
-$node->append_conf('sslconfig.conf',
-	"ssl_max_protocol_version='TLSv1.2'");
-
 $node->restart;
 
 #### Tests.
@@ -146,16 +142,34 @@ $result = $node->safe_psql('trustdb',
 like($result, qr/RSA/, 'ssl_server_cert_types() includes RSA');
 like($result, qr/ECDSA/, 'ssl_server_cert_types() includes ECDSA');
 
+# Helper to rewrite sslconfig.conf from scratch
+sub write_sslconfig
+{
+	my ($node, %opts) = @_;
+	my $conf = $node->data_dir . '/sslconfig.conf';
+	unlink($conf);
+	$node->append_conf('sslconfig.conf', "ssl=on");
+	$node->append_conf('sslconfig.conf',
+		"ssl_ca_file='root+client_ca.crt'");
+	$node->append_conf('sslconfig.conf',
+		"ssl_cert_file='server-cn-only.crt'");
+	$node->append_conf('sslconfig.conf',
+		"ssl_key_file='server-cn-only.key'");
+	foreach my $key (sort keys %opts)
+	{
+		$node->append_conf('sslconfig.conf', "$key=$opts{$key}");
+	}
+}
+
 # Test 8: Verify server rejects mismatched alt cert/key configuration
 note "testing server rejects ssl_alt_cert_file without ssl_alt_key_file";
 
-$node->append_conf('sslconfig.conf',
-	"ssl_alt_key_file=''");
+write_sslconfig($node,
+	ssl_alt_cert_file => "'$ecdsa_crt'");
 
 $result = $node->restart(fail_ok => 1);
 is($result, 0, 'restart fails with ssl_alt_cert_file set but ssl_alt_key_file empty');
 
-# Check the log for the expected error message
 my $log = slurp_file($node->logfile);
 like($log, qr/ssl_alt_cert_file is set but ssl_alt_key_file is not/,
 	'log contains expected error for missing ssl_alt_key_file');
@@ -163,16 +177,8 @@ like($log, qr/ssl_alt_cert_file is set but ssl_alt_key_file is not/,
 # Test 9: Verify the reverse mismatch (key without cert)
 note "testing server rejects ssl_alt_key_file without ssl_alt_cert_file";
 
-ok(unlink($node->data_dir . '/sslconfig.conf'));
-$node->append_conf('sslconfig.conf', "ssl=on");
-$node->append_conf('sslconfig.conf',
-	"ssl_ca_file='root+client_ca.crt'");
-$node->append_conf('sslconfig.conf',
-	"ssl_cert_file='server-cn-only.crt'");
-$node->append_conf('sslconfig.conf',
-	"ssl_key_file='server-cn-only.key'");
-$node->append_conf('sslconfig.conf',
-	"ssl_alt_key_file='$ecdsa_key'");
+write_sslconfig($node,
+	ssl_alt_key_file => "'$ecdsa_key'");
 
 $result = $node->restart(fail_ok => 1);
 is($result, 0, 'restart fails with ssl_alt_key_file set but ssl_alt_cert_file empty');
@@ -181,15 +187,70 @@ $log = slurp_file($node->logfile);
 like($log, qr/ssl_alt_key_file is set but ssl_alt_cert_file is not/,
 	'log contains expected error for missing ssl_alt_cert_file');
 
-# Restore valid config so the node can be stopped cleanly
-ok(unlink($node->data_dir . '/sslconfig.conf'));
-$node->append_conf('sslconfig.conf', "ssl=on");
-$node->append_conf('sslconfig.conf',
-	"ssl_ca_file='root+client_ca.crt'");
+# Test 10: Verify server rejects same-type alt cert (both RSA)
+note "testing server rejects same-type alternate certificate";
+
+write_sslconfig($node,
+	ssl_alt_cert_file => "'server-cn-only.crt'",
+	ssl_alt_key_file => "'server-cn-only.key'");
+
+$result = $node->restart(fail_ok => 1);
+is($result, 0, 'restart fails with same-type alt cert');
+
+$log = slurp_file($node->logfile);
+like($log, qr/alternate certificate has the same key type/,
+	'log contains expected error for same-type alt cert');
+
+# Test 11: Verify single cert still works (no alt cert regression)
+note "testing single cert mode (no alt cert)";
+
+write_sslconfig($node);
+$node->start;
+
+$node->connect_ok(
+	"$common_connstr sslcert=invalid",
+	"connect with single RSA cert (no alt cert)",
+	sql => "SELECT 1");
+
+$result = $node->safe_psql('trustdb',
+	"SELECT ssl_server_cert_type()",
+	connstr => "$common_connstr sslcert=invalid");
+is($result, 'RSA', 'ssl_server_cert_type() returns RSA with single cert');
+
+# Test 12: Verify SIGHUP reload adds alt cert
+note "testing SIGHUP reload adds alt certificate";
+
 $node->append_conf('sslconfig.conf',
-	"ssl_cert_file='server-cn-only.crt'");
+	"ssl_alt_cert_file='$ecdsa_crt'");
 $node->append_conf('sslconfig.conf',
-	"ssl_key_file='server-cn-only.key'");
-$node->start;
+	"ssl_alt_key_file='$ecdsa_key'");
+$node->reload;
+
+# Wait briefly for reload to take effect
+sleep(1);
+
+# Verify ECDSA cipher now works after reload
+my $openssl_after_reload = `echo "QUIT" | openssl s_client -connect $SERVERHOSTADDR:${\$node->port} -starttls postgres -tls1_2 -cipher ECDHE-ECDSA-AES256-GCM-SHA384 2>&1`;
+like($openssl_after_reload, qr/ECDHE-ECDSA-AES256-GCM-SHA384/,
+	'ECDSA cipher works after SIGHUP reload');
+
+# Test 13: Verify SIGHUP reload removes alt cert
+note "testing SIGHUP reload removes alt certificate";
+
+write_sslconfig($node);
+$node->reload;
+
+sleep(1);
+
+# Verify ECDSA cipher no longer works after removing alt cert
+my $openssl_after_remove = `echo "QUIT" | openssl s_client -connect $SERVERHOSTADDR:${\$node->port} -starttls postgres -tls1_2 -cipher ECDHE-ECDSA-AES256-GCM-SHA384 2>&1`;
+unlike($openssl_after_remove, qr/ECDHE-ECDSA-AES256-GCM-SHA384/,
+	'ECDSA cipher fails after alt cert removed via SIGHUP');
+
+# RSA still works
+$node->connect_ok(
+	"$common_connstr sslcert=invalid",
+	"RSA connection works after alt cert removed via SIGHUP",
+	sql => "SELECT 1");
 
 done_testing();
-- 
2.52.0

Attachment: OpenPGP_signature.asc
Description: OpenPGP digital signature

Reply via email to