C2 — Réseaux euclidiensDans le dialogue d’impression, choisissez « Enregistrer au format PDF » comme destination.
Retour

Master 1 · Cryptographie post-quantique

Cours 2Réseaux euclidiens

Le cœur de l'UE : l'objet, ses problèmes difficiles, et les deux standards qui en sont tirés.

4 chapitres · 20 h de travail estimé

  1. 1. Réseaux : fondements5 h
  2. 2. LWE et ses variantes5 h
  3. 3. ML-KEM (Kyber)5 h
  4. 4. ML-DSA (Dilithium) et Falcon5 h

Chapitre 1 · 5 h

Réseaux : fondements

Réseau, base, déterminant ; SVP, CVP et SIVP ; l'arrondi de Babai selon la base ; réduction LLL et BKZ, et l'estimation de sécurité qui en découle.

Le NIST a publié le 13 août 2024 trois normes post-quantiques. Deux d'entre elles, FIPS 203 pour l'encapsulation de clé et FIPS 204 pour la signature, reposent sur le même objet mathématique — et cet objet n'est ni un entier à factoriser, ni un groupe où le logarithme discret serait difficile. C'est un réseau euclidien.

Ce chapitre installe l'objet, ses problèmes difficiles, et la seule chose qui décide de leur difficulté : la base dont on dispose pour le décrire. À la fin, vous aurez fait tourner deux fois le même algorithme d'attaque sur le même réseau, et obtenu deux réponses dont une seule est bonne.

Un réseau, c'est une grille engendrée

Soit b1,,bnb_1, \dots, b_n des vecteurs linéairement indépendants de Rm\mathbb{R}^m. Le réseau qu'ils engendrent est l'ensemble de leurs combinaisons à coefficients entiers :

L(B)={i=1nxibi  |  xiZ}\mathcal{L}(B) = \left\{ \sum_{i=1}^{n} x_i \, b_i \;\middle|\; x_i \in \mathbb{Z} \right\}

Tout tient dans le Z\mathbb{Z}. Avec des coefficients réels, la même famille engendrerait un sous-espace vectoriel — un plan continu, où « le point le plus proche » n'a aucun sens puisque tous les points y sont. Avec des coefficients entiers, on obtient une grille de points isolés, et les questions de proximité redeviennent des questions.

Prenons un exemple qui servira tout le chapitre, dans le plan :

b1=(1,2)b2=(2,1)b_1 = (1, 2) \qquad b_2 = (2, -1)

Quels points obtient-on ? Un point (x,y)=ab1+bb2(x, y) = a\,b_1 + b\,b_2 vérifie x=a+2bx = a + 2b et y=2aby = 2a - b, d'où 2xy=5b2x - y = 5b et x+2y=5ax + 2y = 5a. Les deux coefficients aa et bb sont entiers exactement quand x+2yx + 2y est multiple de 5. Ce réseau est donc

L={(x,y)Z2  :  x+2y0mod5}\mathcal{L} = \{\, (x,y) \in \mathbb{Z}^2 \;:\; x + 2y \equiv 0 \bmod 5 \,\}

C'est un sous-ensemble de Z2\mathbb{Z}^2 qui en retient un point sur cinq. Retenez ce chiffre 5, il va revenir.

Le même réseau, une infinité de bases

Un réseau n'a pas une base, il en a une infinité. Si UU est une matrice à coefficients entiers de déterminant ±1\pm 1 — on la dit unimodulaire — alors BUBU engendre exactement le même réseau que BB. La condition sur le déterminant est ce qui rend U1U^{-1} entière elle aussi : sans elle, on obtiendrait un sous-réseau plus grossier, pas le même réseau.

Appliquons-la à notre exemple, avec UU de déterminant 1-1 :

b1=1b1+2b2=(5,0)b2=2b1+3b2=(8,1)b_1' = 1 \cdot b_1 + 2 \cdot b_2 = (5, 0) \qquad b_2' = 2 \cdot b_1 + 3 \cdot b_2 = (8, 1)
b1\|b_1\|b2\|b_2\|angledéterminant
base courte (1,2),(2,1)(1,2), (2,-1)2,242,2490,0°5
base longue (5,0),(8,1)(5,0), (8,1)5,008,067,1°5

Les deux dernières colonnes disent l'essentiel. L'angle s'effondre — les vecteurs de la seconde base sont presque alignés — mais le déterminant ne bouge pas. C'est une propriété du réseau, pas de la base : géométriquement, c'est l'aire du parallélogramme construit sur les vecteurs de base, et cette aire vaut 5 dans les deux cas. Un point de réseau pour cinq unités d'aire, quelle que soit la manière de le décrire.

On note ce déterminant det(L)\det(\mathcal{L}), et il mesure la densité du réseau. Un petit déterminant, beaucoup de points ; un grand déterminant, des points clairsemés.

Quiz · 1 question

On remplace une base d'un réseau par une autre base du même réseau. Qu'est-ce qui change ?

  • L'ensemble des points du réseaul'objet lui-même
  • La longueur et l'angle des vecteurs de basela description
  • Le déterminant du réseaul'aire fondamentale

Réponse : Changer de base ne change ni les points — c'est le même réseau par définition — ni le déterminant, invariant parce que la matrice de passage a un déterminant de ±1. Seule la FORME de la description change : longueurs et angles. Toute la cryptographie à base de réseaux tient dans cet écart entre un objet invariant et des descriptions inégales.

Trois problèmes, une seule difficulté

Sur un réseau, trois problèmes servent de fondation.

SVP (Shortest Vector Problem) : trouver le vecteur non nul le plus court. Sa longueur est notée λ1(L)\lambda_1(\mathcal{L}). Dans notre exemple, λ1=52,24\lambda_1 = \sqrt{5} \approx 2{,}24.

CVP (Closest Vector Problem) : étant donné un point tt du plan qui n'est pas dans le réseau, trouver le point du réseau le plus proche. C'est le problème central du chapitre.

SIVP (Shortest Independent Vectors Problem) : trouver nn vecteurs indépendants tous courts. C'est la version dont Regev tire, au chapitre suivant, la réduction pire cas vers cas moyen de LWE.

Aucun de ces problèmes n'est utilisé sous sa forme exacte. La cryptographie emploie leurs versions approchées : trouver un vecteur au plus γ\gamma fois plus long que l'optimum, avec γ\gamma polynomial en nn. Cette nuance mérite d'être dite franchement à des étudiants, car elle est souvent escamotée.

SVP exact est NP-difficile sous réductions randomisées, CVP exact est NP-difficile. Mais la sécurité des schémas à réseaux ne repose pas sur cette NP-difficulté. Pour γn\gamma \geq \sqrt{n}, le problème approché tombe dans NPcoNP\mathsf{NP} \cap \mathsf{coNP} : il ne peut donc pas être NP-difficile sans effondrer la hiérarchie polynomiale. Ce sur quoi on s'appuie, c'est l'absence d'algorithme efficace connu — classique ou quantique — pour ces facteurs d'approximation, après quarante ans de tentatives. C'est une hypothèse solide, pas un théorème.

Une borne utile pour situer les ordres de grandeur : le théorème de Minkowski garantit λ1(L)ndet(L)1/n\lambda_1(\mathcal{L}) \leq \sqrt{n} \cdot \det(\mathcal{L})^{1/n}. Ici, 253,16\sqrt{2} \cdot \sqrt{5} \approx 3{,}16, et le vrai λ1\lambda_1 vaut 2,24 — la borne est respectée, et lâche. Pour les grandes dimensions on lui préfère l'heuristique gaussienne, λ1n/2πedet(L)1/n\lambda_1 \approx \sqrt{n / 2\pi e} \cdot \det(\mathcal{L})^{1/n}, qui n'a de sens qu'asymptotiquement.

L'arrondi de Babai

Voici l'algorithme le plus simple qui attaque CVP. Il tient en trois lignes.

Fonction Babai(B, t) : point du réseau    c ← coordonnées de t dans la base B      // n nombres RÉELS    Pour i de 1 à n Faire        a[i] ← Arrondi(c[i])                 // n entiers    FinPour    Retourner a[1]·b[1] + … + a[n]·b[n]FinFonction

On décompose la cible dans la base, on arrondit chaque coordonnée, on recombine. Le résultat est nécessairement un point du réseau, puisque ses coordonnées sont entières.

La lecture géométrique est plus parlante que le code. Arrondir toutes les coordonnées à l'entier le plus proche revient à demander : dans quelle cellule fondamentale la cible tombe-t-elle ? Or cette cellule est un parallélogramme dessiné par la base — un carré si la base est orthogonale, un fuseau très allongé si elle ne l'est pas. Une cellule presque carrée entoure son point ; un fuseau peut contenir un point qui en est très éloigné.

Animation · 7 étapes

Un réseau, deux bases, un même point à approcher

  1. Les points du réseauToutes les combinaisons à coefficients ENTIERS de deux vecteurs, et rien d'autre. La grille est infinie ; la fenêtre n'en montre qu'un morceau.
  2. Une base courteDeux vecteurs de longueur 2,24 et 2,24, à 90,0° l'un de l'autre. L'aire du parallélogramme qu'ils dessinent est le déterminant du réseau : 5.
  3. Une base longue du même réseauLongueurs 5,00 et 8,06, écart de 7,1° seulement. Ces vecteurs engendrent pourtant exactement les mêmes points, et leur parallélogramme a la même aire : 5.
  4. Le point à approcherLa cible (5,5 ; 2,6) n'est pas un point du réseau. Le problème CVP demande le point du réseau qui en est le plus proche.
  5. Arrondi dans la base courteCoordonnées de la cible dans cette base : (2,14 ; 1,68). Arrondies, elles donnent (2 ; 2), donc le point (6 ; 2), à 0,78 de la cible. La cellule est presque carrée : la cible tombe dans celle du bon point.
  6. Arrondi dans la base longueMême cible, même algorithme : (-3,06 ; 2,60) s'arrondit en (-3 ; 3), donc le point (9 ; 3) — à 3,52, soit 4,5 fois plus loin. La cellule est un fuseau : elle contient la cible tout en s'étirant hors du cadre.
  7. Ce que la base décideLe réseau n'a pas changé, l'algorithme non plus. Seule la forme de la base change la réponse. Une base courte est une information qu'on peut garder secrète tout en publiant une base longue du même réseau : c'est l'intuition de la trappe.

Déroulons les nombres pour la cible t=(5,5;2,6)t = (5{,}5\,;\,2{,}6), qui n'est pas dans le réseau.

Base employéeCoordonnées de ttArrondiPoint renvoyéDistance
courte(2,14 ; 1,68)(2 ; 2)(6 ; 2)0,78
longue(−3,06 ; 2,60)(−3 ; 3)(9 ; 3)3,52

