Введение
Аналогично подписи плагинов (статья 188 из цикла про систему плагинов), сами установочные пакеты — особенно если они скачиваются по сети при автообновлении — должны быть защищены от подмены злоумышленником. В статье разберём, как подписывать как сами офлайн-установщики, так и компоненты онлайн-репозитория для автообновления, чтобы пользователь и maintenance tool могли быть уверены в подлинности и целостности скачиваемого содержимого.
Концепция
Для офлайн-установщиков на Windows и macOS используются нативные платформенные механизмы подписи кода (Authenticode на Windows через signtool, codesign на macOS) — это не специфичная для QtIFW технология, а общий механизм подписи любого исполняемого файла на этих платформах, который операционная система проверяет автоматически при запуске и предупреждает пользователя, если подпись отсутствует или недействительна. Для компонентов онлайн-репозитория QtIFW поддерживает дополнительный уровень — подпись содержимого через сертификаты, проверяемые непосредственно maintenance tool перед установкой скачанного обновления, независимо от платформенной подписи самого установщика.
Пример кода: Подпись и верификация установочных пакетов
# Подпись установщика для Windows через Authenticode (signtool из Windows SDK)
signtool sign /f company_certificate.pfx /p "${CERT_PASSWORD}" \
/tr http://timestamp.digicert.com /td sha256 /fd sha256 \
MyAppInstaller.exe
# Подпись установщика для macOS через codesign
codesign --force --deep --sign "Developer ID Application: ООО МойКомпани" \
--options runtime \
MyAppInstaller.app
# Дополнительная проверка нотаризации Apple (требуется для macOS Gatekeeper)
xcrun notarytool submit MyAppInstaller.app.zip \
--apple-id "developer@mycompany.com" \
--team-id "TEAMID123" \
--wait
#!/usr/bin/env bash
# build_and_sign.sh — автоматизация подписи как часть пайплайна релиза
set -e
echo "Сборка установщика..."
binarycreator --offline-only -c config/config.xml -p packages MyAppInstaller.exe
echo "Подпись установщика..."
signtool sign /f "$SIGNING_CERT_PATH" /p "$SIGNING_CERT_PASSWORD" \
/tr http://timestamp.digicert.com /td sha256 /fd sha256 \
MyAppInstaller.exe
echo "Проверка подписи перед публикацией..."
signtool verify /pa MyAppInstaller.exe
if [ $? -ne 0 ]; then
echo "ОШИБКА: подпись установщика не прошла верификацию" >&2
exit 1
fi
echo "Установщик подписан и проверен успешно"
// Проверка платформенной подписи установщика на стороне приложения,
// перед запуском скачанного обновления (для дополнительного слоя защиты)
#ifdef Q_OS_WIN
#include <wintrust.h>
#include <softpub.h>
bool verifyAuthenticodeSignature(const QString &filePath)
{
WINTRUST_FILE_INFO fileInfo = {};
fileInfo.cbStruct = sizeof(WINTRUST_FILE_INFO);
const std::wstring widePath = filePath.toStdWString();
fileInfo.pcwszFilePath = widePath.c_str();
WINTRUST_DATA winTrustData = {};
winTrustData.cbStruct = sizeof(WINTRUST_DATA);
winTrustData.dwUIChoice = WTD_UI_NONE;
winTrustData.fdwRevocationChecks = WTD_REVOKE_WHOLECHAIN;
winTrustData.dwUnionChoice = WTD_CHOICE_FILE;
winTrustData.pFile = &fileInfo;
GUID policyGuid = WINTRUST_ACTION_GENERIC_VERIFY_V2;
const LONG result = WinVerifyTrust(nullptr, &policyGuid, &winTrustData);
return result == ERROR_SUCCESS;
}
#endif
Пояснения к коду
build_and_sign.sh объединяет сборку и подпись в один автоматизированный шаг релизного пайплайна — критичный момент здесь — явная проверка результата signtool verify после подписи, прежде чем публиковать установщик пользователям; без этой верификации возможна ситуация, когда подпись формально была применена, но по какой-то причине (истёкший сертификат, проблема с временной меткой) оказалась некорректной, и без проверки это осталось бы незамеченным до момента, когда пользователи начнут сообщать о предупреждениях ОС при запуске установщика.
verifyAuthenticodeSignature() показывает, как само приложение может программно проверить подпись скачанного файла обновления через нативный Windows API WinVerifyTrust — это даёт дополнительный, явный уровень защиты сверх стандартного поведения ОС (которое обычно проверяет подпись автоматически при попытке запустить исполняемый файл, но не предоставляет лёгкого способа для приложения самостоятельно проверить файл программно перед тем, как инициировать его запуск).
Подводные камни
- Истечение срока действия сертификата подписи кода, использованного для подписи уже выпущенных в прошлом версий установщика — большинство современных платформ (благодаря применению временной метки через
/trвsigntool, как показано в примере) продолжают считать подпись валидной даже после истечения срока сертификата, при условии что подпись была применена, когда сертификат был действителен — но при подписи новых релизов нужен действующий, не просроченный сертификат, и истечение срока без своевременного продления блокирует весь процесс подписи новых релизов. - Хранение пароля от сертификата (
CERT_PASSWORDв примере) непосредственно в открытом виде в скрипте сборки или переменных окружения CI без должной защиты — для CI-систем стоит использовать встроенные механизмы защищённого хранения секретов (secrets manager), а не передавать пароль сертификата как обычную переменную окружения, видимую в логах сборки при недостаточно тщательной настройке маскирования вывода. - Подпись только итогового установщика без подписи компонентов внутри онлайн-репозитория обновлений оставляет процесс автообновления потенциально менее защищённым, чем первоначальная установка — если злоумышленник способен скомпрометировать содержимое онлайн-репозитория (а не сам установщик, который пользователь скачал и подпись которого ОС проверила один раз при первом запуске), последующие автообновления через maintenance tool могут быть менее защищены без отдельного механизма проверки целостности именно содержимого репозитория обновлений.
- Различие требований к подписи и нотаризации между платформами, особенно строгие требования Apple для macOS (обязательная нотаризация через Apple, без которой Gatekeeper будет блокировать запуск приложения для большинства пользователей по умолчанию) — процесс подписи для разных целевых платформ существенно отличается по сложности и требует отдельного внимания и интеграции в пайплайн релиза для каждой платформы отдельно, а не единого универсального решения.