Aller au contenu

Quand l’implémentation parle : temps, cache, fautes et oracles

Quand l’implémentation parle : temps, cache, fautes et oracles

Section intitulée « Quand l’implémentation parle : temps, cache, fautes et oracles »

Un algorithme se juge sur le papier ; une implémentation se juge sur un processeur, dans un centre de données, à côté d’un voisin curieux. Entre les deux s’intercalent des grandeurs que la spécification ignore : le temps de calcul, l’état des caches, la consommation électrique, le comportement face à une erreur matérielle, et la prose des messages d’erreur. Chacune peut dépendre d’un secret, et chacune peut donc le révéler. Ce chapitre décrit ces canaux auxiliaires (side channels), les fautes provoquées et les oracles, puis les protections qui les ferment.

→ Lire l’explication complète de la marmotte

Une attaque par canal auxiliaire n’attaque pas les mathématiques de l’algorithme : elle observe une grandeur physique ou logique produite par son exécution. Six sources reviennent sans cesse. Le temps d’exécution varie quand le code prend des branches différentes ou s’arrête plus tôt selon les données. Le cache du processeur conserve la trace des adresses mémoire lues, donc des index de tableau utilisés. L’énergie consommée et le rayonnement électromagnétique dépendent des bits manipulés. Les fautes, provoquées par un pic de tension, une impulsion laser ou une horloge déréglée, font produire un résultat faux qui peut trahir la clé. Les messages d’erreur distinguent parfois des causes de refus qui auraient dû rester confondues. Les journaux, enfin, conservent ces distinctions plus longtemps et pour plus de lecteurs que prévu.

Contrairement à un bug fonctionnel, une fuite auxiliaire produit le bon résultat et passe tous les tests de conformité : elle ne se manifeste que pour qui mesure autre chose que la sortie, ce qui la rend durable.

L’exemple canonique est la vérification d’un code d’authentification de message, le MAC (Message Authentication Code). Une comparaison naïve parcourt les octets et sort à la première différence :

def compare_naif(attendu, recu):
if len(attendu) != len(recu):
return False
for a, b in zip(attendu, recu):
if a != b:
return False # sortie anticipée : le temps dépend de la position de l'écart
return True

Un attaquant qui soumet des tags commençant par chacune des 256 valeurs possibles observe que l’une d’elles fait répondre le serveur un peu plus tard : le premier octet est trouvé. Il passe au deuxième, et ainsi de suite. Un tag de 16 octets tombe en au plus 16 × 256 = 4 096 séries d’essais, au lieu des 2^128 qu’exigerait une recherche exhaustive. L’opérateur == des langages courants et memcmp en C se comportent ainsi : leur seul contrat est la rapidité.

La comparaison en temps constant accumule les différences sans jamais sortir avant la fin :

def compare_constant(attendu, recu):
if len(attendu) != len(recu):
return False
diff = 0
for a, b in zip(attendu, recu):
diff |= a ^ b # aucune branche ne dépend des données
return diff == 0

En pratique, on n’écrit pas cette fonction soi-même : les bibliothèques en fournissent une, hmac.compare_digest en Python, crypto.timingSafeEqual en Node.js, subtle.ConstantTimeCompare en Go, CRYPTO_memcmp dans OpenSSL. Les fonctions de vérification d’AEAD (Authenticated Encryption with Associated Data) font cette comparaison en interne : raison de plus pour ne jamais vérifier un tag à la main.

Les branches conditionnelles posent le même problème à plus grande échelle. L’exponentiation modulaire « carré et multiplication » de RSA ne multiplie que lorsque le bit courant de l’exposant secret vaut 1 : la durée totale dépend du poids de la clé, et la trace fine de la consommation montre chaque bit. Paul Kocher a publié en 1996 la première attaque temporelle sur ce principe.

L’idée que la latence du réseau noie ces différences est rassurante et fausse. Le bruit réseau est aléatoire, la fuite est systématique : en répétant la mesure et en comparant des moyennes, ou mieux des médianes et des quantiles bas, l’attaquant fait converger l’estimation. David Brumley et Dan Boneh ont extrait en 2003 une clé privée RSA d’un serveur OpenSSL à travers un réseau local. Le bruit augmente le nombre de requêtes nécessaires ; il ne transforme pas une fuite en secret.

Les implémentations logicielles rapides de l’AES (Advanced Encryption Standard) utilisaient des tables précalculées, les T-tables, indexées par des octets qui dépendent directement de la clé et du message. Chaque accès charge une ligne de cache ; un attaquant qui partage ce cache peut savoir quelles lignes ont été touchées. Daniel Bernstein, puis Dag Arne Osvik, Adi Shamir et Eran Tromer ont montré en 2005 que cela suffit à reconstituer des clés AES. Les techniques se sont affinées depuis, comme Flush+Reload (Yarom et Falkner, 2014), qui vide une ligne de mémoire partagée puis mesure si la victime l’a rechargée.

