#!/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())