Les mises à jour automatiques, et pourquoi vous avez besoin d’une couche de signature supplémentaire

En Partie 1, nous avons terminé la configuration du certificat Developer ID et de la notarisation. Cela vous permet de livrer l’application aux utilisateurs pour la première fois. Mais une application ne se termine pas après une seule publication — vous devez continuer à publier de nouvelles versions qui corrigent des bugs et ajoutent des fonctionnalités.

Pour une application du Mac App Store, l’App Store gère les mises à jour à votre place. Une application distribuée directement ne bénéficie pas de cela, vous devez donc intégrer vous-même une fonctionnalité de mise à jour automatique dans l’application. Sur macOS, la norme de facto pour ce rôle est le framework open source Sparkle. Avec Sparkle en place, l’application vérifie périodiquement un « flux de mises à jour (appcast) », et s’il existe une nouvelle version, elle en informe l’utilisateur, la télécharge et l’installe.

Cela soulève une question. Vous signez déjà l’application avec le certificat Developer ID créé en Partie 1, alors pourquoi avez-vous besoin d’encore une autre clé ?

La raison est que les deux signatures vérifient des choses différentes.

  • Certificat Developer ID — utilisé par macOS Gatekeeper pour décider « est-il correct d’installer cette application ? »
  • Clé EdDSA Sparkle — utilisée par Sparkle à l’intérieur de l’application pour décider « le fichier de mise à jour que je viens de télécharger a-t-il vraiment été créé par le développeur de cette application ? »

Les mises à jour automatiques sont une opération sensible en matière de sécurité : l’application télécharge un fichier depuis Internet et se remplace elle-même. Si quelqu’un intercepte le serveur de mises à jour ou le chemin de communication et y insère un faux fichier, cela devient un problème grave. Pour l’éviter, Sparkle n’accepte que les mises à jour signées avec une clé privée que seul le développeur détient, et refuse d’installer quoi que ce soit dont la signature ne correspond pas. C’est effectivement une couche de vérification distincte du certificat.

Dans cet article, nous créerons la paire de clés EdDSA (Ed25519) qui sera utilisée pour cette vérification.

Comme en Partie 1, tous les noms et chemins (FocusTimer, example.com, etc.) sont des valeurs d’exemple. En pratique, remplacez-les par les informations de votre propre application.

Prérequis — Sparkle doit déjà être ajouté à l’application

Avant de créer la clé, le framework Sparkle doit déjà être ajouté comme dépendance de votre projet d’application. Si ce n’est pas encore le cas, ajoutez-le dans Xcode via Swift Package Manager (SPM).

  1. Ouvrez votre projet dans Xcode → File → Add Package Dependencies…
  2. Entrez l’adresse du dépôt dans la zone de recherche : https://github.com/sparkle-project/Sparkle
  3. Définissez la règle de version sur 2.x (la dernière version majeure) et ajoutez-la

Après avoir fait cela, buildez le projet une fois pour que SPM télécharge le package Sparkle. Les outils en ligne de commande fournis avec lui sont la clé de l’étape suivante.

Étape 1 — Localiser les outils en ligne de commande Sparkle

Le package Sparkle inclut des outils en ligne de commande utilisés pour la génération et la signature des clés. Ces outils se trouvent dans le dossier où SPM a téléchargé le package, mais cet emplacement varie selon votre version de Xcode et les paramètres DerivedData. Il est donc plus sûr de le trouver directement.

SPARKLE_BIN=$(find ~/Library/Developer/Xcode/DerivedData \
  -path "*/artifacts/sparkle/Sparkle/bin" -type d 2>/dev/null | head -1)
echo "$SPARKLE_BIN"

Si un seul chemin est imprimé, c’est que tout a fonctionné. Si rien n’apparaît, vous avez sauté l’étape « builder le projet une fois » ci-dessus — lancez un build dans Xcode puis réessayez.

Dans ce dossier se trouvent les outils suivants.

  • generate_keys — génère, sauvegarde, restaure et vérifie la clé de signature (utilisé dans cet article)
  • sign_update — signe les fichiers de mise à jour (utilisé lors des publications réelles)
  • generate_appcast — génère le flux de mises à jour (appcast.xml) (apparaît en Partie 3)

Étape 2 — Générer la clé de signature

Créons maintenant la paire de clés.

"$SPARKLE_BIN/generate_keys"

Une sortie similaire à la suivante apparaît.

Generating a new signing key...
A key has been generated and saved in your keychain. Add the SUPublicEDKey
key to the Info.plist of each app for which you intend to use Sparkle...

    <key>SUPublicEDKey</key>
    <string>5vT3kQbA9mZ0wR1yX8cD2eF4gH6jK7lN0pS2uV5xW8c=</string>

Cette seule commande crée deux clés.

  • Clé publique (public key) — la valeur SUPublicEDKey affichée dans la sortie ci-dessus. Ce n’est pas un secret, et c’est la clé que vous allez intégrer dans l’application.
  • Clé privée (private key) — n’apparaît pas à l’écran. Elle est automatiquement stockée dans le trousseau macOS (Keychain) sous le nom « Private key for signing Sparkle updates ». C’est un vrai secret qui ne doit jamais être laissé sur disque en tant que fichier en clair.

Intégrer la clé publique dans l’application

Insérez la chaîne de clé publique issue de la sortie dans le fichier Info.plist de l’application. Pour l’application exemple, ajoutez les clés suivantes à FocusTimer-Info.plist.

