Verify Play-delivered Android installs

This commit is contained in:
SimpleTest 2026-08-08 18:52:52 +03:00
parent 38e297f83b
commit f3b5e63222
5 changed files with 319 additions and 0 deletions

View File

@ -19,6 +19,23 @@ before production access can be requested.
7. Start with an internal test on the owner’s device, then promote the verified 7. Start with an internal test on the owner’s device, then promote the verified
build to the closed track. build to the closed track.
After installing from the internal-track opt-in link, verify the delivery
boundary before testing authenticated flows:
```bash
./scripts/verify-play-installed-android.sh \
/secure/downloads/play-identities.json \
DEVICE_SERIAL \
1 \
0.1.0
```
The verifier is read-only. It requires the Google Play installer, one of the
recorded Play App Signing SHA-256 identities, the exact expected version, and a
verified `whoneedhelp.com` App Link that resolves to `MainActivity`. A locally
sideloaded APK intentionally fails this gate even if its UI and package name
look correct.
## Tester cohort ## Tester cohort
- Recruit at least 12 real people with Google or Google Workspace accounts. - Recruit at least 12 real people with Google or Google Workspace accounts.

View File

@ -98,6 +98,11 @@
## Testing ## Testing
- [ ] Internal track smoke test passed. - [ ] Internal track smoke test passed.
Before exercising product flows, run
`scripts/verify-play-installed-android.sh` with the protected Play
identity document, physical-device serial, and exact expected version.
It must confirm the Google Play installer, Play signing identity,
verified production App Link, and `MainActivity` resolution.
- [ ] Closed track created and opt-in link tested. - [ ] Closed track created and opt-in link tested.
- [ ] At least 12 testers continuously opted in for 14 days. - [ ] At least 12 testers continuously opted in for 14 days.
- [ ] Tester feedback and fixes documented. - [ ] Tester feedback and fixes documented.

View File

@ -301,6 +301,24 @@ an unmatched or duplicate certificate, and a Firebase project inconsistent
with the configured FCM service account. Provider files remain on disk after with the configured FCM service account. Provider files remain on disk after
the import and must be stored or removed deliberately. the import and must be stored or removed deliberately.
Once the production association has been deployed and the internal-track build
has been installed from Google Play, prove that the physical device is not
still running a sideloaded upload-key build:
```bash
./scripts/verify-play-installed-android.sh \
/secure/downloads/play-identities.json \
DEVICE_SERIAL \
EXPECTED_VERSION_CODE \
EXPECTED_VERSION_NAME
```
The command does not launch, install, uninstall, clear, or reconfigure the app.
It checks the installer, installed version, Play signing identity, Android's
domain-verification state, and implicit production App Link resolution. Passing
it is a prerequisite for the later authenticated Play-delivered smoke test, not
a substitute for that test.
The VAPID helper runs the exact locked `web_push_elixir` generator in an The VAPID helper runs the exact locked `web_push_elixir` generator in an
isolated, network-disabled container, imports the result atomically, removes isolated, network-disabled container, imports the result atomically, removes
its one-run image tag and temporary files, and never prints either key. It its one-run image tag and temporary files, and never prints either key. It

View File

