Fundamentos y criptografía

"Si pensás que la criptografía resolverá tus problemas de seguridad, no entendiste tus problemas y no entendiste la criptografía." — Bruce Schneier.

Qué vas a aprender en este capítulo

La criptografía es la base matemática de toda la seguridad moderna: HTTPS, mensajería cifrada, contraseñas guardadas, firmas digitales, blockchain. Este capítulo explica las primitivas (cifrado simétrico, asimétrico, hash) y cómo se combinan en protocolos como TLS.


1.1 Principios de defensa

💡 Intuición

La seguridad no es un solo muro alto — son múltiples capas de defensa. Si un atacante rompe la primera, encuentra otra. Si rompe esa, otra más. Esto se llama defensa en profundidad.

Una pupusería con una sola puerta de seguridad: si el ladrón la abre, entra. Una con: cerca, alarma, cámaras, caja fuerte → el ladrón puede pasar la primera, pero no las cinco.

📐 Fundamento

Principios fundamentales:

Principio Descripción Ejemplo
Defensa en profundidad Múltiples capas Firewall + WAF + auth + cifrado
Mínimo privilegio Solo permisos necesarios App de cocina no necesita acceso a pagos
Fail securely Al fallar, denegar acceso Si auth service cae, denegar login (no permitir)
Secure by default Seguro sin configuración TLS habilitado por defecto
No security through obscurity No depender del secreto del diseño Algoritmos públicos, claves secretas
Zero trust No confiar en la red interna Autenticar cada request, incluso interno

Modelo de amenazas (Threat Model):

Antes de defender, hay que saber qué se defiende y de quién:

  1. ¿Qué activos tenemos? (datos de clientes, infraestructura, reputación)
  2. ¿Quiénes son los atacantes? (script kiddies, criminales, estado-nación, insiders)
  3. ¿Qué pueden hacer? (ataques de red, ingeniería social, acceso físico)
  4. ¿Qué tan motivados están?
  5. ¿Qué pasaría si tienen éxito?

STRIDE — taxonomía de amenazas (Microsoft):

Letra Amenaza Propiedad violada
Spoofing Pretender ser otro usuario Autenticación
Tampering Modificar datos Integridad
Repudiation Negar haber hecho algo No-repudio
Information Disclosure Filtrar datos Confidencialidad
Denial of Service Hacer el sistema indisponible Disponibilidad
Elevation of Privilege Obtener permisos no autorizados Autorización

1.2 Cifrado simétrico

💡 Intuición

Cifrado simétrico: la misma clave cifra y descifra. Como una caja fuerte: la misma combinación abre y cierra.

Es muy rápido (megabytes por segundo) pero tiene un problema: ¿cómo le pasás la clave al otro lado sin que un atacante la intercepte?

📐 Fundamento

AES (Advanced Encryption Standard): el estándar moderno.

  • Tamaños de clave: 128, 192, 256 bits.
  • AES-256 es virtualmente irrompible con tecnología actual.

Modos de operación:

Modo Características Uso
ECB (Electronic Codebook) Cada bloque cifrado independientemente NUNCA usar (patrones visibles)
CBC (Cipher Block Chaining) Cada bloque depende del anterior + IV Requiere padding
GCM (Galois/Counter Mode) Cifrado autenticado (cifra + verifica integridad) Recomendado actual

Demostración: por qué ECB es malo

Si cifrás una imagen con ECB, el patrón sigue siendo visible. Con CBC o GCM, el resultado parece ruido aleatorio.

from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os

# Generar clave aleatoria de 256 bits
clave = AESGCM.generate_key(bit_length=256)

aesgcm = AESGCM(clave)
nonce = os.urandom(12)  # nonce de 96 bits — único por mensaje

# Cifrar
mensaje = b"Numero de tarjeta: 4532-1234-5678-9012"
asociado = b"pedido_id=12345"  # datos asociados (no cifrados pero autenticados)

ciphertext = aesgcm.encrypt(nonce, mensaje, asociado)
print(f"Cifrado: {ciphertext.hex()}")

# Descifrar
plaintext = aesgcm.decrypt(nonce, ciphertext, asociado)
print(f"Descifrado: {plaintext.decode()}")

# Si alguien modifica el ciphertext o el asociado, decrypt falla
# → garantía de integridad además de confidencialidad

