Aquesta lliçó és diferent de totes les anteriors: no aprendràs cap primitiva nova, perquè ja saps tot el necessari. El que entrenaràs és la mirada de revisor: la capacitat d'obrir un fitxer aliè (o teu de fa dos anys) i que els errors criptogràfics et saltin a la vista abans d'arribar a producció. Recorrerem una galeria d'errors organitzada per famílies —cadascun amb el seu símptoma al codi, per què és explotable, el seu arranjament i la lliçó on el vas aprendre—, reunirem per fi tots els _MALAMENT del curs en una taula-índex (com vam prometre a 05-03), construirem la llista de verificació del revisor de MediNube i acabaràs fent d'auditor amb tres fragments de codi heretat que combinen diversos pecats alhora. Els exercicis d'avui són l'auditoria.
Contingut
- Com es llegeix codi amb ulls de revisor
- Família 1: inventar criptografia
- Família 2: aleatorietat trencada
- Família 3: xifratge simètric mal usat
- Família 4: hashes i contrasenyes
- Família 5: asimètrica mal aplicada
- Família 6: TLS i certificats
- Família 7: secrets i tokens
- La taula-índex: tots els
_MALAMENTdel curs - La llista de verificació del revisor de MediNube
- Vocabulari compartit: OWASP i CWE
Com es llegeix codi amb ulls de revisor
Tres hàbits abans de la galeria. Primer: els errors criptogràfics gairebé mai no donen símptomes — el codi trencat xifra, desxifra i passa els tests; només falla contra un adversari, i l'adversari no és a CI. Segon: els errors vénen en raïms — on hi ha un md5, sol haver-hi a prop un random i un ==; trobar-ne un és el senyal per aturar-se i llegir tot el mòdul. Tercer: revisa contra les 9 regles d'or del mòdul 1 — gairebé cada error d'aquesta galeria és una regla violada, i citar-les dona a la teva revisió un criteri que no sona a opinió personal.
Família 1: inventar criptografia
Regla violada: l'1 (no inventis la teva pròpia criptografia) i la 2 (l'enemic coneix el sistema).
- Símptoma al codi: bucles que fan XOR de les dades amb una clau que es repeteix; "xifratges" a base de sumar desplaçaments; funcions anomenades
ofuscar/codificarusades com si protegissin; comentaris del tipus "ningú sabrà que això és Base64 al revés".
def xifrar_casola(dades: bytes, clau: bytes) -> bytes:
return bytes(d ^ clau[i % len(clau)] for i, d in enumerate(dades)) # XOR repetit- Per què és explotable: el XOR amb clau repetida és el xifratge de Vigenère amb una altra roba: l'anàlisi de freqüències i el
crib draggingel trenquen amb paper i llapis. L'ofuscació cau amb unstringso un descompilador — Kerckhoffs (M1): assumeix que l'atacant té el codi. - Arranjament: AEAD de pyca/cryptography (AES-256-GCM o ChaCha20-Poly1305, M2). Sense excepcions ni "és només per a dades internes".
- On es va aprendre: mòdul 1 complet; la temptació reapareixia a 02-01.
Família 2: aleatorietat trencada
Regla violada: la 3 (tota aleatorietat del CSPRNG) i la 5 (l'entropia mana).
- Símptoma:
import randoma prop de la paraula "token", "clau", "nonce" o "reset";random.seed(...)amb el temps o amb dades de l'usuari; UUID (uuid1/uuid4) usats com a secrets; "aleatori" derivat detime.time()o del PID. - Per què és explotable: el Mersenne Twister de
randomés predictible — amb 624 sortides es reconstrueix l'estat complet, i amb llavor temporal ni això cal: l'atacant prova les llavors de l'últim minut. Ho vam veure ambtoken_recuperacio_MALAMENT(01-03): MalloryClinic podia regenerar el token de recuperació d'ana.perezsabent l'hora aproximada de la petició. Els UUID no prometen impredictibilitat (uuid1 porta MAC i rellotge; uuid4 depèn de la implementació): són identificadors, no secrets. - Arranjament:
secrets.token_urlsafe(32)/secrets.token_bytes(32)/os.urandomper a tot el que hagi de ser secret o impredictible; 128 bits mínim, 256 estàndard. - On es va aprendre: 01-03.
Família 3: xifratge simètric mal usat
Regla violada: la 7 (identifica l'objectiu abans que l'eina) — gairebé sempre per triar el mode sense pensar en la integritat.
| Símptoma al codi | Per què és explotable | Arranjament | Lliçó |
|---|---|---|---|
modes.ECB(...) |
Blocs iguals → xifrat igual: el pingüí de 02-02; estructures i repeticions visibles | AEAD (GCM/ChaCha20-Poly1305) | 02-02 |
Nonce/IV fix (b"\x00"*12), comptador reiniciat, o nonce derivat de la mateixa dada |
En GCM/CTR, reutilitzar nonce amb la mateixa clau trenca la confidencialitat de tots dos missatges i permet falsificar tags | Nonce aleatori de 12 bytes per operació, desat junt al xifrat (format v1 de 02-03) |
02-02/02-03 |
| CBC (o CTR) "a pèl", sense MAC — xifrar sense autenticar | Maleabilitat: l'atacant capgira bits amb efecte controlat; els padding oracle de CBC desxifren sense la clau observant errors del servidor | AEAD sempre; si alguna cosa no és AEAD, HMAC en encrypt-then-MAC — però el 2026, AEAD | 02-02/02-03 |
Clau derivada d'una contrasenya amb un hash simple (sha256(password)) |
Força bruta fora de línia a velocitat de GPU | KDF amb cost: scrypt/Argon2 (02-04); per derivar subclaus d'una clau forta, HKDF | 02-04 |
| AAD buit quan hi ha context a fixar | El xifrat d'un pacient es pot "transplantar" a un altre (confusió de context) | AAD amb el context: pacient=...;format=v1 |
02-03 |
El patró mental d'aquesta família: confidencialitat sense integritat és un parany. Si veus xifrat sense tag d'autenticació, ja tens la troballa.
Família 4: hashes i contrasenyes
Regla violada: la 6 (compara secrets en temps constant) i la 7.
- Símptomes i els seus perquès:
hashlib.md5/sha1per a qualsevol cosa relacionada amb seguretat: col·lisions pràctiques des de fa anys (03-01); en signatures i certificats, falsificables.- Hash ràpid per a contrasenyes (
sha256(password), amb o sense sal): una GPU en prova milers de milions per segon;desar_contrasenya_MALAMENT(03-03) queia en hores davant un diccionari. - Sense sal (o sal global): la mateixa contrasenya produeix el mateix hash → rainbow tables i atac a tots els usuaris alhora.
- Comparació no constant (
==,startswith) de hashes, tags o claus d'API: el temps de resposta filtra quants bytes coincideixen —autenticar_MALAMENT(01-04) iiniciar_sessio_MALAMENT(03-03). sha256(clau + missatge)com a MAC: length extension per la construcció Merkle–Damgård —tag_MALAMENT(03-02): l'atacant estén el missatge i calcula el tag vàlid sense conèixer la clau.
- Arranjaments: SHA-256/SHA-3/BLAKE2 per a integritat; Argon2id amb
PasswordHasher(i pepper HMAC del gestor, 06-01) per a contrasenyes;secrets.compare_digest(o elverifyde la llibreria) per comparar; HMAC per autenticar missatges. - On es va aprendre: mòdul 3 complet.
Família 5: asimètrica mal aplicada
Regla violada: l'1 i la 7.
- RSA sense farciment (de llibre de text) o amb PKCS#1 v1.5 on no toca: l'RSA pur és determinista —
endevinar_resultat_MALAMENT(04-01) mostrava com l'atacant xifra els candidats ("POSITIU"/"NEGATIU") amb la pública i compara; v1.5 en xifratge arrossega els oracles de Bleichenbacher. Arranjament: OAEP per xifrar, PSS per signar (04-01/04-03). - Claus curtes o corbes antiquades: RSA-1024, P-192 (
generar_parell_metge_MALAMENT, 04-02). Arranjament: RSA-3072 mínim, o millor Ed25519/X25519. - DH/ECDH sense autenticar: intercanvi X25519 sense verificar amb qui intercanvies = invitació al MITM de MalloryClinic (04-04). Arranjament: autenticar els extrems (signatures o certificats, com fa TLS).
- Signar sense canonicalitzar:
signar_recepta_MALAMENT(04-03) signavajson.dumps(recepta)sense fixar ordre ni separadors: el mateix contingut produeix bytes diferents segons el diccionari, i la verificació es converteix en una loteria (o alguna cosa pitjor: dues representacions "vàlides" del mateix document). Arranjament: canonicalitzar abans de signar (sort_keys=True, separadors fixos — el formatreceta-v1). - Xifrar dades grans "directament amb RSA": a més d'impossible per mida, és el símptoma de no conèixer el sobre híbrid. Arranjament: KEM/sobre — xifra una clau simètrica, no la dada (04-05).
Família 6: TLS i certificats
Regla violada: la 9 (la criptografia no substitueix la resta de la seguretat) — de res serveix TLS si li treus la part que autentica.
verify=False(oCURLOPT_SSL_VERIFYPEER = 0, o unTrustManagerbuit en Java): el clàssic dels clàssics, el nostrecridar_api_MALAMENT/estat_recepta_MALAMENT(05-02). El canal queda xifrat amb qui sigui — MITM trivial. Sol néixer com a "fix" temporal d'unSSLErrori fossilitzar-se. Arranjament: arreglar la causa (CA interna averify="/etc/medinube/pki/ca.crt", cadena completa al servidor), mai apagar la verificació.- Ignorar caducitats i revocació: certificats que caduquen divendres a la nit, CRL/OCSP sense consultar. Arranjament: renovació automàtica (ACME) + monitorització (
medinube/monitor_certs.py, LLINDAR_DIES=30) — 05-03. - Versions i suites antigues: TLS 1.0/1.1, RC4, 3DES, suites sense PFS. Arranjament: TLS 1.3, mínim 1.2 només amb ECDHE+AEAD (05-02).
- Pinning trencat: fixar el certificat fulla "per a més seguretat" i bloquejar els teus propis usuaris a la primera rotació (pitjor amb vides curtes d'ACME). Si es fa pinning, a la clau de la CA pròpia i amb pla de rotació; per a gairebé tothom, CT + verificació estàndard basten.
Família 7: secrets i tokens
Regla violada: la 2 i la 8.
- Hardcodejar secrets: el
CLAUS_APIde 01-04 i elprivkey.pemcommitejat de 05-03. Arranjament: gestor de secrets, gitleaks a CI, rotació davant qualsevol filtració (06-01). - Loguejar secrets:
logger.info(f"token={token}"), contrasenyes al log de peticions, el token JWT complet als logs d'accés. Els logs viuen anys, es repliquen i molta gent els llegeix. Arranjament: redactar al logger; registrarjti/sub, mai el token. - JWT sense validar
alg/exp/aud/iss: elverificar_MALAMENTde 06-03;alg: nonei la confusió RS256→HS256 entren per aquí. Arranjament:algorithms=[...]tancat,audience,issuer,require(06-03). - Tokens de vida eterna o refresh tokens desats en clar a la BD. Arranjament: TTL curt + refresh rotatori desat com a
sha256(token)(06-03, patró de 03-03).
La taula-índex: tots els _MALAMENT del curs
El que vam prometre a 05-03: la galeria completa del codi heretat de MediNube, reunida. Fes-la servir com a índex de repàs — si algun "pecat" no et resulta obvi a l'instant, aquesta lliçó mereix una relectura:
| Funció | Lliçó | Pecat | Arranjament |
|---|---|---|---|
crear_sessio_MALAMENT / llegir_sessio_MALAMENT |
01-02 (saldada a 06-03) | Base64 com si fos protecció: token editable | JWT EdDSA amb claims i verificació estricta |
token_recuperacio_MALAMENT |
01-03 | random amb llavor temporal per a un token de reset |
secrets.token_urlsafe(32) + desar sha256(token) |
autenticar_MALAMENT |
01-04 | Comparació == de claus d'API (timing) + CLAUS_API hardcodejat |
secrets.compare_digest + gestor de secrets (06-01) |
enviar_webhook_MALAMENT |
03-02 | "Integritat" amb sha256(cos) sense clau: qualsevol la recalcula |
HMAC-SHA256 amb clau per clínica |
tag_MALAMENT |
03-02 | sha256(clau + missatge) com a MAC: length extension |
HMAC |
verificar_MALAMENT (webhooks) |
03-02 | L'anterior + comparació == + sense timestamp anti-replay |
HMAC + compare_digest + finestra de 300 s |
desar_contrasenya_MALAMENT / iniciar_sessio_MALAMENT |
03-03 | Hash ràpid per a contrasenyes, comparació no constant | Argon2id (PasswordHasher) + pepper (06-01) |
endevinar_resultat_MALAMENT |
04-01 | RSA sense farciment: xifrat determinista, s'endevina per diccionari | RSA-OAEP |
generar_parell_metge_MALAMENT |
04-02 | Corba P-192: seguretat efectiva per sota del mínim | Ed25519 (o P-256 si l'ecosistema ho exigeix) |
signar_recepta_MALAMENT |
04-03 | Signar JSON sense canonicalitzar | Canonicalització receta-v1 + Ed25519 |
cridar_api_MALAMENT / estat_recepta_MALAMENT |
05-02 | verify=False: TLS sense autenticació = MITM servit |
Verificació amb la CA correcta; arreglar la causa arrel |
verificar_MALAMENT (JWT) |
06-03 | decode sense algorithms tancat ni aud/iss/exp |
Llista tancada + audience + issuer + require |
La llista de verificació del revisor de MediNube
La llista que MediNube adjunta a tota revisió de codi que toqui seguretat. Agrupada per poder-la repartir; cada punt és una pregunta amb resposta sí/no sobre el diff:
Aleatorietat i secrets
- Tot el secret/impredictible surt de
secrets/os.urandom(mairandom,uuid, temps)? - Els secrets nous vénen del gestor (res hardcodejat, res en
.envcommitejat)? - Cap secret pot acabar en logs, missatges d'error o URL?
- Els tokens d'un sol ús es desen amb hash i caduquen?
Xifratge
- Tot xifratge simètric és AEAD (GCM/ChaCha20-Poly1305) — res d'ECB/CBC/CTR a pèl?
- El nonce és aleatori, de 12 bytes, únic per operació i viatja junt al xifrat?
- L'AAD fixa el context (id de pacient, versió de format)?
- El format porta byte de versió (criptoagilitat) i hi ha pla de rotació de clau?
- Si hi ha contrasenyes com a origen de claus: KDF amb cost (scrypt/Argon2), no un hash?
Hashes, MAC i contrasenyes
- Res de MD5/SHA-1 en contextos de seguretat?
- Contrasenyes només amb Argon2id (i pepper del gestor)?
check_needs_rehashal login? - Autenticació de missatges amb HMAC (mai
sha256(clau+msg)) i comparació ambcompare_digest? - Els webhooks porten timestamp i es rebutja fora de la finestra anti-replay?
Asimètrica i signatures
- RSA només amb OAEP (xifratge) / PSS (signatures), mides ≥ 3072; o directament Ed25519/X25519?
- Tot el que se signa es canonicalitza abans? Els intercanvis de claus estan autenticats?
TLS i certificats
- Zero
verify=False(ni equivalents) a tot el diff, tests inclosos? - Connexions internes (BD inclosa) amb verificació completa (
sslmode=verify-full+ CA interna)? - Els certificats nous entren a la renovació automàtica i al monitor de caducitats?
Tokens i sessions
- Tot
jwt.decodefixaalgorithms(llista tancada),audience,issuerirequire?expcurt? - El token de sessió va en cookie
HttpOnly; Secure; SameSite(nolocalStorage)?
Vocabulari compartit: OWASP i CWE
Quan un auditor extern revisi MediNube, no dirà "família 3": dirà OWASP Top 10 A02 – Cryptographic Failures (la categoria que agrupa gairebé tota aquesta lliçó) i citarà CWE concrets: CWE-327 (algorisme trencat o arriscat), CWE-328 (hash feble), CWE-330 (aleatorietat insuficient), CWE-259/798 (credencials hardcodejades), CWE-295 (validació de certificat incorrecta), CWE-347 (verificació de signatura incorrecta). Et convé conèixer aquests codis com a vocabulari compartit — mapejar les teves troballes a A02/CWE fa que els teus informes siguin llegibles per a seguretat i compliment. No ho desenvolupem més aquí: el portal té un curs específic d'OWASP on aquest marc es treballa a fons.
Errors Comuns i Consells
- Error (del revisor): buscar només el bug puntual. Els errors criptogràfics van en raïms; quan en trobis un, revisa el mòdul sencer i l'historial de git d'aquest fitxer.
- Error: acceptar "és temporal" o "és només intern". El
verify=Falsedecridar_api_MALAMENTva néixer com a fix temporal el 2023. Els apanys criptogràfics fossilitzen: o entren bé, o no entren. - Error: assenyalar sense arranjament. Una troballa sense proposta ("fes servir HMAC amb la clau del gestor, veure 03-02") tendeix a tancar-se com a "won't fix". La taula-índex et dona l'arranjament canònic de cada pecat.
- Consell: automatitza el que sigui automatitzable.
banditi els linters detectenmd5,randomen context de seguretat iverify=False; gitleaks caça secrets. La llista de verificació humana és per al que les eines no veuen (AAD absent, canonicalització, disseny). - Consell: revisa també els tests. És on s'amaguen els
verify=False, les claus de "prova" que acaben en producció i els mocks que desactiven la verificació de signatures.
Exercicis
Els tres fragments següents són "codi heretat" real de MediNube, rescatat de branques antigues. Cadascun combina diversos errors de famílies diferents. Audita'ls: llista cada error, per què és explotable i el seu arranjament (cita la lliçó o el punt de la llista de verificació). Compta abans de mirar les solucions: n'hi ha entre 4 i 6 per fragment.
- Exportació d'historials per a una clínica:
import random, hashlib
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
def exportar_historials(historials: bytes, contrasenya: str) -> bytes:
clau = hashlib.md5(contrasenya.encode()).digest() # 16 bytes, "suficient"
iv = bytes([random.randint(0, 255) for _ in range(16)])
xifrador = Cipher(algorithms.AES(clau), modes.CBC(iv)).encryptor()
farciment = 16 - len(historials) % 16
ct = xifrador.update(historials + bytes([farciment]) * farciment) + xifrador.finalize()
return iv + ct- Receptor de webhooks de resultats de laboratori:
import hashlib, logging
CLAU_WEBHOOK = "clau-compartida-2023" # la mateixa per a totes les cliniques
def rebre_webhook(cos: bytes, capcaleres: dict) -> bool:
esperat = hashlib.sha256(CLAU_WEBHOOK.encode() + cos).hexdigest()
logging.info(f"Webhook rebut, firma={capcaleres['X-MediNube-Firma']}")
if capcaleres["X-MediNube-Firma"] == esperat:
processar(cos)
return True
return False- Client del servei de receptes per a la Farmàcia Robles:
import requests, jwt
def consultar_recepta(id_recepta: str, token: str) -> dict:
dades = jwt.decode(token, options={"verify_signature": False}) # "ja ve de MediNube"
if dades["rol"] != "farmacia":
raise PermissionError
r = requests.get(f"https://api.medinube.example/receptes/{id_recepta}",
headers={"Authorization": f"Bearer {token}"},
verify=False, timeout=5)
return r.json()Solucions
-
Cinc errors. (a)
md5(contrasenya)com a KDF: hash trencat i sense cost — força bruta fora de línia a velocitat de GPU; arranjament: scrypt/Argon2 amb sal (02-04, llista de verificació 9/10). (b)random.randintper a l'IV: predictible (família 2); arranjament:os.urandom(16)— encara que amb l'arranjament (d) desapareix. (c) AES-128 per accident (la clau és de 16 bytes perquè MD5 dona 16): decisió de mida presa per un hash, no per disseny. (d) CBC amb farciment manual i sense autenticació: maleable i candidat a padding oracle en la importació corresponent (família 3); arranjament: AES-256-GCM amb AAD (clinica=...;format=v1). (e) Sense versionat de format: irrotable (llista de verificació 8). Versió correcta: scrypt(contrasenya, sal) → AESGCM, formatversio || sal || nonce || ct+tag— és exactament la còpia de seguretat de 02-04. Bonus: per a un destinatari extern, millor el sobre híbrid de 04-05 i cap contrasenya compartida. -
Cinc errors. (a) Clau hardcodejada al codi (família 7 → gestor, 06-01). (b) La mateixa clau per a totes les clíniques: Clínica Sol pot falsificar webhooks "de" Centre Mèdic Luna; arranjament: clau per clínica (03-02). (c)
sha256(clau + cos): length extension (família 4,tag_MALAMENT); arranjament: HMAC-SHA256. (d) Comparació amb==: timing (llista de verificació 12); arranjament:hmac.compare_digest. (e) SenseX-MediNube-Timestampni finestra anti-replay: un webhook capturat es reinjecta indefinidament (03-02, llista de verificació 13). I ellogging.infoamb la signatura completa freguen la família 7: les signatures vàlides als logs faciliten el replay si en falta (e). -
Quatre errors (i mig). (a)
verify_signature: False: el token no es verifica en absolut — qualsevol fabrica{"rol": "farmacia"}a l'estilcrear_sessio_MALAMENTi passa el control; arranjament: verificar amb la pública de MediNube,algorithms=["EdDSA"],audience,issuer,require(06-03). (b) Encara que verifiqués: sense comprovarexpniaud, tokens eterns i reutilitzables entre serveis. (c)verify=Falsearequests: MITM sobre dades de receptes — i a més envia el Bearer token al possible impostor, regalant la credencial (05-02 + família 7); arranjament: verificació TLS estàndard (és un domini públic amb CA pública; ni tan sols hi ha excusa de CA interna). (d) Autorització basada en un claim (rol) d'un token no verificat = decisió de seguretat sobre dades del client, el pecat original de 01-02. El "mig":r.json()sense comprovar el codi d'estat — no és criptogràfic, però un revisor complet ho anota.
Conclusió
Ja tens el que cap manual dona per si sol: criteri. Saps que els errors criptogràfics no avisen, que vénen en raïms, i tens tres eines per caçar-los: les set famílies (amb el perquè explotable de cadascuna), la taula-índex de tots els _MALAMENT que MediNube ha anat arreglant durant el curs, i una llista de verificació de vint punts a punt per fer servir en la teva propera revisió. La galeria queda tancada: del Base64 editable de 01-02 al verify=False de 2023, tot el codi heretat té ja nom, diagnòstic i arranjament.
Queda una última mirada, i no és cap enrere sinó cap endavant: hi ha un byte reservat al sobre híbrid de MediNube (0x03, des de 04-05) esperant la criptografia que resistirà els ordinadors quàntics. Què amenaça exactament un ordinador quàntic, què sobreviu del nostre arsenal, què són ML-KEM i ML-DSA i què ha de fer MediNube avui — això és l'última lliçó del curs. Ens veiem al futur.
Curs de Criptografia Aplicada
Mòdul 1: Fonaments de la Criptografia
- Què és la criptografia i per a què serveix
- Codificació, ofuscació i xifratge
- Aleatorietat i entropia
- El principi de Kerckhoffs i les regles d'or
Mòdul 2: Criptografia Simètrica
- Xifratge simètric: AES i ChaCha20
- Modes d'operació
- Xifratge autenticat (AEAD)
- Derivació de claus (KDF)
Mòdul 3: Hashes, MAC i Contrasenyes
- Funcions hash criptogràfiques
- Autenticació de missatges amb HMAC
- Emmagatzematge segur de contrasenyes
Mòdul 4: Criptografia Asimètrica
- Fonaments de clau pública i RSA
- Criptografia de corba el·líptica
- Signatures digitals
- Intercanvi de claus: Diffie-Hellman
- Xifratge híbrid
Mòdul 5: PKI, Certificats i TLS
- Certificats X.509 i autoritats de certificació
- TLS a la pràctica
- Gestió del cicle de vida dels certificats
