hmac — Firma y verificación criptográfica de mensajes

Propósito:El módulo hmac implementa el hashing con clave para la autenticación de mensajes, como describe el RFC 2104.

El algoritmo HMAC se puede utilizar para verificar la integridad de la información transmitida entre aplicaciones o almacenada en un lugar potencialmente vulnerable. La idea básica es generar un hash criptográfico de los datos reales combinados con una clave secreta compartida. El hash resultante puede luego ser utilizado para verificar el mensaje transmitido o almacenado para determinar un nivel de confianza, sin transmitir la clave secreta.

Advertencia

Descargo de responsabilidad: no soy un experto en seguridad. Para los detalles completos soble el HMAC, echa un vistazo a RFC 2104.

Firma de mensajes

La función new() crea un objeto nuevo para calcular un firma de mensaje. Este ejemplo utiliza el algoritmo hash MD5 predeterminado.

hmac_simple.py
import hmac

digest_maker = hmac.new(b'secret-shared-key-goes-here')

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)

Cuando el ejecutado, el código lee un archivo de datos y calcula un HMAC firma para él.

$ python3 hmac_simple.py

4bcb287e284f8c21e87e14ba2dc40b16

Tipos de resumen alternativos

Aunque el algoritmo criptográfico predeterminado para hmac es MD5, ese no es el método más seguro para usar. Los hashes MD5 tienen algunos debilidades, como las colisiones (donde dos mensajes diferentes producen el mismo hash). El algoritmo SHA-1 se considera más fuerte, y debe ser utilizado en su lugar.

hmac_sha.py
import hmac
import hashlib

digest_maker = hmac.new(
    b'secret-shared-key-goes-here',
    b'',
    hashlib.sha1,
)

with open('hmac_sha.py', 'rb') as f:
    while True:
        block = f.read(1024)
        if not block:
            break
        digest_maker.update(block)

digest = digest_maker.hexdigest()
print(digest)

La función new() toma tres argumentos. El primero es la clave secreta, que debe compartirse entre los dos puntos finales que están comunicando para que ambos extremos puedan usar el mismo valor. El segundo valor es un mensaje inicial. Si el contenido del mensaje que necesita ser autenticado es pequeño, como una marca de tiempo o HTTP POST, todo el cuerpo del mensaje se puede pasar a new() en lugar de usar el método update(). El último argumento es el módulo de resumen a ser usado. El valor predeterminado es hashlib.md5. Este ejemplo pasa 'sha1', causando que hmac use hashlib.sha1

$ python3 hmac_sha.py

dcee20eeee9ef8a453453f510d9b6765921cf099

Resúmenes binarios

Los ejemplos anteriores usaron el método hexdigest() para producir resúmenes imprimibles. El resumen hexadecimal es una representación diferente de la valor calculado por el método digest(), que es un valor binario que puede incluir caracteres no imprimibles, incluyendo NUL. Algunos servicios Web (Google Checkout, Amazon S3) utilizan la versión codificada en base64 del resumen binario en lugar del resumen hexadecimal.

hmac_base64.py
import base64
import hmac
import hashlib

with open('lorem.txt', 'rb') as f:
    body = f.read()

hash = hmac.new(
    b'secret-shared-key-goes-here',
    body,
    hashlib.sha1,
)

digest = hash.digest()
print(base64.encodestring(digest))

La cadena codificada en base64 termina en una nueva línea, que con frecuencia necesita se eliminada al insertar la cadena en los encabezados http u otros contextos sensibles al formateo.

$ python3 hmac_base64.py

b'olW2DoXHGJEKGU0aE9fOwSVE/o4=\n'

Aplicaciones de firmas de mensajes

La autenticación HMAC debe utilizarse para cualquier servicio de red pública, y en cualquier momento que los datos se almacenan donde la seguridad es importante. Por ejemplo, cuando el envío de datos a través de una red o un socket, dichos datos deben estar firmados y luego se debe probar la firma antes de utilizar los datos. El ejemplo extendido dado aquí está disponible en el archivo hmac_pickle.py.

El primer paso es establecer una función para calcular un resumen para una cadena, y una clase simple para ser instanciada y pasada a través de un canal de comunicación.

hmac_pickle.py
import hashlib
import hmac
import io
import pickle
import pprint


