ENGINEERING NOTE

クラウドMacでiOS権限署名を事前検証する

クラウドMacでiOS権限署名を事前検証する

Xcode でアーカイブが成功していても、アップロード時にプッシュ通知環境、関連ドメイン、アプリ識別子の不一致で失敗することがあります。多くの場合、原因はコンパイルではなく、プロジェクトで宣言したエンタイトルメント、プロビジョニングプロファイルで許可されたエンタイトルメント、最終的に App の署名へ書き込まれたエンタイトルメントという3つの情報源が一致していないことです。クラウドMacはスクリプトから繰り返し利用されることが多く、同じノードで複数のブランチや環境を処理する場合もあります。そのため、リポジトリ内の .entitlements ファイルだけで判断せず、実際に配布する .xcarchive を直接検証する必要があります。

検証対象を先に確定する

以下の手順では、生成済みのアーカイブを入力として使用します。まずメイン App の場所を特定します。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"

App に拡張機能が含まれている場合は、各 .appex も個別に検証します。メイン App の検証が成功していても、通知サービス拡張、Widget、共有拡張で互換性のあるアプリ識別子とエンタイトルメントが使用されているとは限りません。

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

事前検証では、「プロジェクトの設定が正しく見えるか」ではなく、「これからアップロードする成果物」を判定基準にします。前者は入力にすぎず、後者が実際の結果です。

2つの実データからエンタイトルメントを抽出する

codesign を使うと最終署名に含まれる entitlements を取得でき、security cms を使うと App に埋め込まれたプロビジョニングプロファイルをデコードできます。両方を 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"

プロビジョニングプロファイルのトップレベルフィールドを App と直接比較しないでください。許可されたエンタイトルメントは Entitlements 辞書内にあります。作成日時や名前などのフィールドは調査の補助にはなりますが、署名結果を示すものではありません。

重要フィールドを自動比較する

以下のスクリプトは、発生頻度の高い5種類の問題を検査します。文字列では、プロビジョニングプロファイルに含まれる * のワイルドカード範囲を扱えます。配列形式のエンタイトルメントでは、App 側のすべての項目がプロビジョニングプロファイルで許可されている必要があります。スクリプトは問題があるとゼロ以外の終了ステータスを返すため、アーカイブ処理の直後に組み込めます。

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

このスクリプトが比較するのは、App が実際に宣言しているフィールドだけです。たとえば本番用パッケージには aps-environment が必須など、プロジェクト側で存在を必須としている項目がある場合は、必須フィールドのリストも追加し、欠落時にも失敗させる必要があります。

症状から設定元を特定する

症状 最初に確認する項目 よくある原因
プッシュ通知機能の異常 aps-environment アーカイブ環境とプロビジョニングプロファイルの用途が一致していない
Universal Links が機能しない com.apple.developer.associated-domains ドメイン項目が最終署名に書き込まれていない、または拡張機能側の設定が異なる
アップロード時に識別子の不一致が報告される application-identifier Bundle Identifier、チームプレフィックス、またはターゲット設定が混在している
リリース用パッケージをデバッグできてしまう get-task-allow 配布に適さないビルド設定を使用している
拡張機能をインストールできない メイン App と .appex のエンタイトルメント 拡張機能の識別子、App Group、またはプロビジョニングプロファイルの組み合わせに互換性がない

失敗した場合は、まずアーカイブと2つの plist を保存し、その後で CODE_SIGN_ENTITLEMENTSPRODUCT_BUNDLE_IDENTIFIER、現在のビルド設定を確認します。すぐに再署名して元の状態を上書きすると、最も重要な差分の証拠が失われるため避けてください。

関連ドメインと App Group は配列であり、通常は順序に意味がないため、集合として包含関係を比較します。ブール値のフィールドは実際のブール値として読み取る必要があり、文字列の "false"false として扱ってはいけません。これが grep ではなく plistlib を使用する理由でもあります。

クラウドビルドの受け入れ検証に組み込む

スクリプトを Scripts/verify_entitlements.sh として保存し、xcodebuild archive の成功後、エクスポートまたはアップロードの前に実行します。ジョブごとに独立したアーカイブディレクトリを使用し、検証結果、App の署名サマリー、失敗したフィールドをビルド成果物として保存します。ログにはフィールド名とマスキング済みの識別子だけを記録し、秘密鍵の内容や完全なアクセス認証情報は出力しないでください。

受け入れ検証は、パッケージ構造の検証、署名エンタイトルメントの抽出、プロビジョニングプロファイルのデコード、ポリシー比較の実行という4段階に分けることを推奨します。いずれかの段階で失敗したら後続の配布処理を停止しますが、原因分析に備えて .xcarchive は保存します。複数の Target を持つプロジェクトでは、メイン App と各拡張機能に対して個別に実行し、メイン App の検証結果を流用しないでください。

最後に手動でも確認します。現在のジョブで意図した Scheme と Configuration が使用されていること、本番成果物の get-task-allowfalse であること、プッシュ通知環境が配布先の要件に合っていること、関連ドメインにテスト用アドレスが混入していないこと、すべての拡張機能が個別の署名検証に成功していることを確認してください。これにより、従来はアップロード段階まで発覚しなかった問題を、再現可能で追跡しやすいビルド検証へ集約できます。

よくある質問

プロジェクト内のentitlementsファイルだけでは不十分ですか?

不十分です。最終権限はビルド設定、プロファイル、署名処理の影響を受けるため、アーカイブ内の署名済みAppを確認する必要があります。

最初に自動検証すべき権限は何ですか?

application-identifier、チーム識別子、aps-environment、関連ドメイン、get-task-allowを優先すると主要な配布事故を拾えます。

専有物理ノード

次回のビルドをクラウドMacで実行しましょう。

3種類のApple Silicon構成を比較し、現在のワークフローに合うリージョンを5つの海外ノードから選べます。

レンタルプランを選ぶ