Resumen
La opción --cert-status de curl promete hacer fallar la transferencia si la respuesta OCSP grapada (stapled) por el servidor no es válida o no confirma el certificado. En las compilaciones que combinan GnuTLS con Apple SecTrust, esa promesa se rompe justo en el caso que importa: cuando GnuTLS detecta que la respuesta es mala.
El problema está en el mecanismo de reserva de lib/vtls/gtls.c (líneas 1511-1521 en la versión 8.23.0). Si la verificación de GnuTLS falla, curl le pasa la cadena a SecTrust, y si SecTrust la acepta, el resultado negativo de GnuTLS se descarta. Pero a SecTrust se le llama sin la respuesta OCSP grapada, de modo que nadie vuelve a examinarla con rigor:
- En 8.23.0 solo se comprueba que el servidor haya enviado una respuesta (
gtls.c:1436-1451). - En master se lee el código de estado, pero se da por hecho que GnuTLS ya validó la firma y las fechas (
gtls.c:1479-1483).
El efecto práctico es que --cert-status acepta una respuesta "revoked" correctamente firmada en 8.23.0, y una con firma falsificada o caducada tanto en 8.23.0 como en el master probado. Además, ocurre con las opciones por defecto, porque las compilaciones con SecTrust usan el almacén nativo salvo que se indique un fichero de CA (lib/vtls/vtls_config.c:287-288).
El investigador lo presenta como una vía de código distinta a la de su reporte #4118610 (OpenSSL con SecTrust, CVE-2026-111484). La corrección de aquel, cd89b5178b, pasa aquí a propósito NULL, 0, FALSE con el comentario "We check OCSP separately", y la de GnuTLS, 287df0b324, solo añadió la comprobación del código de estado.
Versiones afectadas
- 8.23.0: acepta respuestas revocadas, falsificadas y caducadas.
- master
629646d9b7: acepta falsificadas y caducadas. - 8.20.0 (probada): acepta falsificadas y caducadas.
- 8.18.0 a 8.20.x: tienen el mismo código en esta parte.
- 8.22.0: rechazó en las pruebas todas las respuestas firmadas por una CA intermedia, también las buenas.
Sobre 8.22.0, el rechazo se debe a la comprobación de firma de be6c4ee7fa, que también tiene 8.21.0 (ver el PR 23311 de curl). Ese commit razonaba además que una respuesta mala no podía pasar con la verificación del par activa, porque gnutls_certificate_verify_peers2() la detecta; esta reserva es justo el caso que ese razonamiento no contemplaba.
El origen está en dos commits. eefd03c572 (8.17.0) introdujo la reserva, pero solo para GNUTLS_CERT_SIGNER_NOT_FOUND, es decir, para raíces que GnuTLS no conoce. 6aa8fa3fdf (8.18.0, "Do the same for GnuTLS") la extendió a cualquier fallo de GnuTLS, incluidos los provocados por la respuesta OCSP.
La configuración no es solo de laboratorio: MacPorts ofrece curl +gnutls +sectrust, compilado con --with-gnutls --with-ca-bundle=... --with-apple-sectrust. La variante existe al menos desde su port de 8.19.0, y +http3 requiere +gnutls. Según el reporte, el port actual de 8.22.0 está en la ventana que falla de forma segura, 8.19.0 y 8.20.0 estaban afectados, y 8.23.0 lo estará cuando se actualice.
Pasos de reproducción
El entorno de prueba fue macOS 27.0.1 arm64 con GnuTLS 3.8.13 de Homebrew, y curl compilado desde las etiquetas curl-8_23_0, curl-8_22_0 y curl-8_20_0 y desde master, sin paquete de CA y, en 8.23.0 y master, también con uno (al estilo de MacPorts):
curl 8.23.0-DEV (Darwin) libcurl/8.23.0-DEV GnuTLS/3.8.13 zlib/1.2.12
curl 8.23.1-DEV (Darwin) libcurl/8.23.1-DEV GnuTLS/3.8.13 zlib/1.2.12 (master 629646d9b7)
Features: alt-svc AppleSecTrust AsynchDNS HSTS HTTPS-proxy IPv6 Largefile libz SSL threadsafe UnixSockets
Todo se ejecuta en 127.0.0.1, con los scripts del directorio poc/ adjuntos al reporte (el generador de PKI es el mismo de #4118610).
Paso 1. Compilar curl 8.23.0 o master con estas opciones:
-DCURL_USE_GNUTLS=ON -DUSE_APPLE_SECTRUST=ON
Paso 2. Generar la PKI: raíz, intermedia, servidor "localhost" y respuestas OCSP firmadas por la intermedia (buena, desconocida y revocada), además de una con firma falsificada y otra caducada.
python3 poc/mkpki2.py poc/pki2
Paso 3. Añadir la raíz como confiable en el llavero:
security add-trusted-cert -r trustRoot -k ~/Library/Keychains/login.keychain-db poc/pki2/root.pem
Paso 4. Levantar un servidor que grape la respuesta revocada:
openssl s_server -accept 127.0.0.1:23003 -cert poc/pki2/server.pem -key poc/pki2/server.key -cert_chain poc/pki2/chain.pem -status_file poc/pki2/ocsp-revoked.der -tls1_3 -www
Paso 5. Probar solo con GnuTLS. Falla con el error (60), porque GnuTLS rechaza la respuesta revocada (verify status 34, INVALID y REVOKED):
curl --cacert poc/pki2/root.pem --cert-status https://localhost:23003/
Paso 6. Repetir con SecTrust. En 8.23.0 el registro muestra "SSL certificate verified via Apple SecTrust." y se obtiene HTTP 200. El segundo comando, con las opciones por defecto, también devuelve 200:
curl -v --cacert poc/pki2/root.pem --ca-native --cert-status https://localhost:23003/
curl --cert-status https://localhost:23003/
Paso 7. Reiniciar el servidor con ocsp-forged-good.der o ocsp-stale-good.der y repetir los pasos 5 y 6. Resultado: (60) solo con GnuTLS y 200 con SecTrust, en 8.23.0 y en master.
Paso 8. Limpiar el llavero:
security delete-certificate -c "Verify Test Root CA" ~/Library/Keychains/login.keychain-db
El script poc/matrix.sh recorre todas las respuestas. Se usó la confianza del llavero, sin ningún tipo de hook, y TLS 1.3; con TLS 1.2 el resultado es el mismo. "Solo GnuTLS" es --cacert root.pem --cert-status; "con SecTrust" es lo mismo más --ca-native, o las opciones por defecto, que dan igual con un paquete de CA compilado.
respuesta grapada 8.23.0 GnuTLS 8.23.0 SecTrust master GnuTLS master SecTrust
ninguna (91) (91) (91) (91)
buena 200 200 200 200
revocada (60) 200 (!) (60) (91)
firma falsificada (60) 200 (!) (60) 200 (!)
caducada (60) 200 (!) (60) 200 (!)
La respuesta "unknown" queda fuera de la tabla: 8.23.0 la acepta incluso solo con GnuTLS, pero es otro problema, el que corrige 287df0b324.
Impacto
Quien tenga la clave de un certificado revocado y pueda interceptar la conexión puede superar --cert-status:
- En 8.23.0, grapando la propia respuesta "revoked" vigente de la CA.
- En 8.18.0 a 8.20.x y en el master probado, grapando una respuesta "good" antigua o fabricada por él mismo.
Es precisamente el ataque que la exigencia de OCSP stapling debe impedir. La comprobación de la respuesta desaparece justo cuando GnuTLS la considera mala, y las aplicaciones con libcurl que activan CURLOPT_SSL_VERIFYSTATUS en estas compilaciones quedan igual de expuestas.
Hace falta una configuración concreta (GnuTLS más SecTrust) y una posición de interceptación, además de la clave del certificado revocado. HackerOne recoge la severidad como baja, aunque el investigador señala que las condiciones coinciden con las de CVE-2026-7009 y CVE-2026-111484, ambas de severidad media, con GnuTLS en lugar de OpenSSL.
Remediación
El reporte figura como resuelto y plantea dos vías, aunque no indica cuál se aplicó finalmente:
- Entregar la respuesta grapada a SecTrust en esta ruta y exigir una respuesta positiva cuando
verifystatusesté activo, como hacecd89b5178bpara OpenSSL. Con este cambio aplicado sobre master (fix/gtls-sectrust-ocsp-fix.diff), solo pasaron las respuestas buenas, con opciones por defecto y con--cacertmás--ca-native, en TLS 1.2 y 1.3, y nada cambió sin--cert-status. - Impedir que la reserva anule un fallo en el que GnuTLS haya marcado
GNUTLS_CERT_REVOKED,GNUTLS_CERT_INVALID_OCSP_STATUSoGNUTLS_CERT_REVOCATION_DATA_SUPERSEDED. GnuTLS activa esas marcas incluso cuando no conoce la raíz (verify status 98, 1048642 y 4162 en las pruebas).
Qué aprender de este caso
- Cuando un verificador de reserva sustituye a otro, comprueba que cubra lo mismo: aquí SecTrust validaba la cadena, pero no recibía la respuesta OCSP, y aun así su "sí" borraba el "no" de GnuTLS.
- Revisa los commits que amplían una condición de reserva: pasar de
GNUTLS_CERT_SIGNER_NOT_FOUNDa cualquier fallo convirtió un caso concreto (raíz desconocida) en una forma de ignorar también errores de revocación. - Desconfía del código que asume que "esto ya lo validó otra capa", como la lectura del estado OCSP en master: en cuanto exista una ruta alternativa que se salte esa capa, la suposición deja de ser cierta.
- Al corregir un fallo en un backend TLS, busca el mismo patrón en los demás: este caso es el equivalente en GnuTLS del arreglado para OpenSSL con SecTrust, y una tabla de pruebas por tipo de respuesta y backend lo deja a la vista.