# Воспроизводимые сборки для локальных клиентов агентов

Локальный клиент агента работает рядом с учётными данными и полномочиями, которые действительно важны. Если разработчик скачивает такой клиент, а затем даёт ему доступ к API-аккаунтам или SSH-назначениям, исполняемый файл выпуска заслуживает более внимательной проверки, чем скриншот зелёного задания CI. Вопрос должен быть конкретным: может ли кто-то связать этот точный исполняемый файл с просмотренным исходным кодом и процедурой сборки, которую можно повторить?

Воспроизводимые сборки отвечают лишь на часть этого вопроса. Они не делают вредоносный коммит безопасным и не превращают взломанную учётную запись разработчика в надёжную. Их задача, закрыть отдельный и распространённый разрыв между просмотренным исходным кодом и поставленной программой. В этом разрыве могут скрываться изменённый воркер сборки, подменённая зависимость или случайное локальное изменение.

Для локального клиента агента вроде Sallyport это важно, потому что приложение выполняет действия, пока учётные данные остаются за пределами процесса агента. Читатель должен иметь возможность изучить код, работающий на этой границе, и установить, какой исполняемый файл выпуска его содержит, а не просто довериться странице релиза.

## Подписанная загрузка не доказывает, что её собрали из просмотренного исходного кода

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

Команды часто смешивают эти утверждения, потому что оба вызывают успокаивающие диалоги в macOS. Gatekeeper и системные проверки подписи отвечают на вопросы о действительности цепочки подписи пакета и о том, не изменил ли кто-то его после подписания. Они не отвечают на вопросы о том, собрал ли подписант тег v1.2.3 без локальных исправлений, использовал ли CI вредоносную зависимость или не подменил ли загрузчик архив перед публикацией.

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

Документация Apple о подписи кода описывает подпись как печать на программном пакете. Это точное сравнение, но печать ничего не говорит о кухне, где пакет собирали. Считайте подпись кода гарантией целостности при распространении и подтверждением личности издателя. Независимая пересборка подтверждает соответствие исходного кода и бинарного файла.

Релиз может быть воспроизводимым и всё равно опасным. Если проверяющие приняли плохое изменение, чистая пересборка добросовестно создаст плохую программу. Не представляйте воспроизводимость заменой проверки кода, защищённых прав на выпуск или разумной схемы работы с учётными данными. Она убирает неопределённость при переходе от исходного кода к артефакту.

## Сформулируйте утверждение до сравнения байтов

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

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

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

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

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

Важно различать детерминированные сборки и проверяемые релизы. Сборка может быть детерминированной на одной машине, потому что незаметно читает локальный кэш, текущее время или настройку разработчика. Проверяемый релиз содержит достаточно свидетельств, чтобы другой человек получил те же входные данные и проверил утверждение. Второе требование заставляет команды раскрывать предположения, которые первое может скрыть.

Запишите утверждение в репозитории до первого запроса на публичную проверку. Если сопровождающие не могут сказать, происходит ли сравнение до или после подписания, внешние проверяющие не поймут, что означает несовпадение.

## Запись выпуска должна связывать исходный код, инструкцию и артефакт

Записи выпуска нужны подписанная ссылка на исходный код, неизменяемая инструкция и дайджесты файлов для скачивания. Если убрать любой элемент, цепочка свидетельств разрушится.

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

Минимальный манифест может оставаться обычным текстом и при этом содержать полезные свидетельства:

```text
release: 1.2.3
source_tag: v1.2.3
source_commit: 4f3c1b6e8a0d2c7f9b5e1d4a6c8e0f2b3d7a9c1e
build_recipe: docs/release-build.md@4f3c1b6e8a0d2c7f9b5e1d4a6c8e0f2b3d7a9c1e
xcode: 16.2
macos: 15.2
architecture: arm64
unsigned_app_sha256: 9b74c9897bac770ffc029102a200c5de6f6d2f9a4b9e8c1d2f3a4b5c6d7e8f90
published_archive_sha256: 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
```

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

Инструкция сборки должна описывать больше, чем компилятор. В ней следует указать выпуск Xcode, выбранный SDK, архитектуру, способ разрешения зависимостей, конфигурацию сборки, инструменты генерации кода, влияющие на результат переменные окружения и команду упаковки архива. Если выпуск требует закрытого плагина компилятора или бинарного файла, скачанного вручную, скажите об этом. Молчание превращает недостающий входной файл в тупик для любого внешнего проверяющего.

Разработчики могут проверить связь с исходным кодом обычными инструментами Git:

```sh
git fetch origin tag v1.2.3
git tag -v v1.2.3
git rev-list -n 1 v1.2.3
git show -s --format=%H v1.2.3
```

