Skip to content

Code-sign and notarize the desktop installers (macOS + Windows) #2

Description

@darrendignam

Summary

The desktop installers built by .github/workflows/desktop-installers.yml (via build-desktop.sh) are not code-signed. As a result:

  • macOS: Gatekeeper blocks first launch. The Release notes currently tell users to right-click the app and choose Open, then confirm.
  • Windows: SmartScreen warns on the .msi. Users must choose More info then Run anyway.

For tooling aimed at memory institutions on locked-down workstations, those warnings are a real adoption barrier and often violate local software policy. This issue records what it takes to sign and notarize the four installers so the warnings go away, so we can budget for it and implement it.

Signing needs certificates and CI secrets that must be provisioned first, so this is scoped as a planning issue rather than a ready-to-merge PR.

Current build (for reference)

Four targets, each on its own runner, produced by build-desktop.sh:

Target Runner Output Signed today
linux-x64 ubuntu-latest .deb n/a
mac-x64 macos-latest + Rosetta 2 .dmg no
mac-arm64 macos-latest .dmg no
windows-x64 windows-latest .msi no

Linux packages are not code-signed in the Authenticode/Gatekeeper sense, so this issue covers macOS and Windows only.


macOS: Developer ID signing + notarization

Two separate steps are both required for Gatekeeper to pass silently:

  1. Sign the .app (and the bundled JRE and JavaFX .dylibs inside it) with a Developer ID Application certificate, using the hardened runtime and a secure timestamp.
  2. Notarize the resulting .dmg with Apple's notary service, then staple the ticket so it verifies offline (important for air-gapped machines).

Prerequisites (org / one-off)

  • Apple Developer Program membership (99 USD/year), under an OPF-owned Apple Account, ideally an Organization account so the certificate is not tied to one person. Given the project's history, a shared org account matters.
  • A Developer ID Application certificate (not "Mac App Store"), exported as a password-protected .p12.
  • An App Store Connect API key (.p8 + key id + issuer id) for notarization. Preferred over an Apple ID + app-specific password because it is not tied to a person and does not break on 2FA.

jpackage support

jpackage can sign directly during packaging, so most of this lives in build-desktop.sh behind a "signing material present" check:

--mac-sign \
--mac-signing-key-user-name "Developer ID Application: Open Preservation Foundation (TEAMID)" \
--mac-signing-keychain "$KEYCHAIN_PATH" \
--mac-entitlements branding/mac/entitlements.plist

The JVM needs hardened-runtime exceptions, so entitlements.plist must include:

com.apple.security.cs.allow-jit
com.apple.security.cs.allow-unsigned-executable-memory
com.apple.security.cs.disable-library-validation

disable-library-validation is needed because JavaFX loads native libraries that jpackage does not sign with our leaf identity.

Notarize + staple (after the .dmg is built)

xcrun notarytool submit OpenFixity-<ver>-mac-<arch>.dmg \
  --key ASC_KEY.p8 --key-id "$ASC_KEY_ID" --issuer "$ASC_ISSUER_ID" --wait
xcrun stapler staple OpenFixity-<ver>-mac-<arch>.dmg

Both the mac-x64 and mac-arm64 jobs need this. The Rosetta-built x64 .dmg signs and notarizes the same way as the native arm64 one.

CI additions

  • Decode the base64 .p12 secret, create and unlock a temporary keychain, import the cert, set it default, and delete the keychain at the end of the job.
  • Run the signed build, then notarize and staple.

Secrets

MACOS_CERT_P12_BASE64, MACOS_CERT_PASSWORD, KEYCHAIN_PASSWORD, MACOS_TEAM_ID, ASC_API_KEY_P8_BASE64, ASC_API_KEY_ID, ASC_API_ISSUER_ID.


Windows: Authenticode signing

Prerequisites (org / one-off)

Since June 2023, standard OV and EV code-signing private keys must live on hardware (HSM/USB token) or a cloud signing service. There is no downloadable .pfx for newly issued OV certificates, so the old "PFX in a secret" approach is not available for a new cert. Options:

  • Azure Trusted Signing (recommended). Roughly 10 USD/month, has an official GitHub Action (azure/trusted-signing-action), and needs no hardware. Identity validation for an organization takes a few days.
  • DigiCert KeyLocker or SSL.com eSigner: cloud HSM signing, also CI-friendly, higher cost.
  • Physical EV token: not CI-friendly (manual token on a machine), avoid.

EV certificates get instant SmartScreen reputation. OV / Trusted Signing builds reputation as downloads accumulate, so early users may still see a warning for a while.

jpackage support

jpackage does not sign on Windows, so signing is a separate signtool step in two places:

  1. The app-image launcher OpenFixity.exe, signed before the .msi is built.
  2. The final .msi.
signtool sign /fd SHA256 /tr http://timestamp.acs.microsoft.com /td SHA256 <file>

The RFC3161 timestamp (/tr + /td) is essential so signatures stay valid after the certificate expires.

Implementation note: build-desktop.sh runs jpackage app-image then installer in one pass. To sign the launcher between those, split the Windows path so it signs the app-image .exe before invoking the --type msi step, then signs the .msi. Keep it conditional on signing secrets being present.

Secrets (Azure Trusted Signing)

AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, TRUSTED_SIGNING_ENDPOINT, TRUSTED_SIGNING_ACCOUNT, TRUSTED_SIGNING_CERT_PROFILE.


Wiring pattern

Mirror the existing optional-token pattern used for the OPF upload: signing runs only when its secrets are present, so local builds and forks still produce working unsigned installers. Signing material is never required to build.

Cleanup once signing lands

  • Remove the "not code-signed / right-click Open / More info then Run anyway" text from the Release body in desktop-installers.yml and from INSTALL.md.
  • Add a short note that installers are signed and notarized.

Checklist

Org / procurement:

  • Apple Developer Program membership under an OPF org account
  • Developer ID Application certificate exported as .p12
  • App Store Connect API key for notarization
  • Windows signing via Azure Trusted Signing (or chosen provider), org identity validated

Implementation:

  • Add the secrets above to the repo
  • branding/mac/entitlements.plist with the JVM exceptions
  • macOS signing flags + keychain setup in build-desktop.sh / workflow (both mac jobs)
  • macOS notarize + staple step (both mac jobs)
  • Windows signtool signing of the launcher and the .msi
  • Verify: spctl -a -vv OpenFixity.app passes on macOS; signtool verify /pa OpenFixity-<ver>-windows-x64.msi passes on Windows
  • Remove the unsigned-workaround notes

References

  • jpackage macOS signing options (--mac-sign, --mac-signing-key-user-name, --mac-entitlements)
  • notarytool and stapler (Xcode command line tools)
  • azure/trusted-signing-action GitHub Action
  • Microsoft's June 2023 change requiring hardware/cloud key storage for code-signing certificates

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    buildPackaging and distributionciBuild, test and release automationenhancementNew feature or requestsecuritySecurity, signing and trust

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions