Teknisk artikel

SHAKE256 XOF i Delphi: Keccak-domainseparation

PDFlibPas implementerer SHA-3 og SHAKE256 på én enkelt Keccak-f[1600]-permutation i Object Pascal. De to familier adskiller sig på præcis to steder: pad10*1-domænebyten, $06 for SHA-3 og $1F for SHAKE ifølge FIPS 202 §6.2 og §6.3, og rate-reglen. SHAKE256 låser sin rate på 136 bytes uanset output-længden

Den anden forskel er den, folk får forkert, fordi den slet ikke ligner en forskel. SHA3-256 kører også på rate 136, så en SHAKE256 bygget ved at kopiere SHA-3-kodevejen består hver 32-byte-test, du kaster efter den, og producerer derefter skrald første gang nogen beder om 64. Det følgende handler om, hvordan de to familier endte med at dele én context-record i PDFlibDigest, hvor one-shot-indgangspunktet stopper, og hvorfor streaming-XOF'en skulle eksistere, før Ed448 eller ML-DSA kunne

Hvorfor deler SHA-3 og SHAKE256 én Keccak-context?

Fordi den eneste per-familie-tilstand er en enkelt byte, og PDFlibPas gemmer den i TSHA3Context som Suffix. Når først domæneseparatoren bor i context'en i stedet for at være hardcoded i finaliseringsrutinen, betjener SHA3AbsorbBlock, SHA3Update og SHA3Final begge familier uændret, og der er præcis én kopi af 24-runders-permutationen i biblioteket. Initializerne er der, hvor de to veje divergerer, og de divergerer fuldstændigt

Én Keccak-context, der betjener to familier i PDFlibPas: TSHA3Context gemmer domæneseparatoren i en enkelt Suffix-byte, SHA3Init sætter suffikset til $06 med Rate udledt som 200 minus to gange output-længden, og SHAKE256Init sætter $1F med raten låst på 136 bytes, så SHA3AbsorbBlock, SHA3Update og SHA3Final betjener begge uændret
Fordi den eneste per-familie-tilstand er én byte, deler begge hash-familier én enkelt kopi af 24-runders-permutationen, og de to initializere er det eneste sted, vejene divergerer
Type
  TSHA3Context= Record
    State: Array [0.. 24] Of UInt64;
    Buffer: Array [0.. 135] Of Byte;
    BufferLength: Integer;
    Rate: Integer;
    OutputLength: Integer;
    // Domæneseparator: $06 for SHA3, $1F for 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;   // raten følger digest-størrelsen
  Context.Suffix:= $06;
End;

// SHAKE256: raten er fast på 136 bytes uanset hvad caller'en beder om,
// fordi SHAKE er en extendable-output-funktion
Procedure SHAKE256Init(Var Context: TSHA3Context; OutputBytes: Integer);
Begin
  FillChar(Context, SizeOf(Context), 0);
  Context.OutputLength:= OutputBytes;
  Context.Rate:= 136;
  Context.Suffix:= $1F;
End;

Formlen i SHA3Init giver 136, 104 og 72 bytes rate for SHA3-256, SHA3-384 og SHA3-512, så rate og sikkerhedsniveau er svejset sammen for familien med fast længde. SHAKE256Init bryder bevidst den forbindelse: OutputLength bliver en anmodning om bytes, ikke en erklæring om sikkerhedsstyrke, og sponge-kapaciteten forbliver på 512 bits, uanset hvor meget output du trækker. Den afkobling er definitionen på en extendable-output-funktion, og at skrive det som to initializere over én context gør forskellen synlig i kilden frem for begravet i en kommentar

136-byte-loftet på one-shot-indgangspunktet

