C3 — Les autres famillesDans le dialogue d’impression, choisissez « Enregistrer au format PDF » comme destination.
Retour

Master 1 · Cryptographie post-quantique

Cours 3Les autres familles

Codes, hachage, isogénies et multivarié : les alternatives, leurs compromis, et deux candidats brisés.

3 chapitres · 12 h de travail estimé

  1. 1. Codes correcteurs5 h
  2. 2. Signatures à base de hachage4 h
  3. 3. Isogénies et multivarié3 h

Chapitre 1 · 5 h

Codes correcteurs

Décodage par syndrome et décodage générique, McEliece et Niederreiter, HQC : une sécurité très étudiée contre des clés publiques énormes.

Nous quittons les réseaux. La famille des codes correcteurs offre la plus vieille hypothèse post-quantique en service : McEliece date de 1978, la même année que RSA, et il n'a jamais été cassé. Ce chapitre explique pourquoi, et pourquoi cette solidité n'a pas suffi à en faire le standard.

Codes linéaires : le strict nécessaire

Un code linéaire [n,k,d][n, k, d] sur F2\mathbb{F}_2 est un sous-espace vectoriel de dimension kk dans F2n\mathbb{F}_2^n. On y encode kk bits d'information en nn bits transmis, les nkn - k bits supplémentaires servant à détecter et corriger les erreurs. La distance minimale dd est le plus petit poids d'un mot de code non nul ; un code corrige jusqu'à t=(d1)/2t = \lfloor (d-1)/2 \rfloor erreurs.

Deux matrices décrivent le code. La génératrice GG, de taille k×nk \times n, encode : c=mGc = mG. La matrice de contrôle HH, de taille (nk)×n(n-k) \times n, vérifie : HcT=0Hc^{T} = 0 pour tout mot de code.

Le syndrome est la quantité centrale. Si l'on reçoit y=c+ey = c + eee est le vecteur d'erreur, alors

HyT=HcT+HeT=HeTH y^{T} = H c^{T} + H e^{T} = H e^{T}

Le syndrome ne dépend que de l'erreur, jamais du message. Décoder, c'est retrouver ee à partir de HeTHe^{T} — et c'est exactement le problème sur lequel toute la famille repose.

Le décodage générique est difficile

Voici la dissymétrie qui fonde la cryptographie à base de codes.

Si l'on connaît la structure du code — qu'il est un code de Hamming, de Reed-Solomon, de Goppa — le décodage est un algorithme polynomial, souvent très rapide. C'est même la raison d'être de ces codes en télécommunications.

Si l'on ne dispose que d'une matrice quelconque, sans structure apparente, le problème du décodage par syndrome est NP-difficile, résultat établi par Berlekamp, McEliece et van Tilborg dès 1978. Le meilleur algorithme connu reste le décodage par ensembles d'information (ISD), dont l'idée remonte à Prange en 1962 : deviner un ensemble de positions sans erreur et résoudre linéairement. Soixante ans de raffinements — Stern, MMT, BJMM — n'ont amélioré que la constante dans l'exposant. Le coût reste exponentiel, et sa variante quantique n'apporte qu'un gain modeste.

C'est cette stabilité qui fait la réputation de la famille : la courbe de progression des attaques est remarquablement plate depuis quatre décennies. Pour comparer, l'estimation de sécurité des réseaux, elle, a bougé plusieurs fois depuis 2016.

McEliece : brouiller la structure

La construction de McEliece tient en une phrase. On choisit un code structuré, dont on sait décoder efficacement — historiquement un code de Goppa binaire — et on publie une matrice génératrice brouillée qui décrit le même code sans en laisser voir la structure.

Gpub=SGPG_{\text{pub}} = S \, G \, P

SS est inversible et PP une permutation. Chiffrer, c'est encoder le message avec GpubG_{\text{pub}} puis ajouter volontairement tt erreurs. Déchiffrer, c'est retirer la permutation, décoder avec l'algorithme de Goppa, et défaire SS.

C'est la trappe du chapitre 4 transposée : même objet, deux descriptions, une seule exploitable. Ici, la « bonne base » est la structure de Goppa.

La variante de Niederreiter utilise la matrice de contrôle plutôt que la génératrice : le message est encodé dans le vecteur d'erreur lui-même, et le chiffré est un simple syndrome. Elle est équivalente en sécurité et produit des chiffrés bien plus courts — c'est la forme employée par Classic McEliece.

Quiz · 1 question

Où se trouve la trappe dans McEliece ?

  • Dans le vecteur d'erreur ajouté au chiffrement
  • Dans la connaissance de la structure du code, que la matrice publique brouille
  • Dans la matrice de permutation P, gardée secrète

Réponse : P et S servent à brouiller, mais ce ne sont pas elles le secret utile : c'est de savoir que le code EST un code de Goppa, et lequel, qui permet de décoder en temps polynomial. La matrice publique décrit exactement le même code, sans en laisser voir la structure. Décoder sans elle, c'est le problème NP-difficile du décodage générique.

HQC : le retour de la structure

Classic McEliece a un défaut, un seul, et il est massif : sa clé publique est une matrice dense. Elle pèse 261 kilooctets au niveau de sécurité le plus bas et plus d'un mégaoctet au plus haut.

HQCHamming Quasi-Cyclic — répond à cela comme Ring-LWE a répondu à LWE plein : en introduisant de la structure. Les codes employés sont quasi-cycliques, donc décrits par un petit nombre de coefficients au lieu d'une matrice complète. Le gain est du même ordre : la clé publique tombe à quelques kilooctets.

HQC diffère de McEliece sur un point conceptuel important. Sa sécurité ne repose pas sur la dissimulation d'un code structuré, mais sur le décodage de codes aléatoires quasi-cycliques — le code servant à corriger est public, et le secret est ailleurs. Cela supprime une classe entière d'attaques, celles qui cherchent à distinguer un code masqué d'un code aléatoire.

Le prix, comme pour ML-KEM, est un taux d'échec de déchiffrement non nul qu'il faut dimensionner soigneusement.

Le NIST a retenu HQC en mars 2025 comme second mécanisme d'encapsulation, en secours de ML-KEM. La logique de ce choix mérite d'être explicitée en cours : il ne s'agit pas d'avoir deux schémas équivalents, mais deux schémas reposant sur des hypothèses de difficulté différentes. Si les réseaux tombaient, HQC resterait.

Le compromis, en chiffres

Graphique

Clé publique, en octets

  • X25519 : 3232
  • ML-KEM-768 : 11841184
  • HQC-128 : 22492249
  • McEliece 348864 : 261120261120
  • McEliece 6960119 : 10473191047319
Les trois premières barres sont écrasées au trait minimal : à côté du mégaoctet de Classic McEliece, les 1184 octets de ML-KEM-768 ne se voient pas. C'est cette échelle, et non un doute sur la sécurité, qui a écarté McEliece d'un usage général.
Classic McElieceHQC-128ML-KEM-768
clé publique261 kio à 1 Mio2249 o1184 o
chiffré96 à 208 o4433 o1088 o
hypothèsedécodage générique, 1978décodage quasi-cycliqueModule-LWE
statut NISTnon retenu (ISO en cours)retenu 2025, en secoursFIPS 203

