PDFlibPas implementiert SHA-3 und SHAKE256 auf einer einzigen Keccak-f[1600]-Permutation in Object Pascal. Die beiden Familien unterscheiden sich an genau zwei Stellen: dem pad10*1-Domainbyte, $06 für SHA-3 und $1F für SHAKE laut FIPS 202 §6.2 und §6.3, und der Ratenregel. SHAKE256 fixiert seine Rate bei 136 Bytes, egal wie lang die Ausgabe wird
Genau der zweite Unterschied ist der, den Leute falsch machen, weil er überhaupt nicht wie ein Unterschied aussieht. SHA3-256 läuft ebenfalls mit Rate 136, daher besteht ein SHAKE256, das durch Kopieren des SHA-3-Codepfads entstanden ist, jeden 32-Byte-Test, den Sie ihm hinwerfen – und produziert beim ersten Mal, wenn jemand 64 will, Müll. Im Folgenden geht es darum, wie die beiden Familien in PDFlibDigest zu einem gemeinsamen Context-Record kamen, wo der One-Shot-Einstiegspunkt aufhört und warum das Streaming-XOF existieren musste, bevor Ed448 oder ML-DSA es konnten
Warum teilen sich SHA-3 und SHAKE256 einen Keccak-Kontext?
Weil der einzige familienabhängige Zustand ein einzelnes Byte ist, und PDFlibPas es in TSHA3Context als Suffix speichert. Lebt der Domain-Separator erst einmal im Kontext statt hartkodiert in der Finalisierungsroutine, bedienen SHA3AbsorbBlock, SHA3Update und SHA3Final beide Familien unverändert, und es gibt genau eine Kopie der 24-Runden-Permutation in der Bibliothek. Die Initialisierer sind die Stelle, an der sich die beiden Pfade trennen – und sie trennen sich vollständig
Type
TSHA3Context= Record
State: Array [0.. 24] Of UInt64;
Buffer: Array [0.. 135] Of Byte;
BufferLength: Integer;
Rate: Integer;
OutputLength: Integer;
// Domain-Separator: $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; // rate folgt der Digest-Größe
Context.Suffix:= $06;
End;
// SHAKE256: Die Rate ist bei 136 Bytes fixiert, egal was der Aufrufer
// verlangt, denn SHAKE ist eine 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;
Die Formel in SHA3Init liefert 136, 104 und 72 Bytes Rate für SHA3-256, SHA3-384 und SHA3-512, daher sind Rate und Sicherheitsniveau bei der Familie mit fester Länge zusammengeschweißt. SHAKE256Init bricht diese Verbindung bewusst: OutputLength wird zu einer Anfrage nach Bytes, nicht zu einer Deklaration von Sicherheitsstärke, und die Sponge-Kapazität bleibt bei 512 Bits, egal wie viel Ausgabe Sie ziehen. Diese Entkopplung ist die Definition einer Extendable-Output-Funktion, und sie als zwei Initialisierer über einem Kontext zu schreiben, macht die Unterscheidung im Quelltext sichtbar statt sie in einem Kommentar zu vergraben
Die 136-Byte-Decke am One-Shot-Einstiegspunkt
Die Komfortfunktionen in PDFlibPas lehnen alles außerhalb von 1..136 Bytes ab, und diese Grenze ist ehrlich statt willkürlich. SHAKE256StreamRange und SHAKE256ByteArray laufen beide durch SHA3Final, das den letzten Block padet, einmal permutiert und die Ausgabe direkt aus dem Zustand liest. Nur die ersten Rate Bytes dieses Zustands sind legale Ausgabe, daher liefert ein Squeeze höchstens 136 Bytes; darüber brauchen Sie eine frische Permutation pro Block. Beide echten Konsumenten passen ohnehin ins Fenster: Das PDF-Signaturprofil digestiert 32 Bytes, und Ed448 braucht 114 laut RFC 8032 §5.2. SHAKE256StreamRange nimmt dasselbe FirstLength-SecondStart-SecondLength-Tripel wie SHA256StreamRange und SHA3_256StreamRange, sodass Validierungscode eine Signatur-/ByteRange für jeden Algorithmus gleich begeht und das XOF nie gesondert behandelt
Was geht in pad10*1 schief, wenn die Position auf Rate-1 landet?
Nichts – vorausgesetzt, Sie XORen beide Pad-Bytes in den Zustand, statt sie zuzuweisen. FIPS 202 §B.2 legt das Domain-Suffix an die aktuelle Absorb-Position und $80 an das letzte Byte des Blocks. Lässt die Nachricht die Position zufällig bei Rate- 1, zielen beide Schreibzugriffe auf dasselbe Byte und müssen sich zu $9F kombinieren. Store statt XOR und einer von beiden gewinnt, das Padding ist für genau eine Nachrichtenlänge von 136 stillschweigend falsch, und jedes Regression-Vektor, das Sie besitzen, besteht, weil keiner dort landet. PLShakeXOFFinalize in PDFlibPas XORt beide, wodurch der Überlappungsfall ohne Verzweigung aus dem allgemeinen Fall herausfällt
// Padet den letzten Teilblock und geht in die Squeeze-Phase
Procedure PLShakeXOFFinalize(Var Ctx: TPLShakeXOF);
Begin
If Not Ctx.Absorbing Then
Exit;
Ctx.Absorbing:= False;
// Suffix an SqueezePos, $80 an Rate-1; wenn SqueezePos = Rate-1, landen
// die beiden XORs auf demselben Byte und kombinieren korrekt zu $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: direkt in den Sponge absorbieren
PDFlibPas stellt unbegrenzte Ausgabe über einen eigenen Record bereit, TPLShakeXOF, der nur State als 25 UInt64-Wörter, Rate, Suffix, ein Absorbing-Flag und SqueezePos hält. Es gibt keinen Nachrichtenpuffer: PLShakeXOFAbsorb XORt ankommende Bytes direkt in den Zustand und permutiert, sobald ein Block voll ist, wobei SqueezePos in beiden Phasen als Offset dient. PLShakeXOFInit wählt die Variante mit einem Boolean: True für SHAKE128 mit Rate 168 und False für SHAKE256 mit Rate 136, und PLShakeXOFSqueeze permutiert weiter, bis der Aufrufer die gewünschte Bytezahl hat. Blockübergreifende Ausgabe wurde bei 200 Bytes geprüft, jenseits der ersten Blockgrenze in beiden Varianten
Var
Ctx: TPLShakeXOF;
Digest: Array [0.. 113] Of Byte; // Ed448 will 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;
// Oder, wenn dir ein Puffer rein und einer raus genügt:
// PLShakeXOF(False, Input, Output, 114)
Das Absorbieren über mehrere Aufrufe ist wichtiger, als es aussieht. Ed448 stellt der Nachricht vor dem Hashen eine dom4-Zeichenkette und einen 57-Byte-Wert voran, und ein Rejection-Sampler will Seed und Index-Bytes füttern, ohne vorher Zeichenketten zu konkatenieren. Genau darum ging es, den Streaming-Record vor beiden Konsumenten fertig zu haben: Die Pure-Pascal-Ed448- und Brainpool-Kurvenarbeit braucht 114 Bytes in einem Zug, und der FIPS-204-ML-DSA-Port squeezt aus beiden Varianten unbegrenzt. Keins davon hätte man gegen die One-Shot-API schreiben können
Warum weigert sich PDFlibPas, SHAKE256 als Digest-Auswahl zu exponieren?
Weil es keinen legalen Weg gibt, es zu wählen. TPDFlibDigestAlgorithm in PDFlibDigitalSign führt daAuto, daSHA1, daSHA256, daSHA384, daSHA512, daSHA3_256, daSHA3_384, daSHA3_512 auf und hört auf. Es gibt kein daSHAKE256, und diese Auslassung ist eine Entscheidung, kein Versehen. RFC 8702 registriert SHAKE in CMS, aber in einer PDF-Signatur ist der einzige Ort, an dem ISO/TS 32002 SHAKE256 einsetzt, der interne Digest von Ed448 – dort wählt ihn der Signaturalgorithmus, nicht der Aufrufer. Es als eigenständigen DigestAlgorithm-Wert zu veröffentlichen, würde jedem erlauben, SHAKE256 mit RSA oder ECDSA zu paaren und ein Signaturdictionary zu erzeugen, das kein Validator akzeptiert. Die Fähigkeit wird ausgeliefert, der Selektor nicht, und Code auf der Verifikationsseite ruft SHAKE256StreamRange direkt über das /ByteRange-Layout auf. Wenn Sie einen Digest für Signaturen wählen, die eine Gegenseite heute validieren muss, bleibt daSHA256 bis daSHA3_512 in einem PAdES-B-B- bis B-LTA-Workflow mit Zeitstempel und langfristigen Validierungsdaten die Antwort; ISO/TS 32001 ist das, was regelt, wann SHA-3 in PDF überhaupt Routine wird
Ein XOF testen: das Vektor, das wie bestanden aussieht
Vergleichen Sie die volle Ausgabelänge und verwenden Sie mehr als ein Vektor. Das SHAKE256-Ergebnis der leeren Zeichenkette ist das, das sich jeder merkt, und es ist genau deshalb ein schwacher Test, weil es so breit zitiert wird. Die 32-Byte-SHAKE256-Ausgabe für abc ist schlimmer: Ihre ersten 16 Bytes, 483366601360a8771c6863080cc4114d, fallen mit mehreren benachbarten Vektoren zusammen, daher meldet ein abgeschnittener Vergleich Erfolg, während die unterscheidende Hälfte, 8db44530f8f1e1ee4f94ea37e78b5739, ungeprüft bleibt. Die PDFlibPas-Regression vergleicht beide Vektoren über ihre gesamte Länge. Nehmen Sie auch einen blockübergreifenden Fall dazu, denn eine Squeeze-Schleife, die nie eine Rategrenze überquert, ist überhaupt nicht getestet
Die größere Lektion aus dem Falten von SHAKE in eine bestehende SHA-3-Implementierung ist: Der geteilte Code war nie das Risiko. Keccak-f[1600] ist für beide Familien dieselbe Permutation, und sie stimmt entweder mit den Vektoren überein oder nicht. Das Risiko steckt in den zwei Bytes und einer Ganzzahl, die sich unterscheiden, und in der Decke, die Sie ehrlich benennen müssen, statt die Aufrufer sie entdecken zu lassen. Wie die Digest-Schicht neben Signatur-, Verschlüsselungs- und PDF/A-Werkzeugen in einer nativen Object-Pascal-Codebasis sitzt, zeigt die Produktseite der PDFlibPas-Delphi-PDF-Bibliothek mit dem vollständigen Komponentensatz und der unterstützten Compiler-Matrix