Un endpoint de connexion vérifie un mot de passe avec Argon2. La propriété de sécurité n’est pas que le bon mot de passe passe et que le mauvais échoue — ça, n’importe quelle comparaison de chaînes le fait. La propriété, c’est qu’un mauvais mot de passe coûte du temps à l’attaquant. Les paramètres d’Argon2 existent pour être lents ; c’est ce qui rend une liste de mots impraticable.

Mesuré sur l’application testée : quarante tentatives avec un mauvais mot de passe, 13,4 secondes, environ 336 ms chacune.

Maintenant imaginez un refactor qui remplace le hachage par une comparaison de chaînes. Même 401. Même corps de réponse. Chaque vérification fonctionnelle reste verte. La seule chose visible, c’est que l’endpoint est devenu deux ordres de grandeur plus rapide — et personne ne regarde ça. C’est exactement la régression que personne ne remarque.

Voilà ce que je voulais écrire dans probatum, mon runner de vérification, et que je ne pouvais pas :

[[check]]
name = "a wrong password costs real time"
post = "http://127.0.0.1:3000/auth/login"
body = '{"username":"editor","password":"wrong"}'
expect = 401
min_ms = 20        # ce que je veux, et que je ne peux pas écrire

max_ms existait depuis la v0.5.0 : une réponse trop lente échoue. L’inverse, non. L’issue #8 a été ouverte le 15 août.

Pourquoi pas un contournement

probatum a une échappatoire prévue : run, une commande arbitraire. Pour mesurer un temps, on pense à date +%s%N. J’ai essayé, et ça ne survit pas au contact de l’image :

$ docker run --rm --entrypoint sh ghcr.io/probatum-org/probatum:0.9.0 \
    -c 'command -v date seq curl; date +%s%N'
/bin/date
1786805331

Pas de curl, pas de seq, et le date de BusyBox n’implémente pas %N : %s%N renvoie des secondes entières, et tout calcul de durée construit dessus est du non-sens. Ce n’est pas un reproche à l’image — une image minimale est le bon choix. Mais ça veut dire que run ne peut pas remplacer une assertion de temps comme il remplace d’autres manques. Il fallait le mot-clé.

Quarante-deux jours

Toutes les issues précédentes de probatum s’étaient fermées le jour même : post: (#1), le pot à cookies (#5), absent sur HTTP (#6), put/patch/delete (#7). Chacune a produit sa release dans la journée. #8 a attendu cinq semaines, pendant lesquelles le dépôt n’a pas reçu un commit.

Puis, en trois jours, quatre releases.

v0.12.0 ferme #8. min_ms est le miroir de max_ms, avec la même distinction qui compte : une réponse trop rapide est un échec (exit 1, avec le temps mesuré comme preuve), pas un « n’a pas pu observer » (exit 2). Le login qui répond en 2 ms est faux, pas indisponible. Et le README ajoute une remarque juste : un plancher est une borne plus solide qu’un plafond, parce que le bruit ralentit une mesure et ne l’accélère presque jamais. Déclarer min_ms au-dessus de max_ms est une erreur de configuration.

v0.11.0 est la plus grosse, et elle casse le schéma JSON — de la version 2 à la version 4. Les checks se regroupent en scénarios nommés, avec des étapes numérotées, une portée par OS, et des valeurs capturées qu’une étape produit et qu’une autre consomme. Deux décisions dedans que je veux souligner :

  • Les étapes doivent être déclarées dans l’ordre croissant. [auth.2] écrit après [auth.10] est une erreur, pas quelque chose que le runner réordonne gentiment. Le fichier se lit toujours dans l’ordre où il s’exécute.
  • Un scénario exclu par sa portée OS est quand même validé entièrement avant d’être filtré. Une exclusion ne peut pas cacher une faute de frappe.

Le changement de schéma est cassant et dit tel quel : un consommateur qui lit log_file sans condition, ou qui fait un switch sur le statut, doit gérer le nouveau statut Excluded et le null. Pas de compatibilité silencieuse.

v0.13.0 est le groupe dependabot. v0.13.1 est la release qui m’a le plus appris.

Les 100 000 premières lignes

Depuis la v0.6.0, la sortie capturée d’une commande est plafonnée en mémoire. C’est une bonne règle : un run qui écrit des gigaoctets ne doit pas faire exploser le runner.

Sauf que les règles contains et absent s’évaluaient sur cette copie, pas sur ce que la commande avait réellement produit. Au-delà de la borne, ça mentait dans les deux sens : un contains échouait sur une sortie qui contenait bien le motif — une fausse alerte — et un absent passait par-dessus un FATAL — un faux vert. Le filtre de crash d’un service et le résumé d’une commande réussie avaient le même angle mort.

C’est le défaut dont j’ai passé l’été à parler, dans cidx, dans verbose : un succès annoncé pour un travail qui n’a pas eu lieu. Et il était dans l’outil dont le seul rôle est d’empêcher ça. La v0.13.1 évalue les règles ligne par ligne, au fil de la sortie ; la copie bornée ne sert plus qu’au diagnostic. Le plafond limite ce qu’on garde comme preuve, jamais ce qu’on vérifie. Deux checks de dogfooding verrouillent les deux sens.

Je ne connais pas de meilleure illustration de pourquoi on écrit ces outils pour soi d’abord : on ne trouve pas ça en relisant le code. On le trouve en s’en servant.

Pendant ce temps, verbose

La même semaine, verbose a fait la sienne — 25 PRs, 49 commits, la plus grosse de son histoire — et l’une d’elles dit la même chose que min_ms, depuis l’autre bout de la pile.

proofs.native_stack: 96 déclare qu’une règle ne consommera pas plus de 96 octets de pile native. Ce n’est pas un commentaire ni un vœu : le compilateur compte — les emplacements d’entrée, le rbp sauvegardé, les temporaires de formatage — et refuse d’émettre le binaire si le total dépasse. 96 passe une borne de 96 et échoue à 95. Une déclaration suffisante laisse le code émis identique à l’octet ; une déclaration fausse n’a pas de binaire du tout. Et les analyses que le compilateur ne sait pas faire — pile auto-hébergée, WASM — sont refusées plutôt que devinées.

Une borne basse sur un temps, une borne haute sur une pile. Dans les deux cas, l’auteur écrit un chiffre, et l’outil le tient ou le refuse. Il n’y a pas de troisième état où il fait semblant.

Et la limite habituelle : verbose n’a toujours pas de tag depuis la v0.10.0, ~133 commits d’avance. probatum, lui, en a coupé quatre en trois jours — après cinq semaines sans en couper aucune. Deux rythmes, aucun des deux n’est une cadence. Ce sont des fenêtres.

Trop vite, c’est faux. Ça vaut pour un login, et ça vaut pour un test qui passe sans avoir tout lu.