Regardez la ligne « chiffré » : McEliece produit les chiffrés les plus courts de tout le paysage post-quantique — 96 octets. Sa clé publique est énorme, mais elle peut être transmise une fois et réutilisée. Pour un tunnel VPN à long terme entre deux sites, où la clé s'échange à l'installation et les chiffrés circulent en permanence, McEliece est un excellent choix, et plusieurs agences européennes le recommandent explicitement pour les usages à long terme. Pour un handshake TLS où chaque connexion transporte la clé, il est inutilisable.

La bonne taille dépend de ce qui circule souvent. C'est la leçon d'ingénierie du chapitre, et elle revaudra au chapitre 13.

Quiz · 1 question

Pour quel usage Classic McEliece est-il un bon choix malgré sa clé d'un mégaoctet ?

  • Un handshake TLS grand public
  • Un tunnel VPN durable entre deux sites, où la clé s'échange à l'installation
  • Une carte à puce à mémoire limitée

Réponse : McEliece produit les chiffrés les plus courts du paysage post-quantique — 96 octets. Ce qui coûte, c'est la clé publique, et elle ne coûte qu'UNE FOIS si elle est provisionnée à l'installation. Dans un tunnel durable, ce sont les chiffrés qui circulent en permanence, et McEliece y est optimal. En TLS, où chaque connexion retransmet la clé, il est disqualifié — et une carte à puce ne peut pas stocker un mégaoctet.

À vous

Exercice de code

Implémentez le décodage générique par énumération, puis comparez son coût à celui du décodage structuré — et lisez la dernière table.

Point de départ

// Décodage par syndrome sur le code de Hamming [7, 4, 3].
// Il corrige une erreur. Le point de l'exercice n'est pas de le décoder —
// c'est de mesurer ce que coûte le décodage quand on ne connaît PAS la
// structure du code.

// Matrice de contrôle : les colonnes sont 1..7 écrits en binaire.
const H = [
  [0, 0, 0, 1, 1, 1, 1],
  [0, 1, 1, 0, 0, 1, 1],
  [1, 0, 1, 0, 1, 0, 1],
];

const syndrome = (mot) =>
  H.map((ligne) => ligne.reduce((s, h, i) => s ^ (h & mot[i]), 0));

// Décodage STRUCTURÉ : on connaît le code, le syndrome donne directement la
// position de l'erreur (lue en binaire).
function decoderAvecClef(recu) {
  const s = syndrome(recu);
  const position = s[0] * 4 + s[1] * 2 + s[2];
  if (position === 0) return { mot: recu, erreur: null };
  const corrige = recu.slice();
  corrige[position - 1] ^= 1;
  return { mot: corrige, erreur: position - 1 };
}

// Décodage GÉNÉRIQUE : on ne connaît que H. On énumère les motifs d'erreur
// par poids croissant jusqu'à retomber sur un syndrome nul.
let essais = 0;
function decoderSansClef(recu, poidsMax) {
  essais = 0;
  const n = recu.length;
  const cible = syndrome(recu);
  // À COMPLÉTER — énumérer tous les motifs d'erreur de poids ≤ poidsMax et
  // renvoyer le premier dont le syndrome égale la cible. Incrémentez essais.
  return null;
}

const MOT = [1, 0, 1, 1, 0, 1, 0];          // mot de code valide
const RECU = MOT.slice(); RECU[4] ^= 1;      // une erreur en position 4

console.log("reçu            :", RECU.join(""));
console.log("syndrome        :", syndrome(RECU).join(""));
console.log("avec la clef    :", JSON.stringify(decoderAvecClef(RECU)));
console.log("sans la clef    :", JSON.stringify(decoderSansClef(RECU, 1)), `(${essais} essais)`);

// Le vrai sujet : combien de motifs faut-il énumérer en taille réelle ?
const binom = (n, k) => { let r = 1; for (let i = 0; i < k; i++) r = (r * (n - i)) / (i + 1); return r; };
console.log("\nmotifs d'erreur à énumérer, C(n, t) :");
for (const [n, t] of [[7, 1], [1024, 38], [3488, 64], [6960, 119]]) {
  console.log(`  n = ${String(n).padStart(4)}, t = ${String(t).padStart(3)}  →  2^${Math.log2(binom(n, t)).toFixed(0)}`);
}

Solution

const H = [
  [0, 0, 0, 1, 1, 1, 1],
  [0, 1, 1, 0, 0, 1, 1],
  [1, 0, 1, 0, 1, 0, 1],
];

const syndrome = (mot) =>
  H.map((ligne) => ligne.reduce((s, h, i) => s ^ (h & mot[i]), 0));

function decoderAvecClef(recu) {
  const s = syndrome(recu);
  const position = s[0] * 4 + s[1] * 2 + s[2];
  if (position === 0) return { mot: recu, erreur: null };
  const corrige = recu.slice();
  corrige[position - 1] ^= 1;
  return { mot: corrige, erreur: position - 1 };
}

let essais = 0;
function decoderSansClef(recu, poidsMax) {
  essais = 0;
  const n = recu.length;
  const cible = syndrome(recu);
  const egal = (a, b) => a.every((x, i) => x === b[i]);

  // Énumération par poids croissant. C'est la stratégie de base du décodage
  // générique — et c'est aussi, à des raffinements près, ce que fait encore
  // le meilleur algorithme connu soixante ans plus tard.
  const motifs = [];
  const construire = (debut, restant, courant) => {
    if (restant === 0) { motifs.push(courant.slice()); return; }
    for (let i = debut; i < n; i++) { courant.push(i); construire(i + 1, restant - 1, courant); courant.pop(); }
  };
  for (let poids = 0; poids <= poidsMax; poids++) construire(0, poids, []);

  for (const motif of motifs) {
    essais++;
    const e = new Array(n).fill(0);
    for (const i of motif) e[i] = 1;
    if (egal(syndrome(e), cible)) {
      const corrige = recu.map((b, i) => b ^ e[i]);
      return { mot: corrige, erreur: motif };
    }
  }
  return null;
}

const MOT = [1, 0, 1, 1, 0, 1, 0];
const RECU = MOT.slice(); RECU[4] ^= 1;

console.log("reçu            :", RECU.join(""));
console.log("syndrome        :", syndrome(RECU).join(""));
console.log("avec la clef    :", JSON.stringify(decoderAvecClef(RECU)));
console.log("sans la clef    :", JSON.stringify(decoderSansClef(RECU, 1)), `(${essais} essais)`);

const binom = (n, k) => { let r = 1; for (let i = 0; i < k; i++) r = (r * (n - i)) / (i + 1); return r; };
console.log("\nmotifs d'erreur à énumérer, C(n, t) :");
for (const [n, t] of [[7, 1], [1024, 38], [3488, 64], [6960, 119]]) {
  console.log(`  n = ${String(n).padStart(4)}, t = ${String(t).padStart(3)}  →  2^${Math.log2(binom(n, t)).toFixed(0)}`);
}

// Ce que l'exercice met en scène est EXACTEMENT la trappe de McEliece.
//
// decoderAvecClef fait trois opérations : la structure du code (ici Hamming,
// là un code de Goppa) transforme le syndrome en position d'erreur. C'est la
// clé privée.
//
// decoderSansClef énumère. Sur n = 7 et t = 1, huit essais suffisent. Sur les
// paramètres de Classic McEliece — n = 6960, t = 119 — la dernière ligne
// donne l'ordre de grandeur, et il est astronomique.
//
// La clé publique de McEliece est une matrice génératrice BROUILLÉE : elle
// décrit le même code, mais ne laisse plus voir la structure de Goppa. Même
// objet, deux descriptions, une seule exploitable — exactement la trappe du
// chapitre 4, transposée des réseaux aux codes.
//
// Note : le décodage par ensembles d'information (ISD) fait bien mieux que
// cette énumération naïve, mais reste exponentiel. Soixante ans de
// raffinements n'ont amélioré que la constante dans l'exposant.

