fix(packaging): --generate-key 打印的 PinnedKey 改成 rustfmt 保持的多行形态(M265)

`--generate-key` 与「信任表是空的」报错里给人粘贴的 `PinnedKey` 原来是单行:
    PinnedKey { key_id: "…", public_key_hex: "<64 hex>" },
这一行有 113+ 字符(即使 key_id 为空),超出 .rustfmt.toml 的 max_width = 100,
所以 `cargo fmt` **必然**把它拆成 4 行 —— 人贴单行 → 下一轮 fmt 改动 → 谁忘了跑 fmt
就把 fmt 红带进 main(T30 那次就是这条路径)。

改的是「打印模板」与文档,不是解析器(KID_RE 的 \s* 本来就吃两种形态):
- 新增 render_pinned_key(),字段各占一行、元素缩进 4/字段缩进 8、结尾逗号,
  与 trust.rs 里 cargo fmt 生成的形状逐字一致;两条打印路径都走它。
- 提示文案不再暗示「贴完还要 fmt」,改成「贴完直接提交,不用再跑 fmt」。
- docs:signing.md §3 与 signing-runbook.md §2/§4/§8 的「那一行」→「那一段(4 行)」,
  并在 runbook §7 明写这条断言在没装 rustfmt 时的退化边界。
- --self-test 新增形态断言:① 渲染结果 == 4 行期望字面量 ② 形状与 trust.rs 现存条目
  (字段值抹平后逐行)一致 ③ 单行必然超 max_width、多行每行都不超 ④ 贴进标记之间能解析
  ⑤ 有 rustfmt 时在临时副本上真跑一遍,要求粘贴块字节不变(没有则打 note 跳过,不装成验过)。
This commit is contained in:
AskaEth committed 2026-11-12 09:26:05 +08:00
1 parent cb051ca933
commit 66eb912df0
3 files changed
+128 -26

No files matched your search

