Basi di crittografia

Come ho spiegato nel mio post precedente sto cercando di mettere un po’ in sicurezza degli script in python che uso per l’automazione di alcune operazioni su database. Da questa esigenza mi è venuta voglia di aggiornarmi su quella che è attualmente il livello tecnologico della crittografia. Si tratta di un campo che deve evolvere velocemente perché da una parte i sistemi di elaborazione diventano più potenti rendendo deboli soluzioni che anni fa erano robustissime e dall’altro, il fatto che sempre più servizi informatici siano accessibili a tutto il mondo sulla rete rende necessario sistemi più sicuri. Facendo un po’ di ricerche ho trovato come base di partenza una ottima fonte, il libro di Svetlin Nakov “Practical Cryptography for developers”, un libro abbastanza aggiornato e che fa una buona panoramica, senza voler entrare troppo nei dettagli della matematica necessaria per comprendere il funzionamento degli algoritmi di crittografia. Per qualche motivo l’autore sembra non abbia portato a compimento le sue intenzioni e il libro sembra fermo al 2018 con delle parti rimaste incompiute. Ciò nonostante mi è piaciuto e mi è stato utile. Ho integrato la mia ricerca con delle verifiche che di solito finiscono sempre su wikipedia. Il mio intento non era entrare nei dettagli dei vari algoritmi ma avere una panoramica e senz’altro questo libro mi è stato molto d’aiuto. Ho raccolto un po’ di appunti sugli argomenti principali e li voglio riportare qui, come ho sempre cercato di fare.

Criptazione e chiavi

La criptazione ha l’obiettivo di modificare o meglio nascondere il testo di un messaggio affinché solo il destinatario del messaggio possa decodificarlo e comprenderlo. Per fare ciò è fondamentale il concetto di chiave, che permette di codificare (criptare) il messaggio in modo da renderlo incomprensibile a chi non ha la chiave per poterlo decodificare. Esistono due tipologie di crittografia: 

  1. simmetrica, in cui mittente e destinatario condividono la stessa chiave per codificare e decodificare i messaggi
  2. Asimmetrica, in cui il mittente usa una chiave per codificare e il destinatario usa un’altra chiave per decodificare. Di solito si parla di chiave pubblica e chiave privata, il destinatario condivide la chiave pubblica con cui il mittente codifica il messaggio che può essere decodificato solo con la chiave privata e quindi solo dal destinatario

Uno dei principi base della crittografia è che la codifica è tanto più sicura (quindi il messaggio non può essere decodificato da estranei) quanto più la chiave è lunga e casuale, per questo solitamente si usano algoritmi di derivazione delle chiavi.

Funzioni Hash (Hash Functions)

Sono funzioni deterministiche che effettuano una codifica unidirezionale, data una stringa di input generano un output a lunghezza fissa pseudocasuale dal quale non è teoricamente fattibile risalire alla stringa di partenza. Il risultato di questa funzione viene proprio chiamato “hash” ed una delle caratteristiche di queste funzioni è che la probabilità che due input diversi producano lo stesso hash è pressoché zero. Uno degli utilizzi di questo tipo di funzioni è quindi la generazione di codici per validare il contenuto di un messaggio o file. Avendo a disposizione l’hash del messaggio o file originale è possibile, riapplicando la stessa funzione di hash, verificare facilmente l’uguaglianza dell’hash calcolato con quello che accompagna il messaggio o file. Un esempio ormai obsoleto di tali funzioni è MD5 (dove se non ricordo male MD sta per message digest), essa viene ancora utilizzata, seppur sempre meno, per il controllo di integrità ovvero la verifica che un messaggio (per semplicità d’ora in poi parlerò solo di messaggi e non di file) arrivato a destinazione sia uguale a quello partito. 

Le funzioni di hash sono state una delle prime funzionalità per mettere in sicurezza le password salvate, tipicamente anziché salvare le password in chiaro si salva il loro hash e in fase di login si confronta l’hash calcolato sulla password fornita con l’hash memorizzato. Questo fa si che se un malintenzionato riesce ad accedere all’archivio delle password non è in grado di risalire alla password originale, per via di una una delle caratteristiche delle funzioni di hash  che quella di non essere “invertibili” e quindi di non permettere di  risalire agevolmente alle password originale dagli hash.

