Reports
MEDIA[TLS Y CRIPTOGRAFÍA]#4116076

curl con GnuTLS acepta respuestas OCSP grapadas falsas y da por bueno un certificado revocado con --cert-status

En curl 8.22.0 y 8.23.0 compilado con GnuTLS, --cert-status solo comprobaba que llegase alguna respuesta OCSP grapada, sin mirar su contenido. Un servidor con un certificado revocado podía colar una respuesta falsa de 5 bytes.

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 (en libcurl, CURLOPT_SSL_VERIFYSTATUS) sirve para exigir que el servidor TLS entregue, "grapada" en el handshake, una respuesta OCSP que confirme que su certificado no está revocado. La documentación es clara: si la respuesta no es válida, si indica que el certificado está revocado o si no llega ninguna, la verificación debe fallar.

En las compilaciones de curl con el backend GnuTLS, eso dejó de cumplirse. La función gtls_verify_ocsp_status() de lib/vtls/gtls.c quedó reducida a comprobar que había una respuesta grapada, sin importarla ni leer lo que decía:

rc = gnutls_ocsp_status_request_get(session, &status_request);
if(rc == GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE) {
  failf(data, "No OCSP response received");
  return CURLE_SSL_INVALIDCERTSTATUS;
}
else if(rc < 0) {
  failf(data, "Invalid OCSP response received");
  return CURLE_SSL_INVALIDCERTSTATUS;
}
return CURLE_OK;

Con que llegase algo, devolvía CURLE_OK. Ni el estado global de la respuesta (responseStatus) ni el estado del certificado (certStatus) se revisaban.

Es una regresión. El commit 5e727ca855 ("GnuTLS: remove our own OCSP signature verification", PR #23312), incluido en curl 8.23.0, eliminó las comprobaciones que había añadido el commit aeb1a281ca, que era precisamente la corrección de CVE-2024-8096 en 2024. El código suponía que gnutls_certificate_verify_peers2() ya tenía en cuenta la respuesta grapada, pero la prueba demuestra que no rechaza un certificado revocado cuando la respuesta es no exitosa o de estado desconocido. Al nuevo fallo se le asignó CVE-2026-111483.

Versiones afectadas, según el reporte: curl 8.22.0 y 8.23.0 con GnuTLS, y la rama master en el momento del reporte. Hasta la 8.21.0 la respuesta falsa se rechazaba (código de salida 91). Se reproduce con TLS 1.2 y TLS 1.3, tanto con la herramienta curl como con la API de libcurl.

Pasos de reproducción

Todo en local, con una CA de pruebas y un certificado firmado por ella pero marcado como revocado (motivo keyCompromise), en los ficheros rev.pem y rev.key.

  1. Crear una respuesta OCSP falsa y sin firmar de 5 bytes cuyo responseStatus es tryLater (3):
printf '\x30\x03\x0a\x01\x03' > trylater.der
  1. Levantar un servidor TLS en 127.0.0.1 que presente el certificado revocado y grape esa respuesta:
openssl s_server -quiet -naccept 20 -accept 127.0.0.1:30350 -tls1_2 \
  -cert rev.pem -key rev.key -status_file trylater.der -www &
  1. Conectar con curl compilado con GnuTLS exigiendo la comprobación OCSP:
curl --cacert ca.pem --cert-status \
  --resolve localhost:30350:127.0.0.1 https://localhost:30350/
  1. En curl 8.23.0 con GnuTLS la transferencia termina bien contra el certificado revocado:
RESULT: exit=0 http=200

Pruebas de control con la misma compilación:

good certificate + good staple      exit=0    (correct)
revoked certificate + no staple      exit=91   No OCSP response received
revoked certificate + signed revoked exit=60   certificate error

Con el backend OpenSSL, la misma respuesta falsa se rechaza como debe:

curl: (91) Invalid OCSP response status: trylater (3)

El investigador comprobó además que una respuesta firmada por la CA con certStatus=unknown también se aceptaba (salida 0), cuando debería fallar.

Impacto

Un atacante en posición de red que tenga un certificado revocado, pero aún firmado por una CA de confianza, para el host objetivo puede anular la comprobación de --cert-status o CURLOPT_SSL_VERIFYSTATUS en clientes curl/libcurl compilados con GnuTLS. Le basta con grapar una respuesta OCSP inventada: curl acepta el certificado revocado y continúa con la transferencia, que es justo lo que esa opción existe para impedir.

Remediación

El investigador propuso restaurar lo que eliminó 5e727ca855: importar la respuesta grapada y exigir que sea exitosa y que el certificado tenga estado good:

   if(rc == GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE) {
     failf(data, "No OCSP response received");
     return CURLE_SSL_INVALIDCERTSTATUS;
   }
   else if(rc < 0) {
     failf(data, "Invalid OCSP response received");
     return CURLE_SSL_INVALIDCERTSTATUS;
   }
+  else {
+    gnutls_ocsp_resp_t resp;
+    gnutls_ocsp_cert_status_t status = GNUTLS_OCSP_CERT_UNKNOWN;
+
+    if(gnutls_ocsp_resp_init(&resp) < 0) {
+      failf(data, "Invalid OCSP response received");
+      return CURLE_SSL_INVALIDCERTSTATUS;
+    }
+    if(gnutls_ocsp_resp_import(resp, &status_request) < 0 ||
+       gnutls_ocsp_resp_get_single(resp, 0, NULL, NULL, NULL, NULL,
+                                   &status, NULL, NULL, NULL, NULL) < 0) {
+      gnutls_ocsp_resp_deinit(resp);
+      failf(data, "Invalid OCSP response received");
+      return CURLE_SSL_INVALIDCERTSTATUS;
+    }
+    gnutls_ocsp_resp_deinit(resp);
+    if(status != GNUTLS_OCSP_CERT_GOOD) {
+      failf(data, "Server certificate OCSP status not good (%d)", (int)status);
+      return CURLE_SSL_INVALIDCERTSTATUS;
+    }
+  }

   return CURLE_OK;
 }

Con ese parche la respuesta buena sigue dando salida 0, y las respuestas tryLater, unauthorized y unknown dan salida 91, igual que con OpenSSL. El investigador apunta que una corrección completa quizá debería rechazar también las respuestas con una ventana thisUpdate/nextUpdate no válida, como hacía el código anterior a la regresión. El reporte figura como resuelto en HackerOne; el texto divulgado no detalla qué cambio exacto se incorporó finalmente en curl.

El reporte se envió el 15 de octubre de 2026 y se divulgó el 26 de octubre de 2026. El investigador indica que usó ayuda de IA en las pruebas y la revisión de código, y que la redacción y los hallazgos son suyos.

Qué aprender de este caso

  • Que exista una respuesta OCSP no significa que diga "válido": al revisar código de revocación, busca funciones que salgan con éxito tras gnutls_ocsp_status_request_get() (o su equivalente) sin importar la respuesta ni leer responseStatus y certStatus.
  • Los commits que "quitan código propio porque la librería ya lo hace" son terreno fértil para regresiones de seguridad: si lo eliminado era la corrección de un CVE, comprueba con la prueba de ese CVE que la librería de verdad cubre el caso.
  • Cuando un programa tiene varios backends TLS, ejecutar la misma prueba en todos es la forma más rápida de detectar el que se comporta distinto: aquí OpenSSL devolvía 91 y GnuTLS 0 con la misma respuesta falsa.
  • Una respuesta OCSP falsa cabe en 5 bytes (30 03 0a 01 03) y openssl s_server -status_file la grapa sin más; con eso y un certificado revocado se puede probar en local cualquier cliente que presuma de comprobar OCSP.