urMQTT v0.3.0: шифрование диска, загрузка, которая отвергает измененный образ, и то, что мы сами сломали
Что произошло после третьего текста об urMQTT: шифрование диска в покое с измеренной ценой, тест, который меняет один байт в ядре и показывает, что загрузчик его отвергает, аудиторский пакет с 889 обоснованными местами unsafe, ошибки, которые мы нашли сами (и одно утверждение, которое мы написали неверно), и релиз v0.3.0 со списком того, что еще не доказано, включая красный CI.

Третий текст, urMQTT после VPS, закончился списком «Что дальше»: публичный демо-брокер (D1), второй раунд для медленного одиночного потока, обычный путь против Mosquitto на VPS и, «после релиза», bare metal, IOMMU, Secure Boot и шифрование диска. Этот текст говорит, что из этого сделано, что нет и что мы нашли по дороге. Шифрование диска и часть Secure Boot все же вошли до релиза, потому что стояли на пути к аудиту. Второй раунд для одиночного потока и обычный путь против Mosquitto не сделаны.
Предупреждение заранее прежнее: проект находится на стадии пре-альфа, релиз нацелен на VPS с KVM, а цифры измеряют код, тесты и определенные эксперименты, а не зрелость в продакшене.
Как это выглядело во времени
С предыдущего текста (1.11. в 20:41) до сегодняшнего дня (3.11. в 02:43) в главной ветке 37 новых коммитов, всего 215. Как и раньше, проект строится с Claude Code (коммиты несут Co-Authored-By: Claude), а в этот раз параллельно работали три ветки (D1, H8, подготовка релиза), поэтому порядок коммитов не строго совпадает с порядком работы. Цифры вычислены тем же методом, что и в прошлом тексте (все файлы .rs вне vendor/ и fuzz/, функции #[test], заголовки ## ADR- в docs/DECISIONS.md) и проверены на старом коммите, который дает те же цифры, что и тогда.
| Момент | Коммит | Время | Строк Rust | Тестов | ADR | Коммитов |
|---|---|---|---|---|---|---|
| прошлый текст | d21e3ec | 1.11. 20:41 | 112 408 | 926 | 126 | 178 |
| сегодня | f1a7fbc | 3.11. 02:43 | 115 148 | 962 | 133 | 215 |
Рост кода скромный (+2 740 строк, +36 тестов), потому что в этот раз нового кода было немного: большая часть работы это тесты, скрипты, документация и проверки. Скрипты и JavaScript в число строк не входят.
Шифрование диска в покое (S7)
Что мы планировали. Необязательное шифрование раздела с данными, по умолчанию выключенное, с измеренной ценой. Что мы построили (ADR-240):
- Слой под диском.
CryptDevоборачивает блочное устройство подDisk, поэтому раскладка, таблица размеров, SQLite, телеметрия и резервные копии работают с открытым текстом и ничего не знают о шифровании. Алгоритм AES-256-XTS (IEEE 1619), один сектор в 512 байт на единицу данных, а не блок в 4 KiB, как в плане: хранилище пишет секторами, а единица в 4 KiB превратила бы каждую запись в 512 байт в чтение-изменение-запись, которую сбой питания может разорвать пополам. - Суперблок остается открытым. Два слота суперблока несут флаг и проверку ключа (HMAC-SHA-256 от метки и UUID диска), но никогда не ключ. Все остальное в разделе зашифровано. Сектор, который на носителе целиком нулевой, читается как нули и не расшифровывается, поэтому discard, никогда не записанное пространство и рост диска работают как раньше.
- Конфигурация.
storage.encryption = "aes-256-xts"иstorage.disk_key(файл с 64 байтами ключа). Формат blob становится 4 только при включенном шифровании, поэтому все остальные образы по-прежнему запускаются на более старых ядрах. Более старый читатель отвергает неизвестный флаг в суперблоке. - Без аппаратного AES: отказ. Ядро отвергает
storage.encryptionна процессоре без инструкций AES (или когда[cpu] disableих отключает), вместо того чтобы молча работать на медленной программной версии. - Преобразование происходит вне сети. Ядро никогда не шифрует и не расшифровывает диск при запуске.
urmqtt-disk encrypt,decryptиrekeyделают это при выключенной VM: запись о преобразовании пишется первой и стирается последней (пока она есть, ядро отвергает диск), куски по 1 MiB сначала сохраняются в журнал на хосте, затем записывается преобразованная копия, а суперблок переключается последним. Проверено отключением питания после каждой отдельной записи для всех трех процедур, чисто и при полузаписанном состоянии, с отброшенным и сохраненным кэшем: всегда исправный диск со всеми данными после--resume.
Измерено. Пропускная способность XTS на одно ядро на Apple silicon (один MiB секторов в цикле): 3 109 MB/s шифрование и 3 103 MB/s расшифровка с аппаратным AES, против 252 и 258 MB/s в софте. Поэтому софт мы не разрешаем. db-bench в VM (aarch64, HVF, 5 000 вставок в 50 транзакциях, пять запусков каждого варианта, вперемешку):
| op/s, пять запусков | медиана | |
|---|---|---|
| шифрование выключено | 49 239, 47 401, 50 802, 53 417, 46 531 | 49 239 |
| шифрование включено | 57 790, 46 173, 45 939, 52 787, 49 632 | 49 632 |
Разница медиан +0,8 %, а разброс внутри одного варианта (46 000 до 58 000) больше этого, поэтому цена в этом тесте не измерима. Этого и следовало ожидать: db-bench за весь запуск пишет около 6 MB, это около 2 мс AES, а коммит ждет flush, а не шифр.
В Диагностическом входе теперь есть и AES-XTS рядом с AES-GCM (на HVF ядро измеряет около 800 MiB/s для XTS против около 560 MiB/s для AES-GCM), и он показывает, доступен ли аппаратный AES.

Чего XTS не дает. Нет аутентификации: измененный зашифрованный сектор расшифровывается в шум блоками по 16 байт (CRC и чистильщик это находят), а тот, кто может писать на том, может подменить или откатить целые секторы между двумя снимками. Он защищает украденный том или снимок, а не украденный ISO, потому что ключ находится в конфигурации образа. Остается видимым: GPT, суперблок, UUID и то, какие секторы вообще когда-либо записывались.
Что мы по дороге нашли в собственной работе. Пока писали главу о криптографии, мы обнаружили две вещи. Шаг умножения tweak в XTS имел ветвление, зависящее от бита, полученного из ключа; теперь оно без ветвлений. И утверждение «проверено по вектору 10 из IEEE 1619» покрывало только первый зашифрованный блок; теперь есть еще и наивная эталонная реализация (арифметика u128, блок за блоком), которая проверяет целые секторы в обе стороны на 200 случайных ключах и секторах, а документация говорит, что на самом деле проверено.
Загрузка, которая отвергает измененный образ (H8)
Требование из аудиторского списка было простым и до сих пор непроверенным: ни один тест не показывал, что измененное ядро не запускается. Закрепленный хеш в limine.conf существовал, но без доказательства.
- Тест, который меняет байт.
scripts/tamper.pyпереворачивает один бит в каждой копии ядра (или конфигурационного модуля) внутри собранного ISO или raw-образа, аSMOKE_ONLY=integrityпоказывает, что загрузчик отвергает: 24 проверки пройдены, на aarch64 UEFI, x86_64 UEFI и x86_64 BIOS, с контрольными образцами, которые запускаются. Сообщение об отказе читается с последовательного порта через переключатель только для тестов. На BIOS сообщение попадает только на экран, поэтому этот тест утверждает лишь, что машина не загружается. - Вписанная конфигурация загрузчика (ADR-250).
limine enroll-configработает наBOOTAA64.EFI,BOOTX64.EFIи на ступени BIOS, поэтому отвергается и измененныйlimine.conf(с добавленным пунктом меню или аргументом).urmqtt-config pack --isoвписывает заново. - Путь Secure Boot (ADR-251).
scripts/sign-loaders.sh(пробные ключи, подпись, проверка, хранилище переменных OVMF),mkiso.sh --sb-keyи группаsecurebootна x86_64: подписанный ISO запускается и воспроизводимо собирается, а неподписанный, подписанный неверным ключом и измененный после подписи получаютAccess Denied(10 проверок пройдено). Limine и под Secure Boot по-прежнему отвергает измененное ядро. - Правила установочного входа (ADR-253). План говорил об одном слове в командной строке,
install. Продакшен-ядро на самом деле принимает три (install,live,diag), а пишет толькоinstall. Тест (9 проверок) запускает это на трех машинах с пустым диском, хеш которого не должен измениться. - Секретность образа (ADR-252). Громкое предупреждение, когда продакшен-образ собирается вне CI, и сканирование образа, журналов сборки и последовательных журналов на байты ключей во многих кодировках (11 проверок).
- TPM, только ADR-254. Что значило бы запечатать ключ сервера в vTPM или TPM 2.0. Рекомендация: не строить для релиза, нацеленного на VPS. Мы не проверяли, какие провайдеры вообще предлагают vTPM.
Что это НЕ означает (записано как H8-W1 до H8-W6 в AUDIT.md): установленный диск не загружается под Secure Boot, потому что у его конфигурационного модуля нет хеша (ADR-217); подписанный ISO нельзя потом настроить (сначала настроить, потом подписать); у пути BIOS нет подписи; нет защиты от отката на более старую версию; на aarch64 прошивка здесь не обеспечивает Secure Boot, поэтому проверена только подпись загрузчика; проектного ключа нет, поэтому образы релиза не подписаны для Secure Boot; и ни одна физическая машина не пробовалась.
Аудиторский пакет (H10)
docs/AUDIT.md теперь пакет, которым внешний аудитор может пользоваться без вопросов: система на одной странице, база, которой мы доверяем (89 612 строк собственного Rust в образе, 67 подключенных чужих пакетов), входы и их парсеры, матрица защит с тестами (и защиты без тестов), слабые места со статусом, таблица каждого достижимого по сети цикла и аллокации с границей и тестом, инструкции по сборке и тестированию, предлагаемый объем внешнего аудита в десяти областях и red-team список с 31 атакой и результатами. В SECURITY.md есть таблица STRIDE по компонентам.
- Исправление первого текста. В тексте от 29 октября написано, что у каждого блока
unsafeесть комментарий// SAFETY:, который проверяет lint. Это было неверно: lint (deny(unsafe_code)) лишь ограничивает, где может находитьсяunsafe, а комментарии с H0 учитывал сгенерированный перечень, который уже тогда нашел около 30 мест без обоснования, а при аудите их было 108 (потому что тем временем появились новые виды мест, например атрибуты#[unsafe(no_mangle)]иlink_section). Поэтому именно перечень, а не lint, теперь держит их на нуле. - 889 мест
unsafe, у каждого письменное обоснование. 108, у которых его не было (в основном#[unsafe(no_mangle)]иlink_section, несколькоunsafe fn), получили комментарий (его написал AI-агент, только комментарии, а я просмотрел выборку); сгенерированный перечень в CI фиксирует каждое новое место без обоснования. Обоснование это утверждение, которое нужно проверять, а не доказательство, и это пункт 5 предлагаемого объема. - Пять открытых находок остаются открытыми, записанными: нет фаззинга парсеров PCI и virtio, нет теста, который пишет в таблицу страниц после блокировки, время выполнения P-256, X25519 и Ed25519 не измерено, нет фаззинга намеренно построенных файлов SQLite (ограничено H6), и 53 из 67 подключенных пакетов это исключения
cargo-vet, а не аудиты.
Ошибки, которые мы нашли сами
- Секреты TLS-трафика не стирались (H0-W2, открыто с первого дня аудита): таблица захваченных секретов была
Vec, которыйremoveиswap_removeсдвигали, оставляя копии. Теперь таблица по слотам: слот стирается на месте, когда его берут, отбрасывают или вытесняют, ничего не сдвигается. - Ветвление, зависящее от ключа, в XTS (выше).
log-slow-floodна x86_64 падал («log ring full»), в том числе на исходном коде v0.2.1. Причина не поломка, а размерность теста: сценарий гонял каждое ядро с интервалом 25 мс, а ADR-090 измерял его на 50 мс. На x86_64 COM1 это побайтовый перехватываемый порт-I/O, одному ядру, которое пишет в синки, нужно около 9 мс на строку, а три ядра на 25 мс требуют больше секунды работы в секунду, поэтому кольца заполняются (задуманное переполнение). Теперь тест на x86_64 идет с 50 мс (прошел, 30 из 30 при запуске с базовыми сценариями); на aarch64 остается 25 мс.- Слияние веток сломало старые шаблоны. Повторная запись конфигурации в H8 требовала завершающего маркера в
limine.conf, которого у шаблонов до H8 нет, поэтомуurmqtt-config pack --isoи страница сборщика отвергали каждый релиз до H8, включая v0.2.x, которые предлагает страница. Это поймали тесты сборщика в браузере (4 из 100 упали), а не собственный тест H8. Исправлено (шаблон без маркера настраивается как раньше) и добавлен тест, который падает с исходной ошибкой, если исправление убрать. - Наша документация неверно утверждала, что x86_64 проверен только в эмуляторе. X11 запустил продакшен-образ на настоящем Contabo KVM VPS (4 vCPU AMD EPYC) со взаимным TLS под нагрузкой. Исправлено везде: x86_64 запускался на настоящем KVM VPS на коммите H7 и в эмуляторе, но код, добавленный после этого (шифрование, целостность загрузки), на KVM не запускался и на физическом оборудовании не пробовался.
D1 и предложение «ваш сервис как unikernel»
Публичный демо-брокер построен, но не опубликован: конфигурация (режим live, ACL demo/{id}/#, обычный MQTT только для моста), скрипт, создающий опубликованный пробный CA и демо-сертификаты, мост WebSocket-в-TCP (только стандартная библиотека Python, ограничения соединений, скорости и размера, проверка источника), страница «Попробуйте urMQTT вживую» с клиентом MQTT на обычном JavaScript, runbook и модель угроз. Протестировано на loopback и в QEMU (18 сценариев, 26 тестов моста). Не протестировано: настоящий VPS, DNS, Caddy, образ контейнера. Нужен отдельный VPS и явное разрешение на публичное размещение.
Кроме того, на сайте опубликовано и предложение, Unikernel (15 языков, без цен): адаптация вашего сервиса, чтобы он работал как unikernel (оценка осуществимости, адаптация, пилот, поддержка), с urMQTT в качестве эталона. Текст прямо называет ограничения сегодняшней основы: IPv4, TCP только как сервер, KVM, Rust как естественный выбор, C только как изолированный воркер.
Релиз v0.3.0
Тег v0.3.0 поставлен 3 ноября на коммит f1a7fbc, и workflow релиза завершился успешно. Релиз помечен как пре-релиз и содержит development-ядро aarch64, ISO и образ, шаблонный ISO (ядра для aarch64 и x86_64 и пустую конфигурацию, которую urmqtt-config pack --iso и страница сборщика превращают в ваш образ), SBOM в CycloneDX, SHA256SUMS и его подпись minisign, а также аттестации происхождения SLSA. Проверено после публикации, в пустом каталоге: подпись SHA256SUMS действительна, все пять файлов совпадают с контрольными суммами, обе аттестации (ядра и ISO с предикатом CycloneDX) проходят, в SBOM 221 компонент, а шаблон настраивается (pack --iso заново вписывает шесть копий загрузчика). Ключ подписи по-прежнему пробный.
Порядок был важен, и его стоит записать: страница сборщика обновлена до тега (она знает формат 4 и все более старые шаблоны), и только потом зеркало на aldo.tech забрало релиз. Зеркало перечислило его подписанным и проверенным (ok, формат конфигурации 4, sha256 шаблона равен указанному в SHA256SUMS), а живая страница предлагает его как релиз по умолчанию и собирает ISO в браузере примерно за 20 секунд. В примечаниях к релизу сказано и то, что пишут редко: CI на GitHub не зеленый (см. ниже), поддержка только последней версии (v0.2.1 и старше прекращаются) и что аудит провел сам проект.
На сайте мы по дороге чуть не навредили себе сами: скрипт публикации сайта (publish.sh) делает rsync --delete, а зеркало релизов принадлежит другому пользователю, поэтому каждая публикация заканчивалась ошибкой 23 (и так сломала бы кнопку «Опубликовать» в админке блога). Исправлено исключением для /urmqtt/releases/. Похожая находка: deploy.sh с --delete удаляет страницу сборщика с генератора, поэтому ее нужно вернуть перед публикацией.
Что еще не доказано
- CI на GitHub красный. Не только сегодня: последний запуск полного CI перед тегом имел семь упавших задач, а запуски назад до 31 октября в основном красные. Падали: lint (предупреждения shellcheck в скриптах), TLS interop (все случаи wolfSSL), цепочка поставок (список исключений
vet-exemptions.mdне обновлен на Linux), fuzz smoke (в этой задаче не хватает development-сертификатов), санитайзеры (LeakSanitizer сообщает об утечке в тестах), тесты на Linux aarch64 (один тест инструмента падает с «Broken pipe») и одно доказательство Kani для колеса таймера (timer wheel) не проходит. Мы еще не разобрали, что из этого окружение, а что настоящий дефект; пока не сделаем этого, мы не утверждаем, что релиз прошел CI. - Ключ подписи пробный, а образы релиза development-сборки с пробными сертификатами (кроме шаблона).
- Переводы страниц и этого текста на остальные 13 языков написал AI, и носители языка их не проверяли.
- Шифрование диска не измерено на x86_64 (ни на KVM, ни на физическом оборудовании), и не измерены ни пропускная способность телеметрии, ни время запуска с шифрованием. На странице сборщика его нет (нужен диалог для ключа).
- Независимого аудита безопасности нет. Объем написан; аудитора у нас нет.
- Secure Boot частичный (выше), без физической машины.
- Bare metal, IOMMU, публичный демо-брокер: не сделано или не опубликовано.
Что дальше
- Разобрать и починить CI (сначала окружение, затем доказательство Kani и тест, который падает на aarch64 Linux), потому что релиз без зеленого CI мы не считаем завершенным.
- Решение по D1 (отдельный VPS, DNS, разрешение на публичное размещение).
- Фаззинг парсеров PCI и virtio и тест заблокированных таблиц страниц (H0-W9, H0-W10).
- Диалог для ключа на странице сборщика, чтобы шифрование стало доступно без командной строки.
- После релиза: bare metal (X12 до X16) и IOMMU, в порядке, зависящем от инвентаря оборудования тестового PC.
Код находится на github.com/zeljan-alduk/urMQTT; docs/PROGRESS.md по-прежнему единственный источник истины о том, что действительно работает, docs/PERFORMANCE.md о том, что измерено, а docs/AUDIT.md о том, что еще не доказано. Пре-альфа, как и раньше; сообщения об ошибках и о проблемах безопасности по инструкции в SECURITY.md приветствуются.
Комментарии
Правила комментирования