リモート署名者でデータに署名したい

秘密鍵がメッセージを送信できない場所に存在する場合は、ML_DSA_65 鍵を使用した Prehash と SignPrehash プリミティブをおすすめします。

署名鍵を保持するパーティがメッセージ自体を受信できない、または受信すべきでない場合があります。鍵が HSM または KMS に存在する場合や、メッセージが署名者のリクエスト サイズの上限を超える場合などです。

Prehash プリミティブと SignPrehash プリミティブは、署名を 2 つのステップに分割することで、この問題を解決します。メッセージがある場所で、公開鍵のみを使用して短いプリハッシュ値を計算し、署名者に送信します。署名者は、メッセージを確認することなく、秘密鍵を使用して署名に変換します。外部 Mu モードの ML-DSA(Tink がサポートするアルゴリズム)では、メッセージのサイズに関係なく、プリハッシュ値は 69 バイトです。

返される署名は、元のメッセージに対する通常の署名です。検証ツールは通常のデジタル署名プリミティブを使用し、2 つのステップが関与していることを知る必要はありません。

始める前に

鍵に 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 プリミティブは、デジタル署名の計算を次の 2 つのステップに分割します。

  1. Prehash に必要なのは公開鍵のみです。任意の長さのメッセージを、短い固定サイズのプリハッシュ値に変換します。
  2. SignPrehash には秘密鍵が必要です。プリハッシュ値を署名に変換します。

出力される署名は、元のメッセージに対する通常の署名です。通常のデジタル署名 PublicKeyVerify プリミティブで検証します。検証者は、署名が 2 つのステップで生成されたことを知る必要も気にする必要もありません。

事前ハッシュ値を不透明なバイトとして扱います。サイズは固定で、計算された鍵の ID を保持していますが、レイアウトは Tink のワイヤー形式の一部であり、自分で解析したり構築したりしないでください。Tink を移植する場合や、バイトレベルの詳細が必要な場合は、Tink のワイヤー形式をご覧ください。

このプリミティブのペアは、次の場合に使用します。

  • 署名鍵が別の場所にある(HSM、KMS、RPC 境界の背後など)。完全なメッセージをその境界を越えて送信したくない場合。
  • メッセージが大きい。リモート署名者がリクエスト サイズの上限を適用している。

どちらにも該当しない場合は、代わりにプレーンな Digital Signature プリミティブを使用します。これはよりシンプルで、不正使用しにくいものです。

鍵セット

2 つのプリミティブは、デジタル署名プリミティブの署名と検証と同様に、鍵を異なる方法で選択します。

  • Prehash.Compute は常に公開鍵セットの主キーを使用し、その鍵の ID をプリハッシュ値に記録します。署名に使用する鍵を選択する側です。
  • SignPrehash.Sign は、事前ハッシュ値から鍵 ID を読み取り、一致する有効な秘密鍵セットの鍵で署名します。これは、他のユーザーがすでに選択した内容に従う側です。鍵セット内の有効な鍵にその ID がない場合、呼び出しは失敗します。

SignPrehash キーセット内のすべての鍵には ID 要件が必要です。そうでない場合、プリミティブの作成は失敗します。

最小限のセキュリティ保証

  • 結果として得られる署名は、同じ鍵タイプで Digital Signature プリミティブによって生成された署名と同じプロパティを持ちます。
  • Tink は、特別な予約値と、計算対象の鍵の ID を含む 5 バイトの接頭辞をプリハッシュ値に付加します。SignPrehash は、その鍵でのみ値を署名します。値がその鍵に 暗号的にバインドされているかどうかは、アルゴリズムによって異なります。External Mu ML-DSA の場合はバインドされています。 Tink のワイヤー形式をご覧ください。
  • メッセージの長さは任意です。

注意点

  • 署名者は署名対象を検査できません。SignPrehash を呼び出すことができるユーザーは誰でも任意のメッセージに署名できます。署名者はメッセージの内容にポリシーを適用できません。PublicKeySign へのアクセスを保護する場合と同様に、SignPrehash へのアクセスを保護します。
  • 転送中のプリハッシュ値を保護します。シークレットではありませんが、転送中に変更できる攻撃者は、署名されるものを制御できます。

キーの種類を選択する

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(raw)バリアントを使用するキーは、キー ID がないためサポートされていません。