Как плагин обновлений Tauri ломает HTTPS на ALT Linux

от автора

В апреле я рассказывал здесь, зачем пишу ещё один настольный клиент для S3. В комментариях справедливо заметили, что технических деталей там не было. Исправляюсь, вот одна история изнутри. Клиент написан на Tauri 2 и Rust.

При проверке на российских ОС я наткнулся на странную ошибку. Приложение запускается, первое подключение к хранилищу по HTTPS проходит, а через несколько секунд все следующие запросы падают с ошибкой проверки сертификата. На Ubuntu, Debian и МСВСфере всё работает. Если отключить проверку обновлений, всё работает и на ALT.

Виноват оказался плагин tauri-plugin-updater, который меняет переменные окружения процесса. Разбираю подробно, потому что это касается любого приложения на Tauri 2, где есть этот плагин и TLS на rustls.

Как это выглядит

Система ALT Linux p11, пакет rpm, в приложении включена автоматическая проверка обновлений. Первое подключение к хранилищу по HTTPS успешно. После проверки обновлений новые TLS-соединения падают с invalid peer certificate: UnknownIssuer. Если перед запуском вручную задать SSL_CERT_FILE=/etc/pki/tls/certs/ca-bundle.crt, ошибки нет.

Где лежат сертификаты в разных дистрибутивах

Система

Файл с корневыми сертификатами

Каталог /etc/ssl/certs

Debian, Ubuntu, Astra

/etc/ssl/certs/ca-certificates.crt

есть, с отдельными сертификатами

МСВСфера 10.1 (RHEL 10)

/etc/pki/tls/certs/ca-bundle.crt

есть, с отдельными сертификатами

ALT Linux p11

/etc/pki/tls/certs/ca-bundle.crt

нет

Файла /etc/ssl/certs/ca-certificates.crt нет ни в МСВСфере, ни в ALT. Разница в том, что в МСВСфере сертификаты всё равно находятся через каталог, а в ALT нет ни файла, ни каталога.

Что делает плагин

Я смотрел tauri-plugin-updater версии 2.10.1, в основной ветке репозитория tauri-apps/plugins-workspace код тот же. В начале метода Updater::check() есть такой блок.

// Set SSL certs for linux if they aren't available.#[cfg(target_os = "linux")]{    if std::env::var_os("SSL_CERT_FILE").is_none() {        std::env::set_var("SSL_CERT_FILE", "/etc/ssl/certs/ca-certificates.crt");    }    if std::env::var_os("SSL_CERT_DIR").is_none() {        std::env::set_var("SSL_CERT_DIR", "/etc/ssl/certs");    }}

Задумка понятная, помочь HTTP-клиенту найти сертификаты. Только пути взяты из Debian, а переменные меняются для всего процесса и насовсем. Если приложение проверяет обновления при запуске, то после этого вызова любая библиотека, которая читает SSL_CERT_FILE и SSL_CERT_DIR, видит путь из Debian.

Почему ломается rustls

Системные корневые сертификаты для rustls обычно загружает крейт rustls-native-certs. С версии 0.8 функция load_native_certs() устроена так.

pub fn load_native_certs() -> CertificateResult {    let paths = CertPaths::from_env();    match (&paths.dirs, &paths.file) {        (v, _) if !v.is_empty() => paths.load(),        (_, Some(_)) => paths.load(),        _ => platform::load_native_certs(),    }}

Если задана хотя бы одна из переменных, сертификаты берутся только оттуда, а обычный поиск по известным путям через openssl-probe уже не выполняется. После вызова плагина на ALT SSL_CERT_FILE указывает на файл, которого нет, и SSL_CERT_DIR указывает на каталог, которого тоже нет. Хранилище корневых сертификатов получается пустым, и rustls отвергает любое TLS-соединение с ошибкой UnknownIssuer.

До проверки обновлений переменных нет, openssl-probe находит /etc/pki/tls/certs/ca-bundle.crt, и всё работает. Поэтому первое подключение и проходит.

В МСВСфере 10.1 файла тоже нет, но в каталоге /etc/ssl/certs лежат сертификаты с именами-хешами, и они загружаются. Там ошибка видна только как сообщение о неудачном чтении файла.