Le même réseau, le même algorithme, la même cible, et un résultat 4,5 fois pire. Les deux points renvoyés appartiennent bien au réseau — 6+2×2=106 + 2\times2 = 10 et 9+2×3=159 + 2\times3 = 15 sont tous deux multiples de 5. Mais l'un est le plus proche et l'autre non.

Deux précisions d'honnêteté. D'abord, la base courte est ici exactement orthogonale — (1,2)(2,1)=0(1,2) \cdot (2,-1) = 0 — ce qui rend l'arrondi non seulement bon mais exact : le point renvoyé est le plus proche, et l'exercice ci-dessous le confirme par force brute. Une base seulement réduite, sans être orthogonale, donne un bon résultat, pas toujours l'optimal. Ensuite, l'écart n'est pas systématique : pour beaucoup de cibles, la base longue tombe juste par chance. Ce qui compte est qu'elle ne le garantit pas.

On tient là l'intuition de la trappe : le réseau est public, une base longue peut être publiée sans rien révéler, et la base courte du même réseau constitue le secret qui rend CVP facile. Attention toutefois — et c'est un point que la suite du cours corrigera — les schémas normalisés ne procèdent pas ainsi. GGH et NTRUSign publiaient littéralement une mauvaise base, et tous deux ont été cassés : les signatures fuyaient de l'information sur la base secrète. ML-KEM place son secret ailleurs, dans le bruit de LWE. L'intuition « même objet, deux descriptions, une seule utilisable » reste juste ; sa mise en œuvre naïve, non.

Quiz · 1 question

Pourquoi l'arrondi de Babai se trompe-t-il avec la base longue ?

  • Parce que la base longue engendre un réseau différent
  • Parce que sa cellule fondamentale est un fuseau qui s'étire loin du point qu'elle entoure
  • Parce que l'arrondi accumule des erreurs de calcul flottant

Réponse : Les deux bases engendrent le même réseau, et le calcul est exact — 2,14 et −3,06 s'arrondissent sans ambiguïté. L'algorithme renvoie le point dont la cellule contient la cible ; quand cette cellule est un fuseau très allongé, elle contient des points du plan situés très loin de son centre. La géométrie de la base, rien d'autre.

Réduire une base : Gauss, LLL, BKZ

Si une bonne base rend CVP facile, la question devient : peut-on fabriquer une bonne base à partir d'une mauvaise ? C'est le problème de la réduction, et c'est là que se joue réellement la sécurité.

En dimension 2, la réponse est oui, et l'algorithme est élémentaire. La réduction de Gauss (ou de Lagrange) répète deux opérations : garder le plus court des deux vecteurs en premier, puis raccourcir l'autre en lui retranchant le multiple entier du premier qui le rapproche le plus de l'origine, soit b2b2μb1b_2 \leftarrow b_2 - \lfloor \mu \rceil \, b_1 avec μ=b1,b2/b12\mu = \langle b_1, b_2 \rangle / \|b_1\|^2.

Sur notre base longue :

Tourb1b_1b2b_2μ\muarrondi
départ(5, 0)(8, 1)40/25 = 1,62
1(−2, 1)(5, 0)−10/5 = −2−2
2(−2, 1)(1, 2)arrêt

Deux tours suffisent : on retombe sur (2,1)(-2, 1) et (1,2)(1, 2), c'est-à-dire la base courte au signe et à l'ordre près. En dimension 2, une mauvaise base ne protège rien. La sécurité des réseaux ne vient pas du principe, elle vient de la dimension — ML-KEM-768 travaille sur 768 inconnues, et le réseau attaqué en compte davantage encore.

En dimension quelconque, l'algorithme de référence est LLL (Lenstra, Lenstra, Lovász, 1982). Il s'exécute en temps polynomial et garantit

b12(n1)/2λ1(L)\|b_1\| \leq 2^{(n-1)/2} \cdot \lambda_1(\mathcal{L})

Une garantie exponentielle en nn produite par un algorithme polynomial : en dimension 768, le facteur vaut 23842^{384}, ce qui ne garantit rien du tout. En pratique LLL fait beaucoup mieux que sa borne, et c'est cet écart entre la garantie prouvée et le comportement observé qui oblige toute l'estimation de sécurité à devenir expérimentale.

Pour mesurer ce comportement, on utilise le facteur de Hermite δ0\delta_0, défini par

b1δ0ndet(L)1/n\|b_1\| \approx \delta_0^{\,n} \cdot \det(\mathcal{L})^{1/n}

Plus δ0\delta_0 est proche de 1, meilleure est la réduction. LLL plafonne autour de δ01,02\delta_0 \approx 1{,}02 quelle que soit la dimension, et cela ne suffit pas à casser les schémas normalisés. D'où BKZ, qui généralise LLL : au lieu de travailler sur des paires de vecteurs, il résout SVP exactement sur des blocs de taille β\beta et propage. LLL est le cas β=2\beta = 2. Augmenter β\beta améliore δ0\delta_0 — au prix d'un oracle SVP en dimension β\beta, dont le coût est exponentiel.

Estimer une sécurité qu'on ne sait pas prouver

Le modèle employé par les spécifications de ML-KEM et ML-DSA s'appelle core-SVP. Il compte le coût d'un seul appel à l'oracle SVP en dimension β\beta, en ignorant délibérément le nombre d'appels et les facteurs polynomiaux. Les meilleurs cribles connus donnent 20,292β2^{0{,}292\beta} opérations en classique et 20,265β2^{0{,}265\beta} en quantique.

β\betaδ0\delta_0coût classiquecoût quantique
2 (LLL)≈ 1,0219 (mesuré)polynomialpolynomial
1001,00932292^{29}2272^{27}
2001,00632582^{58}2532^{53}
4001,004021172^{117}21062^{106}
6001,003021752^{175}21592^{159}
8731,002322552^{255}22312^{231}

Attaquer ML-KEM-768 demande, d'après l'analyse publiée, un β\beta de l'ordre de 620, soit environ 21812^{181} opérations classiques ; ML-KEM-1024 se situe vers β=873\beta = 873. Ces chiffres sont des estimations dans un modèle de coût, pas des théorèmes, et il faut le dire aux étudiants sans détour. Le modèle core-SVP est conservateur sur un point — il n'attribue qu'un seul appel d'oracle là où l'attaque réelle en fait beaucoup — et optimiste sur un autre, puisqu'il ignore le coût mémoire du criblage, qui est colossal. Les niveaux de sécurité annoncés bougent quand la cryptanalyse progresse, et ils ont déjà bougé.

Deux remarques pour finir. La colonne quantique n'apporte qu'un gain marginal, 0,265 contre 0,292 : contrairement à Shor sur RSA, aucun effondrement n'est attendu ici, et c'est précisément pour cela que les réseaux ont été retenus. Et la dernière colonne explique le choix des jeux de paramètres : les niveaux 1, 3 et 5 du NIST se lisent directement sur ces exposants.

À vous

Le fil le plus formateur sur les réseaux est d'implémenter puis de casser. Complétez l'arrondi de Babai, puis regardez les deux bases répondre différemment à la même question. La force brute qui suit vérifie laquelle a raison — un luxe permis par la dimension 2, et la raison même pour laquelle les schémas réels travaillent en dimension plusieurs centaines.

Exercice de code

Complétez l'arrondi de Babai, puis comparez ce que les deux bases du même réseau répondent à la même question.

Point de départ

// Deux bases du MÊME réseau : les points {(x, y) entiers | x + 2y ≡ 0 mod 5}.
const COURTE = { b1: [1, 2], b2: [2, -1] };   // orthogonale, vecteurs de norme √5
const LONGUE = { b1: [5, 0], b2: [8, 1] };    // presque alignés, 7,1° d'écart
const CIBLE = [5.5, 2.6];                     // pas un point du réseau

const det = (B) => B.b1[0] * B.b2[1] - B.b1[1] * B.b2[0];

// Coordonnées RÉELLES de t dans la base : t = c1·b1 + c2·b2.
function coordonnees(B, t) {
  const d = det(B);
  return [
    (t[0] * B.b2[1] - t[1] * B.b2[0]) / d,
    (B.b1[0] * t[1] - B.b1[1] * t[0]) / d,
  ];
}

const combiner = (B, c1, c2) => [
  c1 * B.b1[0] + c2 * B.b2[0],
  c1 * B.b1[1] + c2 * B.b2[1],
];

function babai(B, t) {
  const [c1, c2] = coordonnees(B, t);

  // À COMPLÉTER — un point du réseau a des coordonnées ENTIÈRES dans la base.
  // En l'état, la fonction renvoie la cible elle-même : distance nulle, mais
  // le « point » trouvé n'appartient pas au réseau.
  const a1 = c1;
  const a2 = c2;

  const point = combiner(B, a1, a2);
  return { c1, c2, a1, a2, point, d: Math.hypot(point[0] - t[0], point[1] - t[1]) };
}

// Un point est dans le réseau si ses coordonnées dans une base sont entières.
function dansLeReseau(p) {
  const [c1, c2] = coordonnees(COURTE, p);
  return Number.isInteger(Math.round(c1 * 1e9) / 1e9) &&
         Number.isInteger(Math.round(c2 * 1e9) / 1e9);
}

const n2 = (x) => x.toFixed(2);

for (const [nom, B] of [["courte", COURTE], ["longue", LONGUE]]) {
  const r = babai(B, CIBLE);
  console.log(
    `base ${nom.padEnd(7)} coords (${n2(r.c1)}, ${n2(r.c2)})` +
    ` → (${r.a1}, ${r.a2}) → point (${r.point}) ` +
    ` d = ${n2(r.d)}  dans le réseau : ${dansLeReseau(r.point)}`
  );
}

// Référence : le VRAI plus proche, cherché par force brute. Possible ici
// parce que la dimension est 2 ; c'est exactement ce qui devient hors de
// portée en dimension 768.
let meilleur = null;
for (let a = -20; a <= 20; a++) {
  for (let b = -20; b <= 20; b++) {
    const p = combiner(COURTE, a, b);
    const d = Math.hypot(p[0] - CIBLE[0], p[1] - CIBLE[1]);
    if (!meilleur || d < meilleur.d) meilleur = { p, d };
  }
}
console.log(`force brute   → point (${meilleur.p})   d = ${n2(meilleur.d)}`);

// Fourni : la réduction de Gauss, qui transforme une base du plan en la plus
// courte possible. Lancez-la sur LONGUE et regardez ce qu'elle rend.
function gauss(B) {
  let u = B.b1.slice();
  let v = B.b2.slice();
  const norme = (w) => Math.hypot(w[0], w[1]);
  for (let k = 0; k < 50; k++) {
    if (norme(u) > norme(v)) [u, v] = [v, u];
    const mu = Math.round((u[0] * v[0] + u[1] * v[1]) / (u[0] * u[0] + u[1] * u[1]));
    const w = [v[0] - mu * u[0], v[1] - mu * u[1]];
    if (norme(w) >= norme(u)) return { b1: u, b2: w };
    v = w;
  }
  return { b1: u, b2: v };
}
console.log("Gauss(LONGUE) =", JSON.stringify(gauss(LONGUE)));

