diff --git a/check-ios-hotspot-signature.sh b/check-ios-hotspot-signature.sh new file mode 100644 index 0000000..fd62d02 --- /dev/null +++ b/check-ios-hotspot-signature.sh @@ -0,0 +1,299 @@ +#!/usr/bin/env bash +set -u + +ENTITLEMENT_KEY="com.apple.developer.networking.HotspotConfiguration" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TIMESTAMP="$(date '+%Y%m%d-%H%M%S')" +LOG_FILE="${SCRIPT_DIR}/ios-hotspot-signature-check-${TIMESTAMP}.log" +WORK_DIR="" + +exec > >(tee -a "${LOG_FILE}") 2>&1 + +cleanup() { + if [[ -n "${WORK_DIR}" && -d "${WORK_DIR}" ]]; then + rm -rf "${WORK_DIR}" + fi +} +trap cleanup EXIT + +print_usage() { + cat </dev/null 2>&1 +} + +extract_plist_from_binary() { + local source_file="$1" + local target_file="$2" + + python3 - "$source_file" "$target_file" <<'PY' +import sys +from pathlib import Path + +source = Path(sys.argv[1]) +target = Path(sys.argv[2]) +data = source.read_bytes() + +start = data.find(b"", start) +if end < 0: + sys.exit(3) + +target.write_bytes(data[start:end + len(b"")]) +PY +} + +check_plist_key_true() { + local plist_file="$1" + local key="$2" + + python3 - "$plist_file" "$key" <<'PY' +import plistlib +import re +import sys +from pathlib import Path + +path = Path(sys.argv[1]) +key = sys.argv[2] +data = path.read_bytes() + +try: + value = plistlib.loads(data).get(key) + if value is True: + print("true") + sys.exit(0) + if value is None: + print("missing") + sys.exit(1) + print(repr(value)) + sys.exit(2) +except Exception: + text = data.decode("utf-8", errors="ignore") + pattern = re.compile( + r"\s*" + re.escape(key) + r"\s*\s*", + re.IGNORECASE, + ) + if pattern.search(text): + print("true") + sys.exit(0) + if key in text: + print("present_but_not_true_or_unparsed") + sys.exit(2) + print("missing") + sys.exit(1) +PY +} + +find_main_executable() { + local app_dir="$1" + local info_plist="${app_dir}/Info.plist" + + if [[ -f "${info_plist}" ]] && has_command python3; then + local exe_name + exe_name="$(python3 - "$info_plist" <<'PY' +import plistlib +import sys +from pathlib import Path + +try: + info = plistlib.loads(Path(sys.argv[1]).read_bytes()) + print(info.get("CFBundleExecutable", "")) +except Exception: + print("") +PY +)" + if [[ -n "${exe_name}" && -f "${app_dir}/${exe_name}" ]]; then + printf '%s\n' "${app_dir}/${exe_name}" + return 0 + fi + fi + + find "${app_dir}" -maxdepth 1 -type f ! -name '*.plist' ! -name 'PkgInfo' -printf '%s %p\n' \ + | sort -nr \ + | awk 'NR==1 { sub(/^[0-9]+ /, ""); print }' +} + +print_section() { + echo + echo "==== $* ====" +} + +echo "iOS Hotspot Configuration Signature Check" +echo "Started: $(date -Is)" +echo "Script: ${BASH_SOURCE[0]}" +echo "Log: ${LOG_FILE}" +echo + +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + print_usage + exit 0 +fi + +IPA_PATH="${1:-}" +if [[ -z "${IPA_PATH}" ]]; then + print_usage + fail "Missing IPA path." +fi + +if [[ ! -f "${IPA_PATH}" ]]; then + fail "IPA not found: ${IPA_PATH}" +fi + +for cmd in unzip find grep sed awk sort date mktemp tee; do + if ! has_command "${cmd}"; then + fail "Required command not found: ${cmd}" + fi +done + +if ! has_command python3; then + fail "Required command not found: python3" +fi + +if ! has_command openssl; then + warn "openssl not found. embedded.mobileprovision will be parsed with a fallback XML extractor." +fi + +WORK_DIR="$(mktemp -d)" +EXTRACT_DIR="${WORK_DIR}/ipa" +mkdir -p "${EXTRACT_DIR}" + +print_section "Input" +info "IPA: ${IPA_PATH}" +info "IPA size: $(du -h "${IPA_PATH}" | awk '{print $1}')" +info "Work dir: ${WORK_DIR}" + +print_section "Extract IPA" +unzip -q "${IPA_PATH}" -d "${EXTRACT_DIR}" || fail "Failed to unzip IPA." + +APP_DIR="$(find "${EXTRACT_DIR}/Payload" -maxdepth 1 -type d -name '*.app' | head -n 1)" +if [[ -z "${APP_DIR}" ]]; then + fail "No .app directory found under Payload." +fi +pass "Found app bundle: ${APP_DIR}" + +PROFILE_PATH="${APP_DIR}/embedded.mobileprovision" +if [[ ! -f "${PROFILE_PATH}" ]]; then + fail "embedded.mobileprovision not found in app bundle." +fi +pass "Found provisioning profile: ${PROFILE_PATH}" + +EXECUTABLE_PATH="$(find_main_executable "${APP_DIR}")" +if [[ -z "${EXECUTABLE_PATH}" || ! -f "${EXECUTABLE_PATH}" ]]; then + fail "Could not find main app executable." +fi +pass "Found main executable: ${EXECUTABLE_PATH}" + +print_section "Provisioning Profile Entitlements" +PROFILE_PLIST="${WORK_DIR}/embedded.mobileprovision.plist" + +PROFILE_EXTRACTED=0 +if has_command openssl; then + if openssl smime -inform DER -verify -noverify -in "${PROFILE_PATH}" -out "${PROFILE_PLIST}" >/dev/null 2>&1; then + PROFILE_EXTRACTED=1 + pass "Decoded embedded.mobileprovision with openssl smime." + elif openssl cms -inform DER -verify -noverify -in "${PROFILE_PATH}" -out "${PROFILE_PLIST}" >/dev/null 2>&1; then + PROFILE_EXTRACTED=1 + pass "Decoded embedded.mobileprovision with openssl cms." + else + warn "openssl could not decode embedded.mobileprovision; trying XML fallback." + fi +fi + +if [[ "${PROFILE_EXTRACTED}" -eq 0 ]]; then + if extract_plist_from_binary "${PROFILE_PATH}" "${PROFILE_PLIST}"; then + pass "Extracted plist XML from embedded.mobileprovision." + else + fail "Could not extract plist from embedded.mobileprovision." + fi +fi + +PROFILE_RESULT="$(check_plist_key_true "${PROFILE_PLIST}" "${ENTITLEMENT_KEY}" 2>&1)" +PROFILE_STATUS=$? +echo "Profile entitlement value: ${PROFILE_RESULT}" +if [[ "${PROFILE_STATUS}" -eq 0 ]]; then + pass "Provisioning profile contains ${ENTITLEMENT_KEY}=true" +else + fail "Provisioning profile does not contain ${ENTITLEMENT_KEY}=true" +fi + +echo +echo "Profile entitlement context:" +grep -n -A2 -B1 "${ENTITLEMENT_KEY}" "${PROFILE_PLIST}" || true + +print_section "Signed App Entitlements" +echo "Linux cannot run Apple's codesign. This is a best-effort scan of the app executable." + +if grep -a -q "${ENTITLEMENT_KEY}" "${EXECUTABLE_PATH}"; then + pass "Executable contains entitlement key string: ${ENTITLEMENT_KEY}" +else + fail "Executable does not contain entitlement key string. Signed app entitlements likely do not include Hotspot Configuration." +fi + +EXEC_ENTITLEMENTS_PLIST="${WORK_DIR}/signed-entitlements-heuristic.plist" +if extract_plist_from_binary "${EXECUTABLE_PATH}" "${EXEC_ENTITLEMENTS_PLIST}"; then + echo "Extracted executable plist candidate: ${EXEC_ENTITLEMENTS_PLIST}" + EXEC_RESULT="$(check_plist_key_true "${EXEC_ENTITLEMENTS_PLIST}" "${ENTITLEMENT_KEY}" 2>&1)" + EXEC_STATUS=$? + echo "Executable entitlement value: ${EXEC_RESULT}" + if [[ "${EXEC_STATUS}" -eq 0 ]]; then + pass "Executable plist candidate contains ${ENTITLEMENT_KEY}=true" + else + warn "Executable contains entitlement key, but extracted plist candidate did not prove value=true." + warn "Use macOS codesign for authoritative validation." + fi + echo + echo "Executable entitlement context:" + grep -n -A2 -B1 "${ENTITLEMENT_KEY}" "${EXEC_ENTITLEMENTS_PLIST}" || true +else + warn "Could not extract XML plist from executable. Key string was present, but value=true was not proven." + warn "Use macOS codesign for authoritative validation." +fi + +print_section "Summary" +pass "Provisioning profile Hotspot entitlement: true" +pass "Executable Hotspot entitlement key: present" +echo +echo "Full log written to:" +echo " ${LOG_FILE}" +echo +echo "Authoritative macOS check, if available:" +echo " codesign -d --entitlements :- Your.app"