Fuite par le temps à l'authentification — preuve et méthode
Comment nous savons que l'authentification de l'interface web ne révèle pas, par sa durée de réponse, ce que son message d'erreur refuse de dire. Méthodologie, défauts trouvés, données brutes, et commandes pour tout rejouer.
Ce document répond à une question précise, posée par un client, un auditeur, ou l'équipe elle-même dans deux ans :
« Comment savez-vous que l'authentification ne fuit pas par le timing ? »
Il donne la réponse avec ses données, et non sous forme d'affirmation.
1. Ce qui est en jeu
L'écran de connexion affiche un message d'échec invariant : il ne dit jamais laquelle des causes s'applique — compte inconnu, verrouillé, expiré, mot de passe faux, ou compte valide sans rôle LFSi. Ce choix est délibéré et documenté ; il empêche deux fuites :
- l'énumération de comptes — distinguer « ce compte n'existe pas » de « mot de passe erroné » révèle quels comptes existent ;
- l'oracle de mot de passe — le cas « compte sans rôle » n'est atteint qu'après une vérification
crypt(3)réussie. Un message propre à ce cas confirmerait donc à un attaquant qu'il vient de trouver le mot de passe d'un compte qui ne peut pas se connecter ici —rootau premier chef, dont le mot de passe est le secret de brise-glace.
Mais un message invariant ne suffit pas. Si un cas de refus traverse moins de travail que les autres, l'écart de durée le trahit, et l'oracle se rétablit par le chronomètre. C'est cette seconde propriété — l'invariance en temps — que ce document établit.
2. Deux fuites réelles, trouvées et fermées
Aucune n'avait été détectée par les tests, les linters ou la revue de code. Seule la mesure les a révélées.
| Défaut | Écart mesuré | Correctif |
|---|---|---|
roleOf() n'était appelé que pour le cas « sans rôle » — deux lectures de fichiers de plus que les autres refus |
+71 µs de médiane | 6f597c7 — résolution inconditionnelle, chemin de refus unique |
Le champ d'un compte verrouillé (préfixé !) était passé à crypt(3), qui échouait sans calculer yescrypt |
0,40 ms contre 7,5 ms — facteur 19 | eda86e3 — leurre utilisé dès que l'empreinte n'est pas exploitable |
Le second est le plus grave : « ce compte est verrouillé » devenait lisible à distance, avec 7 ms d'écart quand la gigue d'un réseau local se compte en dixièmes de milliseconde. Un seul échantillon suffisait.
Au passage, le leurre lui-même (dummyHash, censé faire payer un calcul complet aux comptes inexistants) n'était pas une chaîne de réglage yescrypt valide : crypt(3) y répondait par son jeton d'échec, donc sans calculer. Tout compte inexistant empruntait le chemin court. Il est désormais produit par crypt(3) lui-même et sa calculabilité est vérifiée par un test.
3. Méthode
Le banc mesure six cas — les cinq refus plus le succès — et compare leurs distributions de latence.
Entrelacement. Un tour = un échantillon de chaque cas. Mesurer les cas en séquence attribuerait au premier toute dérive de la machine pendant la campagne.
Ordre tiré au hasard à chaque tour, graine journalisée. Sans mélange, un effet de position dans le tour est indiscernable d'un effet de chemin de code — les deux se sont révélés de magnitude comparable. Sans graine, deux campagnes ne seraient plus comparables et aucune anomalie ne serait ré-observable.
Assertion d'issue avant chronométrage. Chaque cas doit produire son motif attendu, sous peine d'échec immédiat : « six chemins distincts » est une garantie, pas une espérance.
Refus de mesurer plutôt que mesurer faux. Le banc s'abstient, en disant pourquoi, si la libc ne sait pas calculer le format du leurre, ou si l'empreinte fournie n'est pas celle du mot de passe présenté.
Critères de lecture, chiffrés
Le banc calcule et tranche lui-même :
- Oracle plausible si l'étendue entre les six médianes dépasse le tiers de la dispersion
p10–p90intra-cas — l'écart entre cas devient alors du même ordre que le bruit propre à chacun, donc séparable par moyennage. - Suspect même sous le seuil si un même cas occupe le même extrême sur deux campagnes — un chemin plus court se classe toujours du même côté.
Critère abandonné, et pourquoi : « un écart qui change de signe est du bruit ». Il comparait chaque cas à une référence elle-même bruitée ; quand celle-ci dérive, les six écarts se décalent en bloc. Une campagne réelle les a tous rendus négatifs. Le signe ne jugeait rien.
4. Données brutes
Toutes les campagnes ci-dessous ont été menées sur le nœud de build (aarch64, libxcrypt avec yescrypt), 100 tours après chauffe.
4.1 Campagnes retenues — instrument à ordre mélangé
Référence de comparaison : « mot de passe erroné », c'est-à-dire ce qu'un attaquant obtient en tâtonnant.
| # | graine | rapport étendue/dispersion | plus rapide | plus lent | « sans rôle » vs réf. |
|---|---|---|---|---|---|
| A | 1786874463865153751 |
0,219 | mdp erroné | expiré | +41,1 µs |
| B | 1786874488064079929 |
0,095 | mdp erroné | verrouillé | +26,5 µs |
| C | 1786874521955218987 |
0,069 | verrouillé | absent | +1,2 µs |
| D | 1786874548956841500 |
0,091 | mdp erroné | absent | +6,5 µs |
| E | 1786876191158328033 |
0,046 | mdp erroné | absent | +2,8 µs |
Seuil : 0,333. Les cinq campagnes sont sous le seuil, la meilleure d'un facteur sept.
Campagne E, détail (la plus propre ; mot de passe de référence de même longueur que les autres) :
| cas | p10 | médiane | p90 | écart / réf. |
|---|---|---|---|---|
| 1 absent | 7,005 ms | 7,161 ms | 7,459 ms | +20,8 µs |
| 2 verrouillé | 6,996 ms | 7,151 ms | 7,388 ms | +10,8 µs |
| 3 expiré | 7,013 ms | 7,157 ms | 7,536 ms | +16,6 µs |
| 4 mdp erroné | 6,989 ms | 7,140 ms | 7,610 ms | référence |
| 5 sans rôle | 7,020 ms | 7,143 ms | 7,327 ms | +2,8 µs |
| 6 succès | 7,032 ms | 7,155 ms | 7,480 ms | +14,6 µs |
Médianes par position dans le tour : 7,151 / 7,148 / 7,148 / 7,150 / 7,144 / 7,173 ms — l'ordre étant mélangé, un écart ici désignerait la machine, non un chemin.
4.2 Combinaison des cinq campagnes
Pour la paire qui compte — « sans rôle » contre « mot de passe erroné » — l'erreur-type de la médiane est estimée depuis la dispersion (p90 − p10 ≈ 2,56 σ, ET ≈ 1,253 σ/√n) :
| campagne | écart | ET | écart/ET |
|---|---|---|---|
| A | +41,1 µs | 22,2 µs | +1,85 |
| B | +26,5 µs | 23,2 µs | +1,14 |
| C | +1,2 µs | 19,1 µs | +0,06 |
| D | +6,5 µs | 21,4 µs | +0,30 |
| E | +2,8 µs | 22,2 µs | +0,12 |
Pondération inverse-variance : +14,4 µs ± 9,6 µs, soit z = 1,49 — sous le seuil usuel de 2, et décroissant à mesure que l'instrument s'améliorait (1,72 → 1,60 → 1,49), ce qui est la signature d'un résidu d'instrument et non d'un chemin.
Sur la campagne E, l'étendue entre les six cas vaut 0,94 erreur-type d'une seule médiane, et l'étendue par position la dépasse : le bruit de la machine domine toute différence de chemin.
4.3 Campagnes écartées, et pourquoi
Elles figurent ici parce qu'un dossier qui ne montrerait que les mesures favorables ne vaudrait rien.
| campagnes | défaut de l'instrument | conséquence |
|---|---|---|
avant c1f540b |
ordre d'entrelacement fixe | position dans le tour et cas confondus — étendues de même ordre (68/74/25/32 µs contre 100/45/27/40 µs) |
avant 4fc4122 |
l'empreinte de la fixture n'était pas celle du mot de passe présenté | !ok l'emportait avant role == "" : trois cas sur six retombaient sur le même chemin |
avant 8538267 |
mot de passe de référence plus court que les autres (7 octets contre 10) | le cas de référence différait par deux choses à la fois |
avant eda86e3 |
dummyHash non calculable |
le cas « compte absent » empruntait un chemin court |
Chacune de ces campagnes a imprimé un tableau parfaitement crédible. C'est la raison d'être des garde-fous décrits en §3.
5. Rejouer la mesure
Sur le nœud de build ou la cible — partout où crypt(3) calcule yescrypt :
h="$(mkpasswd -m yescrypt motdepasse)"
LFSI_MESURE_LATENCE=1 LFSI_LATENCE_HASH="$h" \
go test -count=1 -run TestMesureLatenceEntrelacee -v ./cmd/privhelper
Pour rejouer une campagne à l'identique, ajouter sa graine :
LFSI_LATENCE_GRAINE=1786876191158328033
Le banc refuse de s'exécuter — en indiquant le motif — si l'hôte ne sait pas calculer le format du leurre, ou si l'empreinte ne correspond pas au mot de passe de la mesure.
6. Conclusion, et ce qu'elle ne dit pas
Les deux fuites identifiées sont fermées, et la propriété d'invariance temporelle est vérifiée par cinq campagnes indépendantes, toutes sous le seuil, avec un résidu de +14,4 µs à z = 1,49.
Sur l'exploitabilité du résidu : 14 µs face à une gigue réseau de plusieurs centaines de microsecondes, avec un verrouillage anti-martèlement à 5 tentatives par quart d'heure, par compte et par adresse, place toute exploitation statistique hors de portée pratique.
Ce que ce document n'affirme pas :
- il ne couvre que les chemins de
actionAuth(assistant privilégié). D'autres composants exposés — traitement de la requête HTTP, gestion de session — n'ont pas fait l'objet du même examen ; - la mesure porte sur une machine donnée ; un matériel très différent mériterait sa propre campagne ;
- l'absence de signal sous le seuil ne prouve pas l'absence absolue d'écart : elle établit qu'aucun écart n'est séparable du bruit dans ces conditions.
Toute modification de actionAuth doit rejouer cette mesure — la règle est inscrite au brief de réalisation, avec ses critères chiffrés.