diff --git a/scripts/kin-sign-dev-review.sh b/scripts/kin-sign-dev-review.sh index be8cbdc..8254ebb 100644 --- a/scripts/kin-sign-dev-review.sh +++ b/scripts/kin-sign-dev-review.sh @@ -9,6 +9,18 @@ fi # The example reserves separate step deadlines plus job-level cleanup slack. set +x set -euo pipefail +# Keep tool output out of public workflow logs; emit fixed messages via saved fds. +exec 3>&2 4>&1 2>/dev/null 1>/dev/null +stage=input_validation +report_failure() { + local status=$? + trap - EXIT + if (( status != 0 )); then + printf "Kin Dev signing failed at stage: %s\n" "$stage" >&3 + fi + exit "$status" +} +trap report_failure EXIT umask 077 [[ "${GITHUB_REF:-}" == refs/heads/main && "${GITHUB_SHA:-}" =~ ^[0-9a-f]{40}$ ]] [[ "${GITHUB_RUN_ID:-}" =~ ^[0-9]+$ && "${GITHUB_RUN_ATTEMPT:-}" =~ ^[0-9]+$ ]] @@ -16,11 +28,13 @@ umask 077 [[ "${VERSION:-}" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ && "${BUILD:-}" =~ ^[1-9][0-9]*$ ]] [[ "${CERT_SHA256:-}" =~ ^[a-f0-9]{64}$ ]] test -n "${P12_BASE64:-}" && test -n "${P12_PASSWORD:-}" && test -n "${PROFILE_BASE64:-}" +stage=source_and_toolchain test "$(git rev-parse HEAD)" = "$GITHUB_SHA" test -z "$(git status --porcelain)" test "$(git ls-remote https://github.com/dannydover/Kin.git refs/heads/main 2>/dev/null | cut -f1)" = "$GITHUB_SHA" test "$(xcodebuild -version)" = "$(printf 'Xcode 27.0\nBuild version 27A266a')" test "$(uname -m)" = arm64 +stage=profile_staging work="$RUNNER_TEMP/kin-sign-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT" mkdir -m 700 "$work" profile_dir="$HOME/Library/MobileDevice/Provisioning Profiles" @@ -34,6 +48,7 @@ stat -f '%d:%i' "$profile" > "$work/profile-identity" shasum -a 256 "$profile" | cut -d ' ' -f1 > "$work/profile-sha256" printf '%s\n' "$profile" > "$work/owned-profile" printf '%s' "$P12_BASE64" | base64 -D > "$work/identity.p12" +stage=keychain_setup key_password="$(openssl rand -hex 24)" security list-keychains -d user > "$work/search-list-original" 2> "$work/security.log" security create-keychain -p "$key_password" "$work/signing.keychain-db" >>"$work/security.log" 2>&1 @@ -43,18 +58,22 @@ security import "$work/identity.p12" -k "$work/signing.keychain-db" -P "$P12_PAS security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$key_password" "$work/signing.keychain-db" >>"$work/security.log" 2>&1 security list-keychains -d user -s "$work/signing.keychain-db" >>"$work/security.log" 2>&1 unset P12_BASE64 P12_PASSWORD PROFILE_BASE64 key_password +stage=export_options export_options="$work/ExportOptions.plist" TEAM_ID="$TEAM_ID" PROFILE_UUID="$PROFILE_UUID" EXPORT_OPTIONS="$export_options" python3 -c ' import os, plistlib with open(os.environ["EXPORT_OPTIONS"], "xb") as f: plistlib.dump({"destination":"export", "method":"app-store-connect", "signingStyle":"manual", "teamID":os.environ["TEAM_ID"], "signingCertificate":"Apple Distribution", "provisioningProfiles":{"com.intriguingideas.Kin.dev":os.environ["PROFILE_UUID"]}}, f) ' +stage=archive xcodebuild -project Kin.xcodeproj -scheme 'Kin Dev' -configuration Dev -destination 'generic/platform=iOS' \ -derivedDataPath "$work/DerivedData" -archivePath "$work/Kin.xcarchive" archive \ CODE_SIGN_STYLE=Manual "DEVELOPMENT_TEAM=$TEAM_ID" "PROVISIONING_PROFILE_SPECIFIER=$PROFILE_UUID" \ 'CODE_SIGN_IDENTITY=Apple Distribution' >"$work/archive.log" 2>&1 +stage=export xcodebuild -exportArchive -archivePath "$work/Kin.xcarchive" -exportPath "$work/export" \ -exportOptionsPlist "$export_options" >"$work/export.log" 2>&1 +stage=output_verification ipa="$work/export/Kin.ipa" test -f "$ipa" && test ! -L "$ipa" # Reject unsafe names, link entries, oversized archives and multiple apps. @@ -75,7 +94,8 @@ test "$(shasum -a 256 "$work/codesign0" | cut -d ' ' -f1)" = "$CERT_SHA256" codesign --display --entitlements - --xml "$app" >"$work/entitlements.plist" 2>>"$work/verify.log" python3 scripts/check_dev_entitlements.py "$work/entitlements.plist" "$TEAM_ID" shasum -a 256 "$ipa" | cut -d ' ' -f1 > "$work/ipa-sha256" +stage=final_source_verification test "$(git rev-parse HEAD)" = "$GITHUB_SHA" test -z "$(git status --porcelain)" test "$(git ls-remote https://github.com/dannydover/Kin.git refs/heads/main 2>/dev/null | cut -f1)" = "$GITHUB_SHA" -printf '%s\n' 'Dev candidate verified in private workspace; no upload occurred.' +printf '%s\n' 'Dev candidate verified in private workspace; no upload occurred.' >&4 diff --git a/scripts/tests/test_dev_signing_trial.py b/scripts/tests/test_dev_signing_trial.py index f4b0afb..8c12c2f 100644 --- a/scripts/tests/test_dev_signing_trial.py +++ b/scripts/tests/test_dev_signing_trial.py @@ -26,6 +26,45 @@ def setUp(self): '"com.apple.developer.team-identifier":team}))\n') self.env['KIN_DEV_SIGNING_ONLY'] = '1' + def test_failure_stages_are_fixed_and_raw_tool_output_stays_private(self): + tool = self.bin / 'xcodebuild' + tool.write_text(tool.read_text().replace('if [[ "$FAKE_STAGE"', + 'printf "%s\\n" "private /fake/path synthetic-password"; printf "%s\\n" "private /fake/path synthetic-password" >&2; if [[ "$FAKE_STAGE"')) + for stage, code in [('archive', 17), ('export', 19)]: + with self.subTest(stage=stage): + env = dict(self.env, FAKE_STAGE=stage) + result = subprocess.run(['bash', str(self.root / 'export-test.sh')], env=env, + cwd=self.root, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=10) + self.assertEqual(result.returncode, code) + self.assertEqual(result.stderr, ('Kin Dev signing failed at stage: ' + stage + '\n').encode()) + self.assertNotIn(b'synthetic-password', result.stdout + result.stderr) + cleanup = subprocess.run(['bash', str(self.root / 'cleanup-test.sh')], env=env, + cwd=self.root, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=10) + self.assertEqual(cleanup.returncode, 0, cleanup.stderr) + self.assertEqual(list(self.runner.iterdir()), []) + + def test_input_failure_reports_only_input_stage(self): + result = subprocess.run(['bash', str(self.root / 'export-test.sh')], + env=dict(self.env, PROFILE_BASE64=''), cwd=self.root, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=10) + self.assertNotEqual(result.returncode, 0) + self.assertEqual(result.stderr, b'Kin Dev signing failed at stage: input_validation\n') + self.assertFalse((self.root / 'security-calls').exists()) + + def test_keychain_failure_is_redacted_and_cleanup_runs(self): + tool = self.bin / 'security' + tool.write_text(tool.read_text().replace('create-keychain) for last', + 'create-keychain) printf "private /fake/path synthetic-password\\n" >&2; exit 42; for last')) + env = dict(self.env, FAKE_STAGE='success') + result = subprocess.run(['bash', str(self.root / 'export-test.sh')], env=env, + cwd=self.root, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=10) + self.assertEqual(result.returncode, 42) + self.assertEqual(result.stderr, b'Kin Dev signing failed at stage: keychain_setup\n') + cleanup = subprocess.run(['bash', str(self.root / 'cleanup-test.sh')], env=env, + cwd=self.root, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=10) + self.assertEqual(cleanup.returncode, 0, cleanup.stderr) + self.assertEqual(list(self.runner.iterdir()), []) + def test_synthetic_output_check_and_debug_entitlement_failure(self): for debug in ('no', 'yes'): with self.subTest(debug=debug):