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