Articolo tecnico

XOF SHAKE256 in Delphi: separazione di dominio Keccak

PDFlibPas implementa SHA-3 e SHAKE256 su un'unica permutazione Keccak-f[1600] in Object Pascal. Le due famiglie differiscono esattamente in due punti: il byte di dominio pad10*1, $06 per SHA-3 e $1F per SHAKE secondo FIPS 202 §6.2 e §6.3, e la regola del rate. SHAKE256 fissa il suo rate a 136 byte indipendentemente dalla lunghezza dell'output

È la seconda differenza che la gente sbaglia, perché non sembra affatto una differenza. Anche SHA3-256 gira a rate 136, quindi uno SHAKE256 costruito copiando il percorso di codice di SHA-3 supera ogni test a 32 byte che gli lanci e poi produce spazzatura la prima volta che qualcuno chiede 64. Quello che segue è come le due famiglie siano finite a condividere un unico record di contesto in PDFlibDigest, dove si ferma il punto di ingresso one-shot, e perché lo XOF streaming doveva esistere prima che Ed448 o ML-DSA potessero

Perché SHA-3 e SHAKE256 condividono un unico contesto Keccak?

Perché l'unico stato per famiglia è un singolo byte, e PDFlibPas lo salva in TSHA3Context come Suffix. Una volta che il separatore di dominio vive nel contesto invece di essere hardcodato nella routine di finalizzazione, SHA3AbsorbBlock, SHA3Update e SHA3Final servono entrambe le famiglie senza modifiche, e in libreria c'è esattamente una copia della permutazione a 24 round. Gli initializer sono il punto in cui i due percorsi divergono, e divergono del tutto

Un unico contesto Keccak che serve due famiglie in PDFlibPas: TSHA3Context salva il separatore di dominio in un singolo byte Suffix, SHA3Init imposta il suffisso a $06 con Rate derivato come 200 meno il doppio della lunghezza dell'output, e SHAKE256Init imposta $1F con il rate fissato a 136 byte, così SHA3AbsorbBlock, SHA3Update e SHA3Final servono entrambe senza modifiche
Poiché l'unico stato per famiglia è un byte, entrambe le famiglie di hash condividono un'unica copia della permutazione a 24 round, e i due initializer sono l'unico punto in cui i percorsi divergono
Type
  TSHA3Context= Record
    State: Array [0.. 24] Of UInt64;
    Buffer: Array [0.. 135] Of Byte;
    BufferLength: Integer;
    Rate: Integer;
    OutputLength: Integer;
    // Separatore di dominio: $06 per SHA3, $1F per SHAKE (FIPS 202 §6.2/6.3)
    Suffix: Byte;
  End;

Procedure SHA3Init(Var Context: TSHA3Context; OutputBits: Integer);
Begin
  FillChar(Context, SizeOf(Context), 0);
  Context.OutputLength:= OutputBits div 8;
  Context.Rate:= 200- 2* Context.OutputLength;   // il rate segue la dimensione del digest
  Context.Suffix:= $06;
End;

// SHAKE256: il rate è fissato a 136 byte qualunque cosa chieda il chiamante,
// perché SHAKE è una funzione a output estendibile
Procedure SHAKE256Init(Var Context: TSHA3Context; OutputBytes: Integer);
Begin
  FillChar(Context, SizeOf(Context), 0);
  Context.OutputLength:= OutputBytes;
  Context.Rate:= 136;
  Context.Suffix:= $1F;
End;

La formula in SHA3Init dà 136, 104 e 72 byte di rate per SHA3-256, SHA3-384 e SHA3-512, quindi rate e livello di sicurezza sono saldati insieme per la famiglia a lunghezza fissa. SHAKE256Init rompe di proposito quel legame: OutputLength diventa una richiesta di byte, non una dichiarazione di forza di sicurezza, e la capacità della spugna resta a 512 bit qualunque quantità di output prelevi. Quel disaccoppiamento è la definizione di funzione a output estendibile, e scriverlo come due initializer su un unico contesto rende la distinzione visibile nel sorgente invece che sepolta in un commento

Il tetto di 136 byte sul punto di ingresso one-shot

