- PVSM.RU - https://www.pvsm.ru -
Некоторые банки и государственные сервисы используют TLS-цепочки, которые заканчиваются корнем Russian Trusted Root CA. Яндекс Браузер и Атом умеют работать с такими цепочками. Chrome для Android показывает NET::ERR_CERT_AUTHORITY_INVALID, потому что этого корня нет в хранилище Chromium.
Я пользуюсь Chrome и не хотел заводить второй браузер для нескольких сайтов. Поэтому добавил корень в Chromium, ограничил его зонами .ru и .рф и собрал отдельный APK. Проект получил название Ruthenium.

Сайт передаёт браузеру свой сертификат и промежуточные сертификаты. Браузер проверяет имя сайта, сроки и подписи, а затем строит цепочку до доверенного корня.
Russian Trusted Root CA в хранилище Chromium не входит. Поэтому Chrome не может завершить проверку цепочки и отклоняет соединение.
Можно использовать браузер, в котором уже есть поддержка этой цепочки, но я не готов привыкать к новому дизайну
Другой вариант — скачать корневой сертификат с сайта Минцифры и добавить его в пользовательское хранилище Android, но я не понимаю как Android отреагирует на российский сертификат в корне системы
Мне был нужен привычный интерфейс Chrome, но без изменения пользовательского хранилища всей системы. Я взял стабильную Android-версию Chromium и добавил Russian Trusted Root CA только в хранилище браузера.
Собственного TLS-верификатора в Ruthenium нет. Chromium использует штатную проверку цепочки, но получает ещё один корень с ограничениями для .ru и .xn--p1ai. Проверка имени, срока действия, подписей и остальных свойств сертификатов не меняется.
Патч применяется к chrome/browser/net/profile_network_context_service.cc. Сертификат преобразуется из PEM в DER и встраивается в Android-сборку как массив байтов. Ниже показана вся C+±логика, которую добавляет патчер. Опущены только байты массива DER, полученные из сертификата механически.
// BEGIN Russian Trusted Root CA (Android-only, DNS-constrained)
#if BUILDFLAG(IS_ANDROID)
// Source: https://gu-st.ru/content/lending/russian_trusted_root_ca_pem.crt
// DER SHA-256: d26d2d0231b7c39f92cc738512ba54103519e4405d68b5bd703e9788ca8ecf31
constexpr uint8_t kRussianTrustedRootCaDer[] = {
0x30, 0x82, /* ... полный DER сертификата ... */
};
#endif // BUILDFLAG(IS_ANDROID)
// END Russian Trusted Root CA (Android-only, DNS-constrained)
Chromium получает не корень доверия без ограничений, а CertWithConstraints с двумя разрешёнными DNS-суффиксами:
// BEGIN Russian Trusted Root CA scoped trust
#if BUILDFLAG(IS_ANDROID)
auto russian_trusted_root =
cert_verifier::mojom::CertWithConstraints::New();
russian_trusted_root->certificate = std::vector<uint8_t>(
kRussianTrustedRootCaDer,
kRussianTrustedRootCaDer + sizeof(kRussianTrustedRootCaDer));
// A leading dot permits subdomains only. That covers registrable names below
// the TLD while excluding unrelated DNS namespaces.
russian_trusted_root->permitted_dns_names = {".ru", ".xn--p1ai"};
additional_certificates->trust_anchors_with_additional_constraints.push_back(
std::move(russian_trusted_root));
#endif // BUILDFLAG(IS_ANDROID)
// END Russian Trusted Root CA scoped trust
Здесь .xn--p1ai — ASCII-представление .рф, а начальная точка разрешает имена ниже TLD, например bank.ru, но не саму строку ru и не имя bank.ru.example.com.
CertWithConstraints проверяет все DNS-имена из SAN (Subject Alternative Name), а не только hostname открываемой страницы. Если сертификат содержит bank.ru и example.com, цепочка через добавленный корень будет отвергнута даже при открытии bank.ru: одно из заявленных имён находится вне разрешённых зон.
У ограничения есть пробел: permitted_dns_names не применяется к SAN типа iPAddress. Если под этим корнем выпустить сертификат непосредственно на IP-адрес, текущий патч его область не сузит. Поэтому ограничение относится именно к DNS-именам, а не ко всем возможным идентификаторам сертификата.
Перед применением патча PEM строго декодируется, а SHA-256 полученного DER сверяется с lock-файлом. Задача сборки отклоняет неожиданную смену сертификата:
der = decode_pem(pem)
actual_sha256 = hashlib.sha256(der).hexdigest()
if actual_sha256 != EXPECTED_DER_SHA256:
raise ValueError(
f"unexpected certificate SHA-256: {actual_sha256}"
)
Полная вставка вместе с DER-генератором лежит в scripts/patch_chromium.py [1], а ожидаемый отпечаток и оба официальных URL — в certificates/ministry-ca-lock.json [2].
Корень остаётся внутри Ruthenium. Системное хранилище Android не меняется, другие приложения его не получают, браузер не содержит дополнительного кода, кроме Chromium.
На текущей ревизии патчер меняет ровно четыре C+±файла:
chrome/browser/net/profile_network_context_service.cc — добавляет ограниченный доверенный корень;
components/signin/internal/identity_manager/primary_account_manager.cc — по умолчанию запрещает вход в аккаунт на уровне браузера;
chrome/browser/ui/omnibox/omnibox_edit_model.cc — не вызывает недоступный без XR метод и возвращает пустой gfx::Image;
chrome/browser/ui/webui/cr_components/searchbox/searchbox_handler.cc — заменяет ещё два недоступных без XR вызова обычной иконкой поиска.
Сам патч сертификата невелик. Больше времени заняла сборка Chromium 151 для Android с is_official_build=true: без нескольких дополнительных изменений чистый checkout не собирался в APK.
is_official_build=true включает ThinLTO и другие оптимизации релизной сборки, а также ожидает внешние PGO-профили Chromium и V8. На 32-битном ARM после отключения Chrome PGO сборка пытается использовать отдельный профиль Android AFDO. Эти файлы не входят в закреплённые входы релиза, поэтому Ruthenium отключает оба внешних источника профилей:
is_debug = false
is_component_build = false
is_official_build = true
chrome_pgo_phase = 0
clang_use_default_sample_profile = false
ThinLTO при этом остаётся включённым; отключаются только оптимизации, результат которых зависел бы от незафиксированных внешних файлов.
Функции VR и AR в Ruthenium отключены:
enable_vr = false
enable_arcore = false
enable_openxr = false
enable_cardboard = false
В Chromium 151 есть несогласованность условий компиляции. Методы AutocompleteMatch::GetVectorIcon() и OmniboxAction::GetVectorIcon() объявлены на Android только при ENABLE_VR, но несколько мест вызова компилируются и без VR. Можно было вернуть enable_vr=true, но XR-код браузеру не нужен.
В chrome/browser/ui/omnibox/omnibox_edit_model.cc вызов закрыт тем же условием, что и объявление метода. На Android без VR функция возвращает пустое изображение. Это первое из трёх C+±изменений для сборки без XR:
#if !BUILDFLAG(IS_ANDROID) || BUILDFLAG(ENABLE_VR)
bool is_starred_match = IsStarredMatch(match);
const auto& vector_icon_type = match.GetVectorIcon(is_starred_match, turl);
return controller_->client()->GetSizedIcon(vector_icon_type,
vector_icon_color);
#else
// Chromium 151 compiles this desktop-oriented implementation into Android
// even though GetVectorIcon() is unavailable when XR is disabled.
return gfx::Image();
#endif
Ещё два вызова находятся в chrome/browser/ui/webui/cr_components/searchbox/searchbox_handler.cc. Для них резервный вариант — обычная иконка поиска:
#if !BUILDFLAG(IS_ANDROID) || BUILDFLAG(ENABLE_VR)
const bool is_bookmarked =
bookmark_model->IsBookmarked(match.destination_url);
// For starter pack suggestions, use template url to generate proper vector
// icon.
const TemplateURL* associated_keyword_turl =
match.associated_keyword.empty()
? nullptr
: turl_service->GetTemplateURLForKeyword(match.associated_keyword);
mojom_match->icon_path = AutocompleteIconToResourceName(
match.GetVectorIcon(is_bookmarked, associated_keyword_turl));
#else
mojom_match->icon_path = kSearchIconResourceName;
#endif
И тот же guard нужен для иконок действий:
if (action->GetIconImage().IsEmpty()) {
#if !BUILDFLAG(IS_ANDROID) || BUILDFLAG(ENABLE_VR)
icon_path = AutocompleteIconToResourceName(action->GetVectorIcon());
#else
icon_path = kSearchIconResourceName;
#endif
} else {
icon_path = webui::GetBitmapDataUrl(action->GetIconImage().AsBitmap());
}
Ruthenium устанавливается рядом с Chrome под application ID app.ruthenium.browser и хранит свои данные отдельно. У приложения собственное название, иконка и страница About.
Вход в аккаунт на уровне браузера по умолчанию запрещён, а кнопка профиля (identity disc) скрыта. Сборка не настраивает Google Sync и интеграцию Google Chrome с системными аккаунтами Android. На авторизацию в аккаунте Google на сайтах это не влияет.
На C+±стороне это единственная продуктовая замена: значение prefs::kSigninAllowed по умолчанию меняется в primary_account_manager.cc:
-registry->RegisterBooleanPref(prefs::kSigninAllowed, true);
+registry->RegisterBooleanPref(prefs::kSigninAllowed, false);
Java-патч читает тот же Pref.SIGNIN_ALLOWED и скрывает кнопку профиля. Название, иконки и About-экран меняются в ресурсах Android и Java, а не в C++.
Встроенного автообновления пока нет. Новые APK нужно устанавливать вручную.
В репозитории [3] указана ревизия Chromium и опубликованы патч, ресурсы приложения, параметры GN, сертификат с закреплённым SHA-256, конфигурация CI и тесты. Перед сборкой CI загружает указанную ревизию Chromium, получает сертификат с gu-st.ru и сверяет его с закреплённым отпечатком.
Входы сборки связаны с релизом через идентификатор релиза (release identity). В расчёт входят версия и ревизия Chromium, application ID, сертификат подписи, закреплённый корень Минцифры и полное содержимое build/args.gn и scripts/patch_chromium.py. Тег имеет вид android-<версия Chromium>-ca-<CA digest>-<release digest>: CA digest — первые 12 символов SHA-256 DER сертификата, а release digest — первые 12 символов SHA-256 всех перечисленных входов. Ротация корня меняет имя и создаёт отдельный релиз, не затрагивая релиз с уже опубликованными APK. Тег указывает на опубликованный снимок исходников; задача публикации сверяет его SHA-256 с маркером в коммите ветки main.
Рядом с каждым APK публикуются .sha256, результаты aapt2 dump badging и apksigner verify, список ABI, build-info и JSON с данными о происхождении сборки. Лицензия Chromium и публичный сертификат подписи общие для релиза. До публикации CI проверяет ZIP, ABI, application ID, label, подпись и метаданные. После публикации он скачивает затронутые файлы через новое соединение Tor и сравнивает размер и SHA-256. Эти проверки обнаруживают подмену файла, но не заменяют независимую воспроизводимую сборку.
Большинству современных телефонов подходит вариант arm64-v8a.
Откройте GitHub Releases [4] и скачайте последний APK для arm64-v8a.
Разрешите установку приложений из выбранного источника в настройках Android.
Откройте APK и установите Ruthenium.
Ruthenium устанавливается рядом с Chrome. Его можно назначить браузером по умолчанию или открывать в нём только нужные ссылки.
Для старых ARM-устройств публикуется armeabi-v7a, для Android на Intel — x86_64. Узнать архитектуру устройства можно через ADB:
adb shell getprop ro.product.cpu.abi
Рядом с APK лежит файл .sha256. В командах имя выбирается по шаблону, поэтому в каталоге должен находиться ровно один APK нужной архитектуры.
В Linux:
set -- Ruthenium-*-arm64-v8a.apk.sha256
if [ "$#" -ne 1 ] || [ ! -f "$1" ]; then
echo "Ожидался один checksum-файл" >&2
exit 1
fi
sha256sum -c "$1"
В macOS:
set -- Ruthenium-*-arm64-v8a.apk.sha256
if [ "$#" -ne 1 ] || [ ! -f "$1" ]; then
echo "Ожидался один checksum-файл" >&2
exit 1
fi
shasum -a 256 -c "$1"
В Windows нужно посчитать хеш и сравнить его со значением в .sha256:
$apk = @(Get-ChildItem .Ruthenium-*-arm64-v8a.apk)
if ($apk.Count -ne 1) { throw "Ожидался один APK" }
Get-FileHash $apk.FullName -Algorithm SHA256
Установить или обновить приложение через ADB можно командой:
set -- Ruthenium-*-arm64-v8a.apk
if [ "$#" -ne 1 ] || [ ! -f "$1" ]; then
echo "Ожидался один APK" >&2
exit 1
fi
adb install -r "$1"
Мне был нужен Chromium, который открывает сайты с сертификатом Минцифры без установки сертификата в Root CA и Ruthenium решает эту задачу, не создавая дополнительных проблем
Браузер можно использовать как основной. На сайтах .ru и .рф он работает с расширенным RU Trust CA набором сертификатов. На остальных сайтах поведение не распространяется — Ruthenium ведёт себя как обычный Chromium.
Ruthenium не связан с Google, командой Chromium или Минцифры. Лицензии Chromium и сторонних компонентов сохранены. Сообщения об ошибках принимаются в issues репозитория.
Автор: aabaskin
Источник [5]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/chromium/456591
Ссылки в тексте:
[1] scripts/patch_chromium.py: https://github.com/rutheniumteam/ruthenium-android/blob/main/scripts/patch_chromium.py
[2] certificates/ministry-ca-lock.json: https://github.com/rutheniumteam/ruthenium-android/blob/main/certificates/ministry-ca-lock.json
[3] репозитории: https://github.com/rutheniumteam/ruthenium-android
[4] GitHub Releases: https://github.com/rutheniumteam/ruthenium-android/releases
[5] Источник: https://habr.com/ru/articles/1070548/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1070548
Нажмите здесь для печати.