À retenir

Flashcards · 3 cartes

Pourquoi le syndrome est-il la quantité centrale du décodage ?
Parce que H·yᵀ = H·(c+e)ᵀ = H·eᵀ : le syndrome ne dépend QUE de l'erreur, jamais du message. Décoder revient donc à retrouver un vecteur de petit poids à partir de son syndrome — le problème NP-difficile sur lequel toute la famille repose.
Qu'est-ce qui distingue la sécurité de McEliece de celle des réseaux ?
Son ancienneté et sa stabilité. Le problème date de 1978 et le meilleur algorithme connu reste le décodage par ensembles d'information, dont soixante ans de raffinements n'ont amélioré que la constante dans l'exposant. Les estimations de sécurité des réseaux, elles, ont bougé plusieurs fois depuis 2016.
Pourquoi le NIST a-t-il retenu HQC en secours de ML-KEM plutôt qu'un second schéma à réseaux ?
Parce qu'un secours n'a d'intérêt que s'il repose sur une hypothèse DIFFÉRENTE. Deux schémas à réseaux tomberaient ensemble. HQC repose sur le décodage de codes quasi-cycliques : si les réseaux cédaient, il resterait debout. C'est le même raisonnement que l'hybridation du chapitre 13, appliqué au portefeuille de normes.

Chapitre 2 · 4 h

Signatures à base de hachage

Lamport, Winternitz et arbres de Merkle ; SLH-DSA sans état, XMSS et LMS avec état, et le danger de réutilisation d'index.

Cette famille est à part, et il faut le dire dès l'ouverture. Les signatures à base de hachage ne reposent sur aucune structure algébrique : ni factorisation, ni logarithme discret, ni réseau, ni code. Leur seule hypothèse est qu'une fonction de hachage se comporte comme on l'attend. C'est l'hypothèse la mieux étudiée de toute la cryptographie, et la seule dont on soit à peu près certain qu'elle survivra à un ordinateur quantique — Grover ne fait que diviser par deux la sécurité en préimage.

D'où leur rôle : un filet de sécurité. Si les réseaux tombaient demain, SLH-DSA resterait debout.

Lamport : signer un bit

La construction de base est d'une simplicité désarmante, et date de 1979.

Pour signer un message de nn bits, on tire 2n2n valeurs secrètes aléatoires, deux par position : xi,0x_{i,0} et xi,1x_{i,1}. La clé publique est la liste de leurs hachés, yi,b=H(xi,b)y_{i,b} = H(x_{i,b}).

Pour signer, on révèle xi,mix_{i, m_i} pour chaque position ii — la valeur de gauche si le bit vaut 0, celle de droite s'il vaut 1. Pour vérifier, on hache ce qu'on a reçu et on compare à la clé publique.

La sécurité est immédiate : forger une signature exigerait d'inverser HH sur une valeur jamais révélée. Aucune structure, aucune réduction compliquée.

Le défaut l'est tout autant. La clé ne sert qu'une fois. Signer un second message avec la même clé révèle des valeurs supplémentaires, et l'exercice de fin de chapitre montre que deux messages bien choisis suffisent à tout révéler.

Winternitz : troquer du temps contre de la taille

Lamport produit des signatures énormes. Winternitz les réduit en traitant plusieurs bits à la fois, au moyen de chaînes de hachage.

Au lieu de deux valeurs par position, on part d'une valeur xx et on définit H(x),H2(x),,Hw1(x)H(x), H^2(x), \dots, H^{w-1}(x). Signer la valeur vv consiste à révéler Hv(x)H^{v}(x) : le vérificateur applique HH encore w1vw-1-v fois et retombe sur la clé publique. On signe ainsi log2w\log_2 w bits par chaîne au lieu d'un seul.

Le compromis est explicite : augmenter ww raccourcit la signature et allonge le calcul, linéairement en ww. Une somme de contrôle est ajoutée pour empêcher un attaquant d'« avancer » dans les chaînes et de forger une valeur plus grande. WOTS+ est la variante employée dans les schémas normalisés.

Merkle : d'une clé unique à un milliard

Reste le problème central : une clé à usage unique n'est pas utilisable. Merkle l'a résolu en 1979, la même année, et sa solution est aujourd'hui partout — de Git aux chaînes de blocs.

On engendre 2H2^H clés à usage unique, on hache chacune, et on construit un arbre binaire où chaque nœud est le haché de ses deux enfants. La racine devient la clé publique unique, 32 octets pour un nombre arbitraire de signatures.

Animation · 6 étapes

Prouver qu'une clé appartient à l'arbre sans montrer l'arbre

  1. Construire l'arbreChaque feuille est le haché d'une clé à usage unique. Chaque nœud interne est le haché de la concaténation de ses deux enfants. La racine, en haut, résume les 8 feuilles en 32 octets — et c'est elle, et elle seule, qui sert de clé publique.
  2. On veut prouver k2Le signataire publie la feuille k2 et affirme qu'elle appartient à l'arbre. Il ne peut pas envoyer les 8 feuilles : la preuve serait aussi grosse que l'arbre. Il envoie le chemin d'authentification.
  3. Remontée, niveau 1Le vérificateur hache la feuille avec le frère fourni. Il obtient le parent — qu'il n'avait pas reçu, mais qu'il vient de recalculer.
  4. Remontée, niveau 2Même opération un cran plus haut : le nœud recalculé et le frère fourni donnent le parent. Un frère par niveau, donc 3 au total pour 8 feuilles.
  5. Remontée, niveau 3Même opération un cran plus haut : le nœud recalculé et le frère fourni donnent le parent. Un frère par niveau, donc 3 au total pour 8 feuilles.
  6. Comparer à la racineLa valeur obtenue en haut est comparée à la clé publique. Si elles coïncident, la feuille appartenait bien à l'arbre. Coût de la preuve : 3 hachés au lieu de 8 — c'est le logarithme qui rend l'ensemble praticable.

Pour signer avec la ii-ième clé, on fournit la signature à usage unique, plus le chemin d'authentification : les frères rencontrés en remontant à la racine. Le vérificateur recalcule la remontée et compare. Il ne fait pas confiance — il refait le calcul.

Le coût de la preuve est log2\log_2 du nombre de feuilles. Trois hachés pour huit clés, vingt pour un million. C'est cette croissance logarithmique qui rend toute la famille praticable.

Quiz · 1 question

Que contient exactement le chemin d'authentification d'un arbre de Merkle ?

  • Tous les nœuds situés entre la feuille et la racine
  • Le FRÈRE de chaque nœud rencontré en remontant, soit log₂(n) valeurs
  • La racine et la feuille, signées ensemble

Réponse : Les nœuds situés SUR le chemin, le vérificateur les recalcule lui-même — c'est même tout l'intérêt. Ce qu'il ne peut pas recalculer, ce sont les frères, et il faut donc les lui fournir : un par niveau, soit log₂(n) valeurs. Pour un million de feuilles, vingt hachés suffisent à prouver l'appartenance.

Avec état : XMSS, LMS, et le piège opérationnel

XMSS (RFC 8391) et LMS (RFC 8554) mettent tout cela en œuvre. Ils sont normalisés, éprouvés, et produisent des signatures raisonnablement compactes.

