9.17. Szyfrowane gniazda i TLS¶
Wszystko, co omówiliśmy do tej pory, przesyła bajty otwartym tekstem. Każde urządzenie na trasie między kamerą a serwerem – domowy router, dostawca usług internetowych, złośliwy punkt dostępowy w kawiarni – może w zasadzie odczytać lub zmodyfikować to, co przez nie przechodzi. W przypadku większości ruchu internetowego jest to nie do przyjęcia. Standardowym rozwiązaniem jest opakowanie połączenia w warstwę szyfrowania: TLS, czyli protokół Transport Layer Security. Ikona kłódki „HTTPS” w przeglądarce to TLS działający nad TCP, a to samo opakowanie sprawia, że każdy inny protokół internetowy staje się „bezpieczny”. Moduł ssl kamery opakowuje socket w TLS.
9.17.1. Co dodaje TLS i z czym kamera jest dostarczana¶
TLS znajduje się między TCP a aplikacją – aplikacja zapisuje bajty do gniazda opakowanego w TLS, TLS je szyfruje i przekazuje wynik do TCP, a po drugiej stronie proces przebiega odwrotnie. W swojej pełnej postaci TLS daje trzy gwarancje ponad zwykłym TCP:
Poufność. Podsłuchujący na trasie nie mogą odczytać tego, co wymieniają oba punkty końcowe.
Integralność. Każda modyfikacja ruchu w trakcie przesyłania jest wykrywana; połączenie zostaje zerwane, zamiast dostarczać zmanipulowane dane.
Uwierzytelnianie. Serwer udowadnia, że jest tym serwerem, za który się podaje, a nie oszustem (a opcjonalnie również klient udowadnia, kim on jest).
Dwie pierwsze wynikają z samego szyfrowania. Trzecia wymaga certyfikatów po co najmniej jednej stronie oraz czegoś z góry zaufanego, względem czego można te certyfikaty zweryfikować. Kamera OpenMV jest dostarczana bez żadnego wbudowanego magazynu certyfikatów: świeżo wgrana kamera nie ufa żadnemu urzędowi certyfikacji, nie ma własnego certyfikatu serwera, a domyślny tryb weryfikacji (ssl.CERT_NONE) nie sprawdza certyfikatu drugiej strony względem niczego. Tak więc bezpośrednio po uruchomieniu TLS na kamerze daje dwie pierwsze gwarancje – szyfrowanie chroniące przed podsłuchem i manipulacją ze strony biernego obserwatora – ale nie trzecią.
9.17.2. Szyfrowanie połączenia wychodzącego¶
Najprostszym zastosowaniem jest opakowanie wychodzącego połączenia TCP. Przebieg jest następujący: otwórz zwykłe gniazdo TCP, przekaż je do ssl.wrap_socket(), a następnie odczytuj i zapisuj przez opakowane gniazdo dokładnie tak, jak robiłbyś to ze zwykłym:
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()
Opakowanie wykonuje uzgadnianie TLS; potem każdy bajt przechodzący przez s.send jest szyfrowany na wyjściu, a każdy bajt z s.recv był szyfrowany na łączu. Nie skonfigurowano żadnych certyfikatów, nie dostarczono żadnej kotwicy zaufania – TLS po prostu negocjuje efemeryczny klucz sesji z dowolnym serwerem, który odpowie, i go używa.
Uzgadnianie TLS wykonywane przez ssl.wrap_socket(). Znajduje się ono na szczycie już otwartego połączenia TCP z poprzedniego rysunku; gdy obie strony wyślą Finished, reszta konwersacji jest szyfrowana w obu kierunkach.¶
Ostrzeżenie
Jest to TLS wyłącznie szyfrujący, a nie uwierzytelniony. Kamera bezpiecznie komunikuje się z czymkolwiek, co odpowiedziało po drugiej stronie połączenia TCP. Jeśli atakujący metodą man-in-the-middle przekieruje połączenie do kontrolowanego przez siebie serwera, a ten serwer przedstawi jakikolwiek certyfikat, uzgadnianie i tak się powiedzie, a kamera skończy bezpiecznie rozmawiając z atakującym. Używaj tego trybu tylko wtedy, gdy atak man-in-the-middle nie jest częścią modelu zagrożeń – zamknięta sieć lokalna, środowisko deweloperskie, kamera komunikująca się z usługą działającą na tym samym sprzęcie – a nie podczas sięgania do publicznego internetu.
W przypadku prawdziwego uwierzytelniania – gdy kamera weryfikuje publiczny serwer, gdy kamera działa jako serwer TLS lub przy wzajemnym TLS – musisz wprowadzić certyfikaty na urządzenie. Pełny opis znajduje się w Praca z certyfikatami TLS.
To samo opakowanie działa dla przychodzącego ruchu TCP, poprzez wybranie protokołu serwera i przekazanie server_side=True do ssl.wrap_socket(). Powyższe ostrzeżenie wciąż obowiązuje: bez własnego certyfikatu kamera nie może udowodnić klientowi, kim jest, a dociekliwy klient zobaczyłby w większości stosów TLS błąd uzgadniania typu „brak certyfikatu”. To produkcyjny proces obsługi certyfikatów odblokowuje użyteczne uruchamianie kamery jako serwera TLS.
9.17.3. Z asyncio¶
W rozdziale o asyncio pokazano asyncio.open_connection() dla zwykłych klientów TCP. To samo wywołanie przyjmuje słowo kluczowe ssl=True, które opakowuje połączenie w TLS, również bez żadnej konfiguracji certyfikatów:
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())
Para czytnik/zapisywacz stojąca za połączeniem TLS ma ten sam kształt co dla zwykłego połączenia TCP – różni się jedynie konfiguracja. Obowiązuje to samo zastrzeżenie dotyczące uwierzytelniania: samo ssl=True daje tylko szyfrowanie, a nie weryfikację.
9.17.4. DTLS – TLS nad UDP¶
Omawiany dotąd TLS działa nad TCP. Równoległym protokołem dla UDP jest DTLS (Datagram TLS), a moduł ssl kamery obsługuje go w ten sam sposób. Tam gdzie TLS zamienia jedno połączenie TCP w jeden szyfrowany strumień bajtów, DTLS zamienia jedno gniazdo UDP w strumień szyfrowanych, dostarczanych pojedynczo datagramów – a więc właściwości UDP z UDP – wyślij pakiet i miej nadzieję na najlepsze, takie jak utrata, zmiana kolejności czy brak kontroli przepływu, w całości przenoszą się dalej, tyle że bajty wewnątrz każdego datagramu są teraz szyfrowane.
Opakowanie wygląda tak samo jak w przypadku TLS, tyle że z gniazdem SOCK_DGRAM i stałymi protokołu 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()
(Wywołanie connect() na gnieździe UDP nie otwiera połączenia – po prostu zapamiętuje domyślny adres docelowy, dzięki czemu kolejne wywołania send() / recv() nie muszą go powtarzać. DTLS potrzebuje tego stałego adresu docelowego, aby wobec niego przeprowadzić uzgadnianie.)
Uzgadnianie ma ten sam kształt co na diagramie TLS powyżej; różnica polega na tym, że każdy komunikat uzgadniania jest sam w sobie datagramem UDP, a w razie utraty każda ze stron ponowi próbę.
Informacja
Czy utrata pakietów psuje szyfrowanie? Nie. Każdy pakiet DTLS niesie numer sekwencyjny, a szyfrowanie używa tego numeru, aby wytworzyć inny wynik dla każdego pakietu – dzięki czemu to samo wejście nigdy nie szyfruje się dwa razy na te same bajty, a każdy pakiet można odszyfrować samodzielnie, bez konieczności otrzymania poprzedniego. Utracone lub przybyłe w złej kolejności pakiety nie rozsynchronizują obu stron. (Samo uzgadnianie jest jedyną częścią, która musi dotrzeć niezawodnie, a DTLS radzi sobie z tym za pomocą własnej retransmisji.)
Obowiązuje to samo ostrzeżenie o szyfrowaniu bez certyfikatów co powyżej: uzgadnianie DTLS względem partnera z CERT_NONE szyfruje ruch, ale nie weryfikuje, kim jest druga strona. Pełny proces DTLS – certyfikaty, serwerowy plik cookie chroniący przed podszywaniem się, oraz to, jak jest to ta sama powierzchnia co TLS poza stałymi protokołu – jest opisany obok materiału o TLS w Praca z certyfikatami TLS.
Wersja z asyncio wykorzystuje ten sam wzorzec nieblokującego UDP z Gniazda z asyncio. Wykonaj uzgadnianie synchronicznie na początku, przełącz gniazdo w tryb nieblokujący, a następnie odpytuj wewnątrz korutyny:
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)
Uzgadnianie to jedyne miejsce, w którym ta korutyna blokuje pętlę zdarzeń; potem każde s.send / s.recv zwraca wynik natychmiast (lub zgłasza OSError), a await asyncio.sleep_ms pozwala reszcie programu działać dalej.
9.17.5. Dalsze kroki¶
Wszystko więcej niż wyłącznie szyfrujący TLS – weryfikacja certyfikatu publicznego serwera HTTPS, uruchamianie kamery jako uwierzytelnionego serwera TLS, wzajemny TLS między kamerą a zapleczem, wybór kluczy i ich typów, radzenie sobie z wygaśnięciem certyfikatu – znajduje się w Praca z certyfikatami TLS. Ta sekcja omawia, jak generować certyfikaty samopodpisane do testów lokalnych, jak uzyskać certyfikaty podpisane przez CA na potrzeby produkcji, jak wgrać je na kamerę we właściwym formacie (DER), jak zweryfikować publiczny serwer, gdy kamera jest klientem, jak myśleć o ochronie kluczy na urządzeniu, które atakujący może rozłożyć na części, oraz jak zaplanować dzień, w którym certyfikat wygaśnie.
Pełną dokumentację API modułu ssl – obsługiwane wersje TLS, zestawy szyfrów oraz opcje kontekstu – znajdziesz w ssl — moduł SSL/TLS.