✓ProveIt
Examples / Report #3694390 Saved analysis

CVE-2026-7009: OCSP stapling bypass with Apple SecTrust

curl · 2026-04-2518.8s analysis · $0.025 in model calls

What the report claims

curl silently bypasses OCSP stapling checks when built with Apple SecTrust and --cert-status is set, allowing connections without a valid OCSP response.

Compare with curl's recorded outcome

Known vulnerability

A real vulnerability: CVE-2026-7009 OCSP stapling bypass with Apple SecTrust

Shown for comparison after analysis. The pipeline did not see this label.

ProveIt's finding

Code supports the claim

The code supports the reported flaw. A maintainer should review its impact and decide what to do next.

Why this finding?
  • The code at lib/vtls/openssl.c lines 4791 and 4809-4815 shows sectrust_verified causes the OCSP status check (verifystatus) to be skipped entirely when Apple SecTrust verifies the certificate chain.
  • verifystatus() at lines 2253-2256 is the only place that enforces 'No OCSP response received' failure; it is never reached when sectrust_verified is TRUE.
  • ossl_apple_verify at lines 4706-4711 only passes a stapled OCSP response to SecTrust when the server provides one (ocsp_len > 0); otherwise ocsp_len is clamped to 0 and SecTrust uses its default best-effort revocation policy.
  • Apple's documentation for SecTrustSetOCSPResponse confirms it only uses the stapled response when provided, otherwise other methods are used.
  • This violates the documented guarantee that --cert-status aborts when no OCSP response is received, and matches the vulnerability class of CVE-2024-8096 and CVE-2024-0853.
How this analysis ran 5 claims · 23 citations verified

Checked against curl tested commit bfcc1c6eb4 (bfcc1c6eb4d8). Dev evaluation split. Citation verification checks that a referenced line was in the evidence; it does not prove the model interpreted it correctly.

1 · Grounding
grounded
no model · 17 references checked out, 0 did not
2 · Claims
NVIDIA-Nemotron-3-Nano-30B-A3B
5 claims · 3432 tokens · $0.0004
3 · Evidence
3 code snippets
real curl source at bfcc1c6eb4d8 · 4 docs via Tavily
4 · Judgment
Nemotron-3-Ultra-550b-a55b
16218 tokens · 14.8s · $0.0244
5 · Receipts
23 of 23 citations verified
every citation must point at code the judge was shown
Review a draft reply to the reporter

Edit and verify before sending.

Thank you for the detailed report. The code confirms the flaw: when USE_APPLE_SECTRUST is enabled and SecTrust verifies the certificate (lib/vtls/openssl.c:4791), the sectrust_verified flag causes the OCSP stapling check (verifystatus) to be skipped entirely (lines 4809-4815). Since SecTrust only receives a stapled response when the server provides one (lines 4706-4711), a server without OCSP stapling bypasses the --cert-status enforcement. This violates the documented guarantee and is a valid security issue. We will prepare a fix.

Inspect all claims and source code 5 claims · 3 code excerpts

Report claims

C1flaw supported

When Apple SecTrust verifies the certificate chain, the code sets sectrust_verified = TRUE and then skips the OCSP status check because of the condition !sectrust_verified && conn_config->verifystatus, causing the verifystatus() function to never be called.

Read the reasoning

The code explicitly sets sectrust_verified = TRUE at line 4791 when SecTrust verification succeeds, then at lines 4809-4815 the OCSP status check (verifystatus) is conditional on !sectrust_verified, causing it to be skipped entirely when SecTrust handled verification.

C2location supported

The flaw occurs in lib/vtls/openssl.c in the Curl_ossl_check_peer_cert() function, specifically around the sectrust_verified flag handling.

Read the reasoning

The flaw is located in lib/vtls/openssl.c in the Curl_ossl_check_peer_cert function, specifically around lines 4734-4791 (sectrust_verified declaration and setting) and lines 4809-4815 (OCSP check bypass).

C3trigger supported

