urMQTT v0.3.0: Verschlüsselung des Datenträgers, ein Bootvorgang, der ein verändertes Image ablehnt, und was wir selbst kaputt gemacht haben
Was nach dem dritten Text über urMQTT geschah: Verschlüsselung des Datenträgers im Ruhezustand mit gemessenem Aufwand, ein Test, der ein Byte im Kernel verändert und zeigt, dass der Loader ihn ablehnt, ein Audit-Paket mit 889 begründeten unsafe-Stellen, die Fehler, die wir selbst gefunden haben (und eine Behauptung, die wir falsch geschrieben hatten), und das Release v0.3.0 samt einer Liste dessen, was noch nicht bewiesen ist, einschließlich eines roten CI.

Der dritte Text, urMQTT nach dem VPS, endete mit einer Liste „Wie es weitergeht": ein öffentlicher Demo-Broker (D1), eine zweite Runde zum langsamen Einzelfluss, der Klartextpfad gegen Mosquitto auf dem VPS und, „nach dem Release", Bare Metal, IOMMU, Secure Boot und Datenträger-Verschlüsselung. Dieser Text sagt, was davon gemacht wurde, was nicht, und was wir unterwegs gefunden haben. Die Datenträger-Verschlüsselung und ein Teil von Secure Boot sind noch vor dem Release dazugekommen, weil sie auf dem Weg zum Audit lagen. Die zweite Runde zum Einzelfluss und der Klartextpfad gegen Mosquitto sind nicht gemacht worden.
Der Hinweis vorab bleibt derselbe: Das Projekt ist Pre-Alpha, das Release zielt auf VPS mit KVM, und die Zahlen messen Code, Tests und bestimmte Versuche, nicht die Reife im Produktivbetrieb.
Wie es sich zeitlich darstellte
Seit dem letzten Text (1. November, 20:41) bis heute (3. November, 02:43) hat der Hauptzweig 37 neue Commits, insgesamt 215. Wie zuvor entsteht das Projekt mit Claude Code (die Commits tragen Co-Authored-By: Claude), und diesmal liefen drei Zweige parallel (D1, H8, Release-Vorbereitung), die Reihenfolge der Commits ist also nicht streng die Reihenfolge der Arbeit. Die Zahlen sind nach derselben Methode berechnet wie im letzten Text (alle .rs-Dateien außerhalb von vendor/ und fuzz/, #[test]-Funktionen, Überschriften ## ADR- in docs/DECISIONS.md) und am alten Commit gegengeprüft, der dieselben Zahlen liefert wie damals.
| Zeitpunkt | Commit | Zeit | Zeilen Rust | Tests | ADRs | Commits |
|---|---|---|---|---|---|---|
| letzter Text | d21e3ec | 1. Nov., 20:41 | 112.408 | 926 | 126 | 178 |
| heute | f1a7fbc | 3. Nov., 02:43 | 115.148 | 962 | 133 | 215 |
Das Codewachstum ist bescheiden (+2.740 Zeilen, +36 Tests), denn diesmal gab es nicht viel neuen Code: Der größte Teil der Arbeit sind Tests, Skripte, Dokumentation und Prüfungen. Skripte und JavaScript zählen nicht zur Zeilenzahl.
Verschlüsselung des Datenträgers im Ruhezustand (S7)
Was wir geplant haben. Optionale Verschlüsselung der Datenpartition, standardmäßig aus, mit gemessenem Aufwand. Was wir gebaut haben (ADR-240):
- Eine Schicht unter dem Datenträger.
CryptDevumhüllt das Blockgerät unterhalb vonDisk, sodass Layout, Größentabelle, SQLite, Telemetrie und Backups auf Klartext arbeiten und nichts von der Verschlüsselung wissen. Der Algorithmus ist AES-256-XTS (IEEE 1619), ein Sektor von 512 Byte pro Dateneinheit, nicht ein 4-KiB-Block wie im Plan: Der Speicher schreibt in Sektoren, und eine Einheit von 4 KiB würde jeden Schreibvorgang von 512 Byte in ein Lesen-Ändern-Schreiben verwandeln, das ein Stromausfall in der Mitte zerreißen kann. - Der Superblock bleibt offen. Die zwei Superblock-Slots tragen ein Flag und eine Schlüsselprüfung (HMAC-SHA-256 aus einer Bezeichnung und der UUID des Datenträgers), niemals den Schlüssel. Alles andere in der Partition ist verschlüsselt. Ein Sektor, der auf dem Medium vollständig null ist, wird als null gelesen und nicht entschlüsselt, sodass Discard, nie beschriebener Platz und das Wachsen des Datenträgers wie zuvor funktionieren.
- Konfiguration.
storage.encryption = "aes-256-xts"undstorage.disk_key(eine Datei mit 64 Byte Schlüssel). Das Blob-Format wird 4, nur wenn die Verschlüsselung eingeschaltet ist, sodass alle anderen Images weiterhin auf älteren Kernels starten. Ein älterer Leser lehnt das unbekannte Flag im Superblock ab. - Ohne Hardware-AES: Ablehnung. Der Kernel lehnt
storage.encryptionauf einem Prozessor ohne AES-Befehle ab (oder wenn[cpu] disablesie abschaltet), statt stillschweigend eine langsame Softwareversion laufen zu lassen. - Die Umwandlung geschieht offline. Der Kernel verschlüsselt oder entschlüsselt einen Datenträger beim Start nie.
urmqtt-disk encrypt,decryptundrekeyerledigen das bei ausgeschalteter VM: Ein Umwandlungsprotokoll wird zuerst geschrieben und zuletzt gelöscht (solange es existiert, lehnt der Kernel den Datenträger ab), Stücke von 1 MiB werden zuerst in ein Journal auf dem Host gesichert, dann wird die umgewandelte Kopie geschrieben, und der Superblock wird zuletzt umgeschaltet. Erprobt mit einem Stromausfall nach jedem einzelnen Schreibvorgang für alle drei Verfahren, sauber und halb geschrieben, mit verworfenem und mit behaltenem Cache: nach--resumeimmer ein gültiger Datenträger mit allen Daten.
Gemessen. XTS-Durchsatz pro Kern auf Apple Silicon (ein MiB Sektoren in einer Schleife): 3.109 MB/s beim Verschlüsseln und 3.103 MB/s beim Entschlüsseln mit Hardware-AES, gegenüber 252 und 258 MB/s in Software. Deshalb erlauben wir keine Software. db-bench in einer VM (aarch64, HVF, 5.000 Einfügungen in 50 Transaktionen, fünf Läufe jeder Variante, abwechselnd):
| op/s, fünf Läufe | Median | |
|---|---|---|
| Verschlüsselung aus | 49.239; 47.401; 50.802; 53.417; 46.531 | 49.239 |
| Verschlüsselung ein | 57.790; 46.173; 45.939; 52.787; 49.632 | 49.632 |
Der Unterschied der Mediane beträgt +0,8 %, und die Streuung innerhalb einer Variante (46.000 bis 58.000) ist größer, sodass der Aufwand in diesem Test nicht messbar ist. Das ist auch zu erwarten: db-bench schreibt im gesamten Lauf etwa 6 MB, das sind etwa 2 ms AES, und ein Commit wartet auf den Flush, nicht auf die Chiffre.
Der Diagnose-Eintrag zeigt jetzt auch AES-XTS neben AES-GCM (auf HVF misst der Kernel etwa 800 MiB/s für XTS gegenüber etwa 560 MiB/s für AES-GCM) und zeigt, ob Hardware-AES verfügbar ist.

Was XTS nicht leistet. Es gibt keine Authentifizierung: Ein veränderter verschlüsselter Sektor wird zu Rauschen in Blöcken von 16 Byte entschlüsselt (die CRCs und der Scrubber finden das), und wer auf das Volume schreiben kann, kann zwischen zwei Snapshots ganze Sektoren austauschen oder zurückspielen. Es schützt ein gestohlenes Volume oder einen gestohlenen Snapshot, nicht eine gestohlene ISO, weil der Schlüssel in der Konfiguration des Images liegt. Sichtbar bleiben: die GPT, der Superblock, die UUID und welche Sektoren jemals beschrieben wurden.
Was wir unterwegs in unserer eigenen Arbeit gefunden haben. Beim Schreiben des Kryptografie-Kapitels haben wir zwei Dinge festgestellt. Der Schritt, der den XTS-Tweak multipliziert, hatte eine Verzweigung, die von einem aus dem Schlüssel abgeleiteten Bit abhing; jetzt ist er verzweigungsfrei. Und die Behauptung „geprüft gegen den IEEE-1619-Vektor 10" deckte nur den ersten verschlüsselten Block ab; jetzt gibt es zusätzlich eine naive Referenzimplementierung (u128-Arithmetik, Block für Block), die ganze Sektoren in beiden Richtungen mit 200 zufälligen Schlüsseln und Sektoren prüft, und die Dokumentation sagt, was tatsächlich geprüft wurde.
Ein Bootvorgang, der ein verändertes Image ablehnt (H8)
Die Anforderung aus der Audit-Liste war einfach und bis jetzt ungeprüft: Kein Test zeigte, dass ein veränderter Kernel nicht startet. Der Hash-Pin in limine.conf existierte, aber ohne Beweis.
- Ein Test, der ein Byte verändert.
scripts/tamper.pykippt ein Bit in jeder Kopie des Kernels (oder des Konfigurationsmoduls) innerhalb einer gebauten ISO oder eines Raw-Images, undSMOKE_ONLY=integrityzeigt, dass der Loader ablehnt: 24 Prüfungen bestanden, auf aarch64 UEFI, x86_64 UEFI und x86_64 BIOS, mit Kontrollen, die starten. Die Ablehnungsmeldung wird über einen nur für Tests vorgesehenen Schalter vom seriellen Port gelesen. Auf BIOS erreicht die Meldung nur den Bildschirm, daher behauptet dieser Test nur, dass die Maschine nicht startet. - Die Konfiguration des Loaders eingetragen (ADR-250).
limine enroll-configwirkt aufBOOTAA64.EFI,BOOTX64.EFIund die BIOS-Stufe, sodass auch eine verändertelimine.conf(ein hinzugefügter Menüeintrag oder Parameter) abgelehnt wird.urmqtt-config pack --isoträgt sie erneut ein. - Der Secure-Boot-Pfad (ADR-251).
scripts/sign-loaders.sh(Testschlüssel, Signieren, Prüfen, ein OVMF-Variablenspeicher),mkiso.sh --sb-keyund die Gruppesecurebootauf x86_64: Eine signierte ISO startet und wird reproduzierbar gebaut, während eine unsignierte, eine mit dem falschen Schlüssel signierte und eine nach dem Signieren veränderteAccess Deniederhalten (10 Prüfungen bestanden). Limine lehnt auch unter Secure Boot einen veränderten Kernel weiterhin ab. - Die Regeln des Installer-Eintrags (ADR-253). Der Plan sprach von einem Wort auf der Kommandozeile,
install. Der Produktiv-Kernel akzeptiert tatsächlich drei (install,live,diag), und nurinstallschreibt. Der Test (9 Prüfungen) führt das auf drei Maschinen mit leerem Datenträger aus, dessen Hash sich nicht ändern darf. - Geheimhaltung des Images (ADR-252). Eine laute Warnung, wenn ein Produktiv-Image außerhalb von CI gebaut wird, und ein Scan des Images, der Build-Logs und der seriellen Logs nach Schlüsselbytes in vielen Kodierungen (11 Prüfungen).
- TPM, nur ADR-254. Was es bedeuten würde, den Serverschlüssel an ein vTPM oder TPM 2.0 zu versiegeln. Empfehlung: nicht bauen für ein auf VPS ausgerichtetes Release. Wir haben nicht geprüft, welche Anbieter überhaupt ein vTPM anbieten.
Was das NICHT bedeutet (festgehalten als H8-W1 bis H8-W6 in AUDIT.md): Ein installierter Datenträger startet unter Secure Boot nicht, weil sein Konfigurationsmodul keinen Hash hat (ADR-217); eine signierte ISO lässt sich nachträglich nicht konfigurieren (erst konfigurieren, dann signieren); der BIOS-Pfad hat keine Signatur; es gibt keinen Schutz vor dem Zurückspielen einer älteren Version; auf aarch64 erzwingt die Firmware hier Secure Boot nicht, sodass nur die Signatur des Loaders geprüft wurde; es gibt keinen Projektschlüssel, daher sind die Release-Images für Secure Boot unsigniert; und keine physische Maschine wurde ausprobiert.
Das Audit-Paket (H10)
docs/AUDIT.md ist jetzt ein Paket, das ein externer Auditor ohne Rückfragen nutzen kann: das System auf einer Seite, die Basis, der wir vertrauen (89.612 Zeilen eigenes Rust im Image, 67 eingebundene Fremdpakete), die Eingaben und ihre Parser, die Matrix der Schutzmaßnahmen mit ihren Tests (und die Schutzmaßnahmen ohne Tests), die Schwächen mit ihrem Status, eine Tabelle jeder aus dem Netz erreichbaren Schleife und Allokation mit Grenze und Test, Anleitungen zum Bauen und Testen, ein vorgeschlagener Umfang eines externen Audits in zehn Bereichen und eine Red-Team-Liste mit 31 Angriffen und ihren Ergebnissen. SECURITY.md enthält eine STRIDE-Tabelle pro Komponente.
- Eine Berichtigung zum ersten Text. Im Text vom 29. Oktober steht, dass jeder
unsafe-Block einen Kommentar// SAFETY:hat, den ein Lint prüft. Das stimmte nicht: Der Lint (deny(unsafe_code)) beschränkt nur, wounsafestehen darf, und die Kommentare werden seit H0 von einem generierten Inventar erfasst, das schon damals etwa 30 Stellen ohne Begründung fand und beim Audit 108 (weil inzwischen neue Arten von Stellen hinzugekommen waren, zum Beispiel die Attribute#[unsafe(no_mangle)]undlink_section). Deshalb ist es das Inventar und nicht der Lint, das sie jetzt bei null hält. - 889
unsafe-Stellen, jede mit schriftlicher Begründung. Die 108, die keine hatten (meist#[unsafe(no_mangle)]undlink_section, einigeunsafe fn), haben einen Kommentar bekommen (ein KI-Agent hat ihn geschrieben, nur Kommentare, und ich habe eine Stichprobe geprüft); das generierte Inventar in CI erfasst jede neue Stelle ohne Begründung. Eine Begründung ist eine zu prüfende Behauptung, kein Beweis, und das ist Punkt 5 des vorgeschlagenen Umfangs. - Fünf offene Befunde bleiben offen, festgehalten: kein Fuzzing der PCI- und virtio-Parser, kein Test, der nach dem Sperren in die Seitentabellen schreibt, die Laufzeit von P-256, X25519 und Ed25519 wurde nicht gemessen, kein Fuzzing absichtlich präparierter SQLite-Dateien (eingegrenzt durch H6), und 53 der 67 eingebundenen Pakete sind
cargo-vet-Ausnahmen, keine Audits.
Fehler, die wir selbst gefunden haben
- TLS-Verkehrsgeheimnisse wurden nicht gelöscht (H0-W2, offen seit dem ersten Tag des Audits): Die Tabelle der mitgeschnittenen Geheimnisse war ein
Vec, denremoveundswap_removeverschoben und dabei Kopien zurückließen. Jetzt ist die Tabelle slotbasiert: Ein Slot wird an Ort und Stelle gelöscht, wenn er entnommen, verworfen oder verdrängt wird, und nichts wird verschoben. - Eine vom Schlüssel abhängige Verzweigung in XTS (oben).
log-slow-floodauf x86_64 schlug fehl („log ring full"), auch auf dem Quellcode von v0.2.1. Die Ursache war kein Defekt, sondern die Dimensionierung des Tests: Das Szenario trieb jeden Kern mit 25 ms an, während ADR-090 es mit 50 ms gemessen hatte. Auf x86_64 ist COM1 ein pro Byte abgefangener Port-I/O, ein Kern, der in die Senken schreibt, braucht etwa 9 ms pro Zeile, und drei Kerne mit 25 ms verlangen mehr als eine Sekunde Arbeit pro Sekunde, sodass sich die Ringe füllen (der beabsichtigte Überlauf). Der Test läuft auf x86_64 jetzt mit 50 ms (bestanden, 30 von 30 im Lauf mit den Basisszenarien); auf aarch64 bleibt es bei 25 ms.- Das Zusammenführen der Zweige hat die alten Vorlagen kaputt gemacht. Das erneute Eintragen der Konfiguration in H8 verlangte eine Endmarke in
limine.conf, die Vorlagen von vor H8 nicht haben, sodassurmqtt-config pack --isound die Builder-Seite jedes Release vor H8 ablehnten, einschließlich der v0.2.x, die die Seite anbietet. Die Builder-Tests im Browser haben es gefangen (4 von 100 fielen durch), nicht der eigene Test von H8. Behoben (eine Vorlage ohne Marke wird wie zuvor konfiguriert) und einen Test hinzugefügt, der mit dem ursprünglichen Fehler fehlschlägt, wenn die Korrektur entfernt wird. - Unsere Dokumentation behauptete fälschlich, x86_64 sei nur im Emulator geprüft. X11 hat das Produktiv-Image auf einem echten Contabo-KVM-VPS (4 vCPU AMD EPYC) mit gegenseitigem TLS unter Last laufen lassen. Überall berichtigt: x86_64 lief beim H7-Commit auf einem echten KVM-VPS und im Emulator, aber der danach hinzugekommene Code (Verschlüsselung, Boot-Integrität) ist nicht auf KVM gelaufen und nicht auf physischer Hardware ausprobiert worden.
D1 und das Angebot „Ihr Dienst als Unikernel"
Der öffentliche Demo-Broker ist gebaut, aber nicht veröffentlicht: die Konfiguration (Live-Modus, ACL demo/{id}/#, Klartext-MQTT nur für die Bridge), ein Skript, das eine veröffentlichte Test-CA und Demo-Zertifikate erzeugt, eine WebSocket-zu-TCP-Bridge (nur Python-Standardbibliothek, Grenzen für Verbindungen, Rate und Größe, eine Origin-Prüfung), eine Seite „urMQTT live ausprobieren" mit einem MQTT-Client in reinem JavaScript, ein Runbook und ein Bedrohungsmodell. Getestet auf Loopback und in QEMU (18 Szenarien, 26 Bridge-Tests). Nicht getestet: ein echter VPS, DNS, Caddy, das Container-Image. Es braucht einen separaten VPS und eine ausdrückliche Freigabe für die öffentliche Erreichbarkeit.
Außerdem gibt es auf der Website jetzt ein Angebot, Unikernel (15 Sprachen, ohne Preise): die Anpassung Ihres Dienstes, sodass er als Unikernel läuft (Machbarkeitsbewertung, Anpassung, Pilot, Support), mit urMQTT als Referenz. Der Text nennt die Grenzen der heutigen Grundlage ausdrücklich: IPv4, TCP nur als Server, KVM, Rust als natürliche Wahl, C nur als isolierter Worker.
Das Release v0.3.0
Der Tag v0.3.0 wurde am 3. November auf den Commit f1a7fbc gesetzt, und der Release-Workflow war erfolgreich. Das Release ist als Pre-Release markiert und enthält den aarch64-Entwicklungskernel, ISO und Image, die Vorlagen-ISO (Kernel für aarch64 und x86_64 und eine leere Konfiguration, die urmqtt-config pack --iso und die Builder-Seite in Ihr Image verwandeln), ein CycloneDX-SBOM, SHA256SUMS und dessen minisign-Signatur sowie SLSA-Provenienz-Attestierungen. Nach der Veröffentlichung in einem leeren Verzeichnis geprüft: Die Signatur von SHA256SUMS ist gültig, alle fünf Dateien stimmen mit den Prüfsummen überein, beide Attestierungen (Kernel, und ISO mit dem CycloneDX-Prädikat) bestehen, das SBOM hat 221 Komponenten, und die Vorlage lässt sich konfigurieren (pack --iso trägt sechs Kopien des Loaders erneut ein). Der Signaturschlüssel ist weiterhin ein Testschlüssel.
Die Reihenfolge war wichtig und ist es wert, festgehalten zu werden: Die Builder-Seite wurde vor dem Tag aktualisiert (sie kennt Format 4 und alle älteren Vorlagen), und erst dann hat der Mirror auf aldo.tech das Release übernommen. Der Mirror führte es signiert und geprüft auf (ok, Konfigurationsformat 4, das sha256 der Vorlage gleich dem in SHA256SUMS), und die Live-Seite bietet es als Standard-Release an und baut im Browser in etwa 20 Sekunden eine ISO. Die Release-Notes sagen auch, was selten geschrieben wird: CI auf GitHub ist nicht grün (siehe unten), Support gibt es nur für die jeweils letzte Version (v0.2.1 und älter enden), und das Audit hat das Projekt selbst gemacht.
Auf der Website hätten wir uns unterwegs beinahe selbst geschadet: Das Veröffentlichungsskript der Seite (publish.sh) führt rsync --delete aus, und der Release-Mirror gehört einem anderen Benutzer, sodass jede Veröffentlichung mit Fehler 23 endete (und den Knopf „Veröffentlichen" im Blog-Admin kaputt gemacht hätte). Behoben mit einem Ausschluss für /urmqtt/releases/. Ein ähnlicher Befund: deploy.sh mit --delete entfernt die Builder-Seite aus dem Generator, sie muss also vor der Veröffentlichung zurückgelegt werden.
Was noch nicht bewiesen ist
- CI auf GitHub ist rot. Nicht nur heute: Der letzte Lauf des vollständigen CI vor dem Tag hatte sieben fehlgeschlagene Jobs, und die Läufe zurück bis zum 31. Oktober sind überwiegend rot. Fehlgeschlagen sind: Lint (shellcheck-Warnungen in Skripten), TLS-Interop (alle wolfSSL-Fälle), die Lieferkette (die Ausnahmeliste
vet-exemptions.mdist unter Linux nicht aktuell), Fuzz-Smoke (in diesem Job fehlen die Entwicklungszertifikate), die Sanitizer (LeakSanitizer meldet in Tests ein Leck), Tests unter Linux aarch64 (ein Test des Werkzeugs schlägt mit „Broken pipe" fehl) und ein Kani-Beweis für das Zeitrad (timer wheel) besteht nicht. Wir haben noch nicht sortiert, was davon die Umgebung und was ein echter Mangel ist; solange wir das nicht getan haben, behaupten wir nicht, dass das Release CI bestanden hat. - Der Signaturschlüssel ist ein Testschlüssel, und die Release-Images sind Entwicklungs-Builds mit Testzertifikaten (außer der Vorlage).
- Die Übersetzungen der Seiten und dieses Textes in die übrigen 13 Sprachen hat eine KI geschrieben, und sie wurden nicht von Muttersprachlern geprüft.
- Die Datenträger-Verschlüsselung wurde auf x86_64 nicht gemessen (weder auf KVM noch auf physischer Hardware), und weder der Telemetrie-Durchsatz noch die Startzeit mit Verschlüsselung wurden gemessen. Auf der Builder-Seite gibt es sie nicht (sie braucht einen Schlüsseldialog).
- Ein unabhängiges Sicherheitsaudit gibt es nicht. Der Umfang ist geschrieben; einen Auditor haben wir nicht.
- Secure Boot ist unvollständig (oben), ohne physische Maschine.
- Bare Metal, IOMMU, der öffentliche Demo-Broker: nicht fertig oder nicht veröffentlicht.
Wie es weitergeht
- CI sortieren und reparieren (zuerst die Umgebung, dann der Kani-Beweis und der Test, der unter Linux aarch64 fehlschlägt), denn ein Release ohne grünes CI betrachten wir nicht als fertig.
- Eine Entscheidung zu D1 (separater VPS, DNS, Freigabe für die öffentliche Erreichbarkeit).
- Fuzzing der PCI- und virtio-Parser und ein Test für die gesperrten Seitentabellen (H0-W9, H0-W10).
- Ein Schlüsseldialog auf der Builder-Seite, damit die Verschlüsselung ohne Kommandozeile verfügbar wird.
- Nach dem Release: Bare Metal (X12 bis X16) und IOMMU, in einer Reihenfolge, die vom Hardware-Inventar des Test-PCs abhängt.
Der Code liegt auf github.com/zeljan-alduk/urMQTT; docs/PROGRESS.md bleibt die einzige Quelle der Wahrheit darüber, was wirklich funktioniert, docs/PERFORMANCE.md darüber, was gemessen wurde, und docs/AUDIT.md darüber, was noch nicht bewiesen ist. Pre-Alpha, wie zuvor; Fehlerberichte und Sicherheitsmeldungen nach den Anweisungen in SECURITY.md sind willkommen.
Kommentare
Kommentarregeln