Articolo tecnico

Ed448 e curve Brainpool ECDSA in Pascal puro per PDF

PDFlibPas firma e verifica con Ed448 e con le tre curve ECDSA Brainpool in Object Pascal puro. Nessuna libreria crittografica esterna, nessun provider di piattaforma, nessuna DLL: PDFlibEd448 implementa PureEdDSA RFC 8032 su edwards448, mentre PDFlibBrainpool implementa brainpoolP256r1, brainpoolP384r1 e brainpoolP512r1 RFC 5639. Entrambe le unità sono state realizzate allo stesso modo, contro vettori known-answer generati in modo indipendente prima di scrivere una sola riga di Pascal, e meritano di essere raccontate soprattutto per i bug

L'aritmetica dei campi è un codice insolitamente onesto. O corrisponde ai vettori pubblicati byte per byte oppure no, quindi non c'è spazio per un "funziona quasi". Ciò che rende le cose difficili è che un'implementazione sbagliata produce comunque firme, verifica comunque le proprie firme e appare comunque del tutto plausibile

Perché queste curve e perché in Pascal

Le curve Brainpool compaiono nei profili europei di firma qualificata, quindi una libreria che firma documenti per quel mercato non può trattarle come una curiosità esotica. Ed448 fa parte dell'insieme di algoritmi che ISO/TS 32002 porta nel PDF, dove il relativo digest interno è SHAKE256 anziché SHA-2. Nessuna delle due famiglie è disponibile nelle librerie crittografiche Pascal di uso comune, quindi una libreria PDF che le voglia deve implementarsele da sé

L'argomento legato al deployment è lo stesso valido per tutta la crittografia di questa libreria: un'applicazione che distribuisce un unico binario senza dipendenze crittografiche non ha provider da rilevare, versioni da far corrispondere né comportamenti che cambiano quando l'host riceve patch. La firma è proprio l'ambito in cui una dipendenza mobile è l'ultima cosa desiderabile

Le costanti provengono dal testo della specifica, mai dalla memoria

Il primo tentativo per il punto base di edwards448 fu scritto a memoria ed era sbagliato. Non è un errore straordinario, ma è costosissimo, perché un punto base errato produce un sistema autoconsistente: generazione delle chiavi, firma e verifica concordano tra loro e discordano dal resto del mondo

La procedura che funziona consiste nel prendere ogni parametro di dominio dal testo della specifica e verificarlo poi in modo incrociato. Per edwards448 significa prendere da RFC 8032 il numero primo, la costante della curva, l'ordine del gruppo ed entrambe le coordinate decimali del punto base, convertirli nella rappresentazione interna a limb e confrontarli quindi con i vettori di test pubblicati nello stesso documento. Per le curve Brainpool significa i parametri di RFC 5639, un'implementazione indipendente scritta per generare vettori e una verifica incrociata rispetto a una libreria di sistema in entrambe le direzioni, prima di eseguire qualsiasi codice Pascal

I parametri di dominio di Ed448 e Brainpool passano dal testo delle specifiche RFC 8032 e RFC 5639 alla forma a limb e vengono verificati in modo incrociato prima di eseguire qualsiasi codice Pascal
I parametri di dominio di edwards448 e delle curve Brainpool sono presi dal testo RFC, convertiti in limb e verificati in modo incrociato rispetto a vettori indipendenti

Una scorciatoia di derivazione merita un avvertimento perché sembra universale e non lo è: recuperare il punto base da un valore y fisso funziona per la curva 25519 e non funziona per edwards448, dove quel valore non ha radice quadrata. Uno script l'ha smentita in pochi secondi, il che costa molto meno che scoprirlo con un debugger

Il metodo: un mirror a livello di limb prima di qualsiasi Pascal

La tecnica che ha reso gestibili entrambe le unità è un'implementazione mirror in un linguaggio con interi senza limiti, costruita bottom-up. Prima il solo strato aritmetico: moltiplicazione di campo, sottrazione e propagazione del carry, sottoposti a stress test rispetto ai relativi invarianti algebrici su qualche centinaio di casi casuali. Poi la generazione completa delle chiavi dentro il mirror, che è dove vivono i bug semantici e dove trovarli costa poco. Solo allora la trascrizione in Pascal

Flusso di lavoro di un'implementazione mirror con interi senza limiti che valida aritmetica di campo e generazione delle chiavi Pascal per Ed448 e Brainpool
Il flusso mirror bottom-up: prima l'aritmetica, poi la generazione delle chiavi nel mirror, quindi la trascrizione Pascal e il confronto dei valori intermedi

Il vantaggio è diagnostico più che di sviluppo. Una volta noto come corretto, il mirror rende ogni disaccordo tra mirror e Pascal una svista di trascrizione, e esplorare lo stesso valore intermedio in entrambe le implementazioni lo localizza immediatamente. Questo trasforma una classe di bug che altrimenti è quasi impossibile da scovare in debug — un singolo limb sbagliato in fondo a una moltiplicazione scalare — in un confronto di cinque minuti

Quattro cause profonde in Ed448

Tutte e quattro sono state trovate esplorando i valori intermedi, e tutte e quattro sono del tipo che produce output dall'aspetto valido

La prima è una trappola di notazione. La maggior parte delle formule pubblicate per l'addizione di Edwards unificata assume una costante di curva pari a meno uno, mentre edwards448 ha più uno. Trascritta senza modifiche, il numeratore della coordinata y viene scritto come somma dove dovrebbe essere una differenza. La correzione non consiste nell'aggiustare il segno, ma nel ridedurre la forma di prodotto senza inversione dalla legge di addizione affine per la curva corretta, il che produce le quattro espressioni delle coordinate e non lascia spazio a un segno ereditato dalla fonte sbagliata

La seconda riguarda la decompressione dei punti. Recuperare la x affine dalle coordinate proiettive richiede una moltiplicazione per l'inverso di Z. Moltiplicando per l'inverso al quadrato si ottiene un valore che resta una rappresentazione proiettiva valida ed è la coordinata affine sbagliata, quindi il sintomo è una y corretta con una x sbagliata. Ogni volta che una coordinata è giusta e l'altra no, il bug sta nella normalizzazione, non nell'aritmetica

La terza è un'abitudine importata dalla curva più corta. Sia lo scalare per firma sia lo scalare di challenge devono essere ridotti dal digest completo, che per Ed448 è di 114 byte, non dai suoi primi 57. Anche la curva a 32 byte usa l'intero digest a 64 byte, quindi la regola è coerente; è solo l'assunzione che "metà del digest sia la larghezza dello scalare" a essere sbagliata

La quarta è l'ordinamento. Il prefisso di separazione del dominio viene per primo, prima del prefisso di contesto e del messaggio, che non è l'ordine suggerito dalla lettura intuitiva di R e A nella specifica. Sbagliare questo produce firme che verificano solo contro la propria implementazione e contro nient'altro, che è il fallimento più ingannevole possibile

// Progettazione del carry di campo: propagazione con pura semantica
// floor, così limb positivi e negativi funzionano entrambi e la
// sottrazione non richiede bias. Il carry superiore ripiega attraverso
// 2^448 = 2^224 + 1 (mod p), che tocca il limb 0 e il limb 8.
// Limitato a quattro passate; due osservate in pratica
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]);          // floor, non troncamento
      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 versione precedente di quella routine applicava un bias prima di propagare, e con input di grandi dimensioni riversava nelle limb basse un carry spurio di entità errata. Gli schemi di carry basati sul bias sono una fonte persistente di questa classe di difetti; la semantica floor con un ciclo repeat limitato è più facile da ragionare e misurabilmente abbastanza veloce

Due cause profonde in Brainpool

La prima non è affatto crittografia. La rappresentazione funzionante è di 33 limb, quindi il prodotto di due valori ne richiede 66, e l'array dei prodotti era dichiarato con 64. Scrivere oltre la fine corrompeva la memoria adiacente, il che si presentava dapprima come risultati sbagliati ed è diventato un crash solo dopo l'aggiunta di una scansione più ampia. La regola che ne è venuta fuori vale per ogni buffer numerico a dimensione fissa: dimensionarlo dalla larghezza del prodotto nel caso peggiore e aggiungere margine, poi non pensarci più. L'array nel codice distribuito è di 68 limb

