SipHash

SipHash (קיצור של Short Input Hash) הוא שם כולל למשפחה של פונקציות פסאודו-אקראיות בסגנון ARX, שפותחו ב-2012 על ידי ג'ון פיליפ אמסון ודניאל ברנשטיין. SipHash היא מעין פונקציית גיבוב עם מפתח הממוטבת במיוחד עבור קלט קצר ועוצבה במטרה לתת מענה להתקפות מניעת שירות מסוג Hash Flooding ששטף את הרשת ב-2011.

בניגוד לפונקציית גיבוב כמו SHA-2 הפונקציה SipHash אינה חסינת התנגשויות במובן החזק. כלומר התכונה החזקה ביותר של פונקציית גיבוב קריפטוגרפית היא שיהיה קשה למצוא ערכים כלשהם לא בהכרח בעלי משמעות כלשהי המקיימים <math>H(x)=H(y)</math> כאשר <math>y\ne x</math>. בעוד שהפונקציה SipHash רק מבטיחה שבהינתן קלט <math>x</math> ומפתח סודי <math>k</math> יהיה קשה לנחש את <math>k</math> מתוך פלט הפונקציה <math>\text{SipHash}(x,k)</math> או לחלופין יהיה קשה לנחש מידע כלשהו בנוגע לפלט <math>\text{SipHash}(y,k)</math> עבור כל ערך <math>y\ne x</math>, ללא ידיעת מפתח <math>k</math>.

אפשר להשיג אפקט דומה על ידי כל פונקציית MAC המבוססת על פונקציית גיבוב או פונקציית גיבוב אוניברסלית. הפירוש "אוניברסלית" הוא ששני קלטים שונים כמעט אף פעם לא יפיקו פלט זהה אם המפתח אקראי. יתרונה של SipHash הוא ביעילותה ובפשטותה. לצורך השוואה על מעבד AMD FX-8150 הפונקציה מסוגלת לעבד קלט באורך 16 בתים תוך 140 מחזורי שעון והיא כמעט משתווה בתפוקתה לפונקציית גיבוב לא קריפטוגרפית. מסיבה זו היא מתאימה במיוחד להחליף את הפונקציונליות של טבלאות גיבוב. בדרך כלל מסדי נתונים משתמשים בטבלאות גיבוב לא קריפטוגרפיות כדי לייעל את תהליך אחזור המידע. לדוגמה שרת אינטרנט אמור לתת מענה למיליוני בקשות התחברות במקביל. כדי לעקוב אחרי הבקשות, השרת מנהל רשימה מגובבת של המידע הנכנס. מתקיף שמכיר את פונקציית הגיבוב שהשרת משתמש בה (בדרך כלל המידע הזה ציבורי) צריך רק לשלוח לשרת מאות בקשות התחברות שערך הגיבוב שלהן זהה (התנגשויות מרובות), כאשר מספר רב של בקשות מסוג כזה מתקבלות, הדבר מאט את השרת מאוד ואף משבית אותו כי הוא נאלץ לבצע חיפוש במסד הנתונים בסיבוכיות הגרועה ביותר. פונקציית גיבוב עם מפתח סודי אמורה לסכל התקפה כזו לגמרי, אף על פי שהיא אינה פונקציית גיבוב במובן הרגיל, אולם נוכחותו של המפתח הסודי מקשה על מציאת קלטים אחרים שיניבו פלט זהה (עם אותו מפתח), אלא אם כן יעלה בידי המתקיף לנחש את המפתח.

==רקע==

