9.17. סוקטים מוצפנים ו-TLS¶
כל מה שתואר עד כאן מעביר בתים בצורה גלויה. כל התקן שנמצא על המסלול בין המצלמה לשרת – הנתב הביתי, ספק האינטרנט, נקודת גישה זדונית בבית קפה – יכול עקרונית לקרוא או לשנות את מה שעובר דרכו. עבור רוב תעבורת האינטרנט זה אינו מקובל. הפתרון הסטנדרטי הוא לעטוף את החיבור בשכבת הצפנה: TLS, פרוטוקול Transport Layer Security. סמל המנעול של ”HTTPS“ בדפדפן הוא TLS שרץ מעל TCP, ואותה עטיפה היא מה שהופך כל פרוטוקול אינטרנט אחר ל“מאובטח“. מודול ssl של המצלמה הוא מה שעוטף socket ב-TLS.
9.17.1. מה TLS מוסיף, ועם מה המצלמה מגיעה¶
TLS יושב בין TCP לאפליקציה – האפליקציה כותבת בתים לסוקט עטוף ב-TLS, ה-TLS מצפין אותם ומעביר את התוצאה ל-TCP, והתהליך מתהפך בצד השני. בצורתו המלאה TLS מספק שלוש ערובות מעל TCP רגיל:
סודיות. מאזינים על המסלול אינם יכולים לקרוא את מה ששתי נקודות הקצה מחליפות.
שלמות. כל שינוי בתעבורה במהלך המעבר מזוהה; החיבור נשבר במקום למסור נתונים שעברו מניפולציה.
אימות. השרת מוכיח שהוא השרת הנקוב בשמו, ולא מתחזה (ובאופן אופציונלי, גם הלקוח מוכיח מי הוא עצמו).
השניים הראשונים נובעים מההצפנה עצמה. השלישי דורש תעודות בצד אחד לפחות, בתוספת משהו שזכה לאמון מראש כדי לאמת מולו את אותן תעודות. מצלמת OpenMV מגיעה ללא מאגר תעודות מובנה כלל: מצלמה שזה עתה צרבו בה קושחה אינה נותנת אמון בשום רשות אישורים, אין לה תעודת שרת משלה, ומצב האימות בברירת המחדל (ssl.CERT_NONE) אינו בודק את תעודת העמית מול שום דבר. כך שמהקופסה, TLS על המצלמה נותן לך את שתי הערובות הראשונות – הצפנה מפני האזנה ושיבוש בידי צופה פסיבי – אך לא את השלישית.
9.17.2. הצפנת חיבור יוצא¶
השימוש הפשוט ביותר הוא עטיפת חיבור TCP יוצא. הזרימה היא: פתיחת סוקט TCP רגיל, מסירתו ל-ssl.wrap_socket(), ואז קריאה וכתיבה דרך הסוקט העטוף בדיוק כפי שהיית עושה עם הסוקט הרגיל:
import socket
import ssl
addr = socket.getaddrinfo("example.com", 443)[0][-1]
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(addr)
s = ssl.wrap_socket(sock)
s.send(b"GET / HTTP/1.0\r\nHost: example.com\r\n\r\n")
print(s.recv(4096))
s.close()
העטיפה מבצעת את לחיצת היד של TLS; לאחר מכן כל בית שעובר דרך s.send מוצפן בדרכו החוצה וכל בית מ-s.recv היה מוצפן על גבי הקו. שום תעודות לא הוגדרו, שום עוגן אמון לא סופק – TLS פשוט מנהל משא ומתן על מפתח סשן זמני עם השרת שעונה, ומשתמש בו.
לחיצת היד של TLS ש-ssl.wrap_socket() מריץ. היא יושבת על גבי חיבור ה-TCP שכבר פתוח מהאיור הקודם; ברגע ששני הצדדים שלחו Finished, שאר השיחה מוצפנת בשני הכיוונים.¶
אזהרה
זוהי הצפנה בלבד, לא TLS מאומת. המצלמה מדברת בצורה מאובטחת עם מה שלא ענה בקצה השני של חיבור ה-TCP. אם מתקיף שנמצא באמצע (man-in-the-middle) מנתב מחדש את החיבור לשרת שבשליטתו ושרת זה מציג כל תעודה, לחיצת היד עדיין מצליחה והמצלמה בסופו של דבר מדברת בצורה מאובטחת עם התוקף. השתמש במצב זה רק כאשר מתקיף שבאמצע אינו חלק ממודל האיומים – רשת מקומית סגורה, סביבת פיתוח, המצלמה מדברת עם שירות שרץ על אותה חומרה – ולא כאשר פונים לאינטרנט הציבורי.
עבור אימות אמיתי – המצלמה מאמתת שרת ציבורי, המצלמה פועלת כשרת TLS, או TLS הדדי – עליך להביא תעודות אל ההתקן. הסיפור המלא נמצא ב-עבודה עם אישורי TLS.
אותה עטיפה עובדת עבור תעבורת TCP נכנסת, על ידי בחירת פרוטוקול השרת והעברת server_side=True ל-ssl.wrap_socket(). האזהרה למעלה עדיין חלה: ללא תעודה משלה המצלמה אינה יכולה להוכיח מי היא ללקוח, ולקוח סקרן יראה כשל בלחיצת היד מסוג ”no certificate“ ברוב מחסניות ה-TLS. תהליך התעודות בצד הייצור הוא מה שמשחרר את האפשרות להפעיל את המצלמה כשרת TLS באופן שימושי.
9.17.3. עם asyncio¶
פרק ה-asyncio הציג את asyncio.open_connection() עבור לקוחות TCP רגילים. אותה קריאה מקבלת מילת מפתח ssl=True שעוטפת את החיבור ב-TLS, שוב ללא כל הגדרת תעודות:
import asyncio
async def main():
reader, writer = await asyncio.open_connection(
"example.com", 443, ssl=True,
)
writer.write(b"GET / HTTP/1.0\r\nHost: example.com\r\n\r\n")
await writer.drain()
print(await reader.read(4096))
writer.close()
await writer.wait_closed()
asyncio.run(main())
זוג ה-reader/writer שמאחורי חיבור TLS הוא באותה צורה כמו עבור חיבור TCP רגיל – רק ההקמה שונה. אותה הסתייגות לגבי אימות חלה: ssl=True לבדו נותן הצפנה בלבד, לא אימות.
9.17.4. DTLS – TLS מעל UDP¶
TLS כפי שנדון עד כה רוכב על גבי TCP. הפרוטוקול המקביל עבור UDP הוא DTLS (Datagram TLS), ומודול ssl של המצלמה תומך בו באותו אופן. כפי ש-TLS הופך חיבור TCP אחד לזרם בתים מוצפן אחד, DTLS הופך סוקט UDP אחד לזרם של דאטהגרמות מוצפנות שנמסרות בנפרד – כך שתכונות האובדן / סדר-שגוי / היעדר-בקרת-זרימה של UDP מ-UDP – שלח חבילה, קווה לטוב ביותר כולן עוברות הלאה, כשהבתים בתוך כל דאטהגרמה מוצפנים כעת.
העטיפה נראית כמו במקרה של TLS, רק עם סוקט SOCK_DGRAM וקבועי פרוטוקול ה-DTLS:
import socket
import ssl
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.connect(socket.getaddrinfo("example.com", 4433)[0][-1])
ctx = ssl.SSLContext(ssl.PROTOCOL_DTLS_CLIENT)
s = ctx.wrap_socket(sock)
s.send(b"ping")
print(s.recv(64))
s.close()
(קריאה ל-connect() על סוקט UDP אינה פותחת חיבור – היא רק זוכרת יעד ברירת מחדל כך שקריאות send() / recv() עוקבות אינן צריכות לחזור עליו. DTLS זקוק ליעד קבוע זה כדי להריץ מולו את לחיצת היד שלו.)
לחיצת היד היא באותה צורה כמו תרשים ה-TLS למעלה; ההבדל הוא שכל הודעת לחיצת יד היא בעצמה דאטהגרמת UDP, וכל צד ינסה שוב במקרה של אובדן.
הערה
האם אובדן חבילות שובר את ההצפנה? לא. כל חבילת DTLS נושאת מספר רצף, וההצפנה משתמשת במספר זה כדי להפיק פלט שונה עבור כל חבילה – כך שאותו קלט לעולם אינו מוצפן לאותם בתים פעמיים, וכל חבילה ניתנת לפענוח בפני עצמה מבלי שהקודמת לה הגיעה. חבילות אבודות או בסדר שגוי אינן מבטלות את הסנכרון בין שני הצדדים. (לחיצת היד עצמה היא החלק היחיד שחייב להגיע באופן אמין, ו-DTLS מטפל בכך עם שידור חוזר משלו.)
אותה אזהרה של הצפנה-בלבד-ללא-תעודות מלמעלה חלה: לחיצת יד של DTLS מול עמית עם CERT_NONE מצפינה את התעבורה אך אינה מאמתת מיהו הצד השני. תהליך ה-DTLS המלא – תעודות, עוגיית מניעת התחזות (anti-spoofing) בצד השרת, כיצד זהו אותו משטח כמו TLS למעט קבועי הפרוטוקול – מכוסה לצד חומר ה-TLS ב-עבודה עם אישורי TLS.
גרסת ה-asyncio משתמשת באותה תבנית UDP-לא-חוסם מ-Sockets עם asyncio. בצע את לחיצת היד באופן סינכרוני מראש, העבר את הסוקט למצב לא-חוסם, ואז בצע סקירה (poll) בתוך coroutine:
import asyncio
import socket
import ssl
async def dtls_ping(target_addr, period_ms):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.connect(target_addr)
# Handshake while still blocking, then switch to async polling.
ctx = ssl.SSLContext(ssl.PROTOCOL_DTLS_CLIENT)
s = ctx.wrap_socket(sock)
s.setblocking(False)
while True:
try:
s.send(b"ping")
except OSError:
pass
await asyncio.sleep_ms(period_ms)
לחיצת היד היא המקום היחיד שבו coroutine זה חוסם את לולאת האירועים; לאחר מכן, כל s.send / s.recv חוזר מיד (או מעלה OSError), וה-await asyncio.sleep_ms שומר על שאר התוכנית פועלת.
9.17.5. להרחיב הלאה¶
כל מה שמעבר ל-TLS הצפנה-בלבד – אימות תעודה של שרת HTTPS ציבורי, הפעלת המצלמה כשרת TLS מאומת, TLS הדדי בין המצלמה ל-back-end, בחירת מפתחות וסוגי מפתחות, התמודדות עם פקיעת תעודה – נמצא ב-עבודה עם אישורי TLS. חלק זה מכסה כיצד ליצור תעודות בחתימה עצמית לבדיקות מקומיות, כיצד להשיג תעודות חתומות בידי CA לייצור, כיצד להעביר אותן אל המצלמה בפורמט הנכון (DER), כיצד לאמת שרת ציבורי כשהמצלמה היא הלקוח, כיצד לחשוב על הגנת מפתחות בהתקן שתוקף עשוי לפרק, וכיצד לתכנן ליום שבו התעודה פוקעת.
עבור הפניית ה-API המלאה של ssl – גרסאות TLS נתמכות, חבילות צפנים, ואפשרויות הקשר – ראה ssl — מודול SSL/TLS.