Безопасность автообновления: почему одного 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

Оглавление

  1. Сначала вывод
  2. Почему автообновление — зона высокого риска
  3. Антипаттерны
  4. Лучшие практики
  5. Минимальная безопасная схема
  6. Как к этому подходить в Windows-проектах
  7. Минимальный чек-лист
  8. Итог
  9. Источники

На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 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 же сразу держит три вещи:

  1. Ходит наружу за файлами
  2. Доверяет этим файлам
  3. Подменяет уже стоящие исполняемые файлы

То есть путь к выполнению произвольного кода изначально встроен в продукт.

Типичное заблуждение здесь — «раз 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 не принимает ответ сервера на веру. На клиенте нужны как минимум две вещи.

  1. Открытый ключ или цепочка сертификатов, которым доверяют
  2. Механизм проверки metadata, подписанных этим ключом

Иначе говоря, нужно состояние, в котором клиент подтверждает не «сервер говорит, что это последняя версия», а «эти metadata — последняя версия, выпущенная подписантом, которому мы доверяем».

Как доверие тянется от корня до файла — на схеме.

NGOKNGOKNGOKкорневой ключtrust anchor, встраиваемый в клиентключ подписи metadataделегируется от root, часто обновляетсяподписанные update metadataversion / url / hash / size / expiryпроверка подписи, срока, versionобновление прерваноfail-closedартефакт качается в stagingпроверка size / hash / подписи пакетаактивация с сохранением старой версиипроверка здоровья при первом запускеrollback на старую версиюобновление завершено

Со схемы нужно унести две вещи. Сверху вниз доверие идёт одной цепочкой. И где бы цепочка ни оборвалась, выход — обновление прервано или 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

Базовый порядок потока обновления такой.

  1. Получить metadata
  2. Проверить подпись, срок действия и version
  3. Скачать артефакт в область staging
  4. Проверить hash / size / подпись
  5. Подготовить activation, оставив старую версию
  6. Переключиться при перезапуске или через отдельный helper
  7. Проверить здоровье при первом запуске
  8. При проблеме — 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. Источники

Связанные темы

Страницы тем рядом с этой. От статьи можно перейти к связанным услугам и другим статьям.

Технические темы Windows

Входная точка по разработке под Windows, разбору сбоев и использованию существующего кода.

Услуги, связанные с этой темой

Разработка приложений для Windows

Автообновление — это не только UI, а дизайн, в который входят способ распространения, права, восстановление и эксплуатация. Можем помочь и с новой разработкой Windows-приложения, и с пересмотром существующего ПО, начиная с упорядочивания способа обновления.

Технические консультации и ревью архитектуры

Можно прийти уже на этапе постановки: «нужен ли свой updater», «хватит ли MSIX / ClickOnce», «где в текущем дизайне обновления риск».

Профиль автора

Го Комура

Представитель KomuraSoft LLC

Специализируется на разработке ПО для Windows, технических консультациях и разборе сбоев. Особенно силён в проектах, где остаётся существующий код, и в расследовании неисправностей, у которых причина плохо видна.

Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.

Эти страницы показывают тему статьи в более широком контексте услуг и решений.

Статья напрямую связана со следующими услугами.

Частые вопросы

Вопросы, которые часто возникают при консультациях по теме статьи.

Автообновление по 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, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.

Публичные ссылки

Вернуться в блог