9.17. ซ็อกเก็ตที่เข้ารหัสและ TLS#

ทุกอย่างที่กล่าวถึงจนถึงตอนนี้จะส่งข้อมูลผ่านเครือข่ายในรูปแบบที่ไม่ได้เข้ารหัส อุปกรณ์ใดก็ตามที่อยู่บนเส้นทางระหว่างกล้องและเซิร์ฟเวอร์ -- เราเตอร์ภายในบ้าน ผู้ให้บริการอินเทอร์เน็ต หรือ access point ที่เป็นอันตรายในร้านกาแฟ -- อาจอ่านหรือแก้ไขข้อมูลที่ผ่านไปได้ สำหรับการรับส่งข้อมูลอินเทอร์เน็ตส่วนใหญ่นั้นไม่เป็นที่ยอมรับ วิธีแก้ไขมาตรฐานคือการห่อหุ้มการเชื่อมต่อด้วยชั้นการเข้ารหัส: TLS หรือโปรโตคอล Transport Layer Security ไอคอนล็อก "HTTPS" ในเบราว์เซอร์คือ TLS ที่ทำงานบน TCP และการห่อหุ้มแบบเดียวกันนี้คือสิ่งที่ทำให้โปรโตคอลอินเทอร์เน็ตอื่นๆ "ปลอดภัย" โมดูล ssl ของกล้องคือสิ่งที่ห่อหุ้ม socket ด้วย TLS

9.17.1. สิ่งที่ TLS เพิ่มเติมและสิ่งที่กล้องมาพร้อมกัน#

TLS อยู่ระหว่าง TCP กับแอปพลิเคชัน -- แอปพลิเคชันเขียนไบต์ลงใน TLS-wrapped socket, TLS เข้ารหัสแล้วส่งผลลัพธ์ไปยัง TCP และกระบวนการจะย้อนกลับทางอีกด้าน ในรูปแบบที่สมบูรณ์ TLS ให้การรับประกันสามประการเพิ่มเติมจาก TCP ธรรมดา:

  • ความลับ ผู้ดักฟังบนเส้นทางไม่สามารถอ่านสิ่งที่ทั้งสองปลายทางกำลังแลกเปลี่ยนกันได้

  • ความสมบูรณ์ การแก้ไขใดๆ ของการรับส่งข้อมูลระหว่างทางจะถูกตรวจจับ การเชื่อมต่อจะขาดแทนที่จะส่งข้อมูลที่ถูกดัดแปลง

  • การพิสูจน์ตัวตน เซิร์ฟเวอร์พิสูจน์ว่าเป็นเซิร์ฟเวอร์ที่ระบุชื่อ ไม่ใช่ตัวแอบอ้าง (และไคลเอนต์อาจพิสูจน์ตัวตนของ ตัวเอง ด้วยเช่นกัน)

สองข้อแรกมาจากการเข้ารหัสเอง ข้อที่สามต้องการ ใบรับรอง อย่างน้อยหนึ่งฝ่าย บวกกับสิ่งที่เชื่อถือได้ล่วงหน้าเพื่อตรวจสอบใบรับรองเหล่านั้น OpenMV cam มาพร้อมกับ ไม่มี certificate store ในตัวเลย: กล้องที่เพิ่งแฟลชใหม่จะไม่เชื่อถือ certificate authority ใดๆ ไม่มี server certificate เป็นของตัวเอง และโหมดการตรวจสอบเริ่มต้น (ssl.CERT_NONE) จะไม่ตรวจสอบใบรับรองของ peer กับสิ่งใดเลย ดังนั้นโดยค่าเริ่มต้น TLS บนกล้องจะให้การรับประกันสองข้อแรก -- การเข้ารหัสต่อต้านการดักฟังและการดัดแปลงโดยผู้สังเกตการณ์แบบ passive -- แต่ ไม่ใช่ ข้อที่สาม

9.17.2. การเข้ารหัสการเชื่อมต่อขาออก#

