Prehash 和 SignPrehash

Prehash 和 SignPrehash 原语将数字签名的计算分为两步:

  1. 预哈希只需要公钥。它将任意长度的消息转换为简短的固定大小的预哈希值
  2. 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。

示例

以下示例分两步对消息进行签名,然后使用普通的数字签名原语验证生成的签名。

为便于阅读,它们显示为一个代码块。在实际部署中,这两个步骤在不同的位置运行;如需逐步演练,请参阅我想使用远程签名者对数据进行签名

C++

#include "tink/keyset_handle.h"
#include "tink/public_key_verify.h"
#include "tink/signature/config_2026.h"
#include "tink/signature/prehash.h"
#include "tink/signature/sign_prehash.h"

using ::crypto::tink::ConfigSignature2026;
using ::crypto::tink::Prehash;
using ::crypto::tink::PublicKeyVerify;
using ::crypto::tink::SignPrehash;

// 1. Wherever the message is: compute the prehash value. This needs only
//    the public keyset.
absl::StatusOr<std::unique_ptr<Prehash>> prehasher =
    public_handle.GetPrimitive<Prehash>(ConfigSignature2026());
if (!prehasher.ok()) return prehasher.status();

absl::StatusOr<std::string> prehash = (*prehasher)->Compute(message);
if (!prehash.ok()) return prehash.status();

// 2. Wherever the private key is: turn the prehash value into a signature.
absl::StatusOr<std::unique_ptr<SignPrehash>> signer =
    private_handle.GetPrimitive<SignPrehash>(ConfigSignature2026());
if (!signer.ok()) return signer.status();

absl::StatusOr<std::string> signature = (*signer)->Sign(*prehash);
if (!signature.ok()) return signature.status();

// 3. Anywhere: verify the signature over the original message.
absl::StatusOr<std::unique_ptr<PublicKeyVerify>> verifier =
    public_handle.GetPrimitive<PublicKeyVerify>(ConfigSignature2026());
if (!verifier.ok()) return verifier.status();

absl::Status verified = (*verifier)->Verify(*signature, message);

Go

import (
    "github.com/tink-crypto/tink-go/v2/signature"
    "github.com/tink-crypto/tink-go/v2/signprehash"
)

// 1. Wherever the message is: compute the prehash value. This needs only
//    the public keyset.
prehasher, err := signprehash.NewPrehash(publicHandle)
if err != nil {
    return err
}
prehash, err := prehasher.ComputePrehash(message)
if err != nil {
    return err
}

// 2. Wherever the private key is: turn the prehash value into a signature.
signer, err := signprehash.NewPrehashSigner(privateHandle)
if err != nil {
    return err
}
sig, err := signer.SignPrehash(prehash)
if err != nil {
    return err
}

// 3. Anywhere: verify the signature over the original message.
verifier, err := signature.NewVerifier(publicHandle)
if err != nil {
    return err
}
if err := verifier.Verify(sig, message); err != nil {
    return err
}