ИНЖЕНЕРНАЯ ЗАМЕТКА

Предварительная проверка прав подписи iOS в облачном Mac

Предварительная проверка прав подписи iOS в облачном Mac

Предварительная проверка прав подписи iOS в облачном Mac

Архивация в Xcode может завершиться успешно, а при загрузке возникнет ошибка из-за несовпадения среды push-уведомлений, связанных доменов или идентификатора приложения. Обычно причина не в компиляции, а в рассогласовании трёх источников: прав, заявленных в проекте, прав, разрешённых профилем подготовки, и прав подписи, фактически записанных в приложение. Облачные Mac часто используются повторно через сценарии автоматизации, и один узел может обслуживать несколько веток и окружений. Поэтому недостаточно проверять файл .entitlements в репозитории — нужно анализировать непосредственно предназначенный для поставки архив .xcarchive.

Сначала определите объект проверки

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

ARCHIVE_PATH="$HOME/builds/MyApp.xcarchive"
APP_PATH="$ARCHIVE_PATH/Products/Applications/MyApp.app"

test -d "$APP_PATH"
test -f "$APP_PATH/embedded.mobileprovision"
codesign --verify --strict --verbose=2 "$APP_PATH"

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

find "$APP_PATH/PlugIns" -type d -name '*.appex' -print0 2>/dev/null |
while IFS= read -r -d '' item; do
  codesign --verify --strict --verbose=2 "$item"
done

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

Извлеките два фактических набора прав

Команда codesign позволяет получить entitlements из окончательной подписи, а security cms — декодировать встроенный в приложение профиль подготовки. Сохраните оба результата в формате plist, чтобы при последующем сравнении не зависеть от интерфейса Xcode.

WORK_DIR="$(mktemp -d)"
trap 'rm -rf "$WORK_DIR"' EXIT

codesign -d --entitlements :- "$APP_PATH" \
  > "$WORK_DIR/app-entitlements.plist" 2>/dev/null

security cms -D -i "$APP_PATH/embedded.mobileprovision" \
  > "$WORK_DIR/profile.plist"

plutil -lint "$WORK_DIR/app-entitlements.plist"
plutil -lint "$WORK_DIR/profile.plist"

Не сравнивайте приложение напрямую с полями верхнего уровня профиля подготовки. Разрешённые права находятся в словаре Entitlements; такие поля, как время создания и имя, полезны только для дополнительной диагностики и не отражают результат подписи.

Автоматически сравните ключевые поля

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

python3 - "$WORK_DIR/app-entitlements.plist" "$WORK_DIR/profile.plist" <<'PY'
import fnmatch
import plistlib
import sys

with open(sys.argv[1], "rb") as f:
    app = plistlib.load(f)

with open(sys.argv[2], "rb") as f:
    profile = plistlib.load(f)["Entitlements"]

keys = [
    "application-identifier",
    "com.apple.developer.team-identifier",
    "aps-environment",
    "com.apple.developer.associated-domains",
    "get-task-allow",
]

def allowed(actual, expected):
    if isinstance(actual, list) and isinstance(expected, list):
        return all(any(fnmatch.fnmatch(str(v), str(rule)) for rule in expected) for v in actual)
    if isinstance(actual, str) and isinstance(expected, str):
        return fnmatch.fnmatch(actual, expected)
    return actual == expected

failed = []
for key in keys:
    if key not in app:
        continue
    if key not in profile or not allowed(app[key], profile[key]):
        failed.append(key)

if failed:
    print("Entitlement mismatch:", ", ".join(failed))
    raise SystemExit(1)

print("Entitlement preflight passed")
PY

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

Определите источник настройки по симптомам

Симптом Что проверить в первую очередь Распространённая причина
Ошибка push-уведомлений aps-environment Окружение архива не соответствует назначению профиля подготовки
Не работают универсальные ссылки com.apple.developer.associated-domains Домены не попали в окончательную подпись или расширение настроено иначе
При загрузке возникает ошибка несовпадения идентификатора application-identifier Смешаны Bundle Identifier, префикс команды или конфигурации Target
Релизную сборку по-прежнему можно отлаживать get-task-allow Использована конфигурация сборки, не предназначенная для поставки
Не удаётся установить расширение Права основного приложения и .appex Несовместимы идентификатор расширения, группа приложений или набор профилей подготовки

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

Связанные домены и группы приложений представлены массивами. Порядок элементов обычно не имеет значения, поэтому следует проверять включение множеств. Логические поля нужно читать как настоящие логические значения: строку "false" нельзя считать значением false. Именно поэтому для проверки используется plistlib, а не grep.

Включите проверку в приёмку облачной сборки

Сохраните сценарий как Scripts/verify_entitlements.sh и запускайте его после успешного выполнения xcodebuild archive, но до экспорта или загрузки. Для каждого задания используйте отдельный каталог архива, а результаты проверки, сводку подписи приложения и список полей с ошибками сохраняйте как вложения сборки. В журнал записывайте только имена полей и обезличенные идентификаторы; не выводите содержимое закрытых ключей или полные учётные данные доступа.

Рекомендуется разделить приёмку на четыре этапа: проверка структуры пакета, извлечение прав подписи, декодирование профиля подготовки и сравнение с правилами. При сбое на любом этапе дальнейшая поставка должна быть остановлена, но .xcarchive следует сохранить для последующего анализа. В проектах с несколькими Target проверяйте основное приложение и каждое расширение отдельно — использовать для них результаты проверки основного приложения нельзя.

В завершение проведите ручную выборочную проверку: убедитесь, что текущее задание использует ожидаемые Scheme и Configuration, значение get-task-allow в производственном артефакте равно false, среда push-уведомлений соответствует цели поставки, среди связанных доменов нет тестовых адресов, а все расширения прошли независимую проверку подписи. Так ошибки, которые прежде обнаруживались только при загрузке, превращаются в воспроизводимые и отслеживаемые проверки на этапе сборки.

Часто задаваемые вопросы

Почему недостаточно проверить файл entitlements в проекте?

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

Какие права стоит автоматизировать в первую очередь?

Сначала проверяйте application-identifier, идентификатор команды, aps-environment, связанные домены и get-task-allow.

Выделенный физический узел

Запустите следующую сборку в облачном Mac.

Сравните три конфигурации Apple Silicon и выберите подходящий для текущего рабочего процесса регион среди пяти зарубежных узлов.

Выбрать тариф