Inside-out signing with per-binary entitlements to restore passkeys (#2905)

The AuthenticationServices WebAuthn / passkey path on Developer ID
Mac apps requires com.apple.application-identifier in the codesigned
entitlements blob of the calling process. AS does not fall back to
the embedded provisioning profile and does not derive the app id
from team-identifier + bundle-id; without that key the ceremony
fails with AuthorizationError 1004.

PR #2727's nightly build signed every binary inside the bundle
(main app plus Contents/Resources/bin/cmux and Contents/Resources/bin/ghostty)
with the same cmux.entitlements. Injecting application-identifier
into that shared file and re-applying it with --deep pushed the main
app's app id onto the CLI helpers, whose code identifiers are 'cmux'
and 'ghostty'. That mismatch is what amfi on notarized macOS 26
Tahoe rejects with Launchd job spawn failed / errno 163. Local
un-notarized Developer-ID builds survived because amfi is less
strict on those.

Split into two entitlements files and sign inside-out:

- cmux-helper.entitlements: minimal hardened-runtime only
  (disable-library-validation, allow-jit, allow-unsigned-executable-memory).
  Used for CLI helpers, which never need app-id, WebAuthn, camera,
  mic, or AppleEvents.
- cmux.entitlements: unchanged in the tree (shared base, no app-id).
  At sign time the workflow copies it and injects application-identifier
  and team-identifier for the bundle being built
  (com.cmuxterm.app for release, com.cmuxterm.app.nightly for nightly).

Signing order in both workflows:

1. CLI helpers (bin/cmux, bin/ghostty) signed with cmux-helper.entitlements.
2. Main app bundle signed last with the full injected entitlements,
   WITHOUT --deep. --deep would overwrite the helper signatures and
   re-propagate the mismatch.

release.yml also gains the same embedded provisioning profile step
the nightly gained in #2727, so the shipped com.cmuxterm.app build
is authorized to use the WebAuthn browser entitlement too.

Verified locally on macOS 26.3.1: re-signed the previous broken
nightly bundle using this scheme, it launches, passkey ceremony
reaches ASAuthorizationController.performRequests without the 1004
error.

Co-authored-by: Lawrence Chen <[email protected]>
This commit is contained in:
Lawrence Chen
2026-04-14 20:52:15 -07:00
committed by GitHub
co-authored by Lawrence Chen
parent 1085927035
commit 02f741c32a
3 changed files with 98 additions and 8 deletions
+33 -4
View File
@@ -432,21 +432,50 @@ jobs:
echo "Missing APPLE_SIGNING_IDENTITY secret" >&2
exit 1
fi
ENTITLEMENTS="cmux.entitlements"
# Inside-out signing with per-binary entitlements:
# - CLI helpers get a minimal hardened-runtime entitlements file
# (no application-identifier). If we gave them the main app's
# application-identifier, amfi rejects the bundle at launch on
# macOS 26 Tahoe because the helper's signing identifier
# ("cmux" / "ghostty") doesn't match the claimed app id
# (7WLXT3NR37.com.cmuxterm.app.nightly).
# - Main app bundle gets the full entitlements including an
# injected application-identifier, which AuthenticationServices
# requires for passkey / WebAuthn ceremonies
# (ASAuthorizationError 1004 otherwise).
# - No --deep on the top-level sign: it would overwrite the
# helper signatures with the main entitlements and reintroduce
# the mismatch.
HELPER_ENT="cmux-helper.entitlements"
NIGHTLY_APP_ENT="$(mktemp /tmp/cmux-nightly-app-ent.XXXXXX.plist)"
trap 'rm -f "$NIGHTLY_APP_ENT"' EXIT
cp cmux.entitlements "$NIGHTLY_APP_ENT"
/usr/libexec/PlistBuddy -c "Delete :com.apple.application-identifier" "$NIGHTLY_APP_ENT" >/dev/null 2>&1 || true
/usr/libexec/PlistBuddy -c "Delete :com.apple.developer.team-identifier" "$NIGHTLY_APP_ENT" >/dev/null 2>&1 || true
/usr/libexec/PlistBuddy -c "Add :com.apple.application-identifier string 7WLXT3NR37.com.cmuxterm.app.nightly" "$NIGHTLY_APP_ENT"
/usr/libexec/PlistBuddy -c "Add :com.apple.developer.team-identifier string 7WLXT3NR37" "$NIGHTLY_APP_ENT"
for APP_PATH in \
"build-universal/Build/Products/Release/cmux NIGHTLY.app"
do
CLI_PATH="$APP_PATH/Contents/Resources/bin/cmux"
HELPER_PATH="$APP_PATH/Contents/Resources/bin/ghostty"
if [ -f "$CLI_PATH" ]; then
/usr/bin/codesign --force --options runtime --timestamp --sign "$APPLE_SIGNING_IDENTITY" --entitlements "$ENTITLEMENTS" "$CLI_PATH"
/usr/bin/codesign --force --options runtime --timestamp --sign "$APPLE_SIGNING_IDENTITY" --entitlements "$HELPER_ENT" "$CLI_PATH"
fi
if [ -f "$HELPER_PATH" ]; then
/usr/bin/codesign --force --options runtime --timestamp --sign "$APPLE_SIGNING_IDENTITY" --entitlements "$ENTITLEMENTS" "$HELPER_PATH"
/usr/bin/codesign --force --options runtime --timestamp --sign "$APPLE_SIGNING_IDENTITY" --entitlements "$HELPER_ENT" "$HELPER_PATH"
fi
/usr/bin/codesign --force --options runtime --timestamp --sign "$APPLE_SIGNING_IDENTITY" --entitlements "$ENTITLEMENTS" --deep "$APP_PATH"
/usr/bin/codesign --force --options runtime --timestamp --sign "$APPLE_SIGNING_IDENTITY" --entitlements "$NIGHTLY_APP_ENT" "$APP_PATH"
/usr/bin/codesign --verify --deep --strict --verbose=2 "$APP_PATH"
/usr/bin/codesign -d --entitlements :- "$APP_PATH" 2>&1 | grep -q "com.apple.developer.web-browser.public-key-credential"
/usr/bin/codesign -d --entitlements :- "$APP_PATH" 2>&1 | grep -q "7WLXT3NR37.com.cmuxterm.app.nightly"
# Assert helpers do NOT carry the main app's application-identifier
if [ -f "$CLI_PATH" ]; then
/usr/bin/codesign -d --entitlements :- "$CLI_PATH" 2>&1 | grep -q "application-identifier" && {
echo "error: CLI helper unexpectedly carries application-identifier" >&2
exit 1
} || true
fi
done
- name: Notarize apps and dmgs
+53 -4
View File
@@ -247,6 +247,39 @@ jobs:
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" build.keychain
security list-keychains -d user -s build.keychain
- name: Embed release provisioning profile
if: steps.guard_release_assets.outputs.skip_all != 'true'
env:
APPLE_RELEASE_PROVISIONING_PROFILE_BASE64: ${{ secrets.APPLE_RELEASE_PROVISIONING_PROFILE_BASE64 }}
run: |
if [ -z "$APPLE_RELEASE_PROVISIONING_PROFILE_BASE64" ]; then
echo "Missing APPLE_RELEASE_PROVISIONING_PROFILE_BASE64 secret" >&2
exit 1
fi
APP_PATH="build-universal/Build/Products/Release/cmux.app"
PROFILE_PATH="$APP_PATH/Contents/embedded.provisionprofile"
TMP_PROFILE="$(mktemp /tmp/cmux-release-profile.XXXXXX)"
TMP_PLIST="$(mktemp /tmp/cmux-release-profile.XXXXXX.plist)"
trap 'rm -f "$TMP_PROFILE" "$TMP_PLIST"' EXIT
echo "$APPLE_RELEASE_PROVISIONING_PROFILE_BASE64" | base64 --decode > "$TMP_PROFILE"
security cms -D -i "$TMP_PROFILE" > "$TMP_PLIST"
APP_ID="$(/usr/libexec/PlistBuddy -c "Print :Entitlements:com.apple.application-identifier" "$TMP_PLIST")"
if [ "$APP_ID" != "7WLXT3NR37.com.cmuxterm.app" ]; then
echo "Release provisioning profile targets unexpected app ID: $APP_ID" >&2
exit 1
fi
WEBAUTHN_ENTITLEMENT="$(/usr/libexec/PlistBuddy -c "Print :Entitlements:com.apple.developer.web-browser.public-key-credential" "$TMP_PLIST")"
if [ "$WEBAUTHN_ENTITLEMENT" != "true" ]; then
echo "Release provisioning profile missing WebAuthn browser entitlement" >&2
exit 1
fi
PROVISIONS_ALL_DEVICES="$(/usr/libexec/PlistBuddy -c "Print :ProvisionsAllDevices" "$TMP_PLIST")"
if [ "$PROVISIONS_ALL_DEVICES" != "true" ]; then
echo "Release provisioning profile is not a Developer ID all-devices profile" >&2
exit 1
fi
cp "$TMP_PROFILE" "$PROFILE_PATH"
- name: Codesign app
if: steps.guard_release_assets.outputs.skip_all != 'true'
env:
@@ -257,17 +290,33 @@ jobs:
exit 1
fi
APP_PATH="build-universal/Build/Products/Release/cmux.app"
ENTITLEMENTS="cmux.entitlements"
# Inside-out signing (see docs/signing.md reasoning also in
# nightly.yml): CLI helpers get minimal hardened-runtime
# entitlements, main app bundle gets the full entitlements with
# application-identifier injected so AuthenticationServices can
# serve passkey / WebAuthn ceremonies. No --deep on the top
# sign: it would overwrite helper signatures and re-introduce
# the amfi launch rejection seen in the nightly.
HELPER_ENT="cmux-helper.entitlements"
RELEASE_APP_ENT="$(mktemp /tmp/cmux-release-app-ent.XXXXXX.plist)"
trap 'rm -f "$RELEASE_APP_ENT"' EXIT
cp cmux.entitlements "$RELEASE_APP_ENT"
/usr/libexec/PlistBuddy -c "Delete :com.apple.application-identifier" "$RELEASE_APP_ENT" >/dev/null 2>&1 || true
/usr/libexec/PlistBuddy -c "Delete :com.apple.developer.team-identifier" "$RELEASE_APP_ENT" >/dev/null 2>&1 || true
/usr/libexec/PlistBuddy -c "Add :com.apple.application-identifier string 7WLXT3NR37.com.cmuxterm.app" "$RELEASE_APP_ENT"
/usr/libexec/PlistBuddy -c "Add :com.apple.developer.team-identifier string 7WLXT3NR37" "$RELEASE_APP_ENT"
CLI_PATH="$APP_PATH/Contents/Resources/bin/cmux"
HELPER_PATH="$APP_PATH/Contents/Resources/bin/ghostty"
if [ -f "$CLI_PATH" ]; then
/usr/bin/codesign --force --options runtime --timestamp --sign "$APPLE_SIGNING_IDENTITY" --entitlements "$ENTITLEMENTS" "$CLI_PATH"
/usr/bin/codesign --force --options runtime --timestamp --sign "$APPLE_SIGNING_IDENTITY" --entitlements "$HELPER_ENT" "$CLI_PATH"
fi
if [ -f "$HELPER_PATH" ]; then
/usr/bin/codesign --force --options runtime --timestamp --sign "$APPLE_SIGNING_IDENTITY" --entitlements "$ENTITLEMENTS" "$HELPER_PATH"
/usr/bin/codesign --force --options runtime --timestamp --sign "$APPLE_SIGNING_IDENTITY" --entitlements "$HELPER_ENT" "$HELPER_PATH"
fi
/usr/bin/codesign --force --options runtime --timestamp --sign "$APPLE_SIGNING_IDENTITY" --entitlements "$ENTITLEMENTS" --deep "$APP_PATH"
/usr/bin/codesign --force --options runtime --timestamp --sign "$APPLE_SIGNING_IDENTITY" --entitlements "$RELEASE_APP_ENT" "$APP_PATH"
/usr/bin/codesign --verify --deep --strict --verbose=2 "$APP_PATH"
/usr/bin/codesign -d --entitlements :- "$APP_PATH" 2>&1 | grep -q "com.apple.developer.web-browser.public-key-credential"
/usr/bin/codesign -d --entitlements :- "$APP_PATH" 2>&1 | grep -q "7WLXT3NR37.com.cmuxterm.app"
- name: Notarize app
if: steps.guard_release_assets.outputs.skip_all != 'true'
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
</dict>
</plist>