fix(release): harden environment credential bootstrap
This commit is contained in:
parent
5129e1253a
commit
cbb5efb62a
|
|
@ -152,9 +152,10 @@ PUSH_HTTP_RECEIVE_TIMEOUT_MS=
|
|||
PUSH_HTTP_CONNECT_TIMEOUT_MS=
|
||||
PUSH_HTTP_RETRY_DELAY_MS=
|
||||
|
||||
# Direct browser Web Push. Generate one VAPID key pair per environment and
|
||||
# keep the private key only in that environment's .env. The subject must be a
|
||||
# mailto: or HTTPS contact owned by the operator.
|
||||
# Direct browser Web Push. Generate one VAPID key pair per environment with
|
||||
# scripts/generate-vapid-env.sh and keep the private key only in that
|
||||
# environment's .env. The subject must be a mailto: or HTTPS contact owned by
|
||||
# the operator.
|
||||
WEB_PUSH_VAPID_PUBLIC_KEY=
|
||||
WEB_PUSH_VAPID_PRIVATE_KEY=
|
||||
WEB_PUSH_VAPID_SUBJECT=
|
||||
|
|
|
|||
|
|
@ -213,6 +213,9 @@ Provider downloads can be imported into that same file without placing
|
|||
secrets on a command line or printing them:
|
||||
|
||||
```bash
|
||||
./scripts/generate-vapid-env.sh \
|
||||
.env mailto:contact@YOUR_DOMAIN
|
||||
|
||||
chmod 600 /secure/downloads/google-oauth-client.json
|
||||
./scripts/import-google-oauth-client.sh \
|
||||
.env /secure/downloads/google-oauth-client.json
|
||||
|
|
@ -225,10 +228,19 @@ chmod 600 /secure/downloads/fcm-service-account.json
|
|||
.env /secure/downloads/fcm-service-account.json
|
||||
```
|
||||
|
||||
The VAPID helper runs the exact locked `web_push_elixir` generator in an
|
||||
isolated, network-disabled container, imports the result atomically, removes
|
||||
its one-run image tag and temporary files, and never prints either key. It
|
||||
refuses to replace an existing VAPID identity because an unplanned rotation
|
||||
invalidates existing browser subscriptions.
|
||||
|
||||
The importers validate the exact OAuth callback, Android package, Firebase
|
||||
project relationship, and required service-account fields before atomically
|
||||
replacing existing keys. They preserve mode `0600` and never create another
|
||||
permanent environment file. OAuth and service-account downloads still contain
|
||||
permanent environment file. Provider and initializer values that cannot be
|
||||
represented as one unquoted Compose `.env` line are rejected before mutation.
|
||||
Literal dollar signs are stored as `$$`, which Compose resolves back to one
|
||||
`$` inside the container. OAuth and service-account downloads still contain
|
||||
private credentials after import; deliberately move them to protected backup
|
||||
storage or remove them after verification.
|
||||
|
||||
|
|
|
|||
161
scripts/generate-vapid-env.sh
Executable file
161
scripts/generate-vapid-env.sh
Executable file
|
|
@ -0,0 +1,161 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
umask 077
|
||||
|
||||
if [ "$#" -ne 2 ]; then
|
||||
echo "Usage: $0 ENV_FILE VAPID_SUBJECT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
env_file=$1
|
||||
subject=$2
|
||||
|
||||
if [ ! -f "$env_file" ]; then
|
||||
echo "Environment file does not exist: $env_file" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$(stat -c '%a' "$env_file")" != 600 ]; then
|
||||
echo "Environment file must have mode 0600: $env_file" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "$subject" in
|
||||
mailto:?* | https://?*) ;;
|
||||
*)
|
||||
echo "VAPID_SUBJECT must be a non-empty mailto: or https:// URI without whitespace." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$subject" in
|
||||
*'
|
||||
'*)
|
||||
echo "VAPID_SUBJECT must be a non-empty mailto: or https:// URI without whitespace." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if printf '%s' "$subject" | LC_ALL=C grep -q '[[:space:]]'; then
|
||||
echo "VAPID_SUBJECT must be a non-empty mailto: or https:// URI without whitespace." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for command in awk chmod date docker grep mktemp rm stat; do
|
||||
if ! command -v "$command" >/dev/null 2>&1; then
|
||||
echo "Required command is unavailable: $command" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
for key in \
|
||||
WEB_PUSH_VAPID_PUBLIC_KEY \
|
||||
WEB_PUSH_VAPID_PRIVATE_KEY \
|
||||
WEB_PUSH_VAPID_SUBJECT; do
|
||||
key_count=$(
|
||||
awk -F= -v key="$key" '$1 == key { count++ } END { print count + 0 }' \
|
||||
"$env_file"
|
||||
)
|
||||
if [ "$key_count" -ne 1 ]; then
|
||||
echo "Environment must contain exactly one $key entry before VAPID generation." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
existing_value=$(
|
||||
awk -F= -v key="$key" '
|
||||
$1 == key {
|
||||
print substr($0, index($0, "=") + 1)
|
||||
exit
|
||||
}
|
||||
' "$env_file"
|
||||
)
|
||||
if [ -n "$existing_value" ]; then
|
||||
echo "Refusing to rotate an existing VAPID identity: $key is already configured." >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
unset existing_value
|
||||
|
||||
env_dir=$(CDPATH='' cd -- "$(dirname -- "$env_file")" && pwd)
|
||||
temporary_dir=$(mktemp -d "$env_dir/.vapid-generation.XXXXXX")
|
||||
chmod 700 "$temporary_dir"
|
||||
raw_keys="$temporary_dir/generated.txt"
|
||||
values_file="$temporary_dir/values.env"
|
||||
generator_image=${WNH_VAPID_GENERATOR_IMAGE:-}
|
||||
owned_image=false
|
||||
run_id="$(date -u +%Y%m%d%H%M%S)-$$"
|
||||
generator_container="wnh-vapid-tool-$run_id"
|
||||
|
||||
cleanup() {
|
||||
trap - EXIT HUP INT TERM
|
||||
rm -rf "$temporary_dir"
|
||||
docker rm --force "$generator_container" >/dev/null 2>&1 || true
|
||||
if [ "$owned_image" = true ]; then
|
||||
docker image rm "$generator_image" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
cleanup_on_exit() {
|
||||
exit_status=$?
|
||||
cleanup
|
||||
exit "$exit_status"
|
||||
}
|
||||
|
||||
trap cleanup_on_exit EXIT
|
||||
trap 'cleanup; exit 129' HUP
|
||||
trap 'cleanup; exit 130' INT
|
||||
trap 'cleanup; exit 143' TERM
|
||||
|
||||
if [ -z "$generator_image" ]; then
|
||||
generator_image="who-need-help:vapid-tool-$run_id"
|
||||
owned_image=true
|
||||
docker build --quiet --target test --tag "$generator_image" "$ROOT" >/dev/null
|
||||
fi
|
||||
|
||||
docker image inspect "$generator_image" >/dev/null
|
||||
docker run --rm \
|
||||
--name "$generator_container" \
|
||||
--network none \
|
||||
--read-only \
|
||||
--tmpfs /tmp:rw,noexec,nosuid,size=16m \
|
||||
--entrypoint mix \
|
||||
"$generator_image" \
|
||||
generate.vapid.keys >"$raw_keys"
|
||||
chmod 600 "$raw_keys"
|
||||
|
||||
if ! awk -F'"' -v subject="$subject" '
|
||||
/^[[:space:]]*vapid_private_key:/ {
|
||||
private_count++
|
||||
private_key = $2
|
||||
}
|
||||
/^[[:space:]]*vapid_public_key:/ {
|
||||
public_count++
|
||||
public_key = $2
|
||||
}
|
||||
END {
|
||||
valid_private = private_key ~ /^[A-Za-z0-9_-]+$/
|
||||
valid_public = public_key ~ /^[A-Za-z0-9_-]+$/
|
||||
|
||||
if (private_count != 1 ||
|
||||
public_count != 1 ||
|
||||
!valid_private ||
|
||||
!valid_public ||
|
||||
private_key == public_key) {
|
||||
exit 40
|
||||
}
|
||||
|
||||
print "WEB_PUSH_VAPID_PUBLIC_KEY=" public_key
|
||||
print "WEB_PUSH_VAPID_PRIVATE_KEY=" private_key
|
||||
print "WEB_PUSH_VAPID_SUBJECT=" subject
|
||||
}
|
||||
' "$raw_keys" >"$values_file"; then
|
||||
echo "The pinned web_push_elixir generator returned an unexpected key format." >&2
|
||||
exit 1
|
||||
fi
|
||||
chmod 600 "$values_file"
|
||||
|
||||
"$ROOT/scripts/set-env-values.sh" "$env_file" "$values_file" >/dev/null
|
||||
|
||||
echo "Generated a new environment-specific VAPID identity without printing its keys."
|
||||
echo "Updated the single mode-0600 environment file: $env_file"
|
||||
|
|
@ -26,8 +26,10 @@ esac
|
|||
|
||||
if ! jq --exit-status '
|
||||
.type == "service_account"
|
||||
and (.project_id | type == "string" and length > 0)
|
||||
and (.client_email | type == "string" and length > 0)
|
||||
and (.project_id
|
||||
| type == "string" and length > 0 and test("^[^\r\n]+$"))
|
||||
and (.client_email
|
||||
| type == "string" and length > 0 and test("^[^\r\n]+$"))
|
||||
and (.private_key | type == "string" and length > 0)
|
||||
' "$service_account_file" >/dev/null; then
|
||||
echo "FCM service-account JSON is incomplete." >&2
|
||||
|
|
|
|||
|
|
@ -38,12 +38,16 @@ if ! jq --exit-status --arg package "$package_name" '
|
|||
| select(.client_info.android_client_info.package_name == $package)
|
||||
] as $clients
|
||||
| ($clients | length == 1)
|
||||
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 (.project_info.project_id
|
||||
| type == "string" and length > 0 and test("^[^\r\n]+$"))
|
||||
and (.project_info.project_number
|
||||
| type == "string" and length > 0 and test("^[^\r\n]+$"))
|
||||
and ($clients[0].client_info.mobilesdk_app_id
|
||||
| type == "string" and length > 0 and test("^[^\r\n]+$"))
|
||||
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)
|
||||
and ($clients[0].api_key[0].current_key
|
||||
| type == "string" and length > 0 and test("^[^\r\n]+$"))
|
||||
' "$client_file" >/dev/null; then
|
||||
echo "Firebase Android configuration does not contain exactly one complete client for package $package_name." >&2
|
||||
exit 1
|
||||
|
|
|
|||
|
|
@ -10,6 +10,31 @@ usage() {
|
|||
echo "Usage: $0 DOMAIN [OUTPUT_FILE]" >&2
|
||||
}
|
||||
|
||||
require_single_line_env_value() {
|
||||
value_name=$1
|
||||
value=$2
|
||||
|
||||
case "$value" in
|
||||
*'
|
||||
'*)
|
||||
echo "$value_name must not contain control characters because .env stores one value per line." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if printf '%s' "$value" | LC_ALL=C grep -q '[[:cntrl:]]'; then
|
||||
echo "$value_name must not contain control characters because .env stores one value per line." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "$value" in
|
||||
' '* | *' ' | \"* | \'* | *' #'*)
|
||||
echo "$value_name cannot be represented safely as an unquoted Compose .env value." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
if [ -z "$domain" ]; then
|
||||
usage
|
||||
exit 1
|
||||
|
|
@ -69,6 +94,30 @@ test_upstream=${PRODUCTION_TEST_UPSTREAM:-who-need-help-test:4000}
|
|||
edge_compose_project_name=${PRODUCTION_EDGE_COMPOSE_PROJECT_NAME:-who_need_help_edge}
|
||||
git_sha=$(git -C "$ROOT" rev-parse --short=12 HEAD)
|
||||
|
||||
require_single_line_env_value PRODUCTION_DATABASE_MODE "$database_mode"
|
||||
require_single_line_env_value PRODUCTION_APP_TOPOLOGY "$app_topology"
|
||||
require_single_line_env_value PRODUCTION_COMPOSE_PROJECT_NAME "$compose_project_name"
|
||||
require_single_line_env_value PRODUCTION_PUBLIC_EDGE_ENABLED "$public_edge_enabled"
|
||||
require_single_line_env_value PRODUCTION_PUBLIC_EDGE_NETWORK "$public_edge_network"
|
||||
require_single_line_env_value PRODUCTION_PUBLIC_UPSTREAM_NAME "$public_upstream_name"
|
||||
require_single_line_env_value PRODUCTION_CODEX_SESSION_ID "$codex_session_id"
|
||||
require_single_line_env_value PRODUCTION_GOOGLE_OAUTH_CLIENT_ID "$google_oauth_client_id"
|
||||
require_single_line_env_value PRODUCTION_GOOGLE_OAUTH_CLIENT_SECRET "$google_oauth_client_secret"
|
||||
require_single_line_env_value PRODUCTION_WEB_PUSH_VAPID_PUBLIC_KEY "$web_push_vapid_public_key"
|
||||
require_single_line_env_value PRODUCTION_WEB_PUSH_VAPID_PRIVATE_KEY "$web_push_vapid_private_key"
|
||||
require_single_line_env_value PRODUCTION_WEB_PUSH_VAPID_SUBJECT "$web_push_vapid_subject"
|
||||
require_single_line_env_value PRODUCTION_WNH_FIREBASE_APPLICATION_ID "$firebase_application_id"
|
||||
require_single_line_env_value PRODUCTION_WNH_FIREBASE_API_KEY "$firebase_api_key"
|
||||
require_single_line_env_value PRODUCTION_WNH_FIREBASE_PROJECT_ID "$firebase_project_id"
|
||||
require_single_line_env_value PRODUCTION_WNH_FIREBASE_GCM_SENDER_ID "$firebase_sender_id"
|
||||
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_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"
|
||||
|
||||
if [ -z "$codex_session_id" ]; then
|
||||
echo "PRODUCTION_CODEX_SESSION_ID is required for the Build Week feedback page." >&2
|
||||
exit 1
|
||||
|
|
@ -87,6 +136,7 @@ if { [ -n "$android_app_links_package_name" ] || [ -n "$android_app_links_finger
|
|||
fi
|
||||
|
||||
firebase_values="$firebase_application_id $firebase_api_key $firebase_project_id $firebase_sender_id"
|
||||
firebase_configured=false
|
||||
if printf '%s\n' "$firebase_values" | grep -q '[^[:space:]]'; then
|
||||
for value in "$firebase_application_id" "$firebase_api_key" \
|
||||
"$firebase_project_id" "$firebase_sender_id"; do
|
||||
|
|
@ -95,6 +145,15 @@ if printf '%s\n' "$firebase_values" | grep -q '[^[:space:]]'; then
|
|||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
firebase_prefix="1:$firebase_sender_id:android:"
|
||||
if ! printf '%s\n' "$firebase_sender_id" | grep -Eq '^[0-9]+$' ||
|
||||
[ "${firebase_application_id#"$firebase_prefix"}" = "$firebase_application_id" ] ||
|
||||
[ -z "${firebase_application_id#"$firebase_prefix"}" ]; then
|
||||
echo "Production Firebase application ID must belong to the configured numeric sender/project number." >&2
|
||||
exit 1
|
||||
fi
|
||||
firebase_configured=true
|
||||
fi
|
||||
|
||||
vapid_values="$web_push_vapid_public_key $web_push_vapid_private_key $web_push_vapid_subject"
|
||||
|
|
@ -106,6 +165,14 @@ if printf '%s\n' "$vapid_values" | grep -q '[^[:space:]]'; then
|
|||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
case "$web_push_vapid_subject" in
|
||||
mailto:?* | https://?*) ;;
|
||||
*)
|
||||
echo "Production WEB_PUSH_VAPID_SUBJECT must be a non-empty mailto: or https:// URI." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if { [ -n "$fcm_project_id" ] || [ -n "$fcm_service_account_json_base64" ]; } &&
|
||||
|
|
@ -114,6 +181,7 @@ if { [ -n "$fcm_project_id" ] || [ -n "$fcm_service_account_json_base64" ]; } &&
|
|||
exit 1
|
||||
fi
|
||||
|
||||
fcm_credential_project_id=
|
||||
if [ -n "$fcm_service_account_json_base64" ]; then
|
||||
for command in base64 jq; do
|
||||
command -v "$command" >/dev/null 2>&1 || {
|
||||
|
|
@ -121,17 +189,38 @@ if [ -n "$fcm_service_account_json_base64" ]; then
|
|||
exit 1
|
||||
}
|
||||
done
|
||||
if ! printf '%s' "$fcm_service_account_json_base64" |
|
||||
if ! fcm_credential_project_id=$(printf '%s' "$fcm_service_account_json_base64" |
|
||||
base64 --decode 2>/dev/null |
|
||||
jq -e '
|
||||
.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)
|
||||
' >/dev/null 2>&1; then
|
||||
jq -er '
|
||||
. as $credential
|
||||
| (
|
||||
($credential.type == "service_account") and
|
||||
($credential.project_id | type == "string" and length > 0) and
|
||||
($credential.client_email | type == "string" and length > 0) and
|
||||
($credential.private_key | type == "string" and length > 0)
|
||||
)
|
||||
| if . then $credential.project_id else error("incomplete service account") end
|
||||
' 2>/dev/null); then
|
||||
echo "Production FCM credential is not a complete service-account JSON document." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$fcm_credential_project_id" != "$fcm_project_id" ]; then
|
||||
echo "Production FCM service-account project must match PRODUCTION_FCM_PROJECT_ID." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$firebase_configured" = true ] &&
|
||||
[ "$fcm_project_id" != "$firebase_project_id" ]; then
|
||||
echo "Production Firebase Android client and FCM service account must use the same project." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$android_app_links_package_name" ] &&
|
||||
[ "$android_app_links_package_name" != org.whoneedhelp.mobile ]; then
|
||||
echo "Production Android App Links package must be org.whoneedhelp.mobile." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "$compose_project_name" in
|
||||
|
|
@ -203,6 +292,22 @@ smtp_ssl=${PRODUCTION_SMTP_SSL:-false}
|
|||
email_from_address=${PRODUCTION_EMAIL_FROM_ADDRESS:-"contact@$domain"}
|
||||
support_inbox_address=${PRODUCTION_SUPPORT_INBOX_ADDRESS:-}
|
||||
|
||||
require_single_line_env_value PRODUCTION_DATABASE_URL "$database_url"
|
||||
require_single_line_env_value PRODUCTION_DATABASE_SOCKET_DIR "$database_socket_dir"
|
||||
require_single_line_env_value PRODUCTION_HTTP_BIND_ADDRESS "$http_bind_address"
|
||||
require_single_line_env_value PRODUCTION_HTTP_PORT "$http_port"
|
||||
require_single_line_env_value PRODUCTION_TRAEFIK_TRUSTED_IPS "$trusted_proxy_ips"
|
||||
require_single_line_env_value PRODUCTION_EMAIL_DELIVERY_PROVIDER "$email_delivery_provider"
|
||||
require_single_line_env_value PRODUCTION_SMTP_RELAY "$smtp_relay"
|
||||
require_single_line_env_value PRODUCTION_SMTP_PORT "$smtp_port"
|
||||
require_single_line_env_value PRODUCTION_SMTP_USERNAME "$smtp_username"
|
||||
require_single_line_env_value PRODUCTION_SMTP_PASSWORD "$smtp_password"
|
||||
require_single_line_env_value PRODUCTION_SMTP_AUTH "$smtp_auth"
|
||||
require_single_line_env_value PRODUCTION_SMTP_TLS "$smtp_tls"
|
||||
require_single_line_env_value PRODUCTION_SMTP_SSL "$smtp_ssl"
|
||||
require_single_line_env_value PRODUCTION_EMAIL_FROM_ADDRESS "$email_from_address"
|
||||
require_single_line_env_value PRODUCTION_SUPPORT_INBOX_ADDRESS "$support_inbox_address"
|
||||
|
||||
[ "$email_delivery_provider" = smtp ] || {
|
||||
echo "PRODUCTION_EMAIL_DELIVERY_PROVIDER must be smtp." >&2
|
||||
exit 1
|
||||
|
|
@ -326,6 +431,10 @@ TEST_UPSTREAM_VALUE=$test_upstream \
|
|||
replacement["WNH_ANDROID_DEVELOPMENT_SIGNING_KEY_ALIAS"] = ""
|
||||
replacement["WNH_ANDROID_STAGING_SIGNING_KEY_ALIAS"] = ""
|
||||
replacement["CODEX_SESSION_ID"] = ENVIRON["CODEX_SESSION_ID_VALUE"]
|
||||
|
||||
for (key in replacement) {
|
||||
gsub(/\$/, "$$", replacement[key])
|
||||
}
|
||||
}
|
||||
{
|
||||
separator = index($0, "=")
|
||||
|
|
@ -347,6 +456,7 @@ unset postgres_password secret_key_base handover_secret release_cookie metrics_t
|
|||
unset smtp_password
|
||||
unset google_oauth_client_secret
|
||||
unset web_push_vapid_private_key firebase_api_key fcm_service_account_json_base64
|
||||
unset fcm_credential_project_id
|
||||
unset android_app_links_fingerprints
|
||||
|
||||
echo "Generated independent deployment secrets without printing them."
|
||||
|
|
|
|||
|
|
@ -11,6 +11,27 @@ if [[ -z "$domain" ]]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
require_single_line_env_value() {
|
||||
local value_name=$1
|
||||
local value=$2
|
||||
|
||||
if [[ "$value" == *$'\n'* ]]; then
|
||||
echo "$value_name must not contain control characters because .env stores one value per line." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if printf '%s' "$value" | LC_ALL=C grep -q '[[:cntrl:]]'; then
|
||||
echo "$value_name must not contain control characters because .env stores one value per line." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$value" == ' '* || "$value" == *' ' ||
|
||||
"$value" == \"* || "$value" == \'* || "$value" == *' #'* ]]; then
|
||||
echo "$value_name cannot be represented safely as an unquoted Compose .env value." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ ! "$domain" =~ ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$ ]]; then
|
||||
echo "DOMAIN must be a lowercase ASCII DNS hostname without a scheme, port, or path." >&2
|
||||
exit 1
|
||||
|
|
@ -20,7 +41,7 @@ if [[ "$target" != /* ]]; then
|
|||
target="$ROOT/$target"
|
||||
fi
|
||||
|
||||
for command in awk docker git mktemp openssl stat; do
|
||||
for command in awk docker git grep mktemp openssl stat; do
|
||||
command -v "$command" >/dev/null 2>&1 || {
|
||||
echo "Required command is unavailable: $command" >&2
|
||||
exit 1
|
||||
|
|
@ -67,6 +88,29 @@ android_app_links_fingerprints=${TEST_ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS
|
|||
support_inbox_address=${TEST_SUPPORT_INBOX_ADDRESS:-}
|
||||
git_sha=$(git -C "$ROOT" rev-parse --short=12 HEAD)
|
||||
|
||||
require_single_line_env_value TEST_COMPOSE_PROJECT_NAME "$compose_project_name"
|
||||
require_single_line_env_value TEST_PUBLIC_EDGE_NETWORK "$public_edge_network"
|
||||
require_single_line_env_value TEST_PUBLIC_UPSTREAM_NAME "$public_upstream_name"
|
||||
require_single_line_env_value TEST_HTTP_BIND_ADDRESS "$http_bind_address"
|
||||
require_single_line_env_value TEST_HTTP_PORT "$http_port"
|
||||
require_single_line_env_value TEST_MAILPIT_BIND_ADDRESS "$mailpit_bind_address"
|
||||
require_single_line_env_value TEST_MAILPIT_PORT "$mailpit_port"
|
||||
require_single_line_env_value TEST_CODEX_SESSION_ID "$codex_session_id"
|
||||
require_single_line_env_value TEST_GOOGLE_OAUTH_CLIENT_ID "$google_oauth_client_id"
|
||||
require_single_line_env_value TEST_GOOGLE_OAUTH_CLIENT_SECRET "$google_oauth_client_secret"
|
||||
require_single_line_env_value TEST_WEB_PUSH_VAPID_PUBLIC_KEY "$web_push_vapid_public_key"
|
||||
require_single_line_env_value TEST_WEB_PUSH_VAPID_PRIVATE_KEY "$web_push_vapid_private_key"
|
||||
require_single_line_env_value TEST_WEB_PUSH_VAPID_SUBJECT "$web_push_vapid_subject"
|
||||
require_single_line_env_value TEST_WNH_FIREBASE_APPLICATION_ID "$firebase_application_id"
|
||||
require_single_line_env_value TEST_WNH_FIREBASE_API_KEY "$firebase_api_key"
|
||||
require_single_line_env_value TEST_WNH_FIREBASE_PROJECT_ID "$firebase_project_id"
|
||||
require_single_line_env_value TEST_WNH_FIREBASE_GCM_SENDER_ID "$firebase_sender_id"
|
||||
require_single_line_env_value TEST_FCM_PROJECT_ID "$fcm_project_id"
|
||||
require_single_line_env_value TEST_FCM_SERVICE_ACCOUNT_JSON_BASE64 "$fcm_service_account_json_base64"
|
||||
require_single_line_env_value TEST_ANDROID_APP_LINKS_PACKAGE_NAME "$android_app_links_package_name"
|
||||
require_single_line_env_value TEST_ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS "$android_app_links_fingerprints"
|
||||
require_single_line_env_value TEST_SUPPORT_INBOX_ADDRESS "$support_inbox_address"
|
||||
|
||||
[[ "$compose_project_name" =~ ^[a-zA-Z0-9_-]+$ ]] || {
|
||||
echo "TEST_COMPOSE_PROJECT_NAME contains unsupported characters." >&2
|
||||
exit 1
|
||||
|
|
@ -98,6 +142,7 @@ if [[ -n "$android_app_links_package_name" || -n "$android_app_links_fingerprint
|
|||
fi
|
||||
|
||||
firebase_values="$firebase_application_id $firebase_api_key $firebase_project_id $firebase_sender_id"
|
||||
firebase_configured=false
|
||||
if grep -q '[^[:space:]]' <<<"$firebase_values"; then
|
||||
for value in "$firebase_application_id" "$firebase_api_key" \
|
||||
"$firebase_project_id" "$firebase_sender_id"; do
|
||||
|
|
@ -106,6 +151,14 @@ if grep -q '[^[:space:]]' <<<"$firebase_values"; then
|
|||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
firebase_prefix="1:$firebase_sender_id:android:"
|
||||
if [[ ! "$firebase_sender_id" =~ ^[0-9]+$ ||
|
||||
"$firebase_application_id" != "$firebase_prefix"?* ]]; then
|
||||
echo "Test Firebase application ID must belong to the configured numeric sender/project number." >&2
|
||||
exit 1
|
||||
fi
|
||||
firebase_configured=true
|
||||
fi
|
||||
|
||||
vapid_values="$web_push_vapid_public_key $web_push_vapid_private_key $web_push_vapid_subject"
|
||||
|
|
@ -117,6 +170,12 @@ if grep -q '[^[:space:]]' <<<"$vapid_values"; then
|
|||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
if [[ "$web_push_vapid_subject" != mailto:?* &&
|
||||
"$web_push_vapid_subject" != https://?* ]]; then
|
||||
echo "Test WEB_PUSH_VAPID_SUBJECT must be a non-empty mailto: or https:// URI." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ (-n "$fcm_project_id" || -n "$fcm_service_account_json_base64") &&
|
||||
|
|
@ -125,6 +184,7 @@ if [[ (-n "$fcm_project_id" || -n "$fcm_service_account_json_base64") &&
|
|||
exit 1
|
||||
fi
|
||||
|
||||
fcm_credential_project_id=
|
||||
if [[ -n "$fcm_service_account_json_base64" ]]; then
|
||||
for command in base64 jq; do
|
||||
command -v "$command" >/dev/null 2>&1 || {
|
||||
|
|
@ -132,17 +192,38 @@ if [[ -n "$fcm_service_account_json_base64" ]]; then
|
|||
exit 1
|
||||
}
|
||||
done
|
||||
if ! printf '%s' "$fcm_service_account_json_base64" |
|
||||
if ! fcm_credential_project_id=$(printf '%s' "$fcm_service_account_json_base64" |
|
||||
base64 --decode 2>/dev/null |
|
||||
jq -e '
|
||||
.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)
|
||||
' >/dev/null 2>&1; then
|
||||
jq -er '
|
||||
. as $credential
|
||||
| (
|
||||
($credential.type == "service_account") and
|
||||
($credential.project_id | type == "string" and length > 0) and
|
||||
($credential.client_email | type == "string" and length > 0) and
|
||||
($credential.private_key | type == "string" and length > 0)
|
||||
)
|
||||
| if . then $credential.project_id else error("incomplete service account") end
|
||||
' 2>/dev/null); then
|
||||
echo "Test FCM credential is not a complete service-account JSON document." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$fcm_credential_project_id" != "$fcm_project_id" ]]; then
|
||||
echo "Test FCM service-account project must match TEST_FCM_PROJECT_ID." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$firebase_configured" == true &&
|
||||
"$fcm_project_id" != "$firebase_project_id" ]]; then
|
||||
echo "Test Firebase Android client and FCM service account must use the same project." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "$android_app_links_package_name" &&
|
||||
"$android_app_links_package_name" != org.whoneedhelp.mobile.staging ]]; then
|
||||
echo "Test Android App Links package must be org.whoneedhelp.mobile.staging." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for pair in "HTTP:$http_port" "MAILPIT:$mailpit_port"; do
|
||||
|
|
@ -267,6 +348,10 @@ CODEX_SESSION_ID_VALUE=$codex_session_id \
|
|||
replacement["WNH_ANDROID_DEVELOPMENT_SIGNING_KEY_ALIAS"] = ""
|
||||
replacement["WNH_ANDROID_STAGING_SIGNING_KEY_ALIAS"] = "who-need-help-staging"
|
||||
replacement["CODEX_SESSION_ID"] = ENVIRON["CODEX_SESSION_ID_VALUE"]
|
||||
|
||||
for (key in replacement) {
|
||||
gsub(/\$/, "$$", replacement[key])
|
||||
}
|
||||
}
|
||||
{
|
||||
separator = index($0, "=")
|
||||
|
|
@ -282,6 +367,7 @@ trap - EXIT HUP INT TERM
|
|||
unset postgres_password secret_key_base handover_secret release_cookie metrics_token
|
||||
unset google_oauth_client_secret
|
||||
unset web_push_vapid_private_key firebase_api_key fcm_service_account_json_base64
|
||||
unset fcm_credential_project_id
|
||||
unset android_app_links_fingerprints
|
||||
|
||||
"$ROOT/scripts/compose.sh" "$target" config --quiet
|
||||
|
|
|
|||
|
|
@ -105,7 +105,8 @@ chmod 600 "$credential_env"
|
|||
credential_values="$scan_dir/credential-values"
|
||||
printf '%s\n' \
|
||||
'GOOGLE_OAUTH_CLIENT_ID=quality-imported-client' \
|
||||
'GOOGLE_OAUTH_CLIENT_SECRET=quality-imported-secret' >"$credential_values"
|
||||
'GOOGLE_OAUTH_CLIENT_SECRET=quality-imported-secret' \
|
||||
"SMTP_PASSWORD=quality\$literal" >"$credential_values"
|
||||
chmod 600 "$credential_values"
|
||||
credential_output=$(
|
||||
./scripts/set-env-values.sh "$credential_env" "$credential_values"
|
||||
|
|
@ -117,6 +118,32 @@ fi
|
|||
test "$(stat -c '%a' "$credential_env")" = 600
|
||||
grep -Fx 'GOOGLE_OAUTH_CLIENT_ID=quality-imported-client' "$credential_env" >/dev/null
|
||||
grep -Fx 'GOOGLE_OAUTH_CLIENT_SECRET=quality-imported-secret' "$credential_env" >/dev/null
|
||||
grep -Fx "SMTP_PASSWORD=quality\$\$literal" "$credential_env" >/dev/null
|
||||
|
||||
compose_env_probe="$scan_dir/compose-env-probe.yaml"
|
||||
printf '%s\n' \
|
||||
'services:' \
|
||||
' probe:' \
|
||||
" image: $SHELLCHECK_IMAGE" \
|
||||
' network_mode: none' \
|
||||
' entrypoint: ["/usr/bin/env"]' \
|
||||
' environment:' \
|
||||
" SMTP_PASSWORD: \${SMTP_PASSWORD}" \
|
||||
>"$compose_env_probe"
|
||||
resolved_smtp_password=$(
|
||||
docker compose \
|
||||
--project-name "$project" \
|
||||
--env-file "$credential_env" \
|
||||
--file "$compose_env_probe" \
|
||||
run --rm --no-deps probe |
|
||||
awk -F= '
|
||||
$1 == "SMTP_PASSWORD" {
|
||||
print substr($0, index($0, "=") + 1)
|
||||
exit
|
||||
}
|
||||
'
|
||||
)
|
||||
test "$resolved_smtp_password" = "quality\$literal"
|
||||
|
||||
duplicate_env="$scan_dir/credentials-duplicate.env"
|
||||
cp "$credential_env" "$duplicate_env"
|
||||
|
|
@ -212,6 +239,41 @@ if ./scripts/import-firebase-android-config.sh \
|
|||
exit 1
|
||||
fi
|
||||
|
||||
firebase_injected_client="$scan_dir/google-services-injected.json"
|
||||
injected_firebase_project=$(
|
||||
printf 'quality-development\nGOOGLE_OAUTH_CLIENT_SECRET=injected'
|
||||
)
|
||||
jq --null-input \
|
||||
--arg project_id "$injected_firebase_project" \
|
||||
'{
|
||||
project_info: {
|
||||
project_number: "123456789",
|
||||
project_id: $project_id
|
||||
},
|
||||
client: [
|
||||
{
|
||||
client_info: {
|
||||
mobilesdk_app_id: "1:123456789:android:quality",
|
||||
android_client_info: {
|
||||
package_name: "org.whoneedhelp.mobile.staging"
|
||||
}
|
||||
},
|
||||
api_key: [
|
||||
{
|
||||
current_key: "quality-firebase-api-key"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}' >"$firebase_injected_client"
|
||||
credential_hash=$(sha256sum "$credential_env" | awk '{print $1}')
|
||||
if ./scripts/import-firebase-android-config.sh \
|
||||
"$credential_env" "$firebase_injected_client" >/dev/null 2>&1; then
|
||||
echo "Firebase importer accepted a line-breaking project ID." >&2
|
||||
exit 1
|
||||
fi
|
||||
test "$(sha256sum "$credential_env" | awk '{print $1}')" = "$credential_hash"
|
||||
|
||||
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"
|
||||
|
|
@ -277,6 +339,28 @@ printf '%s' "$credential_fcm_base64" |
|
|||
'.project_id == "quality-development" and .private_key == "quality-private-key"' \
|
||||
>/dev/null
|
||||
|
||||
fcm_injected_service_account="$scan_dir/fcm-service-account-injected.json"
|
||||
injected_fcm_project=$(
|
||||
printf 'quality-development\nGOOGLE_OAUTH_CLIENT_SECRET=injected'
|
||||
)
|
||||
jq --null-input \
|
||||
--arg project_id "$injected_fcm_project" \
|
||||
'{
|
||||
type: "service_account",
|
||||
project_id: $project_id,
|
||||
client_email: "quality-fcm@example.invalid",
|
||||
private_key: "quality-private-key"
|
||||
}' >"$fcm_injected_service_account"
|
||||
chmod 600 "$fcm_injected_service_account"
|
||||
credential_hash=$(sha256sum "$credential_env" | awk '{print $1}')
|
||||
if ./scripts/import-fcm-service-account.sh \
|
||||
"$credential_env" "$fcm_injected_service_account" >/dev/null 2>&1; then
|
||||
echo "FCM importer accepted a line-breaking project ID." >&2
|
||||
exit 1
|
||||
fi
|
||||
test "$(sha256sum "$credential_env" | awk '{print $1}')" = "$credential_hash"
|
||||
rm -f "$fcm_injected_service_account"
|
||||
|
||||
echo "Checking the existing load environment upgrade path"
|
||||
legacy_load_env="$scan_dir/legacy-load.env"
|
||||
printf '%s\n' \
|
||||
|
|
@ -304,6 +388,16 @@ quality_fcm_base64=$(
|
|||
'{"type":"service_account","project_id":"quality-production","client_email":"quality-fcm@quality-production.iam.gserviceaccount.com","private_key":"quality-private-key"}' |
|
||||
base64 -w 0
|
||||
)
|
||||
quality_test_fcm_base64=$(
|
||||
printf '%s' \
|
||||
'{"type":"service_account","project_id":"quality-test","client_email":"quality-fcm@quality-test.iam.gserviceaccount.com","private_key":"quality-private-key"}' |
|
||||
base64 -w 0
|
||||
)
|
||||
quality_other_fcm_base64=$(
|
||||
printf '%s' \
|
||||
'{"type":"service_account","project_id":"quality-other","client_email":"quality-fcm@quality-other.iam.gserviceaccount.com","private_key":"quality-private-key"}' |
|
||||
base64 -w 0
|
||||
)
|
||||
test_env="$scan_dir/test.env"
|
||||
if ./scripts/init-test-env.sh test.help.test \
|
||||
"$scan_dir/test.missing-codex.env" >/dev/null 2>&1; then
|
||||
|
|
@ -312,7 +406,16 @@ if ./scripts/init-test-env.sh test.help.test \
|
|||
fi
|
||||
TEST_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
||||
TEST_GOOGLE_OAUTH_CLIENT_ID=quality-test-client \
|
||||
TEST_GOOGLE_OAUTH_CLIENT_SECRET=quality-test-secret \
|
||||
TEST_GOOGLE_OAUTH_CLIENT_SECRET="quality-test\$secret" \
|
||||
TEST_WEB_PUSH_VAPID_PUBLIC_KEY=quality-test-vapid-public \
|
||||
TEST_WEB_PUSH_VAPID_PRIVATE_KEY=quality-test-vapid-private \
|
||||
TEST_WEB_PUSH_VAPID_SUBJECT=mailto:test@help.test \
|
||||
TEST_WNH_FIREBASE_APPLICATION_ID=1:123456789:android:quality-test \
|
||||
TEST_WNH_FIREBASE_API_KEY=quality-test-public-client-key \
|
||||
TEST_WNH_FIREBASE_PROJECT_ID=quality-test \
|
||||
TEST_WNH_FIREBASE_GCM_SENDER_ID=123456789 \
|
||||
TEST_FCM_PROJECT_ID=quality-test \
|
||||
TEST_FCM_SERVICE_ACCOUNT_JSON_BASE64="$quality_test_fcm_base64" \
|
||||
TEST_ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile.staging \
|
||||
TEST_ANDROID_APP_LINKS_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-test-env.sh test.help.test "$test_env" >/dev/null
|
||||
|
|
@ -332,6 +435,12 @@ grep -Fx 'EMAIL_DELIVERY_PROVIDER=smtp' "$test_env" >/dev/null
|
|||
grep -Fx 'SMTP_RELAY=mailpit' "$test_env" >/dev/null
|
||||
grep -Fx 'EMAIL_FROM_NAME="Who Need Help Test"' "$test_env" >/dev/null
|
||||
grep -Fx 'GOOGLE_OAUTH_CLIENT_ID=quality-test-client' "$test_env" >/dev/null
|
||||
grep -Fx "GOOGLE_OAUTH_CLIENT_SECRET=quality-test\$\$secret" "$test_env" >/dev/null
|
||||
grep -Fx 'WEB_PUSH_VAPID_SUBJECT=mailto:test@help.test' "$test_env" >/dev/null
|
||||
grep -Fx 'WNH_FIREBASE_APPLICATION_ID=1:123456789:android:quality-test' "$test_env" >/dev/null
|
||||
grep -Fx 'WNH_FIREBASE_PROJECT_ID=quality-test' "$test_env" >/dev/null
|
||||
grep -Fx 'WNH_FIREBASE_GCM_SENDER_ID=123456789' "$test_env" >/dev/null
|
||||
grep -Fx 'FCM_PROJECT_ID=quality-test' "$test_env" >/dev/null
|
||||
grep -Fx 'ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile.staging' "$test_env" >/dev/null
|
||||
grep -Fx 'ANDROID_APP_LINKS_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' "$test_env" >/dev/null
|
||||
./scripts/validate-test-env.sh "$test_env" test.help.test >/dev/null
|
||||
|
|
@ -347,6 +456,65 @@ if TEST_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
|||
echo "Test environment initializer overwrote an existing file." >&2
|
||||
exit 1
|
||||
fi
|
||||
if TEST_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
||||
TEST_WNH_FIREBASE_APPLICATION_ID=1:987654321:android:quality-test \
|
||||
TEST_WNH_FIREBASE_API_KEY=quality-test-public-client-key \
|
||||
TEST_WNH_FIREBASE_PROJECT_ID=quality-test \
|
||||
TEST_WNH_FIREBASE_GCM_SENDER_ID=123456789 \
|
||||
./scripts/init-test-env.sh test.help.test \
|
||||
"$scan_dir/test.mismatched-firebase.env" >/dev/null 2>&1; then
|
||||
echo "Test environment initializer accepted a Firebase application from another sender." >&2
|
||||
exit 1
|
||||
fi
|
||||
if TEST_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
||||
TEST_FCM_PROJECT_ID=quality-test \
|
||||
TEST_FCM_SERVICE_ACCOUNT_JSON_BASE64="$quality_other_fcm_base64" \
|
||||
./scripts/init-test-env.sh test.help.test \
|
||||
"$scan_dir/test.mismatched-fcm-credential.env" >/dev/null 2>&1; then
|
||||
echo "Test environment initializer accepted an FCM service account from another project." >&2
|
||||
exit 1
|
||||
fi
|
||||
if TEST_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
||||
TEST_WNH_FIREBASE_APPLICATION_ID=1:123456789:android:quality-test \
|
||||
TEST_WNH_FIREBASE_API_KEY=quality-test-public-client-key \
|
||||
TEST_WNH_FIREBASE_PROJECT_ID=quality-test \
|
||||
TEST_WNH_FIREBASE_GCM_SENDER_ID=123456789 \
|
||||
TEST_FCM_PROJECT_ID=quality-other \
|
||||
TEST_FCM_SERVICE_ACCOUNT_JSON_BASE64="$quality_other_fcm_base64" \
|
||||
./scripts/init-test-env.sh test.help.test \
|
||||
"$scan_dir/test.mismatched-firebase-fcm.env" >/dev/null 2>&1; then
|
||||
echo "Test environment initializer accepted different Firebase and FCM projects." >&2
|
||||
exit 1
|
||||
fi
|
||||
if TEST_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
||||
TEST_ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile \
|
||||
TEST_ANDROID_APP_LINKS_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-test-env.sh test.help.test \
|
||||
"$scan_dir/test.production-package.env" >/dev/null 2>&1; then
|
||||
echo "Test environment initializer accepted the production Android package." >&2
|
||||
exit 1
|
||||
fi
|
||||
if TEST_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
||||
TEST_WEB_PUSH_VAPID_PUBLIC_KEY=quality-test-vapid-public \
|
||||
TEST_WEB_PUSH_VAPID_PRIVATE_KEY=quality-test-vapid-private \
|
||||
TEST_WEB_PUSH_VAPID_SUBJECT=ftp://help.test \
|
||||
./scripts/init-test-env.sh test.help.test \
|
||||
"$scan_dir/test.invalid-vapid-subject.env" >/dev/null 2>&1; then
|
||||
echo "Test environment initializer accepted an invalid VAPID subject." >&2
|
||||
exit 1
|
||||
fi
|
||||
injected_test_secret=$(
|
||||
printf 'quality-test-secret\nSMTP_PASSWORD=injected'
|
||||
)
|
||||
if TEST_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
||||
TEST_GOOGLE_OAUTH_CLIENT_ID=quality-test-client \
|
||||
TEST_GOOGLE_OAUTH_CLIENT_SECRET="$injected_test_secret" \
|
||||
./scripts/init-test-env.sh test.help.test \
|
||||
"$scan_dir/test.injected-line.env" >/dev/null 2>&1; then
|
||||
echo "Test environment initializer accepted a line-breaking credential." >&2
|
||||
exit 1
|
||||
fi
|
||||
test ! -e "$scan_dir/test.injected-line.env"
|
||||
production_env="$scan_dir/production.env"
|
||||
missing_codex_env="$scan_dir/production.missing-codex.env"
|
||||
if PRODUCTION_TRAEFIK_TRUSTED_IPS=172.20.0.1/32 \
|
||||
|
|
@ -366,7 +534,7 @@ PRODUCTION_TRAEFIK_TRUSTED_IPS=172.20.0.1/32 \
|
|||
PRODUCTION_SMTP_RELAY=smtp.help.test \
|
||||
PRODUCTION_SMTP_PORT=587 \
|
||||
PRODUCTION_SMTP_USERNAME=quality-user \
|
||||
PRODUCTION_SMTP_PASSWORD=quality-password \
|
||||
PRODUCTION_SMTP_PASSWORD="quality\$password" \
|
||||
PRODUCTION_SMTP_AUTH=always \
|
||||
PRODUCTION_SMTP_TLS=always \
|
||||
PRODUCTION_SMTP_SSL=false \
|
||||
|
|
@ -388,8 +556,67 @@ PRODUCTION_ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS=A5:74:2B:AE:70:C6:D0:34:E3
|
|||
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
|
||||
grep -Fx "SMTP_PASSWORD=quality\$\$password" "$production_env" >/dev/null
|
||||
./scripts/validate-production-env.sh "$production_env" help.test >/dev/null
|
||||
./scripts/check-environment-readiness.sh "$production_env" --require-release >/dev/null
|
||||
if PRODUCTION_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
||||
PRODUCTION_WNH_FIREBASE_APPLICATION_ID=1:987654321:android:quality \
|
||||
PRODUCTION_WNH_FIREBASE_API_KEY=quality-public-client-key \
|
||||
PRODUCTION_WNH_FIREBASE_PROJECT_ID=quality-production \
|
||||
PRODUCTION_WNH_FIREBASE_GCM_SENDER_ID=123456789 \
|
||||
./scripts/init-production-env.sh help.test \
|
||||
"$scan_dir/production.mismatched-firebase-init.env" >/dev/null 2>&1; then
|
||||
echo "Production environment initializer accepted a Firebase application from another sender." >&2
|
||||
exit 1
|
||||
fi
|
||||
if PRODUCTION_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
||||
PRODUCTION_FCM_PROJECT_ID=quality-production \
|
||||
PRODUCTION_FCM_SERVICE_ACCOUNT_JSON_BASE64="$quality_other_fcm_base64" \
|
||||
./scripts/init-production-env.sh help.test \
|
||||
"$scan_dir/production.mismatched-fcm-credential.env" >/dev/null 2>&1; then
|
||||
echo "Production environment initializer accepted an FCM service account from another project." >&2
|
||||
exit 1
|
||||
fi
|
||||
if PRODUCTION_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
||||
PRODUCTION_WNH_FIREBASE_APPLICATION_ID=1:123456789:android:quality \
|
||||
PRODUCTION_WNH_FIREBASE_API_KEY=quality-public-client-key \
|
||||
PRODUCTION_WNH_FIREBASE_PROJECT_ID=quality-production \
|
||||
PRODUCTION_WNH_FIREBASE_GCM_SENDER_ID=123456789 \
|
||||
PRODUCTION_FCM_PROJECT_ID=quality-other \
|
||||
PRODUCTION_FCM_SERVICE_ACCOUNT_JSON_BASE64="$quality_other_fcm_base64" \
|
||||
./scripts/init-production-env.sh help.test \
|
||||
"$scan_dir/production.mismatched-firebase-fcm.env" >/dev/null 2>&1; then
|
||||
echo "Production environment initializer accepted different Firebase and FCM projects." >&2
|
||||
exit 1
|
||||
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 \
|
||||
./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_WEB_PUSH_VAPID_PUBLIC_KEY=quality-vapid-public \
|
||||
PRODUCTION_WEB_PUSH_VAPID_PRIVATE_KEY=quality-vapid-private \
|
||||
PRODUCTION_WEB_PUSH_VAPID_SUBJECT=ftp://help.test \
|
||||
./scripts/init-production-env.sh help.test \
|
||||
"$scan_dir/production.invalid-vapid-subject.env" >/dev/null 2>&1; then
|
||||
echo "Production environment initializer accepted an invalid VAPID subject." >&2
|
||||
exit 1
|
||||
fi
|
||||
injected_smtp_password=$(
|
||||
printf 'quality-password\nGOOGLE_OAUTH_CLIENT_SECRET=injected'
|
||||
)
|
||||
if PRODUCTION_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
||||
PRODUCTION_SMTP_PASSWORD="$injected_smtp_password" \
|
||||
./scripts/init-production-env.sh help.test \
|
||||
"$scan_dir/production.injected-line.env" >/dev/null 2>&1; then
|
||||
echo "Production environment initializer accepted a line-breaking credential." >&2
|
||||
exit 1
|
||||
fi
|
||||
test ! -e "$scan_dir/production.injected-line.env"
|
||||
invalid_fcm_env="$scan_dir/production.invalid-fcm.env"
|
||||
invalid_fcm_base64=$(
|
||||
printf '%s' '{"type":"service_account","project_id":"quality-production"}' |
|
||||
|
|
@ -984,6 +1211,105 @@ done
|
|||
echo "Building the pinned quality image and cached Dialyzer PLTs"
|
||||
docker build --target quality --tag "$quality_image" .
|
||||
|
||||
echo "Checking isolated VAPID generation and atomic single-file import"
|
||||
generated_vapid_env="$scan_dir/generated-vapid.env"
|
||||
cp .env.example "$generated_vapid_env"
|
||||
chmod 600 "$generated_vapid_env"
|
||||
vapid_output=$(
|
||||
WNH_VAPID_GENERATOR_IMAGE="$quality_image" \
|
||||
./scripts/generate-vapid-env.sh \
|
||||
"$generated_vapid_env" mailto:contact@help.test
|
||||
)
|
||||
generated_vapid_public=$(
|
||||
awk -F= '
|
||||
$1 == "WEB_PUSH_VAPID_PUBLIC_KEY" {
|
||||
print substr($0, index($0, "=") + 1)
|
||||
exit
|
||||
}
|
||||
' "$generated_vapid_env"
|
||||
)
|
||||
generated_vapid_private=$(
|
||||
awk -F= '
|
||||
$1 == "WEB_PUSH_VAPID_PRIVATE_KEY" {
|
||||
print substr($0, index($0, "=") + 1)
|
||||
exit
|
||||
}
|
||||
' "$generated_vapid_env"
|
||||
)
|
||||
test -n "$generated_vapid_public"
|
||||
test -n "$generated_vapid_private"
|
||||
test "$generated_vapid_public" != "$generated_vapid_private"
|
||||
grep -Fx 'WEB_PUSH_VAPID_SUBJECT=mailto:contact@help.test' \
|
||||
"$generated_vapid_env" >/dev/null
|
||||
if printf '%s' "$vapid_output" |
|
||||
grep -F "$generated_vapid_public" >/dev/null ||
|
||||
printf '%s' "$vapid_output" |
|
||||
grep -F "$generated_vapid_private" >/dev/null; then
|
||||
echo "VAPID generator printed generated key material." >&2
|
||||
exit 1
|
||||
fi
|
||||
docker run --rm \
|
||||
--network none \
|
||||
--read-only \
|
||||
--volume "$generated_vapid_env:/tmp/generated-vapid.env:ro" \
|
||||
--entrypoint elixir \
|
||||
"$quality_image" \
|
||||
-e '
|
||||
values =
|
||||
"/tmp/generated-vapid.env"
|
||||
|> File.read!()
|
||||
|> String.split("\n", trim: true)
|
||||
|> Enum.reject(&(String.starts_with?(&1, "#") or not String.contains?(&1, "=")))
|
||||
|> Map.new(fn line ->
|
||||
[key, value] = String.split(line, "=", parts: 2)
|
||||
{key, value}
|
||||
end)
|
||||
|
||||
{:ok, public_key} =
|
||||
Base.url_decode64(values["WEB_PUSH_VAPID_PUBLIC_KEY"], padding: false)
|
||||
|
||||
{:ok, private_key} =
|
||||
Base.url_decode64(values["WEB_PUSH_VAPID_PRIVATE_KEY"], padding: false)
|
||||
|
||||
unless byte_size(public_key) == 65 and
|
||||
:binary.first(public_key) == 4 and
|
||||
byte_size(private_key) == 32 do
|
||||
raise "unexpected VAPID key shape"
|
||||
end
|
||||
'
|
||||
if WNH_VAPID_GENERATOR_IMAGE="$quality_image" \
|
||||
./scripts/generate-vapid-env.sh \
|
||||
"$generated_vapid_env" mailto:contact@help.test >/dev/null 2>&1; then
|
||||
echo "VAPID generator rotated an existing environment identity." >&2
|
||||
exit 1
|
||||
fi
|
||||
fresh_vapid_env="$scan_dir/fresh-vapid.env"
|
||||
cp .env.example "$fresh_vapid_env"
|
||||
chmod 600 "$fresh_vapid_env"
|
||||
if WNH_VAPID_GENERATOR_IMAGE="$quality_image" \
|
||||
./scripts/generate-vapid-env.sh \
|
||||
"$fresh_vapid_env" ftp://help.test >/dev/null 2>&1; then
|
||||
echo "VAPID generator accepted an invalid subject." >&2
|
||||
exit 1
|
||||
fi
|
||||
fresh_vapid_hash=$(sha256sum "$fresh_vapid_env" | awk '{print $1}')
|
||||
injected_vapid_subject=$(
|
||||
printf 'mailto:contact@help.test\nGOOGLE_OAUTH_CLIENT_SECRET=injected'
|
||||
)
|
||||
if WNH_VAPID_GENERATOR_IMAGE="$quality_image" \
|
||||
./scripts/generate-vapid-env.sh \
|
||||
"$fresh_vapid_env" "$injected_vapid_subject" >/dev/null 2>&1; then
|
||||
echo "VAPID generator accepted a line-breaking subject." >&2
|
||||
exit 1
|
||||
fi
|
||||
test "$(sha256sum "$fresh_vapid_env" | awk '{print $1}')" = "$fresh_vapid_hash"
|
||||
if find "$scan_dir" -maxdepth 1 -name '.vapid-generation.*' -print |
|
||||
grep -q .; then
|
||||
echo "VAPID generator retained a temporary credential directory." >&2
|
||||
exit 1
|
||||
fi
|
||||
unset generated_vapid_public generated_vapid_private
|
||||
|
||||
echo "Running Elixir format, compiler, xref, Credo, Sobelow, Dialyzer, and Hex audit"
|
||||
docker run --rm "$quality_image" sh -euc '
|
||||
mix format --check-formatted
|
||||
|
|
|
|||
|
|
@ -55,7 +55,9 @@ if ! awk '
|
|||
exit 40
|
||||
}
|
||||
|
||||
replacement[key] = substr($0, index($0, "=") + 1)
|
||||
value = substr($0, index($0, "=") + 1)
|
||||
gsub(/\$/, "$$", value)
|
||||
replacement[key] = value
|
||||
replacement_count++
|
||||
next
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user