← Blog

· 16 min di lettura #rust#mqtt#iot#unikernel#open source#security

urMQTT v0.3.0: cifratura del disco, un avvio che rifiuta un'immagine modificata e ciò che abbiamo rotto noi stessi

Cosa è successo dopo il terzo testo su urMQTT: cifratura del disco a riposo con un costo misurato, un test che modifica un byte nel kernel e mostra che il loader lo rifiuta, un pacchetto di audit con 889 siti unsafe motivati, gli errori che abbiamo trovato noi stessi (e un'affermazione che avevamo scritto in modo sbagliato) e il rilascio v0.3.0 con l'elenco di ciò che non è ancora dimostrato, compresa una CI rossa.

Il terzo testo, urMQTT dopo il VPS, si chiudeva con un elenco «Cosa viene dopo»: un broker demo pubblico (D1), un secondo giro sul flusso singolo lento, il percorso in chiaro contro Mosquitto sul VPS e, «dopo il rilascio», bare metal, IOMMU, Secure Boot e cifratura del disco. Questo testo dice cosa di tutto ciò è stato fatto, cosa no, e cosa abbiamo trovato lungo la strada. La cifratura del disco e una parte di Secure Boot sono entrate comunque prima del rilascio, perché erano sulla strada verso l'audit. Il secondo giro sul flusso singolo e il percorso in chiaro contro Mosquitto non sono stati fatti.

L'avvertenza iniziale resta la stessa: il progetto è pre-alpha, il rilascio punta ai VPS con KVM, e i numeri misurano codice, test e determinati esperimenti, non la maturità in produzione.

Com'è andata nel tempo

Dall'ultimo testo (1 novembre, 20:41) a oggi (3 novembre, 02:43) il ramo principale ha 37 nuovi commit, 215 in tutto. Come prima, il progetto è costruito con Claude Code (i commit portano Co-Authored-By: Claude), e questa volta tre rami hanno lavorato in parallelo (D1, H8, preparazione del rilascio), quindi l'ordine dei commit non è strettamente l'ordine del lavoro. I numeri sono calcolati con lo stesso metodo dell'ultimo testo (tutti i file .rs fuori da vendor/ e fuzz/, le funzioni #[test], i titoli ## ADR- in docs/DECISIONS.md) e verificati sul vecchio commit, che dà gli stessi numeri di allora.

Momento Commit Ora Righe di Rust Test ADR Commit
ultimo testo d21e3ec 1 nov., 20:41 112.408 926 126 178
oggi f1a7fbc 3 nov., 02:43 115.148 962 133 215

La crescita del codice è modesta (+2.740 righe, +36 test), perché questa volta non c'è stato molto codice nuovo: la maggior parte del lavoro sono test, script, documentazione e verifiche. Gli script e il JavaScript non rientrano nel conteggio delle righe.

Cifratura del disco a riposo (S7)

Cosa avevamo pianificato. Cifratura facoltativa della partizione dei dati, disattivata di default, con un costo misurato. Cosa abbiamo costruito (ADR-240):

  • Uno strato sotto il disco. CryptDev avvolge il dispositivo a blocchi sotto Disk, così il layout, la tabella delle dimensioni, SQLite, la telemetria e i backup lavorano su testo in chiaro e non sanno nulla della cifratura. L'algoritmo è AES-256-XTS (IEEE 1619), un settore da 512 byte per unità di dati, e non un blocco da 4 KiB come nel piano: l'archiviazione scrive per settori, e un'unità da 4 KiB trasformerebbe ogni scrittura da 512 byte in una lettura-modifica-scrittura che un'interruzione di corrente può spezzare a metà.
  • Il superblocco resta in chiaro. I due slot del superblocco portano un flag e una verifica della chiave (HMAC-SHA-256 di un'etichetta e dell'UUID del disco), mai la chiave. Tutto il resto della partizione è cifrato. Un settore che sul supporto è interamente zero viene letto come zero e non viene decifrato, quindi discard, spazio mai scritto e crescita del disco funzionano come prima.
  • Configurazione. storage.encryption = "aes-256-xts" e storage.disk_key (un file con 64 byte di chiave). Il formato del blob diventa 4 solo quando la cifratura è attiva, così tutte le altre immagini continuano ad avviarsi su kernel più vecchi. Un lettore più vecchio rifiuta il flag sconosciuto nel superblocco.
  • Senza AES hardware: rifiuto. Il kernel rifiuta storage.encryption su un processore senza istruzioni AES (o quando [cpu] disable le disattiva), invece di eseguire in silenzio una lenta versione software.
  • La conversione è offline. Il kernel non cifra né decifra mai un disco all'avvio. urmqtt-disk encrypt, decrypt e rekey lo fanno con la VM spenta: un record di conversione viene scritto per primo e cancellato per ultimo (finché esiste, il kernel rifiuta il disco), pezzi da 1 MiB vengono prima salvati in un journal sull'host, poi viene scritta la copia convertita, e il superblocco viene commutato per ultimo. Provato con un'interruzione di corrente dopo ogni singola scrittura per tutte e tre le procedure, pulita e scritta a metà, con la cache scartata e conservata: dopo --resume sempre un disco valido con tutti i dati.

Misurato. Throughput XTS per core su Apple silicon (un MiB di settori in un ciclo): 3.109 MB/s in cifratura e 3.103 MB/s in decifratura con AES hardware, contro 252 e 258 MB/s in software. Per questo non permettiamo il software. db-bench in una VM (aarch64, HVF, 5.000 inserimenti in 50 transazioni, cinque esecuzioni di ciascuna variante, alternate):

op/s, cinque esecuzioni mediana
cifratura disattivata 49.239; 47.401; 50.802; 53.417; 46.531 49.239
cifratura attivata 57.790; 46.173; 45.939; 52.787; 49.632 49.632

La differenza delle mediane è +0,8 %, e la dispersione all'interno di una variante (da 46.000 a 58.000) è maggiore, quindi il costo non è misurabile in questo test. È anche ciò che ci aspettiamo: l'intera esecuzione di db-bench scrive circa 6 MB, cioè circa 2 ms di AES, e un commit aspetta il flush, non il cifrario.

La voce Diagnostica ora mostra anche AES-XTS accanto ad AES-GCM (su HVF il kernel misura circa 800 MiB/s per XTS contro circa 560 MiB/s per AES-GCM) e indica se l'AES hardware è disponibile.

La voce Diagnostica avviata nell'emulatore web su aldo.tech, in una scheda del browser: nel riquadro CRYPTO l'ultima riga mostra AES-XTS e «hw AES». I numeri descrivono l'emulatore (6 MiB/s), non urMQTT.
La voce Diagnostica avviata nell'emulatore web su aldo.tech, in una scheda del browser: nel riquadro CRYPTO l'ultima riga mostra AES-XTS e «hw AES». I numeri descrivono l'emulatore (6 MiB/s), non urMQTT.

Cosa XTS non dà. Non c'è autenticazione: un settore cifrato modificato si decifra in rumore a blocchi da 16 byte (i CRC e lo scrubber lo trovano), e chi può scrivere sul volume può scambiare o ripristinare interi settori tra due snapshot. Protegge un volume o uno snapshot rubato, non un'ISO rubata, perché la chiave è nella configurazione dell'immagine. Restano visibili: la GPT, il superblocco, l'UUID e quali settori sono mai stati scritti.

Cosa abbiamo trovato lungo la strada nel nostro stesso lavoro. Scrivendo il capitolo sulla crittografia abbiamo accertato due cose. Il passo che moltiplica il tweak in XTS aveva un ramo dipendente da un bit derivato dalla chiave; ora è privo di rami. E l'affermazione «verificato rispetto al vettore 10 di IEEE 1619» copriva solo il primo blocco cifrato; ora esiste anche un'implementazione di riferimento ingenua (aritmetica u128, blocco per blocco) che verifica settori interi in entrambe le direzioni su 200 chiavi e settori casuali, e la documentazione dice cosa è stato davvero verificato.

Un avvio che rifiuta un'immagine modificata (H8)

Il requisito della lista di audit era semplice e finora non verificato: nessun test mostrava che un kernel modificato non si avvia. Il pin dell'hash in limine.conf esisteva, ma senza prove.

  • Un test che modifica un byte. scripts/tamper.py inverte un bit in ogni copia del kernel (o del modulo di configurazione) dentro un'ISO costruita o un'immagine raw, e SMOKE_ONLY=integrity mostra che il loader rifiuta: 24 controlli superati, su aarch64 UEFI, x86_64 UEFI e x86_64 BIOS, con controlli di riferimento che si avviano. Il messaggio di rifiuto viene letto dalla porta seriale tramite un interruttore riservato ai test. Su BIOS il messaggio arriva solo sullo schermo, quindi quel test afferma soltanto che la macchina non si avvia.
  • La configurazione del loader registrata (ADR-250). limine enroll-config agisce su BOOTAA64.EFI, BOOTX64.EFI e sullo stadio BIOS, così viene rifiutato anche un limine.conf modificato (una voce di menu o un argomento aggiunti). urmqtt-config pack --iso lo registra di nuovo.
  • Il percorso Secure Boot (ADR-251). scripts/sign-loaders.sh (chiavi di prova, firma, verifica, un archivio di variabili OVMF), mkiso.sh --sb-key e il gruppo secureboot su x86_64: un'ISO firmata si avvia e si costruisce in modo riproducibile, mentre una non firmata, una firmata con la chiave sbagliata e una modificata dopo la firma ottengono Access Denied (10 controlli superati). Limine continua a rifiutare un kernel modificato anche sotto Secure Boot.
  • Le regole della voce di installazione (ADR-253). Il piano parlava di una sola parola sulla riga di comando, install. Il kernel di produzione in realtà ne accetta tre (install, live, diag), e solo install scrive. Il test (9 controlli) lo esegue su tre macchine con un disco vuoto il cui hash non deve cambiare.
  • Segretezza dell'immagine (ADR-252). Un avviso rumoroso quando un'immagine di produzione viene costruita fuori dalla CI, e la scansione dell'immagine, dei log di build e dei log seriali alla ricerca di byte della chiave in molte codifiche (11 controlli).
  • TPM, solo ADR-254. Cosa significherebbe sigillare la chiave del server in un vTPM o TPM 2.0. Raccomandazione: non costruirlo per un rilascio orientato ai VPS. Non abbiamo verificato quali fornitori offrano un vTPM.

Cosa questo NON significa (annotato come H8-W1 fino a H8-W6 in AUDIT.md): un disco installato non si avvia sotto Secure Boot perché il suo modulo di configurazione non ha un hash (ADR-217); un'ISO firmata non può essere configurata in seguito (prima configurare, poi firmare); il percorso BIOS non ha firma; non c'è protezione contro il ritorno a una versione più vecchia; su aarch64 il firmware qui non applica Secure Boot, quindi è stata verificata solo la firma del loader; non esiste una chiave di progetto, quindi le immagini del rilascio non sono firmate per Secure Boot; e nessuna macchina fisica è stata provata.

Il pacchetto di audit (H10)

docs/AUDIT.md è ora un pacchetto che un revisore esterno può usare senza fare domande: il sistema in una pagina, la base di cui ci fidiamo (89.612 righe di Rust proprio nell'immagine, 67 pacchetti di terzi collegati), gli ingressi e i loro parser, la matrice delle protezioni con i relativi test (e le protezioni senza test), le debolezze con il loro stato, una tabella di ogni ciclo e allocazione raggiungibile dalla rete con il suo limite e il suo test, le istruzioni per costruire e testare, un ambito proposto per un audit esterno in dieci aree e un elenco red team con 31 attacchi e i loro risultati. SECURITY.md ha una tabella STRIDE per componente.

  • Una correzione al primo testo. Il testo del 29 ottobre dice che ogni blocco unsafe ha un commento // SAFETY: verificato da un lint. Non era vero: il lint (deny(unsafe_code)) limita soltanto dove unsafe può stare, e i commenti sono tracciati fin da H0 da un inventario generato, che già allora trovava circa 30 siti senza motivazione e, all'audit, 108 (perché nel frattempo erano comparsi nuovi tipi di siti, per esempio gli attributi #[unsafe(no_mangle)] e link_section). Per questo è l'inventario, e non il lint, a tenerli ora a zero.
  • 889 siti unsafe, ciascuno con una motivazione scritta. I 108 che non l'avevano (per lo più #[unsafe(no_mangle)] e link_section, qualche unsafe fn) hanno ricevuto un commento (l'ha scritto un agente IA, solo commenti, e io ne ho esaminato un campione); l'inventario generato nella CI segnala ogni nuovo sito senza motivazione. Una motivazione è un'affermazione da rivedere, non una dimostrazione, ed è il punto 5 dell'ambito proposto.
  • Cinque riscontri aperti restano aperti, annotati: nessun fuzzing dei parser PCI e virtio, nessun test che scriva nelle tabelle delle pagine dopo il blocco, il tempo di esecuzione di P-256, X25519 e Ed25519 non è stato misurato, nessun fuzzing di file SQLite costruiti apposta (contenuto da H6), e 53 dei 67 pacchetti collegati sono eccezioni cargo-vet, non audit.

Errori che abbiamo trovato noi stessi

  • I segreti del traffico TLS non venivano cancellati (H0-W2, aperto dal primo giorno dell'audit): la tabella dei segreti catturati era un Vec che remove e swap_remove spostavano lasciando copie. Ora la tabella è a slot: uno slot viene cancellato sul posto quando viene prelevato, scartato o espulso, e nulla viene spostato.
  • Un ramo dipendente dalla chiave in XTS (sopra).
  • log-slow-flood su x86_64 falliva («log ring full»), anche sul sorgente di v0.2.1. La causa non era un difetto, ma il dimensionamento del test: lo scenario spingeva ogni core a 25 ms, mentre l'ADR-090 lo aveva misurato a 50 ms. Su x86_64 COM1 è un I/O su porta intercettato a ogni byte, un core che scrive sui sink impiega circa 9 ms per riga, e tre core a 25 ms richiedono più di un secondo di lavoro al secondo, quindi gli anelli si riempiono (l'overflow previsto). Ora il test su x86_64 gira a 50 ms (superato, 30 su 30 nell'esecuzione che include gli scenari di base); su aarch64 resta a 25 ms.
  • L'unione dei rami ha rotto i vecchi template. La nuova registrazione della configurazione in H8 richiedeva un marcatore finale in limine.conf, che i template precedenti a H8 non hanno, quindi urmqtt-config pack --iso e la pagina del builder rifiutavano ogni rilascio precedente a H8, comprese le v0.2.x che la pagina offre. Lo hanno intercettato i test del builder nel browser (4 su 100 falliti), non il test proprio di H8. Corretto (un template senza marcatore viene configurato come prima) e aggiunto un test che fallisce con l'errore originale quando la correzione viene tolta.
  • La nostra documentazione sosteneva a torto che x86_64 fosse stato verificato solo in un emulatore. X11 ha eseguito l'immagine di produzione su un vero VPS KVM di Contabo (4 vCPU AMD EPYC) con TLS reciproco sotto carico. Corretto ovunque: x86_64 è stato eseguito su un vero VPS KVM al commit H7 e in un emulatore, ma il codice aggiunto dopo (cifratura, integrità dell'avvio) non è girato su KVM, e non è stato provato su hardware fisico.

D1 e l'offerta «il vostro servizio come unikernel»

Il broker demo pubblico è costruito, ma non pubblicato: la configurazione (modalità live, ACL demo/{id}/#, MQTT in chiaro solo per il bridge), uno script che crea una CA di prova pubblicata e certificati demo, un bridge da WebSocket a TCP (solo la libreria standard di Python, limiti su connessioni, frequenza e dimensione, un controllo dell'origine), una pagina «Prova urMQTT dal vivo» con un client MQTT in JavaScript puro, un runbook e un modello di minaccia. Testato su loopback e in QEMU (18 scenari, 26 test del bridge). Non testati: un vero VPS, il DNS, Caddy, l'immagine del container. Servono un VPS separato e un'approvazione esplicita per l'esposizione pubblica.

Inoltre il sito ha ora un'offerta, Unikernel (15 lingue, senza prezzi): l'adattamento del vostro servizio perché giri come unikernel (valutazione di fattibilità, adattamento, pilota, supporto), con urMQTT come riferimento. Il testo dichiara esplicitamente i limiti della base attuale: IPv4, TCP solo come server, KVM, Rust scelta naturale, C solo come worker isolato.

Il rilascio v0.3.0

Il tag v0.3.0 è stato posto il 3 novembre sul commit f1a7fbc, e il workflow di rilascio è riuscito. Il rilascio è contrassegnato come pre-release e contiene il kernel di sviluppo aarch64, l'ISO e l'immagine, l'ISO template (kernel per aarch64 e x86_64 e una configurazione vuota, che urmqtt-config pack --iso e la pagina del builder trasformano nella vostra immagine), un SBOM CycloneDX, SHA256SUMS e la sua firma minisign, e attestazioni di provenienza SLSA. Verificato dopo la pubblicazione, in una directory vuota: la firma di SHA256SUMS è valida, tutti e cinque i file corrispondono alle somme di controllo, entrambe le attestazioni (il kernel, e l'ISO con il predicato CycloneDX) passano, l'SBOM ha 221 componenti, e il template si configura (pack --iso registra di nuovo sei copie del loader). La chiave di firma è ancora una chiave di prova.

L'ordine era importante e vale la pena annotarlo: la pagina del builder è stata aggiornata prima del tag (conosce il formato 4 e tutti i template più vecchi), e solo dopo il mirror su aldo.tech ha preso il rilascio. Il mirror lo ha elencato come firmato e verificato (ok, formato di configurazione 4, sha256 del template uguale a quello in SHA256SUMS), e la pagina live lo offre come rilascio predefinito e costruisce un'ISO nel browser in circa 20 secondi. Le note di rilascio dicono anche ciò che si scrive raramente: la CI su GitHub non è verde (vedi sotto), il supporto vale solo per l'ultima versione (v0.2.1 e precedenti terminano), e l'audit lo ha fatto il progetto stesso.

Sul sito abbiamo quasi fatto un danno a noi stessi lungo la strada: lo script di pubblicazione del sito (publish.sh) esegue rsync --delete, e il mirror dei rilasci appartiene a un altro utente, quindi ogni pubblicazione finiva con l'errore 23 (e avrebbe rotto il pulsante «Pubblica» nell'admin del blog). Corretto con un'esclusione per /urmqtt/releases/. Un riscontro simile: deploy.sh con --delete rimuove la pagina del builder dal generatore, quindi va rimessa prima di pubblicare.

Cosa non è ancora dimostrato

  • La CI su GitHub è rossa. Non solo oggi: l'ultima esecuzione della CI completa prima del tag aveva sette job falliti, e le esecuzioni fino al 31 ottobre sono per lo più rosse. Hanno fallito: il lint (avvisi shellcheck negli script), l'interoperabilità TLS (tutti i casi wolfSSL), la supply chain (l'elenco di eccezioni vet-exemptions.md non è aggiornato su Linux), il fuzz smoke (in quel job mancano i certificati di sviluppo), i sanitizer (LeakSanitizer segnala una perdita nei test), i test su Linux aarch64 (un test dello strumento fallisce con «Broken pipe») e una dimostrazione Kani per la ruota dei timer (timer wheel) non passa. Non abbiamo ancora smistato quali di questi siano ambiente e quali difetti reali; finché non lo facciamo, non affermiamo che il rilascio abbia superato la CI.
  • La chiave di firma è una chiave di prova, e le immagini del rilascio sono build di sviluppo con certificati di prova (tranne il template).
  • Le traduzioni delle pagine e di questo testo nelle altre 13 lingue sono state scritte da un'IA e non sono state verificate da madrelingua.
  • La cifratura del disco non è stata misurata su x86_64 (né su KVM né su hardware fisico), e non sono stati misurati né il throughput della telemetria né il tempo di avvio con la cifratura. Nella pagina del builder non c'è (serve una finestra per la chiave).
  • Non c'è un audit di sicurezza indipendente. L'ambito è scritto; un revisore non lo abbiamo.
  • Secure Boot è parziale (sopra), senza una macchina fisica.
  • Bare metal, IOMMU, broker demo pubblico: non finito o non pubblicato.

Cosa viene dopo

  1. Smistare e riparare la CI (prima l'ambiente, poi la dimostrazione Kani e il test che fallisce su Linux aarch64), perché un rilascio senza CI verde non lo consideriamo finito.
  2. Una decisione su D1 (VPS separato, DNS, approvazione per l'esposizione pubblica).
  3. Fuzzing dei parser PCI e virtio e un test per le tabelle delle pagine bloccate (H0-W9, H0-W10).
  4. Una finestra per la chiave nella pagina del builder, così la cifratura diventa disponibile senza riga di comando.
  5. Dopo il rilascio: bare metal (da X12 a X16) e IOMMU, in un ordine che dipende dall'inventario hardware del PC di prova.

Il codice è su github.com/zeljan-alduk/urMQTT; docs/PROGRESS.md resta l'unica fonte di verità su ciò che funziona davvero, docs/PERFORMANCE.md su ciò che è stato misurato, e docs/AUDIT.md su ciò che non è ancora dimostrato. Pre-alpha, come prima; segnalazioni di bug e segnalazioni di sicurezza secondo le istruzioni in SECURITY.md sono benvenute.

Commenti

Regole dei commenti

Regole dei commenti

I commenti servono a condividere esperienze e domande sui temi del blog. Queste regole mantengono la discussione utile per tutti.

  1. Cortesia Niente insulti, minacce, molestie, incitamento all'odio o discriminazione per qualsiasi motivo. Criticate l'argomento, non la persona.
  2. In tema Un commento deve riguardare l'articolo o la discussione che lo segue.
  3. Niente pubblicità o spam Niente pubblicità, catene di messaggi o link senza contesto. Link e immagini sono benvenuti quando arricchiscono la discussione.
  4. Privacy altrui Non pubblicate dati personali di altri: nomi completi di privati, indirizzi, telefoni, e-mail, foto senza consenso.
  5. Diritto d'autore Pubblicate solo contenuti che potete condividere. Citando, indicate la fonte.
  6. Una sola identità Usate un solo nickname e non spacciatevi per qualcun altro.
  7. Conferma via e-mail Un commento viene pubblicato solo dopo la conferma del link inviato via e-mail. L'indirizzo non viene mostrato né usato per altro; i commenti non confermati vengono eliminati dopo 48 ore.
  8. Moderazione I commenti che violano le regole vengono nascosti o eliminati senza preavviso e una discussione può essere bloccata. Le decisioni dei moderatori sono definitive.
  9. Responsabilità L'autore è responsabile del contenuto del commento. I commenti non riflettono le opinioni di ALDO TECH.
  10. Segnalazioni Segnalate i contenuti inappropriati a info@aldo.tech.