Files
Unlucky-Launcher/packaging/tests/signing_ready_selftest.py
T
AskaEth bff48e27fa feat(packaging): 签名就绪度播报 + 操作手册(M255)
M254 查出 `trust.rs` 的 `PRODUCTION` 表在 PINNED-KEYS 标记之间是空的 ⇒ 任何正式二进制都验不过
任何清单(fail-closed)。工具链早就齐了,缺的是操作步骤与可见性;本条补的就是这两样,不生成钥、
不改 Rust、不碰 release-publish 的判据。

- `packaging/check_signing_ready.py`(只读、零第三方依赖):三行读数 ① 信任表空不空 ② 环境有没有钥
  (顺手对账「这把钥在不在表里」,种子一个字节都不打印)③ 最近一次发布的 .sig/清单痕迹(取不到写
  「未知」)。退出码 0 就绪 / 1 未就绪(打印 fail-closed 后果 + 一行修复命令)/ 2 用法错。
- `packaging/tests/signing_ready_selftest.py`:判据自测,含**两个方向的变异重放**(退出码永远 0 ⇒
  判据①必须红;永远 1 ⇒ 判据②必须红)与接线判据(没人 uses: 它、没有 error 级注解、就绪度那步恒 exit 0)。
- `packaging/docs/signing-runbook.md`:人类照做的 5 步 + 每步判据 + 11 条已知坑 + 没验的部分(无任何密钥值)。
- `.github/workflows/signing-status.yml`:把三行搬进 job summary 的**播报** workflow。guards.yml 已
  399/400,与 gpu-evidence.yml 同先例另开文件;刻意不声明 workflow_call:,未就绪用 ::warning::、
  就绪度那步恒 exit 0,明确「不应被设为 required」。
- `packaging/README.md` / `packaging/docs/signing.md`:索引与指针。
2026-11-11 20:05:45 +08:00

