diff options
Diffstat (limited to 'ci/verify-apk.sh')
| -rwxr-xr-x | ci/verify-apk.sh | 194 |
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" |