Как плагин обновлений 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, ошибки нет.
Где лежат сертификаты в разных дистрибутивах
Система | Файл с корневыми сертификатами | Каталог |
|---|---|---|
Debian, Ubuntu, Astra |
| есть, с отдельными сертификатами |
МСВСфера 10.1 (RHEL 10) |
| есть, с отдельными сертификатами |
ALT Linux p11 |
| нет |
Файла /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 bash
apt-get update && apt-get install -y ca-certificates
В базовом образе alt:p11 пакета ca-certificates нет, поэтому его нужно поставить перед запуском. Вот что получилось у меня.
Система | До | После |
|---|---|---|
Debian 12 | 150 сертификатов | 150, без ошибок |
МСВСфера 10.1 | 150 сертификатов | 150 и ошибка чтения |
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, где эту ошибку и нашёл. Если встречали похожее в других фреймворках, расскажите в комментариях.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.