2026-05-27 | Pinperepette

La Crittografia Ha Cambiato Geometria

Lattice-based cryptography smontata pezzo per pezzo. Reticoli, LLL, LWE, Kyber. Perché il NIST nel 2024 ha standardizzato un algoritmo che non vive nei numeri primi ma in 768 dimensioni. E perché nessuno se n'è accorto.

Lattice Crypto LWE Kyber / ML-KEM Post-Quantum

// Il Cifrario Che Nessuno Ha Visto Arrivare

Sezione 01. La iena e il corso di aggiornamento

La iena ha un posto fisso. Quello tipo da film di Zalone, pubblica amministrazione. Come ogni posto fisso che si rispetti, una volta all'anno la mandano a fare il corso di aggiornamento online. Cybersecurity, GDPR, sicurezza informatica, codice etico. Lezioni registrate, quesiti a risposta multipla, attestato in PDF da stampare e portare al protocollo.

Stasera sta sul divano con l'iPad e le cuffie. Io accanto, col laptop sulle ginocchia, a cazzeggiare su Twitter cercando di non guardare il grafico del Bitcoin. Ogni tanto si toglie una cuffia e mi tira una domanda dal corso. Di solito sono cose viste e riviste: differenza fra simmetrico e asimmetrico, lunghezza minima delle chiavi RSA per la PA, perché MD5 e SHA-1 non si usano più. Roba dei manuali NIST tradotta negli anni Duemila. Le rispondo distratto, "tranello, vai al quesito dopo".

Stasera però in una delle multiple choice spunta una sigla che non avevo mai sentito menzionare in un corso italiano:

«Quale dei seguenti NON è uno standard crittografico approvato dal NIST?»
 A) AES-256
 B) SHA-3
 C) ML-KEM-768
 D) DES

"Andre, che cos'è ML-KEM-768? Nel materiale non c'è".

Le rispondo al volo, mezzo distratto: "Tranello del questionario, roba vecchia rinominata. La risposta è D, DES è deprecato dal 2005. Vai avanti".

Non le basta. Mette giù l'iPad, apre Safari, digita "ML-KEM-768 cos'è" in italiano. Non trova un cazzo. Comunicati NIST tradotti male, due articoli Medium di consulenti che vendono corsi, una pagina di Wikipedia che rimanda all'inglese, un thread su Reddit in cui qualcuno linka un paper di 47 pagine. Mi guarda. "Andre, e poi sarei io quella che non si aggiorna".

Stasera rimedio io. Apro un terminale e le faccio vedere la cosa concreta. Lancio openssl s_client verso un server qualsiasi, e fra le righe di output salta fuori questa:

Server Temp Key: X25519MLKEM768

"Vedi questa? È quello che protegge il login al tuo portale, l'app di Signal aperta nell'altra mano, il pagamento PagoPA di stamattina. La prima metà, X25519, è la crittografia su curve ellittiche che esiste dal 1985. La seconda metà, MLKEM768, è la sigla che il tuo corso ti chiede di riconoscere ma non ti spiega: standardizzata dal NIST il 13 agosto 2024 con il nome FIPS 203".

È il primo algoritmo a chiave pubblica con standard federale americano che non vive nei numeri primi. Niente fattorizzazione, niente logaritmo discreto, niente curve ellittiche. Vive in un reticolo a 768 dimensioni. È il cambio crittografico più profondo dal 1977, l'anno in cui Rivest, Shamir e Adleman hanno pubblicato RSA.

Quarant'anni di aritmetica modulare, finiti.

È già sotto al telefono che la iena ha in mano, ai DM di iMessage e Signal, ai login SSH dei server della sua amministrazione. Chrome lo serve di default da agosto 2023, OpenSSH 9.9 da settembre 2024. Il browser con cui stai leggendo questa pagina lo sta probabilmente usando per parlarci. Solo che in italiano, come ha scoperto la iena due minuti fa, davvero non lo racconta nessuno. Stanotte tiro fuori cosa c'è dentro quella seconda metà del nome.

0
Dimensioni reticolo Kyber768
0
Modulo q (primo, 12 bit)
0
Public key (vs 32 B di ECC)
0
Security level (NIST cat. 3)

// Il Vecchio Mondo: Aritmetica

Sezione 02. Quarantasette anni di numeri primi

Per capire perché il salto è grosso, bisogna ricordarsi su cosa stava in piedi tutto fino a ieri. Ogni cifrario a chiave pubblica usato su Internet negli ultimi quarantasette anni è uno dei tre seguenti, tutti basati su problemi di aritmetica:

Nome Anno Problema sottostante Oggetti matematici
Diffie-Hellman 1976 Logaritmo discreto in \(\mathbb{Z}_p^*\) Interi modulo un primo
RSA 1977 Fattorizzazione di interi Prodotto di due primi grandi
ECC 1985 Logaritmo discreto su curva ellittica Punti su una curva su \(\mathbb{F}_p\)

Tutti e tre sono problemi aritmetici. Dato \(N = pq\) con \(p, q\) primi di 1024 bit, fattorizzare \(N\) richiede oggi tempo subesponenziale tramite il General Number Field Sieve, complessità \(\exp\!\big(c \cdot (\log N)^{1/3} (\log\log N)^{2/3}\big)\). Per \(N\) di 2048 bit si stimano circa \(2^{112}\) operazioni. Su un cluster classico, qualche miliardo di anni. La banca dorme tranquilla.

Dato \(g^x \bmod p\) con \(p\) primo di 2048 bit, ricavare \(x\) ha la stessa complessità del precedente. Su curva ellittica il problema è ancora più secco: dato \(P, Q = xP\) su una curva tipo Curve25519, ricavare \(x\) richiede \(O(\sqrt{n})\) operazioni con l'algoritmo di Pollard rho, dove \(n\) è l'ordine del gruppo. Per Curve25519, \(n \approx 2^{252}\), quindi serve circa \(2^{126}\) operazioni. La stessa banca dorme più tranquilla con chiavi di 32 byte invece che 256.

Aritmetica vuol dire una cosa precisa. Vuol dire che il segreto vive in un anello o in un gruppo di interi (o di punti di una curva). Le operazioni sono somma e prodotto modulo qualcosa. La sicurezza dipende dal fatto che invertire una certa funzione su quel gruppo (estrarre log, fattorizzare, decomporre un punto) sia computazionalmente duro. Questo modello è durato 47 anni esatti. Il 13 agosto 2024 il NIST ha pubblicato lo standard che dice: non basta più.

// Shor Rompe Tutto

Sezione 03. 1994, sette pagine, un disastro

Nel 1994 Peter Shor pubblica un articolo da sette pagine, Algorithms for Quantum Computation. L'articolo descrive un algoritmo che, dato un computer quantistico abbastanza grande, fattorizza interi di \(n\) bit in tempo \(O(n^3)\). Lo stesso algoritmo, con piccole modifiche, calcola il logaritmo discreto in qualsiasi gruppo finito abeliano. Quindi: RSA, Diffie-Hellman, ECC. Tutti rotti, simultaneamente, da un singolo algoritmo.

