HotPDF ejecuta el acuerdo de claves de curva elíptica y la verificación de firmas para PDF en Object Pascal puro, sin enlace a OpenSSL y sin proveedor criptográfico de plataforma en el camino. Eso cubre cinco curvas: P-256, P-384 y P-521 para las familias primas NIST, más X25519 y X448 para el acuerdo de claves en curvas de Montgomery. La razón de escribir ese código en lugar de enlazarlo es el despliegue, no la pureza. Una aplicación Delphi o Free Pascal que distribuye un ejecutable y ninguna DLL criptográfica no tiene desfases de versión que gestionar, ni proveedor por plataforma que detectar, ni nada que cambie de comportamiento cuando un cliente parchea sus bibliotecas del sistema
El costo es que ahora la aritmética es suya. La multiplicación modular de enteros grandes es código implacable: o produce resultados idénticos byte a byte contra los vectores de prueba publicados o produce basura de apariencia plausible, y la distancia entre esos dos estados puede ser una sola comparación. Esta es la historia de esa comparación, porque la forma del error generaliza a cualquier port a Pascal de aritmética de campo
Por qué una biblioteca PDF necesita aritmética de curvas
Dos funciones la reclaman. La primera es el cifrado de documentos con clave pública: el manejador de listas de destinatarios de ISO 32000 envuelve una clave por documento para certificados nombrados, y cuando un destinatario tiene una clave EC el envolvimiento corre por acuerdo de claves y no por transporte de claves RSA. Sin ECDH no hay forma de abrir ese documento. La segunda es la validación de firmas. Verificar una firma ECDSA sobre los bytes de /ByteRange necesita una multiplicación de puntos en la curva del firmante, y P-384 es común en los perfiles gubernamentales y de firma cualificada, donde P-256 se considera el suelo y no la meta. HotPDF expone los resultados de ese trabajo a través de la vía de verificación ECDSA y CMS y a través de el modelo de proveedores de firma conectables
CIOS y la única resta del final
La multiplicación de Montgomery evita la división trabajando en un dominio transformado donde la reducción es un desplazamiento. La variante que usa HotPDF es Coarsely Integrated Operand Scanning, que entrelaza la multiplicación y la reducción limb a limb de modo que el intermedio nunca crece más allá del ancho del módulo más un limb. El cuerpo del bucle es directo y fácil de probar. El final no: después de las pasadas entrelazadas el acumulador puede estar en cualquier punto del rango hasta el doble del módulo, así que el algoritmo termina con una resta condicional que quita una copia del primo si y solo si el acumulador es mayor o igual
Comparar dos números de varios limbs significa recorrer del limb más significativo hacia abajo mientras se carga un borrow. La forma obvia de escribirlo es comparar el limb del acumulador contra el limb del módulo más el borrow entrante. Esa expresión está mal, y está mal de una manera que la mayoría de las curvas esconden
// Incorrecto: P[I] + Borrow puede desbordarse cuando P[I] es $FFFFFFFFFFFFFFFF
if T[I] < P[I] + Borrow then
begin
Borrow := 1;
Break;
end;
// Correcto: comparar sin sumar nunca a un limb
if (T[I] < P[I]) or ((T[I] = P[I]) and (Borrow = 1)) then
begin
Borrow := 1;
Break;
end;
Cómo se ve en realidad un desborde de borrow
Se ve como una curva que funciona en todas partes menos en producción. Los primos de P-384 y P-521 contienen limbs que son totalmente unos, así que P[I] es igual a $FFFFFFFFFFFFFFFF. Súmenle el borrow entrante de uno y un entero sin signo de 64 bits se desborda a cero. La comparación pregunta entonces si el limb del acumulador es menor que cero, decide que no lo es y concluye que no hace falta borrow. Un limb del resultado queda desviado en uno
P-256 escapa porque ninguno de sus limbs es todo unos, así que la suma nunca se desborda y la expresión con error coincide por casualidad con la correcta. Es el peor resultado posible para una suite de pruebas: la curva más probada pasa, las menos probadas fallan intermitentemente según los valores de los operandos, y el fallo se manifiesta como un resultado de verificación "firma inválida" sobre documentos perfectamente válidos. HotPDF llevaba una guarda explícita sobre P-384 justo por esta razón, devolviendo un estado de no disponible en lugar de una respuesta equivocada, hasta que la aritmética estuvo probada contra vectores de referencia
Cómo se localizó realmente el error
No fue leyendo el código. La secuencia productiva fue mecánica, y es reutilizable. Primero, eliminar las constantes: cada limb de p, R y R^2 se regeneró de forma independiente y se comparó limb a limb, lo que descarta la fuente más común de errores de curvas. Segundo, instrumentar la aritmética y no la API: un procedimiento temporal de volcado imprimió los valores intermedios de la multiplicación de Montgomery de R^2, de x^3 y de y^2 para un punto conocido, de modo que pudieran cotejarse contra una verdad calculada independientemente
Esa comparación apuntó directo al culpable. La cadena de x era correcta de punta a punta, mientras que y^2 difería en exactamente un limb por exactamente uno. Una diferencia de uno en un solo limb no es un error de multiplicación, ni de propagación de acarreo, ni de constantes; es un error de cadena de borrows, y la única cadena de borrows de la rutina es la resta condicional final. Un detalle casi descarriló esto: la constante de referencia usada para el volcado estaba escrita ella misma en el orden de bytes equivocado en el primer intento, lo que produjo un desajuste en el valor de y y sugirió brevemente un segundo defecto inexistente. Verifiquen la endianidad de su verdad de referencia antes de confiarle la acusación contra su código
Las trampas vecinas en la misma rutina
Tres modos de fallo más viven a unas líneas de esa comparación, y los tres estuvieron vivos en algún momento del desarrollo
// 1. El acumulador tiene un limb por encima del ancho del módulo. Comparar
// solo los L limbs bajos pierde el caso en que T es exactamente p más
// 2^(64*L), que ocurre para una porción significativa de entradas
// aleatorias porque 2p excede 2^256 para P-256 y 2^384 para P-384
if (T[L] <> 0) or NotLessThanModulus(T, P, L) then
SubtractModulus(T, P, L);
// 2. Una resta genérica de varios limbs tiene el mismo riesgo de desborde:
// cuando Y[I] es $FFFFFFFFFFFFFFFF, Y[I] + Borrow desborda a cero y el
// borrow debe sobrevivir al siguiente limb en lugar de borrarse
Diff := X[I] - Y[I] - Borrow;
NextBorrow := Ord((X[I] < Y[I]) or ((X[I] = Y[I]) and (Borrow = 1)));
El tercero no es código, es procedencia. El primo de P-521 se transcribió al principio con 130 dígitos hexadecimales en lugar de 131, un F de menos, y las constantes de Montgomery se calcularon después de ese primo equivocado, así que las constantes eran autoconsistentes y conjuntamente erróneas. Los parámetros de curva deben derivarse, nunca teclearse: calculen R como (1 shl (64 * L)) mod p a partir del primo que estén usando de verdad, y luego cotejen R * R mod p contra el valor que su constante R^2 declara. Un par de constantes que concuerdan entre sí no prueba nada de ninguna
Estrategia de verificación que escala más allá de una curva
La técnica que hizo manejables X25519 y X448 fue escribir una implementación réplica en un lenguaje con enteros sin límite y transcribir el flujo de control de Pascal línea por línea. Cuando la réplica produce la respuesta correcta y el Pascal no, el defecto es un desliz de transcripción y sondear el mismo valor intermedio en ambas implementaciones lo encuentra en segundos. Los tres errores clásicos de la escalera de RFC 7748 se atraparon así: un swap de tiempo constante cuya segunda línea reutilizaba el valor ya intercambiado, una inversión final que devolvía z elevado a menos uno en lugar de multiplicarlo dentro de X, y una multiplicación por constante pequeña que ensamblaba productos de media palabra con un or bit a bit y perdía el acarreo
Para material de prueba, tomen los vectores como bytes y no como texto. Extraer una clave privada con un patrón de texto es la forma en que una implementación correcta se acusa de un error de un byte que vive por completo en el paso de extracción. Corten el hex de la codificación DER en desplazamientos conocidos y comparen arreglos de bytes
Con la cadena de borrows corregida, las cinco curvas coinciden byte a byte con los vectores de referencia publicados, y HotPDF ya no pone guarda sobre ninguna. Si están integrando firma basada en certificados o cifrado por lista de destinatarios, la conclusión práctica es que la elección de curva es ahora una decisión de política y no una pregunta de capacidad; los perfiles y las trampas de orden de bytes del lado de la firma se cubren en el recorrido de firma PAdES. Los detalles del componente y la matriz de algoritmos soportados están en la página del producto HotPDF Delphi PDF component