Reglas de oro:

  • Nunca reutilizar un nonce con la misma clave — rompe la seguridad de GCM.
  • Generar la clave con os.urandom, nunca con random (no es criptográficamente seguro).
  • Rotar claves periódicamente y guardarlas en un KMS (Key Management Service: AWS KMS, HashiCorp Vault).

1.3 Cifrado asimétrico

💡 Intuición

Cifrado asimétrico: dos claves matemáticamente relacionadas. Una pública (la podés repartir libremente) y una privada (la guardás como un secreto absoluto).

  • Lo cifrado con la pública solo se descifra con la privada → confidencialidad.
  • Lo cifrado con la privada se verifica con la pública → firma digital (autenticidad).

Resuelve el problema del intercambio de claves del cifrado simétrico, pero es ~1000x más lento.

📐 Fundamento

RSA — el algoritmo asimétrico clásico:

from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes, serialization

# Generar par de claves
clave_privada = rsa.generate_private_key(public_exponent=65537, key_size=2048)
clave_publica = clave_privada.public_key()

# Cifrar con clave pública
mensaje = b"Mi mensaje secreto"
ciphertext = clave_publica.encrypt(
    mensaje,
    padding.OAEP(
        mgf=padding.MGF1(algorithm=hashes.SHA256()),
        algorithm=hashes.SHA256(),
        label=None
    )
)

# Descifrar con clave privada
plaintext = clave_privada.decrypt(
    ciphertext,
    padding.OAEP(
        mgf=padding.MGF1(algorithm=hashes.SHA256()),
        algorithm=hashes.SHA256(),
        label=None
    )
)
print(plaintext.decode())  # "Mi mensaje secreto"

# Firma digital
firma = clave_privada.sign(
    mensaje,
    padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH),
    hashes.SHA256()
)

# Verificar firma con clave pública
clave_publica.verify(
    firma,
    mensaje,
    padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH),
    hashes.SHA256()
)  # No lanza excepción → firma válida

Comparación de algoritmos asimétricos:

Algoritmo Tamaño de clave típico Velocidad Uso
RSA-2048 2048 bits Lenta Estándar histórico, en migración
RSA-4096 4096 bits Más lenta Mayor seguridad
ECDSA P-256 256 bits Rápida Recomendado actual
Ed25519 256 bits Muy rápida Recomendado actual (SSH, TLS modernos)

Cifrado híbrido (lo que realmente se usa):

Como asimétrico es lento, en la práctica se combina:

1. Generar una clave simétrica aleatoria (clave de sesión)
2. Cifrar el mensaje grande con la clave simétrica (AES)
3. Cifrar la clave simétrica con la pública del destinatario (RSA)
4. Enviar: ciphertext_simétrico + clave_de_sesión_cifrada

Esto es exactamente lo que hace TLS internamente.


1.4 Funciones hash criptográficas

📐 Fundamento

Una función hash: transforma datos de cualquier tamaño en una huella de tamaño fijo.

import hashlib

mensaje = b"Pupusas de queso"
print(hashlib.sha256(mensaje).hexdigest())
# c8e9...9aa3 (64 caracteres hex = 256 bits)

# Cualquier cambio mínimo cambia el hash completamente
mensaje2 = b"pupusas de queso"  # solo cambia mayúscula
print(hashlib.sha256(mensaje2).hexdigest())
# 1f4b...8e02 (completamente diferente — efecto avalancha)

Propiedades criptográficas:

Propiedad Significado
Preimagen Dado h, no se puede encontrar m tal que hash(m)=h
Segunda preimagen Dado m1, no se puede encontrar m2 ≠ m1 con hash(m1)=hash(m2)
Resistencia a colisiones No se pueden encontrar dos mensajes m1, m2 con hash(m1)=hash(m2)
Efecto avalancha Cambio mínimo en input → cambio masivo en output

Algoritmos hash:

Algoritmo Tamaño Estado
MD5 128 bits ROTO — no usar
SHA-1 160 bits ROTO — no usar
SHA-256 256 bits Recomendado
SHA-512 512 bits Recomendado para mayor seguridad
BLAKE2 Variable Más rápido que SHA-2
BLAKE3 Variable El más moderno y rápido

Hashing para contraseñas — NUNCA usar SHA por sí solo:

# MAL — SHA-256 puro es muy rápido, fácil de bruteforcear
password_hash = hashlib.sha256(b"contrasena123").hexdigest()
# Un GPU moderno puede probar miles de millones de hashes/segundo

