9.17. Versleutelde sockets en TLS

Alles wat tot nu toe is besproken verplaatst bytes onversleuteld. Elk apparaat op het pad tussen de camera en de server – de thuisrouter, de internetprovider, een kwaadaardig toegangspunt in een koffiebar – kan in principe lezen of wijzigen wat erdoorheen gaat. Voor het meeste internetverkeer is dat niet acceptabel. De standaardoplossing is om de verbinding in een versleutelingslaag te verpakken: TLS, het Transport Layer Security-protocol. Het “HTTPS”-slotje in een browser is TLS dat over TCP draait, en diezelfde verpakking is wat elk ander internetprotocol “beveiligd” maakt. De ssl-module van de camera is wat een socket in TLS verpakt.

9.17.1. Wat TLS toevoegt, en wat de cam standaard meelevert

TLS bevindt zich tussen TCP en de toepassing – de toepassing schrijft bytes naar een TLS-verpakte socket, TLS versleutelt ze en geeft het resultaat aan TCP door, en aan de andere kant wordt het proces omgekeerd. In zijn volledige vorm geeft TLS drie garanties bovenop gewone TCP:

  • Vertrouwelijkheid. Afluisteraars op het pad kunnen niet lezen wat de twee eindpunten uitwisselen.

  • Integriteit. Elke wijziging van het verkeer onderweg wordt gedetecteerd; de verbinding wordt verbroken in plaats van geknoeide gegevens af te leveren.

  • Authenticatie. De server bewijst dat hij de genoemde server is, en geen bedrieger (en, optioneel, bewijst de client ook wie hij is).

De eerste twee komen voort uit de versleuteling zelf. De derde heeft certificaten aan ten minste één kant nodig, plus iets vooraf vertrouwds om die certificaten tegen te verifiëren. De OpenMV-cam wordt geleverd met helemaal geen ingebouwde certificaatopslag: een vers geflashte cam vertrouwt geen enkele certificeringsautoriteit, heeft geen eigen servercertificaat, en de standaard verificatiemodus (ssl.CERT_NONE) controleert het certificaat van de peer nergens tegen. Out of the box geeft TLS op de cam je dus de eerste twee garanties – versleuteling tegen afluisteren en knoeien door een passieve waarnemer – maar niet de derde.

9.17.2. Een uitgaande verbinding versleutelen

Het eenvoudigste gebruik is het verpakken van een uitgaande TCP-verbinding. De stroom is: open een normale TCP-socket, geef hem aan ssl.wrap_socket(), en lees en schrijf vervolgens via de verpakte socket precies zoals je dat met de gewone zou doen:

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()

De verpakking voert de TLS-handshake uit; daarna is elke byte via s.send versleuteld op weg naar buiten en was elke byte van s.recv versleuteld op de lijn. Er werden geen certificaten geconfigureerd, geen vertrouwensanker geleverd – TLS onderhandelt simpelweg een tijdelijke sessiesleutel met welke server dan ook antwoordt en gebruikt die.

Een diagram met twee kolommen met de labels "client" en "server". Een gestippelde horizontale lijn bovenaan heeft het label "TCP-verbinding al open". Daaronder tonen drie pijlen de TLS- handshake: "ClientHello" van client naar server, "ServerHello + certificaat + key share" terug, en "Finished" weer vooruit. Een tweede gestippelde horizontale lijn daaronder heeft het label "TLS-sessie open -- alles hierna is versleuteld". Twee dikke bidirectionele pijlen daaronder dragen "versleutelde gegevens".

De TLS-handshake die ssl.wrap_socket() uitvoert. Hij zit bovenop de reeds geopende TCP-verbinding uit de vorige afbeelding; zodra beide kanten Finished hebben verzonden, is de rest van het gesprek in beide richtingen versleuteld.

Waarschuwing

Dit is alleen-versleuteling, geen geauthenticeerde TLS. De cam praat veilig met wat er ook maar aan de andere kant van de TCP-verbinding antwoordde. Als een man-in-the-middle de verbinding omleidt naar een server die hij beheert en die server een willekeurig certificaat presenteert, slaagt de handshake nog steeds en praat de cam uiteindelijk veilig met de aanvaller. Gebruik deze modus alleen wanneer een man-in-the-middle geen deel uitmaakt van het dreigingsmodel – een gesloten lokaal netwerk, een ontwikkelomgeving, de cam die praat met een dienst die op dezelfde hardware draait – niet wanneer je het publieke internet opgaat.

Voor echte authenticatie – de cam die een publieke server verifieert, de cam die als TLS-server fungeert, of wederzijdse TLS – moet je certificaten op het apparaat zetten. Het volledige verhaal staat in Werken met TLS-certificaten.

