hmac - Firma Crittografica e Verifica di Messaggi
Scopo: Il modulo hmac implementa la modalità keyed-hashing per l'autenticazione di messaggi, come descritto in RFC 2104
L'algoritmo HMAC Può essere usato per verificare l'integrità delle informazioni passate tra applicazioni o salvate in un luogo potenzialmente vulnerabile. L'idea base è di generare un hash crittografico dei dati effettivi, combinato con una chiave segreta condivisa. L'hash che ne risulta può poi essere usato per controllare i messaggi trasmessi o salvati per determinare un livello di fiducia, senza trasmettere la chiave segreta.
Firmare i Messaggi
La funzione new()
crea un nuovo oggetto per il calcolo della firma di un messaggio. Questo esempio usa l'algoritmo predefinito MD5.
# hmac_simple.py
import hmac
digest_maker = hmac.new(b'la-chiave-segreta-condivisa-va-qui')
with open('lorem.txt', 'rb') as f:
while True:
block = f.read(1024)
if not block:
break
digest_maker.update(block)
digest = digest_maker.hexdigest()
print(digest)
Quando eseguito, il codice legge un file di dati e calcola la firma HMAC per esso.
$ python3 hmac_simple.py Traceback (most recent call last): File "hmac_simple.py", line 5, in <module> digest_maker = hmac.new(b'la-chiave-segreta-condivisa-va-qui') File "/usr/lib/python3.8/hmac.py", line 153, in new return HMAC(key, msg, digestmod) File "/usr/lib/python3.8/hmac.py", line 51, in __init__ raise TypeError("Missing required parameter 'digestmod'.") TypeError: Missing required parameter 'digestmod'.
Algoritmi di Cifratura Alternativi
Sebbene l'algoritmo di cifratura predefinito per hmac sia MD5, non è il metodo più sicuro da usare. Gli hash MD5 hanno qualche debolezza, tipo le collisioni (laddove due messaggi diversi producono lo stesso hash). L'algoritmo SHA-1 è considerato più robusto, e dovrebbe quindi essere usato al posto di MD5.
# hmac_sha.py
import hmac
import hashlib
digest_maker = hmac.new(
b'la-chiave-segreta-condivisa-va-qui',
b'',
hashlib.sha1,
)
with open('lorem.txt', 'rb') as f:
while True:
block = f.read(1024)
if not block:
break
digest_maker.update(block)
digest = digest_maker.hexdigest()
print(digest)
La funzione new()
riceve 3 argomenti. Il primo è la chiave segreta, che dovrebbe essere condivisa tra gli estremi che stanno comunicando in modo che entrambi possano usare lo stesso valore. Il secondo parametro è un messaggio iniziale. Se il contenuto del messaggio che deve essere autenticato è di piccole dimensioni, tipo un timestamp oppure il contenuto di un HTTP POST, l'intero corpo del messaggio può essere passato a new()
invece che usare il metodo update()
. L'ultimo parametro è il tipo di crittografia da usare. Il predefinito è hashlib.md5
. In questo esempio si passa 'sha1'
facendo sì che venga usato hashlib.sha1
.
$ python3 hmac_sha.py 7136051c014b22f0f79337e4ae006b0b1b47d920
Impronte di Messaggio Binarie
Gli esempi precedenti usavano il metodo hexdigest()
per produrre una impronta di messaggio (digest) stampabile. hexdigest costituisce una diversa rappresentazione del valore calcolato dal metodo digest()
, il quale è un valore binario che potrebbe comprendere caratteri non stampabili, NUL incluso. Alcuni servizi web (Google checkout, Amazon S3) usano la versione codificata in base64 dell'impronta di messaggio binaria invece che l'hexdigest.
# hmac_base64.py
import base64
import hmac
import hashlib
with open('lorem.txt', 'rb') as f:
body = f.read()
hash = hmac.new(
b'la-chiave-segreta-condivisa-va-qui',
body,
hashlib.sha1,
)
digest = hash.digest()
print(base64.encodestring(digest))
La stringa codificata in base64 termina con una riga vuota, che frequentemente deve essere eliminata quando la stringa viene incorporata in header HTTP od altri contesti sensibili alla formattazione.
$ python3 hmac_base64.py hmac_base64.py:17: DeprecationWarning: encodestring() is a deprecated alias since 3.1, use encodebytes() print(base64.encodestring(digest)) b'cTYFHAFLIvD3kzfkrgBrCxtH2SA=\n'
Applicazioni delle Firme di Messaggi
L'autenticazione HMAC dovrebbe essere usata per un qualsiasi servizio di rete pubblico, ed ogniqualvolta che si debbano conservare dati per i quali la sicurezza è importante. Ad esempio quando si spediscono dati attraverso un socket od una pipe, essi dovrebbero essere firmati, quindi la firma dovrebbe essere verificata prima che i dati vengano usati. L'esempio esteso qui sotto è a disposizione nel file hmac_pickle.py
, di seguito ne viene discusso il contenuto in parti separate.
Per prima cosa si imposta una funzione per calcolare l'impronta di messaggio di una stringa, ed una semplice classe da istanziare e passare attraverso un canale di comunicazione.
# hmac_pickle.py
import hashlib
import hmac
import io
import pickle
import pprint
def make_digest(message):
"Restituisce una impronta di messaggio per message"
hash = hmac.new(
b'la-chiave-segreta-condivisa-va-qui',
message,
hashlib.sha1,
)
return hash.hexdigest().encode('utf-8')
class SimpleObject:
"""Dimostra la verifica di un impronta di messaggio prima di
deserializzarlo
"""
def __init__(self, name):
self.name = name
def __str__(self):
return self.name
Successivamente si crea un buffer BytesIO
per rappresentare un socket od una pipe. In questo esempio si usa un formato piuttosto semplice, ma facile da elaborare, per il flusso di dati. L'impronta del messaggio e la lunghezza dei dati vengono scritti seguiti da una riga vuota. A seguire si procede alla rappresentazione serializzata dell'oggetto, in genere utilizzando pickle. In un sistema reale, non si vuole dipendere da un valore di lunghezza, visto che se l'impronta del messaggio è sbagliata probabilmente sarà errata anche la lunghezza. Un qualche tipo di sequenza di terminazione che sia improbabile possa figurare anche nei dati reali sarebbe stata molto più appropriata.
Il programma di esempio poi scrive due oggetti nel flusso. Il primo è scritto usando il valore di impronta di messaggio corretto.
# Simula un socket o pipe su cui scrivere con un buffer
out_s = io.BytesIO()
# Scrive un oggetto valido nel flusso:
# digest\nlength\npickle
o = SimpleObject('impronta di messaggio corrisponde')
pickled_data = pickle.dumps(o)
digest = make_digest(pickled_data)
header = b'%s %d\n' % (digest, len(pickled_data))
print('IN SCRITTURA: {}'.format(header))
out_s.write(header)
out_s.write(pickled_data)
Il secondo oggetto viene scritto nel flusso con una impronta di messaggio non valida, prodotta calcolandola con altri dati invece del pickle.
# Scrive un oggetto non valido per il flusso
o = SimpleObject('impronta di messaggio non corrisponde')
pickled_data = pickle.dumps(o)
digest = make_digest(b'non utilizzo i dati serializzati')
header = b'%s %d\n' % (digest, len(pickled_data))
print('IN SCRITTURA: {}'.format(header))
out_s.write(header)
out_s.write(pickled_data)
out_s.flush()
Adesso che i dati sono nel buffer BytestIO
, è possibile rileggerli. Il primo passo è leggere la riga di dati con l'impronta di messaggio e la lunghezza dati. Quindi si leggono i dati restanti usando il valore di lunghezza. pickle.load()
avrebbe potuto leggere direttamente dal flusso, ma questo implica che ci sia un flusso di dati fidato; questi dati tuttavia non sono sufficientemente sicuri per deserializzarli. La lettura del pickle come stringa dal flusso, senza realmente eseguire la deserializzazione dell'oggetto è più sicura.
# Simula un socket o pipe leggibile con un buffer
in_s = io.BytesIO(out_s.getvalue())
# Legge i dati
while True:
first_line = in_s.readline()
if not first_line:
break
incoming_digest, incoming_length = first_line.split(b' ')
incoming_length = int(incoming_length.decode('utf-8'))
print('\nIN LETTURA:', incoming_digest, incoming_length)
Una volta che abbiamo i dati serializzati in memoria, si può ricalcolare il valore dell'impronta di messaggio e confrontarlo con i dati letti usando compare_digest()
. Se le impronte corrispondono, si presume sia sicuro fidarsi dei dati, quindi vengono deserializzati.
incoming_pickled_data = in_s.read(incoming_length)
actual_digest = make_digest(incoming_pickled_data)
print('REALI:', actual_digest)
if hmac.compare_digest(actual_digest, incoming_digest):
obj = pickle.loads(incoming_pickled_data)
print('OK:', obj)
else:
print('ATTENZIONE: Dati corrotti')
Il risultato mostra che il primo oggetto è verificato ma il secondo viene considerato come "corrotto", come previsto.
$ python3 hmac_pickle.py IN SCRITTURA: b'a0d74798bb0950335feadd6bc2d51bc9d8bfffbe 88\n' IN SCRITTURA: b'858fb8ac42a974aa6a88cead2b5082fa96b4308a 92\n' IN LETTURA: b'a0d74798bb0950335feadd6bc2d51bc9d8bfffbe' 88 REALI: b'a0d74798bb0950335feadd6bc2d51bc9d8bfffbe' OK: impronta di messaggio corrisponde IN LETTURA: b'858fb8ac42a974aa6a88cead2b5082fa96b4308a' 92 REALI: b'3a9b4de15e8ae4b739e0437051699afa8c83cb6a' ATTENZIONE: Dati corrotti
Il confronto di due impronte di messaggio tramite semplice stringa o byte può essere utilizzato in un attacco a tempo per esporre parte o l'intera chiave segreta passando impronte di messaggio di lunghezza diversa. compare_digest()
implementa una funzione di confronto veloce ma con tempo costante per la protezione contro il tipo di attacco sopra esposto.
Vedere anche:
- hmac
- La documentazione della libreria standard per questo modulo.
- RFC 2104
- HMAC: Keyed-Hashing for Message Authentication
- hashlib
- Il modulo hashlib
- pickle
- Libreria di serializzazione
- MD5
- La descrizione dell'algoritmo MD5.
- Signing and Authenticating REST Requests (Amazon AWS)
- Istruzioni per autenticarsi ad S3 usando le credenziali firmate HMAC-SHA1