urMQTT v0.3.0: szyfrowanie dysku, rozruch odrzucający zmieniony obraz i to, co sami zepsuliśmy
Co się wydarzyło po trzecim tekście o urMQTT: szyfrowanie dysku w spoczynku ze zmierzonym kosztem, test, który zmienia jeden bajt w jądrze i pokazuje, że loader je odrzuca, pakiet audytowy z 889 uzasadnionymi miejscami unsafe, błędy, które sami znaleźliśmy (i jedno twierdzenie, które napisaliśmy błędnie), oraz wydanie v0.3.0 z listą tego, co nadal nie zostało udowodnione, łącznie z czerwonym CI.

Trzeci tekst, urMQTT po VPS-ie, kończył się listą „Co dalej": publiczny broker demonstracyjny (D1), druga runda dla powolnego pojedynczego strumienia, zwykła ścieżka w porównaniu z Mosquitto na VPS-ie oraz, „po wydaniu", bare metal, IOMMU, Secure Boot i szyfrowanie dysku. Ten tekst mówi, co z tego zrobiono, czego nie, i co po drodze znaleźliśmy. Szyfrowanie dysku i część Secure Boota weszły jednak przed wydaniem, bo były po drodze do audytu. Druga runda dla pojedynczego strumienia i zwykła ścieżka w porównaniu z Mosquitto nie zostały zrobione.
Zastrzeżenie na wstępie pozostaje to samo: projekt jest w fazie pre-alfa, wydanie jest przeznaczone dla VPS-ów z KVM, a liczby mierzą kod, testy i określone eksperymenty, nie dojrzałość w produkcji.
Jak to wyglądało w czasie
Od poprzedniego tekstu (1 listopada, 20:41) do dziś (3 listopada, 02:43) w głównej gałęzi jest 37 nowych commitów, łącznie 215. Jak poprzednio, projekt powstaje z Claude Code (commity zawierają Co-Authored-By: Claude), a tym razem równolegle pracowały trzy gałęzie (D1, H8, przygotowanie wydania), więc kolejność commitów nie jest ściśle kolejnością pracy. Liczby obliczono tą samą metodą co w poprzednim tekście (wszystkie pliki .rs poza vendor/ i fuzz/, funkcje #[test], nagłówki ## ADR- w docs/DECISIONS.md) i sprawdzono na starym commicie, który daje takie same liczby jak wtedy.
| Moment | Commit | Czas | Linii Rusta | Testów | ADR-ów | Commitów |
|---|---|---|---|---|---|---|
| poprzedni tekst | d21e3ec | 1 listopada, 20:41 | 112 408 | 926 | 126 | 178 |
| dziś | f1a7fbc | 3 listopada, 02:43 | 115 148 | 962 | 133 | 215 |
Przyrost kodu jest skromny (+2 740 linii, +36 testów), bo tym razem nie powstało wiele nowego kodu: większość pracy to testy, skrypty, dokumentacja i weryfikacje. Skrypty i JavaScript nie wchodzą do liczby linii.
Szyfrowanie dysku w spoczynku (S7)
Co planowaliśmy. Opcjonalne szyfrowanie partycji z danymi, domyślnie wyłączone, ze zmierzonym kosztem. Co zbudowaliśmy (ADR-240):
- Warstwa pod dyskiem.
CryptDevowija urządzenie blokowe podDisk, więc układ, tabela rozmiarów, SQLite, telemetria i kopie zapasowe działają na jawnym tekście i nic nie wiedzą o szyfrowaniu. Algorytm to AES-256-XTS (IEEE 1619), jeden sektor 512 bajtów na jednostkę danych, a nie blok 4 KiB jak w planie: magazyn zapisuje sektorami, a jednostka 4 KiB zamieniłaby każdy zapis 512 bajtów w odczyt-modyfikację-zapis, który utrata zasilania może przepołowić. - Superblok pozostaje jawny. Dwa sloty superbloku niosą flagę i sprawdzenie klucza (HMAC-SHA-256 z etykiety i UUID dysku), nigdy klucz. Wszystko inne w partycji jest zaszyfrowane. Sektor, który na nośniku jest w całości zerem, jest odczytywany jako zero i nie jest deszyfrowany, więc discard, nigdy nie zapisana przestrzeń i powiększanie dysku działają jak dotąd.
- Konfiguracja.
storage.encryption = "aes-256-xts"istorage.disk_key(plik z 64 bajtami klucza). Format bloba staje się 4 tylko przy włączonym szyfrowaniu, więc wszystkie inne obrazy nadal uruchamiają się na starszych jądrach. Starszy czytnik odrzuca nieznaną flagę w superbloku. - Bez sprzętowego AES: odmowa. Jądro odrzuca
storage.encryptionna procesorze bez instrukcji AES (lub gdy[cpu] disableje wyłącza), zamiast po cichu uruchamiać powolną wersję programową. - Konwersja odbywa się offline. Jądro nigdy nie szyfruje ani nie deszyfruje dysku przy starcie.
urmqtt-disk encrypt,decryptirekeyrobią to przy wyłączonej maszynie wirtualnej: rekord konwersji zapisywany jest pierwszy, a usuwany ostatni (dopóki istnieje, jądro odrzuca dysk), kawałki po 1 MiB są najpierw zapisywane w dzienniku na hoście, potem zapisywana jest przekonwertowana kopia, a superblok przełączany jest na końcu. Sprawdzone przerwaniem zasilania po każdym pojedynczym zapisie dla wszystkich trzech procedur, w wariancie czystym i połowicznie zapisanym, z porzuconą i zachowaną pamięcią podręczną: zawsze poprawny dysk ze wszystkimi danymi po--resume.
Zmierzone. Przepustowość XTS na rdzeń na Apple silicon (jeden MiB sektorów w pętli): 3 109 MB/s szyfrowanie i 3 103 MB/s deszyfrowanie ze sprzętowym AES, wobec 252 i 258 MB/s programowo. Dlatego wersji programowej nie dopuszczamy. db-bench w VM (aarch64, HVF, 5 000 wstawień w 50 transakcjach, pięć uruchomień każdego wariantu, naprzemiennie):
| op/s, pięć uruchomień | mediana | |
|---|---|---|
| szyfrowanie wyłączone | 49 239, 47 401, 50 802, 53 417, 46 531 | 49 239 |
| szyfrowanie włączone | 57 790, 46 173, 45 939, 52 787, 49 632 | 49 632 |
Różnica median wynosi +0,8 %, a rozrzut w obrębie jednego wariantu (od 46 000 do 58 000) jest większy, więc koszt nie jest mierzalny w tym teście. Tego zresztą oczekujemy: cały przebieg db-bench zapisuje około 6 MB, co daje około 2 ms AES-a, a commit czeka na flush, nie na szyfr.
Wpis diagnostyczny pokazuje teraz także AES-XTS obok AES-GCM (na HVF jądro mierzy około 800 MiB/s dla XTS wobec około 560 MiB/s dla AES-GCM) oraz informuje, czy sprzętowy AES jest dostępny.

Czego XTS nie daje. Nie ma uwierzytelniania: zmieniony zaszyfrowany sektor deszyfruje się do szumu w blokach po 16 bajtów (CRC-e i scrubber to wykrywają), a ktoś, kto może pisać po woluminie, może podmienić lub cofnąć całe sektory między dwoma migawkami. Chroni skradziony wolumin lub migawkę, nie skradziony ISO, bo klucz jest w konfiguracji obrazu. Pozostaje widoczne: GPT, superblok, UUID i to, które sektory kiedykolwiek zapisano.
Co po drodze znaleźliśmy we własnej pracy. Pisząc rozdział o kryptografii, ustaliliśmy dwie rzeczy. Krok mnożenia tweaku w XTS miał gałąź zależną od bitu wyprowadzonego z klucza; teraz jest bez gałęzi. A twierdzenie „sprawdzone względem wektora 10 z IEEE 1619" obejmowało tylko pierwszy zaszyfrowany blok; teraz jest też naiwna implementacja referencyjna (arytmetyka u128, blok po bloku), która sprawdza całe sektory w obu kierunkach na 200 losowych kluczach i sektorach, a dokumentacja mówi, co naprawdę sprawdzono.
Rozruch odrzucający zmieniony obraz (H8)
Wymaganie z listy audytowej było proste i dotąd niezweryfikowane: żaden test nie pokazywał, że zmienione jądro się nie uruchamia. Przypięcie hasha w limine.conf istniało, ale bez dowodu.
- Test, który zmienia bajt.
scripts/tamper.pyodwraca jeden bit w każdej kopii jądra (lub modułu konfiguracji) wewnątrz zbudowanego ISO lub obrazu raw, aSMOKE_ONLY=integritypokazuje, że loader odmawia: 24 sprawdzenia zaliczone, na aarch64 UEFI, x86_64 UEFI i x86_64 BIOS, z kontrolami, które się uruchamiają. Komunikat odmowy odczytywany jest z portu szeregowego przez przełącznik wyłącznie do testów. Na BIOS-ie komunikat trafia tylko na ekran, więc ten test twierdzi tylko, że maszyna się nie uruchamia. - Zapisana konfiguracja loadera (ADR-250).
limine enroll-configdziała naBOOTAA64.EFI,BOOTX64.EFIi etapie BIOS, więc zmienionylimine.conf(dodany wpis menu lub argument) też jest odrzucany.urmqtt-config pack --isozapisuje ją ponownie. - Ścieżka Secure Boot (ADR-251).
scripts/sign-loaders.sh(klucze testowe, podpisywanie, weryfikacja, magazyn zmiennych OVMF),mkiso.sh --sb-keyi grupasecurebootna x86_64: podpisany ISO uruchamia się i buduje powtarzalnie, a niepodpisany, podpisany złym kluczem i zmieniony po podpisaniu dostająAccess Denied(10 sprawdzeń zaliczonych). Limine nadal odrzuca zmienione jądro pod Secure Bootem. - Reguły wpisu instalacyjnego (ADR-253). Plan mówił o jednym słowie w wierszu poleceń,
install. Produkcyjne jądro w rzeczywistości przyjmuje trzy (install,live,diag), a zapisuje tylkoinstall. Test (9 sprawdzeń) uruchamia to na trzech maszynach z pustym dyskiem, którego hash nie może się zmienić. - Tajność obrazu (ADR-252). Głośne ostrzeżenie, gdy obraz produkcyjny budowany jest poza CI, oraz skanowanie obrazu, logów budowy i logów szeregowych pod kątem bajtów klucza w wielu kodowaniach (11 sprawdzeń).
- TPM, tylko ADR-254. Co by znaczyło zapieczętowanie klucza serwera w vTPM lub TPM 2.0. Rekomendacja: nie budować tego dla wydania nastawionego na VPS-y. Nie sprawdzaliśmy, którzy dostawcy w ogóle oferują vTPM.
Czego to NIE oznacza (zapisane jako H8-W1 do H8-W6 w AUDIT.md): zainstalowany dysk nie uruchamia się pod Secure Bootem, bo jego moduł konfiguracji nie ma hasha (ADR-217); podpisanego ISO nie można potem skonfigurować (najpierw skonfigurować, potem podpisać); ścieżka BIOS nie ma podpisu; nie ma ochrony przed cofnięciem do starszej wersji; na aarch64 firmware tutaj nie egzekwuje Secure Boota, więc zweryfikowano tylko podpis loadera; nie ma klucza projektu, więc obrazy wydania nie są podpisane dla Secure Boota; i żadna fizyczna maszyna nie była próbowana.
Pakiet audytowy (H10)
docs/AUDIT.md jest teraz pakietem, z którego zewnętrzny audytor może korzystać bez pytań: system na jednej stronie, baza, której ufamy (89 612 linii własnego Rusta w obrazie, 67 podlinkowanych cudzych pakietów), wejścia i ich parsery, macierz zabezpieczeń z testami (oraz zabezpieczenia bez testów), słabości ze statusem, tabela każdej pętli i alokacji osiągalnej z sieci, z jej granicą i testem, instrukcje budowania i testowania, proponowany zakres audytu zewnętrznego w dziesięciu obszarach oraz lista red team z 31 atakami i wynikami. SECURITY.md ma tabelę STRIDE dla każdego komponentu.
- Sprostowanie pierwszego tekstu. W tekście z 29 października stoi, że każdy blok
unsafema komentarz// SAFETY:sprawdzany przez lint. To nie było prawdą: lint (deny(unsafe_code)) tylko ogranicza, gdzieunsafemoże występować, a komentarze od H0 śledzi wygenerowany inwentarz, który już wtedy znalazł około 30 miejsc bez uzasadnienia, a przy audycie 108 (bo w międzyczasie doszły nowe rodzaje miejsc, na przykład atrybuty#[unsafe(no_mangle)]ilink_section). Dlatego to inwentarz, a nie lint, utrzymuje je teraz na zerze. - 889 miejsc
unsafe, każde z pisemnym uzasadnieniem. 108, które go nie miały (głównie#[unsafe(no_mangle)]ilink_section, kilkaunsafe fn), dostało komentarz (napisał go agent AI, tylko komentarze, a ja przejrzałem próbkę); wygenerowany inwentarz w CI odnotowuje każde nowe miejsce bez uzasadnienia. Uzasadnienie to twierdzenie do zweryfikowania, nie dowód, i to jest punkt 5 proponowanego zakresu. - Pięć otwartych ustaleń pozostaje otwartych, zapisanych: brak fuzzingu parserów PCI i virtio, brak testu, który zapisuje do tablic stron po ich zablokowaniu, czas wykonania P-256, X25519 i Ed25519 nie był mierzony, brak fuzzingu celowo spreparowanych plików SQLite (ograniczone przez H6) oraz 53 z 67 podlinkowanych pakietów to wyjątki
cargo-vet, nie audyty.
Błędy, które sami znaleźliśmy
- Sekrety ruchu TLS nie były zerowane (H0-W2, otwarte od pierwszego dnia audytu): tabela przechwyconych sekretów była
Vec, któryremoveiswap_removeprzesuwały, zostawiając kopie. Teraz tabela opiera się na slotach: slot jest zerowany w miejscu, gdy zostanie pobrany, odrzucony lub wypchnięty, i nic się nie przesuwa. - Gałąź zależna od klucza w XTS (wyżej).
log-slow-floodna x86_64 się wywracał („log ring full"), także na źródle v0.2.1. Przyczyną nie była usterka, lecz zwymiarowanie testu: scenariusz napędzał każdy rdzeń co 25 ms, a ADR-090 mierzył go przy 50 ms. Na x86_64 COM1 to przechwytywane we/wy portu po bajcie, jeden rdzeń piszący do ujść potrzebuje około 9 ms na wiersz, a trzy rdzenie co 25 ms żądają ponad sekundy pracy na sekundę, więc pierścienie się zapełniają (zamierzone przepełnienie). Test działa teraz na x86_64 co 50 ms (zaliczony, 30 z 30 w przebiegu ze scenariuszami bazowymi); na aarch64 zostaje 25 ms.- Scalenie gałęzi zepsuło stare szablony. Ponowny zapis konfiguracji w H8 wymagał znacznika końca w
limine.conf, którego szablony sprzed H8 nie mają, więcurmqtt-config pack --isoi strona konstruktora odrzucały każde wydanie sprzed H8, łącznie z v0.2.x, które strona oferuje. Wychwyciły to testy konstruktora w przeglądarce (4 ze 100 nie przeszły), a nie własny test H8. Naprawione (szablon bez znacznika konfigurowany jest jak dawniej) i dodany test, który wywraca się z pierwotnym błędem, gdy poprawkę usunąć. - Nasza dokumentacja błędnie twierdziła, że x86_64 zweryfikowano tylko w emulatorze. X11 uruchomił obraz produkcyjny na prawdziwym VPS-ie Contabo z KVM (4 vCPU AMD EPYC) z wzajemnym TLS pod obciążeniem. Poprawiono wszędzie: x86_64 działał na prawdziwym VPS-ie z KVM przy commicie H7 i w emulatorze, ale kod dodany później (szyfrowanie, integralność rozruchu) nie był uruchamiany na KVM i nie próbowano go na fizycznym sprzęcie.
D1 i oferta „twoja usługa jako unikernel"
Publiczny broker demonstracyjny jest zbudowany, ale nieopublikowany: konfiguracja (tryb live, ACL demo/{id}/#, zwykły MQTT tylko dla mostu), skrypt, który tworzy opublikowane testowe CA i certyfikaty demo, most WebSocket-do-TCP (tylko biblioteka standardowa Pythona, limity połączeń, szybkości i rozmiaru, sprawdzanie pochodzenia), strona „Wypróbuj urMQTT na żywo" z klientem MQTT w zwykłym JavaScripcie, runbook i model zagrożeń. Przetestowane na loopbacku i w QEMU (18 scenariuszy, 26 testów mostu). Nieprzetestowane: prawdziwy VPS, DNS, Caddy, obraz kontenera. Potrzebuje osobnego VPS-a i wyraźnej zgody na publiczne wystawienie.
Ponadto na stronie opublikowano ofertę, Unikernel (15 języków, bez cen): dostosowanie twojej usługi tak, aby działała jako unikernel (ocena wykonalności, dostosowanie, pilotaż, wsparcie), z urMQTT jako referencją. Tekst wprost podaje ograniczenia dzisiejszej podstawy: IPv4, TCP tylko jako serwer, KVM, Rust naturalny, C tylko jako odizolowany worker.
Wydanie v0.3.0
Tag v0.3.0 został założony 3 listopada na commicie f1a7fbc, a workflow wydania zakończył się sukcesem. Wydanie jest oznaczone jako pre-release i zawiera rozwojowe jądro, ISO i obraz dla aarch64, szablon ISO (jądra dla aarch64 i x86_64 oraz pusta konfiguracja, które urmqtt-config pack --iso i strona konstruktora zamieniają w twój obraz), CycloneDX SBOM, SHA256SUMS i jego podpis minisign oraz atestacje pochodzenia SLSA. Sprawdzone po publikacji, w pustym katalogu: podpis SHA256SUMS jest ważny, wszystkie pięć plików zgadza się z sumami kontrolnymi, obie atestacje (jądra oraz ISO z predykatem CycloneDX) przechodzą, SBOM ma 221 komponentów, a szablon da się skonfigurować (pack --iso ponownie zapisuje sześć kopii loadera). Klucz podpisujący nadal jest kluczem testowym.
Kolejność miała znaczenie i warto ją zapisać: strona konstruktora została zaktualizowana przed tagiem (zna format 4 i wszystkie starsze szablony), dopiero potem lustro na aldo.tech przejęło wydanie. Lustro wymieniło je jako podpisane i zweryfikowane (ok, format konfiguracji 4, sha256 szablonu równy temu z SHA256SUMS), a działająca strona oferuje je jako wydanie domyślne i buduje ISO w przeglądarce w około 20 sekund. Notatki do wydania mówią też to, o czym rzadko się pisze: CI na GitHubie nie jest zielone (patrz niżej), wsparcie jest tylko dla ostatniej wersji (v0.2.1 i starsze się kończą), a audyt wykonał sam projekt.
Na stronie przy okazji o mało nie wyrządziliśmy sobie szkody: skrypt publikujący stronę (publish.sh) robi rsync --delete, a lustro wydań należy do innego użytkownika, więc każda publikacja kończyła się błędem 23 (i zepsułaby przycisk „Opublikuj" w panelu administracyjnym bloga). Naprawione wyłączeniem dla /urmqtt/releases/. Podobne ustalenie: deploy.sh z --delete usuwa stronę konstruktora z generatora, więc trzeba ją przywrócić przed publikacją.
Co nadal nie zostało udowodnione
- CI na GitHubie jest czerwone. Nie tylko dziś: ostatni przebieg pełnego CI przed tagiem miał siedem nieudanych zadań, a przebiegi wstecz do 31 października są w większości czerwone. Nie przeszły: lint (ostrzeżenia shellcheck w skryptach), interop TLS (wszystkie przypadki wolfSSL), łańcuch dostaw (lista wyjątków
vet-exemptions.mdnie jest aktualna na Linuksie), fuzz smoke (w tym zadaniu brakuje certyfikatów rozwojowych), sanitizery (LeakSanitizer zgłasza wyciek w testach), testy na Linux aarch64 (jeden test narzędzia pada z „Broken pipe") oraz jeden dowód Kani dla koła czasowego (timer wheel) nie przechodzi. Nie rozdzieliliśmy jeszcze, co z tego to środowisko, a co prawdziwa wada; dopóki tego nie zrobimy, nie twierdzimy, że wydanie przeszło CI. - Klucz podpisujący jest kluczem testowym, a obrazy wydania są rozwojowe z certyfikatami testowymi (poza szablonem).
- Tłumaczenia stron i tego tekstu na pozostałe 13 języków napisała AI i nie sprawdzili ich native speakerzy.
- Szyfrowanie dysku nie zostało zmierzone na x86_64 (ani na KVM, ani na fizycznym sprzęcie), nie zmierzono też przepustowości telemetrii ani czasu uruchamiania z szyfrowaniem. Nie ma go na stronie konstruktora (potrzebne jest okno dialogowe na klucz).
- Nie ma niezależnego audytu bezpieczeństwa. Zakres jest napisany; audytora nie mamy.
- Secure Boot jest częściowy (wyżej), bez fizycznej maszyny.
- Bare metal, IOMMU, publiczny broker demonstracyjny: nie zrobione lub nieopublikowane.
Co dalej
- Rozdzielić i naprawić CI (najpierw środowisko, potem dowód Kani i test, który pada na aarch64 Linux), bo wydania bez zielonego CI nie uważamy za skończonego.
- Decyzja w sprawie D1 (osobny VPS, DNS, zgoda na publiczne wystawienie).
- Fuzzing parserów PCI i virtio oraz test zablokowanych tablic stron (H0-W9, H0-W10).
- Okno dialogowe na klucz na stronie konstruktora, aby szyfrowanie stało się dostępne bez wiersza poleceń.
- Po wydaniu: bare metal (X12 do X16) i IOMMU, w kolejności zależnej od inwentarza sprzętu testowego PC.
Kod jest na github.com/zeljan-alduk/urMQTT; docs/PROGRESS.md pozostaje jedynym źródłem prawdy o tym, co naprawdę działa, docs/PERFORMANCE.md o tym, co zmierzono, a docs/AUDIT.md o tym, co nadal nie zostało udowodnione. Pre-alfa, jak dotąd; zgłoszenia błędów i zgłoszenia dotyczące bezpieczeństwa według instrukcji w SECURITY.md są mile widziane.
Komentarze
Zasady komentowania