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.

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.

Ha érdekelnek a részletek

Hozzáfűznél valamit?

Dobj egy emailt a blog kukac deadlime pont hu címre.

Feliratkoznál?

Az RSS feed-et ajánljuk, ha kedveled a régi jó dolgokat.