The trigger is the use of the --cert-status or CURLOPT_SSL_VERIFYSTATUS option together with building curl with USE_APPLE_SECTRUST=ON, which causes the code path that sets sectrust_verified to be taken.

Read the reasoning

The trigger requires USE_APPLE_SECTRUST=ON at build time (lines 4734, 4779), --cert-status / CURLOPT_SSL_VERIFYSTATUS at runtime (conn_config->verifystatus at line 4809), and the native CA store option (ssl_config->native_ca_store at line 4780) which maps to --ca-native.

C4impact supported

The impact is that the connection succeeds even when the server provides no OCSP staple, violating the documented guarantee that curl aborts when no OCSP response is received.

Read the reasoning

When sectrust_verified is TRUE, verifystatus() is never called (line 4815), so the 'No OCSP response received' check at line 2253-2255 never executes. Meanwhile, ossl_apple_verify only passes a stapled OCSP response to SecTrust when ocsp_len > 0 (lines 4706-4711); if the server doesn't staple, ocsp_len becomes 0 and SecTrust uses its default revocation policy without a positive OCSP response.

C5reproduction supported

The reproduction involves building curl with Apple SecTrust enabled, running a server that does not provide an OCSP staple, and observing that curl exits with success despite --cert-status being set.

Read the reasoning

The reproduction steps match the code paths: build with USE_APPLE_SECTRUST=ON (line 4734), run with --ca-native (native_ca_store at line 4780) and --cert-status (verifystatus at line 4809), use a server that doesn't staple OCSP (SSL_get_tlsext_status_ocsp_resp returns -1 at line 4706), and observe success instead of CURLE_SSL_INVALIDCERTSTATUS.

What the report says about other projects

confirmed SecTrustSetOCSPResponse only uses a stapled OCSP response when provided; if no data is available, SecTrust falls back to other revocation checking methods.

“This function allows the caller to provide OCSPResponse data (which may be obtained during a TLS/SSL handshake, per RFC3546) as input to a trust evaluation. If this data is available, it can obviate the need to contact an OCSP server for current revocation information.” source

Read the original report (excerpt)

Full report on HackerOne ↗

## Summary When curl is built with `--with-apple-sectrust` (or `-DUSE_APPLE_SECTRUST=ON`) and OpenSSL, the `--cert-status` / `CURLOPT_SSL_VERIFYSTATUS` option is silently bypassed when Apple SecTrust handles certificate chain verification instead of OpenSSL. The user explicitly requests OCSP stapling enforcement, but the connection succeeds even when the server provides no OCSP staple — violating the documented guarantee that curl "aborts the connection" when no OCSP response is received. ## Root Cause In `lib/vtls/openssl.c`, `Curl_ossl_check_peer_cert()`, commit `b4630ed8fa` (2025-10-31, fixing #19307) introduced a `sectrust_verified` flag that skips OpenSSL's `verifystatus()` call when Apple SecTrust verified the certificate chain: ```c #ifdef USE_APPLE_SECTRUST if(!verified && conn_config->verifypeer && ssl_config->native_ca_store) { result = ossl_apple_verify(cf, data, octx, peer, &verified); if(verified) { sectrust_verified = TRUE; } } #endif ... if(conn_config->verifystatus && #ifdef USE_APPLE_SECTRUST !sectrust_verified && /* <-- skips the OCSP check */ #endif !octx->reused_session) { result = verifystatus(cf, data, octx); /* never reached */ } ``` The assumption is "SecTrust already handled OCSP." But SecTrust only receives a stapled OCSP response when `ocsp_len > 0` (in `ossl_apple_verify`). If the server doesn't staple, `SSL_get_tlsext_status_ocsp_resp()` returns -1, which is clamped to 0, and `SecTrustSetOCSPResponse()` is never called. SecTrust's default revocation policy (`kSecRevocationUseAnyAvailableMethod`) is best-effort — `kSecRevocationRequirePositiveResponse` is explicitly `#if 0` in `apple.c`. Result: no OCSP enforcement happens at all. ## Affected Versions Introduced in commit `b4630ed8fa` (2025-10-31). Present in curl 8.17.0, 8.18.0, 8.19.0, and current master. ## Steps To Reproduce ### Step 1: Build two versions of curl Tested on curl master at commit `bfcc1c6eb4`. The bug was introduced in commit `b4630ed8fa` (2025-10-31), so any version >= 8.17.0 is affected. **Build A — OpenSSL only (control):** ``` cmake -DCURL_USE_OPENSSL=ON -DUSE_APPLE_SECTRUST=OFF ``` ``` curl --version curl 8.20.0-DEV (Darwin) libcurl/8.20.0-DEV OpenSSL/3.6.1 zlib/1.2.12 brotli/1.2.0 zstd/1.5.7 libidn2/2.3.8 libpsl/0.21.5 nghttp2/1.68.0 Release-Date: [unreleased] Protocols: dict file ftp ftps gopher go …