קוד אימות מסרים (בקיצור MAC) היא פונקציה המקבלת קלט <math>m</math> באורך כלשהו (בדרך כלל ארוך) ומפתח סודי <math>k</math> ומפיקה תג אימות קצר <math>t</math>. תכונתה העיקרית היא שמתקיף המסוגל להשיג עותקים של תגי אימות עבור קלטים שונים (ייתכן אף לפי בחירתו), לא יצליח עם מידע זה לנחש תג אימות עבור כל מסר אחר שטרם ראה את התג שלו. קוד אימות מסרים חשוב מאוד להבטחת אמינות ושלמות תעבורת הרשת ומניעת ניסיונות זדוניים מצד האקרים לשנות ולשבש את המידע למטרות תקיפת מערכת האבטחה. הרעיון הוא שקוד האימות מאפשר לשני הצדדים לוודא שהמידע שעובר ברשת אותנטי, אם חל בו שינוי כלשהו אפילו בסיבית אחת, הדבר יתגלה בהסתברות מאוד גבוהה על ידי פונקציית האימות, במקרה כזה הצדדים מתפטרים מהמידע המשובש ומעבירים הודעת שגיאה מתאימה. לפי מחקרים שנעשו עולה שכשליש מתעבורת האינטרנט כולל חבילות מידע באורך של כחמישים בתים, שליש באורך 256 בתים ושליש כ-1,500 בתים, רבים מהם צריכים אימות. מרבית התקנים הנוכחיים מתמקדים בפונקציות MAC המותאמות לקלט ארוך. יתרה מזו, לרובם אף תקורה גבוהה הנובעת מתהליך הכנה ארוך או ניהול מפתחות מסובך. לדוגמה HMAC מוסיף בין 73 ל-136 בתים למסר מאומת והוא איטי יחסית. זאת משום שהוא אלגוריתם גנרי המסתמך על פונקציית גיבוב, והאחרונה נוקטת בפעולות רבות כדי למנוע התנגשויות, מה שמיותר במקרה זה כי קיים מפתח סודי. אלגוריתמים אחרים כמו VMAC, UMAC או Poly1305 מנצלים צופן בלוקים כמו AES ולמרות היותם יעילים יותר מפני שהם משתמשים בגרסה יותר קלילה של פונקציית גיבוב הנקראת "אוניברסלית", עדיין קיימת תקורה רבה וכן הסתמכות על אלגוריתמים חיצוניים. ב-SipHash נעשה ניסיון להגיע לאותה רמת ביטחון אך בצורה יעילה יותר ללא תקורה מיותרת.

===יתרונות SipHah===
*ביטחון גבוה. ההצעה SipHash-2-4 היא פונקציה פסאודו-אקראית קריפטוגרפית חזקה והפלט שלה בלתי ניתן להבחנה מערך אקראי שנבחר בהתפלגות אחידה. המפתחים מספקים הוכחות.
*מהירות גבוהה. SipHash-2-4 מהירה יותר מכל פונקציית MAC או PRF ידועה ואף מהווה מתחרה ראויה לפונקציות גיבוב לא קריפטוגרפיות.
*גמישות. SipHash משתמשת במפתח הצפנה באורך 128 סיביות. הפונקציה אינה מכילה תהליך "הרחבת מפתח" ולא קיימת התקורה הנובעת מאחסון וטעינה של מפתחות מהזיכרון.
*פשטות. הפונקציה כוללת בסך הכול ארבע פעולות חיבור, ארבע פעולות XOR וארבע הזזות.
*אוטונומיות. לא נעשה שימוש בפרימיטיביים קריפטוגרפיים חיצוניים.
*זיכרון פנימי קצר. הפונקציה צורכת בסך הכול 4 מילים באורך 64 סיביות כל אחת (בסך הכול 32 בתים בזיכרון). 
*אין צורך בניהול מצב פנימי בין הקריאות. האלגוריתם דטרמיניסטי ואין צורך בווקטור אתחול.
*עמידות מפני התקפת ערוץ צדדי. צפנים מודרניים כמו AES בדרך כלל מעודדים שימוש בטבלאות אחזור מטעמי יעילות. מפני שלא קיימות באלגוריתם SipHash טבלאות אחזור סודיות הוא אינו חשוף להתקפות ערוץ צדדי.
*תקורה מינימלית. תג האימות מכיל רק שישה בתים.

===שימושים===
SipHash נמצא במימושי תוכנה ושפות תכנות רבים, בהם:
*Perl (קיים כאופציה בזמן הידור)
*Python (מגרסה 3.4)
*Ruby
*Rust
*OpenDNS
*Haskell
*OpenBSD

==תיאור האלגוריתם==
להלן תיאור משפחת הפונקציות במבנה SipHash-c-d כאשר שני הפרמטרים <math>c</math> ו-<math>d</math> הם שלמים חיוביים המייצגים את מספר החזרות של הפונקציה הפנימית. פונקציית הסבב הפנימית של האלגוריתם מורכבת משני סוגים של פונקציות כיווץ, הראשונה נקראת "פונקציית כיווץ" והשנייה "פונקציית סיום". שתי הפונקציות למעשה זהות, ההפרדה ביניהן היא עניין סמלי. כדי למנוע התקפות מסוימות, לאחר <math>c</math> חזרות של פונקציית הסבב האלגוריתם מבצע חיבור נוסף עם בלוק הקלט ועם קבוע כלשהו ולאחר מכן מבצע <math>d</math> חזרות נוספות של פונקציית הסבב שנקראת בשלב זה פונקציה מסיימת.

בהינתן קלט <math>m</math> שהוא מחרוזת כלשהי באורך <math>b</math> בתים ומפתח סודי <math>k</math> באורך 16 בתים, תפקיד הפונקציה <math>\text{SipHash-c-d}(k,m) </math> הוא להחזיר תג אימות באורך 64 סיביות כדלהלן:

