PDFlibPas 在 Object Pascal 裡用單一 Keccak-f[1600] 排列實作 SHA-3 與 SHAKE256。兩個家族只差在兩個地方:pad10*1 領域位元組,SHA-3 是 $06、SHAKE 是 $1F(FIPS 202 §6.2 與 §6.3),以及 rate 規則。SHAKE256 把 rate 釘在 136 位元組,不管輸出多長
第二個差異就是大家搞錯的那個,因為它看起來根本不像差異。SHA3-256 也跑在 rate 136 上,所以把 SHA-3 程式路徑複製出來的 SHAKE256 會通過你丟給它的每個 32 位元組測試,然後在第一次有人要 64 位元組時產出垃圾。接下來要講的是這兩個家族最後怎麼在 PDFlibDigest 中共用一個 context record、單次呼叫入口在哪裡停住,以及為什麼串流式 XOF 必須在 Ed448 或 ML-DSA 出現之前就存在
SHA-3 與 SHAKE256 為什麼共用一個 Keccak context
因為每個家族唯一的分家狀態只有一個位元組,PDFlibPas 把它存成 TSHA3Context 的 Suffix。領域分隔符一旦住進 context,而不是寫死在 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;
// 領域分隔符:SHA3 用 $06,SHAKE 用 $1F(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 跟隨摘要尺寸
Context.Suffix:= $06;
End;
// SHAKE256:無論呼叫端要多少位元組,rate 都固定在 136,
// 因為 SHAKE 是 extendable-output function
Procedure SHAKE256Init(Var Context: TSHA3Context; OutputBytes: Integer);
Begin
FillChar(Context, SizeOf(Context), 0);
Context.OutputLength:= OutputBytes;
Context.Rate:= 136;
Context.Suffix:= $1F;
End;
SHA3Init 的公式給出 SHA3-256、SHA3-384 與 SHA3-512 分別為 136、104、72 位元組的 rate,所以定長家族把 rate 與安全等級焊在一起。SHAKE256Init 刻意打破這條連結:OutputLength 變成對位元組的請求,而不是安全強度的宣告,海綿容量無論你抽多少輸出都維持 512 位元。這種解耦就是 extendable-output function 的定義,而用同一個 context 上的兩個初始化器寫出來,讓這個區別在原始碼裡可見,而不是埋在註解裡
單次呼叫入口的 136 位元組上限
PDFlibPas 的便利函式拒絕 1..136 位元組以外的任何東西,這個上限是誠實的而不是任意的。SHAKE256StreamRange 與 SHAKE256ByteArray 都經由 SHA3Final 路由,它補最後一個區塊、排列一次、直接從狀態讀輸出。狀態中只有前 Rate 個位元組是合法輸出,所以一次擠壓最多給 136 位元組;再過去每個區塊都需要一次新的排列。兩個真實消費端反正都落在窗口內:PDF 簽章設定檔摘要 32 位元組,Ed448 依 RFC 8032 §5.2 需要 114。SHAKE256StreamRange 接受與 SHA256StreamRange、SHA3_256StreamRange 相同的 FirstLength、SecondStart、SecondLength 三元組,所以驗證程式對每個演算法都用同一種方式走簽章 /ByteRange,永遠不必特別處理 XOF
pad10*1 的位置落在 rate-1 時會出什麼錯
什麼都不會,前提是你把兩個 pad 位元組都 XOR 進狀態而不是覆寫。FIPS 202 §B.2 把領域後綴放在目前的 absorb 位置,把 $80 放在區塊最後一個位元組。當訊息剛好把位置留在 Rate- 1,這兩次寫入瞄準同一個位元組,必須組合成 $9F。用存取代 XOR,其中一個會贏,padding 在 136 種訊息長度中恰好一種裡無聲出錯,而你手上所有回歸向量都過,因為沒有一個落在這裡。PDFlibPas 的 PLShakeXOFFinalize 兩個都 XOR,讓重疊案例從一般案例自然推出,不需要分支
// 補齊最後一個部分區塊並進入 squeeze 階段
Procedure PLShakeXOFFinalize(Var Ctx: TPLShakeXOF);
Begin
If Not Ctx.Absorbing Then
Exit;
Ctx.Absorbing:= False;
// suffix 在 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:直接吸收進海綿
PDFlibPas 用獨立的 record TPLShakeXOF 暴露無界輸出,它只持有 25 個 UInt64 字的 State、Rate、Suffix、Absorbing 旗標與 SqueezePos。沒有訊息緩衝區:PLShakeXOFAbsorb 把進來的位元組直接 XOR 進狀態,填滿一個區塊就排列一次,SqueezePos 在兩個階段都當偏移量用。PLShakeXOFInit 用一個布林挑變體,True 是 rate 168 的 SHAKE128,False 是 rate 136 的 SHAKE256,PLShakeXOFSqueeze 持續排列直到呼叫端拿到它要的位元組數。跨區塊輸出在 200 位元組處驗證過,兩種變體都越過第一個區塊邊界
Var
Ctx: TPLShakeXOF;
Digest: Array [0.. 113] Of Byte; // Ed448 要 114 位元組,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;
// 或者,當你只需要一個輸入緩衝區配一個輸出緩衝區時:
// PLShakeXOF(False, Input, Output, 114)
分多次呼叫吸收比看起來重要。Ed448 在雜湊前把訊息前綴上 dom4 字串與 57 位元組的值,拒絕取樣器則想直接餵種子與索引位元組,不先串接字串。讓串流式 record 在兩個消費端之前落地正是重點:純 Pascal Ed448 與 Brainpool 曲線工作一次要 114 位元組,FIPS 204 ML-DSA 移植版從兩種變體無限擠壓。兩者都不可能只靠單次 API 寫得出來
PDFlibPas 為什麼拒絕把 SHAKE256 暴露成摘要選擇器
因為沒有合法的方式可以選它。PDFlibDigitalSign 的 TPDFlibDigestAlgorithm 跑 daAuto、daSHA1、daSHA256、daSHA384、daSHA512、daSHA3_256、daSHA3_384、daSHA3_512,然後就停了。沒有 daSHAKE256,而這個缺漏是決定,不是疏忽。RFC 8702 在 CMS 註冊 SHAKE,但在 PDF 簽章內部,ISO/TS 32002 把 SHAKE256 放在的唯一位置是 Ed448 的內部摘要,由簽章演算法自己選,呼叫端無權決定。把它發表成獨立的 DigestAlgorithm 值,等於允許任何人把 SHAKE256 配 RSA 或 ECDSA,產出沒有任何驗證器會接受的簽章字典。能力出貨,選擇器不出貨,驗證端程式碼直接對 /ByteRange 版面呼叫 SHAKE256StreamRange。如果你正在為對方今天就要驗證的簽章選摘要,帶時間戳與長期驗證資料的 PAdES B-B 到 B-LTA 工作流程裡的 daSHA256 到 daSHA3_512 仍是答案;SHA-3 何時在 PDF 中成為常態,由 ISO/TS 32001 規範
測試 XOF:那個看起來會通過的向量
比較完整輸出長度,而且用超過一個向量。空字串的 SHAKE256 結果是大家都背下來的那個,而它之所以是弱測試,正因為被引用的次數太多。abc 的 32 位元組 SHAKE256 輸出更糟:它的前 16 個位元組 483366601360a8771c6863080cc4114d 與幾個鄰近向量一致,所以截斷比較回報成功,而具區別力的後半 8db44530f8f1e1ee4f94ea37e78b5739 沒被檢查。PDFlibPas 的回歸對兩個向量都比較完整長度。再加一個跨區塊案例,因為從未跨過 rate 邊界的擠壓迴圈根本沒被測過
把 SHAKE 併進既有 SHA-3 實作得到的更大教訓是,共用程式碼從來不是風險。Keccak-f[1600] 對兩個家族是同一種排列,它不是吻合向量就是不合。風險住在差異的那兩個位元組與一個整數,以及你必須誠實宣告、而不是讓呼叫端自己去發現的上限。想看摘要層怎麼與簽章、加密與 PDF/A 工具並排坐在原生 Object Pascal 程式庫裡,PDFlibPas Delphi PDF library 產品頁列出完整元件集與支援的編譯器矩陣