קריפטוגרפיה
הצפנה, קידוד וגיבוב: מה כל אחד מהם עושה
הפרדה בין קידוד, הצפנה וגיבוב במעבדות עם Base64, SHA-256 ו-OpenSSL. למה Base64 אינו הגנה ולמה hash הוא חד־כיווני.
מפתחות ציבוריים ופרטיים מאפס: מה חתימה מוכיחה, למה המפתח הפרטי נשאר סודי, ומעבדת Ed25519 שבה קובץ ששונה נכשל באימות.
קריפטוגרפיה של מפתח ציבורי פותרת בעיה שסודות משותפים לא פותרים. שני צדדים שמעולם לא נפגשו יכולים להסכים על סוד ברשת שכל אחד יכול לקרוא. מישהו יכול גם להוכיח שהוא כתב קובץ, בלי לחלוק סוד עם מי שבודק אותו. שני הדברים עובדים כי לזוג מפתחות יש שני חצאים שקשורים זה לזה מתמטית. את החצי הציבורי אפשר לשתף בחופשיות. החצי הפרטי נשאר אצל הבעלים.
אם השתמשת ב-TLS או ב-SSH, כבר הסתמכת על הרעיון הזה. המנעול בדפדפן ובקשת טביעת האצבע בלקוח ה-SSH תלויים שניהם במפתחות ציבוריים. המאמר הזה מסביר את הרעיון שמתחת לכלים האלה. יוצרים זוג מפתחות Ed25519 עם OpenSSL, חותמים על קובץ, מאמתים את החתימה, ואז משנים שורה אחת בקובץ ורואים את האימות נכשל. גרסאות תוכנה חתומות משתמשות באותו מנגנון, והוא הבסיס למפתחות ה-SSH שבמאמר הבא.
זוג מפתחות הוא שני ערכים שנוצרים יחד. המפתח הפרטי סודי. את המפתח הציבורי אפשר להעתיק לקובץ, לשלוח באימייל או להדביק בקונפיגורציה של שרת. השניים קשורים במתמטיקה שעובדת בכיוון אחד. קל לגזור את המפתח הציבורי מהפרטי. שחזור המפתח הפרטי מהציבורי תוכנן להיות בלתי אפשרי מעשית.
קריפטוגרפים קוראים לזה קריפטוגרפיה א־סימטרית, כי שני הצדדים משתמשים במפתחות שונים. הצפנה סימטרית, כמו AES, משתמשת בסוד אחד לשני הכיוונים. שיטה א־סימטרית מחלקת את העבודה: מפתח אחד מבצע פעולה, והאחר הופך אותה או בודק אותה.
תמונה גסה עוזרת. המפתח הציבורי דומה לחריץ של תיבת דואר. כל אחד יכול להכניס דרכו מכתב, ורק לבעלים יש את המפתח שפותח את התיבה. בחתימות התפקידים מתהפכים. הבעלים מטביע על מסמך חותם שרק הוא יכול ליצור, וכל מי שמחזיק בדוגמת הייחוס הציבורית יכול לבדוק את החותם. התמונה לא מושלמת, כי מפתחות הם מספרים ולא פליז. מה שכדאי לזכור הוא הכיוון של כל פעולה.
אנשים מבלבלים ביניהן כל הזמן, אז כדאי להפריד ביניהן.
הצפנה למפתח ציבורי מאפשרת לכל מי שמחזיק במפתח הציבורי שלך ליצור הודעה שרק המפתח הפרטי שלך יכול לפתוח. המטרה היא סודיות. כל אחד יכול לכתוב לך, ורק לך יש דרך לקרוא את מה שנכתב.
חתימה היא ערך שמי שמחזיק במפתח הפרטי מחשב על נתונים מסוימים. כל מי שיש לו את המפתח הציבורי יכול לבדוק אותה. המטרה היא אותנטיות ושלמות: הנתונים הגיעו ממי שמחזיק במפתח, ואיש לא שינה אותם אחרי החתימה. חתימה לא מסתירה כלום. קובץ חתום נשאר קריא לגמרי לכל מי שמחזיק בו.
Ed25519, האלגוריתם במעבדה הזאת, הוא שיטת חתימה בלבד. הוא לא יכול להצפין הודעה. הצפנה במפתח ציבורי משתמשת במבנים אחרים, כמו RSA-OAEP, או בהסכמת מפתח על עקום אליפטי ואחריה צופן סימטרי. לכן "מפתח ציבורי" הוא שם של סוג מפתח, ולא של עבודה אחת. קודם לומדים את העבודה, ואז את האלגוריתם.
TLS משתמש בשתי העבודות. בלחיצת יד של TLS 1.3, השרת חותם על נתונים עם המפתח הפרטי שתואם למפתח הציבורי בתעודה שלו, וכך מראה שהוא שולט במפתח הזה. לאחר מכן הסכמת מפתח נפרדת גוזרת מפתחות סימטריים שמצפינים את הסשן. SSH עובד באותה חלוקה. מפתח מארח חותם על חלק מלחיצת היד, ומפתח משתמש חותם על אתגר אימות. אחרי שמפרידים בין שתי העבודות, קל יותר לקרוא את הפרוטוקולים.
המפתח הפרטי הוא ההוכחה. מי שמחזיק בו יכול לייצר חתימות שמאומתות כזהות הזאת, ולפענח הודעות שהוצפנו אליה. מי שמאמת לא רואה את הפנים או את השם שלך. הוא בודק חתימה מול המפתח הציבורי שהוא סומך עליו. אם מישהו מעתיק את המפתח הפרטי שלך, כל מאמת שסומך על המפתח הציבורי שלך מקבל מעכשיו את העותק כאילו היה הזהות שלך.
מכאן נובעים ארבעה כללים מעשיים:
שיתוף המפתח הציבורי בטוח מעצם התכנון. הבעיה הקשה יותר נמצאת בכיוון השני. מאמת צריך לדעת שמפתח ציבורי באמת שייך לאדם או לפרויקט שהוא נושא את שמו. רשויות תעודות, מפתחות מקובעים לחתימת גרסאות וטביעות אצבע של מארחי SSH קיימים כדי לפתור בדיוק את הבעיה הזאת. חתימה תקפה מוכיחה שמי שמחזיק במפתח חתם על הבתים האלה. היא לא מוכיחה שהמחזיק במפתח הוא מי שציפית לו.
הרצף של גרסה חתומה נראה כך:
שני פרטים חשובים יותר ממה שנדמה. ראשית, חתימה מכסה בתים מדויקים. רווח אחד נוסף, סיומת שורה אחרת או ארכיון שנארז מחדש הם הודעה אחרת, והאימות נכשל. שנית, חתימת Ed25519 לפי RFC 8032 היא דטרמיניסטית. אותו מפתח ואותה הודעה נותנים תמיד אותה חתימה, כך שהחתימה לא צריכה מספר אקראי חדש בכל פעם. זה מסיר מקור נפוץ אחד לכשל. FIPS 186-5 מונה את EdDSA יחד עם ECDSA ו-RSA כאלגוריתמי חתימה דיגיטלית מאושרים.
צריך OpenSSL 3.x. הפלט במעבדה הזאת מגיע מ-Windows 11 עם Git Bash ו-OpenSSL 3.2.4. קודם בודקים את הגרסה עם openssl version. גרסאות ישנות יותר עלולות לא לקבל את אותן אפשרויות.
משתמשים ב-Git Bash, ב-shell של Linux או ב-Terminal של macOS. Windows PowerShell 5.1 כותב UTF-16 בהפניה עם >, וזה משנה את הבתים שעליהם חותמים. את המעבדה מריצים בתיקייה ריקה על המחשב שלך. היא לא פונה לשום שרת.
openssl genpkey -algorithm ed25519 -out priv.pem
openssl pkey -in priv.pem -pubout -out pub.pemשתי הפקודות לא מדפיסות כלום כשהן מצליחות. בודקים את התוצאות:
ls -l priv.pem pub.pemאצלנו הקובץ priv.pem היה 122 בתים והקובץ pub.pem היה 116 בתים. בקובץ pub.pem אפשר לראות את הבלוק -----BEGIN PUBLIC KEY-----. לא מדפיסים ולא מדביקים את priv.pem. במעבדה הזאת הוא החצי הסודי של הזהות, וההרגל שלא להציג אותו לעולם יידרש גם במפתחות SSH.
echo 'release v1.0' > msg.txt
openssl pkeyutl -sign -inkey priv.pem -rawin -in msg.txt -out msg.sig
ls -l msg.sigהאפשרות -rawin אומרת ל-OpenSSL לחתום ישירות על תוכן הקובץ. Ed25519 מחשב hash על הקלט בתוך האלגוריתם, ולכן לא מחשבים hash לקובץ קודם. פקודת החתימה לא מדפיסה כלום. msg.sig שלנו היה 64 בתים.
openssl pkeyutl -verify -pubin -inkey pub.pem -rawin -in msg.txt -sigfile msg.sigפלט:
Signature Verified Successfullyהדגל -pubin אומר שקובץ המפתח מחזיק רק את החצי הציבורי. מאמת לא צריך שום דבר אחר, כך שהבדיקה יכולה לרוץ על מחשב אחר שמעולם לא ראה את priv.pem.
echo 'release v1.1' > msg.txt
openssl pkeyutl -verify -pubin -inkey pub.pem -rawin -in msg.txt -sigfile msg.sigפלט:
Signature Verification Failureהפקודה יוצאת עם סטטוס 1. החתימה בקובץ msg.sig נוצרה על השורה release v1.0, ולכן היא לא יכולה לערוב לשורה release v1.1. OpenSSL לא אומר מה השתנה. הוא מדווח רק שהבתים והחתימה לא מסכימים. מאמת צריך להתייחס לתוצאה הזאת כעצירה מוחלטת, ולא כאזהרה שמדלגים עליה בלחיצה.
הבדיקות האלה מראות בדיוק מה החתימה מכסה. הרצנו כל אחת מהן על אותו מחשב ובאותה תיקייה.
חתימה כפולה על אותו קובץ נותנת את אותם בתים:
echo 'release v1.0' > msg.txt
openssl pkeyutl -sign -inkey priv.pem -rawin -in msg.txt -out a.sig
openssl pkeyutl -sign -inkey priv.pem -rawin -in msg.txt -out b.sig
cmp a.sig b.sig && echo "identical"פלט:
identicalקובץ בלי תו השורה החדשה בסוף נכשל. echo מוסיף תו שורה חדשה, ולכן הקובץ הזה קצר בבית אחד:
printf 'release v1.0' > nonl.txt
openssl pkeyutl -verify -pubin -inkey pub.pem -rawin -in nonl.txt -sigfile a.sigפלט:
Signature Verification Failuremsg.txt הוא 13 בתים והקובץ nonl.txt הוא 12 בתים. החתימה נוצרה על הגרסה של 13 הבתים, ולכן הקובץ הקצר יותר נכשל.
גם זוג מפתחות אחר נכשל, אפילו על הקובץ המקורי:
openssl genpkey -algorithm ed25519 -out other.pem
openssl pkey -in other.pem -pubout -out other.pub
openssl pkeyutl -verify -pubin -inkey other.pub -rawin -in msg.txt -sigfile a.sigפלט:
Signature Verification Failureהקובץ והחתימה תקינים שניהם. המפתח הציבורי שייך לזוג אחר, ולכן הבדיקה נכשלת.
המאמר הבא, מה זה SSH ואיך מפתחות SSH עובדים?, משתמש באותו רעיון של חתימה להתחברות. קובצי id_ed25519 שלך, אם יש כאלה, הם זוג מהסוג הזה בדיוק. כדאי לקרוא גם את המאמר על hashing של סיסמאות, כי סיסמאות ומפתחות פותרים חלקים שונים של האימות, וכדאי להפריד ביניהם.
להעמקה, RFC 8032 מראה איך Ed25519 בונה את החתימות שלו ולמה הוא דטרמיניסטי. FIPS 186-5 מראה איפה EdDSA נמצא בין אלגוריתמי החתימה המאושרים. דפי ה-man של genpkey ושל pkeyutl ב-OpenSSL מראים את שאר האפשרויות, כמו בחירת digest באלגוריתמים שדורשים אותה. אחר כך כדאי לאמת גרסה של פרויקט שכבר משתמשים בו, עם המפתח שהפרויקט מפרסם באתר שלו. להריץ רק קבצים שהורדו מהמקור הרשמי, ורק אחרי שנבדקו.
התקנים ודפי ה-man שלמעלה הם המקורות הראשוניים של המאמר:
openssl-genpkeyopenssl-pkeyutlקריפטוגרפיה
הפרדה בין קידוד, הצפנה וגיבוב במעבדות עם Base64, SHA-256 ו-OpenSSL. למה Base64 אינו הגנה ולמה hash הוא חד־כיווני.
קריפטוגרפיה
למה hashes מהירים נכשלים בסיסמאות, מה salt ו-work factor עושים, ואיך מחשבים ומאמתים Argon2id ב-Python עם argon2-cffi.
קריפטוגרפיה
קריפטוגרפיה מאפס: הצפנה, מפתחות, hashes, חתימות, ולמה צופן קיסר מלמד רעיונות אבל לא מגן על כלום.