From 9619bf7020f592d954d1a85e7c0c2133dc1230bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Renaud=20M=C3=A9trich?= 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 --- 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