def make_digest(message):
    "Return a digest for the message."
    hash = hmac.new(
        b'secret-shared-key-goes-here',
        message,
        hashlib.sha1,
    )
    return hash.hexdigest().encode('utf-8')


class SimpleObject:
    """Demonstrate checking digests before unpickling.
    """

    def __init__(self, name):
        self.name = name

    def __str__(self):
        return self.name

A continuación, crea un buffer BytesIO para representar el socket o pipe. El ejemplo utiliza un formato ingenuo, pero fácil de analizar, para el flujo de datos. El resumen y la longitud de los datos se escriben, seguidos de un línea nueva. La representación serializada del objeto, generada por pickle, sigue. Un sistema real no querría depender de un valor de longitud, ya que si el resumen es incorrecto, la longitud está también probablemente mal. Algún tipo de secuencia de terminador que no es probable que aparezca en los datos reales sería más apropiada.

El programa de ejemplo luego escribe dos objetos en el flujo. El primero se escribe utilizando el valor de resumen correcto.

# Simulate a writable socket or pipe with a buffer
out_s = io.BytesIO()

# Write a valid object to the stream:
#  digest\nlength\npickle
o = SimpleObject('digest matches')
pickled_data = pickle.dumps(o)
digest = make_digest(pickled_data)
header = b'%s %d\n' % (digest, len(pickled_data))
print('WRITING: {}'.format(header))
out_s.write(header)
out_s.write(pickled_data)

El segundo objeto se escribe en el flujo con un resumen no válido, producido mediante el cálculo del resumen de algunos otros datos en lugar de la serialización.

# Write an invalid object to the stream
o = SimpleObject('digest does not match')
pickled_data = pickle.dumps(o)
digest = make_digest(b'not the pickled data at all')
header = b'%s %d\n' % (digest, len(pickled_data))
print('\nWRITING: {}'.format(header))
out_s.write(header)
out_s.write(pickled_data)

out_s.flush()

Ahora que los datos están en el buffer BytesIO, se pueden leer de nuevo. Comienza leyendo la línea de datos con el resumen y la longitud de los datos. Luego lee los datos restantes, usando la longitud valor. pickle.load() podría leer directamente del flujo, pero que asume un flujo de datos de confianza y estos datos aún no son de confianza suficiente para deshacerlos. Leer la serialización como una cadena del flujo, sin desentrañar realmente el objeto, es más seguro.

# Simulate a readable socket or pipe with a buffer
in_s = io.BytesIO(out_s.getvalue())

# Read the data
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('\nREAD:', incoming_digest, incoming_length)

Una vez que los datos están de serializados la memoria, el valor de resumen puede ser recalculado y comparado con los datos leídos usando compare_digest(). Si los resúmenes coinciden, es seguro confiar en el datos y descomprimirlos.

    incoming_pickled_data = in_s.read(incoming_length)

    actual_digest = make_digest(incoming_pickled_data)
    print('ACTUAL:', actual_digest)

    if hmac.compare_digest(actual_digest, incoming_digest):
        obj = pickle.loads(incoming_pickled_data)
        print('OK:', obj)
    else:
        print('WARNING: Data corruption')

La salida muestra que el primer objeto se verifica y el segundo se considera «corrupto», como se esperaba.

$ python3 hmac_pickle.py

WRITING: b'f49cd2bf7922911129e8df37f76f95485a0b52ca 69\n'

WRITING: b'b01b209e28d7e053408ebe23b90fe5c33bc6a0ec 76\n'

READ: b'f49cd2bf7922911129e8df37f76f95485a0b52ca' 69
ACTUAL: b'f49cd2bf7922911129e8df37f76f95485a0b52ca'
OK: digest matches

READ: b'b01b209e28d7e053408ebe23b90fe5c33bc6a0ec' 76
ACTUAL: b'2ab061f9a9f749b8dd6f175bf57292e02e95c119'
WARNING: Data corruption

La comparación de dos resúmenes con una cadena simple o una comparación de bytes puede ser utilizada en un ataque de tiempo para exponer parte o la totalidad de la clave secreta pasando resúmenes de diferentes longitudes. compare_digest() implementa una función de comparación rápida pero de tiempo constante para proteger contra los ataques de tiempo.

Ver también