feat(FN-5609): add macOS desktop signing configuration and entitlements

Adds macOS code signing and notarization infrastructure (FN-5609), introducing hardened runtime entitlements, electron-builder config for signed builds, and signing-enabled release workflows, with tests validating the configuration and docs covering the signing flow.

Fusion-Task-Id: FN-5609

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fusion-Task-Id: FN-5609
This commit is contained in:
gsxdsm
2026-05-27 05:21:56 -07:00
parent 925a466043
commit 7d2cf330db
8 changed files with 199 additions and 13 deletions

View File

@@ -363,6 +363,23 @@ Desktop packaging is configured in `electron-builder.yml`.
- Linux desktop artifacts can include detached GPG signature sidecars (`*.AppImage.asc`, `*.deb.asc`, `*.tar.gz.asc`) when Linux signing secrets are configured in CI; full Linux desktop code-signing rollout remains tracked in FN-5605.
- Linux `.deb` and `.tar.gz` outputs are best-effort and may be absent on some runner images without failing the release.
### macOS code-signing and notarization
The macOS desktop release path signs and notarizes desktop bundles through electron-builder in CI.
- Required CI secrets:
- `APPLE_CERTIFICATE_BASE64` (base64-encoded `.p12` Developer ID Application certificate)
- `APPLE_CERTIFICATE_PASSWORD`
- `APPLE_ID`
- `APPLE_TEAM_ID`
- `APPLE_APP_PASSWORD` (mapped to electron-builder env var `APPLE_APP_SPECIFIC_PASSWORD`)
- Signing uses electron-builder `CSC_LINK` / `CSC_KEY_PASSWORD`.
- Notarization uses electron-builder + `xcrun notarytool` with `APPLE_ID` / `APPLE_APP_SPECIFIC_PASSWORD` / `APPLE_TEAM_ID`.
- With `mac.notarize: true`, CI verifies stapled notarization for `.dmg` and `.app` outputs.
- If `APPLE_CERTIFICATE_BASE64` is empty (for example forked PR contexts), the workflow still publishes unsigned `.dmg` / `.zip` via the unsigned step and passes `-c.mac.notarize=false`; signed verification is skipped in that path.
- Hardened runtime entitlements are pinned in `packages/desktop/build/entitlements.mac.plist`. Any entitlement changes require a follow-up task.
- Local signing/notarization is opt-in; developers can set the same env vars locally to mirror CI behavior.
### Linux signing
Every signed Linux desktop release includes `*.asc` detached signature sidecars alongside the binary artifacts.

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.inherit</key>
<true/>
</dict>
</plist>

View File

@@ -65,6 +65,11 @@ mac:
target:
- target: dmg
- target: zip
hardenedRuntime: true
gatekeeperAssess: false
entitlements: build/entitlements.mac.plist
entitlementsInherit: build/entitlements.mac.plist
notarize: true
win:
target:

View File

@@ -106,6 +106,31 @@ describe("electron-builder desktop config", () => {
expect(linuxArchByTarget.get("deb")).toEqual(["arm64", "x64"]);
expect(linuxArchByTarget.get("tar.gz")).toEqual(["arm64", "x64"]);
});
it("locks mac signing and notarization configuration", async () => {
const builderConfig = await readDesktopFile("electron-builder.yml");
expect(builderConfig).toMatch(/mac:\s*[\s\S]*?hardenedRuntime:\s*true/m);
expect(builderConfig).toMatch(/mac:\s*[\s\S]*?gatekeeperAssess:\s*false/m);
expect(builderConfig).toMatch(/mac:\s*[\s\S]*?entitlements:\s*build\/entitlements\.mac\.plist/m);
expect(builderConfig).toMatch(/mac:\s*[\s\S]*?entitlementsInherit:\s*build\/entitlements\.mac\.plist/m);
expect(builderConfig).toMatch(/mac:\s*[\s\S]*?notarize:\s*true/m);
expect(builderConfig).not.toContain("mac.identity:");
expect(builderConfig).not.toContain("appleId:");
expect(builderConfig).not.toContain("teamId:");
});
it("ships the expected hardened-runtime entitlements plist", async () => {
const entitlements = await readDesktopFile("build/entitlements.mac.plist");
expect(entitlements).toContain("<?xml");
expect(entitlements).toContain("</plist>");
expect(entitlements).toContain("com.apple.security.cs.allow-jit");
expect(entitlements).toContain("com.apple.security.cs.allow-unsigned-executable-memory");
expect(entitlements).toContain("com.apple.security.cs.allow-dyld-environment-variables");
expect(entitlements).toContain("com.apple.security.cs.disable-library-validation");
expect(entitlements).toContain("com.apple.security.inherit");
});
});
describe("desktop windows workflow signing guards", () => {

View File

@@ -69,6 +69,39 @@ describe("desktop release workflow wiring", () => {
});
});
describe("desktop macos signing wiring", () => {
it("wires signed and unsigned macOS packaging paths with verification", async () => {
const release = await readRepoFile(".github/workflows/release.yml");
const testRelease = await readRepoFile(".github/workflows/test-release.yml");
for (const workflow of [release, testRelease]) {
expect(workflow).toContain("Package signed macOS desktop DMG/ZIP");
expect(workflow).toContain("Package unsigned macOS desktop DMG/ZIP");
expect(workflow).toContain("Verify signed and notarized macOS artifacts");
expect(workflow).toContain("secrets.APPLE_CERTIFICATE_BASE64");
expect(workflow).toContain("secrets.APPLE_CERTIFICATE_PASSWORD");
expect(workflow).toContain("secrets.APPLE_ID");
expect(workflow).toContain("secrets.APPLE_TEAM_ID");
expect(workflow).toContain("secrets.APPLE_APP_PASSWORD");
expect(workflow).toContain("CSC_LINK:");
expect(workflow).toContain("CSC_KEY_PASSWORD:");
expect(workflow).toContain("APPLE_APP_SPECIFIC_PASSWORD:");
expect(workflow).toContain("APPLE_TEAM_ID:");
expect(workflow).toContain("APPLE_CERTIFICATE_BASE64 != ''");
expect(workflow).toContain("APPLE_CERTIFICATE_BASE64 == ''");
expect(workflow).toContain("codesign --verify");
expect(workflow).toContain("spctl --assess");
expect(workflow).toContain("xcrun stapler validate");
expect(workflow).toContain("-c.mac.notarize=false");
}
});
});
describe("desktop linux signing wiring", () => {
it("wires Linux GPG secret-guarded signing and asc uploads in both workflows", async () => {
const release = await readRepoFile(".github/workflows/release.yml");