diff --git a/clients/apple/README.md b/clients/apple/README.md index 957c645..1c85a34 100644 --- a/clients/apple/README.md +++ b/clients/apple/README.md @@ -15,8 +15,11 @@ dotnet build clients/apple/VoiceCat.Apple.slnx -c Debug --no-restore Use `publish-macos.sh --dry-run` to validate a local ad-hoc macOS bundle. The dry-run build does not enable hardened runtime because ad-hoc signatures have no Team ID and cannot satisfy macOS -library validation. Distribution builds remain hardened and require `VOICECAT_CODESIGN_IDENTITY`; -optional notarization uses `APPLE_ID`, `APPLE_TEAM_ID`, and `APPLE_APP_PASSWORD`. +library validation. The script normalizes nested signatures and installs the verified bundle at +both `VoiceCat.Mac/bin/Release/net10.0-macos27.0/osx-arm64/VoiceCat.app` and +`VoiceCat.Mac/bin/Release/distribution/VoiceCat.app`. Distribution builds remain hardened and +require `VOICECAT_CODESIGN_IDENTITY`; optional notarization uses `APPLE_ID`, `APPLE_TEAM_ID`, and +`APPLE_APP_PASSWORD`. For a physical iOS device, use `build-ios-device.sh` and `deploy-ios-device.sh`. The host and ReplayKit extension require signing profiles with App Group `group.me.iamtalon.voicecat`. diff --git a/clients/apple/VoiceCat.Mac/VoiceCat.Mac.entitlements b/clients/apple/VoiceCat.Mac/VoiceCat.Mac.entitlements index e839509..d541e4e 100644 --- a/clients/apple/VoiceCat.Mac/VoiceCat.Mac.entitlements +++ b/clients/apple/VoiceCat.Mac/VoiceCat.Mac.entitlements @@ -2,6 +2,7 @@ com.apple.security.app-sandbox + com.apple.security.cs.allow-jit com.apple.security.network.client com.apple.security.device.audio-input com.apple.security.files.user-selected.read-write diff --git a/clients/apple/publish-macos.sh b/clients/apple/publish-macos.sh index 2f4aed1..b304051 100755 --- a/clients/apple/publish-macos.sh +++ b/clients/apple/publish-macos.sh @@ -45,11 +45,39 @@ fi mkdir -p "$distribution" ditto "$built_app" "$app" +entitlements="$script_dir/VoiceCat.Mac/VoiceCat.Mac.entitlements" + +sign_nested_code() { + local identity=$1 + local timestamp_option=${2:-} + local runtime_option=${3:-} + local nested + local sign_options=(--force) + + [[ -n "$timestamp_option" ]] && sign_options+=("$timestamp_option") + [[ -n "$runtime_option" ]] && sign_options+=("$runtime_option") + + # Do not use codesign --deep for signing. It is intended as an emergency repair operation + # and can apply the app's entitlements to nested code. Sign each bundled Mach-O library first, + # then sign the outer app once with its own entitlements. + while IFS= read -r -d '' nested; do + codesign "${sign_options[@]}" --sign "$identity" "$nested" + done < <(find "$app/Contents" -type f \( -name '*.dylib' -o -name '*.so' \) -print0) +} + +install_verified_primary_bundle() { + # dotnet publish must create the workload bundle before it can be normalized. Replace that + # misleading intermediate with the verified result so both documented output paths work. + rm -rf "$built_app" + ditto "$app" "$built_app" +} + if (( dry_run )); then - # Normalize the workload's mixed nested signatures after bundling. - codesign --force --deep --sign - "$app" + sign_nested_code - + codesign --force --entitlements "$entitlements" --sign - "$app" codesign --verify --deep --strict "$app" spctl --assess --type execute "$app" 2>/dev/null || true + install_verified_primary_bundle print "Validated ad-hoc build: $app" exit 0 fi @@ -60,10 +88,12 @@ if [[ -z "$identity" ]]; then exit 2 fi -codesign --force --deep --timestamp --options runtime \ - --entitlements "$script_dir/VoiceCat.Mac/VoiceCat.Mac.entitlements" \ +sign_nested_code "$identity" --timestamp --options=runtime +codesign --force --timestamp --options runtime \ + --entitlements "$entitlements" \ --sign "$identity" "$app" codesign --verify --deep --strict --verbose=2 "$app" +install_verified_primary_bundle archive="$distribution/VoiceCat-macOS-arm64.zip" ditto -c -k --keepParent "$app" "$archive" diff --git a/tests/VoiceCat.Tests/PublishServerScriptTests.cs b/tests/VoiceCat.Tests/PublishServerScriptTests.cs index 7721820..53b01a3 100644 --- a/tests/VoiceCat.Tests/PublishServerScriptTests.cs +++ b/tests/VoiceCat.Tests/PublishServerScriptTests.cs @@ -24,6 +24,21 @@ public class PublishServerScriptTests Assert.Contains("(?:bin|obj)", script); } + [Fact] + public async Task MacPublishPreservesAppEntitlementsAndReplacesBrokenWorkloadBundle() + { + string root = FindRoot(); + string script = await File.ReadAllTextAsync(Path.Combine(root, "clients", "apple", "publish-macos.sh")); + string entitlements = await File.ReadAllTextAsync(Path.Combine( + root, "clients", "apple", "VoiceCat.Mac", "VoiceCat.Mac.entitlements")); + + Assert.Contains("sign_nested_code", script); + Assert.Contains("--entitlements \"$entitlements\"", script); + Assert.Contains("install_verified_primary_bundle", script); + Assert.DoesNotContain("codesign --force --deep", script); + Assert.Contains("com.apple.security.cs.allow-jit", entitlements); + } + [Fact] public async Task DefaultPublishTargetsWindowsAndLinuxWhileRuntimeCanSelectOne() {