Bekvemmelighedsfunktionerne i PDFlibPas afviser alt uden for 1..136 bytes, og den grænse er ærlig frem for vilkårlig. SHAKE256StreamRange og SHAKE256ByteArray ruter begge gennem SHA3Final, som padder den sidste blok, permuterer én gang og læser output direkte ud af staten. Kun de første Rate bytes af den state er lovligt output, så ét squeeze giver højst 136 bytes; ud over det skal du bruge en frisk permutation per blok. Begge rigtige forbrugere passer alligevel inden for vinduet: PDF-signaturprofilen digester 32 bytes, og Ed448 skal bruge 114 ifølge RFC 8032 §5.2. SHAKE256StreamRange tager den samme FirstLength, SecondStart, SecondLength-tripel som SHA256StreamRange og SHA3_256StreamRange, så valideringskode gennemgår en signaturs /ByteRange på samme måde for hver algoritme og special-caser aldrig XOF'en

Hvad går galt i pad10*1, når positionen lander på rate-1?

Intet, forudsat at du XOR'er begge pad-bytes ind i staten i stedet for at tildele dem. FIPS 202 §B.2 placerer domænesuffikset ved den aktuelle absorb-position og $80 ved den sidste byte i blokken. Når beskeden tilfældigvis efterlader positionen ved Rate- 1, rammer de to skrivninger den samme byte og skal kombineres til $9F. Gemmer du i stedet for at XOR'e, vinder den ene af dem, paddingen er lydløst forkert for præcis én beskedlængde ud af 136, og hver regression-vektor, du ejer, passerer, fordi ingen af dem lander der. PLShakeXOFFinalize i PDFlibPas XOR'er begge, hvilket får overlapstilfældet til at falde ud af det generelle tilfælde uden nogen branch

pad10*1-overlapstilfældet i PDFlibPas: PLShakeXOFFinalize placerer domænesuffikset ved den aktuelle absorb-position og $80 ved den sidste byte i blokken; når positionen lander på Rate minus 1, rammer begge skrivninger én byte, XOR kombinerer dem til $9F, og en almindelig store ville korrumpere én beskedlængde ud af 136
At gemme i stedet for at XOR'e lader én pad-byte vinde, og fordi defekten optræder for præcis én beskedlængde ud af 136, kan hver regression-vektor, du ejer, passere, mens paddingen er lydløst forkert
// Padder den sidste delvise blok og går ind i squeeze-fasen
Procedure PLShakeXOFFinalize(Var Ctx: TPLShakeXOF);
Begin
  If Not Ctx.Absorbing Then
    Exit;
  Ctx.Absorbing:= False;
  // suffiks ved SqueezePos, $80 ved Rate-1; når SqueezePos = Rate-1 lander de to
  // XOR'er på samme byte og kombineres korrekt til $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: at absorbere direkte ind i spongen

PDFlibPas eksponerer ubegrænset output gennem en separat record, TPLShakeXOF, der kun holder State som 25 UInt64-ord, Rate, Suffix, et Absorbing-flag og SqueezePos. Der er ingen message-buffer: PLShakeXOFAbsorb XOR'er indkommende bytes direkte ind i staten og permuterer, hver gang den fylder en blok, med SqueezePos som offset i begge faser. PLShakeXOFInit vælger varianten med én boolean, True for SHAKE128 på rate 168 og False for SHAKE256 på rate 136, og PLShakeXOFSqueeze bliver ved med at permutere, indtil caller'en har det byteantal, den bad om. Cross-block-output blev tjekket ved 200 bytes, forbi den første blokgrænse i begge varianter

Streaming-SHAKE i PDFlibPas gennem TPLShakeXOF, en record med State, Rate, Suffix, et Absorbing-flag og SqueezePos, men ingen message-buffer: PLShakeXOFInit vælger SHAKE128 på rate 168 eller SHAKE256 på rate 136, PLShakeXOFAbsorb XOR'er bytes direkte ind i spongen, PLShakeXOFFinalize padder og permuterer, og PLShakeXOFSqueeze bliver ved med at permutere, indtil caller'en har hver byte
At absorbere i flere kald uden først at konkatenere er det, der lader Ed448 trække sine 114 bytes og ML-DSA-porten squeeze i det uendelige, en dækning som intet one-shot-indgangspunkt med sit 136-byte-loft kunne understøtte
Var
  Ctx: TPLShakeXOF;
  Digest: Array [0.. 113] Of Byte;   // Ed448 vil have 114 bytes, 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;