Les instructions matérielles AES-NI des processeurs x86, et leurs équivalents sur ARM, exécutent un tour d’AES sans table en mémoire : c’est la meilleure protection contre cette famille d’attaques. Sur une plateforme qui en est dépourvue, il faut une implémentation dite « bitsliced » ou à temps constant, plus lente, plutôt que la version à tables.

Le cloud mutualisé place sur le même processeur physique, avec des caches partagés, des clients qui ne se connaissent pas. Thomas Ristenpart et ses coauteurs ont montré dès 2009 qu’on pouvait faire cohabiter volontairement une machine virtuelle avec une cible chez un fournisseur public, puis en observer l’activité ; Spectre et Meltdown (2018) ont ajouté l’exécution spéculative à la liste des canaux. Les clés de grande valeur ne se manipulent donc pas sur du matériel partagé : instance dédiée ou module matériel.

Sur une carte à puce ou un objet connecté, l’attaquant tient le matériel dans ses mains. L’analyse simple de consommation (Simple Power Analysis) lit directement sur une trace les opérations exécutées, par exemple la succession des carrés et des multiplications. L’analyse différentielle (Differential Power Analysis, Kocher, Jaffe et Jun, 1999) corrèle des milliers de traces avec une hypothèse sur un octet de clé, et retrouve cet octet même lorsque chaque trace isolée semble muette. Le rayonnement électromagnétique se prête aux mêmes analyses.

Un élément sécurisé (secure element) ou un module matériel de sécurité intègre des contre-mesures : masquage des valeurs intermédiaires par de l’aléa, bruit, capteurs de tension et de lumière, détection de fautes. Il réduit fortement le risque sans l’annuler : sa robustesse vaut celle du profil d’attaquant évalué lors de sa certification, et des produits certifiés ont déjà été cassés (voir les vulnérabilités documentées des HSM).

Pour accélérer la signature RSA, on calcule séparément modulo p et modulo q, puis on recombine par le théorème des restes chinois (CRT, Chinese Remainder Theorem). En 1997, Dan Boneh, Richard DeMillo et Richard Lipton, alors chez Bellcore, ont montré qu’une seule faute dans l’une des deux moitiés suffit à factoriser le module. Si la moitié modulo p est juste et la moitié modulo q fausse, la signature fautive s’ vérifie s’^e ≡ m (mod p) mais pas modulo q. Alors pgcd(s’^e − m, N) = p : la clé privée est entièrement retrouvée, à partir d’une seule signature et du message signé. La contre-mesure est simple et peu coûteuse : vérifier la signature avec la clé publique avant de la renvoyer, et ne rien renvoyer en cas d’incohérence. L’optimisation CRT sans cette vérification est l’archétype de l’optimisation naïve d’un code cryptographique.

Un oracle est un système qui répond à une question que l’attaquant ne devrait pas pouvoir poser. Le cas le plus connu est l’oracle de remplissage. En 1998, Daniel Bleichenbacher a montré qu’un serveur qui indique si un message chiffré avec RSA PKCS#1 v1.5 a un remplissage valide permet, au prix d’un grand nombre de requêtes, de déchiffrer un message sans la clé ; la brèche la question qui rend le module bavard en détaille le mécanisme. En 2002, Serge Vaudenay a transposé l’idée au mode CBC : un serveur qui distingue « remplissage incorrect » de « MAC incorrect » déchiffre le message octet par octet pour qui l’interroge (voir l’oracle de remplissage CBC).

La distinction n’a pas besoin d’être écrite en toutes lettres. Un code HTTP (Hypertext Transfer Protocol), une taille de réponse, un délai ou une ligne de journal différents suffisent. L’attaque Lucky Thirteen (2013) exploitait ainsi, dans TLS (Transport Layer Security), une différence de quelques cycles dans le calcul du MAC selon la longueur du remplissage.

Le blinding (aveuglement) protège l’exponentiation privée. Au lieu de calculer m^d mod N, on tire un aléa r, on calcule (m · r^e)^d = m^d · r mod N, puis on multiplie par r^(−1). La durée et la consommation de l’opération secrète dépendent alors d’une valeur que l’attaquant ne connaît pas et qui change à chaque appel. La vérification redondante traite les fautes : calculer deux fois, ou vérifier le résultat par l’opération inverse, et refuser de sortir une valeur incohérente.

