← Blog

· 16 min lezen #rust#mqtt#iot#unikernel#open source#security

urMQTT v0.3.0: schijfversleuteling, een boot die een gewijzigd image weigert en wat we zelf kapot hebben gemaakt

Wat er gebeurde na de derde tekst over urMQTT: versleuteling van de schijf in rust met gemeten kosten, een test die één byte in de kernel verandert en laat zien dat de loader die weigert, een auditpakket met 889 gemotiveerde unsafe-plaatsen, de fouten die we zelf vonden (en één bewering die we verkeerd hadden opgeschreven), en de release v0.3.0, met een lijst van wat nog niet is bewezen, inclusief een rode CI.

De derde tekst, urMQTT na de VPS, eindigde met een lijst "Wat volgt": een publieke demobroker (D1), een tweede ronde voor de trage enkele stroom, het gewone pad tegen Mosquitto op de VPS en, "na de release", bare metal, IOMMU, Secure Boot en schijfversleuteling. Deze tekst zegt wat daarvan is gedaan, wat niet, en wat we onderweg hebben gevonden. Schijfversleuteling en een deel van Secure Boot haalden de release toch, omdat ze op weg lagen naar de audit. De tweede ronde voor de enkele stroom en het gewone pad tegen Mosquitto zijn niet gedaan.

De kanttekening vooraf blijft dezelfde: het project is pre-alpha, de release richt zich op VPS'en met KVM, en de cijfers meten code, tests en bepaalde experimenten, niet rijpheid in productie.

Hoe het eruitzag in de tijd