// Eller, når én buffer ind og én buffer ud er alt, du behøver:
//   PLShakeXOF(False, Input, Output, 114)

At absorbere i flere kald betyder mere, end det ser ud til. Ed448 prefixer beskeden med en dom4-string og en 57-byte-værdi før hashing, og en rejection-sampler vil fodre et seed og indeks-bytes uden først at konkatenere strenge. At streaming-recorden landede før begge forbrugere var pointen: det rene Pascal Ed448- og Brainpool-kurvearbejde har brug for 114 bytes i ét hug, og FIPS 204 ML-DSA-porten squeezer i det uendelige fra begge varianter. Ingen af dem kunne være skrevet mod one-shot-API'et

Hvorfor nægter PDFlibPas at eksponere SHAKE256 som en digest-selector?

Fordi der ingen lovlig måde er at vælge den. TPDFlibDigestAlgorithm i PDFlibDigitalSign kører daAuto, daSHA1, daSHA256, daSHA384, daSHA512, daSHA3_256, daSHA3_384, daSHA3_512 og stopper. Der er intet daSHAKE256, og udeladelsen er en beslutning, ikke en forglemmelse. RFC 8702 registrerer SHAKE i CMS, men inde i en PDF-signatur er det eneste sted, ISO/TS 32002 placerer SHAKE256, som den interne digest af Ed448, hvor signaturalgoritmen vælger den, og caller'en ikke gør. At udgive den som en fritstående DigestAlgorithm-værdi ville lade enhver parre SHAKE256 med RSA eller ECDSA og producere en signaturordbog, ingen validator vil acceptere. Kapaciteten shipper, selectoren gør ikke, og verifikationssiden kalder SHAKE256StreamRange direkte over /ByteRange-layoutet. Hvis du vælger en digest til signaturer, en modpart skal validere i dag, forbliver daSHA256 til daSHA3_512 inde i en PAdES B-B til B-LTA-workflow med timestamping og langsigtet valideringsdata svaret; ISO/TS 32001 er det, der styrer, hvornår SHA-3 overhovedet bliver rutine i PDF

At teste en XOF: vektoren, der ser ud til at passere

Sammenlign hele output-længden, og brug mere end én vektor. Empty-string-SHAKE256-resultatet er det, alle kan udenad, og det er en svag test netop fordi det er så bredt citeret. 32-byte-SHAKE256-outputtet for abc er værre: Dets første 16 bytes, 483366601360a8771c6863080cc4114d, falder sammen med flere nabovektorer, så en trunkeret sammenligning rapporterer succes, mens den diskriminerende halvdel, 8db44530f8f1e1ee4f94ea37e78b5739, går uchecket. PDFlibPas-regressionen sammenligner begge vektorer over hele deres længde. Tilføj også et cross-block-tilfælde, fordi en squeeze-løkke, der aldrig krydser en rate-grænse, slet ikke er blevet testet

Den bredere lære fra at folde SHAKE ind i en eksisterende SHA-3-implementering er, at den delte kode aldrig var risikoen. Keccak-f[1600] er den samme permutation for begge familier, og den matcher enten vektorerne eller også gør den ikke. Risikoen bor i de to bytes og det ene heltal, der adskiller sig, og i det loft, du skal udtale ærligt i stedet for at lade callers opdage. For at se, hvordan digest-laget passer sammen med signing-, encryption- og PDF/A-værktøjet i en nativ Object Pascal-kodebase, oplister PDFlibPas Delphi PDF-bibliotekets produktside det fulde komponentsæt og den understøttede compiler-matrix