From 760f86453e0b52cbd9b1a82f0fe2925482b3a552 Mon Sep 17 00:00:00 2001 From: joshavant <830519+joshavant@users.noreply.github.com> Date: Mon, 22 Jun 2026 19:11:00 -0500 Subject: [PATCH] feat: wire ios push sandbox tooling --- apps/ios/Config/AppStoreSigning.json | 2 +- apps/ios/Config/Signing.xcconfig | 1 + apps/ios/Signing.xcconfig | 1 + apps/ios/fastlane/Fastfile | 12 ++ apps/ios/fastlane/SETUP.md | 2 +- scripts/ios-configure-signing.sh | 1 + scripts/ios-release-prepare.sh | 4 + scripts/ios-run.sh | 125 +++++++++++++++++++-- test/scripts/ios-configure-signing.test.ts | 1 + test/scripts/ios-release-signing.test.ts | 2 +- test/scripts/ios-run.test.ts | 81 ++++++++++++- 11 files changed, 219 insertions(+), 13 deletions(-) diff --git a/apps/ios/Config/AppStoreSigning.json b/apps/ios/Config/AppStoreSigning.json index 854e229fb09..de3e665954d 100644 --- a/apps/ios/Config/AppStoreSigning.json +++ b/apps/ios/Config/AppStoreSigning.json @@ -12,7 +12,7 @@ "platform": "IOS", "profileKey": "OPENCLAW_APP_PROFILE", "profileName": "OpenClaw App Store ai.openclawfoundation.app", - "capabilities": ["PUSH_NOTIFICATIONS", "APP_GROUPS"], + "capabilities": ["PUSH_NOTIFICATIONS", "APP_GROUPS", "APP_ATTEST"], "appGroups": ["group.ai.openclawfoundation.app.shared"] }, { diff --git a/apps/ios/Config/Signing.xcconfig b/apps/ios/Config/Signing.xcconfig index dbca8ff3540..37357aea32e 100644 --- a/apps/ios/Config/Signing.xcconfig +++ b/apps/ios/Config/Signing.xcconfig @@ -6,6 +6,7 @@ OPENCLAW_IOS_SELECTED_TEAM = $(OPENCLAW_IOS_DEFAULT_TEAM) OPENCLAW_DEVELOPMENT_TEAM = $(OPENCLAW_IOS_SELECTED_TEAM) OPENCLAW_CODE_SIGN_STYLE = Automatic OPENCLAW_CODE_SIGN_IDENTITY = Apple Development +OPENCLAW_CODE_SIGN_ENTITLEMENTS = Sources/OpenClaw.entitlements OPENCLAW_APP_BUNDLE_ID = ai.openclawfoundation.app OPENCLAW_APP_GROUP_ID = group.ai.openclawfoundation.app.shared OPENCLAW_WATCH_APP_BUNDLE_ID = ai.openclawfoundation.app.watchkitapp diff --git a/apps/ios/Signing.xcconfig b/apps/ios/Signing.xcconfig index a03515ca35f..f4fbd4bac91 100644 --- a/apps/ios/Signing.xcconfig +++ b/apps/ios/Signing.xcconfig @@ -6,6 +6,7 @@ OPENCLAW_CODE_SIGN_STYLE = Manual OPENCLAW_CODE_SIGN_IDENTITY = Apple Development +OPENCLAW_CODE_SIGN_ENTITLEMENTS = Sources/OpenClaw.entitlements OPENCLAW_DEVELOPMENT_TEAM = FWJYW4S8P8 OPENCLAW_APP_BUNDLE_ID = ai.openclawfoundation.app diff --git a/apps/ios/fastlane/Fastfile b/apps/ios/fastlane/Fastfile index f78ed00265b..26cb7f2d1c3 100644 --- a/apps/ios/fastlane/Fastfile +++ b/apps/ios/fastlane/Fastfile @@ -495,6 +495,9 @@ def produce_services_for_target(target) if target.fetch("capabilities").include?("APP_GROUPS") services[:app_group] = "on" end + if target.fetch("capabilities").include?("APP_ATTEST") + services[:app_attest] = "on" + end services end @@ -605,6 +608,15 @@ def validate_match_profile_capabilities!(target) ) end end + + if capabilities.include?("APP_ATTEST") + app_attest_environments = profile_plist_array_values(profile_path, "Entitlements:com.apple.developer.devicecheck.appattest-environment") + unless app_attest_environments.include?("production") + UI.user_error!( + "Provisioning profile #{target.fetch("profileName")} for #{target.fetch("bundleId")} is missing production App Attest entitlement; actual environments: #{app_attest_environments.empty? ? "missing" : app_attest_environments.join(", ")}." + ) + end + end end def sync_app_store_signing!(readonly:) diff --git a/apps/ios/fastlane/SETUP.md b/apps/ios/fastlane/SETUP.md index febaa814d8e..c9900b80109 100644 --- a/apps/ios/fastlane/SETUP.md +++ b/apps/ios/fastlane/SETUP.md @@ -65,7 +65,7 @@ pnpm ios:release:signing:check pnpm ios:release:signing:setup ``` -`signing:setup` uses Fastlane `produce` and `modify_services` to create Developer Portal bundle IDs and enable required services before running `match`. The main app and share extension also require the shared App Group from `apps/ios/Config/AppStoreSigning.json`; associate that group with both bundle IDs in the Apple Developer Portal before regenerating profiles. If Fastlane does not already have a valid Apple Developer Portal session, run `fastlane spaceauth` for a release-owner Apple ID and export the resulting `FASTLANE_SESSION`. +`signing:setup` uses Fastlane `produce` and `modify_services` to create Developer Portal bundle IDs and enable required services before running `match`. The main app also requires App Attest, and the main app and share extension both require the shared App Group from `apps/ios/Config/AppStoreSigning.json`; associate that group with both bundle IDs in the Apple Developer Portal before regenerating profiles. If Fastlane does not already have a valid Apple Developer Portal session, run `fastlane spaceauth` for a release-owner Apple ID and export the resulting `FASTLANE_SESSION`. Shared encrypted signing storage: diff --git a/scripts/ios-configure-signing.sh b/scripts/ios-configure-signing.sh index 471f024b33d..1d2e1599f19 100755 --- a/scripts/ios-configure-signing.sh +++ b/scripts/ios-configure-signing.sh @@ -96,6 +96,7 @@ cat >"${tmp_file}" < + Override the sandbox relay URL used with --push-sandbox-simulator. + Defaults to https://ios-push-relay-sandbox.openclaw.ai. + --simulator-proof-secret-env + Environment variable that contains the simulator proof secret. + Defaults to OPENCLAW_SIMULATOR_PUSH_PROOF_SECRET. + -h, --help + Show this help. +EOF +} + run_simctl() { # shellcheck disable=SC2086 ${SIMCTL_BIN} "$@" } +push_sandbox_simulator=0 +push_relay_base_url="${OPENCLAW_PUSH_SANDBOX_RELAY_BASE_URL:-https://ios-push-relay-sandbox.openclaw.ai}" +simulator_proof_secret_env="${OPENCLAW_SIMULATOR_PUSH_PROOF_SECRET_ENV:-OPENCLAW_SIMULATOR_PUSH_PROOF_SECRET}" + +while [[ $# -gt 0 ]]; do + case "$1" in + --push-sandbox-simulator) + push_sandbox_simulator=1 + ;; + --push-relay-base-url) + if [[ $# -lt 2 || -z "$2" ]]; then + echo "ERROR: --push-relay-base-url requires a URL" >&2 + exit 1 + fi + push_relay_base_url="$2" + shift + ;; + --simulator-proof-secret-env) + if [[ $# -lt 2 || -z "$2" ]]; then + echo "ERROR: --simulator-proof-secret-env requires an environment variable name" >&2 + exit 1 + fi + simulator_proof_secret_env="$2" + shift + ;; + -h | --help) + usage + exit 0 + ;; + *) + echo "ERROR: Unknown argument: $1" >&2 + usage >&2 + exit 1 + ;; + esac + shift +done + +xcodebuild_overrides=() +simulator_proof_secret="" + +if [[ ! "${simulator_proof_secret_env}" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then + echo "ERROR: Invalid simulator proof secret environment variable name" >&2 + exit 1 +fi + +if [[ "${push_sandbox_simulator}" == "1" ]]; then + simulator_proof_secret="${!simulator_proof_secret_env:-}" + if [[ -z "${simulator_proof_secret}" ]]; then + echo "ERROR: ${simulator_proof_secret_env} must be set for --push-sandbox-simulator" >&2 + exit 1 + fi + if [[ "${#simulator_proof_secret}" -lt 32 ]]; then + echo "ERROR: ${simulator_proof_secret_env} must contain at least 32 characters" >&2 + exit 1 + fi + + xcodebuild_overrides+=( + "OPENCLAW_PUSH_TRANSPORT=relay" + "OPENCLAW_PUSH_DISTRIBUTION=official" + "OPENCLAW_PUSH_RELAY_BASE_URL=${push_relay_base_url}" + "OPENCLAW_PUSH_APNS_ENVIRONMENT=sandbox" + "OPENCLAW_PUSH_RELAY_PROFILE=simulatorSandbox" + "OPENCLAW_PUSH_PROOF_POLICY=internalSimulator" + "OPENCLAW_APNS_ENTITLEMENT_ENVIRONMENT=development" + ) +fi + +unset "${simulator_proof_secret_env}" +if [[ "${simulator_proof_secret_env}" != "OPENCLAW_SIMULATOR_PUSH_PROOF_SECRET" ]]; then + unset OPENCLAW_SIMULATOR_PUSH_PROOF_SECRET +fi + "${ROOT_DIR}/scripts/ios-configure-signing.sh" "${ROOT_DIR}/scripts/ios-write-version-xcconfig.sh" cd "${IOS_DIR}" "${XCODEGEN_BIN}" generate -"${XCODEBUILD_BIN}" \ - -project OpenClaw.xcodeproj \ - -scheme OpenClaw \ - -destination "${IOS_DESTINATION}" \ - -configuration "${CONFIGURATION}" \ - -derivedDataPath "${DERIVED_DATA_DIR}" \ - build +if [[ "${push_sandbox_simulator}" == "1" ]]; then + "${XCODEBUILD_BIN}" \ + -project OpenClaw.xcodeproj \ + -scheme OpenClaw \ + -destination "${IOS_DESTINATION}" \ + -configuration "${CONFIGURATION}" \ + -derivedDataPath "${DERIVED_DATA_DIR}" \ + build \ + "${xcodebuild_overrides[@]}" +else + "${XCODEBUILD_BIN}" \ + -project OpenClaw.xcodeproj \ + -scheme OpenClaw \ + -destination "${IOS_DESTINATION}" \ + -configuration "${CONFIGURATION}" \ + -derivedDataPath "${DERIVED_DATA_DIR}" \ + build +fi app_path="${DERIVED_DATA_DIR}/Build/Products/${CONFIGURATION}-iphonesimulator/${APP_NAME}.app" if [[ ! -d "${app_path}" ]]; then @@ -54,4 +157,10 @@ if ! boot_output="$(run_simctl boot "${SIMULATOR_TARGET}" 2>&1)"; then fi run_simctl install "${SIMULATOR_TARGET}" "${app_path}" -run_simctl launch "${SIMULATOR_TARGET}" "${bundle_id}" +if [[ "${push_sandbox_simulator}" == "1" ]]; then + # shellcheck disable=SC2086 + SIMCTL_CHILD_OPENCLAW_SIMULATOR_PUSH_PROOF_SECRET="${simulator_proof_secret}" \ + ${SIMCTL_BIN} launch "${SIMULATOR_TARGET}" "${bundle_id}" +else + run_simctl launch "${SIMULATOR_TARGET}" "${bundle_id}" +fi diff --git a/test/scripts/ios-configure-signing.test.ts b/test/scripts/ios-configure-signing.test.ts index 1f460c88369..cc89c743e73 100644 --- a/test/scripts/ios-configure-signing.test.ts +++ b/test/scripts/ios-configure-signing.test.ts @@ -64,6 +64,7 @@ describe.sequential("scripts/ios-configure-signing.sh", () => { expect(stdout).toContain("team=FWJYW4S8P8 app=ai.openclawfoundation.app"); expect(generated).toContain("OPENCLAW_DEVELOPMENT_TEAM = FWJYW4S8P8"); + expect(generated).toContain("OPENCLAW_CODE_SIGN_ENTITLEMENTS = Sources/OpenClaw.entitlements"); expect(generated).toContain("OPENCLAW_APP_BUNDLE_ID = ai.openclawfoundation.app"); expect(generated).toContain("OPENCLAW_SHARE_BUNDLE_ID = ai.openclawfoundation.app.share"); expect(generated).toContain("OPENCLAW_APP_GROUP_ID = group.ai.openclawfoundation.app.shared"); diff --git a/test/scripts/ios-release-signing.test.ts b/test/scripts/ios-release-signing.test.ts index 11762a32b3f..fd124fc95f6 100644 --- a/test/scripts/ios-release-signing.test.ts +++ b/test/scripts/ios-release-signing.test.ts @@ -79,7 +79,7 @@ describe("scripts/ios-release-signing.mjs", () => { expect(output).toContain("Signing branch: main"); expect(output).toContain("Signing setup and sync: Fastlane match"); expect(output).not.toContain("OpenClawWatchExtension"); - expect(output).toContain("capabilities: PUSH_NOTIFICATIONS, APP_GROUPS"); + expect(output).toContain("capabilities: PUSH_NOTIFICATIONS, APP_GROUPS, APP_ATTEST"); expect(output).toContain("app groups: group.ai.openclawfoundation.app.shared"); }); }); diff --git a/test/scripts/ios-run.test.ts b/test/scripts/ios-run.test.ts index afb02395fc1..0c4a917b54b 100644 --- a/test/scripts/ios-run.test.ts +++ b/test/scripts/ios-run.test.ts @@ -39,12 +39,18 @@ function makeFixture(bundleId: string): { root: string; script: string; logFile: path.join(scriptsDir, "ios-configure-signing.sh"), `#!/usr/bin/env bash set -euo pipefail +if [[ -n "\${OPENCLAW_SIMULATOR_PUSH_PROOF_SECRET:-}" || -n "\${CUSTOM_SIMULATOR_PUSH_PROOF_SECRET:-}" ]]; then + printf 'configure-signing-proof-env leaked\\n' >>"${logFile}" +fi `, ); writeExecutable( path.join(scriptsDir, "ios-write-version-xcconfig.sh"), `#!/usr/bin/env bash set -euo pipefail +if [[ -n "\${OPENCLAW_SIMULATOR_PUSH_PROOF_SECRET:-}" || -n "\${CUSTOM_SIMULATOR_PUSH_PROOF_SECRET:-}" ]]; then + printf 'write-version-proof-env leaked\\n' >>"${logFile}" +fi `, ); writeExecutable( @@ -52,6 +58,12 @@ set -euo pipefail `#!/usr/bin/env bash set -euo pipefail printf 'xcodegen %s\\n' "$*" >>"${logFile}" +if [[ -n "\${OPENCLAW_SIMULATOR_PUSH_PROOF_SECRET:-}" ]]; then + printf 'xcodegen-proof-env leaked\\n' >>"${logFile}" +fi +if [[ -n "\${CUSTOM_SIMULATOR_PUSH_PROOF_SECRET:-}" ]]; then + printf 'xcodegen-custom-proof-env leaked\\n' >>"${logFile}" +fi `, ); writeExecutable( @@ -59,6 +71,12 @@ printf 'xcodegen %s\\n' "$*" >>"${logFile}" `#!/usr/bin/env bash set -euo pipefail printf 'xcodebuild %s\\n' "$*" >>"${logFile}" +if [[ -n "\${OPENCLAW_SIMULATOR_PUSH_PROOF_SECRET:-}" ]]; then + printf 'xcodebuild-proof-env leaked\\n' >>"${logFile}" +fi +if [[ -n "\${CUSTOM_SIMULATOR_PUSH_PROOF_SECRET:-}" ]]; then + printf 'xcodebuild-custom-proof-env leaked\\n' >>"${logFile}" +fi derived="" configuration="Debug" while [[ $# -gt 0 ]]; do @@ -90,6 +108,13 @@ PLIST `#!/usr/bin/env bash set -euo pipefail printf 'simctl %s\\n' "$*" >>"${logFile}" +if [[ "$1" == "launch" ]]; then + if [[ -n "\${SIMCTL_CHILD_OPENCLAW_SIMULATOR_PUSH_PROOF_SECRET:-}" ]]; then + printf 'simctl-launch-proof set\\n' >>"${logFile}" + else + printf 'simctl-launch-proof unset\\n' >>"${logFile}" + fi +fi if [[ "$1" == "boot" ]]; then if [[ "\${SIMCTL_BOOT_MODE:-}" == "booted" ]]; then echo "Unable to boot device in current state: Booted" >&2 @@ -113,8 +138,12 @@ sed -n 's:.*CFBundleIdentifier\\([^<]*\\).*:\\1:p' " return { root, script, logFile }; } -function runIosRun(fixture: { root: string; script: string }, extraEnv = {}): string { - return execFileSync(BASH_BIN, bashArgs(fixture.script), { +function runIosRun( + fixture: { root: string; script: string }, + extraEnv = {}, + args: string[] = [], +): string { + return execFileSync(BASH_BIN, [...bashArgs(fixture.script), ...args], { env: { ...process.env, IOS_DERIVED_DATA_DIR: path.join(fixture.root, "DerivedData"), @@ -146,6 +175,54 @@ describe("scripts/ios-run.sh", () => { ); }); + it("builds simulator sandbox relay mode and injects proof secret only at launch", () => { + const fixture = makeFixture("ai.openclawfoundation.app"); + const proofSecret = "x".repeat(32); + + runIosRun( + fixture, + { + OPENCLAW_SIMULATOR_PUSH_PROOF_SECRET: proofSecret, + SIMCTL_BOOT_MODE: "booted", + }, + ["--push-sandbox-simulator"], + ); + + const log = readFileSync(fixture.logFile, "utf8"); + expect(log).toContain("OPENCLAW_PUSH_TRANSPORT=relay"); + expect(log).toContain("OPENCLAW_PUSH_DISTRIBUTION=official"); + expect(log).toContain( + "OPENCLAW_PUSH_RELAY_BASE_URL=https://ios-push-relay-sandbox.openclaw.ai", + ); + expect(log).toContain("OPENCLAW_PUSH_APNS_ENVIRONMENT=sandbox"); + expect(log).toContain("OPENCLAW_PUSH_RELAY_PROFILE=simulatorSandbox"); + expect(log).toContain("OPENCLAW_PUSH_PROOF_POLICY=internalSimulator"); + expect(log).toContain("simctl launch iPhone 17 ai.openclawfoundation.app"); + expect(log).toContain("simctl-launch-proof set"); + expect(log).not.toContain("proof-env leaked"); + expect(log).not.toContain(proofSecret); + }); + + it("scrubs exported simulator proof secrets from normal build helpers", () => { + const fixture = makeFixture("ai.openclawfoundation.app"); + const proofSecret = "x".repeat(32); + const customProofSecret = "y".repeat(32); + + runIosRun(fixture, { + OPENCLAW_SIMULATOR_PUSH_PROOF_SECRET: proofSecret, + OPENCLAW_SIMULATOR_PUSH_PROOF_SECRET_ENV: "CUSTOM_SIMULATOR_PUSH_PROOF_SECRET", + CUSTOM_SIMULATOR_PUSH_PROOF_SECRET: customProofSecret, + SIMCTL_BOOT_MODE: "booted", + }); + + const log = readFileSync(fixture.logFile, "utf8"); + expect(log).toContain("simctl launch iPhone 17 ai.openclawfoundation.app"); + expect(log).toContain("simctl-launch-proof unset"); + expect(log).not.toContain("proof-env leaked"); + expect(log).not.toContain(proofSecret); + expect(log).not.toContain(customProofSecret); + }); + it("does not ignore simulator boot failures other than already booted", () => { const fixture = makeFixture("ai.openclawfoundation.app");