Il trucco è questo: Shor non attacca RSA direttamente, attacca il problema del periodo. Trova in tempo polinomiale il periodo della funzione \(f(x) = a^x \bmod N\) (per RSA) o \(f(x) = g^x\) in un gruppo (per Diffie-Hellman, ECC). Da quel periodo si ricavano direttamente i fattori di \(N\) o l'esponente segreto. È per quello che fattorizzazione e logaritmo discreto cadono entrambi: sotto, sono lo stesso problema computazionale. E sotto un computer quantistico, è il problema su cui la quantum Fourier transform brilla.

La cosa importante non è quando arriva il computer quantistico grande abbastanza. La cosa importante è che esiste l'algoritmo. Significa che la sicurezza della crittografia classica non sta in un teorema, sta in un limite ingegneristico temporaneo. Il traffico cifrato oggi, registrato da chi ne ha interesse, sarà decifrabile retroattivamente quando la macchina arriva. Si chiama harvest now, decrypt later. Detto in italiano: il traffico che oggi sembra sicuro potrebbe essere solo in freezer.

Quanto manca? Le stime continuano a stringersi. Nel 2023 servivano milioni di qubit fisici, finestra credibile 2030-2040. Nel marzo 2026 Google Quantum AI pubblica un paper che taglia i requisiti di un fattore 20 per attaccare ECC su 256 bit: bastano circa 1.200 qubit logici, sotto 500.000 fisici, tempo di esecuzione stimato sotto i 9 minuti. La finestra si sposta a "potenzialmente fine decennio nel migliore caso ingegneristico".

Il 24 aprile 2026 arriva anche il primo brick concreto: Giancarlo Lelli vince il Q-Day Prize di Project Eleven (1 BTC) recuperando una chiave privata da una pubblica su curva ellittica a 15 bit con hardware quantistico cloud. Quindici bit non sono 256, si rompono classicamente con un Raspberry Pi in millisecondi. Il senso del premio è un altro: prima dimostrazione pubblica e verificata che il principio funziona sul ferro vero, non solo in simulazione.

Niente di tutto questo rompe Ethereum, Bitcoin o RSA oggi. Ma il NIST nel 2016 aveva già lanciato il bando per la crittografia post-quantum proprio per questo: il dato cifrato oggi deve sopravvivere ai vent'anni a venire, e le stime continuano a stringersi.

$$\text{Shor}(N) \;:\; \text{tempo } O\!\left((\log N)^3\right) \;\;\text{su computer quantistico}$$

Polinomiale invece di subesponenziale, su un computer quantistico abbastanza grande.

L'aritmetica modulare è rotta. Servono problemi che Shor non sa attaccare. Il candidato più serio, e quello che ha vinto, viene dalla geometria dei numeri.

// Il Salto: Dai Numeri Alla Geometria

Sezione 04. Cos'è un reticolo

Un reticolo in \(\mathbb{R}^n\) è l'insieme di tutte le combinazioni a coefficienti interi di una base. Date \(n\) colonne linearmente indipendenti \(\mathbf{b}_1, \ldots, \mathbf{b}_n \in \mathbb{R}^n\), il reticolo che generano è:

$$\mathcal{L}(\mathbf{B}) = \left\{ \sum_{i=1}^{n} a_i \mathbf{b}_i \;:\; a_i \in \mathbb{Z} \right\}$$

Combinazioni intere, non reali. Cambia tutto.

Spazio vettoriale: combinazioni a coefficienti reali, ottieni una superficie continua (un piano, un iperpiano). Reticolo: combinazioni a coefficienti interi, ottieni un insieme discreto di punti che riempiono lo spazio come una griglia (in generale storta). Sembra una differenza minuta. Non lo è. La geometria del discreto in alta dimensione è un altro pianeta rispetto a quella del continuo.

Lo stesso reticolo si può descrivere con basi diverse. Prendi \(\mathbf{b}_1 = (1, 0)\) e \(\mathbf{b}_2 = (0, 1)\): il reticolo è \(\mathbb{Z}^2\), la griglia di tutti i punti a coordinate intere. Ora prendi \(\mathbf{b}_1' = (101, 100)\) e \(\mathbf{b}_2' = (100, 99)\). Il reticolo generato è esattamente lo stesso: ogni punto a coordinate intere si può scrivere come combinazione a coefficienti interi delle due nuove basi (l'esercizio è banale dato \(\det \mathbf{B}' = -1\)). Ma le due basi guardate sono molto diverse: la prima è corta e ortogonale, la seconda è lunga e quasi parallela.

La distinzione che regge tutta la crittografia post-quantum. Lo stesso reticolo ammette infinite basi. Avere una base buona (corta, ortogonale) significa poter risolvere problemi geometrici facilmente. Avere una base cattiva (lunga, quasi-collineare) significa non vedere niente. La chiave segreta in lattice-crypto è: la base buona. La chiave pubblica è: una base cattiva dello stesso reticolo. Chi conosce la base buona decifra. Chi vede solo la base cattiva, no.

La storia tecnica che porta a Kyber è densa di nomi e di tentativi falliti. Vale qualche riga per ricordarli, perché il lattice-paradigma non è arrivato per ispirazione:

La lezione operativa di questa lista è che il vincitore (Module-LWE) non è arrivato per eleganza, è arrivato perché tutto il resto è stato testato in pubblico. In \(\mathbb{R}^n\) i due problemi geometrici fondamentali sui reticoli sono:

$$\textbf{SVP} \;:\; \min_{\mathbf{v} \in \mathcal{L} \setminus \{0\}} \|\mathbf{v}\| \qquad \textbf{CVP} \;:\; \min_{\mathbf{v} \in \mathcal{L}} \|\mathbf{v} - \mathbf{t}\|$$

Shortest Vector Problem e Closest Vector Problem.

SVP: trova il vettore più corto, non nullo, del reticolo. CVP: dato un punto qualsiasi \(\mathbf{t}\) (non necessariamente sul reticolo), trova il punto del reticolo più vicino a \(\mathbf{t}\). Tutto qua, formalmente.

Detto in modo viscerale: SVP è cercare un ago geometrico dentro un'esplosione combinatoria. Il reticolo è discreto ma illimitato, lo spazio dei candidati cresce esponenziale con la dimensione, e in alta dimensione la nozione stessa di “corto” smette di comportarsi come ti aspetti (lo vediamo fra una sezione). Senza una base buona che indichi la direzione, non hai bussola. Per questo in 2D ci riesci a occhio. In 500D no.

// Reticoli In 2D: Dove Il Cervello Funziona

Sezione 05. Il caso che vedi a occhio

Apro Python, scelgo una base buona e una base cattiva dello stesso reticolo, e plottiamo. La base buona: \(\mathbf{b}_1 = (1, 0)\), \(\mathbf{b}_2 = (0, 1)\). La base cattiva del medesimo reticolo \(\mathbb{Z}^2\): \(\mathbf{b}_1' = (101, 100)\), \(\mathbf{b}_2' = (100, 99)\). Determinante \(-1\), quindi i due reticoli coincidono. Lo verifichiamo:

1import numpy as np
2import matplotlib.pyplot as plt
3
4B_good = np.array([[1, 0], [0, 1]]) # base ortogonale
5B_bad = np.array([[101, 100], [100, 99]]) # stesso reticolo, base quasi-collineare
6
7print("det(B_good) =", int(np.linalg.det(B_good))) # 1
8print("det(B_bad) =", int(np.linalg.det(B_bad))) # -1
9
10# Più lo stesso det in modulo, più il reticolo è lo stesso (a meno di base).
11# Volume fondamentale identico, geometria globale identica, percezione opposta.

