Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 31 additions & 4 deletions TargetBridge-Sender/scripts/build_targetbridge_sender_app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,54 @@ BUILD_DIR="${DERIVED_DATA_DIR}/Build/Products/${CONFIGURATION}"
SOURCE_APP="${BUILD_DIR}/TargetBridge.app"
DEST_DIR="${REPO_ROOT}/build"
DEST_APP="${DEST_DIR}/TargetBridge.app"
# Preserve the upstream disposable CI/development build when no signing setup
# was requested. Once configured (or explicitly required), never fall back.
SIGNING_CONFIG="${TARGETBRIDGE_SIGNING_CONFIG:-$HOME/Library/Application Support/TargetBridge/Build/sender-signing-identity.txt}"
if [[ -n "${TARGETBRIDGE_CODESIGN_IDENTITY:-}" ||
-n "${TARGETBRIDGE_SIGNING_CONFIG:-}" || -e "$SIGNING_CONFIG" ||
"${TARGETBRIDGE_REQUIRE_PERSISTENT_SIGNING:-0}" == 1 ]]; then
"$SCRIPT_DIR/sign_targetbridge_sender_app.sh" --check-identity
else
echo "WARNING: unconfigured disposable build; this ad-hoc app is not an identity-preserving update." >&2
export TARGETBRIDGE_CODESIGN_IDENTITY=-
export TARGETBRIDGE_ALLOW_ADHOC=1
fi

cd "$ROOT"

xcodegen generate

# Package a standalone app, not Xcode's preview executor plus unsigned debug
# dylibs. This also lets strict signing verify the same bundle on Xcode 16+.
xcodebuild \
-scheme TBDisplaySender \
-configuration "$CONFIGURATION" \
-derivedDataPath "$DERIVED_DATA_DIR" \
CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGNING_ALLOWED=NO \
ENABLE_DEBUG_DYLIB=NO \
build

mkdir -p "$DEST_DIR"
rm -rf "$DEST_APP"
ditto "$SOURCE_APP" "$DEST_APP"
STAGING_DIR=$(mktemp -d "$DEST_DIR/.sender-signing.XXXXXX")
STAGED_APP="$STAGING_DIR/TargetBridge.app"
ditto "$SOURCE_APP" "$STAGED_APP"
echo "Cleaning extended attributes..."
xattr -cr "$DEST_APP" || true
xattr -cr "$STAGED_APP"
echo "Signing sender application..."
codesign --force --deep --sign - "$DEST_APP" || true
"$SCRIPT_DIR/sign_targetbridge_sender_app.sh" "$STAGED_APP"
if [[ -e "$DEST_APP" ]]; then
# Retain the old artifact inside the staging directory for recovery.
mv "$DEST_APP" "$STAGING_DIR/Previous TargetBridge.app"
fi
if ! mv "$STAGED_APP" "$DEST_APP"; then
if [[ ! -e "$DEST_APP" && -e "$STAGING_DIR/Previous TargetBridge.app" ]]; then
mv "$STAGING_DIR/Previous TargetBridge.app" "$DEST_APP"
fi
echo "Could not activate the signed build; recovery files: $STAGING_DIR" >&2
exit 1
fi
touch "$DEST_APP"

echo "TargetBridge sender built: $DEST_APP"
Expand Down
39 changes: 39 additions & 0 deletions TargetBridge-Sender/scripts/sign_targetbridge_sender_app.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/bin/zsh
set -euo pipefail