Solution

const COURTE = { b1: [1, 2], b2: [2, -1] };
const LONGUE = { b1: [5, 0], b2: [8, 1] };
const CIBLE = [5.5, 2.6];

const det = (B) => B.b1[0] * B.b2[1] - B.b1[1] * B.b2[0];

function coordonnees(B, t) {
  const d = det(B);
  return [
    (t[0] * B.b2[1] - t[1] * B.b2[0]) / d,
    (B.b1[0] * t[1] - B.b1[1] * t[0]) / d,
  ];
}

const combiner = (B, c1, c2) => [
  c1 * B.b1[0] + c2 * B.b2[0],
  c1 * B.b1[1] + c2 * B.b2[1],
];

function babai(B, t) {
  const [c1, c2] = coordonnees(B, t);

  // L'arrondi, c'est TOUT l'algorithme. La difficulté du problème ne tient
  // pas à cette ligne mais à la base dans laquelle elle est appliquée.
  const a1 = Math.round(c1);
  const a2 = Math.round(c2);

  const point = combiner(B, a1, a2);
  return { c1, c2, a1, a2, point, d: Math.hypot(point[0] - t[0], point[1] - t[1]) };
}

function dansLeReseau(p) {
  const [c1, c2] = coordonnees(COURTE, p);
  return Number.isInteger(Math.round(c1 * 1e9) / 1e9) &&
         Number.isInteger(Math.round(c2 * 1e9) / 1e9);
}

const n2 = (x) => x.toFixed(2);

for (const [nom, B] of [["courte", COURTE], ["longue", LONGUE]]) {
  const r = babai(B, CIBLE);
  console.log(
    `base ${nom.padEnd(7)} coords (${n2(r.c1)}, ${n2(r.c2)})` +
    ` → (${r.a1}, ${r.a2}) → point (${r.point}) ` +
    ` d = ${n2(r.d)}  dans le réseau : ${dansLeReseau(r.point)}`
  );
}

let meilleur = null;
for (let a = -20; a <= 20; a++) {
  for (let b = -20; b <= 20; b++) {
    const p = combiner(COURTE, a, b);
    const d = Math.hypot(p[0] - CIBLE[0], p[1] - CIBLE[1]);
    if (!meilleur || d < meilleur.d) meilleur = { p, d };
  }
}
console.log(`force brute   → point (${meilleur.p})   d = ${n2(meilleur.d)}`);

function gauss(B) {
  let u = B.b1.slice();
  let v = B.b2.slice();
  const norme = (w) => Math.hypot(w[0], w[1]);
  for (let k = 0; k < 50; k++) {
    if (norme(u) > norme(v)) [u, v] = [v, u];
    const mu = Math.round((u[0] * v[0] + u[1] * v[1]) / (u[0] * u[0] + u[1] * u[1]));
    const w = [v[0] - mu * u[0], v[1] - mu * u[1]];
    if (norme(w) >= norme(u)) return { b1: u, b2: w };
    v = w;
  }
  return { b1: u, b2: v };
}
console.log("Gauss(LONGUE) =", JSON.stringify(gauss(LONGUE)));

// Trois choses à lire dans la sortie.
//
// 1. Les deux bases renvoient des points DIFFÉRENTS : (6, 2) à 0,78 de la
//    cible, (9, 3) à 3,52. Même réseau, même algorithme, même cible.
// 2. La force brute confirme (6, 2) : avec une base orthogonale, l'arrondi
//    ne se contente pas d'être bon, il est exact.
// 3. Gauss ramène LONGUE à { b1: [-2, 1], b2: [1, 2] }, soit COURTE au signe
//    et à l'ordre près. En dimension 2, la mauvaise base ne protège rien —
//    la sécurité vient de la dimension, pas du principe.

Ce que la suite en fait

Un réseau tel qu'il vient d'être décrit ne fait pas encore un cryptosystème : il manque le moyen d'engendrer, à la demande, une instance dont on connaît le secret sans que personne d'autre ne puisse le retrouver. C'est le rôle de LWE, au chapitre suivant, qui reformule tout ce chapitre en une seule phrase — résoudre un système linéaire bruité — et l'accompagne d'une réduction pire cas vers cas moyen que la géométrie seule ne fournissait pas.

Ce que vous emportez d'ici : le réseau est l'objet, la base est la description, et toute la construction repose sur l'écart entre les deux.

À retenir

Flashcards · 3 cartes

Qu'est-ce qui est invariant quand on change la base d'un réseau ?
L'ensemble des points et le déterminant — l'aire de la cellule fondamentale. La matrice de passage étant unimodulaire (déterminant ±1), elle préserve le volume. Seuls les longueurs et les angles des vecteurs de base changent, et c'est là-dessus que repose la trappe.
Pourquoi la sécurité des réseaux ne repose-t-elle PAS sur la NP-difficulté de SVP ?
Parce que la cryptographie emploie les versions approchées, avec un facteur γ polynomial en n. Pour γ ≥ √n, le problème est dans NP ∩ coNP, donc pas NP-difficile sauf effondrement de la hiérarchie polynomiale. La confiance vient de l'absence d'algorithme efficace connu, pas d'un théorème.
Que mesure le facteur de Hermite δ₀, et pourquoi BKZ-β le fait-il baisser ?
δ₀ mesure la qualité d'une réduction : ‖b₁‖ ≈ δ₀ⁿ · det(L)^(1/n), donc plus δ₀ est proche de 1, plus le vecteur obtenu est court. BKZ résout SVP exactement sur des blocs de taille β : augmenter β améliore δ₀ au prix d'un coût exponentiel, environ 2^(0,292β) en classique.

Chapitre 2 · 5 h

LWE et ses variantes

Résoudre un système linéaire bruité : réduction pire cas/cas moyen de Regev, Ring-LWE et Module-LWE, SIS et signatures.

Le chapitre 4 a laissé une lacune, et il faut la nommer avant de la combler. Un réseau euclidien est un bel objet, mais rien n'y indique comment engendrer à la demande une instance difficile dont on connaît le secret. La trappe « base courte contre base longue » y ressemblait, mais nous avons vu que GGH et NTRUSign, qui la mettaient en œuvre littéralement, ont été cassés.

LWE répond exactement à cette question, et il le fait dans un langage que tout étudiant d'un cours d'algèbre linéaire comprend en une phrase.

Résoudre un système linéaire, mais bruité

Voici tout LWE en une image. On vous donne un système d'équations linéaires modulaires :

4s1+2s2=141s1+5s2=8(mod17)\begin{aligned} 4 s_1 + 2 s_2 &= 14 \\ 1 s_1 + 5 s_2 &= 8 \end{aligned} \pmod{17}

Vous le résolvez en trente secondes par élimination. Maintenant, on ajoute à chaque second membre une erreur de ±1\pm 1 — une erreur minuscule, sur des valeurs comprises entre 0 et 16. Le système devient :

4s1+2s2=151s1+5s2=7(mod17)\begin{aligned} 4 s_1 + 2 s_2 &= 15 \\ 1 s_1 + 5 s_2 &= 7 \end{aligned} \pmod{17}

Appliquez la même élimination et regardez ce qui se passe.

Animation · 7 étapes

Le même Gauss, sans bruit puis avec : q = 17, secret s = (3, 1)

  1. Sans bruit — les données4×3 + 2×1 = 14 et 1×3 + 5×1 = 8. Deux équations exactes, deux inconnues : c'est un exercice de première année.
  2. Sans bruit — éliminationL1 − 4·L2 supprime s1 : il reste −18·s2 = 14 − 32 = −18.
  3. Sans bruit — le secret tombes2 = 1, puis s1 = 3. Deux équations ont suffi. Un système linéaire modulaire ne protège rien du tout : c'est le point de départ, pas la solution.
  4. Avec bruit — les mêmes équations, décaléesOn ajoute e1 = +1 et e2 = −1 : b1 passe de 14 à 15, b2 de 8 à 7. Deux unités d'écart sur des valeurs comprises entre 0 et 16.
  5. Avec bruit — l'élimination amplifieLa même combinaison donne −18·s2 = 15 − 28 = −13. Le coefficient 4 utilisé pour éliminer s1 a MULTIPLIÉ l'erreur de la seconde équation : le bruit cumulé vaut 5, pas 1.
  6. Avec bruit — le résultat est absurde−s2 ≡ −13 donne s2 ≡ 13 mod 17. La vraie valeur est 1. Un bruit de ±1 sur deux équations a produit une erreur de 12 sur le résultat, dans un modulo de 17 : toute l'information a disparu.
  7. Pourquoi cela tient en grande dimensionEn dimension n, éliminer n−1 inconnues demande des coefficients de l'ordre de q. Le bruit final dépasse q/2 et le résultat est uniformément distribué : il ne contient plus rien. Résoudre LWE demande donc de deviner un secret court AVANT d'éliminer — ce qui ramène exactement au problème de réseau du chapitre 4.

Le résultat n'est pas « approximativement juste », il est absurde : s2=13s_2 = 13 au lieu de 1. La raison est visible à l'étape 5 de l'animation. Pour éliminer s1s_1, on a multiplié la seconde équation par 4 — et ce coefficient a multiplié son erreur par 4. Le bruit cumulé vaut 5, pas 1, dans un modulo de 17.

Généralisez à la dimension nn : éliminer n1n-1 inconnues demande des coefficients de l'ordre de qq, et le bruit final dépasse q/2q/2. Le résultat est alors uniformément distribué sur Zq\mathbb{Z}_q : il ne contient plus aucune information. C'est là toute l'astuce, et elle tient en une phrase — un bruit infime détruit toute méthode algébrique.

Quiz · 1 question

Pourquoi un bruit de ±1 suffit-il à détruire l'élimination de Gauss modulo q ?

  • Parce que les erreurs s'additionnent linéairement et finissent par dépasser 1
  • Parce que les coefficients d'élimination, de l'ordre de q, MULTIPLIENT les erreurs
  • Parce que la division modulaire n'est pas définie quand il y a du bruit

Réponse : L'élimination ne fait pas qu'additionner : elle combine les lignes avec des coefficients. Multiplier une équation par 4 multiplie son erreur par 4. En dimension n, les coefficients atteignent l'ordre de q, donc le bruit cumulé dépasse q/2 et le résidu devient uniforme sur Z_q. La division modulaire, elle, reste parfaitement définie — c'est bien l'amplification qui tue.

La définition, et sa version décisionnelle

Fixons le vocabulaire. Soient nn la dimension, qq le module et χ\chi une distribution de bruit concentrée près de zéro. Un échantillon LWE de secret sZqns \in \mathbb{Z}_q^n est un couple