Adesso il gioco di SVP. Dato \(\mathbb{Z}^2\), il vettore più corto non nullo è banalmente \((1, 0)\) o \((0, 1)\), norma 1. Con la base buona, è un coefficiente della base. Con la base cattiva, devi trovare interi \(a, b\) tali che \(101 a + 100 b = 1\) e \(100 a + 99 b = 0\). Risolvi: \(a = -99, b = 100\). Quindi \(-99\,\mathbf{b}_1' + 100\,\mathbf{b}_2' = (1, 0)\), vettore di norma 1. A occhio non lo vedi: stai sommando due vettori da \(\approx 141\) ciascuno per ottenere un vettore lungo 1. Devi cancellare 140 unità su 141 in entrambe le coordinate.

Visualizziamolo. Su 100 punti del reticolo generati come \(\mathbf{B} \cdot \mathbf{x}\) con \(\mathbf{x} \in \{-5, \ldots, 5\}^2\), il plot della base buona è una griglia ordinata. Il plot della base cattiva è due freccione lunghe e quasi parallele, e i punti del reticolo che ricadono in un cerchio attorno all'origine sembrano sparsi a casaccio.

Reticolo Z² con base buona (1,0)/(0,1) a sinistra e base cattiva (101,100)/(100,99) a destra. Stesso reticolo, geometria percepita opposta.
Lo stesso reticolo Z², due basi. A sinistra: ortogonale, SVP banale, ||b1||=1. A destra: base \(\mathbf{b}_1' = (101,100)\), \(\mathbf{b}_2' = (100,99)\), entrambi di norma ≈ 141, ma il SVP è ancora 1 (cerchiato in rosso). Per ottenerlo servono i coefficienti \(-99\,\mathbf{b}_1' + 100\,\mathbf{b}_2'\). La crittografia a reticoli sta tutta in questa asimmetria.

In due dimensioni questa asimmetria si può comunque annullare: un algoritmo del 1773 di Joseph-Louis Lagrange, perfezionato poi da Gauss, prende una base cattiva 2D e in poche iterazioni la trasforma in una base ridotta (la più corta ortogonale possibile). In 2D SVP è facile, sempre, comunque parta la base. Il problema diventa duro quando la dimensione cresce.

// Il Muro: Da 50 Dimensioni In Su

Sezione 06. L'intuizione si sgretola

In dimensione 2 SVP è banale. In dimensione 3 ancora si vede. Da dimensione 4 in poi il cervello smette di funzionare visivamente, e bisogna fidarsi degli algoritmi. Da dimensione ~70 in poi smettono di funzionare anche gli algoritmi.

Prima di guardare la complessità sul tavolo, una sosta di un minuto. In alta dimensione la geometria stessa diventa aliena, ancora prima del calcolo. Tre fatti che servono per il resto del pezzo:

1. Quasi tutto il volume di una sfera vive vicino alla superficie. In \(n = 768\), il 99.99% del volume sta nel 2% di guscio esterno. Il centro è quasi vuoto.

2. Due vettori casuali sono quasi sempre ortogonali. Il prodotto scalare di due vettori uniformi sulla sfera unitaria ha modulo \(\sim 1/\sqrt{n}\). In \(n = 768\), \(\approx 0.036\). "A caso" vuol dire "perpendicolare".

3. Il rumore gaussiano non vive all'origine. Un vettore gaussiano isotropo in dim \(n\) ha norma concentrata su \(\sqrt{n}\), spessore O(1). La "palla di rumore" in alta dimensione è una buccia.

Ed è qui che il cervello inizia a tradirti, perché continui a immaginarti sfere piene come se fossi ancora in 3D. Prova a sentirla fisicamente: il rumore di cifratura che in 2D ti immagini come una nuvoletta sfocata attorno all'origine, in 768D non è una nuvoletta. È una bolla cava. Una buccia di spessore O(1) attorno a una sfera di raggio \(\sqrt{768} \approx 27.7\). Il centro è vuoto. Il 99% del rumore vive a distanza 27-28 dall'origine, sparpagliato su una superficie sottile come carta velina. La «palla di rumore» piena al centro, l'intuizione che useresti istintivamente in 3D, semplicemente non esiste in 768D. Non è metafora: è aritmetica di concentrazione di misura. Lattice-crypto costruisce il segreto dentro quella buccia.

Otto pannelli: in alto istogrammi della norma di un vettore gaussiano N(0,I_n) per n=1,10,100,768, che collassano in un picco stretto centrato su sqrt(n); in basso istogrammi del prodotto scalare di due vettori uniformi sulla sfera, che collassano in una delta in 0.
Concentrazione di misura, prova empirica (50.000 campioni per cella, 05_high_dim_geometry.py). Sopra: la norma di un gaussiano isotropo collassa esponenzialmente su \(\sqrt{n}\). In dim 768, media = 27.71, std = 0.71. La "palla di rumore" è una buccia O(1)/√n. Sotto: il prodotto scalare di due vettori uniformi sulla sfera collassa su 0; in dim 768 il modulo medio è 0.029. Praticamente, in alta dimensione "a caso" significa "perpendicolare".

Detto a parole: la dimensione prima distrugge l'intuizione, poi distrugge il calcolo. Il tavolone qui sotto fa vedere quanto in fretta. Il problema è NP-hard sotto riduzioni randomizzate (Ajtai, 1998). Gli algoritmi esatti migliori conosciuti hanno costo:

Algoritmo Costo per SVP esatto in dim \(n\)
Enumeration (Kannan) \(2^{O(n \log n)}\)
Sieving classico (best known) circa \(2^{0.29\,n}\)
Sieving quantistico circa \(2^{0.27\,n}\)

Tradotto in numeri reali: per \(n = 512\) anche l'algoritmo migliore richiede attorno a \(2^{150}\) operazioni, fuori portata di qualsiasi calcolo classico. Per Kyber768 i parametri sono dimensionati per dare 192 bit di sicurezza nominale, e qui sta la differenza più importante con il vecchio mondo: il computer quantistico non sa attaccare i reticoli con un colpo di teorema. Lo speedup esiste, ma è un fattore costante nell'esponente, non polinomiale come Shor su RSA. La macchina quantistica grande romperebbe i reticoli poco peggio di una macchina classica.