fail() { print -u2 -- "Sender signing: $*"; exit 1; }
APP="${1:-}"
[[ -n "$APP" && $# -eq 1 ]] || fail "Usage: $0 TargetBridge.app | --check-identity"

CONFIG="${TARGETBRIDGE_SIGNING_CONFIG:-$HOME/Library/Application Support/TargetBridge/Build/sender-signing-identity.txt}"
IDENTITY="${TARGETBRIDGE_CODESIGN_IDENTITY:-}"
if [[ -z "$IDENTITY" && -f "$CONFIG" ]]; then
IDENTITY=$(<"$CONFIG")
fi
if [[ -z "$IDENTITY" ]]; then
fail "No persistent signing identity. Configure TARGETBRIDGE_CODESIGN_IDENTITY or $CONFIG. See docs/Sender-Signing.md. Refusing an implicit ad-hoc build."
fi
if [[ "$IDENTITY" == - ]]; then
[[ "${TARGETBRIDGE_ALLOW_ADHOC:-0}" == 1 ]] || fail "Ad-hoc signing requires TARGETBRIDGE_ALLOW_ADHOC=1; updates may lose privacy permissions."
print -u2 -- "WARNING: disposable ad-hoc development build; do not install over a persistent signed Sender."
else
[[ "$IDENTITY" =~ '^[[:xdigit:]]{40}$' ]] || fail "Use the exact 40-character certificate fingerprint, not a display name"
IDENTITIES=$(/usr/bin/security find-identity -v -p codesigning)
[[ "${(U)IDENTITIES}" == *" ${(U)IDENTITY} "* ]] || fail "Configured certificate is not a valid code-signing identity in this user's Keychain. Unlock/configure it through macOS; no fallback is allowed."
fi
[[ "$APP" != --check-identity ]] || { print -- 'Signing identity preflight passed'; exit 0; }
PLIST="$APP/Contents/Info.plist"
[[ -f "$PLIST" && ! -L "$APP" ]] || fail "Expected a real application bundle"
BUNDLE_ID=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$PLIST")
[[ "$BUNDLE_ID" == com.targetbridge.sender ]] || fail "Wrong bundle identifier: $BUNDLE_ID"

# Use the system-generated designated requirement. Never weaken it to just a
# bundle identifier: certificate-bound identity is what makes updates trustworthy.
/usr/bin/codesign --force --sign "$IDENTITY" "$APP"
/usr/bin/codesign --verify --deep --strict --all-architectures "$APP"
DETAILS=$(/usr/bin/codesign -dv "$APP" 2>&1)
if [[ "$IDENTITY" != - && "$DETAILS" == *"Signature=adhoc"* ]]; then
fail "A persistent signature was requested but the result is ad-hoc"
fi
/usr/bin/codesign -d -r- "$APP" 2>&1
print -- "Sender signature verified: $APP"
42 changes: 42 additions & 0 deletions TargetBridge-Sender/scripts/test_sender_signing.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/bin/zsh
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
WORK=$(mktemp -d "${TMPDIR:-/tmp}/targetbridge-signing-test.XXXXXX")
BASE="${1:?Usage: test_sender_signing.sh signed-TargetBridge.app}"
SIGN="$SCRIPT_DIR/sign_targetbridge_sender_app.sh"
VERIFY="$SCRIPT_DIR/verify_sender_update.sh"
expect_reject() {
local title="$1"; shift
if "$@" >"$WORK/rejection.log" 2>&1; then
print -u2 -- "FAIL: accepted $title"; exit 1
fi
print -- "PASS: rejected $title"
}
ditto "$BASE" "$WORK/A.app"
ditto "$BASE" "$WORK/B.app"
# Changing a bundle resource changes the sealed CodeDirectory, reproducing the
# identity risk without launching a capture process or requesting permissions.
/usr/libexec/PlistBuddy -c 'Set :CFBundleVersion 999999' "$WORK/B.app/Contents/Info.plist"
zsh "$SIGN" "$WORK/B.app"
zsh "$VERIFY" "$WORK/A.app" "$WORK/B.app"
HASH_A=$(/usr/bin/shasum -a 256 "$WORK/A.app/Contents/MacOS/TargetBridge")
HASH_B=$(/usr/bin/shasum -a 256 "$WORK/B.app/Contents/MacOS/TargetBridge")
[[ "${HASH_A%% *}" != "${HASH_B%% *}" ]] || { print -u2 'FAIL: fixtures have identical signed executables'; exit 1; }
print -- 'PASS: different signed executable hashes, stable identity'
expect_reject 'missing identity' env TARGETBRIDGE_CODESIGN_IDENTITY= TARGETBRIDGE_SIGNING_CONFIG="$WORK/missing" zsh "$SIGN" "$WORK/B.app"
expect_reject 'unavailable identity' env TARGETBRIDGE_CODESIGN_IDENTITY=0000000000000000000000000000000000000000 zsh "$SIGN" "$WORK/B.app"
expect_reject 'implicit ad-hoc signing' env TARGETBRIDGE_CODESIGN_IDENTITY=- TARGETBRIDGE_ALLOW_ADHOC=0 zsh "$SIGN" "$WORK/B.app"
ditto "$WORK/B.app" "$WORK/Adhoc.app"
TARGETBRIDGE_CODESIGN_IDENTITY=- TARGETBRIDGE_ALLOW_ADHOC=1 zsh "$SIGN" "$WORK/Adhoc.app"
expect_reject 'ad-hoc update' zsh "$VERIFY" "$WORK/A.app" "$WORK/Adhoc.app"
expect_reject 'ad-hoc baseline' zsh "$VERIFY" "$WORK/Adhoc.app" "$WORK/B.app"
ditto "$WORK/B.app" "$WORK/WrongID.app"
/usr/libexec/PlistBuddy -c 'Set :CFBundleIdentifier com.targetbridge.receiver' "$WORK/WrongID.app/Contents/Info.plist"
expect_reject 'wrong signing bundle ID' zsh "$SIGN" "$WORK/WrongID.app"
expect_reject 'wrong update bundle ID' zsh "$VERIFY" "$WORK/A.app" "$WORK/WrongID.app"
ditto "$WORK/B.app" "$WORK/Tampered.app"
/usr/libexec/PlistBuddy -c 'Set :CFBundleVersion 888888' "$WORK/Tampered.app/Contents/Info.plist"
expect_reject 'tampered candidate' zsh "$VERIFY" "$WORK/A.app" "$WORK/Tampered.app"
expect_reject 'tampered installed baseline' zsh "$VERIFY" "$WORK/Tampered.app" "$WORK/B.app"
zsh "$VERIFY" "$WORK/A.app" "$WORK/B.app"
print -- "All signing checks passed. Fixtures retained at $WORK; no installed app or TCC setting changed."
27 changes: 27 additions & 0 deletions TargetBridge-Sender/scripts/test_sender_signing_guards.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/bin/zsh
# Negative-path checks that do not need a trusted signing certificate.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
WORK=$(mktemp -d "${TMPDIR:-/tmp}/targetbridge-signing-guards.XXXXXX")
BASE="${1:?Usage: test_sender_signing_guards.sh TargetBridge.app}"
SIGN="$SCRIPT_DIR/sign_targetbridge_sender_app.sh"
VERIFY="$SCRIPT_DIR/verify_sender_update.sh"
reject_with() {
local expected="$1"; shift
if "$@" >"$WORK/rejection.log" 2>&1; then
print -u2 -- "FAIL: unexpectedly accepted $expected"; exit 1
fi
/usr/bin/grep -Fq -- "$expected" "$WORK/rejection.log" || { /bin/cat "$WORK/rejection.log"; exit 1; }
print -- "PASS: $expected"
}
reject_with 'No persistent signing identity' env TARGETBRIDGE_CODESIGN_IDENTITY= TARGETBRIDGE_SIGNING_CONFIG="$WORK/missing" zsh "$SIGN" --check-identity
reject_with 'exact 40-character certificate fingerprint' env TARGETBRIDGE_CODESIGN_IDENTITY='unverified display name' zsh "$SIGN" --check-identity
reject_with 'not a valid code-signing identity' env TARGETBRIDGE_CODESIGN_IDENTITY=0000000000000000000000000000000000000000 zsh "$SIGN" --check-identity
reject_with 'Ad-hoc signing requires' env TARGETBRIDGE_CODESIGN_IDENTITY=- TARGETBRIDGE_ALLOW_ADHOC=0 zsh "$SIGN" --check-identity
ditto "$BASE" "$WORK/Adhoc.app"
TARGETBRIDGE_CODESIGN_IDENTITY=- TARGETBRIDGE_ALLOW_ADHOC=1 zsh "$SIGN" "$WORK/Adhoc.app"
reject_with 'Ad-hoc identity cannot preserve' zsh "$VERIFY" "$WORK/Adhoc.app" "$WORK/Adhoc.app"
/usr/libexec/PlistBuddy -c 'Set :CFBundleIdentifier com.targetbridge.receiver' "$WORK/Adhoc.app/Contents/Info.plist"
reject_with 'Wrong bundle identifier' env TARGETBRIDGE_CODESIGN_IDENTITY=- TARGETBRIDGE_ALLOW_ADHOC=1 zsh "$SIGN" "$WORK/Adhoc.app"
reject_with 'Unexpected bundle ID' zsh "$VERIFY" "$WORK/Adhoc.app" "$BASE"
print -- "7 negative checks passed. Persistent-signature and TCC migration tests still require a valid identity and human consent. Fixtures: $WORK"
20 changes: 20 additions & 0 deletions TargetBridge-Sender/scripts/verify_sender_update.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/bin/zsh
set -euo pipefail
fail() { print -u2 -- "Sender update rejected: $*"; exit 1; }
[[ $# -eq 2 ]] || fail "Usage: $0 installed.app candidate.app"
OLD="$1"
NEW="$2"
for APP in "$OLD" "$NEW"; do
[[ -f "$APP/Contents/Info.plist" && ! -L "$APP" ]] || fail "Invalid app: $APP"
ID=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$APP/Contents/Info.plist")
[[ "$ID" == com.targetbridge.sender ]] || fail "Unexpected bundle ID: $ID"
/usr/bin/codesign --verify --deep --strict --all-architectures "$APP"
DETAILS=$(/usr/bin/codesign -dv "$APP" 2>&1)
[[ "$DETAILS" != *"Signature=adhoc"* ]] || fail "Ad-hoc identity cannot preserve update permissions. Migrate once to a persistent signer and reapprove in macOS."
done
REQUIREMENT=$(/usr/bin/codesign -d -r- "$OLD" 2>&1 | /usr/bin/sed -nE 's/^#? ?designated => //p')
[[ -n "$REQUIREMENT" ]] || fail "Missing installed identity requirement"
# '=' marks a literal requirement; without it codesign treats the text as a path.
/usr/bin/codesign --verify --deep --strict --all-architectures -R "=$REQUIREMENT" "$NEW" || fail "Candidate does not satisfy the installed Sender identity"
print -- "PASS: candidate satisfies the installed Sender identity on every architecture."
print -- "This verifies signing continuity, not a TCC grant. Install at /Applications/TargetBridge.app without re-signing."
93 changes: 93 additions & 0 deletions docs/Sender-Signing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Stable Sender identity and privacy permissions

macOS recognises updates by the app's code-signing designated requirement, not
just its name or version. An ad-hoc signature binds that requirement to the
binary's hashes. Rebuilding changes those hashes and can invalidate Screen
Recording and Accessibility consent.

## Build and sign

Use one persistent certificate and keep `com.targetbridge.sender` as the bundle
identifier. Configure `TARGETBRIDGE_CODESIGN_IDENTITY` with the certificate's
fingerprint, or store that public fingerprint in:

`~/Library/Application Support/TargetBridge/Build/sender-signing-identity.txt`

The regular Sender build preserves upstream's disposable ad-hoc CI/development
build when no signing configuration was requested, with a prominent warning.
Set `TARGETBRIDGE_REQUIRE_PERSISTENT_SIGNING=1` to require an update-safe build
even on an unconfigured machine. Configuring an identity or a configuration
file also enables strict preflight: a missing/invalid configured identity never
falls back to ad-hoc. Signing errors and verification failures are fatal.
The build stages the candidate
before replacing the previous build, so a missing/locked signing key leaves the
previous artifact intact.

The packaging build disables Xcode's debug-dylib/preview executor layout, so
the standalone app can be strictly signed without unsigned preview dylibs.
This does not change normal interactive builds in Xcode.

For private local builds only, a persistent self-signed code-signing identity
can be configured using Apple's Certificate Assistant in Keychain Access.
Any change to its trust settings requires the Mac owner's explicit approval;
limit trust to Code Signing. These build scripts never create/trust certificates,
unlock a Keychain, export keys, or change TCC settings. Do not use an unrestricted
trust-root setup. Verify the chosen fingerprint against the actual certificate.

Keep the signing Keychain and always sign on that Mac, even when compilation
happens elsewhere. Do not recreate the certificate for each build. A lost key
requires an intentional identity migration and new consent. A local certificate
is not a substitute for Developer ID and notarization for public distribution.

For distribution, use Developer ID plus the required notarization workflow;
these scripts alone do not implement notarization. Do not mix local and upstream signers if preserving existing consent
is required. No Apple Developer membership is provisioned by these scripts.

When using the signing helper directly, a disposable development build must opt in to both
`TARGETBRIDGE_CODESIGN_IDENTITY=-` and `TARGETBRIDGE_ALLOW_ADHOC=1`. Never install
it over a Sender that is already persistently signed.

## Install/update gate

Before replacing an installed, persistently signed Sender:

```sh
scripts/verify_sender_update.sh /Applications/TargetBridge.app ../build/TargetBridge.app
```

The check rejects ad-hoc signatures, broken bundles, wrong bundle IDs and a
candidate that fails the installed app's designated requirement on any
architecture. Never re-sign, modify Info.plist or otherwise change the bundle
after that check. Preserve the signature with `ditto` and verify it again at the
destination. Keep a recoverable backup outside Applications.

An initial migration from an ad-hoc app intentionally fails this gate: it is an
identity change, not a seamless update. Back up the installed app, replace it
once with the persistently signed app at `/Applications/TargetBridge.app`, and
approve Screen Recording and Accessibility there if macOS asks. Do not edit
TCC databases, run blanket permission resets, or disable system protections.

## Validation

Test two builds with different executable contents signed with the same
identity: both must pass signature verification and the second must satisfy the
first's designated requirement. Test that an ad-hoc update, a different signer,
wrong bundle ID and a tampered resource are rejected.

`scripts/test_sender_signing.sh /path/to/signed/TargetBridge.app` covers signing
continuity with a changed sealed bundle, missing/unavailable identities, explicit
ad-hoc opt-in, bundle ID mismatch and tampering. Run it with a configured valid
identity; a refusal at preflight is not a passing integration test. Also test a
separate compiled revision and, when available, a second signer before release.

Signing continuity is necessary, not proof that TCC granted permissions. The
final acceptance test is a real A-to-B installed update after human consent on
the Sender Mac, checking screen capture and receiver-controlled input without
granting them again. macOS can still ask for consent after a reset, identity
change, certificate loss, or OS privacy-policy change.

References:
- https://developer.apple.com/documentation/technotes/tn3127-inside-code-signing-requirements
- https://developer.apple.com/library/archive/technotes/tn2206/
- https://developer.apple.com/forums/thread/819406
- https://support.apple.com/guide/keychain-access/kyca8916/mac