1. אתחול. מאתחלים את המצב הפנימי (Internal state) של האלגוריתם בערכים:
<center>
:<math>v_0 = k_0 \oplus \text{0x736f6d6570736575}</math>
:<math>v_1 = k_1 \oplus \text{0x646f72616e646f6d}</math>
:<math>v_2 = k_0 \oplus \text{0x6c7967656e657261}</math>
:<math>v_3 = k_1 \oplus\text{0x7465646279746573}</math>
</center>
כאשר <math>k_0</math> ו-<math>k_1</math> הם שני החצאים של המפתח <math>k</math>, כל אחד מהם באורך 64 סיביות לפי סדר בתים קטן. לקבועים אילו אין משמעות מתמטית, הם בסך הכול קידוד בבסיס הקסדצימלי של המשפט "some pseudorandomly generated bytes" (ללא רווחים) לפי סדר בתים גדול. כל ערך אחר שונה מאפס טוב ובלבד שיהיה הבדל ניכר בין ערכי <math>v_i</math>. לא נבדק האם אפשר לנצל שלב זה כווקטור אתחול מותאם אישית.

2. כיווץ. תחילה מציגים את <math>b</math> הבתים של הקלט <math>m</math> כמחרוזת המכילה <math>w=\lceil(b+1)/8\rceil</math> מילים באורך 64 סיביות כל אחת, לפי סדר בתים קטן: <math>m_0,m_1,...,m_{w-1}</math>, כאשר הבית האחרון במילה האחרונה <math>m_{w-1}</math> מכיל את קידוד אורך הקלט <math>b</math> בבתים מודולו 256. הקלט חייב להכיל לפחות מילה אחת, אם הקלט נמוך ממילה אחת או שאורכו אינו כפולה של מילים באורך 64 סיביות, מרפדים באפסים כנדרש (למעט בית האחרון). לדוגמה אם מחרוזת הקלט היא "01234abcde" (בבסיס הקסדצימלי) אז <math>b=5</math>, <math>w=1</math> והמילה <math>m_0</math> מכילה את:
<center>
:<math>m_0=\text{0x050000debc4a2301}</math> 
</center>
מחברים באופן איטרטיבי את כל מילות הקלט <math>m_i</math> עם המצב הפנימי כאשר <math>0\le i\le w-1</math>, על ידי:
<center>
:<math>v_3=v_3\oplus m_i</math>
</center>
מבצעים <math>c</math> איטרציות של פונקציית הסבב SipRound (המפורטת בהמשך) ולאחר מכן מבצעים על כל מילות הקלט:
<center>
:<math>v_0=v_0\oplus m_i</math>
</center>
3. סיומת. לאחר עיבוד כל מילות הקלט כמתואר, מבצעים XOR של הקבוע <math>\text{0xff}</math> עם המילה השלישית של המצב הפנימי:
<center>
:<math>v_2=v_2\oplus \text{0xff}</math>
</center>
ואז מבצעים <math>d</math> איטרציות של פונקציית הסבב SipRound ומחזירים את 64 הסיביות שהן תוצאה של:
<center>
:<math>v_0\oplus v_1\oplus v_2\oplus v_3</math> 
</center>

===פונקציית הסבב SipRound===
מבנה ARX של הפונקציה הפנימית SipRound
הטרנספורמציה SipRound המומחשת בתרשים משמאל מבצעת את הפעולות הבאות על המצב הפנימי של האלגוריתם:
{| dir="ltr" align="center"
|-
| width=150 | <math>v_0=v_0\boxplus v_1</math> || <math>v_2=v_2\boxplus v_3</math>
|-
| <math>v_1=v_1\lll 13</math> || <math>v_3=v_3\lll 16</math>
|-
| <math>v_1=v_1 \oplus v_0</math> || <math>v_3=v_3 \oplus v_2</math>
|-
| <math>v_0=v_0\lll 32</math> || 
|-
| <math>v_2=v_2\boxplus v_1</math> || <math>v_0=v_0\boxplus v_3</math>
|-
| <math>v_1=v_1\lll 17</math> || <math>v_3=v_3\lll 21</math>
|-
| <math>v_1=v_1\oplus v_2</math> || <math>v_3=v_3\oplus v_0</math>
|-
| <math>v_2=v_2\lll 32</math> || 
|}
כאשר הסימן <math>\oplus</math> הוא XOR, הסימן <math>\lll</math> מייצג הזזה מעגלית של סיביות שמאלה, בגבולות מילה באורך 64 סיביות, לפי מספר הפוזיציות המצוין לאחריו. הסימן <math>\boxplus</math> מייצג חיבור מודולו <math>2^{64}</math>. היסטי ההזזות נבחרו באופן אמפירי, תוך מתן דגש על ערכים המיושרים לגבולות בתים או מילים להאצת ביצועים ומניעת עיכובים בצינור העיבוד.

