NOTE D’INGÉNIERIE

Précontrôler les droits de signature iOS sur un Mac cloud

Précontrôler les droits de signature iOS sur un Mac cloud

Une archive peut être créée avec succès dans Xcode, puis échouer lors du téléversement en raison d’une incompatibilité liée à l’environnement des notifications push, aux domaines associés ou à l’identifiant de l’application. Le problème ne vient généralement pas de la compilation, mais d’un défaut d’alignement entre trois sources : les droits déclarés par le projet, ceux autorisés par le profil de provisionnement et ceux effectivement inscrits dans la signature de l’App. Comme les Mac cloud sont souvent réutilisés par des scripts et qu’un même nœud peut traiter plusieurs branches et environnements, il ne suffit pas d’examiner le fichier .entitlements du dépôt. Il faut contrôler directement le fichier .xcarchive destiné à la distribution.

Définir d’abord l’élément à contrôler

La procédure suivante prend comme entrée une archive déjà générée. Commencez par localiser l’App principale. Ne contrôlez pas directement les produits temporaires de DerivedData, car leur signature finale n’est peut-être pas encore terminée.

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"

Si l’App contient des extensions, chaque fichier .appex doit également être vérifié séparément. Une validation réussie de l’App principale ne garantit pas que l’extension de notifications, le Widget ou l’extension de partage utilise des identifiants d’application et des droits compatibles.

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

Le précontrôle doit se fonder sur « l’artefact qui sera téléversé », et non sur « un projet qui semble correctement configuré ». Le premier représente le résultat réel ; le second n’est qu’une donnée d’entrée.

Extraire les deux listes de droits réelles

codesign permet de lire les entitlements inscrits dans la signature finale, tandis que security cms décode le profil de provisionnement embarqué dans l’App. Enregistrez les deux résultats au format plist afin de pouvoir les comparer sans dépendre de l’interface de 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"

Ne comparez pas directement les champs de premier niveau du profil de provisionnement avec l’App. Les droits autorisés se trouvent dans le dictionnaire Entitlements. Les champs tels que la date de création ou le nom servent uniquement à faciliter l’identification et ne reflètent pas le résultat de la signature.

Comparer automatiquement les champs essentiels

Le script ci-dessous contrôle cinq catégories de problèmes fréquents. Pour les chaînes, il prend en charge les plages définies par le caractère générique * dans le profil de provisionnement. Pour les droits sous forme de tableaux, chaque valeur déclarée par l’App doit être autorisée par le profil. Le script renvoie un code différent de zéro en cas d’échec et peut donc être exécuté immédiatement après l’étape d’archivage.

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

Le script compare uniquement les champs effectivement déclarés par l’App. Si le projet impose la présence d’un droit particulier — par exemple aps-environment dans un paquet de production — ajoutez également une liste de champs obligatoires afin que leur absence provoque un échec.

Identifier la source de la configuration selon le symptôme

Symptôme Contrôle prioritaire Cause fréquente
Dysfonctionnement des notifications push aps-environment L’environnement d’archivage ne correspond pas à l’usage prévu du profil de provisionnement
Liens universels inopérants com.apple.developer.associated-domains Le domaine n’a pas été inscrit dans la signature finale ou la configuration diffère pour une extension
Incompatibilité d’identifiant lors du téléversement application-identifier Mélange entre le Bundle Identifier, le préfixe d’équipe ou les configurations de Target
Paquet de distribution encore débogable get-task-allow Utilisation d’une configuration de build inadaptée à la distribution
Échec d’installation d’une extension Droits de l’App principale et du fichier .appex Combinaison incompatible entre l’identifiant de l’extension, les groupes d’apps ou les profils de provisionnement

En cas d’échec, conservez d’abord l’archive et les deux fichiers plist, puis vérifiez CODE_SIGN_ENTITLEMENTS, PRODUCT_BUNDLE_IDENTIFIER et la configuration de build active. Ne resignez pas immédiatement l’artefact, car vous effaceriez les différences qui constituent les éléments de diagnostic les plus précieux.

Les domaines associés et les groupes d’apps sont des tableaux. Leur ordre n’étant généralement pas significatif, il faut comparer leur relation d’inclusion comme des ensembles. Les champs booléens doivent être lus comme de véritables valeurs booléennes : la chaîne "false" ne doit pas être interprétée comme false. C’est également pour cette raison qu’il convient d’utiliser plistlib plutôt que grep.

Intégrer le contrôle à la validation du build cloud

Enregistrez le script sous Scripts/verify_entitlements.sh, puis exécutez-le après la réussite de xcodebuild archive et avant l’exportation ou le téléversement. Utilisez un répertoire d’archive distinct pour chaque tâche et conservez comme pièces jointes du build le résultat du contrôle, le résumé de la signature de l’App et la liste des champs en échec. Les journaux doivent uniquement contenir les noms des champs et des identifiants anonymisés ; n’y inscrivez jamais le contenu d’une clé privée ni des identifiants d’accès complets.

Il est recommandé de diviser la validation en quatre étapes : vérifier la structure du paquet, extraire les droits de la signature, décoder le profil de provisionnement et appliquer la comparaison des règles. Tout échec doit interrompre la distribution, tout en conservant le fichier .xcarchive pour l’analyse ultérieure. Dans un projet comportant plusieurs Targets, exécutez la procédure séparément pour l’App principale et chaque extension ; le résultat du contrôle de l’App principale ne peut pas être réutilisé.

Terminez par une vérification manuelle : assurez-vous que la tâche en cours utilise le Scheme et la Configuration prévus, que get-task-allow vaut false pour l’artefact de production, que l’environnement des notifications push correspond à la cible de distribution, qu’aucune adresse de test ne figure parmi les domaines associés et que toutes les extensions passent leur propre vérification de signature. Les erreurs qui n’apparaissaient auparavant qu’au moment du téléversement sont ainsi ramenées à un contrôle de build reproductible et traçable.

Questions fréquentes

Pourquoi le fichier entitlements du projet ne suffit-il pas ?

Il représente une entrée du build, pas le résultat final. Les réglages, le profil et la signature peuvent modifier les droits réellement présents dans l’App archivée.

Quels droits faut-il automatiser en premier ?

Contrôlez d’abord application-identifier, l’identifiant d’équipe, aps-environment, les domaines associés et get-task-allow.

Nœud physique dédié

Exécutez votre prochaine compilation sur un Mac dans le cloud.

Comparez trois configurations Apple Silicon et choisissez, parmi cinq nœuds internationaux, la région adaptée à votre flux de travail actuel.

Choisir une formule de location