340 lines
18 KiB
Python
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 python3
"""`packaging/check_signing_ready.py` 的判据自测(M255):让「三行读数 + 0/1/2 退出码」变成可执行的事实。
为什么它必须存在:这个脚本的全部价值是「说清今天差什么」。它一旦退化成「永远 exit 0」,
它就从**播报**变成了本仓栽过四次的**假绿尺子** —— 而且没人会当场发现(输出看起来一模一样)。
所以这里的第一类用例判的是退出码语义,第二类用例判的是**把退出码改绿之后自测会红**(变异重放)。
钉住的判据(每一条都对应 mission 的验收项):
| 判据 | 用例 | 抓什么 |
|---|---|---|
| ① 表空 ⇒ `exit 1` 且三行都打印 | `empty_table` | 今天的状态(`PRODUCTION` 空表)必须读成「未就绪」,且不许少打印第 ③ 行 |
| ② 副本夹一条假 `PinnedKey` ⇒ `exit 0` | `pinned_and_key` | 就绪的正路真的存在(**不碰真实 `trust.rs`**:全部走 `--trust <临时副本>`) |
| 变异:把退出码改成永远 0 ⇒ 用例必须红 | `mutation_always_zero` | 上面 ① 的期望值不被「永远绿」满足 ⇒ 自测当场报红 |
| 三行之外的读数 | `key_mismatch` / `no_key` / `release_*` | ② 的对账(钥不在表里)、③ 的有/无/未知 |
| 不泄密 | `no_seed_leak` | 种子的十六进制在 stdout/stderr 里一次都不出现 |
| 用法错 = 2 | `markers_missing` / `usage` | 标记丢了、参数写错 ⇒ 2,不是 1 |
| 接线不是门禁 | `wiring` | 没有其它 workflow `uses:` 它;文件里没有 `::error::`;就绪度那步 `exit 0` |
**不覆盖(如实记)**:本文件自己也是「守门人住在被守对象旁边」——整个 `signing-status.yml` 被删掉后
没有任何东西会跑这份自测(与 `gpu_shot_ci_selftest.py` 同一条边界)。它也不判「用户机器上真的能更新」,
那是 `cargo test -p unlucky-core --lib update::tests::signature_cases` 与一次真 Release 的事。
公开的测试种子:`DUMMY_SEED` 是**由一句固定文本哈希出来的**假种子(同 `trust.rs` 里 `TEST_ONLY` 的那把
公开测试钥同一路数),只用来造读数,签不出任何正式清单;文档与本文件都不含任何真实私钥。
python3 packaging/tests/signing_ready_selftest.py
"""
from __future__ import annotations
import hashlib
import os
import re
import subprocess
import sys
import tempfile
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent.parent
SCRIPT = ROOT / "packaging/check_signing_ready.py"
WORKFLOW_DIR = ROOT / ".github/workflows"
WORKFLOW = WORKFLOW_DIR / "signing-status.yml"
KEY_ENV = "ULL_MANIFEST_SIGNING_KEY"
FAIL_CLOSED = "这会让已安装用户永远验不过(fail-closed)"
LABELS = ("① 信任表 PRODUCTION", "② 环境签名钥", "③ 最近一次发布痕迹")
sys.path.insert(0, str(ROOT / "packaging"))
from ed25519_ref import public_key # noqa: E402
DUMMY_SEED = hashlib.sha256(b"ull-m255-signing-ready-selftest-not-a-real-key").hexdigest()
DUMMY_PUB = public_key(bytes.fromhex(DUMMY_SEED)).hex()
OTHER_SEED = hashlib.sha256(b"ull-m255-selftest-the-other-dummy-key").hexdigest()
TRUST_TEMPLATE = """\
pub(crate) const PRODUCTION: &[PinnedKey] = &[
// >>> PINNED-KEYS >>>
{body} // <<< PINNED-KEYS <<<
];
"""
def secret_json(seed: str = DUMMY_SEED, key_id: str = "selftest-key") -> str:
return '{"key_id": "%s", "seed_hex": "%s"}' % (key_id, seed)
def write(path: Path, text: str) -> Path:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text, encoding="utf-8", newline="\n")
return path
def run(args: list[str], env_key: str | None, summary: Path | None = None, script: Path = SCRIPT) -> tuple[int, str]:
"""跑一次脚本,返回 `(退出码, stdout+stderr)`。`env_key` = 塞进 `KEY_ENV` 的内容(`None` = 不设)。"""
env = {k: v for k, v in os.environ.items() if k != KEY_ENV}
env.pop("GITHUB_STEP_SUMMARY", None)
# 子进程与父进程的编码显式对齐成 UTF-8:本机 Windows 默认 gbk,中文读数会 decode 失败,
# 而 CI(ubuntu)本来就是 UTF-8。这条不是洁癖 —— 编码不一致会让整份自测在 Windows 上假红/假绿。
env["PYTHONIOENCODING"] = "utf-8"
if env_key is not None:
env[KEY_ENV] = env_key
if summary is not None:
env["GITHUB_STEP_SUMMARY"] = str(summary)
done = subprocess.run(
[sys.executable, str(script), *args],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
env=env,
cwd=str(ROOT),
check=False,
)
return done.returncode, done.stdout + done.stderr
def case_empty_table(work: Path, failures: list[str], script: Path = SCRIPT) -> None:
"""① 今天的状态:空表 ⇒ exit 1,三行都要打印,且必须点明 fail-closed 后果与修复命令。
`script` 可换:变异用例把**同一组断言**原样套在「退出码永远 0」的副本上,断言集合必须报红。
"""
trust = write(work / "empty/trust.rs", TRUST_TEMPLATE.format(body=""))
rc, out = run(["--trust", str(trust)], None, script=script)
if rc != 1:
failures.append(f"判据①:空表应 exit 1,实际 {rc}\n{out}")
missing = [label for label in LABELS if label not in out]
if missing:
failures.append(f"判据①:空表时没打印这三行中的 {missing}\n{out}")
if FAIL_CLOSED not in out:
failures.append(f"判据①:未就绪时必须写明「{FAIL_CLOSED}」(后果,不是术语)\n{out}")
if "修复:" not in out:
failures.append(f"判据①:未就绪时必须给一行修复命令\n{out}")
def case_pinned_and_key(work: Path, failures: list[str], script: Path = SCRIPT) -> None:
"""② 副本里夹一条假 PinnedKey + 环境里是同一把钥 ⇒ exit 0(真实 trust.rs 一个字节都不碰)。"""
body = f' PinnedKey {{ key_id: "selftest-key", public_key_hex: "{DUMMY_PUB}" }},\n'
trust = write(work / "pinned/trust.rs", TRUST_TEMPLATE.format(body=body))
rc, out = run(["--trust", str(trust)], secret_json(), script=script)
if rc != 0:
failures.append(f"判据②:表非空 + 钥同源应 exit 0,实际 {rc}\n{out}")
missing = [label for label in LABELS if label not in out]
if missing:
failures.append(f"判据②:就绪时也要打印三行,缺 {missing}\n{out}")
if "就绪" not in out:
failures.append(f"判据②:就绪时必须明说「就绪」\n{out}")
if FAIL_CLOSED in out:
failures.append(f"判据②:就绪时不该出现 fail-closed 那段警告\n{out}")
# 不泄密:种子的十六进制在输出里一次都不许出现(公钥可以出现,它是公开物)。
if DUMMY_SEED in out:
failures.append(f"判据②:输出里出现了**种子**(私钥)—— 这是泄密\n{out}")
def case_key_mismatch(work: Path, failures: list[str]) -> None:
"""② 的对账:表里钉的是 A,环境里是 B ⇒ 未就绪(发布时 --check-pinned-keyring 会红)。"""
body = f' PinnedKey {{ key_id: "selftest-key", public_key_hex: "{DUMMY_PUB}" }},\n'
trust = write(work / "mismatch/trust.rs", TRUST_TEMPLATE.format(body=body))
rc, out = run(["--trust", str(trust)], secret_json(OTHER_SEED, "other-key"))
if rc != 1:
failures.append(f"②对账:钥不在表里应 exit 1,实际 {rc}\n{out}")
if "不在" not in out:
failures.append(f"②对账:必须明说这把钥不在信任表里\n{out}")
if DUMMY_SEED in out or OTHER_SEED in out:
failures.append(f"②对账:输出里出现了种子\n{out}")
def case_no_key(work: Path, failures: list[str]) -> None:
"""② 没有钥:即便表是好的,也算未就绪(签不出任何清单)。"""
body = f' PinnedKey {{ key_id: "selftest-key", public_key_hex: "{DUMMY_PUB}" }},\n'
trust = write(work / "nokey/trust.rs", TRUST_TEMPLATE.format(body=body))
rc, out = run(["--trust", str(trust)], None)
if rc != 1:
failures.append(f"②无钥:应 exit 1,实际 {rc}\n{out}")
if KEY_ENV not in out:
failures.append(f"②无钥:必须点名环境变量 {KEY_ENV}(换个人也知道该配什么)\n{out}")
def case_release_evidence(work: Path, failures: list[str]) -> None:
"""③ 只取证:有留档 / 有清单没签名 / 什么都没给 = 未知;且它不影响退出码。"""
body = f' PinnedKey {{ key_id: "selftest-key", public_key_hex: "{DUMMY_PUB}" }},\n'
trust = write(work / "release/trust.rs", TRUST_TEMPLATE.format(body=body))
signed = work / "release/signed"
write(signed / "manifest.json", '{"channel": "stable", "releases": []}\n')
write(signed / "manifest.json.sig", '{"format": 1, "signatures": []}\n')
rc, out = run(["--trust", str(trust), "--dist", str(signed)], secret_json())
if rc != 0 or "有 ——" not in out:
failures.append(f"③有:留档齐全时应写「有」且退出码仍是 0,实际 rc={rc}\n{out}")
bare = work / "release/bare"
write(bare / "manifest.json", '{"channel": "stable", "releases": []}\n')
rc, out = run(["--trust", str(trust), "--dist", str(bare)], secret_json())
if "没有" not in out:
failures.append(f"③无:只有 manifest.json 时必须点明「没有 .sig」\n{out}")
nowhere = work / "release/absent"
rc, out = run(["--trust", str(trust), "--dist", str(nowhere)], secret_json())
if "未知" not in out:
failures.append(f"③未知:取不到证据时必须写「未知」,不许写成「无」(本地没留档 ≠ 上次没签)\n{out}")
if rc != 0:
failures.append(f"③未知:③ 不参与就绪判定,退出码应仍是 0,实际 {rc}\n{out}")
def case_usage(work: Path, failures: list[str]) -> None:
"""用法错 = 2:参数写错、标记丢了(脚本形不成判断)。绝不能混进 1(那是「未就绪」的读数)。"""
rc, out = run(["--nonsense"], None)
if rc != 2:
failures.append(f"用法错:未知参数应 exit 2,实际 {rc}\n{out}")
broken = write(work / "broken/trust.rs", "pub(crate) const PRODUCTION: &[PinnedKey] = &[];\n")
rc, out = run(["--trust", str(broken)], None)
if rc != 2:
failures.append(f"用法错:标记缺失应 exit 2(不是 1),实际 {rc}\n{out}")
rc, out = run(["--trust", str(work / "no-such-dir/trust.rs")], None)
if rc != 2:
failures.append(f"用法错:trust.rs 不存在应 exit 2,实际 {rc}\n{out}")
def case_summary(work: Path, failures: list[str]) -> None:
"""`--line`:CI 用,把三行搬进 $GITHUB_STEP_SUMMARY;且必须写明「不是门禁」。"""
trust = write(work / "summary/trust.rs", TRUST_TEMPLATE.format(body=""))
summary = work / "summary-step.md"
rc, out = run(["--trust", str(trust), "--line"], None, summary=summary)
if rc != 1:
failures.append(f"summary:空表仍应 exit 1,实际 {rc}\n{out}")
if not summary.is_file():
failures.append("summary:给了 $GITHUB_STEP_SUMMARY 却没写文件")
return
text = summary.read_text(encoding="utf-8")
missing = [label for label in LABELS if label not in text]
if missing:
failures.append(f"summary:三行没全部进 summary,缺 {missing}\n{text}")
if "不代表签名就绪" not in text:
failures.append(f"summary:必须写清「绿色不代表签名就绪」(播报 ≠ 门禁)\n{text}")
TARGET_LINE = " return 0 if ready else 1\n"
def mutant(work: Path, name: str, replacement: str) -> Path | None:
"""造一份「只改了退出码那一行」的副本。副本自成一体(模块一起拷):脚本按 `__file__` 上一级找
`packaging/`,不靠 PYTHONPATH(实测本机 3.12 根本不把 PYTHONPATH 放进 sys.path)。"""
source = SCRIPT.read_text(encoding="utf-8")
if source.count(TARGET_LINE) != 1:
return None
root = work / name
path = write(root / "packaging/check_signing_ready_mutated.py", source.replace(TARGET_LINE, replacement))
for module in ("sign_manifest.py", "ed25519_ref.py"):
write(root / f"packaging/{module}", (ROOT / f"packaging/{module}").read_text(encoding="utf-8"))
write(root / "trust.rs", TRUST_TEMPLATE.format(body=""))
return path
def case_stray_hint(work: Path, failures: list[str]) -> None:
"""① 的「标记之外还有条目」提示:只**点名**,不替人断言「贴错了」。
实测抓到的误报:真实 `trust.rs` 里 `TEST_ONLY` 那一条**本来就在标记之外**(那是它的正确位置),
上一版措辞「另有 N 条写在标记之外(贴错位置,等于没钉)」会对今天的状态喊狼来了。
"""
outside = 'pub(crate) const TEST_ONLY: &[PinnedKey] = &[PinnedKey { key_id: "test-only-key", public_key_hex: "%s" }];\n'
trust = write(
work / "stray/trust.rs",
outside % ("ab" * 32) + TRUST_TEMPLATE.format(body=""),
)
rc, out = run(["--trust", str(trust)], None)
if rc != 1:
failures.append(f"①提示:标记外有条目时表仍是空的 ⇒ 应 exit 1,实际 {rc}\n{out}")
if "test-only-key" not in out:
failures.append(f"①提示:标记之外的条目必须**点名**(否则人不知道贴漏了哪一条)\n{out}")
if "TEST_ONLY" not in out:
failures.append(f"①提示:必须说清 `TEST_ONLY` 本来就在标记之外(否则这就是一句误报)\n{out}")
def case_mutation(work: Path, failures: list[str]) -> None:
"""变异重放:改坏被测对象,**同一组断言原样套上去必须报红**(两个方向都试)。
这是本 mission 唯一认可的形状 —— 不是「我写了断言」,而是「把对象改坏,断言真的会红」。
"""
# ── 变异 A:退出码永远 0(「未就绪」伪装成「就绪」)⇒ 判据① 必须红 ──
always_zero = mutant(work, "mutant-zero", " return 0\n")
if always_zero is None:
failures.append(f"变异 A:{SCRIPT.name} 里找不到唯一的 `{TARGET_LINE.strip()}`,变异无处下手(脚本形状变了就更新这里)")
else:
trust = work / "mutant-zero/trust.rs"
probe: list[str] = []
case_empty_table(work / "mutant-zero", probe, script=always_zero)
# 副本必须真的「跑得起来、只有退出码变了」:崩溃(rc=2)也会让 probe 非空,那就变成假绿了。
rc, out = run(["--trust", str(trust)], None, script=always_zero)
if rc != 0:
failures.append(f"变异 A:副本在空表上应返回 0(这就是变异本身),实际 {rc}\n{out}")
missing = [label for label in LABELS if label not in out]
if missing:
failures.append(f"变异 A:副本连三行读数都没打印(缺 {missing}),它不是在测退出码那一条\n{out}")
if not probe:
failures.append(f"变异 A:「退出码永远 0」的副本竟然让判据①全过 —— 断言抓不住改绿(副本空表返回 {rc})")
elif not any("exit 1" in item for item in probe):
failures.append(f"变异 A:副本被判红,但不是因为退出码那一条 —— 变异用例没对准判据①:{probe}")
# ── 变异 B:退出码永远 1(「就绪」伪装成「未就绪」)⇒ 判据② 必须红 ──
# 反向也要试:只测一个方向的话,「就绪的正路根本不存在」这种回归仍然全绿。
always_one = mutant(work, "mutant-one", " return 1\n")
if always_one is None:
failures.append(f"变异 B:{SCRIPT.name} 里找不到唯一的 `{TARGET_LINE.strip()}`")
else:
probe = []
case_pinned_and_key(work / "mutant-one", probe, script=always_one)
if not probe:
failures.append("变异 B:「退出码永远 1」的副本竟然让判据②全过 —— 就绪的正路没被断言(假绿的反面)")
elif not any("exit 0" in item for item in probe):
failures.append(f"变异 B:副本被判红,但不是因为退出码那一条 —— 变异用例没对准判据②:{probe}")
def case_wiring(failures: list[str]) -> None:
"""接线判据:它**只是播报**。别人 `uses:` 它 = 它成了某条线的必需检查 ⇒ 红。"""
if not WORKFLOW.is_file():
failures.append(f"接线:{WORKFLOW} 不存在")
return
text = WORKFLOW.read_text(encoding="utf-8")
for phrase in ("播报,不是门禁", "不应被设为 required"):
if phrase not in text:
failures.append(f"接线:文件头必须写清「{phrase}」(不阻断要是可核的事实,不是靠没接线)")
if "::error::" in text:
failures.append("接线:播报 workflow 里不许出现 `::error::`(那会让人以为构建挂了,与播报定位冲突)")
if "rc=$?" not in text or "exit 0" not in text:
failures.append("接线:就绪度那一步必须抓 `rc=$?` 后 `exit 0`(未就绪不许让 job 变红)")
wired: list[str] = []
for path in sorted(WORKFLOW_DIR.glob("*.y*ml")):
if path.resolve() == WORKFLOW.resolve():
continue
for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
if re.match(r"^\s*uses:\s*", line) and WORKFLOW.name in line:
wired.append(f"{path.relative_to(ROOT).as_posix()}:{number}: {line.strip()}")
if wired:
failures.append(
"接线:有 workflow `uses:` 了 " + WORKFLOW.name + " ⇒ 播报变成了别人的必需检查:\n " + "\n ".join(wired)
)
def main() -> int:
failures: list[str] = []
with tempfile.TemporaryDirectory(prefix="ull-signing-ready-") as temp:
work = Path(temp)
case_empty_table(work, failures)
case_pinned_and_key(work, failures)
case_key_mismatch(work, failures)
case_no_key(work, failures)
case_release_evidence(work, failures)
case_stray_hint(work, failures)
case_usage(work, failures)
case_summary(work, failures)
case_mutation(work, failures)
case_wiring(failures)
for line in failures:
print(f"FAIL {line}", file=sys.stderr)
print("signing-ready selftest:", "ok" if not failures else f"{len(failures)} 处失败")
return 0 if not failures else 1
if __name__ == "__main__":
raise SystemExit(main())