HexBrain

curl · Report #4124276

GnuTLS + Apple SecTrust discards authenticated OCSP failure

N/Astatus
mediumseverity
2upvotes
26 Oct 2026disclosed

How it was found

Summary

curl 8.23.0 and current master can accept a TLS certificate despite an explicit CURLOPT_SSL_VERIFYSTATUS=1 / --cert-status requirement when curl uses the GnuTLS backend together with Apple SecTrust.

GnuTLS correctly marks an invalid or revoked stapled OCSP response as invalid. Because a certificate trusted only by the Apple keychain is not trusted by GnuTLS's file store, curl then invokes SecTrust. The GnuTLS caller passes no OCSP response to SecTrust and does not require OCSP:

Curl_vtls_apple_verify(cf, data, peer, chain->num_certs,
                       gtls_chain_get_der, chain, NULL, 0, FALSE);

If SecTrust accepts the certificate chain, curl clears GnuTLS's entire verification result, including GNUTLS_CERT_INVALID_OCSP_STATUS.

  • Released curl 8.23.0 subsequently checks only that a staple exists and therefore accepts an authentic REVOKED response.
  • Current master parses the declared status but does not authenticate the OCSP responder or signature. It therefore accepts a matching GOOD response signed by an unrelated, self-signed certificate.

Peer and hostname verification remain enabled. --insecure is not used.

Affected versions and configuration

Confirmed affected:

  • curl 8.23.0 (3163ec065643ba20709f190111f42c3e1fe95273)
  • master at 59d2537b40f9929fde00399be0f274001ad36aea

Required configuration:

  • an Apple operating system;
  • curl built with GnuTLS and Apple SecTrust; and
  • CURLOPT_SSL_VERIFYSTATUS=1 / --cert-status while native CA verification is active.

Apple SecTrust support began in 8.17.0. The bypass described here begins with 5e727ca855429a684614a24646cb06fd965a1f7d, which removed curl's independent GnuTLS OCSP signature verification. That commit is in 8.23.0 but not 8.22.0. Earlier affected configurations fail closed instead of accepting the bad staple.

Reproduction

The attached bundle contains a Linux harness so reviewers can deterministically exercise curl's exact USE_APPLE_SECTRUST GnuTLS branch without changing a macOS trust store. It replaces only Curl_vtls_apple_verify() with a narrow stub that:

  1. returns success, modeling SecTrust accepting a locally trusted chain; and
  2. fails if the GnuTLS caller passes any OCSP bytes or asks SecTrust to require OCSP.

lib/vtls/gtls.c, the TLS handshake, GnuTLS verification, OCSP parser, HTTP request, and response are unmodified. Thus this harness tests both the exact fallback branch and its exact SecTrust arguments; it does not bypass or replace the vulnerable OCSP decision.

Prerequisites are CMake, a C compiler, GnuTLS development headers, OpenSSL's CLI, and patch.

For current master:

./reproduce-linux.sh /path/to/curl-current current

Observed:

current unrelated-signer GOOD: exit=0
* SecTrust harness accepted the certificate-only check
* SSL certificate verified via Apple SecTrust.
< HTTP/1.0 200 ok

current signed REVOKED control: exit=91
* SecTrust harness accepted the certificate-only check
* SSL certificate verified via Apple SecTrust.
* peer certificate is revoked

For the 8.23.0 source tree:

./reproduce-linux.sh /path/to/curl-8.23.0 8.23.0

Observed:

8.23.0 authentic REVOKED: exit=0
* SecTrust harness accepted the certificate-only check
* SSL certificate verified via Apple SecTrust.
< HTTP/1.0 200 ok

A control build without the SecTrust-success fallback rejects the same forged GOOD staple with curl exit 60:

SSL certificate verification failed: attached OCSP status response is invalid

All runs used ASan and UBSan and produced no sanitizer diagnostic. This is a logical verification bypass, not a memory-safety issue.

Direct macOS confirmation

  1. Run ./generate-fixtures.sh.
  2. Build the target curl revision on macOS with GnuTLS and USE_APPLE_SECTRUST=ON.
  3. Add evidence/root.pem as a trusted root in a disposable user keychain that SecTrust searches. Do not pass this root through --cacert; GnuTLS must lack the anchor so that the documented SecTrust fallback is exercised.
  4. Serve the fixture leaf and staple:
openssl s_server -accept 24443 -www -tls1_2 \
  -cert evidence/leaf.pem -key evidence/leaf.key \
  -status_file evidence/forged-good.der
  1. In another terminal, run:
curl --resolve tls.test:24443:127.0.0.1 \
  --cert-status https://tls.test:24443/

Current master returns 0 for forged-good.der. Curl 8.23.0 returns 0 even when the server uses signed-revoked.der. A build given the root with --cacert evidence/root.pem rejects the forged response because GnuTLS then retains authority over its complete verification result.

The generated leaf deliberately has no AIA or CRL endpoint. SecTrust therefore has no independent online revocation source; because the GnuTLS caller passes require_ocsp=false, its best-effort revocation policy can accept the trusted chain without authenticating the server's staple.

Root cause

gnutls_certificate_verify_peers2() produces an invalid status containing GNUTLS_CERT_INVALID_OCSP_STATUS. gtls_verify_cert() then treats successful SecTrust chain validation as replacing that complete result:

verified = !result;
if(verified) {
  *certverifyresult = 0;
}

However, the preceding SecTrust call received NULL, 0, FALSE, so SecTrust never authenticated the stapled response.

Current gtls_verify_ocsp_status() imports the response, matches its certificate ID, and reads its declared GOOD/REVOKED value. It never calls gnutls_ocsp_resp_verify() or another signature-verification function. Its comment assumes that GnuTLS's result is still authoritative, but that result was just discarded by the SecTrust fallback.

Why the recent fixes do not cover this

  • PR #23420 / commit 287df0b324 added GnuTLS response parsing. It does not authenticate a response after the SecTrust fallback replaces GnuTLS's invalid result.
  • PR #23430 / commit cd89b5178b passes the raw staple and require_ocsp=true from the OpenSSL caller. The GnuTLS caller explicitly passes NULL, 0, FALSE because the response is assumed to have been validated separately. That assumption fails precisely when SecTrust is the component that makes the chain acceptable.

Suggested fix

When GnuTLS verification falls back to SecTrust and verifystatus is enabled:

  1. retrieve the raw staple with gnutls_ocsp_status_request_get();
  2. pass those bytes to Curl_vtls_apple_verify() with require_ocsp=true;
  3. retain the existing parser to require a GOOD entry matching the peer certificate.

Merely preserving GNUTLS_CERT_INVALID_OCSP_STATUS would make valid native-only chains fail closed: GnuTLS cannot validate an OCSP signer whose trust anchor exists only in Apple's store. The verifier that supplies the missing trust anchor must authenticate the same stapled response.

proposed-fix.patch implements this plumbing against the tested master commit. It compiles cleanly in the exact-branch harness; the harness then rejects the forged response at the SecTrust boundary because OCSP bytes and require_ocsp=true now reach it.

Impact

An attacker who possesses the private key for a revoked but otherwise trusted server certificate can defeat a caller's explicit OCSP-stapling requirement and complete a TLS connection. This is an improper certificate-validation issue (CWE-295).

The closest curl precedents were both rated Medium:

  • CVE-2024-8096, "OCSP stapling bypass with GnuTLS"
  • CVE-2026-7009, "OCSP stapling bypass with Apple SecTrust"

The latter advisory explicitly limits itself to OpenSSL-compatible backends. This report concerns GnuTLS and code introduced after that advisory's fixed range.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original