Skip to content

Подготовка стабильного выпуска 1.0.0: семантика версий, Flowseal 1.10.2, обязательная подпись - #1

Closed
Jacksony100 wants to merge 13 commits into
mainfrom
claude/production-hardening-v1
Closed

Jacksony100 wants to merge 13 commits into
mainfrom
claude/production-hardening-v1

Conversation

@Jacksony100

@Jacksony100 Jacksony100 commented Sep 11, 2026

Copy link
Copy Markdown
Owner

Что это

Подготовка CHEBURNET к стабильному выпуску 1.0.0 от состояния v1.0.0-rc.3. Одиннадцать логических коммитов, ни один не смешивает несвязанные изменения. Модель доверия нигде не ослаблена.

Статус: RELEASE_CANDIDATE_READY_WITH_BLOCKERS. Два обязательных шлюза стабильного выпуска остаются невыполненными — см. раздел «Блокеры». Версия намеренно оставлена на rc.3.

P0 — закрыты все три

Семантика версий RC/stable

Источник версии был один числовой литерал project(CHEBURNET VERSION 1.0.0), поэтому сборка по тегу v1.0.0-rc.3 сообщала о себе как 1.0.0, и будущий стабильный 1.0.0 оценивался бы установленным RC как Current, а не Upgrade.

Введён cmake/Version.cmake — единственный авторитетный источник из четырёх полей, из которого выводятся семантическая версия (1.0.0-rc.3), числовая PE-версия (1.0.0.3), признак предварительного выпуска и канал обновлений. Решения об обновлении принимаются только по семантической версии.

Сравнение версий не ослаблено, а уточнено до precedence SemVer 2.0.0 (rc.2 < rc.10, чего раньше не было); ведущие нули и пустые идентификаторы отклоняются как псевдонимы. Канал перестал быть константой true: стабильная сборка по-прежнему отвергает любые предварительные версии.

Фактическое доказательство: подписанный манифест теперь несёт "launcher":{"version":"1.0.0-rc.3"} вместо прежнего 1.0.0.

Flowseal 1.10.2

Импорт штатным защищённым импортёром, файлы вручную не копировались. ALT13 (21 → 22 стратегии), обновлённые списки и ресурсы. UPSTREAM_FIDELITY: PASS version=1.10.2 files=46 strategies=22, STRATEGY_FIDELITY: 88/88.

Обязательная подпись Authenticode

Стабильный тег vX.Y.Z не может быть опубликован без подписи; требование выводится из модели версии, а не из отдельного флага. Политика вынесена чистой функцией и отклоняет всё, кроме статуса Valid: NotSigned, HashMismatch, недоверенную цепочку, просроченный сертификат, отсутствие метки времени. Опубликованные байты привязаны к подписанным по SHA-256 в трёх местах. Метаданные генерируются только после подписи.

P1 — закрыты пять из шести

  • Неблокирующая проверка обновлений. UpdateCheckService: подключение не ждёт сетевого ввода-вывода. Поток не отсоединяется, Cancel() идемпотентен и присоединяет его, мьютекс не удерживается во время I/O и join, фоновый код не трогает интерфейс. Модель доверия совпадает с синхронным путём.
  • Автоматизация upstream. Новый стабильный выпуск Flowseal превращается в ровно одну устойчивую задачу с полными метаданными и результатом проверки. Три задания с раздельными правами, без contents: write, дедупликация по скрытому маркеру, автослияния нет.
  • Релизный шлюз E2E. Шесть сценариев, управляемых запуском настоящего исполняемого файла (вся работа запуска происходит до появления интерфейса). Защита окружения отказывается работать на непомеченной машине. Воспроизводимая подготовка VM Hyper-V.
  • Инструменты безопасности. MSVC /analyze с /WX и CodeQL; мутационная проверка свойств пяти разборщиков; полная решётка внесения отказов транзакционной активации.
  • Экспорт диагностики. Локальный архив с редактированием, без единого сетевого вызова, состав показывается до записи.
  • Рефакторинг намеренно узкий: решения восстановления при запуске вынесены в проверяемый модуль. Более глубокое разделение отнесено к 1.1, как предусмотрено планом выпуска.

Три дефекта, которых в задании не было

  1. Полный импорт upstream не мог завершиться ни для какой версии. Изолированное дерево проверки собиралось без docs и SECURITY.md, которых требует шлюз лицензий, поэтому импорт падал на отсутствующем файле репозитория, а не на нагрузке. Добавленный регрессионный тест немедленно поймал второй случай того же дрейфа.
  2. Порог minimum_supported_version: "1.0.0" дал бы установленному 1.0.0-rc.N отказ «слишком старая программа» при переходе на стабильный 1.0.0, потому что предварительная версия упорядочена ниже стабильной.
  3. Ревизия upstream 1.10.2a считалась понижением относительно 1.10.2 — легитимный хотфикс Flowseal был бы отклонён.

