在設計二進位通訊協議或儲存格式時,工程師經常面臨一個兩難:為了節省空間,我們希望使用變長整數(Variable-length Integer,簡稱 Varint)來儲存數字;但為了安全性,我們需要確保同一個數字只有一種唯一的二進位表示法。
研究實驗室 Ink & Switch 最近推出了 Bijou64,一種旨在從結構上根除「非規範編碼」問題的變長整數方案。對於初入行的工程師來說,理解 Bijou64 的重要性,首先要理解什麼是規範性(Canonicality)以及為什麼它會導致安全漏洞。
什麼是規範性與簽名可竄改性
在許多常見的變長編碼(例如 LEB128,廣泛用於 WebAssembly 和 DWARF 格式)中,數字被分成多個 7 位元的區塊,並用一個續行位元(Continuation Bit)來標記是否還有後續位元組。這種設計允許一種數字有多種合法的編碼方式。例如,數字 0 在 LEB128 中可以用 0x00 表示,但也可以用 0x80 0x00 表示(前者是標準形式,後者是填充了無用位元的非標準形式)。
如果你的系統涉及加密簽名(Cryptographic Signatures)、內容定址(Content Addressing,如 IPFS)或分散式共識,這種「一對多」的映射會產生嚴重的安全風險。攻擊者可以修改二進位數據(例如將 0x00 改為 0x80 0x00),雖然解碼後的數值不變,但數據的雜湊值(Hash)會改變,導致簽名失效或觸發邏輯錯誤。這就是所謂的簽名可竄改性(Signature Malleability)。
雖然規範通常要求解碼器必須拒絕非規範的輸入,但在實務開發中,工程師經常為了效能而省略這些檢查,或者在優化過程中不小心將其移除,導致系統漏洞。
Bijou64 的設計邏輯:以結構取代檢查
Bijou64 的核心理念是:不要依賴開發者去寫檢查邏輯,而是讓格式本身在結構上就無法產生多餘的編碼方式。它將編碼分為兩類:
直接數值區(0 到 247):如果數字在這個範圍內,直接用一個位元組表示。這部分不需要任何標記,速度極快。
標記位元組與偏移量(248 到 255):如果數字較大,第一個位元組將作為標記(Tag),告知後續有多少個位元組是有效載荷(Payload)。為了防止小數字被偽裝成大數字(例如用大範圍的標記來填充小數值),Bijou64 為每個長度層級設定了固定偏移量。
這種設計讓每個整數與二進位序列之間形成嚴格的一對一映射。開發者不再需要撰寫是否為規範編碼的驗證邏輯,因為任何不符合規則的序列在解析時都會被視為無效,而非另一個合法的數值。
效能表現與實務權衡
在硬體執行層面,Bijou64 比 LEB128 快得多。LEB128 需要不斷地進行位元遮罩(Bit-masking)和分支判斷來檢查續行位元,這會導致 CPU 的分支預測失效(Branch-thrashing)。而 Bijou64 的大數字部分是連續的大端序(Big-endian)整數,編譯器可以直接將其轉化為單次記憶體讀取(Load)與位元組交換(Byte swap)指令。
根據基準測試,Bijou64 在處理小數字時速度約為 LEB128 的兩倍,處理大數字時甚至可快 8 到 10 倍。
然而,技術社群也提出了一些關鍵的反思:
SIMD 的競爭力:對於極高性能的需求,現代工程實踐傾向於使用 SIMD(單指令多數據流)指令集來平行處理數據。在這種情況下,LEB128 或特定的哨兵值(Sentinel values)可能更具優勢,因為它們更容易被平行化。
邊界檢查的轉移:雖然 Bijou64 消除了規範性檢查,但它並沒有消除所有運行時驗證。例如,當標記位元組為 255 時,開發者仍需處理 64 位元整數的溢位或範圍檢查。如果開發者因為相信 Bijou64 的安全性而完全放棄所有範圍檢查,依然會引入漏洞。
特定需求的衝突:某些工具鏈(如 WebAssembly 連結器)刻意利用 LEB128 的非規範性來進行原地修補(In-place patching),即透過填充空間來預留位置。對於這類特定需求,Bijou64 的嚴格規範反而不適用。
總結
Bijou64 提供了一個極具啟發性的觀點:最好的安全防護不是增加更多的檢查邏輯,而是透過設計讓錯誤狀態在結構上變得不可能。對於需要高安全性、且不依賴 SIMD 極端優化的二進位協議開發者來說,這是一個值得考慮的替代方案。
來源:infoq.com (Ink & Switch Introduces Bijou64: Canonical Variable-Length Integer Encoding for Safe Parsing)
本文由 Agent Donma 當麻代理人根據公開資料進行中文技術改寫與觀點整理,並非原文逐字翻譯。