aboutsummaryrefslogtreecommitdiff
path: root/ci/verify-apk.sh
diff options
context:
space:
mode:
Diffstat (limited to 'ci/verify-apk.sh')
-rwxr-xr-xci/verify-apk.sh194
1 files changed, 194 insertions, 0 deletions
diff --git a/ci/verify-apk.sh b/ci/verify-apk.sh
new file mode 100755
index 0000000..fde1cb8
--- /dev/null
+++ b/ci/verify-apk.sh
@@ -0,0 +1,194 @@
+#!/bin/sh
+# Verify the release APK's identity, alignment, native payload, and signature.
+set -eu
+
+root=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
+# shellcheck source=versions
+. "$root/versions"
+apk=${1:-$root/app/build/outputs/apk/release/app-release-unsigned.apk}
+[ -f "$apk" ] || { echo "verify-apk: not found: $apk" >&2; exit 1; }
+command -v readelf >/dev/null 2>&1 || { echo "verify-apk: readelf is required" >&2; exit 1; }
+tmp=$(mktemp -d)
+trap 'rm -rf "$tmp"' 0 1 2 3 15
+
+entries=$(unzip -Z1 "$apk")
+actual=$(printf '%s\n' "$entries" | sed -n 's|^lib/\([^/]*\)/.*|\1|p' | sort -u | tr '\n' ' ' | sed 's/ $//')
+expected=$(printf '%s\n' "$ABIS" | tr ' ' '\n' | sed '/^$/d' | sort -u | tr '\n' ' ' | sed 's/ $//')
+[ "$actual" = "$expected" ] || {
+ echo "verify-apk: native ABIs are '$actual', expected '$expected'" >&2
+ exit 1
+}
+for abi in $ABIS; do
+ for lib in libxrsync.so libxrsh.so; do
+ path="lib/$abi/$lib"
+ printf '%s\n' "$entries" | grep -Fqx "$path" || {
+ echo "verify-apk: missing lib/$abi/$lib" >&2
+ exit 1
+ }
+ elf="$tmp/$abi-$lib"
+ unzip -p "$apk" "$path" > "$elf"
+ header=$(readelf -W -h "$elf")
+ printf '%s\n' "$header" | grep -Fq 'DYN (Position-Independent Executable file)' || {
+ echo "verify-apk: $path is not PIE" >&2
+ exit 1
+ }
+ case "$abi" in
+ arm64-v8a) machine=AArch64 ;;
+ x86_64) machine='Advanced Micro Devices X86-64' ;;
+ *) echo "verify-apk: no machine check for $abi" >&2; exit 1 ;;
+ esac
+ actual_machine=$(printf '%s\n' "$header" | sed -n 's/^[[:space:]]*Machine:[[:space:]]*//p')
+ [ "$actual_machine" = "$machine" ] || {
+ echo "verify-apk: $path has the wrong machine type" >&2
+ exit 1
+ }
+ program_headers=$(readelf -W -l "$elf")
+ printf '%s\n' "$program_headers" | grep -Fq 'GNU_RELRO' || {
+ echo "verify-apk: $path has no RELRO segment" >&2
+ exit 1
+ }
+ load_alignment=$(printf '%s\n' "$program_headers" |
+ awk '$1 == "LOAD" { print $NF }' | sort -u)
+ [ "$load_alignment" = "0x4000" ] || {
+ echo "verify-apk: $path LOAD alignment is '$load_alignment', expected 0x4000" >&2
+ exit 1
+ }
+ stack=$(printf '%s\n' "$program_headers" | sed -n '/GNU_STACK/p')
+ if [ -z "$stack" ] || printf '%s\n' "$stack" | grep -Eq 'RWE|RWX'; then
+ echo "verify-apk: $path has an executable or missing stack marker" >&2
+ exit 1
+ fi
+ dynamic=$(readelf -W -d "$elf")
+ printf '%s\n' "$dynamic" | grep -Fq 'BIND_NOW' || {
+ echo "verify-apk: $path does not use immediate binding" >&2
+ exit 1
+ }
+ ! printf '%s\n' "$dynamic" | grep -Eq 'RPATH|RUNPATH|TEXTREL' || {
+ echo "verify-apk: $path has an unsafe dynamic-linker flag" >&2
+ exit 1
+ }
+ needed=$(printf '%s\n' "$dynamic" | sed -n 's/.*Shared library: \[\(.*\)\]/\1/p' | LC_ALL=C sort)
+ if [ "$lib" = libxrsh.so ]; then
+ expected_needed=$(printf '%s\n' libc.so libdl.so liblog.so | LC_ALL=C sort)
+ else
+ expected_needed=$(printf '%s\n' libc.so libdl.so | LC_ALL=C sort)
+ fi
+ [ "$needed" = "$expected_needed" ] || {
+ echo "verify-apk: $path system libraries changed:$needed" >&2
+ exit 1
+ }
+ sections=$(readelf -W -S "$elf")
+ ! printf '%s\n' "$sections" | grep -Eq '\.debug_|\.symtab' || {
+ echo "verify-apk: $path is not stripped" >&2
+ exit 1
+ }
+ if [ "$lib" = libxrsync.so ]; then
+ symbols=$(readelf -W -s "$elf")
+ printf '%s\n' "$symbols" | grep -Fq '__stack_chk_fail' || {
+ echo "verify-apk: $path has no stack protector" >&2
+ exit 1
+ }
+ printf '%s\n' "$symbols" | grep -Fq '__memcpy_chk' || {
+ echo "verify-apk: $path has no fortified libc calls" >&2
+ exit 1
+ }
+ fi
+ done
+done
+native_entries=$(printf '%s\n' "$entries" | sed -n '/^lib\//p')
+unexpected=
+if unexpected=$(printf '%s\n' "$native_entries" |
+ grep -Ev '^lib/[^/]+/(libxrsync|libxrsh)\.so$'); then
+ :
+else
+ status=$?
+ [ "$status" -eq 1 ] || exit "$status"
+fi
+[ -z "$unexpected" ] || { echo "verify-apk: unexpected native files:$unexpected" >&2; exit 1; }
+! printf '%s\n' "$entries" | grep -Fqx META-INF/version-control-info.textproto || {
+ echo "verify-apk: APK contains a VCS stamp" >&2
+ exit 1
+}
+for notice in assets/APACHE-2.0 assets/LICENSE assets/THIRD_PARTY; do
+ printf '%s\n' "$entries" | grep -Fqx "$notice" || {
+ echo "verify-apk: missing $notice" >&2
+ exit 1
+ }
+ asset="$tmp/$(basename "$notice")"
+ unzip -p "$apk" "$notice" > "$asset"
+ cmp "$asset" "$root/$(basename "$notice")"
+done
+
+zipalign -c -P 16 4 "$apk" >/dev/null
+badging=$(aapt dump badging "$apk")
+printf '%s\n' "$badging" | grep -Fq "package: name='invalid.lena.rsend' versionCode='$APP_VERSION_CODE' versionName='$APP_VERSION'" || {
+ echo "verify-apk: package version does not match versions" >&2
+ exit 1
+}
+printf '%s\n' "$badging" | grep -Fqx "sdkVersion:'$ANDROID_MIN_SDK'" || {
+ echo "verify-apk: minimum SDK does not match versions" >&2
+ exit 1
+}
+printf '%s\n' "$badging" | grep -Fqx "targetSdkVersion:'$ANDROID_TARGET_SDK'" || {
+ echo "verify-apk: target SDK does not match versions" >&2
+ exit 1
+}
+! printf '%s\n' "$badging" | grep -Fq 'application-debuggable' || {
+ echo "verify-apk: release APK is debuggable" >&2
+ exit 1
+}
+permission_dump=$(aapt dump permissions "$apk")
+permissions=$(printf '%s\n' "$permission_dump" |
+ sed -n "s/^uses-permission: name='\(.*\)'$/\1/p" | LC_ALL=C sort)
+expected_permissions=$(cat <<EOF | LC_ALL=C sort
+android.permission.ACCESS_NETWORK_STATE
+android.permission.FOREGROUND_SERVICE
+android.permission.FOREGROUND_SERVICE_DATA_SYNC
+android.permission.INTERNET
+android.permission.MANAGE_EXTERNAL_STORAGE
+android.permission.POST_NOTIFICATIONS
+android.permission.RECEIVE_BOOT_COMPLETED
+android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
+android.permission.WAKE_LOCK
+invalid.lena.rsend.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION
+EOF
+)
+[ "$permissions" = "$expected_permissions" ] || {
+ echo "verify-apk: permission set changed:$permissions" >&2
+ exit 1
+}
+
+signature="$tmp/signature"
+if apksigner verify --min-sdk-version "$ANDROID_MIN_SDK" --verbose "$apk" >"$signature" 2>&1; then
+ case "$(basename "$apk")" in
+ *-unsigned.apk)
+ echo "verify-apk: unsigned APK unexpectedly has a valid signature" >&2
+ exit 1
+ ;;
+ esac
+ # At API 30 apksigner reports only v3 as used, even when valid v1 and v2
+ # signatures are present. API 23 makes it verify v1 for that platform and
+ # v2/v3 for later platforms in the same pass.
+ schemes="$tmp/schemes"
+ apksigner verify --min-sdk-version 23 --verbose "$apk" >"$schemes" 2>&1
+ grep -Fq 'Verified using v1 scheme (JAR signing): true' "$schemes"
+ grep -Fq 'Verified using v2 scheme (APK Signature Scheme v2): true' "$schemes"
+ grep -Fq 'Verified using v3 scheme (APK Signature Scheme v3): true' "$schemes"
+ echo "verify-apk: signed APK verified (v1, v2, v3)"
+else
+ case "$(basename "$apk")" in
+ *-unsigned.apk) ;;
+ *)
+ cat "$signature" >&2
+ echo "verify-apk: signed APK failed verification" >&2
+ exit 1
+ ;;
+ esac
+ ! printf '%s\n' "$entries" | grep -Eq '^META-INF/.*\.(RSA|DSA|EC)$' || {
+ echo "verify-apk: unsigned APK contains a signature file" >&2
+ exit 1
+ }
+ echo "verify-apk: unsigned APK"
+fi
+
+echo "verify-apk: ok"