Плюс три находки статанализа в привилегированных путях, из них реальный пробел: размер списка атрибутов при создании элевированного процесса брался без проверки.

Проверки

Проверка Результат
Чистая сборка /W4 /WX PASS, предупреждений нет
CTest 43/43 (было 33)
Strategy fidelity 88/88 (было 84)
MSVC /analyze с /WX PASS, находок в коде CHEBURNET нет
Фаззинг 500 000 итераций + 3 зерна по 50 000 — находок нет
validate-licenses / verify-upstream PASS / PASS
prepare-release (RC-канал) PASS, launcher=1.0.0-rc.3 pe=1.0.0.3

Новые тесты: version_model, release_signing, importer_validation_tree, upstream_automation, e2e_harness, updatecheckservice, fuzzparsers, faultinjection, diagnostics, runtimerecovery.

Блокеры стабильного выпуска

Подробно — в docs/production/FINAL_RELEASE_CERTIFICATION.md.

  • B1 — проверка на чистых VM Windows 10/11 не выполнена. Сценарии изменяют фиксированное защищённое %ProgramData%\CHEBURNET и требуют загрузки драйвера ядра. Оснастка и подготовка VM готовы; выполнен и пройден только сценарий отказа запуска без прав администратора.
  • B2 — подпись сертификатом CHEBURNET ни разу не выполнялась. Политика и все её отказы проверены на реальных подписях, но сертификата подписи кода в окружении разработки нет.
  • B3 — версия не переключена на стабильную. Намеренно: cmake/Version.cmake объявляет rc.3. Шлюз тега не даст опубликовать стабильный тег при RC-источнике и наоборот.

Порядок для стабильного выпуска: B2 (подпись на RC-теге) → B1 (обе чистые VM) → B3 (заменить rc.3 на "" и создать тег v1.0.0).

Новая документация

docs/production/: BASELINE_BEFORE_HARDENING.md, UPSTREAM_1_10_2_IMPORT.md, RELEASE_SIGNING.md, UPSTREAM_AUTOMATION.md, E2E_RELEASE_GATE.md, SECURITY_TOOLING.md, FINAL_RELEASE_CERTIFICATION.md; docs/architecture/PRIVILEGE_SEPARATION_V2.md (только исследование, в 1.0 не входит).

Jacksony and others added 12 commits September 11, 2026 06:45
Phase 0 не меняет поведение: документ фиксирует базовый commit, чистую
сборку, состав и результат полного набора тестов (33/33 CTest + 84/84
strategy fidelity), текущую версию upstream (1.10.1) и точную природу
дефекта семантики версий RC/stable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Дефект: исходная версия была единственным числом 1.0.0, поэтому сборка по
тегу v1.0.0-rc.3 сообщала о себе «1.0.0», и будущий стабильный 1.0.0
оценивался установленным RC как Current, а не Upgrade.

Модель версии (cmake/Version.cmake) теперь единственный источник истины и
хранит четыре поля: major/minor/patch и prerelease. Из них выводятся
семантическая версия (1.0.0-rc.3), числовая PE-версия (1.0.0.3), признак
предварительного выпуска и канал обновлений. Решения об обновлении
принимаются только по семантической версии; PE-версия информационная.

Сравнение версий не ослаблено, а уточнено: prerelease сравнивается по
правилам precedence SemVer 2.0.0 (rc.2 < rc.10), ведущие нули и пустые
идентификаторы отклоняются как псевдонимы, ревизия upstream (1.10.1a)
теперь корректно считается новее базового выпуска, а не понижением.

Канал перестал быть константой true: стабильная сборка по-прежнему
отклоняет любые предварительные версии, RC-сборка может перейти на новый
RC или на стабильный выпуск.

Пороги совместимости в манифесте вынесены в параметр со значением
1.0.0-rc.1, иначе установленный 1.0.0-rc.N получал бы LauncherTooOld при
переходе на стабильный 1.0.0.

Шлюз выпуска: тег обязан точно совпадать с исходной версией и каналом,
поэтому стабильный тег не может опубликовать RC-семантику и наоборот.

Тесты: новый набор version_model (34-й тест CTest) и расширенные
updateversion/updatemanifest/release_metadata. Порядок версий задан одной
канонической таблицей tests/version-order-cases.json, которую независимо
проверяют реализация на C++ и реализация на PowerShell.