Ils sont aussi à état. Le signataire doit mémoriser quel index il a déjà consommé, et cet état doit être mis à jour de façon durable et atomique avant l'émission de chaque signature.

Il faut insister, parce que l'énoncé paraît anodin et que ses conséquences ne le sont pas. Rejouer un index n'affaiblit pas la clé : il la détruit. Un attaquant qui observe deux signatures produites avec le même index peut forger. Et les opérations qui rejouent un état sont exactement celles que toute exploitation informatique pratique quotidiennement :

  • restaurer une sauvegarde ;
  • cloner ou reprendre l'instantané d'une machine virtuelle ;
  • répliquer un service derrière un répartiteur de charge ;
  • redémarrer après une panne, entre l'écriture de la signature et celle de l'état.

Le NIST, dans la SP 800-208, encadre ces schémas d'exigences strictes et les réserve explicitement à des contextes où l'état peut être garanti — typiquement la signature de firmware, opération rare, centralisée, sous contrôle matériel.

Sans état : SLH-DSA

SLH-DSA (FIPS 205), issu de SPHINCS+, supprime l'état. L'idée : au lieu de consommer les feuilles dans l'ordre, en choisir une pseudo-aléatoirement à partir du message. Comme une collision reste possible, on remplace les signatures à usage unique par des signatures à usages peu nombreux (FORS), qui tolèrent quelques répétitions, et on empile plusieurs arbres en hypertree pour maintenir la taille raisonnable.

Le résultat est un schéma sans état, sans structure algébrique, et dont la sécurité ne dépend que du haché. Le prix est franc :

clé publiquesignature
SLH-DSA-128s32 o7 856 o
SLH-DSA-128f32 o17 088 o
SLH-DSA-256s64 o29 792 o
ML-DSA-44 (comparaison)1312 o2 420 o
Ed25519 (comparaison)32 o64 o

Graphique

Taille des signatures, en octets

  • Ed25519 : 6464
  • ML-DSA-44 : 24202420
  • SLH-DSA-128s : 78567856
  • SLH-DSA-128f : 1708817088
  • SLH-DSA-256s : 2979229792
Les variantes « s » sont optimisées pour la petite taille, les « f » pour la rapidité de signature : 7856 contre 17088 octets au même niveau de sécurité. Face aux 64 octets d'Ed25519, l'écart va de 120 à 460 — c'est le prix de n'avoir aucune hypothèse algébrique à parier.

La signature est cent fois plus grosse qu'Ed25519, et la signature est lente. En échange, on obtient la garantie la plus solide du paysage. Ce n'est pas un schéma à déployer partout ; c'est un schéma à déployer là où l'on ne peut pas se permettre d'avoir tort — racines de confiance, signature de firmware à très longue durée de vie, ancrage d'une PKI.

Quiz · 1 question

Pourquoi SLH-DSA accepte-t-il des signatures dix fois plus grosses que ML-DSA ?

  • Parce que sa sécurité ne repose que sur la fonction de hachage, sans aucune hypothèse algébrique
  • Parce qu'il doit stocker son état dans la signature
  • Parce qu'il vise un niveau de sécurité supérieur

Réponse : SLH-DSA est justement SANS état — c'est ce qui le distingue de XMSS et LMS. Et il couvre les mêmes niveaux NIST que ML-DSA. Ce qu'on achète avec ces octets, c'est l'absence de pari : ni réseau, ni code, ni courbe, seulement une fonction de hachage, dont la résistance est l'hypothèse la mieux étudiée de la discipline. C'est le prix du filet de sécurité.

À vous

Exercice de code

Complétez la vérification du chemin d'authentification, puis regardez la seconde partie : deux signatures avec le même index suffisent à tout forger.

Point de départ

// Un haché JOUET — 32 bits, non cryptographique. Il suffit à faire tourner
// la mécanique ; ne l'utilisez jamais pour autre chose qu'un exercice.
function h(x) {
  let v = 0x811c9dc5;
  for (const car of String(x)) {
    v ^= car.charCodeAt(0);
    v = Math.imul(v, 0x01000193) >>> 0;
  }
  return v >>> 0;
}
const hex = (v) => v.toString(16).padStart(8, "0");

// ─────────── Partie 1 : l'arbre de Merkle ───────────

const FEUILLES = ["k0", "k1", "k2", "k3", "k4", "k5", "k6", "k7"].map(h);

// Construit tous les niveaux, des feuilles à la racine.
function construire(feuilles) {
  const niveaux = [feuilles];
  while (niveaux.at(-1).length > 1) {
    const bas = niveaux.at(-1);
    const haut = [];
    for (let i = 0; i < bas.length; i += 2) haut.push(h(bas[i] + ":" + bas[i + 1]));
    niveaux.push(haut);
  }
  return niveaux;
}

// Le chemin d'authentification : le FRÈRE rencontré à chaque niveau.
function chemin(niveaux, indice) {
  const preuve = [];
  let i = indice;
  for (let n = 0; n < niveaux.length - 1; n++) {
    preuve.push({ valeur: niveaux[n][i ^ 1], aDroite: (i & 1) === 0 });
    i >>= 1;
  }
  return preuve;
}

// Le vérificateur : il ne connaît QUE la feuille, la preuve et la racine.
function verifier(feuille, preuve, racine) {
  let courant = feuille;
  for (const { valeur, aDroite } of preuve) {
    // À COMPLÉTER — recombiner courant et valeur dans le BON ORDRE.
    courant = courant;
  }
  return courant === racine;
}

const niveaux = construire(FEUILLES);
const racine = niveaux.at(-1)[0];
const preuve = chemin(niveaux, 2);

console.log("racine (clé publique) :", hex(racine));
console.log("preuve pour k2        :", preuve.map((p) => hex(p.valeur)).join(" "));
console.log("taille de la preuve   :", preuve.length, "hachés pour", FEUILLES.length, "feuilles");
console.log("vérification k2       :", verifier(FEUILLES[2], preuve, racine));
console.log("vérification d'un faux:", verifier(h("intrus"), preuve, racine));

// ─────────── Partie 2 : ce que coûte un index rejoué ───────────
// Fourni. Signature de Lamport sur 8 bits, utilisée DEUX fois.

const bits = (n) => Array.from({ length: 8 }, (_, i) => (n >> (7 - i)) & 1);
const sk = Array.from({ length: 8 }, (_, i) => [h("s" + i + "a"), h("s" + i + "b")]);
const pk = sk.map(([a, b]) => [h(a), h(b)]);

const signer = (m) => bits(m).map((b, i) => sk[i][b]);
const verifierLamport = (m, sig) => bits(m).every((b, i) => h(sig[i]) === pk[i][b]);

const M1 = 0b10110010;
const M2 = 0b01001101;   // le complément : ensemble, ils révèlent TOUT

const connus = {};
for (const m of [M1, M2]) signer(m).forEach((v, i) => (connus[i + ":" + bits(m)[i]] = v));

const forger = (cible) => bits(cible).map((b, i) => connus[i + ":" + b]);
const CIBLE = 0b11111111;
const faux = forger(CIBLE);

console.log("\n── réutilisation d'index ──");
console.log("valeurs révélées      :", Object.keys(connus).length, "/ 16");
console.log("signature forgée sur", CIBLE.toString(2), ":", verifierLamport(CIBLE, faux) ? "ACCEPTÉE" : "refusée");

Solution

