curl · Report #4124276
GnuTLS + Apple SecTrust discards authenticated OCSP failure
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
REVOKEDresponse. - Current master parses the declared status but does not authenticate the OCSP
responder or signature. It therefore accepts a matching
GOODresponse 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-statuswhile 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:
- returns success, modeling SecTrust accepting a locally trusted chain; and
- 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
- Run
./generate-fixtures.sh. - Build the target curl revision on macOS with GnuTLS and
USE_APPLE_SECTRUST=ON. - Add
evidence/root.pemas 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. - 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
- 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
287df0b324added GnuTLS response parsing. It does not authenticate a response after the SecTrust fallback replaces GnuTLS's invalid result. - PR #23430 / commit
cd89b5178bpasses the raw staple andrequire_ocsp=truefrom the OpenSSL caller. The GnuTLS caller explicitly passesNULL, 0, FALSEbecause 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:
- retrieve the raw staple with
gnutls_ocsp_status_request_get(); - pass those bytes to
Curl_vtls_apple_verify()withrequire_ocsp=true; - retain the existing parser to require a
GOODentry 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.