前言
量子計算威脅現況
2026 年,量子計算的進展已經從理論走向實際威脅。雖然具備完全加密破解能力的量子電腦尚未出現,但「先儲存後破解 (Store Now, Decrypt Later)」的攻擊策略已成為現實威脅:攻擊者現在就竊取加密資料,等量子電腦成熟後再破解。
關鍵時間點:
- 2024 年 8 月 13 日:NIST 正式發布 FIPS 203 (ML-KEM) 與 FIPS 204 (ML-DSA) 標準
- 2026 年:企業級 HSM 開始全面支援 PQC 演算法
- 2030-2035 年:預估為 RSA-2048/ECC P-256 的安全終結時間點
對於需要長期保密的資料(醫療記錄、金融交易、政府機密),遷移至後量子密碼學已刻不容緩。
NIST PQC 標準化完成
美國國家標準技術研究所(NIST)經過 8 年的評選過程,最終確定了以下後量子演算法標準:
| 標準 | 演算法 | 類型 | 安全基礎 | 用途 |
|---|---|---|---|---|
| FIPS 203 | ML-KEM (Kyber) | 金鑰封裝機制 (KEM) | Module-LWE | 金鑰交換、加密 |
| FIPS 204 | ML-DSA (Dilithium) | 數位簽章 | Module-LWE | 身份驗證、簽章 |
| FIPS 205 | SLH-DSA (SPHINCS+) | 數位簽章 | 雜湊函數 | 無狀態簽章 |
這些標準的發布意味著後量子密碼學已從研究階段進入生產部署階段。
為什麼用 Luna HSM
- 實體金鑰保護:私鑰永不離開 FIPS 140-3 Level 3 認證硬體
- 防篡改機制:物理入侵自動銷毀金鑰
- 合規要求:金融、醫療等行業法規強制要求
- 效能加速:硬體加速密碼學運算
Thales Luna HSM 特色:
- 支援最新 FIPS 203/204 標準(Firmware 7.8.0+)
- 原生 BIP32/SLIP10 HD 金鑰衍生
- Luna 專有延伸 API(CA_* 函數)
- 網路 HSM 與叢集支援
為什麼要三個語言各做一份
本文最初只談 Rust。後來我們把同一套 PKCS#11 綁定與服務層各自用 Go 與 Java 重寫一遍, 成為三份獨立實作。這不是為了比較誰快——是為了讓錯誤無所遁形。
密碼學程式有一個討厭的性質:錯得很徹底的時候,它照樣會跑。簽章與驗證用同一套邏輯, 契約寫錯了會一起錯,round-trip 照樣通過;跨語言互測也一樣,只要兩邊錯得一致就測不出來。 單一實作面對這類缺陷是全盲的。
三份實作互為對照之後,情況變了:
- 第三方會照出既有兩方的盲點。 把 Java 加進來做一致性稽核時,發現的 18 項服務層分歧 裡,有 2 項不是「新來的沒跟上」,而是修在既有的 Go 與 Rust 側——其中一項是三方都不 對、只有其中一方例外。若只有兩份實作,那兩個缺口不會被看見。
- 具體的缺口會被逼出來。 稽核當下,Java 缺 RSA-PSS、EdDSA、原生 ECIES;Go 缺
counterBits 範圍守衛與 GMAC tag 下限守衛(會讓攻擊者有機會把 tag 降到不安全長度);
Rust 的 OAEP
source欄位填了一個未定義值。這些都不是靠讀自己的程式碼讀得出來的。 - 有一個缺口被誤診了數個月。 Go 的對稱金鑰模板漏了能力旗標,導致 CMAC/GMAC/wrap 全部不可用——這個問題長期被當成 HSM partition policy 的設定問題,直到 Java 側「一開始 就寫對」的模板擺在旁邊,才看出是自己的 bug。
三份實作現在在所有已識別項目上完全對齊;本文的每一段實作敘述都同時適用於三者,差異則 在「跨語言一致性」一節明列。
技術架構
系統分層設計
我們的 PQC 服務採用清晰的分層架構,從 API Gateway 到 HSM 硬體的完整封裝:

