diff --git a/.env.example b/.env.example index b2fc3f2..b645e3c 100644 --- a/.env.example +++ b/.env.example @@ -98,6 +98,11 @@ WNH_FIREBASE_GCM_SENDER_ID= # production. Keep both empty until the matching signed APK/AAB is available. ANDROID_APP_LINKS_PACKAGE_NAME= ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS= +# Production-only evidence from Google Play Console. This must contain the +# Play App Signing certificate fingerprint(s), not the local upload key, and +# every value must also appear in ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS. +# Development and test leave this empty. +ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS= # Public identifier of the locally held Google Play upload key. The private # keystore and its randomized password live outside the repository under # ~/.config/who_need_help/android-release/. diff --git a/android/README.md b/android/README.md index 378f6ca..b07c261 100644 --- a/android/README.md +++ b/android/README.md @@ -122,6 +122,13 @@ separate app-signing key used for distributed APKs: - - +The production checkout therefore keeps the locally measured upload +certificate and the Play Console app-signing certificate as distinct evidence. +`ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS` publishes every active identity, +while `ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS` must contain the +Play-delivered identity as a verified subset. A release is not marked ready +from the upload certificate alone. + ## Public development and staging builds The installable `development` and `staging` build types use the explicit public diff --git a/docs/operations.md b/docs/operations.md index bcd1a79..8751bc2 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -160,15 +160,19 @@ separate upload material under ```dotenv WNH_ANDROID_SIGNING_KEY_ALIAS=who-need-help-upload ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile -ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS=UPLOAD_OR_PLAY_SHA256 +ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS=UPLOAD_SHA256,PLAY_APP_SIGNING_SHA256 +ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS=PLAY_APP_SIGNING_SHA256 ``` Run `./scripts/android-release-build.sh` from that production release checkout. It produces an APK, Play AAB, package report, signing report, and lint report. After Play App Signing is enabled, add the Play signing certificate fingerprint to the comma-separated App Links value; the upload certificate alone does not -describe Play-delivered APKs. The production environment validator accepts -multiple SHA-256 fingerprints and rejects partial or malformed configuration. +describe Play-delivered APKs. Record the same Play fingerprint separately in +`ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS`; release readiness verifies +that every Play identity is present in the public App Links list. The production +environment validator accepts multiple SHA-256 fingerprints and rejects partial, +malformed, upload-only, or inconsistent configuration. ### Release capability inputs diff --git a/scripts/check-environment-readiness.sh b/scripts/check-environment-readiness.sh index 9ce9c89..fdd4040 100755 --- a/scripts/check-environment-readiness.sh +++ b/scripts/check-environment-readiness.sh @@ -103,6 +103,20 @@ firebase_client_values_valid() { -n "${application_id#"$prefix"}" ]] } +valid_sha256_fingerprint_list() { + local fingerprint compact + local -a fingerprint_list + + IFS=',' read -r -a fingerprint_list <<<"$1" + [[ ${#fingerprint_list[@]} -gt 0 ]] || return 1 + + for fingerprint in "${fingerprint_list[@]}"; do + compact=${fingerprint//:/} + compact=${compact//[[:space:]]/} + [[ "$compact" =~ ^[0-9A-Fa-f]{64}$ ]] || return 1 + done +} + failures=0 warnings=0 @@ -248,17 +262,55 @@ case "$deployment_env" in production) expected_android_package=org.whoneedhelp.mobile ;; esac -if all_empty ANDROID_APP_LINKS_PACKAGE_NAME ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS; then +if all_empty ANDROID_APP_LINKS_PACKAGE_NAME \ + ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS \ + ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS; then missing "Android App Links" "package name and signing certificate fingerprint" elif all_set ANDROID_APP_LINKS_PACKAGE_NAME ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS; then - if [[ -n "$expected_android_package" && - "$(value ANDROID_APP_LINKS_PACKAGE_NAME)" == "$expected_android_package" ]]; then - ready "Android App Links" "package and signing fingerprints match this environment" - else + if [[ -z "$expected_android_package" || + "$(value ANDROID_APP_LINKS_PACKAGE_NAME)" != "$expected_android_package" ]]; then invalid "Android App Links" "package does not match DEPLOYMENT_ENV=$deployment_env" + elif ! valid_sha256_fingerprint_list \ + "$(value ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS)"; then + invalid "Android App Links" "published signing fingerprints are malformed" + elif [[ "$deployment_env" != production ]]; then + ready "Android App Links" "package and signing fingerprints match this environment" + elif ! is_set ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS; then + missing "Android App Links" "production requires the Play App Signing SHA-256 fingerprint" + elif ! valid_sha256_fingerprint_list \ + "$(value ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS)"; then + invalid "Android App Links" "Play App Signing fingerprints are malformed" + else + published_fingerprints=$(value ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS) + play_fingerprints=$(value ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS) + all_play_fingerprints_published=true + IFS=',' read -r -a play_fingerprint_list <<<"$play_fingerprints" + IFS=',' read -r -a published_fingerprint_list <<<"$published_fingerprints" + for play_fingerprint in "${play_fingerprint_list[@]}"; do + compact_play=${play_fingerprint//:/} + compact_play=${compact_play//[[:space:]]/} + play_found=false + for published_fingerprint in "${published_fingerprint_list[@]}"; do + compact_published=${published_fingerprint//:/} + compact_published=${compact_published//[[:space:]]/} + if [[ "${compact_play^^}" == "${compact_published^^}" ]]; then + play_found=true + break + fi + done + if [[ "$play_found" != true ]]; then + all_play_fingerprints_published=false + break + fi + done + if [[ "$all_play_fingerprints_published" == true ]]; then + ready "Android App Links" "published identities include the Play App Signing certificate" + else + invalid "Android App Links" "Play App Signing fingerprint is absent from the published identities" + fi fi else - partial "Android App Links" "package and signing fingerprints must be configured together" + partial "Android App Links" "package and signing fingerprints are incomplete" fi android_signing_alias= diff --git a/scripts/init-production-env.sh b/scripts/init-production-env.sh index c8157be..c47a36f 100755 --- a/scripts/init-production-env.sh +++ b/scripts/init-production-env.sh @@ -46,7 +46,7 @@ if ! printf '%s\n' "$domain" | exit 1 fi -for command in awk docker git grep mktemp openssl stat; do +for command in awk docker git grep mktemp openssl stat tr; do if ! command -v "$command" >/dev/null 2>&1; then echo "Required command is unavailable: $command" >&2 exit 1 @@ -89,6 +89,7 @@ fcm_project_id=${PRODUCTION_FCM_PROJECT_ID:-} fcm_service_account_json_base64=${PRODUCTION_FCM_SERVICE_ACCOUNT_JSON_BASE64:-} android_app_links_package_name=${PRODUCTION_ANDROID_APP_LINKS_PACKAGE_NAME:-} android_app_links_fingerprints=${PRODUCTION_ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS:-} +android_play_app_signing_fingerprints=${PRODUCTION_ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS:-} test_domain=${PRODUCTION_TEST_DOMAIN:-"test.$domain"} test_upstream=${PRODUCTION_TEST_UPSTREAM:-who-need-help-test:4000} edge_compose_project_name=${PRODUCTION_EDGE_COMPOSE_PROJECT_NAME:-who_need_help_edge} @@ -114,6 +115,7 @@ require_single_line_env_value PRODUCTION_FCM_PROJECT_ID "$fcm_project_id" require_single_line_env_value PRODUCTION_FCM_SERVICE_ACCOUNT_JSON_BASE64 "$fcm_service_account_json_base64" require_single_line_env_value PRODUCTION_ANDROID_APP_LINKS_PACKAGE_NAME "$android_app_links_package_name" require_single_line_env_value PRODUCTION_ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS "$android_app_links_fingerprints" +require_single_line_env_value PRODUCTION_ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS "$android_play_app_signing_fingerprints" require_single_line_env_value PRODUCTION_TEST_DOMAIN "$test_domain" require_single_line_env_value PRODUCTION_TEST_UPSTREAM "$test_upstream" require_single_line_env_value PRODUCTION_EDGE_COMPOSE_PROJECT_NAME "$edge_compose_project_name" @@ -129,9 +131,17 @@ if { [ -n "$google_oauth_client_id" ] || [ -n "$google_oauth_client_secret" ]; } exit 1 fi -if { [ -n "$android_app_links_package_name" ] || [ -n "$android_app_links_fingerprints" ]; } && - { [ -z "$android_app_links_package_name" ] || [ -z "$android_app_links_fingerprints" ]; }; then - echo "Production Android App Links package and fingerprints must either both be set or both be empty." >&2 +if { + [ -n "$android_app_links_package_name" ] || + [ -n "$android_app_links_fingerprints" ] || + [ -n "$android_play_app_signing_fingerprints" ] +} && + { + [ -z "$android_app_links_package_name" ] || + [ -z "$android_app_links_fingerprints" ] || + [ -z "$android_play_app_signing_fingerprints" ] + }; then + echo "Production Android App Links require the package, published fingerprints, and Play App Signing fingerprints together." >&2 exit 1 fi @@ -223,6 +233,42 @@ if [ -n "$android_app_links_package_name" ] && exit 1 fi +if [ -n "$android_app_links_fingerprints" ]; then + if ! printf '%s\n' "$android_app_links_fingerprints" | + tr ',' '\n' | + while IFS= read -r fingerprint; do + compact_fingerprint=$(printf '%s' "$fingerprint" | tr -d ':[:space:]') + printf '%s\n' "$compact_fingerprint" | + grep -Eq '^[0-9A-Fa-f]{64}$' || exit 1 + done; then + echo "PRODUCTION_ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS contains an invalid SHA-256 fingerprint." >&2 + exit 1 + fi + + published_compact_fingerprints=$( + printf '%s' "$android_app_links_fingerprints" | + tr ',' '\n' | + tr -d ':[:space:]' | + tr '[:lower:]' '[:upper:]' + ) + if ! printf '%s\n' "$android_play_app_signing_fingerprints" | + tr ',' '\n' | + while IFS= read -r play_fingerprint; do + compact_play_fingerprint=$( + printf '%s' "$play_fingerprint" | + tr -d ':[:space:]' | + tr '[:lower:]' '[:upper:]' + ) + printf '%s\n' "$compact_play_fingerprint" | + grep -Eq '^[0-9A-F]{64}$' || exit 1 + printf '%s\n' "$published_compact_fingerprints" | + grep -Fqx "$compact_play_fingerprint" || exit 1 + done; then + echo "Every production Play App Signing fingerprint must also be published in the Android App Links fingerprint list." >&2 + exit 1 + fi +fi + case "$compose_project_name" in *[!a-zA-Z0-9_-]* | '') echo "PRODUCTION_COMPOSE_PROJECT_NAME must contain only letters, numbers, underscores, or hyphens." >&2 @@ -360,6 +406,7 @@ FCM_PROJECT_ID_VALUE=$fcm_project_id \ FCM_SERVICE_ACCOUNT_JSON_BASE64_VALUE=$fcm_service_account_json_base64 \ ANDROID_APP_LINKS_PACKAGE_NAME_VALUE=$android_app_links_package_name \ ANDROID_APP_LINKS_FINGERPRINTS_VALUE=$android_app_links_fingerprints \ +ANDROID_PLAY_APP_SIGNING_FINGERPRINTS_VALUE=$android_play_app_signing_fingerprints \ EDGE_COMPOSE_PROJECT_NAME_VALUE=$edge_compose_project_name \ PRIMARY_UPSTREAM_VALUE="$public_upstream_name:4000" \ TEST_DOMAIN_VALUE=$test_domain \ @@ -427,6 +474,7 @@ TEST_UPSTREAM_VALUE=$test_upstream \ replacement["FCM_SERVICE_ACCOUNT_JSON_BASE64"] = ENVIRON["FCM_SERVICE_ACCOUNT_JSON_BASE64_VALUE"] replacement["ANDROID_APP_LINKS_PACKAGE_NAME"] = ENVIRON["ANDROID_APP_LINKS_PACKAGE_NAME_VALUE"] replacement["ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS"] = ENVIRON["ANDROID_APP_LINKS_FINGERPRINTS_VALUE"] + replacement["ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS"] = ENVIRON["ANDROID_PLAY_APP_SIGNING_FINGERPRINTS_VALUE"] replacement["WNH_ANDROID_SIGNING_KEY_ALIAS"] = "who-need-help-upload" replacement["WNH_ANDROID_DEVELOPMENT_SIGNING_KEY_ALIAS"] = "" replacement["WNH_ANDROID_STAGING_SIGNING_KEY_ALIAS"] = "" diff --git a/scripts/quality.sh b/scripts/quality.sh index 5ef5839..ddf96b2 100755 --- a/scripts/quality.sh +++ b/scripts/quality.sh @@ -334,6 +334,7 @@ printf '%s\n' \ 'WNH_BASE_URL=https://dev.help.test' \ 'ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile.development' \ "ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS=$android_fingerprint" \ + 'ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS=' \ >"$android_env" chmod 600 "$android_env" ./scripts/validate-android-environment.sh \ @@ -354,7 +355,7 @@ sed -i \ ./scripts/validate-android-environment.sh "$android_env" test >/dev/null sed -i \ - 's|^DEPLOYMENT_ENV=.*|DEPLOYMENT_ENV=production|; s|^ANDROID_APP_LINKS_PACKAGE_NAME=.*|ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile|' \ + "s|^DEPLOYMENT_ENV=.*|DEPLOYMENT_ENV=production|; s|^ANDROID_APP_LINKS_PACKAGE_NAME=.*|ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile|; s|^ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS=.*|ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS=$android_fingerprint|" \ "$android_env" ./scripts/validate-android-environment.sh \ "$android_env" production >/dev/null @@ -602,6 +603,7 @@ PRODUCTION_FCM_PROJECT_ID=quality-production \ PRODUCTION_FCM_SERVICE_ACCOUNT_JSON_BASE64="$quality_fcm_base64" \ PRODUCTION_ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile \ PRODUCTION_ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS=A5:74:2B:AE:70:C6:D0:34:E3:75:44:B6:2E:37:A3:75:C0:E0:05:64:74:50:F4:0F:29:B2:A9:84:F9:FD:B8:FB \ +PRODUCTION_ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS=A5:74:2B:AE:70:C6:D0:34:E3:75:44:B6:2E:37:A3:75:C0:E0:05:64:74:50:F4:0F:29:B2:A9:84:F9:FD:B8:FB \ PRODUCTION_SUPPORT_INBOX_ADDRESS=support@help.test \ ./scripts/init-production-env.sh help.test "$production_env" >/dev/null test "$(stat -c '%a' "$production_env")" = 600 @@ -641,11 +643,21 @@ fi if PRODUCTION_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \ PRODUCTION_ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile.staging \ PRODUCTION_ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS=A5:74:2B:AE:70:C6:D0:34:E3:75:44:B6:2E:37:A3:75:C0:E0:05:64:74:50:F4:0F:29:B2:A9:84:F9:FD:B8:FB \ + PRODUCTION_ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS=A5:74:2B:AE:70:C6:D0:34:E3:75:44:B6:2E:37:A3:75:C0:E0:05:64:74:50:F4:0F:29:B2:A9:84:F9:FD:B8:FB \ ./scripts/init-production-env.sh help.test \ "$scan_dir/production.staging-package.env" >/dev/null 2>&1; then echo "Production environment initializer accepted the staging Android package." >&2 exit 1 fi +if PRODUCTION_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \ + PRODUCTION_ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile \ + PRODUCTION_ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS=A5:74:2B:AE:70:C6:D0:34:E3:75:44:B6:2E:37:A3:75:C0:E0:05:64:74:50:F4:0F:29:B2:A9:84:F9:FD:B8:FB \ + PRODUCTION_ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS=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 \ + ./scripts/init-production-env.sh help.test \ + "$scan_dir/production.unpublished-play-signing.env" >/dev/null 2>&1; then + echo "Production environment initializer accepted a Play fingerprint absent from assetlinks." >&2 + exit 1 +fi if PRODUCTION_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \ PRODUCTION_WEB_PUSH_VAPID_PUBLIC_KEY=quality-vapid-public \ PRODUCTION_WEB_PUSH_VAPID_PRIVATE_KEY=quality-vapid-private \ @@ -739,6 +751,38 @@ if ./scripts/check-environment-readiness.sh \ exit 1 fi +upload_only_app_links_env="$scan_dir/production.upload-only-app-links.env" +cp "$production_env" "$upload_only_app_links_env" +sed -i \ + 's|^ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS=.*|ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS=|' \ + "$upload_only_app_links_env" +if ./scripts/validate-production-env.sh \ + "$upload_only_app_links_env" help.test >/dev/null 2>&1; then + echo "Production validation accepted upload-only Android App Links." >&2 + exit 1 +fi +if ./scripts/check-environment-readiness.sh \ + "$upload_only_app_links_env" --require-release >/dev/null 2>&1; then + echo "Environment readiness accepted upload-only Android App Links." >&2 + exit 1 +fi + +unpublished_play_app_links_env="$scan_dir/production.unpublished-play-app-links.env" +cp "$production_env" "$unpublished_play_app_links_env" +sed -i \ + 's|^ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS=.*|ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS=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|' \ + "$unpublished_play_app_links_env" +if ./scripts/validate-production-env.sh \ + "$unpublished_play_app_links_env" help.test >/dev/null 2>&1; then + echo "Production validation accepted an unpublished Play App Signing fingerprint." >&2 + exit 1 +fi +if ./scripts/check-environment-readiness.sh \ + "$unpublished_play_app_links_env" --require-release >/dev/null 2>&1; then + echo "Environment readiness accepted an unpublished Play App Signing fingerprint." >&2 + exit 1 +fi + grep -Fx 'COMPOSE_PROJECT_NAME=who_need_help_production' "$production_env" >/dev/null grep -E '^APP_IMAGE=who-need-help:production-[0-9a-f]{12}$' "$production_env" >/dev/null grep -Fx 'EDGE_COMPOSE_PROJECT_NAME=who_need_help_edge' "$production_env" >/dev/null @@ -746,6 +790,7 @@ grep -Fx 'PRIMARY_DOMAIN=help.test' "$production_env" >/dev/null grep -Fx 'TEST_DOMAIN=test.help.test' "$production_env" >/dev/null grep -Fx 'ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile' "$production_env" >/dev/null grep -Fx 'ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS=A5:74:2B:AE:70:C6:D0:34:E3:75:44:B6:2E:37:A3:75:C0:E0:05:64:74:50:F4:0F:29:B2:A9:84:F9:FD:B8:FB' "$production_env" >/dev/null +grep -Fx 'ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS=A5:74:2B:AE:70:C6:D0:34:E3:75:44:B6:2E:37:A3:75:C0:E0:05:64:74:50:F4:0F:29:B2:A9:84:F9:FD:B8:FB' "$production_env" >/dev/null ./scripts/validate-edge-env.sh "$production_env" >/dev/null test_checkout="$scan_dir/test-checkout" diff --git a/scripts/validate-android-environment.sh b/scripts/validate-android-environment.sh index d8cd6ad..3e540fb 100755 --- a/scripts/validate-android-environment.sh +++ b/scripts/validate-android-environment.sh @@ -65,6 +65,12 @@ phx_port=$(read_unique PHX_URL_PORT) base_url=$(read_unique WNH_BASE_URL) app_links_package=$(read_unique ANDROID_APP_LINKS_PACKAGE_NAME) app_links_fingerprints=$(read_unique ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS) +play_app_signing_fingerprints= +if [[ "$expected_environment" == production ]]; then + play_app_signing_fingerprints=$( + read_unique ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS + ) +fi [[ "$deployment_environment" == "$expected_environment" ]] || { echo "Android build requires DEPLOYMENT_ENV=$expected_environment." >&2 @@ -107,4 +113,30 @@ for fingerprint in "${fingerprints[@]}"; do } done +if [[ "$expected_environment" == production ]]; then + IFS=',' read -r -a play_fingerprints <<<"$play_app_signing_fingerprints" + for play_fingerprint in "${play_fingerprints[@]}"; do + compact_play=${play_fingerprint//:/} + compact_play=${compact_play//[[:space:]]/} + [[ "$compact_play" =~ ^[0-9A-Fa-f]{64}$ ]] || { + echo "ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS contains an invalid value." >&2 + exit 1 + } + + play_found=false + for fingerprint in "${fingerprints[@]}"; do + compact=${fingerprint//:/} + compact=${compact//[[:space:]]/} + if [[ "${compact^^}" == "${compact_play^^}" ]]; then + play_found=true + break + fi + done + [[ "$play_found" == true ]] || { + echo "The Play App Signing fingerprint is absent from the published App Links identities." >&2 + exit 1 + } + done +fi + echo "Verified $expected_environment Android origin, application ID, and App Links identity." diff --git a/scripts/validate-production-env.sh b/scripts/validate-production-env.sh index b1c96a1..83d16f4 100755 --- a/scripts/validate-production-env.sh +++ b/scripts/validate-production-env.sh @@ -131,6 +131,7 @@ fcm_service_account_file=$(optional_value FCM_SERVICE_ACCOUNT_FILE) fcm_service_account_json_base64=$(optional_value FCM_SERVICE_ACCOUNT_JSON_BASE64) android_app_links_package_name=$(optional_value ANDROID_APP_LINKS_PACKAGE_NAME) android_app_links_fingerprints=$(optional_value ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS) +android_play_app_signing_fingerprints=$(optional_value ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS) codex_session_id=$(require_value CODEX_SESSION_ID) edge_compose_project_name=$(require_value EDGE_COMPOSE_PROJECT_NAME) caddy_image=$(require_value CADDY_IMAGE) @@ -444,9 +445,13 @@ if [[ -n "$fcm_project_id" || -n "$fcm_service_account_file" || fi fi -if [[ -n "$android_app_links_package_name" || -n "$android_app_links_fingerprints" ]]; then - [[ -n "$android_app_links_package_name" && -n "$android_app_links_fingerprints" ]] || { - echo "Android App Links package and fingerprints must either both be set or both be empty." >&2 +if [[ -n "$android_app_links_package_name" || + -n "$android_app_links_fingerprints" || + -n "$android_play_app_signing_fingerprints" ]]; then + [[ -n "$android_app_links_package_name" && + -n "$android_app_links_fingerprints" && + -n "$android_play_app_signing_fingerprints" ]] || { + echo "Production Android App Links require the package, published fingerprints, and Play App Signing fingerprints together." >&2 exit 1 } [[ "$android_app_links_package_name" =~ ^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)+$ ]] || { @@ -471,6 +476,34 @@ if [[ -n "$android_app_links_package_name" || -n "$android_app_links_fingerprint exit 1 } done + + IFS=',' read -r -a play_fingerprints <<<"$android_play_app_signing_fingerprints" + [[ ${#play_fingerprints[@]} -gt 0 ]] || { + echo "ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS is empty." >&2 + exit 1 + } + for play_fingerprint in "${play_fingerprints[@]}"; do + compact_play_fingerprint=${play_fingerprint//:/} + compact_play_fingerprint=${compact_play_fingerprint//[[:space:]]/} + [[ "$compact_play_fingerprint" =~ ^[0-9A-Fa-f]{64}$ ]] || { + echo "ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS contains an invalid SHA-256 fingerprint." >&2 + exit 1 + } + + play_fingerprint_found=false + for fingerprint in "${android_fingerprints[@]}"; do + compact_fingerprint=${fingerprint//:/} + compact_fingerprint=${compact_fingerprint//[[:space:]]/} + if [[ "${compact_fingerprint^^}" == "${compact_play_fingerprint^^}" ]]; then + play_fingerprint_found=true + break + fi + done + [[ "$play_fingerprint_found" == true ]] || { + echo "Every Play App Signing fingerprint must also be published in ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS." >&2 + exit 1 + } + done fi case "$codex_session_id" in