Reports
MEDIA[TLS Y CRIPTOGRAFÍA]#4118610

curl con OpenSSL y Apple SecTrust acepta grapas OCSP falsificadas, caducadas o "unknown" pese a --cert-status

Cuando Apple SecTrust valida la cadena, curl deja de comprobar la grapa OCSP por su cuenta y SecTrust la evalúa en modo tolerante: una respuesta falsificada, caducada o "unknown" pasa --cert-status sin error.

Resumen
Resumen en castellano de un reporte público, no una traducción literal. El código y los comandos se mantienen como en el original.

Resumen

La opción --cert-status de curl promete que la conexión fallará si la respuesta OCSP grapada por el servidor (OCSP stapling) no es válida o no confirma el certificado. En compilaciones de curl con OpenSSL y soporte de Apple SecTrust, esa promesa solo se cumplía a medias.

El origen está en un arreglo anterior. El de CVE-2026-7009 (commit 51905671e0) convirtió en error fatal la ausencia de grapa cuando SecTrust verifica la cadena. Pero si la grapa existe, curl sigue sin llamar a su propia verifystatus() en lib/vtls/openssl.c:4864-4873, por la condición !octx->sectrust_verified, y delega el trabajo en SecTrust.

SecTrust recibe la grapa mediante SecTrustSetOCSPResponse() (lib/vtls/apple.c:229), pero la evalúa con la política de revocación tolerante de curl: kSecRevocationUseAnyAvailableMethod (apple.c:154). La opción estricta, kSecRevocationRequirePositiveResponse, está dentro de un #if 0 en apple.c:155-167. Así, una grapa que SecTrust no puede validar equivale a "sin información" y el handshake termina bien.

El comentario de ese #if 0 justifica el modo tolerante porque las respuestas OCSP positivas no están disponibles en general para los certificados de internet. El investigador señala que ese motivo no sirve aquí: quien usa --cert-status ya ha exigido una respuesta grapada, y por tanto una respuesta positiva es justo lo que pide.

Según el reporte, afecta de la 8.20.0 a la 8.23.0 y a master (780405b584). Se probó en macOS 27.0.1 arm64 con la 8.23.0 compilada desde la etiqueta curl-8_23_0 y con la 8.22.0 publicada en Homebrew, y ambas se comportan igual. Las versiones 8.20.0 y 8.21.0 contienen el mismo código.

La compilación 8.23.0 de la prueba usó estas opciones:

-DCURL_USE_OPENSSL=ON -DUSE_APPLE_SECTRUST=ON -DCURL_CA_BUNDLE=none -DCURL_CA_PATH=none

Pasos de reproducción

Todo ocurre en 127.0.0.1, con los scripts del directorio poc/ adjuntos al reporte original.

  1. Generar una PKI de prueba: raíz, intermediaria y servidor "localhost", además de grapas firmadas por la intermediaria (good, unknown, revoked) y dos defectuosas (firma falsificada y caducada):
python3 poc/mkpki2.py poc/pki2
  1. Añadir la raíz de prueba como confiable en el llavero, igual que en el reporte #3694390:
security add-trusted-cert -r trustRoot -k ~/Library/Keychains/login.keychain-db poc/pki2/root.pem
  1. Levantar un servidor TLS que grape la respuesta caducada:
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-stale-good.der -tls1_3 -www
  1. Conectar dejando la verificación a SecTrust. curl registra "SSL certificate verified via Apple SecTrust." y devuelve HTTP 200:
curl -v --ca-native --cert-status https://localhost:23003/
  1. Conectar con el mismo binario dejando la verificación a OpenSSL. Ahora falla con (91) OCSP response has expired:
curl --cacert poc/pki2/root.pem --cert-status https://localhost:23003/
  1. Retirar la raíz de prueba del llavero:
security delete-certificate -c "Verify Test Root CA" ~/Library/Keychains/login.keychain-db

El script poc/matrix.sh repite la prueba con cada grapa. Este es el resultado con el mismo binario 8.23.0, TLS 1.3 y sin ningún hook:

| Grapa enviada | Verifica SecTrust (--ca-native) | Verifica OpenSSL (--cacert) | |---|---|---| | ninguna | (91) No OCSP response received | (91) No OCSP response received | | good | 200 | 200 | | revoked | (60) | (91) revocation reason: keyCompromise | | firma falsificada | 200 | (91) OCSP response verification failed | | caducada (stale) | 200 | (91) OCSP response has expired | | unknown | 200 | (91) status verification FAILED |

La grapa caducada tenía su nextUpdate 20 días en el pasado. El investigador también adjuntó la salida en bruto para TLS 1.2 y para el binario de Homebrew.

Impacto

Con SecTrust como verificador, --cert-status solo detecta dos casos: que falte la grapa o que venga una "revoked" correctamente firmada. Todo lo demás se acepta.

Eso da una vía clara a un atacante que tenga la clave de un certificado revocado. Puede grapar una respuesta "good" antigua, capturada antes de la revocación, o cualquier respuesta "good" autofirmada, y el cliente la dará por buena. Es precisamente el ataque que debería frenar la exigencia de grapado OCSP.

Las condiciones son las mismas que en CVE-2026-7009, de severidad media: una compilación de la familia OpenSSL con Apple SecTrust en la que SecTrust verifique la cadena. Según el reporte, eso incluye todas esas compilaciones que no tengan su propio paquete de CA. En el curl de Homebrew, incluye cualquier certificado que encadene con una raíz confiable solo en el llavero de macOS, como una CA de empresa.

Remediación

El reporte figura como resuelto y recibió CVE propio (CVE-2026-111484), aunque el texto no detalla el cambio definitivo. La solución que propuso el investigador es añadir kSecRevocationRequirePositiveResponse a la política de revocación en Curl_vtls_apple_verify() cuando verifystatus está activo, y mantener el modo tolerante en el resto de transferencias.

Lo comprobó añadiendo solo esa marca (poc/requirepos_interpose.c): el mismo binario aceptó las grapas buenas y rechazó las unknown, revoked, falsificadas, caducadas y ausentes.

Qué aprender de este caso

  • Cuando un arreglo cubre un caso de una comprobación delegada, como aquí la grapa ausente, revisa también los demás resultados posibles de esa comprobación: firma mala, caducidad y estado "unknown". El fallo vivía en la variante que CVE-2026-7009 no tocó.
  • Si un cliente TLS tiene dos backends de verificación, lanza la misma matriz de respuestas inválidas contra ambos y compara resultados. La tabla de este reporte destapó el problema con una sola diferencia de opciones, --ca-native frente a --cacert.
  • Desconfía de condiciones del tipo !octx->sectrust_verified que saltan una verificación propia: comprueba que el componente en el que se delega aplica la misma exigencia y no un modo tolerante.
  • Cuando el usuario pide de forma explícita una garantía estricta, como --cert-status, esa petición debe endurecer la política del verificador. Un modo "best effort" pensado para el caso general no debería seguir vigente en ese camino.