ci: lock the signing keychain before runtime checks

Merged
LA
22d ago
+967-188

Summary

CI hardening for the macOS and iOS signing jobs (cmuxterm-hq#2053). The signing keychain now exists only while a step signs. It is deleted before any step runs code from the bundle, and created again when a later step must sign.

  • New scripts/ci/signing-keychain.sh owns the keychain: create (certificate decoded into a private temp file and deleted right after import, idle lock 600 s), unlock (after a long Apple wait), delete, is-present, assert-deleted.
  • New scripts/ci/lib/signing-runtime-guard.sh: the scripts that run bundle code (smoke-launch-macos-app.sh, smoke-signed-app-cli.sh, the diff sidecar handshake, verify_remote_daemon_release.py --verify-cli) refuse to start while a CI signing keychain exists, and they drop signing and publishing credentials from the environment they pass to the app.
  • sign-cmux-bundle.sh keeps every static check, and defers the diff sidecar handshake while the keychain exists (verify-diff-sidecar-artifact.sh --static-only). Each signing job runs the full sidecar check, with the handshake, after the keychain is deleted. No check is removed.
  • notarize-nightly-dmg.sh deletes the keychain right after the DMG signature, before Apple's wait and before its launch smokes. resume-helper-notarization.sh deletes it again before the continuation launches the app.
  • nightly.yml, release.yml, repair-v0-64-25-helper-rpaths.yml, cmux-next-dev-daily.yml: delete the keychain after codesign, then run the runtime checks and smokes, then import again only for the helper re-seal and DMG signature.
  • release.yml: the CLI memory guard and theme picker checks now run before the first import (the import and the universal helper submission move after the thin variant cut). The universal Notarize app step signs the DMG and deletes the keychain before its launch smokes. The two thin DMGs are one step each, with their own import, because the packaging script deletes the keychain after each DMG. persist-credentials: false on checkout.
  • auto-resume-nightly-notarization.yml and resume-nightly-notarization.yml: the App Store Connect key, signing identity and job token move from job env: to the steps that use them. A recovered DMG needs no signature, so only the Computer Use helper path imports the certificate.
  • iOS (ios-testflight.yml, ios-appstore-upload.yml, ios-app-store.yml): the .p12 is deleted right after import, the idle lock is the job timeout instead of 6 h, and the keychain is deleted right after the archive step. coderouter-release.yml: idle lock 600 s, .p12 deleted after import, keychain deleted after the last signature.
  • New guard tests/test_ci_signing_keychain_is_deleted_before_bundle_code.py (wired in ci-guards.yml): fails when a workflow step runs bundle code while a signing keychain is open, when a keychain job has no always() delete, when a Developer ID import is inline or a keychain job holds a secret in job env:, when an idle lock is longer than the job, and when the signing scripts lose their delete-before-smoke order.
  • tests/test_notarize_nightly_dmg.sh gains a case that runs the packaging script with a managed keychain and a fake security, and requires unlock, DMG signature, keychain delete, then smoke.

Not in this PR

  • App checks run outside the keychain window, but still in the signing job. Job-level isolation (every check that runs bundle code moves to a separate job with no secrets that downloads the signed app) follows in a separate PR.
  • release.yml still builds in the same job that later signs. The build runs before the first import, so no keychain exists during the build, but the build and the signing steps share a workspace. Splitting the build into a job with no secrets (as nightly.yml already does) changes the stable release shape and cannot be exercised before a tag; it is a separate change.
  • The import flags are unchanged (-T /usr/bin/codesign -T /usr/bin/security, extractable key). Making the key non-extractable is a separate change that needs its own signing run.
  • create-dmg (npm) still runs while the keychain exists, between the helper re-seal and the DMG signature.

Testing

  • python3 tests/test_ci_signing_keychain_is_deleted_before_bundle_code.py: fails on current main (first commit), passes on this branch.
  • tests/test_notarize_nightly_dmg.sh, tests/test_ci_release_per_arch_stable.py, tests/test_nightly_universal_build.sh, tests/test_ci_release_dmg_lzma.sh, tests/test_repair_v06425_workflow.sh, tests/test_notarize_computer_use_helper.{py,sh}, tests/test_sign_cmux_bundle_tunnel_gate.sh, tests/test_remote_daemon_release_verification.py, the nightly continuation tests, tests/test_ci_signing_secrets_need_release_environment.py, tests/test_ci_workflow_guards_are_wired.py: pass on a Linux VM.
  • actionlint built from manaflow-ai/actionlint v1.7.12-manaflow.1 on the 11 touched workflows: no new finding (the same pre-existing shellcheck infos as main).
  • Not yet verified: one real signing run. This PR changes only workflows and scripts that run in protected release environments.

Changelog

none


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Hardens the macOS and iOS signing jobs so the code-signing keychain exists only while a step is signing. Bundle code never runs while the keychain exists, and signing, publishing, and notarization credentials never reach the app.

Keychain lifetime

  • New scripts/ci/signing-keychain.sh owns the CI keychain: 600 s idle lock, certificate file deleted right after import, keychain deleted after the last signature.
  • The notarization scripts delete the keychain after the DMG signature and before Apple waits or launch smokes, and re-import for each DMG signature; the decoded App Store Connect notary key is also deleted before any launch smoke.
  • The notarization continuations import the certificate only for the Computer Use helper path; a recovered DMG needs no signature.
  • iOS and CodeRouter jobs delete the .p12 after import and the keychain after the last signature, with the idle lock set to the job timeout.

Guardrails

  • Scripts that run bundle code (smoke-launch-macos-app.sh, smoke-signed-app-cli.sh, the diff sidecar handshake, verify_remote_daemon_release.py --verify-cli) refuse to start while a CI signing keychain exists and drop signing, publishing, and notary credentials from the app's environment.
  • sign-cmux-bundle.sh defers the sidecar handshake while the keychain exists; each signing job runs the full check after the delete.
  • Secrets and the job token move from job env: to the steps that use them.
  • New guard test fails on current main and passes on this branch: it catches bundle code running while a keychain is open, missing always() deletes, and secrets in job-level env.

Summary by CodeRabbit

  • Security & Reliability
    • Signing credentials are more tightly scoped during release and recovery workflows, and temporary signing materials are cleared sooner.
    • Release checks verify signed application artifacts and run bundled-app checks after signing credentials are cleared.
    • Notarization recovery and architecture-specific disk image signing use more consistent validation and cleanup steps.
  • Tests
    • Added automated checks for signing-keychain cleanup and release workflow ordering.