La seconda è una forma di elevamento a potenza confusa. Esistono due forme corrette di square-and-multiply e consumano l'esponente in direzioni opposte: la forma right-to-left moltiplica e poi eleva al quadro la base e deve leggere i bit dal lato meno significativo, mentre la forma left-to-right eleva al quadro e poi moltiplica e legge dal lato più significativo. Il ciclo di inversione modulare aveva un corpo right-to-left con una scansione dei bit dal più significativo. Entrambe le metà sono da manuale, la combinazione no, e il risultato è un inverso sbagliato che sembra comunque un elemento di campo plausibile

Due forme di elevamento square-and-multiply con direzioni di bit opposte e la forma mista che calcolava inversi modulari Brainpool sbagliati
Entrambe le forme square-and-multiply sono corrette da sole; abbinare un corpo right-to-left a una scansione dal più significativo produce un inverso sbagliato ma plausibile
// Raddoppio e addizione jacobiana quando il record di destinazione
// può essere la stessa variabile di una sorgente. Una copia dell'intero
// record all'ingresso è l'unico presidio affidabile: scrivere i limb
// di R inquina le letture successive di P
procedure BPPointDouble(var R: TBPPoint; const P: TBPPoint;
  const Curve: TBPCurve);
var
  Pin: TBPPoint;
begin
  Pin := P;        // prima copiare, poi calcolare solo da Pin
  // ... M = 3X^2 + A*Z^4, S = 4*X*Y^2, X3 = M^2 - 2S, ...
end;

Due lezioni di processo che costano più dei bug

La correzione incrementale al volo non converge su una unità crittografica. Una bozza fu patchata ripetutamente finché non accumulò 32 routine duplicate e una struttura danneggiata, e fu risolta solo riscrivendola. Lo schema da adottare è scriverla una volta sola partendo da un mirror validato oppure riscriverla; una sequenza di correzioni locali a un'aritmetica che non si comprende ancora si accumula più velocemente di quanto corregga

E controllare il timestamp dell'eseguibile prima di credere a un risultato di test. Una build incrementale che compila ma non ricollega esegue il binario precedente, il che ha generato un intero giro di piste false su probe mancanti e output duplicato. Quando si fa debug di crittografia, un risultato inspiegabile dovrebbe suggerire la domanda "è il binario che ho appena compilato" prima di "l'algoritmo è sbagliato"

Prestazioni, ambito e come richiamarla

La riduzione modulare nell'unità Brainpool è uno shift-subtract bit-serial dal bit più alto impostato del prodotto, quindi una moltiplicazione costa all'incirca quanto la larghezza in bit. Una verifica P-256 si colloca nelle prime centinaia di millisecondi, il che è normale per firmare o verificare documenti e sarebbe inadeguato per un terminatore TLS. La riduzione di Barrett è l'aggiornamento ovvio e richiede un valore di lavoro più ampio di quello trasportato dall'attuale rappresentazione, quindi è una modifica da fare quando un carico di lavoro la richiede, non preventivamente

uses
  PDFlibEd448, PDFlibBrainpool;

var
  PublicKey, Signature: AnsiString;
  Curve: TBPCurve;
  R, S, PubX, PubY: TBPValue;
begin
  // Ed448: PureEdDSA, SHAKE256 interno, chiavi a 57 byte
  if Ed448PublicKeyFromSeed(Seed, PublicKey) and
     Ed448Sign(DocumentDigest, Seed, Signature) then
    Assert(Ed448Verify(DocumentDigest, PublicKey, Signature));

  // Brainpool: il chiamante fornisce il nonce per firma, quindi la
  // politica dei nonce resta all'applicazione
  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;

Si noti che il punto di ingresso di firma Brainpool riceve il nonce invece di generarlo. È deliberato: la generazione del nonce è la singola cosa più catastrofica da sbagliare in ECDSA, poiché un valore ripetuto o prevedibile rivela la chiave privata, e la decisione su da dove venga la casualità appartiene all'applicazione e al suo regime di conformità, non a una libreria PDF

Queste curve affiancano il lavoro post-quantum descritto nell'articolo su FIPS 204 ML-DSA, e si innestano nella stessa pipeline di firma e validazione trattata in firma e validazione PAdES. Per i certificati di test su queste curve, la via della generazione locale è descritta in certificati autofirmati con CryptoAPI. L'intera matrice degli algoritmi è elencata nella pagina di prodotto di losLab PDF Developer Library