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