Esta lección salda la deuda más antigua del curso. En el módulo 1 aprendiste los cuatro objetivos de la criptografía y desde entonces has conseguido tres: confidencialidad (módulo 2), integridad y autenticidad (módulo 3). El cuarto — el no repudio — quedó prometido, y en 03-02 viste exactamente por qué un MAC no puede dárnoslo. Hoy lo consigues con la firma digital: el dueño de una clave privada firma, y cualquiera con la pública verifica — sin secretos compartidos, sin posibilidad de negar la autoría. Verás la mecánica (se firma el hash, no el documento), los dos esquemas que importan (RSA-PSS y Ed25519), y lo aplicarás al caso central del módulo: las recetas electrónicas de MediNube, que la Dra. Lucía Ferrer de la Clínica Sol firmará y una farmacia externa verificará. Por el camino te toparás con un enemigo inesperado — la canonicalización del JSON — y con la pregunta incómoda del módulo, que aquí se vuelve urgente.
Contenido
- El cuarto objetivo: por qué el MAC no basta
- La tabla definitiva: hash vs HMAC vs firma digital
- La mecánica: firmar el hash, verificar con la pública
- Firmas con RSA: PSS, y por qué no PKCS#1 v1.5
- Firmas con Ed25519: la API mínima
- El caso central: recetas electrónicas firmadas
- El enemigo silencioso: la canonicalización
- Firmar documentos: huella + firma
- La advertencia legal: esto no es (todavía) una firma cualificada
- La farmacia y la pregunta de siempre
El cuarto objetivo: por qué el MAC no basta
Recuerda el webhook firmado de 03-02: MediNube y la Clínica Sol comparten una clave HMAC, y la cabecera X-MediNube-Firma demuestra que el mensaje lo generó alguien con esa clave y que no fue alterado. Integridad y autenticidad: conseguidas. Pero imagina esta disputa:
La Clínica Sol afirma: "MediNube nos envió la orden de borrar el historial de ana.perez, aquí está el mensaje con su HMAC válido". MediNube responde: "Ese HMAC no prueba nada: la clínica también tiene la clave y pudo fabricar el mensaje ella misma".
Y MediNube tiene razón. Con una clave compartida, todo lo que uno puede autenticar, el otro puede falsificarlo. Un HMAC válido demuestra que lo generó una de las dos partes, pero no cuál — y ante un tercero (un juez, un auditor), eso es no poder demostrar nada. Esto es la ausencia de no repudio: el emisor siempre puede repudiar (negar) la autoría.
La firma digital rompe el empate con la asimetría de 04-01: se firma con la clave privada, que solo tiene una persona, y se verifica con la pública, que puede tener todo el mundo. Si la firma verifica, la generó el dueño de la privada — nadie más pudo, ni siquiera quien verifica. El emisor ya no puede negar la autoría (mientras su privada no haya sido robada — de ahí la obsesión por protegerla).
La tabla definitiva: hash vs HMAC vs firma digital
En 03-02 dejamos esta tabla anunciada con una columna en penumbra. Aquí está completa — es de las tablas más importantes del curso:
| Hash (SHA-256) | HMAC (HMAC-SHA256) | Firma digital (Ed25519, RSA-PSS) | |
|---|---|---|---|
| ¿Usa clave? | No | Sí, compartida | Sí, par pública/privada |
| Integridad (detectar alteración) | Sí* | Sí | Sí |
| Autenticidad (saber quién lo emitió) | No | Sí, entre quienes comparten la clave | Sí, ante cualquiera |
| No repudio (el emisor no puede negar) | No | No (ambas partes pueden generar el MAC) | Sí (solo el dueño de la privada firma) |
| ¿Quién puede verificar? | Cualquiera con el hash de referencia | Solo quien tiene la clave secreta | Cualquiera con la clave pública |
| Coste computacional | Mínimo | Mínimo | Mayor (aunque Ed25519 es muy rápido) |
| Caso de uso en MediNube | Huella de exports (integridad.py), tokens (recuperacion.py) |
Webhooks (webhooks.py) |
Recetas electrónicas (esta lección) |
* El hash solo da integridad si el valor de referencia llega por un canal íntegro — la lección de 03-01.
Regla mental para elegir: si emisor y verificador son el mismo sistema o dos partes que confían mutuamente y solo temen a terceros, HMAC basta y es más simple. En cuanto necesites que terceros verifiquen o que el emisor no pueda desdecirse, necesitas firma digital. Una receta que verificará cualquier farmacia de España es el caso de libro.
La mecánica: firmar el hash, verificar con la pública
Conceptualmente, firmar un documento de 10 MB no aplica la operación asimétrica a los 10 MB (recuerda de 04-01 que RSA solo procesa unos cientos de bytes, y sería lentísimo). El esquema universal es:
- Firmar: el emisor calcula el hash del mensaje (SHA-256 o similar) y aplica la operación de firma con su clave privada sobre ese hash. El resultado — la firma — son unas decenas o cientos de bytes que viajan junto al mensaje.
- Verificar: el receptor recalcula el hash del mensaje recibido y comprueba, con la clave pública del emisor, que la firma se corresponde con ese hash. Si el mensaje cambió un solo bit, o la firma la hizo otra clave, la verificación falla.
sequenceDiagram
participant D as Dra. Ferrer (privada)
participant R as Red / almacenamiento
participant F as Farmacia (pública de la Dra.)
D->>D: h = SHA-256(receta)
D->>D: firma = sign(privada, h)
D->>R: receta + firma
R->>F: receta + firma
F->>F: h' = SHA-256(receta recibida)
F->>F: verify(pública, firma, h')
alt firma válida
F->>F: la receta es íntegra y la emitió la Dra. ✔
else InvalidSignature
F->>F: alterada o de otra clave: RECHAZAR ✘
end
Fíjate en las propiedades que emergen:
- Todo lo que aprendiste de hashes en 03-01 se hereda: la firma cubre el documento entero, y la resistencia a colisiones del hash es parte de la seguridad de la firma (por eso firmar con MD5/SHA-1 está roto de verdad, no en teoría).
- La firma no cifra: el mensaje viaja en claro junto a ella. Firma y cifrado son objetivos independientes y se combinan cuando hacen falta ambos (lo harás en 04-05).
- En las APIs de
pyca/cryptographyel hash interno lo hace la librería: tú pasas el mensaje completo asign()/verify(). La mecánica de dos pasos queda dentro, pero entenderla te hará falta en el apartado 8.
Firmas con RSA: PSS, y por qué no PKCS#1 v1.5
Si la clave es RSA (como la del hospital público que exige RSA en la integración de MediNube), la firma necesita un esquema de padding — la historia rima con la de 04-01. Hay dos:
- PKCS#1 v1.5 (firma): el heredado, determinista, omnipresente en sistemas antiguos. A diferencia de su primo de cifrado, no está roto — pero su seguridad tiene demostraciones más débiles y las implementaciones descuidadas de su verificación han producido ataques históricos (falsificaciones de Bleichenbacher de 2006 contra verificadores laxos).
- RSA-PSS (Probabilistic Signature Scheme): el moderno. Incorpora sal aleatoria (cada firma del mismo documento es distinta) y tiene pruebas de seguridad sólidas. Es a la firma RSA lo que OAEP al cifrado RSA: la forma correcta en código nuevo.
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding, rsa
priv_rsa = rsa.generate_private_key(public_exponent=65537, key_size=3072)
pub_rsa = priv_rsa.public_key()
mensaje = b"Informe mensual de actividad - MediNube"
pss = padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.DIGEST_LENGTH, # sal = tamaño del hash: el valor recomendado
)
firma = priv_rsa.sign(mensaje, pss, hashes.SHA256())
print(len(firma)) # 384 bytes: siempre el tamaño del módulo, como en OAEP
# Verificación: no devuelve True/False, LANZA EXCEPCIÓN si falla.
from cryptography.exceptions import InvalidSignature
try:
pub_rsa.verify(firma, mensaje, pss, hashes.SHA256())
print("Firma válida")
except InvalidSignature:
print("FIRMA INVÁLIDA: documento alterado o clave incorrecta")Dos detalles de API que debes grabarte:
verifyno devuelve un booleano: si la firma es válida no devuelve nada, y si no lo es lanzaInvalidSignature. Es un diseño deliberado — imposible ignorar el resultado por accidente. Tu código debe capturar la excepción y tratar el fallo como lo que es: un rechazo.- La firma RSA-PSS es aleatoria: firma dos veces el mismo mensaje y obtendrás dos firmas distintas, ambas válidas. Nunca compares firmas entre sí para "verificar" — verifica siempre con
verify.
Firmas con Ed25519: la API mínima
Para el sistema de recetas, MediNube decidió en 04-02 usar Ed25519. Su API es la más pequeña posible — sin padding que configurar, sin hash que elegir (usa internamente SHA-512), sin sal que ajustar:
from cryptography.hazmat.primitives.asymmetric import ed25519
from cryptography.exceptions import InvalidSignature
priv = ed25519.Ed25519PrivateKey.generate()
pub = priv.public_key()
mensaje = b"Receta de prueba"
firma = priv.sign(mensaje) # y ya está
print(len(firma)) # 64 bytes, SIEMPRE
try:
pub.verify(firma, mensaje) # y ya está
print("Firma válida")
except InvalidSignature:
print("FIRMA INVÁLIDA")Compara: 64 bytes de firma frente a 384 de RSA-3072, y cero decisiones de configuración. Además, Ed25519 es determinista: el mismo mensaje con la misma clave produce siempre la misma firma. Esto no es un defecto (a diferencia del RSA de libro de texto, aquí hay un diseño que lo hace seguro) sino una protección: los esquemas de curva elíptica clásicos (ECDSA, el que usarías con P-256) exigen un número aleatorio fresco por firma, y si ese número se repite o es predecible, la clave privada se puede recuperar de las propias firmas — así se extrajo la clave de firmado de la PlayStation 3 en 2010. Ed25519 elimina esa decisión peligrosa por construcción: la filosofía Bernstein de 04-02, aplicada. Si algún día un tercero te exige ECDSA con P-256, pyca/cryptography lo hace bien (priv.sign(mensaje, ec.ECDSA(hashes.SHA256()))); pero cuando elijas tú, Ed25519.
El caso central: recetas electrónicas firmadas
Ahora, el sistema de verdad. Requisitos del equipo médico de MediNube:
- La Dra. Lucía Ferrer (Clínica Sol, colegiada ficticia
CS-4471) emite recetas para sus pacientes desdeportal.medinube.example. - Una farmacia externa — llamémosla Farmacia Robles — debe poder verificar que la receta la emitió la doctora y que nadie la ha alterado (ni la dosis, ni el medicamento, ni el paciente).
- Si hay una disputa ("yo nunca receté eso"), la firma debe poder presentarse ante un tercero: no repudio.
Una receta es un documento estructurado. La representamos como JSON:
receta = {
"formato": "receta-v1",
"medico": "dra.lucia.ferrer",
"colegiada": "CS-4471",
"paciente": "ana.perez",
"medicamento": "Amoxicilina 500 mg",
"posologia": "1 comprimido cada 8 horas, 7 dias",
"fecha_emision": "2026-07-08",
}Y aquí aparece un problema que no es criptográfico sino de representación: la firma se calcula sobre bytes, y un mismo diccionario puede convertirse en bytes de muchas maneras distintas.
El enemigo silencioso: la canonicalización
El primer intento del código heredado de MediNube era este:
import json
def firmar_receta_MAL(receta: dict, priv) -> bytes:
# MAL: json.dumps sin fijar orden ni separadores.
return priv.sign(json.dumps(receta).encode("utf-8"))¿Qué falla? Que json.dumps no produce siempre los mismos bytes para el mismo contenido:
- El orden de las claves depende del orden de inserción del diccionario. La farmacia, al reconstruir la receta desde su base de datos, puede obtener las claves en otro orden → JSON distinto → hash distinto →
InvalidSignaturesobre una receta perfectamente legítima. - Los separadores por defecto (
", "y": ", con espacios) pueden diferir entre librerías y lenguajes (¿y si la farmacia verifica en Java o en JavaScript?). - Los caracteres no ASCII pueden serializarse escapados (
"Pérez") o literales ("Pérez"), según la opciónensure_ascii.
El resultado en producción: verificaciones que fallan aleatoriamente, y la tentación fatal de "arreglarlo" relajando la verificación. La solución correcta es la canonicalización: definir una única serialización válida y usarla siempre, al firmar y al verificar. Con esto ya podemos escribir el módulo real, medinube/recetas.py:
# medinube/recetas.py
import json
from cryptography.hazmat.primitives.asymmetric import ed25519
from cryptography.exceptions import InvalidSignature
def canonicalizar_receta(receta: dict) -> bytes:
"""Serialización canónica: mismos datos -> mismos bytes, SIEMPRE.
- sort_keys=True: orden alfabético de claves, no de inserción.
- separators sin espacios: sin ambigüedad de formato.
- ensure_ascii=False + UTF-8: una única representación de acentos.
"""
return json.dumps(
receta, sort_keys=True, separators=(",", ":"), ensure_ascii=False,
).encode("utf-8")
def firmar_receta(receta: dict, priv_medico: ed25519.Ed25519PrivateKey) -> str:
"""Firma la receta y devuelve la firma en hex, estilo 'v1=<hex>' (como 03-02)."""
firma = priv_medico.sign(canonicalizar_receta(receta))
return "v1=" + firma.hex()
def verificar_receta(receta: dict, firma: str,
pub_medico: ed25519.Ed25519PublicKey) -> bool:
"""True si la firma corresponde a la receta y a la clave pública dadas."""
if not firma.startswith("v1="):
return False # versión desconocida: rechazar
try:
pub_medico.verify(bytes.fromhex(firma[3:]), canonicalizar_receta(receta))
return True
except (InvalidSignature, ValueError):
return False # ValueError: hex malformadoY el flujo completo, de la consulta a la farmacia:
# En la consulta: la Dra. Ferrer firma (su privada se cargó del PEM # cifrado de 04-02, con su passphrase — jamás vive en el código). priv_dra = ed25519.Ed25519PrivateKey.generate() # en real: load_pem_private_key(...) pub_dra = priv_dra.public_key() firma = firmar_receta(receta, priv_dra) print(firma[:20], "...") # v1=8f3a... (64 bytes en hex) # En la Farmacia Robles: verificación con la PÚBLICA de la doctora. print(verificar_receta(receta, firma, pub_dra)) # True # Un atacante (o un error) cambia la dosis: receta_alterada = dict(receta, medicamento="Amoxicilina 1000 mg") print(verificar_receta(receta_alterada, firma, pub_dra)) # False # La farmacia reconstruye la receta con las claves en OTRO orden: receta_reordenada = dict(reversed(list(receta.items()))) print(verificar_receta(receta_reordenada, firma, pub_dra)) # True ← canonicalización
La última línea es la victoria sobre firmar_receta_MAL: el orden de las claves ya no importa, porque ambos lados canonicalizan antes de tocar la criptografía. Este problema es tan universal que existen estándares dedicados (JCS — JSON Canonicalization Scheme, RFC 8785); nuestra receta de sort_keys + separators + UTF-8 es la versión mínima del mismo principio, suficiente si controlas ambos extremos.
Firmar documentos: huella + firma
¿Y cuando lo que hay que firmar no es un JSON de 300 bytes sino el export RGPD de 200 MB de 03-01? Pasarlo entero a sign() obliga a tenerlo en memoria y a que el verificador lo procese completo. El patrón práctico reutiliza lo que ya tienes en medinube/integridad.py: la función huella_export que calcula el SHA-256 del fichero por bloques. Se firma la huella:
# medinube/integridad.py ya nos da la huella (03-01):
# huella = huella_export("export_ana_perez.zip") # hex de SHA-256
def firmar_export(ruta: str, priv) -> str:
"""Firma la huella SHA-256 del export: documento = huella + firma."""
huella = huella_export(ruta) # hash por bloques, de 03-01
return "v1=" + priv.sign(huella.encode("ascii")).hex()
def verificar_export_firmado(ruta: str, firma: str, pub) -> bool:
huella = huella_export(ruta) # recalcular SIEMPRE
try:
pub.verify(bytes.fromhex(firma[3:]), huella.encode("ascii"))
return True
except (InvalidSignature, ValueError):
return FalseConceptualmente no hay nada nuevo — la firma siempre firmaba un hash por dentro; aquí simplemente hacemos explícito el primer paso para poder hashear por bloques. Lo que gana MediNube: el export RGPD que antes solo tenía huella (integridad) ahora tiene huella firmada (integridad + autenticidad + no repudio): Ana Pérez puede demostrar ante un tercero que ese export se lo emitió MediNube y no lo fabricó ella.
La advertencia legal: esto no es (todavía) una firma cualificada
Parada obligatoria antes de que nadie despliegue esto en una farmacia real. Lo que hemos construido es una firma digital criptográfica, técnicamente sólida. Pero "firma electrónica" con plenos efectos legales es un concepto jurídico regulado en la UE por el reglamento eIDAS, que distingue niveles (simple, avanzada, cualificada) con requisitos que van mucho más allá de las matemáticas: certificados cualificados emitidos por prestadores acreditados, dispositivos seguros de creación de firma, sellado de tiempo... Y una receta médica real añade su propia normativa sanitaria y de protección de datos (RGPD, datos de categoría especial).
Nuestro sistema de recetas es un ejemplo técnico con datos ficticios. Un despliegue real exigiría revisión de seguridad y de compliance por especialistas (eIDAS, RGPD, normativa de receta electrónica) — como todos los sistemas de este curso que tocan datos sanitarios o firmas con valor legal. La criptografía que estás aprendiendo es la base común de todos esos niveles legales; el envoltorio jurídico no es materia de este curso.
La farmacia y la pregunta de siempre
Queda el elefante en la habitación, y ya lo has visto venir. Todo el flujo de la Farmacia Robles empieza con "la farmacia tiene pub_dra, la clave pública de la Dra. Ferrer". ¿Y cómo la obtuvo? ¿Cómo sabe que es de ella?
Si un atacante consigue que la farmacia acepte su clave pública como "la de la Dra. Ferrer", puede firmar recetas falsas que verificarán perfectamente. La matemática de la firma es impecable; el eslabón roto es, otra vez, la identidad de la clave pública — la misma grieta de 04-01, ahora con recetas de medicamentos en juego. La solución real (que alguien de confianza certifique que esa clave pertenece a esa doctora, con número de colegiada incluido) es exactamente el módulo 5. Hasta entonces, la dejamos explícitamente abierta — y en la próxima lección la verás convertida en un ataque activo con nombre propio.
Errores Comunes y Consejos
- Tratar
verifycomo si devolviera un booleano. No devuelve nada o lanzaInvalidSignature. El error típico es llamar averifysintry/except"para probar" y dejar que la excepción tumbe el proceso, o al revés: capturarExceptiona secas y tragarse errores que no son de firma. CapturaInvalidSignatureespecíficamente y trátala como rechazo. - Comparar firmas entre sí. "Firmo lo recibido y comparo con la firma adjunta" funciona de casualidad con esquemas deterministas y falla siempre con RSA-PSS (aleatorio). La operación de verificación existe por algo: úsala.
- Firmar JSON sin canonicalizar. El bug de
firmar_receta_MAL: verificaciones que fallan según el orden de claves, el idioma o la librería del otro extremo. Canonicaliza en ambos lados, siempre. - PKCS#1 v1.5 para firmar en código nuevo. No está roto como su primo de cifrado, pero PSS es superior en todo. Reserva v1.5 para interoperar con sistemas que lo exijan, documentándolo.
- MD5 o SHA-1 como hash de firma. Aquí las colisiones matan directamente: se han fabricado pares de documentos distintos con la misma firma SHA-1 válida. SHA-256 mínimo (03-01).
- Creer que la firma cifra. La receta viaja en claro junto a su firma; cualquiera puede leerla. Si además hace falta confidencialidad (una receta es dato sanitario), hay que cifrar además de firmar — el combo llega en 04-05.
- Confundir "cifrar con la privada" con firmar. Es una descripción folclórica de cómo funcionaba RSA por dentro, y es dañina: no describe PSS, no describe Ed25519, y lleva a APIs mal usadas. Firmar es firmar: una operación propia, con su padding y su semántica.
- Descuidar la privada del firmante. El no repudio vale lo que valga la custodia de la clave: si la privada de la doctora anda en un PEM sin cifrar en un repositorio, cualquiera "es" la doctora. PEM cifrado (04-01/04-02) hoy; gestor de secretos y HSM en el módulo 6.
Ejercicios
-
Detector de alteraciones. Genera un par Ed25519 y firma el mensaje
b"Derivacion de ana.perez al Centro Medico Luna". (a) Verifica la firma correcta. (b) Altera un solo byte del mensaje y comprueba que saltaInvalidSignature. (c) Altera un byte de la firma y comprueba que también. (d) Verifica el mensaje original con la clave pública de otro par recién generado: ¿qué ocurre y qué significa? -
Cazando el bug de canonicalización. Usando
firmar_receta_MAL(firmajson.dumps(receta).encode()sin opciones): construyereceta_bcon los mismos pares clave-valor querecetapero insertados en orden inverso, firmarecetay trata de verificar la firma contrajson.dumps(receta_b).encode(). Comprueba que falla. Repite concanonicalizar_recetay comprueba que ambas serializaciones producen bytes idénticos (==). Explica por qué este bug es especialmente traicionero en producción. -
La disputa. La Farmacia Robles presenta una receta de 900 mg de un medicamento con firma
v1=...que verifica con la clave pública de la Dra. Ferrer. La doctora afirma que ella recetó 500 mg. Razona (sin código): (a) ¿puede la doctora sostener que la farmacia alteró la dosis después de la firma? (b) ¿Qué dos explicaciones quedan en pie? (c) ¿Qué habría pasado en el mismo escenario si el sistema usara HMAC con clave compartida entre MediNube y la farmacia?
Soluciones
from cryptography.hazmat.primitives.asymmetric import ed25519
from cryptography.exceptions import InvalidSignature
priv = ed25519.Ed25519PrivateKey.generate()
pub = priv.public_key()
msg = b"Derivacion de ana.perez al Centro Medico Luna"
firma = priv.sign(msg)
pub.verify(firma, msg) # (a) no lanza nada: válida
alterado = bytes([msg[0] ^ 1]) + msg[1:] # (b) un bit del primer byte
try:
pub.verify(firma, alterado)
except InvalidSignature:
print("(b) alterar el mensaje invalida la firma")
firma_rota = bytes([firma[0] ^ 1]) + firma[1:]
try:
pub.verify(firma_rota, msg) # (c)
except InvalidSignature:
print("(c) alterar la firma tambien la invalida")
otra_pub = ed25519.Ed25519PrivateKey.generate().public_key()
try:
otra_pub.verify(firma, msg) # (d)
except InvalidSignature:
print("(d) clave distinta: invalida")(d) Lanza InvalidSignature: una firma solo verifica con la pública del par que firmó. Es la propiedad que da autenticidad — y también la que explota el ataque del apartado 10: si te cuelan una clave pública falsa "de la doctora", las firmas del atacante verificarán con ella. Verificar une la firma a una clave, no a una persona; unir clave y persona es el módulo 5.
import json
receta = {"medico": "dra.lucia.ferrer", "paciente": "ana.perez",
"medicamento": "Amoxicilina 500 mg"}
receta_b = dict(reversed(list(receta.items()))) # mismo contenido, otro orden
print(json.dumps(receta) == json.dumps(receta_b)) # False
print(canonicalizar_receta(receta) == canonicalizar_receta(receta_b)) # True
firma = firmar_receta_MAL(receta, priv) # firma bytes en orden A
try:
pub.verify(firma, json.dumps(receta_b).encode()) # verifica bytes en orden B
except InvalidSignature:
print("Receta legitima RECHAZADA por el orden de las claves")Es traicionero porque no falla en las pruebas: mientras firma y verificación pasen por el mismo diccionario en el mismo proceso, el orden coincide y todo verifica. Falla semanas después, en el sistema de la farmacia, con otro lenguaje u otra base de datos que devuelve las claves en otro orden — un fallo intermitente que parece un bug de la criptografía y acaba tentando a alguien a "relajar" la verificación. La canonicalización elimina la clase entera de fallos.
- (a) No: si la farmacia hubiera cambiado la dosis después de firmar, la verificación fallaría — la firma cubre cada byte de la receta canonicalizada. Que verifique demuestra que esos 900 mg son exactamente lo que se firmó con esa clave. (b) Quedan dos: o la doctora realmente firmó 900 mg (y ahora lo niega — justo lo que el no repudio permite descartar que prospere), o su clave privada está comprometida (alguien más firmó con ella). Por eso la custodia de la privada es inseparable del valor probatorio, y por eso existen la revocación y los HSM (módulos 5 y 6). (c) Con HMAC compartido no habría nada que discutir: la farmacia puede generar MACs válidos de cualquier receta, así que el MAC no prueba quién la creó. Es la diferencia exacta entre autenticidad y no repudio — la primera fila que le faltaba a la tabla de 03-02, ya cerrada.
Conclusión
El cuarto objetivo está conseguido. La firma digital invierte el flujo del cifrado asimétrico — se firma con la privada, cualquiera verifica con la pública — y con eso da lo que ningún MAC podía: no repudio, porque solo el dueño de la privada pudo generar la firma. Sabes cómo funciona por dentro (se firma el hash del mensaje, heredando todo lo aprendido de hashes), qué esquemas usar (RSA-PSS cuando te impongan RSA, Ed25519 cuando elijas tú: 64 bytes, API mínima, determinista y sin decisiones peligrosas), y que verify habla con excepciones (InvalidSignature), no con booleanos. En MediNube quedó funcionando medinube/recetas.py — canonicalización JSON (sort_keys, separadores fijos, UTF-8), firma v1=<hex> — con el que la Dra. Ferrer firma recetas que la Farmacia Robles verifica, y los exports RGPD ganaron firma sobre su huella. Recuerda las dos sombras: esto es un ejemplo técnico, no una firma cualificada eIDAS; y todo el edificio descansa en que la clave pública de la doctora sea de la doctora — pregunta que sigue abierta hasta el módulo 5. Con firmas y curvas en la mochila, toca por fin saldar la deuda original del curso, la que arrastramos desde 02-01: cómo acuerdan una clave secreta dos partes que nunca se han visto, hablando por un canal donde todos escuchan. Es el intercambio de claves de Diffie-Hellman, y te espera en 04-04. Nos vemos allí.
Curso de Criptografía Aplicada
Módulo 1: Fundamentos de la Criptografía
- Qué es la criptografía y para qué sirve
- Codificación, ofuscación y cifrado
- Aleatoriedad y entropía
- El principio de Kerckhoffs y las reglas de oro
Módulo 2: Criptografía Simétrica
- Cifrado simétrico: AES y ChaCha20
- Modos de operación
- Cifrado autenticado (AEAD)
- Derivación de claves (KDF)
Módulo 3: Hashes, MAC y Contraseñas
- Funciones hash criptográficas
- Autenticación de mensajes con HMAC
- Almacenamiento seguro de contraseñas
Módulo 4: Criptografía Asimétrica
- Fundamentos de clave pública y RSA
- Criptografía de curva elíptica
- Firmas digitales
- Intercambio de claves: Diffie-Hellman
- Cifrado híbrido
Módulo 5: PKI, Certificados y TLS
- Certificados X.509 y autoridades de certificación
- TLS en la práctica
- Gestión del ciclo de vida de certificados
