Ошибка notarytool при нотарификации macOS: чек-лист

Сначала сохраните Submission ID и скачайте JSON-лог, а уже затем исправляйте пакет и отправляйте новую версию. При ошибке notarytool нельзя смешивать четыре разных события: загрузку архива, проверку Apple, прикрепление ticket и локальную проверку Gatekeeper. Такой порядок подходит вам, если вы распространяете приложение через сайт, DMG, PKG или ZIP и хотите повторяемо выполнять публикацию на локальном либо удалённом Mac. (документация Apple по workflow нотарификации)

Эта инструкция предназначена для трёх групп:

  • для независимых разработчиков macOS-приложений, которые распространяют DMG, PKG или ZIP напрямую;
  • для владельцев скриптов и CI, где нотарификация периодически возвращает Invalid или долго остаётся в промежуточном состоянии;
  • для небольших команд, которые переносят подпись, Keychain и публикацию на удалённый Mac и хотят проверить среду до первой реальной поставки.

01 Сначала определите слой, на котором произошёл сбой

Нотарификация — не то же самое, что code signing, а Gatekeeper — не то же самое, что сервис нотарификации. Подпись подтверждает целостность объекта и личность разработчика. Apple Notary Service автоматически сканирует отправленный объект и возвращает результат. Затем ticket может быть найден онлайн или прикреплён к распространяемому файлу. При запуске Gatekeeper проверяет цепочку доверия, подпись, ticket и состояние объекта на конкретном Mac. (описание процесса нотарификации Apple)

Соберите минимальный набор доказательств:

  1. результат команды notarytool submit;
  2. Submission ID;
  3. JSON-лог через notarytool log;
  4. вывод codesign;
  5. вывод spctl;
  6. результат stapler validate, если ticket уже прикреплялся.

Пример с заполнителями:

xcrun notarytool submit "<ARCHIVE_PATH>" \
  --keychain-profile "<KEYCHAIN_PROFILE>" \
  --wait \
  --output-format json > "<SUBMIT_RESULT>.json"

После получения идентификатора не удаляйте его из лога сборки:

xcrun notarytool log "<SUBMISSION_ID>" \
  --keychain-profile "<KEYCHAIN_PROFILE>" \
  "<NOTARY_LOG>.json"

Apple прямо рекомендует сохранять идентификатор и использовать его для загрузки журнала. В JSON-логе находятся статус, краткое описание результата, пути к проблемным объектам, уровень ошибки и предупреждения. Лог стоит читать даже после успешной проверки: предупреждения могут указывать на будущую нестабильность публикации.

Результат Что он означает Следующее действие
Загрузка не завершилась Архив не был принят сервисом или возникла проблема соединения Проверить credentials, сеть, выбранный Xcode и повторить загрузку только после сохранения вывода команды
Объект обрабатывается Файл принят, но итоговой проверки ещё нет Не создавать новый Submission без причины; использовать сохранённый идентификатор и получить текущий статус
Invalid Сервис нашёл критическую проблему в содержимом, подписи или структуре Скачать JSON-лог и исправлять конкретный путь или entitlement
Accepted Объект прошёл автоматическую проверку Проверить предупреждения, выполнить stapling и локальную проверку
Accepted, но ticket не прикрепляется Нотарификация прошла, но локальная операция с контейнером или сетью не завершилась Проверить формат, доступ на запись, сеть и сам объект, к которому выполняется stapling

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

Важно. Не заменяйте JSON-лог коротким сообщением «notarization failed». Для исправления нужен как минимум путь к проблемному объекту, текст сообщения и уровень error или warning.

Как посмотреть конкретную причину после Invalid?
Сначала используйте Submission ID в notarytool log, затем откройте JSON и найдите массив issues. Не начинайте с чтения вывода Gatekeeper: он показывает результат локальной политики запуска, а не всегда первопричину отказа сервиса.

02 Сверьте Developer ID и полную цепочку подписей