Проверено: чистая сборка без предупреждений, CTest 34/34, strategy
fidelity 84/84.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Импорт выполнен штатным защищённым импортёром, файлы вручную не
копировались. Provenance указывает на неизменяемый выпуск 1.10.2,
archive_sha256=5eaac9fb...62d5, upstream_commit=dfd8e613...4dec.

Состав: добавлена стратегия ALT13 (21 -> 22), изменена EXP, добавлен
ресурс tls_clienthello_sochi_park.bin, ресурс quic_initial_4pda.to.bin
переименован в quic_initial_4pda_to.bin, удалён quic_initial_dbankcloud_ru.bin,
обновлены списки. Проверено, что удалённые и переименованные ресурсы не
остались во встроенной нагрузке после чистой переконфигурации.

Расширение грамматики строгого парсера не потребовалось: ALT13 использует
уже поддерживаемый синтаксис.

Попутно исправлен дефект самого импортёра: изолированное дерево проверки
собиралось без docs и SECURITY.md, которых требует validate-licenses.ps1,
поэтому полный импорт не мог завершиться успешно ни для какой версии
upstream и падал на отсутствующем файле репозитория, а не на нагрузке.
Список копирования дополнен, добавлен регрессионный тест
importer_validation_tree: он извлекает список из исходника импортёра,
собирает по нему дерево и прогоняет против него релизные шлюзы без сети.

Проверено: чистая сборка без предупреждений, CTest 35/35, strategy
fidelity 88/88 (22 стратегии x 4 режима), UPSTREAM_FIDELITY PASS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Стабильный тег vX.Y.Z больше не может быть опубликован без подписи.
Требование выводится из авторитетной модели версии, а не из отдельного
флага: канал prerelease сохраняет прежнее исключение, стабильный канал
исключения не имеет.

Политика вынесена в scripts/authenticode.ps1 чистой функцией и
отклоняет всё, кроме статуса Valid: NotSigned, HashMismatch (файл изменён
после подписи), недоверенную цепочку, просроченный сертификат и
отсутствие метки времени, когда метка настроена. Промежуточного состояния
«предупредить и продолжить» в политике нет.

Подпись проверяется дважды и разными средствами: signtool сразу после
подписи и независимо через API подписи Windows перед публикацией
(scripts/verify-authenticode.ps1).

Опубликованные байты привязаны к подписанным: SHA-256 подписанного файла
фиксируется до упаковки, после упаковки требуется точное совпадение, а
перед самой публикацией сверяются SHA-256 публикуемого файла,
launcher.sha256 в подписанном манифесте и файл CHEBURNET.exe.sha256.
Метаданные генерируются только после подписи, порядок закреплён
регрессией по тексту workflow.

Тесты: новый release_signing покрывает отсутствующий сертификат,
недоверенный самоподписанный сертификат, файл, изменённый после подписи,
непрошедшую итоговую проверку, отказ стабильного тега до сборки
артефактов и сохранение исключения для prerelease. Положительный путь
проверяется на реальной доверенной подписи Microsoft, потому что менять
хранилища доверия машины релизная оснастка не вправе.

Попутно: новый тест importer_validation_tree сразу поймал, что дерево
проверки импортёра не содержит .github, который теперь читает
security_regression; список копирования дополнен.

Проверено: чистая сборка без предупреждений, CTest 36/36, strategy
fidelity 88/88, реальный прогон prepare-release на RC-канале даёт
корректные метаданные (launcher.version=1.0.0-rc.3).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Проверка при запуске выполнялась синхронно перед подключением, поэтому
недоступный DNS, прокси или сеть выглядели как зависание программы и
задерживали подключение на время тайм-аутов.

Добавлен UpdateCheckService: одна фоновая проверка, результат забирается
из цикла главного меню, который и так опрашивает клавиатуру каждые
120-250 мс. Подключение больше не ждёт сетевого ввода-вывода вообще.

Модель доверия не изменена: фоновый поток вызывает тот же
UpdateManager::CheckNow, то есть независимое получение манифеста и
отдельной подписи, проверка точных байтов и оценка версий совпадают с
синхронным путём. CheckNow получил только необязательный флаг отмены,
который передаётся в WinHTTP и влияет исключительно на транспорт.

Безопасность времени жизни: поток никогда не отсоединяется, Cancel()
идемпотентен и присоединяет его, деструктор вызывает Cancel(), сервис
объявлен после ui_/pm_ и потому разрушается раньше них. Мьютекс не
удерживается ни во время сетевого ввода-вывода, ни во время join.
Фоновый код ничего не рисует и не трогает интерфейс. Исключение в
проверке вырождается в нефатальный результат «нет сети».

Явная проверка из меню сначала отменяет и присоединяет фоновую, чтобы
два запроса не конкурировали за кэш проверенного манифеста.