@ -528,6 +528,101 @@ if ./scripts/import-play-android-config.sh \
fi fi
test "$(sha256sum "$play_env" | awk '{print $1}')" = "$play_hash_after" test "$(sha256sum "$play_env" | awk '{print $1}')" = "$play_hash_after"
echo "Checking Google Play installed Android verification"
fake_play_adb="$scan_dir/fake-play-adb"
cat >"$fake_play_adb" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
if [[ ${1:-} != -s || ${2:-} != quality-play-device ]]; then
echo "unexpected device selection" >&2
exit 1
fi
shift 2
case "${1:-} ${2:-} ${3:-}" in
"get-state ")
printf 'device\n'
;;
"shell pm path")
printf 'package:/data/app/quality/base.apk\n'
;;
"shell dumpsys package")
cat <<REPORT
Package [org.whoneedhelp.mobile] (quality):
versionCode=${FAKE_PLAY_VERSION_CODE:-1} minSdk=24 targetSdk=37
versionName=${FAKE_PLAY_VERSION_NAME:-0.1.0}
installerPackageName=${FAKE_PLAY_INSTALLER:-com.android.vending}
REPORT
;;
"shell pm get-app-links")
cat <<REPORT
org.whoneedhelp.mobile:
Signatures: [${FAKE_PLAY_SIGNATURE:?Set FAKE_PLAY_SIGNATURE}]
Domain verification state:
whoneedhelp.com: ${FAKE_PLAY_DOMAIN_STATE:-verified}
REPORT
;;
"shell cmd package")
printf '%s\n' "${FAKE_PLAY_ACTIVITY:-org.whoneedhelp.mobile/.MainActivity}"
;;
*)
printf 'unexpected adb command: %s\n' "$*" >&2
exit 1
;;
esac
EOF
chmod 700 "$fake_play_adb"
play_device_output=$(
WNH_ADB_BIN="$fake_play_adb" \
FAKE_PLAY_SIGNATURE="$play_sha256_two_colon" \
./scripts/verify-play-installed-android.sh \
"$play_identities" quality-play-device 1 0.1.0
)
printf '%s' "$play_device_output" |
grep -F 'Google Play installed Android verification passed.' >/dev/null
if printf '%s' "$play_device_output" |
grep -F "$play_sha256_two_colon" >/dev/null; then
echo "Play-installed verifier printed a signing fingerprint." >&2
exit 1
fi
if WNH_ADB_BIN="$fake_play_adb" \
FAKE_PLAY_SIGNATURE="$play_sha256_two_colon" \
FAKE_PLAY_INSTALLER=null \
./scripts/verify-play-installed-android.sh \
"$play_identities" quality-play-device 1 0.1.0 >/dev/null 2>&1; then
echo "Play-installed verifier accepted a sideloaded package." >&2
exit 1
fi
if WNH_ADB_BIN="$fake_play_adb" \
FAKE_PLAY_SIGNATURE="$play_upload_colon" \
./scripts/verify-play-installed-android.sh \
"$play_identities" quality-play-device 1 0.1.0 >/dev/null 2>&1; then
echo "Play-installed verifier accepted the upload certificate as a Play identity." >&2
exit 1
fi
if WNH_ADB_BIN="$fake_play_adb" \
FAKE_PLAY_SIGNATURE="$play_sha256_one_colon" \
FAKE_PLAY_DOMAIN_STATE=none \
./scripts/verify-play-installed-android.sh \
"$play_identities" quality-play-device 1 0.1.0 >/dev/null 2>&1; then
echo "Play-installed verifier accepted an unverified production App Link." >&2
exit 1
fi
if WNH_ADB_BIN="$fake_play_adb" \
FAKE_PLAY_SIGNATURE="$play_sha256_one_colon" \
FAKE_PLAY_ACTIVITY=com.android.browser/.BrowserActivity \
./scripts/verify-play-installed-android.sh \
"$play_identities" quality-play-device 1 0.1.0 >/dev/null 2>&1; then
echo "Play-installed verifier accepted browser App Link resolution." >&2
exit 1
fi
echo "Checking Android environment isolation" echo "Checking Android environment isolation"
./scripts/android-play-policy-check.sh >/dev/null ./scripts/android-play-policy-check.sh >/dev/null
android_fingerprint=D7:C4:F1:12:4D:F4:68:E5:B3:54:DE:D8:96:E8:01:51:29:41:F1:8A:71:0C:18:B0:E7:98:AA:2B:81:DA:11:DF android_fingerprint=D7:C4:F1:12:4D:F4:68:E5:B3:54:DE:D8:96:E8:01:51:29:41:F1:8A:71:0C:18:B0:E7:98:AA:2B:81:DA:11:DF

View File