# BIEN — usar bcrypt, scrypt, o Argon2 (lentos y con sal automática)
import bcrypt
password_hash = bcrypt.hashpw(b"contrasena123", bcrypt.gensalt(rounds=12))
# bcrypt es deliberadamente lento → solo se pueden probar miles/segundo

HMAC (Hash-based Message Authentication Code):

Verifica que un mensaje no fue modificado y que viene de quien dice:

import hmac, hashlib

clave_compartida = b"clave_secreta_compartida_entre_partes"
mensaje = b"transferencia: $100 de A a B"

# Emisor genera HMAC
mac = hmac.new(clave_compartida, mensaje, hashlib.sha256).hexdigest()

# Receptor verifica
mac_recibido = mac
mac_calculado = hmac.new(clave_compartida, mensaje, hashlib.sha256).hexdigest()

if hmac.compare_digest(mac_recibido, mac_calculado):
    print("Mensaje válido y no modificado")

Importante: Usar hmac.compare_digest() en lugar de == para evitar timing attacks (un atacante puede medir cuánto tarda la comparación letra por letra y deducir el HMAC correcto).


1.5 TLS y la PKI

💡 Intuición

Cuando entrás a https://banco.com, ¿cómo sabe tu navegador que está hablando con el banco real y no con un imitador? La respuesta: certificados digitales firmados por Autoridades Certificadoras (CAs) en las que tu navegador confía.

📐 Fundamento

TLS Handshake (TLS 1.3 simplificado):

Cliente                                    Servidor
   │                                            │
   │ ──── ClientHello (cipher suites, ECDHE) ──→│
   │                                            │
   │ ←──── ServerHello + Certificate + ────────│
   │       Firma digital + ECDHE_pubkey         │
   │                                            │
   │ Verifica certificado contra CAs confiables │
   │ Calcula clave maestra (ECDHE)              │
   │                                            │
   │ ──── Finished (cifrado con clave maestra) ─→│
   │                                            │
   │ ←──── Aplicación: HTTP cifrado ────────────│

Verificación del certificado:

  1. El servidor envía su certificado (contiene clave pública + dominio + firma de CA).
  2. El cliente verifica que la firma sea válida usando la clave pública de la CA.
  3. La clave pública de la CA está pre-instalada en el sistema operativo / navegador.
  4. El cliente verifica que el dominio del certificado coincida con el dominio al que se conectó.
  5. Verifica que el certificado no esté expirado o revocado.

Cadena de confianza:

Root CA (DigiCert, Let's Encrypt, etc.)  ← preinstalada en SO
   │ firma
   ▼
Intermediate CA
   │ firma
   ▼
Certificate de la-esquina.com  ← contiene clave pública del servidor

Let's Encrypt: Autoridad Certificadora gratuita y automatizada. Permite obtener certificados TLS válidos sin costo:

# En el servidor de producción
certbot --nginx -d la-esquina.com -d www.la-esquina.com
# → instala automáticamente certificado válido por 90 días
# → certbot lo renueva automáticamente

Forward Secrecy: Si la clave privada del servidor es comprometida en el futuro, los mensajes pasados siguen siendo seguros (porque la clave de sesión se generó efímeramente con ECDHE y nunca se guardó). TLS 1.3 lo hace por defecto.


1.6 Ejercicios

✏️ Ejercicio 1.1 — Elegir el algoritmo correcto

Para cada caso de uso, elegí el algoritmo apropiado:

a. Cifrar la base de datos completa de pagos en disco (encryption at rest). b. Verificar que un archivo descargado no fue modificado. c. Guardar la contraseña de un usuario en la base de datos. d. Firmar un documento electrónico para que sea legalmente válido. e. Cifrar la comunicación entre el navegador del cliente y el servidor de La Esquina Cloud.

✏️ Ejercicio 1.2 — Modelo de amenazas (STRIDE)

Para cada componente de La Esquina Cloud, identificá la amenaza STRIDE más crítica y propone una mitigación:

a. API que recibe órdenes de pedidos desde la app móvil. b. Base de datos de pagos. c. Panel administrativo del dueño (web). d. Servicio de notificaciones por SMS.


1.7 Para profundizar


Definiciones nuevas: triada CIA, defensa en profundidad, modelo de amenazas, STRIDE, cifrado simétrico, AES, GCM, nonce, IV, cifrado asimétrico, RSA, ECDSA, Ed25519, función hash criptográfica, SHA-256, bcrypt, HMAC, timing attack, TLS, PKI, certificado digital, CA, Let's Encrypt, Forward Secrecy, ECDHE.