PDFlibPas implementuje SHA-3 a SHAKE256 na jediné permutaci Keccak-f[1600] v Object Pascalu. Obě rodiny se liší přesně na dvou místech: doménovém bajtu pad10*1, $06 pro SHA-3 a $1F pro SHAKE podle FIPS 202 §6.2 a §6.3, a pravidle rate. SHAKE256 má rate ukotvené na 136 bajtech bez ohledu na délku výstupu
Ta druhá odlišnost je ta, kterou lidé kazí, protože vůbec nevypadá jako odlišnost. SHA3-256 také běží s rate 136, takže SHAKE256 postavené zkopírováním cesty kódu SHA-3 projde každým 32bajtovým testem, který na něj hodíte, a poprvé vyrobí odpad, když někdo požádá o 64. Následuje to, jak obě rodiny skončily sdílející jeden kontextový záznam v PDFlibDigest, kde končí jednorázový vstupní bod, a proč musel streamovací XOF existovat dřív, než mohlo Ed448 nebo ML-DSA
Proč SHA-3 a SHAKE256 sdílejí jeden Keccak kontext?
Protože jediný stav závislý na rodině je jediný bajt a PDFlibPas ho ukládá v TSHA3Context jako Suffix. Jakmile doménový oddělovač žije v kontextu místo toho, aby byl natvrdo zapsaný ve finalizační rutině, slouží SHA3AbsorbBlock, SHA3Update a SHA3Final oběma rodinám beze změny a v knihovně je přesně jedna kopie 24kolové permutace. Inicializátory jsou místo, kde se obě cesty rozcházejí, a rozcházejí se úplně
Type
TSHA3Context= Record
State: Array [0.. 24] Of UInt64;
Buffer: Array [0.. 135] Of Byte;
BufferLength: Integer;
Rate: Integer;
OutputLength: Integer;
// Doménový oddělovač: $06 pro SHA3, $1F pro 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; // rate se řídí velikostí digestu
Context.Suffix:= $06;
End;
// SHAKE256: rate je pevně 136 bajtů, ať volající žádá cokoli,
// protože SHAKE je funkce s rozšiřitelným výstupem
Procedure SHAKE256Init(Var Context: TSHA3Context; OutputBytes: Integer);
Begin
FillChar(Context, SizeOf(Context), 0);
Context.OutputLength:= OutputBytes;
Context.Rate:= 136;
Context.Suffix:= $1F;
End;
Vzorec v SHA3Init dává 136, 104 a 72 bajtů rate pro SHA3-256, SHA3-384 a SHA3-512, takže u rodiny s pevnou délkou jsou rate a úroveň zabezpečení svařené dohromady. SHAKE256Init toto spojení záměrně ruší: OutputLength se stává žádostí o bajty, ne prohlášením o síle zabezpečení, a kapacita houby zůstává 512 bitů, ať odeberete výstupu kolik chcete. Toto odpoutání je definicí funkce s rozšiřitelným výstupem a zapsat ho jako dva inicializátory nad jedním kontextem dělá rozdíl viditelným ve zdrojáku, ne pohřbeným v komentáři
Strop 136 bajtů na jednorázovém vstupním bodu
Pohodlné funkce v PDFlibPas odmítají cokoli mimo 1..136 bajtů a ten limit je upřímný, ne svévolný. SHAKE256StreamRange i SHAKE256ByteArray jdou přes SHA3Final, který doplní poslední blok, jednou permutuje a čte výstup přímo ze stavu. Jen prvních Rate bajtů toho stavu je legální výstup, takže jeden squeeze dá nejvýš 136 bajtů; za tím potřebujete čerstvou permutaci pro každý blok. Oba skuteční konzumenti se do okna stejně vejdou: profil PDF podpisu dělá digest 32 bajtů a Ed448 potřebuje 114 podle RFC 8032 §5.2. SHAKE256StreamRange bere stejnou trojici FirstLength, SecondStart, SecondLength jako SHA256StreamRange a SHA3_256StreamRange, takže validační kód prochází /ByteRange podpisu stejně pro každý algoritmus a nikdy nedělá speciální případ pro XOF
Co se pokazí v pad10*1, když pozice dopadne na rate-1?
Nic, pokud oba pad bajty do stavu XORujete místo přiřazení. FIPS 202 §B.2 dává doménový suffix na aktuální pozici absorbování a $80 na poslední bajt bloku. Když zpráva náhodou nechá pozici na Rate- 1, tyto dva zápisy míří na stejný bajt a musí se spojit do $9F. Uložíte místo XORu a jeden z nich vyhraje, padding je potichu špatný přesně pro jednu délku zprávy ze 136 a každý regresní vektor, který vlastníte, projde, protože žádný z nich tam nesedí. PLShakeXOFFinalize v PDFlibPas XORuje oba, což nechá případ překryvu vypadnout z obecného bez větvení
// Doplní poslední částečný blok a vstoupí do fáze squeeze
Procedure PLShakeXOFFinalize(Var Ctx: TPLShakeXOF);
Begin
If Not Ctx.Absorbing Then
Exit;
Ctx.Absorbing:= False;
// suffix na SqueezePos, $80 na Rate-1; když SqueezePos = Rate-1, oba
// XORy dopadnou na stejný bajt a správně se spojí do $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: absorbování přímo do houby
PDFlibPas vystavuje neomezený výstup přes samostatný záznam TPLShakeXOF, který drží jen State jako 25 slov UInt64, Rate, Suffix, příznak Absorbing a SqueezePos. Není tu žádný buffer zprávy: PLShakeXOFAbsorb XORuje příchozí bajty přímo do stavu a permutuje, kdykoli zaplní blok, přičemž SqueezePos slouží jako offset v obou fázích. PLShakeXOFInit vybírá variantu jedním booleanem, True pro SHAKE128 s rate 168 a False pro SHAKE256 s rate 136, a PLShakeXOFSqueeze permutuje dál, dokud volající nemá počet bajtů, o který žádal. Výstup přes hranici bloku byl zkontrolován na 200 bajtech, za první hranicí bloku u obou variant
Var
Ctx: TPLShakeXOF;
Digest: Array [0.. 113] Of Byte; // Ed448 chce 114 bajtů, 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;
// Nebo když potřebujete jen jeden buffer dovnitř a jeden ven:
// PLShakeXOF(False, Input, Output, 114)
Absorbování v několika voláních záleží víc, než vypadá. Ed448 prefixuje zprávu řetězcem dom4 a 57bajtovou hodnotou před hashováním a rejection sampler chce krmit seed a indexové bajty bez předchozího spojování řetězců. To, že streamovací záznam vznikl dřív než oba konzumenti, byl smysl: čistě pascalová práce s křivkami Ed448 a Brainpool potřebuje 114 bajtů najednou a port FIPS 204 ML-DSA mačká z obou variant neomezeně. Ani jedno by se nedalo napsat proti jednorázovému API
Proč PDFlibPas odmítá vystavit SHAKE256 jako volič digestu?
Protože neexistuje legální způsob, jak ho vybrat. TPDFlibDigestAlgorithm v PDFlibDigitalSign proběhne daAuto, daSHA1, daSHA256, daSHA384, daSHA512, daSHA3_256, daSHA3_384, daSHA3_512 a skončí. Žádné daSHAKE256 neexistuje a to vynechání je rozhodnutí, ne přehlédnutí. RFC 8702 registruje SHAKE v CMS, ale uvnitř PDF podpisu je jediné místo, kam ISO/TS 32002 SHAKE256 dává, interní digest Ed448, kde ho vybírá algoritmus podpisu a volající ne. Zveřejnění jako samostatné hodnoty DigestAlgorithm by komukoli umožnilo spárovat SHAKE256 s RSA nebo ECDSA a vyrobit slovník podpisu, který žádný validátor nepřijme. Schopnost se dodává, volič ne, a kód na straně ověřování volá SHAKE256StreamRange přímo nad rozložením /ByteRange. Pokud vybíráte digest pro podpisy, které dnes musí validovat protistrana, zůstává odpovědí daSHA256 až daSHA3_512 v pracovním postupu PAdES B-B až B-LTA s časovým razítkem a daty dlouhodobé validace; kdy se SHA-3 v PDF vůbec stane rutinou, řídí ISO/TS 32001
Testování XOF: vektor, který vypadá, že prošel
Porovnejte celou délku výstupu a použijte víc než jeden vektor. Výsledek SHAKE256 pro prázdný řetězec je ten, který si každý pamatuje, a je to slabý test právě proto, že je tak široce citovaný. 32bajtový výstup SHAKE256 pro abc je horší: jeho prvních 16 bajtů, 483366601360a8771c6863080cc4114d, se shoduje s několika sousedními vektory, takže zkrácené porovnání hlásí úspěch, zatímco rozlišující polovina, 8db44530f8f1e1ee4f94ea37e78b5739, zůstává nezkontrolovaná. Regrese v PDFlibPas porovnává oba vektory přes celou jejich délku. Přidejte i případ přes hranici bloku, protože squeeze smyčka, která nikdy nepřekročí hranici rate, nebyla otestována vůbec
Širší ponaučení ze začlenění SHAKE do existující implementace SHA-3 je, že sdílený kód nikdy nebyl rizikem. Keccak-f[1600] je pro obě rodiny stejná permutace a buď odpovídá vektorům, nebo ne. Riziko žije ve dvou bajtech a jednom celém čísle, které se liší, a ve stropu, který musíte uvést upřímně, místo abyste nechali volající, aby ho objevili. Chcete-li vidět, jak vrstva digestu zapadá vedle nástrojů pro podepisování, šifrování a PDF/A v nativní codebase Object Pascalu, produktová stránka knihovny PDFlibPas Delphi PDF vyjmenovává kompletní sadu komponent a podporovanou matici kompilátorů