Biztonságos a vonal?
Titkosított csatornák és egy kis Python
Vannak azok a remek filmes jelenetek, amikor főhősünk felhív valakit és megkérdezik tőle, hogy biztonságos-e a vonal. Érdekes téma lenne a vezetékes telefonvonalak lehallgatása is, vagy hogy ebben az esetben mit jelenthet az, hogy biztonságos-e a kapcsolat. De mi azért maradunk a digitális világban.
Azon belül is a rengeteg eszköz egyikét, a TLS-t, vagy korábbi nevén az SSL-t fogjuk kicsit közelebbről megvizsgálni. De még mielőtt rátérhetnénk a TLS által nyújtott titkosított csatornák szépségeire, ejtenünk kell pár szót a tanúsítványokról.
Digitális tanúsítványok
Természetesen ezt is, mint minden más kriptográfiával kapcsolatos dolgot a fekete mágia működteti. Vagy a matematika, kinek hogy tetszik.
A tanúsítvány nem több, mint egy publikus kulcs, némi metaadat és egy aláírás egy remélhetőleg megbízható harmadik személytől (Certificate authority, vagy röviden CA), aki ezzel igazolja, hogy a publikus kulcs és a metaadat összetartozik.
Ez a megbízható harmadik fél például lehet egy szervezet (pl. Let's Encrypt), aki domain-ekhez állít ki tanúsítványokat, miután leellenőrzi, hogy tényleg hozzád tartozik-e, vagy akár a munkáltatód, aki a VPN szerverhez osztogat tanúsítványokat.
Amit még érdemes tudni, hogy ezeket a tanúsítványokat egymás után lehet fűzni és ha valamelyikben megbízol, akkor az összes alatta lévő által kiadott tanúsítványban is megbízol.
Generáljunk kulcsokat
Mivel nem tartozom azon beavatottak közé, akik képesek az openssl
parancs misztikus varázsigéit elkántálni, ezért egy easy-rsa
nevű eszközt fogunk használni, ami nagyban megkönnyíti a dolgunkat.
Először is szerezzük be az eszközt:
$ git clone https://github.com/OpenVPN/easy-rsa.git
$ cd easy-rsa/easyrsa3/
Inicializáljunk egy új PKI-t (Public key infrastructure). Ez esetünkben csak létrehoz egy könyvtárstruktúrát a pki/
könyvtár alá, amit az easy-rsa
használ a továbbiakban.
$ ./easyrsa init-pki
Generáljunk egy CA-t, egy szerver- és egy kliens tanúsítványt. Természetesen jelszó nélkül, hogy később könnyebb dolgunk legyen (valós használatra viszont ez kevésbé egészséges).
$ ./easyrsa build-ca nopass
$ ./easyrsa build-server-full server nopass
$ ./easyrsa build-client-full client nopass
Ha minden jól ment, lett hat darab fájlunk, amit a későbbi scriptekben fel fogunk használni. A .crt
végűek megoszthatóak a világgal, a .key
végűeket pedig inkább tartsuk csak meg magunknak.
pki/ca.crt
pki/issued/client.crt
pki/issued/server.crt
pki/private/ca.key
pki/private/client.key
pki/private/server.key
A jó öreg titkosítatlan szöveg
A példákban egy-egy socket klienst és szervert fogunk megvalósítani. Mielőtt azonban belevágnánk fejszénket a titkosítás fájába, nézzük meg hogyan is néz ki egy titkosítás nélkül:
plain_text_client.py
import socket
target = ('127.0.0.1', 12321)
data = b'Hello, world'
with socket.create_connection(target) as sock:
sock.sendall(data)
print('Sent', repr(data))
data = sock.recv(1024)
print('Received', repr(data))
plain_text_server.py
import socket
target = ('127.0.0.1', 12321)
with socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) as sock:
sock.bind(target)
sock.listen(5)
while True:
conn, addr = sock.accept()
data = conn.recv(1024)
conn.sendall(b'ECHO ' + data)
conn.close()
A tcpdump
parancs segítségével meg is tudjuk nézni, hogy a kommunikáció tényleg nincs titkosítva.
# tcpdump -i lo port 12321 -w plain_text.pcap
Az egészet lementhetjük egy pcap
fájlba, amit aztán pl. Wireshark segítségével emészthetőbb formában is megnézhetünk.
Én csak titkosítok, nem érdekel más
Még mielőtt belemennénk a következő példába, itt szeretném felhívni a figyelmet, hogy ez továbbra sem olyan kód, amit a való világban érdemes használni, mindjárt arra is kitérünk, hogy miért.
encrypt_client.py
import socket
import ssl
target = ('127.0.0.1', 12321)
data = b'Hello, world'
context = ssl.SSLContext(ssl.PROTOCOL_TLS)
with socket.create_connection(target) as sock:
with context.wrap_socket(sock) as tls_sock:
tls_sock.sendall(data)
print('Sent', repr(data))
data = tls_sock.recv(1024)
print('Received', repr(data))
encrypt_server.py
import socket
import ssl
target = ('127.0.0.1', 12321)
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain('pki/issued/server.crt', 'pki/private/server.key')
with socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) as sock:
sock.bind(target)
sock.listen(5)
with context.wrap_socket(sock, server_side=True) as tls_sock:
while True:
conn, addr = tls_sock.accept()
data = conn.recv(1024)
conn.sendall(b'ECHO ' + data)
conn.close()
A tcpdump
-ot és a Wireshark-ot segítségül hívva ismét megnézhetjük a forgalmat, történt-e valami változás.
A részletes elemzés házi feladat, de az jól látszik, hogy sokkal több csomag utazott a TLS miatt. Valamint az sem mellékes, hogy nem értünk az adatból egy szót se. Mi akkor a probléma?
Igazából nem érdekel minket, hogy kivel beszélünk. Bárki lehet a vonal másik végén, amíg titkosítottan próbál velünk kommunikálni. Így ha valaki a kliens és a szerver közé tud állni, a kliensnek nem fog feltűnni, hogy megváltozott a tanúsítvány.
Python esetén érdemes átfutni az ssl
modul Security considerations részét, mielőtt élesben is használjuk ezeket a dolgokat.
Legyünk biztosak abban, hogy kivel beszélünk
Biztosabbak lehetünk a dolgunkban, ha megköveteljük a szervertől, hogy a tanúsítványát valamilyen általunk megbízhatónak ítélt CA írja alá. Ehhez a kliensünk context
-jén a következőket kell beállítani:
context.verify_mode = ssl.CERT_REQUIRED
context.load_verify_locations('pki/ca.crt')
A CA-tól függően ez akár elég is lehet, de ha egy olyan CA-ról beszélünk, akitől bárki tud tanúsítványt igényelni, akkor ugyanott vagyunk, mint az előző pontban. A kliens context
-jébe viszont bekapcsolhatunk még egy validációt:
context.check_hostname = True
Ehhez viszont az is kell, hogy a kliens socket becsomagolásakor megmondjuk, hogy szerintünk kivel fogunk beszélgetni:
context.wrap_socket(sock, server_hostname='server')
Ez a server
string nem mellesleg ugyanaz a server
, mint amit a ./easyrsa build-server-full server nopass
parancsban adtunk meg és szerepel a tanúsítvány metaadatai között.
Amikor a másik félnek is bizonyíték kell, nem ígéret
A kliens már biztos lehet a szerver kilétében, de szerverünk még bárkivel leáll beszélgetni. Ezt orvosolhatjuk azzal, hogy a szerver is követeljen meg a klienstől tanúsítványt.
A szintek itt is hasonlóan alakulnak, mint a kliensnél és hasonló következményekkel is járnak, úgyhogy ugorjunk is rögtön arra a változatra, ahol csak egy bizonyos CA által kiállított tanúsítványt fogadunk el egy megfelelő nevű klienstől (ami így remekül használható autentikációra is).
client_cert_client.py
import socket
import ssl
target = ('127.0.0.1', 12321)
data = b'Hello, world'
context = ssl.SSLContext(ssl.PROTOCOL_TLS)
context.verify_mode = ssl.CERT_REQUIRED
context.check_hostname = True
context.load_verify_locations('pki/ca.crt')
context.load_cert_chain('pki/issued/client.crt', 'pki/private/client.key')
with socket.create_connection(target) as sock:
with context.wrap_socket(sock, server_hostname='server') as tls_sock:
tls_sock.sendall(data)
print('Sent', repr(data))
data = tls_sock.recv(1024)
print('Received', repr(data))
client_cert_server.py
import socket
import ssl
target = ('127.0.0.1', 12321)
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.verify_mode = ssl.CERT_REQUIRED
context.load_verify_locations('pki/ca.crt')
context.load_cert_chain('pki/issued/server.crt', 'pki/private/server.key')
with socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) as sock:
sock.bind(target)
sock.listen(5)
with context.wrap_socket(sock, server_side=True) as tls_sock:
while True:
conn, addr = tls_sock.accept()
try:
ssl.match_hostname(conn.getpeercert(), 'client')
except:
conn.close()
continue
data = conn.recv(1024)
conn.sendall(b'ECHO ' + data)
conn.close()
Bár ad rá a modul segédfüggvényt (ssl.match_hostname()
), de a validációt itt már nekünk kell kézzel megcsinálnunk. A hívásban lévő client
string itt is ugyanaz, mint az ./easyrsa build-client-full client nopass
parancsban megadott client
.
Végezetül csak annyit, hogy ha valami titkosított, az még nem feltétlenül jelenti azt, hogy biztonságos is. A kriptográfiára jellemző, hogy rengeteg helyen félrecsúszhatnak a dolgok (nem biztonságos beállítások, elavult verziók használata) és mi itt csak a felszínt kapargattuk meg egy kicsit.