Code signing
Sign and notarize on macOS, sign with PFX or Azure Artifact Signing on Windows, and GPG-sign DEB/RPM on Linux.
Unsigned desktop apps die at the gate on macOS and Windows: Gatekeeper refuses them, SmartScreen warns users away, the Microsoft Store rejects the upload. On Linux there is no gatekeeper, but a direct-download .deb/.rpm has no built-in proof of where it came from. Nucleus wires signing and notarization into the same Gradle DSL that drives packaging — one block per OS, or a single unified signing { } block.
TL;DR
- macOS: Developer ID Application +
notarytoolnotarization, three auth modes. - Windows:
.pfxcertificate or Azure Artifact Signing (cloud-based). - Linux: GPG-sign DEB/RPM with your own key for distribution outside a store. Default DEB method is a detached
.ascsignature (needs onlygpg); RPM is signed in its header. - Signing your Linux packages does not change how they install — verification stays optional for the end user.
- CI ready — composite actions for keychain setup and secret-driven signing.
Install
Comes with the Gradle plugin.
Quickstart
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") // your GPG key id
// passphrase.set(System.getenv("LINUX_GPG_PASSPHRASE")) // only if the key has one
}
}The key is read from your local GPG keyring. In CI, point keyFile at an exported key instead — see CI secret management.
How it works
macOS — Developer ID + notarization
Direct-distribution formats (DMG, ZIP) are signed with Developer ID Application, then submitted to Apple for notarization. Mac App Store builds (PKG) follow a separate path: 3rd Party Mac Developer Application + provisioning profiles, no notarization — Apple reviews via Transporter.
Notarization supports three mutually exclusive auth modes:
-
Apple ID + app-specific password (simple, dev-friendly):
notarization { appleID.set("dev@example.com") password.set(System.getenv("MAC_NOTARIZATION_PASSWORD")) teamID.set("TEAMID") } -
notarytoolkeychain profile — store credentials once withxcrun notarytool store-credentials AC_PASSWORD …, then:notarization { keychainProfile.set("AC_PASSWORD") } -
App Store Connect API key — recommended for CI:
notarization { apiKey.set("/path/to/AuthKey_ABC123.p8") apiKeyId.set("ABC123") apiIssuer.set("12345678-90ab-cdef-1234-567890abcdef") }
Configuring more than one mode in the same build is rejected at validation. Pick one.
macOS — inside-out signing for universal binaries
lipo invalidates all code signatures, so universal builds re-sign after merge in a strict inside-out order: .dylib and .jnilib with runtime entitlements, main executables with app entitlements, then the runtime bundle, then the .app itself. All codesign invocations use --options runtime --timestamp. The build-macos-universal CI action handles this — see CI/CD.
Windows — PFX certificate
The classic path: a .pfx or .p12 file, optionally password-protected, with a timestamp server. Algorithm defaults to 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"
}
}Common timestamp servers: DigiCert (http://timestamp.digicert.com), Sectigo (http://timestamp.sectigo.com), GlobalSign (http://timestamp.globalsign.com).
Windows — Azure Artifact Signing
For cloud HSM signing without local certificate files:
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 — GPG signing for DEB/RPM
There is no OS gatekeeper to satisfy on Linux. The point of signing here is to let anyone who downloads your .deb/.rpm directly (outside an apt/dnf repository) prove it really came from you and was not tampered with. You sign with your own GPG key — no paid certificate, no authority.
1. Generate a key once (skip if you already have one):
gpg --full-generate-key # RSA 4096, your name + email
gpg --list-secret-keys --keyid-format=long
# sec rsa4096/AB12CD34EF56 ... → the key id is "AB12CD34EF56"2. Enable signing with that key id:
linux {
signing {
enabled.set(true)
keyId.set("AB12CD34EF56")
}
}3. Build. Each package comes out signed, with the public key written next to it:
MyApp-1.0.0-linux-x64.deb
MyApp-1.0.0-linux-x64.deb.asc # detached signature (DEB)
MyApp-1.0.0-linux-x64.deb.pub.asc # your public key
MyApp-1.0.0-linux-x64.rpm # signature embedded in the RPM header
MyApp-1.0.0-linux-x64.rpm.pub.ascPublish the package and its .pub.asc (plus the .deb.asc for DEB) on your download page.
4. Your users verify before trusting a download:
# DEB — detached signature
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 — header signature
sudo rpm --import MyApp-1.0.0-linux-x64.rpm.pub.asc
rpm -K MyApp-1.0.0-linux-x64.rpm
# → digests signatures OKSigning does not change installation. A signed package installs exactly like an unsigned one — sudo dpkg -i MyApp.deb, sudo rpm -i MyApp.rpm, or the equivalent apt/dnf command all work unchanged, with or without the key imported. Verification is an optional step the user runs before installing. It only becomes mandatory if the user explicitly enforces it (e.g. a debsig-verify policy, or strict RPM signature checking).
DEB signing methods
debMethod controls how the DEB is signed (DebSignMethod):
Detached(default) — writes a detached<pkg>.deb.asc, verified withgpg --verify <pkg>.deb.asc <pkg>.deb. Needs onlygpg, so it works on every distro. This is the default becausedpkg-sigwas removed from recent Debian/Ubuntu.DpkgSig— embeds an_gpgoriginmember viadpkg-sig, verified with a plaingpg --verify <pkg>.deb. Requiresdpkg-siginstalled at build time.Debsig— embeds an_gpgoriginmember viadebsigs, verified withdebsig-verify. Additionally requires a per-key policy and keyring on the client.
linux {
signing {
enabled.set(true)
keyId.set("AB12CD34EF56")
debMethod = DebSignMethod.Detached // Detached | DpkgSig | Debsig
}
}The signing key never touches your real keyring during the build: Nucleus imports keyFile into a throwaway GNUPGHOME and discards it afterwards.
Unified signing block
Prefer one entry point for all platforms? Wrap the per-OS blocks in a single signing { }. It operates on the same settings, so you can mix the two styles freely:
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") }
}
}CI secret management
Never commit certificates or private keys. In GitHub Actions, base64-encode the PFX and decode it at build time. For macOS, the setup-macos-signing composite action creates a temporary keychain and imports certificates from secrets — see CI/CD.
For Linux, export the private key and store it as secrets:
gpg --armor --export-secret-keys AB12CD34EF56 # paste into the LINUX_GPG_PRIVATE_KEY secretAdd LINUX_GPG_PRIVATE_KEY, LINUX_GPG_KEY_ID, and (if set) LINUX_GPG_PASSPHRASE. The release workflows write a temporary key file and feed these to the build via the compose.desktop.linux.signing.* properties; when the secrets are absent, the build is simply left unsigned. Configure the build to read them:
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"))
}
}Reference
macOS.signing { }—sign,identity,keychain,prefixmacOS.notarization { }—appleID+password+teamID, orkeychainProfile(+keychainPath), orapiKey+apiKeyId+apiIssuerwindows.signing { }—enabled,certificateFile,certificatePassword,certificateSha1,certificateSubjectName,algorithm,timestampServer, plus Azure fieldslinux.signing { }—enabled,keyId,keyFile,passphrase,debMethod(Detached|DpkgSig|Debsig)signing { macOS { } windows { } linux { } }— unified block over the same per-OS settings
Full reference in the Gradle DSL reference.
Notes
- Entitlements for hardened runtime. Minimal JVM entitlements:
com.apple.security.cs.allow-jit,com.apple.security.cs.allow-unsigned-executable-memory,com.apple.security.cs.allow-dyld-environment-variables. See sandboxing for the sandboxed PKG entitlements. - Equivalent Gradle properties. Every notarization field has a
compose.desktop.mac.notarization.<name>Gradle property — useful when you'd rather pass secrets via-Pthan env vars. Linux signing has the same:compose.desktop.linux.signandcompose.desktop.linux.signing.{keyId,keyFile,passphrase}. - Linux signing is opt-in. It is off until
linux.signing.enabledistrueand akeyIdis set. Repository-level signing (apt/dnfRelease.gpg,InRelease) is a separate concern and out of scope — this signs the standalone package for direct download. .pfxrotation. Update the certificate at least 30 days before expiry — signed-but-expired binaries fail SmartScreen until a fresh, timestamped signature is applied.