Cuando firma un PDF, normalmente piensa en la clave de firma como algo que usted controla. Reside en un archivo .pfx que usted generó, protegido por una contraseña que eligió. El código que lee ese archivo parece fontanería, no un límite. Esa intuición es incorrecta en el momento en que el certificado deja de ser suyo. Una herramienta de escritorio que permite a un usuario elegir cualquier .pfx, un servidor que acepta una credencial cargada o un firmador por lotes que recibe certificados a través de la red, todos entregan bytes influenciados por un atacante a un analizador antes de que se procese un solo byte de firma. Un lector de PKCS#12 es una superficie de ataque, en el mismo sentido en que lo es un decodificador de imágenes o un cargador de fuentes
Este artículo recorre dos trampas contra las que todo lector de PKCS#12 debe protegerse, ambas en la ruta que importa una credencial de firma, y cómo HotPDF las cierra. Ninguna es exótica. Las dos vienen de la misma causa raíz que afecta a casi cualquier analizador binario escrito en un lenguaje con enteros de ancho fijo: una longitud o un recuento del archivo se confía un paso más allá de lo que debería. Una lleva a una lectura fuera de límites; la otra, a un proceso que se cuelga hasta que usted lo mata
Por dónde viajan los bytes
Importar un .pfx para firmar un documento no es una sola operación, es una tubería corta, y cada etapa analiza algo que un atacante puede haber escrito. El contenedor es una estructura PKCS#12 tal como se define en RFC 7292, un nido de bolsas AuthenticatedSafe envuelto al rededor de una cubierta cifrada que contiene la clave privada. Leerlo significa recorrer ASN.1, derivar una clave a partir de la contraseña, descifrar y luego entregar la clave RSA recuperada al código que crea la firma
En HotPDF, esas etapas se mapean en unidades distintas. La lógica del contenedor PKCS#12 reside en HPDFPFX. Cada etiqueta, longitud y valor que toca es decodificado por el lector ASN.1 en HPDFASN1. La derivación de claves y el descifrado PBES2 se encuentran en HPDFCrypt junto con PBKDF2HMACSHA256. Cuando se recupera la clave, HPDFRSA y el generador CMS SignedData en HPDFCMS la convierten en la firma separada incrustada en el PDF. El punto de entrada público que dirige toda la cadena es una sola llamada
// Impulsa la canalización completa: carga el PDF de marcador de posición, analiza el PFX,
// deriva la clave, construye CMS SignedData y escribe la salida firmada.
if THotPDF.SignPDFWithPFX('Prepared.pdf', 'Signed.pdf',
'signer.pfx', 'p@ssw0rd') then
// firma incrustada
else
// la firma no se completó
;
Cada byte de signer.pfx fluye a través de HPDFASN1 y HPDFPFX antes de que ocurra cualquier criptografía. Si esas dos unidades no tienen cuidado con lo que el archivo afirma, la criptografía posterior nunca tendrá la oportunidad de importar
Trampa uno: una longitud ASN.1 que desborda la guarda
ASN.1 en DER y BER codifica cada elemento como una etiqueta, una longitud y esa cantidad de bytes de contenido. La longitud es el campo en el que se debe confiar pero verificar, porque le indica al analizador qué tan lejos debe leer, y fue escrita por quien produjo el archivo. La norma X.690 §8.1.3 define dos codificaciones. La forma corta empaqueta una longitud de 0 a 127 en un solo byte. La forma larga, utilizada para cualquier tamaño mayor, emplea un byte inicial cuyos siete bits bajos indican el recuento de bytes de longitud que siguen, y luego esa cantidad de bytes big-endian transportan el valor real. Por lo tanto, cuatro bytes de longitud pueden declarar un tamaño de contenido que se aproxima a los cuatro gigabytes
Después de decodificar dicho valor, el analizador tiene que comprobar que el contenido realmente cabe dentro del búfer antes de confiar en él. La comprobación natural es confirmar que la posición actual más la longitud del contenido no supera el final de los datos. Escrita de la manera obvia, con la posición, la longitud del contenido y el total contenidos en enteros con signo de 32 bits, esa protección no funciona
// El peligro: aritmética de 32 bits con signo. Con ContentLen cerca de MaxInt,
// Pos + ContentLen se desborda a un valor NEGATIVO, por lo que la comparación
// es falsa y una longitud falsificada de ~2 GB pasa directamente.
if Pos + ContentLen > Total then
raise EHPDFASN1Error.Create('content overruns buffer');
El problema es la suma, no la comparación. Cuando ContentLen está cerca de MaxInt (2147483647), Pos + ContentLen desborda el rango con signo de 32 bits y se convierte en un número negativo. Una suma negativa nunca es mayor que Total, por lo que la protección reporta que todo está bien y permite que el analizador proceda con una longitud de contenido de aproximadamente dos gigabytes que el búfer no contiene. Lo que sucede a continuación es el daño: el lector asigna un búfer para esa longitud declarada y copia en él, un SetLength seguido de un Move que lee del origen. Al origen solo le quedan unos pocos cientos de bytes, por lo que la copia lee mucho más allá del final de la entrada, una lectura fuera de límites que, en el mejor de los casos, provoca una caída y, en el peor, filtra memoria del proceso adyacente en el análisis
La única guarda correcta ensancha la suma intermedia antes de la comparación, de modo que la adición no puede desbordar el tipo en el que se calcula. HotPDF promueve ambos operandos a Int64:
// Correcto: ambos operandos se amplían a Int64 antes de la suma, por lo que la suma
// no puede desbordarse. Ahora una longitud falsificada de 2 GB falla en la comprobación de límites.
if ContentLen < 0 then
raise EHPDFASN1Error.Create('negative content length after decoding.');
if Int64(Pos) + Int64(ContentLen) > Int64(Total) then
raise EHPDFASN1Error.Create('content overruns buffer');
Un Int64 contiene la suma de dos valores de 32 bits sin pérdida, así que la comparación ve el número real y rechaza la longitud forjada. La comprobación aparte de no negatividad sobre ContentLen cierra el caso gemelo en el que un valor decodificado queda negativo por sí solo. En HotPDF esta guarda vive en HPDFASN1ParseNode, la función que produce el nodo sobre el que se apoyan todos los demás auxiliares. Como HPDFASN1Content dimensiona su SetLength y su Move directamente a partir de la longitud de contenido del nodo, un nodo que superara una guarda débil envenenaría cada lectura tomada a partir de él. Aplicar el límite en el punto de decodificación es lo que hace seguras las funciones auxiliares que hay por encima
Trampa dos: un recuento de iteraciones PBKDF2 usado como arma
La segunda trampa no es un error de memoria, es el archivo diciéndole a su CPU cuánto tiene que trabajar. PKCS#12 protege su material de claves con PBES2, el esquema basado en contraseña de PKCS#5, especificado en el RFC 8018. PBES2 ejecuta una función de derivación de claves, aquí PBKDF2 con HMAC-SHA-256, y después un cifrador, aquí AES-256-CBC. PBKDF2 toma un recuento de iteraciones, y ese recuento es un parámetro que viaja en el archivo. Toda su razón de ser es ser lento: más iteraciones significa que cada conjetura de contraseña cuesta más, lo cual es bueno contra un atacante sin conexión. El RFC 8018 §4.2 dice explícitamente que un recuento mayor es mejor para la seguridad, y a propósito no fija ningún techo
Esa apertura está bien cuando el archivo lo generó usted. Es un arma cuando lo generó el atacante. El recuento de iteraciones es un factor de trabajo controlado por el atacante, y un factor de trabajo controlado por el atacante es una denegación de servicio por complejidad algorítmica. Un .pfx forjado puede codificar un recuento de iteraciones de miles de millones; un analizador que lo lee diligentemente y llama a PBKDF2 para esas rondas de HMAC-SHA-256 hace que el proceso desaparezca en un bucle que no volverá en minutos u horas con un solo archivo suministrado. En un servidor de firma que procesa una credencial por petición, una única subida maliciosa deja a un trabajador bloqueado
El recuento empeora el desbordamiento antes de hacer girar la CPU. El valor de iteración vive en el archivo como un INTEGER de ASN.1, que no tiene ancho fijo, mientras que el campo que PBKDF2 consume finalmente es un Integer de 32 bits. Decodificar el INTEGER directamente en ese campo trunca los valores grandes, y un valor forjado para caer en el bit de signo reaparece como negativo o como algún número pequeño sin relación, de modo que ni siquiera el tamaño del trabajo es lo que el archivo parecía pedir. HotPDF lee el valor a ancho completo y lo acota antes de estrecharlo:
// Lea el número de iteraciones como Int64 primero y luego recorte a un rango razonable
// ANTES de que se reduzca al campo Iterations de 32 bits que usa PBKDF2.
LIter := HPDFASN1ToInteger(Data, Node); // devuelve Int64
if (LIter < 1) or (LIter > 100000000) then
raise EHPDFPFXError.CreateFmt(
'PBKDF2 iteration count %d is outside the accepted range 1..100000000',
[LIter]);
Iterations := Integer(LIter); // seguro: ya acotado
Leer en un Int64 significa que el valor decodificado es el real, no un fantasma truncado de él. El límite inferior rechaza recuentos de cero y negativos, que no tienen sentido para una derivación de clave. El límite superior, cien millones, se encuentra muy por encima de cualquier archivo PKCS#12 legítimo, que hoy utiliza desde decenas hasta unos pocos cientos de miles de iteraciones, al tiempo que limita el peor de los casos a una cantidad de trabajo acotada y sobrevivible. Solo después de que el valor ha pasado ese rango se reduce al campo de 32 bits, por lo que el truncamiento ya no puede sorprender a nadie. En HotPDF, esta limitación reside en ParsePBES2Params, donde se decodifican los parámetros PBKDF2 en camino a PBKDF2HMACSHA256
Por qué ambas guardas son la misma guarda
Las dos trampas parecen distintas, una es un desbordamiento de búfer y la otra un proceso colgado, pero cometen el mismo error. En ambos casos un número de un archivo no confiable viaja hacia un tipo de ancho fijo un paso antes de tiempo, antes de haberse contrastado con la realidad. La longitud se suma en 32 bits antes de la prueba de límites; el recuento de iteraciones se estrecha a 32 bits antes de la prueba de rango. Ambas se resuelven con la misma disciplina: decodificar a ancho completo, comprobar contra el límite real y solo entonces estrechar. El Int64 intermedio no es una elección de estilo, es el único ancho en el que la guarda puede ver el valor que el atacante escribió de verdad. Una cota que desborda no es una cota, y un recuento sin techo no es un parámetro, es un regulador remoto de su propia CPU
Orientación práctica para un canal de firma
La lección particular es validar la entrada de certificados no confiables de la misma manera que validaría cualquier carga no confiable. Limite el tamaño del .pfx que acepta, ya que uno legítimo es de kilobytes, no de megabytes. Trate un fallo de análisis como una entrada rechazada de rutina, no como un error que merezca un rastreo de pila para el usuario. Si firma en un servidor, ejecute la importación donde un trabajador estancado no pueda derribar el servicio con él, y establezca un tiempo de espera para la operación de modo que un archivo inesperadamente costoso esté limitado por el reloj de pared, así como por el límite de iteración
La lección más amplia va más allá de los certificados. El endurecimiento del analizador no es una auditoría única de una unidad, es una propiedad de cada lugar donde su biblioteca lee bytes que no escribió. Una biblioteca PDF analiza una gran cantidad de fuentes no confiables: fuentes incrustadas en un documento, imágenes en media docena de códecs, filtros de flujo y, en la ruta de firma, certificados. Cada uno de ellos es superficie de ataque, y cada uno merece la misma sospecha en cada longitud y en cada recuento. HotPDF crea la ruta de importación y firma sobre las unidades reforzadas HPDFASN1, HPDFPFX, HPDFCrypt y HPDFCMS descritas aquí, para que la credencial que le entregue, de dondequiera que provenga, sea analizada defensivamente antes de que se confíe en ella
El flujo de trabajo de firma que protegen estas comprobaciones se cubre de principio a fin en nuestra guía práctica sobre firmas digitales PAdES en Delphi, y la misma postura defensiva aplicada al cifrado de documentos, incluida la ruta de clave AES-256 que comparte este código base, se describe en el artículo sobre cifrado AES-256 y seguridad. Todo ello se distribuye como parte de HotPDF Delphi Component para Delphi y C++Builder, junto con las API de carga, edición, cifrado y firma cubiertas en otras secciones de este blog