games-development-ai/deploy/tests/test-staging-rc.sh
lili bcb7fec906
Some checks failed
contract-gates / contract-gates (push) Has been cancelled
docs-gate / docs-gate (push) Has been cancelled
fix(release): bind dogfood gates to runtime state
2026-07-23 02:49:08 -07:00

989 lines
52 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# staging RC 脚本回归:只用假 SSH 与本地假 HTTP 服务,不连接远端、不启动 systemd。
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
ACTIVATE="$ROOT_DIR/deploy/activate-staging-rc.sh"
PREPARE="$ROOT_DIR/deploy/prepare-staging-rc.sh"
SMOKE="$ROOT_DIR/deploy/smoke-test.sh"
STORAGE_AUDIT="$ROOT_DIR/deploy/generate-staging-storage-audit.sh"
RESTORE_VERIFY="$ROOT_DIR/deploy/verify-mysql-dump-restore.sh"
GENERATION_GATE_REF="sha256:$(printf '0%.0s' $(seq 1 64)):r1/$(printf '0%.0s' $(seq 1 64)).attestation"
GENERATION_GATE_RUN_ID="r1-test-run-20260723"
GENERATION_ATTESTATION_EXPECTED_ISSUER="attacker-controlled-issuer"
GENERATION_ATTESTATION_TRUSTED_PUBLIC_KEY_FILE="/tmp/attacker-controlled-public-key.pem"
FIXED_GENERATION_ATTESTATION_ISSUER="r1-mini-infra"
FIXED_GENERATION_ATTESTATION_PUBLIC_KEY_SHA256="e3cc17cd86805b053f61450b5c2c6e4e168bb01dbf1e83840124290d1196a304"
REPOSITORY_GENERATION_ATTESTATION_PUBLIC_KEY="$ROOT_DIR/deploy/trust/r1-attestation-ed25519.pub"
TMP_DIR="$(mktemp -d)"
SERVER_PID=""
export GENERATION_GATE_RUN_ID GENERATION_ATTESTATION_EXPECTED_ISSUER
export GENERATION_ATTESTATION_TRUSTED_PUBLIC_KEY_FILE
cleanup() {
if [ -n "$SERVER_PID" ]; then
kill "$SERVER_PID" 2>/dev/null || true
wait "$SERVER_PID" 2>/dev/null || true
fi
rm -rf "$TMP_DIR"
}
trap cleanup EXIT
fail() {
printf 'TEST_FAIL %s\n' "$*" >&2
exit 1
}
# 执行 prepare 内嵌的真实 Surefire XML 校验器,覆盖全跳过拒绝与部分跳过放行边界。
test_surefire_report_guard() {
local surefire_verifier="$TMP_DIR/verify-security-java-reports.py"
local surefire_fixture="$TMP_DIR/surefire-fixture"
local surefire_report_dir="$surefire_fixture/game-cloud/security/target/surefire-reports"
awk '
/^import pathlib,sys,xml.etree.ElementTree as ET$/ { capture=1 }
capture && /^PY$/ { exit }
capture { print }
' "$PREPARE" >"$surefire_verifier"
rg -q 'surefire-reports/TEST-\*\.xml' "$surefire_verifier" || fail "无法提取 prepare 的 Surefire XML 校验器"
mkdir -p "$surefire_report_dir" "$surefire_fixture/evidence"
cat >"$surefire_report_dir/TEST-com.example.AllSkippedSecurityTest.xml" <<'XML'
<?xml version="1.0" encoding="UTF-8"?>
<testsuite name="com.example.AllSkippedSecurityTest" tests="1" failures="0" errors="0" skipped="1"/>
XML
if (cd "$surefire_fixture" && python3 "$surefire_verifier" \
"$surefire_fixture/evidence/security-java-tests-summary.tsv" AllSkippedSecurityTest) \
>"$TMP_DIR/surefire-all-skipped.log" 2>&1; then
fail "Surefire 校验器错误放行 tests=1、skipped=1 的全跳过测试类"
fi
rg -q 'executed=0' "$TMP_DIR/surefire-all-skipped.log" || fail "全跳过拒绝原因未记录实际执行数"
# 边界正例:同类存在一个 skipped,但仍至少执行一个测试时必须允许并产出摘要。
cat >"$surefire_report_dir/TEST-com.example.AllSkippedSecurityTest.xml" <<'XML'
<?xml version="1.0" encoding="UTF-8"?>
<testsuite name="com.example.AllSkippedSecurityTest" tests="2" failures="0" errors="0" skipped="1"/>
XML
(cd "$surefire_fixture" && python3 "$surefire_verifier" \
"$surefire_fixture/evidence/security-java-tests-summary.tsv" AllSkippedSecurityTest)
rg -q $'^AllSkippedSecurityTest\\t2\\t0\\t0\\t1$' \
"$surefire_fixture/evidence/security-java-tests-summary.tsv" || fail "部分跳过但有执行的测试类未生成正确摘要"
}
if [ "${STAGING_RC_TEST_CASE:-}" = "surefire-report-guard" ]; then
test_surefire_report_guard
printf 'TEST_PASS Surefire XML 全跳过拒绝与部分跳过边界通过\n'
exit 0
fi
bash -n "$ACTIVATE" "$PREPARE" "$SMOKE" "$STORAGE_AUDIT" "$RESTORE_VERIFY"
rg -q 'docker exec -i .*MYSQL_CONTAINER' "$STORAGE_AUDIT" || fail "存储审计未把 SQL stdin 接入 MySQL 容器"
# 假 SSH 只捕获参数和 stdin 中的远端脚本;不执行任何远端命令。
cat >"$TMP_DIR/fake-ssh" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
printf '%s\n' "$@" >"$RC_TEST_TMP/ssh-args.txt"
cat >"$RC_TEST_TMP/remote-script.sh"
SH
chmod +x "$TMP_DIR/fake-ssh"
export RC_TEST_TMP="$TMP_DIR"
RC_COMMIT="0000000000000000000000000000000000000000" \
GENERATION_GATE_EVIDENCE_REF="$GENERATION_GATE_REF" \
BOOTSTRAP_ADMIN_TOKEN_FILE=/root/game-staging/credentials/bootstrap-admin.token \
SSH_BIN="$TMP_DIR/fake-ssh" \
bash "$ACTIVATE" >"$TMP_DIR/activate-wrapper.log"
rg -q '^ProxyCommand=none$' "$TMP_DIR/ssh-args.txt" || fail "SSH 未显式禁用代理"
rg -q '^root@100\.64\.0\.7$' "$TMP_DIR/ssh-args.txt" || fail "SSH 默认目标不是 mini-desktop 直连地址"
bash -n "$TMP_DIR/remote-script.sh"
rg -q 'pre-migration-ruoyi-vue-pro\.sql\.gz' "$TMP_DIR/remote-script.sh" || fail "缺迁移前数据库快照"
rg -q 'generate-staging-storage-audit\.sh' "$TMP_DIR/remote-script.sh" || fail "缺实时存储审计"
rg -q 'verify-mysql-dump-restore\.sh' "$TMP_DIR/remote-script.sh" || fail "缺隔离恢复验证"
rg -q 'failure_handler' "$TMP_DIR/remote-script.sh" || fail "缺失败回滚处理"
rg -q 'start_stack.*48080 4173 4174 8300 9501 true' "$TMP_DIR/remote-script.sh" || fail "缺正式五端口启动"
rg -q '^install_maintenance_gate()' "$TMP_DIR/remote-script.sh" || fail "缺正式端口维护门"
rg -q '^release_maintenance_gate()' "$TMP_DIR/remote-script.sh" || fail "缺维护门解除函数"
rg -q '^verify_stack()' "$TMP_DIR/remote-script.sh" || fail "缺五服务逐项复核函数"
rg -Fq 'INPUT 1 ! -i lo -j "$maintenance_chain"' "$TMP_DIR/remote-script.sh" || \
fail "维护门没有在 INPUT 首位封锁全部非 loopback 入口"
rg -q 'maintenance_ports="48080,4173,4174,8300,9501"' "$TMP_DIR/remote-script.sh" || \
fail "维护门未覆盖全部五个正式端口"
rg -q 'mv -Tf.*active_pointer' "$TMP_DIR/remote-script.sh" || fail "active 指针不是原子替换"
rg -Fq 'spring.cloud.nacos.discovery.enabled=${discovery_enabled}' "$TMP_DIR/remote-script.sh" || \
fail "backend launcher 没有显式控制 Nacos discovery"
rg -q 'start_candidate_probe.*14173 14174 18300 19501' "$TMP_DIR/remote-script.sh" || \
fail "候选非数据面探针未启动"
rg -q 'verify_generation_gate_evidence' "$TMP_DIR/remote-script.sh" || fail "缺生成 R1 证据校验"
! rg -q 'git .*cat-file.*expected_commit' "$TMP_DIR/remote-script.sh" || fail "R1 attestation 仍要求属于被证明 commit,存在自引用"
rg -q 'attestation_root' "$TMP_DIR/remote-script.sh" || fail "缺独立 R1 attestation 根目录"
rg -q 'openssl pkeyutl -verify' "$TMP_DIR/remote-script.sh" || fail "R1 attestation 缺 detached signature 校验"
rg -q 'expected_generation_gate_run_id' "$TMP_DIR/remote-script.sh" || fail "R1 attestation 未绑定独立 run-id"
rg -q 'expected_attestation_issuer' "$TMP_DIR/remote-script.sh" || fail "R1 attestation 未绑定 issuer"
rg -q 'trusted_attestation_public_key_file' "$TMP_DIR/remote-script.sh" || fail "R1 attestation 未绑定信任公钥"
rg -Fq 'expected_attestation_issuer="r1-mini-infra"' "$TMP_DIR/remote-script.sh" || fail "R1 issuer 未固定"
rg -Fq 'trusted_attestation_public_key_file="/root/game-staging/trust/r1-attestation-ed25519.pub"' \
"$TMP_DIR/remote-script.sh" || fail "R1 公钥路径未固定"
rg -Fq "trusted_attestation_public_key_sha256=\"$FIXED_GENERATION_ATTESTATION_PUBLIC_KEY_SHA256\"" \
"$TMP_DIR/remote-script.sh" || fail "R1 公钥摘要未固定"
! rg -q 'attacker-controlled-(issuer|public-key)' "$TMP_DIR/ssh-args.txt" || fail "R1 信任根仍由调用参数传入"
rg -q 'flock -n' "$TMP_DIR/remote-script.sh" || fail "activation 缺单实例部署锁"
rg -q 'candidate_backend=SKIPPED_NO_ISOLATED_DATA_PLANE' "$TMP_DIR/remote-script.sh" || \
fail "候选探活没有明确跳过共享数据面 backend"
rg -q 'run_flyway_migrations' "$TMP_DIR/remote-script.sh" || fail "迁移未拆成独立受控步骤"
rg -q 'restore_database_snapshot' "$TMP_DIR/remote-script.sh" || fail "迁移后失败缺数据库快照补偿"
rg -q 'FIRST_RC_FULL_ROLLBACK=UNAVAILABLE' "$TMP_DIR/remote-script.sh" || fail "首个 RC 未明确禁止宣称完整回滚"
rg -q 'manage-dogfood-reviewers\.py' "$TMP_DIR/remote-script.sh" || fail "激活未调用审核账号轮换工具"
rg -q 'provision-dogfood-with-nacos-transaction\.sh' "$TMP_DIR/remote-script.sh" || \
fail "激活未在 admin 禁用前执行账号预置事务"
rg -q 'ADMIN_TOKEN_FILE="\$bootstrap_admin_token_file"' "$TMP_DIR/remote-script.sh" || \
fail "激活账号预置未使用 bootstrap token 文件"
rg -q 'PROVISION_PASS accounts=40 creators=10 role_mismatches=0 claimed_accounts=40 duplicate_claims=0' \
"$TMP_DIR/remote-script.sh" || fail "激活未核对 40 人账号终验"
rg -q '请先运行受控 new-api import' "$TMP_DIR/remote-script.sh" || \
fail "额度池不足没有给出受控 new-api import 指引"
rg -q -- '--ack ROTATE_DOGFOOD_REVIEWERS' "$TMP_DIR/remote-script.sh" || fail "审核账号轮换 ACK 不精确"
rg -q 'reviewer-runtime-verification\.json' "$TMP_DIR/remote-script.sh" || fail "缺 reviewer/readonly 登录与旧 token 失效运行态证据"
rg -Fq 'if status != 401 or body.get("code") != 401:' "$TMP_DIR/remote-script.sh" || \
fail "bootstrap token 失效验收未严格要求 HTTP 401 且业务 code=401,非 401 可能被误判为失效"
rg -q 'rm -f.*bootstrap_admin_token_file' "$TMP_DIR/remote-script.sh" || fail "轮换成功后未删除 bootstrap token 文件"
! rg -q 'admin123' "$ACTIVATE" || fail "激活脚本写入了默认公开密码"
for required_marker in actor_prompt_eval judge_prompt_eval game_content_gold live_prompt full_gate fresh_25 historical_11 production_shadow_20 runtime_eligible; do
rg -q "$required_marker" "$TMP_DIR/remote-script.sh" || fail "生成门未核对 R1 字段:$required_marker"
done
gate_line="$(rg -n 'verify_generation_gate_evidence' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
snapshot_line="$(rg -n '迁移前数据库快照' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
[ "$gate_line" -lt "$snapshot_line" ] || fail "生成门校验没有发生在数据库快照/迁移前"
candidate_probe_line="$(rg -n '启动不接共享数据面的候选探针' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
maintenance_install_line="$(rg -n '^install_maintenance_gate$' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
stop_old_line="$(rg -n '^stop_stack .*48080 4173 4174 8300 9501$' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
restore_verify_line="$(rg -n '在隔离临时 MySQL 容器中验证快照可恢复' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
migration_line="$(rg -n '^run_flyway_migrations$' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
[ "$candidate_probe_line" -lt "$stop_old_line" ] || fail "候选非数据面探针没有在停止旧栈前完成"
[ "$maintenance_install_line" -lt "$stop_old_line" ] || fail "维护门没有在严格停栈前安装"
[ "$stop_old_line" -lt "$snapshot_line" ] || fail "迁移快照前旧栈仍可能写入,失败补偿会丢数据"
[ "$snapshot_line" -lt "$restore_verify_line" ] || fail "隔离恢复验证没有使用停写后的最终快照"
[ "$restore_verify_line" -lt "$migration_line" ] || fail "未先完成隔离恢复验证就执行正式迁移"
reviewer_line="$(rg -n 'manage-dogfood-reviewers\.py' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
provision_line="$(rg -n 'provision-dogfood-with-nacos-transaction\.sh' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
provision_pass_line="$(rg -n 'PROVISION_PASS accounts=40 creators=10 role_mismatches=0 claimed_accounts=40 duplicate_claims=0' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
runtime_verify_line="$(rg -n '狗粮审核账号轮换与运行态验证 PASS' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
stable_smoke_line="$(rg -n 'stable-smoke\.log' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
active_switch_line="$(rg -n '正式栈全绿,原子切换 active 指针' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
stable_start_line="$(rg -n '^start_stack .*48080 4173 4174 8300 9501 true$' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
maintenance_release_line="$(rg -n '^release_maintenance_gate$' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
[ "$maintenance_install_line" -lt "$stable_start_line" ] || fail "正式栈启动前未封锁外部入口"
[ "$maintenance_install_line" -lt "$snapshot_line" ] || fail "停写快照前未封锁外部入口"
[ "$provision_line" -lt "$provision_pass_line" ] || fail "账号预置后未核对终验"
[ "$provision_pass_line" -lt "$reviewer_line" ] || fail "admin 禁用发生在账号预置恢复前"
[ "$reviewer_line" -lt "$runtime_verify_line" ] || fail "审核账号轮换后未做运行态验证"
[ "$runtime_verify_line" -lt "$stable_smoke_line" ] || fail "审核账号运行态验证没有位于写 smoke 前"
[ "$stable_smoke_line" -lt "$active_switch_line" ] || fail "active 指针在 smoke 前切换"
[ "$active_switch_line" -lt "$maintenance_release_line" ] || fail "事务提交前提前解除外部入口封锁"
token_delete_line="$(rg -n '^delete_bootstrap_token$' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
[ "$active_switch_line" -lt "$token_delete_line" ] || fail "bootstrap token 未在 smoke+active 后最后删除"
failure_handler_body="$(sed -n '/^failure_handler()/,/^}/p' "$TMP_DIR/remote-script.sh")"
! printf '%s\n' "$failure_handler_body" | rg -q '^ delete_bootstrap_token' || \
fail "激活失败路径删除了补偿后重试所需的 bootstrap token"
printf '%s\n' "$failure_handler_body" | rg -q 'cleanup_dogfood_provision_runtime' || \
fail "激活失败补偿未定向清理本次限流桶与 OAuth token 缓存"
printf '%s\n' "$failure_handler_body" | rg -q 'verify_stack.*rollback' || \
fail "回滚后未逐项复核五服务"
printf '%s\n' "$failure_handler_body" | rg -q 'release_maintenance_gate' || \
fail "旧栈完整恢复后未解除维护门"
rg -q '^verify_active_pointer()' "$TMP_DIR/remote-script.sh" || \
fail "active 指针恢复后缺 realpath/commit 复核函数"
pointer_restore_line="$(printf '%s\n' "$failure_handler_body" | rg -n 'restore_active_pointer.*previous' | cut -d: -f1)"
pointer_verify_line="$(printf '%s\n' "$failure_handler_body" | rg -n 'verify_active_pointer.*previous' | cut -d: -f1)"
rollback_release_line="$(printf '%s\n' "$failure_handler_body" | rg -n 'release_maintenance_gate' | cut -d: -f1)"
[ -n "$pointer_restore_line" ] && [ -n "$pointer_verify_line" ] && [ -n "$rollback_release_line" ] \
&& [ "$pointer_restore_line" -lt "$pointer_verify_line" ] \
&& [ "$pointer_verify_line" -lt "$rollback_release_line" ] || \
fail "失败回滚未按 active 恢复→realpath/commit 复核→最后解除维护门排序"
printf '%s\n' "$failure_handler_body" | rg -q 'rollback_rc=' || \
fail "失败回滚证据未记录总 rollback_rc"
! printf '%s\n' "$failure_handler_body" | rg -q 'exit 72' || \
fail "运行态 cleanup 失败仍直接 exit 72,跳过最终证据落盘"
cleanup_rc_line="$(printf '%s\n' "$failure_handler_body" | rg -n 'rc=72' | cut -d: -f1)"
failure_status_line="$(printf '%s\n' "$failure_handler_body" | rg -n "printf 'failed commit=" | cut -d: -f1)"
failure_chmod_line="$(printf '%s\n' "$failure_handler_body" | rg -n 'chmod -R go-rwx' | cut -d: -f1)"
failure_exit_line="$(printf '%s\n' "$failure_handler_body" | rg -n 'exit "\$rc"' | cut -d: -f1)"
[ "$cleanup_rc_line" -lt "$failure_status_line" ] \
&& [ "$failure_status_line" -lt "$failure_chmod_line" ] \
&& [ "$failure_chmod_line" -lt "$failure_exit_line" ] || \
fail "cleanup rc=72 后未按 RC_STATUS→chmod→统一 exit 顺序完整落证"
cleanup_function_body="$(sed -n '/^cleanup_dogfood_provision_runtime()/,/^}/p' "$TMP_DIR/remote-script.sh")"
printf '%s\n' "$cleanup_function_body" | rg -q -- '--cleanup' || fail "激活补偿未进入定向清理模式"
printf '%s\n' "$cleanup_function_body" | rg -q 'account_provision_cleanup_manifest.*account_provision_run_id' || \
fail "激活补偿未用本次 run-id 约束清理清单"
start_stack_body="$(sed -n '/^start_stack()/,/^}/p' "$TMP_DIR/remote-script.sh")"
if printf '%s\n' "$start_stack_body" \
| rg '^ (run_unit|wait_port|wait_json_health|wait_url) ' \
| rg -v '\|\| return 1$' >/dev/null; then
fail "start_stack 存在依赖 errexit 的启动或探活步骤"
fi
verify_stack_body="$(sed -n '/^verify_stack()/,/^}/p' "$TMP_DIR/remote-script.sh")"
[ "$(printf '%s\n' "$verify_stack_body" | rg -c 'systemctl is-active --quiet')" -eq 5 ] || \
fail "回滚复核没有逐项检查五个 systemd 服务"
# 用有状态假 iptables 执行维护门真实函数,验证链清理失败时独立护栏仍封锁入口。
cat >"$TMP_DIR/fake-iptables" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
state="${RC_TEST_TMP:?}/iptables-state"
mkdir -p "$state"
printf '%s\n' "$*" >>"$state/calls.log"
case "$*" in
'-w 5 -nL GDA_STAGING_MAINT') [ -f "$state/chain" ] ;;
'-w 5 -N GDA_STAGING_MAINT') touch "$state/chain" ;;
'-w 5 -C GDA_STAGING_MAINT -p tcp -m multiport --dports 48080,4173,4174,8300,9501 -j REJECT')
[ -f "$state/reject-rule" ] ;;
'-w 5 -A GDA_STAGING_MAINT -p tcp -m multiport --dports 48080,4173,4174,8300,9501 -j REJECT')
touch "$state/reject-rule" ;;
'-w 5 -C INPUT ! -i lo -j GDA_STAGING_MAINT') [ -f "$state/input-jump" ] ;;
'-w 5 -I INPUT 1 ! -i lo -j GDA_STAGING_MAINT') touch "$state/input-jump" ;;
'-w 5 -D INPUT ! -i lo -j GDA_STAGING_MAINT') rm -f "$state/input-jump" ;;
'-w 5 -nL GDA_STAGING_RELEASE_GUARD') [ -f "$state/release-guard-chain" ] ;;
'-w 5 -N GDA_STAGING_RELEASE_GUARD') touch "$state/release-guard-chain" ;;
'-w 5 -C GDA_STAGING_RELEASE_GUARD -p tcp -m multiport --dports 48080,4173,4174,8300,9501 -j REJECT')
[ -f "$state/release-guard-reject" ] ;;
'-w 5 -A GDA_STAGING_RELEASE_GUARD -p tcp -m multiport --dports 48080,4173,4174,8300,9501 -j REJECT')
touch "$state/release-guard-reject" ;;
'-w 5 -C INPUT ! -i lo -j GDA_STAGING_RELEASE_GUARD') [ -f "$state/release-guard-jump" ] ;;
'-w 5 -I INPUT 1 ! -i lo -j GDA_STAGING_RELEASE_GUARD') touch "$state/release-guard-jump" ;;
'-w 5 -D INPUT ! -i lo -j GDA_STAGING_RELEASE_GUARD') rm -f "$state/release-guard-jump" ;;
'-w 5 -F GDA_STAGING_MAINT')
[ ! -f "$state/fail-F" ] || exit 91
rm -f "$state/reject-rule" ;;
'-w 5 -X GDA_STAGING_MAINT')
[ ! -f "$state/fail-X" ] || exit 92
rm -f "$state/chain" ;;
*) printf '未知 fake iptables 调用:%s\n' "$*" >&2; exit 96 ;;
esac
SH
chmod +x "$TMP_DIR/fake-iptables"
{
printf '%s\n' 'set -euo pipefail'
printf 'IPTABLES_BIN=%q\n' "$TMP_DIR/fake-iptables"
printf 'evidence=%q\n' "$TMP_DIR/maintenance-evidence"
printf '%s\n' 'maintenance_chain=GDA_STAGING_MAINT' \
'maintenance_release_guard_chain=GDA_STAGING_RELEASE_GUARD' \
'maintenance_ports=48080,4173,4174,8300,9501' 'maintenance_gate_active=0'
printf '%s\n' 'log_step() { :; }'
sed -n '/^maintenance_rule_present()/,/^}/p' "$TMP_DIR/remote-script.sh"
sed -n '/^maintenance_release_guard_present()/,/^}/p' "$TMP_DIR/remote-script.sh"
sed -n '/^install_maintenance_gate()/,/^}/p' "$TMP_DIR/remote-script.sh"
sed -n '/^release_maintenance_gate()/,/^}/p' "$TMP_DIR/remote-script.sh"
printf '%s\n' 'mkdir -p "$evidence"' 'install_maintenance_gate' \
'[ "$maintenance_gate_active" -eq 1 ]' 'maintenance_rule_present' \
'release_maintenance_gate' '[ "$maintenance_gate_active" -eq 0 ]'
} >"$TMP_DIR/maintenance-harness.sh"
bash "$TMP_DIR/maintenance-harness.sh"
[ ! -e "$TMP_DIR/iptables-state/chain" ] \
&& [ ! -e "$TMP_DIR/iptables-state/reject-rule" ] \
&& [ ! -e "$TMP_DIR/iptables-state/input-jump" ] \
&& [ ! -e "$TMP_DIR/iptables-state/release-guard-jump" ] \
&& [ -e "$TMP_DIR/iptables-state/release-guard-chain" ] \
&& [ -e "$TMP_DIR/iptables-state/release-guard-reject" ] || fail "维护门解除后的 guard 状态非法"
rg -q '^maintenance_gate=OPENING ' "$TMP_DIR/maintenance-evidence/maintenance-gate.status" || \
fail "维护门未留下最终开放提交前证据"
tail -1 "$TMP_DIR/iptables-state/calls.log" | rg -Fq \
-- '-D INPUT ! -i lo -j GDA_STAGING_RELEASE_GUARD' || \
fail "最终开放不是最后一条 iptables 命令"
for failed_operation in F X; do
rm -rf "$TMP_DIR/iptables-state" "$TMP_DIR/maintenance-evidence"
{
sed -n '1,/^mkdir -p "\$evidence"/p' "$TMP_DIR/maintenance-harness.sh"
printf '%s\n' 'install_maintenance_gate'
printf 'touch %q\n' "$TMP_DIR/iptables-state/fail-$failed_operation"
printf '%s\n' 'set +e' 'release_maintenance_gate' 'release_rc=$?' 'set -e' \
'[ "$release_rc" -ne 0 ]' '[ "$maintenance_gate_active" -eq 1 ]' \
'maintenance_release_guard_present'
} >"$TMP_DIR/maintenance-fail-$failed_operation.sh"
bash "$TMP_DIR/maintenance-fail-$failed_operation.sh" || fail "-$failed_operation 失败后维护门未保持封锁"
done
# 执行真实 stop_stack:服务停止失败、unit 残留或端口残留均必须阻止快照继续。
stop_stack_body="$(sed -n '/^stop_stack()/,/^}/p' "$TMP_DIR/remote-script.sh")"
{
printf '%s\n' 'set -euo pipefail' 'state="${RC_TEST_TMP:?}/stop-stack-state"' 'mkdir -p "$state"'
printf '%s\n' 'log_step() { :; }' 'unit_name() { printf "%s-%s" "$1" "$2"; }'
cat <<'SH'
systemctl() {
local action="$1"
shift
if [ "${1:-}" = "--quiet" ]; then shift; fi
local unit="${1:-}"
case "$action" in
is-active) [ -f "$state/unit-$unit" ] ;;
stop)
[ "$unit" != "${STOP_FAIL_UNIT:-}" ] || return 93
rm -f "$state/unit-$unit"
;;
*) return 94 ;;
esac
}
port_listening() {
[ -f "$state/port-$1" ]
}
SH
printf '%s\n' "$stop_stack_body"
cat <<'SH'
for service in backend studio admin cheap-agent cheap-worker; do
touch "$state/unit-gda-staging-$service"
done
stop_stack gda-staging 48080 4173 4174 8300 9501
for service in backend studio admin cheap-agent cheap-worker; do
[ ! -e "$state/unit-gda-staging-$service" ]
done
touch "$state/unit-gda-staging-backend"
set +e
STOP_FAIL_UNIT=gda-staging-backend stop_stack gda-staging 48080 4173 4174 8300 9501
stop_rc=$?
set -e
[ "$stop_rc" -ne 0 ]
[ -e "$state/unit-gda-staging-backend" ]
rm -f "$state"/unit-*
touch "$state/port-48080"
set +e
stop_stack gda-staging 48080 4173 4174 8300 9501
port_rc=$?
set -e
[ "$port_rc" -ne 0 ]
SH
} >"$TMP_DIR/stop-stack-harness.sh"
bash "$TMP_DIR/stop-stack-harness.sh" || fail "stop_stack 未严格传播服务或端口残留"
# 在关闭 errexit 的回滚环境中执行真实 start_stack,首个启动失败必须立即返回且不得继续探活。
{
printf '%s\n' 'set -uo pipefail' 'calls="${RC_TEST_TMP:?}/start-stack-calls.log"'
printf '%s\n' 'write_launchers() { printf "/tmp/launchers"; }' \
'unit_name() { printf "%s-%s" "$1" "$2"; }' \
'run_unit() { printf "run_unit %s\n" "$1" >>"$calls"; return 1; }' \
'wait_port() { printf "unexpected wait_port\n" >>"$calls"; return 0; }' \
'wait_json_health() { printf "unexpected wait_json_health\n" >>"$calls"; return 0; }' \
'wait_url() { printf "unexpected wait_url\n" >>"$calls"; return 0; }'
printf '%s\n' "$start_stack_body"
printf '%s\n' 'set +e' \
'start_stack /tmp/release rollback test-prefix 48080 4173 4174 8300 9501 true' \
'rc=$?' 'set -e' '[ "$rc" -eq 1 ]' \
'[ "$(wc -l <"$calls" | tr -d " ")" -eq 1 ]' \
'! rg -q "^unexpected " "$calls"'
} >"$TMP_DIR/start-stack-harness.sh"
bash "$TMP_DIR/start-stack-harness.sh"
# 执行失败回滚状态机:即使新 active 尚未完成标记,也必须恢复并复核旧指针后才开放入口。
{
cat <<'SH'
#!/usr/bin/env bash
set -uo pipefail
case_name="${1:?}"
state="${RC_TEST_TMP:?}/rollback-state-$case_name"
calls="$state/calls.log"
evidence="$state/evidence"
release="$state/release"
mkdir -p "$evidence" "$release"
commit=0000000000000000000000000000000000000000
candidate_prefix=gda-candidate
stable_prefix=gda-staging
switch_started=1
pointer_changed=0
previous_was_active=1
previous_valid=1
previous=/root/game-staging/releases/1111111111111111111111111111111111111111
database_restore_required=1
database_restore_rc=not-run
runtime_cleanup_rc=not-run
bootstrap_token_validated=0
maintenance_gate_active=1
run_id=test-run
log_step() { printf 'log:%s\n' "$*" >>"$calls"; }
capture_journals() { :; }
stop_stack() { printf 'stop:%s\n' "$1" >>"$calls"; }
cleanup_dogfood_provision_runtime() { return 0; }
restore_database_snapshot() { printf 'database\n' >>"$calls"; return 0; }
start_stack() { printf 'start-old-stack\n' >>"$calls"; return 0; }
verify_stack() { printf 'verify-old-stack\n' >>"$calls"; return 0; }
restore_active_pointer() {
printf 'restore-active\n' >>"$calls"
[ "$case_name" != pointer-fail ] || return 88
}
verify_active_pointer() { printf 'verify-active\n' >>"$calls"; return 0; }
release_maintenance_gate() { printf 'release-maintenance\n' >>"$calls"; maintenance_gate_active=0; }
SH
printf '%s\n' "$failure_handler_body"
printf '%s\n' 'false' 'failure_handler'
} >"$TMP_DIR/rollback-state-harness.sh"
chmod +x "$TMP_DIR/rollback-state-harness.sh"
set +e
bash "$TMP_DIR/rollback-state-harness.sh" success
rollback_success_rc=$?
set -e
[ "$rollback_success_rc" -eq 1 ] || fail "完整回滚没有保留原始激活失败码"
success_calls="$TMP_DIR/rollback-state-success/calls.log"
database_line="$(rg -n '^database$' "$success_calls" | cut -d: -f1 || true)"
service_line="$(rg -n '^verify-old-stack$' "$success_calls" | cut -d: -f1 || true)"
restore_pointer_line="$(rg -n '^restore-active$' "$success_calls" | cut -d: -f1 || true)"
verify_pointer_line="$(rg -n '^verify-active$' "$success_calls" | cut -d: -f1 || true)"
release_line="$(rg -n '^release-maintenance$' "$success_calls" | cut -d: -f1 || true)"
[ -n "$database_line" ] && [ -n "$service_line" ] && [ -n "$restore_pointer_line" ] \
&& [ -n "$verify_pointer_line" ] && [ -n "$release_line" ] \
&& [ "$database_line" -lt "$service_line" ] \
&& [ "$service_line" -lt "$restore_pointer_line" ] \
&& [ "$restore_pointer_line" -lt "$verify_pointer_line" ] \
&& [ "$verify_pointer_line" -lt "$release_line" ] || \
fail "完整回滚未按数据库→旧栈→active 恢复复核→维护门释放执行"
rg -q '^rollback_rc=0$' "$TMP_DIR/rollback-state-success/evidence/rollback-result.txt" || \
fail "完整回滚证据未记录 rollback_rc=0"
set +e
bash "$TMP_DIR/rollback-state-harness.sh" pointer-fail
rollback_pointer_fail_rc=$?
set -e
[ "$rollback_pointer_fail_rc" -eq 73 ] || fail "active 指针恢复失败未返回统一回滚失败码 73"
pointer_fail_calls="$TMP_DIR/rollback-state-pointer-fail/calls.log"
! rg -q '^release-maintenance$' "$pointer_fail_calls" || fail "active 指针失败后仍开放正式入口"
rg -q '^rollback_pointer_rc=88$' "$TMP_DIR/rollback-state-pointer-fail/evidence/rollback-result.txt" || \
fail "active 指针失败证据未记录原始非零返回码"
rg -q '^rollback_rc=88$' "$TMP_DIR/rollback-state-pointer-fail/evidence/rollback-result.txt" || \
fail "active 指针失败未计入总 rollback_rc"
# 执行真实 active 指针函数,验证目标 realpath 与 RC 目录名、Git HEAD 三者必须一致。
pointer_functions="$TMP_DIR/active-pointer-functions.sh"
{
printf '%s\n' 'set -euo pipefail'
sed -n '/^restore_active_pointer()/,/^}/p' "$TMP_DIR/remote-script.sh"
sed -n '/^verify_active_pointer()/,/^}/p' "$TMP_DIR/remote-script.sh"
} >"$pointer_functions"
pointer_root="$TMP_DIR/pointer-releases"
pointer_seed="$TMP_DIR/pointer-seed"
mkdir -p "$pointer_root" "$pointer_seed"
git -C "$pointer_seed" init -q
git -C "$pointer_seed" config user.name staging-rc-test
git -C "$pointer_seed" config user.email staging-rc-test@example.invalid
printf 'v1\n' >"$pointer_seed/artifact.txt"
git -C "$pointer_seed" add artifact.txt
git -C "$pointer_seed" commit -q -m v1
pointer_commit="$(git -C "$pointer_seed" rev-parse HEAD)"
pointer_release="$pointer_root/$pointer_commit"
mv "$pointer_seed" "$pointer_release"
{
printf 'root=%q\n' "$pointer_root"
printf 'active_pointer=%q\n' "$pointer_root/active"
printf '%s\n' 'run_id=pointer-test' 'log_step() { :; }' \
'mv() { if [ "${1:-}" = -Tf ]; then command mv -f "$2" "$3"; else command mv "$@"; fi; }'
printf 'source %q\n' "$pointer_functions"
printf 'restore_active_pointer %q\n' "$pointer_release"
printf 'verify_active_pointer %q\n' "$pointer_release"
} >"$TMP_DIR/active-pointer-pass.sh"
bash "$TMP_DIR/active-pointer-pass.sh" || fail "active 指针 realpath/commit 正例失败"
[ "$(readlink -f "$pointer_root/active")" = "$(realpath "$pointer_release")" ] || \
fail "active 指针未恢复到旧 RC 的 canonical 路径"
printf 'v2\n' >>"$pointer_release/artifact.txt"
git -C "$pointer_release" add artifact.txt
git -C "$pointer_release" commit -q -m v2
if bash -c "root=$(printf '%q' "$pointer_root"); active_pointer=$(printf '%q' "$pointer_root/active"); \
run_id=pointer-test; log_step() { :; }; source $(printf '%q' "$pointer_functions"); \
verify_active_pointer $(printf '%q' "$pointer_release")"; then
fail "active RC 的 Git HEAD 漂移后仍通过 commit 复核"
fi
if rg -q '/root/games-development-ai' "$TMP_DIR/remote-script.sh"; then
fail "激活脚本引用了远端脏旧仓"
fi
# 生成门引用是激活必填项,且必须在 SSH 前拒绝缺失或不安全路径。
: >"$TMP_DIR/ssh-called"
cat >"$TMP_DIR/reject-ssh" <<'SH'
#!/usr/bin/env bash
printf 'called\n' >>"$RC_TEST_TMP/ssh-called"
exit 99
SH
chmod +x "$TMP_DIR/reject-ssh"
if RC_COMMIT="0000000000000000000000000000000000000000" SSH_BIN="$TMP_DIR/reject-ssh" \
bash "$ACTIVATE" >"$TMP_DIR/missing-generation-gate.log" 2>&1; then
fail "缺生成门引用仍允许激活"
fi
[ ! -s "$TMP_DIR/ssh-called" ] || fail "缺生成门引用后仍发生 SSH"
if RC_COMMIT="0000000000000000000000000000000000000000" \
GENERATION_GATE_EVIDENCE_REF="sha256:$(printf '1%.0s' $(seq 1 64)):../gate.txt" \
SSH_BIN="$TMP_DIR/reject-ssh" bash "$ACTIVATE" >"$TMP_DIR/unsafe-generation-gate.log" 2>&1; then
fail "不安全生成门相对路径仍允许激活"
fi
[ ! -s "$TMP_DIR/ssh-called" ] || fail "非法生成门路径后仍发生 SSH"
# 真激活缺 bootstrap token 文件路径时必须在 SSH 前 fail-closed,不能回退仓库默认密码。
if RC_COMMIT="0000000000000000000000000000000000000000" \
GENERATION_GATE_EVIDENCE_REF="$GENERATION_GATE_REF" SSH_BIN="$TMP_DIR/reject-ssh" \
bash "$ACTIVATE" >"$TMP_DIR/missing-bootstrap-token.log" 2>&1; then
fail "缺 BOOTSTRAP_ADMIN_TOKEN_FILE 仍允许激活"
fi
[ ! -s "$TMP_DIR/ssh-called" ] || fail "缺 bootstrap token 路径后仍发生 SSH"
# 旧变量仅允许受控路径和 hash;任意 URL 必须在 SSH 前失败。
if RC_COMMIT="0000000000000000000000000000000000000000" \
GENERATION_GATE_EVIDENCE_REF="$GENERATION_GATE_REF" \
OBJECT_SNAPSHOT_REF="fake://object-snapshot/rc-test" SSH_BIN="$TMP_DIR/fake-ssh" \
bash "$ACTIVATE" >"$TMP_DIR/invalid-snapshot.log" 2>&1; then
fail "任意 OBJECT_SNAPSHOT_REF 仍允许激活"
fi
# prepare 必须在 ready 前运行发布工具回归与安全关键 Java 测试,并把日志哈希绑定 RC commit。
for test_name in test-staging-rc.sh test-provision-dogfood-users.sh \
test-provision-dogfood-nacos-transaction.sh test-import-newapi-pool.sh \
test-dogfood-smoke.sh; do
rg -q "$test_name" "$PREPARE" || fail "prepare 未运行发布工具回归:$test_name"
done
rg -q 'release-tool-tests\.log' "$PREPARE" || fail "prepare 未保存发布工具测试日志"
rg -q 'security-java-tests\.log' "$PREPARE" || fail "prepare 未保存安全关键 Java 测试日志"
rg -q 'DogfoodStartupValidatorTest' "$PREPARE" || fail "prepare 未运行 Dogfood 启动安全测试"
rg -q 'DogfoodSecurityStartupValidatorTest' "$PREPARE" || fail "prepare 未运行 Dogfood 配置安全测试"
rg -q 'TokenAuthenticationFilterTest' "$PREPARE" || fail "prepare 未运行令牌认证过滤器测试"
rg -q 'AuthenticationEntryPointImplTest' "$PREPARE" || fail "prepare 未运行未认证响应测试"
rg -q 'RuntimePackageServiceImplTest' "$PREPARE" || fail "prepare 未运行 artifact CAS 测试"
rg -q 'RuntimePackageApiImplDogfoodTest' "$PREPARE" || fail "prepare 未运行 runtime RPC 安全测试"
rg -q 'AdminNewapiQuotaControllerTest' "$PREPARE" || fail "prepare 未运行配额修复入口测试"
rg -q 'NewapiQuotaClaimConsumerTest' "$PREPARE" || fail "prepare 未运行配额 MQ 消费测试"
rg -q 'NewapiQuotaServiceImplTest' "$PREPARE" || fail "prepare 未运行配额服务测试"
rg -q 'GameVersionServiceImplTest' "$PREPARE" || fail "prepare 未运行版本产物绑定测试"
rg -q 'ProjectVersionApiImplDogfoodTest' "$PREPARE" || fail "prepare 未运行 project RPC 写边界测试"
rg -q 'FeedApiImplDogfoodTest' "$PREPARE" || fail "prepare 未运行 feed RPC 写边界测试"
rg -q 'DifyCallbackServiceImplTest' "$PREPARE" || fail "prepare 未运行生成回调产物绑定测试"
rg -q 'PublishOrchestrationServiceImplTest' "$PREPARE" || fail "prepare 未运行发布编排测试"
rg -q 'test-dogfood-reviewer-security\.py' "$PREPARE" || fail "prepare 未运行审核账号安全测试"
rg -q 'test-collect-dogfood-account-report\.sh' "$PREPARE" || fail "prepare 未运行账号数据库签名采集测试"
rg -q 'test_dogfood_ops\.py' "$PREPARE" || fail "prepare 未运行波次证据账本测试"
rg -q 'backend_security_tests=PASS' "$PREPARE" || fail "prepare 未把安全 Java 测试结果绑定 RC"
rg -q 'security_java_tests_sha256=' "$PREPARE" || fail "prepare 摘要未绑定安全测试日志哈希"
rg -q 'verify_security_java_reports' "$PREPARE" || fail "prepare 未校验安全测试报告实际包含全部目标类"
rg -q 'security-java-tests-summary\.tsv' "$PREPARE" || fail "prepare 未保存可审计的 Java 测试计数摘要"
rg -q 'security_java_tests_summary_sha256=' "$PREPARE" || fail "prepare 摘要未绑定 Java 测试计数证据"
test_surefire_report_guard
rg -q 'FRONTEND_SECRET_SCAN_PASS' "$PREPARE" || fail "prepare 未扫描前端制品中的默认凭据"
rg -q 'frontend_secret_scan_sha256=' "$PREPARE" || fail "prepare 摘要未绑定前端凭据扫描证据"
rg -Fq "frontend_forbidden_patterns='admin123|test1|mock-token|mock-studio-token|bootstrap-admin\\.token|DOGFOOD_PASSWORD_SEED'" \
"$PREPARE" || fail "prepare 前端扫描模式未完整覆盖 test1、mock-studio-token 与既有模式"
rg -q "printf 'patterns=%s" "$PREPARE" || fail "prepare 扫描证据未记录实际模式集"
rg -q 'frontend_secret_scan_patterns_sha256=' "$PREPARE" || fail "prepare 摘要未绑定扫描模式集证据"
rg -q 'runtime-tree-manifest\.py capture' "$PREPARE" || fail "prepare 未签发 Python 运行树清单"
rg -q 'runtime_tree_manifest_sha256=' "$PREPARE" || fail "prepare 摘要未绑定 Python 运行树清单"
rg -q 'runtime-tree-manifest\.py verify' "$ACTIVATE" || fail "activate 未校验 Python 运行树漂移"
frontend_forbidden_patterns="$(sed -n "s/^frontend_forbidden_patterns='\\(.*\\)'$/\\1/p" "$PREPARE")"
[ -n "$frontend_forbidden_patterns" ] || fail "无法从 prepare 解析前端禁止模式集"
for forbidden_sample in admin123 test1 mock-token mock-studio-token bootstrap-admin.token DOGFOOD_PASSWORD_SEED; do
printf '%s\n' "$forbidden_sample" | rg -q -i "$frontend_forbidden_patterns" || \
fail "前端扫描正则未命中禁止样本:$forbidden_sample"
done
if printf '%s\n' 'production-studio-token' | rg -q -i "$frontend_forbidden_patterns"; then
fail "前端扫描正则误伤安全样本"
fi
! rg -q 'backend_tests=NOT_RUN' "$PREPARE" || fail "prepare 仍宣称后端测试未运行"
venv_line="$(rg -n '准备 commit 内独立 Python 虚拟环境' "$PREPARE" | cut -d: -f1)"
tool_test_line="$(rg -n '运行发布工具回归测试' "$PREPARE" | cut -d: -f1)"
[ "$venv_line" -lt "$tool_test_line" ] || fail "发布工具回归未使用 commit 内已建 venv"
rg -q 'cheap-worker/\.venv/bin.*PATH' "$PREPARE" || fail "发布工具回归未显式使用 commit 内 Python"
rg -q '原子切换 active' "$PREPARE" || fail "prepare 完成提示仍未使用 active 指针"
# 独立签发者用仓外私钥签名;测试 SSH 桩只把固定生产路径映射到临时目录。
cat >"$TMP_DIR/exec-ssh" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
while [ "$#" -gt 0 ] && [ "$1" != bash ]; do shift; done
[ "${1:-}" = bash ] || exit 97
shift
[ "${1:-}" = -s ] || exit 97
shift
[ "${1:-}" = -- ] || exit 97
shift
remote_script="$(mktemp)"
trap 'rm -f "$remote_script"' EXIT
sed "s|/root/game-staging/trust/r1-attestation-ed25519.pub|$RC_TEST_TRUSTED_PUBLIC_KEY|g" \
>"$remote_script"
PATH="$RC_TEST_FAKE_BIN:$PATH" bash "$remote_script" "$@"
SH
chmod +x "$TMP_DIR/exec-ssh"
mkdir -p "$TMP_DIR/r1-fake-bin"
cat >"$TMP_DIR/r1-fake-bin/stat" <<'SH'
#!/usr/bin/env bash
if [ "${1:-}" = -c ] && [ "${2:-}" = %u ]; then
printf '%s\n' "${RC_TEST_FORCE_BAD_TRUST_OWNER:-0}"
exit 0
fi
exec /usr/bin/stat "$@"
SH
cat >"$TMP_DIR/r1-fake-bin/openssl" <<'SH'
#!/usr/bin/env bash
if [ "${1:-}" = pkeyutl ] && printf '%s\n' "$*" | rg -q -- ' -verify( |$)'; then
signature=""
while [ "$#" -gt 0 ]; do
if [ "$1" = -sigfile ]; then signature="$2"; break; fi
shift
done
[ -f "$signature" ] && [ "$(wc -c <"$signature" | tr -d ' ')" -eq 64 ]
exit
fi
exec /usr/bin/openssl "$@"
SH
chmod +x "$TMP_DIR/r1-fake-bin/stat" "$TMP_DIR/r1-fake-bin/openssl"
export RC_TEST_FAKE_BIN="$TMP_DIR/r1-fake-bin"
attestation_root="$(realpath "$TMP_DIR")/attestations"
mkdir -p "$attestation_root/r1"
openssl genpkey -algorithm ED25519 -out "$TMP_DIR/r1-private.pem" >/dev/null 2>&1
chmod 600 "$TMP_DIR/r1-private.pem"
RC_ROOT="$(realpath "$TMP_DIR")/r1-releases"
trusted_public_key="$(realpath "$TMP_DIR")/r1-trust/r1-attestation-ed25519.pub"
mkdir -p "$(dirname "$trusted_public_key")"
cp "$REPOSITORY_GENERATION_ATTESTATION_PUBLIC_KEY" "$trusted_public_key"
chmod 755 "$(dirname "$trusted_public_key")"
chmod 644 "$trusted_public_key"
export RC_ROOT RC_TEST_TRUSTED_PUBLIC_KEY="$trusted_public_key"
for fixture_commit in 0000000000000000000000000000000000000000 1111111111111111111111111111111111111111; do
mkdir -p "$RC_ROOT/$fixture_commit/deploy/trust"
cp "$REPOSITORY_GENERATION_ATTESTATION_PUBLIC_KEY" \
"$RC_ROOT/$fixture_commit/deploy/trust/r1-attestation-ed25519.pub"
done
[ "$(sha256sum "$trusted_public_key" | awk '{print $1}')" = "$FIXED_GENERATION_ATTESTATION_PUBLIC_KEY_SHA256" ] \
|| fail "仓内 R1 公钥摘要与固定值不一致"
read -r issued_at expires_at expired_issued_at expired_expires_at < <(python3 - <<'PY'
from datetime import datetime, timedelta, timezone
now = datetime.now(timezone.utc).replace(microsecond=0)
fmt = "%Y-%m-%dT%H:%M:%SZ"
print((now - timedelta(minutes=1)).strftime(fmt),
(now + timedelta(minutes=30)).strftime(fmt),
(now - timedelta(hours=2)).strftime(fmt),
(now - timedelta(hours=1)).strftime(fmt))
PY
)
attestation_file="$attestation_root/r1/r1.attestation"
cat >"$attestation_file" <<EOF
schema=generation-r1-gate/2
rc_commit=0000000000000000000000000000000000000000
run_id=$GENERATION_GATE_RUN_ID
issuer=$FIXED_GENERATION_ATTESTATION_ISSUER
issued_at=$issued_at
expires_at=$expires_at
r1_status=PASS
actor_prompt_eval=PASS
judge_prompt_eval=PASS
game_content_gold=PASS
live_prompt=PASS
full_gate=PASS
fresh_25=PASS
historical_11=PASS
production_shadow_20=PASS
runtime_eligible=true
EOF
chmod 600 "$attestation_file"
attestation_hash="$(sha256sum "$attestation_file" | awk '{print $1}')"
mv "$attestation_file" "$attestation_root/r1/$attestation_hash.attestation"
attestation_file="$attestation_root/r1/$attestation_hash.attestation"
openssl pkeyutl -sign -inkey "$TMP_DIR/r1-private.pem" -rawin \
-in "$attestation_file" -out "$attestation_file.sig"
chmod 600 "$attestation_file.sig"
attestation_ref="sha256:$attestation_hash:r1/$attestation_hash.attestation"
RC_COMMIT=0000000000000000000000000000000000000000 \
GENERATION_GATE_EVIDENCE_REF="$attestation_ref" GENERATION_ATTESTATION_ROOT="$attestation_root" \
SSH_BIN="$TMP_DIR/exec-ssh" bash "$ACTIVATE" --verify-r1-attestation >"$TMP_DIR/attestation-pass.log" 2>&1 || {
sed -n '1,120p' "$TMP_DIR/attestation-pass.log" >&2
fail "固定信任根的独立 attestation 正例失败"
}
rg -q 'GENERATION_ATTESTATION_PASS' "$TMP_DIR/attestation-pass.log" || fail "独立 attestation 未通过"
# 调用者提供的 issuer 与公钥路径必须被忽略,不能替换固定信任根。
RC_COMMIT=0000000000000000000000000000000000000000 \
GENERATION_GATE_EVIDENCE_REF="$attestation_ref" GENERATION_ATTESTATION_ROOT="$attestation_root" \
GENERATION_ATTESTATION_EXPECTED_ISSUER=other-issuer \
GENERATION_ATTESTATION_TRUSTED_PUBLIC_KEY_FILE="$TMP_DIR/attacker.pem" \
SSH_BIN="$TMP_DIR/exec-ssh" bash "$ACTIVATE" --verify-r1-attestation >/dev/null
cp "$trusted_public_key" "$TMP_DIR/trusted-public-key.backup"
printf 'tampered\n' >"$trusted_public_key"
if RC_COMMIT=0000000000000000000000000000000000000000 \
GENERATION_GATE_EVIDENCE_REF="$attestation_ref" GENERATION_ATTESTATION_ROOT="$attestation_root" \
SSH_BIN="$TMP_DIR/exec-ssh" bash "$ACTIVATE" --verify-r1-attestation >/dev/null 2>&1; then
fail "固定公钥内容篡改后仍允许 attestation"
fi
mv "$TMP_DIR/trusted-public-key.backup" "$trusted_public_key"
chmod 644 "$trusted_public_key"
release_public_key="$RC_ROOT/0000000000000000000000000000000000000000/deploy/trust/r1-attestation-ed25519.pub"
printf '\n' >>"$release_public_key"
if RC_COMMIT=0000000000000000000000000000000000000000 \
GENERATION_GATE_EVIDENCE_REF="$attestation_ref" GENERATION_ATTESTATION_ROOT="$attestation_root" \
SSH_BIN="$TMP_DIR/exec-ssh" bash "$ACTIVATE" --verify-r1-attestation >/dev/null 2>&1; then
fail "远端公钥与 RC 仓内公钥不一致时仍允许 attestation"
fi
cp "$REPOSITORY_GENERATION_ATTESTATION_PUBLIC_KEY" "$release_public_key"
chmod 600 "$trusted_public_key"
if RC_COMMIT=0000000000000000000000000000000000000000 \
GENERATION_GATE_EVIDENCE_REF="$attestation_ref" GENERATION_ATTESTATION_ROOT="$attestation_root" \
SSH_BIN="$TMP_DIR/exec-ssh" bash "$ACTIVATE" --verify-r1-attestation >/dev/null 2>&1; then
fail "固定公钥权限漂移后仍允许 attestation"
fi
chmod 644 "$trusted_public_key"
chmod 777 "$(dirname "$trusted_public_key")"
if RC_COMMIT=0000000000000000000000000000000000000000 \
GENERATION_GATE_EVIDENCE_REF="$attestation_ref" GENERATION_ATTESTATION_ROOT="$attestation_root" \
SSH_BIN="$TMP_DIR/exec-ssh" bash "$ACTIVATE" --verify-r1-attestation >/dev/null 2>&1; then
fail "固定公钥父目录可被其他用户写入时仍允许 attestation"
fi
chmod 755 "$(dirname "$trusted_public_key")"
if RC_TEST_FORCE_BAD_TRUST_OWNER=1000 RC_COMMIT=0000000000000000000000000000000000000000 \
GENERATION_GATE_EVIDENCE_REF="$attestation_ref" GENERATION_ATTESTATION_ROOT="$attestation_root" \
SSH_BIN="$TMP_DIR/exec-ssh" bash "$ACTIVATE" --verify-r1-attestation >/dev/null 2>&1; then
fail "固定公钥 owner 不是 root 时仍允许 attestation"
fi
mv "$attestation_file.sig" "$attestation_file.sig.missing"
if RC_COMMIT=0000000000000000000000000000000000000000 \
GENERATION_GATE_EVIDENCE_REF="$attestation_ref" GENERATION_ATTESTATION_ROOT="$attestation_root" \
SSH_BIN="$TMP_DIR/exec-ssh" bash "$ACTIVATE" --verify-r1-attestation >/dev/null 2>&1; then
fail "缺 detached signature 仍允许 attestation"
fi
mv "$attestation_file.sig.missing" "$attestation_file.sig"
cp "$attestation_file.sig" "$TMP_DIR/valid-attestation.sig"
printf 'x' >>"$attestation_file.sig"
if RC_COMMIT=0000000000000000000000000000000000000000 \
GENERATION_GATE_EVIDENCE_REF="$attestation_ref" GENERATION_ATTESTATION_ROOT="$attestation_root" \
SSH_BIN="$TMP_DIR/exec-ssh" bash "$ACTIVATE" --verify-r1-attestation >/dev/null 2>&1; then
fail "篡改 detached signature 仍允许 attestation"
fi
mv "$TMP_DIR/valid-attestation.sig" "$attestation_file.sig"
if RC_COMMIT=0000000000000000000000000000000000000000 \
GENERATION_GATE_EVIDENCE_REF="$attestation_ref" GENERATION_ATTESTATION_ROOT="$attestation_root" \
GENERATION_GATE_RUN_ID=other-run \
SSH_BIN="$TMP_DIR/exec-ssh" bash "$ACTIVATE" --verify-r1-attestation >/dev/null 2>&1; then
fail "run-id 漂移 attestation 未拒绝"
fi
chmod 644 "$attestation_file"
if RC_COMMIT=0000000000000000000000000000000000000000 \
GENERATION_GATE_EVIDENCE_REF="$attestation_ref" GENERATION_ATTESTATION_ROOT="$attestation_root" \
SSH_BIN="$TMP_DIR/exec-ssh" bash "$ACTIVATE" --verify-r1-attestation >/dev/null 2>&1; then
fail "权限过宽 attestation 未拒绝"
fi
chmod 600 "$attestation_file"
ln -s "$attestation_file" "$attestation_root/r1/symlink.attestation"
if RC_COMMIT=0000000000000000000000000000000000000000 \
GENERATION_GATE_EVIDENCE_REF="sha256:$attestation_hash:r1/symlink.attestation" \
GENERATION_ATTESTATION_ROOT="$attestation_root" \
SSH_BIN="$TMP_DIR/exec-ssh" bash "$ACTIVATE" --verify-r1-attestation >/dev/null 2>&1; then
fail "symlink attestation 未拒绝"
fi
if RC_COMMIT=1111111111111111111111111111111111111111 \
GENERATION_GATE_EVIDENCE_REF="$attestation_ref" GENERATION_ATTESTATION_ROOT="$attestation_root" \
SSH_BIN="$TMP_DIR/exec-ssh" bash "$ACTIVATE" --verify-r1-attestation >/dev/null 2>&1; then
fail "rc_commit 漂移 attestation 未拒绝"
fi
expired_file="$attestation_root/r1/expired.attestation"
sed -e "s/^issued_at=.*/issued_at=$expired_issued_at/" \
-e "s/^expires_at=.*/expires_at=$expired_expires_at/" "$attestation_file" >"$expired_file"
chmod 600 "$expired_file"
expired_hash="$(sha256sum "$expired_file" | awk '{print $1}')"
mv "$expired_file" "$attestation_root/r1/$expired_hash.attestation"
expired_file="$attestation_root/r1/$expired_hash.attestation"
openssl pkeyutl -sign -inkey "$TMP_DIR/r1-private.pem" -rawin \
-in "$expired_file" -out "$expired_file.sig"
chmod 600 "$expired_file.sig"
if RC_COMMIT=0000000000000000000000000000000000000000 \
GENERATION_GATE_EVIDENCE_REF="sha256:$expired_hash:r1/$expired_hash.attestation" \
GENERATION_ATTESTATION_ROOT="$attestation_root" \
SSH_BIN="$TMP_DIR/exec-ssh" bash "$ACTIVATE" --verify-r1-attestation >/dev/null 2>&1; then
fail "已过期 attestation 未拒绝"
fi
# 直接验证 DB-only 审计:生成、外部引用 fail-closed、旧 hash/路径校验。
FAKE_RC="$TMP_DIR/fake-rc"
mkdir -p "$FAKE_RC/evidence"
git -C "$FAKE_RC" init -q
git -C "$FAKE_RC" config user.email test@example.invalid
git -C "$FAKE_RC" config user.name staging-test
printf 'rc\n' >"$FAKE_RC/README"
git -C "$FAKE_RC" add README
git -C "$FAKE_RC" commit -qm init
cat >"$TMP_DIR/fake-audit-mysql" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
cat >/dev/null
cat <<'OUT'
game_version_rows=53
game_version_package_url_nonempty=0
game_runtime_package_rows=54
game_runtime_package_url_nonempty=0
game_runtime_package_payload_rows=48
game_source_project_rows=41
game_source_project_url_nonempty=0
game_source_project_payload_rows=41
game_aigc_task_rows=78
game_aigc_result_package_url_nonempty=0
infra_file_rows=0
OUT
SH
chmod +x "$TMP_DIR/fake-audit-mysql"
audit_output="$(RC_RELEASE="$FAKE_RC" STORAGE_AUDIT_MYSQL_BIN="$TMP_DIR/fake-audit-mysql" \
bash "$STORAGE_AUDIT")"
audit_ref="$(printf '%s\n' "$audit_output" | sed -n 's/^STORAGE_AUDIT_PASS ref=//p')"
[[ "$audit_ref" =~ ^sha256:[0-9a-f]{64}:evidence/storage-audit\.tsv$ ]] || fail "审计引用格式非法"
rg -q '^object_storage_status=DB_ONLY_NA$' "$FAKE_RC/evidence/storage-audit.tsv" || fail "缺 DB_ONLY_NA"
rg -q '^minio_involved_prefix_count=0$' "$FAKE_RC/evidence/storage-audit.tsv" || fail "缺 MinIO 前缀事实"
RC_RELEASE="$FAKE_RC" STORAGE_AUDIT_MYSQL_BIN="$TMP_DIR/fake-audit-mysql" \
EXPECTED_STORAGE_AUDIT_REF="$audit_ref" bash "$STORAGE_AUDIT" >/dev/null
bad_ref="sha256:$(printf '0%.0s' $(seq 1 64)):evidence/storage-audit.tsv"
if RC_RELEASE="$FAKE_RC" STORAGE_AUDIT_MYSQL_BIN="$TMP_DIR/fake-audit-mysql" \
EXPECTED_STORAGE_AUDIT_REF="$bad_ref" bash "$STORAGE_AUDIT" >/dev/null 2>&1; then
fail "错误 storage audit hash 未被拒绝"
fi
cat >"$TMP_DIR/fake-audit-external" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
cat >/dev/null
sed 's/game_runtime_package_url_nonempty=0/game_runtime_package_url_nonempty=1/' "$RC_TEST_TMP/audit-values"
SH
cp "$TMP_DIR/fake-audit-mysql" "$TMP_DIR/audit-values-script"
"$TMP_DIR/fake-audit-mysql" </dev/null >"$TMP_DIR/audit-values"
chmod +x "$TMP_DIR/fake-audit-external"
if RC_RELEASE="$FAKE_RC" STORAGE_AUDIT_MYSQL_BIN="$TMP_DIR/fake-audit-external" \
bash "$STORAGE_AUDIT" >/dev/null 2>&1; then
fail "发现外部 URL 时仍签发 DB_ONLY_NA"
fi
# 隔离恢复验证使用假 Docker 覆盖 run/ready/import/query/stop,不触碰真实容器。
printf 'CREATE DATABASE test;\n' | gzip -9 >"$TMP_DIR/dump.sql.gz"
cat >"$TMP_DIR/fake-docker" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
printf '%s\n' "$*" >>"$RC_TEST_TMP/docker-calls.log"
case "$1" in
run) printf 'fake-container-id\n' ;;
stop) ;;
exec)
if printf '%s\n' "$*" | grep -q 'SELECT 1'; then
exit 0
fi
if printf '%s\n' "$*" | grep -q -- '-N -B'; then
cat >/dev/null
cat <<'OUT'
flyway_failed_rows=0
game_version_rows=53
game_version_package_url_nonempty=0
game_runtime_package_rows=54
game_runtime_package_url_nonempty=0
game_runtime_package_payload_rows=48
game_source_project_rows=41
game_source_project_url_nonempty=0
game_source_project_payload_rows=41
game_aigc_task_rows=78
game_aigc_result_package_url_nonempty=0
infra_file_rows=0
OUT
else
cat >/dev/null
fi
;;
*) exit 2 ;;
esac
SH
chmod +x "$TMP_DIR/fake-docker"
DUMP_FILE="$TMP_DIR/dump.sql.gz" EXPECTED_AUDIT_FILE="$FAKE_RC/evidence/storage-audit.tsv" \
DOCKER_BIN="$TMP_DIR/fake-docker" RESTORE_VERIFY_TIMEOUT_SEC=10 \
bash "$RESTORE_VERIFY" >"$TMP_DIR/restore.log"
rg -q '^RESTORE_VERIFY_PASS ' "$TMP_DIR/restore.log" || fail "假容器恢复未通过"
rg -q '^run .*--network none ' "$TMP_DIR/docker-calls.log" || fail "恢复容器未隔离网络"
rg -q '^stop ' "$TMP_DIR/docker-calls.log" || fail "恢复容器未销毁"
DUMP_FILE="$TMP_DIR/dump.sql.gz" EXPECTED_AUDIT_FILE="$FAKE_RC/evidence/storage-audit.tsv" \
RESTORE_VERIFY_DRY_RUN=1 bash "$RESTORE_VERIFY" >"$TMP_DIR/restore-dry.log"
rg -q '^RESTORE_VERIFY_DRY_RUN_PASS' "$TMP_DIR/restore-dry.log" || fail "恢复 dry-run 输入校验失败"
# 本地假服务验证 readiness smoke 全程只有 GET。
cat >"$TMP_DIR/fake-readiness.py" <<'PY'
import json
import os
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from socketserver import TCPServer
port_file = os.environ["RC_TEST_PORT_FILE"]
method_file = os.environ["RC_TEST_METHOD_FILE"]
class Handler(BaseHTTPRequestHandler):
def log_message(self, _format, *_args):
return
def send_json(self, payload):
body = json.dumps(payload).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def record(self):
with open(method_file, "a", encoding="utf-8") as handle:
handle.write(self.command + " " + self.path + "\n")
def do_GET(self):
self.record()
if self.path == "/actuator/health":
self.send_json({"status": "UP"})
elif self.path.startswith("/app-api/feed/zones"):
self.send_json({"code": 0, "data": [{"id": 2}]})
elif self.path.startswith("/app-api/feed/stream"):
self.send_json({"code": 0, "data": {"list": [{"gameId": 1, "versionId": 2}], "hasMore": False}})
elif self.path.startswith("/app-api/runtime/package/2"):
self.send_json({"code": 0, "data": {"versionId": 2}})
else:
self.send_json({"code": 404, "data": None})
def do_POST(self):
self.record()
self.send_json({"code": 500})
class LocalThreadingHTTPServer(ThreadingHTTPServer):
def server_bind(self):
# 跳过 HTTPServer.server_bind 的反向 DNS 查询,离线构建机不能依赖主机名解析。
TCPServer.server_bind(self)
self.server_name, self.server_port = self.server_address[:2]
server = LocalThreadingHTTPServer(("127.0.0.1", 0), Handler)
with open(port_file, "w", encoding="utf-8") as handle:
handle.write(str(server.server_port))
server.serve_forever()
PY
export RC_TEST_PORT_FILE="$TMP_DIR/port"
export RC_TEST_METHOD_FILE="$TMP_DIR/methods"
python3 "$TMP_DIR/fake-readiness.py" &
SERVER_PID=$!
for _ in $(seq 1 300); do
[ -s "$TMP_DIR/port" ] && break
sleep 0.1
done
[ -s "$TMP_DIR/port" ] || fail "假 readiness 服务未启动"
port="$(<"$TMP_DIR/port")"
BASE="http://127.0.0.1:$port" STUDIO_BASE="http://127.0.0.1:4173" \
bash "$SMOKE" >"$TMP_DIR/smoke.log"
if rg -n -v '^GET ' "$TMP_DIR/methods"; then
fail "默认 smoke 出现写请求"
fi
printf 'TEST_PASS staging RC 存储审计、隔离恢复与只读 smoke 均通过\n'