==ביטחון ויעילות==
SipHash-c-d כאשר <math>c\ge 2</math> וכן <math>d\ge 4</math> אמורה לספק לדעת המפתחים מקסימום ביטחון כפונקציה פסאודו אקראית. הביטחון הגבוה ביותר שניתן להשיג עם כל פונקציית PRF לאימות מסרים. לשימוש רגיל ההמלצה שלהם היא SipHash-2-4 (כלומר שני סבבים של הפונקציה הפנימית ואחריהן ארבעה סבבים של הפונקציה המסיימת) ולשימוש שמרני יותר SipHash-4-8 שהיא איטית פי שניים מהראשונה. מהיבט תאורטי, ביטחון פונקציית PRF סטנדרטית מניח שלמתקיף גישה לפלט הפונקציה על קלטים שנבחרו על ידו באופן אדפטיבי (שזהו המודל המחמיר ביותר), אך הוא אינו רשאי לראות את המפתח, או לצורך הדיון כאן, אין ביכולתו להשיג גישה ל"מפתחות קשורים", "מפתחות ידועים" או "מפתחות נבחרים" (כל אילו מודלים של ביטחון שמניחים שלמתקיף שליטה מסוימת גם אם חלקית ועקיפה על חומר המפתח הסודי). במצב זה ביטחון הפונקציה תלוי באורך המפתח. היות שהמפתח באורך 128 סיביות, אם המתקיף מבצע <math>s</math> שאילתות (ניסיונות אימות) סיכוייו לנחש את המפתח שואפים ל-<math>2^{s-128}</math>. בהנחה שלא קיימת דרך טובה מכוח גס. הביטחון המושג מוגבל גם בגלל אורך הפלט. כלומר כאשר משתמשים ב-SipHash כקוד אימות, אם המתקיף מסוגל לבצע <math>2^s</math> שאילתות הוא יצליח לזייף תג אימות בהסתברות של <math>2^{s-64}</math>. חשוב להדגיש ש-SipHash אינה ולא הייתה מיועדת להיות פונקציה חסינת התנגשויות.

SipHash אימצה טכניקות מהפונקציות BLAKE, Skein ו-JH שהיו מועמדות תקן הגיבוב SHA-3 ועלו לגמר. פונקציות הגיבוב המנויות עברו ביקורת מדוקדקת של מומחים והוצהרו נכון להיום כבטוחות ולכן הסתמכות על חלקים מהם מקלה על ניתוח ביטחון האלגוריתם. מבנה הפונקציה הפנימית הוא ARX (קיצור של חיבור/הזזה/XOR) שהוכח כבטוח ונמצא בשימוש צפנים מודרניים כמו Salsa20. מ-Blake נלקחו המינימליות והפשטות מ-Skein נלקח תהליך הערבוב MIX ומ-JH אומץ רעיון טעינת הקלט לפני ואחרי כל בלוק. היתרון הוא שבניגוד לסגנון הספוג של SHA-3, אפשר לספוג חלק מהקלט ללא ירידה בביטחון מקור ראשון (Preimage).

המפתחים מציינים כי האלגוריתם נבדק נוכח מספר גדול של התקפות קריפטואנליטיות מודרניות ונמצא בטוח לטענתם. כוח גס בסיבוכיות של <math>2^{127}</math> אינו מעשי. ניחוש הזיכרון הפנימי דורש בממוצע <math>d2^{191}</math> פעולות ולכן אינו מעשי. המחברים ניסו התקפות דיפרנציאליות שונות וכן התקפות קריפטוגרפיות אחרות כמו התקפת נקודות שבת וכן התקפת Cube והתקפת Rebound.

בפונקציית הסבב של SipHash יש בסך הכול 14 פעולות על משתנים באורך 64 סיביות. לכן ב-SipHash-2-4 יש בסך הכול 30 פעולות עבור כל שמונה בתים של קלט, 3.75 פעולות פר בית בממוצע. מעבד ארבע ליבות מבזבז כמחזור שעון אחד עבור כל בית של קלט. האלגוריתם נבדק על מספר מעבדים ומערכות הפעלה, הן בתוכנה והן בחומרה והביצועים השוו לפונקציות גיבוב לא קריפטוגרפיות כמו CityHash של גוגל וכן SpookyHash ו-MD5. לפי התוצאות SipHash קרובה בביצועיה לפונקציות הגיבוב המהירות ביותר הקיימות כיום. לצורך השוואה על מעבד Xeon E5620 של אינטל, ליבת Core i7, קלט באורך 8 בתים עובד תוך 92 מחזורי שעון בערך.