function h(x) {
  let v = 0x811c9dc5;
  for (const car of String(x)) {
    v ^= car.charCodeAt(0);
    v = Math.imul(v, 0x01000193) >>> 0;
  }
  return v >>> 0;
}
const hex = (v) => v.toString(16).padStart(8, "0");

const FEUILLES = ["k0", "k1", "k2", "k3", "k4", "k5", "k6", "k7"].map(h);

function construire(feuilles) {
  const niveaux = [feuilles];
  while (niveaux.at(-1).length > 1) {
    const bas = niveaux.at(-1);
    const haut = [];
    for (let i = 0; i < bas.length; i += 2) haut.push(h(bas[i] + ":" + bas[i + 1]));
    niveaux.push(haut);
  }
  return niveaux;
}

function chemin(niveaux, indice) {
  const preuve = [];
  let i = indice;
  for (let n = 0; n < niveaux.length - 1; n++) {
    preuve.push({ valeur: niveaux[n][i ^ 1], aDroite: (i & 1) === 0 });
    i >>= 1;
  }
  return preuve;
}

function verifier(feuille, preuve, racine) {
  let courant = feuille;
  for (const { valeur, aDroite } of preuve) {
    // L'ORDRE est la seule subtilité, et c'est une vraie faille quand on
    // l'oublie : un arbre où h(a:b) et h(b:a) seraient interchangeables
    // laisserait forger des preuves en permutant les frères.
    courant = aDroite ? h(courant + ":" + valeur) : h(valeur + ":" + courant);
  }
  return courant === racine;
}

const niveaux = construire(FEUILLES);
const racine = niveaux.at(-1)[0];
const preuve = chemin(niveaux, 2);

console.log("racine (clé publique) :", hex(racine));
console.log("preuve pour k2        :", preuve.map((p) => hex(p.valeur)).join(" "));
console.log("taille de la preuve   :", preuve.length, "hachés pour", FEUILLES.length, "feuilles");
console.log("vérification k2       :", verifier(FEUILLES[2], preuve, racine));
console.log("vérification d'un faux:", verifier(h("intrus"), preuve, racine));

const bits = (n) => Array.from({ length: 8 }, (_, i) => (n >> (7 - i)) & 1);
const sk = Array.from({ length: 8 }, (_, i) => [h("s" + i + "a"), h("s" + i + "b")]);
const pk = sk.map(([a, b]) => [h(a), h(b)]);

const signer = (m) => bits(m).map((b, i) => sk[i][b]);
const verifierLamport = (m, sig) => bits(m).every((b, i) => h(sig[i]) === pk[i][b]);

const M1 = 0b10110010;
const M2 = 0b01001101;

const connus = {};
for (const m of [M1, M2]) signer(m).forEach((v, i) => (connus[i + ":" + bits(m)[i]] = v));

const forger = (cible) => bits(cible).map((b, i) => connus[i + ":" + b]);
const CIBLE = 0b11111111;
const faux = forger(CIBLE);

console.log("\n── réutilisation d'index ──");
console.log("valeurs révélées      :", Object.keys(connus).length, "/ 16");
console.log("signature forgée sur", CIBLE.toString(2), ":", verifierLamport(CIBLE, faux) ? "ACCEPTÉE" : "refusée");

// Les deux moitiés de l'exercice disent la même chose sous deux angles.
//
// PARTIE 1 — la preuve fait 3 hachés pour 8 feuilles, et ferait 20 hachés
// pour un million. C'est le logarithme, et c'est ce qui rend praticable une
// clé publique de 32 octets qui autorise des milliards de signatures.
// Notez que l'ordre de concaténation compte : un arbre où h(a:b) et h(b:a)
// seraient équivalents laisserait forger des preuves.
//
// PARTIE 2 — c'est la catastrophe. Signer DEUX messages complémentaires
// avec la même clé de Lamport révèle les 16 valeurs secrètes, et permet
// alors de signer N'IMPORTE QUEL message. Pas « affaiblit » : permet.
//
// Voilà pourquoi XMSS et LMS sont dits « à état » et pourquoi cet état est
// leur talon d'Achille opérationnel. Restaurer une sauvegarde, cloner une
// machine virtuelle, répliquer un service derrière un répartiteur de
// charge — toutes ces opérations parfaitement banales rejouent un index et
// détruisent la clé. SLH-DSA supprime l'état, au prix d'une signature dix
// fois plus grosse. Le prix se discute ; le risque, non.

À retenir

Flashcards · 3 cartes

Pourquoi les signatures à base de hachage servent-elles de filet de sécurité ?
Parce qu'elles ne reposent sur aucune structure algébrique — ni factorisation, ni réseau, ni code — mais seulement sur la résistance d'une fonction de hachage, l'hypothèse la mieux étudiée de la discipline. Grover ne fait que diviser par deux la sécurité en préimage. Si les réseaux tombaient, SLH-DSA resterait debout.
Quel est le danger opérationnel des schémas à état comme XMSS et LMS ?
Rejouer un index ne les affaiblit pas, il DÉTRUIT la clé : deux signatures au même index permettent de forger. Or restaurer une sauvegarde, cloner une machine virtuelle ou répliquer un service derrière un répartiteur de charge rejouent tous un état. La SP 800-208 les réserve aux contextes où l'état est garanti — typiquement la signature de firmware.
Comment SLH-DSA se passe-t-il d'état ?
En choisissant la feuille pseudo-aléatoirement à partir du message plutôt que séquentiellement. Comme des collisions restent possibles, les signatures à usage unique sont remplacées par des signatures à usages peu nombreux (FORS), tolérantes à quelques répétitions, et plusieurs arbres sont empilés en hypertree. Coût : une signature de 8 à 30 kio.

Chapitre 3 · 3 h

Isogénies et multivarié

La cassure de SIKE en 2022 et ce qu'elle enseigne, CSIDH et SQIsign, l'échec de Rainbow : la valeur pédagogique des candidats brisés.

Ce chapitre est le plus court du cours et le plus important pour la formation du jugement. Il porte sur deux familles qui ont échoué en public, devant témoins, après avoir passé plusieurs tours d'un processus de normalisation international. Ce n'est pas un chapitre d'histoire : c'est un chapitre de méthode.

Isogénies : l'idée

Une isogénie est une application entre deux courbes elliptiques qui préserve la structure de groupe. On peut la voir comme un morphisme reliant une courbe à une autre.

L'objet cryptographique n'est plus la courbe, mais le graphe dont les sommets sont des courbes elliptiques supersingulières et les arêtes des isogénies de petit degré. Ce graphe a une propriété remarquable : il est expandeur. En s'y promenant au hasard quelques dizaines de pas, on aboutit à un sommet impossible à relier à son point de départ — trouver le chemin entre deux courbes données est le problème difficile.

L'attrait était considérable : les clés étaient les plus petites de tout le paysage post-quantique, quelques centaines d'octets seulement, là où les réseaux en demandent plusieurs milliers.

SIKE, et l'après-midi du 30 juillet 2022

SIDH, proposé en 2011, était le protocole d'échange de clés fondé sur ces graphes. SIKE en était la version encapsulation, candidate au processus NIST, arrivée jusqu'au quatrième tour. Onze ans d'examen public.

Le 30 juillet 2022, Wouter Castryck et Thomas Decru publient une attaque qui récupère la clé secrète. Elle s'exécute en une heure environ sur un seul cœur de processeur ordinaire pour les paramètres du premier niveau de sécurité. Des optimisations publiées dans les semaines suivantes ramèneront ce temps à quelques minutes, et Damien Robert généralisera l'attaque en temps polynomial pour toute courbe de départ.

