Signature de code
Signer et notariser sur macOS, signer avec PFX ou Azure Artifact Signing sur Windows, et signer DEB/RPM en GPG sur Linux.
Les apps desktop non signées meurent à la porte sur macOS et Windows : Gatekeeper les refuse, SmartScreen avertit l'utilisateur, le Microsoft Store rejette l'upload. Sur Linux il n'y a pas de gatekeeper, mais un .deb/.rpm téléchargé en direct n'apporte aucune preuve de son origine. Nucleus branche signature et notarisation dans le même DSL Gradle que le packaging — un bloc par OS, ou un seul bloc unifié signing { }.
TL;DR
- macOS : Developer ID Application + notarisation
notarytool, trois modes d'auth. - Windows : certificat
.pfxou Azure Artifact Signing (cloud). - Linux : signe DEB/RPM avec ta propre clé GPG pour la distribution hors store. La méthode DEB par défaut est une signature détachée
.asc(ne nécessite quegpg) ; le RPM est signé dans son header. - Signer tes paquets Linux ne change rien à leur installation — la vérification reste optionnelle pour l'utilisateur.
- CI ready — actions composites pour le setup keychain et la signature depuis secrets.
Installation
Livré avec le plugin Gradle.
Démarrage rapide
macOS {
signing {
sign.set(true)
identity.set("Developer ID Application: My Company (TEAMID)")
}
notarization {
appleID.set("dev@example.com")
password.set(System.getenv("MAC_NOTARIZATION_PASSWORD"))
teamID.set("TEAMID")
}
}windows {
signing {
enabled = true
certificateFile.set(file("certs/certificate.pfx"))
certificatePassword = System.getenv("WIN_CSC_KEY_PASSWORD")
algorithm = SigningAlgorithm.Sha256
timestampServer = "http://timestamp.digicert.com"
}
}linux {
signing {
enabled.set(true)
keyId.set("AB12CD34EF56") // l'id de ta clé GPG
// passphrase.set(System.getenv("LINUX_GPG_PASSPHRASE")) // seulement si la clé en a une
}
}La clé est lue depuis ton trousseau GPG local. En CI, pointe plutôt keyFile sur une clé exportée — voir Gestion des secrets en CI.
Comment ça marche
macOS — Developer ID + notarisation
Les formats de distribution directe (DMG, ZIP) sont signés avec Developer ID Application puis soumis à Apple pour notarisation. Les builds Mac App Store (PKG) suivent un autre chemin : 3rd Party Mac Developer Application + provisioning profiles, pas de notarisation — Apple review via Transporter.
La notarisation supporte trois modes d'auth mutuellement exclusifs :
-
Apple ID + mot de passe app-specific (simple, dev-friendly) :
notarization { appleID.set("dev@example.com") password.set(System.getenv("MAC_NOTARIZATION_PASSWORD")) teamID.set("TEAMID") } -
Profil keychain
notarytool— stocke les credentials une fois avecxcrun notarytool store-credentials AC_PASSWORD …, puis :notarization { keychainProfile.set("AC_PASSWORD") } -
Clé API App Store Connect — recommandé en CI :
notarization { apiKey.set("/path/to/AuthKey_ABC123.p8") apiKeyId.set("ABC123") apiIssuer.set("12345678-90ab-cdef-1234-567890abcdef") }
Configurer plusieurs modes dans le même build est rejeté à la validation. Choisis-en un.
macOS — signature inside-out pour les binaires universels
lipo invalide toutes les signatures, donc les builds universels re-signent après la fusion dans un ordre strict inside-out : .dylib et .jnilib avec runtime entitlements, exécutables principaux avec app entitlements, puis le runtime bundle, puis le .app lui-même. Tous les codesign utilisent --options runtime --timestamp. L'action CI build-macos-universal gère ça — voir CI/CD.
Windows — certificat PFX
Le chemin classique : un fichier .pfx ou .p12, éventuellement protégé par mot de passe, avec un serveur de timestamping. L'algorithme par défaut est SHA-256 :
windows {
signing {
enabled = true
certificateFile.set(file(System.getenv("WIN_CSC_LINK") ?: "certs/certificate.pfx"))
certificatePassword = System.getenv("WIN_CSC_KEY_PASSWORD")
timestampServer = "http://timestamp.digicert.com"
}
}Serveurs de timestamping courants : DigiCert (http://timestamp.digicert.com), Sectigo (http://timestamp.sectigo.com), GlobalSign (http://timestamp.globalsign.com).
Windows — Azure Artifact Signing
Pour signer via HSM cloud sans fichier de certificat local :
windows {
signing {
enabled = true
publisherName = "My Publisher"
azureTenantId = System.getenv("AZURE_TENANT_ID")
azureEndpoint = "https://eastus.codesigning.azure.net"
azureCertificateProfileName = "my-profile"
azureCodeSigningAccountName = "my-account"
}
}Linux — signature GPG des DEB/RPM
Sur Linux il n'y a pas de gatekeeper OS à satisfaire. L'intérêt de la signature ici : permettre à quiconque télécharge ton .deb/.rpm en direct (hors dépôt apt/dnf) de prouver qu'il vient bien de toi et n'a pas été altéré. Tu signes avec ta propre clé GPG — sans certificat payant, sans autorité.
1. Génère une clé une fois (passe cette étape si tu en as déjà une) :
gpg --full-generate-key # RSA 4096, ton nom + email
gpg --list-secret-keys --keyid-format=long
# sec rsa4096/AB12CD34EF56 ... → l'id de la clé est "AB12CD34EF56"2. Active la signature avec cet id de clé :
linux {
signing {
enabled.set(true)
keyId.set("AB12CD34EF56")
}
}3. Builde. Chaque paquet sort signé, avec la clé publique écrite à côté :
MyApp-1.0.0-linux-x64.deb
MyApp-1.0.0-linux-x64.deb.asc # signature détachée (DEB)
MyApp-1.0.0-linux-x64.deb.pub.asc # ta clé publique
MyApp-1.0.0-linux-x64.rpm # signature intégrée au header RPM
MyApp-1.0.0-linux-x64.rpm.pub.ascPublie le paquet et son .pub.asc (plus le .deb.asc pour le DEB) sur ta page de téléchargement.
4. Tes utilisateurs vérifient avant de faire confiance à un téléchargement :
# DEB — signature détachée
gpg --import MyApp-1.0.0-linux-x64.deb.pub.asc
gpg --verify MyApp-1.0.0-linux-x64.deb.asc MyApp-1.0.0-linux-x64.deb
# → Good signature from "Your Name <you@example.com>"
# RPM — signature du header
sudo rpm --import MyApp-1.0.0-linux-x64.rpm.pub.asc
rpm -K MyApp-1.0.0-linux-x64.rpm
# → digests signatures OKLa signature ne change rien à l'installation. Un paquet signé s'installe exactement comme un non signé — sudo dpkg -i MyApp.deb, sudo rpm -i MyApp.rpm, ou la commande apt/dnf équivalente fonctionnent à l'identique, avec ou sans la clé importée. La vérification est une étape optionnelle que l'utilisateur lance avant d'installer. Elle ne devient obligatoire que si l'utilisateur l'impose explicitement (ex. une policy debsig-verify, ou une vérification RPM stricte).
Méthodes de signature DEB
debMethod contrôle comment le DEB est signé (DebSignMethod) :
Detached(défaut) — écrit un<pkg>.deb.ascdétaché, vérifié avecgpg --verify <pkg>.deb.asc <pkg>.deb. Ne nécessite quegpg, donc marche sur toutes les distros. C'est le défaut parce quedpkg-siga été retiré des Debian/Ubuntu récents.DpkgSig— intègre un membre_gpgoriginviadpkg-sig, vérifié avec un simplegpg --verify <pkg>.deb. Nécessitedpkg-siginstallé au build.Debsig— intègre un membre_gpgoriginviadebsigs, vérifié avecdebsig-verify. Nécessite en plus une policy et un keyring par clé côté client.
linux {
signing {
enabled.set(true)
keyId.set("AB12CD34EF56")
debMethod = DebSignMethod.Detached // Detached | DpkgSig | Debsig
}
}La clé de signature ne touche jamais ton vrai trousseau pendant le build : Nucleus importe keyFile dans un GNUPGHOME jetable, supprimé ensuite.
Bloc signing unifié
Tu préfères un seul point d'entrée pour toutes les plateformes ? Enveloppe les blocs par OS dans un seul signing { }. Il agit sur les mêmes settings, donc tu peux mélanger les deux styles librement :
nativeDistributions {
signing {
macOS { sign.set(true); identity.set("Developer ID Application: My Company (TEAMID)") }
windows { enabled = true; certificateFile.set(file("certs/certificate.pfx")) }
linux { enabled.set(true); keyId.set("AB12CD34EF56") }
}
}Gestion des secrets en CI
Ne commit jamais de certificats ni de clés privées. Sur GitHub Actions, encode le PFX en base64 et décode-le au build. Pour macOS, l'action composite setup-macos-signing crée un keychain temporaire et importe les certificats depuis les secrets — voir CI/CD.
Pour Linux, exporte la clé privée et stocke-la en secrets :
gpg --armor --export-secret-keys AB12CD34EF56 # à coller dans le secret LINUX_GPG_PRIVATE_KEYAjoute LINUX_GPG_PRIVATE_KEY, LINUX_GPG_KEY_ID et (si définie) LINUX_GPG_PASSPHRASE. Les workflows de release écrivent un fichier de clé temporaire et les passent au build via les propriétés compose.desktop.linux.signing.* ; quand les secrets sont absents, le build est simplement laissé non signé. Configure le build pour les lire :
linux {
signing {
enabled.set(true)
keyId.set(System.getenv("LINUX_GPG_KEY_ID"))
keyFile.set(file(System.getenv("LINUX_GPG_KEY_FILE") ?: "build/signing-key.asc"))
passphrase.set(System.getenv("LINUX_GPG_PASSPHRASE"))
}
}Référence
macOS.signing { }—sign,identity,keychain,prefixmacOS.notarization { }—appleID+password+teamID, oukeychainProfile(+keychainPath), ouapiKey+apiKeyId+apiIssuerwindows.signing { }—enabled,certificateFile,certificatePassword,certificateSha1,certificateSubjectName,algorithm,timestampServer, plus champs Azurelinux.signing { }—enabled,keyId,keyFile,passphrase,debMethod(Detached|DpkgSig|Debsig)signing { macOS { } windows { } linux { } }— bloc unifié sur les mêmes settings par OS
Référence complète dans la référence Gradle DSL.
Notes
- Entitlements pour le hardened runtime. Entitlements minimales JVM :
com.apple.security.cs.allow-jit,com.apple.security.cs.allow-unsigned-executable-memory,com.apple.security.cs.allow-dyld-environment-variables. Voir sandboxing pour les entitlements du PKG sandboxé. - Propriétés Gradle équivalentes. Chaque champ de notarisation a une propriété Gradle
compose.desktop.mac.notarization.<name>— utile quand on préfère passer les secrets via-Pplutôt qu'en variables d'env. La signature Linux a les mêmes :compose.desktop.linux.signetcompose.desktop.linux.signing.{keyId,keyFile,passphrase}. - La signature Linux est opt-in. Elle reste désactivée tant que
linux.signing.enabledn'est pastrueavec unkeyIddéfini. La signature au niveau dépôt (apt/dnfRelease.gpg,InRelease) est un sujet distinct, hors périmètre — ici on signe le paquet autonome pour le téléchargement direct. - Rotation du
.pfx. Mets à jour le certificat au moins 30 jours avant expiration — un binaire signé mais expiré échoue SmartScreen jusqu'à une nouvelle signature timestampée.