erplibre/test/test_qemu_sommes.py
Mathieu Benoit 3d0ed8915f [FIX] qemu images : vérifier les sommes publiées, partout et par défaut
La vérification existait sous un drapeau, et pour Ubuntu SEULEMENT : les
autres images arrivaient sans que rien ne les regarde, alors que leurs
éditeurs publient tous une somme. Six y entrent, relevées sur les dépôts
plutôt que devinées — Debian est en sha512, les familles RHEL nomment
« CHECKSUM », Rocky l'écrit en forme BSD, Arch et openSUSE posent une
somme par image. Fedora en reste dehors, son fichier portant un numéro de
construction que l'URL de l'image ne donne pas.

Injoignable n'arrête plus rien, un écart si. Le menu du cache sait aussi
oublier une URL, « --detient » faisant l'aperçu.

--- EN ---

Verification existed behind a flag, and for Ubuntu ONLY: the other images
arrived with nothing looking at them, though every publisher ships a sum.
Six now enter, read off the repositories rather than guessed — Debian is
sha512, the RHEL families name it "CHECKSUM", Rocky writes the BSD form,
Arch and openSUSE ship a sum per image. Fedora stays out, its file
carrying a build number the image URL does not give.

Unreachable no longer stops anything, a mismatch does. The cache menu can
also forget one URL, "--detient" serving as the preview.

Assisted-by: Claude Opus 5
2026-09-16 21:32:03 -04:00

164 lines
6.4 KiB
Python