(a,  b)avecaZqn uniforme,b=a,s+emodq,eχ(a, \; b) \quad \text{avec} \quad a \leftarrow \mathbb{Z}_q^n \text{ uniforme}, \quad b = \langle a, s\rangle + e \bmod q, \quad e \leftarrow \chi

Le problème de recherche demande de retrouver ss à partir de mm échantillons. Le problème décisionnel demande seulement de distinguer une suite d'échantillons LWE d'une suite de couples uniformes. Ces deux problèmes sont équivalents pour les paramètres usuels, et c'est le décisionnel qui sert dans les preuves : il donne directement l'indistinguabilité dont les jeux IND-CPA ont besoin.

Deux paramètres gouvernent la difficulté : la dimension nn et le rapport bruit sur module α=σ/q\alpha = \sigma / q. Un bruit trop faible rend le problème facile ; un bruit trop fort rend le déchiffrement impossible. Tout le dimensionnement de ML-KEM tient dans cet équilibre.

La réduction de Regev

Voici ce qui distingue LWE de toutes les hypothèses classiques, et il faut l'énoncer précisément parce que c'est souvent déformé.

Regev a démontré en 2005 que résoudre LWE en moyenne est au moins aussi difficile que résoudre certains problèmes de réseau dans le pire cas — GapSVP et SIVP avec un facteur d'approximation O~(n/α)\tilde{O}(n/\alpha). La réduction originale est quantique ; Peikert en a donné en 2009 une version classique, au prix d'un module qq plus grand.

Ce que cela signifie, concrètement : si quelqu'un trouve un algorithme qui casse LWE sur des instances tirées au hasard — c'est-à-dire sur les clés que produit votre générateur — alors le même algorithme résout toutes les instances du problème de réseau correspondant, y compris les plus difficiles. Il n'existe donc pas de « clés faibles » à craindre : le tirage aléatoire ne peut pas tomber sur une instance accidentellement facile.

Comparez avec RSA. Personne ne sait montrer que factoriser un module tiré au hasard est aussi difficile que factoriser le pire module possible. On y croit ; on ne le démontre pas.

Deux réserves, pour rester honnête. D'abord, la réduction porte sur des facteurs d'approximation polynomiaux, pour lesquels le problème de réseau n'est ni prouvé NP-difficile ni supposé l'être — c'est la nuance du chapitre 4. Ensuite, elle est lâche : appliquée telle quelle, elle imposerait des paramètres bien plus gros que ceux de ML-KEM. Les paramètres réels sont fixés par l'estimation d'attaque du chapitre 4, pas par la réduction. Celle-ci sert de garantie structurelle, pas de règle de dimensionnement.

De LWE au réseau, et retour

La boucle se referme ici. À partir de mm échantillons (A,b)(A, b), on construit le réseau qq-aire

Λ={vZm  :  vAx(modq) pour un x}\Lambda = \{\, v \in \mathbb{Z}^m \;:\; v \equiv A x \pmod q \text{ pour un } x \,\}

Le vecteur bb est proche de ce réseau — à distance e\|e\|, qui est petite. Résoudre LWE, c'est donc résoudre CVP sur Λ\Lambda avec la promesse que la cible est anormalement proche. L'attaque primale plonge le tout dans un réseau où le secret devient un vecteur unique et court, et lance BKZ ; l'attaque duale cherche des vecteurs courts du réseau orthogonal pour distinguer. Dans les deux cas, le coût est celui du chapitre 4 : 20,292β2^{0{,}292\beta}.

Autrement dit, LWE n'est pas un nouveau problème difficile. C'est une présentation du problème de réseau qui rend l'engendrement d'instances trivial : tirer AA au hasard, tirer ss et ee petits, publier (A,As+e)(A, As+e).

Quiz · 1 question

Que garantit exactement la réduction de Regev ?

  • Que LWE est NP-difficile
  • Que casser LWE sur des instances aléatoires implique résoudre un problème de réseau dans le PIRE cas
  • Que les paramètres de ML-KEM sont prouvés sûrs

Réponse : C'est une réduction du pire cas vers le cas moyen : elle interdit l'existence de clés accidentellement faibles. Elle ne dit rien de la NP-difficulté — le facteur d'approximation visé est polynomial, régime pour lequel la NP-difficulté n'est ni établie ni attendue. Et elle est trop lâche pour fixer des paramètres : ceux de ML-KEM viennent de l'estimation de coût de BKZ, pas de la réduction.

Ring-LWE et Module-LWE : ce qu'on gagne, ce qu'on risque

LWE « plein » a un défaut rédhibitoire : la matrice AA fait m×nm \times n éléments. Pour des paramètres réalistes, la clé publique se compte en mégaoctets. C'est inutilisable.

Ring-LWE remplace les vecteurs par des éléments de l'anneau RqR_q du chapitre 3. Un seul polynôme aa remplace toute une matrice, parce que la multiplication dans l'anneau encode implicitement une matrice circulante. On gagne un facteur nn en taille de clé et, grâce à la NTT, un facteur n/lognn/\log n en temps de calcul. Le gain est spectaculaire.

Il n'est pas gratuit. Cette matrice implicite n'est pas quelconque : elle a une structure algébrique, et une structure est toujours une prise potentielle. On sait aujourd'hui que certains problèmes sur les réseaux idéaux — pas Ring-LWE lui-même, mais des cousins proches — admettent des algorithmes quantiques meilleurs que dans le cas général pour certains facteurs d'approximation. Ces résultats ne cassent pas Ring-LWE. Ils démontrent que la structure n'est pas neutre, et qu'elle mérite une prudence que le cas non structuré n'exige pas.

Module-LWE est le compromis, et c'est celui que le NIST a normalisé. On travaille avec des vecteurs de dimension kk dont les entrées sont des éléments de l'anneau. Pour k=1k = 1 on retrouve Ring-LWE ; pour k=nk = n on retrouve LWE plein. ML-KEM prend k=2,3k = 2, 3 ou 44 avec n=256n = 256.

Le bénéfice est double, et le second est le plus intéressant en pratique. D'une part, on réduit la structure algébrique en jouant sur kk plutôt que sur nn. D'autre part, changer de niveau de sécurité ne change que kk : l'anneau reste le même, donc la NTT, les tables et le code d'arithmétique sont identiques pour les trois jeux de paramètres. Une implémentation, trois niveaux.

SIS, le problème dual

LWE sert au chiffrement. Les signatures ont besoin de son pendant, SISShort Integer Solution.

Étant donné AZqn×mA \in \mathbb{Z}_q^{n \times m} uniforme, trouver z0z \neq 0 court tel que

Az0(modq)A z \equiv 0 \pmod q

Il existe une infinité de solutions dès que m>nm > n ; la difficulté est d'en trouver une petite. Ajtai a démontré dès 1996 la difficulté pire cas vers cas moyen de SIS, ce qui en fait historiquement la première construction de ce type.

L'intuition de la dualité : LWE cache un secret dans du bruit, SIS demande d'exhiber un objet court. Le chiffrement a besoin de cacher, la signature a besoin de prouver qu'on sait produire quelque chose de court sans révéler comment. Le chapitre 7 montre comment ML-DSA convertit cela en signature.

À vous

Exercice de code

Complétez le score de la force brute, puis lisez la dernière colonne : c'est elle qui dit pourquoi la dimension 768 protège et pas la dimension 2.

Point de départ

// LWE en miniature. q = 17, dimension n = 2, secret s = (3, 1).
// Bruit dans {-1, 0, +1}.

const q = 17;
const n = 2;
const SECRET = [3, 1];
const mod = (x) => ((x % q) + q) % q;

const produit = (a, s) => mod(a.reduce((t, ai, i) => t + ai * s[i], 0));

// Un échantillon LWE : (a, b) avec b = <a, s> + e.
function echantillon(bruit) {
  const a = Array.from({ length: n }, () => Math.floor(Math.random() * q));
  const e = bruit ? Math.floor(Math.random() * 3) - 1 : 0;
  return { a, b: mod(produit(a, SECRET) + e), e };
}

// Attaque par force brute : essayer tous les secrets, garder celui qui
// explique le mieux les échantillons.
function forceBrute(echantillons, borne) {
  let meilleur = null;
  let essais = 0;
  for (let s1 = 0; s1 < q; s1++) {
    for (let s2 = 0; s2 < q; s2++) {
      essais++;
      const s = [s1, s2];
      // À COMPLÉTER — comptez combien d'échantillons ce candidat explique,
      // c'est-à-dire pour lesquels |b - <a,s>| (centré sur 0) est ≤ borne.
      const score = 0;
      if (!meilleur || score > meilleur.score) meilleur = { s, score };
    }
  }
  return { ...meilleur, essais };
}

// Écart centré : dans Z_q, 16 est à distance 1 de 0, pas 16.
const ecart = (x) => Math.min(mod(x), q - mod(x));

const SANS = Array.from({ length: 8 }, () => echantillon(false));
const AVEC = Array.from({ length: 8 }, () => echantillon(true));

console.log("secret réel :", SECRET);
console.log("sans bruit  :", JSON.stringify(forceBrute(SANS, 0)));
console.log("avec bruit  :", JSON.stringify(forceBrute(AVEC, 1)));

// Le coût, maintenant. En dimension n, il y a q^n secrets possibles.
console.log("\ncoût de la force brute :");
for (const dim of [2, 4, 8, 256, 768]) {
  const bits = dim * Math.log2(q);
  console.log(`  n = ${String(dim).padStart(3)}  →  q^n = 2^${bits.toFixed(0)}`);
}

Solution

const q = 17;
const n = 2;
const SECRET = [3, 1];
const mod = (x) => ((x % q) + q) % q;

const produit = (a, s) => mod(a.reduce((t, ai, i) => t + ai * s[i], 0));

function echantillon(bruit) {
  const a = Array.from({ length: n }, () => Math.floor(Math.random() * q));
  const e = bruit ? Math.floor(Math.random() * 3) - 1 : 0;
  return { a, b: mod(produit(a, SECRET) + e), e };
}

const ecart = (x) => Math.min(mod(x), q - mod(x));

function forceBrute(echantillons, borne) {
  let meilleur = null;
  let essais = 0;
  for (let s1 = 0; s1 < q; s1++) {
    for (let s2 = 0; s2 < q; s2++) {
      essais++;
      const s = [s1, s2];
      // Un candidat est d'autant meilleur qu'il laisse un résidu PETIT sur
      // chaque échantillon. C'est la seule signature du bon secret : le
      // mauvais candidat produit des résidus uniformes sur tout Z_q.
      const score = echantillons.filter((ech) => ecart(ech.b - produit(ech.a, s)) <= borne).length;
      if (!meilleur || score > meilleur.score) meilleur = { s, score };
    }
  }
  return { ...meilleur, essais };
}