===קוד לדוגמה של SipHash-2-4===
<syntaxhighlight lang="CPP">
#define U8TO32_LE(p) \
 (((uint32_t)((p)[0]) ) | ((uint32_t)((p)[1]) << 8) | \
 ((uint32_t)((p)[2]) << 16) | ((uint32_t)((p)[3]) << 24)) \

#define U8TO64_LE(p) \
 ((uint64_t)U8TO32_LE(p) | ((uint64_t)U8TO32_LE((p) + 4)) << 32 )

#define ROTL64(v, s) \
 ((v) << (s)) | ((v) >> (64 - (s)))

#define ROTL64_TO(v, s) ((v) = ROTL64((v), (s)))

#define ADD64_TO(v, s) ((v) += (s))
#define XOR64_TO(v, s) ((v) ^= (s))
#define XOR64_INT(v, x) ((v) ^= (x))

static const char sip_init_state_bin[] = "uespemos""modnarod""arenegyl""setybdet";
#define sip_init_state (*(uint64_t (*)[4])sip_init_state_bin)

#define SIP_COMPRESS(v0, v1, v2, v3) \
do { \
 ADD64_TO((v0), (v1)); \
 ADD64_TO((v2), (v3)); \
 ROTL64_TO((v1), 13); \
 ROTL64_TO((v3), 16); \
 XOR64_TO((v1), (v0)); \
 XOR64_TO((v3), (v2)); \
 ROTL64_TO((v0), 32); \
 ADD64_TO((v2), (v1)); \
 ADD64_TO((v0), (v3)); \
 ROTL64_TO((v1), 17); \
 ROTL64_TO((v3), 21); \
 XOR64_TO((v1), (v2)); \
 XOR64_TO((v3), (v0)); \
 ROTL64_TO((v2), 32); \
} while(0)

#define SIP_2_ROUND(m, v0, v1, v2, v3) \
do { \
 XOR64_TO((v3), (m)); \
 SIP_COMPRESS(v0, v1, v2, v3); \
 SIP_COMPRESS(v0, v1, v2, v3); \
 XOR64_TO((v0), (m)); \
} while (0)

uint64_t sip_hash24(uint8_t key[16], uint8_t *data, size_t len)
{
 uint64_t k0, k1;
 uint64_t v0, v1, v2, v3;
 uint64_t m, last;
 uint8_t *end = data + len - (len % sizeof(uint64_t));

 k0 = U8TO64_LE(key);
 k1 = U8TO64_LE(key + sizeof(uint64_t));

 v0 = k0; XOR64_TO(v0, sip_init_state[0]);
 v1 = k1; XOR64_TO(v1, sip_init_state[1]);
 v2 = k0; XOR64_TO(v2, sip_init_state[2]);
 v3 = k1; XOR64_TO(v3, sip_init_state[3]);

 for (; data != end; data += sizeof(uint64_t)) {
 m = U8TO64_LE(data);
 SIP_2_ROUND(m, v0, v1, v2, v3);
 }

 last = len << 56;
 #define OR_BYTE(n) (last |= ((uint64_t) end[n]) << ((n) * 8))

 switch (len % sizeof(uint64_t)) {
 case 7: OR_BYTE(6);
 case 6: OR_BYTE(5);
 case 5: OR_BYTE(4);
 case 4: OR_BYTE(3);
 case 3: OR_BYTE(2);
 case 2: OR_BYTE(1);
 case 1: OR_BYTE(0); 
 break;
 case 0: break;
 }

 SIP_2_ROUND(last, v0, v1, v2, v3);

 XOR64_INT(v2, 0xff);

 SIP_COMPRESS(v0, v1, v2, v3);
 SIP_COMPRESS(v0, v1, v2, v3);
 SIP_COMPRESS(v0, v1, v2, v3);
 SIP_COMPRESS(v0, v1, v2, v3);

 XOR64_TO(v0, v1);
 XOR64_TO(v0, v2);
 XOR64_TO(v0, v3);
 return v0;
}
</syntaxhighlight>

==ראו גם==
* פונקציית גיבוב קריפטוגרפית
* קוד אימות מסרים
* טבלת גיבוב
* SHA-3

==קישורים חיצוניים==
* 

==הערות שוליים==

קטגוריה:פונקציות גיבוב קריפטוגרפיות