Bitlab releases without any signing credentials. That mode is real and supported — macOS builds are ad-hoc signed and Windows installers are unsigned — but users pay for it with a Gatekeeper or SmartScreen warning on first launch, and macOS auto-update stays disabled. This document is the path from that default to trusted installers.
Signing is resolved per platform and all-or-nothing per platform. The
release-policy job fails the release if a group is only half configured, so a
typo cannot silently downgrade an intended trusted build. See
releases.md for how that decision flows through the workflow.
| Platform | Secrets group | Result when configured |
|---|---|---|
| macOS | CSC_LINK, CSC_KEY_PASSWORD, APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, APPLE_TEAM_ID |
Developer ID signed + notarized, Hardened Runtime on, auto-update enabled |
| Windows | WIN_CSC_LINK, WIN_CSC_KEY_PASSWORD |
Authenticode signed |
| Linux | none | AppImage is never signed; integrity comes from SHA256SUMS |
- A paid Apple Developer Program membership (individual or organization). A free Apple ID cannot issue Developer ID certificates.
- A Mac with Xcode or the Xcode Command Line Tools.
The certificate type must be Developer ID Application. Apple Development
and Mac App Distribution certificates exist in the same account and look
similar, but neither one produces an app that Gatekeeper trusts outside the App
Store, and notarization will reject them.
The Xcode path is shorter and generates the private key for you:
- Xcode → Settings → Accounts → select your Apple ID → Manage Certificates…
- Click + → Developer ID Application.
- The certificate and its private key land in your login keychain.
If you prefer the web console, or Xcode refuses because the account already has the maximum number of certificates:
- Open Keychain Access → menu Certificate Assistant → Request a Certificate From a Certificate Authority…
- Enter your email and name, select Saved to disk, and save
CertificateSigningRequest.certSigningRequest. - Go to developer.apple.com/account/resources/certificates → + → Developer ID Application → upload the CSR.
- Download the resulting
.cerand double-click it to install it into the login keychain.
Confirm the identity exists and note the ten-character Team ID in parentheses:
security find-identity -v -p codesigningYou want a line reading Developer ID Application: Your Name (ABCDE12345).
- Keychain Access → login keychain → My Certificates.
- Find
Developer ID Application: …and expand it — it must show a private key underneath. Without the private key the export is useless. - Right-click the certificate → Export "Developer ID Application: …" → format Personal Information Exchange (.p12).
- Set a strong password. This becomes
CSC_KEY_PASSWORD.
CSC_LINK takes the certificate as a base64 string:
base64 -i ~/Desktop/bitlab-developer-id.p12 | pbcopyPaste the clipboard into the secret. Delete the .p12 from disk afterwards, or
move it into a password manager — it is your signing identity.
Your real Apple ID password will not work, and an account with two-factor authentication requires this step:
- Sign in at appleid.apple.com.
- Sign-In and Security → App-Specific Passwords → +.
- Name it something like
bitlab-notarizationand copy the generatedxxxx-xxxx-xxxx-xxxxvalue. It is shown once.
That value is APPLE_APP_SPECIFIC_PASSWORD; APPLE_ID is the email address of
the account that owns it.
Either read it from the parentheses in security find-identity output, or open
developer.apple.com/account →
Membership details → Team ID. It is APPLE_TEAM_ID.
For an organization account, the Apple ID used for notarization must belong to that team; a personal Apple ID with its own separate team will notarize against the wrong Team ID and fail.
In the repository: Settings → Environments → release → Add secret.
The release jobs read them from the release environment, not from repository
secrets, so adding them at the repository level alone has no effect.
| Secret | Value |
|---|---|
CSC_LINK |
base64 of the .p12 |
CSC_KEY_PASSWORD |
the .p12 export password |
APPLE_ID |
Apple ID email |
APPLE_APP_SPECIFIC_PASSWORD |
the xxxx-xxxx-xxxx-xxxx value |
APPLE_TEAM_ID |
ten-character Team ID |
The next tagged release picks them up automatically: forceCodeSigning turns on,
Hardened Runtime stays enabled, electron-builder notarizes through notarytool
and staples the ticket, and BITLAB_AUTO_UPDATE_ENABLED becomes true for macOS.
Signing and notarization can be rehearsed on your own machine:
bun run electron:dist:mac
codesign --verify --deep --strict --verbose=2 "apps/electron/release/mac-arm64/Bitlab.app"
spctl --assess --type execute --verbose "apps/electron/release/mac-arm64/Bitlab.app"
xcrun stapler validate "apps/electron/release/Bitlab-0.1.0-arm64.dmg"spctl should report accepted and source=Notarized Developer ID. Anything
mentioning rejected or source=Unnotarized Developer ID means the ticket was
not stapled.
For notarization to run locally you need the same Apple credentials exported in your shell:
export APPLE_ID="you@example.com"
export APPLE_APP_SPECIFIC_PASSWORD="xxxx-xxxx-xxxx-xxxx"
export APPLE_TEAM_ID="ABCDE12345"Notarization is a network round-trip to Apple and typically takes 2–15 minutes per artifact. A release builds four macOS artifacts (two DMG, two ZIP), so budget accordingly against the workflow's 120-minute job timeout.
| Symptom | Cause |
|---|---|
No identity found in CI |
CSC_LINK is not valid base64, or the .p12 was exported without its private key |
The specified item could not be found in the keychain |
wrong CSC_KEY_PASSWORD |
Notarization returns Invalid with The signature does not include a secure timestamp |
Hardened Runtime or timestamping was disabled; do not pass -c.mac.hardenedRuntime=false on a signed build |
Notarization returns Invalid with Team is not yet configured for notarization |
the Apple ID is not a member of the team owning the certificate |
| App still shows "damaged and can't be opened" after signing | the ticket was not stapled; check xcrun stapler validate |
Windows is harder than macOS now, and not because of the workflow. Since June
2023 the CA/Browser Forum requires code-signing private keys to live on
FIPS-140-2 hardware — a USB token or a cloud HSM. A plain exportable .pfx for
WIN_CSC_LINK is no longer something a certificate authority will issue for a
new OV or EV certificate, so the two Windows secrets only work if you already
hold a legacy exportable certificate.
Realistic options:
- Ship unsigned. SmartScreen shows a warning that users dismiss through More info → Run anyway. Reputation accumulates with download volume and the warning eventually stops for that binary. This is the current default.
- Azure Trusted Signing —
Microsoft's managed signing service, roughly $10/month, no hardware token.
Requires a verified legal identity and, for organizations, three years of
business history. It needs a different electron-builder configuration than
WIN_CSC_LINK; wire it as a customsignhook. - SignPath Foundation — free certificates and signing infrastructure for OSS projects that meet its criteria.
If you do hold an exportable .pfx, the encoding is the same as macOS:
base64 -i certificate.pfx | pbcopyStore it as WIN_CSC_LINK and its password as WIN_CSC_KEY_PASSWORD in the
release environment. The workflow then adds forceCodeSigning and verifies
every produced .exe with Get-AuthenticodeSignature.
Keep these instructions in the release notes for as long as builds are unsigned.
macOS refuses an ad-hoc-signed app with "Bitlab is damaged and can't be opened". The quarantine attribute, not the app, is the problem:
xattr -dr com.apple.quarantine /Applications/Bitlab.appWindows SmartScreen: More info → Run anyway.
Both cases are worth pairing with the SHA256SUMS file published on every
release, so a cautious user can verify the download instead of trusting the
warning dialog:
shasum -a 256 -c SHA256SUMS --ignore-missing