OpenSSL: check reuse of sessions for verify status

OpenSSL records its peer verification status inside its SSL_SESSION
objects. When a session is later reused, the SSL connection inherits
this verify status.

Session keys prevent reuse of sessions between connections that verify
the peer and those who do not. However, when Apple SecTrust is used
to verify a connection, this does not update the Sessions verify
status (and there is no setter). On session reuse, OpenSSL fails
the verification and Apple SecTrust cannot verify either since the
certificate peer chain is not available.

Fix this by checking the verification status on session reuse and
remove the session again if the peer needs to be verified, but the
session is not.

Reported-by: Christian Schmitza
Fixes #20435
Closes #20446
This commit is contained in:
Stefan Eissing 2026-01-27 13:28:09 +01:00 committed by Daniel Stenberg
parent af508e3641
commit 065b149df0
No known key found for this signature in database
GPG key ID: 5CC908FDB71E12C2
7 changed files with 73 additions and 37 deletions

View file

@ -286,7 +286,8 @@ class TestDownload:
if not client.exists():
pytest.skip(f'example client not built: {client.name}')
r = client.run(args=[
'-n', f'{count}', '-P', f'{pause_offset}', '-V', proto, url
'-n', f'{count}', '-P', f'{pause_offset}',
'-C', env.ca.cert_file, '-V', proto, url
])
r.check_exit_code(0)
srcfile = os.path.join(httpd.docs_dir, docname)
@ -305,7 +306,8 @@ class TestDownload:
pytest.skip(f'example client not built: {client.name}')
r = client.run(args=[
'-n', f'{count}', '-m', f'{max_parallel}',
'-P', f'{pause_offset}', '-V', proto, url
'-P', f'{pause_offset}', '-C', env.ca.cert_file,
'-V', proto, url
])
r.check_exit_code(0)
srcfile = os.path.join(httpd.docs_dir, docname)
@ -329,6 +331,7 @@ class TestDownload:
pytest.skip(f'example client not built: {client.name}')
r = client.run(args=[
'-n', f'{count}', '-m', f'{max_parallel}', '-a',
'-C', env.ca.cert_file,
'-P', f'{pause_offset}', '-V', proto, url
])
r.check_exit_code(0)
@ -354,6 +357,7 @@ class TestDownload:
pytest.skip(f'example client not built: {client.name}')
r = client.run(args=[
'-n', f'{count}', '-m', f'{max_parallel}', '-a',
'-C', env.ca.cert_file,
'-A', f'{abort_offset}', '-V', proto, url
])
r.check_exit_code(42) # CURLE_ABORTED_BY_CALLBACK
@ -379,6 +383,7 @@ class TestDownload:
pytest.skip(f'example client not built: {client.name}')
r = client.run(args=[
'-n', f'{count}', '-m', f'{max_parallel}', '-a',
'-C', env.ca.cert_file,
'-F', f'{fail_offset}', '-V', proto, url
])
r.check_exit_code(23) # CURLE_WRITE_ERROR
@ -487,7 +492,8 @@ class TestDownload:
if not client.exists():
pytest.skip(f'example client not built: {client.name}')
r = client.run(args=[
'-n', f'{count}', '-P', f'{pause_offset}', '-V', proto, url
'-n', f'{count}', '-P', f'{pause_offset}',
'-C', env.ca.cert_file, '-V', proto, url
])
r.check_exit_code(0)
srcfile = os.path.join(httpd.docs_dir, docname)
@ -549,6 +555,7 @@ class TestDownload:
pytest.skip(f'example client not built: {client.name}')
r = client.run(args=[
'-n', f'{count}',
'-C', env.ca.cert_file,
'-e', # use TLS earlydata
'-f', # forbid reuse of connections
'-r', f'{env.domain1}:{port}:127.0.0.1',
@ -596,6 +603,7 @@ class TestDownload:
r = client.run(args=[
'-n', f'{count}',
'-m', f'{max_parallel}',
'-C', env.ca.cert_file,
'-x', # always use a fresh connection
'-M', str(max_host_conns), # limit conns per host
'-r', f'{env.domain1}:{port}:127.0.0.1',
@ -634,6 +642,7 @@ class TestDownload:
r = client.run(args=[
'-n', f'{count}',
'-m', f'{max_parallel}',
'-C', env.ca.cert_file,
'-x', # always use a fresh connection
'-T', str(max_total_conns), # limit total connections
'-r', f'{env.domain1}:{port}:127.0.0.1',
@ -673,7 +682,8 @@ class TestDownload:
pytest.skip(f'example client not built: {client.name}')
r = client.run(args=[
'-n', f'{count}', '-m', f'{count}',
'-P', f'{pause_offset}', '-V', proto, url
'-P', f'{pause_offset}', '-C', env.ca.cert_file,
'-V', proto, url
])
r.check_exit_code(0)

View file

@ -184,6 +184,7 @@ class TestCaddy:
pytest.skip(f'example client not built: {client.name}')
r = client.run(args=[
'-n', f'{count}',
'-C', env.ca.cert_file,
'-e', # use TLS earlydata
'-f', # forbid reuse of connections
'-r', f'{env.domain1}:{caddy.port}:127.0.0.1',

View file

@ -382,7 +382,7 @@ class TestSSLUse:
if not env.have_h3():
pytest.skip("h3 not supported")
if not env.curl_uses_lib('quictls') and \
not (env.curl_uses_lib('openssl') and env.curl_uses_lib('ngtcp2')) and \
not env.curl_uses_lib('openssl') and \
not env.curl_uses_lib('gnutls') and \
not env.curl_uses_lib('wolfssl'):
pytest.skip("QUIC session reuse not implemented")
@ -395,6 +395,7 @@ class TestSSLUse:
r = client.run(args=[
'-n', f'{count}',
'-f', # forbid reuse of connections
'-C', env.ca.cert_file,
'-r', f'{env.domain1}:{env.port_for("h3")}:127.0.0.1',
'-V', 'h3', url
])

View file

@ -123,7 +123,8 @@ class TestShutdown:
if not client.exists():
pytest.skip(f'example client not built: {client.name}')
r = client.run(args=[
'-n', f'{count}', '-f', '-V', proto, url
'-n', f'{count}', '-f', '-C', env.ca.cert_file,
'-V', proto, url
])
r.check_exit_code(0)
shutdowns = [line for line in r.trace_lines
@ -199,6 +200,7 @@ class TestShutdown:
pytest.skip(f'example client not built: {client.name}')
r = client.run(args=[
'-n', f'{count}', # that many transfers
'-C', env.ca.cert_file,
'-f', # forbid conn reuse
'-m', '10', # max parallel
'-T', '5', # max total conns at a time

View file

@ -211,13 +211,13 @@ static int my_progress_d_cb(void *userdata,
static int setup_hx_download(CURL *curl, const char *url, struct transfer_d *t,
long http_version, struct curl_slist *host,
CURLSH *share, int use_earlydata,
int fresh_connect)
int fresh_connect, char *cafile)
{
curl_easy_setopt(curl, CURLOPT_SHARE, share);
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, http_version);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
if(cafile)
curl_easy_setopt(curl, CURLOPT_CAINFO, cafile);
curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "");
curl_easy_setopt(curl, CURLOPT_BUFFERSIZE, (long)(128 * 1024));
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, my_write_d_cb);
@ -292,11 +292,12 @@ static CURLcode test_cli_hx_download(const char *URL)
size_t max_host_conns = 0;
size_t max_total_conns = 0;
int fresh_connect = 0;
char *cafile = NULL;
CURLcode result = CURLE_OK;
(void)URL;
while((ch = cgetopt(test_argc, test_argv, "aefhm:n:xA:F:M:P:r:T:V:"))
while((ch = cgetopt(test_argc, test_argv, "aefhm:n:xA:C:F:M:P:r:T:V:"))
!= -1) {
const char *opt = coptarg;
curl_off_t num;
@ -329,6 +330,10 @@ static CURLcode test_cli_hx_download(const char *URL)
if(!curlx_str_number(&opt, &num, LONG_MAX))
abort_offset = (size_t)num;
break;
case 'C':
curlx_free(cafile);
cafile = curlx_strdup(coptarg);
break;
case 'F':
if(!curlx_str_number(&opt, &num, LONG_MAX))
fail_offset = (size_t)num;
@ -432,7 +437,7 @@ static CURLcode test_cli_hx_download(const char *URL)
t->curl = curl_easy_init();
if(!t->curl ||
setup_hx_download(t->curl, url, t, http_version, host, share,
use_earlydata, fresh_connect)) {
use_earlydata, fresh_connect, cafile)) {
curl_mfprintf(stderr, "[t-%zu] FAILED setup\n", i);
result = (CURLcode)1;
goto cleanup;
@ -515,7 +520,7 @@ static CURLcode test_cli_hx_download(const char *URL)
t->curl = curl_easy_init();
if(!t->curl ||
setup_hx_download(t->curl, url, t, http_version, host, share,
use_earlydata, fresh_connect)) {
use_earlydata, fresh_connect, cafile)) {
curl_mfprintf(stderr, "[t-%zu] FAILED setup\n", i);
result = (CURLcode)1;
goto cleanup;
@ -566,6 +571,7 @@ cleanup:
optcleanup:
curlx_free(resolve);
curlx_free(cafile);
return result;
}