การใช้งานที่ง่ายที่สุดคือการห่อหุ้มการเชื่อมต่อ TCP ขาออก ขั้นตอนคือ: เปิด TCP socket ธรรมดา ส่งให้กับ ssl.wrap_socket() แล้วอ่านและเขียนผ่าน wrapped socket ในแบบเดียวกับที่ใช้กับ 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 handshake หลังจากนั้นทุกไบต์ผ่าน s.send จะถูกเข้ารหัสขาออก และทุกไบต์จาก s.recv ถูกเข้ารหัสบนสาย ไม่มีการกำหนดค่าใบรับรอง ไม่มีการระบุ trust anchor -- TLS เพียงแค่เจรจา ephemeral session key กับเซิร์ฟเวอร์ที่ตอบรับและนำมาใช้

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 handshake ที่ ssl.wrap_socket() ดำเนินการ อยู่บนการเชื่อมต่อ TCP ที่เปิดอยู่แล้วจากรูปก่อนหน้า เมื่อทั้งสองฝ่ายส่ง Finished แล้ว การสนทนาที่เหลือจะถูกเข้ารหัสทั้งสองทิศทาง#

Warning

นี่คือการเข้ารหัสเท่านั้น ไม่ใช่ TLS ที่พิสูจน์ตัวตน กล้องจะสื่อสารอย่างปลอดภัยกับ อะไรก็ตาม ที่ตอบรับที่ปลายอีกด้านของการเชื่อมต่อ TCP หากผู้โจมตีแบบ man-in-the-middle เปลี่ยนเส้นทางการเชื่อมต่อไปยังเซิร์ฟเวอร์ที่ตนควบคุมและเซิร์ฟเวอร์นั้นนำเสนอ ใบรับรองใดก็ได้ handshake ก็ยังสำเร็จและกล้องก็จะสื่อสารอย่างปลอดภัยกับผู้โจมตี ใช้โหมดนี้เฉพาะเมื่อ man-in-the-middle ไม่ใช่ส่วนหนึ่งของ threat model -- เครือข่ายท้องถิ่นที่ปิด สภาพแวดล้อมการพัฒนา กล้องที่สื่อสารกับบริการที่ทำงานบน hardware เดียวกัน -- ไม่ใช่ เมื่อเชื่อมต่อไปยังอินเทอร์เน็ตสาธารณะ

สำหรับการพิสูจน์ตัวตนจริงๆ -- กล้องตรวจสอบเซิร์ฟเวอร์สาธารณะ กล้องทำหน้าที่เป็น TLS server หรือ mutual TLS -- คุณต้องนำใบรับรองมาใส่ในอุปกรณ์ เรื่องราวทั้งหมดอยู่ใน การทำงานกับ TLS certificate

การห่อหุ้มแบบเดียวกันนี้ใช้ได้กับการรับส่ง TCP ขาเข้าด้วย โดยการเลือก server protocol และส่ง server_side=True ไปยัง ssl.wrap_socket() คำเตือนข้างต้นยังคงใช้ได้: หากไม่มีใบรับรองของตัวเอง กล้องไม่สามารถพิสูจน์ตัวตนต่อ client ได้ และ client ที่อยากรู้อยากเห็นจะเห็นความล้มเหลวของ handshake "no certificate" บน TLS stack ส่วนใหญ่ เวิร์กโฟลว์ production-side cert คือสิ่งที่ปลดล็อกการรันกล้องเป็น TLS server ในแบบที่มีประโยชน์

9.17.3. ด้วย asyncio#

บท asyncio แสดง asyncio.open_connection() สำหรับ TCP client ธรรมดา การเรียกแบบเดียวกันรับคีย์เวิร์ด 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 หนึ่งรายการเป็น encrypted byte stream หนึ่งรายการ ในขณะที่ DTLS แปลง UDP socket หนึ่งรายการเป็นสตรีมของ encrypted datagram ที่ส่งแยกกัน -- ดังนั้นคุณสมบัติการสูญหาย / ไม่เป็นลำดับ / ไม่มี flow control ของ UDP จาก UDP -- ส่งแพ็กเก็ตแล้วหวังว่าจะดีที่สุด ทั้งหมดยังคงอยู่ โดยไบต์ภายในแต่ละ datagram ถูกเข้ารหัสแล้ว