Первая команда показывает объект тега и сообщает, смог ли Git проверить его подпись с помощью локально доверенного открытого ключа. Последние две команды должны вывести тот же полный хеш коммита, который записан в манифесте. Успешной подписи неизвестным ключом недостаточно. Командам нужен документированный список отпечатков ключей подписи выпуска, который поддерживается под контролем версий, иначе доверие просто переместится в скрытое локальное хранилище ключей.

## Подпись macOS меняет байты после компиляции

Разработчикам macOS следует рассматривать компиляцию, подпись, нотарификацию и упаковку как отдельные этапы, потому что каждый из них может изменить артефакт.

Пакет приложения содержит больше одного исполняемого файла. В нём могут находиться основная программа, встроенные фреймворки, помощники входа в систему, помощники командной строки, метаданные, ресурсы и права. Инструмент codesign записывает подписи во всей этой структуре. Подпись после последнего копирования может изменить содержимое, а билет нотарификации позже может быть прикреплён к пакету или установщику. Инструмент создания образа диска тоже может записать временные метки и особенности структуры файловой системы.

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

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

Проверяйте подписанное приложение отдельно:

```sh
codesign -dv AppName.app 2\u003e\u00261
codesign -d --entitlements :- AppName.app 2\u003e/dev/null
spctl -a -vv AppName.app
```

Команда с правами выводит список прав, который будет оценивать macOS. Изучайте его как код, а не как декоративные данные. Неожиданное сетевое расширение, разрешение на автоматизацию, возможность отладки или изменённый идентификатор приложения могут иметь больше последствий, чем изменившийся значок. Команда проверки сообщает цепочку полномочий и результат оценки политики на конкретном Mac.

Приведённая команда содержит параметр с двумя дефисами только потому, что этого требует codesign. Это техническая особенность команды, а не обычной типографики текста. В собственной документации выпуска приводите команду без изменений и сохраняйте её вывод вместе с манифестом.

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

```sh
lipo -info AppName.app/Contents/MacOS/AppName
shasum -a 256 AppName.app/Contents/MacOS/AppName
```

Первая команда показывает архитектуры, например arm64 и x86_64. Вторая выводит строку с дайджестом SHA-256 и путём к файлу. Собирайте и сравнивайте каждую цель отдельно. Выпуск может иметь одинаковый срез arm64, тогда как срез x86_64 получен из другого набора инструментов или состояния исходного кода.

## Зависимости и пути сборки первыми нарушают воспроизводимость

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

Файлы блокировок зависимостей помогают, но сами по себе проблему не решают. Файл блокировок может записывать версию, но не дайджест архива. Менеджер пакетов может загрузить плагин компилятора, заново создать граф пакетов или обратиться к реестру во время сборки. Бинарная зависимость могла быть пересобрана под той же меткой версии. Если клиент агента использует генерацию кода, сам генератор становится входным файлом, который нужно версионировать и проверять.

Начните с того, что вынесите получение зависимостей в отдельную зафиксированную фазу. Сохраняйте контрольные суммы архивов пакетов, встроенных фреймворков, входных файлов для генерации кода и всех загружаемых инструментов. Для долгоживущих проектов храните внутренний кэш артефактов или архив выпуска с этими материалами. URL зависимости указывает место, но не является неизменяемой идентичностью.

Вторая частая проблема, это пути сборки. Отладочная информация может содержать абсолютные пути. Сгенерированные файлы могут включать имя пользователя, временный каталог, локаль, часовой пояс или текущую дату. Архиваторы могут сортировать файлы в порядке перечисления файловой системы. Компилятор может встраивать идентификатор сборки, основанный на случайных данных.

Задайте контролируемое окружение в документированной инструкции. Точные переменные зависят от языка и инструментов, но схема расследования остаётся прежней:

```sh
export TZ=UTC
export LANG=C
export LC_ALL=C
export SOURCE_DATE_EPOCH=1735689600
mkdir -p /tmp/release-build
cd /tmp/release-build
```

`SOURCE_DATE_EPOCH` является соглашением, описанным на reproducible-builds.org, и задаёт инструментам стабильную временную метку. Она работает только тогда, когда инструменты её поддерживают. Не добавляйте переменную в shell-скрипт в надежде, что этого достаточно. Докажите результат, собрав программу в разных каталогах и сравнив выходные файлы.

Я видел, как команды тратили дни на изучение разных хешей, хотя единственным различием был сгенерированный исходный файл с путём к рабочему каталогу. Они меняли флаги компилятора, пересобирали зависимости и в итоге находили путь в диагностической строке. Инструмент сравнения бинарных файлов показал бы это за несколько минут. Начинайте с различающихся байтов и прослеживайте их до входного файла. Догадки по скриптам сборки медленнее и намного менее надёжны.

