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_CONNECT_TIMEOUT_MS=
|
||||||
PUSH_HTTP_RETRY_DELAY_MS=
|
PUSH_HTTP_RETRY_DELAY_MS=
|
||||||
|
|
||||||
# Direct browser Web Push. Generate one VAPID key pair per environment and
|
# Direct browser Web Push. Generate one VAPID key pair per environment with
|
||||||
# keep the private key only in that environment's .env. The subject must be a
|
# scripts/generate-vapid-env.sh and keep the private key only in that
|
||||||
# mailto: or HTTPS contact owned by the operator.
|
# environment's .env. The subject must be a mailto: or HTTPS contact owned by
|
||||||
|
# the operator.
|
||||||
WEB_PUSH_VAPID_PUBLIC_KEY=
|
WEB_PUSH_VAPID_PUBLIC_KEY=
|
||||||
WEB_PUSH_VAPID_PRIVATE_KEY=
|
WEB_PUSH_VAPID_PRIVATE_KEY=
|
||||||
WEB_PUSH_VAPID_SUBJECT=
|
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:
|
secrets on a command line or printing them:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
./scripts/generate-vapid-env.sh \
|
||||||
|
.env mailto:contact@YOUR_DOMAIN
|
||||||
|
|
||||||
chmod 600 /secure/downloads/google-oauth-client.json
|
chmod 600 /secure/downloads/google-oauth-client.json
|
||||||
./scripts/import-google-oauth-client.sh \
|
./scripts/import-google-oauth-client.sh \
|
||||||
.env /secure/downloads/google-oauth-client.json
|
.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
|
.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
|
The importers validate the exact OAuth callback, Android package, Firebase
|
||||||
project relationship, and required service-account fields before atomically
|
project relationship, and required service-account fields before atomically
|
||||||
replacing existing keys. They preserve mode `0600` and never create another
|
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
|
private credentials after import; deliberately move them to protected backup
|
||||||
storage or remove them after verification.
|
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 '
|
if ! jq --exit-status '
|
||||||
.type == "service_account"
|
.type == "service_account"
|
||||||
and (.project_id | type == "string" and length > 0)
|
and (.project_id
|
||||||
and (.client_email | type == "string" and length > 0)
|
| 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)
|
and (.private_key | type == "string" and length > 0)
|
||||||
' "$service_account_file" >/dev/null; then
|
' "$service_account_file" >/dev/null; then
|
||||||
echo "FCM service-account JSON is incomplete." >&2
|
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)
|
| select(.client_info.android_client_info.package_name == $package)
|
||||||
] as $clients
|
] as $clients
|
||||||
| ($clients | length == 1)
|
| ($clients | length == 1)
|
||||||
and (.project_info.project_id | type == "string" and length > 0)
|
and (.project_info.project_id
|
||||||
and (.project_info.project_number | type == "string" and length > 0)
|
| type == "string" and length > 0 and test("^[^\r\n]+$"))
|
||||||
and ($clients[0].client_info.mobilesdk_app_id | type == "string" and length > 0)
|
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
|
and ($clients[0].client_info.mobilesdk_app_id
|
||||||
| startswith("1:" + $project_number + ":android:"))
|
| 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
|
' "$client_file" >/dev/null; then
|
||||||
echo "Firebase Android configuration does not contain exactly one complete client for package $package_name." >&2
|
echo "Firebase Android configuration does not contain exactly one complete client for package $package_name." >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,31 @@ usage() {
|
||||||
echo "Usage: $0 DOMAIN [OUTPUT_FILE]" >&2
|
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
|
if [ -z "$domain" ]; then
|
||||||
usage
|
usage
|
||||||
exit 1
|
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}
|
edge_compose_project_name=${PRODUCTION_EDGE_COMPOSE_PROJECT_NAME:-who_need_help_edge}
|
||||||
git_sha=$(git -C "$ROOT" rev-parse --short=12 HEAD)
|
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
|
if [ -z "$codex_session_id" ]; then
|
||||||
echo "PRODUCTION_CODEX_SESSION_ID is required for the Build Week feedback page." >&2
|
echo "PRODUCTION_CODEX_SESSION_ID is required for the Build Week feedback page." >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|
@ -87,6 +136,7 @@ if { [ -n "$android_app_links_package_name" ] || [ -n "$android_app_links_finger
|
||||||
fi
|
fi
|
||||||
|
|
||||||
firebase_values="$firebase_application_id $firebase_api_key $firebase_project_id $firebase_sender_id"
|
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
|
if printf '%s\n' "$firebase_values" | grep -q '[^[:space:]]'; then
|
||||||
for value in "$firebase_application_id" "$firebase_api_key" \
|
for value in "$firebase_application_id" "$firebase_api_key" \
|
||||||
"$firebase_project_id" "$firebase_sender_id"; do
|
"$firebase_project_id" "$firebase_sender_id"; do
|
||||||
|
|
@ -95,6 +145,15 @@ if printf '%s\n' "$firebase_values" | grep -q '[^[:space:]]'; then
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
done
|
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
|
fi
|
||||||
|
|
||||||
vapid_values="$web_push_vapid_public_key $web_push_vapid_private_key $web_push_vapid_subject"
|
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
|
exit 1
|
||||||
}
|
}
|
||||||
done
|
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
|
fi
|
||||||
|
|
||||||
if { [ -n "$fcm_project_id" ] || [ -n "$fcm_service_account_json_base64" ]; } &&
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
fcm_credential_project_id=
|
||||||
if [ -n "$fcm_service_account_json_base64" ]; then
|
if [ -n "$fcm_service_account_json_base64" ]; then
|
||||||
for command in base64 jq; do
|
for command in base64 jq; do
|
||||||
command -v "$command" >/dev/null 2>&1 || {
|
command -v "$command" >/dev/null 2>&1 || {
|
||||||
|
|
@ -121,17 +189,38 @@ if [ -n "$fcm_service_account_json_base64" ]; then
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
done
|
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 |
|
base64 --decode 2>/dev/null |
|
||||||
jq -e '
|
jq -er '
|
||||||
.type == "service_account" and
|
. as $credential
|
||||||
(.project_id | type == "string" and length > 0) and
|
| (
|
||||||
(.client_email | type == "string" and length > 0) and
|
($credential.type == "service_account") and
|
||||||
(.private_key | type == "string" and length > 0)
|
($credential.project_id | type == "string" and length > 0) and
|
||||||
' >/dev/null 2>&1; then
|
($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
|
echo "Production FCM credential is not a complete service-account JSON document." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
fi
|
||||||
|
|
||||||
case "$compose_project_name" in
|
case "$compose_project_name" in
|
||||||
|
|
@ -203,6 +292,22 @@ smtp_ssl=${PRODUCTION_SMTP_SSL:-false}
|
||||||
email_from_address=${PRODUCTION_EMAIL_FROM_ADDRESS:-"contact@$domain"}
|
email_from_address=${PRODUCTION_EMAIL_FROM_ADDRESS:-"contact@$domain"}
|
||||||
support_inbox_address=${PRODUCTION_SUPPORT_INBOX_ADDRESS:-}
|
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 ] || {
|
[ "$email_delivery_provider" = smtp ] || {
|
||||||
echo "PRODUCTION_EMAIL_DELIVERY_PROVIDER must be smtp." >&2
|
echo "PRODUCTION_EMAIL_DELIVERY_PROVIDER must be smtp." >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|
@ -326,6 +431,10 @@ TEST_UPSTREAM_VALUE=$test_upstream \
|
||||||
replacement["WNH_ANDROID_DEVELOPMENT_SIGNING_KEY_ALIAS"] = ""
|
replacement["WNH_ANDROID_DEVELOPMENT_SIGNING_KEY_ALIAS"] = ""
|
||||||
replacement["WNH_ANDROID_STAGING_SIGNING_KEY_ALIAS"] = ""
|
replacement["WNH_ANDROID_STAGING_SIGNING_KEY_ALIAS"] = ""
|
||||||
replacement["CODEX_SESSION_ID"] = ENVIRON["CODEX_SESSION_ID_VALUE"]
|
replacement["CODEX_SESSION_ID"] = ENVIRON["CODEX_SESSION_ID_VALUE"]
|
||||||
|
|
||||||
|
for (key in replacement) {
|
||||||
|
gsub(/\$/, "$$", replacement[key])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
separator = index($0, "=")
|
separator = index($0, "=")
|
||||||
|
|
@ -347,6 +456,7 @@ unset postgres_password secret_key_base handover_secret release_cookie metrics_t
|
||||||
unset smtp_password
|
unset smtp_password
|
||||||
unset google_oauth_client_secret
|
unset google_oauth_client_secret
|
||||||
unset web_push_vapid_private_key firebase_api_key fcm_service_account_json_base64
|
unset web_push_vapid_private_key firebase_api_key fcm_service_account_json_base64
|
||||||
|
unset fcm_credential_project_id
|
||||||
unset android_app_links_fingerprints
|
unset android_app_links_fingerprints
|
||||||
|
|
||||||
echo "Generated independent deployment secrets without printing them."
|
echo "Generated independent deployment secrets without printing them."
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,27 @@ if [[ -z "$domain" ]]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
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
|
echo "DOMAIN must be a lowercase ASCII DNS hostname without a scheme, port, or path." >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|
@ -20,7 +41,7 @@ if [[ "$target" != /* ]]; then
|
||||||
target="$ROOT/$target"
|
target="$ROOT/$target"
|
||||||
fi
|
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 || {
|
command -v "$command" >/dev/null 2>&1 || {
|
||||||
echo "Required command is unavailable: $command" >&2
|
echo "Required command is unavailable: $command" >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|
@ -67,6 +88,29 @@ android_app_links_fingerprints=${TEST_ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS
|
||||||
support_inbox_address=${TEST_SUPPORT_INBOX_ADDRESS:-}
|
support_inbox_address=${TEST_SUPPORT_INBOX_ADDRESS:-}
|
||||||
git_sha=$(git -C "$ROOT" rev-parse --short=12 HEAD)
|
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_-]+$ ]] || {
|
[[ "$compose_project_name" =~ ^[a-zA-Z0-9_-]+$ ]] || {
|
||||||
echo "TEST_COMPOSE_PROJECT_NAME contains unsupported characters." >&2
|
echo "TEST_COMPOSE_PROJECT_NAME contains unsupported characters." >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|
@ -98,6 +142,7 @@ if [[ -n "$android_app_links_package_name" || -n "$android_app_links_fingerprint
|
||||||
fi
|
fi
|
||||||
|
|
||||||
firebase_values="$firebase_application_id $firebase_api_key $firebase_project_id $firebase_sender_id"
|
firebase_values="$firebase_application_id $firebase_api_key $firebase_project_id $firebase_sender_id"
|
||||||
|
firebase_configured=false
|
||||||
if grep -q '[^[:space:]]' <<<"$firebase_values"; then
|
if grep -q '[^[:space:]]' <<<"$firebase_values"; then
|
||||||
for value in "$firebase_application_id" "$firebase_api_key" \
|
for value in "$firebase_application_id" "$firebase_api_key" \
|
||||||
"$firebase_project_id" "$firebase_sender_id"; do
|
"$firebase_project_id" "$firebase_sender_id"; do
|
||||||
|
|
@ -106,6 +151,14 @@ if grep -q '[^[:space:]]' <<<"$firebase_values"; then
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
done
|
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
|
fi
|
||||||
|
|
||||||
vapid_values="$web_push_vapid_public_key $web_push_vapid_private_key $web_push_vapid_subject"
|
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
|
exit 1
|
||||||
}
|
}
|
||||||
done
|
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
|
fi
|
||||||
|
|
||||||
if [[ (-n "$fcm_project_id" || -n "$fcm_service_account_json_base64") &&
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
fcm_credential_project_id=
|
||||||
if [[ -n "$fcm_service_account_json_base64" ]]; then
|
if [[ -n "$fcm_service_account_json_base64" ]]; then
|
||||||
for command in base64 jq; do
|
for command in base64 jq; do
|
||||||
command -v "$command" >/dev/null 2>&1 || {
|
command -v "$command" >/dev/null 2>&1 || {
|
||||||
|
|
@ -132,17 +192,38 @@ if [[ -n "$fcm_service_account_json_base64" ]]; then
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
done
|
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 |
|
base64 --decode 2>/dev/null |
|
||||||
jq -e '
|
jq -er '
|
||||||
.type == "service_account" and
|
. as $credential
|
||||||
(.project_id | type == "string" and length > 0) and
|
| (
|
||||||
(.client_email | type == "string" and length > 0) and
|
($credential.type == "service_account") and
|
||||||
(.private_key | type == "string" and length > 0)
|
($credential.project_id | type == "string" and length > 0) and
|
||||||
' >/dev/null 2>&1; then
|
($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
|
echo "Test FCM credential is not a complete service-account JSON document." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
fi
|
||||||
|
|
||||||
for pair in "HTTP:$http_port" "MAILPIT:$mailpit_port"; do
|
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_DEVELOPMENT_SIGNING_KEY_ALIAS"] = ""
|
||||||
replacement["WNH_ANDROID_STAGING_SIGNING_KEY_ALIAS"] = "who-need-help-staging"
|
replacement["WNH_ANDROID_STAGING_SIGNING_KEY_ALIAS"] = "who-need-help-staging"
|
||||||
replacement["CODEX_SESSION_ID"] = ENVIRON["CODEX_SESSION_ID_VALUE"]
|
replacement["CODEX_SESSION_ID"] = ENVIRON["CODEX_SESSION_ID_VALUE"]
|
||||||
|
|
||||||
|
for (key in replacement) {
|
||||||
|
gsub(/\$/, "$$", replacement[key])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
separator = index($0, "=")
|
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 postgres_password secret_key_base handover_secret release_cookie metrics_token
|
||||||
unset google_oauth_client_secret
|
unset google_oauth_client_secret
|
||||||
unset web_push_vapid_private_key firebase_api_key fcm_service_account_json_base64
|
unset web_push_vapid_private_key firebase_api_key fcm_service_account_json_base64
|
||||||
|
unset fcm_credential_project_id
|
||||||
unset android_app_links_fingerprints
|
unset android_app_links_fingerprints
|
||||||
|
|
||||||
"$ROOT/scripts/compose.sh" "$target" config --quiet
|
"$ROOT/scripts/compose.sh" "$target" config --quiet
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,8 @@ chmod 600 "$credential_env"
|
||||||
credential_values="$scan_dir/credential-values"
|
credential_values="$scan_dir/credential-values"
|
||||||
printf '%s\n' \
|
printf '%s\n' \
|
||||||
'GOOGLE_OAUTH_CLIENT_ID=quality-imported-client' \
|
'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"
|
chmod 600 "$credential_values"
|
||||||
credential_output=$(
|
credential_output=$(
|
||||||
./scripts/set-env-values.sh "$credential_env" "$credential_values"
|
./scripts/set-env-values.sh "$credential_env" "$credential_values"
|
||||||
|
|
@ -117,6 +118,32 @@ fi
|
||||||
test "$(stat -c '%a' "$credential_env")" = 600
|
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_ID=quality-imported-client' "$credential_env" >/dev/null
|
||||||
grep -Fx 'GOOGLE_OAUTH_CLIENT_SECRET=quality-imported-secret' "$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"
|
duplicate_env="$scan_dir/credentials-duplicate.env"
|
||||||
cp "$credential_env" "$duplicate_env"
|
cp "$credential_env" "$duplicate_env"
|
||||||
|
|
@ -212,6 +239,41 @@ if ./scripts/import-firebase-android-config.sh \
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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"
|
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_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"
|
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"' \
|
'.project_id == "quality-development" and .private_key == "quality-private-key"' \
|
||||||
>/dev/null
|
>/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"
|
echo "Checking the existing load environment upgrade path"
|
||||||
legacy_load_env="$scan_dir/legacy-load.env"
|
legacy_load_env="$scan_dir/legacy-load.env"
|
||||||
printf '%s\n' \
|
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"}' |
|
'{"type":"service_account","project_id":"quality-production","client_email":"quality-fcm@quality-production.iam.gserviceaccount.com","private_key":"quality-private-key"}' |
|
||||||
base64 -w 0
|
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"
|
test_env="$scan_dir/test.env"
|
||||||
if ./scripts/init-test-env.sh test.help.test \
|
if ./scripts/init-test-env.sh test.help.test \
|
||||||
"$scan_dir/test.missing-codex.env" >/dev/null 2>&1; then
|
"$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
|
fi
|
||||||
TEST_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
TEST_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
||||||
TEST_GOOGLE_OAUTH_CLIENT_ID=quality-test-client \
|
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_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 \
|
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
|
./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 'SMTP_RELAY=mailpit' "$test_env" >/dev/null
|
||||||
grep -Fx 'EMAIL_FROM_NAME="Who Need Help Test"' "$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_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_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
|
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
|
./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
|
echo "Test environment initializer overwrote an existing file." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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"
|
production_env="$scan_dir/production.env"
|
||||||
missing_codex_env="$scan_dir/production.missing-codex.env"
|
missing_codex_env="$scan_dir/production.missing-codex.env"
|
||||||
if PRODUCTION_TRAEFIK_TRUSTED_IPS=172.20.0.1/32 \
|
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_RELAY=smtp.help.test \
|
||||||
PRODUCTION_SMTP_PORT=587 \
|
PRODUCTION_SMTP_PORT=587 \
|
||||||
PRODUCTION_SMTP_USERNAME=quality-user \
|
PRODUCTION_SMTP_USERNAME=quality-user \
|
||||||
PRODUCTION_SMTP_PASSWORD=quality-password \
|
PRODUCTION_SMTP_PASSWORD="quality\$password" \
|
||||||
PRODUCTION_SMTP_AUTH=always \
|
PRODUCTION_SMTP_AUTH=always \
|
||||||
PRODUCTION_SMTP_TLS=always \
|
PRODUCTION_SMTP_TLS=always \
|
||||||
PRODUCTION_SMTP_SSL=false \
|
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 \
|
PRODUCTION_SUPPORT_INBOX_ADDRESS=support@help.test \
|
||||||
./scripts/init-production-env.sh help.test "$production_env" >/dev/null
|
./scripts/init-production-env.sh help.test "$production_env" >/dev/null
|
||||||
test "$(stat -c '%a' "$production_env")" = 600
|
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/validate-production-env.sh "$production_env" help.test >/dev/null
|
||||||
./scripts/check-environment-readiness.sh "$production_env" --require-release >/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_env="$scan_dir/production.invalid-fcm.env"
|
||||||
invalid_fcm_base64=$(
|
invalid_fcm_base64=$(
|
||||||
printf '%s' '{"type":"service_account","project_id":"quality-production"}' |
|
printf '%s' '{"type":"service_account","project_id":"quality-production"}' |
|
||||||
|
|
@ -984,6 +1211,105 @@ done
|
||||||
echo "Building the pinned quality image and cached Dialyzer PLTs"
|
echo "Building the pinned quality image and cached Dialyzer PLTs"
|
||||||
docker build --target quality --tag "$quality_image" .
|
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"
|
echo "Running Elixir format, compiler, xref, Credo, Sobelow, Dialyzer, and Hex audit"
|
||||||
docker run --rm "$quality_image" sh -euc '
|
docker run --rm "$quality_image" sh -euc '
|
||||||
mix format --check-formatted
|
mix format --check-formatted
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,9 @@ if ! awk '
|
||||||
exit 40
|
exit 40
|
||||||
}
|
}
|
||||||
|
|
||||||
replacement[key] = substr($0, index($0, "=") + 1)
|
value = substr($0, index($0, "=") + 1)
|
||||||
|
gsub(/\$/, "$$", value)
|
||||||
|
replacement[key] = value
|
||||||
replacement_count++
|
replacement_count++
|
||||||
next
|
next
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user