Есть и вторая проблема. std::env::set_var в многопоточной программе небезопасна, в редакции Rust 2024 её пометили как unsafe как раз потому, что одновременное чтение окружения из другого потока может привести к неопределённому поведению. А check() выполняется в асинхронной среде, где в это же время другие потоки открывают соединения.

Как воспроизвести

Понадобится Docker. Tauri для проверки не нужен, достаточно повторить то, что делает плагин.

fn main() {    let before = rustls_native_certs::load_native_certs();    println!("до: {} сертификатов", before.certs.len());    std::env::set_var("SSL_CERT_FILE", "/etc/ssl/certs/ca-certificates.crt");    std::env::set_var("SSL_CERT_DIR", "/etc/ssl/certs");    let after = rustls_native_certs::load_native_certs();    println!("после: {} сертификатов, ошибки: {:?}", after.certs.len(), after.errors);}
docker run --rm -it alt:p11 bashapt-get update && apt-get install -y ca-certificates

В базовом образе alt:p11 пакета ca-certificates нет, поэтому его нужно поставить перед запуском. Вот что получилось у меня.

Система

До

После

Debian 12

150 сертификатов

150, без ошибок

МСВСфера 10.1

150 сертификатов

150 и ошибка чтения ca-certificates.crt

ALT Linux p11

121 сертификат

0, файл и каталог не найдены

В ALT после двух вызовов set_var приложение остаётся без единого корневого сертификата.

Как исправить у себя

Плагин ставит пути, только если переменные ещё не заданы. Значит, достаточно задать их самому, раньше него и правильно. Найти правильные пути помогает тот же openssl-probe, которым rustls-native-certs пользуется по умолчанию.

[target.'cfg(target_os = "linux")'.dependencies]openssl-probe = "0.2"
#[cfg(target_os = "linux")]fn pin_system_ca_paths() {    let found = openssl_probe::probe();    if std::env::var_os("SSL_CERT_FILE").is_none() {        if let Some(file) = found.cert_file.as_ref() {            std::env::set_var("SSL_CERT_FILE", file);        }    }    if std::env::var_os("SSL_CERT_DIR").is_none() {        if let Some(dir) = found.cert_dir.first() {            std::env::set_var("SSL_CERT_DIR", dir);        }    }}#[cfg(not(target_os = "linux"))]fn pin_system_ca_paths() {}pub fn run() {    pin_system_ca_paths(); // первой строкой, пока других потоков ещё нет    tauri::Builder::default()        // ...}

Функцию важно вызвать первой строкой в run(). В этот момент асинхронная среда ещё не запущена, других потоков нет, и менять окружение безопасно. Переменные, которые пользователь задал сам, функция не трогает.

После этого на ALT p11 HTTPS работает и до, и после проверки обновлений. В МСВСфере пропадает ошибка чтения, а на Debian и Ubuntu ничего не меняется, openssl-probe находит там те же пути, что зашиты в плагин.

Как стоило бы исправить в самом плагине

Если плагину нужны пути к сертификатам для своего HTTP-клиента, их лучше передавать клиенту напрямую, а не через окружение процесса. А если без переменных никак, то искать реальные пути через openssl-probe вместо путей из Debian. Я подготовил задачу для репозитория tauri-apps/plugins-workspace и после публикации добавлю сюда ссылку.

Что я для себя вынес

Библиотека, которая меняет переменные окружения, меняет поведение всех остальных библиотек в процессе. Если видите set_var в зависимости, проверьте, кто ещё читает эти переменные.

Пути к сертификатам в Linux не стандартизованы. TLS стоит проверять хотя бы на трёх семействах, Debian, RHEL и ALT.

Если ошибка появляется не сразу, а после какой-то фоновой задачи, помогает отключать фоновые задачи по одной.

Проверял на ALT Linux p11, МСВСфере 10.1 и Debian 12 в контейнерах и в самом S3 TM Browser, где эту ошибку и нашёл. Если встречали похожее в других фреймворках, расскажите в комментариях.

ссылка на оригинал статьи https://habr.com/ru/articles/1084888/