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
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
// 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
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