From 44e724befd4f39cf1c3028c69d1a19170301d78a Mon Sep 17 00:00:00 2001 From: Theo Gravity Date: Mon, 26 Oct 2026 07:57:06 -0700 Subject: [PATCH] fix(ci): stop the helper keychain auto-locking mid-build; smoke takes -R= The search-list fix worked: the darwin-arm64 shards signed the helper and notarized cleanly. Two more blockers then surfaced on run 40328004030: - Both darwin-x64 shards hung ~8 minutes into cargo build, exactly where the helper's build script codesigns. A fresh keychain defaults to locking after 300 idle seconds (show-keychain-info on the mini: lock-on-sleep timeout=300s); arm64 signed inside the window, x64's slower cross-build did not, and codesign waits forever for an unlock no headless runner can answer. A bare set-keychain-settings clears the timers in all four keychain blocks. - The helper identity check passed every requirement check by failing: codesign -v takes requirement TEXT only as -R= (usage: -R=|-R ); the space form is always read as a file, so smoke died with 'invalid requirement specification'. Verified on the mini: space form fails identically on any app, -R= parses and evaluates. While here, the keychain list became prepend-only, never owned: shards share the runner user's keychain domain, and the whole-list restore (or trap restoring login) can take the list back from under a build still signing. The trap now removes only its own keychain. Simulated on the mini: sibling entries survive. --- .github/workflows/release.yml | 126 +++++++++++++++++++++----------- docs/release-and-ci.md | 26 ++++--- scripts/smoke-desktop-bundle.sh | 5 +- 3 files changed, 104 insertions(+), 53 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d9fbfb169..7254bb282 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -714,18 +714,28 @@ jobs: [ -n "${!v:-}" ] || { echo "missing $v secret (mac signing provisioning)"; exit 1; } done # Ephemeral keychain, alive for exactly this step (trap cleans - # up + restores the search list). The login keychain is - # deliberately left OFF the search list for the job: any copy - # of the identity it happens to hold must never satisfy (or - # shadow) the lookup — that is precisely the host-state - # dependency this provisioning replaces. + # up + removes it from the search list). The keychain is + # PREPENDED to the list, never swapped in as the whole list: + # concurrent darwin jobs share this runner user's keychain + # domain, and a replacing write (or a trap restoring login) + # takes the list back from under a sibling build that is still + # signing. Ours leads, so the identity found is THIS copy, + # imported from this step's p12 - not some host-state copy the + # login keychain happens to hold. CI_TMP="$(mktemp -d)" KC="$CI_TMP/ci-build.keychain-db" KCPW="$(/usr/bin/openssl rand -hex 16)" - trap 'rm -rf "$CI_TMP"; security delete-keychain "$KC" 2>/dev/null || true; security list-keychains -d user -s "$HOME/Library/Keychains/login.keychain-db"' EXIT + trap 'rm -rf "$CI_TMP"; security delete-keychain "$KC" 2>/dev/null || true; REST="$(security list-keychains -d user | tr -d "\"" | grep -v "$KC" || true)"; [ -n "$REST" ] && security list-keychains -d user -s $REST || true' EXIT security create-keychain -p "$KCPW" "$KC" - security list-keychains -d user -s "$KC" + security list-keychains -d user -s "$KC" $(security list-keychains -d user | tr -d '"') security unlock-keychain -p "$KCPW" "$KC" + # A fresh keychain defaults to locking after five idle minutes. + # A slow cargo build reaches its codesign calls well past that + # and codesign then waits forever for an unlock no headless + # runner can answer (run 40328004030: both darwin-x64 shards + # hung ~8 minutes in, the arm64 ones signed inside the window). + # Clear the timers before anything can go idle. + security set-keychain-settings "$KC" printf '%s' "$MACOS_CERT_P12_BASE64" | base64 -d > "$CI_TMP/cert.p12" security import "$CI_TMP/cert.p12" -k "$KC" -P "$MACOS_CERT_PASSWORD" -T /usr/bin/codesign rm -f "$CI_TMP/cert.p12" @@ -898,30 +908,41 @@ jobs: # time), and a release helper must carry the Developer ID, not an # ad-hoc signature, because the permission grant follows the # cdhash. Build a second keychain for exactly that one call and - # make it the SOLE entry of the user's search list, the same way - # the CLI shards provision theirs above: codesign resolves a - # named identity through the search list, not the --keychain - # argument (probe run 40325026239 on this image: the same - # keychain failed "item could not be found" while unlisted and - # signed cleanly once listed). Login stays off the list exactly - # as on the CLI path. When Tauri's bundle-time keychain takes the - # list over later, both hold the SAME identity from the SAME p12, - # so the tauri#11992 trap - DIFFERENT identities in one view - - # cannot bite. The trap restores the baseline at step end. + # PREPEND it to the user's search list, the same way the CLI + # shards provision theirs above: codesign resolves a named + # identity through the search list, not the --keychain argument + # (probe run 40325026239 on this image: the same keychain failed + # "item could not be found" while unlisted and signed cleanly + # once listed). Prepend, never own: concurrent shards share this + # runner user's keychain domain, and a write replacing the whole + # list (or a trap restoring login) takes it back from under a + # build still running - ours leads, so codesign finds THIS copy. + # When Tauri's bundle-time keychain takes the list over later, + # both hold the SAME identity from the SAME p12, so the + # tauri#11992 trap - DIFFERENT identities in one view - cannot + # bite. The trap removes only this keychain from the list at + # step end. KC="$CI_TMP/helper-sign.keychain-db" KCPW="$(/usr/bin/openssl rand -hex 16)" security create-keychain -p "$KCPW" "$KC" - security list-keychains -d user -s "$KC" + security list-keychains -d user -s "$KC" $(security list-keychains -d user | tr -d '"') security unlock-keychain -p "$KCPW" "$KC" + # A fresh keychain defaults to locking after five idle minutes. + # A slow cargo build reaches its codesign calls well past that + # and codesign then waits forever for an unlock no headless + # runner can answer (run 40328004030: both darwin-x64 shards + # hung ~8 minutes in, the arm64 ones signed inside the window). + # Clear the timers before anything can go idle. + security set-keychain-settings "$KC" printf '%s' "$MACOS_CERT_P12_BASE64" | base64 -d > "$CI_TMP/cert.p12" security import "$CI_TMP/cert.p12" -k "$KC" -P "$MACOS_CERT_PASSWORD" -T /usr/bin/codesign rm -f "$CI_TMP/cert.p12" security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KCPW" "$KC" >/dev/null 2>&1 security find-identity -v -p codesigning "$KC" | grep -q "Developer ID Application" \ || { echo "imported p12 yielded no valid Developer ID Application identity"; exit 1; } - # The keychain is the search list for this step (listed above); - # the trap restores the baseline when the step ends. - trap 'rm -rf "$CI_TMP"; security delete-keychain "$KC" 2>/dev/null || true; security list-keychains -d user -s "$HOME/Library/Keychains/login.keychain-db"' EXIT + # The keychain leads the search list for this step (prepended + # above); the trap removes only it when the step ends. + trap 'rm -rf "$CI_TMP"; security delete-keychain "$KC" 2>/dev/null || true; REST="$(security list-keychains -d user | tr -d "\"" | grep -v "$KC" || true)"; [ -n "$REST" ] && security list-keychains -d user -s $REST || true' EXIT export APPLE_SIGNING_KEYCHAIN="$KC" ;; esac @@ -1378,18 +1399,28 @@ jobs: [ -n "${!v:-}" ] || { echo "missing $v secret (mac signing provisioning)"; exit 1; } done # Ephemeral keychain, alive for exactly this step (trap cleans - # up + restores the search list). The login keychain is - # deliberately left OFF the search list for the job: any copy - # of the identity it happens to hold must never satisfy (or - # shadow) the lookup — that is precisely the host-state - # dependency this provisioning replaces. + # up + removes it from the search list). The keychain is + # PREPENDED to the list, never swapped in as the whole list: + # concurrent darwin jobs share this runner user's keychain + # domain, and a replacing write (or a trap restoring login) + # takes the list back from under a sibling build that is still + # signing. Ours leads, so the identity found is THIS copy, + # imported from this step's p12 - not some host-state copy the + # login keychain happens to hold. CI_TMP="$(mktemp -d)" KC="$CI_TMP/ci-build.keychain-db" KCPW="$(/usr/bin/openssl rand -hex 16)" - trap 'rm -rf "$CI_TMP"; security delete-keychain "$KC" 2>/dev/null || true; security list-keychains -d user -s "$HOME/Library/Keychains/login.keychain-db"' EXIT + trap 'rm -rf "$CI_TMP"; security delete-keychain "$KC" 2>/dev/null || true; REST="$(security list-keychains -d user | tr -d "\"" | grep -v "$KC" || true)"; [ -n "$REST" ] && security list-keychains -d user -s $REST || true' EXIT security create-keychain -p "$KCPW" "$KC" - security list-keychains -d user -s "$KC" + security list-keychains -d user -s "$KC" $(security list-keychains -d user | tr -d '"') security unlock-keychain -p "$KCPW" "$KC" + # A fresh keychain defaults to locking after five idle minutes. + # A slow cargo build reaches its codesign calls well past that + # and codesign then waits forever for an unlock no headless + # runner can answer (run 40328004030: both darwin-x64 shards + # hung ~8 minutes in, the arm64 ones signed inside the window). + # Clear the timers before anything can go idle. + security set-keychain-settings "$KC" printf '%s' "$MACOS_CERT_P12_BASE64" | base64 -d > "$CI_TMP/cert.p12" security import "$CI_TMP/cert.p12" -k "$KC" -P "$MACOS_CERT_PASSWORD" -T /usr/bin/codesign rm -f "$CI_TMP/cert.p12" @@ -1562,30 +1593,41 @@ jobs: # time), and a release helper must carry the Developer ID, not an # ad-hoc signature, because the permission grant follows the # cdhash. Build a second keychain for exactly that one call and - # make it the SOLE entry of the user's search list, the same way - # the CLI shards provision theirs above: codesign resolves a - # named identity through the search list, not the --keychain - # argument (probe run 40325026239 on this image: the same - # keychain failed "item could not be found" while unlisted and - # signed cleanly once listed). Login stays off the list exactly - # as on the CLI path. When Tauri's bundle-time keychain takes the - # list over later, both hold the SAME identity from the SAME p12, - # so the tauri#11992 trap - DIFFERENT identities in one view - - # cannot bite. The trap restores the baseline at step end. + # PREPEND it to the user's search list, the same way the CLI + # shards provision theirs above: codesign resolves a named + # identity through the search list, not the --keychain argument + # (probe run 40325026239 on this image: the same keychain failed + # "item could not be found" while unlisted and signed cleanly + # once listed). Prepend, never own: concurrent shards share this + # runner user's keychain domain, and a write replacing the whole + # list (or a trap restoring login) takes it back from under a + # build still running - ours leads, so codesign finds THIS copy. + # When Tauri's bundle-time keychain takes the list over later, + # both hold the SAME identity from the SAME p12, so the + # tauri#11992 trap - DIFFERENT identities in one view - cannot + # bite. The trap removes only this keychain from the list at + # step end. KC="$CI_TMP/helper-sign.keychain-db" KCPW="$(/usr/bin/openssl rand -hex 16)" security create-keychain -p "$KCPW" "$KC" - security list-keychains -d user -s "$KC" + security list-keychains -d user -s "$KC" $(security list-keychains -d user | tr -d '"') security unlock-keychain -p "$KCPW" "$KC" + # A fresh keychain defaults to locking after five idle minutes. + # A slow cargo build reaches its codesign calls well past that + # and codesign then waits forever for an unlock no headless + # runner can answer (run 40328004030: both darwin-x64 shards + # hung ~8 minutes in, the arm64 ones signed inside the window). + # Clear the timers before anything can go idle. + security set-keychain-settings "$KC" printf '%s' "$MACOS_CERT_P12_BASE64" | base64 -d > "$CI_TMP/cert.p12" security import "$CI_TMP/cert.p12" -k "$KC" -P "$MACOS_CERT_PASSWORD" -T /usr/bin/codesign rm -f "$CI_TMP/cert.p12" security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KCPW" "$KC" >/dev/null 2>&1 security find-identity -v -p codesigning "$KC" | grep -q "Developer ID Application" \ || { echo "imported p12 yielded no valid Developer ID Application identity"; exit 1; } - # The keychain is the search list for this step (listed above); - # the trap restores the baseline when the step ends. - trap 'rm -rf "$CI_TMP"; security delete-keychain "$KC" 2>/dev/null || true; security list-keychains -d user -s "$HOME/Library/Keychains/login.keychain-db"' EXIT + # The keychain leads the search list for this step (prepended + # above); the trap removes only it when the step ends. + trap 'rm -rf "$CI_TMP"; security delete-keychain "$KC" 2>/dev/null || true; REST="$(security list-keychains -d user | tr -d "\"" | grep -v "$KC" || true)"; [ -n "$REST" ] && security list-keychains -d user -s $REST || true' EXIT export APPLE_SIGNING_KEYCHAIN="$KC" ;; esac diff --git a/docs/release-and-ci.md b/docs/release-and-ci.md index 829796d90..af9e02229 100644 --- a/docs/release-and-ci.md +++ b/docs/release-and-ci.md @@ -765,16 +765,22 @@ the experimental probe. That signing runs during `cargo build`, before Tauri builds its own throwaway keychain at bundle time, so the desktop shards build a dedicated keychain for it and hand its path to the build script through `APPLE_SIGNING_KEYCHAIN`. The -shard then makes that keychain the sole entry of the user's search list, exactly -as the CLI darwin shards provision theirs: `codesign` resolves a named identity -through the search list, not the `--keychain` argument, and on this runner image -`security create-keychain` does not add the keychain to that list. Run -40306776766 failed with "item could not be found" for exactly that reason, and -probe run 40325026239 on the same image confirmed the split: the same keychain -refused the identity while unlisted and signed cleanly once listed. When Tauri's -bundle-time keychain takes the list over later it carries the same single -identity from the same p12, so the two views are never in conflict, and the -step's trap restores the baseline search list when it ends. +shard then PREPENDS that keychain to the user's search list: `codesign` resolves +a named identity through the search list, not the `--keychain` argument, and on +this runner image `security create-keychain` does not add the keychain to that +list (run 40306776766 failed with "item could not be found"; probe run +40325026239 confirmed the split: the same keychain refused the identity while +unlisted and signed cleanly once listed). Prepend, never own: the CLI shards +share this runner user's keychain domain, and a write that replaces the whole +list (or an EXIT trap restoring login) takes it back from under a build that is +still running. Ours leads, so the identity codesign finds is the copy imported +from this step's p12. A fresh keychain also locks after five idle minutes by +default, which a cargo build reaches its signing calls past (run 40328004030: +both `darwin-x64` desktop shards hung with codesign waiting on an unlock no +headless runner can answer); a bare `security set-keychain-settings` clears the +timers. When Tauri's bundle-time keychain takes the list over later it carries +the same single identity from the same p12, so the two views are never in +conflict. The trap removes only this step's keychain from the list when it ends. The helper has its own `Info.plist` version, which must increase when its code changes. Both apps reuse the newest installed helper; neither may downgrade it diff --git a/scripts/smoke-desktop-bundle.sh b/scripts/smoke-desktop-bundle.sh index f222b717a..7795c7ff4 100755 --- a/scripts/smoke-desktop-bundle.sh +++ b/scripts/smoke-desktop-bundle.sh @@ -296,7 +296,10 @@ done HELPER_BUNDLE="$APP_BUNDLE/Contents/Helpers/Subshell Display Helper.app" [ -x "$HELPER_BUNDLE/Contents/MacOS/subshell-display-helper" ] || fail "the native display helper is missing" -codesign --verify --strict -R 'anchor apple generic and identifier "dev.subshell.display-helper" and certificate leaf[subject.OU] = "DVRYX6BV6J"' "$HELPER_BUNDLE" || fail "the display helper has the wrong signing identity" +# -R must carry the requirement as -R=: the space-separated -R +# form is ALWAYS read as a file path, so it fails "invalid requirement +# specification" on every run no matter what signed the helper. +codesign --verify --strict '-R=anchor apple generic and identifier "dev.subshell.display-helper" and certificate leaf[subject.OU] = "DVRYX6BV6J"' "$HELPER_BUNDLE" || fail "the display helper has the wrong signing identity" helper_sig="$(codesign -dvv "$HELPER_BUNDLE" 2>&1 || true)" case "$helper_sig" in *"flags="*"runtime"*) ;; *) fail "the display helper is missing the hardened runtime" ;; esac