Le mécanisme mérite d'être compris, parce qu'il change complètement la leçon à en tirer. SIDH ne publiait pas seulement une courbe d'arrivée : pour permettre aux deux parties de calculer la même valeur commune, il publiait aussi l'image de points de torsion auxiliaires par l'isogénie secrète. C'est cette information supplémentaire que l'attaque exploite, en s'appuyant sur un théorème de Kani datant de 1997 — un résultat de géométrie algébrique qui n'avait jamais été rapproché de SIDH.

Autrement dit : le problème général des isogénies n'a pas été cassé. C'est la manière dont SIDH en révélait un morceau qui l'a été. La distinction est essentielle, et c'est elle qui explique que CSIDH et SQIsign, qui ne publient pas ces points de torsion, restent debout.

Quiz · 1 question

Qu'a exactement cassé l'attaque de Castryck et Decru ?

  • Le problème général de recherche d'isogénies entre courbes supersingulières
  • La publication par SIDH des images de points de torsion auxiliaires
  • L'implémentation de référence de SIKE, par un canal auxiliaire

Réponse : Le problème d'isogénie général reste ouvert, et c'est pourquoi CSIDH et SQIsign survivent. Ce qui est tombé, c'est l'information supplémentaire que SIDH devait publier pour que le protocole fonctionne : l'image de points de torsion par l'isogénie secrète. Un théorème de Kani de 1997, jamais rapproché de SIDH en onze ans, permet de la transformer en attaque. Ce n'était pas non plus un défaut d'implémentation — le schéma lui-même était cassé.

Ce que SIKE enseigne

Trois leçons, et elles valent pour tout le reste du cours.

L'examen public ne prouve pas l'absence d'attaque. SIDH a été scruté onze ans par une communauté active. Le résultat qui l'a tué existait depuis 1997, dans une branche voisine des mathématiques. Personne n'avait fait le rapprochement. « Personne n'a trouvé d'attaque » est une observation, pas une garantie.

Une cassure peut être totale et instantanée. Il n'y a pas eu d'érosion progressive des paramètres, pas de signal d'alerte, pas de fenêtre pour réagir. Le passage d'« il tient » à « une heure sur un portable » a été immédiat.

D'où l'hybridation. Un déploiement qui combinait X25519 et SIKE n'a rien perdu le 30 juillet 2022 : il lui restait la sécurité classique de X25519. Un déploiement SIKE seul était nu. C'est l'argument décisif en faveur de l'approche que l'ANSSI impose et que le chapitre 13 détaille — et sans SIKE, cet argument passerait pour une précaution excessive.

CSIDH et SQIsign

La famille n'est pas morte, et il serait injuste de la présenter ainsi.

CSIDH utilise une action de groupe commutative sur les courbes supersingulières définies sur Fp\mathbb{F}_p. Sa commutativité est un atout — elle rend possibles des primitives que les réseaux ne fournissent pas facilement — et une faiblesse : l'algorithme quantique de Kuperberg pour le problème du décalage caché s'y applique, avec un coût sous-exponentiel. Le dimensionnement de CSIDH reste discuté pour cette raison.

Graphique

Clé publique + signature, en octets

  • Ed25519 : 9696
  • SQIsign : 241241
  • Falcon-512 : 15631563
  • ML-DSA-44 : 37323732
  • SLH-DSA-128s : 78887888
SQIsign tient en 241 octets — deux fois et demie Ed25519, quinze fois moins que ML-DSA-44. C'est la seule famille post-quantique dont les objets approchent les tailles classiques, et c'est la raison pour laquelle on continue de la travailler malgré la cassure de SIKE. Le prix est la vitesse : signer et vérifier coûtent des ordres de grandeur de plus que ML-DSA.

SQIsign est une signature dont les objets sont d'une compacité sans rivale — une clé publique de quelques dizaines d'octets, une signature de moins de deux cents. Comparez aux 2420 octets de ML-DSA-44. Son défaut est la vitesse : signer et vérifier coûtent des ordres de grandeur de plus que ML-DSA. Elle est candidate au processus additionnel de signatures du NIST, et représente le meilleur espoir de la famille.

Le multivarié, et Rainbow

L'autre famille de ce chapitre repose sur la difficulté de résoudre un système d'équations quadratiques à plusieurs variables sur un corps fini — problème NP-difficile dans le cas général.

La trappe s'appelle huile et vinaigre. On construit un système central où les variables sont séparées en deux groupes, et où l'on s'interdit tout produit huile × huile. Fixer les variables de vinaigre au hasard rend alors le système linéaire en les variables d'huile : il se résout par élimination de Gauss. La clé publique est ce système composé avec un mélange linéaire secret, qui cache quelles combinaisons de variables forment l'espace d'huile.

Rainbow empilait plusieurs couches de cette construction pour réduire la taille des clés. Il était finaliste du troisième tour du NIST, à un cheveu de la normalisation.

En février 2022, Ward Beullens publie un article au titre sans équivoque : Breaking Rainbow Takes a Weekend on a Laptop. Les paramètres du premier niveau de sécurité tombent en une cinquantaine d'heures sur un ordinateur portable. L'attaque exploite précisément la structure en couches ajoutée pour gagner en taille : elle offrait une prise supplémentaire pour localiser l'espace d'huile.

Le point qui doit rester : la sécurité d'un schéma multivarié ne se lit pas dans la taille de l'espace de recherche. Elle se lit dans la difficulté de retrouver la structure cachée, et cette difficulté-là est bien plus délicate à estimer. Le schéma UOV originel, sans les couches de Rainbow, survit et reste candidat — avec des clés publiques de plusieurs dizaines de kilooctets, mais des signatures d'une centaine d'octets.

Quiz · 1 question

Rainbow a été cassé alors que l'espace de recherche brut se comptait en 2^400. Pourquoi ?

  • Parce que le corps fini choisi était trop petit
  • Parce que l'attaque ne cherche pas une solution par force brute mais RETROUVE la structure cachée — l'espace d'huile
  • Parce que l'implémentation de référence contenait une erreur

Réponse : La taille de l'espace de recherche ne dit rien quand une structure y est cachée : casser un schéma multivarié consiste à localiser le sous-espace d'huile, pas à énumérer des solutions. La structure en couches de Rainbow — ajoutée pour réduire les clés — offrait justement une prise pour cela. C'est la même leçon qu'avec Ring-LWE au chapitre 5 : toute structure ajoutée pour gagner en taille est aussi une prise potentielle.

À vous

Exercice de code

Implémentez l'inversion par la trappe, puis comparez à la force brute — et lisez pourquoi la taille de l'espace de recherche n'a pas protégé Rainbow.

Point de départ

// La trappe « huile et vinaigre », celle de Rainbow et de UOV.
//
// n = 4 variables sur F_7 : deux VINAIGRE (x0, x1) et deux HUILE (x2, x3).
// Règle unique du système central : jamais de produit huile × huile.
// C'est cette absence qui rend le système inversible — et c'est elle que
// l'attaque de Beullens a fini par retrouver dans Rainbow.

const q = 7;
const V = [0, 1];   // indices vinaigre
const O = [2, 3];   // indices huile
const mod = (x) => ((x % q) + q) % q;
const inv = (a) => { for (let i = 1; i < q; i++) if (mod(a * i) === 1) return i; return null; };

