From 37ebc3a8597dab22963cc0d7b66a5dd740032a28 Mon Sep 17 00:00:00 2001 From: offyotto Date: Sun, 7 Jun 2026 08:23:36 +0500 Subject: [PATCH] Fix Developer ID DNS proxy signing --- Locale.xcodeproj/project.pbxproj | 126 ++++++++++++++++++ .../xcschemes/LocaleAppDirect.xcscheme | 77 +++++++++++ Resources/ExportOptions-DeveloperID.plist | 16 +++ Resources/LocaleDNSProxyDirect.entitlements | 16 +++ Resources/LocaleDirect.entitlements | 20 +++ docs/RELEASING.md | 29 ++++ script/export_developer_id.sh | 100 ++++++++++++++ script/package_notarized.sh | 38 +++++- 8 files changed, 420 insertions(+), 2 deletions(-) create mode 100644 Locale.xcodeproj/xcshareddata/xcschemes/LocaleAppDirect.xcscheme create mode 100644 Resources/ExportOptions-DeveloperID.plist create mode 100644 Resources/LocaleDNSProxyDirect.entitlements create mode 100644 Resources/LocaleDirect.entitlements create mode 100755 script/export_developer_id.sh diff --git a/Locale.xcodeproj/project.pbxproj b/Locale.xcodeproj/project.pbxproj index eb9caef..c84a671 100644 --- a/Locale.xcodeproj/project.pbxproj +++ b/Locale.xcodeproj/project.pbxproj @@ -100,6 +100,8 @@ 30F000000000000000000002 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 40F000000000000000000001 /* Locale.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Locale.entitlements; sourceTree = ""; }; 40F000000000000000000002 /* LocaleDNSProxy.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = LocaleDNSProxy.entitlements; sourceTree = ""; }; + 40F000000000000000000007 /* LocaleDirect.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = LocaleDirect.entitlements; sourceTree = ""; }; + 40F000000000000000000008 /* LocaleDNSProxyDirect.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = LocaleDNSProxyDirect.entitlements; sourceTree = ""; }; 40F000000000000000000003 /* AppIcon.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = AppIcon.icns; sourceTree = ""; }; 40F000000000000000000004 /* Assets.car */ = {isa = PBXFileReference; lastKnownFileType = file; path = Assets.car; sourceTree = ""; }; 40F000000000000000000005 /* Locale-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Locale-Info.plist"; sourceTree = ""; }; @@ -203,6 +205,8 @@ 40F000000000000000000006 /* LocaleDNSProxy-Info.plist */, 40F000000000000000000001 /* Locale.entitlements */, 40F000000000000000000002 /* LocaleDNSProxy.entitlements */, + 40F000000000000000000007 /* LocaleDirect.entitlements */, + 40F000000000000000000008 /* LocaleDNSProxyDirect.entitlements */, 40F000000000000000000003 /* AppIcon.icns */, 40F000000000000000000004 /* Assets.car */, ); @@ -517,6 +521,59 @@ }; name = Release; }; + 80C000000000000000000003 /* Direct */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 14.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-Osize"; + SWIFT_VERSION = 5.0; + }; + name = Direct; + }; 81C000000000000000000001 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -565,6 +622,31 @@ }; name = Release; }; + 81C000000000000000000003 /* Direct */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = Resources/LocaleDirect.entitlements; + CODE_SIGNING_ALLOWED = NO; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = "20260603.2"; + DEVELOPMENT_TEAM = 6VDP675K4L; + ENABLE_HARDENED_RUNTIME = YES; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = "Resources/Locale-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MARKETING_VERSION = 1.0.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.offyotto.Locale; + PRODUCT_MODULE_NAME = LocaleApp; + PRODUCT_NAME = Locale; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = NO; + SWIFT_VERSION = 5.0; + }; + name = Direct; + }; 82C000000000000000000001 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -589,6 +671,18 @@ }; name = Release; }; + 82C000000000000000000003 /* Direct */ = { + isa = XCBuildConfiguration; + buildSettings = { + DEFINES_MODULE = YES; + MACH_O_TYPE = staticlib; + PRODUCT_MODULE_NAME = LocaleShared; + PRODUCT_NAME = LocaleShared; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = Direct; + }; 83C000000000000000000001 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -643,6 +737,34 @@ }; name = Release; }; + 83C000000000000000000003 /* Direct */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = Resources/LocaleDNSProxyDirect.entitlements; + CODE_SIGNING_ALLOWED = NO; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = "20260603.2"; + DEVELOPMENT_TEAM = 6VDP675K4L; + ENABLE_HARDENED_RUNTIME = YES; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = "Resources/LocaleDNSProxy-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../../../Frameworks", + ); + MARKETING_VERSION = 1.0.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.offyotto.Locale.LocaleDNSProxy; + PRODUCT_MODULE_NAME = LocaleDNSProxy; + PRODUCT_NAME = LocaleDNSProxy; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + WRAPPER_NAME = dev.offyotto.Locale.LocaleDNSProxy.systemextension; + WRAPPER_EXTENSION = systemextension; + }; + name = Direct; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -651,6 +773,7 @@ buildConfigurations = ( 80C000000000000000000001 /* Debug */, 80C000000000000000000002 /* Release */, + 80C000000000000000000003 /* Direct */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -660,6 +783,7 @@ buildConfigurations = ( 81C000000000000000000001 /* Debug */, 81C000000000000000000002 /* Release */, + 81C000000000000000000003 /* Direct */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -669,6 +793,7 @@ buildConfigurations = ( 82C000000000000000000001 /* Debug */, 82C000000000000000000002 /* Release */, + 82C000000000000000000003 /* Direct */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -678,6 +803,7 @@ buildConfigurations = ( 83C000000000000000000001 /* Debug */, 83C000000000000000000002 /* Release */, + 83C000000000000000000003 /* Direct */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; diff --git a/Locale.xcodeproj/xcshareddata/xcschemes/LocaleAppDirect.xcscheme b/Locale.xcodeproj/xcshareddata/xcschemes/LocaleAppDirect.xcscheme new file mode 100644 index 0000000..dbbd347 --- /dev/null +++ b/Locale.xcodeproj/xcshareddata/xcschemes/LocaleAppDirect.xcscheme @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Resources/ExportOptions-DeveloperID.plist b/Resources/ExportOptions-DeveloperID.plist new file mode 100644 index 0000000..03ea090 --- /dev/null +++ b/Resources/ExportOptions-DeveloperID.plist @@ -0,0 +1,16 @@ + + + + + destination + export + method + developer-id + signingStyle + automatic + stripSwiftSymbols + + teamID + 6VDP675K4L + + diff --git a/Resources/LocaleDNSProxyDirect.entitlements b/Resources/LocaleDNSProxyDirect.entitlements new file mode 100644 index 0000000..d29fd7b --- /dev/null +++ b/Resources/LocaleDNSProxyDirect.entitlements @@ -0,0 +1,16 @@ + + + + + com.apple.developer.networking.networkextension + + dns-proxy-systemextension + + com.apple.security.app-sandbox + + com.apple.security.application-groups + + group.dev.offyotto.Locale + + + diff --git a/Resources/LocaleDirect.entitlements b/Resources/LocaleDirect.entitlements new file mode 100644 index 0000000..1ba47a5 --- /dev/null +++ b/Resources/LocaleDirect.entitlements @@ -0,0 +1,20 @@ + + + + + com.apple.developer.networking.networkextension + + dns-proxy-systemextension + + com.apple.developer.system-extension.install + + com.apple.security.app-sandbox + + com.apple.security.application-groups + + group.dev.offyotto.Locale + + com.apple.security.files.user-selected.read-write + + + diff --git a/docs/RELEASING.md b/docs/RELEASING.md index b4fa85b..4fd9c6e 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -19,6 +19,31 @@ Open `Locale.xcodeproj`, select the `LocaleApp` scheme, and archive that scheme. The app and `LocaleDNSProxy` targets use automatic signing with the development team `6VDP675K4L`. +For TestFlight or App Store submission, choose **App Store Connect** in +Organizer. Do not choose Direct Distribution for this path. Apple's Store +profiles carry the Network Extension value `dns-proxy`, which is what the +`Release` configuration uses. + +If you intentionally need a Direct Distribution archive, switch the scheme to +`LocaleAppDirect` before archiving. Direct profiles carry +`dns-proxy-systemextension`, so that scheme archives the separate `Direct` +configuration with `Resources/LocaleDirect.entitlements` and +`Resources/LocaleDNSProxyDirect.entitlements`. The Direct configuration archives +unsigned on purpose; Xcode signs it during export with the Direct/Developer ID +profiles. + +The repeatable CLI path for Developer ID export is: + +```bash +./script/export_developer_id.sh +``` + +It writes: + +```text +dist/developer-id/Locale.app +``` + Do not archive the Swift package workspace directly. Xcode treats package archives as Generic Xcode Archives because there is no installable app target owning the archive. @@ -46,6 +71,10 @@ dist/Locale-notarized.zip It also validates the stapled app with `spctl` and `xcrun stapler validate`. +The notarization script signs with the Direct entitlement files because +Developer ID and Direct system extension profiles use the `-systemextension` +Network Extension values. + ## Network Extension Notes `LocaleDNSProxy` is staged as a system extension at: diff --git a/script/export_developer_id.sh b/script/export_developer_id.sh new file mode 100755 index 0000000..f71965c --- /dev/null +++ b/script/export_developer_id.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +TEAM_ID="${TEAM_ID:-6VDP675K4L}" +ARCHIVE_PATH="${ARCHIVE_PATH:-$ROOT_DIR/dist/archives/LocaleDirect.xcarchive}" +EXPORT_PATH="${EXPORT_PATH:-$ROOT_DIR/dist/developer-id}" +EXPORT_OPTIONS="${EXPORT_OPTIONS:-$ROOT_DIR/Resources/ExportOptions-DeveloperID.plist}" +APP_BUNDLE="$EXPORT_PATH/Locale.app" +DNS_PROXY_BUNDLE_ID="dev.offyotto.Locale.LocaleDNSProxy" +DNS_PROXY_BUNDLE="$APP_BUNDLE/Contents/Library/SystemExtensions/$DNS_PROXY_BUNDLE_ID.systemextension" +APP_ENTITLEMENTS="$ROOT_DIR/Resources/LocaleDirect.entitlements" +DNS_PROXY_ENTITLEMENTS="$ROOT_DIR/Resources/LocaleDNSProxyDirect.entitlements" +APP_PROFILE_NAME="Mac Team Direct Provisioning Profile: dev.offyotto.Locale" +DNS_PROXY_PROFILE_NAME="Mac Team Direct Provisioning Profile: dev.offyotto.Locale.LocaleDNSProxy" + +find_identity() { + security find-identity -v -p codesigning \ + | sed -n 's/.*"\(Developer ID Application: .*'"$TEAM_ID"'.*\)"/\1/p' \ + | head -n 1 +} + +find_profile() { + local expected_name="$1" + local profile name tmp + + for profile in "$HOME"/Library/Developer/Xcode/UserData/Provisioning\ Profiles/*.provisionprofile; do + [[ -e "$profile" ]] || continue + tmp="$(mktemp /tmp/locale-profile.XXXXXX.plist)" + if security cms -D -i "$profile" >"$tmp" 2>/dev/null; then + name="$(/usr/libexec/PlistBuddy -c 'Print :Name' "$tmp" 2>/dev/null || true)" + rm -f "$tmp" + if [[ "$name" == "$expected_name" ]]; then + echo "$profile" + return 0 + fi + else + rm -f "$tmp" + fi + done + + return 1 +} + +cd "$ROOT_DIR" +rm -rf "$ARCHIVE_PATH" "$EXPORT_PATH" +mkdir -p "$(dirname "$ARCHIVE_PATH")" "$EXPORT_PATH" + +IDENTITY="$(find_identity)" +APP_PROFILE="$(find_profile "$APP_PROFILE_NAME" || true)" +DNS_PROXY_PROFILE="$(find_profile "$DNS_PROXY_PROFILE_NAME" || true)" + +if [[ -z "$IDENTITY" ]]; then + echo "No Developer ID Application signing identity found for team $TEAM_ID." >&2 + exit 1 +fi + +if [[ -z "$APP_PROFILE" || -z "$DNS_PROXY_PROFILE" ]]; then + echo "Missing Direct provisioning profiles. Open Xcode signing once to regenerate them." >&2 + exit 1 +fi + +xcodebuild \ + -project Locale.xcodeproj \ + -scheme LocaleAppDirect \ + -configuration Direct \ + -destination 'generic/platform=macOS' \ + -archivePath "$ARCHIVE_PATH" \ + archive + +xcodebuild \ + -exportArchive \ + -archivePath "$ARCHIVE_PATH" \ + -exportPath "$EXPORT_PATH" \ + -exportOptionsPlist "$EXPORT_OPTIONS" \ + -allowProvisioningUpdates + +cp "$APP_PROFILE" "$APP_BUNDLE/Contents/embedded.provisionprofile" +cp "$DNS_PROXY_PROFILE" "$DNS_PROXY_BUNDLE/Contents/embedded.provisionprofile" + +codesign \ + --force \ + --options runtime \ + --timestamp \ + --identifier "$DNS_PROXY_BUNDLE_ID" \ + --entitlements "$DNS_PROXY_ENTITLEMENTS" \ + --sign "$IDENTITY" \ + "$DNS_PROXY_BUNDLE" + +codesign \ + --force \ + --options runtime \ + --timestamp \ + --entitlements "$APP_ENTITLEMENTS" \ + --sign "$IDENTITY" \ + "$APP_BUNDLE" + +codesign --verify --deep --strict --verbose=2 "$APP_BUNDLE" + +echo "Developer ID export: $APP_BUNDLE" diff --git a/script/package_notarized.sh b/script/package_notarized.sh index b5852a7..1ff2ab6 100755 --- a/script/package_notarized.sh +++ b/script/package_notarized.sh @@ -10,8 +10,10 @@ DIST_DIR="$ROOT_DIR/dist" APP_BUNDLE="$DIST_DIR/$APP_NAME.app" DNS_PROXY_BUNDLE_ID="dev.offyotto.Locale.LocaleDNSProxy" DNS_PROXY_BUNDLE="$APP_BUNDLE/Contents/Library/SystemExtensions/$DNS_PROXY_BUNDLE_ID.systemextension" -APP_ENTITLEMENTS="$ROOT_DIR/Resources/Locale.entitlements" -DNS_PROXY_ENTITLEMENTS="$ROOT_DIR/Resources/LocaleDNSProxy.entitlements" +APP_ENTITLEMENTS="$ROOT_DIR/Resources/LocaleDirect.entitlements" +DNS_PROXY_ENTITLEMENTS="$ROOT_DIR/Resources/LocaleDNSProxyDirect.entitlements" +APP_PROFILE_NAME="Mac Team Direct Provisioning Profile: dev.offyotto.Locale" +DNS_PROXY_PROFILE_NAME="Mac Team Direct Provisioning Profile: dev.offyotto.Locale.LocaleDNSProxy" SIGNED_ZIP="$DIST_DIR/$APP_NAME-signed.zip" NOTARIZED_ZIP="$DIST_DIR/$APP_NAME-notarized.zip" @@ -26,6 +28,28 @@ find_identity() { | head -n 1 } +find_profile() { + local expected_name="$1" + local profile name tmp + + for profile in "$HOME"/Library/Developer/Xcode/UserData/Provisioning\ Profiles/*.provisionprofile; do + [[ -e "$profile" ]] || continue + tmp="$(mktemp /tmp/locale-profile.XXXXXX.plist)" + if security cms -D -i "$profile" >"$tmp" 2>/dev/null; then + name="$(/usr/libexec/PlistBuddy -c 'Print :Name' "$tmp" 2>/dev/null || true)" + rm -f "$tmp" + if [[ "$name" == "$expected_name" ]]; then + echo "$profile" + return 0 + fi + else + rm -f "$tmp" + fi + done + + return 1 +} + IDENTITY="$(find_identity)" if [[ -z "$IDENTITY" ]]; then cat >&2 <&2 + exit 1 +fi + cd "$ROOT_DIR" "$ROOT_DIR/script/build_and_run.sh" --build-only +cp "$APP_PROFILE" "$APP_BUNDLE/Contents/embedded.provisionprofile" +cp "$DNS_PROXY_PROFILE" "$DNS_PROXY_BUNDLE/Contents/embedded.provisionprofile" + codesign --force --options runtime --timestamp --identifier "$DNS_PROXY_BUNDLE_ID" --entitlements "$DNS_PROXY_ENTITLEMENTS" --sign "$IDENTITY" "$DNS_PROXY_BUNDLE" codesign --force --options runtime --timestamp --entitlements "$APP_ENTITLEMENTS" --sign "$IDENTITY" "$APP_BUNDLE" codesign --verify --deep --strict --verbose=2 "$APP_BUNDLE"