Тесты: updatecheckservice покрывает успех, офлайн, отмену, разрушение
во время незавершённой проверки, результат, пришедший после смены
экрана, исключение в проверке и повторный запуск. Тест детерминирован —
фиктивная проверка ждёт флаг, а не спит; 40 прогонов подряд без сбоев.

Проверено: чистая сборка без предупреждений, CTest 37/37, strategy
fidelity 88/88.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Запланированный процесс раньше печатал результат проверки в журнал запуска
и не оставлял ничего, с чем можно работать. Теперь он состоит из трёх
заданий с раздельными правами.

detect (contents: read) сравнивает встроенную нагрузку с последним
неизменяемым стабильным выпуском и выдаёт машиночитаемый отчёт.
validate (contents: read) выполняет полный защищённый импорт в одноразовой
рабочей копии, собирает, прогоняет весь набор тестов и проверяет
побайтовую достоверность; различия и отчёт уходят в артефакты запуска.
task (issues: write) создаёт либо обновляет ровно одну задачу на версию.

Задача содержит старую и новую версию, ссылку на выпуск, SHA-256 и размер
архива, commit upstream, число стратегий до и после, списки добавленных,
удалённых и изменённых стратегий и файлов нагрузки, результат проверки,
ссылку на запуск и команды для локального воспроизведения.

Дедупликация идёт по скрытому маркеру cheburnet-upstream:<версия> в теле
задачи, а не по заголовку: заголовок человек может отредактировать,
маркер нет. Поиск охватывает задачи в любом состоянии, поэтому повторный
запуск не создаёт дубликат.

Выбран вариант с задачей, а не с черновым pull request, потому что ветка
требует contents: write у запланированной автоматики, обрабатывающей
содержимое стороннего выпуска. Импортёр детерминирован и сверяет архив с
неизменяемым digest релизного ассета, поэтому локальный запуск
воспроизводит ровно то, что проверил CI, и право записи не добавляет
гарантий. Решение и требования для перехода на PR зафиксированы в
docs/production/UPSTREAM_AUTOMATION.md.

Дельта нагрузки вычисляется из фактического вывода git status
(scripts/upstream-delta.ps1) и вынесена в отдельную библиотеку, чтобы
разбор переименований и закавыченных путей проверялся офлайн.

Тесты: новый upstream_automation покрывает разбор дельты, обязательные
поля отчёта, отказ от черновых/предварительных/изменяемых выпусков,
отсутствие contents: write и pull-requests: write, наличие issues: write,
запуск защищённого импортёра, дедупликацию по маркеру, отсутствие
автослияния и закрепление всех сторонних actions по commit SHA.
SHA для actions/download-artifact сверен с GitHub API, а не подобран.

Проверено: чистая сборка без предупреждений, CTest 38/38, YAML всех
процессов разбирается.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Модульные тесты проверяют логику, но не проверяют, что на чистой машине
создаётся защищённое дерево с нужными ACL, распаковывается рабочая среда,
прерванное обновление не повышается автоматически и повреждённая среда
откатывается.

Оснастка scripts/e2e/Invoke-CheburnetE2E.ps1 использует то, что вся работа
запуска выполняется в конструкторе App до появления интерфейса: сценарии
управляются запуском настоящего исполняемого файла и проверкой
наблюдаемого состояния, а не имитацией нажатий. Пакетный движок не
запускается и драйвер WinDivert не загружается ни в одном сценарии.

Сценарии: отказ запуска без прав администратора, первая установка,
повторный запуск с существующей конфигурацией, прерванное обновление,
откат при повреждённой активной среде и fail-closed при подложенном
junction в защищённом каталоге журналов. Каждый сценарий привязан к
критерию приёмки.

Привилегированные сценарии изменяют машину, поэтому оснастка отказывается
работать, если машина не помечена файлом-маркером как одноразовая, если
сессия не элевирована или если уже запущен чужой winws.exe.

Самопроверка -SelfTest не требует прав, ничего не пишет вне временного
каталога и входит в CTest как e2e_harness: проверяет состав сценариев,
их привязку к критериям, отказ защиты окружения и схему доказательств.

scripts/e2e/New-CheburnetTestVm.ps1 воспроизводимо создаёт VM Hyper-V
поколения 2 с Secure Boot и виртуальным TPM, подготовительный диск с
артефактами выпуска и скрипт запуска в госте, сверяющий SHA-256 лончера с
опубликованным. Выбор редакции Windows и сама установка остаются ручными
осознанно: универсальный unattend.xml нельзя зафиксировать, а угадывание
молча дало бы не то окружение.

На машине разработки сценарий отказа запуска без прав выполнен и пройден;
остальные намеренно пропущены защитой окружения. Проверка на чистых VM
Windows 10 и Windows 11 не выполнена и зафиксирована как явный блокер
стабильного выпуска в docs/production/E2E_RELEASE_GATE.md.

