14.4.5. אימות שרת ציבורי (המצלמה כלקוח)¶
כל מה שנאמר בעמוד הקודם על כך שלקוח ”כבר מחזיק בשורש“ נכון לגבי דפדפנים, טלפונים ומחשבים – אבל הוא אינו נכון לגבי המצלמה. ה-ssl של MicroPython מגיע ללא מאגר אמון מובנה: מצלמה שזה עתה הוטענה בקושחה אינה סומכת על אף רשות אישורים (CA) כלל, וברירת המחדל (ssl.CERT_NONE) אינה מאמתת דבר וחשופה לחלוטין להתקפת man-in-the-middle. לכן כשהמצלמה היא הלקוח שמתחבר החוצה אל שרת TLS ציבורי (API מסוג HTTPS, ברוקר MQTT, …) ואתה רוצה שהיא תאמת באמת את אותו שרת, עליך לספק בעצמך את עוגן האמון.
המנגנון זהה לדוגמת הלקוח עם אישור עצמי-חתום ב-תעודות חתומות-עצמית; ההבדל היחיד הוא שהקובץ שאתה טוען הוא אישור CA אמיתי במקום האישור של העמית עצמו:
השג את אישור ה-CA שמעגן את שרשרת השרת. ”מעגן“ משמעו האישור שנמצא בראש (או סמוך לראש) שרשרת השרת, אותו אתה בוחר כנקודת ההתחלה של האמון שלך. שרת TLS שולח את אישור העלה שלו ובדרך כלל גם את האישור(ים) הביניים שלו; הוא לעולם אינו שולח את השורש שלו. עליך להשיג את עוגן האמון הזה בעצמך באופן בלתי תלוי בשרת – פשוט לסמוך על מה שהשרת מגיש לך היה מבטל את כל מטרת האימות.
ראשית גלה איזו CA הנפיקה בפועל את אישור השרת. לדוגמה, מול
openmv.ioopenssl s_client -connect openmv.io:443 -showcerts < /dev/nullבלוק ה-
Certificate chainמפרט כל אישור עם הנושא שלו (s:) והמנפיק שלו (i:); גרסאות חדשות יותר של OpenSSL מדפיסות גם שורותa:(סוג המפתח) ו-v:(תוקף) שאפשר להתעלם מהן כאן:Certificate chain 0 s:CN=openmv.io i:C=US, O=Let's Encrypt, CN=E8 1 s:C=US, O=Let's Encrypt, CN=E8 i:C=US, O=Internet Security Research Group, CN=ISRG Root X1
רשומה 0 היא העלה (
openmv.io), שהונפק על ידי הבינייםE8. רשומה 1 היא אותו ביניים, שהונפק על ידי השורשISRG Root X1. המנפיק (i:) של הרשומה העליונה ביותר נוקב בשם השורש – כאןISRG Root X1. (הביניים הואE8ולא ה-R10/R11שאולי ראית במקומות אחרים מכיוון ש-openmv.ioמשתמש באישור ECDSA; Let’s Encrypt חותם על עלי ECDSA עם אישורי הביניים מסדרתEשלו, ועל עלי RSA עם אלה מסדרתR. שניהם משתרשרים אלISRG Root X1.)OpenSSL מדפיס גם שורות
depth=ועשוי לדווח על השורש עםVerification: OK. זה קורה רק מכיוון שהמחשב שלך כבר סומך עלISRG Root X1– השרת לא שלח אותו (שרת לעולם אינו שולח את השורש שלו), ולמצלמה, שאין לה מאגר אמון, גם לא יהיה אותו. זו בדיוק הסיבה שעליך לספק אותו.הורד את אותו שורש ממאגר השורשים שהרשות עצמה מפרסמת. Let’s Encrypt מקטלגת את כל שלה בעמוד האישורים של Let’s Encrypt; הקובץ הישיר ל-ISRG Root X1 הוא isrgrootx1.pem (היא מציעה אותו גם מקודד מראש כ-isrgrootx1.der). רשויות CA אחרות מפרסמות את שלהן בעמוד ”אישורי שורש“ / ”מאגר“ דומה; הקבוצה הציבורית הקנונית היא תוכנית ה-CA של Mozilla (CCADB). ודא שהורדת את הקובץ הנכון על ידי השוואת טביעת האצבע שלו לערך שהרשות מפרסמת (הוסף
-inform DERאם הורדת את ה-.der):openssl x509 -in isrgrootx1.pem -noout -subject -fingerprint -sha256אם אתה מעדיף לא לעקוב אחר שורש, אתה יכול במקום זאת להעתיק את הביניים ישירות מתוך פלט ה-
-showcerts(בלוק ה------BEGIN CERTIFICATE-----השני), לסמוך עליו, ולקבל שתצטרך לרענן אותו בכל פעם שהרשות מסובבת את הביניים – לעיתים קרובות הרבה יותר מאשר את השורש (ראה את שיקול הדעת למטה).המר אותו ל-DER, בדיוק כמו קודם:
openssl x509 -in isrgrootx1.pem -outform DER -out ca.derהעתק את
ca.derאל המצלמה (מערכת הקבצים או ROMFS) וטען אותו כעוגן האמון:import socket import ssl import ntptime ntptime.settime() # validity check needs the clock addr = socket.getaddrinfo("api.example.com", 443)[0][-1] sock = socket.socket() sock.connect(addr) ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) ctx.verify_mode = ssl.CERT_REQUIRED ctx.load_verify_locations(cafile="ca.der") ssock = ctx.wrap_socket(sock, server_hostname="api.example.com")
server_hostnameנדרש כאן: הוא מניע את SNI והוא השם שנבדק מול ה-subjectAltNameשל אישור השרת.
טיפ
קיצור דרך למקרה הנפוץ. Let’s Encrypt היא ה-CA הציבורית הנפוצה ביותר, וכרגע גם אישורי ה-RSA וגם אישורי ה-ECDSA שלה משתרשרים אל ISRG Root X1 (כפי שמראה דוגמת openmv.io למעלה). אם השרתים שהמצלמה שלך מדברת איתם משתמשים ב-Let’s Encrypt, אתה יכול לדלג על הבדיקה לחלוטין: פשוט שים את isrgrootx1.der על המצלמה והפעל עליו load_verify_locations.
זה אינו גורם ל-TLS לעבוד מול כל אתר. שרת שאישורו מגיע מ-CA אחרת (DigiCert, Google Trust Services, Amazon, Sectigo, …) עדיין ייכשל באימות, ומכיוון שהמצלמה סומכת על אישור DER יחיד לכל ssl.SSLContext אינך יכול לצרף כל שורש כפי שדפדפן עושה. כשמתעורר ספק, זהה את ה-CA האמיתית של השרת כפי שמוצג למעלה וסמוך על אותו שורש.
על איזה אישור אתה סומך הוא שיקול דעת:
השורש (מומלץ). ארוך-טווח – לעיתים קרובות עשרות שנים – כך ש-
ca.derישתנה לעיתים נדירות. הוא מחייב שהשרת ישלח את הביניים שלו כדי ש-mbedTLS יוכל לבנות את המסלול עלה ← ביניים ← השורש המהימן שלך; כמעט כל שרת ציבורי מוגדר נכון עושה זאת.הביניים. עובד גם כן, וממשיך לעבוד אפילו אם שרת משמיט את הביניים, אך אישורי ביניים מסובבים לעיתים קרובות הרבה יותר משורשים, כך שתצטרך לרענן את
ca.derבתדירות גבוהה יותר.העלה עצמו (certificate pinning). ההדוק ביותר, אך העלה משתנה בכל חידוש – בערך כל 90 יום עבור Let’s Encrypt – כך שזה הגיוני רק כשאתה גם שולט בשרת ויכול לדחוף את ה-pin החדש לכל מצלמה בו-זמנית. זה בדיוק מה שעושה דוגמת הלקוח עם אישור עצמי-חתום.
הערה
ssl.SSLContext.load_verify_locations() מקבל אישור CA יחיד מקודד-DER, כך שהמצלמה סומכת בדיוק על עוגן אחד בכל פעם. כדי להגיע לשרתים תחת CA שונות, השתמש ב-ssl.SSLContext נפרד לכל עוגן. ומכיוון שאותו אישור עצמו יפוג בסופו של דבר או יסובב על ידי ה-CA, התייחס אליו כמו לכל אישור אחר במכשיר.