9.17. Socluri criptate și TLS¶
Tot ce a fost prezentat până aici transmite octeți în clar. Orice dispozitiv aflat pe traseul dintre cameră și server – routerul de acasă, furnizorul de servicii de internet, un punct de acces rău intenționat dintr-o cafenea – poate, în principiu, citi sau modifica ceea ce trece prin el. Pentru majoritatea traficului de internet, acest lucru nu este acceptabil. Soluția standard este să se învelească conexiunea într-un strat de criptare: TLS, protocolul Transport Layer Security. Pictograma de lacăt „HTTPS” dintr-un browser este TLS rulând peste TCP, iar aceeași învelire face ca orice alt protocol de internet să fie „securizat”. Modulul ssl al camerei este cel care învelește un socket în TLS.
9.17.1. Ce adaugă TLS și ce vine inclus cu camera¶
TLS se află între TCP și aplicație – aplicația scrie octeți într-un soclu învelit în TLS, TLS îi criptează și transmite rezultatul către TCP, iar procesul se inversează de cealaltă parte. În forma sa completă, TLS oferă trei garanții în plus față de TCP simplu:
Confidențialitate. Cei care ascultă pe traseu nu pot citi ceea ce schimbă cele două capete.
Integritate. Orice modificare a traficului în tranzit este detectată; conexiunea se întrerupe în loc să livreze date alterate.
Autentificare. Serverul dovedește că este serverul numit, nu un impostor (și, opțional, clientul dovedește la rândul lui cine este).
Primele două provin din criptarea în sine. A treia necesită certificate pe cel puțin una dintre părți, plus ceva considerat de încredere în prealabil, față de care să se verifice acele certificate. Camera OpenMV vine fără niciun depozit de certificate încorporat: o cameră proaspăt programată nu are încredere în nicio autoritate de certificare, nu are propriul certificat de server și modul de verificare implicit (ssl.CERT_NONE) nu verifică certificatul partenerului față de nimic. Așadar, în mod implicit, TLS pe cameră îți oferă primele două garanții – criptare împotriva interceptării și a alterării de către un observator pasiv – dar nu și pe a treia.
9.17.2. Criptarea unei conexiuni de ieșire¶
Cea mai simplă utilizare este învelirea unei conexiuni TCP de ieșire. Fluxul este: deschide un soclu TCP normal, transmite-l către ssl.wrap_socket(), apoi citește și scrie prin soclul învelit exact așa cum ai face-o cu cel simplu:
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()
Învelirea efectuează strângerea de mână (handshake) TLS; după aceea, fiecare octet trimis prin s.send este criptat la ieșire și fiecare octet primit de la s.recv a fost criptat pe fir. Nu a fost configurat niciun certificat, nu a fost furnizată nicio ancoră de încredere – TLS pur și simplu negociază o cheie de sesiune efemeră cu orice server răspunde și o folosește.
Strângerea de mână TLS pe care o rulează ssl.wrap_socket(). Aceasta se află deasupra conexiunii TCP deja deschise din figura anterioară; odată ce ambele părți au trimis Finished, restul conversației este criptat în ambele direcții.¶
Atenționare
Aceasta este doar criptare, nu TLS autentificat. Camera comunică securizat cu orice a răspuns la celălalt capăt al conexiunii TCP. Dacă un atacator de tip om-la-mijloc redirecționează conexiunea către un server pe care îl controlează, iar acel server prezintă orice certificat, strângerea de mână reușește totuși, iar camera ajunge să comunice securizat cu atacatorul. Folosește acest mod doar atunci când un atacator om-la-mijloc nu face parte din modelul de amenințare – o rețea locală închisă, un mediu de dezvoltare, camera comunicând cu un serviciu care rulează pe același hardware – nu atunci când accesezi internetul public.
Pentru autentificare reală – camera verificând un server public, camera acționând ca server TLS sau TLS reciproc – trebuie să aduci certificate pe dispozitiv. Povestea completă se află în Lucrul cu certificate TLS.
Aceeași învelire funcționează pentru traficul TCP de intrare, selectând protocolul de server și transmițând server_side=True către ssl.wrap_socket(). Avertismentul de mai sus este în continuare valabil: fără un certificat propriu, camera nu poate dovedi cine este față de client, iar un client curios ar vedea o eroare de strângere de mână „fără certificat” pe majoritatea stivelor TLS. Fluxul de lucru cu certificate din partea de producție este cel care deblochează rularea camerei ca server TLS într-un mod util.
9.17.3. Cu asyncio¶
Capitolul asyncio a arătat asyncio.open_connection() pentru clienții TCP simpli. Același apel acceptă un cuvânt-cheie ssl=True care învelește conexiunea în TLS, din nou fără nicio configurare de certificat:
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())
Perechea cititor/scriitor din spatele unei conexiuni TLS are aceeași formă ca pentru o conexiune TCP simplă – diferă doar configurarea. Se aplică aceeași precauție privind autentificarea: ssl=True singur oferă doar criptare, nu și verificare.
9.17.4. DTLS – TLS peste UDP¶
TLS, așa cum a fost discutat până acum, se bazează pe TCP. Protocolul paralel pentru UDP este DTLS (Datagram TLS), iar modulul ssl al camerei îl suportă în același mod. Acolo unde TLS transformă o conexiune TCP într-un flux de octeți criptat, DTLS transformă un soclu UDP într-un flux de datagrame criptate, livrate individual – astfel încât proprietățile de pierdere / dezordine / lipsă de control al fluxului ale UDP din UDP – trimite un pachet și speră la ce e mai bun se transferă toate, octeții din interiorul fiecărei datagrame fiind acum criptați.
Învelirea arată la fel ca în cazul TLS, doar cu un soclu SOCK_DGRAM și constantele de protocol 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()
(Apelarea connect() pe un soclu UDP nu deschide o conexiune – doar reține o destinație implicită, astfel încât apelurile ulterioare send() / recv() să nu fie nevoite să o repete. DTLS are nevoie de acea destinație fixă pentru a-și rula strângerea de mână față de ea.)
Strângerea de mână are aceeași formă ca diagrama TLS de mai sus; diferența este că fiecare mesaj de strângere de mână este el însuși o datagramă UDP, iar oricare dintre părți va reîncerca în caz de pierdere.
Notă
Pierderea pachetelor strică criptarea? Nu. Fiecare pachet DTLS poartă un număr de secvență, iar criptarea folosește acel număr pentru a produce o ieșire diferită pentru fiecare pachet – astfel încât aceeași intrare nu se criptează niciodată de două ori în aceiași octeți, iar orice pachet poate fi decriptat de unul singur fără ca cel anterior să fi sosit. Pachetele pierdute sau dezordonate nu desincronizează cele două părți. (Strângerea de mână în sine este singura parte care trebuie să ajungă în mod fiabil, iar DTLS gestionează acest lucru cu propria sa retransmisie.)
Se aplică același avertisment de mai sus privind criptarea fără certificate: o strângere de mână DTLS față de un partener CERT_NONE criptează traficul, dar nu verifică cine este cealaltă parte. Fluxul de lucru complet DTLS – certificate, cookie-ul anti-falsificare din partea serverului, modul în care aceasta este aceeași suprafață ca TLS, în afară de constantele de protocol – este tratat alături de materialul TLS în Lucrul cu certificate TLS.
Versiunea asyncio folosește același tipar UDP neblocant din Socket-uri cu asyncio. Efectuează strângerea de mână sincron, în avans, comută soclul în mod neblocant, apoi interoghează în interiorul unei corutine:
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)
Strângerea de mână este singurul loc în care această corutină blochează bucla de evenimente; după aceea, fiecare s.send / s.recv revine imediat (sau ridică OSError), iar await asyncio.sleep_ms menține restul programului în funcțiune.
9.17.5. Mergând mai departe¶
Tot ce este mai mult decât TLS doar pentru criptare – verificarea certificatului unui server HTTPS public, rularea camerei ca server TLS autentificat, TLS reciproc între cameră și un back-end, alegerea cheilor și a tipurilor de chei, gestionarea expirării certificatului – se află în Lucrul cu certificate TLS. Acea secțiune acoperă modul de generare a certificatelor auto-semnate pentru testarea locală, modul de obținere a certificatelor semnate de o autoritate de certificare (CA) pentru producție, modul de transferare a acestora pe cameră în formatul corect (DER), modul de verificare a unui server public atunci când camera este clientul, modul de a gândi protecția cheilor pe un dispozitiv pe care un atacator l-ar putea demonta și modul de planificare pentru ziua în care certificatul expiră.
Pentru referința completă a API-ului ssl – versiunile TLS suportate, suitele de cifruri și opțiunile de context – consultă ssl — modulul SSL/TLS.