Для распространения вне Mac App Store Apple требует подходящий сертификат Developer ID. Для приложения используется Developer ID Application, а для установочного пакета — Developer ID Installer. Сертификаты Mac Distribution, Apple Development, локальные и ad hoc identities не являются заменой Developer ID для такого сценария. (официальные сведения Apple о Developer ID)

Проверьте внешнее приложение:

codesign --display --verbose=4 "<APP_PATH>"
codesign --verify --deep --strict --verbose=4 "<APP_PATH>"

Затем проверьте identity:

codesign -dvvv "<APP_PATH>" 2>&1
security find-identity -v -p codesigning

Вместо просмотра только MyApp.app пройдите по содержимому:

  • Contents/Frameworks;
  • Contents/PlugIns;
  • XPC-сервисы;
  • встроенные command-line tools;
  • сторонние динамические библиотеки;
  • исполняемые файлы, которые приложение запускает самостоятельно.

Наружная подпись может выглядеть корректной, когда внутри лежит один бинарный файл с повреждённой подписью. Именно поэтому путь из JSON-лога важнее общего результата codesign --verify.

Ищите в журнале такие классы причин:

  • отсутствует приватный ключ, соответствующий сертификату;
  • сертификат не относится к Developer ID;
  • цепочка доверия на Mac неполная;
  • после подписи файл был изменён;
  • отсутствует secure timestamp;
  • пакет подписан не той identity;
  • installer подписан как приложение или приложение подписано как installer.

Apple отдельно указывает, что для Developer ID нужно включать secure timestamp. Для старых и новых сертификатов также важно не делать вывод о результате только по дате окончания действия: правила для уже подписанных приложений и для новых установочных пакетов различаются. Проверяйте актуальные сведения в официальных материалах Apple, а не по локальной догадке.

Объект Что проверяется Типичная ошибка в цепочке
.app Developer ID Application, целостность Mach-O, timestamp Использована development identity или изменённый бинарник
Framework Отдельная подпись и расположение внутри приложения Фреймворк переподписан после подписи приложения
Plug-in или XPC Подпись вложенного исполняемого объекта Внутри остался unsigned executable
.pkg Developer ID Installer и подпись установочного пакета PKG создан после подписи payload или подписан неподходящей identity
DMG Подписанный объект внутри и целостность образа Изменённое содержимое или образ без доступа на запись для stapler
ZIP Содержимое архива, а не возможность stapling к самому ZIP Ожидание, что ticket можно прикрепить непосредственно к архиву

03 Hardened Runtime и entitlements проверяйте как контракт

Включить Hardened Runtime недостаточно. Это обязательная основа для нотарификации, но каждое исключение должно соответствовать реальной функции приложения. Apple рекомендует включать только необходимые entitlements; shared libraries, frameworks и in-process plug-ins наследуют entitlements хост-процесса. (документация Apple по Hardened Runtime)

Проверьте фактические entitlements после подписи:

codesign -d --entitlements :- "<APP_PATH>"

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

Формат. Entitlements должны быть корректным XML property list. Бинарный plist, лишние символы или неправильный тип значения могут привести к отказу ещё до проверки функциональности.

Избыточность. Не добавляйте исключение «на всякий случай». Например, разрешения для JIT, unsigned executable memory или отключения library validation расширяют поверхность доверия. Если приложение не использует соответствующую возможность, entitlement лучше убрать.

Несоответствие после подписи. Если вы изменили entitlements, plist, исполняемый файл или вложенный объект после codesign, прежняя подпись уже не описывает фактическое содержимое. Переподписывать только внешнее .app в такой ситуации недостаточно.

Особое внимание уделите com.apple.security.get-task-allow. В пользовательской сборке это entitlement не должно оставаться включённым в значении true. Apple приводит его как одну из известных причин отказа при кастомном workflow.

Почему подписанное macOS-приложение всё ещё не проходит нотарификацию?
Потому что подпись и политика Hardened Runtime проверяются вместе. Приложение может иметь действительный Developer ID, но содержать запрещённый debug entitlement, неподходящее исключение, плохо сформированный plist или вложенный объект, подписанный до изменения его содержимого.