各層職責:
- API Gateway Layer:提供多協議存取介面(REST、gRPC、NATS)
- Domain Layer:業務邏輯、金鑰管理、稽核日誌
- HSM Layer:PKCS#11 操作封裝、連接池管理、PQC 專有功能
- Binding Layer:底層 PKCS#11 C API 綁定——Rust 走 FFI、Go 走 cgo、Java 走手寫 JNI
- Hardware:實體 HSM 硬體
這四層在三份實作裡一一對應,差異只在最底層的綁定方式與各自語言的框架選擇。
為什麼三個語言都自己實作綁定,而不用現成套件?
三份實作各自面對的現成選項不同,但結論一致:都選擇自行實作。原因如下。
1. 現成綁定與 Luna HSM 的相容性問題
以 Rust 為例,Parallax Second 維護的 rust-cryptoki 是該生態中最成熟的 PKCS#11 綁定,但在與 Luna Network HSM 配合使用時存在嚴重的穩定性問題:
已知問題(來自 GitHub Issues):
- Issue #72:
pkcs11.open_session_no_callback在 Luna Network HSM 上崩潰(SIGSEGV) - Issue #50:
ctx.open_session在 Luna HSM 上發生記憶體錯誤
這些問題源於 Luna HSM 對 PKCS#11 標準的特定實作細節,通用綁定無法完全相容。
Java 側則走過一段彎路:早期版本建在 SafeNet 官方的 jcprov 之上,後來整個換成自家手寫
的 JNI。換掉的理由與 Rust 相同——需要 vendor 專有延伸,而官方 provider 不提供。
2. Luna 專有延伸 API 支援
Luna HSM 提供了大量超越 PKCS#11 標準的專有延伸 API(CA_* 函數),這些函數涵蓋下列用途:
| 延伸類別 | API 函數 | 用途 |
|---|---|---|
| PQC 支援 | CA_ML_DSA_*, CA_ML_KEM_* | 後量子密碼學操作 |
| HD 金鑰衍生 | CA_BIP32_*, CA_SLIP10_* | 原生 BIP32/SLIP10 支援 |
| 系統監控 | CA_GetHSMStats, CA_GetHSMCapabilitySet | HSM 健康監控 |
| 儲存管理 | CA_GetHSMStorageInformation, CA_GetTokenStorageInformation | 容量監控 |
| 韌體管理 | CA_GetFirmwareVersion | 版本資訊 |
通用 PKCS#11 綁定不支援這些延伸,如果使用它們,我們將無法:
- 使用 ML-KEM 的封裝/解封裝(
CA_EncapsulateKey/CA_DecapsulateKey) - 實作 BIP32 HD 錢包功能
- 監控 HSM 健康狀態與容量
- 充分利用 Luna HSM 的進階功能
3. 精細錯誤處理
Luna HSM 定義了大量專有錯誤碼(例如 CKR_BIP32_CHILD_INDEX_INVALID, CKR_LUNA_OPERATION_FAILED),需要結構化錯誤處理:
錯誤分類:
Operation- 操作失敗(包含錯誤碼和描述)KeyNotFound- 金鑰不存在HsmDisconnected- HSM 斷線(可重試)Pin- PIN 相關錯誤
透過錯誤類型判斷是否可重試,實作智能重試、降級策略、精準警示等企業級功能。
4. 性能優化與 Session 管理
三方各自實作了 Session 池,生命週期管理相同:
- 建立:開啟 Session → 登入 → 快取符號表 → 健康檢查
- 回收:檢測斷線 → 清理狀態 → 重新登入
關鍵優化:
- 符號快取:避免重複 dlsym 呼叫
- 健康檢查:自動檢測 HSM 斷線並重建連接(帶 TTL 快取,避免每次借出都打一次 HSM)
- 預熱 Pool:啟動時預先建立 Session
- 平行安全:支援多執行緒平行存取
5. 完全控制與長期維護
自行實作綁定帶來的優勢:
- 完全掌控:不依賴上游套件的更新節奏
- 快速修復:發現問題可立即修補,無需等待上游
- 客製化:針對 Luna HSM 特性深度優化
- 穩定性:避免上游 breaking changes 影響生產環境
代價是初期開發成本(每份綁定約 6,000 行以上),而且乘以三。這個代價是否值得,取決於你 是否真的需要 vendor 延伸——如果只用標準 PKCS#11 機制,現成綁定仍然是對的選擇。
三份實作的技術選型對照
| 層 | Rust | Go | Java |
|---|---|---|---|
| Web 框架 | Axum | Echo | Quarkus(JAX-RS) |
| 非同步模型 | Tokio | goroutine | 原生 thread(1:1) |
| Session 池 | 自製,每 worker 專屬 queue | 自製,共用 queue | 自製,每 worker 專屬 queue |
| 綁定方式 | FFI(libloading) | cgo | 手寫 JNI |
| 訊息佇列 | NATS | NATS | NATS |
| 序列化 | Serde(snake_case) | encoding/json | Jackson(全域 snake_case) |
Session 池的佇列模型差異值得特別注意,因為它會讓「填同一個數字」變成不公平的比較: Rust 與 Java 的深度參數是每個 worker 的積壓上限(總量 = workers × depth),Go 的 參數則直接是總積壓上限。後面的效能測試為此做了換算。
PQC 演算法實作
ML-DSA (Dilithium) 簽章演算法
ML-DSA (Module-Lattice-Based Digital Signature Algorithm) 是 NIST FIPS 204 標準化的後量子數位簽章演算法,前身為 Dilithium。
安全參數集
| 模式 | NIST 安全級別 | 公鑰大小 | 私鑰大小 | 簽章大小 | 等效 RSA 強度 |
|---|---|---|---|---|---|
| ML-DSA-44 | Level 2 | 1,312 B | 2,560 B | 2,420 B | RSA-2048 |
| ML-DSA-65 | Level 3 | 1,952 B | 4,032 B | 3,309 B | RSA-3072 |
| ML-DSA-87 | Level 5 | 2,592 B | 4,896 B | 4,627 B | RSA-4096 |
建議:一般應用使用 ML-DSA-65,高安全需求使用 ML-DSA-87。
簽章長度是可以拿來當斷言的。上表的簽章大小是 FIPS 204 的定值,三份實作都用它來 驗證
CKA_PARAMETER_SET真的生效——這個屬性若沒被套用,HSM 會靜默回退到預設參數集, round-trip 照樣通過,長度是唯一的破綻。
使用流程
基本操作步驟:
- 建立 Session Pool - 預先建立 Session 連線
- 產生金鑰對 - 指定安全級別(44/65/87)和儲存方式
- 執行簽章 - 使用私鑰對訊息簽章(ML-DSA-65 為 3,309 bytes)
- 驗證簽章 - 使用公鑰驗證,防止偽造攻擊
沒有專用 API:ML-DSA 在三份實作裡都走泛用的 keygen 與 sign/verify,機制常數與
CKA_PARAMETER_SET 只是參數。真正需要小心的不是函式簽名,而是模板:
CKA_PARAMETER_SET公私鑰兩邊都要放,理由見上。- 組錯模板只會拿到
CKR_ATTRIBUTE_VALUE_INVALID——一個完全沒有指向性的錯誤碼。這些 規則是實測出來的,不是讀規格讀得到的。
關鍵設計考量:
-
金鑰生命週期:
- Session 金鑰(
CKA_TOKEN = false):連線結束自動銷毀 - Token 金鑰(
CKA_TOKEN = true):持久化,需手動刪除
- Session 金鑰(
-
簽章不可重現:Luna 預設走 FIPS 204 的 hedged(隨機化)模式,相同訊息多次簽章會 產生不同的簽章值,但都能通過驗證。不要把「簽章位元組相同」寫進測試斷言。
-
錯誤處理:根據錯誤類型決定重試策略(金鑰不存在、HSM 斷線、未預期錯誤)
ML-KEM (Kyber) 金鑰封裝機制
ML-KEM (Module-Lattice-Based Key Encapsulation Mechanism) 是 NIST FIPS 203 標準化的後量子金鑰封裝演算法,前身為 Kyber。
安全參數集
| 模式 | NIST 安全級別 | 公鑰大小 | 私鑰大小 | 密文大小 | 共享密鑰 | 等效 AES 強度 |
|---|---|---|---|---|---|---|
| ML-KEM-512 | Level 1 | 800 B | 1,632 B | 768 B | 32 B | AES-128 |
| ML-KEM-768 | Level 3 | 1,184 B | 2,400 B | 1,088 B | 32 B | AES-192 |
| ML-KEM-1024 | Level 5 | 1,568 B | 3,168 B | 1,568 B | 32 B | AES-256 |
建議:一般應用使用 ML-KEM-768,極高安全需求使用 ML-KEM-1024。
KEM 工作原理
使用流程
Alice 端(接收方):
- 產生金鑰對 - 產生 ML-KEM-768 公私鑰對
- 導出公鑰 - 公鑰大小 1,184 bytes,傳送給 Bob
- 解封裝 - 使用私鑰從密文恢復共享密鑰(32 bytes)
ML-KEM 的模板陷阱:公鑰要帶
CKA_ENCAPSULATE、私鑰要帶CKA_DECAPSULATE, 不是 sign/verify。另外 Luna 的封裝/解封裝走的是 vendor 延伸 (CA_EncapsulateKey/CA_DecapsulateKey),不是標準的 encrypt/decrypt——這正是 前面說「通用 PKCS#11 綁定做不到」的具體例子。implicit rejection:用錯的私鑰解封裝,ML-KEM 規格要求回一把「確定性但不同」的 共享密鑰,不保證回錯誤。呼叫端不能假設錯誤的私鑰會讓解封裝失敗。
Bob 端(發送方):
- 封裝 - 使用 Alice 公鑰產生密文(1,088 bytes)和共享密鑰(32 bytes)
- 加密資料 - 使用共享密鑰進行 AES-GCM 加密
進階用法:若公鑰儲存在 HSM 中,可直接使用 handle,避免傳輸公鑰 bytes
ML-KEM 安全性考量:
- Nonce 唯一性:使用共享密鑰時,必須確保 nonce 不重複(使用計數器或隨機數)
- 密鑰使用次數:建議定期輪換金鑰對(如每 1,000,000 次封裝操作)
- 私鑰保護:私鑰永不離開 HSM,只能用於解封裝操作
HSS/LMS 一次性簽章
HSS (Hierarchical Signature System) 與 LMS (Leighton-Micali Signature) 是基於雜湊函數的後量子簽章演算法,已納入 NIST SP 800-208 標準。
特性與適用場景
| 特性 | 說明 |
|---|---|
| 安全基礎 | 雜湊函數(SHA-256/SHA-512)- 抗量子攻擊 |
| 有狀態 | 私鑰每次簽章後狀態改變(一次性簽章) |
| 簽章次數 | 由樹高 H 決定:2^H 次簽章 |
| 後量子安全 | 完全基於雜湊函數,無需額外假設 |
| 驗證速度 | 極快(僅需雜湊運算) |
| 簽章大小 | 較大(數 KB),與樹高相關 |
適用場景:
- 韌體簽章:BIOS、IoT 韌體(簽章次數有限)
- 安全啟動:Secure Boot 驗證
- 軟體更新:APK、OTA 更新包簽章
- 程式碼簽章:發布前簽署可執行檔
- 不適用:高頻簽章場景(如 TLS handshake)
樹高配置與簽章次數
| LMS 類型 | 樹高 H | 最大簽章次數 | 公鑰大小 | 簽章大小 |
|---|---|---|---|---|
LMS_SHA256_M32_H5 | 5 | 32 | ~60 B | ~1,300 B |
LMS_SHA256_M32_H10 | 10 | 1,024 | ~60 B | ~2,600 B |
LMS_SHA256_M32_H15 | 15 | 32,768 | ~60 B | ~4,000 B |
LMS_SHA256_M32_H20 | 20 | 1,048,576 | ~60 B | ~5,500 B |
LMS_SHA256_M32_H25 | 25 | 33,554,432 | ~60 B | ~7,000 B |
HSS 多層樹(提升簽章次數):
| HSS 配置 | 總簽章次數 | 說明 |
|---|---|---|
| 1-level (H=10) | 1,024 | 單層樹 |
| 2-level (H=10, H=10) | 1,048,576 | 兩層樹:1024 × 1024 |
| 2-level (H=15, H=15) | 1,073,741,824 | 兩層樹:32768 × 32768 |
實作要點
HSS/LMS 金鑰對產生與簽章流程:
- 產生 HSS 金鑰對:2 層 H10/H10(1,048,576 次簽章),標籤
firmware_signing_key, 持久化儲存 - 查詢剩餘簽章次數:讀
CKA_HSS_KEYS_REMAINING,初始值 1,048,576 - 簽章韌體:輸入韌體原始內容,輸出簽章 bytes
- 驗證簽章:確認簽章有效性
- 追蹤狀態變化:簽章後剩餘次數變成 1,048,575,HSM 自動更新內部狀態
不要傳預先算好的雜湊值。
CKM_HSS收的是原始訊息,由 HSM 依 LMS 規則自行 雜湊——Thales 官方程式指南明言「the API does not support passing pre-computed hashes」。把 SHA-256 摘要餵進去,HSM 會把那 32 bytes 當成訊息本身簽下去;簽出來的 東西自己驗得過、跨語言也驗得過,但它不是 RFC 8554 合規的簽章,外部驗證器會拒絕。單段訊息上限 48,108 bytes,更大的檔案要用 multi-part(
C_SignUpdate/C_SignFinal, 需 firmware 7.9.1+)。
模板規則(實測得來,違反只會拿到 CKR_ATTRIBUTE_VALUE_INVALID):
CKA_HSS_LEVELS、CKA_HSS_LMS_TYPES、CKA_HSS_LMOTS_TYPES 只能放私鑰模板,放進
公鑰模板會被 Luna firmware 7.9.x 拒絕;公鑰模板只需 class、key type、token、verify、label。
keygen 成本隨樹高暴增,這是選配置時最容易低估的一項:
| 樹高 | keygen 耗時(實測量級) |
|---|---|
| H5 | 約 60 ms |
| H10 | 約 2.6 s |
| H15 | 約 92 s |
| H20 | 可達數小時 |
H20 的 keygen 是單一阻塞呼叫,期間會鎖住整個 partition。配置要照實際簽章量挑,不要 無腦選大樹。
狀態管理與備份:
關鍵警告:HSS/LMS 私鑰是有狀態的,每次簽章後狀態會改變。如果:
- 備份舊狀態後繼續簽章
- 稍後恢復舊備份
- 重複使用相同的一次性私鑰
→ 會導致安全性完全崩潰(簽章可被偽造)
正確備份策略:Luna HSM依照FIPS規格要求,無法備份
Luna HSM 自動狀態保護:
- HSM 自動追蹤 HSS/LMS 私鑰狀態
- 防止狀態回溯攻擊
- 當簽章次數耗盡時自動拒絕簽章
RESTful API 設計
API 端點規劃
我們的 PQC 服務提供完整的 RESTful API,符合 OpenAPI 3.0 規範:
基礎 URL: https://api.example.com/api/v1
三份實作對外的路由表是逐條相同的 50 條(雙向差集為空)。這是靜態比對加真機實測 得到的結果,不是設計意圖的宣稱。
PQC 金鑰管理
| 方法 | 端點 | 功能 | 請求體 | 回應 |
|---|---|---|---|---|
| POST | /pqc/generate | 產生 PQC 金鑰對 | PqcGenerateKeyInput | PqcGenerateKeyOutput |
| GET | /pqc/public-key | 取得公鑰 | - | PqcPublicKeyOutput |
| DELETE | /pqc/keys/{key_id} | 刪除金鑰 | - | SuccessResponse |
PQC 簽章操作
| 方法 | 端點 | 功能 | 請求體 | 回應 |
|---|---|---|---|---|
| POST | /pqc/sign | ML-DSA/HSS 簽章 | PqcSignInput | PqcSignOutput |
| POST | /pqc/verify | 驗證簽章 | PqcVerifyInput | PqcVerifyOutput |
PQC 金鑰封裝
| 方法 | 端點 | 功能 | 請求體 | 回應 |
|---|---|---|---|---|
| POST | /pqc/encapsulate | ML-KEM 封裝 | MlKemEncapsulateInput | MlKemEncapsulateOutput |
| POST | /pqc/decapsulate | ML-KEM 解封裝 | MlKemDecapsulateInput | MlKemDecapsulateOutput |
API 調用範例
基本流程
-
產生金鑰對 (
POST /pqc/generate)- 輸入:label、algorithm(ML_DSA_65/ML_KEM_768 等)、token_object
- 輸出:key_id、public_key(Base64)、HSM handles
-
簽章 (
POST /pqc/sign)- 輸入:label、algorithm、data(Base64)
- 輸出:signature(Base64,3,309 bytes)、signed_at
-
驗證簽章 (
POST /pqc/verify)- 輸入:label、algorithm、data、signature
- 輸出:valid(true/false)、verified_at
-
ML-KEM 封裝 (
POST /pqc/encapsulate)- 輸入:label、algorithm
- 輸出:ciphertext(1,088 bytes)、shared_secret(32 bytes)
- 共享密鑰僅出現一次,需立即使用或安全儲存
-
ML-KEM 解封裝 (
POST /pqc/decapsulate)- 輸入:label、algorithm、ciphertext
- 輸出:shared_secret(32 bytes)
錯誤處理
標準錯誤回應格式:
{
"error": {
"code": "KEY_NOT_FOUND",
"message": "找不到金鑰: my_ml_dsa_key",
"details": {
"label": "my_ml_dsa_key",
"suggestion": "請檢查金鑰標籤是否正確,或使用 key_id 查找"
},
"request_id": "req_1234567890"
}
}
錯誤碼對照表:
| 錯誤碼 | HTTP 狀態碼 | 說明 | 處理建議 |
|---|---|---|---|
KEY_NOT_FOUND | 404 | 金鑰不存在 | 檢查 label/key_id |
INVALID_ALGORITHM | 400 | 不支援的演算法 | 查看支援的演算法列表 |
HSM_DISCONNECTED | 503 | HSM 斷線 | 稍後重試 |
SIGNATURE_INVALID | 400 | 簽章驗證失敗 | 檢查資料與簽章 |
PIN_LOCKED | 403 | HSM PIN 鎖定 | 聯絡管理員 |
RATE_LIMIT_EXCEEDED | 429 | 請求速率超限 | 降低請求頻率 |
INTERNAL_ERROR | 500 | 內部錯誤 | 檢查日誌,聯絡支援 |
API 認證與授權
支援的認證方式:
- API Token(推薦):
curl -H "Authorization: Bearer YOUR_API_TOKEN" ...
- mTLS (Mutual TLS):
curl --cert client.crt --key client.key --cacert ca.crt ...
權限模型:
| 角色 | 權限 |
|---|---|
admin | 所有操作(包含金鑰管理) |
operator | 簽章/驗證/封裝/解封裝 |
viewer | 僅查詢公鑰與狀態 |
企業級特性
SOC2 稽核日誌
合規要求稽核每一次密碼學操作,我們實作了符合 SOC2 標準的稽核系統:
稽核日誌設計
獨立稽核資料庫 (audit.db):
CREATE TABLE audit_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL, -- ISO 8601 格式
session_id TEXT NOT NULL, -- 關聯請求
actor_id TEXT, -- 使用者/服務帳號
actor_ip TEXT NOT NULL, -- 客戶端 IP
action TEXT NOT NULL, -- 操作類型
resource_type TEXT NOT NULL, -- 資源類型
resource_id TEXT, -- 資源 ID
algorithm TEXT, -- 使用的演算法
status TEXT NOT NULL, -- Success/Failure/Warning
error_code TEXT, -- 錯誤碼(如有)
metadata TEXT, -- JSON 格式的額外資訊
hmac_chain TEXT NOT NULL, -- 防篡改 HMAC 鏈
created_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX idx_audit_timestamp ON audit_logs(timestamp);
CREATE INDEX idx_audit_actor ON audit_logs(actor_id);
CREATE INDEX idx_audit_action ON audit_logs(action);
防篡改機制
稽核日誌使用 HMAC 鏈式防篡改設計:
Log Entry 1: data_1 → HMAC(key, data_1) = hash_1
Log Entry 2: data_2 + hash_1 → HMAC(key, data_2 + hash_1) = hash_2
Log Entry 3: data_3 + hash_2 → HMAC(key, data_3 + hash_2) = hash_3
...
實作邏輯:
AuditLogger 結構:
db:SQLite 連線池hmac_key:HMAC 金鑰(儲存在 HSM 中)last_hmac:最新記錄的 HMAC 值(用 RwLock 保護平行存取)
記錄事件流程 (log_event):
- 序列化事件為 JSON
- 讀取上一筆記錄的 HMAC 值
- 計算目前記錄的 HMAC(使用 HSM 中的金鑰):
HMAC(event_data + prev_hmac) - 原子寫入資料庫(包含所有欄位與 HMAC 鏈)
- 更新記憶體中的
last_hmac
驗證完整性流程 (verify_integrity):
- 從資料庫讀取所有日誌(按 ID 升冪排序)
- 逐筆驗證:
- 重新計算預期 HMAC
- 比對資料庫中儲存的 HMAC
- 若不符,回傳
false(日誌被篡改)
- 所有記錄驗證通過,回傳
true
稽核事件範例
{
"timestamp": "2026-02-03T14:30:00.123Z",
"session_id": "sess_abc123",
"actor_id": "user_john_doe",
"actor_ip": "192.168.1.100",
"action": "pqc_sign",
"resource_type": "private_key",
"resource_id": "my_ml_dsa_key",
"algorithm": "ML_DSA_65",
"status": "Success",
"metadata": {
"data_length": 1024,
"signature_length": 3293,
"request_id": "req_xyz789"
},
"hmac_chain": "a1b2c3d4e5f6..."
}
Session Pool 與效能優化
連接池設計
三方各自實作,但生命週期契約相同:
建立 Session:
- 開啟新 PKCS#11 Session
- 使用 PIN 登入 HSM(RW Session 需要)
- 交給 worker 持有
回收 Session:
- 健康檢查:產生隨機數測試 Session 是否有效(帶 TTL 快取,避免每次借出都打 HSM)
- Session 正常 → 可重用
- HSM 斷線錯誤 → 標記需重建
配置建議:
| 場景 | Pool Size | Session 類型 |
|---|---|---|
| 低流量 API | 10-20 | RW (讀寫) |
| 中等流量 | 30-50 | RW |
| 高流量 | 50-100 | RW + RO (只讀) 混合 |
| 批次處理 | 100-200 | 專用 Pool |
效能優化技巧
1. 符號快取(Symbol Caching)
問題:每次 FFI 調用都執行 dlsym 查找函數符號,耗時 ~10-50 µs
解決方案:一次性載入所有 PKCS#11 函數指標並快取
- 建立
FfiSymbols結構儲存函數指標(C_Initialize,C_Sign,C_Verify等) - 初始化時使用
library.get()載入所有符號 - 後續調用直接使用快取的函數指標
效能提升:每次操作節省 ~10-50 µs
2. 批次操作
批次簽章策略 (batch_sign):
- 從連接池取得單一 Session
- 循環處理多個訊息簽章(共享同一個 Session)
- 減少 Session 取得/釋放的開銷
3. 預熱(Warm-up)
預熱連接池流程 (warmup_pool):
- 應用啟動時預先建立所有 Session(達到
max_size) - 測試每個 Session(產生隨機數驗證連線)
- 釋放 Session 回連接池
- 避免首次請求的冷啟動延遲
錯誤處理與韌性
Circuit Breaker 模式
Circuit Breaker 機制(防止 HSM 故障時雪崩效應):
三種狀態:
- Closed(關閉):正常運作,所有請求通過
- Open(開啟):達到失敗閾值,拒絕所有請求(快速失敗)
- HalfOpen(半開):超時後嘗試恢復,允許部分請求測試
運作邏輯:
-
Closed 狀態:
- 正常處理請求
- 失敗時記錄錯誤計數
- 達到閾值(如 5 次)→ 切換到 Open
-
Open 狀態:
- 直接拒絕請求(不呼叫 HSM)
- 超時後(如 30 秒)→ 切換到 HalfOpen
-
HalfOpen 狀態:
- 允許請求通過測試
- 成功 → 切換回 Closed
- 失敗 → 切換回 Open
使用範例:
- 初始化:
CircuitBreaker::new(failure_threshold: 5, timeout: 30s) - 包裹 HSM 操作:
circuit_breaker.call(|| session.ml_dsa_sign(...))
重試策略
指數退避(第 n 次重試等待 100 × 2ⁿ 毫秒)本身是常識,真正需要對齊的是哪些錯誤碼
該重試。三份實作在這件事上逐碼相同,這是整套跨語言契約裡最硬的一段:
| 分類 | 處置 | 碼數 |
|---|---|---|
| 斷線碼 | 重載 Cryptoki 函式庫 | 三方各 15 碼,逐碼相同 |
| Session 失效碼 | 重開 session/重新登入 | Rust 與 Java 各 5 碼;Go 多 1 碼(刻意) |
| 可重試但不恢復 | 重試,但不觸發任何本地恢復 | CKR_CONTAINER_HANDLE_INVALID |
斷線碼包含 CKR_DEVICE_ERROR、CKR_TOKEN_NOT_PRESENT、CKR_UNABLE_TO_CONNECT、
CKR_NETWORK_ERROR 等 15 個;三方都刻意排除 CKR_USER_NOT_AUTHORIZED 與
CKR_CONTEXT_INVALID——那些是授權或狀態問題,重載函式庫不會讓它們變好,只會把一次
明確失敗變成一串昂貴的無效重試。
效能基準測試
這一節的每個數字都來自 lang_bench_luna(我們的跨語言基準測試專案)對同一顆真實
Thales Luna HSM、同一台機器、同一次執行時間窗內跑出來的結果,三份實作依序執行、不同時
對 HSM 施壓。原始輸出是 JSON,可重跑驗證。
舊版本的這一節掛著一組無法追溯來源的估算值(例如 ML-DSA-65 簽章 2.5 ms),與真機 量測差了一個量級——真機連
generate_random(32)這種最小操作都要 4–7 ms,因為那是一趟 網路 HSM 的來回。那組數字已全部移除。
測試環境
| 項目 | 內容 |
|---|---|
| CPU | AMD RYZEN AI MAX+ PRO 395 |
| OS | Linux x86_64 |
| Rust / Go / Java | rustc 1.97.1 / go1.26.0 / OpenJDK 17.0.20 |
| HSM | Thales Luna,slot 0,libCryptoki2_64.so |
| 執行時間 | 2026-09-14 11:48(單次 ./run.sh) |
方法論:每項操作先暖機再量測(各 50–100 次),記錄每一次呼叫的耗時後算出 mean/p50/p95/p99,計時一律用各語言的單調時鐘。金鑰在計時範圍外建立、跑完刪除。
傳統演算法(單一 Session,單位 ms)
| 操作 | 語言 | mean | p50 | p95 | p99 |
|---|---|---|---|---|---|
generate_random(32) | Rust | 6.07 | 5.83 | 8.26 | 9.96 |
| Go | 7.36 | 7.52 | 9.88 | 14.35 | |
| Java | 4.64 | 4.50 | 6.06 | 8.94 | |
| session open + login | Rust | 13.10 | 12.98 | 15.72 | 17.13 |
| Go | 17.42 | 17.01 | 21.94 | 23.00 | |
| Java | 11.70 | 10.91 | 15.29 | 17.33 | |
| AES-GCM 1KiB 加密 | Rust | 6.41 | 6.01 | 9.05 | 11.05 |
| Go | 6.69 | 6.39 | 10.07 | 11.55 | |
| Java | 4.30 | 4.25 | 5.52 | 6.35 | |
| ECDSA P-256 簽章 | Rust | 6.68 | 6.38 | 10.20 | 11.44 |
| Go | 6.47 | 5.73 | 9.68 | 11.36 | |
| Java | 4.78 | 4.57 | 6.17 | 6.44 | |
| RSA-2048 簽章 | Rust | 7.43 | 6.54 | 14.16 | 19.27 |
| Go | 5.25 | 4.83 | 7.29 | 9.54 | |
| Java | 5.53 | 4.93 | 7.54 | 17.19 |
Java 在 generate_random、AES-GCM、ECDSA 三項上都最快。這不能讀成「Java 比 Rust
快」——本次是 Java 綁定從 SafeNet jcprov 換成自家手寫 JNI 之後的首次量測,兩者不是
同一條程式路徑。合理的讀法是:手寫 JNI 的呼叫開銷確實比官方 provider 低,而三份
現行實作的綁定層成本落在同一個量級。
ML-DSA(FIPS 204,單位 ms)
| 操作 | 語言 | mean | p50 | p95 | p99 |
|---|---|---|---|---|---|
| ML-DSA-44 簽章 | Rust | 15.60 | 14.21 | 27.95 | 38.00 |
| Go | 12.67 | 11.58 | 20.30 | 23.24 | |
| Java | 15.31 | 13.35 | 32.47 | 38.20 | |
| ML-DSA-44 驗證 | Rust | 10.05 | 9.46 | 13.70 | 22.44 |
| Go | 7.58 | 7.51 | 9.22 | 12.81 | |
| Java | 8.17 | 7.81 | 9.89 | 11.98 | |
| ML-DSA-65 簽章 | Rust | 22.05 | 19.11 | 34.80 | 43.76 |
| Go | 21.94 | 18.34 | 41.32 | 66.85 | |
| Java | 22.49 | 19.56 | 44.69 | 64.52 | |
| ML-DSA-65 驗證 | Rust | 12.48 | 11.97 | 16.89 | 20.11 |
| Go | 10.14 | 9.88 | 11.81 | 15.51 | |
| Java | 11.01 | 10.88 | 13.24 | 14.03 | |
| ML-DSA-87 簽章 | Rust | 27.52 | 23.36 | 45.94 | 81.20 |
| Go | 24.98 | 22.79 | 41.84 | 57.44 | |
| Java | 25.61 | 23.35 | 41.01 | 52.47 | |
| ML-DSA-87 驗證 | Rust | 15.35 | 15.06 | 18.08 | 19.78 |
| Go | 15.13 | 14.93 | 17.67 | 18.94 | |
| Java | 15.45 | 15.11 | 18.46 | 20.01 |
簽章成本隨參數集上升(p50 約 11.6 → 18.3 → 22.8 ms),約為驗證的 1.5–1.8 倍。參數集 越大,三份實作的差距越小——ML-DSA-87 驗證的三個 p50 落在 14.93–15.11 ms,差距 0.8%。 綁定層那點差異被 HSM 端的運算成本淹沒了。
ML-KEM(FIPS 203,單位 ms)
| 操作 | 語言 | mean | p50 | p95 | p99 |
|---|---|---|---|---|---|
| ML-KEM-512 封裝 | Rust | 6.33 | 5.59 | 9.42 | 12.13 |
| Go | 6.00 | 5.56 | 8.34 | 9.32 | |
| Java | 6.28 | 5.81 | 8.32 | 9.67 | |
| ML-KEM-512 解封裝 | Rust | 6.10 | 5.73 | 7.79 | 8.38 |
| Go | 7.75 | 7.67 | 10.09 | 12.38 | |
| Java | 6.25 | 5.82 | 8.70 | 10.09 | |
| ML-KEM-768 封裝 | Rust | 7.14 | 6.88 | 9.69 | 11.35 |
| Go | 7.26 | 7.07 | 9.48 | 10.73 | |
| Java | 6.33 | 6.34 | 7.68 | 8.39 | |
| ML-KEM-768 解封裝 | Rust | 6.82 | 6.63 | 7.94 | 10.14 |
| Go | 6.69 | 6.59 | 8.08 | 8.31 | |
| Java | 7.30 | 6.98 | 9.18 | 10.33 | |
| ML-KEM-1024 封裝 | Rust | 7.49 | 7.01 | 9.62 | 10.28 |
| Go | 7.70 | 7.19 | 9.82 | 12.01 | |
| Java | 7.49 | 7.50 | 8.67 | 9.38 | |
| ML-KEM-1024 解封裝 | Rust | 9.05 | 8.88 | 11.06 | 11.97 |
| Go | 9.66 | 9.12 | 13.40 | 14.75 | |
| Java | 9.75 | 9.64 | 11.97 | 12.96 |
封裝與解封裝的成本接近,隨參數集溫和上升(p50 從 512 的約 5.6 ms 到 1024 的約 9.1–9.6 ms),成本曲線比 ML-DSA 平緩得多。
這裡量的是純 KEM 操作(回傳 HSM 內 handle 的低階呼叫)。若改用會把共享密鑰匯出成 明文的便利函式,數字會被額外的 wrap + decrypt 墊高——那是匯出的成本,不是 KEM 的成本。
HSS/LMS(2 層 H5/H5,單位 ms)
| 操作 | 語言 | mean | p50 | p95 | p99 |
|---|---|---|---|---|---|
| HSS 簽章 | Rust | 13.49 | 13.30 | 16.32 | 16.51 |
| Go | 13.71 | 13.31 | 16.98 | 24.12 | |
| Java | 14.48 | 14.73 | 17.32 | 18.29 | |
| HSS 驗證 | Rust | 7.81 | 7.66 | 10.36 | 15.13 |
| Go | 7.76 | 8.04 | 10.63 | 11.53 | |
| Java | 8.03 | 7.98 | 10.97 | 16.88 |
HSS 簽章 p50 約 13.3–14.7 ms,落在 ML-DSA-44 與 ML-DSA-65 之間。以雜湊為基礎的方案 在這顆 HSM 上並沒有比格密碼便宜——它的價值在安全假設(只依賴雜湊函數)與驗證速度, 不在簽章吞吐。
併發吞吐(128 workers,各 6,400 次)
為了公平,三邊的總積壓上限統一換算成 1,024(Rust 與 Java 填每 worker 深度 8,Go 填 共用佇列 1,024)。
| 運算 | 語言 | wall clock (ms) | 吞吐 (ops/sec) | 平均延遲 (ms) |
|---|---|---|---|---|
| AES-GCM 1KiB 加密 | Rust | 472 | 13566 | 0.074 |
| Go | 477 | 13419 | 0.075 | |
| Java | 745 | 8594 | 0.116 | |
| ML-DSA-65 簽章 | Rust | 98466 | 65 | 15.385 |
| Go | 98840 | 65 | 15.444 | |
| Java | 98441 | 65 | 15.382 |
最重要的一個發現:PQC 簽章的天花板在 HSM,不在語言
把單一 session 的延遲換算成單執行緒吞吐,再跟 128 workers 的實測相比:
| 運算 | 語言 | 單執行緒 (ops/s) | 128 workers 理論值 | 實測 | 效率 |
|---|---|---|---|---|---|
| AES-GCM 1KiB | Rust | 156 | 19962 | 13566 | 68.0% |
| Go | 150 | 19145 | 13419 | 70.1% | |
| Java | 232 | 29747 | 8594 | 28.9% | |
| ML-DSA-65 簽章 | Rust | 45 | 5804 | 65 | 1.1% |
| Go | 46 | 5835 | 65 | 1.1% | |
| Java | 44 | 5691 | 65 | 1.1% |
對稱加密在 128 workers 下還拿得到 29–70% 的效率,ML-DSA-65 簽章只有 1.1%——而且 三份實作的絕對吞吐收斂在 65 ops/sec,彼此差距不到 0.4%。
三個獨立實作、三種併發模型(tokio blocking pool、goroutine、原生 thread)碰巧給出同一 個數字的機率極低。結論很直接:HSM 內部把 PQC 簽章排成了一條隊伍。128 個 worker 打 進去,吞吐只比單執行緒(約 45 ops/s)高不到一半。對照組乾淨得不能再乾淨——同一顆 HSM、 同一組 pool、同一批 worker,換成 AES-GCM 就能跑到一萬三千多 ops/s。
對容量規劃的意義:PQC 簽章服務的吞吐上限由 HSM 決定。加 worker 沒用,換語言沒用, 換更快的 pool 實作也沒用。要提高吞吐只能加 HSM。 如果你的遷移計畫假設「PQC 只是換個 演算法,容量規劃照舊」,這個數字值得先拿去驗算一遍。
與傳統演算法對比
| 演算法 | 簽章 p50 | 驗證 p50 | 簽章大小 | 抗量子攻擊 |
|---|---|---|---|---|
| ML-DSA-65 | 18.3–19.6 ms | 9.9–12.0 ms | 3,309 B | 是 |
| ML-DSA-44 | 11.6–14.2 ms | 7.5–9.5 ms | 2,420 B | 是 |
| HSS/LMS (H5/H5) | 13.3–14.7 ms | 7.7–8.0 ms | 數 KB | 是 |
| RSA-2048 | 4.8–6.5 ms | — | 256 B | 否 |
| ECDSA P-256 | 4.6–6.4 ms | — | 64 B | 否 |
(範圍為三份實作的最小值到最大值。RSA 與 ECDSA 的驗證未納入本次量測。)
觀察:在這顆 HSM 上,ML-DSA-65 簽章約為 ECDSA P-256 的 3 倍、ML-DSA-44 約為 2.2 倍。 這與純軟體實作的基準差距很大——因為量到的主要是 HSM 端的運算與一趟網路來回,不是演算法 本身在 CPU 上的理論成本。簽章大小則是實打實的劣勢:3,309 B 對 64 B,差 50 倍以上,這會 直接反映在憑證鏈大小與 TLS handshake 的傳輸量上。
這些數字的限度
- 單次 run 的變異可能很大。 同一天稍早跑過一次同一套程式,
generate_random_32的 Rust mean 是 4.03 ms,正式這次是 6.07 ms——相隔數分鐘,差了 50%。請以相對關係為準, 不要把絕對數字當永久基準。 - 1.1% 的併發效率是單次量測。 它之所以可信,是因為三份獨立實作同時給出同一個數字, 而不是因為跑了很多次。真要當容量規劃依據,應該重跑確認。
- 沒有量 PQC 的 keygen 成本。 ML-DSA 與 ML-KEM 的 keygen 排除在計時外;HSS 的 keygen 更是刻意用小樹避開(H5 約 60 ms,H15 已達 92 s),那條成本曲線值得單獨一份報告。
- 執行緒模型的差異無法完全隔離。 併發數字裡混著 tokio blocking pool、goroutine 與 原生 thread 的差異,量不出「純 HSM 端」的成本。
跨語言一致性:三份實作對得起來嗎?
三份實作只有在行為真的一致時才有價值——否則「三個語言都支援」只是三個各自為政的半成品。 我們用靜態比對加真機實測做了一輪完整稽核,結論如下。
能力對照
| 能力 | Rust | Go | Java |
|---|---|---|---|
| ML-DSA 44 / 65 / 87 | ✅ | ✅ | ✅ |
| ML-KEM 512 / 768 / 1024 | ✅ | ✅ | ✅ |
| HSS/LMS(含剩餘次數查詢) | ✅ | ✅ | ✅ |
| RSA-PSS / OAEP(可配置雜湊) | ✅ | ✅ | ✅ |
| EdDSA(pure Ed25519) | ✅ | ✅ | ✅ |
| 原生 ECIES | ✅ | ✅ | ✅ |
| 高階 signMessage(HSM 內雜湊) | ✅ | ✅ | ✅ |
| BIP32 / SLIP10 HD 衍生 | ✅ | ✅ | ✅ |
| 斷線錯誤碼分類 | 15 碼 | 15 碼 | 15 碼 |
| REST 路由 | 50 條 | 50 條 | 50 條 |
上表是稽核結束後的狀態。開始的時候不是這樣:Java 缺 RSA-PSS、EdDSA、原生 ECIES 與
ECDH KDF 變體;Go 缺數個對稱運算的安全守衛;Rust 的 OAEP source 欄位填了未定義值。
補齊這些花了四輪,全部真機驗證通過。
互通是實測的,不是推論的
三份服務打的是同一顆 HSM,所以可以直接用「A 加密、B 解密」驗證:
- Java 產的 AES-GCM 密文 → Go 解密成功
- Java 產的
AES_KEY_WRAPblob → Go unwrap 成功(兩邊送的機制常數不同——一邊送標準 值、一邊送 vendor 值——密文仍互通) - 三份實作的 Ed25519 簽章逐位元相同,儘管它們送的機制參數契約不同
順帶一提:這輪實測推翻了我們兩處純讀程式碼得出的判斷。只讀 DTO 不看實作會誤判。
最值得記住的一課:自簽自驗是盲的
三份實作曾經一起做錯一件事,而且所有測試都是綠的。
問題出在「宣稱支援某個標準演算法」這件事上。HSM 自己簽、自己驗,用的是同一套契約—— 契約寫錯了,簽與驗會一起錯,round-trip 照樣通過。跨語言互測也救不了:三方一致地錯, 測出來還是一致。
唯一有效的辦法是接外部獨立的驗證器:
- Ed25519 接
ed25519-dalek驗,才確認 Luna 的 prehash 模式堅持收完整訊息、不接受 呼叫端預先算好的摘要——我們原本的理解是反的 - HSS 接 Python 的
hsslms(獨立的 RFC 8554 實作)驗,才確認簽的是訊息本身而非 其摘要:用原始訊息驗通過,用 SHA-256 摘要驗失敗 CKM_ECDSA_SHA256接 Pythoncryptography驗,才確認輸出是標準 ECDSA
這條規則值得寫進團隊守則:凡宣稱「支援某標準演算法」,必須用外部獨立驗證器驗過, 不能只靠 HSM 自簽自驗。 少了這一步,你會帶著錯誤的契約交付,而且三個語言一致地錯。
部署與運維
Docker 容器化部署
多階段建置策略(三份實作共用同一套輪廓,只有 builder 映像不同):
-
Stage 1 (Builder)
- 基礎映像:
rust:slim/golang:bookworm/maven:3-eclipse-temurin-17 - 安裝建置依賴(ca-certificates、libssl-dev)
- 先只複製依賴宣告檔並預先下載依賴,利用 Docker 層快取
- 編譯 release 版本
- 基礎映像:
-
Stage 2 (Runtime)
- 基礎映像:
debian:bookworm-slim(Java 版為eclipse-temurin:17-jre) - 僅包含執行時依賴
- 建立非 root 用戶(安全性)
- 暴露端口:8080(REST)、50051(gRPC)、9100(Metrics)
- 健康檢查:30 秒間隔
- 基礎映像:
三份實作都不能用 distroless 或 Alpine 當 runtime:容器內必須有 Luna Client 的
libCryptoki2_64.so與Chrystoki.conf,而該函式庫依賴 glibc。Java 版另外需要能 把打包在 jar 內的 JNI 函式庫抽到可執行的暫存目錄——唯讀根檔案系統要記得留一個可寫的/tmp。
Docker Compose 部署配置:
關鍵配置項:
- Volume 掛載:Luna Client 庫(只讀)、Chrystoki.conf、稽核日誌
- 環境變數:HSM_PIN、SLOT_ID、SESSION_POOL_SIZE(50)
- 資源限制:CPU 4 核、記憶體 4 GB(限制)、2 核/2 GB(預留)
- 重啟策略:unless-stopped(除非手動停止)
- 日誌輪替:最大 100 MB,保留 10 個檔案
- 網路隔離:獨立 crypto-network
整合監控服務:
- Prometheus(指標收集)
- Grafana(視覺化)
- Alertmanager(警示)
啟動服務:
# 建立 .env 檔案
cat > .env << EOF
HSM_PIN=your-secret-pin
EOF
# 啟動所有服務
docker compose up -d
# 查看日誌
docker compose logs -f crypto-service
# 健康檢查
curl http://localhost:8080/health
curl http://localhost:9100/metrics # Prometheus metrics
監控整合
Prometheus 配置 (monitoring/prometheus.yml):
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'crypto-service'
static_configs:
- targets: ['crypto-service:9100']
scrape_interval: 15s
metrics_path: /metrics
關鍵監控指標:
| Metric 名稱 | 類型 | 說明 |
|---|---|---|
crypto_service_requests_total | Counter | 總請求數(按 endpoint、method、status) |
crypto_service_request_duration_seconds | Histogram | 請求延遲分佈 |
crypto_service_pqc_operations_total | Counter | PQC 操作計數(按 algorithm、operation) |
crypto_service_hsm_session_pool_size | Gauge | Session Pool 大小 |
crypto_service_hsm_session_pool_idle | Gauge | 閒置 Session 數量 |
crypto_service_hsm_errors_total | Counter | HSM 錯誤計數(按 error_type) |
crypto_service_audit_logs_total | Counter | 稽核日誌寫入數 |
警示規則範例 (monitoring/alerts.yml):
groups:
- name: crypto_service
rules:
# HSM 斷線警示
- alert: HSMDisconnected
expr: rate(crypto_service_hsm_errors_total{error_type="hsm_disconnected"}[5m]) > 0
for: 1m
labels:
severity: critical
annotations:
summary: "HSM 已斷線"
description: "過去 5 分鐘內偵測到 HSM 斷線錯誤"
# 高錯誤率警示
- alert: HighErrorRate
expr: rate(crypto_service_requests_total{status="error"}[5m]) > 10
for: 5m
labels:
severity: warning
annotations:
summary: "錯誤率過高"
description: "過去 5 分鐘平均錯誤率超過 10 req/sec"
# Session Pool 耗盡
- alert: SessionPoolExhausted
expr: crypto_service_hsm_session_pool_idle == 0
for: 2m
labels:
severity: warning
annotations:
summary: "Session Pool 耗盡"
description: "所有 HSM Session 都在使用中,可能需要增加 pool size"
Kubernetes 部署
Deployment 關鍵配置:
| 項目 | 配置 |
|---|---|
| 副本數 | 3(高可用) |
| 節點選擇器 | hsm.luna.enabled: "true"(專用節點) |
| 端口 | 8080(HTTP)、50051(gRPC)、9100(Metrics) |
| 環境變數 | HSM_PIN(Secret)、SESSION_POOL_SIZE(50) |
| Volume 掛載 | Luna Client、Chrystoki.conf、應用配置、稽核儲存(PVC) |
| 資源配置 | Request: 2 核/2 GB,Limit: 4 核/4 GB |
| 健康檢查 | Liveness: 30 秒,Readiness: 10 秒 |
Service 配置:
- 類型:ClusterIP(內部服務)
- 端口映射:80→8080(HTTP)、50051(gRPC)、9100(Metrics)
關鍵考量:
- 節點親和性:僅部署到已安裝 Luna Client 的節點
- 秘密管理:HSM PIN 儲存在 Kubernetes Secret 中
- 持久化儲存:稽核日誌使用 PVC(PersistentVolumeClaim)
- Prometheus 整合:透過 annotations 自動服務發現
最佳實踐與陷阱
金鑰管理最佳實踐
推薦做法
1. 金鑰類型選擇
- 開發/測試:使用 Session 金鑰(
CKA_TOKEN = false,程式結束自動清除) - 生產環境:使用 Token 金鑰(
CKA_TOKEN = true,持久化)
2. 使用描述性的金鑰標籤
標籤裡要包含用途、演算法與日期:
| 好 | 差 |
|---|---|
api_auth_ml_dsa_65_2026_02 | key1 |
firmware_sign_hss_lms_2026 | test |
kem_session_ml_kem_768_20260203 | my_key |
3. 定期輪換金鑰
- 建議每 90 天輪換一次
- 流程:產生新金鑰 → 更新配置 → 等待舊金鑰閒置 → 刪除舊金鑰
4. 匯出公鑰備份
- 公鑰可匯出並安全分享
- 私鑰永不離開 HSM
DON’T(避免的做法)
1. 不要在 HSM 中儲存過多金鑰
- Luna HSM 儲存容量有限
- 應定期清理不再使用的金鑰
- 金鑰數量與查詢時間成正比
2. 不要重複使用相同標籤
- 可能導致
find_key_by_label()找到舊金鑰 - 應先刪除舊金鑰或使用不同標籤(加入日期)
3. 不要忽略金鑰使用次數限制(HSS/LMS)
- HSS/LMS 金鑰簽章次數有上限(由樹高決定)
- 應定期檢查並提前輪換
4. 不要硬編碼 HSM PIN
- 硬編碼在程式碼中極度危險
- 從環境變數讀取
- 更好:從 Secret Manager(Vault/AWS Secrets Manager)讀取
安全考量
1. 網路隔離
Firewall 規則:
- 只允許 Crypto Service IP 存取 HSM(Port 1792)
- 拒絕其他來源的連線
2. mTLS 認證
gRPC Server 配置要點:
- 載入伺服器憑證和私鑰
- 配置客戶端 CA 憑證(驗證客戶端身份)
- 啟用雙向 TLS 認證
3. API 速率限制
三個框架各有中介軟體可用(Tower、Echo middleware、Quarkus filter),設定值才是重點。 把上限訂在 HSM 撐得住的範圍內——依前面的實測,PQC 簽章的天花板是每顆 HSM 約 65 ops/sec, 訂一個遠高於它的速率限制只是把排隊從 HSM 前面搬到服務裡面。
4. 輸入驗證
至少要擋兩件事:
- 資料大小上限(例如 1 MB)。HSS 另有硬限制:單段訊息 48,108 bytes,超過必須走 multi-part,不驗證就會拿到難以診斷的 HSM 錯誤。
- 演算法白名單。只接受明確支援的參數集,不要讓呼叫端送任意機制常數進來。
效能調優
1. Session Pool 大小調整
經驗法則:
Pool Size = 預期 QPS × 平均操作延遲(秒)× 1.5
但用 PQC 的真機數字算下去,會得到一個不太舒服的結論:
預期 QPS 1,000、ML-DSA-65 簽章延遲 0.022 秒
→ Pool Size = 1000 × 0.022 × 1.5 = 33
33 個 session 看起來很合理——問題是這顆 HSM 的 ML-DSA 簽章吞吐上限只有約 65 ops/sec (見效能測試一節,128 workers 實測)。1,000 QPS 這個前提本身就不成立,再大的 pool 也只是 讓請求在 HSM 前面排更長的隊。
所以 PQC 服務的容量規劃順序應該反過來:先用實測吞吐算出需要幾顆 HSM,再決定 pool 大小。Pool 只能讓你把單顆 HSM 的容量用滿,不能創造容量。
動態調整:監控 pool 使用率(借出中 / 總數),持續超過 80% 就該檢討——但要先確認 瓶頸在 pool 還是在 HSM。若 HSM 已經打滿,加 pool size 不會改善延遲,只會讓 p99 更難看。
2. 快取公鑰
公鑰不是秘密,而且從 HSM 讀取需要一趟往返(實測數 ms 起跳)。用一個帶 TTL 的 記憶體快取(label → 公鑰 bytes,容量上限 1,000、存活 1 小時)就能省掉絕大多數的往返。
注意快取的是公鑰。私鑰永不離開 HSM,沒有東西可以快取。
3. 批次處理時共用 Session
逐次簽章時,每次都向 pool 借還一次 session 是純粹的浪費。批次處理應該借一次、簽完整批 再還:
- 效率低:迴圈裡每次都
pool.get()→ 簽章 → 釋放 - 效率高:借一個 session → 迴圈內重複使用 → 整批結束後一次釋放
這對三份實作都適用,也是唯一能在 HSM 吞吐上限之內擠出額外效能的方向——它省的是 pool 的排隊與借還開銷,不是 HSM 的運算時間。
4. 不要試圖用平行化突破 PQC 的吞吐上限
前面的實測已經給了很明確的答案:128 個 worker 打同一顆 HSM,ML-DSA-65 簽章的吞吐只有 單執行緒的 1.4 倍。加執行緒、換語言、換 pool 實作都不會改變這件事。要更多吞吐就要更多 HSM。
未來展望
混合密碼學方案
雙重簽章憑證(Composite Signatures):
Certificate {
tbsCertificate: {
subject: "CN=example.com",
subjectPublicKeyInfo: {
algorithm: id-composite-key,
subjectPublicKey: (ECC P-256 公鑰, ML-DSA-65 公鑰)
}
},
signatureAlgorithm: id-composite-signature,
signature: (ECDSA 簽章, ML-DSA-65 簽章)
}
優勢:
- 同時提供傳統與後量子安全性
- 平滑遷移路徑(舊系統驗證 ECDSA,新系統驗證雙重)
- 符合 IETF Draft: Composite Signatures
NIST 第四輪候選演算法
| 演算法 | 類型 | 狀態 |
|---|---|---|
| FN-DSA(FALCON) | 簽章 | 草案標準化中(FIPS 206) |
| HQC | KEM | 已獲 NIST 選為備援 KEM 標準(2025 年 3 月),草案編撰中 |
| BIKE | KEM | 未獲選 |
HQC 之所以值得注意,是因為它的安全基礎(糾錯碼)與 ML-KEM(格)完全不同——萬一格密碼 被找到結構性弱點,HQC 是不會一起倒的那條備援路線。代價是公鑰與密文都比 ML-KEM 大得多。
FALCON 特色:
- 簽章大小極小(~666 B for FALCON-512)
- 驗證極快
- 適合資源受限環境(IoT)
量子安全通訊協議
TLS 1.3 PQC 支援(RFC 9370):
ClientHello:
- supported_groups: x25519_mlkem768, secp256r1_mlkem768
- signature_algorithms: ecdsa_secp256r1_mlkem768
ServerHello:
- key_share: x25519_mlkem768
- signature: ecdsa_secp256r1_mlkem768
混合金鑰交換:
- X25519 (ECDH) + ML-KEM-768
- 即使一方被破解,另一方仍提供保護
結語與資源
關鍵要點
- 後量子威脅真實存在:「先儲存後破解」攻擊已在進行中
- NIST 標準已完成:ML-DSA / ML-KEM 已正式標準化(FIPS 203/204),HSM 已可用
- 語言不是重點,HSM 才是:三份獨立實作在同一顆 HSM 上的 PQC 延遲幾乎打平, ML-DSA-87 驗證的 p50 差距只有 0.8%
- PQC 簽章的吞吐天花板在 HSM:128 workers 實測 65 ops/sec、效率 1.1%,三個語言 收斂到同一個數字。容量規劃要先算 HSM 數量,不是先調 pool
- 自製綁定有其必要:ML-KEM 的封裝走 vendor 延伸,通用 PKCS#11 綁定做不到;代價是 每份約 6,000 行,而且要乘以語言數
- 模板比 API 難:PQC 沒有專用函式,難的是
CKA_PARAMETER_SET放哪、HSS 參數只能放 私鑰模板這類實測規則——組錯只會拿到毫無指向性的CKR_ATTRIBUTE_VALUE_INVALID - 自簽自驗是盲的:凡宣稱支援某標準演算法,必須接外部獨立驗證器。少了這一步,三個 語言會一致地錯
三份實作值得嗎?
誠實地說:如果你的目標只是「讓服務跑起來」,不值得——一份實作就夠,維護成本是 三倍起跳。
它的價值在別的地方。加入第三份實作時發現的 18 項服務層分歧裡,有 2 項是修在既有的兩份 上;一個被誤診為 HSM 設定問題長達數月的缺口,是在另一份實作「一開始就寫對」的程式碼擺 在旁邊時才被看出來的。多一份獨立實作,等於多一組不共享假設的眼睛。
如果你只打算做一份,那就把外部獨立驗證器補上——那是單一實作能買到的、最接近第二雙眼睛 的東西。
專案資源
參考文件:
- NIST Post-Quantum Cryptography
- FIPS 203 (ML-KEM)
- FIPS 204 (ML-DSA)
- Thales Luna HSM 文件
- rust-cryptoki GitHub
延伸閱讀:
- IETF: Composite Signatures and Encryption
- RFC 9370: TLS 1.3 Hybrid Key Exchange
- NIST SP 800-208: HSS/LMS
本文初版完成於 2026-02-03,2026-09-14 改寫為三語言版本並補上真機效能實測。 基於 Thales Luna HSM Firmware 7.9.x、Rust 1.97 / Go 1.26 / OpenJDK 17 與 NIST FIPS 203/204 標準撰寫。效能數字取自 2026-09-14 的單次量測,run-to-run 會有變異,請以相對關係為準。
如有任何問題或建議,歡迎透過聯絡我們 頁面與我們聯繫。