Un service jouet déchiffre un message AES-CBC puis vérifie un HMAC (Hash-based Message Authentication Code) dans l’ordre « déchiffrer, retirer le remplissage, vérifier le MAC ». Il renvoie 400 padding invalide ou 400 MAC invalide. L’expérience consiste à envoyer mille messages dont on modifie le dernier octet de l’avant-dernier bloc, puis à compter les deux réponses : environ une valeur sur 256 donne un remplissage valide, donc un « MAC invalide ». Cette asymétrie suffit à l’attaque. Si l’on unifie les messages mais que le remplissage invalide court-circuite le calcul du MAC, les temps de réponse forment deux distributions séparées : l’oracle est toujours là.

La correction tient en trois règles. D’abord, vérifier l’intégrité avant de déchiffrer : avec un schéma encrypt-then-MAC, le MAC porte sur le chiffré, et un chiffré altéré est rejeté sans que le remplissage soit jamais examiné. Ensuite, comparer le MAC en temps constant. Enfin, renvoyer une seule erreur, 400 message invalide, et journaliser la même chose. Mieux encore, remplacer l’assemblage par un AEAD (AES-GCM ou ChaCha20-Poly1305), qui ne comporte pas de remplissage et dont la vérification est faite par la bibliothèque.

Les journaux dangereux d’une API cryptographique

Section intitulée « Les journaux dangereux d’une API cryptographique »

Une revue de journaux d’API (Application Programming Interface) chiffrante cherche au minimum : les clés et secrets de session (y compris un fichier SSLKEYLOGFILE oublié en production), les clairs avant chiffrement, le tag attendu affiché à côté du tag reçu (un cadeau), la cause précise d’un échec de déchiffrement et les traces de pile exposant des valeurs intermédiaires. Un journal lisible par une équipe entière transforme une fuite locale en oracle permanent.

Un code écrit sans branche peut en retrouver une après optimisation : aucun langage courant ne fait du temps constant une garantie, et le compilateur a le droit de produire un saut conditionnel s’il le juge plus rapide. D’où trois précautions : préférer les primitives des bibliothèques maintenues au code maison, vérifier le binaire avec des outils dédiés (dudect, par tests statistiques sur les temps mesurés, ou Valgrind instrumenté pour signaler les branches qui dépendent de données marquées secrètes), et recommencer à chaque changement de compilateur ou d’options.

Une fuite auxiliaire n’est pas un bug fonctionnel : le résultat est juste, c’est son contexte qui parle. Une erreur uniforme n’est pas un silence total : le client apprend que sa requête a échoué, seule la cause reste interne, dans une métrique agrégée ou un journal restreint. Un module cryptographique matériel réduit la surface d’attaque physique sans la supprimer, et ne protège en rien contre un oracle logiciel construit autour de lui. Enfin, un algorithme à nombre d’opérations fixe n’est pas une implémentation à temps constant : entre les deux s’interposent le compilateur, le cache et le processeur, qui n’ont jamais lu la spécification.

Sans ces protections, un algorithme réputé sûr devient interrogeable : chaque différence de temps ou de message d’erreur fournit un peu d’information, et l’attaquant n’a qu’à répéter la question, comme le montre la brèche « la question qui rend le module bavard ». Rien n’apparaît comme une intrusion : les requêtes sont valides, les réponses aussi, et les journaux enregistrent au mieux une série d’échecs banals. Des journaux trop détaillés deviennent même l’oracle, lisible par quiconque y a accès. Sur une machine partagée, le voisin n’a pas besoin d’envoyer la moindre requête : il lui suffit d’observer le cache. La clé ainsi reconstituée ne laisse aucune trace de vol, puisqu’elle n’a jamais été copiée.

Un programme qui chiffre ou vérifie des données ne produit pas seulement un résultat : il met un certain temps à répondre, occupe la mémoire d’une certaine façon, consomme de l’électricité et renvoie parfois des messages d’erreur. Si l’une de ces traces varie selon la valeur d’un secret, un attaquant peut la mesurer et en déduire ce secret, morceau par morceau. Le cas le plus simple est la comparaison : un code qui compare deux valeurs caractère par caractère et s’arrête à la première différence répond plus vite quand le début est faux. En répétant les essais et en faisant des statistiques, l’attaquant retrouve la valeur attendue malgré les variations du réseau. La mémoire rapide du processeur, appelée cache, révèle de même quelles parties d’un tableau ont été lues, y compris entre clients qui partagent une machine chez un hébergeur. Sur du matériel embarqué, la consommation électrique et les ondes émises suffisent parfois. Une erreur provoquée volontairement pendant un calcul peut aussi produire un résultat faux qui révèle la clé. Enfin, un message d’erreur différent selon la cause du refus transforme le programme en oracle, c’est-à-dire en source de réponses exploitables. Les protections consistent à écrire du code dont la durée ne dépend pas des secrets, à mélanger les calculs avec une valeur aléatoire, à vérifier un résultat avant de le renvoyer et à renvoyer une erreur identique quelle que soit la cause.

À lire aussi

Pages voisines

: les deux pages se citent l’une l’autre.