PDFlibPas implementerar SHA-3 och SHAKE256 på en enda Keccak-f[1600]-permutation i Object Pascal. De två familjerna skiljer sig på exakt två ställen: pad10*1-domänbyten, $06 för SHA-3 och $1F för SHAKE enligt FIPS 202 §6.2 och §6.3, och rateregeln. SHAKE256 låser sin rate vid 136 byte oavsett utdatalängd
Den andra skillnaden är den folk får fel, för den ser inte ut som en skillnad alls. SHA3-256 kör också vid rate 136, så en SHAKE256 byggd genom att kopiera SHA-3-kodvägen passerar varje 32-byte-test du kastar på den och producerar sedan skräp första gången någon ber om 64. Vad som följer är hur de två familjerna hamnade på att dela en enda kontextpost i PDFlibDigest, var one-shot-ingångspunkten slutar, och varför den strömmande XOF:en måste finnas innan Ed448 eller ML-DSA kunde
Varför delar SHA-3 och SHAKE256 en Keccak-kontext?
Därför att det enda per-familj-tillståndet är en enda byte, och PDFlibPas lagrar den i TSHA3Context som Suffix. När domänseparatorn bor i kontexten i stället för att vara hardcodad i finaliseringsrutinen tjänar SHA3AbsorbBlock, SHA3Update och SHA3Final båda familjerna oförändrade, och det finns exakt en kopia av den 24-rundiga permutationen i biblioteket. Initialiserarna är där de två vägarna skiljs åt, och de skiljs åt fullständigt
Type
TSHA3Context= Record
State: Array [0.. 24] Of UInt64;
Buffer: Array [0.. 135] Of Byte;
BufferLength: Integer;
Rate: Integer;
OutputLength: Integer;
// Domänseparator: $06 för SHA3, $1F för 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öljer digeststorleken
Context.Suffix:= $06;
End;
// SHAKE256: raten är fast vid 136 byte vad än anroparen ber om,
// eftersom SHAKE är 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;
Formeln i SHA3Init ger 136, 104 och 72 byte rate för SHA3-256, SHA3-384 och SHA3-512, så rate och säkerhetsnivå är ihopvetsade för den fastlängdsfamiljen. SHAKE256Init bryter den länken medvetet: OutputLength blir en begäran om byte, inte en deklaration av säkerhetsstyrka, och svampkapaciteten förblir 512 bitar oavsett hur mycket utdata du drar. Den frikopplingen är definitionen av en extendable-output-funktion, och att skriva den som två initialiserare över en kontext gör distinktionen synlig i källkoden snarare än begravd i en kommentar
136-byte-taket på one-shot-ingångspunkten
Bekvämlighetsfunktionerna i PDFlibPas vägrar allt utanför 1..136 byte, och den gränsen är ärlig snarare än godtycklig. SHAKE256StreamRange och SHAKE256ByteArray går båda genom SHA3Final, som paddar det sista blocket, permuterar en gång och läser utdata direkt ur tillståndet. Bara de första Rate byten av det tillståndet är laglig utdata, så en squeeze ger som mest 136 byte; bortom det behöver du en färsk permutation per block. Båda verkliga konsumenterna ryms i fönstret ändå: PDF-signaturprofilen digerar 32 byte, och Ed448 behöver 114 enligt RFC 8032 §5.2. SHAKE256StreamRange tar samma FirstLength, SecondStart, SecondLength-trippel som SHA256StreamRange och SHA3_256StreamRange, så valideringskod vandrar igenom en signaturs /ByteRange på samma sätt för varje algoritm och specialbehandlar aldrig XOF:en
Vad går fel i pad10*1 när positionen hamnar på rate-1?
Ingenting, förutsatt att du XOR:ar båda pad-byten in i tillståndet i stället för att tilldela dem. FIPS 202 §B.2 placerar domänsuffixet vid den aktuella absorbpositionen och $80 vid blockets sista byte. När meddelandet råkar lämna positionen vid Rate- 1 riktar de två skrivningarna in sig på samma byte och måste kombineras till $9F. Lagra i stället för att XOR:a och en av dem vinner, paddingen är tyst fel för exakt en meddelandelängd av 136, och varje regressionvektor du äger passerar eftersom ingen av dem hamnar där. PLShakeXOFFinalize i PDFlibPas XOR:ar båda, vilket får överlappningsfallet att falla ur det allmänna fallet utan gren
// Paddar det sista partiella blocket och går in i squeeze-fasen
Procedure PLShakeXOFFinalize(Var Ctx: TPLShakeXOF);
Begin
If Not Ctx.Absorbing Then
Exit;
Ctx.Absorbing:= False;
// suffix vid SqueezePos, $80 vid Rate-1; när SqueezePos = Rate-1 hamnar de två
// XOR:arna på samma byte och kombineras korrekt till $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: att absorbera rakt in i svampen
PDFlibPas exponerar obegränsad utdata genom en separat post, TPLShakeXOF, som bara håller State som 25 UInt64-ord, Rate, Suffix, en Absorbing-flagga och SqueezePos. Det finns ingen meddelandebuffert: PLShakeXOFAbsorb XOR:ar inkommande byte direkt in i tillståndet och permuterar närhelst den fyller ett block, med SqueezePos som offset i båda faserna. PLShakeXOFInit väljer variant med en boolean, True för SHAKE128 vid rate 168 och False för SHAKE256 vid rate 136, och PLShakeXOFSqueeze fortsätter permutera tills anroparen har det antal byte den bad om. Utdata över blockgränser kontrollerades vid 200 byte, förbi den första blockgränsen i båda varianterna
Var
Ctx: TPLShakeXOF;
Digest: Array [0.. 113] Of Byte; // Ed448 vill ha 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;
// Eller, när en buffert in och en buffert ut är allt du behöver:
// PLShakeXOF(False, Input, Output, 114)
Att absorbera i flera anrop spelar större roll än det ser ut. Ed448 prefixar meddelandet med en dom4-sträng och ett 57-byte-värde före hashning, och en rejection sampler vill mata ett seed och index-byte utan att först konkatenera strängar. Att den strömmande posten landade före endera konsumenten var poängen: det rena Pascal-Ed448- och Brainpool-kurvarbetet behöver 114 byte i ett svep, och FIPS 204-ML-DSA-porten squeezar oändligt från båda varianterna. Ingen av dem kunde ha skrivits mot one-shot-API:et
Varför vägrar PDFlibPas att exponera SHAKE256 som digest-väljare?
Därför att det inte finns något lagligt sätt att välja den. TPDFlibDigestAlgorithm i PDFlibDigitalSign kör daAuto, daSHA1, daSHA256, daSHA384, daSHA512, daSHA3_256, daSHA3_384, daSHA3_512, och stannar. Det finns ingen daSHAKE256, och utelämnandet är ett beslut, inte en förbiseelse. RFC 8702 registrerar SHAKE i CMS, men inuti en PDF-signatur är det enda ställe ISO/TS 32002 placerar SHAKE256 som Ed448:s interna digest, där signaturalgoritmen väljer den och anroparen inte gör det. Att publicera den som ett fristående DigestAlgorithm-värde skulle låta vem som helst para ihop SHAKE256 med RSA eller ECDSA och producera en signaturdictionary ingen validator accepterar. Förmågan levereras, väljaren gör det inte, och verifieringssidan anropar SHAKE256StreamRange över /ByteRange-layouten direkt. Väljer du digest för signaturer som en motpart måste validera i dag, förblir daSHA256 till daSHA3_512 inuti ett PAdES-B-B-till-B-LTA-arbetsflöde med tidsstämpling och långtidsvalideringsdata svaret; ISO/TS 32001 är det som styr när SHA-3 över huvud taget blir rutin i PDF
Att testa en XOF: vektorn som ser ut att gå igenom
Jämför hela utdatalängden och använd mer än en vektor. SHAKE256-resultatet för den tomma strängen är det alla memorerar, och det är ett svagt test just för att det citeras så brett. 32-byte-utdatan för abc är värre: dess första 16 byte, 483366601360a8771c6863080cc4114d, sammanfaller med flera grannvektorer, så en trunkerad jämförelse rapporterar framgång medan den diskriminerande halvan, 8db44530f8f1e1ee4f94ea37e78b5739, går okontrollerad. PDFlibPas-regressionen jämför båda vektorerna över hela sin längd. Lägg också till ett fall över blockgräns, för en squeeze-loop som aldrig korsar en rate-gräns har inte testats över huvud taget
Den bredare läxan av att vika in SHAKE i en befintlig SHA-3-implementation är att den delade koden aldrig var risken. Keccak-f[1600] är samma permutation för båda familjerna och den antingen matchar vektorerna eller inte. Risken bor i de två byten och en heltal som skiljer sig, och i taket du måste uttala ärligt i stället för att låta anropare upptäcka det. För att se hur digestlagret passar bredvid signerings-, krypterings- och PDF/A-verktygen i en native Object Pascal-kodbas listar PDFlibPas Delphi PDF-bibliotekets produktsida hela komponentsetet och den stödda kompilatormatrisen