]> git.ipfire.org Git - thirdparty/curl.git/commitdiff
OpenSSL: check reuse of sessions for verify status
authorStefan Eissing <stefan@eissing.org>
Tue, 27 Jan 2026 12:28:09 +0000 (13:28 +0100)
committerDaniel Stenberg <daniel@haxx.se>
Tue, 27 Jan 2026 13:03:47 +0000 (14:03 +0100)
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

lib/vtls/gtls.c
lib/vtls/openssl.c
tests/http/test_02_download.py
tests/http/test_08_caddy.py
tests/http/test_17_ssl_use.py
tests/http/test_19_shutdown.py
tests/libtest/cli_hx_download.c

index 80cd588f1c69546b5f0bcf446a9af7d0bbe81852..f4cbe88080edecc6e7a4694d325aebf18c400d6c 100644 (file)
@@ -1572,8 +1572,6 @@ static CURLcode glts_apple_verify(struct Curl_cfilter *cf,
   result = Curl_vtls_apple_verify(cf, data, peer, chain->num_certs,
                                   gtls_chain_get_der, chain, NULL, 0);
   *pverified = !result;
-  if(*pverified)
-    infof(data, "  SSL certificate verified by Apple SecTrust.");
   return result;
 }
 #endif /* USE_APPLE_SECTRUST */
index f0e89b617ce207edc0a220b0ae9b817634d26595..ae1fe6cbb1ab15c203fb396d64d8740a47f308f5 100644 (file)
@@ -3371,32 +3371,42 @@ ossl_init_session_and_alpns(struct ossl_ctx *octx,
                               sizeof(error_buffer)));
         }
         else {
-          infof(data, "SSL reusing session with ALPN '%s'",
-                scs->alpn ? scs->alpn : "-");
-          octx->reused_session = TRUE;
+          if(conn_cfg->verifypeer &&
+             (SSL_get_verify_result(octx->ssl) != X509_V_OK)) {
+            /* Session was from unverified connection, cannot reuse here */
+            SSL_set_session(octx->ssl, NULL);
+            infof(data, "SSL session not peer verified, not reusing");
+          }
+          else {
+            infof(data, "SSL reusing session with ALPN '%s'",
+                  scs->alpn ? scs->alpn : "-");
+            octx->reused_session = TRUE;
+            infof(data, "SSL verify result: %lx",
+                  SSL_get_verify_result(octx->ssl));
 #ifdef HAVE_OPENSSL_EARLYDATA
-          if(ssl_config->earlydata && scs->alpn &&
-             SSL_SESSION_get_max_early_data(ssl_session) &&
-             !cf->conn->connect_only &&
-             (SSL_version(octx->ssl) == TLS1_3_VERSION)) {
-            bool do_early_data = FALSE;
-            if(sess_reuse_cb) {
-              result = sess_reuse_cb(cf, data, &alpns, scs, &do_early_data);
-              if(result) {
-                SSL_SESSION_free(ssl_session);
-                return result;
+            if(ssl_config->earlydata && scs->alpn &&
+               SSL_SESSION_get_max_early_data(ssl_session) &&
+               !cf->conn->connect_only &&
+               (SSL_version(octx->ssl) == TLS1_3_VERSION)) {
+              bool do_early_data = FALSE;
+              if(sess_reuse_cb) {
+                result = sess_reuse_cb(cf, data, &alpns, scs, &do_early_data);
+                if(result) {
+                  SSL_SESSION_free(ssl_session);
+                  return result;
+                }
+              }
+              if(do_early_data) {
+                /* We only try the ALPN protocol the session used before,
+                 * otherwise we might send early data for the wrong protocol */
+                Curl_alpn_restrict_to(&alpns, scs->alpn);
               }
             }
-            if(do_early_data) {
-              /* We only try the ALPN protocol the session used before,
-               * otherwise we might send early data for the wrong protocol */
-              Curl_alpn_restrict_to(&alpns, scs->alpn);
-            }
-          }
 #else
-          (void)ssl_config;
-          (void)sess_reuse_cb;
+            (void)ssl_config;
+            (void)sess_reuse_cb;
 #endif
+          }
         }
         SSL_SESSION_free(ssl_session);
       }
@@ -4681,8 +4691,15 @@ static CURLcode ossl_apple_verify(struct Curl_cfilter *cf,
 
   if(!chain.num_certs &&
      (conn_config->verifypeer || conn_config->verifyhost)) {
-    failf(data, "SSL: could not get peer certificate");
-    result = CURLE_PEER_FAILED_VERIFICATION;
+    if(!octx->reused_session) {
+      failf(data, "SSL: could not get peer certificate chain");
+      result = CURLE_PEER_FAILED_VERIFICATION;
+    }
+    else {
+      /* when session was reused, there is no peer cert chain */
+      *pverified = FALSE;
+      return CURLE_OK;
+    }
   }
   else {
 #ifdef HAVE_BORINGSSL_LIKE
@@ -4758,6 +4775,7 @@ CURLcode Curl_ossl_check_peer_cert(struct Curl_cfilter *cf,
 
   ossl_verify = SSL_get_verify_result(octx->ssl);
   ssl_config->certverifyresult = ossl_verify;
+  infof(data, "OpenSSL verify result: %lx", ossl_verify);
 
   verified = (ossl_verify == X509_V_OK);
   if(verified)
index ba11880b0b55193ed9a37fd4060f75cd88003fd9..b4ea2a42d1ca86120413eee3ec85c0b9f080a72e 100644 (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)
 
index 9229ce1f22b4e4215a69a84bcaec0dd447753346..b854dceb453cff5005904462b984dc189a350dd3 100644 (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',
index 4ef8ad1f941b9b7fc41df37e1ce5777b1d574797..34a8a2d3948a31284000553b3f886c54e5315444 100644 (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
         ])
index efa7657fe9390e0cdadb2599fb1db859ab09a2b7..7cb8ce6415f72a12ed76fc3036d2e6ec2ed381fd 100644 (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
index 9bd954240901e6aab45b64e6fa78e283286d35c7..51e0d46715379c2513010cc1e1ae646533f1101d 100644 (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;
 }