Curso de Criptograf铆a Post-Cu谩ntica

Un enfoque introductorio a la seguridad en la era cu谩ntica

Gu铆a de Laboratorio: Implementaci贸n de ML-KEM

Objetivos de Aprendizaje

Requisitos Previos

Nota: Esta pr谩ctica implementa una versi贸n simplificada de ML-KEM con fines educativos. No debe utilizarse en entornos de producci贸n.

Introducci贸n Te贸rica

ML-KEM (Module-Lattice-Based Key Encapsulation Mechanism), anteriormente conocido como Kyber, es un mecanismo de encapsulamiento de claves basado en ret铆culos modulares. Fue seleccionado por el NIST como el est谩ndar para encapsulamiento de claves post-cu谩ntico (FIPS 203).

ML-KEM basa su seguridad en la dificultad del problema de Learning With Errors (LWE) en su variante modular. Este problema es considerado resistente a ataques cu谩nticos, incluyendo el algoritmo de Shor.

El esquema consta de tres algoritmos principales:

  1. KeyGen: Genera un par de claves p煤blica/privada.
  2. Encaps: Utiliza la clave p煤blica para encapsular (cifrar) una clave sim茅trica compartida.
  3. Decaps: Utiliza la clave privada para desencapsular (descifrar) la clave sim茅trica compartida.

ML-KEM viene en tres variantes con diferentes niveles de seguridad:

Parte 1: Implementaci贸n Simplificada de ML-KEM

1.1 Configuraci贸n del Entorno

Crea un nuevo archivo Python llamado ml_kem_simplified.py e importa las bibliotecas necesarias:

import numpy as np
import hashlib
import os
import matplotlib.pyplot as plt
from scipy.stats import norm

1.2 Implementaci贸n de Funciones Auxiliares

Primero, implementaremos algunas funciones auxiliares necesarias:

# Par谩metros para ML-KEM-512 simplificado
PARAMS = {
    'n': 256,       # Dimensi贸n del polinomio
    'k': 2,         # Dimensi贸n del m贸dulo
    'q': 3329,      # M贸dulo
    'eta1': 3,      # Par谩metro de ruido para la clave secreta
    'eta2': 2,      # Par谩metro de ruido para el error
    'du': 10,       # Bits para comprimir u
    'dv': 4,        # Bits para comprimir v
}

# Funci贸n para generar una distribuci贸n centrada binomial
def centered_binomial_distribution(eta, size):
    """
    Genera muestras de una distribuci贸n centrada binomial con par谩metro eta.
    Esta es una aproximaci贸n a la distribuci贸n normal discreta utilizada en ML-KEM.
    """
    a = np.random.randint(0, 2, size=(size, eta))
    b = np.random.randint(0, 2, size=(size, eta))
    return np.sum(a, axis=1) - np.sum(b, axis=1)

# Funci贸n para generar una matriz A uniforme aleatoria
def gen_matrix_A(k, n, q, seed=None):
    """
    Genera una matriz A uniforme aleatoria de dimensiones (k, k, n) con coeficientes en Z_q.
    En la implementaci贸n real, esto se har铆a usando XOF (funci贸n de expansi贸n de salida extendida).
    """
    if seed is not None:
        np.random.seed(seed)
    return np.random.randint(0, q, size=(k, k, n))

# Funci贸n para comprimir un polinomio
def compress(x, d, q):
    """
    Comprime un polinomio x m贸dulo q a d bits por coeficiente.
    """
    factor = 2**d / q
    return np.round(factor * x).astype(int) % (2**d)

# Funci贸n para descomprimir un polinomio
def decompress(x, d, q):
    """
    Descomprime un polinomio x de d bits por coeficiente a m贸dulo q.
    """
    factor = q / (2**d)
    return np.round(factor * x).astype(int) % q

# Funci贸n para multiplicar polinomios en el anillo R_q
def poly_mul(a, b, q):
    """
    Multiplica dos polinomios en el anillo R_q = Z_q[X]/(X^n + 1).
    Esta es una implementaci贸n simplificada que no utiliza NTT para eficiencia.
    """
    n = len(a)
    c = np.zeros(n, dtype=int)
    
    for i in range(n):
        for j in range(n):
            idx = (i + j) % n
            if i + j >= n:
                c[idx] = (c[idx] - a[i] * b[j]) % q
            else:
                c[idx] = (c[idx] + a[i] * b[j]) % q
    
    return c

