HexBrain

curl · Report #4123304

OpenSSL+Apple SecTrust: SecTrust accepts certificates that the app's CURLOPT_SSL_CTX_FUNCTION trust store or verify callback rejected

N/Astatus
lowseverity
3upvotes
26 Oct 2026disclosed

How it was found

Summary:

On OpenSSL builds with Apple SecTrust (USE_APPLE_SECTRUST, for example Homebrew's curl), libcurl runs the handshake with SSL_VERIFY_NONE, calls the application's CURLOPT_SSL_CTX_FUNCTION before it, and, if OpenSSL's verification fails, asks Apple SecTrust and accepts the server when SecTrust trusts it (lib/vtls/openssl.c:3946, :3979 and :4838-4847 in 8.23.0; :4868 in master). SecTrust is used by default unless CURLOPT_CAINFO, CURLOPT_CAPATH or CURLOPT_CAINFO_BLOB is set (lib/vtls/vtls_config.c:287-288), so when an application narrows trust in its SSL_CTX callback, by replacing the trust store with its own CA (as TAK's Commo library does, see Impact) or by rejecting the certificate in a cert verify callback (key pinning), SecTrust overrides that rejection for any certificate macOS trusts and CURLINFO_SSL_VERIFYRESULT reads 0. CURLOPT_SSL_CTX_FUNCTION(3) suggests using the callback "to add additional validation code for certificates", and curl published the same outcome for wolfSSL's CA cache as CVE-2026-82208 (Low): a certificate "rejected by the callback-selected store is then incorrectly accepted". The code comment anticipates that "the application intercepted the OpenSSL callback and installed its own" verification but skips SecTrust only when that verification succeeds, and 6aa8fa3fdf, which made SecTrust run after any OpenSSL failure, assumed that with a CAfile or CApath "the native store is disabled, so this does not affect use cases where users asks curl to use a specific set of trust anchors", which does not hold when the application sets its trust anchors or pin in the callback. The same binary rejects the server when the same CA is given with CURLOPT_CAINFO, an OpenSSL libcurl without SecTrust rejects in both cases, and an application that sets SSL_VERIFY_PEER itself aborts the handshake and is not affected.

Affected version

The replaced-store case since 8.17.0, the first release with SecTrust (eefd03c572, which fell back only on X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY); the verify-callback case since 8.18.0 (6aa8fa3fdf). Up to 8.23.0 and master 59d2537b40; libcurl only (the curl tool has no SSL_CTX callback). Tested on macOS 27.0.1 arm64 with OpenSSL 3.6.4:

libcurl/8.17.0-DEV OpenSSL/3.6.4 zlib/1.2.12     curl-8_17_0, -DCURL_USE_OPENSSL=ON -DUSE_APPLE_SECTRUST=ON
libcurl/8.23.0-DEV OpenSSL/3.6.4 zlib/1.2.12     curl-8_23_0, same options
libcurl/8.23.1-DEV OpenSSL/3.6.4 zlib/1.2.12     master 59d2537b40, same options
libcurl/8.22.0 OpenSSL/3.6.4 ... AppleIDN ...    Homebrew's curl as shipped (--with-apple-sectrust --with-ca-fallback)

Steps To Reproduce:

The attached zip works against any public HTTPS site, so no keychain changes are needed. In its poc/ directory, ./mkpki.sh && ./build.sh && ./run_public.sh example.com does steps 1 to 4 in one go.

  1. ./mkpki.sh creates pki/appca.pem (the only CA the application trusts) and pki/appsrv.pem (a certificate with an unrelated key).
  2. ./build.sh builds poc_ctx_store.c against Homebrew's libcurl, or ./build.sh <build dir> against a curl build with -DCURL_USE_OPENSSL=ON -DUSE_APPLE_SECTRUST=ON -DBUILD_STATIC_LIBS=ON. It sets only CURLOPT_URL, a write callback and one of the two SSL_CTX callbacks in poc_ctx_store.c; verification options stay at their defaults.
  3. ./poc https://example.com/ ctx pki/appca.pem: the callback replaces the store with an X509_STORE holding only appca.pem. Expected (60); actual 0 and HTTP 200. With VERBOSE=1 curl logs OpenSSL verify result: 14, then SSL certificate verified via Apple SecTrust.
  4. ./poc https://example.com/ pin pki/appsrv.pem: the callback's SSL_CTX_set_cert_verify_callback() verifies the chain and then rejects because the key does not match (X509_V_ERR_APPLICATION_VERIFICATION). Expected (60); actual 0 and HTTP 200 (OpenSSL verify result: 32, then verified via Apple SecTrust).
  5. Controls: ./poc https://example.com/ cainfo pki/appca.pem gives (60) on the same binary; on a libcurl without SecTrust, steps 3 and 4 give (60) with verify results 20 and 50; pinning example.com's real key gives 200 everywhere.
                                      ctx (own CA only)   pin mismatch   cainfo (control)
8.17.0 OpenSSL+SecTrust               0, HTTP 200         60 (50)        60
8.23.0 OpenSSL+SecTrust               0, HTTP 200         0, HTTP 200    60
master 59d2537b40 OpenSSL+SecTrust    0, HTTP 200         0, HTTP 200    60
Homebrew curl 8.22.0                  0, HTTP 200         0, HTTP 200    60
master, OpenSSL without SecTrust      60                  60             60

The zip also has the same matrix against local servers, verbose logs, the code excerpts and a tested fix: in SecTrust builds, do not enable the native store by default when CURLOPT_SSL_CTX_FUNCTION is set (CURLSSLOPT_NATIVE_CA still enables it). With it, steps 3 and 4 fail with (60), pinning the right key still works, and transfers without a callback still verify via SecTrust. A narrower alternative is to skip the fallback only when the callback replaced the store or installed a verify callback.

AI disclosure: I used an AI assistant (Claude) for code review, test setup and drafting. I re-ran the PoC myself on every build in the table above with the attached scripts; the logs in the zip are the raw output.

Impact

An application that limits which servers it trusts through CURLOPT_SSL_CTX_FUNCTION, to its own CA or to a pinned key, silently accepts every certificate macOS trusts when it runs on an OpenSSL libcurl built with Apple SecTrust, such as Homebrew's. An attacker who can intercept the connection and holds a certificate for the host from any CA the system trusts gets a normal transfer with CURLINFO_SSL_VERIFYRESULT 0; on managed Macs that includes the corporate or MDM-installed roots of TLS-inspection products, which are exactly what private-CA trust and pinning are used to keep out. Like CVE-2026-82208 this needs an application that narrows trust in the callback, and real code does: TAK's Commo library (github.com/TAK-Product-Center/atak-civ at 6cefd4c833, commoncommo/core/impl/urlrequestmanager.cpp:866-883 and :640-642) replaces the store with only the TAK server's CA, leaves CURLOPT_SSL_VERIFYPEER on and sets no CURLOPT_CAINFO, and it has an iOS build that links a static OpenSSL libcurl, while curl offers USE_APPLE_SECTRUST on all Apple platforms. Such an application fails open without any error once its libcurl is built with SecTrust, nothing tells its author that CURLOPT_CAINFO must be set to keep SecTrust out, and the same application is safe with any libcurl built without SecTrust.

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