Если приложение загружает плагины, держите в уме ещё одну границу: плагин не объявляет собственные entitlements так же, как отдельное приложение. Хост должен предоставить разрешения, необходимые для фактической работы загружаемого кода. Это особенно важно для JIT, unsigned executable memory и library validation.

04 Вложенный код и формат контейнера требуют разных проверок

Нельзя применять одинаковую процедуру к DMG, PKG и ZIP. Apple принимает эти форматы для нотарификации, но объектом, который вы распространяете, является конкретный контейнер. Внутри DMG может находиться PKG и приложение; при стороннем установщике payload и сам установщик могут потребовать отдельных этапов нотарификации.

Для приложения начните с проверки подписи:

codesign --verify --deep --strict --verbose=4 "<APP_PATH>"
spctl --assess --type execute --verbose=4 "<APP_PATH>"

Для PKG:

pkgutil --check-signature "<PKG_PATH>"
spctl --assess --type install --verbose=4 "<PKG_PATH>"

Для DMG сначала проверьте образ:

hdiutil verify "<DMG_PATH>"

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

Для ZIP есть отдельное ограничение: ticket нельзя прикрепить непосредственно к ZIP. Нужно распаковать архив, выполнить stapling для объектов, которые были помещены в архив, а затем собрать новый ZIP. Поэтому успешная нотарификация ZIP и готовность ZIP к офлайн-проверке — разные состояния.

Одинакова ли диагностика для DMG, PKG и ZIP?
Первый слой одинаков: Submission ID, JSON-лог и проверка пути из issues. Второй слой различается. Для PKG важны Developer ID Installer и порядок подписи payload, для DMG — целостность образа и доступ на запись, для ZIP — содержимое после распаковки и повторная упаковка после stapling.

05 Credentials, сеть и stapler отделяйте от отказа сканирования

Ошибка доступа к Keychain не доказывает, что приложение отвергнуто. То же относится к проблеме сети, невозможности скачать лог или ошибке stapler. Эти операции выполняются на разных этапах.

Для повторяемого workflow не передавайте пароль Apple Account в открытом виде в скрипте. Apple документирует сохранение credentials через notarytool store-credentials и использование имени профиля через --keychain-profile. На удалённом Mac это означает, что нужно заранее проверить:

  • какой пользователь запускает скрипт;
  • какой Keychain доступен в неинтерактивной сессии;
  • разблокирован ли Keychain в момент сборки;
  • имеет ли процесс доступ к нужному item;
  • не очищается ли профиль после перезапуска машины;
  • где хранятся логи и кто может их прочитать.

Пример с заполнителями:

xcrun notarytool store-credentials "<KEYCHAIN_PROFILE>" \
  --apple-id "<APPLE_ID>" \
  --team-id "<TEAM_ID>" \
  --password "<APP_SPECIFIC_PASSWORD>"

Не вставляйте реальные Team ID, Key ID, Issuer ID, Apple ID, profile name и пути в публичный журнал. Для CI используйте отдельный профиль и ограничьте права процесса. Если команда работает через API вместо notarytool, приватный ключ также должен храниться вне исходного кода и не попадать в артефакты сборки. (техническая заметка Apple о переходе на современные инструменты нотарификации)

После Accepted выполните stapling:

xcrun stapler staple "<DISTRIBUTION_PATH>"
xcrun stapler validate "<DISTRIBUTION_PATH>"

Что делать, если нотарификация успешна, но stapler не прикрепляет ticket?
Проверьте, что вы обращаетесь к поддерживаемому объекту, что файл доступен на запись и что Mac может получить ticket по сети. Для ZIP stapling выполняется не к архиву, а к объектам внутри него. Ошибка stapler после Accepted не означает автоматически, что подпись приложения отклонена.

