Безопасность автообновления: почему одного HTTPS недостаточно
· Обновлено: · Го Комура · Разработка под Windows, Безопасность, updater, Автообновление, Подпись, MSIX, ClickOnce
История изменений (1 обновлений, последнее 30 Aug 2026)
Журнал изменений этой статьи. Там, где версия до правки была заархивирована, она остаётся доступной для чтения по постоянной ссылке с DOI.
- Русский текст переписан как полноценный технический перевод, а не калька с японского. Утверждения статьи не менялись.
- Первая публикация
Цитирование статьи(DOI (зарегистрированный архив): 10.5281/zenodo.21619824)
Приведённые ниже DOI относятся к ранее зарегистрированным архивным версиям, которые могут отличаться от текущего текста. Для ссылки на текущий текст используйте URL этой страницы.
Го Комура (2026). Безопасность автообновления: почему одного HTTPS недостаточно. KomuraSoft LLC. https://comcomponent.com/ru/blog/2026/04/09/000-comcomponent-autoupdate-security/
- DOI (зарегистрированный архив)
- 10.5281/zenodo.21619824
- DOI (последняя зарегистрированная версия)
- 10.5281/zenodo.21619825
Оглавление
- Сначала вывод
- Почему автообновление — зона высокого риска
- Антипаттерны
- Лучшие практики
- Минимальная безопасная схема
- Как к этому подходить в Windows-проектах
- Минимальный чек-лист
- Итог
- Источники
На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 27, с доказательствами и степенью уверенности) и определения основных понятий собраны на странице сведений карты знаний (на японском). Данные: JSON-LD / Turtle
1. Сначала вывод
Сразу зафиксируем, к чему обычно приходят на практике.
- Если требования позволяют, сначала опираться на готовую инфраструктуру обновлений: MSIX App Installer, ClickOnce и т. п.
- Если нужен свой updater, первым делом ставить не UI, а проверку подписи и восстановление после сбоя
- Сведения об обновлении вроде
latest.jsonсчитать не неподписанным конфигом, а подписанными metadata - TLS нужен, но его недостаточно
- Решение обновлять строить не на «сервер так сказал», а на «клиент проверил и счёл корректным»
- Ключи подписи для разработки и продакшена разделять и защищать HSM или сервисом подписи
- При сбое обновления — fail-closed, а не fail-open
- Updater без защиты от rollback безопаснее считать заранее уязвимым к откату на дырявую версию
- Пока проверку подписи ещё нельзя внедрить, ручная раздача подписанных installer безопаснее автообновления
Сначала зафиксируем термины. Дальше они используются в этих значениях.
| Термин | Значение |
|---|---|
| fail-closed | При аномалии система падает в сторону «остановиться». Не прошла проверка подписи — обновление не продолжается |
| fail-open | При аномалии система падает в сторону «идти дальше». Показали предупреждение и всё равно обновляют |
| staging | Новую версию сначала раскладывают в отдельное место и переключаются только после проверки |
| kill switch | Механизм, которым с сервера можно сразу остановить уже идущую доставку обновления |
| trust anchor | Точка, которой клиент доверяет с самого начала: корневой открытый ключ или зафиксированная цепочка сертификатов |
Короче: суть автообновления не в том, «как скачивать», а в том, «чему доверять, где проверять и как возвращаться, когда сломалось».
2. Почему автообновление — зона высокого риска
Обычная функция живёт внутри приложения. Updater же сразу держит три вещи:
- Ходит наружу за файлами
- Доверяет этим файлам
- Подменяет уже стоящие исполняемые файлы
То есть путь к выполнению произвольного кода изначально встроен в продукт.
Типичное заблуждение здесь — «раз HTTPS, значит безопасно». TLS, конечно, нужен. Но он защищает в основном канал и подлинность конечной точки. Если скомпрометирован сам сервер обновлений, на легитимный CDN положили неверный артефакт или подменили неподписанный manifest — одного TLS мало.
Даже если смотреть только на угрозы, которые систематизирует TUF (The Update Framework — спецификация модели доверия для обновления ПО), у систем обновления их уже немало.
- Подсунуть произвольное вредоносное ПО
- Rollback — откатить на старую уязвимую версию
- Freeze — не показывать новую версию
- Mix-and-match — смешать несогласованные metadata и артефакты
То есть автообновление — это не «передача файлов», а «раздача доверия». Безопасно оно начинает работать только после того, как эту сторону спроектировали.
2.1 Таблица угроз и мер
Угрозы и меры в статье разнесены по главам. Сначала одна сводная таблица соответствий. Для скелета дизайна её достаточно.
| Угроза | Что происходит | Закрывает ли один TLS | Основная мера | Подробнее |
|---|---|---|---|---|
| Раздача поддельного артефакта | Файл злоумышленника ставят как легитимное обновление | Нет (бессилен при компрометации origin и ошибочной публикации) | Signed metadata и проверка hash / подписи артефакта на клиенте | 4.2 / 4.3 / 4.4 |
| rollback | Подпись верна, но клиента откатывают на старую версию с известной уязвимостью | Нет (и подпись, и TLS корректны) | Хранить максимальную известную release version и отклонять всё старше | 4.8 |
| freeze | Новая версия уже есть, а клиенту продолжают отдавать старые metadata и не дают обновиться | Нет | Поле expires_at в metadata и отказ от слишком старых |
4.3 / 4.8 |
| mix-and-match | Отдают несогласованную пару metadata и артефакта | Нет | Зафиксировать в manifest hash / size / version целевого artifact | 4.3 / 4.8 |
| Компрометация ключа подписи | Раздают вредоносное обновление с легитимной подписью | Нет | Разделение ключей разработки и продакшена, HSM или сервис подписи, согласование и журнал аудита, разделение root-ключа и ключа metadata | 4.5 |
| Сбой или обрыв обновления | Упали посреди замены — приложение больше не стартует | Вне скоупа TLS | staging + атомарная активация + rollback | 4.6 |
| Обход проверки | В продакшене остаётся лазейка вроде skipVerify |
Вне скоупа TLS | Fail-closed как спецификация, без флагов обхода | 3.7 / 4.6 |
| Нельзя остановить инцидент | Проблемную версию продолжают раздавать | Вне скоупа TLS | blocklist, minimum allowed version, kill switch | 4.8 / 7 |
Что колонка «закрывает ли один TLS» везде «нет» или «вне скоупа» — как раз тезис статьи. TLS защищает канал. Содержимое того, что раздают, и право это раздавать защищают другие механизмы.
3. Антипаттерны
Сначала соберём опасные схемы, которые часто встречаются на практике.
| Антипаттерн | В чём опасность | Минимальное исправление |
|---|---|---|
Забрать version.json по HTTPS и сразу запустить zip / exe по URL |
Слабо против компрометации origin, подмены настроек, ошибочной публикации | Перейти на клиентскую проверку signed metadata и артефактов |
| Подписан только бинарник, manifest без подписи | Можно подменить URL, version, channel, флаг обязательного обновления | Signed manifest с version / hash / size / channel / expiry |
| Ключ подписи лежит файлом на машине разработки или в CI | При компрометации раздают легитимно подписанный malware | HSM / сервис подписи + согласование + журнал аудита |
| При сбое обновления «игнорировать ошибку проверки и продолжить» | В момент инцидента открывается самый слабый путь | Fail-closed |
| Перезапись без сохранения старой версии | Отключение питания, нехватка диска, сбой посередине — и приложение не стартует | staging + атомарная активация + rollback |
| Пускать старую версию только по сравнению номеров | Проходит rollback на уязвимую версию | Монотонно растущая release version и хранение максимальной известной |
| Весь updater от имени администратора | При компрометации широкий радиус поражения | Загрузка и проверка с низкими правами, замена — в минимальном helper |
| Начинать с дифференциальных обновлений | Сложная реализация, больше дыр в проверке | Сначала полное обновление пакетом |
Ниже чуть подробнее.
3.1 Остановиться на «HTTPS, значит всё в порядке»
Это самый частый случай.
- При старте читают
latest.json - Достают
downloadUrl - Качают zip / exe
- Распаковывают и подменяют
- Конец
С виду похоже на обновление, но корень доверия слишком сильно сидит в ответе сервера. Если скомпрометированы сервер обновлений или настройки доставки, вредоносное обновление раздают поверх совершенно корректного HTTPS.
TLS нужен. Но одним TLS проектирование updater не заканчивается.
3.2 Подпись есть, но клиент её не проверяет
Даже если файл подписали на релизе, это ничего не даёт, пока клиент на подпись не смотрит.
Частая картина:
- в CI подписывают
- но updater смотрит только hash
- и сам hash берётся из неподписанного manifest
Тогда в момент подмены manifest вместе с ним подменяют и hash. «Мы смотрим hash, значит безопасно» выполняется, только когда защищено и происхождение этого hash.
3.3 Manifest не подписан
В системе обновления защищать нужно не только сам исполняемый файл. Как минимум опасно, если можно подделать следующее:
- version / release id
- URL или имя файла загрузки
- hash / size
- channel (stable / beta и т. п.)
- обязательность обновления
- применимые ОС / архитектуру
- срок действия metadata
- минимально необходимую версию updater
То есть правильный ориентир — всё, на чём стоит решение обновлять, класть в signed metadata.
3.4 С ключом подписи обращаются небрежно
Безопасность обновления в значительной доле — это безопасность ключей.
Если продакшен-ключ подписи лежит так, это уже довольно опасно:
- так и остался в хранилище сертификатов на машине разработки
- залит в CI как secret в виде
.pfx - один и тот же закрытый ключ раздали нескольким людям локально
- подпись для разработки и продакшена сидит на одной цепочке доверия
Тогда даже корректный updater не остановит «легитимно подписанное вредоносное обновление».
3.5 Перезапись без сохранения старой версии
В обновлении важнее сценарий сбоя, чем сценарий успеха.
- загрузка оборвалась посередине
- распаковка не удалась
- питание пропало во время замены
- новая версия стартовала, но упала на первой миграции
Если к этому моменту старая версия уже стёрта, восстановление становится тяжёлым. На практике больнее не факт «обновление не удалось», а то, что «на площадке приложение перестало запускаться».
3.6 Rollback не продуман
Даже подписанная легитимная версия может быть выгодна атакующему, если это старая уязвимая.
Например:
- в version 1.8 есть известная уязвимость
- на площадках уже стоит 2.3
- атакующий заново раздаёт 1.8
Если это проходит, ситуация опасна, хотя сама подпись верна.
Мало проверить «подписано ли». Нужно ещё «можно ли ставить именно эту версию сейчас».
3.7 Fail-open
В продакшене этого делать нельзя больше всего.
- проверка подписи не прошла — показали предупреждение и пошли дальше
- есть скрытый флаг, которым можно игнорировать истечение сертификата
- отладочный
skipVerify=trueостаётся и в продакшене
Именно в сбое или атаке такие лазейки становятся основным путём.
4. Лучшие практики
4.1 Сначала сесть на готовую инфраструктуру обновлений
Безопаснее сначала усомниться, нужен ли свой updater вообще.
Для Windows, пока требования позволяют, в первую очередь стоит смотреть сюда:
- MSIX + App Installer
- ClickOnce
- Store / MDM / внутреннюю инфраструктуру распространения
- MSI + корпоративное управление доставкой
Причина простая: часть ответственности за само обновление можно отдать платформе. Свободы, конечно, меньше, зато проще согласовать UI обновления, distribution manifest, подпись пакета и эксплуатацию.
Свой updater нужен, например, когда:
- нужно жёстко контролировать несколько каналов stable / beta / preview
- нужны поэтапная доставка и доля rollout
- момент обновления нужно тонко крутить под свои бизнес-ограничения
- есть конфигурация, которая на MSIX / ClickOnce не садится
Даже тогда полезнее понимать это не как «хотим свободы», а как «берём ответственность за обновление на себя» — меньше шансов сбиться.
4.2 Якорь доверия держать на клиенте
Безопасный updater не принимает ответ сервера на веру. На клиенте нужны как минимум две вещи.
- Открытый ключ или цепочка сертификатов, которым доверяют
- Механизм проверки metadata, подписанных этим ключом
Иначе говоря, нужно состояние, в котором клиент подтверждает не «сервер говорит, что это последняя версия», а «эти metadata — последняя версия, выпущенная подписантом, которому мы доверяем».
Как доверие тянется от корня до файла — на схеме.
flowchart TD
ROOT["корневой ключ<br/>trust anchor, встраиваемый в клиент"] --> SIGNKEY["ключ подписи metadata<br/>делегируется от root, часто обновляется"]
SIGNKEY --> META["подписанные update metadata<br/>version / url / hash / size / expiry"]
META --> CHECK1{"проверка подписи, срока, version"}
CHECK1 -- "NG" --> STOP["обновление прервано<br/>fail-closed"]
CHECK1 -- "OK" --> DL["артефакт качается в staging"]
DL --> CHECK2{"проверка size / hash / подписи пакета"}
CHECK2 -- "NG" --> STOP
CHECK2 -- "OK" --> ACT["активация с сохранением старой версии"]
ACT --> HEALTH{"проверка здоровья при первом запуске"}
HEALTH -- "NG" --> RB["rollback на старую версию"]
HEALTH -- "OK" --> DONE["обновление завершено"]
Со схемы нужно унести две вещи. Сверху вниз доверие идёт одной цепочкой. И где бы цепочка ни оборвалась, выход — обновление прервано или rollback, а не пока пойдём дальше.
4.3 Проектировать вокруг signed metadata
Как минимум в metadata обновления нужно включить следующее и сделать это подписываемым.
| Поле | Зачем |
|---|---|
| release version / release id | Защита от rollback, аудит |
| имя artifact, URL, package type | Фиксирует, какой файл брать |
| hash, size | Обнаружение подделки и битой доставки |
| channel | Не смешивать beta со stable |
| целевые ОС / architecture | Не отдать не ту сборку |
| minimum updater version | Остановить старый updater при смене протокола |
| expires_at | Защита от freeze |
| published_at | Аудит, разбор |
| mandatory / optional | Даже ветвление UX обновления нельзя подделать |
Главное здесь — все данные для решения обновлять собрать в signed metadata. Логика живёт на клиенте, подлинность информации держит подпись — так меньше инцидентов.
Без конкретного вида это не падает в реализацию, поэтому минимальный пример. Сначала содержимое, которое подписывают.
{
"schema_version": 1,
"channel": "stable",
"release_version": "2.4.1",
"published_at": "2026-04-09T01:00:00Z",
"expires_at": "2026-04-16T01:00:00Z",
"minimum_updater_version": "2.0.0",
"minimum_allowed_version": "2.2.0",
"mandatory": false,
"artifacts": [
{
"os": "windows",
"arch": "x64",
"package_type": "msi",
"file_name": "MyApp-2.4.1-x64.msi",
"url": "https://updates.example.com/stable/MyApp-2.4.1-x64.msi",
"size": 48234496,
"sha256": "5f2c...64 hex-цифры..."
}
]
}
Его оборачивают вместе с подписью.
{
"signed": {
"schema_version": 1,
"channel": "stable",
"release_version": "2.4.1",
"_comment": "сюда кладётся тот же объект, что выше"
},
"signatures": [
{
"keyid": "3f9a...",
"sig": "MEUCIQ..."
}
]
}
Такая форма нужна потому, что все значения для решения обновлять лежат внутри signed. URL, version, channel, флаг обязательности — все внутри, поэтому подмена одной оболочки падает на проверке. Порядок на клиенте фиксируют так: «проверить signed → если прошло, пользоваться только значениями оттуда». Если до проверки уже читают url и начинают качать, смысл этой формы пропадает.
В реализации есть ещё одно место, на которое стоит смотреть заранее. У JSON от порядка ключей и пробелов меняется байтовая последовательность. Проверка подписи идёт по байтам, поэтому сначала решите, подписываете вы «каноническое представление» или «принятые байты как есть». Если этого не решить, сервер подписывает сгенерированный объект, а клиент — результат повторной сериализации, и корректное обновление не проходит проверку. Если же это расхождение начинают обходить фразой «ну ладно, пропустим», это первый шаг к fail-open.
4.4 Проверять и сам артефакт
После проверки metadata у скачанного артефакта смотрят ещё:
- size
- hash
- подпись пакета / подпись кода
- издателя или ожидаемый идентификатор
Для Windows PE / MSI / MSIX безопаснее исходить из того, что Authenticode или подпись пакета проверяет сам клиент. На macOS для согласованности лучше исходить из того, что Developer ID и notarization действуют и на пути обновления.
4.5 Ключи защищает не функция, а эксплуатация
В управлении ключами разница проявляется не столько в коде, сколько в эксплуатации.
Как минимум безопаснее разделить:
- ключ подписи для разработки
- ключ подписи для staging
- ключ подписи для продакшена
А продакшен-ключ ещё желательно проектировать вместе с:
- HSM
- облачным сервисом подписи
- системой подписи с согласованием
- журналом аудита
- процедурой key rotation
- подписью с timestamp
«Продакшен-сборка прошла — CI сам подписал» удобно, но при компрометации радиус поражения тоже большой. Как минимум должно быть видно, кто, что и когда подписал.
Когда эксплуатация уже стоит, ещё безопаснее разделить редко меняемый root trust и ключ, которым часто переподписывают metadata обновления. Схема, где root держат ближе к offline, а metadata обновления подписывают другим ключом, обычно уменьшает радиус поражения при компрометации ключа.
4.6 Fail-closed и staged update
Базовый порядок потока обновления такой.
- Получить metadata
- Проверить подпись, срок действия и version
- Скачать артефакт в область staging
- Проверить hash / size / подпись
- Подготовить activation, оставив старую версию
- Переключиться при перезапуске или через отдельный helper
- Проверить здоровье при первом запуске
- При проблеме — rollback
Здесь важны два пункта: не подменять файлы, пока проверка не закончена не идти дальше, если проверка не прошла.
4.7 Сужать права updater
Весь updater от имени администратора лучше не гонять.
Идеальное разделение такое:
- загрузка и проверка: низкие права
- только замена файлов: helper с минимальными правами
- helper не делает ничего сверх «положить проверенный package в нужное место»
Чем сильнее дизайн требует повышения прав, тем опаснее он становится, если до повышения не разграничено, что уже проверено.
4.8 Закрывать rollback / freeze / mix-and-match с самого начала
Добавлять это потом тяжело, поэтому лучше заложить сразу.
-
Защита от rollback Клиент хранит «максимальную из уже виденных metadata version / release version» и отклоняет всё старше
-
Защита от freeze У metadata есть expiry, слишком старые metadata отклоняются
-
Защита от mix-and-match Части metadata согласованы между собой. Как минимум в самом manifest зафиксированы hash / size / version целевого artifact
Плюс, если через signed metadata можно раздать blocklist конкретных build или minimum allowed version, инцидент локализуется быстрее.
Даже без полного внедрения TUF эти три свойства очень важны.
4.9 Сначала полное обновление
Дифференциальные обновления экономят канал, но для первой реализации они сложны.
- от какой старой версии к какой новой идёт эта дельта
- hash до применения дельты
- итоговый hash после применения
- восстановление при сбое посередине
- уборка частично применённых и устаревших дельт
Всё это наваливается сразу. Для первой версии достаточно безопасно подменить полный подписанный пакет.
5. Минимальная безопасная схема
Даже без полного TUF минимальная безопасная схема своего updater обычно выглядит так.
5.1 Что хранит клиент
Клиент хранит материал, чтобы сомневаться в ответе сервера. Если его нет, «можно ли обновлять» решается только со слов сервера.
- доверенный корневой открытый ключ или зафиксированная цепочка сертификатов
- текущая работающая version
- максимальная из уже виденных metadata version / release version
- разрешённый channel
- предыдущая версия для rollback
5.2 Что возвращает сервер
Сервер возвращает не само решение, а материал для него. По отдельности этому нельзя доверять: смысл появляется только после проверки через trust anchor из 5.1.
- подписанные update metadata
- артефакты с подписью разработчика или платформы
- при необходимости — сведения blocklist / minimum allowed version
5.3 Типичный поток
взять metadata
↓
проверить подпись, expiry, version, channel
↓
скачать артефакт в staging
↓
проверить size / hash / package signature
↓
активировать, оставив старую версию
↓
если первый запуск не удался — rollback
Важно здесь то, что одного ответа сервера обновлений недостаточно, чтобы что-то считать состоявшимся. Состоявшимся это делает trust anchor на клиенте и логика проверки.
6. Как к этому подходить в Windows-проектах
Для Windows-приложений проще выстраивать логику, отталкиваясь от способа распространения.
- если требования садятся — MSIX App Installer
- если это внутреннее .NET-приложение, которому подходит per-user, — ClickOnce
- если нужны службы, драйверы, shell extension или свой контроль каналов — в сравнение стоит включить и MSI + свой updater
Но выбор своего updater работу не уменьшает. Скорее наоборот, её становится больше.
- проверка Authenticode / подписи пакета
- signed manifest
- защита от rollback
- разделение прав update helper
- стратегия обновления самого updater
6.1 Как на клиенте проверять подпись Authenticode
Одной фразы «проверяем Authenticode» для реализации мало, поэтому вот входные точки в Windows.
| Что нужно | Чем |
|---|---|
| Проверять из кода updater | API WinVerifyTrust (wintrust.dll). С WINTRUST_ACTION_GENERIC_VERIFY_V2 применяется политика проверки Authenticode |
| Проверять из эксплуатационных шагов или CI | PowerShell Get-AuthenticodeSignature |
| Подписывать и проверять на релизе | signtool sign / signtool verify из Windows SDK |
В PowerShell минимальная проверка выглядит так.
$path = ".\MyApp-2.4.1-x64.msi"
$sig = Get-AuthenticodeSignature -FilePath $path
if ($sig.Status -ne 'Valid') {
throw "signature check failed: $($sig.Status) / $($sig.StatusMessage)"
}
# Одного «подпись действительна» мало. Нужно ещё зафиксировать, чья это подпись.
# Но Subject (различающееся имя) не уникален. Сертификат с теми же CN/O/C
# может выдать любой другой ЦС, и если этот терминал ему доверяет,
# Status будет Valid, а сравнение Subject тоже пройдёт.
# Фиксировать нужно цепочку издателя или открытый ключ; Subject — только фильтр сверху.
$expectedIssuers = @( # отпечатки издателя (промежуточный ЦС / корень)
'9F86D081884C7D659A2FEAA0C55AD015A3BF4F1B' # ← подставьте фактическое значение
)
$expectedSubject = 'CN=Example Software Inc., O=Example Software Inc., C=JP'
# Цепочку здесь собирают только чтобы пройтись по издателям.
# Доверять или нет уже решил Status = Valid выше.
# X509ChainPolicy.VerificationTime по умолчанию — момент вызова конструктора
# (то есть сейчас), поэтому обычный Build падает с NotTimeValid,
# как только у сертификата подписи истечёт срок. Подпись с timestamp
# после истечения срока всё ещё Valid, и если оставить значение по умолчанию,
# updater начнёт отбрасывать все прошлые корректно проверенные релизы
# в момент смены сертификата.
# Если известен момент подписи — кладите его в VerificationTime.
# Если нет — срок уже проверен выше, в этом Build на него не смотрим
$chain = [System.Security.Cryptography.X509Certificates.X509Chain]::new()
$chain.ChainPolicy.VerificationFlags = 'IgnoreNotTimeValid'
$chain.ChainPolicy.RevocationMode = 'NoCheck'
try {
$built = $chain.Build($sig.SignerCertificate)
# Где-то на пути от подписанта к корню должен быть ожидаемый издатель.
# Если Build не удался, цепочка заполнена только частично,
# и эта проверка тоже не пройдёт (fail-closed)
$chainThumbprints = @($chain.ChainElements | ForEach-Object { $_.Certificate.Thumbprint })
if (-not ($expectedIssuers | Where-Object { $chainThumbprints -contains $_ })) {
throw "unexpected issuing chain (built=$built): $($chainThumbprints -join ' / ')"
}
}
finally {
$chain.Dispose()
}
if ($sig.SignerCertificate.Subject -ne $expectedSubject) {
throw "unexpected signer: $($sig.SignerCertificate.Subject)"
}
Здесь стоит держать в голове пять ограничений.
Status=Validзначит только «подпись как подпись корректна». Чья это подпись, проверяют отдельно- Совпадение
Subjectне доказывает, что это «тот самый» подписант. Различающееся имя — не уникальный идентификатор. И внутренний корпоративный ЦС, и публичный ЦС могут выдать сертификат подписи кода с одним и тем жеCN=Example Software Inc., O=Example Software Inc., C=JP. Если клиент этому ЦС доверяет, подменённая сборка, подписанная другим ключом и другим издателем, пройдёт иStatus = Valid, и сравнениеSubject. Фиксировать нужно цепочку издателя (отпечаток ожидаемого промежуточного ЦС / корня есть на пути от подписанта к корню) или открытый ключ, аSubjectиспользовать уже как фильтр поверх этого - Раз зафиксировали — заранее продумайте процедуру смены. Иначе в день смены сертификата или ЦС обновление останавливается на всех терминалах. Ожидаемые значения обязательно держите массивом и сначала примите и старое, и новое (сначала раздать updater, который уже пускает нового издателя → сменить подпись → убрать старое значение, три шага). «Сначала раздать» можно только если сам updater умеет обновляться, поэтому это проектируют вместе со «стратегией обновления самого updater» из начала главы 6
- Подпись без timestamp перестаёт проходить проверку в момент истечения сертификата. На релизе timestamp ставят всегда. В 4.5 подпись с timestamp названа именно поэтому
X509Chain, которым идут по издателям, не должен заново судить срок действия по текущему времени.X509ChainPolicy.VerificationTimeпо умолчанию — момент вызова конструктора, то есть сейчас. Документация прямо пишет: при проверке подписанного сообщения подпись должна быть действительна на момент подписи, а не на момент проверки, поэтому это свойство важно. Если оставить значение по умолчанию, пакет, который благодаря timestamp получилStatus = Valid, сразу следом отбросят как «сертификат истёк». Смысл timestamp пропадает, и в момент смены сертификата падают все прошлые релизы. Если момент подписи известен, его кладут вVerificationTime; если нет — ставятIgnoreNotTimeValid. Это не ослабление. Срок и доверие уже решилStatus = Validуровнем выше, а этотBuildнужен только чтобы узнать, кто издал. По той же причине отзыв здесь не проверяют (CRL для уже истёкшего сертификата вовсе не обязан продолжать публиковаться). Остановить утечку ключа нужно не CRL, а blocklist и minimum allowed version (4.8)
И ещё: результат проверки зависит от хранилища сертификатов и настроек доверия этого терминала. Если на клиенте доверие настроено слабо, смысл Status = Valid тоже слабее. Если updater сам держит ожидаемого издателя, как выше, расширение доверия на терминале на него не влияет.
Типичная опасная схема в Windows — прямая цепочка DownloadFile -> unzip -> kill process -> overwrite -> restart.
Она может работать, но слаба и по безопасности, и по восстановлению.
Практика, при которой пользователей проводят через предупреждения SmartScreen и UAC кнопками «Подробнее → Выполнить», — это не дизайн обновления, а привыкание к предупреждениям. Правильный путь обновления — не приучать игнорировать предупреждения, а собирать распространение и проверку так, чтобы предупреждения появлялись редко.
Само сравнение способов распространения разобрано и в другой статье: Как выбрать способ распространения Windows-приложения — MSI/MSIX/ClickOnce/xcopy/собственный updater
7. Минимальный чек-лист
Перед выпуском своего updater стоит проверить хотя бы следующее.
- Metadata обновления подписаны
- В metadata есть version / hash / size / channel / expiry
- Клиент проверяет подпись и version
- Подписанта фиксируют не по различающемуся имени (Subject), а по цепочке издателя или открытому ключу
- Есть процедура смены зафиксированных значений (период, когда принимают и старое, и новое)
- Проверяют hash артефакта и подпись платформы
- Продакшен-ключ подписи отделён от среды разработки
- Остаются журнал использования ключа и записи согласования
- Используют подпись с timestamp
- При staging-обновлении переключаются, оставляя старую версию
- Есть условия и процедура rollback
- При сбое проверки останавливаются fail-closed
- Есть политика обновления самого updater
- Можно раздать blocklist / minimum allowed version
- Есть kill switch, который останавливает поэтапную доставку
- Можно наблюдать долю сбоев, долю rollback, сбои проверки подписи
Если в этом чек-листе много пустых пунктов, эффективнее сначала собрать модель доверия распространения, а не UI updater.
8. Итог
В безопасности автообновления в итоге остаётся вот что.
Проектировать не удобство обновления, а то, кому доверять и как клиент это доверие проверяет.
Отсюда практические решения можно сжать так.
- Если готовой инфраструктуры хватает — сначала сесть на неё
- Если пишете свой updater, signed metadata и управление ключами нужны раньше, чем отдельная возня с HTTPS
- Updater без восстановления после сбоя и rollback в продакшене тяжело жить
- Updater — это не функция доставки, а сама граница безопасности продукта
Если текущая схема близка к latest.json + подмена zip, первым делом чинить нужно не загрузку, а то, куда положено доверие.
Уже одно это заметно меняет уровень риска.
9. Источники
- CISA Secure by Design Pledge
- NIST: Security Considerations for Code Signing
- NIST Secure Software Development Framework (SSDF)
- The Update Framework Specification
- TUF: Roles and metadata
- TUF: Security
- Microsoft Learn: Authenticode Digital Signatures
- Microsoft Learn: WinVerifyTrust function
- Microsoft Learn: Get-AuthenticodeSignature
- Microsoft Learn: Auto-update and repair apps - MSIX
- Microsoft Learn: ClickOnce Deployment and Security
- Apple Developer: Developer ID
- CA/Browser Forum: Baseline Requirements for the Issuance and Management of Publicly-Trusted Code Signing Certificates
Связанные темы
Страницы тем рядом с этой. От статьи можно перейти к связанным услугам и другим статьям.
Технические темы Windows
Входная точка по разработке под Windows, разбору сбоев и использованию существующего кода.
Услуги, связанные с этой темой
Разработка приложений для Windows
Автообновление — это не только UI, а дизайн, в который входят способ распространения, права, восстановление и эксплуатация. Можем помочь и с новой разработкой Windows-приложения, и с пересмотром существующего ПО, начиная с упорядочивания способа обновления.
Технические консультации и ревью архитектуры
Можно прийти уже на этапе постановки: «нужен ли свой updater», «хватит ли MSIX / ClickOnce», «где в текущем дизайне обновления риск».
Профиль автора
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке ПО для Windows, технических консультациях и разборе сбоев. Особенно силён в проектах, где остаётся существующий код, и в расследовании неисправностей, у которых причина плохо видна.
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Почему Windows показывает сообщение «Windows защитил ваш компьютер»
Разбираем на практике, почему при распространении Windows-приложения появляется предупреждение SmartScreen: подпись кода, сертификаты EV/...
Как выбрать способ распространения Windows-приложения — MSI/MSIX/ClickOnce/xcopy/свой updater
Способ распространения Windows-приложения — это не предпочтение формата установщика, а выбор глубины интеграции с ОС и того, кто отвечает...
Именованные каналы на практике — от проектирования до безопасности IPC в Windows
Практический разбор именованных каналов — стандартного IPC в Windows. По первоисточникам: выбор байтового режима и режима сообщений, серв...
Если Microsoft Defender принял ваше Windows-приложение за вирус — ложные срабатывания и влияние на производительность
Разбираем штатный порядок действий, если Microsoft Defender ложно определяет ваше Windows-приложение как вредоносное. Как устроена соврем...
Ускоряет ли Windows отключение целостности памяти (HVCI) — что это значит, как делать и как решать
Действительно ли отключение целостности памяти (HVCI) ускоряет ПК с Windows? Когда помогает, когда нет, как выключить и вернуть и как при...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Разработка приложений для Windows
Распространение, обновление, подпись, rollback и выбор между MSIX и ClickOnce для Windows-приложений нужно продумывать не только на уровне кода, но и вместе со способом доставки и эксплуатационным дизайном.
Технические консультации и ревью дизайна
Граница доверия автообновления, signed metadata, работа с ключами и fail-closed важнее собрать как общую архитектуру, чем как отдельную реализацию.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- Автообновление по HTTPS уже можно считать безопасным?
- TLS нужен, но его недостаточно. TLS защищает в основном канал и подлинность конечной точки. Он не закрывает случаи, когда скомпрометирован сам сервер обновлений, на легитимный CDN положили неверный артефакт или подменили неподписанный manifest. Решение обновлять нужно принимать не потому, что «так сказал сервер», а потому, что «клиент проверил подписанные metadata и счёл их корректными».
- Что включать в metadata обновления и подписывать?
- Базовый принцип — собрать в signed metadata все данные, по которым принимается решение обновить. Конкретно: release version, URL и имя артефакта, hash и size, channel (stable / beta и т. п.), целевые ОС и архитектуру, minimum updater version, срок действия metadata (expires_at), флаг обязательного обновления. Если подписан только бинарник, а manifest остаётся без подписи, остаются URL, версия и флаг обязательности, которые можно подменить.
- Что такое rollback-атака и как от неё защититься?
- Это атака, при которой злоумышленник заново раздаёт старую, но легитимно подписанную версию с известной уязвимостью и откатывает клиентов на неё. Сама подпись верна, поэтому одной проверки подписи мало. Клиент должен хранить максимальную из уже виденных release version и отклонять всё старше. Плюс срок действия metadata, чтобы закрыть freeze-атаку, которая прячет новую версию, и зафиксированные в manifest hash, size и version артефакта, чтобы закрыть mix-and-match.
- Писать свой updater или взять готовый механизм?
- Если требования позволяют, безопаснее сначала опереться на готовую инфраструктуру обновлений — MSIX App Installer, ClickOnce и т. п. Тогда ответственность за обновление в значительной части уходит на платформу. Свой updater нужен, когда требования на неё не садятся: жёсткий контроль нескольких каналов, поэтапная доставка и подобное. Даже тогда первым делом нужны не UI, а проверка подписи и восстановление после сбоя: fail-closed — не идти дальше, если проверка не прошла, — и возможность откатиться, оставляя старую версию на месте при переключении.
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.