我想使用远程签名者对数据进行签名

如果私钥存储在无法发送消息的位置,我们建议使用 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 原语将数字签名的计算分为两步:

  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。