Если сеть ограничена корпоративным прокси или правилами дата-центра, проверяйте доступ именно с той машины, где работает stapler. Закрытый исходящий HTTPS может ломать workflow уже после успешной отправки.

06 Решение принимайте по условиям, а не по удобству

Используйте этот развилочный список перед тем, как переносить публикацию на постоянный Mac:

  • Если JSON-лог содержит конкретный путь и error — исправляйте подпись, entitlement или структуру именно этого объекта.
  • Если ошибка появляется до создания Submission ID — проверяйте credentials, выбранный Xcode, Keychain и сеть.
  • Если статус остаётся промежуточным — не объявляйте пакет отклонённым; сохраните идентификатор и получите актуальный статус.
  • Если Accepted получен, но stapler validate не проходит — проверяйте формат контейнера, доступ на запись и сетевой доступ.
  • Если codesign проходит, а spctl блокирует запуск — проверяйте Gatekeeper на чистом тестовом Mac и не делайте вывод только по машине разработчика.
  • Если проблема повторяется после каждой сборки — переносите не отдельную команду, а весь набор: сертификат, приватный ключ, Keychain profile, Xcode Command Line Tools, скрипты, логи и тестовый сценарий.
  • Если приложение требует физический интерфейс, локальную отладку или длительную тяжёлую сборку — удалённый Mac может быть неудобен; сначала оцените требования к доступу и постоянству среды.
  • Если Mac нужен только для выпуска релизов, подписи и нотарификации — постоянный удалённый хост обычно логичнее случайных ручных запусков на личном компьютере.

Нужна не просто «рабочая команда», а воспроизводимая цепочка:

  1. выбрать активный Xcode через xcode-select;
  2. собрать архив в чистом каталоге;
  3. экспортировать приложение;
  4. проверить все вложенные исполняемые объекты;
  5. выполнить подпись и проверку identity;
  6. создать DMG, PKG или ZIP;
  7. отправить реальный распространяемый файл через notarytool;
  8. сохранить Submission ID и JSON-лог;
  9. проверить Accepted и предупреждения;
  10. выполнить stapling там, где это поддерживается;
  11. проверить ticket через stapler validate;
  12. запустить spctl на отдельном тестовом Mac;
  13. сохранить обезличенные результаты как артефакт выпуска.

Готовый распространяемый продукт следует тестировать на другом Mac, по возможности в сценарии чистой установки и обновления. Это важно: машина разработчика может иметь локальные сертификаты, кэш, ранее разрешённые объекты и настройки, которых не будет у пользователя. (рекомендации Apple по упаковке macOS-приложений)

07 Когда удалённый Mac становится частью надёжной публикации

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

  • Mac выключается или переходит в недоступное состояние;
  • Keychain доступен только в интерактивной пользовательской сессии;
  • Xcode Command Line Tools меняются без фиксации версии;
  • сертификат есть без приватного ключа;
  • скрипт сохраняет только текст ошибки, но не Submission ID;
  • рабочий каталог очищается раньше, чем скачан лог;
  • сеть разрешает загрузку, но блокирует stapler;
  • подпись выполняется на одном объекте, а пользователю отправляется другой.

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

Если локальный Mac не может постоянно оставаться доступным, вы можете рассмотреть аренду удалённого Mac для задач публикации. Такой вариант не отменяет ваших обязанностей по защите Developer ID и credentials, но даёт постоянную macOS-среду для Xcode, Keychain, скриптов и повторной проверки.

После единичного исправления спросите себя не «прошла ли эта отправка», а «сможет ли команда повторить её через неделю без ручной настройки». Если нет, текущая схема уже имеет реальные недостатки: зависимость от личного компьютера, непостоянный Keychain, плавающий toolchain и потерю диагностических логов. В этом случае удалённый Mac от CALMVPS может быть практичнее случайного запуска на локальной машине — особенно для подписания, нотарификации и публикации релизов по расписанию. Перед выбором проверьте условия и доступные варианты аренды Mac, а затем проведите описанный выше приёмочный тест на вашем реальном DMG, PKG или ZIP.