+112 -18
View File
@@ -25,7 +25,10 @@ import hashlib
import json
import os
import re
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
FORMAT = 1
@@ -33,8 +36,11 @@ ALG = "ed25519"
KEY_ENV = "ULL_MANIFEST_SIGNING_KEY"
SIG_SUFFIX = ".sig"
TRUST_RS = Path(__file__).resolve().parent.parent / "crates/core/src/update/trust.rs"
RUSTFMT_TOML = Path(__file__).resolve().parent.parent / ".rustfmt.toml"
PIN_BEGIN = ">>> PINNED-KEYS >>>"
PIN_END = "<<< PINNED-KEYS <<<"
# 粘贴块排版:`trust.rs` 里数组元素缩进 4 空格、结构体字段再缩进 4(= 元素缩进 + 4)。
PASTE_INDENT = " "
KID_RE = re.compile(r'key_id:\s*"([^"]+)"\s*,\s*public_key_hex:\s*"([0-9a-fA-F]{64})"')
# Ed25519 的参考实现搬到了 `packaging/ed25519_ref.py`(M190 r1:本文件贴着 400 行红线)。
# 目的与路径都写在那边的文件头:发布侧签名必须零第三方依赖,而仓里验签用的是 ed25519-dalek ——
@@ -85,7 +91,66 @@ def pinned_keys(trust_text: str) -> list[tuple[str, str]]:
raise SystemExit(f"error: {TRUST_RS} 里找不到 {PIN_BEGIN} / {PIN_END} 标记")
return KID_RE.findall(trust_text[begin:end])
def render_pinned_key(key_id: str, public_key_hex: str) -> str:
"""渲染一条**粘贴即可、`cargo fmt` 不再改形**的 `PinnedKey`(M265)。
为什么必须多行:单行形态即使 `key_id` 为空也有 113 字符,超出 `.rustfmt.toml` 的 `max_width = 100`,
所以 `cargo fmt` **必然**把它拆成下面这个形状 —— T30 的 fmt 红就是这么进的 main(人贴单行 → 下一轮
fmt 改动 → 谁忘了跑 fmt 就带红进主干)。反过来说这个形状过一遍 rustfmt 不变,`--self-test` 会验。
"""
pad = PASTE_INDENT
return (
f"{pad}PinnedKey {{\n"
f'{pad} key_id: "{key_id}",\n'
f'{pad} public_key_hex: "{public_key_hex}",\n'
f"{pad}}},"
)
def rustfmt_max_width() -> int:
"""`.rustfmt.toml` 里的 `max_width`(读不到就用 rustfmt 的默认 100)。"""
try:
for line in RUSTFMT_TOML.read_text(encoding="utf-8").splitlines():
key, _, value = line.partition("=")
if key.strip() == "max_width":
return int(value.strip())
except (OSError, ValueError):
pass
return 100
def pinned_entry_blocks(trust_text: str) -> list[str]:
"""标记之间每条 `PinnedKey { … }` 的**原样文本**(含缩进与结尾逗号)。
与 `pinned_keys()` 互补:那个只看字段值(跨行跨不跨行都吃),这个看**排版**。
"""
begin, end = trust_text.find(PIN_BEGIN), trust_text.find(PIN_END)
if begin < 0 or end < 0 or end < begin:
return []
return re.findall(r"^[ \t]*PinnedKey \{.*?^[ \t]*\},", trust_text[begin:end], re.MULTILINE | re.DOTALL)
def rustfmt_keeps_shape(snippet: str) -> bool | None:
"""把 `snippet` 按 `trust.rs` 的上下文贴进临时副本、真跑一遍 `rustfmt`:形态被保持 ⇒ True。
`rustfmt` 不在 PATH 上 ⇒ None(自测退化成结构断言,并把「跳过」说出来,不装成验过)。
"""
rustfmt = shutil.which("rustfmt")
if not rustfmt:
return None
args = [rustfmt, "--edition", "2021"]
if RUSTFMT_TOML.is_file():
args += ["--config-path", str(RUSTFMT_TOML)]
with tempfile.TemporaryDirectory() as tmp:
src = Path(tmp) / "pasted.rs"
src.write_text(
"struct PinnedKey {\n key_id: &'static str,\n public_key_hex: &'static str,\n}\n\n"
f"const PRODUCTION: &[PinnedKey] = &[\n // {PIN_BEGIN}\n{snippet}\n // {PIN_END}\n];\n",
encoding="utf-8",
newline="\n",
)
result = subprocess.run([*args, str(src)], capture_output=True, text=True, timeout=120)
if result.returncode != 0:
raise RuntimeError(f"rustfmt 跑挂了:{result.stderr.strip()}")
# 比 `snippet in text` 更硬:fmt 之后标记之间那一条必须**逐字节**还是原样(含缩进与结尾逗号)。
return pinned_entry_blocks(src.read_text(encoding="utf-8")) == [snippet]
def do_generate_key(key_id: str) -> int:
seed = os.urandom(32)
@@ -97,8 +162,9 @@ def do_generate_key(key_id: str) -> int:
secret = json.dumps({"key_id": key_id, "seed_hex": seed.hex()}, ensure_ascii=False)
print("# ① 私钥(**只**贴进仓库 Secret " + KEY_ENV + ",别写进任何文件/日志/提交):")
print(secret)
print("# ② 公钥(贴进 crates/core/src/update/trust.rs 的 PINNED-KEYS 标记之间):")
print(f' PinnedKey {{ key_id: "{key_id}", public_key_hex: "{pk_hex}" }},')
print("# ② 公钥(整段贴进 crates/core/src/update/trust.rs 的 PINNED-KEYS 标记之间;")
print("# 形状就是 `cargo fmt` 会保持的那一种 —— 贴完直接提交,不用再跑 fmt):")
print(render_pinned_key(key_id, pk_hex))
print(f"# 公钥十六进制:{pk_hex}")
return 0
@@ -163,8 +229,9 @@ def do_check_pinned(key_path: str | None) -> int:
# 「说人话的红」:因为什么、缺什么、怎么补,一行一行写清楚(不涉密,只回显公钥)。
print("error: 信任表是空的(fail-closed):客户端一把公钥都没有,任何清单都验不过,所以拒绝发布。", file=sys.stderr)
print(f"error: 本次签名钥 = key_id {key_id} / 公钥 {pk_hex}", file=sys.stderr)
print("error: 补法:把这行贴进 crates/core/src/update/trust.rs 的 PINNED-KEYS 标记之间 ——", file=sys.stderr)
print(f'error: PinnedKey {{ key_id: "{key_id}", public_key_hex: "{pk_hex}" }},', file=sys.stderr)
print("error: 补法:把下面这段贴进 crates/core/src/update/trust.rs 的 PINNED-KEYS 标记之间 ——", file=sys.stderr)
for line in render_pinned_key(key_id, pk_hex).splitlines():
print(f"error: {line}", file=sys.stderr)
print("error: 手册:packaging/docs/signing.md §3", file=sys.stderr)
return 1
for pinned_id, pinned_hex in keys:
@@ -235,36 +302,63 @@ def do_self_test() -> int:
"6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a"),
]
failures = 0
def fail(message: str) -> None:
nonlocal failures
print(f"FAIL: {message}", file=sys.stderr)
failures += 1
for index, (seed_hex, pk_hex, message_hex, sig_hex) in enumerate(vectors, 1):
seed, pk, message = bytes.fromhex(seed_hex), bytes.fromhex(pk_hex), bytes.fromhex(message_hex)
if public_key(seed).hex() != pk_hex:
print(f"FAIL vector {index}: public key mismatch", file=sys.stderr)
failures += 1
fail(f"vector {index}: public key mismatch")
if sign(seed, message).hex() != sig_hex:
print(f"FAIL vector {index}: signature mismatch", file=sys.stderr)
failures += 1
fail(f"vector {index}: signature mismatch")
if not verify(pk, message, bytes.fromhex(sig_hex)):
print(f"FAIL vector {index}: verify rejected", file=sys.stderr)
failures += 1
fail(f"vector {index}: verify rejected")
tampered = bytearray(message)
tampered.append(0)
if verify(pk, bytes(tampered), bytes.fromhex(sig_hex)):
print(f"FAIL vector {index}: tampered message still verifies", file=sys.stderr)
failures += 1
fail(f"vector {index}: tampered message still verifies")
# 标记解析:信任表解析器必须在示例上命中一对,且在空表上返回空。
sample = f'{PIN_BEGIN}\n PinnedKey {{ key_id: "k1", public_key_hex: "{"ab" * 32}" }},\n{PIN_END}'
if pinned_keys(sample) != [("k1", "ab" * 32)]:
print("FAIL: pinned_keys() 没解析出示例条目", file=sys.stderr)
failures += 1
fail("pinned_keys() 没解析出示例条目")
# 多行形态(M261):`trust.rs` 里那一条是 `cargo fmt` 生成的,字段一行一个 —— 靠 `KID_RE` 的 `\s*`
# 跨行命中。若解析器退化成按行匹配,一次 fmt 就会把整张信任表**静默**读成空表(= 全量客户端验签失败)。
multiline = f'{PIN_BEGIN}\n PinnedKey {{\n key_id: "k1",\n public_key_hex: "{"ab" * 32}",\n }},\n{PIN_END}'
if pinned_keys(multiline) != [("k1", "ab" * 32)]:
print("FAIL: pinned_keys() 没解析出多行(rustfmt)形态的条目", file=sys.stderr)
failures += 1
fail("pinned_keys() 没解析出多行(rustfmt)形态的条目")
if pinned_keys(f"{PIN_BEGIN}\n{PIN_END}") != []:
print("FAIL: pinned_keys() 在空表上应返回空", file=sys.stderr)
failures += 1
fail("pinned_keys() 在空表上应返回空")
# 粘贴块形态(M265):`--generate-key` 与「信任表是空的」报错里给人贴的那段 `PinnedKey` 必须是
# rustfmt **不会改形**的多行形态。单行形态是合法 Rust、也照样能解析,所以这条路一旦回退成单行,
# 除了「谁忘了跑 fmt」之外没有任何东西会红 —— T30 的 fmt 红就是这么进的 main。
hex64 = "ab" * 32
rendered = render_pinned_key("k1", hex64)
want = f' PinnedKey {{\n key_id: "k1",\n public_key_hex: "{hex64}",\n }},'
if rendered != want:
fail(f"粘贴块不是多行形态:{rendered!r}")
# 为什么 rustfmt 必然把它排成这样:单行(哪怕 `key_id` 为空)就超出 `max_width`,而多行每行都不超。
width = rustfmt_max_width()
one_line = f' PinnedKey {{ key_id: "", public_key_hex: "{hex64}" }},'
if len(one_line) <= width or any(len(line) > width for line in rendered.splitlines()):
fail(f"粘贴块与 max_width={width} 的关系变了(单行 {len(one_line)} 字符应当超宽)")
# 判据的核心:形状 = `trust.rs` 里**现存**条目的排班(字段值抹平后逐行比对)。
shape = lambda text: [re.sub(r'"[^"]*"', '""', line) for line in text.splitlines()]
blocks = pinned_entry_blocks(TRUST_RS.read_text(encoding="utf-8") if TRUST_RS.is_file() else "")
if not blocks:
print("note: trust.rs 标记之间没有条目,跳过「与现存排版一致」那条", file=sys.stderr)
for block in blocks:
if shape(block) != shape(rendered):
fail(f"粘贴块排版与 trust.rs 现存条目不一致:{block!r}")
if pinned_keys(f"{PIN_BEGIN}\n{rendered}\n{PIN_END}") != [("k1", hex64)]:
fail("粘贴块贴进标记之间后解析不出来(贴了等于没贴)")
keeps = rustfmt_keeps_shape(rendered)
if keeps is None:
print("note: PATH 里没有 rustfmt,跳过「真跑一遍 fmt 不改形」那条", file=sys.stderr)
elif not keeps:
fail("真跑一遍 rustfmt 改了粘贴块的形态")
print("self-test:", "ok" if failures == 0 else f"{failures} 处失败")
return 0 if failures == 0 else 1