Technický článek

SHAKE256 XOF v Delphi: Keccak domain separation

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ě

Jeden Keccak kontext sloužící dvěma rodinám v PDFlibPas: TSHA3Context ukládá doménový oddělovač do jediného bajtu Suffix, SHA3Init nastavuje suffix na $06 s Rate odvozeným jako 200 minus dvojnásobek délky výstupu a SHAKE256Init nastavuje $1F s rate ukotveným na 136 bajtech, takže SHA3AbsorbBlock, SHA3Update a SHA3Final slouží oběma beze změny
Protože jediný stav závislý na rodině je jeden bajt, obě hash rodiny sdílejí jedinou kopii 24kolové permutace a oba inicializátory jsou jediné místo, kde se cesty rozcházejí
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í

Případ překryvu pad10*1 v PDFlibPas: PLShakeXOFFinalize umisťuje doménový suffix na aktuální pozici absorbování a $80 na poslední bajt bloku; když pozice dopadne na Rate minus 1, míří oba zápisy na jeden bajt, XOR je spojí do $9F a obyčejné uložení by poškodilo jednu délku zprávy ze 136
Uložení místo XORu nechá vyhrát jeden pad bajt, a protože se vada objeví přesně pro jednu délku zprávy ze 136, může každý regresní vektor, který vlastníte, projít, zatímco padding je potichu špatný
// 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

Streamovací SHAKE v PDFlibPas přes TPLShakeXOF, záznam se State, Rate, Suffix, příznakem Absorbing a SqueezePos, ale bez bufferu zprávy: PLShakeXOFInit vybírá SHAKE128 s rate 168 nebo SHAKE256 s rate 136, PLShakeXOFAbsorb XORuje bajty přímo do houby, PLShakeXOFFinalize doplňuje a permutuje a PLShakeXOFSqueeze permutuje dál, dokud volající nemá každý bajt
Absorbování v několika voláních bez předchozího spojování je to, co nechá Ed448 odebrat svých 114 bajtů a port ML-DSA mačkat neomezeně, pokrytí, které by žádný jednorázový vstupní bod se svým stropem 136 bajtů neunesl
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ů