Sinds de vorige tekst (1 november, 20:41) tot vandaag (3 november, 02:43) telt de hoofdbranch 37 nieuwe commits, 215 in totaal. Zoals eerder wordt het project gebouwd met Claude Code (de commits dragen Co-Authored-By: Claude), en deze keer liepen drie branches parallel (D1, H8, release-voorbereiding), dus de volgorde van de commits is niet strikt de volgorde van het werk. De cijfers zijn berekend met dezelfde methode als in de vorige tekst (alle .rs-bestanden buiten vendor/ en fuzz/, #[test]-functies, ## ADR--koppen in docs/DECISIONS.md) en gecontroleerd op de oude commit, die dezelfde cijfers geeft als toen.

Moment Commit Tijd Regels Rust Tests ADR's Commits
vorige tekst d21e3ec 1 november, 20:41 112.408 926 126 178
vandaag f1a7fbc 3 november, 02:43 115.148 962 133 215

De groei van de code is bescheiden (+2.740 regels, +36 tests), omdat er deze keer niet veel nieuwe code was: het meeste werk bestaat uit tests, scripts, documentatie en controles. Scripts en JavaScript tellen niet mee in het aantal regels.

Versleuteling van de schijf in rust (S7)

Wat we planden. Optionele versleuteling van de datapartitie, standaard uitgeschakeld, met gemeten kosten. Wat we bouwden (ADR-240):

  • Een laag onder de schijf. CryptDev omhult het blokapparaat onder Disk, zodat de indeling, de groottetabel, SQLite, de telemetrie en de back-ups op leesbare tekst werken en niets van de versleuteling weten. Het algoritme is AES-256-XTS (IEEE 1619), één sector van 512 byte per data-eenheid, en niet een blok van 4 KiB zoals in het plan: de opslag schrijft in sectoren, en een eenheid van 4 KiB zou elke schrijfactie van 512 byte omzetten in een lees-wijzig-schrijf die een stroomuitval middendoor kan breken.
  • Het superblok blijft open. De twee slots van het superblok dragen een vlag en een sleutelcontrole (HMAC-SHA-256 van een label en de UUID van de schijf), nooit de sleutel. Al het andere in de partitie is versleuteld. Een sector die op het medium helemaal nul is, wordt als nul gelezen en niet ontsleuteld, zodat discard, nooit beschreven ruimte en het laten groeien van de schijf werken als voorheen.
  • Configuratie. storage.encryption = "aes-256-xts" en storage.disk_key (een bestand met 64 byte sleutel). Het blobformaat wordt 4 alleen als versleuteling aanstaat, zodat alle andere images nog op oudere kernels starten. Een oudere lezer weigert de onbekende vlag in het superblok.
  • Geen hardware-AES: weigering. De kernel weigert storage.encryption op een processor zonder AES-instructies (of als [cpu] disable ze uitschakelt), in plaats van stilletjes een trage softwareversie te draaien.
  • De conversie is offline. De kernel versleutelt of ontsleutelt nooit een schijf bij het opstarten. urmqtt-disk encrypt, decrypt en rekey doen dat met uitgeschakelde VM: het conversierecord wordt als eerste geschreven en als laatste gewist (zolang het bestaat, weigert de kernel de schijf), brokken van 1 MiB worden eerst in een journaal op de host bewaard, daarna wordt de geconverteerde kopie geschreven, en het superblok wordt als laatste omgezet. Getest met een stroomuitval na elke afzonderlijke schrijfactie voor alle drie de procedures, schoon en half geschreven, met de cache weggegooid en behouden: altijd een geldige schijf met alle gegevens na --resume.

Gemeten. XTS-doorvoer per kern op Apple silicon (één MiB aan sectoren in een lus): 3.109 MB/s versleutelen en 3.103 MB/s ontsleutelen met hardware-AES, tegenover 252 en 258 MB/s in software. Daarom staan we software niet toe. db-bench in een VM (aarch64, HVF, 5.000 inserts in 50 transacties, vijf runs van elke variant, afwisselend):

ops/s, vijf runs mediaan
versleuteling uit 49.239; 47.401; 50.802; 53.417; 46.531 49.239
versleuteling aan 57.790; 46.173; 45.939; 52.787; 49.632 49.632

Het verschil tussen de medianen is +0,8 %, en de spreiding binnen één variant (46.000 tot 58.000) is groter dan dat, dus de kosten zijn in deze test niet meetbaar. Dat verwachten we ook: de hele db-bench-run schrijft ongeveer 6 MB, wat ongeveer 2 ms AES is, en een commit wacht op de flush, niet op het cijfer.

Het diagnose-item toont nu ook AES-XTS naast AES-GCM (op HVF meet de kernel ongeveer 800 MiB/s voor XTS tegenover ongeveer 560 MiB/s voor AES-GCM) en laat zien of hardware-AES beschikbaar is.

Het diagnose-item gestart in de webemulator op aldo.tech, in een browsertab: in het CRYPTO-paneel toont de laatste regel AES-XTS en "hw AES". De cijfers beschrijven de emulator (6 MiB/s), niet urMQTT.
Het diagnose-item gestart in de webemulator op aldo.tech, in een browsertab: in het CRYPTO-paneel toont de laatste regel AES-XTS en "hw AES". De cijfers beschrijven de emulator (6 MiB/s), niet urMQTT.

Wat XTS niet geeft. Er is geen authenticatie: een gewijzigde versleutelde sector ontsleutelt tot ruis in blokken van 16 byte (de CRC's en de scrubber vinden dat), en wie naar het volume kan schrijven, kan hele sectoren verwisselen of terugdraaien tussen twee snapshots. Het beschermt een gestolen volume of snapshot, niet een gestolen ISO, want de sleutel staat in de configuratie van het image. Wat zichtbaar blijft: de GPT, het superblok, de UUID en welke sectoren ooit zijn beschreven.

Wat we onderweg in ons eigen werk vonden. Bij het schrijven van het hoofdstuk over cryptografie ontdekten we twee dingen. De stap die de XTS-tweak vermenigvuldigt, had een vertakking die afhing van een bit afgeleid van de sleutel; nu is hij vertakkingsvrij. En de bewering "gecontroleerd tegen IEEE 1619-vector 10" dekte alleen het eerste versleutelde blok; nu is er ook een naïeve referentie-implementatie (u128-rekenwerk, blok voor blok) die hele sectoren in beide richtingen controleert op 200 willekeurige sleutels en sectoren, en de documentatie zegt wat werkelijk is gecontroleerd.

Een boot die een gewijzigd image weigert (H8)

De eis uit de auditlijst was eenvoudig en tot nu toe niet geverifieerd: geen enkele test liet zien dat een gewijzigde kernel niet start. De hash-pin in limine.conf bestond, maar zonder bewijs.

  • Een test die een byte verandert. scripts/tamper.py draait één bit om in elke kopie van de kernel (of van de configuratiemodule) binnen een gebouwde ISO of raw image, en SMOKE_ONLY=integrity laat zien dat de loader weigert: 24 controles geslaagd, op aarch64 UEFI, x86_64 UEFI en x86_64 BIOS, met controles die wel starten. De weigeringsmelding wordt van de seriële poort gelezen via een schakelaar die alleen voor tests bedoeld is. Op BIOS komt de melding alleen op het scherm, dus die test beweert alleen dat de machine niet start.
  • De configuratie van de loader ingeschreven (ADR-250). limine enroll-config werkt op BOOTAA64.EFI, BOOTX64.EFI en de BIOS-fase, zodat ook een gewijzigde limine.conf (met een toegevoegd menu-item of argument) wordt geweigerd. urmqtt-config pack --iso schrijft opnieuw in.
  • Het Secure Boot-pad (ADR-251). scripts/sign-loaders.sh (testsleutels, ondertekenen, verifiëren, een OVMF-variabelenopslag), mkiso.sh --sb-key en de groep secureboot op x86_64: een ondertekende ISO start en wordt reproduceerbaar gebouwd, terwijl een niet-ondertekende, een met de verkeerde sleutel ondertekende en een na het ondertekenen gewijzigde Access Denied krijgen (10 controles geslaagd). Limine weigert onder Secure Boot nog steeds een gewijzigde kernel.
  • De regels van het installatie-item (ADR-253). Het plan sprak van één woord op de opdrachtregel, install. De productiekernel accepteert in werkelijkheid drie (install, live, diag), en alleen install schrijft. De test (9 controles) draait dit op drie machines met een lege schijf waarvan de hash niet mag veranderen.
  • Geheimhouding van het image (ADR-252). Een luide waarschuwing als een productie-image buiten de CI wordt gebouwd, en een scan van het image, de buildlogs en de seriële logs op sleutelbytes in veel codeerwijzen (11 controles).
  • TPM, alleen ADR-254. Wat het zou betekenen om de serversleutel te verzegelen in een vTPM of TPM 2.0. Aanbeveling: niet bouwen voor een op VPS gerichte release. We hebben niet gecontroleerd of er aanbieders zijn die een vTPM bieden.

Wat dit NIET betekent (vastgelegd als H8-W1 tot en met H8-W6 in AUDIT.md): een geïnstalleerde schijf start niet onder Secure Boot omdat de configuratiemodule geen hash heeft (ADR-217); een ondertekende ISO kan achteraf niet worden geconfigureerd (eerst configureren, dan ondertekenen); het BIOS-pad heeft geen handtekening; er is geen bescherming tegen terugzetten naar een oudere versie; op aarch64 dwingt de firmware hier Secure Boot niet af, dus alleen de handtekening van de loader is geverifieerd; er is geen projectsleutel, dus de release-images zijn niet ondertekend voor Secure Boot; en geen enkele fysieke machine is geprobeerd.

Het auditpakket (H10)

docs/AUDIT.md is nu een pakket dat een externe auditor zonder vragen kan gebruiken: het systeem op één pagina, de basis die we vertrouwen (89.612 regels eigen Rust in het image, 67 gekoppelde pakketten van derden), de invoer en de bijbehorende parsers, de matrix van beveiligingen met hun tests (en de beveiligingen zonder tests), de zwakheden met hun status, een tabel van elke via het netwerk bereikbare lus en allocatie met zijn grens en test, instructies voor bouwen en testen, een voorgestelde reikwijdte voor een externe audit in tien gebieden en een red-teamlijst met 31 aanvallen en hun resultaten. SECURITY.md heeft een STRIDE-tabel per component.

  • Een correctie op de eerste tekst. In de tekst van 29 oktober staat dat elk unsafe-blok een // SAFETY:-commentaar heeft dat door een lint wordt gecontroleerd. Dat klopte niet: de lint (deny(unsafe_code)) beperkt alleen waar unsafe mag staan, en de commentaren worden sinds H0 bijgehouden door een gegenereerde inventaris, die al toen ongeveer 30 plaatsen zonder motivering vond en bij de audit 108 (omdat er intussen nieuwe soorten plaatsen waren bijgekomen, bijvoorbeeld de attributen #[unsafe(no_mangle)] en link_section). Daarom is het de inventaris, en niet de lint, die ze nu op nul houdt.
  • 889 unsafe-plaatsen, elk met een schriftelijke motivering. De 108 die er geen hadden (vooral #[unsafe(no_mangle)] en link_section, enkele unsafe fn) kregen een commentaar (een AI-agent schreef het, alleen commentaren, en ik heb een steekproef beoordeeld); de gegenereerde inventaris in de CI registreert elke nieuwe plaats zonder motivering. Een motivering is een bewering die moet worden beoordeeld, geen bewijs, en dat is punt 5 van de voorgestelde reikwijdte.
  • Vijf open bevindingen blijven open, vastgelegd: geen fuzzing van de PCI- en virtio-parsers, geen test die naar de paginatabellen schrijft nadat ze zijn vergrendeld, de looptijd van P-256, X25519 en Ed25519 is niet gemeten, geen fuzzing van bewust vervaardigde SQLite-bestanden (ingeperkt door H6), en 53 van de 67 gekoppelde pakketten zijn cargo-vet-uitzonderingen, geen audits.

Fouten die we zelf vonden

  • TLS-verkeersgeheimen werden niet gewist (H0-W2, open sinds de eerste dag van de audit): de tabel met vastgelegde geheimen was een Vec die door remove en swap_remove werd verschoven en kopieën achterliet. Nu is de tabel slotgebaseerd: een slot wordt ter plekke gewist wanneer het wordt genomen, weggegooid of verdrongen, en niets wordt verschoven.
  • Een van de sleutel afhankelijke vertakking in XTS (hierboven).
  • log-slow-flood op x86_64 faalde ("log ring full"), ook op de broncode van v0.2.1. De oorzaak was geen defect, maar de dimensionering van de test: het scenario liet elke kern op 25 ms draaien, terwijl ADR-090 het op 50 ms had gemeten. Op x86_64 is COM1 per byte afgevangen poort-I/O, een kern die naar de sinks schrijft heeft ongeveer 9 ms per regel nodig, en drie kernen op 25 ms vragen meer dan een seconde werk per seconde, dus de ringen lopen vol (de bedoelde overloop). De test draait nu op x86_64 op 50 ms (geslaagd, 30 van 30 in de run die de basisscenario's omvat); op aarch64 blijft het 25 ms.
  • Het samenvoegen van de branches maakte de oude templates kapot. Het opnieuw inschrijven van de configuratie in H8 vereiste een eindmarkering in limine.conf, die templates van vóór H8 niet hebben, dus urmqtt-config pack --iso en de builderpagina weigerden elke release van vóór H8, inclusief de v0.2.x die de pagina aanbiedt. De buildertests in de browser ving het op (4 van 100 faalden), niet de eigen test van H8. Hersteld (een template zonder markering wordt geconfigureerd als voorheen) en er is een test toegevoegd die met de oorspronkelijke fout faalt als het herstel wordt weggehaald.
  • Onze documentatie beweerde ten onrechte dat x86_64 alleen in een emulator was geverifieerd. X11 draaide het productie-image op een echte Contabo KVM-VPS (4 vCPU AMD EPYC) met wederzijdse TLS onder belasting. Overal gecorrigeerd: x86_64 draaide op een echte KVM-VPS bij de H7-commit en in een emulator, maar de code die daarna is toegevoegd (versleuteling, bootintegriteit) heeft niet op KVM gedraaid, en is niet op fysieke hardware geprobeerd.

D1 en het aanbod "uw service als unikernel"

De publieke demobroker is gebouwd, maar niet gepubliceerd: de configuratie (live-modus, ACL demo/{id}/#, gewone MQTT alleen voor de brug), een script dat een gepubliceerde test-CA en democertificaten maakt, een WebSocket-naar-TCP-brug (alleen de standaardbibliotheek van Python, limieten op verbindingen, snelheid en grootte, een controle van de herkomst), een pagina "Probeer urMQTT live" met een MQTT-client in gewoon JavaScript, een runbook en een dreigingsmodel. Getest op loopback en in QEMU (18 scenario's, 26 brugtests). Niet getest: een echte VPS, DNS, Caddy, het containerimage. Er is een aparte VPS en uitdrukkelijke goedkeuring voor publieke blootstelling nodig.

Daarnaast heeft de site nu een aanbod, Unikernel (15 talen, zonder prijzen): uw service aanpassen zodat hij als unikernel draait (haalbaarheidsbeoordeling, aanpassing, pilot, ondersteuning), met urMQTT als referentie. De tekst noemt de grenzen van de huidige basis uitdrukkelijk: IPv4, TCP alleen als server, KVM, Rust als natuurlijke keuze, C alleen als geïsoleerde worker.

De release v0.3.0

De tag v0.3.0 is op 3 november gezet op de commit f1a7fbc, en de releaseworkflow slaagde. De release is gemarkeerd als pre-release en bevat de ontwikkel-aarch64-kernel, -ISO en -image, de template-ISO (kernels voor aarch64 en x86_64 en een lege configuratie, die urmqtt-config pack --iso en de builderpagina omzetten in uw image), een CycloneDX-SBOM, SHA256SUMS en de minisign-handtekening ervan, en SLSA-herkomstattesten. Na publicatie geverifieerd, in een lege map: de handtekening van SHA256SUMS is geldig, alle vijf de bestanden komen overeen met de controlesommen, beide attesten (de kernel, en de ISO met het CycloneDX-predicaat) slagen, de SBOM heeft 221 componenten en de template wordt geconfigureerd (pack --iso schrijft zes kopieën van de loader opnieuw in). De ondertekeningssleutel is nog steeds een testsleutel.

De volgorde deed ertoe en is het waard om op te schrijven: de builderpagina is bijgewerkt vóór de tag (hij kent formaat 4 en alle oudere templates), en pas daarna nam de spiegel op aldo.tech de release over. De spiegel vermeldde hem als ondertekend en geverifieerd (ok, configuratieformaat 4, de sha256 van de template gelijk aan die in SHA256SUMS), en de live pagina biedt hem aan als standaardrelease en bouwt in de browser in ongeveer 20 seconden een ISO. De releasenotities zeggen ook wat zelden wordt opgeschreven: de CI op GitHub is niet groen (zie hieronder), ondersteuning is alleen voor de laatste versie (v0.2.1 en ouder vallen af), en de audit is door het project zelf gedaan.

Op de site hebben we onderweg bijna onszelf schade berokkend: het publicatiescript van de site (publish.sh) draait rsync --delete, en de releasespiegel is van een andere gebruiker, dus elke publicatie eindigde met fout 23 (en zou de knop "Publiceren" in de blogadmin hebben kapotgemaakt). Hersteld met een uitzondering voor /urmqtt/releases/. Een vergelijkbare bevinding: deploy.sh met --delete verwijdert de builderpagina uit de generator, dus die moet vóór het publiceren worden teruggezet.

Wat nog niet is bewezen

  • De CI op GitHub is rood. Niet alleen vandaag: de laatste run van de volledige CI vóór de tag had zeven mislukte jobs, en de runs terug tot 31 oktober zijn grotendeels rood. De mislukte: lint (shellcheck-waarschuwingen in scripts), TLS-interop (alle wolfSSL-gevallen), de toeleveringsketen (de uitzonderingenlijst vet-exemptions.md is op Linux niet bijgewerkt), fuzz smoke (in die job ontbreken de ontwikkelcertificaten), de sanitizers (LeakSanitizer meldt een lek in tests), tests op Linux aarch64 (één test van de tool faalt met "Broken pipe") en één Kani-bewijs voor het timerwiel (timer wheel) slaagt niet. We hebben nog niet uitgezocht welke hiervan de omgeving zijn en welke echte gebreken; zolang we dat niet hebben gedaan, beweren we niet dat de release de CI heeft doorstaan.
  • De ondertekeningssleutel is een testsleutel, en de release-images zijn ontwikkelbuilds met testcertificaten (behalve de template).
  • De vertalingen van de pagina's en van deze tekst naar de overige 13 talen zijn door AI geschreven en niet door moedertaalsprekers gecontroleerd.
  • Schijfversleuteling is niet gemeten op x86_64 (niet op KVM en niet op fysieke hardware), en ook de doorvoer van de telemetrie en de opstarttijd met versleuteling zijn niet gemeten. Het staat niet op de builderpagina (er is een sleuteldialoog voor nodig).
  • Er is geen onafhankelijke beveiligingsaudit. De reikwijdte is opgeschreven; we hebben geen auditor.
  • Secure Boot is gedeeltelijk (hierboven), zonder fysieke machine.
  • Bare metal, IOMMU, de publieke demobroker: niet gedaan of niet gepubliceerd.

Wat volgt

  1. De CI uitzoeken en repareren (eerst de omgeving, dan het Kani-bewijs en de test die faalt op aarch64 Linux), omdat we een release zonder groene CI niet als af beschouwen.
  2. Een besluit over D1 (een aparte VPS, DNS, goedkeuring voor publieke blootstelling).
  3. Fuzzing van de PCI- en virtio-parsers en een test voor de vergrendelde paginatabellen (H0-W9, H0-W10).
  4. Een sleuteldialoog op de builderpagina, zodat versleuteling beschikbaar wordt zonder de opdrachtregel.
  5. Na de release: bare metal (X12 tot en met X16) en IOMMU, in een volgorde die afhangt van de hardware-inventaris van de test-pc.

De code staat op github.com/zeljan-alduk/urMQTT; docs/PROGRESS.md blijft de enige bron van waarheid over wat echt werkt, docs/PERFORMANCE.md over wat is gemeten en docs/AUDIT.md over wat nog niet is bewezen. Pre-alpha, zoals eerder; foutmeldingen en beveiligingsmeldingen volgens de instructies in SECURITY.md zijn welkom.

Reacties

Reactieregels

Reactieregels

De reacties zijn er om ervaringen en vragen over de onderwerpen van het blog te delen. Deze regels houden de discussie nuttig voor iedereen.

  1. Blijf beleefd Geen beledigingen, bedreigingen, intimidatie, haatzaaien of discriminatie op welke grond dan ook. Bekritiseer het argument, niet de persoon.
  2. Blijf bij het onderwerp Een reactie moet over het bericht of de discussie eronder gaan.
  3. Geen reclame of spam Geen advertenties, kettingberichten of links zonder context. Links en afbeeldingen zijn welkom als ze iets toevoegen.
  4. Privacy van anderen Plaats geen persoonsgegevens van anderen: volledige namen van privépersonen, adressen, telefoonnummers, e-mails, foto's zonder toestemming.
  5. Auteursrecht Plaats alleen inhoud die u mag delen. Vermeld de bron bij citaten.
  6. Eén identiteit Gebruik één bijnaam en doe u niet voor als iemand anders.
  7. Bevestiging per e-mail Een reactie wordt pas geplaatst nadat u de per e-mail gestuurde link bevestigt. Het adres wordt niet getoond en nergens anders voor gebruikt; onbevestigde reacties worden na 48 uur verwijderd.
  8. Moderatie Reacties die de regels overtreden worden zonder waarschuwing verborgen of verwijderd, en een discussie kan worden gesloten. Beslissingen van de moderatie zijn definitief.
  9. Verantwoordelijkheid De auteur is verantwoordelijk voor de inhoud van een reactie. Reacties geven niet de mening van ALDO TECH weer.
  10. Melden Meld ongepaste inhoud via info@aldo.tech.