בכנס פייקון 2024 שמחתי להעביר הרצאה על הרגלים לא טובים שמשתמשים לעיתים קרובות בקריפטוגרפיה, נושא שנשמע אולי מורכב, אבל למעשה מדובר במלכודות נפוצות שיכולות להפתיע כל מפתח. הכרות עם המלכודות האלו יסייעו לכל מתכנת פייתון שכותב קוד לפרודקשן ויכולה להציל אתכם ממש. מעבר לזה – דווקא בגלל שחלק מהדברים האלו יכולים להגרם משימוש ב-LLM – חשוב ממש להכיר אותן. הכרות עם דברים מעבר לכתיבת קוד היא יתרון משמעותי שיש למתכנתים אנושיים שיכולים להנחות את ה-LLM בהתאם.
חשוב לומר אני לא קריפטוגרף, אבל אני משתמש ביישומים קריפטוגרפיים, לא בקריפטוגרפיה עצמה. בהרצאה דיברתי על נושאים באופן יותר יישומי כמו Root CA, Trusts, TLS, וכיצד כל אלה משתלבים בפיתוח.
ראשית, ההרצאה:
עכשיו מאמר עם קצת מידע ויותר פירוט.
תקשורת מאובטחת ו-TLS:
כשמתחילים לפתח, אחד הדברים הראשונים שאנחנו מתמודדים איתם הוא יצירת בקשות HTTPS, מה שמביא אותנו לשימוש בקריפטוגרפיה. לדוגמה, כשאנחנו שולחים בקשה לשרת, אנחנו רוצים להבטיח שמידע רגיש לא ייחשף למישהו שיתפוס אותו בדרך. זה נשמע כזה מפחיד. מי יקרא אותו? מה אנשים ב-FBI פותחים אנטנות? לא. תעשו tracrt הפקודה הזו תדגים כמה תחנות עוברהמידע שאתם שולחים משרת איי לשרת בי או משרת איי לקליינט. ובלי הגנה של קריפטוגרפיה. אפשר לתפוס אותו בדרך להסתכל בפנים או קי, ולא רק לבחון את הבקשה ולפגוע בה מבחינת סודיות, אלא גם לשנות דברים.
דרך אגב במאמר מוסגר אם אתם רוצים לראות כמה דברים מעניינים עוברים באוויר, שאתם בעצם נמצאים בו, תקנו SDR באמזון זה עולה כמאה דולר, בעליאקספרס זה הולך $60 מחברים למחשב לראות כמה דברים עוברים באוויר.
אבל אני לא מדבר על האזנות באוויר כמובן. אני מדבר על תנועה אמיתית שעוברת בנקודות: שרתים שונים, ממשלות, ארגונים, בעל בית הקפה שגולשים בוא, השכן החטטן. כל אחד מהם בעצם יכול לבוא ולשנות את המידע. זה נקרא אדם באמצע – Man In the Middle.
ללא קריפטוגרפיה, כל מי שנמצא בתווך בין שרת A לשרת B יכול לקרוא את המידע או לשנות אותו. למשל, אם אתה שולח בקשה להעברת כסף, מישהו יכול לשנות את הסכום או את היעד. TLS מגן מפני התקפות "Man in the Middle" (MITM), שבהן צד שלישי מנסה להאזין או להתערב בתקשורת שלנו.
איך הקריפטוגרפיה מגינה עלינו?
כדי להבין את ההגנה שמספק TLS, נדבר קצת על תהליך ה-handshake. זהו תהליך ראשוני שבו שני הצדדים, הקליינט והשרת, מבצעים חילופי מפתחות ואימות זהות כדי להבטיח חיבור מאובטח.
- השלב הראשון – Client Hello:
התהליך מתחיל כשהקליינט (יכול להיות דפדפן, אפליקציה, או כל תוכנה אחרת) אומר "שלום" לשרת. הוא שולח רשימה של פרמטרים קריפטוגרפיים, כולל גרסאות TLS נתמכות, רשימת צופנים אפשריים, ומספר אקראי שייווצר מאוחר יותר ליצירת מפתח ההצפנה. - השלב השני – Server Hello:
השרת עונה ב"שלום" משלו, בוחר את הצופן ואת גרסת TLS שתשמש לחיבור, ושולח את התעודה הדיגיטלית שלו. התעודה מאומתת על ידי Root CA כדי להבטיח שהשרת הוא מי שהוא טוען להיות. בנוסף, השרת שולח גם מספר אקראי משלו. - אימות ואישור:
בשלב הזה, הלקוח מאמת את התעודה הקריפטוגרפית של השרת. הוא בודק את החתימה הקריפטוגרפית כדי לוודא שהתעודה נחתמה על ידי גורם מאושר, ושלא פג תוקפה. אם התעודה לא תקפה או לא ניתנה על ידי רשות מהימנה, הקשר ייכשל. - חילופי מפתחות:
לאחר האימות, מתבצע חילופי מפתחות. זה נעשה באמצעות הצפנה אסימטרית, שבה הקליינט והשרת יוצרים מפתח משותף סודי. מפתח זה ישמש להצפנה סימטרית, שהיא מהירה יותר ומתאימה לתקשורת מתמשכת. - Session Key:
בשלב הזה, שני הצדדים משתמשים במספרים האקראיים ובחילופי המפתחות כדי לייצר מפתח סימטרי משותף. מפתח זה, שנקרא "session key," ישמש להצפנה והפענוח של כל התקשורת העתידית בין הקליינט והשרת. רק אז התקשורת הופכת מוצפנת לחלוטין.
אז אפשר להבין שתהליך האימות והאישור על ידי RootCA הוא קריטי. למה? כי הוא מונע אפקטיבית התחזות. התקפה מסוג Man in the Middle מתרחשת כאשר תוקף מצליח להיכנס לתווך שבין שני צדדים (למשל, בין שרת A לשרת B) וליירט את התקשורת ביניהם. התוקף לא רק מאזין למידע שעובר, אלא גם מסוגל להתחזות לכל אחד מהצדדים.
איך זה עובד בפועל? נניח שאני, התוקף, יושב בתווך בין שרת A לשרת B. כאשר שרת A מתחיל את תהליך ה-handshake ושולח "Client Hello" לשרת B, אני יוצר קשר עם שרת B ומציג את עצמי כאילו אני שרת A. במקביל, אני יוצא מול שרת A ומתחזה לשרת B. כך, אני יוצר שני חיבורים מוצפנים בנפרד, אחד עם שרת A ואחד עם שרת B.
מה קורה בתהליך?
- שרת A חושב שהוא מתקשר ישירות עם שרת B, אבל למעשה הוא מתקשר איתי.
- שרת B חושב שהוא מתקשר ישירות עם שרת A, אבל שוב, הוא מתקשר איתי.
- אני, התוקף, מצליח לפענח את התקשורת של שני הצדדים, לשנות את המידע אם אני רוצה, ואז להצפין אותו מחדש ולשלוח אותו ליעדו.
הסכנה כאן היא שהתקשורת אינה מאובטחת באמת, כי התוקף שולט בערוץ המידע ויכול לבצע כל שינוי שהוא רוצה. לכן, חיוני להשתמש ב-Root CA ואימות תעודות כדי להבטיח שהתקשורת לא ניתנת לזיוף, וגם לוודא שהתהליך מתבצע בצורה מאובטחת.
בעיה אולטרא נפוצה: שימוש ב-verify=False
כששולחים בקשות עם ספריית requests
בפייתון ויוצרים קשר עם שרת dev או סטייג׳ זה או אחר – מדובר בשרתים שהם self signed certificate ואז מקבלים הודעת שגיאה.
איך עוקפים אותה? הכי קל זה על ידי הגדרת verify=False
. השימוש ב-verify=False
מסיר את אימות התעודה עם ה-Cert ומותיר את התקשורת פגיעה להתקפות MITM. זהו פתרון בעייתי וגם הוא נוטה לזלוג לפרודקשן.
דוגמה לשימוש ב-verify=False:
import requests
response = requests.get("https://example.com", verify=False)
print(response.content)
יש כמה דרכים לא לעבוד עם verify=False ואחת מהן היא שימוש בתעודת PEM עם Certificate Pinning
במקום להשבית את אימות התעודה, נשתמש בתעודת PEM שרלוונטית לשרת:
import requests
cert_path = "path/to/server_cert.pem"
response = requests.get("https://example.com", verify=cert_path)
print(response.content)
הסבר:
cert_path
הוא נתיב לקובץ ה-PEM שמכיל את התעודה המאומתת של השרת. יש לוודא שהתעודה נשמרה בצורה מאובטחת.- הפרמטר
verify=cert_path
מורה ל-requests
להשתמש בתעודה הזו כדי לאמת את השרת, מה שמגן מפני התקפות MITM.
יש כלים אוטומטיים/סריקת קוד שמתריעים על verify=False אבל לא תמיד הם מופעלים או שהתעלמות מהם תשבור את הבילד. לא מעט פעמים – במיוחד בכל מיני POC אלו ואחרים אנחנו נוטים לבחור ב Verify=false – אבל כדאי
גרסאות TLS:
חשוב לדעת שאין גרסת TLS אחת. יש כמה גרסאות ואפשר לתמוך בכמה ו-TLS אינו חסין לחלוטין מפני פגיעויות, במיוחד כשמדובר בגרסאות ישנות יותר. תוקפים יכולים לנצל חולשות בפרוטוקולים ישנים כמו TLS 1.0 ו-1.1, שחשופים להתקפות כמו POODLE ו-BEAST. לכן, חשוב להבטיח שהמערכת שלך תומכת רק בגרסאות TLS עדכניות (1.2 ו-1.3).
כיצד לזהות פגיעויות ב-TLS?
ניתן להשתמש בכלים שונים כדי לבדוק את תצורת ה-TLS של השרתים שלך ולזהות בעיות פוטנציאליות. הראיתי בהרצאה את הכלי הפופולרי של SSL Labs. מכניסים את ה-URL שלכם ומקבלים תוצאה. זה הכל!
מה שחשוב לזכור זה שאם אתם תומכים בגרסאות ישנות – אתם פגיעים לכל החולשות של הגרסאות הישנות!
חולשות ב-TLS הנובעות מבקשות לקוח: כאן מגיעה הנקודה החשובה על חולשות שנובעות מבקשות של לקוחות. גם אם השרת שלך מוגדר בצורה מאובטחת, בקשות מסוימות מצד הלקוח עלולות להחליש את תצורת ה-TLS, מה שמוביל לסיכונים משמעותיים.
דוגמה – לקוח מתקשר ומדווח על תקלה – יש לו מערכות ישנות שלא מצליחות להתחבר לאתר כי הן תומכולת רק בגרסאות מיושנות. למשל גרסת דפדפן של אקספלורר 10 שלא תומכת ב TLS V1.3. הוא לא ממש אומר מה הסיבה אלא שולח לוג שגיאה. כמו כל מתכנת טוב אתה מפעיל את ה-LLM הקרוב ונאמר לך שזה חוסר תאימות ל-TLS – מה הבעיה? נגרום למערכת לתמוך גם בתקן TLS V1.1. מה הבעיה?
כאשר לקוח מתחבר לשרת, הוא שולח רשימה של צופנים וגרסאות TLS שהוא תומך בהם. אם השרת מוגדר בצורה שתומכת בצופנים חלשים או בגרסאות ישנות, התוקף יכול לכפות על השרת להשתמש באופציה הפחות מאובטחת מתוך הרשימה. זה נקרא "התקפת downgrade, שבה תוקף מאלץ את התקשורת להשתמש בגרסה או בהצפנה חלשה. כמובן שמי שמוצא את זה מוצא את זה בזמן הכי פחות נוח ואם אתה מוריד את התמיכה – יש לך בעיה עם הלקוח. זו בעיה שה-LLM לא יפתור לך ובלי הכרות מוקדמת? תהיו בבעיה.
ועוד קצת מידע על מספרים רנדומליים קריפטוגרפיים:
בקריפטוגרפיה, מספרים רנדומליים משחקים תפקיד מרכזי באבטחה של מפתחות הצפנה, nonce, וקטורים של אתחול (IV), ועוד. הבעיה היא שמספרים "רנדומליים" אינם באמת רנדומליים אם יוצרים אותם בצורה לא נכונה. אם המספר ניתן לניבוי, כל האבטחה של המערכת מתערערת וחבל.
דוגמאות לשימוש במספרים רנדומליים רגילים:
רבים מהמפתחים משתמשים בפונקציה random
של פייתון ליצירת מספרים רנדומליים שזה אחלה לרוב השימושים. אך פונקציה זו אינה מתאימה לשימושים קריפטוגרפיים, כי היא מבוססת על מחולל פסאודו-רנדומלי שניתן לניבוי.
import random random_number = random.randint(0, 100) print(random_number)
המספר שנוצר כאן אינו באמת רנדומלי מבחינה קריפטוגרפית, והמחולל מבוסס על זמן, מה שמקל על ניבוי התוצאות בתרחישים מסוימים.
כיצד ליצור מספרים רנדומליים קריפטוגרפיים:
כדי ליצור מספרים רנדומליים בצורה מאובטחת, משתמשים בפונקציות ייעודיות כמו os.urandom
או ספריות קריפטוגרפיות.
דוגמה לשימוש נכון:
# יצירת מספר רנדומלי קריפטוגרפי של 16 בתים random_bytes = os.urandom(16) print(random_bytes)
או, אם אתה זקוק למספרים רנדומליים בצורה יותר קריאה (כמו מספרים שלמים), ניתן להשתמש בספריית secrets
, שהיא חלק מפייתון ומיועדת לאבטחה קריפטוגרפית:
דוגמה עם ספריית secrets:
# יצירת מספר רנדומלי קריפטוגרפי בטווח 0-100 secure_random_number = secrets.randbelow(101) print(secure_random_number) # יצירת מחרוזת רנדומלית מאובטחת, לדוגמה לצורך סיסמה secure_token = secrets.token_hex(16) print(secure_token)
בסופו של דבר, הכרות עם קריפטוגרפיה בסיסית היא חלק בארסנל הידע המקצועי שלנו. בעולם שבו קל לייצר קוד עם LLM – חשוב להכיר את ה-pitfalls השונים ולהבין היטב את האילוצים שבהם הקוד שלנו עובד ואת המערכת שבה הוא עובד. זה היתרון המרכזי והתפקיד המשמעותי של מתכנתים בעידן ה-AI לפי דעתי.
בהרצאה לא נכנס כל החומר על בעיות קריפטוגרפיות בפייתון. יש את הפוסט הזה שבו הרחבתי על בעיות אחרות ומעניינות לא פחות במימוש של פונקציות קריפטוגרפיות בפייתון.