// Deux polynômes quadratiques SANS terme huile × huile.
// coefQuad[k][i][j] = coefficient de x_i·x_j dans l'équation k.
const coefQuad = [
  [[3, 1, 2, 5], [0, 4, 6, 1], [0, 0, 0, 0], [0, 0, 0, 0]],
  [[2, 6, 1, 3], [0, 5, 2, 4], [0, 0, 0, 0], [0, 0, 0, 0]],
];
// Les deux dernières LIGNES sont nulles : aucun terme ne commence par une
// variable d'huile, donc aucun produit huile × huile n'existe.

function evaluer(k, x) {
  let t = 0;
  for (let i = 0; i < 4; i++) for (let j = 0; j < 4; j++) t += coefQuad[k][i][j] * x[i] * x[j];
  return mod(t);
}

// INVERSION AVEC LA TRAPPE : on fixe le vinaigre, le système devient LINÉAIRE
// en l'huile, et deux équations à deux inconnues se résolvent.
function inverserAvecTrappe(cible, vinaigre) {
  // À COMPLÉTER — pour chaque équation k, construire la ligne linéaire
  // [a_k2, a_k3 | b_k] obtenue en fixant x0 et x1, puis résoudre.
  //
  // Indication : le coefficient de x_h (h dans O) vaut
  //   somme sur i dans V de ( coefQuad[k][i][h] + coefQuad[k][h][i] ) * x_i
  // et le terme constant est l'évaluation avec l'huile mise à zéro.
  return null;
}

// INVERSION SANS LA TRAPPE : force brute sur toutes les variables.
let essais = 0;
function inverserSansTrappe(cible) {
  essais = 0;
  for (let a = 0; a < q; a++) for (let b = 0; b < q; b++)
    for (let c = 0; c < q; c++) for (let d = 0; d < q; d++) {
      essais++;
      const x = [a, b, c, d];
      if (evaluer(0, x) === cible[0] && evaluer(1, x) === cible[1]) return x;
    }
  return null;
}

const CIBLE = [4, 2];
const VINAIGRE = [3, 5];

const avec = inverserAvecTrappe(CIBLE, VINAIGRE);
console.log("avec trappe  :", JSON.stringify(avec),
  avec ? "→ " + JSON.stringify([evaluer(0, avec), evaluer(1, avec)]) : "");
const sans = inverserSansTrappe(CIBLE);
console.log("force brute  :", JSON.stringify(sans), `(${essais} essais)`);

console.log("\ncoût de la force brute selon la taille :");
for (const [corps, n] of [[7, 4], [16, 40], [16, 100], [256, 112]]) {
  console.log(`  F_${String(corps).padStart(3)}, n = ${String(n).padStart(3)}  →  2^${(n * Math.log2(corps)).toFixed(0)}`);
}

Solution

const q = 7;
const V = [0, 1];
const O = [2, 3];
const mod = (x) => ((x % q) + q) % q;
const inv = (a) => { for (let i = 1; i < q; i++) if (mod(a * i) === 1) return i; return null; };

const coefQuad = [
  [[3, 1, 2, 5], [0, 4, 6, 1], [0, 0, 0, 0], [0, 0, 0, 0]],
  [[2, 6, 1, 3], [0, 5, 2, 4], [0, 0, 0, 0], [0, 0, 0, 0]],
];

function evaluer(k, x) {
  let t = 0;
  for (let i = 0; i < 4; i++) for (let j = 0; j < 4; j++) t += coefQuad[k][i][j] * x[i] * x[j];
  return mod(t);
}

function inverserAvecTrappe(cible, vinaigre) {
  // Le vinaigre étant fixé, tout terme x_i·x_h avec i vinaigre et h huile
  // devient un coefficient CONSTANT devant x_h. Et comme il n'y a aucun
  // terme huile × huile, il ne reste rien de quadratique : le système est
  // linéaire. Voilà toute la trappe.
  const lignes = [0, 1].map((k) => {
    const coef = O.map((h) =>
      mod(V.reduce((t, i) => t + (coefQuad[k][i][h] + coefQuad[k][h][i]) * vinaigre[i], 0)),
    );
    const constante = evaluer(k, [vinaigre[0], vinaigre[1], 0, 0]);
    return [...coef, mod(cible[k] - constante)];
  });

  // Élimination de Gauss sur 2 × 2 dans F_7.
  let [L0, L1] = lignes;
  if (L0[0] === 0) [L0, L1] = [L1, L0];
  if (L0[0] === 0) return null;
  const p = inv(L0[0]);
  L0 = L0.map((v) => mod(v * p));
  L1 = L1.map((v, i) => mod(v - L1[0] * L0[i]));
  if (L1[1] === 0) return null;
  const x3 = mod(L1[2] * inv(L1[1]));
  const x2 = mod(L0[2] - L0[1] * x3);
  return [vinaigre[0], vinaigre[1], x2, x3];
}

let essais = 0;
function inverserSansTrappe(cible) {
  essais = 0;
  for (let a = 0; a < q; a++) for (let b = 0; b < q; b++)
    for (let c = 0; c < q; c++) for (let d = 0; d < q; d++) {
      essais++;
      const x = [a, b, c, d];
      if (evaluer(0, x) === cible[0] && evaluer(1, x) === cible[1]) return x;
    }
  return null;
}

const CIBLE = [4, 2];
const VINAIGRE = [3, 5];

const avec = inverserAvecTrappe(CIBLE, VINAIGRE);
console.log("avec trappe  :", JSON.stringify(avec),
  avec ? "→ " + JSON.stringify([evaluer(0, avec), evaluer(1, avec)]) : "");
const sans = inverserSansTrappe(CIBLE);
console.log("force brute  :", JSON.stringify(sans), `(${essais} essais)`);

console.log("\ncoût de la force brute selon la taille :");
for (const [corps, n] of [[7, 4], [16, 40], [16, 100], [256, 112]]) {
  console.log(`  F_${String(corps).padStart(3)}, n = ${String(n).padStart(3)}  →  2^${(n * Math.log2(corps)).toFixed(0)}`);
}

// Ce que l'exercice met à nu.
//
// La trappe n'est pas une clé secrète au sens habituel : c'est la
// CONNAISSANCE D'UN SOUS-ESPACE — celui des variables d'huile, sur lequel le
// système est linéaire. Fixez le vinaigre, et deux équations quadratiques
// deviennent deux équations linéaires.
//
// La clé publique d'un schéma multivarié n'est pas ce système-ci : c'est sa
// composition avec un mélange linéaire secret T, qui cache QUELLES
// combinaisons de variables forment l'espace d'huile. Casser le schéma, ce
// n'est donc pas résoudre un système quadratique aléatoire — c'est
// RETROUVER CE SOUS-ESPACE.
//
// Et c'est exactement ce qu'a fait Ward Beullens contre Rainbow en 2022. La
// structure en couches de Rainbow, ajoutée pour réduire la taille des clés,
// offrait une prise supplémentaire pour localiser l'espace d'huile. Les
// paramètres du premier niveau de sécurité sont tombés en une cinquantaine
// d'heures sur un ordinateur portable, alors que la dernière colonne
// ci-dessus prédisait 2^400 pour une attaque par force brute.
//
// Morale : la sécurité d'un schéma multivarié ne se lit PAS dans la taille
// de l'espace de recherche. Elle se lit dans la difficulté de retrouver la
// structure cachée — et cette difficulté-là est bien plus dure à estimer.

À retenir

Flashcards · 3 cartes

