如果私钥存储在无法发送消息的位置,我们建议使用 ML_DSA_65 密钥的 Prehash 和 SignPrehash 原语。
有时,持有签名密钥的一方无法(或不应)接收消息本身:密钥位于 HSM 或 KMS 中,或者消息大于签名者的请求大小限制。
Prehash 和 SignPrehash 原语通过将签名拆分为两个步骤来解决此问题。您仅使用公钥计算消息的短预哈希值,并将其发送给签名者。签名者使用私钥将其转换为签名,但绝不会看到消息。在外部 Mu 模式下使用 ML-DSA(Tink 在此模式下支持的算法)时,无论消息大小如何,预哈希值均为 69 字节。
您收到的签名是原始消息的普通签名:验证者使用常规的数字签名原语,无需按需知密其中涉及两个步骤。
准备工作
创建密钥具有 ID 要求的 ML-DSA 密钥集 - 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 边界后面,并且您不想将完整消息跨越该边界进行传输。
- 消息过大,并且远程签名者强制执行请求大小限制。
如果上述两种情况都不适用,请改用简单的数字签名原语:它更简单,而且更难被滥用。
密钥集
这两个原语选择密钥的方式不同,就像签名和验证对数字签名原语所做的那样:
Prehash.Compute始终使用公开密钥集的主密钥,并在预哈希值中记录该密钥的 ID。它会选择用于生成签名的密钥。SignPrehash.Sign从预哈希值中读取密钥 ID,并使用私钥集中匹配的已启用的密钥进行签名。它指的是跟随他人已做出的选择的一方。如果密钥集中没有已启用的密钥具有该 ID,则调用失败。
SignPrehash 密钥集中的每个密钥都必须具有 ID 要求,否则创建原语会失败。
最低安全保障
- 生成的签名与使用相同密钥类型的 Digital Signature 原语生成的签名具有相同的属性。
- Tink 会在预哈希值前面添加 5 个字节,其中包含一个特殊的预留值以及计算该预哈希值所用的密钥的 ID。
SignPrehash仅使用该密钥对值进行签名。该值是否以加密方式绑定到相应密钥取决于算法;对于外部 Mu ML-DSA,该值是绑定的,请参阅 Tink wire format。 - 消息长度不限。
注意事项
- 签名者无法检查其签名的内容。任何可以调用
SignPrehash的人都可以获取任意已签名的消息,而签名者无法对消息内容应用政策。保护对SignPrehash的访问权限,就像保护对PublicKeySign的访问权限一样。 - 保护传输中的预哈希值。它不是密钥,但如果攻击者能够在传输过程中修改它,就能控制签名的内容。
选择密钥类型
如 RFC 9881 中所述,外部 Mu 模式下的 ML-DSA 是 Tink 支持的唯一用于 Prehash 和 SignPrehash 的算法。您应将标准 ML-DSA 密钥与这些原语搭配使用。
对于大多数使用场景,我们建议使用 ML_DSA_65。
密钥必须具有 ID 要求,因为每个预哈希值都以包含相应密钥 ID 的前缀开头。接受以下变体:
TINK- 生成的签名以 Tink 常规的 5 字节输出前缀开头。NO_PREFIX_WITH_PREHASH_ID- 生成的签名不带任何输出前缀,而密钥仍具有预哈希值所需的 ID。
不支持使用 NO_PREFIX(原始)变体的密钥,因为它们没有密钥 ID。