const SANS = Array.from({ length: 8 }, () => echantillon(false));
const AVEC = Array.from({ length: 8 }, () => echantillon(true));

console.log("secret réel :", SECRET);
console.log("sans bruit  :", JSON.stringify(forceBrute(SANS, 0)));
console.log("avec bruit  :", JSON.stringify(forceBrute(AVEC, 1)));

console.log("\ncoût de la force brute :");
for (const dim of [2, 4, 8, 256, 768]) {
  const bits = dim * Math.log2(q);
  console.log(`  n = ${String(dim).padStart(3)}  →  q^n = 2^${bits.toFixed(0)}`);
}

// Trois observations, dans l'ordre où elles comptent.
//
// 1. La force brute RETROUVE le secret, avec ou sans bruit. LWE n'est pas
//    difficile en dimension 2 — 289 candidats, c'est instantané. Le bruit
//    ne rend pas le problème insoluble, il supprime seulement les méthodes
//    ALGÉBRIQUES (l'élimination de Gauss de l'animation).
//
// 2. Le score du bon secret est 8/8 sans bruit, et 8/8 avec bruit si la
//    borne est bien choisie. Trop serrée, on rate le vrai secret ; trop
//    lâche, plusieurs candidats deviennent indistinguables. Le rapport
//    entre q et l'écart-type du bruit est LE paramètre de sécurité.
//
// 3. La dernière colonne est la vraie leçon. En dimension 768, il y a
//    2^3138 secrets possibles. La force brute est morte, l'élimination est
//    morte : il ne reste que la réduction de réseau du chapitre 4, avec le
//    coût exponentiel en β qu'on lui connaît.

À retenir

Flashcards · 3 cartes

Pourquoi le bruit de LWE détruit-il l'élimination de Gauss ?
Parce que l'élimination combine les lignes avec des coefficients qui MULTIPLIENT les erreurs. En dimension n, ces coefficients sont de l'ordre de q, le bruit cumulé dépasse q/2 et le résidu devient uniforme sur Z_q. Il ne reste aucune méthode algébrique : seule la réduction de réseau s'applique encore.
Module-LWE : que gagne-t-on par rapport à LWE plein et à Ring-LWE ?
Par rapport à LWE plein : un polynôme au lieu d'une matrice, donc des clés en kilooctets au lieu de mégaoctets, et la NTT pour la vitesse. Par rapport à Ring-LWE : moins de structure algébrique, réglable par le rang k. Et un bénéfice pratique décisif — changer de niveau de sécurité ne change que k, l'anneau et donc tout le code d'arithmétique restent identiques.
Quelle est la différence entre LWE et SIS ?
LWE cache un secret dans du bruit : retrouver s dans b = As + e. SIS demande d'exhiber un objet court : trouver z ≠ 0 petit avec Az = 0 mod q. LWE sert au chiffrement, SIS aux signatures. Tous deux disposent d'une réduction pire cas vers cas moyen — Regev 2005 pour LWE, Ajtai 1996 pour SIS.

Chapitre 3 · 5 h

ML-KEM (Kyber)

Du PKE au KEM IND-CCA2 par Fujisaki-Okamoto : compression, échantillonnage, jeux 512/768/1024, tailles et taux d'échec de déchiffrement.

ML-KEM est normalisé par la FIPS 203 et déployé aujourd'hui dans les navigateurs. C'est le schéma post-quantique que vos étudiants rencontreront le plus sûrement en production. Ce chapitre le construit brique par brique, en partant d'un objet que le chapitre précédent a rendu presque évident.

Un KEM, et pourquoi pas un chiffrement

Première clarification, souvent sautée. ML-KEM ne chiffre pas de messages. C'est un mécanisme d'encapsulation de clé — trois algorithmes :

KeyGen()(ek,dk)Encaps(ek)(c,K)Decaps(dk,c)K\begin{aligned} \mathrm{KeyGen}() &\to (ek, dk) \\ \mathrm{Encaps}(ek) &\to (c, K) \\ \mathrm{Decaps}(dk, c) &\to K \end{aligned}

Encaps ne prend aucun message en entrée. Elle produit une clé symétrique KK tirée au hasard et un chiffré cc qui permet au détenteur de dkdk de la retrouver. On chiffre ensuite les vraies données avec KK et un AEAD.

Pourquoi cette forme plutôt qu'un chiffrement à clé publique ordinaire ? Parce qu'elle est plus simple à sécuriser. Un KEM n'a pas à gérer de bourrage, ni de messages de taille variable, ni de messages choisis par l'attaquant. Sa seule obligation est que KK soit indistinguable d'une clé aléatoire. En pratique, c'est aussi la seule chose dont les protocoles ont besoin : TLS, SSH et les autres veulent une clé de session, pas un chiffrement asymétrique de charge utile.

Le chiffrement sous-jacent

Sous ML-KEM se trouve un chiffrement à clé publique, appelé K-PKE, qui est du Module-LWE à peine déguisé.

Génération. Tirer AA pseudo-aléatoire de taille k×kk \times k sur RqR_q, et deux vecteurs courts ss et ee. Publier t=As+et = As + e, garder ss.

Chiffrement de mm. Tirer rr, e1e_1, e2e_2 courts, et calculer

u=ATr+e1,v=tTr+e2+q/2mu = A^{T} r + e_1, \qquad v = t^{T} r + e_2 + \left\lceil q/2 \right\rfloor \cdot m

Déchiffrement. Calculer vsTuv - s^{T} u et arrondir chaque coefficient au multiple de q/2q/2 le plus proche.

Le calcul décisif tient en deux lignes :

vsTu=(eTr+e2sTe1)petit+q/2mv - s^{T}u = \underbrace{(e^{T}r + e_2 - s^{T}e_1)}_{\text{petit}} + \left\lceil q/2 \right\rfloor \cdot m

Le message est encodé en plaçant chaque bit soit près de 0, soit près de q/2q/2. Le terme d'erreur est petit ; tant qu'il reste sous q/4q/4, l'arrondi retombe sur le bon bit. Tout le dimensionnement de ML-KEM consiste à garder ce terme d'erreur sous q/4q/4.

La compression, et le bruit qu'elle achète

Publier uu et vv avec 12 bits par coefficient donnerait des chiffrés inutilement gros. On les compresse en jetant des bits de poids faible :

Compressd(x)=2dqxmod2d\mathrm{Compress}_d(x) = \left\lceil \frac{2^d}{q} \cdot x \right\rfloor \bmod 2^d

La décompression rend une valeur approchée, à q/2d+1q/2^{d+1} près. Cette erreur d'arrondi s'ajoute au budget de bruit — c'est pour cela que la compression n'est pas gratuite, et qu'on ne peut pas compresser autant qu'on voudrait.

Le point à faire remarquer en cours : uu et vv ne sont pas compressés au même taux. uu garde 10 ou 11 bits, vv seulement 4 ou 5. Ce n'est pas une inconséquence — vv ne transporte qu'un bit de message par coefficient, décodé en comparant à q/2q/2, donc une erreur de cent unités y est sans conséquence. Chaque terme est compressé selon ce qu'il transporte.

Quiz · 1 question

Pourquoi la compression de ML-KEM n'est-elle pas sans coût de sécurité ?

  • Parce qu'elle réduit la dimension du réseau
  • Parce que l'erreur d'arrondi s'ajoute au terme de bruit et rapproche du seuil q/4
  • Parce qu'elle rend le chiffrement déterministe

Réponse : La compression ne touche ni à la dimension ni au caractère probabiliste. Elle ajoute une erreur d'arrondi bornée par q/2^(d+1), qui vient s'ajouter au bruit LWE déjà présent. Le déchiffrement n'est correct que si le total reste sous q/4 : compresser davantage économise des octets et rapproche du seuil d'échec.

De IND-CPA à IND-CCA2 : Fujisaki-Okamoto

Le K-PKE ci-dessus est IND-CPA, et seulement IND-CPA. Il est malléable : un attaquant qui modifie un chiffré valide obtient un chiffré qui se déchiffre encore, en un message différent mais lié. Contre un attaquant capable de soumettre des chiffrés au déchiffrement, c'est fatal.

La transformation de Fujisaki-Okamoto, dans sa variante avec rejet implicite, corrige cela avec une idée simple. On ré-encapsule :

  1. déchiffrer cc pour obtenir mm' ;
  2. dériver l'aléa rr' à partir de mm' lui-même — le chiffrement devient déterministe ;
  3. rechiffrer mm' avec rr', obtenant cc' ;
  4. si c=cc' = c, renvoyer la clé ; sinon, renvoyer une clé bidon dérivée d'un secret interne.

Le test c=cc' = c prouve que cc a réellement été produit par la procédure d'encapsulation. Un chiffré fabriqué ne peut pas passer, puisque l'attaquant devrait deviner mm' à l'avance.

Le point délicat est la dernière ligne. On ne renvoie pas d'erreur : on renvoie une clé pseudo-aléatoire, fausse mais indistinguable d'une vraie. C'est le rejet implicite. Dire « échec » créerait un oracle : l'attaquant apprendrait quels chiffrés sont valides, et c'est exactement ce dont il a besoin. Le chapitre 11 en donne la preuve, et le chapitre 12 montre comment une implémentation bavarde rouvre l'oracle par le temps d'exécution.

Les trois jeux de paramètres

ML-KEM-512ML-KEM-768ML-KEM-1024
rang kk234
niveau NIST135
clé publique800 o1184 o1568 o
clé privée1632 o2400 o3168 o
chiffré768 o1088 o1568 o
secret partagé32 o32 o32 o
échec de déchiffrement21392^{-139}21642^{-164}21742^{-174}

Notez ce que la table ne contient pas : ni nn, ni qq, ni l'anneau. Ils sont identiques pour les trois niveaux. Seul kk change, avec les taux de compression. C'est le bénéfice d'implémentation du Module-LWE annoncé au chapitre précédent.

Graphique

Clé publique + chiffré, en octets

  • X25519 : 6464
  • RSA-2048 : 512512
  • ML-KEM-512 : 15681568
  • ML-KEM-768 : 22722272
  • ML-KEM-1024 : 31363136
Le total échangé dans un handshake, clé publique plus chiffré. ML-KEM-768 coûte 2272 octets contre 64 pour X25519 : un facteur 35. C'est cher, mais cela reste quelques kilooctets — la comparaison avec McEliece, au chapitre 8, remettra cette facture en perspective.

Le taux d'échec de déchiffrement

ML-KEM peut échouer. C'est déroutant pour qui vient de RSA, et c'est structurel : si le terme de bruit dépasse q/4q/4, un bit est mal décodé et la clé obtenue est fausse.

