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:
@@ -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.
|
||||
|
||||
16
packages/desktop/build/entitlements.mac.plist
Normal file
16
packages/desktop/build/entitlements.mac.plist
Normal 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>
|
||||
@@ -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:
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user