PDFlibPas имплементира SHA-3 и SHAKE256 върху една единствена Keccak-f[1600] пермутация на Object Pascal. Двете семейства се различават точно на две места: pad10*1 domain байта — $06 за SHA-3 и $1F за SHAKE според FIPS 202 §6.2 и §6.3 — и rate правилото. SHAKE256 фиксира rate-а си на 136 байта, независимо от дължината на изхода
Втората разлика е онази, която хората объркват, защото изобщо не изглежда като разлика. SHA3-256 също върви с rate 136, така че SHAKE256, построен чрез копиране на SHA-3 code path-а, минава всеки 32-байтов тест, който му подхвърлите, и после произвежда боклук първия път, когато някой поиска 64. Това, което следва, е как двете семейства са завършили, споделяйки един context запис в PDFlibDigest, къде спира one-shot entry point-ът, и защо streaming XOF-ът е трябвало да съществува, преди Ed448 или ML-DSA изобщо да могат
Защо SHA-3 и SHAKE256 споделят един Keccak context?
Защото единственото състояние за семейство е един байт, и PDFlibPas го пази в TSHA3Context като Suffix. След като domain separator-ът живее в context-а, вместо да е hard-coded в finalization рутината, SHA3AbsorbBlock, SHA3Update и SHA3Final обслужват и двете семейства непроменени, и в библиотеката има точно едно копие на 24-кръговата пермутация. Инициализаторите са мястото, където двата пътя се разделят, и се разделят напълно
Type
TSHA3Context= Record
State: Array [0.. 24] Of UInt64;
Buffer: Array [0.. 135] Of Byte;
BufferLength: Integer;
Rate: Integer;
OutputLength: Integer;
// Domain separator: $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; // rate следва размера на digest-а
Context.Suffix:= $06;
End;
// SHAKE256: rate е фиксиран на 136 байта, каквото и да поиска извикващият,
// защото SHAKE е функция с разширяем изход
Procedure SHAKE256Init(Var Context: TSHA3Context; OutputBytes: Integer);
Begin
FillChar(Context, SizeOf(Context), 0);
Context.OutputLength:= OutputBytes;
Context.Rate:= 136;
Context.Suffix:= $1F;
End;
Формулата в SHA3Init дава 136, 104 и 72 байта rate за SHA3-256, SHA3-384 и SHA3-512, така че rate и нивото на сигурност са заварени заедно за семейството с фиксирана дължина. SHAKE256Init умишлено скъсва тази връзка: OutputLength става заявка за байтове, а не декларация за сила на сигурността, а капацитетът на sponge-а си остава 512 бита, колкото и изход да изтеглите. Това развързване е дефиницията на extendable-output функция, а написването му като два инициализатора върху един context прави разликата видима в кода, вместо заровена в коментар
136-байтовият таван на one-shot entry point-а
Удобните функции в PDFlibPas отказват всичко извън 1..136 байта, и този лимит е честен, а не произволен. SHAKE256StreamRange и SHAKE256ByteArray минават и двете през SHA3Final, която допълва последния блок, пермутира веднъж и чете изхода направо от състоянието. Само първите Rate байта от това състояние са легален изход, така че един squeeze дава най-много 136 байта; отвъд това ви трябва прясна пермутация за всеки блок. И двата реални consumer-а се побират в прозореца така или иначе: PDF signature профилът диджестира 32 байта, а Ed448 иска 114 според RFC 8032 §5.2. SHAKE256StreamRange приема същата тройка FirstLength, SecondStart, SecondLength като SHA256StreamRange и SHA3_256StreamRange, така че validation кодът обхожда signature /ByteRange по същия начин за всеки алгоритъм и никога не прави специален случай за XOF-а
Какво се обърква в pad10*1, когато позицията кацне на rate-1?
Нищо, стига да XOR-нете и двата pad байта в състоянието, вместо да ги присвоявате. FIPS 202 §B.2 слага domain suffix-а на текущата absorb позиция и $80 на последния байт на блока. Когато съобщението случайно остави позицията на Rate- 1, тези два записа целят същия байт и трябва да се комбинират в $9F. Присвоите вместо XOR и единият печели, padding-ът е тихо грешен за точно една дължина на съобщението от 136, а всеки regression вектор, който притежавате, минава, защото никой от тях не каца там. PLShakeXOFFinalize в PDFlibPas XOR-ва и двата, което прави припокриващия се случай да излезе от общия без никакъв branch
// Допълва последния частичен блок и преминава във фазата squeeze
Procedure PLShakeXOFFinalize(Var Ctx: TPLShakeXOF);
Begin
If Not Ctx.Absorbing Then
Exit;
Ctx.Absorbing:= False;
// суфиксът на SqueezePos, $80 на Rate-1; когато SqueezePos = Rate-1, двете
// XOR-ите попадат върху един и същ байт и се комбинират правилно до $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: absorb директно в sponge-а
PDFlibPas излага неограничен изход през отделен запис, TPLShakeXOF, който държи само State като 25 UInt64 думи, Rate, Suffix, флаг Absorbing и SqueezePos. Няма буфер за съобщението: PLShakeXOFAbsorb XOR-ва входящите байтове директно в състоянието и пермутира винаги когато напълни блок, като SqueezePos служи като отместване и в двете фази. PLShakeXOFInit избира варианта с един boolean — True за SHAKE128 с rate 168 и False за SHAKE256 с rate 136 — а PLShakeXOFSqueeze продължава да пермутира, докато caller-ът получи броя байтове, които е поискал. Крос-блоковият изход е проверен при 200 байта, отвъд първата блокова граница и в двата варианта
Var
Ctx: TPLShakeXOF;
Digest: Array [0.. 113] Of Byte; // Ed448 wants 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;
// Или, когато ти трябва само един входен и един изходен buffer:
// PLShakeXOF(False, Input, Output, 114)
Absorb в няколко извиквания има значение повече, отколкото изглежда. Ed448 префиксва съобщението с dom4 string и 57-байтова стойност, преди да хешира, а rejection sampler иска да подава seed и индексни байтове, без първо да конкатенира низове. Точката беше streaming записът да се появи преди който и да е от двата consumer-а: чистата Pascal работа с Ed448 и Brainpool кривите иска 114 байта наведнъж, а FIPS 204 ML-DSA портът squeeze-ва безкрайно и от двата варианта. Нито един от тях не би могъл да бъде написан срещу one-shot API-то
Защо PDFlibPas отказва да изложи SHAKE256 като digest селектор?
Защото няма легален начин да се избере. TPDFlibDigestAlgorithm в PDFlibDigitalSign минава през daAuto, daSHA1, daSHA256, daSHA384, daSHA512, daSHA3_256, daSHA3_384, daSHA3_512 и спира. Няма daSHAKE256, а пропускът е решение, не недоглеждане. RFC 8702 регистрира SHAKE в CMS, но вътре в PDF подпис единственото място, където ISO/TS 32002 поставя SHAKE256, е като вътрешен digest на Ed448, където алгоритъмът за подпис го избира, а caller-ът — не. Публикуването му като самостоятелна стойност DigestAlgorithm би позволило на всеки да сдвои SHAKE256 с RSA или ECDSA и да произведе signature речник, който никой валидатор няма да приеме. Възможността се доставя, селекторът — не, а verification кодът вика SHAKE256StreamRange директно върху /ByteRange оформлението. Ако избирате digest за подписи, които насрещната страна трябва да валидира днес, daSHA256 до daSHA3_512 в PAdES B-B до B-LTA работен процес с timestamping и дългосрочни validation данни си остава отговорът; ISO/TS 32001 е това, което управлява кога SHA-3 изобщо става рутина в PDF
Тестване на XOF: векторът, който изглежда, че минава
Сравнявайте пълната дължина на изхода и ползвайте повече от един вектор. Резултатът на SHAKE256 за празния string е този, който всички помнят, и е слаб тест точно защото е толкова цитиран. 32-байтовият SHAKE256 изход за abc е по-лош: първите му 16 байта, 483366601360a8771c6863080cc4114d, съвпадат с няколко съседни вектора, така че осечената проверка докладва успех, докато разграничаващата половина, 8db44530f8f1e1ee4f94ea37e78b5739, остава непроверена. PDFlibPas regression-ът сравнява двата вектора по цялата им дължина. Добавете и крос-блоков случай, защото squeeze цикъл, който никога не пресича rate граница, изобщо не е тестван
По-широкият урок от вграждането на SHAKE в съществуваща SHA-3 имплементация е, че споделеният код никога не е бил рискът. Keccak-f[1600] е същата пермутация и за двете семейства, и тя или съвпада с векторите, или не. Рискът живее в двата байта и едното цяло число, които се различават, и в тавана, който трябва да заявите честно, вместо да оставите caller-ите да го открият. За да видите как digest слоят пасва до signing, encryption и PDF/A инструментите в нативна Object Pascal кодова база, продуктовата страница на PDFlibPas Delphi PDF библиотеката изброява пълния набор от компоненти и поддържаната матрица на компилаторите