Cours 4 · Sécurité et implémentationLeçon 2 sur 2
Canaux auxiliaires et implémentation
6 h de lecture8 sections Version PDF
Temps, consommation, fautes ; attaques sur l'échantillonnage gaussien et le rejet ; masquage, temps constant, oracles de déchiffrement sur les KEM.
Le chapitre précédent s'est terminé sur une limite : aucun jeu de sécurité ne donne à l'adversaire un chronomètre, une sonde de courant ou un laser. C'est pourtant par là que passent la majorité des attaques réelles. Un schéma parfaitement prouvé peut tomber en quelques minutes sur une carte à puce, et cela n'invalide en rien la preuve — cela montre qu'elle parlait d'autre chose.
Ce chapitre est le plus long du cours. Ce n'est pas un hasard : c'est là que se joue la sécurité effective des déploiements.
Le modèle change
Les modèles du chapitre 11 traitent l'implémentation comme une boîte noire : l'adversaire soumet des entrées et lit des sorties. Un circuit réel n'est pas une boîte noire. Il met un certain temps, consomme un certain courant, émet un certain rayonnement, et peut être perturbé.
Chacune de ces grandeurs est une sortie supplémentaire, non prévue par la spécification, et souvent corrélée aux valeurs secrètes manipulées. C'est tout le domaine des canaux auxiliaires, ouvert par Kocher en 1996 avec les attaques temporelles, puis en 1999 avec l'analyse différentielle de consommation.
Une seule position diffère de l'attendue : la troisième, 199 au lieu de 200. L'attaquant ne le sait pas encore — c'est ce qu'il cherche à découvrir.
L'animation compare deux fonctions qui font la même chose et donnent le même résultat. La première sort dès qu'elle trouve une différence ; la seconde lit tout. Cette différence d'écriture, qui paraîtrait un détail d'optimisation à toute relecture ordinaire, sépare essais de 2048.
Le temps d'exécution
C'est le canal le plus accessible — il se mesure à distance, à travers un réseau — et le plus fréquent en pratique.
La règle est simple à énoncer et exigeante à respecter : aucun branchement, aucun accès mémoire indexé et aucune borne de boucle ne doivent dépendre d'une valeur secrète.
Les trois cas se valent en gravité. Un branchement fait varier le nombre d'instructions. Un accès mémoire indexé par un secret — une table de substitution, par exemple — fait varier l'état du cache, et un attaquant qui partage le processeur peut l'observer. Une borne de boucle secrète est le cas de l'animation.
Le point le plus contre-intuitif est qu'une division peut fuir. Sur beaucoup de processeurs, le temps de la division entière dépend des opérandes. C'est exactement ce qui s'est produit avec KyberSlash, signalée fin 2023 : l'implémentation de référence de ML-KEM contenait une division par dont le temps dépendait d'une valeur secrète, ce qui permettait de reconstruire la clé. Le schéma était prouvé, la spécification correcte, l'implémentation officielle. Le code fuyait.
Pire encore : écrire du code sans branchement ne suffit pas si le compilateur en réintroduit. Des cas documentés montrent un optimiseur transformant une expression écrite sans branchement — précisément pour éviter la fuite — en un saut conditionnel, parce que c'est plus rapide. La vérification doit donc porter sur le binaire produit, pas seulement sur la source. C'est une exigence que peu d'équipes appliquent.
Quelle règle résume les contre-mesures temporelles ?
Consommation, rayonnement, fautes
Sur un composant auquel l'attaquant a un accès physique — carte à puce, module de sécurité, objet connecté — trois autres canaux s'ouvrent.
L'analyse simple de consommation (SPA) lit le déroulement de l'algorithme sur une seule trace de courant. Les motifs sont souvent visibles à l'œil nu sur l'oscilloscope.
L'analyse différentielle (DPA) est bien plus puissante. Elle corrèle des milliers de traces avec une hypothèse sur un fragment de clé : la bonne hypothèse fait apparaître un pic de corrélation. Elle fonctionne même quand le signal est très inférieur au bruit, parce que la statistique accumule.
Les attaques par faute perturbent le calcul — variation de tension, impulsion laser, horloge dégradée — et exploitent le résultat erroné. Sur une signature déterministe, comparer une exécution correcte et une exécution fautée du même message livre le secret : c'est la raison pour laquelle la FIPS 204 fait de la signature aléatoire le mode par défaut, comme vu au chapitre 7.
Ce qui est propre à la cryptographie post-quantique
Un cours qui se contenterait des généralités ci-dessus manquerait l'essentiel. Les schémas à réseaux offrent des prises que RSA et les courbes elliptiques n'avaient pas.
L'échantillonnage. Tirer du bruit est une opération secrète, et elle est bien plus
complexe qu'un simple random(). L'échantillonneur gaussien de Falcon a été attaqué à
plusieurs reprises, et des attaques par cache visant l'échantillonnage gaussien de schémas
antérieurs sont documentées depuis 2016. C'est précisément pour cela que ML-KEM emploie une
binomiale centrée, qui se calcule en comptant des bits.
Le rejet d'échantillonnage. Le nombre de tours de ML-DSA est aléatoire et corrélé au secret : c'est justement parce que dépasse le seuil qu'on rejette. Un signataire dont le temps total laisse voir ce nombre rend une information exploitable. L'implémentation doit donc masquer le nombre de tours, ce qui n'est pas trivial quand il est intrinsèquement variable.
L'oracle d'échec de déchiffrement. Le rejet implicite du chapitre 6 ne ferme l'oracle que si les deux branches — succès et échec — sont indiscernables en temps comme en consommation. Une implémentation qui court-circuite le calcul de la clé bidon quand la ré-encapsulation réussit rouvre exactement le canal qu'on croyait fermé.
Le masquage coûte plus cher qu'ailleurs. La contre-mesure de référence consiste à partager chaque valeur secrète en plusieurs parts aléatoires, de sorte qu'aucune part seule ne corrèle au secret. Le problème est que les schémas à réseaux alternent de l'arithmétique modulaire — la NTT — et des opérations booléennes — hachage, compression, encodage. Or le masquage arithmétique et le masquage booléen ne sont pas compatibles : il faut convertir de l'un à l'autre, et ces conversions sont coûteuses. Un ML-KEM masqué à l'ordre 2 ou 3 est plusieurs fois plus lent que sa version nue.
Pourquoi le masquage est-il plus coûteux pour ML-KEM que pour AES ?
Que faire, concrètement
Quatre recommandations, dans l'ordre où elles doivent être appliquées.
Ne réimplémentez pas. Utilisez les implémentations éprouvées — celles qui sont auditées, testées en temps constant et maintenues. Écrire soi-même un ML-KEM correct est un projet de plusieurs mois-personnes, et l'exercice de ce chapitre montre à quel point la faute est facile.
Vérifiez le binaire. Des outils d'analyse dynamique détectent les branchements et accès mémoire dépendant d'entrées marquées comme secrètes. Intégrez-les à l'intégration continue, pas à une revue ponctuelle : une mise à jour de compilateur peut réintroduire une fuite dans un code inchangé.
Dimensionnez les contre-mesures selon le modèle de menace. Le masquage et la redondance contre les fautes ne se justifient que si l'attaquant a un accès physique. Pour un serveur en centre de données, le temps constant suffit et le reste est du gaspillage.
Testez les fautes si le matériel est exposé. Une carte à puce ou un objet connecté déployé sur le terrain doit être évalué en injection, pas seulement en analyse statique.
À vous
L'exercice remplace le chronomètre par un compteur d'octets lus — l'horloge d'un navigateur est trop grossière pour une mesure fiable, mais le principe est identique. Le rapport entre les deux colonnes est la totalité du chapitre.
Écrivez la boucle de forge octet par octet, puis lancez-la contre les deux implémentations : 2048 requêtes contre 2^64.
// Forger une étiquette d'authentification de 8 octets sans connaître le // secret, uniquement en observant le TEMPS de vérification. // // Mesurer des nanosecondes dans un navigateur n'est pas fiable : l'horloge // y est volontairement grossière. On remplace donc le chronomètre par un // COMPTEUR d'octets lus, qui est ce que le chronomètre mesurerait. Le // principe de l'attaque est identique ; seule la métrologie est simplifiée. const ETIQUETTE_SECRETE = [0x8f, 0x2a, 0xc7, 0x10, 0x55, 0xe3, 0x9b, 0x04]; let octetsLus = 0; // La victime : comparaison naïve, avec sortie anticipée. function verifierNaif(soumise) { for (let i = 0; i < ETIQUETTE_SECRETE.length; i++) { octetsLus++; if (soumise[i] !== ETIQUETTE_SECRETE[i]) return false; } return true; } // La même, en temps constant. function verifierConstant(soumise) { let diff = 0; for (let i = 0; i < ETIQUETTE_SECRETE.length; i++) { octetsLus++; diff |= soumise[i] ^ ETIQUETTE_SECRETE[i]; } return diff === 0; } // L'oracle : combien d'octets la victime a-t-elle lus ? function mesurer(verifier, soumise) { octetsLus = 0; const ok = verifier(soumise); return { ok, cout: octetsLus }; } let requetes = 0; function forger(verifier) { requetes = 0; const trouvee = new Array(8).fill(0); for (let position = 0; position < 8; position++) { // À COMPLÉTER — pour chaque valeur d'octet de 0 à 255, mesurer le coût // et retenir celle qui en provoque le PLUS : c'est la bonne, puisque // la boucle est allée un cran plus loin. trouvee[position] = 0; } return trouvee; } const hex = (t) => t.map((b) => b.toString(16).padStart(2, "0")).join(" "); console.log("secret réel :", hex(ETIQUETTE_SECRETE)); const contreNaif = forger(verifierNaif); console.log("contre le naïf :", hex(contreNaif), verifierNaif(contreNaif) ? "→ ACCEPTÉE" : "→ refusée", `(${requetes} requêtes)`); const contreConstant = forger(verifierConstant); console.log("contre le constant :", hex(contreConstant), verifierConstant(contreConstant) ? "→ ACCEPTÉE" : "→ refusée", `(${requetes} requêtes)`); console.log("\nforce brute nécessaire : 2^" + (8 * 8));
À retenir
QCM du bloc III — Sécurité et implémentation
Huit questions sur ce qu'une preuve garantit et sur ce qu'elle laisse ouvert. Les distracteurs y sont plus retors qu'ailleurs, parce que les erreurs de ce bloc sont des erreurs de raisonnement, pas de mémoire.
Un schéma seulement IND-CPA est déployé face à un attaquant qui peut soumettre des chiffrés au déchiffrement.
Vous avez parcouru les 8 sections.
Marquez-la terminée pour faire avancer votre parcours, ou revenez sur un point avant de passer à la suite.