add signature script.
This commit is contained in:
1 parent
97c2a18c10
commit
855d24bc64
1 file changed
+299
@@ -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 <<EOF
|
||||
Usage:
|
||||
$(basename "$0") /path/to/App.ipa
|
||||
|
||||
Checks:
|
||||
1. embedded.mobileprovision contains ${ENTITLEMENT_KEY}=true
|
||||
2. App executable appears to contain ${ENTITLEMENT_KEY}=true
|
||||
|
||||
Notes:
|
||||
Linux cannot run Apple's codesign tool. The executable check is a best-effort
|
||||
scan of the signed Mach-O payload. For authoritative signed entitlements,
|
||||
verify on macOS with: codesign -d --entitlements :- Your.app
|
||||
EOF
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo "[FAIL] $*"
|
||||
echo
|
||||
echo "Full log: ${LOG_FILE}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
warn() {
|
||||
echo "[WARN] $*"
|
||||
}
|
||||
|
||||
pass() {
|
||||
echo "[PASS] $*"
|
||||
}
|
||||
|
||||
info() {
|
||||
echo "[INFO] $*"
|
||||
}
|
||||
|
||||
has_command() {
|
||||
command -v "$1" >/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"<?xml")
|
||||
if start < 0:
|
||||
start = data.find(b"<plist")
|
||||
if start < 0:
|
||||
sys.exit(2)
|
||||
|
||||
end = data.find(b"</plist>", start)
|
||||
if end < 0:
|
||||
sys.exit(3)
|
||||
|
||||
target.write_bytes(data[start:end + len(b"</plist>")])
|
||||
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"<key>\s*" + re.escape(key) + r"\s*</key>\s*<true\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"
|
||||
Reference in new issue
Block a user