Проверено: чистая сборка без предупреждений, CTest 39/39.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Статический анализ MSVC включается опцией CHEBURNET_ENABLE_ANALYZE и
работает с /WX, поэтому новая находка останавливает сборку. Все находки
первого прогона разобраны по существу, а не подавлены:

C6387 в ProcessManager: размер списка атрибутов процесса брался из
вызова определения размера без проверки. При неожиданном отказе размер
остался бы нулевым, выделение — нулевой длины, а последующие вызовы
писали бы через указатель без хранилища, причём в привилегированном пути
создания дочернего процесса. Добавлена проверка диапазона и указателя,
путь fail-closed.

C6262 в WinHttpClient: буфер передачи 64 КиБ лежал в автоматической
памяти одного кадра, а эта функция теперь исполняется ещё и в фоновом
потоке проверки обновлений. Буфер перенесён в динамическую память.

C6326 в Launcher и UiScreens: непустота встроенного resource manifest
проверялась во время исполнения, хотя это свойство генерации. Проверка
вынесена в static_assert, экран состояния показывает фактическое число
записей.

Подавлено ровно одно предупреждение — C28285, дефект SAL-аннотации
CryptStringToBinaryA в самом wincrypt.h Windows SDK. Обоснование
зафиксировано рядом с подавлением.

Добавлен процесс CI static-analysis: задание MSVC /analyze и задание
CodeQL (c-cpp, security-and-quality) с security-events: write только у
него. SHA обоих сторонних actions сверены с GitHub API.

Фаззинг: тест fuzzparsers мутирует корректные входные данные
детерминированным генератором с фиксированным зерном и проверяет, что
принятое значение удовлетворяет инвариантам разборщика, а не только что
разборщик не упал. Цели: разбор версии, манифест обновления, строгий
JSON, путь внутри пакета, имя файла в защищённой области. Зерно и число
итераций переопределяются переменными окружения для длинных кампаний;
выполнено 500 000 итераций на зерне по умолчанию и по 50 000 на трёх
других зёрнах без находок.

Фаззер нашёл ошибку в предикате самого теста: обходом пути считалось
".." в любом месте строки, тогда как обходом он является только как целый
компонент. Предикат исправлен, добавлены явные враждебные формы путей.

Внесение отказов: тест faultinjection проходит полную решётку точек
отказа транзакционной активации и проверяет семь инвариантов, включая то,
что кандидат становится текущим только при полной фиксации, что процесс
не останавливается до одобрения предварительной проверкой и что откат не
запускает кандидата повторно.

Проверено: чистая сборка без предупреждений, сборка с /analyze без
находок, CTest 41/41, strategy fidelity 88/88.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Пункт меню «Экспорт диагностики» создаёт локальный архив
CHEBURNET-diagnostics-ГГГГММДД-ЧЧММСС.zip в каталоге журналов. Перед
записью показывается полный состав архива и точный путь, затем требуется
отдельное подтверждение. Отправки нет: в коде экспорта отсутствуют
сетевые вызовы, и это закреплено регрессионной проверкой по именам
сетевых API.

В архив входят manifest.json с описанием самого состава, summary.json с
версиями, происхождением движка, состоянием рабочей среды и целостности,
настройками, сборкой Windows, вердиктом подписи лончера и ожидаемыми
SHA-256 файлов CHEBURNET, а также ограниченный хвост журнала.

Редактирование скрывает имя пользователя, имя компьютера, путь профиля,
любой путь внутри \Users\, адреса IPv4 и IPv6 и строки, похожие на
токены. Хеши SHA-256 и SHA-1 сохраняются намеренно: без них диагностика
теряет смысл, а о пользователе они ничего не сообщают.

ZIP пишется без сжатия собственным кодом: сторонняя библиотека ради одной
операции не вводится, формат детерминирован. Файл создаётся с CREATE_NEW,
поэтому существующий архив никогда не перезаписывается молча, при ошибке
записи частичный файл удаляется, а имена записей проверяются до записи.

Тест diagnostics проверяет редактирование (включая регистронезависимость
и идемпотентность), сохранение версий и хешей, CRC-32 по каноническому
вектору, структуру архива, отказ перезаписи, отказ на враждебных именах
записей и работу с путями, содержащими пробелы и кириллицу.

Найдено при разработке: символ '=' в наборе символов токена склеивал пару
"ключ=значение" в один токен, из-за чего редактировался и сам SHA-256.
Исправлено: дополнение base64 обрабатывается только в хвосте прогона.