<key>SUFeedURL</key>
<string>https://updates.example.com/appcast.xml</string>
<key>SUPublicEDKey</key>
<string>5vT3kQbA9mZ0wR1yX8cD2eF4gH6jK7lN0pS2uV5xW8c=</string>
  • SUPublicEDKey — la clé publique que vous venez de générer. L’application utilise cette clé pour vérifier la signature des mises à jour téléchargées.
  • SUFeedURL — l’adresse du flux de mises à jour. Ce domaine n’existe pas encore ; nous le créons en Partie 3. Pour l’instant, c’est simplement un espace réservé.

Comme la clé publique est intégrée dans l’application et que seul le développeur détient la clé privée, l’application n’acceptera que les mises à jour signées avec la clé privée. C’est la structure centrale de la vérification des mises à jour Sparkle.

Étape 3 — Sauvegarder la clé privée (absolument !)

Si vous sautez cette étape, vous pourriez le regretter amèrement plus tard.

La clé privée est stockée dans le trousseau, donc elle est là sur l’ordinateur que vous utilisez maintenant. Mais si vous perdez l’ordinateur, si le disque tombe en panne, ou si vous réinstallez macOS, cette clé disparaît avec.

Que se passe-t-il si la clé privée est perdue ? Les mises à jour signées avec une nouvelle clé seront rejetées par les applications des utilisateurs existants (les applications avec l’ancienne clé publique intégrée). En d’autres termes, vous ne pourrez plus jamais envoyer de mises à jour automatiques aux utilisateurs qui utilisent déjà votre application. Votre seule option sera de dire à chaque utilisateur individuellement : « veuillez télécharger la nouvelle version vous-même et la réinstaller. »

Sauvegardez donc la clé immédiatement après l’avoir créée.

"$SPARKLE_BIN/generate_keys" -x ~/focustimer-sparkle-private.key
cat ~/focustimer-sparkle-private.key

La chaîne base64 d’une seule ligne imprimée par cat est la clé privée. Exemple :

Hn4Kp9Lr2Qs5Tv8Wx1Yz3Ab6Cd0Ef7Gh4Ij5Kl8MnQ0=

Stockez cette chaîne comme note sécurisée (secure note) dans un gestionnaire de mots de passe tel que 1Password. Donnez à la note un nom facile à retrouver plus tard, par exemple FocusTimer Sparkle EdDSA Private Key.

Immédiatement après avoir confirmé la sauvegarde, supprimez le fichier en clair laissé sur le disque.

rm ~/focustimer-sparkle-private.key

La règle est de ne jamais laisser la clé privée sur le disque en tant que fichier en clair. Conservez la sauvegarde uniquement dans un gestionnaire de mots de passe chiffré.

Un piège courant — Le symbole % ne fait pas partie de la clé

Lorsque vous imprimez la clé avec cat, le terminal (en particulier zsh) peut ajouter un symbole % à la fin de la ligne.

Hn4Kp9Lr2Qs5Tv8Wx1Yz3Ab6Cd0Ef7Gh4Ij5Kl8MnQ0=%

Ce % est simplement l’indicateur du shell signifiant que « la sortie s’est terminée sans saut de ligne » — il ne fait pas partie de la clé. Si vous copiez ce % dans votre sauvegarde, la clé sera corrompue lorsque vous tenterez de la restaurer plus tard. Une chaîne base64 se termine généralement par =, donc excluez le % après le = lors de la sauvegarde.

Étape 4 — Restaurer sur un autre ordinateur

Lorsque vous devez builder l’application sur un nouvel ordinateur, remettez la clé privée sauvegardée dans le trousseau.

echo "votre_chaîne_base64_sauvegardée" > ~/focustimer-sparkle-private.key
"$SPARKLE_BIN/generate_keys" -f ~/focustimer-sparkle-private.key
rm ~/focustimer-sparkle-private.key

L’option -f signifie « importer la clé du fichier dans le trousseau ». Une fois la restauration terminée, supprimez immédiatement le fichier en clair ici aussi.

Étape 5 — Vérification

Vérifiez que la clé a été correctement installée.

"$SPARKLE_BIN/generate_keys" -p

Une seule ligne contenant la clé publique est imprimée. Cette valeur doit correspondre exactement à la valeur que vous avez mise dans SUPublicEDKey dans le Info.plist à l’Étape 2. Si elles diffèrent, la clé publique intégrée dans l’application et la clé de signature réelle sont désynchronisées, et la vérification des mises à jour échouera.

Récapitulatif de la Partie 2

Si vous avez suivi jusqu’ici, vous disposez maintenant des éléments suivants.

  • ✅ Une paire de clés EdDSA Sparkle générée (clé publique + clé privée)
  • ✅ La clé publique intégrée dans le Info.plist de l’application (SUPublicEDKey)
  • ✅ La clé privée sauvegardée en toute sécurité dans un gestionnaire de mots de passe
  • ✅ La familiarité avec la procédure de restauration sur un autre ordinateur

L’application dispose maintenant d’un moyen de vérifier « si une mise à jour téléchargée est authentique ». Mais il manque encore une chose. En Partie 2, vous avez écrit https://updates.example.com/appcast.xml pour SUFeedURL, mais il n’y a toujours rien à cette adresse.

Dans la prochaine partie, nous créerons le dépôt public où vivront le flux de mises à jour (appcast.xml) et les fichiers .dmg, nous le connecterons à un domaine que nous contrôlons, et nous finirons les paramètres de build — ce qui conclura la configuration initiale.