PDFlibPas firma y verifica con Ed448 y con las tres curvas ECDSA de Brainpool en Object Pascal puro. Sin biblioteca criptográfica externa, sin proveedor de plataforma, sin DLL: PDFlibEd448 implementa PureEdDSA de RFC 8032 sobre edwards448, y PDFlibBrainpool implementa brainpoolP256r1, brainpoolP384r1 y brainpoolP512r1 de RFC 5639. Ambas implementaciones se construyeron del mismo modo, contra vectores de respuesta conocida generados de forma independiente antes de escribir ni una línea de Pascal, y ambas merecen un artículo sobre todo por sus errores
La aritmética de cuerpos es un código inusualmente honesto. O coincide byte a byte con los vectores publicados o no coincide, así que no hay espacio para el «funciona a medias». Lo que la hace difícil es que una implementación errónea sigue produciendo firmas, sigue verificando sus propias firmas y sigue pareciendo completamente plausible
Por qué estas curvas, y por qué en Pascal
Las curvas Brainpool aparecen en los perfiles europeos de firma cualificada, de modo que una biblioteca que firma documentos para ese mercado no puede tratarlas como algo exótico. Ed448 forma parte del conjunto de algoritmos que ISO/TS 32002 aporta a PDF, donde su resumen interno es SHAKE256 en lugar de SHA-2. Ninguna de las dos familias está disponible en las bibliotecas criptográficas Pascal de uso común, así que una biblioteca PDF que las quiera tiene que implementarlas por sí misma
El argumento de despliegue es el mismo que se aplica a toda la criptografía de esta biblioteca: una aplicación que distribuye un único binario sin dependencias criptográficas no tiene proveedor que detectar, ni versión que emparejar, ni comportamiento que cambie cuando el equipo anfitrión recibe parches. La firma es justo el área donde menos se desea una dependencia movediza
Las constantes salen del texto de la especificación, nunca de memoria
El primer intento con el punto base de edwards448 se escribió de memoria y salió mal. No es un error extraordinario, pero sí carísimo, porque un punto base erróneo produce un sistema autoconsistente: vuestra generación de claves, firma y verificación concuerdan entre sí y discrepan con el resto del mundo
El procedimiento que funciona es tomar cada parámetro de dominio del texto de la especificación y después verificarlo de forma cruzada. Para edwards448 eso significa el primo, la constante de la curva, el orden del grupo y ambas coordenadas decimales del punto base tomados de RFC 8032, convertidos a la representación interna de limbs y comprobados contra los vectores de prueba publicados en el mismo documento. Para las curvas Brainpool significa los parámetros de RFC 5639, una implementación independiente escrita para generar vectores y una verificación cruzada contra una biblioteca del sistema en ambos sentidos antes de que corriera ningún código Pascal
Un atajo de derivación merece una advertencia porque parece universal y no lo es: recuperar el punto base a partir de un valor fijo de y funciona en la curva 25519 y no funciona en edwards448, donde ese valor no tiene raíz cuadrada. Un script lo desmintió en segundos, que es mucho más barato que descubrirlo con el depurador
El método: una réplica a nivel de limbs antes de cualquier Pascal
La técnica que hizo manejables ambas unidades es una implementación espejo en un lenguaje con enteros sin límite, construida de abajo arriba. Primero solo la capa aritmética: multiplicación de cuerpo, resta y propagación de acarreos, sometidas a pruebas de estrés contra sus invariantes algebraicos en un par de centenares de casos aleatorios. Después la generación de claves completa dentro del espejo, que es donde viven los errores semánticos y donde cuesta poco encontrarlos. Solo entonces, la transcripción a Pascal
El beneficio es diagnóstico más que de desarrollo. Una vez que el espejo se sabe correcto, cualquier discrepancia entre espejo y Pascal es un desliz de transcripción, y sondear el mismo valor intermedio en ambas implementaciones lo localiza al instante. Eso convierte una clase de error que de otro modo es casi indepurable, un único limb equivocado en el fondo de una multiplicación escalar, en una comparación de cinco minutos
Cuatro causas raíz en Ed448
Los cuatro se encontraron sondeando valores intermedios, y los cuatro son del tipo que produce salidas de apariencia válida
La primera es una trampa de notación. La mayoría de las fórmulas publicadas para la adición de Edwards unificada asumen una constante de curva igual a menos uno, y edwards448 tiene más uno. Trasladada sin cambios, el numerador de la coordenada y queda escrito como una suma cuando debería ser una diferencia. La corrección no consiste en parchear el signo, sino en volver a derivar la forma de producto sin inversiones a partir de la ley de adición afín de la curva correcta, lo que produce las cuatro expresiones de coordenadas y no deja hueco para heredar un signo de la fuente equivocada
La segunda está en la descompresión de puntos. Recuperar la x afín desde coordenadas proyectivas exige una multiplicación por el inverso de Z. Multiplicar por el inverso al cuadrado da un valor que sigue siendo una representación proyectiva válida y es la coordenada afín equivocada, así que el síntoma es una y correcta con una x errónea. Siempre que una coordenada esté bien y la otra no, el error está en la normalización, no en la aritmética
La tercera es un hábito importado de la curva más corta. Tanto el escalar por firma como el escalar de desafío deben reducirse a partir del resumen completo, que para Ed448 son 114 bytes, y no de sus primeros 57. La curva de 32 bytes también usa su resumen completo de 64 bytes, de modo que la regla es consistente; lo único erróneo es el supuesto de que «la mitad del resumen es la anchura del escalar»
La cuarta es el orden. El prefijo de separación de dominios va primero, antes del prefijo de contexto y del mensaje, que no es el orden que sugiere la lectura intuitiva de R y A en la especificación. Equivocarse aquí produce firmas que verifican contra vuestra propia implementación y contra nada más, el fallo más engañoso posible
// Diseño de acarreo del cuerpo: propagación pura con semántica de
// suelo, así que funcionan tanto los limbs positivos como los
// negativos y la resta no necesita sesgo.
// El acarreo superior se pliega mediante 2^448 = 2^224 + 1 (mod p),
// que toca el limb 0 y el limb 8. Acotado a cuatro rondas; dos
// observadas en la práctica
procedure FeCarry(var A: TFe448);
var
I, Round: Integer;
Carry: Int64;
begin
for Round := 1 to 4 do
begin
Carry := 0;
for I := 0 to 15 do
begin
A[I] := A[I] + Carry;
Carry := Floor28(A[I]); // suelo, no truncamiento
A[I] := A[I] - (Carry shl 28);
end;
if Carry = 0 then
Break;
A[0] := A[0] + Carry; // 2^448 == 1
A[8] := A[8] + Carry; // 2^448 == 2^224
end;
end;
Una versión anterior de esa rutina aplicaba un sesgo antes de propagar, y con entradas grandes plegaba un acarreo espurio de magnitud equivocada en los limbs bajos. Los esquemas de acarreo basados en sesgo son una fuente persistente de esta clase de defecto; la semántica de suelo con un bucle repeat acotado resulta más fácil de razonar y, medida, es suficientemente rápida
Dos causas raíz en Brainpool
La primera no es criptografía en absoluto. La representación de trabajo son 33 limbs, así que el producto de dos valores necesita 66, y el array de producto se declaró con 64. Escribir más allá del final corrompía memoria adyacente, lo que se presentó primero como resultados erróneos y solo se convirtió en un fallo cuando se añadió un barrido más amplio. La regla que salió de ahí merece aplicarse a cada buffer numérico de tamaño fijo: dimensionarlo desde la anchura del producto en el peor caso, añadir margen y no volver a pensar en ello. El array del código publicado tiene 68 limbs
La segunda es una forma de exponenciación mezclada. Hay dos formas correctas de cuadrar-y-multiplicar y consumen el exponente en direcciones opuestas: la forma de derecha a izquierda multiplica y después cuadra la base, y debe leer los bits desde el extremo menos significativo, mientras que la forma de izquierda a derecha cuadra y después multiplica, y lee desde el extremo más significativo. El bucle de inversión modular tenía un cuerpo de derecha a izquierda con un recorrido de bits que empezaba por el más significativo. Ambas mitades son de manual; la combinación no, y el resultado es un inverso equivocado que aún parece un elemento de cuerpo plausible
// Duplicación y adición jacobianas cuando el registro de destino puede
// ser la misma variable que un origen. Copiar el registro entero al
// entrar es la única defensa fiable: escribir los limbs de R contamina
// las lecturas posteriores de P
procedure BPPointDouble(var R: TBPPoint; const P: TBPPoint;
const Curve: TBPCurve);
var
Pin: TBPPoint;
begin
Pin := P; // copiar primero y calcular solo desde Pin
// ... M = 3X^2 + A*Z^4, S = 4*X*Y^2, X3 = M^2 - 2S, ...
end;
Dos lecciones de proceso que costaron más que los errores
El parcheo incremental no converge en una unidad criptográfica. Un borrador se parcheó repetidamente hasta cargar 32 rutinas duplicadas y una estructura dañada, y solo se arregló reescribiéndolo. El patrón a adoptar es escribirlo una vez desde un espejo validado o reescribirlo; una secuencia de arreglos locales sobre aritmética que aún no se entiende acumula más rápido de lo que corrige
Y comprobad la marca de tiempo del ejecutable antes de creeros un resultado de prueba. Una compilación incremental que compila pero no reenlaza ejecuta el binario anterior, lo que fabricó toda una ronda de pistas falsas sobre sondas ausentes y salidas duplicadas. Al depurar criptografía, un resultado inexplicable debería plantear «¿es este el binario que acabo de compilar?» antes que «¿está mal el algoritmo?»
Rendimiento, alcance y cómo llamarlo
La reducción modular en la unidad Brainpool es un shift-subtract serie por bits desde el bit encendido más alto del producto, así que una multiplicación cuesta del orden de la anchura de bits. Una verificación P-256 cae en los primeros centenares de milisegundos, lo cual no llama la atención para firmar o verificar documentos y sería insuficiente para un terminador TLS. La reducción de Barrett es la mejora obvia y necesita un valor de trabajo más ancho del que lleva la representación actual, así que es un cambio para cuando una carga de trabajo lo pida, no preventivamente
uses
PDFlibEd448, PDFlibBrainpool;
var
PublicKey, Signature: AnsiString;
Curve: TBPCurve;
R, S, PubX, PubY: TBPValue;
begin
// Ed448: PureEdDSA, SHAKE256 interno, claves de 57 bytes
if Ed448PublicKeyFromSeed(Seed, PublicKey) and
Ed448Sign(DocumentDigest, Seed, Signature) then
Assert(Ed448Verify(DocumentDigest, PublicKey, Signature));
// Brainpool: quien llama aporta el nonce por firma, de modo que la
// política de nonces se queda en la aplicación
Curve := BPLoadCurve(bpP256r1);
if BPKeyGen(PubX, PubY, PrivateD, Curve) and
BPSignFixedK(R, S, Hash, PrivateD, Nonce, Curve) then
Assert(BPVerify(R, S, Hash, PubX, PubY, Curve));
end;
Nótese que el punto de entrada de firma de Brainpool recibe el nonce en vez de generarlo. Es deliberado: la generación de nonces es lo único más catastrófico que puede hacerse mal en ECDSA, porque un valor repetido o predecible revela la clave privada, y la decisión de dónde sale la aleatoriedad pertenece a la aplicación y a su régimen de cumplimiento, no a una biblioteca PDF
Estas curvas acompañan al trabajo poscuántico descrito en el artículo sobre FIPS 204 ML-DSA, y se enchufan al mismo pipeline de firma y validación que cubre la firma y validación PAdES. Para certificados de prueba en estas curvas, la ruta de generación local se describe en certificados autofirmados con CryptoAPI. La matriz completa de algoritmos está en la página de producto de la losLab PDF Developer Library