Le funzioni di convenienza in PDFlibPas rifiutano qualsiasi cosa fuori da 1..136 byte, e quel limite è onesto, non arbitrario. SHAKE256StreamRange e SHAKE256ByteArray passano entrambe da SHA3Final, che riempie l'ultimo blocco, permuta una volta e legge l'output dritto dallo stato. Solo i primi Rate byte di quello stato sono output legale, quindi una squeeze produce al massimo 136 byte; oltre serve una permutazione fresca per ogni blocco. Entrambi i consumatori reali stanno comunque nella finestra: il profilo firma PDF digerisce 32 byte, e Ed448 ne serve 114 secondo RFC 8032 §5.2. SHAKE256StreamRange prende la stessa tripla FirstLength, SecondStart, SecondLength di SHA256StreamRange e SHA3_256StreamRange, quindi il codice di validazione percorre il /ByteRange di una firma allo stesso modo per ogni algoritmo e non fa mai un caso speciale dello XOF

Cosa va storto in pad10*1 quando la posizione finisce su rate-1?

Niente, a patto che tu faccia XOR di entrambi i byte di padding nello stato invece di assegnarli. FIPS 202 §B.2 mette il suffisso di dominio alla posizione corrente di assorbimento e $80 all'ultimo byte del blocco. Quando il messaggio lascia per caso la posizione a Rate- 1, quelle due scritture colpiscono lo stesso byte e devono combinarsi in $9F. Se memorizzi invece di fare XOR, uno dei due vince, il padding è silenziosamente sbagliato per esattamente una lunghezza di messaggio su 136, e ogni vettore di regressione che possiedi passa perché nessuno finisce lì. PLShakeXOFFinalize in PDFlibPas fa XOR di entrambi, il che fa ricadere il caso di sovrapposizione in quello generale senza rami

Il caso di sovrapposizione pad10*1 in PDFlibPas: PLShakeXOFFinalize mette il suffisso di dominio alla posizione corrente di assorbimento e $80 all'ultimo byte del blocco; quando la posizione finisce su Rate meno 1 entrambe le scritture colpiscono un byte, lo XOR le combina in $9F e una semplice memorizzazione corromperebbe una lunghezza di messaggio su 136
Memorizzare invece di fare XOR lascia vincere un byte di padding, e poiché il difetto appare per esattamente una lunghezza di messaggio su 136, ogni vettore di regressione che possiedi può passare mentre il padding è silenziosamente sbagliato
// Riempie l'ultimo blocco parziale ed entra nella fase di squeeze
Procedure PLShakeXOFFinalize(Var Ctx: TPLShakeXOF);
Begin
  If Not Ctx.Absorbing Then
    Exit;
  Ctx.Absorbing:= False;
  // suffisso a SqueezePos, $80 a Rate-1; quando SqueezePos = Rate-1 i due
  // XOR finiscono sullo stesso byte e si combinano correttamente in $9F
  Ctx.State[Ctx.SqueezePos div 8]:= Ctx.State[Ctx.SqueezePos div 8]xor
    (UInt64(Ctx.Suffix)shl (8* (Ctx.SqueezePos and 7)));
  Ctx.State[(Ctx.Rate- 1)div 8]:= Ctx.State[(Ctx.Rate- 1)div 8]xor
    (UInt64($80)shl (8* ((Ctx.Rate- 1)and 7)));
  SHA3Permute(Ctx.State);
  Ctx.SqueezePos:= 0;
End;

TPLShakeXOF: assorbire dritto nella spugna

PDFlibPas espone output senza limiti attraverso un record separato, TPLShakeXOF, che tiene solo State come 25 parole UInt64, Rate, Suffix, un flag Absorbing e SqueezePos. Non c'è buffer di messaggio: PLShakeXOFAbsorb fa XOR dei byte in arrivo direttamente nello stato e permuta ogni volta che riempie un blocco, con SqueezePos che fa da offset in entrambe le fasi. PLShakeXOFInit sceglie la variante con un booleano, True per SHAKE128 a rate 168 e False per SHAKE256 a rate 136, e PLShakeXOFSqueeze continua a permutare finché il chiamante ha il numero di byte richiesto. L'output oltre il confine di blocco è stato verificato a 200 byte, oltre il primo confine di blocco in entrambe le varianti

SHAKE streaming in PDFlibPas attraverso TPLShakeXOF, un record con State, Rate, Suffix, un flag Absorbing e SqueezePos ma senza buffer di messaggio: PLShakeXOFInit sceglie SHAKE128 a rate 168 o SHAKE256 a rate 136, PLShakeXOFAbsorb fa XOR dei byte dritti nella spugna, PLShakeXOFFinalize riempie e permuta, e PLShakeXOFSqueeze continua a permutare finché il chiamante ha tutti i byte
Assorbire in più chiamate senza prima concatenare è ciò che permette a Ed448 di prelevare i suoi 114 byte e al porting ML-DSA di fare squeeze all'infinito, una copertura che nessun punto di ingresso one-shot con il suo tetto di 136 byte potrebbe sostenere
Var
  Ctx: TPLShakeXOF;
  Digest: Array [0.. 113] Of Byte;   // Ed448 vuole 114 byte, RFC 8032 §5.2