@ -0,0 +1,184 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
usage() {
cat >&2 <<'EOF'
Usage: verify-play-installed-android.sh PLAY_IDENTITIES_JSON DEVICE_SERIAL EXPECTED_VERSION_CODE EXPECTED_VERSION_NAME
Verifies, without changing the device, that org.whoneedhelp.mobile was
installed by Google Play, is signed by one of the supplied Play App Signing
SHA-256 identities, has the expected version, and owns the verified production
App Link.
EOF
}
if [[ $# -ne 4 ]]; then
usage
exit 2
fi
identities_file=$1
device_serial=$2
expected_version_code=$3
expected_version_name=$4
package_name=org.whoneedhelp.mobile
app_link_host=whoneedhelp.com
app_link_url=https://whoneedhelp.com/safety
expected_activity=org.whoneedhelp.mobile/.MainActivity
adb_bin=${WNH_ADB_BIN:-adb}
if [[ "$identities_file" != /* ]]; then
identities_file="$ROOT/$identities_file"
fi
for command in jq sed tr; do
command -v "$command" >/dev/null 2>&1 || {
echo "Required command is unavailable: $command" >&2
exit 1
}
done
command -v "$adb_bin" >/dev/null 2>&1 || {
echo "adb is unavailable: $adb_bin" >&2
exit 1
}
[[ -f "$identities_file" && ! -L "$identities_file" ]] || {
echo "Play identities must be a regular non-symlink file: $identities_file" >&2
exit 1
}
case "$(stat -c '%a' "$identities_file")" in
400 | 600) ;;
*)
echo "Play identities must have mode 0400 or 0600: $identities_file" >&2
exit 1
;;
esac
[[ -n "$device_serial" && "$device_serial" != *$'\n'* && "$device_serial" != *$'\r'* ]] || {
echo "DEVICE_SERIAL must be a non-empty single-line value." >&2
exit 1
}
[[ "$expected_version_code" =~ ^[1-9][0-9]*$ ]] || {
echo "EXPECTED_VERSION_CODE must be a positive integer." >&2
exit 1
}
[[ -n "$expected_version_name" && "$expected_version_name" != *$'\n'* && "$expected_version_name" != *$'\r'* ]] || {
echo "EXPECTED_VERSION_NAME must be a non-empty single-line value." >&2
exit 1
}
if ! jq --exit-status --arg package "$package_name" '
def valid_sha256:
test("^[0-9A-Fa-f]{64}$|^([0-9A-Fa-f]{2}:){31}[0-9A-Fa-f]{2}$");
def normalized_sha256:
ascii_upcase | gsub(":"; "");
(.package_name == $package)
and (.identities | type == "array" and length > 0)
and all(
.identities[];
(.sha256 | type == "string" and valid_sha256)
)
and (([.identities[].sha256 | normalized_sha256] | unique | length)
== (.identities | length))
' "$identities_file" >/dev/null; then
echo "Play identities are incomplete, malformed, duplicated, or belong to another package." >&2
exit 1
fi
mapfile -t expected_fingerprints < <(
jq --raw-output '.identities[].sha256 | ascii_upcase | gsub(":"; "")' \
"$identities_file"
)
adb_device() {
"$adb_bin" -s "$device_serial" "$@"
}
[[ "$(adb_device get-state 2>/dev/null | tr -d '\r')" == device ]] || {
echo "The selected Android device is not connected and authorised." >&2
exit 1
}
package_path=$(adb_device shell pm path "$package_name" 2>/dev/null | tr -d '\r')
[[ "$package_path" == package:* ]] || {
echo "$package_name is not installed on the selected device." >&2
exit 1
}
package_report=$(adb_device shell dumpsys package "$package_name")
observed_version_code=$(
sed -n 's/.*versionCode=\([0-9][0-9]*\).*/\1/p' <<<"$package_report" | head -n 1
)
observed_version_name=$(
sed -n 's/^[[:space:]]*versionName=\(.*\)$/\1/p' <<<"$package_report" | head -n 1 | tr -d '\r'
)
installer=$(
sed -n 's/^[[:space:]]*installerPackageName=\(.*\)$/\1/p' <<<"$package_report" | head -n 1 | tr -d '\r'
)
[[ "$observed_version_code" == "$expected_version_code" ]] || {
echo "Installed versionCode does not match the expected Play release." >&2
exit 1
}
[[ "$observed_version_name" == "$expected_version_name" ]] || {
echo "Installed versionName does not match the expected Play release." >&2
exit 1
}
[[ "$installer" == com.android.vending ]] || {
echo "The installed package was not delivered by Google Play." >&2
exit 1
}
links_report=$(adb_device shell pm get-app-links "$package_name")
signature_line=$(
sed -n 's/^[[:space:]]*Signatures: \[\(.*\)\][[:space:]]*$/\1/p' \
<<<"$links_report" | head -n 1
)
[[ -n "$signature_line" ]] || {
echo "Android did not report a signing identity for the installed package." >&2
exit 1
}
signature_match=false
IFS=',' read -r -a observed_signatures <<<"$signature_line"
for observed_signature in "${observed_signatures[@]}"; do
observed_compact=$(printf '%s' "$observed_signature" | tr '[:lower:]' '[:upper:]' | tr -d ':[:space:]')
for expected_fingerprint in "${expected_fingerprints[@]}"; do
if [[ "$observed_compact" == "$expected_fingerprint" ]]; then
signature_match=true
break 2
fi
done
done
[[ "$signature_match" == true ]] || {
echo "The installed package is not signed by a supplied Play App Signing identity." >&2
exit 1
}
if ! grep -Eq "^[[:space:]]+$app_link_host:[[:space:]]+verified[[:space:]]*$" \
<<<"$links_report"; then
echo "The production Android App Link domain is not verified on the device." >&2
exit 1
fi
resolved_activity=$(
adb_device shell cmd package resolve-activity --brief \
-a android.intent.action.VIEW \
-c android.intent.category.BROWSABLE \
-d "$app_link_url" |
tr -d '\r'
)
if ! grep -Fx "$expected_activity" <<<"$resolved_activity" >/dev/null; then
echo "The production App Link does not resolve to Who Need Help MainActivity." >&2
exit 1
fi
echo "Google Play installed Android verification passed."
echo "Package: $package_name"
echo "Version: $observed_version_name ($observed_version_code)"
echo "Installer: Google Play"
echo "Signing identity: supplied Play App Signing set member"
echo "App Link: $app_link_host verified and resolved to MainActivity"