Artículo técnico

Ed448 y ECDSA Brainpool en Pascal puro para PDF

PDFlibPas firma y verifica con Ed448 y con las tres curvas ECDSA Brainpool en Object Pascal puro. Sin biblioteca criptográfica externa, sin proveedor de plataforma, sin DLL: PDFlibEd448 implementa el PureEdDSA de RFC 8032 sobre edwards448, y PDFlibBrainpool implementa brainpoolP256r1, brainpoolP384r1 y brainpoolP512r1 de RFC 5639. Ambas se construyeron del mismo modo, contra vectores de respuesta conocida generados de forma independiente antes de escribir una sola línea de Pascal, y ambas merecen este artículo sobre todo por sus errores

La aritmética de campo es un código inusualmente honesto: coincide byte a byte con los vectores publicados o no coincide, así que no hay espacio para un resultado apenas aceptable. Lo difícil es que una implementación errónea sigue produciendo firmas, sigue verificando sus propias firmas y sigue pareciendo totalmente 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 exóticas. 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 de Pascal de uso común, así que una biblioteca PDF que las quiera debe implementarlas por su cuenta

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 conciliar, ni comportamientos que cambien cuando el sistema anfitrión recibe parches. La firma es justamente el área donde menos van a querer una dependencia cambiante

Las constantes salen del texto de la especificación, nunca de la memoria

El primer intento de escribir el punto base de edwards448 se hizo de memoria y salió mal. No es un error llamativo, pero sí muy costoso, porque un punto base equivocado produce un sistema autoconsistente: su generación de claves, su firma y su verificación concuerdan entre sí y discrepan del resto del mundo

El procedimiento que funciona es tomar cada parámetro de dominio del texto de la especificación y luego verificarlo de forma cruzada. Para edwards448 eso significa el primo, la constante de la curva, el orden del grupo y las dos coordenadas decimales del punto base tomados de RFC 8032, convertidos a la representación interna por limbs y comprobados después contra los vectores de prueba publicados en el propio 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 ejecutar nada en Pascal

Los parámetros de dominio de Ed448 y Brainpool fluyen del texto de RFC 8032 y RFC 5639 hacia la forma por limbs y se verifican de forma cruzada antes de ejecutar nada en Pascal
Los parámetros de dominio de edwards448 y las curvas Brainpool se toman del texto RFC, se convierten a limbs y se verifican de forma cruzada contra vectores independientes

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, algo mucho más barato que descubrirlo depurando

El método: una réplica a nivel de limbs antes de nada en Pascal

La técnica que hizo manejables ambas unidades es una implementación réplica en un lenguaje con enteros sin límite, construida de abajo hacia arriba. Primero solo la capa aritmética: multiplicación de campo, resta y propagación de acarreos, sometida a pruebas intensivas contra sus invariantes algebraicos con un par de centenares de casos aleatorios. Después la generación completa de claves dentro de la réplica, que es donde viven los errores semánticos y donde salen barato de encontrar. Solo entonces, la transcripción a Pascal

Flujo de trabajo de una implementación réplica con enteros sin límite que valida la aritmética de campo en Pascal y la generación de claves para Ed448 y Brainpool
El flujo réplica de abajo hacia arriba: primero la aritmética, luego la generación de claves dentro de la réplica y al final la transcripción a Pascal con comparación de valores intermedios

La recompensa es diagnóstica más que de desarrollo. Una vez que se sabe que la réplica es correcta, cualquier discrepancia entre réplica y Pascal es un desliz de transcripción, y sondear el mismo valor intermedio en ambas implementaciones lo localiza de inmediato. Eso convierte una clase de error que de otro modo es casi indepurable, un solo limb equivocado en lo profundo de una multiplicación escalar, en una comparación de cinco minutos

Cuatro causas raíz en Ed448

Las cuatro se encontraron sondeando valores intermedios, y las cuatro son del tipo que produce resultados de apariencia válida

La primera es una trampa de notación. La mayoría de las fórmulas publicadas para la adición unificada de Edwards asumen una constante de curva igual a menos uno, y edwards448 tiene más uno. Trasladada sin cambios, la numeradora de la coordenada y se escribe como suma cuando debería ser una resta. La corrección no consiste en parchear el signo, sino en rederivar la forma de producto sin inversión 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 espacio 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, de modo 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 en Ed448 es de 114 bytes, y no de sus primeros 57. La curva de 32 bytes también usa su resumen completo de 64 bytes, así que la regla es consistente; lo único falso es el supuesto de que la mitad del resumen es el ancho del escalar