Les probabilités affichées ci-dessus — de l'ordre de 21642^{-164} — rendent l'événement inobservable : il ne se produira jamais, sur aucun parc, pendant aucune durée. Mais elles ne sont pas nulles, et cela a deux conséquences qu'il faut énoncer.

D'abord, ce taux est un paramètre de sécurité, pas seulement de fiabilité. Un attaquant capable de provoquer des échecs, en soumettant des chiffrés spécialement construits, apprendrait de l'information sur le secret — chaque échec est une inégalité satisfaite par ss. C'est l'attaque par échec de déchiffrement, et c'est pourquoi le taux doit être astronomiquement bas plutôt que simplement négligeable en pratique.

Ensuite, cela impose que la ré-encapsulation FO soit implémentée en temps constant, échec compris. Une implémentation qui met plus de temps sur un échec que sur un succès restaure exactement l'oracle qu'on cherchait à fermer.

Quiz · 1 question

Pourquoi ML-KEM renvoie-t-il une clé bidon plutôt qu'une erreur quand la ré-encapsulation échoue ?

  • Pour simplifier l'interface de programmation
  • Parce qu'une erreur constituerait un oracle indiquant à l'attaquant quels chiffrés sont valides
  • Parce que le protocole ne prévoit pas de canal d'erreur

Réponse : C'est le rejet implicite, et c'est un choix de sécurité. La clé renvoyée est dérivée d'un secret interne z et du chiffré : elle est fausse, donc la suite du protocole échouera de toute façon, mais elle est indistinguable d'une vraie clé. Signaler l'échec dirait à l'attaquant que son chiffré modifié a été détecté — précisément l'information qu'une attaque CCA exploite.

À vous

Exercice de code

Implémentez Compress et Decompress, puis vérifiez que l'erreur mesurée colle à la borne q/2^(d+1) — et comprenez pourquoi du et dv ne sont pas égaux.

Point de départ

// La compression de ML-KEM : jeter des bits de poids faible pour réduire
// le chiffré, en acceptant l'erreur que cela introduit.

const q = 3329;

// Compress_d : ramène un élément de Z_q sur d bits.
function compresser(x, d) {
  // À COMPLÉTER — arrondir (2^d / q) · x, puis réduire modulo 2^d.
  return 0;
}

// Decompress_d : retour approximatif vers Z_q.
function decompresser(y, d) {
  // À COMPLÉTER — arrondir (q / 2^d) · y.
  return 0;
}

// Écart centré dans Z_q : 3328 est à distance 1 de 0.
const ecart = (a, b) => {
  const d = Math.abs(a - b) % q;
  return Math.min(d, q - d);
};

console.log("d | bits/coef | erreur max | erreur moyenne | borne q/2^(d+1)");
for (const d of [12, 11, 10, 5, 4, 3]) {
  let max = 0, total = 0;
  for (let x = 0; x < q; x++) {
    const e = ecart(x, decompresser(compresser(x, d), d));
    max = Math.max(max, e);
    total += e;
  }
  console.log(
    `${String(d).padStart(2)} | ${String(d).padStart(9)} | ${String(max).padStart(10)}` +
    ` | ${(total / q).toFixed(2).padStart(14)} | ${(q / 2 ** (d + 1)).toFixed(1).padStart(15)}`
  );
}

// Taille du chiffré ML-KEM : 32 · (du·k + dv) octets.
console.log("\nchiffré ML-KEM selon (du, dv) :");
for (const [nom, k, du, dv] of [["512", 2, 10, 4], ["768", 3, 10, 4], ["1024", 4, 11, 5]]) {
  console.log(`  ML-KEM-${nom.padEnd(4)} k=${k} du=${du} dv=${dv}  →  ${32 * (du * k + dv)} octets`);
}

Solution

const q = 3329;

function compresser(x, d) {
  // L'arrondi est la seule chose qui se passe ici : on projette Z_q, qui a
  // 3329 éléments, sur 2^d valeurs. Toute l'information au-delà de d bits
  // est jetée volontairement.
  return Math.round((2 ** d / q) * x) % 2 ** d;
}

function decompresser(y, d) {
  return Math.round((q / 2 ** d) * y);
}

const ecart = (a, b) => {
  const d = Math.abs(a - b) % q;
  return Math.min(d, q - d);
};

console.log("d | bits/coef | erreur max | erreur moyenne | borne q/2^(d+1)");
for (const d of [12, 11, 10, 5, 4, 3]) {
  let max = 0, total = 0;
  for (let x = 0; x < q; x++) {
    const e = ecart(x, decompresser(compresser(x, d), d));
    max = Math.max(max, e);
    total += e;
  }
  console.log(
    `${String(d).padStart(2)} | ${String(d).padStart(9)} | ${String(max).padStart(10)}` +
    ` | ${(total / q).toFixed(2).padStart(14)} | ${(q / 2 ** (d + 1)).toFixed(1).padStart(15)}`
  );
}

console.log("\nchiffré ML-KEM selon (du, dv) :");
for (const [nom, k, du, dv] of [["512", 2, 10, 4], ["768", 3, 10, 4], ["1024", 4, 11, 5]]) {
  console.log(`  ML-KEM-${nom.padEnd(4)} k=${k} du=${du} dv=${dv}  →  ${32 * (du * k + dv)} octets`);
}

// Trois lectures de la table.
//
// 1. L'erreur maximale colle à la borne théorique q / 2^(d+1). Ce n'est pas
//    une coïncidence : arrondir sur 2^d niveaux découpe Z_q en intervalles
//    de largeur q/2^d, et l'erreur d'arrondi vaut au plus la moitié.
//
// 2. Passer de 12 à 10 bits par coefficient économise 17 % du chiffré et
//    coûte une erreur de l'ordre de 1,6 — dérisoire devant le bruit LWE
//    déjà présent. C'est un excellent marché, et c'est pourquoi du = 10.
//
// 3. dv = 4 sur le second terme paraît brutal (erreur ~104) et ne l'est
//    pas : ce terme ne porte qu'UN BIT de message par coefficient, décodé
//    en comparant à q/2 ≈ 1664. Une erreur de 104 reste très loin du seuil.
//    Chaque terme est compressé selon ce qu'il transporte, pas uniformément.
//
// C'est tout le raisonnement de dimensionnement de ML-KEM : la compression
// ajoute du bruit, le bruit ajouté doit rester sous le budget qui sépare
// encore du seuil de décodage, et ce budget résiduel fixe le taux d'échec
// de déchiffrement.

À retenir

Flashcards · 3 cartes

Quelle inégalité gouverne tout le dimensionnement de ML-KEM ?
Le terme d'erreur e^T·r + e2 − s^T·e1, augmenté des erreurs de compression, doit rester sous q/4 — le seuil au-delà duquel l'arrondi retombe sur le mauvais bit. Chaque octet gagné par compression consomme une part de ce budget, et ce qu'il en reste fixe le taux d'échec de déchiffrement.
Que fait exactement la transformation de Fujisaki-Okamoto ?
Elle rend le chiffrement déterministe en dérivant l'aléa du message lui-même, puis RÉ-ENCAPSULE au déchiffrement et compare au chiffré reçu. Ce test prouve que le chiffré a été produit honnêtement, ce qui fait passer de IND-CPA à IND-CCA2. En cas d'échec, elle renvoie une clé bidon déterministe — jamais une erreur.
Qu'est-ce qui change entre ML-KEM-512, 768 et 1024 ?
Le rang k du module (2, 3, 4) et les taux de compression. L'anneau, n = 256 et q = 3329 sont identiques : la NTT, les tables et toute l'arithmétique sont communes aux trois niveaux. Une seule implémentation couvre les niveaux NIST 1, 3 et 5.

Chapitre 4 · 5 h

ML-DSA (Dilithium) et Falcon

Fiat-Shamir avec avortements et rejet d'échantillonnage ; Falcon, NTRU et échantillonneur gaussien, et leurs compromis d'implémentation.

Le NIST a normalisé deux signatures à base de réseaux plutôt qu'une, et ce n'est pas une indécision. ML-DSA (FIPS 204) est le choix par défaut, robuste et simple à implémenter. Falcon produit des signatures deux à quatre fois plus courtes, au prix d'une implémentation que peu d'équipes savent écrire correctement. Ce chapitre explique le mécanisme commun, puis ce qui les sépare.

Signer avec un réseau : le piège

L'idée naturelle a été essayée, et elle a échoué. Elle mérite d'être racontée, parce que la solution ne se comprend qu'en fonction du problème.

Dans le schéma GGH, puis dans NTRUSign, signer consistait à résoudre CVP : le message donne un point du plan, la signature est le point du réseau le plus proche, calculé grâce à la bonne base secrète. Vérifier consistait à contrôler que la signature est un point du réseau proche du message — ce qui ne demande que la base publique.

Le raisonnement est correct. Il est aussi fatal. Chaque signature est un vecteur dont l'écart au message est distribué dans la cellule fondamentale de la base secrète. Collectez quelques milliers de signatures, et la forme de cette cellule apparaît. Nguyen et Regev l'ont formalisé en 2006 sous le nom d'apprentissage du parallélépipède : quelques centaines de signatures suffisaient à reconstruire la clé privée de NTRUSign.

La leçon est générale et vaut d'être martelée en cours : une signature qui dépend statistiquement du secret finit par le révéler. Le nombre de signatures qu'un attaquant peut collecter n'est pas borné.

Fiat-Shamir avec avortements

La réponse de Lyubashevsky est le paradigme de ML-DSA. On part d'un protocole d'identification à trois passes — engagement, défi, réponse — qu'on rend non interactif en calculant le défi comme un haché du message et de l'engagement : c'est la transformation de Fiat-Shamir.

w=Ay,c=H(message,w),z=y+csw = A y, \qquad c = H(\mathit{message}, w), \qquad z = y + c\,s

Le masque yy, tiré uniformément et frais à chaque signature, cache le secret ss. Mais il ne le cache pas toujours : selon les tirages, zz peut sortir de la plage atteignable sans ss, et cette sortie est corrélée au secret. C'est exactement le défaut de NTRUSign, en plus discret.

D'où l'avortement. On n'accepte zz que s'il tombe dans une fenêtre réduite, zγβ\|z\|_\infty \leq \gamma - \beta, atteignable quel que soit le secret. Sinon on jette tout — y compris le défi — et on recommence avec un nouveau masque.

Animation · 8 étapes