Статический анализ на новом модуле выдал C28020 на std::array::operator[]
с беззнаковым счётчиком; перебор заменён на range-for, индексирования
больше нет. Сборка с /analyze снова чиста.

Высота меню теперь выводится из фактического числа пунктов, а не из
зашитой константы.

Проверено: чистая сборка без предупреждений, сборка с /analyze без
находок, CTest 42/42, strategy fidelity 88/88.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Самая критичная часть запуска — обработка прерванного обновления и
реакция на нарушенную целостность активной среды — жила внутри
конструктора App вперемешку с работой с процессами, файловой системой и
ProgramData. Проверить её можно было только запуском всей программы с
правами администратора, поэтому ни одна ветка не была покрыта тестом.

Решения вынесены в src/app/RuntimeRecovery.* чистыми функциями от
наблюдаемого состояния. Поведение не изменено: App по-прежнему выполняет
остановку, откат и запись состояния, но сначала спрашивает, что именно
нужно сделать.

Тест runtimerecovery проходит всю таблицу переходов: отсутствие
ожидающего состояния, недоверенная запись процесса, работающая ожидающая
среда, работающая текущая среда, посторонняя среда, регистронезависимость
сравнения путей и путь с общим префиксом. Отдельно проверено, что
ожидающая версия никогда не повышается до текущей и что откат возможен
только при записанной предыдущей рабочей версии.

Рефакторинг намеренно узкий: экраны интерфейса и менеджер обновлений не
трогались, чтобы не создавать риск перед стабильным выпуском.

Проверено: чистая сборка без предупреждений, сборка с /analyze без
находок, CTest 43/43, strategy fidelity 88/88.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Добавлена docs/production/FINAL_RELEASE_CERTIFICATION.md с фактическими
результатами всех выполненных проверок, метаданными лончера и нагрузки и
перечнем оставшихся блокеров с точным следующим действием по каждому.

Статус выпуска: RELEASE_CANDIDATE_READY_WITH_BLOCKERS. Два обязательных
шлюза не выполнены и не могут быть выполнены в этом окружении: проверка в
чистом окружении Windows 10 и Windows 11 требует одноразовой машины и
загрузки драйвера ядра, фактическая подпись требует сертификата
подписи кода. Заявлять готовность при непроверенных обязательных шлюзах
было бы неверно.

Добавлена docs/architecture/PRIVILEGE_SEPARATION_V2.md — проектная
записка о разделении привилегий. Только исследование, в состав 1.0 не
входит: описаны целевая структура, узкий набор команд IPC без
универсального выполнения, честная оценка того, что привилегированный
разбор пакета разделение не убирает, цена решения и условия, при которых
работа оправдана.

CHANGELOG дополнен разделом подготовки стабильного выпуска,
docs/CURRENT_BEHAVIOR.md переведён на 1.10.2, каталоги dist-* добавлены
в .gitignore.

Проверено: CTest 43/43, strategy fidelity 88/88.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
docs/ARCHITECTURE.md: добавлены UpdateCheckService, RuntimeRecovery и
Diagnostics, раздел о единственном источнике версии и ссылки на документы
проверки выпуска.

docs/SMOKE_TEST.md: ручной список теперь начинается с указания, что часть
сценариев автоматизирована релизным шлюзом E2E, и дополнен проверкой
экспорта диагностики — показ состава до записи, отсутствие в архиве имени
пользователя, имени компьютера, путей профиля, адресов и токенов, отказ
перезаписи существующего архива и отсутствие сетевого обращения.
Нумерация пунктов приведена в порядок.

Проверено: CTest 43/43, strategy fidelity 88/88, LICENSE_VALIDATION PASS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-advanced-security

Copy link
Copy Markdown

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

Comment thread src/app/Diagnostics.cpp
return L"CHEBURNET-diagnostics-" + LocalTimestamp() + L".zip";
}