การห่อหุ้มมีลักษณะเหมือนกับกรณี TLS เพียงแต่ใช้ SOCK_DGRAM socket และค่าคงที่ของโปรโตคอล 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 socket ไม่ได้เปิดการเชื่อมต่อ -- เพียงแต่จำปลายทางเริ่มต้นเพื่อให้การเรียก send() / recv() ครั้งถัดไปไม่ต้องระบุซ้ำ DTLS ต้องการปลายทางที่ตายตัวนั้นเพื่อดำเนินการ handshake)

handshake มีรูปร่างเหมือนกับแผนภาพ TLS ข้างต้น ความแตกต่างคือแต่ละข้อความ handshake เป็น UDP datagram เอง และทั้งสองฝ่ายจะลองส่งใหม่เมื่อเกิดการสูญหาย

Note

การสูญเสียแพ็กเก็ตทำให้การเข้ารหัสเสียหายหรือไม่? ไม่ แต่ละแพ็กเก็ต DTLS มีหมายเลขลำดับ และการเข้ารหัสใช้หมายเลขนั้นเพื่อสร้างผลลัพธ์ที่แตกต่างกันสำหรับแต่ละแพ็กเก็ต -- ดังนั้นอินพุตเดียวกันจะไม่เข้ารหัสเป็นไบต์เดิมสองครั้ง และสามารถถอดรหัสแพ็กเก็ตใดก็ได้โดยไม่ต้องรอให้แพ็กเก็ตก่อนหน้ามาถึง แพ็กเก็ตที่สูญหายหรือมาผิดลำดับจะไม่ทำให้ทั้งสองฝ่ายไม่สอดคล้องกัน (handshake เป็นส่วนเดียวที่ต้องมาถึงอย่างน่าเชื่อถือ และ DTLS จัดการด้วยการส่งใหม่ของตัวเอง)

คำเตือนเกี่ยวกับการเข้ารหัสเท่านั้นโดยไม่มีใบรับรองจากข้างต้นยังคงใช้ได้: DTLS handshake กับ peer CERT_NONE จะเข้ารหัสการรับส่งข้อมูลแต่ไม่ตรวจสอบว่าอีกฝ่ายเป็นใคร เวิร์กโฟลว์ DTLS แบบสมบูรณ์ -- ใบรับรอง, anti-spoofing cookie ของ server-side, วิธีที่นี่มีพื้นผิวเดียวกับ TLS นอกจากค่าคงที่ของโปรโตคอล -- มีอยู่ในส่วน TLS ใน การทำงานกับ TLS certificate

เวอร์ชัน asyncio ใช้รูปแบบ non-blocking-UDP เดียวกันจาก Sockets กับ asyncio ทำ handshake แบบ synchronous ก่อน แล้วสลับ socket เป็น non-blocking จากนั้น 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)

handshake เป็นจุดเดียวที่ coroutine นี้บล็อก event loop หลังจากนั้น s.send / s.recv ทุกครั้งจะคืนค่าทันที (หรือ raise OSError) และ await asyncio.sleep_ms ทำให้โปรแกรมที่เหลือทำงานต่อไปได้

9.17.5. ขั้นตอนต่อไป#

ทุกสิ่งที่ มากกว่า TLS ที่เข้ารหัสเท่านั้น -- การตรวจสอบใบรับรองของ HTTPS server สาธารณะ การรันกล้องเป็น authenticated TLS server, mutual TLS ระหว่างกล้องและ back-end การเลือก key และประเภท key การจัดการกับ certificate หมดอายุ -- อยู่ใน การทำงานกับ TLS certificate ส่วนนั้นครอบคลุมวิธีสร้าง self-signed certificate สำหรับการทดสอบในเครื่อง วิธีรับ CA-signed certificate สำหรับ production วิธีนำมาใส่ในกล้องในรูปแบบที่ถูกต้อง (DER) วิธีตรวจสอบ public server เมื่อกล้องเป็น client วิธีคิดเรื่องการปกป้อง key บนอุปกรณ์ที่ผู้โจมตีอาจถอดออกได้ และวิธีวางแผนสำหรับวันที่ใบรับรองหมดอายุ

สำหรับ API reference ของ ssl แบบสมบูรณ์ -- เวอร์ชัน TLS ที่รองรับ cipher suite และตัวเลือก context -- ดูที่ ssl --- โมดูล SSL/TLS