9.17. المقابس المشفّرة وبروتوكول TLS

كل ما تناولناه حتى الآن ينقل البايتات في صورة نصية مكشوفة. أي جهاز يقع على المسار بين الكاميرا والخادم -- موجّه الشبكة المنزلي، ومزوّد خدمة الإنترنت، ونقطة وصول خبيثة في مقهى -- يمكنه من حيث المبدأ قراءة ما يمر عبره أو تعديله. وبالنسبة لمعظم حركة مرور الإنترنت لا يُعد هذا مقبولًا. والحل القياسي هو تغليف الاتصال بطبقة من التشفير: TLS، أي بروتوكول أمان طبقة النقل. إن أيقونة القفل الخاصة بـ "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 فحسب على مفتاح جلسة مؤقت مع أي خادم يستجيب ويستخدمه.

A diagram with two columns labelled "client" and "server". A dashed horizontal line near the top is labelled "TCP connection already open". Below it, three arrows show the TLS handshake: "ClientHello" from client to server, "ServerHello + certificate + key share" back, and "Finished" forward again. A second dashed horizontal line below is labelled "TLS session open -- everything after this is encrypted". Two thick bidirectional arrows below it carry "encrypted data".

مصافحة TLS التي تُجريها ssl.wrap_socket(). تقع فوق اتصال TCP المفتوح بالفعل من الشكل السابق؛ وبمجرد أن يرسل كلا الطرفين Finished، يصبح بقية الحوار مشفّرًا في كلا الاتجاهين.

تحذير

هذا تشفير فقط، وليس TLS مُصادَقًا. تتحدث الكاميرا بأمان مع أي شيء استجاب على الطرف الآخر من اتصال TCP. فإذا أعاد وسيط متطفّل توجيه الاتصال إلى خادم يتحكم به وقدّم ذلك الخادم أي شهادة، فإن المصافحة تنجح رغم ذلك وينتهي الأمر بالكاميرا متحدثةً بأمان مع المهاجم. استخدم هذا الوضع فقط عندما لا يكون الوسيط المتطفّل جزءًا من نموذج التهديد -- شبكة محلية مغلقة، أو بيئة تطوير، أو الكاميرا تتحدث مع خدمة تعمل على العتاد نفسه -- وليس عند الوصول إلى الإنترنت العام.

للمصادقة الحقيقية -- تحقق الكاميرا من خادم عام، أو عمل الكاميرا كخادم TLS، أو TLS المتبادل -- تحتاج إلى جلب شهادات إلى الجهاز. القصة الكاملة موجودة في العمل مع شهادات TLS.

يعمل التغليف نفسه مع حركة مرور TCP الواردة، باختيار بروتوكول الخادم وتمرير server_side=True إلى ssl.wrap_socket(). وما زال التحذير أعلاه ساريًا: فبدون شهادة خاصة بها لا تستطيع الكاميرا إثبات هويتها للعميل، وسيرى العميل الفضولي فشلًا في مصافحة "لا شهادة" على معظم حزم 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())

زوج القارئ/الكاتب خلف اتصال TLS له الشكل نفسه كما في اتصال TCP العادي -- الفارق في الإعداد وحده. وينطبق التحذير نفسه بشأن المصادقة: فاستخدام ssl=True وحده يمنح التشفير فقط، لا التحقق.

9.17.4. DTLS -- بروتوكول TLS فوق UDP

TLS كما نوقش حتى الآن يعمل فوق TCP. والبروتوكول الموازي لـ UDP هو DTLS (أي 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 الكامل -- الشهادات، وملف تعريف الارتباط المضاد للانتحال على جانب الخادم، وكيف أن هذا هو السطح نفسه كـ TLS باستثناء ثوابت البروتوكول -- مُغطّى إلى جانب مادة TLS في العمل مع شهادات TLS.

تستخدم نسخة asyncio النمط نفسه لـ UDP غير الحاجب من المقابس مع asyncio. أجرِ المصافحة بشكل متزامن مقدمًا، وحوّل المقبس إلى الوضع غير الحاجب، ثم استطلِع داخل دالة مشتركة (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)

المصافحة هي الموضع الوحيد الذي تحجب فيه هذه الدالة المشتركة حلقة الأحداث؛ وبعد ذلك، يعود كل s.send / s.recv على الفور (أو يطلق OSError)، ويُبقي await asyncio.sleep_ms بقية البرنامج قيد التشغيل.

9.17.5. للمضي أبعد

كل ما هو أكثر من TLS بالتشفير فقط -- التحقق من شهادة خادم HTTPS عام، وتشغيل الكاميرا كخادم TLS مُصادَق، وTLS المتبادل بين الكاميرا وخلفية النظام، واختيار المفاتيح وأنواعها، والتعامل مع انتهاء صلاحية الشهادة -- موجود في العمل مع شهادات TLS. يتناول ذلك القسم كيفية توليد شهادات موقّعة ذاتيًا للاختبار المحلي، وكيفية الحصول على شهادات موقّعة من سلطة إصدار للإنتاج، وكيفية إدخالها إلى الكاميرا بالتنسيق الصحيح (DER)، وكيفية التحقق من خادم عام حين تكون الكاميرا هي العميل، وكيفية التفكير في حماية المفاتيح على جهاز قد يفككه مهاجم، وكيفية التخطيط ليوم انتهاء صلاحية الشهادة.

للاطلاع على المرجع الكامل لواجهة ssl -- إصدارات TLS المدعومة، ومجموعات الشفرات، وخيارات السياق -- راجع ssl --- وحدة SSL/TLS.