Source code

lib/vtls/openssl.clines 4722–4832 · definition of Curl_ossl_check_peer_cert
4722CURLcode Curl_ossl_check_peer_cert(struct Curl_cfilter *cf,
C2: Curl_ossl_check_peer_cert function definition
4723 struct Curl_easy *data,
4724 struct ossl_ctx *octx,
4725 struct ssl_peer *peer)
4726{
4727 struct connectdata *conn = cf->conn;
4728 struct ssl_config_data *ssl_config = Curl_ssl_cf_get_config(cf, data);
4729 struct ssl_primary_config *conn_config = Curl_ssl_cf_get_primary_config(cf);
4730 CURLcode result = CURLE_OK;
4731 long ossl_verify;
4732 X509 *server_cert;
4733 bool verified = FALSE;
4734#if !defined(OPENSSL_NO_OCSP) && defined(USE_APPLE_SECTRUST)
C2: sectrust_verified variable declared under USE_APPLE_SECTRUST
C3: #if !defined(OPENSSL_NO_OCSP) && defined(USE_APPLE_SECTRUST) guard for sectrust_verified
C5: USE_APPLE_SECTRUST build flag controls sectrust_verified code path
4735 bool sectrust_verified = FALSE;
4736#endif
4737
4738 if(data->set.ssl.certinfo && !octx->reused_session) {
4739 /* asked to gather certificate info. Reused sessions do not have cert
4740 chains */
4741 result = ossl_certchain(data, octx->ssl);
4742 if(result)
4743 return result;
4744 }
4745
4746 server_cert = SSL_get1_peer_certificate(octx->ssl);
4747 if(!server_cert) {
4748 /* no verification at all, this maybe acceptable */
4749 if(!(conn_config->verifypeer || conn_config->verifyhost))
4750 goto out;
4751
4752 failf(data, "SSL: could not get peer certificate");
4753 result = CURLE_PEER_FAILED_VERIFICATION;
4754 goto out;
4755 }
4756
4757#ifdef CURLVERBOSE
4758 result = ossl_infof_cert(cf, data, server_cert);
4759 if(result)
4760 goto out;
4761 infof_certstack(data, octx->ssl);
4762#endif
4763
4764 if(conn_config->verifyhost) {
4765 result = ossl_verifyhost(data, conn, peer, server_cert);
4766 if(result)
4767 goto out;
4768 }
4769 /* `verifyhost` is either OK or not requested from here on */
4770
4771 ossl_verify = SSL_get_verify_result(octx->ssl);
4772 ssl_config->certverifyresult = ossl_verify;
4773 infof(data, "OpenSSL verify result: %lx", ossl_verify);
4774
4775 verified = (ossl_verify == X509_V_OK);
4776 if(verified)
4777 infof(data, "SSL certificate verified via OpenSSL.");
4778
4779#ifdef USE_APPLE_SECTRUST
C2: Apple SecTrust verification block
C3: #ifdef USE_APPLE_SECTRUST guard for SecTrust verification path
4780 if(!verified && conn_config->verifypeer && ssl_config->native_ca_store) {
C3: Condition: !verified && conn_config->verifypeer && ssl_config->native_ca_store
C5: ssl_config->native_ca_store corresponds to --ca-native runtime flag
4781 /* we verify using Apple SecTrust *unless* OpenSSL already verified.
4782 * This may happen if the application intercepted the OpenSSL callback
4783 * and installed its own. */
4784 result = ossl_apple_verify(cf, data, octx, peer, &verified);
4785 if(result && (result != CURLE_PEER_FAILED_VERIFICATION))
4786 goto out; /* unexpected error */
4787 if(verified) {
4788 infof(data, "SSL certificate verified via Apple SecTrust.");
4789 ssl_config->certverifyresult = X509_V_OK;
4790#ifndef OPENSSL_NO_OCSP
4791 sectrust_verified = TRUE;
C1: sectrust_verified = TRUE set when Apple SecTrust verifies the certificate
4792#endif
4793 }
4794 }
4795#endif
4796
4797 if(!verified) {
4798 /* no trust established, report the OpenSSL status */
4799 if(conn_config->verifypeer) {
4800 failf(data, "SSL certificate OpenSSL verify result: %s (%ld)",
4801 X509_verify_cert_error_string(ossl_verify), ossl_verify);
4802 result = CURLE_PEER_FAILED_VERIFICATION;
4803 goto out;
4804 }
4805 infof(data, " SSL certificate verification failed, continuing anyway!");
4806 }
4807
4808#ifndef OPENSSL_NO_OCSP
4809 if(conn_config->verifystatus &&
C1: verifystatus check guarded by !sectrust_verified condition
C2: OCSP status check with sectrust_verified condition
C3: conn_config->verifystatus corresponds to --cert-status / CURLOPT_SSL_VERIFYSTATUS
C5: conn_config->verifystatus corresponds to --cert-status runtime flag
4810#ifdef USE_APPLE_SECTRUST
C1: #ifdef USE_APPLE_SECTRUST block with !sectrust_verified condition
4811 !sectrust_verified && /* already verified via apple sectrust, cannot
C1: comment: 'already verified via apple sectrust, cannot verifystate via OpenSSL in that case as it does not have the trust anchors'
4812 * verifystate via OpenSSL in that case as it
4813 * does not have the trust anchors */
4814#endif
4815 !octx->reused_session) {
C1: verifystatus() call only reached when !sectrust_verified
C4: verifystatus() never called when sectrust_verified is TRUE
4816 /* do not do this after Session ID reuse */
4817 result = verifystatus(cf, data, octx);
4818 if(result)
4819 goto out;
4820 }
4821#endif
4822
4823 result = ossl_check_issuer(cf, data, server_cert);
4824 if(result)
4825 goto out;
4826
4827 result = ossl_check_pinned_key(cf, data, server_cert);
4828
4829out:
4830 X509_free(server_cert);
4831 return result;
4832}
lib/vtls/openssl.clines 2225–2370 · definition of verifystatus
2225static CURLcode verifystatus(struct Curl_cfilter *cf,
2226 struct Curl_easy *data,
2227 struct ossl_ctx *octx)
2228{
2229 int i, ocsp_status;
2230#ifdef HAVE_BORINGSSL_LIKE
2231 const uint8_t *status;
2232#else
2233 unsigned char *status;
2234#endif
2235 const unsigned char *p;
2236 CURLcode result = CURLE_OK;
2237 OCSP_RESPONSE *rsp = NULL;
2238 OCSP_BASICRESP *br = NULL;
2239 X509_STORE *st = NULL;
2240 STACK_OF(X509) *ch = NULL;
2241 X509 *cert;
2242 OCSP_CERTID *id = NULL;
2243 int cert_status, crl_reason;
2244 ASN1_GENERALIZEDTIME *rev, *thisupd, *nextupd;
2245 int ret;
2246 long len;
2247
2248 (void)cf;
2249 DEBUGASSERT(octx);
2250
2251 len = (long)SSL_get_tlsext_status_ocsp_resp(octx->ssl, &status);
2252
2253 if(!status) {
C4: verifystatus() fails with 'No OCSP response received' when status is missing
2254 failf(data, "No OCSP response received");
C4: failf(data, "No OCSP response received")
2255 result = CURLE_SSL_INVALIDCERTSTATUS;
C4: result = CURLE_SSL_INVALIDCERTSTATUS
2256 goto end;
2257 }
2258 p = status;
2259 rsp = d2i_OCSP_RESPONSE(NULL, &p, len);
2260 if(!rsp) {
2261 failf(data, "Invalid OCSP response");
2262 result = CURLE_SSL_INVALIDCERTSTATUS;
2263 goto end;
2264 }
2265
2266 ocsp_status = OCSP_response_status(rsp);
2267 if(ocsp_status != OCSP_RESPONSE_STATUS_SUCCESSFUL) {
2268 failf(data, "Invalid OCSP response status: %s (%d)",
2269 OCSP_response_status_str(ocsp_status), ocsp_status);
2270 result = CURLE_SSL_INVALIDCERTSTATUS;
2271 goto end;
2272 }
2273
2274 br = OCSP_response_get1_basic(rsp);
2275 if(!br) {
2276 failf(data, "Invalid OCSP response");
2277 result = CURLE_SSL_INVALIDCERTSTATUS;
2278 goto end;
2279 }
2280
2281 ch = SSL_get_peer_cert_chain(octx->ssl);
2282 if(!ch) {
2283 failf(data, "Could not get peer certificate chain");
2284 result = CURLE_SSL_INVALIDCERTSTATUS;
2285 goto end;
2286 }
2287 st = SSL_CTX_get_cert_store(octx->ssl_ctx);
2288
2289 if(OCSP_basic_verify(br, ch, st, 0) <= 0) {
2290 failf(data, "OCSP response verification failed");
2291 result = CURLE_SSL_INVALIDCERTSTATUS;
2292 goto end;
2293 }
2294
2295 /* Compute the certificate's ID */
2296 cert = SSL_get1_peer_certificate(octx->ssl);
2297 if(!cert) {
2298 failf(data, "Error getting peer certificate");
2299 result = CURLE_SSL_INVALIDCERTSTATUS;
2300 goto end;
2301 }
2302
2303 for(i = 0; i < (int)sk_X509_num(ch); i++) {
2304 X509 *issuer = sk_X509_value(ch, (ossl_valsize_t)i);
2305 if(X509_check_issued(issuer, cert) == X509_V_OK) {
2306 /* Note to analysis tools: using SHA1 here is fine. The `id`
2307 * generated is used as a hash lookup key, not as a verifier
2308 * of the OCSP data itself. This all according to RFC 5019. */
2309 id = OCSP_cert_to_id(EVP_sha1(), cert, issuer);
2310 break;
2311 }
2312 }
2313 X509_free(cert);
2314
2315 if(!id) {
2316 failf(data, "Error computing OCSP ID");
2317 result = CURLE_SSL_INVALIDCERTSTATUS;
2318 goto end;
2319 }
2320
2321 /* Find the single OCSP response corresponding to the certificate ID */
2322 ret = OCSP_resp_find_status(br, id, &cert_status, &crl_reason, &rev,
2323 &thisupd, &nextupd);
2324 OCSP_CERTID_free(id);
2325 if(ret != 1) {
2326 failf(data, "Could not find certificate ID in OCSP response");
2327 result = CURLE_SSL_INVALIDCERTSTATUS;
2328 goto end;
2329 }
2330
2331 /* Validate the OCSP response issuing and update times.
2332 * - `thisupd` is the time the OCSP response was issued
2333 * - `nextupd` is the time the OCSP response should be updated
2334 * (valid life time assigned by the OCSP responder)
2335 * - 3rd param: how many seconds of clock skew we allow between
2336 * our clock and the instance that issued the OCSP response
2337 * - 4th param: how many seconds in the past `thisupd` may be, with
2338 * -1 meaning there is no limit. */
2339 if(!OCSP_check_validity(thisupd, nextupd, 300L, -1L)) {
2340 failf(data, "OCSP response has expired");
2341 result = CURLE_SSL_INVALIDCERTSTATUS;
2342 goto end;
2343 }
2344
2345 infof(data, "SSL certificate status: %s (%d)",
2346 OCSP_cert_status_str(cert_status), cert_status);
2347
2348 switch(cert_status) {
2349 case V_OCSP_CERTSTATUS_GOOD:
2350 break;
2351
2352 case V_OCSP_CERTSTATUS_REVOKED:
2353 result = CURLE_SSL_INVALIDCERTSTATUS;
2354 failf(data, "SSL certificate revocation reason: %s (%d)",
2355 OCSP_crl_reason_str(crl_reason), crl_reason);
2356 goto end;
2357
2358 case V_OCSP_CERTSTATUS_UNKNOWN:
2359 default:
2360 result = CURLE_SSL_INVALIDCERTSTATUS;
2361 goto end;
2362 }
2363
2364end:
2365 if(br)
2366 OCSP_BASICRESP_free(br);
2367 OCSP_RESPONSE_free(rsp);
2368
2369 return result;
2370}
lib/vtls/openssl.clines 4673–4719 · definition of ossl_apple_verify
4673static CURLcode ossl_apple_verify(struct Curl_cfilter *cf,
4674 struct Curl_easy *data,
4675 struct ossl_ctx *octx,
4676 struct ssl_peer *peer,
4677 bool *pverified)
4678{
4679 struct ssl_primary_config *conn_config = Curl_ssl_cf_get_primary_config(cf);
4680 struct ossl_certs_ctx chain;
4681 CURLcode result;
4682
4683 memset(&chain, 0, sizeof(chain));
4684 chain.sk = SSL_get_peer_cert_chain(octx->ssl);
4685 chain.num_certs = chain.sk ? sk_X509_num(chain.sk) : 0;
4686
4687 if(!chain.num_certs &&
4688 (conn_config->verifypeer || conn_config->verifyhost)) {
4689 if(!octx->reused_session) {
4690 failf(data, "SSL: could not get peer certificate chain");
4691 result = CURLE_PEER_FAILED_VERIFICATION;
4692 }
4693 else {
4694 /* when session was reused, there is no peer cert chain */
4695 *pverified = FALSE;
4696 return CURLE_OK;
4697 }
4698 }
4699 else {
4700#ifdef HAVE_BORINGSSL_LIKE
4701 const uint8_t *ocsp_data = NULL;
4702#else
4703 unsigned char *ocsp_data = NULL;
4704#endif
4705 long ocsp_len = 0;
4706 if(conn_config->verifystatus && !octx->reused_session)
C4: ossl_apple_verify only gets OCSP staple when verifystatus is set and not reused session
C5: Server without OCSP staple causes SSL_get_tlsext_status_ocsp_resp to return -1
4707 ocsp_len = (long)SSL_get_tlsext_status_ocsp_resp(octx->ssl, &ocsp_data);
4708
4709 /* SSL_get_tlsext_status_ocsp_resp() returns the length of the OCSP
4710 response data or -1 if there is no OCSP response data. */
4711 if(ocsp_len < 0)
C4: ocsp_len clamped to 0 when SSL_get_tlsext_status_ocsp_resp returns -1 (no staple)
4712 ocsp_len = 0; /* no data available */
4713 result = Curl_vtls_apple_verify(cf, data, peer, chain.num_certs,
4714 ossl_chain_get_der, &chain,
4715 ocsp_data, ocsp_len);
4716 }
4717 *pverified = !result;
4718 return result;
4719}