如果私密金鑰位於無法傳送訊息的位置,建議使用 ML_DSA_65 金鑰搭配 Prehash 和 SignPrehash 基本類型。
有時,持有簽署金鑰的一方無法 (或不應) 接收郵件本身:金鑰位於 HSM 或 KMS 中,或郵件大小超過簽署者的要求大小限制。
Prehash 和 SignPrehash 基本體會將簽署作業分成兩個步驟,解決這個問題。您可以使用公開金鑰,在訊息所在位置計算簡短的預先雜湊值,然後傳送給簽署者。簽署者會使用私密金鑰將訊息轉換為簽章,但不會看到訊息內容。在外部 Mu 模式下使用 ML-DSA 時 (Tink 在此支援的演算法),無論訊息大小為何,前置雜湊值都是 69 個位元組。
您收到的簽章是原始訊息的普通簽章: 驗證者會使用一般的數位簽章基本類型,不需要知道涉及兩個步驟。
事前準備
建立 ML-DSA 金鑰組,其中金鑰必須有 ID 需求,也就是 TINK 變體,或 NO_PREFIX_WITH_PREHASH_ID (如果您不希望結果簽章有輸出前置字元)。將私密金鑰集提供給簽署者,並將對應的公開金鑰集提供給預先雜湊處理端。簽署者應為預先雜湊處理端可產生的每個金鑰 ID 啟用金鑰;請參閱「金鑰集」。
步驟 1:計算前置雜湊值
在訊息所在位置執行這項操作。這項操作只需要公開金鑰集。
C++
#include "tink/keyset_handle.h" #include "tink/signature/config_2026.h" #include "tink/signature/prehash.h" absl::StatusOr<std::unique_ptr<crypto::tink::Prehash>> prehasher = public_handle.GetPrimitive<crypto::tink::Prehash>( crypto::tink::ConfigSignature2026()); if (!prehasher.ok()) return prehasher.status(); absl::StatusOr<std::string> prehash = (*prehasher)->Compute(message); if (!prehash.ok()) return prehash.status();
Go
import "github.com/tink-crypto/tink-go/v2/signprehash" prehasher, err := signprehash.NewPrehash(publicHandle) if err != nil { return err } prehash, err := prehasher.ComputePrehash(message) if err != nil { return err }
步驟 2:將前置雜湊值傳送給簽署者
將前置雜湊值傳送給私密金鑰持有者。這不是密碼,但您必須保護其傳輸中的完整性:攻擊者若能在傳輸過程中修改這個值,就能控制簽署的內容。
步驟 3:簽署前置雜湊值
在私密金鑰所在位置執行這項操作。
C++
#include "tink/keyset_handle.h" #include "tink/signature/config_2026.h" #include "tink/signature/sign_prehash.h" absl::StatusOr<std::unique_ptr<crypto::tink::SignPrehash>> signer = private_handle.GetPrimitive<crypto::tink::SignPrehash>( crypto::tink::ConfigSignature2026()); if (!signer.ok()) return signer.status(); absl::StatusOr<std::string> signature = (*signer)->Sign(prehash); if (!signature.ok()) return signature.status();
Go
import "github.com/tink-crypto/tink-go/v2/signprehash" signer, err := signprehash.NewPrehashSigner(privateHandle) if err != nil { return err } sig, err := signer.SignPrehash(prehash) if err != nil { return err }
步驟 4:驗證簽名
驗證是針對原始訊息的普通數位簽章流程,而不是針對前置雜湊值。
C++
absl::StatusOr<std::unique_ptr<crypto::tink::PublicKeyVerify>> verifier = public_handle.GetPrimitive<crypto::tink::PublicKeyVerify>( crypto::tink::ConfigSignature2026()); if (!verifier.ok()) return verifier.status(); absl::Status verified = (*verifier)->Verify(signature, message);
Go
import "github.com/tink-crypto/tink-go/v2/signature" verifier, err := signature.NewVerifier(publicHandle) if err != nil { return err } if err := verifier.Verify(sig, message); err != nil { return err }
Prehash 和 SignPrehash
Prehash 和 SignPrehash 基本體會將數位簽章的運算分成兩個步驟:
- 前置雜湊只需要公開金鑰。可將任意長度的訊息轉換為固定大小的簡短前置雜湊值。
- SignPrehash 需要私密金鑰。將前置雜湊值轉換為簽章。
產生的簽章是原始訊息的普通簽章。您可以使用一般的數位簽章 PublicKeyVerify 基本類型進行驗證,驗證者不需要知道 (或在意) 簽章是分兩步驟產生。
將前置雜湊值視為不透明位元組。這項值的大小固定,且會攜帶計算所用金鑰的 ID,但其版面配置屬於 Tink 的線路格式,您不應自行剖析或建構。如要移植 Tink 或需要位元層級的詳細資料,請參閱「Tink 傳輸格式」。
在下列情況下,請使用這對基元:
- 簽署金鑰位於其他位置,例如 HSM、KMS 或 RPC 邊界後方,且您不想跨越該邊界傳送完整訊息。
- 訊息過大,且遠端簽署者強制執行要求大小限制。
如果上述情況均不適用,請改用簡單的數位簽章基本類型,因為這種基本類型較簡單,也較不容易誤用。
金鑰組
這兩個基本型別會以不同方式選取金鑰,就像簽署和驗證會針對 Digital Signature 基本型別執行一樣:
Prehash.Compute一律會使用公開金鑰組的主要金鑰,並在預先雜湊值中記錄該金鑰的 ID。這個角色會選擇用來建立簽章的金鑰。SignPrehash.Sign會從前置雜湊值讀取金鑰 ID,並使用私密金鑰集中相符的已啟用金鑰簽署。也就是「跟隨」他人選擇的一方。如果金鑰集中沒有已啟用且具有該 ID 的金鑰,呼叫就會失敗。
SignPrehash 鍵集中的每個金鑰都必須有 ID 需求,否則建立基本體的作業會失敗。
安全性保障不足
- 產生的簽章與數位簽章基本體使用相同金鑰類型產生的簽章具有相同屬性。
- Tink 會在前置雜湊值加上 5 個位元組,其中包含特殊保留值和計算該值的金鑰 ID。
SignPrehash只會使用該金鑰簽署值。值是否以密碼編譯方式繫結至該金鑰,取決於演算法;如果是 External Mu ML-DSA,則會繫結至該金鑰,請參閱 Tink 線路格式。 - 訊息長度不限。
注意事項
- 簽署者無法檢查簽署內容。任何可以撥打電話給
SignPrehash的人都能取得任意訊息的簽章,而簽署者無法對訊息內容套用政策。保護「SignPrehash」的存取權,就像保護「PublicKeySign」的存取權一樣。 - 保護傳輸中的前雜湊值。這不是密鑰,但攻擊者可以在傳輸過程中修改這項資訊,進而控制簽署內容。
選擇車鑰類型
如 RFC 9881 所述,Tink 僅支援 External Mu 模式的 ML-DSA,用於 Prehash 和 SignPrehash。您應搭配這些基本類型使用標準 ML-DSA 金鑰。
我們建議在多數情況下使用 ML_DSA_65。
金鑰必須符合 ID 規定,因為每個前置雜湊值開頭都有前置字串,內含計算該值所用的金鑰 ID。接受的變體如下:
TINK- 產生的簽章會以 Tink 常用的 5 位元組輸出前置字元開頭。NO_PREFIX_WITH_PREHASH_ID- 產生的簽章不含輸出前置字元,但金鑰仍有前置雜湊值需要的 ID。
系統不支援使用 NO_PREFIX (原始) 變體的金鑰,因為這類金鑰沒有金鑰 ID。