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 กับเซิร์ฟเวอร์ที่ตอบรับและนำมาใช้
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