אימות תקינות קבצים בלינוקס באמצעות sha256sum
למדו לאמת הורדות בעזרת הפקודה sha256sum והדגל -c. המדריך מציג תרגול מעשי שבו משבשים קובץ בכוונה כדי לראות את הודעת השגיאה FAILED ולהבין מה באמת מוכיח ה-checksum.
אימות הורדה באמצעות checksum בתוך שתי דקות
כדי לאמת הורדה באמצעות checksum, בצעו hashing לקובץ שקיבלתם ותנו לכלי להשוות את ה-hash לזה שפרסם המפיץ. sha256sum מבצע את שני חלקי המשימה: בפני עצמו הוא מציג digest, ועם הדגל -c הוא קורא רשימת digests ומדווח אילו קבצים תואמים. מדריך זה מריץ את התהליך המלא על קובץ שתיצרו, ולאחר מכן משבש את הקובץ בכוונה כדי שתראו את הכשל מתרחש במקום רק לקרוא עליו.
זכרו משפט אחד לאורך כל הדרך. checksum אומר לכם האם הבתים שבידיכם הם הבתים שיצרו את ה-digest, אך הוא לא אומר דבר על מי שיצר אותם. השאלה השנייה דורשת חתימה ומפתח שאתם סומכים עליו. החלק האחרון של מדריך זה מראה בדיוק היכן עובר הגבול בין השניים.
יצירת קובץ לתרגול
עבדו בתוך ספריית עבודה זמנית כדי להבטיח ששום דבר כאן לא ישפיע על שאר המערכת. כל הפקודות להלן מגיעות מתוך GNU coreutils, סט הפקודות הבסיסי הקיים בכל שרת Ubuntu או Debian, כך שאין צורך בהתקנה של דבר.
mkdir -p ~/checksum-demo
cd ~/checksum-demo
printf 'the payload of a totally ordinary download\n' > payload.txt
sha256sum payload.txtתקבלו שורה אחת: 64 תווים הקסדצימליים, שני רווחים, ולאחריהם שם הקובץ. 64 התווים הללו הם ה-digest של הקובץ. הריצו את הפקודה שוב והשורה תהיה זהה, כיוון שפעולת ה-hashing היא דטרמיניסטית: אותו קלט תמיד מניב את אותו פלט. שנו תו אחד בקובץ והריצו את הפקודה שוב; ה-digest לא ישתנה רק במעט. הוא ייראה שונה לחלוטין, כיוון ששינוי של ביט אחד בקלט הופך בערך מחצית מביטי הפלט. תכונה זו היא שהופכת מחרוזת של 64 תווים לתחליף שמיש עבור קובץ image של 4 GB.
שמירת קובץ SHA256SUMS ובדיקתו
ערך גיבוב (digest) שמופיע על המסך אינו מועיל לאחר יום. שמרו אותו לקובץ, בפורמט שבו sha256sum עצמו כותב, כדי שהכלי יוכל לקרוא אותו בחזרה במועד מאוחר יותר.
sha256sum payload.txt > SHA256SUMS
cat SHA256SUMS
sha256sum -c SHA256SUMSsha256sum -c קורא כל שורה ברשימה, מחשב את ה-hash של הקובץ המצוין באותה שורה, ומשווה בין שני ערכי הגיבוב. הרצה תקינה מדפיסה שורה אחת עבור כל קובץ:
payload.txt: OKבדקו גם את סטטוס היציאה (exit status), כיוון שסקריפט קורא אותו ולעולם אינו קורא את הטקסט. echo $? מדפיס 0 לאחר הרצה נקייה. השם SHA256SUMS הוא מוסכמה ולא כלל מחייב, אך הפצות ורוב דפי השחרורים משתמשים בו, לכן השתמשו בו גם אתם כדי שהאדם הבא ידע מה מכיל הקובץ מבלי לפתוח אותו.
שינוי בייט אחד וצפייה בכישלון הבדיקה
כעת נשבש את הקובץ בכוונה. הפעולה הבאה כותבת בייט בודד ב-offset 5 ומשאירה את שאר הקובץ ללא שינוי, כך שהקובץ שומר על אורכו ועל שמו.
printf 'X' | dd of=payload.txt bs=1 seek=5 conv=notrunc status=none
cat payload.txt
sha256sum -c SHA256SUMSconv=notrunc הוא ה-flag הקריטי כאן: בלעדיו, dd מקצר את הקובץ בנקודה שבה הכתיבה נעצרת, ואז הייתם בוחנים סוג נזק מובן מאליו הרבה יותר. הבדיקה כעת מדפיסה:
payload.txt: FAILED
sha256sum: WARNING: 1 computed checksum did NOT matchecho $? מדפיס 1. המשמעות של FAILED היא שהקובץ נקרא, אך ה-digest שלו לא תאם לזה שברשימה. החזירו את הבייטים המקוריים וודאו שהבדיקה חוזרת ל-OK:
printf 'the payload of a totally ordinary download\n' > payload.txt
sha256sum -c SHA256SUMSזהו כל ההרגל. הבדל של בייט אחד, בכל מקום בקובץ, מפיק FAILED. הורדה שנקטעה בגלל חיבור שנפל, שרת mirror שמגיש גרסה מאתמול, proxy שכתב מחדש את הקובץ במהלך המעבר, או דיסק שהחזיר bad block: כולם מובילים לאותה שורת פלט.
כאשר הרשימה מציינת קובץ שלא הורדת
קובץ SHA256SUMS אמיתי מהפצה מפרט את כל התמונות שהפרויקט משחרר, ואתה הורדת רק אחת מהן. שחזר מצב זה כאן.
printf 'a second file\n' > notes.txt
sha256sum payload.txt notes.txt > SHA256SUMS.all
rm notes.txt
sha256sum -c SHA256SUMS.allpayload.txt: OK
sha256sum: notes.txt: No such file or directory
notes.txt: FAILED open or read
sha256sum: WARNING: 1 listed file could not be readFAILED open or read הוא כשל שונה מ-FAILED, וערבוב ביניהם מבזבז זמן. FAILED מציין שהבתים אינם תקינים. FAILED open or read מציין ש-sha256sum כלל לא קיבל את הקובץ, ולכן לא בוצעה השוואה. בהורדה אמיתית, הסיבה הנפוצה היא ספריית העבודה, מכיוון שהשמות ברשימה הם יחסיים למיקום שבו אתה מריץ את הפקודה. עברו לספרייה המכילה את הקובץ והריצו שוב. כדי לבדוק רק את מה שברשותך בפועל, בקשו זאת:
sha256sum --ignore-missing -c SHA256SUMS.allפעולה זו מדפיסה payload.txt: OK ומסיימת עם קוד יציאה 0. אם אף אחד מהשמות המפורטים אינו קיים, --ignore-missing לא תסתיים בהצלחה שקטה על אפס קבצים. היא תדווח ש-no file was verified ותסתיים עם קוד יציאה שאינו אפס, וזהו ההתנהגות הרצויה, כיוון שבדיקה שעברה בהצלחה מבלי לבדוק דבר היא כשל שלעולם לא תבחין בו.
הדבקת תקציר שפורסם ללא בדיקה ויזואלית
השוואה ויזואלית של 64 תווים הקסדצימליים היא המקום שבו הרגל זה נכשל. אנשים בודקים את ארבעת התווים הראשונים ואת ארבעת האחרונים ומכריזים על התאמה, וזו בדיוק ההשוואה שתוקף נחוש בונה עליה. תנו לכלי לבצע את ההשוואה במקומכם. הגדירו את EXPECTED לתקציר שהעתקתם מהמפרסם, באמצעות EXPECTED= ואחריו הערך שהודבק, ולאחר מכן בנו את השורה הבודדת ש־-c מצפה לה:
printf '%s %s\n' "$EXPECTED" payload.txt > payload.sha256
cat payload.sha256
sha256sum -c payload.sha256שני רווחים מפרידים בין התקציר לבין שם הקובץ, וזו הסיבה שמחרוזת הפורמט מכילה שניים כאלו. זהו המבנה ש־sha256sum כותב והמבנה ש־-c מנתח. קובץ המכיל תקציר בלבד אינו נחשב לשורת checksum, ולכן הבדיקה דוחה את הקובץ כולו עם no properly formatted checksum lines found במקום לנסות לנחש לאיזה קובץ התכוונתם. פרויקטים מסוימים מפרסמים את הפורמט בסגנון BSD, כלומר SHA256 (payload.txt) = ואחריו התקציר. חבילת GNU coreutils כותבת פורמט זה עם sha256sum --tag payload.txt וקוראת אותו חזרה עם -c, כך ששני המבנים תקינים לשמירה.
כאשר בדיקה מתנהגת בצורה מוזרה, בדקו את הרשימה עצמה עם cat -A SHA256SUMS, שמסמן את סוף כל שורה עם $ ומציג תווים שלא ניתן לראות בדרך אחרת. שורה שמסתיימת ב־^M$ מכילה תו carriage return שנאסף מעורך טקסט של Windows. הכלי sha256sum של GNU מתעלם מתו סיום זה ועדיין מדפיס OK, כך שרשימת CRLF אינה הגורם לכשל בבדיקה שלכם, אם כי כלים שאינם חלק מ-coreutils סלחניים פחות כלפיו. נרמלו את העותק שאתם שומרים באמצעות tr -d '\r' < SHA256SUMS > SHA256SUMS.clean.
מה מוכיח checksum, ומה הוא אינו מוכיח?
checksum מוכיח דבר אחד בלבד: הבתים שנמצאים על הדיסק שלך הם אותם בתים שיצרו את ה-digest שפורסם. זה מכסה באופן מלא נזק מקרי. זה מכסה גם תוקף רשלן שהחליף את הקובץ בשרת מראה (mirror) להורדות, אך לא הייתה לו גישה לדף שבו פורסם ה-digest.
הוא אינו מוכיח דבר לגבי זהות היוצר. digest הוא עובדה לגבי בתים, לא עובדה לגבי אנשים. אם דף אחד מגיש גם את הקובץ וגם את ה-digest, הרי שמי שיכול לשנות את האחד יכול לשנות גם את השני, ושורת ה-OK שלך מעידה רק על כך שהשרת מסכים עם עצמו. לכן, זהו הכלל שהופך את השימוש ב-checksum לכדאי: קחו את ה-digest ממקור שונה מזה שממנו הורדתם את הקובץ. למשל, הדומיין הרשמי של הפרויקט ב-TLS (אבטחת שכבת תעבורה), בזמן שהקובץ הגיע משרת מראה או מ-torrent. כעת, על התוקף לשלוט בשני מקומות במקום באחד. ה-checksum גם אינו אומר דבר על מה שהבתים המאומתים יעשו ברגע שתריצו אותם; זו שאלה נפרדת שכדאי לשאול לגבי כל דבר שמתבצע בשמכם, החל מסקריפט התקנה ועד תוסף dsh שרץ עם ההרשאות של ה-agent שלכם.
גם לאלגוריתם יש חשיבות. נכון לאוגוסט 2026, לא ידוע על התנגשויות (collisions) ב-SHA-256 (אלגוריתם גיבוב מאובטח, פלט של 256 סיביות), וזו הסיבה שמפיצים משתמשים בו. MD5 (message digest 5) ו-SHA-1 אינם עומדים במבחן המציאות: ניתן ליצור שני קבצים שונים עם אותו digest ב-MD5 מאז 2004, והתנגשות SHA-1 מבוססת chosen-prefix פורסמה ב-2020. קובץ MD5SUMS עדיין יזהה הורדה קטועה, כיוון ששיבוש אקראי אינו התנגשות מתוכננת. הוא אינו יכול לעצור מישהו שמנסה להטעות אתכם. כאשר פרויקט מפרסם את שניהם, השתמשו בשורת ה-SHA-256.
היכן שחתימות נכנסות לתמונה
חתימה סוגרת את הפער שמותיר אחריו ה-digest. המפיץ חותם על קובץ ה-digest באמצעות מפתח פרטי, ואתם מאמתים זאת באמצעות המפתח הציבורי שלו: gpg --verify SHA256SUMS.asc SHA256SUMS. אם האימות עובר, הרי שרשימת ה-digests הגיעה ממי שמחזיק במפתח זה. לאחר מכן, sha256sum -c SHA256SUMS מקשר בין הקובץ שעל הדיסק שלכם לבין הרשימה, ושרשרת האמון נמתחת מהמפתח ועד לרמת הבתים הבודדים.
נקודת התורפה עוברת למפתח עצמו. הורדת המפתח מאותו דף שסיפק את הקובץ מעניקה לתוקף את שני החלקים גם יחד. GnuPG מציגה זאת בכנות, ואימות ראשוני מדפיס Good signature יחד עם WARNING: This key is not certified with a trusted signature!. המשמעות של Good signature היא שהמתמטיקה תקינה. אין זה אומר שהמפתח שייך בהכרח לפרויקט שאליו התכוונתם. השיגו את ה-fingerprint ממקור שני, כגון תיעוד הפרויקט בדומיין אחר או חבילת הפצה שכבר כוללת את המפתח, והשוו את ה-fingerprint המלא במקום להסתפק בשמונה התווים האחרונים. זוהי אותה רמת זהירות הנדרשת ממפתח SSH פרטי, מאותה סיבה בדיוק: המפתח הוא החלטת האמון, וכל מה שבא אחריו יורש אותה.
תהליכי בנייה הדירים (Reproducible builds) מקדמים את הרעיון צעד אחד קדימה. digest מפורסם עדיין קושר אתכם לקובץ בינארי שמכונה אחת בנתה. כאשר תהליך הבנייה של פרויקט הוא הדיר, כל אחד יכול לקמפל את אותו קוד מקור ולקבל פלט זהה ברמת הבייט, כך שבוני תוכנה עצמאיים יכולים לאשר את ה-digest המפורסם במקום שתצטרכו להסתמך על מילתו של שרת בודד. חשיבות הדבר עולה מדי שנה, ככל שיותר קוד מגיע דרך צינורות אוטומטיים וטלאים שנכתבו על ידי מכונה. ההחלטה מה לקבל לתוך תהליך הבנייה היא שאלה של מדיניות, ו-מדיניות קוד פתוח עבור קוד בסיוע בינה מלאכותית מטפלת באותה שרשרת אספקה מהצד השני.
מנהל החבילות שלכם כבר עושה זאת עבורכם
במערכות Debian ו-Ubuntu, הכלי apt מריץ את השרשרת הזו בכל התקנה באופן אוטומטי. אינדקס החבילות נושא חתימת SHA-256 עבור כל קובץ .deb. הקובץ Release מכיל את החתימות של קובצי האינדקס הללו, והקובץ InRelease נושא חתימה דיגיטלית על Release, הנבדקת מול המפתחות ב-/usr/share/keyrings וב-/etc/apt/trusted.gpg.d. כאשר השרשרת נקטעת, apt מדווח על כך: The following signatures couldn't be verified because the public key is not available: NO_PUBKEY כאשר חסר מפתח של מאגר צד-שלישי, או Hash Sum mismatch כאשר האינדקס שהורד אינו תואם ל-Release החתום, מה שבדרך כלל מעיד על כך ששרת proxy מתווך סיפק קובץ מיושן או שהורדתם משרת מראה (mirror) במהלך סנכרון.
זהו הסטנדרט שיש לשאוף אליו כאשר דף הבית של פרויקט מנחה אתכם להזרים סקריפט מ-curl ישירות לתוך ה-shell. שום דבר לא מאמת את הבתים (bytes) ואתם לעולם לא רואים אותם. השרת יכול גם להחזיר תוכן אחד לסקריפט ותוכן אחר לדפדפן, ולא תהיה לכם עותק לבדיקה לאחר מכן. הורידו את הקובץ באמצעות curl -fsSL <url> -o install.sh, בצעו לו hash, קראו אותו עם less, ורק אז הריצו אותו. ההרגל הזה גוזל כעשרים שניות, וזהו אותו הרגל שכדאי להתחיל בו ב-שרת VPS חדש בעשר הדקות הראשונות שלו, לפני שמותקן דבר מה נוסף על השרת.
שמרו רשימת גיבובים (digests) עבור מה שאתם מתקינים ידנית
חבילות המותקנות באמצעות apt מנוטרות על ידי המערכת. קובץ בינארי שהעתקתם לתוך /usr/local/bin אינו מנוטר, ושום רכיב במערכת לא עוקב אחריו. רשימת גיבובים הופכת זאת למשהו שניתן לבדוק לפי דרישה:
printf 'a second file\n' > notes.txt
sha256sum *.txt > inventory.sha256
sha256sum -c --quiet inventory.sha256--quiet לא מדפיס דבר כאשר כל הקבצים תואמים, ומדפיס רק את השורות שנכשלו כאשר יש אי-התאמה; לכן, שתיקה היא סימן להצלחה, ו-echo $? מאשר זאת באמצעות 0. זהו הפורמט לשימוש במשימה מתוזמנת. --status מרחיק לכת עוד יותר ולא מדפיס דבר כלל, ומותיר אתכם רק עם סטטוס היציאה (exit status). הפנו את אותו דפוס לקבצים אמיתיים בעזרת sha256sum /usr/local/bin/* > ~/local-bin.sha256 ותקבלו קו בסיס (baseline). הנתיבים נשמרים ברשימה בדיוק כפי שהקלדתם אותם, לכן שימוש בנתיבים מוחלטים (absolute paths) מאפשר לבדיקה לעבוד מכל ספרייה.
היו ברורים לגבי הערך של קו בסיס זה. הוא מזהה קובץ שהשתנה. הוא אינו מזהה תוקף שכבר מחזיק בהרשאות root, מכיוון שתוקף כזה יכול לשכתב את inventory.sha256 בקלות שבה שיכתב את הקובץ הבינארי. שמרו את הרשימה מחוץ למכונה אם אתם רוצים שתהיה לה משמעות, וזהו חלק מהשאלה הרחבה יותר של כמה מה-VPS אתם באמת מבטחים ומי עוד יכול להגיע לדיסק שמתחתיו.
FAQ
האם checksum תואם מעיד על כך שההורדה בטוחה?
לא. המשמעות היא שהבתים שבידיך תואמים לערך ה-digest מולו ביצעת את הבדיקה. אם תוקף שולט בדף שפרסם את ה-digest, הוא יפרסם את ה-digest של הקובץ שלו, והבדיקה שלך תציג OK. התאמה היא טענה לעקביות בלבד. טענה לבטיחות דורשת חתימה מאומתת מול מפתח שהושג ממקור אחר, ורק אז ה-digest יורש את רמת האמון הזו.
מדוע sha256sum -c מציג FAILED open or read?
מכיוון שהוא לא קרא את הקובץ. שורה נפרדת ממש מעל מציינת No such file or directory עם השם שהוא חיפש. השמות בתוך קובץ SHA256SUMS הם יחסיים לספרייה שבה מריצים את הפקודה, לכן יש לעבור לספרייה המכילה את ההורדה ולהריץ שוב. אם הרשימה כוללת גם קבצים שלא הורדו, יש להוסיף --ignore-missing. פקודת FAILED פשוטה ללא open or read מעידה על מצב הפוך: הקובץ נקרא, אך ה-digest שלו לא תאם.
האם MD5 מספיק טוב לאימות הורדה?
עבור נזק מקרי, כן. העברה קטועה או בלוק דיסק פגום לא יפיקו במקרה digest של MD5 תואם. מול תוקף, לא. ניתן לייצר שני קבצים שונים עם אותו digest של MD5 מאז שנת 2004, ו-SHA-1 נפרץ בשיטת chosen-prefix collision בשנת 2020. השתמשו בשורת ה-SHA-256 כאשר פרויקט מפרסם את שניהם, והתייחסו לפרויקט שמציע MD5 בלבד כסימן לתהליך הפצה מיושן.
מה ההבדל בין sha256sum -c לבין gpg --verify?
sha256sum -c מוכיח שקובץ תואם ל-digest. gpg --verify מוכיח שקובץ ה-digest נחתם על ידי בעל מפתח פרטי מסוים. הם עונים על שאלות שונות, לכן יש להריץ את שניהם כאשר פרויקט מציע את שניהם. החתימה הופכת את רשימת ה-digest למהימנה, ורשימת ה-digest הופכת את הקובץ שהורד למהימן.
כיצד בודקים קובץ מול digest שמופיע בדף אינטרנט?
אל תשוו את התווים בעין. שמרו את ה-digest ואת שם הקובץ בשורה אחת, מופרדים על ידי שני רווחים, ולאחר מכן הריצו sha256sum -c מול הקובץ וקראו את ה-OK או ה-FAILED שהוא מציג. בניית השורה באמצעות printf '%s %s\n' מונעת טעויות עיצוב שגורמות ל-sha256sum לדחות את הקובץ עם no properly formatted checksum lines found.