‫Prehash ו-SignPrehash

הפרימיטיבים Prehash ו-SignPrehash מפצלים את החישוב של חתימה דיגיטלית לשני שלבים:

  1. Prehash צריך רק את המפתח הציבורי. היא הופכת הודעה באורך שרירותי לערך קצר של טרום-גיבוב בגודל קבוע.
  2. ‫SignPrehash צריך את המפתח הפרטי. היא הופכת ערך לפני גיבוב לחתימה.

החתימה שמתקבלת היא חתימה רגילה על ההודעה המקורית. האימות מתבצע באמצעות הפרימיטיב הרגיל של חתימה דיגיטלית PublicKeyVerify, והמאמתים לא צריכים לדעת – או לא אכפת להם – שהחתימה נוצרה בשני שלבים.

התייחסות לערך שלפני הגיבוב כאל בייטים אטומים. הגודל שלו קבוע והוא מכיל את המזהה של המפתח שעבורו הוא חושב, אבל הפריסה שלו היא חלק מפורמט החוט של Tink, ואסור לנתח או ליצור אותו בעצמכם. אם אתם מעבירים נתונים באמצעות Tink או שאתם צריכים את הפרטים ברמת הבייט, כדאי לעיין במאמר בנושא פורמט הנתונים של Tink.

משתמשים בצמד הפרימיטיבים הזה במקרים הבאים:

  • מפתח החתימה נמצא במקום אחר, למשל ב-HSM, ב-KMS או מאחורי גבול RPC, ואתם לא רוצים לשלוח את ההודעה המלאה מעבר לגבול הזה.
  • ההודעה גדולה, והחתימה מרחוק אוכפת מגבלת גודל על הבקשה.

אם אף אחת מהאפשרויות האלה לא רלוונטית, אפשר להשתמש במקום זאת בפרימיטיב הפשוט Digital Signature: הוא פשוט יותר וקשה יותר לשימוש לא נכון.

ערכות מפתחות

שני הפרימיטיבים בוחרים מפתחות בצורה שונה, כמו שפרימיטיבים של חתימה ואימות עושים עבור הפרימיטיב חתימה דיגיטלית:

  • ‫Prehash.Compute תמיד משתמש במפתח הראשי של קבוצת המפתחות הציבורית, ומתעד את מזהה המפתח הזה בערך שלפני הגיבוב. זה הצד שבוחר את המפתח שבו תיצור החתימה.
  • ‫SignPrehash.Sign קורא את מזהה המפתח מתוך ערך הגיבוב המוקדם וחותם באמצעות המפתח התואם המופעל של קבוצת המפתחות הפרטיים. הצד שעוקב אחרי בחירה שמישהו אחר כבר עשה. אם אין מפתח מופעל ב-keyset עם המזהה הזה, הקריאה תיכשל.

לכל מפתח ב-keyset של SignPrehash צריך להיות מזהה, אחרת יצירת הפרימיטיב תיכשל.

התחייבויות מינימליות לאבטחה

  • החתימה שמתקבלת כוללת את אותם מאפיינים כמו חתימה שנוצרה על ידי הפרימיטיב Digital Signature עם אותו סוג מפתח.
  • ‫Tink מוסיף לערך שלפני הגיבוב קידומת של 5 בייטים שמכילה ערך מיוחד שמור ואת המזהה של המפתח שעבורו בוצע החישוב. ‫SignPrehash חותם על הערך רק באמצעות המפתח הזה. האם הערך קשור באופן קריפטוגרפי למפתח הזה תלוי באלגוריתם. במקרה של External Mu ML-DSA, הוא קשור. אפשר לקרוא על כך במאמר בנושא פורמט Tink wire.
  • אורך ההודעות יכול להיות שרירותי.

דברים שכדאי לשים לב אליהם

  • החותם לא יכול לבדוק את מה שהוא חותם עליו. כל מי שיכול להתקשר אל SignPrehash יכול לקבל חתימה על הודעה שרירותית, ולחותם אין אפשרות להחיל מדיניות על תוכן ההודעה. הגנה על הגישה אל SignPrehash בדיוק כמו הגנה על הגישה אל PublicKeySign.
  • הגנה על הערך לפני הגיבוב בזמן ההעברה. הוא לא סודי, אבל תוקף שיכול לשנות אותו במהלך ההעברה שולט במה שייחתם.

בחירת סוג המפתח

‫ML-DSA במצב External Mu, כפי שמתואר ב-RFC 9881, הוא האלגוריתם היחיד ש-Tink תומך בו עבור Prehash ו-SignPrehash. מומלץ להשתמש במפתח ML-DSA רגיל עם הפרימיטיבים האלה.

מומלץ להשתמש ב-ML_DSA_44 ברוב תרחישי השימוש.

למפתח חייבת להיות דרישת מזהה, כי כל ערך לפני הגיבוב מתחיל בקידומת שמכילה את המזהה של המפתח שעבורו הוא חושב. אלה הווריאציות המותרות:

  • ‫TINK – החתימה שמתקבלת מתחילה בקידומת הרגילה של Tink, שהיא פלט של 5 בייט.
  • ‫NO_PREFIX_WITH_PREHASH_ID – לחתימה שמתקבלת אין קידומת פלט, אבל למפתח עדיין יש את המזהה שערך הגיבוב המקדים צריך.

לא ניתן להשתמש במפתחות שמשתמשים בווריאציה NO_PREFIX (raw), כי אין להם מזהה מפתח.

דוגמאות

בדוגמאות הבאות, הודעה נחתמת בשני שלבים ואז החתימה שמתקבלת מאומתת באמצעות הפרימיטיב הרגיל Digital Signature.

הם מוצגים כבלוק אחד כדי שיהיה קל לקרוא אותם. בפריסה אמיתית, שני השלבים מתבצעים במקומות שונים. במאמר אני רוצה לחתום על נתונים באמצעות חותם מרחוק יש הסבר מפורט.

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
}

Python

from tink import signature

# Needed for PublicKeyVerify in step 3. Due to the internal structure of
# Tink Python, Prehash and SignPrehash are an exception where calling
# signature.register() is not necessary; do not expect the same for other
# primitives.
signature.register()

# 1. Wherever the message is: compute the prehash value. This needs only
#    the public keyset.
prehasher = public_handle.primitive(signature.Prehash)
prehash = prehasher.compute(message)

# 2. Wherever the private key is: turn the prehash value into a signature.
signer = private_handle.primitive(signature.SignPrehash)
sig = signer.sign(prehash)

# 3. Anywhere: verify the signature over the original message.
verifier = public_handle.primitive(signature.PublicKeyVerify)
verifier.verify(sig, message)