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:
1 parent
cb051ca933
commit
66eb912df0
3 files changed
+128
-26
No files matched your search
+112
-18
@@ -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
|
||||
|
||||
|
||||
Reference in new issue
Block a user