ci: lock the signing keychain before runtime checks
Merged
LA
22d ago+967+967+012345678901234567890123456789-188-188-012345678901234567890123456789
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.showns 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.shkeeps 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.shdeletes the keychain right after the DMG signature, before Apple's wait and before its launch smokes.resume-helper-notarization.shdeletes 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 universalNotarize appstep 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: falseon checkout.auto-resume-nightly-notarization.ymlandresume-nightly-notarization.yml: the App Store Connect key, signing identity and job token move from jobenv: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.p12is 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,.p12deleted after import, keychain deleted after the last signature. - New guard
tests/test_ci_signing_keychain_is_deleted_before_bundle_code.py(wired inci-guards.yml): fails when a workflow step runs bundle code while a signing keychain is open, when a keychain job has noalways()delete, when a Developer ID import is inline or a keychain job holds a secret in jobenv:, 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.shgains a case that runs the packaging script with a managed keychain and a fakesecurity, 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.ymlstill 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 (asnightly.ymlalready 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 currentmain(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
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.showns 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
.p12after 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.shdefers 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
mainand passes on this branch: it catches bundle code running while a keychain is open, missingalways()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.