## Соберите дважды и классифицируйте каждое различие

Две чистые сборки должны либо совпасть, либо дать небольшой и объяснимый набор различий. Считайте необъяснимые различия дефектами выпуска, даже если приложение работает нормально.

Запускайте инструкцию в отдельных рабочих каталогах и, если возможно, под разными учётными записями. Первая пара тестов выявляет загрязнение временем, путями и кэшем. Вторая машина с той же документированной версией ОС и Xcode показывает, зависит ли инструкция от случайного локального состояния. После подготовки зависимостей отключите сеть. Если сборка ломается без сети, в ней есть скрытая загрузка или недокументированный этап генерации.

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

Небольшая проверка shell делает первый результат однозначным:

```sh
shasum -a 256 build-a/AppName.app/Contents/MacOS/AppName
shasum -a 256 build-b/AppName.app/Contents/MacOS/AppName
cmp -s build-a/AppName.app/Contents/MacOS/AppName build-b/AppName.app/Contents/MacOS/AppName
printf '%s\\n' $?
```

Первые две строки должны вывести один и тот же дайджест. `cmp` возвращает ноль, если файлы совпадают, а последняя строка в этом случае выводит `0`. Ненулевой результат означает, что исполняемые файлы различаются. Не сводите его к зелёному или красному значку на панели. Сохраните оба файла и изучите различие на уровне байтов подходящим инструментом.

Разнесите каждое различие по одной из четырёх категорий: ожидаемые метаданные выпуска, утечка окружения, недетерминированный результат инструмента или неизвестная причина. Неизвестная категория должна оставаться видимой. Команды сталкиваются с проблемами, когда добавляют широкое исключение, например игнорируют все plist-файлы или все подписи, чтобы сравнение стало успешным. Исключения должны называть единственное ожидаемое поле и объяснять причину его изменения.

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

## Процедура проверки должна работать для скачанных релизов

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

Сначала проверьте дайджест архива по подписанному манифесту, полученному через независимый канал. В macOS уже доступна команда `shasum`:

```sh
shasum -a 256 AppName-1.2.3.dmg
```

Сравните напечатанный дайджест с манифестом посимвольно. Затем проверьте подключённое приложение с помощью `codesign` и `spctl`, чтобы подтвердить ожидающие полномочия подписи и принятие пакета macOS. Эти проверки защищают от повреждённой или подменённой загрузки, но пока не подтверждают соответствие исходному коду.

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

Обычный путь пользователя должен оставаться соразмерным риску. Большинство пользователей проверит подписанный манифест выпуска и подпись приложения. Сопровождающим, командам безопасности и независимым проверяющим стоит выполнять пересборку выбранных выпусков или делать это перед широким развёртыванием. Цель в том, чтобы глубокая проверка была возможной и привычной для тех, кто за неё отвечает.

Не путайте аудит действий с проверкой артефакта. Sallyport может офлайн проверить цепочку хешей зашифрованной истории аудита с помощью `sp audit verify`, то есть установить, не изменились ли записанные действия агента. Воспроизводимость выпуска отвечает на другой вопрос: соответствует ли установленный клиент заявленным сопровождающими исходному коду и инструкции.

## Происхождение сборки в CI, это свидетельство, а не замена пересборке

Сведения о происхождении в CI показывают, где работало задание сборки и какие заявленные входные данные оно использовало. Это помогает расследованию, но само по себе не устанавливает, что результат задания соответствует просмотренному исходному коду.

Модель происхождения SLSA проводит полезное различие между артефактом и утверждениями о том, как система сборки его создала. Подписанное утверждение может связать дайджест артефакта с описанием сборки и ревизией исходного кода. Это сильное свидетельство, если вы доверяете репозиторию, личности CI, изоляции исполнителя и правам на выпуск. Но доверять сборщику всё равно приходится.

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

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

CI также требует разделения обязанностей. Личность, способная менять исходный код, не должна автоматически получать доступ к ключу подписи выпуска. Задание, упаковывающее приложение, не должно незаметно получать секрет подписи до одобрения коммита выпуска проверяющими. Эти меры не делают бинарный файл воспроизводимым, но уменьшают вероятность того, что одна скомпрометированная учётная запись одновременно изменит исходный код, сборку и распространение.

## Включите проверку в договор выпуска

Команды получают воспроизводимые релизы, когда делают проверку ожидаемым результатом выпуска, а не исследовательским проектом, за который кто-то берётся после инцидента.

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

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

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