diff --git a/CHANGELOG.md b/CHANGELOG.md index 936fcd0..4fc38c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased +## v2.4.2 - 2026-07-29 + +- Remove the protected time-sensitive notification entitlement from ad-hoc, unsigned-package, and Developer ID build paths until a provisioning profile can authorize it; this fixes the macOS `OS_REASON_EXEC` launch rejection introduced in v2.4.1. +- Keep automatic-backup failures audible and compatible with macOS's persistent alert style, and elevate them to time-sensitive only when the running app can prove that its signature actually carries the authorized entitlement. +- Add a real isolated menu-bar launch smoke test and a mutated-package regression test, so both macOS execution and release verification reject the exact launch-blocking v2.4.1 failure shape. + ## v2.4.1 - 2026-07-29 - Mark automatic-backup failure alerts as time-sensitive and preserve the required signing entitlement across source installs, local builds, and release packages, so Focus may present the warning without the app taking focus or opening a window. diff --git a/Makefile b/Makefile index 5e3ff5f..72d745f 100644 --- a/Makefile +++ b/Makefile @@ -16,8 +16,7 @@ OBJC_FLAGS := -fobjc-arc -Wall -Wextra -Werror MACOS_DEPLOYMENT_TARGET ?= 13.0 APP_ARCH_FLAGS ?= -arch arm64 -arch x86_64 APP_OBJC_FLAGS := $(OBJC_FLAGS) -mmacosx-version-min=$(MACOS_DEPLOYMENT_TARGET) $(APP_ARCH_FLAGS) -USER_NOTIFICATIONS_FRAMEWORK := -framework UserNotifications -APP_ENTITLEMENTS := macos/GDriveBackupTiger/GDriveBackupTiger.entitlements +USER_NOTIFICATIONS_FRAMEWORK := -framework UserNotifications -framework Security .PHONY: build install dry-run pkg test clean @@ -40,7 +39,10 @@ build: test -s "$(APP_DIR)/Contents/Resources/Assets.car"; \ ./scripts/trash-path.sh "$$ICON_WORK" xattr -cr "$(APP_DIR)" - codesign --force --deep --entitlements "$(APP_ENTITLEMENTS)" --sign - "$(APP_DIR)" + # Restricted notification entitlements require an Apple-issued signing + # identity. Embedding one in an ad-hoc signature passes codesign verification + # but macOS rejects the app at exec time. + codesign --force --deep --sign - "$(APP_DIR)" install: ./install.sh @@ -65,6 +67,7 @@ test: bash tests/launch-agent-safety-test.sh bash tests/release-metadata-test.sh bash tests/release-workflow-test.sh + bash tests/package-entitlement-safety-test.sh bash tests/app-build-artifacts-test.sh bash tests/update-flow-safety-test.sh @set -e; RUN_STATE_TEST_BIN="$$(/usr/bin/mktemp "$${TMPDIR:-/tmp}/gdrive-run-state-test.XXXXXX")"; \ diff --git a/README.md b/README.md index 99a8309..04d8411 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ macOS launchd backup setup for Google Drive, powered by `rclone`, with a tiny Mac OS X Tiger-inspired status window. “Tiger” describes the visual style; the app requires macOS 13 Ventura or later and does not run on Mac OS X 10.4 Tiger. -Current release: `v2.4.1` with time-sensitive persistent automatic-failure alerts and a safe retry for transient NAS read failures, passive handling of unknown external disks, verified APFS and NAS identity, one coherent Dock presence, optional end-to-end `rclone crypt` backups, retained versions, verified recovery, named profiles, diagnostics, and a persistent menu bar overview. +Current release: `v2.4.2` with launch-safe, audible persistent automatic-failure alerts and a safe retry for transient NAS read failures, passive handling of unknown external disks, verified APFS and NAS identity, one coherent Dock presence, optional end-to-end `rclone crypt` backups, retained versions, verified recovery, named profiles, diagnostics, and a persistent menu bar overview. It backs up: @@ -87,7 +87,7 @@ rclone lsd gdrive: For most users, download the latest installer from the GitHub releases page: 1. Open -2. Download `GDrive-Backup-Tiger-2.4.1.pkg` from `Assets`. +2. Download `GDrive-Backup-Tiger-2.4.2.pkg` from `Assets`. 3. Double-click the package and follow the macOS Installer. 4. Open `/Applications/GDrive Backup Tiger.app` to choose language, external disk, NAS, and schedule settings. @@ -104,13 +104,13 @@ The package is currently unsigned because the project does not yet have an Apple 1. Click `Done`, not `Move to Trash`. 2. Open `System Settings > Privacy & Security`. -3. Scroll to `Security` and click `Open Anyway` for `GDrive-Backup-Tiger-2.4.1.pkg`. +3. Scroll to `Security` and click `Open Anyway` for `GDrive-Backup-Tiger-2.4.2.pkg`. 4. Confirm with `Open Anyway`, then install the package. Advanced users can also remove the download quarantine flag before opening: ```bash -xattr -d com.apple.quarantine "$HOME/Downloads/GDrive-Backup-Tiger-2.4.1.pkg" +xattr -d com.apple.quarantine "$HOME/Downloads/GDrive-Backup-Tiger-2.4.2.pkg" ``` ### Install from source @@ -240,7 +240,7 @@ Legacy profiles without the key retain exact-path behavior. Merely opening setup Saved schedules run unattended after the script verifies the configured destination. `GDRIVE_BACKUP_CONFIRM=1` still protects mount-triggered runs with a prompt. Set it to `0` only if you also deliberately want those mount-triggered backups to start unattended whenever the configured volume is mounted. Set `GDRIVE_BACKUP_PAUSED=1` to silence schedule and mount-triggered runs without changing the saved schedule. The menu bar toggles this setting; **Backup now** always remains manual and available. Set `GDRIVE_BACKUP_NOTIFY_FAILURES=0` to disable macOS alerts for automatic failures and missed daily runs. The menu bar and overview continue to show backup status even when alerts are disabled or macOS notification permission is denied. -To keep a failure visible until a person dismisses it or a later automatic backup succeeds, set the macOS notification style for **GDrive Backup Tiger** to **Persistent** (`System Settings` → `Notifications` → `GDrive Backup Tiger`). Leave **Time Sensitive Notifications** enabled there so macOS may present backup failures during Focus. macOS controls both presentation settings; the app never opens a modal window or takes foreground focus for an automatic failure. A manual success deliberately leaves the alert in place. +To keep a failure visible until a person dismisses it or a later automatic backup succeeds, set the macOS notification style for **GDrive Backup Tiger** to **Persistent** (`System Settings` → `Notifications` → `GDrive Backup Tiger`). macOS controls this presentation setting; the app never opens a modal window or takes foreground focus for an automatic failure. A manual success deliberately leaves the alert in place. The public unsigned package uses normal active notification delivery and therefore does not bypass Focus. A future properly provisioned signed build may elevate the same alert to time-sensitive delivery only when macOS confirms the required protected entitlement. For NAS targets, `GDRIVE_BACKUP_NAS_MOUNT_TIMEOUT_SECONDS` bounds the macOS mount request (default `90`, range `1`–`300`) and `GDRIVE_BACKUP_NAS_READY_TIMEOUT_SECONDS` bounds the subsequent verified-writable readiness wait (default `60`, range `0`–`300`). Set `GDRIVE_BACKUP_AUTO_CREATE_VOLUME=0` if you want to create the backup volume yourself. Set `GDRIVE_BACKUP_NAS_START_ON_MOUNT=1` only if mount events should also start the configured NAS backup; the default `0` reserves mount-triggered runs for the external APFS target. diff --git a/docs/version-history.md b/docs/version-history.md index 2939635..806925f 100644 --- a/docs/version-history.md +++ b/docs/version-history.md @@ -32,7 +32,8 @@ records are restored transparently as historical source milestones. | v2.3.1 | 22 | `c3c94b2` | Historical source milestone published retrospectively | | v2.3.2 | 23 | `7ecda45` | Historical source milestone published retrospectively | | v2.4.0 | 24 | tag `v2.4.0` | Published tested release | -| v2.4.1 | 25 | tag `v2.4.1` | Current tested release | +| v2.4.1 | 25 | tag `v2.4.1` | Superseded; its protected entitlement caused macOS to reject the unsigned app at launch | +| v2.4.2 | 26 | tag `v2.4.2` | Current tested release | No historical binary installer is reconstructed and presented as an original artifact. Retrospective release pages expose GitHub's source archives and state diff --git a/install.sh b/install.sh index 7d3d363..1ee7417 100755 --- a/install.sh +++ b/install.sh @@ -365,6 +365,7 @@ fi install -m 644 "$ROOT/macos/GDriveBackupTiger/Info.plist" "$APP_CONTENTS/Info.plist" clang -fobjc-arc -Wall -Wextra -mmacosx-version-min=13.0 \ -arch arm64 -arch x86_64 -framework Cocoa -framework UserNotifications \ + -framework Security \ "$ROOT/macos/GDriveBackupTiger/main.m" \ "$ROOT/macos/GDriveBackupTiger/ConfigSupport.m" \ "$ROOT/macos/GDriveBackupTiger/ProfileSupport.m" \ @@ -396,9 +397,9 @@ test -s "$APP_CONTENTS/Resources/Assets.car" "$ROOT/scripts/trash-path.sh" "$ICON_WORK" /usr/bin/xattr -cr "$APP_DIR" -codesign --force --deep \ - --entitlements "$ROOT/macos/GDriveBackupTiger/GDriveBackupTiger.entitlements" \ - --sign - "$APP_DIR" >/dev/null +# The source installer uses an ad-hoc signature, which cannot carry protected +# Apple notification entitlements without being rejected at exec time. +codesign --force --deep --sign - "$APP_DIR" >/dev/null sudo install -m 755 "$ROOT/bin/backup-google-drive.sh" /usr/local/bin/backup-google-drive.sh install -m 644 "$AGENT_SRC" "$AGENT_DST" diff --git a/macos/GDriveBackupTiger/Info.plist b/macos/GDriveBackupTiger/Info.plist index 778e805..2e11b6b 100644 --- a/macos/GDriveBackupTiger/Info.plist +++ b/macos/GDriveBackupTiger/Info.plist @@ -30,9 +30,9 @@ CFBundlePackageType APPL CFBundleVersion - 25 + 26 CFBundleShortVersionString - 2.4.1 + 2.4.2 LSMinimumSystemVersion 13.0 NSPrincipalClass diff --git a/macos/GDriveBackupTiger/main.m b/macos/GDriveBackupTiger/main.m index e556f39..2132bb5 100644 --- a/macos/GDriveBackupTiger/main.m +++ b/macos/GDriveBackupTiger/main.m @@ -1,4 +1,5 @@ #import +#import #import #include #include @@ -1792,6 +1793,24 @@ - (void)forgetUnknownExternalAttachmentsExceptDiskIDs: } } +- (BOOL)timeSensitiveBackupNotificationsEnabled { + SecTaskRef task = SecTaskCreateFromSelf(kCFAllocatorDefault); + if (!task) return NO; + + CFErrorRef error = NULL; + CFTypeRef value = SecTaskCopyValueForEntitlement( + task, + CFSTR("com.apple.developer.usernotifications.time-sensitive"), + &error); + BOOL enabled = value && + CFGetTypeID(value) == CFBooleanGetTypeID() && + CFBooleanGetValue((CFBooleanRef)value); + if (value) CFRelease(value); + if (error) CFRelease(error); + CFRelease(task); + return enabled; +} + - (UNMutableNotificationContent *)backupNotificationContentForDecision: (NSDictionary *)decision { UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init]; @@ -1800,9 +1819,11 @@ - (UNMutableNotificationContent *)backupNotificationContentForDecision: content.sound = UNNotificationSound.defaultSound; content.categoryIdentifier = @"GDT_BACKUP_ALERT"; if (@available(macOS 12.0, *)) { - // Backup failures must be noticeable during Focus without activating - // the app or stealing a full-screen workspace. - content.interruptionLevel = UNNotificationInterruptionLevelTimeSensitive; + // A protected level without its signed entitlement can make delivery + // fail. Ad-hoc builds keep the durable alert at the normal active level. + content.interruptionLevel = [self timeSensitiveBackupNotificationsEnabled] + ? UNNotificationInterruptionLevelTimeSensitive + : UNNotificationInterruptionLevelActive; } return content; } diff --git a/packaging/build-pkg.sh b/packaging/build-pkg.sh index cde4200..599ac8e 100755 --- a/packaging/build-pkg.sh +++ b/packaging/build-pkg.sh @@ -25,7 +25,7 @@ fi BUILD_DIR="$(/usr/bin/mktemp -d "${TMPDIR:-/tmp}/gdrive-pkg-build.XXXXXX")" PKG_ROOT="$BUILD_DIR/root" -DIST_DIR="$ROOT/dist" +DIST_DIR="${DIST_DIR:-$ROOT/dist}" COMPONENT_PKG="$BUILD_DIR/${PKG_NAME}" FINAL_PKG="$DIST_DIR/${PKG_NAME}" APP_PATH="$PKG_ROOT/Applications/GDrive Backup Tiger.app" @@ -42,6 +42,9 @@ mkdir -p "$PKG_ROOT/usr/local/bin" \ /usr/bin/make -C "$ROOT" build APP_DIR="$APP_PATH" >&2 if [[ -n "$APP_SIGN_IDENTITY" ]]; then + # A Developer ID signature alone does not authorize protected notification + # capabilities. Keep this build launchable until a verified provisioning + # profile flow exists. /usr/bin/codesign \ --force \ --options runtime \ @@ -52,7 +55,6 @@ if [[ -n "$APP_SIGN_IDENTITY" ]]; then --force \ --options runtime \ --timestamp \ - --entitlements "$ROOT/macos/GDriveBackupTiger/GDriveBackupTiger.entitlements" \ --sign "$APP_SIGN_IDENTITY" \ "$APP_PATH" >&2 fi diff --git a/packaging/verify-pkg.sh b/packaging/verify-pkg.sh index e7a102b..42de6b2 100755 --- a/packaging/verify-pkg.sh +++ b/packaging/verify-pkg.sh @@ -28,16 +28,16 @@ cleanup() { trap cleanup EXIT archive_listing="$(/usr/bin/xar -tf "$PKG_PATH")" -has_signature=0 +pkg_has_signature=0 if /usr/bin/grep -qx 'Signature' <<<"$archive_listing"; then - has_signature=1 + pkg_has_signature=1 fi -if [[ "$SIGNATURE_EXPECTATION" == "unsigned" && $has_signature -ne 0 ]]; then +if [[ "$SIGNATURE_EXPECTATION" == "unsigned" && $pkg_has_signature -ne 0 ]]; then printf 'Expected an unsigned package, but a signature is present: %s\n' "$PKG_PATH" >&2 exit 1 fi -if [[ "$SIGNATURE_EXPECTATION" == "signed" && $has_signature -eq 0 ]]; then +if [[ "$SIGNATURE_EXPECTATION" == "signed" && $pkg_has_signature -eq 0 ]]; then printf 'Expected a signed package, but no signature is present: %s\n' "$PKG_PATH" >&2 exit 1 fi @@ -129,17 +129,24 @@ if ! /usr/bin/grep -Fq "/bin/chmod 600 \"\$config_file\"" "$POSTINSTALL"; then fi /usr/bin/codesign --verify --deep --strict "$APP_PATH" APP_ENTITLEMENTS="$VERIFY_ROOT/app-entitlements.plist" -if ! /usr/bin/codesign --display --entitlements :- "$APP_PATH" \ - >"$APP_ENTITLEMENTS" 2>/dev/null || +has_time_sensitive_entitlement=0 +if /usr/bin/codesign --display --entitlements :- "$APP_PATH" \ + >"$APP_ENTITLEMENTS" 2>/dev/null && [[ "$(/usr/libexec/PlistBuddy \ -c 'Print :com.apple.developer.usernotifications.time-sensitive' \ - "$APP_ENTITLEMENTS" 2>/dev/null || true)" != "true" ]]; then + "$APP_ENTITLEMENTS" 2>/dev/null || true)" == "true" ]]; then + has_time_sensitive_entitlement=1 +fi + +if [[ $has_time_sensitive_entitlement -ne 0 ]]; then printf '%s\n' \ - 'Packaged app is missing the time-sensitive notification entitlement.' >&2 + 'Packaged app must not carry the restricted time-sensitive notification entitlement.' >&2 exit 1 fi if [[ "$SIGNATURE_EXPECTATION" == "signed" ]]; then + # The installer and embedded app have independent signatures. Validate the + # app authority directly instead of inferring it from the outer package. app_signature="$(/usr/bin/codesign --display --verbose=4 "$APP_PATH" 2>&1)" if ! /usr/bin/grep -Fq 'Authority=Developer ID Application:' <<<"$app_signature"; then printf '%s\n' 'Signed package does not contain a Developer ID Application-signed app.' >&2 diff --git a/tests/app-build-artifacts-test.sh b/tests/app-build-artifacts-test.sh index 1fdc155..89c3289 100644 --- a/tests/app-build-artifacts-test.sh +++ b/tests/app-build-artifacts-test.sh @@ -5,8 +5,13 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd)" STAGE="$(/usr/bin/mktemp -d "${TMPDIR:-/tmp}/gdrive-app-build-test.XXXXXX")" APP="$STAGE/GDrive Backup Tiger.app" ENTITLEMENTS="$STAGE/app-entitlements.plist" +SMOKE_PID="" cleanup() { + if [[ -n "$SMOKE_PID" ]] && /bin/kill -0 "$SMOKE_PID" 2>/dev/null; then + /bin/kill -TERM "$SMOKE_PID" 2>/dev/null || true + wait "$SMOKE_PID" 2>/dev/null || true + fi if [[ -e "$STAGE" ]]; then "$ROOT/scripts/trash-path.sh" "$STAGE" fi @@ -32,21 +37,48 @@ done /usr/bin/file "$APP/Contents/MacOS/GDriveBackupTiger" \ | /usr/bin/grep -Fq 'Mach-O universal binary with 2 architectures' -if ! /usr/bin/codesign --display --entitlements :- "$APP" >"$ENTITLEMENTS" 2>/dev/null || +if /usr/bin/codesign --display --entitlements :- "$APP" >"$ENTITLEMENTS" 2>/dev/null && [[ "$(/usr/libexec/PlistBuddy \ -c 'Print :com.apple.developer.usernotifications.time-sensitive' \ - "$ENTITLEMENTS" 2>/dev/null || true)" != "true" ]]; then - printf '%s\n' 'not ok - isolated app build preserves the time-sensitive notification entitlement' + "$ENTITLEMENTS" 2>/dev/null || true)" == "true" ]]; then + printf '%s\n' \ + 'not ok - an ad-hoc app carries a restricted entitlement and macOS rejects it at launch' + exit 1 +fi + +if /usr/bin/grep -Fq 'GDriveBackupTiger.entitlements' "$ROOT/install.sh"; then + printf '%s\n' \ + 'not ok - the source installer embeds a restricted entitlement in an ad-hoc signature' + exit 1 +fi + +if /usr/bin/grep -Fq 'GDriveBackupTiger.entitlements' \ + "$ROOT/packaging/build-pkg.sh"; then + printf '%s\n' \ + 'not ok - package builds embed a protected entitlement without a provisioning profile' exit 1 fi -for signing_entrypoint in Makefile install.sh packaging/build-pkg.sh; do - if ! /usr/bin/grep -Fq 'GDriveBackupTiger.entitlements' "$ROOT/$signing_entrypoint"; then - printf 'not ok - %s preserves notification entitlements while signing\n' \ - "$signing_entrypoint" +# Give the isolated copy its own Launch Services identity so this test can run +# beside an installed controller with the production bundle identifier. +/usr/libexec/PlistBuddy \ + -c 'Set :CFBundleIdentifier com.commcats.gdrivebackup.smoketest' \ + "$APP/Contents/Info.plist" +/usr/bin/codesign --force --deep --sign - "$APP" >/dev/null +"$APP/Contents/MacOS/GDriveBackupTiger" --menubar >/dev/null 2>&1 & +SMOKE_PID=$! +for _ in {1..20}; do + if ! /bin/kill -0 "$SMOKE_PID" 2>/dev/null; then + wait "$SMOKE_PID" 2>/dev/null || true + printf '%s\n' \ + 'not ok - macOS rejected or immediately terminated the isolated menu bar app' exit 1 fi + /bin/sleep 0.1 done +/bin/kill -TERM "$SMOKE_PID" +wait "$SMOKE_PID" 2>/dev/null || true +SMOKE_PID="" printf '%s\n' \ - 'ok - isolated app build produces signed universal binary, icon assets, and notification entitlement' + 'ok - isolated app build is Universal 2, signed, launchable, and free of restricted entitlements' diff --git a/tests/notification-integration-test.m b/tests/notification-integration-test.m index cf2b7fb..8d8bb7d 100644 --- a/tests/notification-integration-test.m +++ b/tests/notification-integration-test.m @@ -12,6 +12,7 @@ - (void)deliverBackupNotificationDecision:(NSDictionary completion:(void (^)(BOOL delivered))completion; - (UNMutableNotificationContent *)backupNotificationContentForDecision: (NSDictionary *)decision; +- (BOOL)timeSensitiveBackupNotificationsEnabled; - (void)clearBackupFailureNotificationsForConfig: (NSDictionary *)config summary:(NSDictionary *)summary @@ -40,6 +41,7 @@ @interface NotificationTestDelegate : AppDelegate @property(nonatomic, strong) NSUserDefaults *testDefaults; @property(nonatomic) NSInteger deliveryCalls; @property(nonatomic) BOOL deliverySucceeds; +@property(nonatomic) BOOL testTimeSensitiveNotificationsEnabled; @property(nonatomic, copy) NSArray *removedNotificationIdentifiers; @property(nonatomic, copy) NSArray *extraDeliveredNotificationIdentifiers; @end @@ -208,6 +210,10 @@ - (NSUserDefaults *)backupNotificationDefaultsStore { return self.testDefaults; } +- (BOOL)timeSensitiveBackupNotificationsEnabled { + return self.testTimeSensitiveNotificationsEnabled; +} + - (void)deliverBackupNotificationDecision:(NSDictionary *)decision completion:(void (^)(BOOL delivered))completion { (void)decision; @@ -427,21 +433,35 @@ int main(void) { @"a stale asynchronous retry decision is revalidated before launch"); SEL contentSelector = NSSelectorFromString(@"backupNotificationContentForDecision:"); + typedef UNMutableNotificationContent *(*ContentMethod)( + id, SEL, NSDictionary *); + ContentMethod contentMethod = nil; UNMutableNotificationContent *content = nil; if ([delegate respondsToSelector:contentSelector]) { - typedef UNMutableNotificationContent *(*ContentMethod)(id, SEL, NSDictionary *); - ContentMethod contentMethod = (ContentMethod)[delegate methodForSelector:contentSelector]; + contentMethod = (ContentMethod)[delegate methodForSelector:contentSelector]; content = contentMethod(delegate, contentSelector, first); } - BOOL timeSensitiveLevel = NO; + BOOL activeLevel = NO; if (@available(macOS 12.0, *)) { - timeSensitiveLevel = - content.interruptionLevel == UNNotificationInterruptionLevelTimeSensitive; + activeLevel = + content.interruptionLevel == UNNotificationInterruptionLevelActive; } Assert(content.sound != nil && [content.categoryIdentifier isEqualToString:@"GDT_BACKUP_ALERT"] && - timeSensitiveLevel, - @"automatic backup alerts are time-sensitive and audible without opening a window"); + activeLevel, + @"ad-hoc builds keep backup alerts audible without requesting a protected level"); + + delegate.testTimeSensitiveNotificationsEnabled = YES; + UNMutableNotificationContent *entitledContent = contentMethod + ? contentMethod(delegate, contentSelector, first) : nil; + BOOL timeSensitiveLevel = NO; + if (@available(macOS 12.0, *)) { + timeSensitiveLevel = + entitledContent.interruptionLevel == + UNNotificationInterruptionLevelTimeSensitive; + } + Assert(timeSensitiveLevel, + @"properly entitled builds elevate automatic backup alerts to time-sensitive"); NSSet *categories = nil; SEL categoriesSelector = NSSelectorFromString(@"appNotificationCategories"); diff --git a/tests/package-entitlement-safety-test.sh b/tests/package-entitlement-safety-test.sh new file mode 100644 index 0000000..a25f22c --- /dev/null +++ b/tests/package-entitlement-safety-test.sh @@ -0,0 +1,50 @@ +#!/bin/bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +STAGE="$(/usr/bin/mktemp -d "${TMPDIR:-/tmp}/gdrive-pkg-entitlement-test.XXXXXX")" +DIST="$STAGE/dist" +EXPANDED="$STAGE/expanded" +MUTATED_PKG="$STAGE/restricted-entitlement.pkg" +VERIFY_OUTPUT="$STAGE/verify-output.txt" + +cleanup() { + if [[ -e "$STAGE" ]]; then + "$ROOT/scripts/trash-path.sh" "$STAGE" + fi +} +trap cleanup EXIT + +PKG_PATH="$(DIST_DIR="$DIST" "$ROOT/packaging/build-pkg.sh")" +"$ROOT/packaging/verify-pkg.sh" --expect-unsigned "$PKG_PATH" >/dev/null + +/usr/sbin/pkgutil --expand-full "$PKG_PATH" "$EXPANDED" +APP="$EXPANDED/Payload/Applications/GDrive Backup Tiger.app" + +# Reproduce the v2.4.1 failure shape: codesign accepts the ad-hoc signature, +# but macOS rejects the protected entitlement when the app is executed. +/usr/bin/codesign \ + --force \ + --deep \ + --entitlements "$ROOT/macos/GDriveBackupTiger/GDriveBackupTiger.entitlements" \ + --sign - \ + "$APP" >/dev/null +/usr/sbin/pkgutil --flatten "$EXPANDED" "$MUTATED_PKG" + +if "$ROOT/packaging/verify-pkg.sh" --expect-unsigned "$MUTATED_PKG" \ + >"$VERIFY_OUTPUT" 2>&1; then + printf '%s\n' \ + 'not ok - package verification accepted a launch-blocking entitlement' + exit 1 +fi + +if ! /usr/bin/grep -Fq \ + 'Packaged app must not carry the restricted time-sensitive notification entitlement.' \ + "$VERIFY_OUTPUT"; then + printf '%s\n' \ + 'not ok - package verification failed without identifying the restricted entitlement' + exit 1 +fi + +printf '%s\n' \ + 'ok - package verification rejects the launch-blocking entitlement mutation' diff --git a/tests/release-workflow-test.sh b/tests/release-workflow-test.sh index 9055b22..00b6e8e 100755 --- a/tests/release-workflow-test.sh +++ b/tests/release-workflow-test.sh @@ -87,8 +87,9 @@ check_contains "$WORKFLOW" "SHA256SUMS.txt" \ check_contains "$WORKFLOW" "scripts/changelog-release-notes.sh" \ "release workflow derives notes from the versioned changelog" check_contains "$WORKFLOW" "gh release create" "release workflow creates the GitHub release" -check_contains "$PKG_VERIFIER" "com.apple.developer.usernotifications.time-sensitive" \ - "package verification rejects an app without the time-sensitive notification entitlement" +check_contains "$PKG_VERIFIER" \ + "Packaged app must not carry the restricted time-sensitive notification entitlement." \ + "package verification rejects a launch-blocking entitlement independently of package signing" if (( failures > 0 )); then printf '%s release workflow check(s) failed.\n' "$failures"