מומלץ להשתמש בפרימיטיבים Prehash ו-SignPrehash עם מפתח ML_DSA_65 כשהמפתח הפרטי נמצא במקום שלא ניתן לשלוח ממנו את ההודעה.
לפעמים הצד שמחזיק במפתח החתימה לא יכול – או לא צריך – לקבל את ההודעה עצמה: המפתח נמצא ב-HSM או ב-KMS, או שההודעה גדולה יותר ממגבלת גודל הבקשה של החותם.
הפרימיטיבים Prehash ו-SignPrehash פותרים את הבעיה הזו על ידי פיצול החתימה לשני שלבים. אתם מחשבים ערך קצר של טרום-גיבוב במקום שבו ההודעה נמצאת, באמצעות המפתח הציבורי בלבד, ושולחים אותו לחתום. החותם הופך אותו לחתימה באמצעות המפתח הפרטי, בלי לראות את ההודעה. ב-ML-DSA במצב External Mu – האלגוריתם ש-Tink תומך בו כאן – ערך ה-prehash הוא 69 בייט, לא משנה מה גודל ההודעה.
החתימה שמתקבלת היא חתימה רגילה על ההודעה המקורית: המאמתים משתמשים בפרימיטיב הרגיל של חתימה דיגיטלית ולא צריכים לדעת שהיו מעורבים שני שלבים.
לפני שמתחילים
יוצרים קבוצת מפתחות של ML-DSA שהמפתחות שלה כוללים דרישה למזהה – או TINK אם רוצים קידומת פלט בחתימה שמתקבלת, או NO_PREFIX_WITH_PREHASH_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: שולחים את ערך ה-prehash לחותם
שולחים את ערך הגיבוב המקדים למי שמחזיק במפתח הפרטי. הוא לא סודי, אבל צריך להגן על השלמות שלו בזמן ההעברה: תוקף שיכול לשנות אותו בזמן ההעברה שולט במה שייחתם.
שלב 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 מפצלים את החישוב של חתימה דיגיטלית לשני שלבים:
- Prehash צריך רק את המפתח הציבורי. הפונקציה הופכת הודעה באורך שרירותי לערך קצר של טרום-גיבוב בגודל קבוע.
- הפונקציה SignPrehash צריכה את המפתח הפרטי. הפונקציה הופכת ערך לפני גיבוב לחתימה.
החתימה שמתקבלת היא חתימה רגילה על ההודעה המקורית. האימות מתבצע באמצעות הפרימיטיב הרגיל של חתימה דיגיטלית PublicKeyVerify, והמאמתים לא צריכים לדעת – או לא אכפת להם – שהחתימה נוצרה בשני שלבים.
הערך שלפני הגיבוב נחשב לבייטים אטומים. הגודל שלו קבוע והוא מכיל את המזהה של המפתח שעבורו הוא חושב, אבל הפריסה שלו היא חלק מפורמט החוט של Tink, ואסור לנתח או ליצור אותו בעצמכם. אם אתם מעבירים נתונים באמצעות Tink או שאתם צריכים את הפרטים ברמת הבייט, כדאי לעיין במאמר בנושא פורמט Tink wire.
משתמשים בצמד הפרימיטיבים הזה במקרים הבאים:
- מפתח החתימה נמצא במקום אחר, למשל ב-HSM, ב-KMS או מאחורי גבול RPC, ואתם לא רוצים לשלוח את ההודעה המלאה מעבר לגבול הזה.
- ההודעה גדולה, והחתימה מרחוק אוכפת מגבלת גודל על הבקשה.
אם אף אחת מהאפשרויות האלה לא רלוונטית, אפשר להשתמש במקום זאת בפרימיטיב הפשוט של חתימה דיגיטלית: הוא פשוט יותר וקשה יותר לשימוש לא נכון.
ערכות מפתחות
שני הפרימיטיבים בוחרים מפתחות בצורה שונה, כמו שפרימיטיבים של חתימה ואימות עושים עבור הפרימיטיב חתימה דיגיטלית:
-
Prehash.Computeתמיד משתמש במפתח הראשי של קבוצת המפתחות הציבורית, ומתעד את מזהה המפתח הזה בערך שלפני הגיבוב. הצד שבוחר באיזה מפתח תיצור החתימה. -
SignPrehash.Signקורא את מזהה המפתח מתוך ערך הגיבוב המוקדם וחותם באמצעות המפתח התואם המופעל של קבוצת המפתחות הפרטיים. זה הצד שמציית לבחירה שכבר נעשתה על ידי מישהו אחר. אם אין מפתח מופעל ב-keyset עם המזהה הזה, הקריאה תיכשל.
לכל מפתח בערכת מפתחות SignPrehash צריך להיות מזהה, אחרת יצירת הפרימיטיב תיכשל.
ערבויות אבטחה מינימליות
- לחתימה שמתקבלת יש את אותם מאפיינים כמו לחתימה שנוצרת על ידי הפרימיטיב Digital Signature עם אותו סוג מפתח.
- Tink מוסיף לערך שלפני הגיבוב קידומת של 5 בייטים שמכילה ערך מיוחד שמור ואת המזהה של המפתח שעבורו בוצע החישוב.
SignPrehashחותם על הערך רק באמצעות המפתח הזה. האם הערך קשור באופן קריפטוגרפי למפתח הזה תלוי באלגוריתם. במקרה של External Mu ML-DSA, הוא קשור. אפשר לקרוא על כך במאמר Tink wire format. - ההודעות יכולות להיות בכל אורך.
דברים שכדאי לשים לב אליהם
- החותם לא יכול לבדוק את מה שהוא חותם עליו. כל מי שיכול להתקשר אל
SignPrehashיכול לקבל חתימה על הודעה שרירותית, ולחותם אין אפשרות להחיל מדיניות על תוכן ההודעה. הגנה על הגישה אלSignPrehashבדיוק כמו הגנה על הגישה אלPublicKeySign. - הגנה על הערך לפני הגיבוב בזמן ההעברה. הוא לא סודי, אבל תוקף שיכול לשנות אותו במהלך ההעברה שולט במה שייחתם.
בחירת סוג המפתח
ML-DSA במצב External Mu, כפי שמתואר ב-RFC 9881, הוא האלגוריתם היחיד ש-Tink תומך בו עבור Prehash ו-SignPrehash. מומלץ להשתמש במפתח ML-DSA רגיל עם הפרימיטיבים האלה.
מומלץ להשתמש ב-ML_DSA_65 ברוב תרחישי השימוש.
למפתח חייבת להיות דרישת מזהה, כי כל ערך של גיבוב מראש מתחיל בקידומת שמכילה את המזהה של המפתח שעבורו הוא חושב. אלה הווריאציות המותרות:
-
TINK– החתימה שמתקבלת מתחילה בקידומת הרגילה של Tink, שהיא פלט של 5 בייטים -
NO_PREFIX_WITH_PREHASH_ID– לחתימה שמתקבלת אין קידומת פלט, אבל למפתח עדיין יש את המזהה שערך הגיבוב המקדים צריך.
לא ניתן להשתמש במפתחות שמשתמשים בווריאציה NO_PREFIX (raw), כי אין להם מזהה מפתח.