Artículo técnico

Aritmética de curvas NIST en Pascal puro para firmar PDF

HotPDF realiza acuerdo de claves de curva elíptica y 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 vez 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 desalineación de versiones que gestionar, ni proveedor por plataforma que detectar, y nada que cambie de comportamiento cuando un cliente parchea sus bibliotecas del sistema

El coste es que ahora la aritmética es vuestra. 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 única comparación. Esta es la historia de esa comparación, porque la forma del error se generaliza a cualquier port a Pascal de aritmética de cuerpos

Por qué necesita aritmética de curvas una biblioteca PDF?

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 concretos, y cuando un destinatario tiene una clave EC el envoltorio corre por acuerdo de claves en lugar de transporte de claves RSA. Sin ECDH no hay forma de abrir tal documento. La segunda es la validación de firmas. Verificar una firma ECDSA sobre los bytes del /ByteRange necesita una multiplicación de puntos en la curva del firmante, y P-384 es común en perfiles gubernamentales y de firma cualificada, donde P-256 se considera el suelo y no el objetivo. HotPDF expone los resultados de ese trabajo a través de la ruta de verificación ECDSA y CMS y de el modelo conectable de proveedores de firma

Diagrama de dónde se usa la aritmética de curvas en Pascal puro de HotPDF: cifrado ECDH de listas de destinatarios y verificación de firmas ECDSA sobre ByteRange
El acuerdo de claves abre documentos cifrados EC para destinatarios concretos, mientras que la validación de firmas necesita multiplicación de puntos en la curva del firmante

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á de la anchura del módulo más un limb. El cuerpo del bucle es directo y fácil de probar. La cola no: tras 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 elimina una copia del primo si y solo si el acumulador es mayor o igual que él

Comparar dos números de varios limbs significa descender desde el limb más significativo arrastrando 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 forma que la mayoría de las curvas esconden

// Equivocado: P[I] + Borrow puede desbordar 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 es realmente un desbordamiento 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 formados enteramente por unos, así que P[I] equivale a $FFFFFFFFFFFFFFFF. Sumadle el borrow entrante de uno y un entero sin signo de 64 bits desborda a cero. La comparación pregunta entonces si el limb del acumulador es menor que cero, decide que no, y concluye que no hace falta borrow. Un limb del resultado queda desviado en uno

Diagrama de desbordamiento de borrow en la reducción Montgomery contrastando la comparación de limbs equivocada con la propagación de borrow correcta en la aritmética P-384 de HotPDF
Sumar el borrow a un limb de todos unos desborda a cero, así que P-384 y P-521 no reciben la resta mientras P-256 esconde el defecto

P-256 se escapa porque ninguno de sus limbs son todos unos, así que la suma nunca desborda y la expresión con errores casualmente coincide con la correcta. Ese es el peor desenlace 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 aparece como un resultado de verificación «firma inválida» en documentos perfectamente válidos. HotPDF llevaba una puerta explícita sobre P-384 precisamente por esto, devolviendo un estado de no disponible en lugar de una respuesta equivocada, hasta que la aritmética se probó contra vectores de referencia

Cómo se localizó realmente el error

No 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 imprimía 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 con una verdad calculada independientemente

Esa comparación señaló directamente al culpable. La cadena de x era correcta de principio a fin, mientras que y^2 difería en exactamente un limb por exactamente uno. Una diferencia de uno en un único limb no es un error de multiplicación, ni de propagación de acarreos, 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 lo descarriló: la constante de referencia usada para el volcado estaba ella misma escrita en el orden de bytes equivocado en el primer intento, lo que produjo una discrepancia en el valor de y y sugirió brevemente un segundo defecto inexistente. Verificad la endianness de vuestra verdad de referencia antes de fiaros de ella para acusar a vuestro código

Diagrama de flujo de cómo se localizó el error de curva de HotPDF regenerando constantes, volcando intermedios de Montgomery y comparando contra la verdad espejo
Una diferencia de exactamente uno en un único limb apuntó directo a la única cadena de borrows de la rutina, y una referencia con bytes intercambiados casi desvió la caza

Las trampas vecinas en la misma rutina

Tres modos de fallo más viven a pocas 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 de la anchura del módulo. Comparar
//    solo los L limbs bajos pierde el caso en que T equivale exactamente a p
//    más 2^(64*L), que ocurre para una parte significativa de entradas
//    aleatorias porque 2p supera 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 desbordamiento:
//    cuando Y[I] es $FFFFFFFFFFFFFFFF, Y[I] + Borrow desborda a cero y el
//    borrow debe sobrevivir al siguiente limb en lugar de limpiarse
Diff := X[I] - Y[I] - Borrow;
NextBorrow := Ord((X[I] < Y[I]) or ((X[I] = Y[I]) and (Borrow = 1)));

La tercera 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 menos, y las constantes de Montgomery se calcularon luego a partir de ese primo equivocado, así que las constantes eran autoconsistentes y conjuntamente erróneas. Los parámetros de curva deben derivarse, nunca teclearse: calculad R como (1 shl (64 * L)) mod p a partir del primo que realmente usáis, y después cotejad R * R mod p contra el valor que vuestra constante R^2 afirma. Un par de constantes que concuerdan entre sí no prueba nada sobre ninguna de las dos

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 espejo en un lenguaje con enteros sin límite y transcribir a ella el flujo de control Pascal línea a línea. Cuando el espejo 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 cazaron 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 en 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 el material de prueba, tomad los vectores como bytes y no como texto. Extraer una clave privada con un patrón textual es la manera de que una implementación correcta sea acusada de un error de un byte que vive por completo en el paso de extracción. Recortad el hex de la codificación DER en offsets conocidos y comparad arrays 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 puerta a ninguna. Si estáis integrando firma basada en certificados o cifrado de listas 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 cuestión de capacidad; los perfiles y las trampas de orden de bytes del lado de 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 de producto de HotPDF Delphi PDF component