# Funci贸n para multiplicar matrices de polinomios
def matrix_poly_mul(A, s, q):
    """
    Multiplica una matriz de polinomios A por un vector de polinomios s.
    """
    k = A.shape[0]
    n = A.shape[2]
    result = np.zeros((k, n), dtype=int)
    
    for i in range(k):
        for j in range(k):
            result[i] = (result[i] + poly_mul(A[i, j], s[j], q)) % q
    
    return result

# Funci贸n para a帽adir vectores de polinomios
def poly_add(a, b, q):
    """
    Suma dos vectores de polinomios m贸dulo q.
    """
    return (a + b) % q

# Funci贸n para generar un hash
def hash_function(data):
    """
    Funci贸n de hash utilizada en ML-KEM.
    En la implementaci贸n real, se utilizar铆an funciones espec铆ficas como SHA3.
    """
    return hashlib.sha256(data).digest()

1.3 Implementaci贸n de los Algoritmos Principales

Ahora implementaremos los tres algoritmos principales de ML-KEM:

# Algoritmo KeyGen
def keygen(params=PARAMS):
    """
    Genera un par de claves p煤blica/privada para ML-KEM.
    """
    n = params['n']
    k = params['k']
    q = params['q']
    eta1 = params['eta1']
    
    # Generar matriz A aleatoria
    A = gen_matrix_A(k, n, q)
    
    # Generar vector secreto s con ruido
    s = np.zeros((k, n), dtype=int)
    for i in range(k):
        s[i] = centered_binomial_distribution(eta1, n)
    
    # Generar vector de error e con ruido
    e = np.zeros((k, n), dtype=int)
    for i in range(k):
        e[i] = centered_binomial_distribution(eta1, n)
    
    # Calcular t = A路s + e
    t = poly_add(matrix_poly_mul(A, s, q), e, q)
    
    # Clave p煤blica: (t, A)
    # Clave privada: s
    return {'public': (t, A), 'private': s}

