urMQTT v0.3.0 : chiffrement du disque, un démarrage qui refuse une image modifiée, et ce que nous avons cassé nous-mêmes
Ce qui s'est passé après le troisième texte sur urMQTT : chiffrement du disque au repos avec un coût mesuré, un test qui modifie un octet du noyau et montre que le chargeur le refuse, un dossier d'audit avec 889 sites unsafe justifiés, les erreurs que nous avons trouvées nous-mêmes (et une affirmation que nous avions écrite à tort), et la version v0.3.0 avec la liste de ce qui n'est pas encore prouvé, y compris une CI au rouge.

Le troisième texte, urMQTT après le VPS, se terminait par une liste « La suite » : un broker de démonstration public (D1), une deuxième série sur le flux unique lent, le chemin en clair face à Mosquitto sur le VPS et, « après la version », le bare metal, l'IOMMU, Secure Boot et le chiffrement du disque. Ce texte dit ce qui a été fait, ce qui ne l'a pas été, et ce que nous avons trouvé en chemin. Le chiffrement du disque et une partie de Secure Boot sont finalement entrés avant la version, parce qu'ils se trouvaient sur la route de l'audit. La deuxième série sur le flux unique et le chemin en clair face à Mosquitto n'ont pas été faits.
La remarque préalable reste la même : le projet est en pré-alpha, la version vise les VPS avec KVM, et les chiffres mesurent du code, des tests et des expériences précises, pas la maturité en production.
Comment cela s'est déroulé dans le temps
Depuis le dernier texte (1er novembre, 20:41) jusqu'à aujourd'hui (3 novembre, 02:43), la branche principale compte 37 nouveaux commits, 215 au total. Comme avant, le projet est construit avec Claude Code (les commits portent Co-Authored-By: Claude), et cette fois trois branches ont travaillé en parallèle (D1, H8, préparation de la version), de sorte que l'ordre des commits n'est pas strictement l'ordre du travail. Les chiffres sont calculés avec la même méthode que dans le dernier texte (tous les fichiers .rs hors de vendor/ et fuzz/, les fonctions #[test], les titres ## ADR- dans docs/DECISIONS.md) et vérifiés sur l'ancien commit, qui donne les mêmes chiffres qu'alors.
| Moment | Commit | Heure | Lignes de Rust | Tests | ADR | Commits |
|---|---|---|---|---|---|---|
| dernier texte | d21e3ec | 1er nov., 20:41 | 112 408 | 926 | 126 | 178 |
| aujourd'hui | f1a7fbc | 3 nov., 02:43 | 115 148 | 962 | 133 | 215 |
La croissance du code est modeste (+2 740 lignes, +36 tests), car cette fois il n'y a pas eu beaucoup de nouveau code : l'essentiel du travail, ce sont des tests, des scripts, de la documentation et des vérifications. Les scripts et le JavaScript ne comptent pas dans le nombre de lignes.
Chiffrement du disque au repos (S7)
Ce que nous avions prévu. Un chiffrement facultatif de la partition de données, désactivé par défaut, avec un coût mesuré. Ce que nous avons construit (ADR-240) :
- Une couche sous le disque.
CryptDevenveloppe le périphérique bloc sousDisk, de sorte que la disposition, la table des tailles, SQLite, la télémétrie et les sauvegardes travaillent sur du texte en clair et ignorent tout du chiffrement. L'algorithme est AES-256-XTS (IEEE 1619), un secteur de 512 octets par unité de données, et non un bloc de 4 Kio comme prévu : le stockage écrit par secteurs, et une unité de 4 Kio transformerait chaque écriture de 512 octets en lecture-modification-écriture qu'une coupure de courant peut couper en deux. - Le superbloc reste en clair. Les deux emplacements du superbloc portent un indicateur et une vérification de clé (HMAC-SHA-256 d'une étiquette et de l'UUID du disque), jamais la clé. Tout le reste de la partition est chiffré. Un secteur entièrement nul sur le support est lu comme nul et n'est pas déchiffré, si bien que le discard, l'espace jamais écrit et l'agrandissement du disque fonctionnent comme avant.
- Configuration.
storage.encryption = "aes-256-xts"etstorage.disk_key(un fichier de 64 octets de clé). Le format du blob passe à 4 uniquement lorsque le chiffrement est activé, de sorte que toutes les autres images démarrent toujours sur des noyaux plus anciens. Un lecteur plus ancien refuse l'indicateur inconnu dans le superbloc. - Sans AES matériel : refus. Le noyau refuse
storage.encryptionsur un processeur sans instructions AES (ou lorsque[cpu] disableles désactive), au lieu d'exécuter discrètement une version logicielle lente. - La conversion se fait hors ligne. Le noyau ne chiffre ni ne déchiffre jamais un disque au démarrage.
urmqtt-disk encrypt,decryptetrekeyle font avec la VM éteinte : un enregistrement de conversion est écrit en premier et effacé en dernier (tant qu'il existe, le noyau refuse le disque), des morceaux de 1 Mio sont d'abord sauvegardés dans un journal sur l'hôte, puis la copie convertie est écrite, et le superbloc est basculé en dernier. Essayé avec une coupure de courant après chaque écriture pour les trois procédures, proprement et à moitié écrit, avec le cache abandonné et conservé : après--resume, toujours un disque valide avec toutes ses données.
Mesuré. Débit XTS par cœur sur Apple silicon (un Mio de secteurs dans une boucle) : 3 109 Mo/s en chiffrement et 3 103 Mo/s en déchiffrement avec AES matériel, contre 252 et 258 Mo/s en logiciel. C'est pourquoi nous n'autorisons pas le logiciel. db-bench dans une VM (aarch64, HVF, 5 000 insertions en 50 transactions, cinq exécutions de chaque variante, en alternance) :
| op/s, cinq exécutions | médiane | |
|---|---|---|
| chiffrement désactivé | 49 239 ; 47 401 ; 50 802 ; 53 417 ; 46 531 | 49 239 |
| chiffrement activé | 57 790 ; 46 173 ; 45 939 ; 52 787 ; 49 632 | 49 632 |
La différence des médianes est de +0,8 %, et la dispersion au sein d'une même variante (46 000 à 58 000) est plus grande, donc le coût n'est pas mesurable dans ce test. C'est d'ailleurs ce qu'on attend : db-bench écrit environ 6 Mo sur toute l'exécution, soit environ 2 ms d'AES, et un commit attend le flush, pas le chiffrement.
L'entrée Diagnostic affiche maintenant aussi AES-XTS à côté d'AES-GCM (sur HVF, le noyau mesure environ 800 Mio/s pour XTS contre environ 560 Mio/s pour AES-GCM) et indique si l'AES matériel est disponible.

Ce que XTS n'apporte pas. Il n'y a pas d'authentification : un secteur chiffré modifié se déchiffre en bruit par blocs de 16 octets (les CRC et le scrubber le détectent), et quelqu'un qui peut écrire sur le volume peut échanger ou restaurer des secteurs entiers entre deux instantanés. Il protège un volume ou un instantané volé, pas une ISO volée, parce que la clé se trouve dans la configuration de l'image. Restent visibles : la GPT, le superbloc, l'UUID et les secteurs qui ont déjà été écrits.
Ce que nous avons trouvé en chemin dans notre propre travail. En écrivant le chapitre sur la cryptographie, nous avons constaté deux choses. L'étape de multiplication du tweak dans XTS comportait un branchement dépendant d'un bit dérivé de la clé ; elle est maintenant sans branchement. Et l'affirmation « vérifié contre le vecteur 10 d'IEEE 1619 » ne couvrait que le premier bloc chiffré ; il existe maintenant aussi une implémentation de référence naïve (arithmétique u128, bloc par bloc) qui vérifie des secteurs entiers dans les deux sens sur 200 clés et secteurs aléatoires, et la documentation dit ce qui a réellement été vérifié.
Un démarrage qui refuse une image modifiée (H8)
L'exigence issue de la liste d'audit était simple et jusqu'ici non vérifiée : aucun test ne montrait qu'un noyau modifié ne démarre pas. L'empreinte épinglée dans limine.conf existait, mais sans preuve.
- Un test qui modifie un octet.
scripts/tamper.pyinverse un bit dans chaque copie du noyau (ou du module de configuration) à l'intérieur d'une ISO construite ou d'une image brute, etSMOKE_ONLY=integritymontre que le chargeur refuse : 24 vérifications réussies, sur aarch64 UEFI, x86_64 UEFI et x86_64 BIOS, avec des témoins qui démarrent. Le message de refus est lu sur le port série grâce à un commutateur réservé aux tests. Sur BIOS, le message n'arrive qu'à l'écran, donc ce test affirme seulement que la machine ne démarre pas. - La configuration du chargeur enregistrée (ADR-250).
limine enroll-configagit surBOOTAA64.EFI,BOOTX64.EFIet l'étape BIOS, si bien qu'unlimine.confmodifié (une entrée de menu ou un argument ajouté) est lui aussi refusé.urmqtt-config pack --isol'enregistre à nouveau. - Le chemin Secure Boot (ADR-251).
scripts/sign-loaders.sh(clés de test, signature, vérification, un magasin de variables OVMF),mkiso.sh --sb-keyet le groupesecurebootsur x86_64 : une ISO signée démarre et se construit de façon reproductible, tandis qu'une ISO non signée, une signée avec une mauvaise clé et une modifiée après la signature reçoiventAccess Denied(10 vérifications réussies). Limine refuse toujours un noyau modifié sous Secure Boot. - Les règles de l'entrée d'installation (ADR-253). Le plan parlait d'un seul mot sur la ligne de commande,
install. Le noyau de production en accepte en fait trois (install,live,diag), et seulinstallécrit. Le test (9 vérifications) l'exécute sur trois machines avec un disque vierge dont l'empreinte ne doit pas changer. - Secret de l'image (ADR-252). Un avertissement bruyant lorsqu'une image de production est construite hors de la CI, et l'analyse de l'image, des journaux de construction et des journaux série à la recherche d'octets de clé dans de nombreux encodages (11 vérifications).
- TPM, seulement l'ADR-254. Ce que signifierait sceller la clé du serveur dans un vTPM ou un TPM 2.0. Recommandation : ne pas le construire pour une version orientée VPS. Nous n'avons pas vérifié quels fournisseurs proposent un vTPM.
Ce que cela ne signifie PAS (consigné sous H8-W1 à H8-W6 dans AUDIT.md) : un disque installé ne démarre pas sous Secure Boot, car son module de configuration n'a pas d'empreinte (ADR-217) ; une ISO signée ne peut pas être configurée ensuite (configurer, puis signer) ; le chemin BIOS n'a pas de signature ; il n'y a pas de protection contre le retour à une version plus ancienne ; sur aarch64, le firmware n'applique pas ici Secure Boot, donc seule la signature du chargeur a été vérifiée ; il n'y a pas de clé de projet, donc les images de la version ne sont pas signées pour Secure Boot ; et aucune machine physique n'a été essayée.
Le dossier d'audit (H10)
docs/AUDIT.md est désormais un dossier qu'un auditeur externe peut utiliser sans poser de questions : le système sur une page, la base à laquelle nous faisons confiance (89 612 lignes de Rust maison dans l'image, 67 paquets tiers liés), les entrées et leurs analyseurs, la matrice des protections avec leurs tests (et les protections sans tests), les faiblesses avec leur statut, un tableau de chaque boucle et allocation atteignable par le réseau, avec sa borne et son test, les instructions de construction et de test, un périmètre proposé pour un audit externe en dix domaines et une liste red team de 31 attaques avec leurs résultats. SECURITY.md contient un tableau STRIDE par composant.
- Une correction du premier texte. Le texte du 29 octobre dit que chaque bloc
unsafeporte un commentaire// SAFETY:vérifié par un lint. Ce n'était pas exact : le lint (deny(unsafe_code)) limite seulement les endroits oùunsafepeut apparaître, et les commentaires sont suivis depuis H0 par un inventaire généré, qui trouvait déjà alors environ 30 sites sans justification et, lors de l'audit, 108 (parce que de nouveaux types de sites étaient apparus entre-temps, par exemple les attributs#[unsafe(no_mangle)]etlink_section). C'est donc l'inventaire, et non le lint, qui les maintient maintenant à zéro. - 889 sites
unsafe, chacun avec une justification écrite. Les 108 qui n'en avaient pas (surtout#[unsafe(no_mangle)]etlink_section, quelquesunsafe fn) ont reçu un commentaire (un agent IA l'a écrit, uniquement des commentaires, et j'en ai relu un échantillon) ; l'inventaire généré dans la CI signale tout nouveau site sans justification. Une justification est une affirmation à examiner, pas une preuve, et c'est le point 5 du périmètre proposé. - Cinq constats ouverts restent ouverts, consignés : pas de fuzzing des analyseurs PCI et virtio, pas de test qui écrit dans les tables de pages après leur verrouillage, le temps d'exécution de P-256, X25519 et Ed25519 n'a pas été mesuré, pas de fuzzing de fichiers SQLite fabriqués exprès (limité par H6), et 53 des 67 paquets liés sont des exemptions
cargo-vet, pas des audits.
Les erreurs que nous avons trouvées nous-mêmes
- Les secrets de trafic TLS n'étaient pas effacés (H0-W2, ouvert depuis le premier jour de l'audit) : la table des secrets capturés était un
Vecqueremoveetswap_removedéplaçaient en laissant des copies. Maintenant la table fonctionne par emplacements : un emplacement est effacé sur place lorsqu'il est pris, abandonné ou évincé, et rien n'est déplacé. - Un branchement dépendant de la clé dans XTS (ci-dessus).
log-slow-floodsur x86_64 échouait (« log ring full »), y compris sur les sources de v0.2.1. La cause n'était pas un défaut, mais le dimensionnement du test : le scénario faisait tourner chaque cœur à 25 ms, alors que l'ADR-090 l'avait mesuré à 50 ms. Sur x86_64, COM1 est une E/S par port interceptée à chaque octet, un cœur qui écrit dans les puits met environ 9 ms par ligne, et trois cœurs à 25 ms demandent plus d'une seconde de travail par seconde, si bien que les anneaux se remplissent (le débordement voulu). Le test tourne maintenant à 50 ms sur x86_64 (réussi, 30 sur 30 dans l'exécution qui inclut les scénarios de base) ; sur aarch64, il reste à 25 ms.- La fusion des branches a cassé les anciens modèles. Le nouvel enregistrement de la configuration dans H8 exigeait un marqueur de fin dans
limine.conf, que les modèles antérieurs à H8 n'ont pas, de sorte queurmqtt-config pack --isoet la page du constructeur refusaient toute version antérieure à H8, y compris les v0.2.x que la page propose. Ce sont les tests du constructeur dans le navigateur qui l'ont détecté (4 sur 100 ont échoué), pas le test propre à H8. Corrigé (un modèle sans marqueur est configuré comme avant) et un test ajouté qui échoue avec l'erreur d'origine lorsqu'on retire la correction. - Notre documentation affirmait à tort que x86_64 n'avait été vérifié que dans un émulateur. X11 a fait tourner l'image de production sur un vrai VPS KVM de Contabo (4 vCPU AMD EPYC) avec du TLS mutuel sous charge. Corrigé partout : x86_64 a tourné sur un vrai VPS KVM au commit H7 et dans un émulateur, mais le code ajouté depuis (chiffrement, intégrité du démarrage) n'a pas tourné sur KVM, et n'a pas été essayé sur du matériel physique.
D1 et l'offre « votre service en unikernel »
Le broker de démonstration public est construit, mais non publié : la configuration (mode live, ACL demo/{id}/#, MQTT en clair seulement pour le pont), un script qui produit une CA de test publiée et des certificats de démonstration, un pont WebSocket vers TCP (uniquement la bibliothèque standard de Python, limites de connexions, de débit et de taille, vérification de l'origine), une page « Essayez urMQTT en direct » avec un client MQTT en JavaScript pur, un runbook et un modèle de menaces. Testé en bouclage et dans QEMU (18 scénarios, 26 tests du pont). Non testé : un vrai VPS, le DNS, Caddy, l'image de conteneur. Il faut un VPS distinct et une autorisation explicite pour l'exposition publique.
En outre, le site propose maintenant une offre, Unikernel (15 langues, sans prix) : l'adaptation de votre service pour qu'il fonctionne en unikernel (évaluation de faisabilité, adaptation, pilote, support), avec urMQTT comme référence. Le texte indique explicitement les limites de la base actuelle : IPv4, TCP seulement en serveur, KVM, Rust comme choix naturel, C seulement comme travailleur isolé.
La version v0.3.0
Le tag v0.3.0 a été posé le 3 novembre sur le commit f1a7fbc, et le workflow de publication a réussi. La version est marquée comme pre-release et contient le noyau de développement aarch64, l'ISO et l'image, l'ISO modèle (noyaux pour aarch64 et x86_64 et une configuration vide, que urmqtt-config pack --iso et la page du constructeur transforment en votre image), un SBOM CycloneDX, SHA256SUMS et sa signature minisign, ainsi que des attestations de provenance SLSA. Vérifié après la publication, dans un répertoire vide : la signature de SHA256SUMS est valide, les cinq fichiers correspondent aux sommes de contrôle, les deux attestations (le noyau, et l'ISO avec le prédicat CycloneDX) passent, le SBOM compte 221 composants, et le modèle se configure (pack --iso réenregistre six copies du chargeur). La clé de signature est toujours une clé de test.
L'ordre importait et mérite d'être consigné : la page du constructeur a été mise à jour avant le tag (elle connaît le format 4 et tous les modèles plus anciens), et c'est seulement ensuite que le miroir sur aldo.tech a repris la version. Le miroir l'a listée comme signée et vérifiée (ok, format de configuration 4, sha256 du modèle égal à celui de SHA256SUMS), et la page en ligne la propose comme version par défaut et construit une ISO dans le navigateur en environ 20 secondes. Les notes de version disent aussi ce qu'on écrit rarement : la CI sur GitHub n'est pas verte (voir plus bas), le support ne porte que sur la dernière version (v0.2.1 et les précédentes s'arrêtent), et l'audit a été fait par le projet lui-même.
Sur le site, nous avons failli nous nuire à nous-mêmes en chemin : le script de publication du site (publish.sh) exécute rsync --delete, et le miroir des versions appartient à un autre utilisateur, de sorte que chaque publication se terminait par l'erreur 23 (et aurait cassé le bouton « Publier » de l'administration du blog). Corrigé par une exclusion pour /urmqtt/releases/. Un constat semblable : deploy.sh avec --delete supprime la page du constructeur du générateur, il faut donc la remettre avant de publier.
Ce qui n'est pas encore prouvé
- La CI sur GitHub est rouge. Pas seulement aujourd'hui : la dernière exécution de la CI complète avant le tag avait sept jobs en échec, et les exécutions remontant jusqu'au 31 octobre sont pour la plupart rouges. Ont échoué : le lint (avertissements shellcheck dans les scripts), l'interopérabilité TLS (tous les cas wolfSSL), la chaîne d'approvisionnement (la liste d'exemptions
vet-exemptions.mdn'est pas à jour sous Linux), le fuzz smoke (les certificats de développement manquent dans ce job), les sanitizers (LeakSanitizer signale une fuite dans les tests), les tests sous Linux aarch64 (un test de l'outil échoue avec « Broken pipe ») et une preuve Kani pour la roue de temporisation (timer wheel) ne passe pas. Nous n'avons pas encore trié ce qui relève de l'environnement et ce qui est un vrai défaut ; tant que nous ne l'aurons pas fait, nous n'affirmons pas que la version a passé la CI. - La clé de signature est une clé de test, et les images de la version sont des builds de développement avec des certificats de test (sauf le modèle).
- Les traductions des pages et de ce texte dans les 13 autres langues ont été écrites par une IA et n'ont pas été relues par des locuteurs natifs.
- Le chiffrement du disque n'a pas été mesuré sur x86_64 (ni sur KVM ni sur du matériel physique), et ni le débit de la télémétrie ni le temps de démarrage avec chiffrement n'ont été mesurés. Il n'est pas sur la page du constructeur (il faut une boîte de dialogue pour la clé).
- Il n'y a pas d'audit de sécurité indépendant. Le périmètre est écrit ; nous n'avons pas d'auditeur.
- Secure Boot est partiel (ci-dessus), sans machine physique.
- Bare metal, IOMMU, broker de démonstration public : pas terminé ou pas publié.
La suite
- Trier et réparer la CI (l'environnement d'abord, puis la preuve Kani et le test qui échoue sous Linux aarch64), car nous ne considérons pas comme terminée une version sans CI verte.
- Une décision sur D1 (VPS distinct, DNS, autorisation d'exposition publique).
- Fuzzing des analyseurs PCI et virtio et un test pour les tables de pages verrouillées (H0-W9, H0-W10).
- Une boîte de dialogue pour la clé sur la page du constructeur, afin que le chiffrement soit disponible sans ligne de commande.
- Après la version : bare metal (X12 à X16) et IOMMU, dans un ordre qui dépend de l'inventaire matériel du PC de test.
Le code est sur github.com/zeljan-alduk/urMQTT ; docs/PROGRESS.md reste la seule source de vérité sur ce qui fonctionne réellement, docs/PERFORMANCE.md sur ce qui a été mesuré, et docs/AUDIT.md sur ce qui n'est pas encore prouvé. Pré-alpha, comme avant ; les rapports de bogues et les signalements de sécurité selon les instructions de SECURITY.md sont les bienvenus.
Commentaires
Règles des commentaires