Que faut-il retenir de la cassure de SIKE en 2022 ?
Trois choses. L'examen public ne prouve rien : onze ans de scrutin n'ont pas révélé un théorème de 1997. Une cassure peut être totale et instantanée, sans érosion progressive ni fenêtre pour réagir. Et un déploiement hybride X25519 + SIKE n'aurait rien perdu ce jour-là — c'est l'argument décisif en faveur de l'hybridation.
Le problème des isogénies est-il cassé ?
Non. Ce qui est cassé, c'est la publication par SIDH des images de points de torsion auxiliaires, nécessaire à son protocole. Le problème général de recherche d'isogénies reste ouvert, et CSIDH comme SQIsign — qui ne publient pas ces points — restent debout. SQIsign offre les objets les plus compacts du paysage, au prix d'une lenteur considérable.
Où se trouve la trappe d'un schéma multivarié, et comment Rainbow est-il tombé ?
La trappe est la connaissance de l'espace d'HUILE, le sous-espace sur lequel le système central est linéaire une fois le vinaigre fixé. La clé publique le cache par un mélange linéaire. Beullens (2022) a montré que la structure en couches de Rainbow — ajoutée pour réduire les clés — permettait de localiser cet espace : niveau 1 cassé en 53 heures sur un portable.

QCM du bloc II — Les autres familles

Neuf questions sur les codes, le hachage, les isogénies et le multivarié. Plusieurs demandent de CHOISIR une famille pour un usage donné : c'est la compétence que ce bloc vise réellement.

QCM de bloc · 9 questions

Les autres familles

1. Le syndrome H·yᵀ d'un mot reçu y = c + e :

  • dépend à la fois du message et de l'erreur
  • ne dépend que du message émis
  • ne dépend que de l'erreur

Réponse : H·cᵀ = 0 pour tout mot de code, par définition de la matrice de contrôle. Il reste H·yᵀ = H·eᵀ : le message s'annule intégralement. C'est ce qui permet de reformuler le décodage comme « retrouver un vecteur de petit poids à partir de son syndrome », le problème NP-difficile sur lequel toute la famille repose.

2. On vous propose Classic McEliece pour le handshake TLS d'un site grand public. Que répondez-vous ?

  • Mauvais choix : chaque connexion retransmettrait une clé publique de 261 kio à 1 Mio
  • Excellent choix : ses chiffrés ne font que 96 octets
  • Impossible : McEliece n'est pas un mécanisme d'encapsulation

Réponse : Les chiffrés de 96 octets sont bien les plus courts du paysage post-quantique, et McEliece est bien un KEM — les deux autres options contiennent chacune une affirmation vraie. Mais en TLS, c'est la CLÉ qui circule à chaque connexion, et le mégaoctet est rédhibitoire. Le même schéma est excellent pour un tunnel durable où la clé est provisionnée une fois à l'installation.

3. Pourquoi le NIST a-t-il retenu HQC en secours de ML-KEM plutôt qu'un second schéma à réseaux ?

  • Parce que HQC est plus rapide que ML-KEM
  • Parce qu'un secours ne vaut que s'il repose sur une hypothèse de difficulté différente
  • Parce que HQC a des clés publiques plus petites

Réponse : HQC est plus lent que ML-KEM et ses objets sont plus gros — 2249 octets de clé contre 1184, et 4433 de chiffré contre 1088. Il n'est donc supérieur sur aucun critère de performance. Son intérêt est ailleurs : il repose sur le décodage de codes quasi-cycliques. Si les réseaux tombaient, deux schémas à réseaux tomberaient ensemble ; HQC resterait debout.

4. Combien de hachés compte le chemin d'authentification d'un arbre de Merkle à 1 048 576 feuilles ?

  • 1 048 576
  • 20
  • 2

Réponse : Un frère par niveau, et il y a log₂(2²⁰) = 20 niveaux. Les nœuds situés SUR le chemin, le vérificateur les recalcule lui-même — c'est tout l'intérêt. Cette croissance logarithmique est ce qui rend praticable une clé publique de 32 octets autorisant un million de signatures.

5. Une machine virtuelle qui signe avec XMSS est restaurée depuis un instantané pris la veille. Conséquence ?

  • Aucune : l'état se resynchronise au premier démarrage
  • Les signatures émises la veille deviennent invalides
  • Des index vont être rejoués : la clé doit être considérée comme compromise

Réponse : Rien ne resynchronise un état local, et les signatures déjà émises restent parfaitement vérifiables — c'est même le problème. La machine va réutiliser des index déjà consommés, et deux signatures au même index suffisent à forger. Restaurer une sauvegarde, cloner une VM ou répliquer derrière un répartiteur de charge sont des opérations banales qui détruisent une clé XMSS.

6. Comment SLH-DSA se passe-t-il d'état ?

  • En choisissant la feuille pseudo-aléatoirement d'après le message, avec des signatures à usages peu nombreux (FORS) tolérantes aux répétitions
  • En stockant l'index consommé dans la signature elle-même
  • En n'employant qu'une seule clé à usage unique, renouvelée à chaque signature

Réponse : Stocker l'index dans la signature ne servirait à rien : le signataire devrait toujours savoir lesquels il a déjà utilisés. La solution est de ne plus consommer les feuilles dans l'ordre mais d'en tirer une d'après le message. Comme des collisions redeviennent possibles, les signatures à usage unique cèdent la place à FORS, qui tolère quelques répétitions — au prix d'une signature de 8 à 30 kio.

7. Qu'a exactement cassé l'attaque de Castryck et Decru en 2022 ?

  • La publication par SIDH des images de points de torsion auxiliaires
  • Le problème général de recherche d'isogénies entre courbes supersingulières
  • L'implémentation de référence de SIKE, par un canal auxiliaire

Réponse : Ce n'était pas un défaut d'implémentation — le schéma lui-même est tombé. Mais le problème général d'isogénie reste ouvert, et c'est pourquoi CSIDH et SQIsign survivent : ils ne publient pas ces points de torsion. SIDH devait le faire pour que les deux parties convergent, et c'est cette information supplémentaire qu'un théorème de Kani de 1997 permet de retourner en attaque.

8. Rainbow disposait d'un espace de recherche de l'ordre de 2^400 et est tombé en 53 heures sur un ordinateur portable. Pourquoi ?

  • Le corps fini retenu était trop petit
  • L'implémentation de référence contenait une erreur
  • L'attaque n'énumère pas : elle retrouve la structure cachée, l'espace d'huile

Réponse : La taille de l'espace de recherche ne dit rien quand une structure y est cachée. Casser un schéma multivarié consiste à localiser le sous-espace sur lequel le système central est linéaire — pas à énumérer des solutions. La structure en couches de Rainbow, ajoutée pour réduire la taille des clés, offrait justement une prise pour cela.

9. Quelle conclusion opérationnelle tirer de SIKE et Rainbow pris ensemble ?

  • Il faut renoncer aux familles proposées après 2000
  • L'examen public ne prouve pas l'absence d'attaque, et l'hybridation est la seule protection contre une cassure surprise
  • Le processus de normalisation du NIST a échoué

Réponse : Le processus a fait son travail : les deux schémas sont tombés PENDANT l'évaluation, pas après normalisation. Et renoncer aux familles récentes reviendrait à renoncer aux réseaux, donc à tout. Ce que ces deux cassures établissent, c'est qu'onze ans de scrutin public ne garantissent rien et qu'une cassure peut être totale et instantanée — d'où l'hybridation, qui aurait laissé intacte la sécurité classique le 30 juillet 2022.