Mostriamo il muro empiricamente. Implementiamo LLL (l'algoritmo polinomiale che approssima SVP) in pure-Python e plottiamo tempo e qualità dell'output al crescere di \(n\). Lo script reale è in scripts/la-crittografia-ha-cambiato-geometria/02_lll_scaling.py:

1# estratto: LLL pure-Python su basi q-ary, delta=0.75
2def lll(B, delta=0.75):
3 B = B.astype(np.int64).copy()
4 n = B.shape[0]
5 Bstar, mu = gram_schmidt(B)
6 norms2 = np.array([np.dot(Bstar[i], Bstar[i]) for i in range(n)])
7 k = 1
8 while k < n:
9 for j in range(k - 1, -1, -1): # size reduction
10 q = round(mu[k, j])
11 if q: B[k] -= q * B[j]; mu[k, :j+1] -= q * mu[j, :j+1]
12 if norms2[k] >= (delta - mu[k, k-1]**2) * norms2[k-1]:
13 k += 1 # Lovasz OK
14 else:
15 B[[k-1, k]] = B[[k, k-1]] # swap e ricomincia
16 Bstar, mu = gram_schmidt(B); k = max(k-1, 1)
17 return B

Risultato reale su MacBook Intel x86_64 (Python 3.14, numpy 2.4, base q-ary con \(q \approx 2^{20}\), seed=42):

1 n | t_lll | ||b1|| | q
2 10 | 0.080s | 1.090e+03 | 1048585
3 20 | 1.431s | 1.542e+03 | 1048585
4 30 | 7.914s | 2.232e+03 | 1048585
5 40 | 24.613s | 3.944e+03 | 1048585
6 50 | 64.953s | 5.716e+03 | 1048585
7 60 | 126.217s | 8.269e+03 | 1048585
Due grafici. Sinistra: tempo LLL in funzione di n in scala log, salita rapida. Destra: norma di b1 dopo LLL al crescere di n, cresce esponenziale.
Sinistra: il tempo cresce esponenziale (ricorda che pure-Python è ~10-100x più lento di fpylll, ma lo scaling asintotico è lo stesso). Destra: anche la norma del vettore restituito cresce esponenziale con la dimensione. LLL approssima SVP, e l'approssimazione peggiora al crescere di n.

Questo è un implementazione di riferimento didattica. In fpylll (ottimizzato, GMP per la precisione, Cython) le stesse dimensioni girerebbero in millisecondi-secondi invece che secondi-minuti. Ma il punto è lo scaling, non la costante: il tempo cresce come \(O(n^4 \log B)\) o peggio, e la qualità dell'output crolla con la dimensione. In dimensione 60 la norma è già 8000 volte sopra il SVP atteso, e l'esponente non lo ferma nessuno. LLL approssima SVP entro un fattore \(2^{(n-1)/2}\) nel caso peggiore. In dimensione 768 quel fattore è \(2^{383}\). LLL solleva il sasso a 1 cm da terra, in \(\mathbb{R}^{768}\) il sasso pesa quanto la Luna.

Per vedere a colpo d'occhio cosa fa LLL su una base storta, ne ho generata una su \(\mathbb{Z}^2\) (b1 = (13, 21), b2 = (5, 8); determinante \(-1\), quindi reticolo \(\mathbb{Z}^2\) stesso) e l'ho ridotta:

Due pannelli affiancati: a sinistra il reticolo Z^2 con una base cattiva (13,21)/(5,8), parallelogramma fondamentale lungo e storto; a destra lo stesso reticolo con la base ridotta da LLL, (0,-1)/(-1,0), parallelogramma quadrato.
LLL su \(\mathbb{Z}^2\) (06_lll_visualize.py). A sinistra la base cattiva (13,21)/(5,8), norme 24.70 e 9.43. A destra la base ridotta (0,-1)/(-1,0), norme 1.00 e 1.00. Stessi punti, stesso reticolo, stessa determinante. Quel collasso da 24.7 a 1.00 in 2D è LLL al suo meglio. In 768D non funziona più così.

E questo è ancora il caso facile. Sessanta dimensioni, una base q-ary generica, LLL puro. Kyber non vive in 60 dimensioni, vive in 768. Non è una base random: ha struttura modulare (Module-LWE) pensata apposta per dare ai parametri di sicurezza un margine confortevole rispetto al miglior attacco noto. Anche con BKZ in modalità aggressiva su cluster reali, le stime serie dicono che rompere Kyber768 richiede circa \(2^{180}\)-\(2^{200}\) operazioni. Non è questione di «basta un cluster più grosso»: lo speedup ti porta da impossibile a impossibile.

La iena, dal divano, senza togliersi le cuffie: "Andre, venerdì si va in montagna. La nera ieri ha sgomberato di nuovo il mangime". Annuisco al laptop. La nera è una delle due galline che teniamo in montagna, esiste solo per il caos. La bianca, l'altra, fa due uova al giorno e si fa i fatti suoi. La iena non si aspetta una risposta. Si rimette la cuffia.

L'apparente paradosso del “LLL gira ancora”. In crittografia non basta che l'algoritmo finisca: deve finire con il vettore corto. LLL in alta dimensione finisce con un vettore lungo. Per migliorare la qualità si usa BKZ (Block Korkine-Zolotarev): rifa LLL su blocchi di dimensione \(\beta\), ricomponendo. Costo \(2^{O(\beta)}\), qualità \(2^{O(n/\beta)}\). Per romperlo davvero servono \(\beta\) grandi, e a quel punto si torna esponenziali.

// LLL e Babai: Gli Strumenti

Sezione 07. Cosa fa l'avversario quando ha la base cattiva

LLL (Lenstra, Lenstra, Lovász, 1982) è il punto di partenza di ogni attacco a lattice-crypto. Riceve in input una base \(\mathbf{B}\) qualsiasi e in tempo polinomiale produce una base equivalente ridotta, in cui i vettori sono “corti quanto LLL sa renderli”. Le due condizioni che definiscono una base LLL-ridotta sono:

$$|\mu_{i,j}| \leq \tfrac{1}{2}, \qquad \delta \cdot \|\mathbf{b}_i^*\|^2 \leq \|\mu_{i+1,i}\mathbf{b}_i^* + \mathbf{b}_{i+1}^*\|^2$$

Size reduction (sinistra) e Lovász condition (destra). \(\mathbf{b}_i^*\) sono i Gram-Schmidt, \(\delta \in (1/4, 1)\) tipicamente 0.99.

Tradotto: i vettori della base sono “quasi ortogonali” nel senso di Gram-Schmidt, e ordinati per lunghezza crescente. Il primo vettore della base ridotta soddisfa \(\lVert \mathbf{b}_1 \rVert \leq 2^{(n-1)/2} \cdot \lambda_1(\mathcal{L})\), dove \(\lambda_1\) è il vettore più corto. Il fattore di approssimazione cresce esponenziale nella dimensione: in 50D è ancora gestibile (\(\approx 2^{24.5}\), corregge spesso esatto), in 500D è \(2^{249.5}\), inutile.

Per CVP esiste un parente: l'algoritmo nearest-plane di Babai (1986). Dato un target \(\mathbf{t}\) e una base LLL-ridotta \(\mathbf{B}\), proietta \(\mathbf{t}\) iterativamente sugli iperpiani della base e ritorna un vettore del reticolo vicino. Approssima CVP entro un fattore \(2^{n/2}\).

1# Babai nearest-plane, implementazione semplificata (Galbraith 18.1)
2def babai_nearest_plane(B, t):
3 # B: base LLL-ridotta come matrice numpy n x n (riga = vettore)
4 # t: target in R^n
5 n = len(B)
6 Bstar, _ = np.linalg.qr(B.T) # Gram-Schmidt ortonormale
7 Bstar = Bstar.T * np.linalg.norm(B, axis=1)[:, None]
8 b = t.copy().astype(float)
9 for i in range(n - 1, -1, -1):
10 c = round(np.dot(b, Bstar[i]) / np.dot(Bstar[i], Bstar[i]))
11 b = b - c * B[i]
12 return t - b # elemento del reticolo vicino a t

Babai vince contro CVP quando il target è molto vicino al reticolo (entro un fattore polinomiale dal vettore più corto). Quando il target è troppo lontano, Babai dà comunque un punto del reticolo, ma quello sbagliato. La crittografia sceglie il rumore in modo che Babai con la base buona ce la faccia e Babai con la base cattiva no. È il dettaglio centrale.

// LWE: Il Rumore Come Sicurezza

Sezione 08. Learning With Errors, Regev 2005

Fino a qui stiamo ancora solo cercando vettori corti. Adesso entra il rumore.

Il problema su cui sta in piedi Kyber non è SVP esatto, è un cugino più addomesticato e con un dettaglio rivoluzionario: il rumore stesso è ingrediente attivo. Si chiama Learning With Errors, lo ha introdotto Oded Regev nel 2005, e nel 2018 ha vinto il Premio Gödel per quel paper. La definizione sembra innocua:

$$\mathbf{A} \in \mathbb{Z}_q^{m \times n} \text{ uniforme}, \;\; \mathbf{s} \in \mathbb{Z}_q^n \text{ segreto}, \;\; \mathbf{e} \in \mathbb{Z}_q^m \text{ piccolo gaussiano}$$ $$\mathbf{b} = \mathbf{A}\mathbf{s} + \mathbf{e} \pmod{q}$$

Dato (A, b), recuperare s. Senza rumore: sistema lineare, banale. Con rumore: durissimo.

Senza il termine \(\mathbf{e}\), la cosa è algebra del primo anno: \(\mathbf{s} = \mathbf{A}^{-1} \mathbf{b}\) (se \(m \geq n\) e \(\mathbf{A}\) ha rango pieno). Con \(\mathbf{e}\) gaussiano piccolo (deviazione standard tipicamente attorno a 1-2 in \(\mathbb{Z}_q\)), il sistema diventa approssimativamente lineare, e tutta la struttura collassa. Il problema diventa equivalente a un'istanza di CVP su un reticolo costruito a partire da \(\mathbf{A}\), e quel reticolo vive in dimensione \(\geq n\). LWE è almeno tanto duro quanto trovare il vettore più corto in quel reticolo (worst-case to average-case reduction, Regev 2005).

Questa è la frase più importante del pezzo. Per la prima volta nella storia della crittografia, la sicurezza non viene da un problema duro “naturale” (come fattorizzare un intero). Viene da un problema duro perturbato di rumore. Aggiungere gaussiano scelto bene è ciò che rende un sistema lineare un problema NP. La sicurezza diventa una grandezza fisica: la magnitudine del rumore. Troppo poco rumore → rotto. Troppo rumore → il legittimo destinatario non decifra. C'è una finestra precisa.

Detto dal punto di vista dell'attaccante, il cambio è ancora più secco. L'attaccante non sta più invertendo una funzione elegante. Sta cercando struttura dentro rumore geometrico in 768 dimensioni. Sono due lavori diversi: il primo è algebra esatta con backdoor matematici noti, il secondo è inferenza statistica in alta dimensione, su vettori che vivono in geometrie dove l'intuizione umana ha smesso di funzionare cinque sezioni fa.

La iena, una cuffia abbassata: "Andre, è come quando metti l'ape disegnata sopra il QR code e il telefono lo legge lo stesso?". Quasi, le dico, ma rovesciato. Lì Reed-Solomon corregge gli errori introdotti dal disegno. Qui invece l'errore è esattamente ciò che protegge il segreto. È la differenza fra cancellare il rumore e cifrare dentro al rumore. Provo a spiegarglielo. Lei alza il sopracciglio destro, che è il suo modo di dirmi "Andre, ne dai per scontate troppe".

Implementiamo un giocattolo. Schema: l'utente Bob ha una chiave segreta \(\mathbf{s} \in \mathbb{Z}_q^n\) e una chiave pubblica \((\mathbf{A}, \mathbf{b}) = (\mathbf{A}, \mathbf{A}\mathbf{s} + \mathbf{e})\) con \(\mathbf{e}\) piccolo. Alice vuole inviare un bit \(\mu \in \{0, 1\}\). Calcola \(\mathbf{r} \in \{0,1\}^m\) sparso, e produce il cifrato:

$$u = \mathbf{r}^\top \mathbf{A} \pmod{q}, \qquad v = \mathbf{r}^\top \mathbf{b} + \mu \cdot \lfloor q/2 \rfloor \pmod{q}$$

Cifrato (u, v) ∈ Z_q^n x Z_q. Encoding del bit dentro il modulo.

L'idea, raccontata a voce, è quasi pigra: spingi il bit a metà del modulo e lascia che il rumore lo nasconda. Chi ha la chiave \(\mathbf{s}\) sa togliere il rumore senza far cadere il bit. Chi non ce l'ha, no.

Bob calcola \(v - u^\top \mathbf{s} = \mathbf{r}^\top \mathbf{b} + \mu \cdot \lfloor q/2 \rfloor - \mathbf{r}^\top \mathbf{A}\mathbf{s} = \mathbf{r}^\top \mathbf{e} + \mu \cdot \lfloor q/2 \rfloor\). Se il termine \(\mathbf{r}^\top \mathbf{e}\) ha modulo molto più piccolo di \(q/4\), allora Bob recupera \(\mu\) guardando se il risultato è più vicino a \(0\) oppure a \(\lfloor q/2 \rfloor\). Cifrato e decifrato:

1# 03_lwe_toy.py: LWE giocattolo, un bit per volta
2import numpy as np
3
4n, m, q = 256, 512, 3329 # parametri stile Kyber-toy
5sigma = 3.2 # deviazione standard del rumore
6
7def keygen():
8 A = np.random.randint(0, q, size=(m, n))
9 s = np.random.randint(0, q, size=n)
10 e = np.round(np.random.normal(0, sigma, m)).astype(int)
11 b = (A @ s + e) % q
12 return (A, b), s # pk, sk
13
14def encrypt(pk, mu):
15 A, b = pk
16 r = np.random.randint(0, 2, size=m) # vettore sparso 0/1
17 u = (r @ A) % q
18 v = (r @ b + mu * (q // 2)) % q
19 return u, v
20
21def decrypt(sk, ct):
22 u, v = ct
23 d = (v - u @ sk) % q
24 return 1 if abs(d - q//2) < abs(d) else 0
25
26pk, sk = keygen()
27for mu in [0, 1] * 500:
28 assert decrypt(sk, encrypt(pk, mu)) == mu

Mille bit cifrati e decifrati, zero errori (verificato con 03_lwe_toy.py, seed=2026, sigma=3.2: Errori: 0). Adesso, l'esperimento istruttivo: cambia sigma e misura il failure rate. Lo script 03_lwe_toy.py fa un sigma-sweep su \(\sigma \in \{0.5, 1, 2, 3.2, 5, 8, 15, 30, 60, 120\}\), 1000 bit ciascuno:

SigmaErrori / 1000Failure rateStato
0.500.0%OK ma poco rumore = poca sicurezza
3.2 (Kyber-like)00.0%Sweet spot
8.000.0%OK
15.010.1%Limite
30.0585.8%Decryption rotta
60.019919.9%Inutile
120.051951.9%Indistinguibile da random
Failure rate empirico di LWE in scala log al variare di sigma. Plateau a 0 fino a sigma=8, salita ripida da sigma=15 in poi, asintoto a 50% (random guess) per sigma>100.
La finestra di LWE in azione. Sotto \(\sigma \approx 15\) il rumore non è abbastanza da rompere la decifratura. Sopra \(\sigma \approx 30\) le componenti di rumore sommate cominciano a saltare oltre \(q/4\) e i bit si invertono. A \(\sigma = 120\) la rate è il 52%: indistinguibile dal lancio di una moneta. Il sweet spot è stretto, e Kyber lo coglie.

Per vedere dove il rumore mangia il bit, plottiamo gli istogrammi del valore decifrato \(d = (v - u^\top \mathbf{s}) \;\mathrm{mod}\; q\) per i due valori di \(\mu\), al variare di \(\sigma\). Cinquemila cifrature per \(\mu=0\) e cinquemila per \(\mu=1\), per ogni \(\sigma\):

Tre pannelli con istogrammi di d=(v-u^T s) mod q. Sigma 3.2: due cluster sottili separati attorno a 0 e q/2. Sigma 30: cluster larghi ma ancora distinti. Sigma 120: distribuzioni quasi uniformi su [0,q), indistinguibili.
La finestra come distribuzioni (07_lwe_window.py). \(\sigma=3.2\): due cluster sottili ben separati attorno a 0 (verde, \(\mu=0\)) e \(q/2 = 1664\) (viola, \(\mu=1\)). Bob distingue a colpo d'occhio. \(\sigma=30\): cluster più larghi, qualcuno comincia a sconfinare oltre la soglia \(q/4\) (linea tratteggiata grigia). \(\sigma=120\): le due distribuzioni sono praticamente uniformi su \([0, q)\), indistinguibili. La «finestra» di LWE è lo stretto intervallo di \(\sigma\) in cui il primo pannello continua a funzionare e l'attaccante non ce la fa.

La cifratura funziona sempre, indipendentemente da \(\sigma\). Quello che cambia è la decifratura. Per garantire al destinatario legittimo di leggere il messaggio bisogna che il rumore stia entro una forbice precisa. Per garantire all'attaccante di non leggerlo, bisogna che il rumore sia abbastanza grande da rendere intrattabile il problema di reticolo sottostante. La sicurezza vive nella forbice, e quando l'analisi crittoanalitica si raffina, la forbice si stringe ulteriormente.

Connection con il vecchio mondo. In RSA il rumore non esiste: la cifratura è deterministica algebra (poi randomizzata con padding tipo OAEP). In ECDH il rumore non esiste. Tutto il passaggio è algebra esatta. In LWE il rumore è il segreto. Cambiare un singolo bit del seed di \(\mathbf{e}\) produce un cifrato completamente diverso. La sicurezza è statistica, non aritmetica. È questo lo shift di paradigma.

Vale la pena fermarsi un istante, perché l'inversione è gigantesca. Per quarant'anni nell'informatica seria l'errore è stato il nemico: bit flip in memoria, rumore termico nei canali, perturbazione sui dati in transito. Tutta la teoria dell'informazione di Shannon, i codici Reed-Solomon che ho smontato su questo blog (L'Ape Sta Nel Polinomio), l'ECC nelle RAM dei server, gli hash di verifica, i parity check: tutto progettato per cancellare il rumore. La crittografia stessa è sempre stata algebra esatta in cui un bit sbagliato faceva fallire la decifratura, punto.

In LWE quel paradigma si rovescia. L'errore smette di essere il nemico e diventa infrastruttura. Aggiungi un gaussiano piccolo a un sistema lineare e quello che era un esercizio del primo anno diventa un problema NP-hard. La sicurezza non resiste al rumore: nasce dal rumore. Non è mai successo prima nella storia della crittografia. È il fatto più strano del pezzo, ed è anche il motivo per cui lattice-crypto regge dove l'aritmetica modulare cade. Per quarant'anni abbiamo combattuto il rumore. Adesso lo cifriamo dentro.

Detto più brutalmente: la crittografia ha smesso di fidarsi della purezza matematica e ha cominciato a usare il caos controllato. RSA era una funzione precisa, deterministica, invertibile con una chiave e non invertibile senza. LWE è una distribuzione statistica: ogni cifratura del medesimo bit produce un cifrato diverso, ogni decifratura è una stima sotto soglia, il segreto è un'eco gaussiana dentro un sistema lineare quasi-rotto. È il primo cifrario a chiave pubblica che assomiglia più al machine learning che all'algebra esatta. Non è un caso che a vincere il bando NIST sia stato proprio lo schema costruito sul concetto più sporco di tutta la famiglia post-quantum.

// Kyber In Pratica

Sezione 09. Module-LWE, NTT, 1184 byte di chiave pubblica

Kyber non è LWE puro. Sarebbe troppo lento (matrici \(m \times n\) intere) e troppo grosso (chiavi pubbliche da centinaia di KB). Kyber usa una variante chiamata Module-LWE: invece di lavorare su \(\mathbb{Z}_q\), lavora su un anello di polinomi \(R_q = \mathbb{Z}_q[x] / (x^{256} + 1)\). I “vettori” sono vettori di polinomi, le “matrici” sono matrici \(k \times k\) di polinomi. Stesso problema sottostante (CVP/SVP su un reticolo di dimensione \(256 k\)), ma con struttura algebrica che permette la moltiplicazione veloce via NTT (Number Theoretic Transform), l'analogo intero della FFT.

Parametro Kyber512 Kyber768 Kyber1024
Sicurezza NIST Cat. 1 (AES-128) Cat. 3 (AES-192) Cat. 5 (AES-256)
k (rank modulo) 2 3 4
Dimensione reticolo 512 768 1024
q (modulo) 3329 3329 3329
Chiave pubblica (B) 800 1184 1568
Cifrato (B) 768 1088 1568
Chiave privata (B) 1632 2400 3168

Curve25519 ha chiave pubblica e privata di 32 byte ciascuna. Kyber768 sta a 1184 + 2400 byte. Trenta volte di più. È il prezzo del cambio di geometria. Il payload TLS aumenta di circa 2 KB per handshake, motivo per cui Chrome serve Kyber + X25519 in hybrid: se Kyber dovesse risultare rotto domani, il livello classico tiene; e viceversa.

Il modulo \(q = 3329\) è primo, 12 bit, e \(3329 = 2^{8} \cdot 13 + 1\). Questa scelta non è estetica: \(q\) divide \(2 \cdot 256\), quindi esiste una radice 256-esima primitiva dell'unità in \(\mathbb{Z}_q\), e l'NTT in dimensione 256 si può fare modulo \(q\) con la trasformata di Cooley-Tukey identica a una FFT. La moltiplicazione di due polinomi di grado 255 passa da \(O(256^2)\) a \(O(256 \log 256)\). Su CPU moderne, Kyber768 fa una key encapsulation in centinaia di microsecondi (pyca/cryptography sopra OpenSSL), e scende a 25-50 µs con implementazioni di riferimento AVX2 ottimizzate come PQClean o AWS-LC.

1# 04_kyber_demo.py: ML-KEM-768 reale via pyca/cryptography
2from cryptography.hazmat.primitives.asymmetric import mlkem
3
4# Bob genera coppia
5sk = mlkem.MLKEM768PrivateKey.generate()
6pk = sk.public_key()
7
8# Alice incapsula. L'API ritorna (shared_secret, ciphertext)
9ss_alice, ct = pk.encapsulate() # ss=32 B, ct=1088 B
10
11# Bob decapsula con la sua chiave privata
12ss_bob = sk.decapsulate(ct)
13
14assert ss_alice == ss_bob # entrambi hanno la stessa chiave di sessione

L'API a Encapsulate/Decapsulate è deliberata: Kyber non cifra messaggi di lunghezza arbitraria, restituisce solo una chiave simmetrica di 32 byte (256 bit), su cui poi si fa AES-GCM o ChaCha20-Poly1305 per cifrare il payload reale. Questa è la struttura di ogni KEM moderno. Il segreto condiviso è derivato dalla parte determinata della cifratura, poi passato per SHAKE256 per ottenere un'uscita uniforme indipendente dai dettagli interni.

Benchmark mediano su 2000 run, stesso laptop di sopra (Intel x86_64, Python 3.14, cryptography 47, OpenSSL backend), confronto con X25519 (curve ellittiche pre-quantum):

Metrica X25519 Kyber768 Rapporto
Public key (byte)32118437×
Wire (ct / DH msg, byte)32108834×
KeyGen (µs)1135925.2×
Encap / Exchange (µs)411573.8×
Decap (µs)n/a235n/a

Trenta volte di dati in più sul filo, 3-5 volte di CPU. È il prezzo della geometria. Per un handshake TLS in più servono ~2 KB in più e ~150 µs in più di CPU. Per la maggior parte dei carichi reali è rumore di fondo. Per data center ad alto QPS con TLS resumption assente, è un costo che si misura. Motivo per cui Chrome serve Kyber + X25519 in hybrid: se Kyber dovesse risultare rotto domani, il livello classico tiene; e viceversa.

Tutto il codice di questo articolo è replicabile. Cartella scripts/la-crittografia-ha-cambiato-geometria su GitHub. Quattro script auto-contenuti:

Dipendenze: numpy, matplotlib, cryptography ≥ 44. README della cartella con istruzioni complete. Niente fpylll, niente oqs-python, niente compilazione di librerie C: tutto gira su un Python 3.10+ vuoto.

La iena, dall'altra stanza: "Andre, mercoledì smielo. Telaio 3 e 4 dell'arnia in fondo sono già pieni". Annuisco al monitor. Annuisco automaticamente, lo so. Lei lo sa benissimo che non ho registrato. Continua a parlare, credo per registrare il fatto che non ho ascoltato.

Tutto quello che ho scritto fin qui è il livello teorico: reticolo, rumore, modulo, NTT. Sotto vive un altro livello, quello implementativo: sampling discreti, divisioni intere, accessi in cache, timing del decapsulation. Nella storia della crittografia è sempre in quello strato che nascono i guai veri, e Kyber non fa eccezione. Ci torno tra due sezioni.

// Dove Vive Già

Sezione 10. ML-KEM nel mondo, da agosto 2023

Il NIST ha pubblicato FIPS 203 il 13 agosto 2024. L'implementazione concreta è partita molto prima del bollino federale. Ecco una mappa dello stato di adozione che ho potuto verificare:

Sul telefono che hai in tasca, oggi, c'è più lattice-crypto che curve ellittiche se misuri in cifrato che passa. Ma se chiedi alla gente per strada qual è l'algoritmo che protegge i loro DM, nessuno risponde “Module-LWE su \(\mathbb{Z}_{3329}[x]/(x^{256}+1)\) in dimensione 768”. Il cambio è avvenuto a porte chiuse, in commit pubblici che non sono diventati news. La crittografia è un'infrastruttura silenziosa anche quando cambia paradigma.

Un capitolo a parte sono le blockchain. Bitcoin ed Ethereum firmano ogni transazione con ECDSA su secp256k1, esattamente la curva che il paper Google di marzo 2026 ha messo in cima alla lista degli obiettivi quantistici credibili. Il rischio specifico è harvest now, decrypt later: ogni indirizzo che ha già mandato almeno una transazione ha esposto la propria chiave pubblica on-chain, e quando arriverà la macchina quantistica abbastanza grande, quelle pub key diventano private. Gli indirizzi che hanno solo ricevuto, no, perché mostrano l'hash della pub key, non la pub key.

La risposta di Ethereum è EIP-8141 (signature agility tramite account abstraction), in discussione per l'hard fork Hegotá previsto nel secondo semestre del 2026. L'idea è chirurgica: ogni wallet decide individualmente se restare su ECDSA o migrare a una firma post-quantum (probabili candidati ML-DSA / Dilithium, oppure SLH-DSA / SPHINCS+, entrambi standardizzati dal NIST nel 2024). Niente fork del protocollo che forzi tutti a muoversi insieme, niente flag day. Migrazione opt-in, vagliata dal mercato. È lo stesso pattern dell'hybrid TLS di Chrome, traslato sul livello "ogni utente per sé". Bitcoin sta discutendo schemi analoghi (BIP-360 e successori), più lentamente.

// La Matematica Regge, Il Software No

Sezione 11. Quello che il bollino NIST non garantisce

Prima di chiudere serve un'avvertenza. Tutto il pezzo è raccontato col tono dello standard che si afferma, e l'impressione che può restare è che “Kyber = sicurezza definitiva”. Non è così. Lattice-based crypto è oggi la migliore candidata disponibile per il post-quantum, ed è quella su cui ha scommesso il NIST. Ma la storia della crittografia è piena di schemi che sembravano solidi finché non sono crollati per un dettaglio inatteso. Tre cose vale la pena tenere a mente.

La storia recente della PQC è un cimitero. Nella stessa competizione NIST in cui Kyber ha vinto, altri due finalisti seri sono caduti tra il 2022 e il 2023. Rainbow, schema di firme multivariate, è stato rotto da Ward Beullens nel febbraio 2022 con un attacco da poche ore di laptop. SIKE, schema di key exchange basato su isogenie di curve ellittiche supersingolari, è stato rotto da Castryck e Decru in un weekend nel luglio 2022, mentre era al quarto round della competizione. Entrambi erano arrivati a un passo dalla standardizzazione. Lattice-based è di gran lunga il più studiato e il più in piedi tra i candidati, ma il messaggio di SIKE è chiaro: la durezza di un problema crittografico è uno stato dell'arte, non un teorema chiuso.

La matematica regge, ma il software ci sta in mezzo. Anche dando per buono che Module-LWE sia tanto duro quanto crediamo, fra il teorema e il binario in produzione c'è un'implementazione, e le implementazioni si rompono altrimenti. Il caso più istruttivo è KyberSlash, scoperto da Daniel J. Bernstein e collaboratori a fine 2023, divulgato a inizio 2024.

Pensaci concretamente. Un server da qualche parte gira il decapsulation di Kyber. Riceve un cifrato, restituisce la chiave di sessione, l'handshake TLS prosegue. Il risultato è sempre quello giusto. La matematica del paper Crystals-Kyber funziona alla perfezione. Solo che dentro al codice, in un punto preciso, c'è una divisione intera con il divisore che dipende dai bit della chiave privata. Sulle CPU reali (Intel, ARM, RISC-V indifferentemente) la divisione non gira a tempo costante: a seconda dell'operando, impiega qualche ciclo in più o in meno.

L'attaccante non rompe niente. Non fattorizza, non risolve LWE, non riduce un reticolo. Si limita a misurare il tempo. Dieci microsecondi, undici microsecondi, dodici. Da remoto, su rete, in mezzo al rumore di Internet. Mille richieste, diecimila, un milione. Le distribuzioni statistiche cominciano a separarsi. Bit dopo bit, esce la chiave privata da 2400 byte. La matematica del paper era intatta. Il C, no. La sicurezza non viveva in \(\mathbb{Z}_{3329}[x]/(x^{256}+1)\), viveva nel timing della divisione. Quello stava sulla CPU di chiunque, e si misurava da fuori.

La patch è arrivata in poche settimane su tutte le librerie (libcrux, pq-crystals, oqs, BoringSSL, Open Quantum Safe). Hanno sostituito la divisione con una sequenza di shift e moltiplicazioni a tempo costante. Ma per i mesi prima della disclosure, ogni server che girasse l'implementazione di riferimento del NIST era vulnerabile. Lo standard FIPS 203 era pubblicato. Il bollino federale c'era. E perdeva la chiave per microsecondi.

La famiglia di problemi è più ampia di KyberSlash. Sampling gaussiano: per generare il rumore \(\mathbf{e}\) servono campioni da una distribuzione discreta, e i metodi non a tempo costante leakano via cache timing. NTT timing: la trasformata interna ha pattern di accesso alla memoria che dipendono dai coefficienti. Decapsulation failures: con probabilità piccolissima il rumore esce dalla forbice e la decifratura fallisce; sono state pubblicate famiglie di chosen ciphertext attacks che inducono fallimenti per estrarre informazione sulla chiave. Tutti questi attacchi sono side-channel, non rompono il problema sottostante. Ma la chiave la portano via lo stesso.

Perché l'hybrid con X25519 non è eccesso di zelo. Quando Chrome serve X25519MLKEM768 con due algoritmi in parallelo, non sta solo coprendosi contro il computer quantistico. Sta coprendosi anche contro il caso in cui domani esca un paper che indebolisce Kyber, o un nuovo KyberSlash su un'implementazione diversa. L'hybrid è una scommessa congiunta: per leggere il traffico devi rompere entrambi. Una mossa difensiva onesta, in un momento in cui il nuovo paradigma ha ancora pochi anni di crittoanalisi sulle spalle.

Quindi: la geometria che ha vinto al NIST è la cosa migliore che oggi sappiamo costruire. Non è una garanzia eterna. La differenza fra un teorema e uno standard sta tutta qui.

// La Crittografia Ha Cambiato Geometria

Sezione 12. Conclusione

Chiudo il terminale. Ricapitoliamo la riga di output da cui sono partito: X25519MLKEM768. Quattro parole gettate dentro un handshake TLS. La prima metà, X25519, è aritmetica modulare su una curva ellittica scoperta nel 1985, che vive in un campo di 256 bit. La seconda metà, MLKEM768, è geometria su un reticolo di 768 dimensioni costruito sopra un anello di polinomi modulo \(x^{256}+1\). La prima è rotta da Shor in tempo polinomiale. La seconda no, perché Shor non sa attaccare CVP/SVP.

Tra i due algoritmi c'è il punto di flesso di mezzo secolo di crittografia moderna. Prima la sicurezza nasceva dalla durezza di un'operazione algebrica: fattorizzare, estrarre logaritmi. Adesso la sicurezza nasce dalla durezza di un problema geometrico: trovare il vettore più corto, trovare il punto più vicino. La differenza non è cosmetica. È una migrazione dell'oggetto matematico: dai numeri interi alla geometria dei numeri.

Stessa migrazione, in altra forma, è già in corso da anni nell'AI. Gli embeddings non sono numeri, sono vettori in spazi ad alta dimensione (toccato in Vicino Vuol Dire Vettore e Il Volto È Un Vettore). Il senso di una parola, di un volto, di una canzone, di una pagina web vive in geometrie da 512, 1024, 4096 dimensioni. La crittografia post-quantum vive in geometrie da 768, 1024 dimensioni. Sono cugine più di quanto sembri: stessa famiglia di intuizione spezzata, stessa famiglia di matematica che la sostituisce.

L'informatica moderna sta migrando da un mondo dove la matematica utile era quella che si poteva fare a mente (somme, prodotti, divisioni, congruenze) a un mondo dove la matematica utile è quella che il cervello non sa visualizzare. Distanze in 768 dimensioni, rumore gaussiano come ingrediente attivo, reticoli storti dove il vettore più corto si trova solo se già conosci la base buona. È un'informatica che chiede strumenti diversi per essere capita.

Il NIST ha standardizzato il 13 agosto 2024. Chrome lo serve di default da quasi tre anni. Signal e iMessage lo usano per i tuoi DM da due. OpenSSH lo usa quando entri su un server da venti mesi. La crittografia ha cambiato geometria nel 2023, lo standard l'ha ratificato nel 2024, oggi è il 2026 e tutto questo è infrastruttura ordinaria. Solo che nessuno l'ha detto a voce.

E poi c'è il resto. Tutta questa geometria gigantesca (il reticolo di Module-LWE che Shor non sa attaccare, le 768 dimensioni dove «corto» smette di voler dire quello che vuol dire in tre, il rumore gaussiano che è diventato il materiale stesso del segreto) sta in piedi solo finché regge il silicio sotto. Un branch predictor che indovina male, una divisione intera che impiega dodici nanosecondi in più del previsto, una cache che leakà bit per millisecondi sul singolo decapsulation: e la chiave svanisce dalla CPU senza che la matematica abbia perso un capello. KyberSlash è successo. Ne succederanno altri. Il teorema regge. Il binario perde. La crittografia ha cambiato geometria, ma alla fine si rompe ancora esattamente dove si rompeva quarant'anni fa: nel ferro.

Letta brutalmente: 768 dimensioni, reticoli q-ary, worst-case-to-average-case di Ajtai, BKZ tunato a mano, sieving quantistico, otto anni di competizione NIST, FIPS 203. E poi perdi la chiave perché una divisione intera impiega 20 nanosecondi in più del previsto. Questa è la crittografia reale del 2026: una corda tesa fra una geometria che il cervello non sa visualizzare e un silicio che nessuno sa garantire.

Riapro il terminale. La riga Server Temp Key: X25519MLKEM768 è ancora lì, identica a un'ora fa. Stessi caratteri, stesso handshake. Solo che adesso so cosa nascondono quelle quattordici lettere e cifre: un reticolo a 768 dimensioni, un anello \(\mathbb{Z}_{3329}[x]/(x^{256}+1)\), un rumore gaussiano calibrato per vivere in una buccia di carta velina, una storia di tentativi falliti e di un solo sopravvissuto. So anche cosa può rompersi nel prossimo handshake: non la matematica, che è impeccabile. Una cache line. Un branch predictor. Una divisione intera che impiega 20 nanosecondi in più del previsto.

Il prossimo pacchetto TLS parte fra un istante.

"Il segreto non è più un primo grosso. È un vettore corto in un reticolo storto, annaffiato di rumore."

Module-LWE su \(\mathbb{Z}_{3329}[x]/(x^{256}+1)\), rank 3, dimensione reticolo 768, sicurezza nominale 192 bit contro attacchi classici e quantistici noti. Chiave pubblica 1184 byte (37x rispetto a Curve25519), encapsulation 157 µs mediana su pyca/cryptography, 25-50 µs con AVX2 ottimizzato. FIPS 203 dal 13 agosto 2024. Chrome, Signal, iMessage, OpenSSH già in produzione da più di due anni. Vulnerabilità implementative documentate: KyberSlash, fine 2023, patch su tutte le librerie nel primo trimestre 2024.

Signal Pirate