Signature avec avortements : on recommence jusqu'à ce que rien ne fuie

  1. Tour 1 — tirer le masquey est tiré uniformément et ne sert qu'une fois. C'est lui qui masque le secret dans la réponse : réutiliser y suffirait à révéler s par simple soustraction.
  2. Tour 1 — la réponsez = y + c·s. Comme s est petit, z reste dans le voisinage de y — mais pas toujours assez petit.
  3. Tour 1 — rejeté121 dépasse le seuil : la distribution de z dépendrait alors de s, et un attaquant qui collecte assez de signatures reconstruirait le secret. On jette TOUT, y compris c, et on recommence.
  4. Tour 2 — nouveau masqueUn y frais, indépendant du précédent. Le rejet ne coûte rien en sécurité, seulement en temps.
  5. Tour 2 — rejeté aussiEncore trop grand. Le nombre de tours suit une loi géométrique : pour ML-DSA-65, l'espérance tourne autour de 4 à 5 tours.
  6. Tour 3 — nouveau masqueTroisième tirage. Rien n'est mémorisé des tours précédents.
  7. Tour 3 — accepté97 ≤ 112 : la distribution de z est désormais indépendante de s, et la preuve de sécurité peut la simuler sans connaître le secret. C'est cela, et rien d'autre, que le rejet achète.
  8. Le coût cachéLe temps de signature est VARIABLE — trois tours ici, un seul parfois, dix parfois. Une implémentation qui laisse fuir ce nombre de tours donne à l'attaquant une information corrélée au secret : c'est le sujet du chapitre 12.

Ce que le rejet achète est précis : la loi de zz devient indépendante de ss. Le simulateur de la preuve de sécurité peut alors produire des signatures parfaitement distribuées sans connaître le secret, ce qui est exactement ce qu'exige une réduction. La signature ne fuit plus rien, parce qu'elle ne dépend statistiquement plus de rien.

Le prix est un nombre de tours aléatoire. Retenez-le : c'est un temps d'exécution variable au cœur d'une opération secrète, et le chapitre 12 en fera son miel.

Quiz · 1 question

Qu'achète exactement le rejet d'échantillonnage dans ML-DSA ?

  • Une signature plus courte
  • L'indépendance statistique entre la signature et le secret
  • La résistance aux collisions de la fonction de hachage

Réponse : Le rejet coûte du temps et ne gagne aucun octet. Ce qu'il garantit, c'est que la loi de z est identique quel que soit s : la preuve de sécurité peut alors simuler des signatures sans le secret, et un attaquant qui en collecte un million n'apprend rien. C'est la réponse directe à l'attaque par apprentissage du parallélépipède qui a tué NTRUSign.

ML-DSA en pratique

ML-DSA repose simultanément sur Module-LWE — pour que la clé publique cache le secret — et sur Module-SIS — pour qu'on ne puisse pas forger une réponse courte sans le connaître. Il travaille dans le même anneau que ML-KEM, avec n=256n = 256, mais un module q=8380417q = 8\,380\,417 qui autorise la NTT complète.

ML-DSA-44ML-DSA-65ML-DSA-87
dimensions (k,)(k, \ell)(4, 4)(6, 5)(8, 7)
niveau NIST235
clé publique1312 o1952 o2592 o
clé privée2560 o4032 o4896 o
signature2420 o3309 o4627 o

Deux détails d'ingénierie méritent une mention en cours.

Le premier est le mécanisme d'indices. Transmettre ww en entier coûterait très cher. On ne transmet que ses bits de poids fort, plus un petit « indice » qui permet au vérificateur de reconstituer ce dont il a besoin. C'est une compression, comme dans ML-KEM, et elle explique une bonne part de l'écart entre la taille naïve et la taille réelle.

Le second est le caractère aléatoire de la signature. La FIPS 204 prévoit une variante déterministe, où le masque est dérivé du message et de la clé, et une variante « couverte » qui y ajoute de l'aléa frais — cette dernière étant le défaut. La raison est défensive : une signature déterministe se signe deux fois à l'identique, ce qui permet à un attaquant capable d'injecter une faute de comparer les deux exécutions et d'en déduire le secret. Le déterminisme, qui est une vertu pour la reproductibilité, est ici une prise.

Falcon : plus court, plus difficile

Falcon suit une voie entièrement différente, le cadre GPV : une trappe permet d'échantillonner un vecteur court du réseau, proche d'une cible donnée, selon une distribution gaussienne exactement calibrée. C'est cette calibration qui empêche la fuite — au lieu de rejeter comme ML-DSA, on échantillonne directement dans la bonne loi.

Le réseau employé est un réseau NTRU, plus compact que les réseaux modulaires, d'où des objets nettement plus petits :

Falcon-512Falcon-1024
clé publique897 o1793 o
signature (moyenne)≈ 666 o≈ 1280 o

Graphique

Taille des signatures, en octets

  • Ed25519 : 6464
  • RSA-2048 : 256256
  • Falcon-512 : 666666
  • ML-DSA-44 : 24202420
  • ML-DSA-87 : 46274627
Falcon-512 tient en 666 octets contre 2420 pour ML-DSA-44 à sécurité comparable : un facteur 3,6. Sur une chaîne de certificats qui en porte cinq, l'écart devient décisif — et c'est exactement l'argument du chapitre 13.

Pourquoi ML-DSA reste-t-il le choix par défaut malgré cet écart ? À cause de l'implémentation, et c'est un point que les étudiants doivent entendre clairement.

L'échantillonneur gaussien de Falcon exige de l'arithmétique à virgule flottante en double précision. Un schéma cryptographique qui dépend du comportement exact du flottant est fragile : les résultats varient d'une architecture à l'autre, et les plateformes embarquées sans unité flottante sont exclues ou forcées à une émulation lente. Surtout, écrire cet échantillonneur en temps constant est notoirement délicat, et des attaques par canaux auxiliaires visant précisément l'échantillonnage gaussien ont été publiées.

ML-DSA, lui, ne manipule que des entiers, et son rejet — quoique de durée variable — se protège par des techniques bien comprises. On a préféré le schéma qu'on sait implémenter correctement, exactement comme au chapitre 3 pour le choix de la binomiale contre la gaussienne. C'est une constante de la conception post-quantique.

Quiz · 1 question

Pourquoi ML-DSA est-il recommandé par défaut alors que Falcon produit des signatures 3 à 4 fois plus courtes ?

  • Parce que Falcon repose sur une hypothèse jugée moins sûre
  • Parce que l'échantillonneur gaussien de Falcon exige du flottant double précision et est très difficile à écrire en temps constant
  • Parce que Falcon n'atteint pas le niveau NIST 5

Réponse : Falcon-1024 atteint bien le niveau 5, et l'hypothèse NTRU est étudiée depuis 1996. Le problème est l'implémentation : dépendre du comportement exact du flottant rend le schéma fragile d'une architecture à l'autre et exclut les plateformes sans FPU, et l'échantillonnage gaussien en temps constant a déjà donné lieu à des attaques publiées. Le NIST a privilégié ce qu'une équipe ordinaire sait implémenter sans se tromper.

À vous

L'exercice reproduit la fuite en une dimension. Sans rejet, le secret se lit dans une simple moyenne — pas de cryptanalyse, pas de réseau, une moyenne.

Exercice de code

Ajoutez la condition de rejet, puis comparez les deux moyennes : sans rejet, le secret se lit dans une simple moyenne.

Point de départ

// Pourquoi ML-DSA rejette. Version à une dimension, mais le mécanisme est
// exactement celui du schéma normalisé.
//
//   secret s, masque y uniforme dans [-γ, γ], défi c ∈ {0, 1}
//   réponse z = y + c·s
//
// Sans rejet, la loi de z DÉPEND de s. Avec rejet, elle n'en dépend plus.

const SECRET = 7;       // ce que l'attaquant cherche
const GAMMA = 100;      // amplitude du masque
const BETA = 10;        // borne sur |c·s|, connue publiquement
const N = 200000;

const alea = (a, b) => a + Math.floor(Math.random() * (b - a + 1));

// Signature SANS rejet : on renvoie z quoi qu'il arrive.
function signerNaif() {
  const y = alea(-GAMMA, GAMMA);
  const c = alea(0, 1);
  return { z: y + c * SECRET, c, accepte: true };
}

// Signature AVEC rejet : on ne renvoie z que s'il tient dans la fenêtre
// réduite [-(γ-β), γ-β], atteignable quel que soit le secret.
function signerAvecRejet() {
  const y = alea(-GAMMA, GAMMA);
  const c = alea(0, 1);
  const z = y + c * SECRET;
  // À COMPLÉTER — n'accepter que si |z| ≤ GAMMA - BETA.
  const accepte = true;
  return { z, c, accepte };
}

// L'attaque : moyenner les z des signatures où c = 1. Sans rejet, cette
// moyenne converge vers le secret.
function attaque(signer, nom) {
  let somme = 0, compte = 0, produites = 0, tours = 0;
  while (produites < N) {
    tours++;
    const sig = signer();
    if (!sig.accepte) continue;
    produites++;
    if (sig.c === 1) { somme += sig.z; compte++; }
  }
  console.log(
    `${nom.padEnd(16)} moyenne(z | c=1) = ${(somme / compte).toFixed(3).padStart(7)}` +
    `   secret réel = ${SECRET}   tours/signature = ${(tours / produites).toFixed(2)}`
  );
}

attaque(signerNaif, "sans rejet");
attaque(signerAvecRejet, "avec rejet");

Solution

const SECRET = 7;
const GAMMA = 100;
const BETA = 10;
const N = 200000;

const alea = (a, b) => a + Math.floor(Math.random() * (b - a + 1));

function signerNaif() {
  const y = alea(-GAMMA, GAMMA);
  const c = alea(0, 1);
  return { z: y + c * SECRET, c, accepte: true };
}

function signerAvecRejet() {
  const y = alea(-GAMMA, GAMMA);
  const c = alea(0, 1);
  const z = y + c * SECRET;
  // La fenêtre [-(γ-β), γ-β] est INCLUSE dans l'image de y + c·s pour tout
  // secret admissible. Restreindre z à cette fenêtre rend donc sa loi
  // uniforme sur elle — indépendamment de s, de c, et de tout le reste.
  const accepte = Math.abs(z) <= GAMMA - BETA;
  return { z, c, accepte };
}

function attaque(signer, nom) {
  let somme = 0, compte = 0, produites = 0, tours = 0;
  while (produites < N) {
    tours++;
    const sig = signer();
    if (!sig.accepte) continue;
    produites++;
    if (sig.c === 1) { somme += sig.z; compte++; }
  }
  console.log(
    `${nom.padEnd(16)} moyenne(z | c=1) = ${(somme / compte).toFixed(3).padStart(7)}` +
    `   secret réel = ${SECRET}   tours/signature = ${(tours / produites).toFixed(2)}`
  );
}

attaque(signerNaif, "sans rejet");
attaque(signerAvecRejet, "avec rejet");