Altri funzioni di hash più comunemente utilizzate sono MD5, SHA-1, SHA-2, SHA-3, BLAKE2 e BLAKE3.  MD5, SHA-1 sono oramai considerati insicuri e da non usare crittografia ma ancora usati in altri ambiti quale il controllo di integrità, oppure, come nel caso di sha-1 come generatore di hash univoci (vedi id commit in git).  La prima versione di SHA (SHA-1) genera hash di 160 bit, le versioni 2 e 3 (SHA-2 e SHA-3) hanno varianti con hash a 256 e 512 per cui si trova la notazione SHA-256 o SHA3-512 (ci sono poi anche ulteriori varianti a 224 384 bit che sono in realtà derivate rispettivamente dalla variante a 256 bit e 512 bit) quindi se si trova la notazione SHA-XXX si tratta implicitamente di SHA-2 mentre per SHA-3 si trova la notazione SHA3-XXX. Lo esplicito perché io sinceramente quanto ho iniziato a studiare questo argomento ho fatto un po’ di fatica all’inizio a districarmi tra queste notazioni per capire esattamente a che versioni ci si riferisse. SHA-1 non ha la specifica del numero di bit dell’hash prodotto perché ha una unica variante a 160 bit. Per completezza esiste anche un SHA-0, obsoleto e un RIPEMD-160. Ce ne sono ancora altri ma non intendo fare una rassegna esaustiva, rimando all’ottimo libro di Nakov per gli approfondimenti.

E’ abbastanza evidente che con il passare del tempo, il progredire della ricerca e l’aumento delle prestazioni di elaborazione dei calcolatori gli algoritmi devono essere aggiornati e per questo osserviamo l’esistenza di diverse versioni o il nascere di nuove, se si vuole essere sicuri occorre aggiornare gli algoritmi utilizzati dai propri sistemi. Wikipedia al solito è una fonte di informazione ottima, segnalo solo che almeno un paio di pagine in italiano sono poco aggiornate, quindi consiglio di andare direttamente alla versione inglese. Qui si trova uno schemino di confronto dei vari algoritmi di hash usati in crittografia

Message Authentication Codes (MAC) e hash based MAC (HMAC)

Un MAC è un codice calcolato da una chiave e da un messaggio che serve a stabilire l’autenticità di un messaggio. Il principio è che mittente e destinatario in qualche modo condividono una chiave nota solo a loro. Il mittente che deve inviare un messaggio genera un codice (il MAC) usando il testo del messaggio e la chiave, invia il messaggio e il MAC al destinatario e quest’ultimo potrà verificare l’autenticità del messaggio confrontando il MAC ricevuto con quello calcolato da lui con il messaggio e la chiave che solo lui e il mittente conoscono.  I MAC hanno le stesse caratteristiche degli hash crittografici, sono irreversibili e generano codici praticamente unici. I MAC basati su algoritmi di hashing vengono chiamati hash based MAC e quindi identificato con la sigla HMAC. Gli utilizzi tipici degli HMAC sono l’autenticazione di messaggi, la verifica di integrità di messaggi e la derivazione di chiavi

Algoritmi di derivazione delle chiavi 

Si tratta di algoritmi utilizzati per derivare chiavi di criptazione (con determinate caratteristiche) da semplici password generate solitamente da umani. Esempi di questi algoritmi sono PBKDF2, bcrypt, argon2 e scrypt. Questi algoritmi servono a generare (o meglio derivare) delle chiavi che abbiano requisiti necessari a garantire la necessaria efficacia e sicurezza di criptazione nel caso di criptazione a chiave unica (simmetrica). La caratteristica più importante che deve avere una chiave, come abbiamo detto sopra, è quella di essere quanto più “casuale” possibile, questo permette di prevenire approcci basati sulla statistica. Per rendere difficile l’uso di dizionari di hash (chiamati “rainbow table”) queste funzioni aggiungono alla password di partenza un “salt”, un codice generato anche esso con qualche funzione “casuale” da aggiungere alla chiave di partenza, quindi generando hash diversi per le stesse password se si usano salt diversi. Per rendere poi infattibili i cosiddetti attacchi di forza bruta (brute force attacks) ovvero il calcolo di tutti i possibili hash, gli algoritmi di derivazione introducono un certo costo computazionale reiterando migliaia o centinaia di migliaia di volte la stessa operazione. Il numero di iterazioni è un compromesso fra la necessità di una autenticazione veloce (perché nei sistemi di autenticazione questi algoritmi hanno preso il posto delle semplici funzioni di hash per il salvataggio delle password) e la necessità di garantirne  la non decodificabilità. Infatti uno dei limiti delle semplici funzioni di hash è che moderni sistemi dedicati possono calcolare milioni di hash al secondo rendendo quindi un attacco di forza bruta fattibile, usando algoritmi più complessi questo diventa molto più complesso.

L’algoritmo PBKDF2 è oramai ritenuto non molto sicuro perché debole contro attacchi di forza bruta basati sull’uso di GPU o ASIC (circuiti di elaborazione dedicati); Bcrypt, Scrypt e Argon2 sono algoritmi più recenti e più robusti contro attacchi perché richiedono molte più risorse computazionali. Questi algoritmi prevedono esplicitamente dei parametri di input che incidono sull’uso di memoria e cpu mentre PBKDF2 prevede solo il numero di iterazioni che può incidere sull’uso di CPU.

Lascia un commento