Bundle BuildBundle(const RuntimePaths& paths, const Config& config,
Comment thread tests/test_main.cpp
Comment on lines +1460 to +1621
static int test_fuzzparsers() {
g_fail = 0;
const int kIterations = static_cast<int>(FuzzEnv("CHEBURNET_FUZZ_ITERATIONS", 4000));
const std::uint64_t fuzzSeed = FuzzEnv("CHEBURNET_FUZZ_SEED", 0xC4EB2026ull);
std::printf(" fuzz seed=%llu iterations=%d\n", static_cast<unsigned long long>(fuzzSeed),
kIterations);
Prng prng(fuzzSeed);

// ---- version parser ----------------------------------------------------
for (int i = 0; i < kIterations; ++i) {
const std::string seed = kVersionSeeds[prng.Below(std::size(kVersionSeeds))];
const std::string mutated = MutateBytes(seed, prng);
const auto parsed = update::ParseVersion(mutated);
if (!parsed) continue;
// Accepted: the invariants the comparator relies on must hold.
if (parsed->parts.empty() || parsed->parts.size() > 8) {
++g_fail;
std::printf(" FAIL version invariant (parts) for '%s'\n", mutated.c_str());
continue;
}
if (parsed->prerelease && parsed->prereleaseIds.empty()) {
++g_fail;
std::printf(" FAIL version invariant (prerelease ids) for '%s'\n", mutated.c_str());
continue;
}
if (parsed->revision.size() > 1) {
++g_fail;
std::printf(" FAIL version invariant (revision) for '%s'\n", mutated.c_str());
continue;
}
// Comparison must be a strict, antisymmetric, reflexive order.
if (update::CompareVersions(*parsed, *parsed) != 0) {
++g_fail;
std::printf(" FAIL version not reflexive for '%s'\n", mutated.c_str());
continue;
}
const auto other = update::ParseVersion(kVersionSeeds[prng.Below(std::size(kVersionSeeds))]);
if (other) {
const int forward = update::CompareVersions(*parsed, *other);
const int reverse = update::CompareVersions(*other, *parsed);
if ((forward == 0) != (reverse == 0) || (forward < 0) != (reverse > 0)) {
++g_fail;
std::printf(" FAIL version not antisymmetric for '%s'\n", mutated.c_str());
}
}
}

// ---- update manifest ---------------------------------------------------
const std::string manifestSeed = ValidManifest();
for (int i = 0; i < kIterations; ++i) {
const std::string mutated = MutateBytes(manifestSeed, prng);
const update::ManifestResult parsed = update::ParseManifest(mutated);
if (!parsed.ok) continue;
const update::Manifest& manifest = parsed.manifest;
// Accepted manifests must still satisfy every contract the updater
// relies on before it will touch the network or the filesystem.
const bool contract =
manifest.schema == 1 && manifest.channel == "stable" &&
manifest.launcher.sha256.size() == 64 && manifest.payload.sha256.size() == 64 &&
manifest.launcher.size > 0 && manifest.payload.size > 0 &&
update::IsHttpsUrl(manifest.launcher.url) &&
update::IsHttpsUrl(manifest.payload.url) &&
update::IsHttpsUrl(manifest.payload.upstreamReleaseUrl) &&
manifest.payload.provider == "Flowseal/zapret-discord-youtube" &&
update::ParseVersion(manifest.launcher.version).has_value() &&
update::ParseVersion(manifest.payload.version).has_value() &&
!manifest.keyId.empty();
if (!contract) {
++g_fail;
std::printf(" FAIL manifest accepted while violating its contract (iteration %d)\n", i);
}
}

// ---- strict JSON -------------------------------------------------------
const std::string jsonSeed =
"{\"a\":1,\"b\":[true,false,null,1.5e3],\"c\":{\"d\":\"\\u0416\"}}";
for (int i = 0; i < kIterations; ++i) {
const std::string mutated = MutateBytes(jsonSeed, prng);
json::ParseOptions options;
options.maxBytes = 4096;
options.maxDepth = 8;
options.maxValues = 128;
const json::ParseResult parsed = json::Parse(mutated, options);
if (parsed.ok && mutated.size() > options.maxBytes) {
++g_fail;
std::printf(" FAIL json accepted an oversized document (iteration %d)\n", i);
}
}

// ---- package path allowlist -------------------------------------------
for (int i = 0; i < kIterations; ++i) {
const std::string seed = kPackagePathSeeds[prng.Below(std::size(kPackagePathSeeds))];
const std::string mutated = MutateBytes(seed, prng);
std::string normalized;
if (!update::NormalizePackagePath(mutated, normalized)) continue;
// Accepted: the normalized path must be inside the exact allowlist,
// relative and free of traversal. Note that ".." only means traversal
// as a COMPLETE component: "WinDivert64.s..ys" is an ordinary name.
const bool allowed = normalized.rfind("bin/", 0) == 0 ||
normalized.rfind("lists/", 0) == 0 ||
normalized == "strategies/catalog.json" ||
normalized == "provenance.json" ||
normalized == "runtime-manifest.json";
bool traversal = false;
std::size_t at = 0;
while (at <= normalized.size()) {
const std::size_t slash = normalized.find('/', at);
const std::string_view component = std::string_view(normalized).substr(
at, slash == std::string::npos ? std::string_view::npos : slash - at);
if (component.empty() || component == "." || component == "..") traversal = true;
if (slash == std::string::npos) break;
at = slash + 1;
}
const bool safe = allowed && !traversal &&
normalized.find('\\') == std::string::npos &&
normalized.find(':') == std::string::npos &&
normalized.find('\0') == std::string::npos &&
!normalized.empty() && normalized.front() != '/';
if (!safe) {
++g_fail;
std::printf(" FAIL package path accepted unsafe normalization: '%s'\n",
normalized.c_str());
}
}

// Explicit traversal and device-name forms must always be rejected.
for (const char* hostile : {"../bin/winws.exe", "bin/../../evil", "bin/./winws.exe",
"/bin/winws.exe", "bin\\winws.exe", "C:/bin/winws.exe",
"bin/", "", "bin/con.exe", "bin/winws.exe ", "bin/winws.exe.",
"bin/..", "bin/nul", "lists/../../../etc/passwd"}) {
std::string normalized;
if (update::NormalizePackagePath(hostile, normalized)) {
++g_fail;
std::printf(" FAIL package path accepted a hostile input: '%s'\n", hostile);
}
}

// ---- staging file names ------------------------------------------------
const wchar_t* const stagingSeeds[] = {L"CHEBURNET-new.exe", L"engine-1.10.2.cbpkg"};
for (int i = 0; i < kIterations; ++i) {
std::wstring seed = stagingSeeds[prng.Below(std::size(stagingSeeds))];
switch (prng.Below(5)) {
case 0: seed[prng.Below(seed.size())] = static_cast<wchar_t>(prng.Next() & 0xFFFF); break;
case 1: seed.insert(prng.Below(seed.size()), 1, L'\\'); break;
case 2: seed.insert(prng.Below(seed.size()), 1, L'.'); break;
case 3: seed.resize(prng.Below(seed.size() + 1)); break;
case 4: seed.insert(prng.Below(seed.size() + 1), 1, L' '); break;
default: break;
}
if (!update::IsSafeStagingFileName(seed)) continue;
const bool safe = !seed.empty() && seed.size() <= 128 && seed.front() != L'.' &&
seed.back() != L'.' && seed.back() != L' ' &&
seed.find(L'\\') == std::wstring::npos &&
seed.find(L'/') == std::wstring::npos &&
seed.find(L':') == std::wstring::npos;
if (!safe) {
++g_fail;
std::printf(" FAIL staging name accepted an unsafe value\n");
}
}
return g_fail;
}
Два отказа CI — обе ошибки мои.

1. Политика Authenticode отвергала просроченный сертификат даже при
   наличии контрподписи метки времени. Это неверно по существу:
   контрподпись доказывает, что файл был подписан, пока сертификат
   действовал, Windows проверяет цепочку на момент метки и сообщает об
   этом статусом Valid. Моя проверка отвергала бы собственные выпуски
   CHEBURNET в день истечения сертификата подписи, прямо противореча
   документу RELEASE_SIGNING.md, где метка названа средством сохранения
   проверяемости после истечения.

   Обнаружено в CI: системный бинарный файл Windows на раннере подписан
   сертификатом, истёкшим в 2024 году, но его подпись по-прежнему Valid
   благодаря метке времени.

   Теперь окно действия сертификата сверяется с текущим временем только
   для подписи БЕЗ метки времени, где нет доказательства момента
   подписания. Добавлены три регрессии: просроченный сертификат с меткой
   принимается, без метки отвергается, ещё не действующий без метки
   отвергается.

2. Кириллица внутри блоков run у шагов с shell: powershell. Раннер
   записывает блок во временный .ps1 без BOM, а Windows PowerShell 5.1
   читает такой файл как ANSI, поэтому текст ломал разбор и шаг падал до
   запуска чего-либо. Это тот же класс ошибки, который уже был закрыт
   внутри файлов репозитория, но пропущен в workflow.

   Сообщения в inline-PowerShell переведены на ASCII в static-analysis.yml
   и upstream-check.yml. Блоки с shell: bash не тронуты: там UTF-8
   читается корректно.

   Добавлен шлюз workflow_encoding: он разбирает все процессы, отличает
   шаги powershell от bash и запрещает не-ASCII только в первых. Проверено,
   что тест действительно падает на внесённом нарушении и что он не
   проходит вхолостую, если разбор шагов перестанет работать.

Проверено: чистая сборка без предупреждений, CTest 44/44, strategy
fidelity 88/88.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Jacksony100

Copy link
Copy Markdown
Owner Author

Изменения влиты в main напрямую линейной историей (fast-forward, 78cc980..a35bbc7), все 14 коммитов сохранены отдельно. Перед вливанием из сообщений коммитов удалены служебные трейлеры соавторства, поэтому SHA отличаются от показанных в этом PR. Рабочая ветка переименована в release/production-hardening-v1; ветка этого PR удалена как устаревшая.

@Jacksony100
Jacksony100 deleted the claude/production-hardening-v1 branch September 11, 2026 09:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants