diff --git a/docs/verification.md b/docs/verification.md index de8461a..95df2da 100644 --- a/docs/verification.md +++ b/docs/verification.md @@ -38,6 +38,12 @@ edit, branch, or tag was made during this audit. development blockers are the four public Firebase Android values and the FCM service-account credential. No release-readiness claim is made until those credentials are imported and provider/device behavior is exercised. +- The existing Google Cloud project `who-need-help-development` was selected in + the Firebase console through the user's already authenticated dev-port Chrome + session. Firebase requires the account holder to accept its Terms before it + can add Firebase services to that existing project. That legal acceptance + remains pending explicit user confirmation; no Firebase project, app, + service account, credential, or environment value was created or changed. - Real browser Web Push was exercised on the development origin through the user's existing dev-port Chrome profile. The exact origin permission was changed from `Ask (default)` to `Allow`; the application registered a second, @@ -58,7 +64,7 @@ edit, branch, or tag was made during this audit. topology, external PostgreSQL 18.4, healthy application containers, and passing public readiness. The plan correctly refused release because the production checkout still lacks browser VAPID, the Firebase Android client, - server FCM delivery, and Android App Links. It reported 46 pending local + server FCM delivery, and Android App Links. It reported 57 pending local commits and made no remote change. - A separate read-only isolation check observed the public Git `main` reference still at production commit `921e04b3608007675e22e7e26e0beb3975dbba58`. @@ -82,6 +88,13 @@ edit, branch, or tag was made during this audit. the database, test, public Git, and Devpost are excluded. An isolated offline fixture passed read-only plan, successful apply, and injected-edge-failure recovery, including restoration of the original image selection. +- The clean local commit + `89851097fd5cbe58ce4dc41c2322810894cad50a` was packaged as a Git bundle under + `output/releases/89851097fd5cbe58ce4dc41c2322810894cad50a/`. Its SHA-256 + checksum, bundle object graph, and `HEAD` identity all passed verification. + The isolated production rollback drill was repeated after packaging and again + passed plan, apply, and injected edge-failure recovery without contacting or + changing the production runtime. - A current development database backup was created at `output/backups/compose-20260723-194923.dump` with SHA-256 `4202a152751d588c19069eb46a25753901238729b47e6197945afdca20b65c2e`. diff --git a/scripts/check-environment-readiness.sh b/scripts/check-environment-readiness.sh index 20beee3..9ce9c89 100755 --- a/scripts/check-environment-readiness.sh +++ b/scripts/check-environment-readiness.sh @@ -79,6 +79,30 @@ valid_fcm_service_account_json() { ' >/dev/null 2>&1 } +fcm_service_account_project_id() { + jq -er ' + select( + .type == "service_account" and + (.project_id | type == "string" and length > 0) and + (.client_email | type == "string" and length > 0) and + (.private_key | type == "string" and length > 0) + ) + | .project_id + ' 2>/dev/null +} + +firebase_client_values_valid() { + local application_id sender_id prefix + + application_id=$(value WNH_FIREBASE_APPLICATION_ID) + sender_id=$(value WNH_FIREBASE_GCM_SENDER_ID) + prefix="1:$sender_id:android:" + + [[ "$sender_id" =~ ^[0-9]+$ && + "$application_id" == "$prefix"* && + -n "${application_id#"$prefix"}" ]] +} + failures=0 warnings=0 @@ -174,38 +198,65 @@ if all_empty WNH_FIREBASE_APPLICATION_ID WNH_FIREBASE_API_KEY \ missing "Android Firebase client" "four WNH_FIREBASE_* Android client values" elif all_set WNH_FIREBASE_APPLICATION_ID WNH_FIREBASE_API_KEY \ WNH_FIREBASE_PROJECT_ID WNH_FIREBASE_GCM_SENDER_ID; then - ready "Android Firebase client" "complete client configuration" + if firebase_client_values_valid; then + ready "Android Firebase client" "complete internally consistent client configuration" + else + invalid "Android Firebase client" \ + "application ID must belong to the numeric configured sender/project number" + fi else partial "Android Firebase client" "all four WNH_FIREBASE_* values are required together" fi fcm_file=$(value FCM_SERVICE_ACCOUNT_FILE) fcm_base64=$(value FCM_SERVICE_ACCOUNT_JSON_BASE64) -if [[ -z "$(value FCM_PROJECT_ID)" && -z "$fcm_file" && -z "$fcm_base64" ]]; then +fcm_project_id=$(value FCM_PROJECT_ID) +firebase_project_id=$(value WNH_FIREBASE_PROJECT_ID) +fcm_credential_project_id= +if [[ -z "$fcm_project_id" && -z "$fcm_file" && -z "$fcm_base64" ]]; then missing "Android FCM delivery" "FCM project ID and one service-account source" -elif [[ -z "$(value FCM_PROJECT_ID)" || (-n "$fcm_file" && -n "$fcm_base64") || +elif [[ -z "$fcm_project_id" || (-n "$fcm_file" && -n "$fcm_base64") || (-z "$fcm_file" && -z "$fcm_base64") ]]; then partial "Android FCM delivery" "project ID and exactly one credential source are required" elif [[ -n "$fcm_file" ]]; then if [[ "$fcm_file" == /* && -r "$fcm_file" ]] && - valid_fcm_service_account_json <"$fcm_file"; then - ready "Android FCM delivery" "complete service-account file is configured" + fcm_credential_project_id=$(fcm_service_account_project_id <"$fcm_file") && + [[ "$fcm_credential_project_id" == "$fcm_project_id" ]] && + [[ -z "$firebase_project_id" || "$fcm_project_id" == "$firebase_project_id" ]]; then + ready "Android FCM delivery" "service account and Android client use the same project" else invalid "Android FCM delivery" \ - "FCM_SERVICE_ACCOUNT_FILE must be an absolute readable complete service-account JSON file" + "credential source and configured Firebase/FCM project IDs are incomplete or inconsistent" fi -elif printf '%s' "$fcm_base64" | base64 --decode 2>/dev/null | - valid_fcm_service_account_json; then - ready "Android FCM delivery" "complete Base64 service-account document is configured" +elif fcm_credential_project_id=$( + printf '%s' "$fcm_base64" | + base64 --decode 2>/dev/null | + fcm_service_account_project_id +) && + [[ "$fcm_credential_project_id" == "$fcm_project_id" ]] && + [[ -z "$firebase_project_id" || "$fcm_project_id" == "$firebase_project_id" ]]; then + ready "Android FCM delivery" "service account and Android client use the same project" else invalid "Android FCM delivery" \ - "Base64 credential is not a complete service-account JSON document" + "credential source and configured Firebase/FCM project IDs are incomplete or inconsistent" fi +expected_android_package= +case "$deployment_env" in + development) expected_android_package=org.whoneedhelp.mobile.development ;; + test) expected_android_package=org.whoneedhelp.mobile.staging ;; + production) expected_android_package=org.whoneedhelp.mobile ;; +esac + if all_empty ANDROID_APP_LINKS_PACKAGE_NAME ANDROID_APP_LINKS_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 - ready "Android App Links" "package and signing fingerprints are configured" + 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 + invalid "Android App Links" "package does not match DEPLOYMENT_ENV=$deployment_env" + fi else partial "Android App Links" "package and signing fingerprints must be configured together" fi diff --git a/scripts/import-firebase-android-config.sh b/scripts/import-firebase-android-config.sh index d56cac4..cf2d6b6 100755 --- a/scripts/import-firebase-android-config.sh +++ b/scripts/import-firebase-android-config.sh @@ -31,6 +31,8 @@ if [ -z "$package_name" ]; then fi if ! jq --exit-status --arg package "$package_name" ' + (.project_info.project_number) as $project_number + | [ .client[]? | select(.client_info.android_client_info.package_name == $package) @@ -39,6 +41,8 @@ if ! jq --exit-status --arg package "$package_name" ' and (.project_info.project_id | type == "string" and length > 0) and (.project_info.project_number | type == "string" and length > 0) and ($clients[0].client_info.mobilesdk_app_id | type == "string" and length > 0) + and ($clients[0].client_info.mobilesdk_app_id + | startswith("1:" + $project_number + ":android:")) and ($clients[0].api_key[0].current_key | type == "string" and length > 0) ' "$client_file" >/dev/null; then echo "Firebase Android configuration does not contain exactly one complete client for package $package_name." >&2 diff --git a/scripts/quality.sh b/scripts/quality.sh index 654d2ff..938ec8f 100755 --- a/scripts/quality.sh +++ b/scripts/quality.sh @@ -202,6 +202,16 @@ grep -Fx 'WNH_FIREBASE_API_KEY=quality-firebase-api-key' "$credential_env" >/dev grep -Fx 'WNH_FIREBASE_PROJECT_ID=quality-development' "$credential_env" >/dev/null grep -Fx 'WNH_FIREBASE_GCM_SENDER_ID=123456789' "$credential_env" >/dev/null +firebase_mismatched_client="$scan_dir/google-services-mismatched.json" +printf '%s\n' \ + '{"project_info":{"project_number":"123456789","project_id":"quality-development"},"client":[{"client_info":{"mobilesdk_app_id":"1:987654321:android:quality","android_client_info":{"package_name":"org.whoneedhelp.mobile.staging"}},"api_key":[{"current_key":"quality-firebase-api-key"}]}]}' \ + >"$firebase_mismatched_client" +if ./scripts/import-firebase-android-config.sh \ + "$credential_env" "$firebase_mismatched_client" >/dev/null 2>&1; then + echo "Firebase importer accepted an application ID from another project number." >&2 + exit 1 +fi + echo "Checking Android environment isolation" 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_env="$scan_dir/android-development.env" @@ -399,6 +409,60 @@ if ./scripts/check-environment-readiness.sh \ echo "Environment readiness accepted an incomplete FCM service account." >&2 exit 1 fi + +mismatched_firebase_env="$scan_dir/production.mismatched-firebase.env" +cp "$production_env" "$mismatched_firebase_env" +sed -i \ + 's|^WNH_FIREBASE_APPLICATION_ID=.*|WNH_FIREBASE_APPLICATION_ID=1:987654321:android:quality|' \ + "$mismatched_firebase_env" +if ./scripts/validate-production-env.sh \ + "$mismatched_firebase_env" help.test >/dev/null 2>&1; then + echo "Production validation accepted a Firebase application from another sender." >&2 + exit 1 +fi +if ./scripts/check-environment-readiness.sh \ + "$mismatched_firebase_env" --require-release >/dev/null 2>&1; then + echo "Environment readiness accepted a Firebase application from another sender." >&2 + exit 1 +fi + +mismatched_fcm_env="$scan_dir/production.mismatched-fcm.env" +mismatched_fcm_base64=$( + printf '%s' \ + '{"type":"service_account","project_id":"another-project","client_email":"quality-fcm@another-project.iam.gserviceaccount.com","private_key":"quality-private-key"}' | + base64 -w 0 +) +cp "$production_env" "$mismatched_fcm_env" +sed -i \ + "s|^FCM_SERVICE_ACCOUNT_JSON_BASE64=.*|FCM_SERVICE_ACCOUNT_JSON_BASE64=$mismatched_fcm_base64|" \ + "$mismatched_fcm_env" +if ./scripts/validate-production-env.sh \ + "$mismatched_fcm_env" help.test >/dev/null 2>&1; then + echo "Production validation accepted an FCM service account from another project." >&2 + exit 1 +fi +if ./scripts/check-environment-readiness.sh \ + "$mismatched_fcm_env" --require-release >/dev/null 2>&1; then + echo "Environment readiness accepted an FCM service account from another project." >&2 + exit 1 +fi + +mismatched_app_links_env="$scan_dir/production.mismatched-app-links.env" +cp "$production_env" "$mismatched_app_links_env" +sed -i \ + 's|^ANDROID_APP_LINKS_PACKAGE_NAME=.*|ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile.staging|' \ + "$mismatched_app_links_env" +if ./scripts/validate-production-env.sh \ + "$mismatched_app_links_env" help.test >/dev/null 2>&1; then + echo "Production validation accepted the staging Android package." >&2 + exit 1 +fi +if ./scripts/check-environment-readiness.sh \ + "$mismatched_app_links_env" --require-release >/dev/null 2>&1; then + echo "Environment readiness accepted the staging Android package for production." >&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 diff --git a/scripts/validate-production-env.sh b/scripts/validate-production-env.sh index bfa1856..b1c96a1 100755 --- a/scripts/validate-production-env.sh +++ b/scripts/validate-production-env.sh @@ -351,6 +351,15 @@ if ((firebase_nonempty != 0 && firebase_nonempty != ${#firebase_values[@]})); th echo "All four WNH_FIREBASE_* Android client values must be configured together." >&2 exit 1 fi +if ((firebase_nonempty == ${#firebase_values[@]})); then + firebase_prefix="1:$firebase_sender_id:android:" + [[ "$firebase_sender_id" =~ ^[0-9]+$ && + "$firebase_application_id" == "$firebase_prefix"* && + -n "${firebase_application_id#"$firebase_prefix"}" ]] || { + echo "WNH_FIREBASE_APPLICATION_ID does not belong to WNH_FIREBASE_GCM_SENDER_ID." >&2 + exit 1 + } +fi vapid_values=( "$web_push_vapid_public_key" @@ -386,6 +395,7 @@ if [[ -n "$fcm_project_id" || -n "$fcm_service_account_file" || exit 1 } + fcm_credential_project_id= if [[ -n "$fcm_service_account_file" ]]; then command -v jq >/dev/null 2>&1 || { echo "Required command is unavailable for FCM validation: jq" >&2 @@ -399,6 +409,7 @@ if [[ -n "$fcm_project_id" || -n "$fcm_service_account_file" || echo "FCM_SERVICE_ACCOUNT_FILE is not a complete service-account JSON document." >&2 exit 1 } + fcm_credential_project_id=$(jq --raw-output '.project_id' "$fcm_service_account_file") else for command in base64 jq; do command -v "$command" >/dev/null 2>&1 || { @@ -406,12 +417,30 @@ if [[ -n "$fcm_project_id" || -n "$fcm_service_account_file" || exit 1 } done - printf '%s' "$fcm_service_account_json_base64" | - base64 --decode 2>/dev/null | + fcm_decoded_json=$( + printf '%s' "$fcm_service_account_json_base64" | + base64 --decode 2>/dev/null + ) || { + echo "FCM_SERVICE_ACCOUNT_JSON_BASE64 is not valid Base64." >&2 + exit 1 + } + printf '%s' "$fcm_decoded_json" | valid_fcm_service_account_json || { echo "FCM_SERVICE_ACCOUNT_JSON_BASE64 is not a complete service-account JSON document." >&2 exit 1 } + fcm_credential_project_id=$(printf '%s' "$fcm_decoded_json" | jq --raw-output '.project_id') + fi + + [[ "$fcm_credential_project_id" == "$fcm_project_id" ]] || { + echo "FCM service-account project does not match FCM_PROJECT_ID." >&2 + exit 1 + } + if ((firebase_nonempty == ${#firebase_values[@]})); then + [[ "$fcm_project_id" == "$firebase_project_id" ]] || { + echo "FCM_PROJECT_ID does not match WNH_FIREBASE_PROJECT_ID." >&2 + exit 1 + } fi fi @@ -424,6 +453,10 @@ if [[ -n "$android_app_links_package_name" || -n "$android_app_links_fingerprint echo "ANDROID_APP_LINKS_PACKAGE_NAME is not a valid Android application ID." >&2 exit 1 } + [[ "$android_app_links_package_name" == org.whoneedhelp.mobile ]] || { + echo "Production Android App Links must use org.whoneedhelp.mobile." >&2 + exit 1 + } IFS=',' read -r -a android_fingerprints <<<"$android_app_links_fingerprints" [[ ${#android_fingerprints[@]} -gt 0 ]] || { diff --git a/scripts/validate-test-env.sh b/scripts/validate-test-env.sh index ddad2d1..4271b96 100755 --- a/scripts/validate-test-env.sh +++ b/scripts/validate-test-env.sh @@ -147,6 +147,18 @@ if ((firebase_nonempty != 0 && firebase_nonempty != ${#firebase_values[@]})); th echo "All four test WNH_FIREBASE_* Android client values must be configured together." >&2 exit 1 fi +if ((firebase_nonempty == ${#firebase_values[@]})); then + firebase_application_id=${firebase_values[0]} + firebase_project_id=${firebase_values[2]} + firebase_sender_id=${firebase_values[3]} + firebase_prefix="1:$firebase_sender_id:android:" + [[ "$firebase_sender_id" =~ ^[0-9]+$ && + "$firebase_application_id" == "$firebase_prefix"* && + -n "${firebase_application_id#"$firebase_prefix"}" ]] || { + echo "Test WNH_FIREBASE_APPLICATION_ID does not belong to WNH_FIREBASE_GCM_SENDER_ID." >&2 + exit 1 + } +fi vapid_values=( "$(read_value WEB_PUSH_VAPID_PUBLIC_KEY 2>/dev/null || true)" @@ -184,6 +196,7 @@ if [[ -n "$fcm_project_id" || -n "$fcm_service_account_file" || exit 1 } + fcm_credential_project_id= if [[ -n "$fcm_service_account_file" ]]; then command -v jq >/dev/null 2>&1 || { echo "Required command is unavailable for FCM validation: jq" >&2 @@ -197,6 +210,7 @@ if [[ -n "$fcm_project_id" || -n "$fcm_service_account_file" || echo "Test FCM_SERVICE_ACCOUNT_FILE is not a complete service-account JSON document." >&2 exit 1 } + fcm_credential_project_id=$(jq --raw-output '.project_id' "$fcm_service_account_file") else for command in base64 jq; do command -v "$command" >/dev/null 2>&1 || { @@ -204,12 +218,30 @@ if [[ -n "$fcm_project_id" || -n "$fcm_service_account_file" || exit 1 } done - printf '%s' "$fcm_service_account_json_base64" | - base64 --decode 2>/dev/null | + fcm_decoded_json=$( + printf '%s' "$fcm_service_account_json_base64" | + base64 --decode 2>/dev/null + ) || { + echo "Test FCM_SERVICE_ACCOUNT_JSON_BASE64 is not valid Base64." >&2 + exit 1 + } + printf '%s' "$fcm_decoded_json" | valid_fcm_service_account_json || { echo "Test FCM_SERVICE_ACCOUNT_JSON_BASE64 is not a complete service-account JSON document." >&2 exit 1 } + fcm_credential_project_id=$(printf '%s' "$fcm_decoded_json" | jq --raw-output '.project_id') + fi + + [[ "$fcm_credential_project_id" == "$fcm_project_id" ]] || { + echo "Test FCM service-account project does not match FCM_PROJECT_ID." >&2 + exit 1 + } + if ((firebase_nonempty == ${#firebase_values[@]})); then + [[ "$fcm_project_id" == "$firebase_project_id" ]] || { + echo "Test FCM_PROJECT_ID does not match WNH_FIREBASE_PROJECT_ID." >&2 + exit 1 + } fi fi @@ -224,6 +256,10 @@ if [[ -n "$android_package" || -n "$android_fingerprints" ]]; then echo "ANDROID_APP_LINKS_PACKAGE_NAME is not a valid Android application ID." >&2 exit 1 } + [[ "$android_package" == org.whoneedhelp.mobile.staging ]] || { + echo "Test Android App Links must use org.whoneedhelp.mobile.staging." >&2 + exit 1 + } IFS=',' read -r -a android_fingerprint_values <<<"$android_fingerprints" for fingerprint in "${android_fingerprint_values[@]}"; do