Quand l'implémentation trahit le secret

Les attaques temporelles (timing)

Le plus simple des canaux auxiliaires à comprendre est le temps. Si la durée d'un calcul dépend de la valeur du secret, alors chronométrer ce calcul, c'est déjà espionner le secret.

L'idée : le temps parle

Un ordinateur ne met pas toujours le même temps à faire une opération. Une boucle qui s'arrête plus tôt, une branche if qui saute une étape, un accès mémoire déjà présent en cache : tout cela raccourcit ou allonge l'exécution. Si ces variations sont corrélées au secret, l'attaquant les exploite.

L'exemple classique : la comparaison naïve

Imaginons un système qui vérifie un mot de passe (ou une signature) en comparant octet par octet, et qui s'arrête au premier octet faux :

comparer(saisie, secret):
    pour i de 0 à longueur - 1:
        si saisie[i] != secret[i]:
            retourner FAUX        <- sortie anticipée !
    retourner VRAI

Le défaut : plus la saisie a de premiers octets corrects, plus la fonction tourne longtemps avant de renvoyer FAUX. En mesurant ce temps, l'attaquant devine les octets un par un :

secret = "K7z..."

essai "A???"  -> échec au 1er octet   (rapide)
essai "K???"  -> échec au 2e octet    (un peu plus long)  => 'K' est bon !
essai "K7??"  -> échec au 3e octet    (encore plus long)  => '7' est bon !

On ne teste plus 256^n combinaisons, mais 256 × n : le secret tombe octet par octet. Une attaque exponentielle devient linéaire.

La même faille dans RSA

L'exponentiation modulaire m = c^d mod n du déchiffrement RSA parcourt les bits de l'exposant secret d. Une implémentation naïve fait une multiplication supplémentaire uniquement quand le bit vaut 1 :

bit = 1  ->  élévation au carré + multiplication   (lent)
bit = 0  ->  élévation au carré seulement           (rapide)

Le temps total révèle alors le nombre — voire la position — des bits à 1 de la clé privée. Des attaques réelles ont récupéré des clés TLS ainsi.

La parade : le temps constant

La contre-mesure porte un nom précis : le code à temps constant (constant-time). Sa règle est stricte :

Le temps d'exécution ne doit dépendre ni des branches, ni des accès mémoire qui dépendent du secret.

Concrètement, une comparaison à temps constant examine tous les octets, sans jamais s'arrêter en cours de route :

comparer_ct(saisie, secret):
    diff = 0
    pour i de 0 à longueur - 1:
        diff = diff | (saisie[i] XOR secret[i])   # accumule, ne sort jamais
    retourner (diff == 0)

Le résultat est identique, mais la durée est la même quel que soit le secret : le chronomètre n'apprend plus rien. Les bibliothèques sérieuses fournissent des fonctions dédiées (par exemple des comparaisons dites constant-time) précisément pour cela.

En résumé

Si le temps de calcul dépend du secret — comparaison à sortie anticipée, exponentiation branchante — le chronomètre révèle le secret bit à bit, transformant une recherche exponentielle en recherche linéaire. La parade est le code à temps constant : pas de branche ni d'accès mémoire dépendant du secret, une durée toujours identique.