Esercitazioni su hash, salt, HMAC e Base64
Molti meccanismi di sicurezza si basano su una funzione di hash: un calcolo che da un dato di qualsiasi lunghezza ricava un valore breve e di lunghezza fissa, dal quale non si può risalire al dato di partenza. Il laboratorio mostra due contesti in cui queste funzioni si usano: la conservazione delle password nelle basi di dati e la sicurezza delle comunicazioni. Chiude con Base64, una codifica che si incontra spesso accanto a questi strumenti e che a volte viene scambiata per una forma di protezione.
Le esercitazioni si svolgono nel browser. I calcoli avvengono sul computer in uso: nessun dato inserito nella pagina viene inviato in rete.
Le schede affrontano tre argomenti. Le prime due sono strettamente collegate; la terza e la quarta si possono svolgere anche separatamente, ma richiamano concetti delle precedenti.
La scheda 2 approfondisce il fattore di costo introdotto nella scheda 1.
Come fa un server a verificare una password senza memorizzarla?
Perché una funzione di hash veloce come SHA-256 non basta, a cosa serve il salt e come funziona bcrypt.
Quanto deve essere lento bcrypt?
Come si regola il tempo di calcolo e perché si può aumentare nel corso degli anni.
Usa la funzione di hash della scheda 1, con l'aggiunta di una chiave.
Come si verifica che un messaggio non sia stato modificato e provenga da chi conosce una chiave segreta?
Perché un hash semplice non protegge da una modifica intenzionale e come il destinatario verifica il codice ricevuto.
Compare nella stringa di bcrypt (scheda 1) e nei token firmati con HMAC (scheda 3).
Che differenza c'è tra codificare e cifrare?
Come Base64 scrive i byte con caratteri di testo, quanto allunga i dati e perché chiunque può decodificarli.
Una funzione di hash trasforma un input di qualsiasi lunghezza in un valore di lunghezza fissa, detto hash o digest. SHA-256 produce sempre 256 bit, mostrati qui come 64 cifre esadecimali (ogni cifra rappresenta 4 bit). L'hash viene chiamato anche impronta del dato: lo identifica senza contenerlo.
Per l'uso con le password contano due proprietà. La funzione è deterministica: lo stesso input produce sempre lo stesso hash, e questo permette al server di confrontare la password inserita all'accesso con quella registrata. È inoltre unidirezionale: dall'input l'hash si calcola subito, mentre dall'hash non è praticabile risalire all'input. Il server può quindi conservare solo gli hash, e chi sottrae l'archivio non vi trova le password.
SHA-256Prova. Cambia una sola lettera della password e osserva l'hash. Cambia in quasi tutte le cifre, senza somiglianze con il valore precedente: è l'effetto valanga. Due password simili non producono hash simili, quindi l'hash non indica quanto un tentativo sia vicino alla password corretta.
L'unidirezionalità non impedisce un altro tipo di attacco. Chi ha sottratto l'archivio può scegliere password candidate (parole comuni, nomi, date, password già trapelate da altri servizi), calcolarne l'hash e confrontarlo con quelli sottratti. Il numero di tentativi non ha limiti, perché i calcoli avvengono sui computer dell'attaccante e non sul server: si parla di attacco offline.
In questo attacco conta la velocità della funzione. SHA-256 è progettato per essere veloce, perché si usa anche per controllare file di grandi dimensioni: con una sola scheda grafica si calcolano miliardi di hash SHA-256 al secondo. bcrypt è progettato per essere lento: con il fattore di costo 12, usato in questo passo, la stessa scheda ne calcola nell'ordine delle migliaia al secondo. Per l'utente che accede la differenza non si nota, perché il server calcola un solo hash; per chi prova milioni di candidati il tempo complessivo si moltiplica.
Il pulsante calcola con entrambe le funzioni l'hash della password scritta al passo 1.
Password del passo 1:
Prova. Premi il pulsante e confronta i due tempi. I valori misurati nel browser sono indicativi: dipendono dal computer e da un'implementazione JavaScript di bcrypt più lenta di quelle usate sui server. Conta il rapporto tra i due tempi.
Poiché SHA-256 è deterministico, in un archivio senza salt due utenti con la stessa password hanno lo stesso hash. Con un solo calcolo l'attaccante individua tutti gli utenti che usano una password comune. Può anche preparare in anticipo gli hash delle password più diffuse (tabelle precalcolate, tra cui le rainbow table) e ricavare una password con una semplice ricerca.
Il salt è un valore casuale, diverso per ogni utente, che viene combinato con la password prima del calcolo dell'hash. bcrypt lo genera automaticamente a ogni calcolo. Il salt non è segreto: è scritto nella stessa stringa del risultato. Serve a rendere diversi gli hash di password uguali, e obbliga l'attaccante a ripetere i calcoli per ciascun utente. Le tabelle precalcolate, costruite senza conoscere il salt, non sono più utilizzabili.
Anche qui si calcola l'hash della password del passo 1. Ogni pressione del pulsante aggiunge una riga alla tabella, con la password usata e i due risultati.
Password del passo 1:
Prova. Premi più volte il pulsante senza cambiare la password. La colonna SHA-256 resta invariata, quella bcrypt cambia a ogni riga perché cambia il salt. Per ridurre l'attesa, in questo passo bcrypt usa il fattore di costo 10.
Se l'hash cambia a ogni calcolo, il server deve comunque poter verificare la password all'accesso. Per farlo legge dalla stringa memorizzata il fattore di costo e il salt, ricalcola l'hash della password inserita con gli stessi parametri e confronta il risultato con la parte finale della stringa. I due valori coincidono solo se la password è quella registrata.
Qui la stringa memorizzata dal server è l'ultima calcolata al passo 2 o 3. Il campo della password inserita all'accesso parte con la stessa password: modificala per simulare un accesso con una password sbagliata.
Prova. Premi «Verifica» con la password proposta, poi cambiane un solo carattere e verifica di nuovo.
La stringa prodotta da bcrypt contiene tutto ciò che serve alla verifica. Il simbolo
$ separa la versione dell'algoritmo e il fattore di costo. Seguono, senza separatore,
il salt e l'hash, che si distinguono per posizione: i primi 22 caratteri sono il salt, i
successivi 31 l'hash. Salt e hash sono scritti in una variante di Base64, la codifica
trattata nella scheda 4.
$2a$. Le librerie più recenti usano $2b$: l'algoritmo è lo stesso, la
revisione corregge un difetto di alcune implementazioni.
Per conservare le password servono due misure insieme. Il salt rende diversi gli hash di password uguali e rende inutili i calcoli fatti in anticipo. La lentezza dell'algoritmo limita il numero di tentativi che l'attaccante può fare contro ciascun utente. Aggiungere un salt a SHA-256 risolve solo il primo problema, perché la funzione resta veloce; bcrypt li risolve entrambi.
Al proprio interno bcrypt ripete un'operazione per un numero di volte pari a 2 elevato al fattore di costo. Aumentare il fattore di 1 raddoppia le ripetizioni e quindi il tempo: il costo 11 richiede il doppio del tempo del costo 10, il costo 12 il quadruplo. Il tempo cresce quindi in modo esponenziale rispetto al fattore.
Ogni misura aggiunge una riga alla tabella, ordinata per costo. La barra mostra il tempo in proporzione alla misura più lunga; l'ultima colonna riporta il rapporto con il costo immediatamente inferiore, quando è stato misurato.
| Costo | Tempo (ms) | Rapporto | Confronto |
|---|
Prova. Misura i costi da 8 a 14, uno dopo l'altro, e osserva la colonna del rapporto: dovrebbe essere vicina a 2. Con i costi più bassi le misure sono meno regolari, perché i tempi sono brevi e pesano di più le altre attività del browser.
Il fattore si sceglie in base al server: il valore più alto che consente di verificare una password in un tempo accettabile per l'utente, di solito nell'ordine di qualche centinaio di millisecondi. Le raccomandazioni OWASP indicano per bcrypt un fattore di costo di almeno 10.
Il fattore è scritto in ogni stringa bcrypt, quindi gli hash già memorizzati restano verificabili anche dopo un aumento. Il server può ricalcolare l'hash con il nuovo costo al successivo accesso dell'utente, l'unico momento in cui dispone della password in chiaro.
Il fattore di costo rende bcrypt adattabile nel tempo. Quando la potenza di calcolo disponibile aumenta, basta incrementare il fattore: ogni unità in più raddoppia il lavoro dell'attaccante, senza cambiare algoritmo né formato degli hash. Per l'utente che accede il costo resta quello di un singolo calcolo.
Nelle schede 1 e 2 la funzione di hash è servita a conservare le password nella base di dati di un servizio: il dato da proteggere restava sul server, e il pericolo era che qualcuno sottraesse l'archivio. Le funzioni di hash si usano anche nella sicurezza delle comunicazioni, dove il problema è diverso. Un messaggio attraversa una rete e passa per dispositivi che né il mittente né il destinatario controllano; lungo il percorso può essere letto, ma anche modificato.
Chi riceve un messaggio deve poter controllare che il contenuto sia arrivato come è stato scritto e che l'abbia scritto chi dichiara di averlo fatto. La cifratura, che rende il contenuto illeggibile a chi non ha la chiave, da sola non basta: non impedisce che il messaggio venga alterato. Per questo controllo si usano strumenti specifici, e HMAC è uno dei più diffusi. Si trova, per esempio, nelle richieste firmate inviate alle API dei servizi web, nelle notifiche automatiche che un servizio invia a un altro (webhook), nei token di accesso JWT e all'interno del protocollo TLS, che protegge le connessioni HTTPS.
Un hash inviato insieme a un messaggio permette al destinatario di accorgersi di un errore di trasmissione: ricalcola l'hash del messaggio ricevuto e lo confronta con quello allegato. Non protegge invece da una modifica intenzionale. Chi intercetta il messaggio può cambiarlo e calcolare il nuovo hash, perché SHA-256 non richiede alcun segreto; il destinatario riceve così un messaggio alterato accompagnato da un hash corretto.
HMAC (Hash-based Message Authentication Code) introduce una chiave segreta condivisa da mittente e destinatario. Il codice dipende dal messaggio e dalla chiave: senza la chiave non si può calcolare il codice corretto per un messaggio modificato. HMAC non è la semplice concatenazione di chiave e messaggio: applica la funzione di hash due volte secondo uno schema standard (RFC 2104), che evita debolezze note della concatenazione.
SHA-256 del messaggio (calcolabile da chiunque)Prova. Cambia l'importo da 100 a 900. Cambiano sia l'hash sia l'HMAC, ma solo il primo può essere ricalcolato da chi non conosce la chiave. Cambia poi la chiave: lo stesso messaggio produce un HMAC diverso.
Il mittente invia il messaggio insieme al suo HMAC. Il destinatario ricalcola l'HMAC del messaggio ricevuto con la propria copia della chiave e lo confronta con quello ricevuto. Se i due valori coincidono, il messaggio non è stato modificato ed è stato prodotto da chi possiede la chiave.
HMAC ricalcolato dal destinatarioProva. Simula un attacco modificando il messaggio ricevuto, per esempio l'importo: la verifica non riesce. Ripristina il messaggio e cambia invece la chiave del destinatario: anche in questo caso la verifica non riesce.
HMAC garantisce l'integrità e l'autenticità di un messaggio, ma richiede che mittente e destinatario condividano la stessa chiave prima di comunicare. Come scambiarsi la chiave in modo sicuro è il problema della distribuzione delle chiavi, che la crittografia asimmetrica affronta. Inoltre, poiché la chiave è nota a entrambi, un HMAC non permette di dimostrare a un terzo quale dei due abbia scritto il messaggio.
Molti canali sono stati progettati per trasportare testo: la posta elettronica, gli indirizzi
web, formati come JSON. I dati binari, per esempio un'immagine, una chiave o l'output di una
funzione di hash, contengono byte che in questi canali possono essere alterati o interpretati
come caratteri di controllo. Base64 riscrive qualsiasi sequenza di byte usando 64 caratteri
stampabili: le lettere maiuscole e minuscole, le cifre, + e /.
Prova. Scrivi nel riquadro e osserva la codifica. In UTF-8 le lettere accentate occupano due byte e il simbolo € tre: il conteggio dei byte lo mostra.
Per distinguere 64 simboli bastano 6 bit. Base64 divide quindi i dati in gruppi di 6 bit
invece che di 8: ogni gruppo di 3 byte (24 bit) diventa 4 caratteri, e il risultato è circa un
terzo più lungo dell'originale. Quando il numero di byte non è un multiplo di 3, la codifica si
completa con uno o due caratteri =. Per testi di pochi byte l'aumento percentuale è
maggiore, perché questi caratteri di riempimento pesano di più.
Prova. Aggiungi o togli un carattere alla volta e osserva la percentuale
e il numero di = finali.
La decodifica ricostruisce esattamente i byte originali senza bisogno di alcuna chiave. Questa è la differenza con la cifratura: un testo cifrato si riporta in chiaro solo conoscendo la chiave, un testo in Base64 può essere decodificato da chiunque. Un esempio: nell'autenticazione HTTP Basic il browser invia nome utente e password codificati in Base64. Senza HTTPS, chi intercetta la richiesta li legge dopo una decodifica immediata.
Negli indirizzi web i caratteri + e / hanno un significato proprio, e
= separa i parametri dai loro valori. La variante Base64url sostituisce +
con - e / con _ ed elimina gli = finali, così la
stringa può essere inserita in un URL senza modifiche. È la variante usata, per esempio, nei token
JWT, che possono essere firmati con un HMAC come quello della scheda 3.
Codificare non è cifrare. Base64 cambia la rappresentazione dei dati per farli passare attraverso canali di solo testo, ma non li nasconde. Una password o un token in Base64 vanno trattati come se fossero in chiaro.