From 1bfac9c560229f1d68d64340ed44c1402461087e Mon Sep 17 00:00:00 2001 From: John McChesney TenEyck Jr Date: Sun, 24 May 2026 00:48:06 +0100 Subject: [PATCH 1/2] Add native app signing preflight Closes #52. Runs native app Swift tests through xcodebuild in the PR macOS lane, signs APW.app with the associated-domain entitlement during bundle construction, and adds an extended-validation preflight that verifies xcodebuild tests, codesign, and embedded entitlements. Also hardens macOS OpenSSL discovery so an x86 Homebrew prefix cannot poison arm64 extended validation. --- .github/workflows/pr-fast-ci.yml | 10 ++++- docs/bootstrap/onboarding.md | 1 + native-app/APW.entitlements | 10 +++++ scripts/build-native-app.sh | 3 +- scripts/ci/run-extended-validation.sh | 35 ++++++++++++++--- scripts/ci/run-native-app-preflight.sh | 53 ++++++++++++++++++++++++++ 6 files changed, 104 insertions(+), 8 deletions(-) create mode 100644 native-app/APW.entitlements create mode 100755 scripts/ci/run-native-app-preflight.sh diff --git a/.github/workflows/pr-fast-ci.yml b/.github/workflows/pr-fast-ci.yml index 9127082..22435b5 100644 --- a/.github/workflows/pr-fast-ci.yml +++ b/.github/workflows/pr-fast-ci.yml @@ -89,8 +89,14 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha }} - - name: Run native app Swift tests - run: swift test --package-path native-app + - name: Run native app xcodebuild tests + working-directory: native-app + run: >- + xcodebuild + -scheme APW-Package + -destination 'platform=macOS' + -derivedDataPath .xcode-derived + test validate-secrets: name: Validate Secrets diff --git a/docs/bootstrap/onboarding.md b/docs/bootstrap/onboarding.md index 8948b5a..5be5826 100644 --- a/docs/bootstrap/onboarding.md +++ b/docs/bootstrap/onboarding.md @@ -40,6 +40,7 @@ - Docker, service-container, browser, and `container:` workloads stay on GitHub-hosted runners. - Keep PR checks cheap. Add heavy validation to `scripts/ci/run-extended-validation.sh` instead of the PR lane. - APW extended validation requires both Rust (`cargo`) and the macOS Swift toolchain, so the `extended-checks` job must run on the org macOS self-hosted pool (`[self-hosted, private, macOS, ARM64, xcode]`) rather than the Synology shell-only pool. + - Extended validation runs `scripts/ci/run-native-app-preflight.sh`, which exercises the Swift package through `xcodebuild`, builds `APW.app`, verifies codesign, and confirms the associated-domain entitlement is embedded. ## Release Prep diff --git a/native-app/APW.entitlements b/native-app/APW.entitlements new file mode 100644 index 0000000..ed7f22a --- /dev/null +++ b/native-app/APW.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.developer.associated-domains + + webcredentials:example.com + + + diff --git a/scripts/build-native-app.sh b/scripts/build-native-app.sh index 6609f63..dbec5cc 100755 --- a/scripts/build-native-app.sh +++ b/scripts/build-native-app.sh @@ -11,6 +11,7 @@ CONTENTS_DIR="$APP_DIR/Contents" MACOS_DIR="$CONTENTS_DIR/MacOS" PLIST_PATH="$CONTENTS_DIR/Info.plist" EXECUTABLE_PATH="$PACKAGE_DIR/.build/release/$EXECUTABLE_NAME" +ENTITLEMENTS_PATH="$PACKAGE_DIR/APW.entitlements" VERSION="$(awk -F ' = ' '$1 == "version" { gsub(/"/, "", $2); print $2; exit }' "$ROOT_DIR/rust/Cargo.toml")" if [[ -z "$VERSION" ]]; then @@ -53,7 +54,7 @@ cat >"$PLIST_PATH" </dev/null 2>&1; then - if ! codesign -s - --force --deep "$APP_DIR" 2>/dev/null; then + if ! codesign -s - --force --deep --entitlements "$ENTITLEMENTS_PATH" "$APP_DIR" 2>/dev/null; then echo "Warning: ad-hoc code signing failed for $APP_DIR. The bundle may be rejected by Gatekeeper." >&2 fi fi diff --git a/scripts/ci/run-extended-validation.sh b/scripts/ci/run-extended-validation.sh index 431540c..0a9c337 100755 --- a/scripts/ci/run-extended-validation.sh +++ b/scripts/ci/run-extended-validation.sh @@ -27,7 +27,17 @@ run_step() { # with a clear error if the toolchain is unavailable. ensure_openssl_on_macos() { [[ "${OSTYPE:-}" == darwin* ]] || return 0 - if [[ -n "${OPENSSL_DIR:-}" ]] && command -v pkg-config >/dev/null 2>&1; then + local host_arch + host_arch="$(uname -m)" + openssl_prefix_matches_host() { + local prefix="$1" + local dylib="$prefix/lib/libssl.dylib" + [[ -f "$dylib" ]] || return 1 + file "$dylib" | grep -q "$host_arch" + } + if [[ -n "${OPENSSL_DIR:-}" ]] \ + && command -v pkg-config >/dev/null 2>&1 \ + && openssl_prefix_matches_host "$OPENSSL_DIR"; then return 0 fi if ! command -v brew >/dev/null 2>&1; then @@ -39,21 +49,36 @@ ensure_openssl_on_macos() { brew list pkg-config >/dev/null 2>&1 || brew install pkg-config fi local prefix - prefix="$(brew --prefix openssl@3 2>/dev/null || true)" + prefix="" + for candidate in /opt/homebrew/opt/openssl@3 /usr/local/opt/openssl@3; do + if openssl_prefix_matches_host "$candidate"; then + prefix="$candidate" + break + fi + done + if [[ -z "$prefix" ]]; then + prefix="$(brew --prefix openssl@3 2>/dev/null || true)" + fi if [[ -z "$prefix" || ! -d "$prefix" ]]; then brew install openssl@3 prefix="$(brew --prefix openssl@3)" fi + if ! openssl_prefix_matches_host "$prefix"; then + echo "Homebrew OpenSSL at $prefix does not contain $host_arch libraries." >&2 + echo "Install a host-architecture openssl@3 or export OPENSSL_DIR manually." >&2 + exit 1 + fi export OPENSSL_DIR="$prefix" export OPENSSL_INCLUDE_DIR="$prefix/include" export OPENSSL_LIB_DIR="$prefix/lib" - export PKG_CONFIG_PATH="$prefix/lib/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}" + export PKG_CONFIG_PATH="$prefix/lib/pkgconfig" } ensure_openssl_on_macos require_tool cargo require_tool swift +require_tool xcodebuild run_step "Rust native app end-to-end tests" \ cargo test --manifest-path rust/Cargo.toml --test native_app_e2e @@ -64,8 +89,8 @@ run_step "Rust security regression tests" \ run_step "Rust clippy" \ cargo clippy --manifest-path rust/Cargo.toml --all-targets -- -D warnings -run_step "Swift native app release build" \ - ./scripts/build-native-app.sh +run_step "Native app xcodebuild, signing, and entitlement preflight" \ + bash scripts/ci/run-native-app-preflight.sh echo echo "APW extended validation passed." diff --git a/scripts/ci/run-native-app-preflight.sh b/scripts/ci/run-native-app-preflight.sh new file mode 100755 index 0000000..81736e5 --- /dev/null +++ b/scripts/ci/run-native-app-preflight.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$ROOT_DIR" + +require_tool() { + local tool="$1" + if ! command -v "$tool" >/dev/null 2>&1; then + echo "Missing required tool: $tool" >&2 + exit 1 + fi +} + +if [[ "$(uname -s)" != "Darwin" ]]; then + echo "Native app preflight requires macOS." >&2 + exit 1 +fi + +require_tool xcodebuild +require_tool swift +require_tool codesign +require_tool plutil + +( + cd native-app + xcodebuild \ + -scheme APW-Package \ + -destination 'platform=macOS' \ + -derivedDataPath .xcode-derived \ + test +) + +./scripts/build-native-app.sh >/dev/null + +app_dir="native-app/dist/APW.app" +entitlements_file="$(mktemp "${TMPDIR:-/tmp}/apw-entitlements.XXXXXX")" +trap 'rm -f "$entitlements_file"' EXIT + +codesign --verify --deep --strict --verbose=2 "$app_dir" +codesign -d --entitlements :- "$app_dir" >"$entitlements_file" 2>/dev/null + +if ! plutil -lint "$entitlements_file" >/dev/null; then + echo "APW.app entitlements are not valid plist output." >&2 + exit 1 +fi + +if ! grep -q 'webcredentials:example.com' "$entitlements_file"; then + echo "APW.app is missing the webcredentials:example.com entitlement." >&2 + exit 1 +fi + +echo "Native app xcodebuild, codesign, and entitlement preflight passed." From 87d8743c59a5f30a4ed34c22aa91db4de3cc2c5a Mon Sep 17 00:00:00 2001 From: John McChesney TenEyck Jr Date: Sun, 24 May 2026 05:54:48 +0100 Subject: [PATCH 2/2] Compare signed app entitlements in preflight --- scripts/ci/run-fast-checks.sh | 1 + scripts/ci/run-native-app-preflight.sh | 24 ++++++++++++++++++--- scripts/test-native-app-preflight-config.sh | 21 ++++++++++++++++++ 3 files changed, 43 insertions(+), 3 deletions(-) create mode 100755 scripts/test-native-app-preflight-config.sh diff --git a/scripts/ci/run-fast-checks.sh b/scripts/ci/run-fast-checks.sh index 46dacc7..557bb12 100755 --- a/scripts/ci/run-fast-checks.sh +++ b/scripts/ci/run-fast-checks.sh @@ -42,5 +42,6 @@ done < <(find .github/scripts scripts -type f -name '*.sh' -print0) ./scripts/test-render-homebrew-formula.sh ./scripts/test-extended-validation-config.sh +./scripts/test-native-app-preflight-config.sh echo "APW fast checks passed." diff --git a/scripts/ci/run-native-app-preflight.sh b/scripts/ci/run-native-app-preflight.sh index 81736e5..19a5cf2 100755 --- a/scripts/ci/run-native-app-preflight.sh +++ b/scripts/ci/run-native-app-preflight.sh @@ -22,6 +22,12 @@ require_tool swift require_tool codesign require_tool plutil +PLIST_BUDDY="/usr/libexec/PlistBuddy" +if [[ ! -x "$PLIST_BUDDY" ]]; then + echo "Missing required tool: $PLIST_BUDDY" >&2 + exit 1 +fi + ( cd native-app xcodebuild \ @@ -34,19 +40,31 @@ require_tool plutil ./scripts/build-native-app.sh >/dev/null app_dir="native-app/dist/APW.app" +source_entitlements="native-app/APW.entitlements" entitlements_file="$(mktemp "${TMPDIR:-/tmp}/apw-entitlements.XXXXXX")" -trap 'rm -f "$entitlements_file"' EXIT +expected_domains_file="$(mktemp "${TMPDIR:-/tmp}/apw-expected-domains.XXXXXX")" +actual_domains_file="$(mktemp "${TMPDIR:-/tmp}/apw-actual-domains.XXXXXX")" +trap 'rm -f "$entitlements_file" "$expected_domains_file" "$actual_domains_file"' EXIT codesign --verify --deep --strict --verbose=2 "$app_dir" codesign -d --entitlements :- "$app_dir" >"$entitlements_file" 2>/dev/null +if ! plutil -lint "$source_entitlements" >/dev/null; then + echo "APW source entitlements are not valid plist output." >&2 + exit 1 +fi + if ! plutil -lint "$entitlements_file" >/dev/null; then echo "APW.app entitlements are not valid plist output." >&2 exit 1 fi -if ! grep -q 'webcredentials:example.com' "$entitlements_file"; then - echo "APW.app is missing the webcredentials:example.com entitlement." >&2 +"$PLIST_BUDDY" -c "Print :com.apple.developer.associated-domains" "$source_entitlements" >"$expected_domains_file" +"$PLIST_BUDDY" -c "Print :com.apple.developer.associated-domains" "$entitlements_file" >"$actual_domains_file" + +if ! cmp -s "$expected_domains_file" "$actual_domains_file"; then + echo "APW.app associated-domain entitlements differ from native-app/APW.entitlements." >&2 + diff -u "$expected_domains_file" "$actual_domains_file" >&2 || true exit 1 fi diff --git a/scripts/test-native-app-preflight-config.sh b/scripts/test-native-app-preflight-config.sh new file mode 100755 index 0000000..6c8704b --- /dev/null +++ b/scripts/test-native-app-preflight-config.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT="scripts/ci/run-native-app-preflight.sh" + +require_line() { + local pattern="$1" + local message="$2" + if ! grep -Fq "$pattern" "$SCRIPT"; then + echo "$message" >&2 + exit 1 + fi +} + +require_line "xcodebuild" "Native app preflight must run xcodebuild tests." +require_line "codesign --verify --deep --strict" "Native app preflight must verify the signed app bundle." +require_line "codesign -d --entitlements :-" "Native app preflight must extract embedded entitlements." +require_line "Print :com.apple.developer.associated-domains" "Native app preflight must compare associated-domain entitlements." +require_line "cmp -s \"\$expected_domains_file\" \"\$actual_domains_file\"" "Native app preflight must fail on entitlement drift." + +echo "Native app preflight contract test passed."