Biztonságos a vonal?
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.
A www.google.com tanúsítvány-lánca.
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.
A küldött adat szépen látható a hálózati forgalomban.
É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.
Nincs itt semmi látnivaló.
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.