diff --git a/.github/workflows/ios-appstore-gm.yml b/.github/workflows/ios-appstore-gm.yml index 0c3d20e7..54aee916 100644 --- a/.github/workflows/ios-appstore-gm.yml +++ b/.github/workflows/ios-appstore-gm.yml @@ -84,10 +84,15 @@ jobs: --projectId="$INFISICAL_PROJECT_ID" --env=prod --path=/ 2>/dev/null || true )" [ -n "$val" ] || continue - echo "::add-mask::$val" + while IFS= read -r line; do + if [ -n "$line" ]; then + line="${line//%/%25}"; line="${line//$'\r'/%0D}" + printf '::add-mask::%s\n' "$line" + fi + done <<< "$val" { echo "${name}<> "$GITHUB_ENV" done - unset TOKEN CLIENT_ID CLIENT_SECRET val + unset TOKEN CLIENT_ID CLIENT_SECRET val line - name: Import signing + ASC key run: bash "$GITHUB_WORKSPACE/scripts/ios-appstore-gm-prepare.sh" diff --git a/.github/workflows/ios-ship.yml b/.github/workflows/ios-ship.yml index 03947af8..88e2357b 100644 --- a/.github/workflows/ios-ship.yml +++ b/.github/workflows/ios-ship.yml @@ -299,18 +299,28 @@ jobs: echo "::error::Missing ASC key material in Infisical prod." exit 1 fi + # Mask every line of the raw PEM before staging it. One add-mask + # carrying a multiline value only masks its first line. + while IFS= read -r line; do + if [ -n "$line" ]; then + line="${line//%/%25}"; line="${line//$'\r'/%0D}" + printf '::add-mask::%s\n' "$line" + fi + done <<< "$key_material" # One line, so GITHUB_ENV carries it natively, and the raw PEM never # reaches a step environment. The same form the client reads first. + # Mask the base64 form before it reaches GITHUB_ENV: GitHub prints + # every later step's env block in the public log. + key_b64="$(printf '%s' "$key_material" | base64 | tr -d '\n')" + echo "::add-mask::${key_b64}" printf 'APPLE_API_KEY_ID=%s\n' "$ASC_KEY_ID" >> "$GITHUB_ENV" printf 'APPLE_API_ISSUER_ID=%s\n' "$ASC_ISSUER_ID" >> "$GITHUB_ENV" - printf 'APPLE_API_KEY_P8_BASE64=%s\n' \ - "$(printf '%s' "$key_material" | base64 | tr -d '\n')" \ - >> "$GITHUB_ENV" + printf 'APPLE_API_KEY_P8_BASE64=%s\n' "$key_b64" >> "$GITHUB_ENV" printf '%s\n' "$key_material" | bash "$GITHUB_WORKSPACE/scripts/ios-stage-asc-key.sh" ASC_KEY_PATH="$(sed -n 's/^ASC_KEY_PATH=//p' "$GITHUB_ENV" | tail -n 1)" export ASC_KEY_PATH bash "$GITHUB_WORKSPACE/scripts/ios-appstore-gm-prepare.sh" - unset key_material TOKEN CLIENT_ID CLIENT_SECRET val IOS_DIST_P12_BASE64 IOS_DIST_P12_PASSWORD + unset key_material key_b64 line TOKEN CLIENT_ID CLIENT_SECRET val IOS_DIST_P12_BASE64 IOS_DIST_P12_PASSWORD # Plan-build: compute each app's release build ONCE here and hand it to # the ship steps as --build, so the two ships of one release can never diff --git a/scripts/test-ios-stage-asc-key.sh b/scripts/test-ios-stage-asc-key.sh index b95eb79c..046c9f88 100755 --- a/scripts/test-ios-stage-asc-key.sh +++ b/scripts/test-ios-stage-asc-key.sh @@ -46,6 +46,16 @@ if ! grep -Fq 'scripts/ios-stage-asc-key.sh' "$workflow"; then exit 1 fi +# The base64 form must be masked before it is written to GITHUB_ENV. +# GitHub prints later steps' env blocks, so an unmasked write puts the +# base64 private key in the public log. +mask_line="$(grep -n -F '::add-mask::${key_b64}' "$workflow" | head -n 1 | cut -d: -f1 || true)" +env_line="$(grep -n -F 'APPLE_API_KEY_P8_BASE64=%s' "$workflow" | head -n 1 | cut -d: -f1 || true)" +if [[ -z "$mask_line" || -z "$env_line" || "$mask_line" -ge "$env_line" ]]; then + echo 'base64 ASC key is not masked before it reaches GITHUB_ENV' >&2 + exit 1 +fi + # A second handoff cannot overwrite the first key, including through aliases. bash "$repo_root/scripts/ios-stage-asc-key.sh" < "$tmp_dir/fixture" > "$tmp_dir/stdout" 2> "$tmp_dir/stderr" second_path="$(tail -n 1 "$GITHUB_ENV" | sed 's/^ASC_KEY_PATH=//')" @@ -105,10 +115,25 @@ env -u ASC_KEY_P8 PATH="$tmp_dir/bin:$PATH" \ INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET=synthetic-secret \ ASC_TEST_FIXTURE="$tmp_dir/fixture" ASC_TEST_IMPORT_MARKER="$tmp_dir/imported" \ bash "$tmp_dir/workflow-load.sh" > "$tmp_dir/workflow-stdout" 2> "$tmp_dir/workflow-stderr" -if grep -Fq -f "$tmp_dir/fixture" "$tmp_dir/workflow-env" "$tmp_dir/workflow-stdout" "$tmp_dir/workflow-stderr"; then +# Mask carriers are the one place a fixture line may appear: the Actions +# runner consumes `::add-mask::` lines instead of listing them, but the +# local harness captures stdout verbatim. Strip carriers before the leak +# check, then assert each line and the base64 form were in fact carried. +grep -v '^::add-mask::' "$tmp_dir/workflow-stdout" > "$tmp_dir/workflow-stdout-unmasked" || true +if grep -Fq -f "$tmp_dir/fixture" "$tmp_dir/workflow-env" "$tmp_dir/workflow-stdout-unmasked" "$tmp_dir/workflow-stderr"; then echo 'workflow leaked multiline key material' >&2 exit 1 fi +while IFS= read -r fixture_line; do + grep -Fqx "::add-mask::$fixture_line" "$tmp_dir/workflow-stdout" +done < "$tmp_dir/fixture" +workflow_b64="$(sed -n 's/^APPLE_API_KEY_P8_BASE64=//p' "$tmp_dir/workflow-env")" +[[ -n "$workflow_b64" ]] +grep -Fqx "::add-mask::$workflow_b64" "$tmp_dir/workflow-stdout" +if grep -Fq "$workflow_b64" "$tmp_dir/workflow-stdout-unmasked" "$tmp_dir/workflow-stderr"; then + echo 'workflow printed the base64 key outside a mask carrier' >&2 + exit 1 +fi [[ "$(grep -c '^ASC_KEY_PATH=' "$tmp_dir/workflow-env")" == 1 ]] workflow_key="$(sed -n 's/^ASC_KEY_PATH=//p' "$tmp_dir/workflow-env")" cmp "$tmp_dir/fixture" "$workflow_key"