Dezelfde verpakking werkt voor inkomend TCP-verkeer, door het serverprotocol te selecteren en server_side=True aan ssl.wrap_socket() door te geven. De bovenstaande waarschuwing geldt nog steeds: zonder een eigen certificaat kan de cam niet aan de client bewijzen wie hij is, en een nieuwsgierige client zou op de meeste TLS-stacks een “no certificate”-handshakefout zien. De certificaatworkflow aan de productiekant is wat het zinvol draaien van de cam als TLS-server mogelijk maakt.

9.17.3. Met asyncio

Het asyncio-hoofdstuk toonde asyncio.open_connection() voor gewone TCP-clients. Dezelfde aanroep accepteert een ssl=True-keyword dat de verbinding in TLS verpakt, opnieuw zonder enige certificaatconfiguratie:

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())

Het reader/writer-paar achter een TLS-verbinding heeft dezelfde vorm als bij een gewone TCP-verbinding – alleen de opzet verschilt. Hetzelfde voorbehoud over authenticatie geldt: ssl=True alleen geeft enkel versleuteling, geen verificatie.

9.17.4. DTLS – TLS over UDP

TLS zoals tot nu toe besproken rijdt bovenop TCP. Het parallelle protocol voor UDP is DTLS (Datagram TLS), en de ssl-module van de camera ondersteunt het op dezelfde manier. Waar TLS één TCP-verbinding omzet in één versleutelde bytestroom, zet DTLS één UDP-socket om in een stroom van versleutelde, afzonderlijk afgeleverde datagrammen – dus de eigenschappen van UDP rond verlies / verkeerde volgorde / geen flow control uit UDP – verstuur een pakket, hoop op het beste blijven allemaal van toepassing, met de bytes binnen elk datagram nu versleuteld.

De verpakking ziet er hetzelfde uit als bij TLS, alleen met een SOCK_DGRAM-socket en de DTLS-protocolconstanten:

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() aanroepen op een UDP-socket opent geen verbinding – het onthoudt slechts een standaardbestemming zodat latere send() / recv()-aanroepen die niet hoeven te herhalen. DTLS heeft die vaste bestemming nodig om zijn handshake tegenaan te draaien.)

De handshake heeft dezelfde vorm als het TLS-diagram hierboven; het verschil is dat elk handshakebericht zelf een UDP-datagram is, en elke kant opnieuw probeert bij verlies.

Notitie

Breekt het verliezen van pakketten de versleuteling? Nee. Elk DTLS-pakket draagt een volgnummer, en de versleuteling gebruikt dat nummer om voor elk pakket andere uitvoer te produceren – dus dezelfde invoer versleutelt nooit twee keer naar dezelfde bytes, en elk pakket kan op zichzelf worden ontsleuteld zonder dat het vorige is aangekomen. Verloren of in verkeerde volgorde aangekomen pakketten brengen de twee kanten niet uit synchronisatie. (De handshake zelf is het enige deel dat betrouwbaar moet aankomen, en DTLS regelt dat met zijn eigen hertransmissie.)

Dezelfde waarschuwing over alleen-versleuteling-zonder-certs van hierboven geldt: een DTLS-handshake tegen een CERT_NONE-peer versleutelt het verkeer maar verifieert niet wie de andere kant is. De volledige DTLS-workflow – certificaten, de anti-spoofing-cookie aan de serverkant, hoe dit hetzelfde oppervlak is als TLS afgezien van de protocolconstanten – wordt samen met het TLS-materiaal behandeld in Werken met TLS-certificaten.

De asyncio-versie gebruikt hetzelfde non-blocking-UDP-patroon uit Sockets met asyncio. Doe de handshake vooraf synchroon, zet de socket op non-blocking, en poll vervolgens binnen een 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)

De handshake is de enige plek waar deze coroutine de event loop blokkeert; daarna keert elke s.send / s.recv onmiddellijk terug (of werpt OSError), en houdt de await asyncio.sleep_ms de rest van het programma draaiende.

9.17.5. Verder gaan

Alles meer dan alleen-versleuteling-TLS – het verifiëren van het certificaat van een publieke HTTPS-server, de cam draaien als geauthenticeerde TLS-server, wederzijdse TLS tussen de cam en een back-end, het kiezen van sleutels en sleuteltypes, het omgaan met het verlopen van certificaten – staat in Werken met TLS-certificaten. Die sectie behandelt hoe je zelfondertekende certificaten genereert voor lokaal testen, hoe je door een CA ondertekende certificaten verkrijgt voor productie, hoe je ze in het juiste formaat (DER) op de cam krijgt, hoe je een publieke server verifieert wanneer de cam de client is, hoe je nadenkt over sleutelbescherming op een apparaat dat een aanvaller uit elkaar kan halen, en hoe je je voorbereidt op de dag dat het certificaat verloopt.

Voor de volledige ssl-API-referentie – ondersteunde TLS-versies, cipher suites en context-opties – zie ssl — SSL/TLS-module.