La cuarta es el orden. El prefijo de separación de dominio va primero, antes del prefijo de contexto y del mensaje, que no es el orden que sugiere una lectura intuitiva de R y A en la especificación. Equivocarse aquí produce firmas que verifican contra su propia implementación y contra nada más, que es la forma de fallo más engañosa posible

// Diseño del acarreo de campo: propagación pura con semántica de
// piso, de modo que los limbs positivos y los negativos funcionan y
// la resta no necesita sesgo. El acarreo superior se repliega
// mediante 2^448 = 2^224 + 1 (mod p), lo que toca el limb 0 y el
// limb 8. Limitado 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]);          // piso, 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 este tipo de defecto; la semántica de piso con un bucle repeat acotado es más fácil de razonar y, medido en la práctica, suficientemente rápido

Dos causas raíz en Brainpool

La primera no es criptografía en absoluto. La representación con la que se trabaja es de 33 limbs, así que el producto de dos valores necesita 66, y el arreglo del producto se declaró con 64. Escribir más allá del final corrompió memoria contigua, lo que primero se manifestó como resultados erróneos y solo se volvió un fallo cuando se añadió un barrido más amplio. La regla que salió de ahí vale para todo búfer numérico de tamaño fijo: dimensionarlo según el ancho del producto en el peor caso, añadir margen y volver a pensarlo nunca más. El arreglo del código de producción usa 68 limbs

La segunda es una forma de exponenciación mal combinada. Hay dos formas correctas de cuadrar y multiplicar y consumen el exponente en direcciones opuestas: la forma de derecha a izquierda multiplica y luego cuadra la base, y debe leer los bits desde el extremo menos significativo, mientras que la forma de izquierda a derecha cuadra y luego 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 del más significativo primero. Ambas mitades son de manual; la combinación no, y el resultado es un inverso equivocado que todavía parece un elemento de campo plausible

Dos formas de exponenciación cuadrar y multiplicar con direcciones de bits opuestas y la forma mixta que calculó inversos modulares Brainpool erróneos
Ambas formas de cuadrar y multiplicar son correctas por separado; combinar un cuerpo de derecha a izquierda con un recorrido del bit más significativo primero da un inverso erróneo plausible
// Duplicación y adición jacobianas cuando el registro destino puede
// ser la misma variable que un origen. Copiar el registro completo
// al entrar es la única defensa confiable: 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 parchado incremental al vuelo no converge en una unidad criptográfica. Un borrador recibió parches una y otra vez hasta acumular 32 rutinas duplicadas y una estructura dañada, y solo se arregló reescribiéndolo. El patrón a adoptar es escribirlo una vez desde una réplica validada o reescribirlo; una sucesión de parches locales sobre aritmética que todavía no se entiende acumula defectos más rápido de lo que corrige

Y comprueben la marca de tiempo del ejecutable antes de creer en un resultado de prueba. Una compilación incremental que compila pero no vuelve a enlazar ejecuta el binario anterior, lo que fabricó toda una ronda de pistas falsas sobre sondeos ausentes y salidas duplicadas. Al depurar criptografía, un resultado inexplicable debería hacerse la pregunta de si este es el binario que acaban de compilar antes que la de si el algoritmo está mal

Rendimiento, alcance y cómo invocarlo

La reducción modular en la unidad Brainpool es por desplazamiento y resta bit a bit desde el bit encendido más alto del producto, así que una multiplicación cuesta más o menos del orden del ancho de bits. Una verificación P-256 cae en las centenas bajas de milisegundos, lo que no llama la atención al 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 que el que lleva la representación actual, así que es un cambio para cuando la carga de trabajo lo pida, no de forma preventiva

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 suministra 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 Brainpool recibe el nonce en lugar de generarlo. Es deliberado: la generación de nonces es lo más catastrófico que se puede hacer 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 el trabajo poscuántico descrito en el artículo sobre ML-DSA de FIPS 204, y se conectan al mismo flujo de firma y validación tratado en firma y validación PAdES. Para certificados de prueba en estas curvas, la vía de generación local se describe en certificados autofirmados con CryptoAPI. La matriz completa de algoritmos está en la página del producto losLab PDF Developer Library