// Ce que la sortie montre, et qui est le cœur du chapitre.
//
// SANS REJET : la moyenne des z pour lesquels c = 1 converge vers 7. Le
// secret se lit directement dans une statistique sur les signatures. Aucune
// cryptanalyse, aucun réseau, aucune réduction : une moyenne. C'est la
// fuite qui a tué NTRUSign, à ceci près qu'il fallait là-bas reconstruire
// un parallélépipède au lieu de calculer une moyenne.
//
// AVEC REJET : la moyenne tombe à zéro. La loi de z est uniforme sur
// [-90, 90] QUEL QUE SOIT le secret — c'est ce qui permet au simulateur de
// la preuve de sécurité de produire des signatures sans connaître s.
//
// Le prix se lit dans la dernière colonne : environ 1,1 tour par signature
// ici. Pour ML-DSA-65, avec beaucoup plus de coefficients à faire tenir
// simultanément dans la fenêtre, l'espérance monte à quelques tours — et
// surtout, elle est ALÉATOIRE. Un signataire dont le temps d'exécution
// laisse voir le nombre de tours redonne à l'attaquant une information
// corrélée au secret : c'est le chapitre 12.

À retenir

Flashcards · 3 cartes

Qu'est-ce que l'apprentissage du parallélépipède, et qu'a-t-il cassé ?
L'attaque de Nguyen et Regev (2006) contre NTRUSign. Chaque signature étant distribuée dans la cellule fondamentale de la base secrète, quelques centaines de signatures suffisaient à reconstruire cette cellule, donc la clé privée. C'est le problème auquel le rejet d'échantillonnage de ML-DSA répond.
Pourquoi la FIPS 204 fait-elle de la signature aléatoire (« couverte ») le mode par défaut ?
Une signature déterministe produit deux fois le même calcul pour le même message. Un attaquant capable d'injecter une faute peut comparer une exécution correcte et une exécution fautée et en déduire le secret. L'aléa frais rend cette comparaison impossible. Le déterminisme, vertu pour la reproductibilité, est ici une prise.
ML-DSA ou Falcon : sur quel critère choisit-on ?
Taille contre difficulté d'implémentation. Falcon-512 signe en 666 octets contre 2420 pour ML-DSA-44, ce qui compte sur une chaîne de certificats. Mais son échantillonneur gaussien exige du flottant double précision et résiste mal à l'écriture en temps constant. ML-DSA n'utilise que des entiers : c'est le choix par défaut, Falcon celui des contextes contraints en bande passante avec une équipe compétente.

QCM du bloc I — Réseaux euclidiens

Douze questions sur les quatre chapitres du bloc — c'est le plus long du cursus, et celui dont tout le reste dépend. Si vous devez n'en réussir qu'un, c'est celui-ci.

QCM de bloc · 12 questions

Réseaux euclidiens

1. Le déterminant d'un réseau vaut 5 pour une base donnée. Que vaut-il pour une autre base du même réseau ?

  • 5 — c'est un invariant du réseau, pas de la base
  • Il dépend de la base : une base réduite donne un déterminant plus petit
  • 5 divisé par le facteur de réduction de la nouvelle base

Réponse : La matrice de passage entre deux bases d'un même réseau est unimodulaire, de déterminant ±1 : elle préserve les volumes. Le déterminant mesure l'aire de la cellule fondamentale, donc la densité des points — une propriété de l'objet. Ce qui change d'une base à l'autre, ce sont les longueurs et les angles, et c'est précisément là-dessus que repose la trappe.

2. L'arrondi de Babai appliqué avec une base ORTHOGONALE :

  • échoue systématiquement, l'orthogonalité étant le pire cas
  • donne une approximation à un facteur 2^((n−1)/2) près
  • donne exactement le point du réseau le plus proche

Réponse : Avec une base orthogonale, la cellule fondamentale centrée sur un point est un pavé droit, et c'est exactement la cellule de Voronoï : le point dont la cellule contient la cible EST le plus proche. Le facteur 2^((n−1)/2) est la garantie de LLL sur la longueur du premier vecteur, une tout autre grandeur. Une base seulement réduite donne un bon résultat, pas toujours l'optimal.

3. Passer de BKZ-200 à BKZ-400 dans le modèle core-SVP classique :

  • double le coût de l'attaque
  • le multiplie par environ 2^59, pour un δ₀ qui passe de 1,0063 à 1,0040
  • le réduit, les blocs plus grands étant traités plus efficacement

Réponse : Le coût est 2^(0,292β) : de 2^58 à 2^117, soit un facteur 2^59. C'est tout l'intérêt du paramètre β — il achète de la qualité de réduction à un prix exponentiel. Doubler β ne double pas le coût, il l'élève au carré ; et augmenter la taille des blocs ne peut jamais coûter moins cher, puisque BKZ appelle un oracle SVP en dimension β.

4. Un collègue affirme : « les réseaux sont sûrs parce que SVP est NP-difficile ». Que corrigez-vous ?

  • Rien, l'énoncé est exact
  • SVP n'est pas NP-difficile, même dans sa version exacte
  • La cryptographie emploie les versions APPROCHÉES à facteur polynomial, régime où la NP-difficulté n'est ni établie ni attendue

Réponse : SVP exact est bien NP-difficile sous réductions randomisées — la deuxième option est fausse. Mais aucun schéma n'emploie SVP exact : tous reposent sur des versions approchées à facteur γ polynomial, pour lesquelles le problème tombe dans NP ∩ coNP dès γ ≥ √n. La confiance vient de quarante ans d'échecs cryptanalytiques, pas d'un théorème de complexité.

5. Pourquoi un bruit de ±1 met-il en échec l'élimination de Gauss modulo q = 3329 en dimension 768 ?

  • Parce que les coefficients d'élimination, de l'ordre de q, multiplient les erreurs jusqu'à dépasser q/2
  • Parce que la division modulaire cesse d'être définie en présence de bruit
  • Parce que le système devient sous-déterminé

Réponse : L'élimination ne se contente pas d'additionner les lignes : elle les combine avec des coefficients, qui multiplient les erreurs autant que les inconnues. Après 767 éliminations, le bruit cumulé dépasse q/2 et le résidu devient uniforme sur Z_q. La division modulaire reste parfaitement définie, et le système reste déterminé — c'est bien l'amplification qui détruit l'information.

6. Qu'apporte Module-LWE que Ring-LWE n'apporte pas ?

  • Des clés publiques nettement plus petites
  • Un réglage de la structure algébrique par le rang k, et un même code d'arithmétique pour tous les niveaux de sécurité
  • Une réduction pire cas vers cas moyen, dont Ring-LWE est dépourvu

Réponse : Ring-LWE donne déjà les clés compactes, et dispose lui aussi d'une réduction pire cas vers cas moyen. Le module apporte deux autres choses : on module la quantité de structure algébrique en jouant sur k plutôt que sur n, et surtout changer de niveau de sécurité ne change que k — l'anneau, la NTT et les tables restent identiques pour ML-KEM-512, 768 et 1024.

7. À quoi sert principalement le problème SIS ?

  • À accélérer la NTT
  • À chiffrer, en complément de LWE
  • À construire des signatures

Réponse : LWE cache un secret dans du bruit : c'est ce qu'il faut pour chiffrer. SIS demande d'exhiber un vecteur court non nul dans le noyau d'une matrice : c'est ce qu'il faut pour prouver qu'on sait produire quelque chose de court sans révéler comment, donc pour signer. ML-DSA repose simultanément sur les deux — Module-LWE pour cacher le secret, Module-SIS pour empêcher la forge.

8. On souhaite réduire de 10 % la taille d'un chiffré ML-KEM en compressant davantage. Quelle en est la conséquence ?

  • Le bruit d'arrondi augmente, se rapproche du seuil q/4, et le taux d'échec de déchiffrement monte
  • Aucune : la compression de ML-KEM est sans perte
  • La dimension du réseau sous-jacent diminue d'autant

Réponse : La compression jette des bits de poids faible : elle est délibérément avec perte, et l'erreur d'arrondi q/2^(d+1) vient s'ajouter au bruit LWE. Le déchiffrement n'est correct que tant que le total reste sous q/4. La dimension, elle, ne bouge pas — la compression ne touche qu'à l'encodage du chiffré, jamais au réseau.

9. Pourquoi le terme v est-il compressé sur 4 bits quand u l'est sur 10 ?

  • Parce que v contient des valeurs numériquement plus petites
  • Parce que v ne transporte qu'un bit de message par coefficient, décodé par comparaison à q/2
  • Parce que v n'intervient pas dans le déchiffrement

Réponse : Les deux termes vivent dans le même Z_q et v intervient bel et bien au déchiffrement — c'est même lui qui porte le message. Mais chacun de ses coefficients ne code qu'un bit, décodé en regardant s'il est plus près de 0 ou de q/2 : une erreur d'une centaine d'unités reste très loin du seuil. Chaque terme est compressé selon ce qu'il transporte, pas uniformément.

10. Le temps de signature de ML-DSA varie d'un message à l'autre. Est-ce normal ?

  • Non : c'est le signe d'une implémentation défectueuse
  • Oui, et c'est sans conséquence : le rejet est indépendant du secret
  • Oui — c'est le rejet d'échantillonnage — mais le nombre de tours est corrélé au secret et doit être masqué

Réponse : La variabilité est intrinsèque : on rejette tant que z sort de la fenêtre, et le nombre de tours suit une loi géométrique. Mais on rejette PRÉCISÉMENT parce que z, qui vaut y + c·s, a dépassé le seuil : le nombre de tours n'est donc pas indépendant du secret. Une implémentation qui laisse voir ce compte rend une information exploitable — c'est le chapitre 12.

11. Falcon-512 signe en 666 octets contre 2420 pour ML-DSA-44. Pourquoi ML-DSA reste-t-il recommandé par défaut ?

  • Parce que l'échantillonneur gaussien de Falcon exige du flottant double précision et résiste mal à l'écriture en temps constant
  • Parce que Falcon n'atteint pas le niveau NIST 5
  • Parce que l'hypothèse NTRU est bien plus récente que Module-LWE

Réponse : Falcon-1024 atteint le niveau 5, et NTRU date de 1996 — il est plus ancien que Module-LWE, pas plus récent. Le motif est l'implémentation : dépendre du comportement exact du flottant rend le schéma fragile d'une architecture à l'autre, exclut les plateformes sans unité flottante, et l'échantillonnage gaussien en temps constant a déjà donné lieu à des attaques publiées.

12. Que le rejet d'échantillonnage empêche-t-il ?

  • Les collisions de la fonction de hachage employée pour le défi
  • Que la loi de z dépende du secret — donc l'attaque par apprentissage du parallélépipède
  • Les échecs de vérification chez le destinataire

Réponse : Le rejet ne coûte aucun octet et ne gagne aucune sécurité de hachage. Ce qu'il achète est statistique : en n'acceptant z que dans une fenêtre atteignable quel que soit le secret, il rend sa loi indépendante de s. Le simulateur de la preuve peut alors produire des signatures sans connaître le secret — et un attaquant qui en collecte un million n'apprend rien, contrairement à NTRUSign.