Begin
  PLShakeXOFInit(Ctx, False);        // False = SHAKE256, rate 136
  PLShakeXOFAbsorb(Ctx, @Dom4[1], Length(Dom4));
  PLShakeXOFAbsorb(Ctx, @Prefix[1], Length(Prefix));
  PLShakeXOFAbsorb(Ctx, @Msg[1], Length(Msg));
  PLShakeXOFFinalize(Ctx);
  PLShakeXOFSqueeze(Ctx, @Digest[0], SizeOf(Digest));
End;

// Oppure, quando ti basta un buffer in ingresso e uno in uscita:
//   PLShakeXOF(False, Input, Output, 114)

Assorbire in più chiamate conta più di quanto sembri. Ed448 antepone al messaggio una stringa dom4 e un valore di 57 byte prima di fare l'hash, e un rejection sampler vuole alimentare un seed e byte di indice senza concatenare prima le stringhe. Fare arrivare il record streaming prima di entrambi i consumatori era il punto: il lavoro su curve Ed448 e Brainpool in Pascal puro serve 114 byte in un colpo solo, e il port FIPS 204 di ML-DSA fa squeeze all'infinito da entrambe le varianti. Nessuno dei due avrebbe potuto essere scritto contro l'API one-shot

Perché PDFlibPas si rifiuta di esporre SHAKE256 come selettore di digest?

Perché non c'è un modo legale di selezionarlo. TPDFlibDigestAlgorithm in PDFlibDigitalSign va da daAuto, daSHA1, daSHA256, daSHA384, daSHA512, daSHA3_256, daSHA3_384, daSHA3_512 e si ferma. Non esiste daSHAKE256, e l'omissione è una decisione, non una svista. RFC 8702 registra SHAKE in CMS, ma dentro una firma PDF l'unico posto in cui ISO/TS 32002 mette SHAKE256 è come digest interno di Ed448, dove lo sceglie l'algoritmo di firma e non il chiamante. Pubblicarlo come valore DigestAlgorithm a sé stante permetterebbe a chiunque di accoppiare SHAKE256 con RSA o ECDSA e produrre un dizionario di firma che nessun validatore accetterà. La capacità viene distribuita, il selettore no, e il codice lato verifica chiama SHAKE256StreamRange direttamente sul layout /ByteRange. Se stai scegliendo un digest per firme che una controparte deve validare oggi, daSHA256 fino a daSHA3_512 dentro un flusso di lavoro PAdES da B-B a B-LTA con timestamp e dati di validazione a lungo termine resta la risposta; ISO/TS 32001 è ciò che governa quando SHA-3 diventerà routine nel PDF

Testare uno XOF: il vettore che sembra passare

Confronta l'intera lunghezza dell'output e usa più di un vettore. Il risultato SHAKE256 della stringa vuota è quello che tutti ricordano a memoria, ed è un test debole proprio perché è citato ovunque. L'output SHAKE256 a 32 byte di abc è peggio: i suoi primi 16 byte, 483366601360a8771c6863080cc4114d, coincidono con diversi vettori vicini, quindi un confronto troncato riporta successo mentre la metà discriminante, 8db44530f8f1e1ee4f94ea37e78b5739, resta non verificata. La regressione di PDFlibPas confronta entrambi i vettori su tutta la loro lunghezza. Aggiungi anche un caso oltre il confine di blocco, perché un ciclo di squeeze che non attraversa mai un confine di rate non è stato testato affatto

La lezione più ampia dal piegare SHAKE dentro un'implementazione SHA-3 esistente è che il codice condiviso non è mai stato il rischio. Keccak-f[1600] è la stessa permutazione per entrambe le famiglie e o corrisponde ai vettori o no. Il rischio vive nei due byte e nell'intero che differiscono, e nel tetto che devi dichiarare onestamente invece di lasciare che i chiamanti lo scoprano. Per vedere come il livello digest si inserisce accanto a firma, cifratura e tooling PDF/A in un codebase Object Pascal nativo, la pagina prodotto della libreria PDF Delphi PDFlibPas elenca l'insieme completo dei componenti e la matrice dei compilatori supportati