Из серии заметок о bring‑up‑е собственной ОС на трёх платах: макетке STM32F4VE с модулем ESP32 на стоковой ESP‑AT, самом ESP32 и планшете Samsung Galaxy Tab S6 (Snapdragon 855). «Паразит в планшете» заканчивался выводом: радиопрошивка видит весь трафик канального уровня, ключи WPA уезжают в неё (подробнее — «Пароль у нас, ключ у модема»), поэтому конфиденциальность строится выше — на TLS. Эта заметка — о том, что у нас на этом уровне есть на самом деле, чего нет, и что понадобится, чтобы было.
1. Честное состояние в одном абзаце
На всех трёх платах HTTPS работает и шифрует канал. Ни на одной из них сертификат сервера не проверяется. Транспорт закрыт от пассивного наблюдателя и открыт для любого, кто может ответить на имя api.cursor.com или id.0xd.to раньше настоящего сервера — для точки доступа, провайдера, хотспота и, в первую очередь по близости, для прошивки радио. Это задокументировано в коде словами «trust the wire» и в модели угроз как единственный пункт, который превращает «модем видит трафик» (неизбежно) в «модем подменяет трафик и токены» (устранимо). Ниже — почему так вышло, что именно течёт через эти соединения и какой ценой это закрывается.
2. Что есть: два TLS на трёх платах
2.1 Свой стек: ESP32 и планшет
crates/tls — обёртка над embedded-tls (версия 0.19, TLS 1.3, один набор шифров Aes128GcmSha256). TlsStream берёт любой ByteStream ядра — TCP‑поток smoltcp, сокет модема — и отдаёт ByteStream обратно, так что HTTP‑клиент из crates/ai и всё написанное против трейта ходит по HTTPS без изменений. Буферы записей — 16 КиБ + 640 байт на приём и 4 КиБ на передачу — принадлежат вызывающему и живут ровно один обмен: модуль web либо выделяет их на каждый запрос, либо (на платах с кучей в пару сотен килобайт) резервирует раз при подключении, чтобы не искать 16‑килобайтный блок в раздробленной куче посреди запроса. Состояние рукопожатия сверх буферов — около 2,5 КиБ на x86‑64; тест в крейте следит, чтобы оно не разрослось за 8 КиБ.
Случайность для рукопожатия — устройство Entropy платформы. Где его нет, Rng предупреждает в лог и переходит на xorshift, засеянный временем с загрузки — «достаточно, чтобы поговорить с тестовым сервером, не достаточно, чтобы хранить секрет», как сказано в самом крейте. По таблицам устройств платформ: у ESP32 аппаратный RNG есть, у хоста и Android — /dev/urandom; у QEMU, F4VE и планшета entropy: None. Для F4VE это неважно (см. 2.2), для QEMU — ожидаемо. Для планшета, где tls и web уже включены в сборку, это означает, что сегодня рукопожатие там засевается uptime — отдельный пункт к исправлению, который стоит подтвердить живым прогоном.
Криптопровайдер — свой, минимальный: случайность из Rng, без верификатора, без клиентского сертификата. Библиотечный UnsecureProvider не взят потому, что тащит ECDSA‑подписчика для клиентской аутентификации — sec1 + der, около 22 КиБ кода Xtensa, которые ни один наш сервер не спрашивает. Это единственная цифра о стоимости криптографии, которую дают исходники; к ней вернёмся в §5.
Политика проверки — отсутствует. В embedded-tls верификатор — метод провайдера, по умолчанию отвечающий «не реализовано», и наш провайдер его не переопределяет: сертификат сервера принимается любой. Имя хоста при этом уходит в SNI — серверы за CDN без него не отвечают, — но SNI здесь лишь адрес, не проверка.
На схеме это выглядит так — фрагмент общей карты серии из «Чей это компьютер», теми же именами узлов. Важно, чего на ней нет: радио не видит открытого текста TLS — до ath10k доходят уже зашифрованные записи, и всё, что читает прошивка WLAN, это шифртекст плюс метаданные (SNI, DNS, ритм запросов). Открытый текст она получает только по пунктиру — там, где мы сами не проверяем, с кем говорим, и чем засеваем рукопожатие.
Две жирные рамки — два места, где конфиденциальность на планшете решается на самом деле: провайдер без верификатора и Rng без энтропии. Всё остальное на схеме — транспорт.
2.2 Чужой стек: F4VE за модемом
На F4VE у микроконтроллера криптографии нет вообще. Modem::connect_tls шлёт модулю три команды: AT+CIPSSLCCONF=0, AT+CIPSSLCSNI="host", AT+CIPSTART="SSL","host",443. Рукопожатие делает mbedTLS внутри ESP‑AT; MCU видит открытые байты по UART. Первая команда — явный выбор «не аутентифицировать сервер»: у модуля нет набора корневых сертификатов, и он принимает того, кто ответит на имя. Ошибки двух конфигурационных команд драйвер игнорирует — прошивка без них просто не умеет SSL, и это выяснится на подключении. Живой прогон (TASKS, строка 23): рукопожатие с api.cursor.com около 2 с, запрос и ответ 491/509 байт.
Здесь два наблюдателя открытого текста, а не один: сам модуль (его прошивка — закрытый код Espressif, как в §5.2 «Паразита») и провод UART, физически доступный на макетке. По конструкции второе не лечится, первое — ниже.
Та же схема для макетки — и она устроена наоборот: здесь TLS завершает модем, и открытый текст HTTP лежит не в нашей памяти, а на проводе между двумя чипами (обозначения — как на общей карте; узлы F4VE свои, планшетных здесь нет).
Стрелки от модуля к MCU на схеме нет, и это не упущение: у ESP‑AT нет ни DMA, ни общей памяти с STM32 — только байты по UART, которые драйвер разбирает как текст. Модуль видит и может подменить всё, что идёт через него, но в память микроконтроллера ему пути нет; на планшете, где радио — DMA‑мастер в нашей RAM, обе беды живут в одном чипе.
2.3 Третий стек: хост
Для полноты: на хосте и под Android тот же сервис Fetch реализован через ureq с rustls и верификатором платформы (под Android — набор корней Mozilla, потому что системный доступен только через JNI). Один сервис, на одной платформе проверяющий сервер и на трёх других — нет: правило «общая политика в одном месте» здесь нарушено фактом жизни, и это подсказывает, где её потом класть (§6).
3. Что течёт через эти соединения
Fetch — один сервис для всех потребителей, и это удобно перечислить, потому что список потребителей и есть список того, что подменяемо.
- API поставщиков ИИ (
crates/ai,tools::ai). Каждый запрос несётAuthorization: Bearer <ключ>из настроек устройства; ответ — текст, который устройство показывает как ответ модели. Подмена даёт и ключ, и содержимое разговора в обе стороны. - Аккаунт 0xD (
tools::account). Device flow OAuth 2.0 противid.0xd.to: код на экране, опрос/oauth2/token, затемuserinfo. Refresh token лежит в сторе подsys/account, access token — в RAM,AccountHandle::bearer()раздаёт его модулям. Тот, кто отвечает заid.0xd.to, получает refresh token при первом же обновлении — то есть учётную запись, а не сессию. net.fetch(tools::web). HTTP как возможность машины для агента и соседних узлов (esp32.net.fetch). Что через него пойдёт, решает вызывающий; сервис не отличает страницу от ключа.- Проба связности и captive portal (
tools::captive). Пробаconnectivitycheck.gstatic.com/generate_204идёт поhttp://и по смыслу не аутентифицируется: её задача — узнать, кто перехватывает. Но формы порталов, которые устройство затем заполняет, несут номера телефонов и одноразовые коды, и часть порталов живёт наhttps://.
Отдельно — чего нет: обновления образов (sys.update.*) и модули .oxm на планшете сегодня идут по агентскому линку через USB, не по сети. Модель угроз честно называет это защитой «пока не подключили», а не архитектурной: как только обновление пойдёт через Fetch без подписи образов, MITM станет исполнением кода при следующей загрузке.
4. Почему «нет часов» — это проблема, а не отговорка
Сертификат действителен в окне notBefore … notAfter. Проверка цепочки без знания текущего времени либо принимает истёкшие сертификаты (и тогда утёкший ключ прошлогоднего сертификата работает вечно), либо отвергает все. Верификатор embedded-tls при отсутствии часов подставляет время 0 и — по комментарию в его исходнике — проверка срока гарантированно проваливается. Так что «нет часов» означает не «проверка чуть слабее», а «проверка невозможна тем кодом, что есть».
Но у нас не совсем «нет часов». Ядро держит WallClock — смещение между uptime и Unix‑временем, — которое может быть не задано. Источники, с рангом: SNTP (ESP32 и планшет, по net.up и раз в сутки), RTC (на F4VE — на кристалле 32 768 Гц с батарейкой CR1220, на планшете — RTC PMIC плюс смещение в 128 байтах SDAM), время соседнего узла и time.set от пользователя. Точнее сказать так: часы бывают, но (а) их может не быть в момент первого запроса, и (б) единственный их сетевой источник — SNTP без аутентификации, по тому же каналу, которым распоряжается тот же наблюдатель.
Насколько (б) страшно — стоит взвесить, а не пугаться. Часы в проверке сертификата решают одно: истёк ли настоящий сертификат, подписанный доверенным корнем. Подделать подпись, сдвинув часы, нельзя. Атакующий, управляющий SNTP, расширяет окно для скомпрометированного и уже истёкшего ключа — это заметно меньшая дыра, чем «принимаем любой сертификат». Из этого следуют варианты.
5. Варианты и их цена
Закрепить корень (pinned root) и требовать часы. Один‑два DER‑сертификата удостоверяющих центров, подписавших наши конечные точки; проверка цепочки, имени и срока; без времени — отказ. Плюс: переживает плановые перевыпуски сертификатов серверов. Минус: первый запрос после холодной загрузки без RTC ждёт SNTP; «отказ без часов» надо явно решать (ждать, спросить соседний узел, взять RTC).
Якорь монотонного времени вместо часов. Устройство знает, что сейчас не раньше, чем: дата сборки образа, затем последнее успешно проверенное время (SNTP или заголовок Date из уже проверенного TLS‑ответа), сохранённое в сторе и никогда не идущее назад. С нижней границей L ≤ now окно проверяется наполовину честно: сертификат с notAfter < L точно истёк — отказ; notBefore > L — неизвестно, принимать или нет, но «ещё не начал действовать» редко бывает атакой. Это снимает зависимость от SNTP при загрузке и стоит одного ключа в сторе; строгость — ниже, чем у настоящих часов, и это надо записать в модель угроз, а не спрятать.
Закрепить лист или его ключ (SPKI‑pin). Часы не нужны вовсе: сравнивается хэш публичного ключа сервера. Годится для сервера, которым мы управляем (id.0xd.to) и чей ключ мы решаем не менять при перевыпуске. Не годится для api.cursor.com и API поставщиков — их ключи меняются без нас, и устройство «окирпичится» тихо.
TOFU — доверие при первом подключении. Первое соединение запоминает SPKI сервера в сторе, последующие требуют совпадения. Защищает от MITM после первого раза, не от первого. И зависит от стора: на ESP32 это раздел флеша, на F4VE — 60 КиБ NOR, а на планшете стор сегодня живёт в RAM (Block = NoBlock), так что там TOFU обнулялся бы каждой загрузкой.
Что из этого первым. Устройство разговаривает с маленьким фиксированным набором имён: id.0xd.to, конечная точка выбранного поставщика ИИ, проба связности. Для такого набора закреплённый корень (или два) — правильный первый шаг: он закрывает MITM для всего списка разом, не требует хранить состояние на устройстве и не ломается при плановом перевыпуске серверных сертификатов. Часы к нему — RTC там, где она есть, SNTP там, где нет, и монотонный якорь как страховка от «не проверить, потому что не знаем, какой год». SPKI‑pin — второй слой для собственного сервера, поверх корня, не вместо.
Цена, насколько её можно назвать. Исходники дают один ориентир: подписчик ECDSA с sec1 и der — около 22 КиБ Xtensa. Верификатор — разбор X.509 (webpki), проверка подписи по P‑256 (арифметика P‑256 уже в образе — её p256 использует и TLS, и SAE в crates/wpa) и один‑два корня по ~1–1,5 КиБ DER — величина того же порядка, но измерения нет, и любое число до сборки будет догадкой. Куча: цепочка приходит внутри записи, которую и так вмещает 16‑килобайтный буфер; верификатору embedded-tls нужна ещё копия листа (параметр CERT_SIZE) — единицы килобайт на время рукопожатия. Для ESP32 с кучей ~220 КиБ, плоским уровнем ~137 и пиками 170–200 это тесно, но возможно. Для F4VE вопрос не стоит: TLS там считает модуль, а флеш‑бюджет образа меряется сотнями байт (remote 507–508 КиБ против слота 496 КиБ).
Две детали библиотеки, которые придётся учесть — обе по исходнику embedded-tls 0.19, и обе стоит перепроверить на версии в сборке:
- Штатный
CertVerifierпроверяет только первый сертификат цепочки напротив якоря доверия; промежуточные не передаются (в коде —TODO: Support intermediates). Публичные центры подписывают листы промежуточными сертификатами, так что «закрепить ISRG Root X1» этим верификатором не сработает: пришлось бы закреплять промежуточный, который меняется чаще, — либо писать собственныйTlsVerifierвcrates/tls, отдающийwebpkiостаток цепочки как intermediates. Второе правильнее и не выглядит большим. - Набор алгоритмов подписи в сборке без
allocуembedded-tls— только ECDSA P‑256/P‑384 и Ed25519; RSA требует его фичиallocи крейтаrsa. Какие цепочки уapi.cursor.comи поставщиков ИИ сегодня — из репозитория не узнать; это надо снять с живых серверов до выбора корней.
6. Где это живёт
Ответ подсказывает §2.3: политика проверки — в crates/tls, одна для всех потребителей Fetch, потому что TlsStream — единственное место, через которое HTTPS уходит с платы своим стеком. Провайдер уже там; верификатор — его метод. Платформа, как и с Entropy, лишь даёт: якоря доверия (статический DER в образе), часы (kernel.unix_ns(), который уже Option) и, для монотонного якоря, ключ в сторе. Модуль web пробрасывает это в TlsStream::open так же, как сейчас пробрасывает Rng. Хост и Android с ureq остаются на верификаторе платформы — там он честнее любого нашего.
Для F4VE политика тоже одна, но исполняется в модуле: ESP‑AT умеет режимы CIPSSLCCONF с проверкой сервера по сертификату, загруженному в раздел модуля, — это утверждение о прошивке Espressif, не о нашем коде, и его надо проверить на той версии ESP‑AT, что стоит на макетке, прежде чем на него опираться. Драйвер esp-at тогда меняет одну команду и перестаёт глотать её ошибку.
7. Обратно к паразиту
Что меняет проверка для радиопрошивки, у которой ключи трафика и весь эфир. Без неё она — MITM: отвечает на id.0xd.to, забирает refresh token, подменяет ответ модели, а при сетевых обновлениях — код. Ей для этого не нужен ни DMA, ни один из каналов, которые запирают stage‑1 SMMU, тень rmtfs и scratch tqftp. Иначе говоря, вся работа по изоляции ниже теряет смысл на уровне приложения: зачем ломать забор, если дверь не заперта.
С проверкой она — наблюдатель метаданных: имена (SNI в TLS 1.3 без ECH идёт открыто), адреса, время и объёмы запросов. Это немало — по ритму запросов к API ИИ видно, когда и сколько человек разговаривает, — но это другая категория угрозы, и с ней приоритеты из «Паразита» сходятся: IOMMU закрывает пивот в хост, TLS с проверкой — содержимое, тень вместо диска — персистентность. Пока стоит CIPSSLCCONF=0 и провайдер без верификатора, из трёх закрыто два, и третье — самое дешёвое.
Источники: crates/tls/src/lib.rs (Buffers, Rng, Provider, тест handshake_state_fits_a_board), crates/tls/Cargo.toml; crates/esp-at/src/{modem,lib}.rs (connect_tls, open); crates/tools/src/{web,account,ai,captive}.rs, crates/ai/src/{http,dialect}.rs; crates/hal/src/entropy.rs; platforms/*/src/platform.rs (таблицы устройств), platforms/hosted/src/fetch.rs, platforms/stm32f4ve/src/rtc.rs, platforms/sdm855/src/persist.rs; crates/kernel/src/{time,boot}.rs (WallClock, unix_ns); docs/wifi-threat-model.md §6.2, §6.4; ARCHITECTURE.md (net, esp-at, web/Fetch, Account, Time); TASKS.md строки 23, 37, 48, 55, 56; исходник embedded-tls 0.19 (src/webpki.rs, src/config.rs, Cargo.toml) из локального реестра cargo.
Умный дом под ключ на собственной ОС: 0xd-os и 0xd-uiМы строим умные дома под ключ на компонентах собственной разработки. В основе — ОС 0xd-os, которая одинаково работает на флагманском планшете и в устройстве с 512 КБ памяти.
Зачем агенту безопасности нужна песочницаАгент, который «просто запускает инструменты» на разборе, — второй атакующий, пока у каждого вызова нет гранта. AGI Core изолирует инструменты; Forge держит список скиллов в UIDE.