Skip to content

release: conditional Developer ID signing + notarization (unblocks macOS 26.3.1 launchd)#62

Open
Augustas11 wants to merge 1 commit into
mainfrom
feat/release-developer-id-signing
Open

release: conditional Developer ID signing + notarization (unblocks macOS 26.3.1 launchd)#62
Augustas11 wants to merge 1 commit into
mainfrom
feat/release-developer-id-signing

Conversation

@Augustas11

Copy link
Copy Markdown
Owner

Summary

Wires up Developer ID signing + notarization in the release workflow, conditional on operator-supplied secrets. Can merge immediately; activates when secrets are populated.

Why

Discovered 2026-06-12 while attempting to install v1.3.1 on the operator's macOS 26.3.1 Mac. launchctl bootstrap fails with the generic 5: Input/output error regardless of plist content, domain, or sudo escalation. AMFI kernel logs surface the root cause:

AMFI: 'macprovider-cli' has no CMS blob?
AMFI: 'macprovider-cli': Unrecoverable CT signature issue, bailing out.

macOS 26.3.1 tightened launchd's AMFI policy to require a CMS certificate chain. Adhoc-signed binaries (what Swift's default linker-signing produces, and what every release through v1.3.1 has shipped) lack the CMS blob. Earlier macOS versions accepted them; 26.3.1 does not. Existing already-loaded launchd services keep working — the policy applies to fresh bootstrap calls, which is exactly what install.sh triggers.

Operational impact: blocks all fresh v1.3.1+ installs on macOS 26.3.1+ until signing is wired up. Cannot upgrade existing pool members (air5, air8gb on v1.2.5) without taking them offline. The M1-1 / FR-C9 deploy is on hold until this PR lands and the operator populates the secrets.

What's in this PR

.github/workflows/release.yml — new step "Sign + notarize binary"

Inserted between existing Build package and Tier-2 provider artifact preflight so the preflight script sees the signed tarball.

Pipeline:

  1. Import .p12 cert into a transient keychain
  2. codesign --options runtime --timestamp --sign <Developer ID>
  3. xcrun notarytool submit --wait (1-15 min)
  4. xcrun stapler staple (offline launches work)
  5. Re-tar with signed binary
  6. Delete transient keychain

Gated by APPLE_DEVELOPER_ID_CERT_P12_BASE64. If empty: emits a GitHub Actions warning and ships an adhoc-signed binary (current behavior). If the cert secret is set but supporting secrets are missing: hard fails (partial setup would silently produce mis-labeled artifacts).

phase3-binary/dist/release-signing-runbook.md — operator setup

Step-by-step: enroll in Apple Developer Program, generate cert via Keychain Access, export .p12, generate app-specific notary password, find Team ID, populate the 5 new GitHub repo secrets, verify on a macOS 26.3.1+ client. Includes common follow-up failure modes (staple failed, ticket not found, Gatekeeper quarantine).

New secrets the operator needs to populate

Secret Source
APPLE_DEVELOPER_ID_CERT_P12_BASE64 base64 -i path/to/cert.p12 | pbcopy
APPLE_DEVELOPER_ID_CERT_PASSWORD .p12 export password
APPLE_NOTARY_APPLE_ID enrolled Apple ID email
APPLE_NOTARY_PASSWORD app-specific password from appleid.apple.com
APPLE_NOTARY_TEAM_ID Team ID from developer.apple.com

Existing MACPROVIDER_RELEASE_SIGNING_KEY_PEM (checksums signing) is unrelated and stays as-is.

Cost

  • $99/year Apple Developer Program
  • ~5 minutes added to each release build (notarization wait, asynchronous on Apple's side)
  • No per-release fee

Test plan

  • Workflow YAML structurally valid (top-level keys: ['name', 'on', 'permissions', 'jobs'] — verified locally)
  • Merging without secrets populated: next release ships adhoc-signed binary + GitHub Actions warning (verified via the if [ -z ... ] early-exit branch)
  • After operator populates secrets: next tag push produces a signed + notarized + stapled binary
  • On a macOS 26.3.1+ client, curl get.streamvc.live/install.sh | bash succeeds at Install as a background service? [Y/n] y (currently fails with I/O error)
  • launchctl print gui/$(id -u)/live.streamvc.macprovider shows state = running

Not in this PR (separate follow-ups)

  • TOFU gate on RequireProviderTokens=true — the v1.3.1 coordinator enforces TOFU unconditionally, which combined with old binaries in the pool (that can't persist assigned_provider_token) brick-kicks providers on the next reconnect. SPEC-003 v0.8.2 FR-C9 was designed assuming the settling window would let providers self-mint cleanly before flag flip, but TOFU bites immediately. Needs a flag-gated variant.
  • Duplicate limit_req_zone in phase4-coordinator/dist/nginx-coordinator.streamvc.live.conf:18-19 — collides with the same zone declared by api.streamvc.live vhost. M1-4 (PR m1-4: rate-limit /ws/provider + per-IP semaphore (SECU-1) #39) didn't account for cross-vhost zone uniqueness. Operator hot-patched the live Pearl conf to comment out the duplicates; the source-of-truth file still has them.
  • Revoke the two orphan tokens before redeploying coordinator v1.3.1+. 372c3372023e (air5) and d630849e8acb (air8gb) — minted during the brief 13:54-22:49Z v1.3.1 window today, harmless under v1.3.0-24 but would brick both providers on the next v1.3.1 deploy.

🤖 Generated with Claude Code

macOS 26.3.1 tightened launchd's AMFI policy to reject adhoc-signed
binaries under `launchctl bootstrap`. Discovered 2026-06-12 during the
v1.3.1 deploy attempt — fresh installs on macOS 26.3.1 fail with
"Bootstrap failed: 5: Input/output error" because the binary has no
CMS blob. Earlier macOS versions accepted adhoc-signed binaries; the
policy change is OS-version-bound, not signature-format-bound.

The fix is to ship a Developer ID Application signed and notarized
binary. This commit adds the workflow step but guards it on operator
secrets so it can merge immediately and activate when the operator
populates the Apple Developer secrets — degrades gracefully to the
current (adhoc-signed) behavior with a GitHub Actions warning when the
secrets are absent.

Workflow step order:

1. Build package (existing)
2. NEW: Sign + notarize binary
   - import .p12 cert into transient keychain
   - codesign --options runtime --timestamp --sign <Developer ID>
   - notarytool submit --wait
   - stapler staple
   - re-tar with signed binary
   - delete keychain
3. Tier-2 preflight (existing — now sees the signed tarball)
4. Prepare release assets (existing)
5. Publish release (existing)

Five new operator secrets:

- APPLE_DEVELOPER_ID_CERT_P12_BASE64
- APPLE_DEVELOPER_ID_CERT_PASSWORD
- APPLE_NOTARY_APPLE_ID
- APPLE_NOTARY_PASSWORD
- APPLE_NOTARY_TEAM_ID

The existing MACPROVIDER_RELEASE_SIGNING_KEY_PEM (checksums signing) is
unrelated and stays as-is.

Operator runbook lives in phase3-binary/dist/release-signing-runbook.md:
how to enroll in the Apple Developer Program, generate the cert, export
.p12, generate the app-specific notary password, find the Team ID, and
populate the secrets. Plus the verification command for a macOS 26.3.1+
client and the common follow-up failure modes.

Once the secrets are populated, the next tagged release passes through
the new step. The expected duration overhead is 1-15 minutes for
notarization on top of the existing ~5-10 minute build.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant