Code Signing Setup Guide
May 28, 2026 · View on GitHub
This document explains how to configure code signing for kb release binaries so they don't trigger OS security warnings on macOS (Gatekeeper) or Windows (SmartScreen), and how Linux desktop artifact signatures are published for authenticity verification.
Overview
The release workflow automatically signs binaries when the appropriate secrets are configured:
- macOS: Codesign with hardened runtime + Apple notarization
- Windows: Authenticode signing with timestamp
- Linux: GPG detached-signature signing of
.AppImage/.deb/.tar.gzdesktop artifacts
Signing is optional — if secrets are not configured, the build succeeds and signing steps are skipped.
Required GitHub Secrets
macOS Signing
| Secret | Description |
|---|---|
APPLE_CERTIFICATE_BASE64 | Base64-encoded .p12 Developer ID Application certificate |
APPLE_CERTIFICATE_PASSWORD | Password used when exporting the .p12 certificate |
APPLE_IDENTITY | Signing identity string (e.g., Developer ID Application: Your Name (TEAMID)) |
APPLE_ID | Apple ID email address used for notarization |
APPLE_TEAM_ID | Apple Developer Team ID (10-character alphanumeric) |
APPLE_APP_PASSWORD | App-specific password for notarization |
Windows Signing
| Secret | Description |
|---|---|
WINDOWS_CERTIFICATE_BASE64 | Base64-encoded .pfx Authenticode code signing certificate |
WINDOWS_CERTIFICATE_PASSWORD | Password for the .pfx certificate |
Linux Signing
| Secret | Description |
|---|---|
LINUX_GPG_PRIVATE_KEY | Base64 of an ASCII-armored exported private key (gpg --armor --export-secret-keys) |
LINUX_GPG_PASSPHRASE | Passphrase that unlocks the exported private key |
LINUX_GPG_KEY_ID | Long key ID or full fingerprint used with gpg --local-user |
macOS Setup Instructions
1. Obtain a Developer ID Application Certificate
- Enroll in the Apple Developer Program
- In Xcode or the Apple Developer portal, create a Developer ID Application certificate
- Export the certificate from Keychain Access as a
.p12file with a password
2. Encode the Certificate as Base64
base64 -i certificate.p12 | pbcopy
Paste the result as the APPLE_CERTIFICATE_BASE64 secret.
3. Find Your Team ID
Your Team ID is visible at developer.apple.com/account under Membership Details. It's a 10-character alphanumeric string (e.g., ABC1234DEF).
4. Create an App-Specific Password
- Go to appleid.apple.com
- Sign in and navigate to Sign-In and Security → App-Specific Passwords
- Generate a new password and label it (e.g., "kb notarization")
- Use this as the
APPLE_APP_PASSWORDsecret
5. Determine Your Signing Identity
The signing identity looks like:
Developer ID Application: Your Name (TEAMID)
You can find it by running:
security find-identity -v -p codesigning
Linux Signing
1. Generate a release signing key
gpg --full-generate-key
Recommended: RSA 4096, with either no expiration or a 2-year expiration.
2. Export and base64-encode the private key for GitHub Actions secret storage
gpg --armor --export-secret-keys <key-id> | base64 -w0 | pbcopy
Linux clipboard alternative:
gpg --armor --export-secret-keys <key-id> | base64 -w0 | xclip -selection clipboard
Store this output as LINUX_GPG_PRIVATE_KEY.
3. Set Linux signing secrets
Set repository-level Actions secrets:
LINUX_GPG_PRIVATE_KEYLINUX_GPG_PASSPHRASELINUX_GPG_KEY_ID
4. Publish the public key for users
Export and publish the public key in release notes and/or a repository KEYS file:
gpg --armor --export <key-id>
5. User verification flow
gpg --import KEYS && gpg --verify Fusion-<version>-linux-x64.AppImage.asc Fusion-<version>-linux-x64.AppImage
If LINUX_GPG_PRIVATE_KEY is absent (forks/initial setup), CI continues and ships unsigned Linux artifacts without .asc sidecars.
Windows Setup Instructions
1. Obtain an Authenticode Code Signing Certificate
Purchase a code signing certificate from a trusted Certificate Authority:
- DigiCert
- Sectigo (Comodo)
- GlobalSign
- SSL.com
2. Export as .pfx
Export the certificate with its private key as a .pfx (PKCS#12) file. Set a strong password.
3. Encode the Certificate as Base64
[Convert]::ToBase64String([IO.File]::ReadAllBytes("certificate.pfx")) | Set-Clipboard
Or on Linux/macOS:
base64 -i certificate.pfx
Paste the result as the WINDOWS_CERTIFICATE_BASE64 secret.
How Signing Works in the Release Flow
- A tag push (
v*) triggers the release workflow - Each platform job builds the standalone binary
- macOS CLI jobs:
scripts/sign-macos.shruns codesign + notarization for standalone CLI binaries - macOS desktop jobs: electron-builder signs
.dmg/.zipoutputs usingCSC_LINK/CSC_KEY_PASSWORD, notarizes withAPPLE_ID/APPLE_APP_SPECIFIC_PASSWORD/APPLE_TEAM_ID, and validates signatures/stapling in CI - Windows jobs:
scripts/sign-windows.ps1runs Authenticode signing - Linux desktop jobs:
scripts/sign-linux.shrunsgpg --detach-sign --armorfor each.AppImage/.deb/.tar.gzartifact and verifies each signature withgpg --verify - Checksums are generated after signing (so they match the signed binaries)
- Signed binaries and checksums are uploaded to the GitHub Release
The test-release workflow (workflow_dispatch) includes the same signing steps but guards them with secret-availability checks — signing is skipped if secrets are not configured.
Desktop Windows packaging (.github/workflows/desktop-windows.yml) now uses the same WINDOWS_CERTIFICATE_BASE64 / WINDOWS_CERTIFICATE_PASSWORD secrets to sign NSIS and portable EXE artifacts via electron-builder (CSC_LINK / CSC_KEY_PASSWORD) and validates signatures with Get-AuthenticodeSignature when signing is enabled.
Troubleshooting
macOS: "The signature of the binary is invalid"
- Ensure the certificate is a Developer ID Application certificate (not Developer ID Installer or Mac App Distribution)
- Check that the certificate hasn't expired
- Verify the base64 encoding is correct:
echo "$APPLE_CERTIFICATE_BASE64" | base64 --decode | file -
macOS: Notarization fails with "Invalid credentials"
- Verify
APPLE_IDis your Apple ID email - Verify
APPLE_APP_PASSWORDis an app-specific password (not your Apple ID password) - Verify
APPLE_TEAM_IDmatches the team that issued the certificate
macOS: Notarization fails with "The software is not signed"
- Ensure the
--options runtimeflag is used during codesign (hardened runtime is required for notarization) - The
sign-macos.shscript handles this automatically
Windows: "signtool not found"
signtool.exeis included in the Windows SDK, which is pre-installed on GitHub Actions Windows runners- For local testing, install the Windows SDK or Visual Studio Build Tools
Windows: "The specified PFX password is not correct"
- Double-check the
WINDOWS_CERTIFICATE_PASSWORDsecret matches the password used when exporting the.pfx
Linux signing skipped
- Expected when
LINUX_GPG_PRIVATE_KEYis not set (forks, dry runs, first-time setup) - In this path, Linux artifacts upload without
.ascsidecars by design
Linux signing fails with "No secret key"
LINUX_GPG_KEY_IDdoes not match the imported private key fingerprint/key ID- Re-export and re-check key ID via
gpg --list-secret-keys --keyid-format=long
Linux verification fails with "BAD signature"
- Artifact changed between sign and verify; investigate runner storage path and artifact staging
- Ensure signatures are generated from the exact file that is later uploaded
Signing step skipped
- In the test-release workflow, signing is intentionally skipped when secrets are not configured
- Verify the secrets are set at the repository level in Settings → Secrets and variables → Actions
Desktop EXE signing skipped
- The desktop Windows workflow intentionally falls back to unsigned artifacts when
WINDOWS_CERTIFICATE_BASE64is empty - In that unsigned path,
Verify signed artifactsis skipped by design - Set both
WINDOWS_CERTIFICATE_BASE64andWINDOWS_CERTIFICATE_PASSWORDat the repository level to enable signed desktop EXE output
Desktop DMG/ZIP signing skipped
- The macOS desktop release jobs intentionally fall back to unsigned artifacts when
APPLE_CERTIFICATE_BASE64is empty - In that unsigned path, packaging runs with
-c.mac.notarize=falseso builds do not block on missing Apple notarization credentials - The
Verify signed and notarized macOS artifactsstep is skipped by design for unsigned runs
Notarization fails for desktop bundle
- Confirm
APPLE_APP_PASSWORDis configured in GitHub secrets and mapped toAPPLE_APP_SPECIFIC_PASSWORDin workflow env - Confirm
APPLE_TEAM_IDmatches the team that issued the Developer ID certificate - Confirm the Developer ID certificate in
APPLE_CERTIFICATE_BASE64is valid and not expired - Confirm hardened runtime entitlements are present via
packages/desktop/build/entitlements.mac.plist