#!/usr/bin/env python3
# © 2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
"""L'image téléchargée est vérifiée contre la somme que son éditeur publie.
La vérification existait, sous un drapeau, et pour Ubuntu SEULEMENT : les
huit autres images arrivaient sans que rien ne les regarde, alors que leurs
éditeurs publient tous une somme à côté. Le risque couvert est une image
substituée sur un miroir, et le coût une lecture du fichier qu'on vient de
télécharger.
Ni le nom du fichier de sommes ni l'algorithme ne se devinent, et les
extraits plus bas sont pris des dépôts eux-mêmes : Debian publie du sha512
quand tout le reste est en sha256, les familles RHEL nomment leur fichier
« CHECKSUM » sans dire l'algorithme, Rocky l'écrit en forme BSD, et Arch
comme openSUSE posent une somme PAR image.
"""
import importlib.util
import sys
import unittest
from pathlib import Path
sys.argv = ["todo.py"]
RACINE = Path(__file__).resolve().parents[1]
def _deploy_qemu():
chemin = RACINE / "script/qemu/deploy_qemu.py"
spec = importlib.util.spec_from_file_location("deploy_qemu", chemin)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
DQ = _deploy_qemu()
# Un extrait de chaque format, relevé sur le dépôt de la distribution. Les
# empreintes sont RACCOURCIES et inventées : ce qu'on éprouve est
# l'appariement, pas la valeur, et une somme réelle figée ici vieillirait au
# premier renouvellement d'image.
FORMATS = {
"ubuntu": (
"aaaa *autre-image-amd64.img\n"
"bbbb *ubuntu-24.04-server-cloudimg-amd64.img\n",
"ubuntu-24.04-server-cloudimg-amd64.img",
"bbbb",
),
"debian": (
"cccc debian-12-genericcloud-arm64.qcow2\n"
"dddd debian-12-genericcloud-amd64.qcow2\n",
"debian-12-genericcloud-amd64.qcow2",
"dddd",
),
"rocky": (
"# Rocky-10-GenericCloud.latest.x86_64.qcow2: 544997376 bytes\n"
"SHA256 (Rocky-10-GenericCloud.latest.x86_64.qcow2) = eeee\n",
"Rocky-10-GenericCloud.latest.x86_64.qcow2",
"eeee",
),
"opensuse": (
"ffff Leap-16.0-Minimal-VM.x86_64-Cloud.qcow2\n",
"Leap-16.0-Minimal-VM.x86_64-Cloud.qcow2",
"ffff",
),
}
class LAppariement(unittest.TestCase):
def test_every_published_format_is_read(self):
for distro, (texte, nom, attendu) in FORMATS.items():
with self.subTest(distro=distro):
self.assertEqual(attendu, DQ.expected_sum(texte, nom))
def test_a_longer_name_ending_the_same_is_not_a_match(self):
"""Le nom est comparé EN ENTIER, pas par sa fin. Un fichier de sommes
porte toutes les images d'un répertoire, et un nom plus long qui se
termine par celui qu'on cherche rendrait la somme d'une AUTRE image —
la vérification échouerait alors en accusant l'image juste.
La forme d'origine comparait la fin de la LIGNE, ce qui revient au
même défaut."""
texte = "1111 prefixe-image.qcow2\n2222 image.qcow2\n"
self.assertEqual("2222", DQ.expected_sum(texte, "image.qcow2"))
def test_signature_and_comment_lines_fall_away(self):
texte = (
"-----BEGIN PGP SIGNED MESSAGE-----\n"
"Hash: SHA256\n"
"\n"
"# une ligne de commentaire\n"
"3333 image.qcow2\n"
)
self.assertEqual("3333", DQ.expected_sum(texte, "image.qcow2"))
def test_an_absent_name_yields_nothing(self):
self.assertEqual("", DQ.expected_sum("4444 autre.qcow2\n", "x.qcow2"))
class OuChaqueDistributionPublie(unittest.TestCase):
def test_the_six_that_publish_are_resolved(self):
"""Une somme par répertoire chez les uns, à côté de l'image chez les
autres : le nom ne se déduit pas de l'image seule."""
for distro in DQ.SUMS_SOURCE:
with self.subTest(distro=distro):
url = f"https://exemple.invalid/d/image-{distro}.qcow2"
su, algo = DQ.sums_url_for(url, distro)
self.assertTrue(su.startswith("https://exemple.invalid/d/"))
self.assertIn(algo, ("sha256", "sha512"))
def test_debian_is_the_one_in_sha512(self):
"""L'algorithme vient de la table, pas du nom du fichier."""
self.assertEqual("sha512", DQ.SUMS_SOURCE["debian"][1])
for distro, (_nom, algo) in DQ.SUMS_SOURCE.items():
if distro != "debian":
with self.subTest(distro=distro):
self.assertEqual("sha256", algo)
def test_the_per_image_forms_carry_the_image_name(self):
for distro in ("opensuse", "arch"):
with self.subTest(distro=distro):
url = "https://exemple.invalid/d/monimage.qcow2"
su, _a = DQ.sums_url_for(url, distro)
self.assertIn("monimage.qcow2", su)
def test_a_distribution_that_publishes_nothing_says_so(self):
"""NixOS a sa somme ÉPINGLÉE dans le dépôt, vérifiée toujours ; son
image tierce ne publie rien à côté."""
self.assertNotIn("nixos", DQ.SUMS_SOURCE)
self.assertEqual(
("", ""), DQ.sums_url_for("https://x/i.qcow2", "nixos")
)
class LaVerificationEstLeDefaut(unittest.TestCase):
SRC = (RACINE / "script/qemu/deploy_qemu.py").read_text(encoding="utf-8")
def test_no_flag_is_needed_any_more(self):
self.assertIn("do_verify = not args.no_verify", self.SRC)
def test_skipping_it_must_be_asked_for(self):
self.assertIn('"--no-verify"', self.SRC)
def test_unreachable_sums_do_not_stop_a_deployment(self):
"""Un fichier de sommes injoignable est une panne de DISPONIBILITÉ —
miroir en travaux, réseau coupé. Refuser de déployer pour cela
rendrait la vérification plus coûteuse que le risque qu'elle couvre.
"""
i = self.SRC.index("def verify_sha256(")
corps = self.SRC[i : self.SRC.index("def hash_password", i)]
self.assertIn("image NON vérifiée", corps)
self.assertNotIn('sys.exit(f"Impossible', corps)
def test_a_mismatch_still_stops_everything(self):
"""Elle, c'est une panne d'INTÉGRITÉ."""
i = self.SRC.index("def verify_sha256(")
corps = self.SRC[i : self.SRC.index("def hash_password", i)]
self.assertIn("NON conforme", corps)
self.assertIn("image.unlink", corps)
if __name__ == "__main__":
unittest.main()