14.4.5. التحقق من خادم عام (الكاميرا كعميل)

كل ما ورد في الصفحة السابقة عن أن العميل "يملك الجذر بالفعل" صحيح بالنسبة للمتصفحات والهواتف وأجهزة الكمبيوتر، لكنه ليس صحيحاً بالنسبة للكاميرا. لا تأتي وحدة ssl في MicroPython بأي مخزن ثقة مدمج: فالكاميرا حديثة التحديث لا تثق بأي جهة تصديق على الإطلاق، والوضع الافتراضي (ssl.CERT_NONE) لا يتحقق من أي شيء ويكون مفتوحاً على مصراعيه أمام هجوم الوسيط. لذا عندما تكون الكاميرا هي العميل الذي يتصل بخادم TLS عام (واجهة برمجة تطبيقات HTTPS، أو وسيط MQTT، ...) وتريد منها أن تتحقق فعلاً من ذلك الخادم، فعليك أن توفّر مرساة الثقة بنفسك.

الآلية مماثلة لمثال العميل ذي التوقيع الذاتي في الشهادات الموقّعة ذاتياً؛ والفرق الوحيد هو أن الملف الذي تحمّله هو شهادة جهة تصديق حقيقية بدلاً من شهادة النظير الخاصة به:

  1. احصل على شهادة جهة التصديق التي ترسي سلسلة الخادم. "الإرساء" يعني الشهادة الموجودة في (أو قرب) أعلى سلسلة الخادم والتي تختارها نقطة انطلاق ثقتك. يرسل خادم TLS شهادته الطرفية وعادة شهادته (شهاداته) الوسيطة؛ لكنه لا يرسل أبداً جذره. عليك أن تحصل على مرساة الثقة تلك بنفسك وبشكل مستقل عن الخادم -- فمجرد الوثوق بأي شيء يسلّمه لك الخادم يبطل الغرض الكامل من التحقق.

    اكتشف أولاً أي جهة تصديق أصدرت فعلاً شهادة الخادم. على سبيل المثال، مقابل openmv.io

    openssl 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). تنشر جهات التصديق الأخرى جذورها في صفحة "شهادات الجذر" / "المستودع" مماثلة؛ والمجموعة العامة المرجعية هي برنامج جهات تصديق Mozilla (CCADB). تأكّد من أنك جلبت الملف الصحيح بمقارنة بصمته بالقيمة التي تنشرها جهة التصديق (أضف -inform DER إذا نزّلت ملف .der):

    openssl x509 -in isrgrootx1.pem -noout -subject -fingerprint -sha256
    

    إذا كنت تفضّل عدم تتبّع جذر، يمكنك بدلاً من ذلك نسخ الشهادة الوسيطة مباشرةً من مُخرَج -showcerts (كتلة -----BEGIN CERTIFICATE----- الثانية)، والوثوق بها، وقبول أنه يجب عليك تحديثها كلما دوّرت جهة التصديق الشهادة الوسيطة -- وهو ما يحدث أكثر بكثير من الجذر (انظر المفاضلة أدناه).

  2. حوّلها إلى DER، تماماً كما في السابق:

    openssl x509 -in isrgrootx1.pem -outform DER -out ca.der
    
  3. انسخ 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 هي جهة التصديق العامة الأوسع استخداماً، وتتسلسل كل من شهاداتها من نوعي RSA و ECDSA حالياً إلى ISRG Root X1 (كما يبيّن مثال openmv.io أعلاه). إذا كانت الخوادم التي تتحدث معها كاميرتك تستخدم Let's Encrypt، فيمكنك تخطّي الفحص كلياً: ما عليك سوى وضع isrgrootx1.der على الكاميرا وتمريره إلى load_verify_locations.

هذا لا يجعل TLS يعمل مع كل موقع. فالخادم الذي تأتي شهادته من جهة تصديق مختلفة (DigiCert أو Google Trust Services أو Amazon أو Sectigo، ...) سيظل يفشل في التحقق، ولأن الكاميرا تثق بشهادة DER واحدة لكل ssl.SSLContext فلا يمكنك تجميع كل جذر بالطريقة التي يفعلها المتصفح. عند الشك، حدّد جهة التصديق الفعلية للخادم كما هو موضّح أعلاه وثق بذلك الجذر.

أي شهادة تثق بها هي مسألة مفاضلة:

  • الجذر (موصى به). طويل العمر -- غالباً عقوداً -- بحيث نادراً ما يتغيّر ca.der. يتطلّب من الخادم إرسال شهادته الوسيطة حتى يتمكن mbedTLS من بناء المسار الطرفية ← الوسيطة ← جذرك الموثوق؛ وعملياً كل خادم عام مُهيّأ بشكل صحيح يفعل ذلك.

  • الشهادة الوسيطة. تعمل أيضاً، وتظل تعمل حتى لو أغفل خادم إرسال شهادته الوسيطة، لكن الشهادات الوسيطة تُدوّر أكثر بكثير من الجذور، لذا سيتعيّن عليك تحديث ca.der بوتيرة أكبر.

  • الشهادة الطرفية نفسها (تثبيت الشهادة). الأكثر إحكاماً، لكن الشهادة الطرفية تتغيّر مع كل تجديد -- كل 90 يوماً تقريباً مع Let's Encrypt -- لذا لا يكون هذا منطقياً إلا عندما تتحكم أنت أيضاً بالخادم وتستطيع دفع التثبيت الجديد إلى كل كاميرا بالتزامن. وهذا بالضبط ما يفعله مثال العميل ذي التوقيع الذاتي.

ملاحظة

تأخذ ssl.SSLContext.load_verify_locations() شهادة جهة تصديق واحدة مُرمّزة بصيغة DER، لذا تثق الكاميرا بمرساة واحدة بالضبط في كل مرة. للوصول إلى خوادم تحت جهات تصديق مختلفة، استخدم ssl.SSLContext منفصلاً لكل مرساة. ولأن تلك الشهادة نفسها ستنتهي صلاحيتها في النهاية أو تُدوّرها جهة التصديق، عاملها مثل أي شهادة أخرى على الجهاز.