9.17. Verschlüsselte Sockets und TLS¶
Alles, was bisher behandelt wurde, bewegt Bytes im Klartext umher. Jedes Gerät auf dem Weg zwischen der Kamera und dem Server – der Heimrouter, der Internetdienstanbieter, ein bösartiger Access Point in einem Café – kann grundsätzlich lesen oder verändern, was hindurchläuft. Für den Großteil des Internetverkehrs ist das nicht akzeptabel. Die übliche Lösung besteht darin, die Verbindung in eine Verschlüsselungsschicht zu hüllen: TLS, das Transport Layer Security-Protokoll. Das „HTTPS“-Schlosssymbol in einem Browser ist TLS, das über TCP läuft, und dieselbe Umhüllung macht auch jedes andere Internetprotokoll „sicher“. Das ssl-Modul der Kamera ist das, was einen socket in TLS hüllt.
9.17.1. Was TLS hinzufügt und womit die Kamera ausgeliefert wird¶
TLS sitzt zwischen TCP und der Anwendung – die Anwendung schreibt Bytes in einen TLS-umhüllten Socket, TLS verschlüsselt sie und übergibt das Ergebnis an TCP, und auf der anderen Seite wird der Vorgang umgekehrt. In seiner vollen Form gibt TLS zusätzlich zum einfachen TCP drei Garantien:
Vertraulichkeit. Lauscher auf dem Weg können nicht lesen, was die beiden Endpunkte austauschen.
Integrität. Jede Veränderung des Verkehrs während der Übertragung wird erkannt; die Verbindung bricht ab, anstatt manipulierte Daten zu liefern.
Authentifizierung. Der Server beweist, dass er der benannte Server ist und kein Hochstapler (und optional beweist auch der Client, wer er ist).
Die ersten beiden ergeben sich aus der Verschlüsselung selbst. Die dritte erfordert Zertifikate auf mindestens einer Seite sowie etwas Vorab-Vertrautes, anhand dessen diese Zertifikate überprüft werden können. Die OpenMV Cam wird ganz ohne integrierten Zertifikatspeicher ausgeliefert: Eine frisch geflashte Kamera vertraut keiner Zertifizierungsstelle, hat kein eigenes Serverzertifikat, und der Standard-Verifizierungsmodus (ssl.CERT_NONE) prüft das Zertifikat der Gegenstelle nicht gegen irgendetwas. Standardmäßig liefert TLS auf der Kamera also die ersten beiden Garantien – Verschlüsselung gegen Abhören und Manipulation durch einen passiven Beobachter – aber nicht die dritte.
9.17.2. Verschlüsseln einer ausgehenden Verbindung¶
Die einfachste Anwendung ist das Umhüllen einer ausgehenden TCP-Verbindung. Der Ablauf ist: einen normalen TCP-Socket öffnen, ihn an ssl.wrap_socket() übergeben und dann über den umhüllten Socket genauso lesen und schreiben, wie man es mit dem einfachen tun würde:
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()
Das Umhüllen führt den TLS-Handshake durch; danach wird jedes Byte über s.send auf dem Weg nach draußen verschlüsselt, und jedes Byte von s.recv war auf der Leitung verschlüsselt. Es wurden keine Zertifikate konfiguriert, kein Vertrauensanker bereitgestellt – TLS handelt einfach mit dem antwortenden Server einen kurzlebigen Sitzungsschlüssel aus und verwendet ihn.
Der TLS-Handshake, den ssl.wrap_socket() ausführt. Er sitzt auf der bereits offenen TCP-Verbindung aus der vorherigen Abbildung; sobald beide Seiten Finished gesendet haben, ist der Rest der Unterhaltung in beide Richtungen verschlüsselt.¶
Warnung
Dies ist reine Verschlüsselung, kein authentifiziertes TLS. Die Kamera kommuniziert sicher mit demjenigen, der am anderen Ende der TCP-Verbindung geantwortet hat. Wenn ein Man-in-the-Middle die Verbindung auf einen von ihm kontrollierten Server umleitet und dieser Server irgendein Zertifikat vorlegt, gelingt der Handshake trotzdem, und die Kamera kommuniziert am Ende sicher mit dem Angreifer. Verwenden Sie diesen Modus nur, wenn ein Man-in-the-Middle nicht Teil des Bedrohungsmodells ist – ein geschlossenes lokales Netzwerk, eine Entwicklungsumgebung, die Kamera, die mit einem Dienst auf derselben Hardware kommuniziert – nicht, wenn Sie ins öffentliche Internet hinausreichen.
Für echte Authentifizierung – die Kamera, die einen öffentlichen Server verifiziert, die Kamera, die als TLS-Server agiert, oder gegenseitiges TLS – müssen Sie Zertifikate auf das Gerät bringen. Die vollständige Geschichte finden Sie in Arbeiten mit TLS-Zertifikaten.
Dieselbe Umhüllung funktioniert für eingehenden TCP-Verkehr, indem man das Serverprotokoll auswählt und server_side=True an ssl.wrap_socket() übergibt. Die obige Warnung gilt weiterhin: Ohne ein eigenes Zertifikat kann die Kamera dem Client nicht beweisen, wer sie ist, und ein neugieriger Client würde auf den meisten TLS-Stacks einen „no certificate“-Handshake-Fehler sehen. Der produktionsseitige Zertifikat-Workflow ist das, was den sinnvollen Betrieb der Kamera als TLS-Server freischaltet.
9.17.3. Mit asyncio¶
Das asyncio-Kapitel zeigte asyncio.open_connection() für einfache TCP-Clients. Derselbe Aufruf akzeptiert ein ssl=True-Schlüsselwort, das die Verbindung in TLS hüllt, wiederum ohne jegliche Zertifikateinrichtung:
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())
Das Reader/Writer-Paar hinter einer TLS-Verbindung hat dieselbe Form wie bei einer einfachen TCP-Verbindung – nur die Einrichtung unterscheidet sich. Derselbe Vorbehalt bezüglich der Authentifizierung gilt: ssl=True allein liefert nur Verschlüsselung, keine Verifizierung.
9.17.4. DTLS – TLS über UDP¶
TLS, wie bisher besprochen, setzt auf TCP auf. Das parallele Protokoll für UDP ist DTLS (Datagram TLS), und das ssl-Modul der Kamera unterstützt es auf dieselbe Weise. Während TLS eine TCP-Verbindung in einen verschlüsselten Byte-Strom verwandelt, verwandelt DTLS einen UDP-Socket in einen Strom verschlüsselter, einzeln zugestellter Datagramme – so übertragen sich die Eigenschaften von UDP aus UDP – sende ein Paket, hoffe auf das Beste (Verlust / falsche Reihenfolge / keine Flusssteuerung) allesamt, wobei die Bytes innerhalb jedes Datagramms nun verschlüsselt sind.
Die Umhüllung sieht genauso aus wie beim TLS-Fall, nur mit einem SOCK_DGRAM-Socket und den DTLS-Protokollkonstanten:
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()
(Der Aufruf von connect() auf einem UDP-Socket öffnet keine Verbindung – er merkt sich nur ein Standardziel, damit nachfolgende send() / recv()-Aufrufe es nicht wiederholen müssen. DTLS benötigt dieses feste Ziel, um seinen Handshake dagegen auszuführen.)
Der Handshake hat dieselbe Form wie das TLS-Diagramm oben; der Unterschied besteht darin, dass jede Handshake-Nachricht selbst ein UDP-Datagramm ist und beide Seiten bei Verlust erneut versuchen.
Bemerkung
Bricht der Verlust von Paketen die Verschlüsselung? Nein. Jedes DTLS-Paket trägt eine Sequenznummer, und die Verschlüsselung verwendet diese Nummer, um für jedes Paket eine andere Ausgabe zu erzeugen – so verschlüsselt dieselbe Eingabe nie zweimal zu denselben Bytes, und jedes Paket kann für sich entschlüsselt werden, ohne dass das vorherige angekommen sein muss. Verlorene oder in falscher Reihenfolge eintreffende Pakete bringen die beiden Seiten nicht aus dem Takt. (Der Handshake selbst ist der eine Teil, der zuverlässig ankommen muss, und DTLS handhabt das mit seiner eigenen Neuübertragung.)
Dieselbe Warnung „reine Verschlüsselung ohne Zertifikate“ von oben gilt: Ein DTLS-Handshake gegen eine CERT_NONE-Gegenstelle verschlüsselt den Verkehr, verifiziert aber nicht, wer die andere Seite ist. Der vollständige DTLS-Workflow – Zertifikate, der serverseitige Anti-Spoofing-Cookie, wie dies abgesehen von den Protokollkonstanten dieselbe Oberfläche wie TLS ist – wird zusammen mit dem TLS-Material in Arbeiten mit TLS-Zertifikaten behandelt.
Die asyncio-Version verwendet dasselbe nicht-blockierende UDP-Muster aus Sockets mit asyncio. Führen Sie den Handshake vorab synchron durch, schalten Sie den Socket auf nicht-blockierend um und pollen Sie dann innerhalb einer 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)
Der Handshake ist die eine Stelle, an der diese Coroutine die Ereignisschleife blockiert; danach kehrt jeder s.send / s.recv sofort zurück (oder löst OSError aus), und das await asyncio.sleep_ms hält den Rest des Programms am Laufen.
9.17.5. Weiterführendes¶
Alles, was mehr ist als reine Verschlüsselung mit TLS – das Verifizieren des Zertifikats eines öffentlichen HTTPS-Servers, der Betrieb der Kamera als authentifizierter TLS-Server, gegenseitiges TLS zwischen der Kamera und einem Back-End, die Wahl von Schlüsseln und Schlüsseltypen, der Umgang mit dem Ablauf von Zertifikaten – finden Sie in Arbeiten mit TLS-Zertifikaten. Dieser Abschnitt behandelt, wie man selbstsignierte Zertifikate für lokale Tests erzeugt, wie man CA-signierte Zertifikate für die Produktion erhält, wie man sie im richtigen Format (DER) auf die Kamera bringt, wie man einen öffentlichen Server verifiziert, wenn die Kamera der Client ist, wie man über den Schlüsselschutz auf einem Gerät nachdenkt, das ein Angreifer auseinandernehmen könnte, und wie man für den Tag plant, an dem das Zertifikat abläuft.
Die vollständige ssl-API-Referenz – unterstützte TLS-Versionen, Cipher Suites und Kontextoptionen – finden Sie unter ssl — SSL/TLS-Modul.