מחולל HMAC
מחשב HMAC-SHA-1, SHA-256, SHA-384 ו־SHA-512 מהודעה וממפתח, ומזהה מאיזה מהם הגיעה חתימה נתונה.
הזינו מפתח כדי לחשב את החתימות.
מה הכלי הזה עושה
HMAC הופך הודעה וסוד משותף לחתימה קצרה. כל מי שמחזיק באותו סוד יכול לחשב אותה מחדש ולראות אם ההודעה הגיעה ללא שינוי ואם היא הגיעה ממישהו שגם הוא יודע את הסוד. הכלי הזה מחשב את כל ארבע הווריאציות הנפוצות בבת אחת, ובהינתן חתימה שקיבלתם — אומר לכם איזו מהן ייצרה אותה.
הכיוון השני הוא בדרך כלל הסיבה שאנשים מגיעים. אימות webhook נכשל, והשאלה האמיתית אינה «האם החתימה תקפה» אלא «איזה מבין כמה דברים סבירים אני עושה אחרת מהשולח». קידומת מסוג sha256= לפני חתימה שמדביקים מזוהה ומוסרת, כך שאפשר להדביק את ערך הכותרת בדיוק כפי שהגיע.
המפתח הוא המקום שבו זה משתבש
HMAC חותם בתים עם בתים. ההודעה בדרך כלל ברורה — גוף הבקשה הגולמי — אבל המפתח כמעט אף פעם לא, מפני שסוד מגיע כמחרוזת ומחרוזת אינה בתים עד שמחליטים איך לקרוא אותה:
a3f2 -> 61 33 66 32 4 bytes כטקסט a3f2 -> a3 f2 2 bytes כ־hex
שתי הקריאות לגיטימיות, שתיהן מייצרות חתימה תקינה לחלוטין, ואין בין שתי החתימות שום דבר משותף. שום דבר אינו מזהיר אתכם: אין שגיאה, אין תלונה על אורך, רק ערך שאינו תואם למה שהשולח חישב. לכן קידוד המפתח כאן הוא בחירה גלויה ולא ניחוש — כשחתימה לא תואמת, זה הדבר הראשון להחליף.
כאומדן גס: סוד עם קידומת כמו whsec_ או רצף של אותיות וספרות בשתי רישיות מיועד בדרך כלל כטקסט; מחרוזת של בדיוק 32 או 64 תווים שמשתמשת רק ב־0-9 וב־a-f היא בדרך כלל hex; ומחרוזת שנגמרת ב־= היא כמעט בוודאות base64. אבל בדקו בתיעוד של השולח ולא לפי הצורה, כי הצורה אינה הוכחה.
ההודעה חייבת להיות הבתים המדויקים
החצי השני של אי-התאמה הוא ההודעה. HMAC מוגדר מעל בתים, ולכן כל דבר שמשנה את הבתים משנה את החתימה לחלוטין — אין ניקוד חלקי ואין כמעט-פגיעה.
- סריאליזציה מחדש של JSON. ניתוח גוף ובנייתו מחדש עלולים לשנות סדר מפתחות, רווחים או ייצוג מספרים. חתמו ואמתו את הגוף הגולמי שקיבלתם, לעולם לא עותק שעבר הלוך ושוב.
- מעבר שורה בסוף. חלק מהכלים מוסיפים אחד כששומרים את הגוף לקובץ; זה בית, וזה משנה הכול.
- קידוד תווים. גוף שמכיל טקסט שאינו ASCII חייב להיקרא באותו קידוד בשני הצדדים — בפועל UTF-8.
- דחיסה או middleware. אם משהו לפני המטפל שלכם מפרק דחיסה או משכתב את הגוף, אמתו לפני שזה קורה ולא אחרי.
גם ספקים רבים אינם חותמים על הגוף לבדו. Stripe חותם על חותמת זמן ועל הגוף מחוברים בנקודה; AWS חותם על בקשה קנונית שנבנית ממתודה, נתיב, כותרות וגיבוב של המטען. אם האימות נכשל מול הגוף הפשוט, המחרוזת החתומה כנראה אינה הגוף הפשוט — זה מתועד אצל השולח ושווה לקרוא לפני שממשיכים לנפות.
למה SHA-1 מוצע כאן ולא בכלי ההאש
כלי ההאש מסמן את SHA-1 כשבור. הכלי הזה מציג HMAC-SHA-1 בלי אזהרה, וזה מכוון ולא פספוס.
SHA-1 אינו שמיש לחתימות ולתעודות מפני שאפשר לבנות התנגשויות: אפשר לגרום לשני מסמכים שונים לחלוק תקציר. HMAC אינו תלוי בתכונה הזו. אבטחתו נשענת על המפתח הסודי, והבנייה — גיבוב ההודעה פעמיים כשהמפתח מעורבב בשתיהן — מחזיקה מעמד גם כשהגיבוב הבסיסי חלש להתנגשויות. HMAC-SHA-1 נותר תקין ועדיין זה מה ש־OAuth 1.0a וחתימת AWS ישנה משתמשים בו, ולכן כלי שהיה מסרב לחשב אותו היה פשוט פחות שימושי בלי להיות בטוח יותר.
לכל דבר חדש, SHA-256 היא ברירת המחדל ההגיונית. תקצירים ארוכים יותר אינם חזקים יותר באופן משמעותי כאן — תקרת האבטחה היא המפתח ולא אורך התקציר — ולכן שווה לבחור ב־SHA-384 וב־SHA-512 רק כשמשהו שאתם חייבים לעבוד מולו מבקש אותם.
השוואת חתימות בבטחה
העמוד הזה משווה בהשוואת מחרוזות רגילה, וזה בסדר כאן: אתם מחזיקים את המפתח בעצמכם, אז אין מה לדלוף. בשרת שמאמת בקשה נכנסת זה לא בסדר. השוואה רגילה נעצרת בבית הראשון שנבדל, ולכן הזמן שהיא לוקחת מסגיר כמה מהניחוש היה נכון, ותוקף שיכול לשלוח הרבה בקשות יכול לשחזר חתימה תקפה בית אחר בית.
השתמשו בהשוואה בזמן קבוע שהפלטפורמה שלכם מספקת — crypto.timingSafeEqual ב־Node, hmac.compare_digest ב־Python, hash_equals ב־PHP — על הבתים הגולמיים ולא על מחרוזות hex. לצד זה, השוו מול חתימה שחישבתם בעצמכם ולא סמכו על אלגוריתם ששמו מופיע בבקשה, ודחו הודעה שחותמת הזמן שלה רחוקה מעכשיו כדי שחתימה תקפה ישנה לא תשוחזר.
HMAC אינו האש ואינו חתימה דיגיטלית
שלושה דברים מתבלבלים כאן, וההבדל חשוב למה שאפשר לטעון אחר כך:
- האש לוקח הודעה ומייצר תקציר. כל אחד יכול לחשב אותו, ולכן הוא מוכיח שההודעה לא השתנתה בטעות — לא שהיא הגיעה ממישהו מסוים.
- HMAC לוקח הודעה וסוד משותף. שני הצדדים מחזיקים באותו מפתח, ולכן הוא מוכיח שהשולח ידע את הסוד. הוא אינו יכול להוכיח מי מהצדדים שלח, כי כל אחד מהם יכול היה לייצר אותו.
- חתימה דיגיטלית משתמשת במפתח פרטי שרק השולח מחזיק, ובמפתח ציבורי שכל אחד יכול לאמת איתו. זה מה שנותן אי-הכחשה: השולח אינו יכול להתכחש לה אחר כך.
לכן HMAC הוא הכלי הנכון בין שתי מערכות שכבר חולקות סוד — webhooks, ממשקי API פנימיים, אסימוני הפעלה — והלא נכון כשצריך להוכיח לצד שלישי מי שלח משהו. שימו לב גם שהוא מאמת אך אינו מסתיר: ההודעה נוסעת גלויה, ו־HMAC אינו אומר דבר על סודיות.
אימות חתימת webhook של Stripe
Stripe הוא המתכון היחיד כאן שאינו הדבקה, ובדרך כלל זה זה שאנשים מגיעים אליו אחרי שכבר ניסו. הכותרת Stripe-Signature היא רשימת רכיבים מופרדים בפסיקים ולא חתימה, והמחרוזת ש־Stripe חתם עליה אינה גוף הבקשה לבדו. התיעוד של Stripe עצמו שובר את הכותרת לשורות לשם הקריאות; כותרת אמיתית מגיעה בשורה אחת.
Stripe-Signature: t=1492774577, v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd, v0=6ffbb59b2300aae63f272406069a9788598b792a944a07aba816edb039989a39
- הודעה: הערך שברכיב t=, אחריו נקודה, ואחריה גוף הבקשה הגולמי בדיוק כפי שהגיע. Stripe קורא למחרוזת הזו המטען החתום, וזו כל הסיבה שחתימת Stripe לעולם אינה תואמת לגוף לבדו.
- מפתח: סוד החתימה של אותה נקודת קצה בלבד, שלם, כולל הקידומת whsec_ שלו. זה אינו מפתח ה־API שלכם, ואינו הסוד של נקודת קצה אחרת — אותה כתובת שנרשמה פעמיים מחזיקה שני סודות.
- קידוד המפתח: טקסט. סוד חתימה הוא מחרוזת ש־Stripe בחר, ואף שהוא נראה מקרי הוא אינו hex ואינו base64; קריאה שלו כאחד מהם נותנת בתים אחרים וחתימה שאינה תואמת לשום דבר.
- חתימה לבדיקה: רכיב ה־v1= לבדו, לא הכותרת כולה. v1= נדבק כמו שהוא, מפני שהתווית הזו מוסרת בדיוק כמו sha256= — אבל הכותרת השלמה לא, מפני שה־t= שבתחילתה נלקח כתווית ומה שבא אחריו אינו חתימה בכלל.
שלושה דברים נוספים על הכותרת הזו שווים ידיעה לפני שמנפים משהו אחר. התעלמו מכל סכימה שאינה v1: רכיב ה־v0 שנשלח באירועי בדיקה אינו חתימה אמיתית במכוון, וההתעלמות מהשאר היא מה שמונע שסכימה חלשה תיכפה עליכם. בזמן שסוד של נקודת קצה מתחלף שני הסודות חיים עד יממה והכותרת נושאת רכיב v1 אחד לכל סוד, שרק אחד מהם יתאים. וכל ניסיון מסירה נחתם מחדש, ולכן ניסיון חוזר אינו נושא את החתימה של הראשון — חותמת הזמן שבתוך המחרוזת החתומה היא גם מה שהופך דחייה של חתימה ישנה להגנה אמיתית ולא למחווה, מפני שאי אפשר לשנות אותה בלי לשבור את החתימה.
אימות חתימת webhook של GitHub
GitHub הוא ההדבקה שהכלי הזה נבנה סביבה, והמתכון היחיד כאן שאפשר לבדוק מקצה לקצה, מפני ש־GitHub מפרסם דוגמה מלאה. החתימה מגיעה בכותרת X-Hub-Signature-256 כתווית sha256= ואחריה תקציר hex, והתווית הזו מזוהה ומוסרת כאן, כך שערך הכותרת נכנס בדיוק כפי שהגיע.
X-Hub-Signature-256: sha256=757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17
- הודעה: גוף הבקשה הגולמי, בית אחר בית. הדוגמה ש־GitHub מפרסם חותמת על הגוף Hello, World! ללא שום דבר אחריו — בלי מעבר שורה בסוף.
- מפתח: הסוד שהקלדתם בהגדרות ה־webhook, בדיוק כפי שהקלדתם אותו. הדוגמה ש־GitHub מפרסם משתמשת ב־It's a Secret to Everybody.
- קידוד המפתח: טקסט. סוד של GitHub הוא מחרוזת שבחרתם, ולכן הוא לעולם אינו hex — גם סוד שהוא כולו ספרות hex נקרא כתווים שהקלדתם.
- חתימה לבדיקה: ערך הכותרת כולו, כולל sha256=. hex מושווה בלי תלות ברישיות, ולכן לא משנה באיזו צורה היומנים שלכם מדפיסים אותו.
שלושת הערכים האלה יחד הם בדיקת השפיות המהירה ביותר שיש: הדביקו אותם והעמוד הזה ידווח על התאמה ל־HMAC-SHA-256 ב־hex, מה שאומר שגם המחשבון וגם ההבנה שלכם את המתכון נכונים לפני שתנסו אותם על מסירה אמיתית. GitHub שולח גם כותרת ותיקה יותר, X-Hub-Signature, שהיא HMAC-SHA-1 על אותו גוף ונשמרת למטרות תאימות בלבד; מפני שהעמוד הזה מחשב את כל ארבעת התקצירים בבת אחת, חתימה מכל אחת מהכותרות מזוהה בלי שתצטרכו לומר מאיזו. הדבר האחד שהוא אינו יכול להציל הוא גוף שהגיע כמשהו שאינו הבתים ש־GitHub שלח — מטענים יכולים לשאת תווים מחוץ ל־ASCII, ושני הצדדים חייבים לקרוא אותם כ־UTF-8.
אימות חתימת webhook של Shopify
Shopify כותב את התקציר שלו ב־base64 ולא ב־hex, וזה ההבדל היחיד שחשוב כאן: אותם 32 בתים הופכים ל־44 תווים שנגמרים בסימן שווה אחד, והתו הזה הוא ריפוד ולא תווית, ולכן שום דבר אינו מוסר מתחילתו.
X-Shopify-Hmac-SHA256: dXEH6g6yUJ/CESIczphLijdXC211hsIsRvQ3nIsEPhc=
- הודעה: גוף הבקשה הגולמי, בית אחר בית כפי שנמסר. האזהרה של Shopify עצמו היא על middleware שמנתח גוף — אמתו קודם ונתחו אחר כך, מפני שמנתח שכבר הפך את הגוף לאובייקט זרק את הבתים.
- מפתח: סוד הלקוח של האפליקציה שה־webhook שייך לה. לא אסימון הגישה שלה, ולא מפתח ה־API שיושב לידו באותו פאנל.
- קידוד המפתח: טקסט. כמו בשני האחרים, הסוד הוא מחרוזת ולא קידוד של בתים.
- חתימה לבדיקה: ערך הכותרת כולו. הוא נדבק כמו שהוא, ומפני שגם hex וגם base64 נבדקים מול כל תקציר, הקידוד שהעמוד הזה מדווח עליו הוא בעצמו התשובה לשאלה באיזו צורה השולח כתב אותו.
הערך בבלוק שלמעלה נמצא שם כדי להראות את הצורה: אלה אותם 32 בתים כמו בדוגמה של GitHub, כתובים ב־base64 במקום ב־hex. שווה לראות את השניים זה מול זה, מפני שזה כל תוכן ההבדל בין שתי הכותרות — תקציר אחד, שתי צורות כתיבה — וזו הסיבה שהעמוד הזה מציין את הקידוד לצד האלגוריתם ולא רק אומר לכם שמשהו התאים.
שאלות נפוצות
- החתימה שלי לא תואמת. מה לבדוק קודם?
- את קידוד המפתח, ואחר כך את בתי ההודעה. סוד שנראה כמו hex מיועד לעיתים קרובות כ־hex ולא כטקסט, ושניהם מייצרים חתימות שונות לגמרי בלי שום שגיאה. אחר כך ודאו שאתם חותמים על הגוף הגולמי בדיוק כפי שהתקבל ולא על עותק שעבר סריאליזציה מחדש.
- למה SHA-1 מוצע כאן כשכלי ההאש אומר שהוא שבור?
- מפני ש־HMAC אינו נשען על עמידות להתנגשויות, שהיא התכונה ש־SHA-1 איבד. אבטחתו מגיעה מהמפתח. HMAC-SHA-1 עדיין תקין ועדיין בשימוש ב־OAuth 1.0a ובחתימת AWS ישנה, אף ש־SHA-256 היא ברירת המחדל הנכונה לכל דבר חדש.
- האם תקציר ארוך יותר בטוח יותר?
- לא באופן משמעותי. חוזק HMAC חסום על ידי הסוד ולא על ידי אורך התקציר, ולכן SHA-512 אינה טובה פי ארבעה מ־SHA-256. בחרו את זו שהצד השני מצפה לה.
- מה ההבדל בין HMAC לחתימה דיגיטלית?
- HMAC משתמש בסוד אחד ששני הצדדים יודעים, ולכן הוא מוכיח שהשולח ידע את הסוד אבל לא איזה צד שלח. חתימה דיגיטלית משתמשת במפתח פרטי שרק השולח מחזיק, ולכן צד שלישי יכול לאמת אותה והשולח אינו יכול להתכחש לה. אם אתם צריכים את התכונה האחרונה, HMAC הוא הכלי הלא נכון.
- האם להשוות חתימות עם ===?
- לא בשרת. השוואה רגילה חוזרת ברגע שבתים נבדלים, והתזמון הזה מסגיר כמה מחתימה מנוחשת היה נכון. השתמשו בהשוואה בזמן קבוע של הפלטפורמה על הבתים הגולמיים. בעמוד הזה זה לא משנה, כי אתם כבר מחזיקים את המפתח.
- האם המפתח שלי נשלח לאנשהו?
- לא. הכול מחושב בדפדפן שלכם עם WebCrypto; ההודעה והמפתח לעולם אינם עוזבים את המכשיר.
- איך מאמתים חתימת webhook של Stripe?
- חברו שלושה דברים למחרוזת אחת — חותמת הזמן מרכיב ה־t= שבכותרת, נקודה, וגוף הבקשה הגולמי — וחתמו עליה עם סוד החתימה של נקודת הקצה כשהוא נקרא כטקסט, ואז השוו את התוצאה לרכיב ה־v1= שבכותרת. הפרק שלמעלה עובר על זה שדה אחר שדה. מה שמפיל את רוב האנשים הוא השלב הראשון: הגוף לבדו אינו מה ש־Stripe חתם עליו.
- איך מאמתים חתימת webhook של GitHub?
- חתמו על גוף הבקשה הגולמי עם סוד ה־webhook כשהוא נקרא כטקסט, ב־SHA-256, והשוו את תקציר ה־hex לכותרת X-Hub-Signature-256. התווית sha256= יכולה להישאר כשמדביקים אותה כאן. GitHub מפרסם דוגמה מלאה — הגוף Hello, World! עם הסוד It's a Secret to Everybody — והיא הדרך המהירה ביותר לבדוק את ההבנה שלכם את המתכון לפני שמנסים אותו על מסירה אמיתית.
- איך מאמתים חתימת webhook של Shopify?
- חתמו על גוף הבקשה הגולמי עם סוד הלקוח של האפליקציה שה־webhook שייך לה, כשהוא נקרא כטקסט, ב־SHA-256, והשוו את תקציר ה־base64 לכותרת X-Shopify-Hmac-SHA256. ערך הכותרת כולו נדבק כמו שהוא — סימן השווה בסוף הוא ריפוד של base64. כשזה לא תואם, הסיבה הרגילה היא middleware שניתח את הגוף לפני שהמטפל שלכם ראה אותו בכלל.
- איך מאמתים חתימת webhook של כל ספק אחר?
- ארבע שאלות מכריעות את זה, ולתיעוד של השולח יש את כל ארבען: איזו כותרת נושאת את החתימה, על איזו מחרוזת באמת חותמים, איך אמור להיקרא הסוד, והאם התקציר הוא hex או base64. מלאו אותן בשדות שלמעלה. אם זה עדיין לא תואם, התשובה היא כמעט תמיד השנייה — ספקים רבים מאוד חותמים על הגוף עם משהו נוסף מחובר אליו ולא על הגוף לבדו.
כלים קשורים
- מנפה קודי TOTP / 2FA
הפקה וניפוי של קודי 2FA, עם הגזירה המלאה.
- Bcrypt
צרו גיבוב bcrypt, או בדקו סיסמה מול גיבוב קיים.
- מפענח / מאמת JWT
פענוח ואימות של JSON Web Tokens — חתימה ו־claims.
- מחולל גיבובים
MD5, SHA-1, SHA-256, SHA-384 ו־SHA-512 בבת אחת.