|
| 1 | +# Atom++ — Code Signing & Notarization Runbook (macOS) |
| 2 | + |
| 3 | +How to turn the **unsigned / ad-hoc** Atom++ build into a **Developer-ID-signed, notarized, stapled** |
| 4 | +`.dmg` that launches cleanly on any Mac with no Gatekeeper warnings. |
| 5 | + |
| 6 | +> **Status (2026-06-28):** Apple Developer enrollment in progress (awaiting Apple support, up to ~2 |
| 7 | +> business days). This runbook captures the full process so it's ready the moment the account + cert |
| 8 | +> land. Until then, `scripts/make-dmg.sh` keeps producing an **ad-hoc-signed** dmg (runs locally; |
| 9 | +> needs right-click→Open or `xattr -dr com.apple.quarantine` on other Macs). |
| 10 | +
|
| 11 | +--- |
| 12 | + |
| 13 | +## 0. Where we are vs. where we're going |
| 14 | + |
| 15 | +| | Today (ad-hoc) | Goal (Developer ID + notarized) | |
| 16 | +| --- | --- | --- | |
| 17 | +| Signature | `codesign --sign -` (ad-hoc) | `Developer ID Application: … (TEAMID)` | |
| 18 | +| Hardened runtime | no | **yes** (`--options runtime`, required to notarize) | |
| 19 | +| Entitlements | none | Electron/V8 JIT entitlements | |
| 20 | +| Notarized by Apple | no | **yes** (`notarytool`) | |
| 21 | +| Stapled ticket | no | **yes** (`stapler`) | |
| 22 | +| Other Macs | Gatekeeper blocks (right-click→Open) | **launches cleanly, no warning** | |
| 23 | + |
| 24 | +**The pipeline:** enroll → create *Developer ID Application* cert → create a notarization credential → |
| 25 | +sign inside-out with hardened runtime + entitlements → notarize the dmg → staple → verify. |
| 26 | + |
| 27 | +--- |
| 28 | + |
| 29 | +## 1. Enroll in the Apple Developer Program *(manual — only you can do this)* |
| 30 | + |
| 31 | +- **Cost:** $99 / year. **Time:** minutes to ~48h for approval. |
| 32 | +- Pre-req: your **Apple ID must have two-factor auth** enabled (<https://appleid.apple.com>). |
| 33 | +- Go to <https://developer.apple.com/programs/> → **Enroll**. |
| 34 | +- **Entity type:** |
| 35 | + - **Individual / Sole Proprietor** — fastest; the app's signer shows your *personal legal name*. |
| 36 | + - **Organization** — shows your *company name*, supports a team, but needs a **D-U-N-S number** |
| 37 | + (free; can take a few days) and legal authority to enroll the entity. |
| 38 | +- Pay the **$99** (Apple Developer iPhone app or web) and wait for the approval email. |
| 39 | + |
| 40 | +> Recommendation for an indie/personal release: **Individual** now; convert to Organization later if needed. |
| 41 | +
|
| 42 | +--- |
| 43 | + |
| 44 | +## 2. Create the **Developer ID Application** certificate *(manual)* |
| 45 | + |
| 46 | +⚠️ **Pick the exact right type** — distribution outside the App Store needs **"Developer ID Application"**: |
| 47 | +- NOT "Apple Distribution" (App-Store only). |
| 48 | +- NOT "Developer ID Installer" (that's for `.pkg`; we ship a `.dmg`, so we don't need it). |
| 49 | + |
| 50 | +**Via Xcode (easiest — also creates the private key):** |
| 51 | +1. Install **Xcode** (Mac App Store, free). |
| 52 | +2. Xcode → **Settings → Accounts** → add your Apple ID → **Manage Certificates…** |
| 53 | +3. **+** → **Developer ID Application**. The cert + private key land in your **login keychain**. |
| 54 | + |
| 55 | +**Verify (this exact identity string is what the build uses):** |
| 56 | +```bash |
| 57 | +security find-identity -v -p codesigning |
| 58 | +# → "Developer ID Application: Your Name (TEAMID1234)" |
| 59 | +``` |
| 60 | +Record your **Team ID** (`TEAMID1234`) and the full identity string. |
| 61 | + |
| 62 | +> Back up the cert+key: Keychain Access → My Certificates → right-click the Developer ID Application |
| 63 | +> cert → **Export** → save the `.p12` somewhere safe. Losing the private key means re-issuing. |
| 64 | +
|
| 65 | +--- |
| 66 | + |
| 67 | +## 3. Create a notarization credential *(manual)* |
| 68 | + |
| 69 | +`notarytool` must authenticate to Apple. Pick one and store it once in the keychain: |
| 70 | + |
| 71 | +**Option A — App-specific password (simplest):** |
| 72 | +1. <https://appleid.apple.com> → **Sign-In and Security → App-Specific Passwords** → generate (name it `atompp-notary`). |
| 73 | +2. Store it: |
| 74 | + ```bash |
| 75 | + xcrun notarytool store-credentials "atompp-notary" \ |
| 76 | + --apple-id "you@example.com" \ |
| 77 | + --team-id "TEAMID1234" \ |
| 78 | + --password "xxxx-xxxx-xxxx-xxxx" # the app-specific password (not your Apple ID password) |
| 79 | + ``` |
| 80 | + |
| 81 | +**Option B — App Store Connect API key (best for CI):** |
| 82 | +- App Store Connect → **Users and Access → Integrations → App Store Connect API → +** → download the |
| 83 | + `.p8`, note **Key ID** + **Issuer ID**. |
| 84 | + ```bash |
| 85 | + xcrun notarytool store-credentials "atompp-notary" \ |
| 86 | + --key AuthKey_XXXXXX.p8 --key-id "KEYID" --issuer "ISSUER-UUID" |
| 87 | + ``` |
| 88 | + |
| 89 | +Either way the saved profile is referenced later as `--keychain-profile "atompp-notary"`. No secret ever |
| 90 | +enters the repo. |
| 91 | + |
| 92 | +--- |
| 93 | + |
| 94 | +## 4. Entitlements *(in the repo — Electron/V8 needs these or it crashes under the hardened runtime)* |
| 95 | + |
| 96 | +Create `build/entitlements/atompp.entitlements.plist`: |
| 97 | +```xml |
| 98 | +<?xml version="1.0" encoding="UTF-8"?> |
| 99 | +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
| 100 | +<plist version="1.0"> |
| 101 | +<dict> |
| 102 | + <key>com.apple.security.cs.allow-jit</key><true/> |
| 103 | + <key>com.apple.security.cs.allow-unsigned-executable-memory</key><true/> |
| 104 | + <key>com.apple.security.cs.disable-library-validation</key><true/> |
| 105 | + <key>com.apple.security.cs.allow-dyld-environment-variables</key><true/> |
| 106 | +</dict> |
| 107 | +</plist> |
| 108 | +``` |
| 109 | +The same set is applied to the **helper apps** (GPU / Renderer / Plugin) — they inherit these so V8's JIT |
| 110 | +works in every process. (This mirrors what upstream VS Code / `@electron/osx-sign` apply.) |
| 111 | + |
| 112 | +--- |
| 113 | + |
| 114 | +## 5. Sign — inside-out, hardened runtime *(automated by `scripts/sign-notarize.sh`, to be added)* |
| 115 | + |
| 116 | +Electron apps **must be signed from the inside out** (every nested `.dylib`/`.framework` and each |
| 117 | +`*.app` helper first, the main bundle last). Plain `--deep` is unreliable for this — prefer |
| 118 | +[`@electron/osx-sign`](https://github.com/electron/osx-sign) which knows the correct order, or a manual |
| 119 | +loop. Key flags on every binary: |
| 120 | +```bash |
| 121 | +codesign --force --timestamp --options runtime \ |
| 122 | + --entitlements build/entitlements/atompp.entitlements.plist \ |
| 123 | + --sign "Developer ID Application: Your Name (TEAMID1234)" <path> |
| 124 | +``` |
| 125 | +- `--options runtime` → hardened runtime (mandatory for notarization). |
| 126 | +- `--timestamp` → secure timestamp (mandatory; needs network). |
| 127 | +- Sign helpers/frameworks **before** the outer `Atom++.app`. |
| 128 | + |
| 129 | +**Verify the signature before notarizing:** |
| 130 | +```bash |
| 131 | +codesign --verify --deep --strict --verbose=2 "VSCode-darwin-arm64/Atom++.app" |
| 132 | +codesign -dv --verbose=4 "VSCode-darwin-arm64/Atom++.app" # → "Authority=Developer ID Application…", "flags=…runtime" |
| 133 | +``` |
| 134 | + |
| 135 | +--- |
| 136 | + |
| 137 | +## 6. Notarize the `.dmg` |
| 138 | + |
| 139 | +Build the dmg (`scripts/make-dmg.sh`), then: |
| 140 | +```bash |
| 141 | +xcrun notarytool submit "Atom++-arm64.dmg" --keychain-profile "atompp-notary" --wait |
| 142 | +``` |
| 143 | +- `--wait` blocks until Apple finishes (usually a few minutes) and prints **Accepted / Invalid**. |
| 144 | +- On **Invalid**, get the details: |
| 145 | + ```bash |
| 146 | + xcrun notarytool log <submission-id> --keychain-profile "atompp-notary" |
| 147 | + ``` |
| 148 | + |
| 149 | +--- |
| 150 | + |
| 151 | +## 7. Staple the ticket |
| 152 | + |
| 153 | +```bash |
| 154 | +xcrun stapler staple "Atom++-arm64.dmg" |
| 155 | +``` |
| 156 | +Stapling embeds the notarization ticket so Gatekeeper validates **offline** (no network on first launch). |
| 157 | +You can also staple the `.app` *before* building the dmg, then staple the dmg too. |
| 158 | + |
| 159 | +--- |
| 160 | + |
| 161 | +## 8. Verify it will pass Gatekeeper on a clean Mac |
| 162 | + |
| 163 | +```bash |
| 164 | +xcrun stapler validate "Atom++-arm64.dmg" |
| 165 | +spctl -a -t open --context context:primary-signature -v "Atom++-arm64.dmg" |
| 166 | +# → "source=Notarized Developer ID" / "accepted" |
| 167 | +``` |
| 168 | +Best real test: copy the dmg to a **different Mac** (or a fresh user account), open it, drag to |
| 169 | +Applications, double-click — it should launch with **no warning**. |
| 170 | + |
| 171 | +--- |
| 172 | + |
| 173 | +## 9. Troubleshooting |
| 174 | + |
| 175 | +| Symptom | Cause / fix | |
| 176 | +| --- | --- | |
| 177 | +| `notarytool` → **Invalid**, log says "not signed with a valid Developer ID" | Re-sign with the *Developer ID Application* identity + `--options runtime`. | |
| 178 | +| Log: "The executable does not have the hardened runtime enabled" | Missing `--options runtime` on some nested binary — re-sign inside-out. | |
| 179 | +| App crashes on launch ("killed", code 137 / SIGKILL) after signing | Missing JIT entitlements (§4) on the main app or a helper. | |
| 180 | +| "The signature does not include a secure timestamp" | Add `--timestamp` (needs network when signing). | |
| 181 | +| Gatekeeper still warns after notarizing | Forgot to **staple** (§7), or stapled the app but not the dmg. | |
| 182 | +| `errSecInternalComponent` during codesign | Keychain locked / wrong identity — `security unlock-keychain`; check `security find-identity`. | |
| 183 | + |
| 184 | +--- |
| 185 | + |
| 186 | +## 10. Repo integration plan (to wire once the cert is installed) |
| 187 | + |
| 188 | +1. `build/entitlements/atompp.entitlements.plist` — the entitlements from §4 (tracked). |
| 189 | +2. `scripts/sign-notarize.sh` — Developer-ID signs inside-out (`@electron/osx-sign` or manual loop), |
| 190 | + notarizes (§6), staples (§7), verifies (§8). **Gated on env vars** so it's a no-op without a cert: |
| 191 | + - `ATOMPP_SIGN_IDENTITY` — e.g. `Developer ID Application: Your Name (TEAMID1234)` |
| 192 | + - `ATOMPP_NOTARY_PROFILE` — e.g. `atompp-notary` |
| 193 | +3. `scripts/make-dmg.sh` — when `ATOMPP_SIGN_IDENTITY` is set, call `sign-notarize.sh` instead of the |
| 194 | + ad-hoc `codesign --sign -`; otherwise behave exactly as today (ad-hoc, unnotarized). |
| 195 | + |
| 196 | +So a signed release becomes: |
| 197 | +```bash |
| 198 | +export ATOMPP_SIGN_IDENTITY="Developer ID Application: Your Name (TEAMID1234)" |
| 199 | +export ATOMPP_NOTARY_PROFILE="atompp-notary" |
| 200 | +./scripts/build-macos.sh && ./scripts/make-dmg.sh # signs → dmg → notarize → staple → verify |
| 201 | +``` |
| 202 | + |
| 203 | +--- |
| 204 | + |
| 205 | +## Quick reference (once enrolled) |
| 206 | + |
| 207 | +```bash |
| 208 | +security find-identity -v -p codesigning # confirm cert |
| 209 | +xcrun notarytool store-credentials "atompp-notary" --apple-id … --team-id … --password … |
| 210 | +# build + sign + notarize + staple (via the wired scripts): |
| 211 | +ATOMPP_SIGN_IDENTITY="Developer ID Application: … (TEAMID)" \ |
| 212 | +ATOMPP_NOTARY_PROFILE="atompp-notary" ./scripts/make-dmg.sh |
| 213 | +xcrun stapler validate Atom++-arm64.dmg && spctl -a -t open --context context:primary-signature -v Atom++-arm64.dmg |
| 214 | +``` |
0 commit comments