# Algoritmo Encaps
def encaps(public_key, params=PARAMS):
    """
    Encapsula una clave sim茅trica compartida utilizando la clave p煤blica.
    """
    t, A = public_key
    n = params['n']
    k = params['k']
    q = params['q']
    eta1 = params['eta1']
    eta2 = params['eta2']
    du = params['du']
    dv = params['dv']
    
    # Generar vector r con ruido
    r = np.zeros((k, n), dtype=int)
    for i in range(k):
        r[i] = centered_binomial_distribution(eta1, n)
    
    # Generar vector de error e1 con ruido
    e1 = np.zeros((k, n), dtype=int)
    for i in range(k):
        e1[i] = centered_binomial_distribution(eta2, n)
    
    # Generar error e2 con ruido
    e2 = centered_binomial_distribution(eta2, n)
    
    # Calcular u = A^T路r + e1
    u = poly_add(matrix_poly_mul(np.transpose(A, (1, 0, 2)), r, q), e1, q)
    
    # Calcular v = t^T路r + e2 + 鈱坬/2鈱嬄穖
    m = np.random.randint(0, 2, size=n)  # Mensaje aleatorio binario
    v_temp = np.zeros(n, dtype=int)
    for i in range(k):
        v_temp = (v_temp + poly_mul(t[i], r[i], q)) % q
    v = (v_temp + e2 + ((q // 2) * m)) % q
    
    # Comprimir u y v
    u_compressed = np.zeros((k, n), dtype=int)
    for i in range(k):
        u_compressed[i] = compress(u[i], du, q)
    v_compressed = compress(v, dv, q)
    
    # Calcular la clave compartida
    shared_key = hash_function(np.concatenate([u_compressed.flatten(), v_compressed]).tobytes())
    
    # Cifrado: (u_compressed, v_compressed)
    ciphertext = (u_compressed, v_compressed)
    
    return ciphertext, shared_key

# Algoritmo Decaps
def decaps(ciphertext, private_key, public_key, params=PARAMS):
    """
    Desencapsula la clave sim茅trica compartida utilizando la clave privada.
    """
    u_compressed, v_compressed = ciphertext
    s = private_key
    n = params['n']
    k = params['k']
    q = params['q']
    du = params['du']
    dv = params['dv']
    
    # Descomprimir u y v
    u = np.zeros((k, n), dtype=int)
    for i in range(k):
        u[i] = decompress(u_compressed[i], du, q)
    v = decompress(v_compressed, dv, q)
    
    # Calcular v' = u^T路s
    v_prime = np.zeros(n, dtype=int)
    for i in range(k):
        v_prime = (v_prime + poly_mul(u[i], s[i], q)) % q
    
    # Recuperar m
    m_prime = np.zeros(n, dtype=int)
    for i in range(n):
        # Comparar v[i] con v_prime[i] para determinar si el bit es 0 o 1
        diff = (v[i] - v_prime[i]) % q
        if diff > q // 4 and diff < 3 * q // 4:
            m_prime[i] = 1
    
    # Calcular la clave compartida
    shared_key = hash_function(np.concatenate([u_compressed.flatten(), v_compressed]).tobytes())
    
    return shared_key

1.4 Funci贸n Principal y Visualizaci贸n

Finalmente, implementaremos la funci贸n principal y algunas visualizaciones:

def main():
    print("Implementaci贸n simplificada de ML-KEM (anteriormente Kyber)")
    print("Par谩metros utilizados:", PARAMS)
    
    # Generar claves
    print("\nGenerando par de claves...")
    keys = keygen()
    public_key = keys['public']
    private_key = keys['private']
    print("Claves generadas.")
    
    # Encapsular clave compartida
    print("\nEncapsulando clave compartida...")
    ciphertext, shared_key_sender = encaps(public_key)
    print("Clave encapsulada.")
    print("Longitud de la clave compartida:", len(shared_key_sender), "bytes")
    print("Primeros bytes de la clave compartida:", shared_key_sender[:8].hex())
    
    # Desencapsular clave compartida
    print("\nDesencapsulando clave compartida...")
    shared_key_receiver = decaps(ciphertext, private_key, public_key)
    print("Clave desencapsulada.")
    print("Primeros bytes de la clave desencapsulada:", shared_key_receiver[:8].hex())
    
    # Verificar que ambas partes tienen la misma clave
    if shared_key_sender == shared_key_receiver:
        print("\n隆脡xito! Ambas partes han establecido la misma clave compartida.")
    else:
        print("\n隆Error! Las claves compartidas no coinciden.")
    
    # Visualizaciones
    plt.figure(figsize=(15, 10))
    
    # Distribuci贸n de la clave privada
    plt.subplot(2, 2, 1)
    s_flat = private_key.flatten()
    plt.hist(s_flat, bins=range(min(s_flat)-1, max(s_flat)+2), alpha=0.7, rwidth=0.85)
    plt.title('Distribuci贸n de la clave privada')
    plt.xlabel('Valor')
    plt.ylabel('Frecuencia')
    plt.grid(True, alpha=0.3)
    
    # Distribuci贸n de la clave p煤blica
    plt.subplot(2, 2, 2)
    t_flat = public_key[0].flatten()
    plt.hist(t_flat, bins=30, alpha=0.7, rwidth=0.85)
    plt.title('Distribuci贸n de la clave p煤blica (t)')
    plt.xlabel('Valor')
    plt.ylabel('Frecuencia')
    plt.grid(True, alpha=0.3)
    
    # Distribuci贸n del cifrado (u)
    plt.subplot(2, 2, 3)
    u_flat = ciphertext[0].flatten()
    plt.hist(u_flat, bins=range(min(u_flat)-1, max(u_flat)+2), alpha=0.7, rwidth=0.85)
    plt.title('Distribuci贸n del cifrado (u comprimido)')
    plt.xlabel('Valor')
    plt.ylabel('Frecuencia')
    plt.grid(True, alpha=0.3)
    
    # Distribuci贸n del cifrado (v)
    plt.subplot(2, 2, 4)
    v_flat = ciphertext[1]
    plt.hist(v_flat, bins=range(min(v_flat)-1, max(v_flat)+2), alpha=0.7, rwidth=0.85)
    plt.title('Distribuci贸n del cifrado (v comprimido)')
    plt.xlabel('Valor')
    plt.ylabel('Frecuencia')
    plt.grid(True, alpha=0.3)
    
    plt.tight_layout()
    plt.savefig('ml_kem_visualization.png')
    plt.show()

if __name__ == "__main__":
    main()

Parte 2: An谩lisis de Seguridad y Rendimiento

2.1 Comparaci贸n con ECDH

Implementaremos una versi贸n simplificada de ECDH (Elliptic Curve Diffie-Hellman) para comparar con ML-KEM:

# Crear un nuevo archivo ecdh_comparison.py
import time
import numpy as np
import matplotlib.pyplot as plt
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.hkdf import HKDF

# Importar nuestra implementaci贸n de ML-KEM
from ml_kem_simplified import keygen, encaps, decaps, PARAMS

def ecdh_keygen():
    """
    Genera un par de claves para ECDH.
    """
    private_key = ec.generate_private_key(ec.SECP256R1())
    public_key = private_key.public_key()
    return private_key, public_key

def ecdh_shared_key(private_key, peer_public_key):
    """
    Calcula la clave compartida ECDH.
    """
    shared_key = private_key.exchange(ec.ECDH(), peer_public_key)
    # Derivar una clave sim茅trica usando HKDF
    derived_key = HKDF(
        algorithm=hashes.SHA256(),
        length=32,
        salt=None,
        info=b'handshake data',
    ).derive(shared_key)
    return derived_key

def compare_performance(num_trials=10):
    """
    Compara el rendimiento de ML-KEM y ECDH.
    """
    # Tiempos para ML-KEM
    ml_kem_keygen_times = []
    ml_kem_encaps_times = []
    ml_kem_decaps_times = []
    
    # Tiempos para ECDH
    ecdh_keygen_times = []
    ecdh_shared_key_times = []
    
    for _ in range(num_trials):
        # ML-KEM
        start_time = time.time()
        keys = keygen()
        ml_kem_keygen_times.append(time.time() - start_time)
        
        start_time = time.time()
        ciphertext, shared_key_sender = encaps(keys['public'])
        ml_kem_encaps_times.append(time.time() - start_time)
        
        start_time = time.time()
        shared_key_receiver = decaps(ciphertext, keys['private'], keys['public'])
        ml_kem_decaps_times.append(time.time() - start_time)
        
        # ECDH
        start_time = time.time()
        alice_private, alice_public = ecdh_keygen()
        ecdh_keygen_times.append(time.time() - start_time)
        
        bob_private, bob_public = ecdh_keygen()
        
        start_time = time.time()
        alice_shared = ecdh_shared_key(alice_private, bob_public)
        ecdh_shared_key_times.append(time.time() - start_time)
        
        bob_shared = ecdh_shared_key(bob_private, alice_public)
        
        # Verificar que ambas partes tienen la misma clave
        assert alice_shared == bob_shared
        assert shared_key_sender == shared_key_receiver
    
    # Calcular promedios
    ml_kem_keygen_avg = np.mean(ml_kem_keygen_times)
    ml_kem_encaps_avg = np.mean(ml_kem_encaps_times)
    ml_kem_decaps_avg = np.mean(ml_kem_decaps_times)
    
    ecdh_keygen_avg = np.mean(ecdh_keygen_times)
    ecdh_shared_key_avg = np.mean(ecdh_shared_key_times)
    
    # Visualizar resultados
    plt.figure(figsize=(12, 6))
    
    # Tiempos de generaci贸n de claves
    plt.subplot(1, 2, 1)
    plt.bar(['ML-KEM', 'ECDH'], [ml_kem_keygen_avg, ecdh_keygen_avg])
    plt.title('Tiempo de Generaci贸n de Claves')
    plt.ylabel('Tiempo (segundos)')
    plt.grid(True, alpha=0.3)
    
    # Tiempos de establecimiento de clave compartida
    plt.subplot(1, 2, 2)
    plt.bar(['ML-KEM (Encaps+Decaps)', 'ECDH (2脳SharedKey)'], 
            [ml_kem_encaps_avg + ml_kem_decaps_avg, 2 * ecdh_shared_key_avg])
    plt.title('Tiempo de Establecimiento de Clave')
    plt.ylabel('Tiempo (segundos)')
    plt.grid(True, alpha=0.3)
    
    plt.tight_layout()
    plt.savefig('ml_kem_vs_ecdh.png')
    plt.show()
    
    # Imprimir resultados
    print("\nComparaci贸n de Rendimiento (promedio de", num_trials, "pruebas):")
    print("\nML-KEM:")
    print("  Generaci贸n de claves:", ml_kem_keygen_avg, "segundos")
    print("  Encapsulamiento:", ml_kem_encaps_avg, "segundos")
    print("  Desencapsulamiento:", ml_kem_decaps_avg, "segundos")
    print("  Total:", ml_kem_keygen_avg + ml_kem_encaps_avg + ml_kem_decaps_avg, "segundos")
    
    print("\nECDH:")
    print("  Generaci贸n de claves:", ecdh_keygen_avg, "segundos")
    print("  C谩lculo de clave compartida:", ecdh_shared_key_avg, "segundos")
    print("  Total (2 partes):", 2 * ecdh_keygen_avg + 2 * ecdh_shared_key_avg, "segundos")
    
    # Comparar tama帽os
    alice_private_serialized = alice_private.private_bytes(
        encoding=serialization.Encoding.DER,
        format=serialization.PrivateFormat.PKCS8,
        encryption_algorithm=serialization.NoEncryption()
    )
    
    alice_public_serialized = alice_public.public_bytes(
        encoding=serialization.Encoding.DER,
        format=serialization.PublicFormat.SubjectPublicKeyInfo
    )
    
    # Tama帽os aproximados para ML-KEM-512
    ml_kem_public_size = PARAMS['k'] * PARAMS['k'] * PARAMS['n'] * np.log2(PARAMS['q']) / 8
    ml_kem_private_size = PARAMS['k'] * PARAMS['n'] * np.log2(2 * PARAMS['eta1'] + 1) / 8
    ml_kem_ciphertext_size = (PARAMS['k'] * PARAMS['n'] * PARAMS['du'] + PARAMS['n'] * PARAMS['dv']) / 8
    
    print("\nTama帽os (bytes):")
    print("  ECDH clave privada:", len(alice_private_serialized))
    print("  ECDH clave p煤blica:", len(alice_public_serialized))
    print("  ML-KEM clave privada (aprox.):", int(ml_kem_private_size))
    print("  ML-KEM clave p煤blica (aprox.):", int(ml_kem_public_size))
    print("  ML-KEM cifrado (aprox.):", int(ml_kem_ciphertext_size))

if __name__ == "__main__":
    compare_performance()

Parte 3: Ejercicios y Preguntas de Reflexi贸n

3.1 Ejercicios

  1. Modifica el c贸digo para implementar ML-KEM-768 ajustando los par谩metros adecuadamente.
  2. Implementa una funci贸n para medir el tiempo de ejecuci贸n de cada operaci贸n en ML-KEM y compara los resultados con diferentes tama帽os de par谩metros.
  3. Modifica el c贸digo para simular un ataque de "clave relacionada" y analiza su efectividad.
  4. Implementa una versi贸n h铆brida que combine ML-KEM con ECDH para una transici贸n segura.

3.2 Preguntas de Reflexi贸n

  1. 驴Cu谩les son las ventajas y desventajas de ML-KEM en comparaci贸n con ECDH?
  2. 驴Por qu茅 ML-KEM utiliza una distribuci贸n centrada binomial en lugar de una distribuci贸n normal?
  3. 驴C贸mo afecta el tama帽o de los par谩metros (n, k, q) a la seguridad y eficiencia de ML-KEM?
  4. 驴Qu茅 estrategias de implementaci贸n podr铆an mejorar el rendimiento de ML-KEM en dispositivos con recursos limitados?
  5. 驴Cu谩les son los desaf铆os para la adopci贸n generalizada de ML-KEM en sistemas existentes?

Parte 4: Extensi贸n (Opcional) - Implementaci贸n en Hardware

Si tienes experiencia con programaci贸n de hardware (FPGA o microcontroladores), puedes intentar implementar una versi贸n optimizada de ML-KEM:

Esta extensi贸n es avanzada y requiere conocimientos adicionales de programaci贸n de hardware.

Entregables

Al finalizar esta pr谩ctica, deber谩s entregar:

  1. C贸digo fuente de las implementaciones (ml_kem_simplified.py y ecdh_comparison.py)
  2. Capturas de pantalla o gr谩ficos generados durante la ejecuci贸n
  3. Un informe breve (m谩ximo 3 p谩ginas) que incluya:
    • Resultados obtenidos en las pruebas de rendimiento
    • An谩lisis comparativo entre ML-KEM y ECDH
    • Respuestas a las preguntas de reflexi贸n
    • Conclusiones sobre la viabilidad de ML-KEM para aplicaciones pr谩cticas

Recursos Adicionales