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

+10 -5
View File
@@ -14,7 +14,7 @@
| 环节 | 在哪 | 今天的状态 | | 环节 | 在哪 | 今天的状态 |
|---|---|---| |---|---|---|
| 生成钥 | `sign_manifest.py --generate-key` | 有;打印 Secret JSON + `PinnedKey` 那一行 | | 生成钥 | `sign_manifest.py --generate-key` | 有;打印 Secret JSON + `PinnedKey` 那一段(多行,fmt 保持的形态) |
| 钥 ↔ 信任表对账 | `sign_manifest.py --check-pinned-keyring` | 有;**今天必然红**(表空 ⇒ 「信任表是空的」) | | 钥 ↔ 信任表对账 | `sign_manifest.py --check-pinned-keyring` | 有;**今天必然红**(表空 ⇒ 「信任表是空的」) |
| 签名 | `sign_manifest.py --sign dist/manifest.json` → `.sig` | 有;**今天必然红**(`ULL_MANIFEST_SIGNING_KEY` 没有,前面先红) | | 签名 | `sign_manifest.py --sign dist/manifest.json` → `.sig` | 有;**今天必然红**(`ULL_MANIFEST_SIGNING_KEY` 没有,前面先红) |
| 回验 | `sign_manifest.py --verify-pinned dist/manifest.json` | 有;**今天必然红**(表空 ⇒ 「无从回验」) | | 回验 | `sign_manifest.py --verify-pinned dist/manifest.json` | 有;**今天必然红**(表空 ⇒ 「无从回验」) |
@@ -60,7 +60,8 @@ python3 packaging/sign_manifest.py --generate-key --key-id <key-id,例 ull-rel
# 名字必须**逐字**是:ULL_MANIFEST_SIGNING_KEY # 名字必须**逐字**是:ULL_MANIFEST_SIGNING_KEY
# (等价 CLI:gh secret set ULL_MANIFEST_SIGNING_KEY,然后按提示粘贴,别用 -b 把值留在 shell 历史里) # (等价 CLI:gh secret set ULL_MANIFEST_SIGNING_KEY,然后按提示粘贴,别用 -b 把值留在 shell 历史里)
# ③ 把打印出来的那一行 PinnedKey { … } 贴进 trust.rs 的 PINNED-KEYS 标记之间(标记本身别动) # ③ 把打印出来的那一段 PinnedKey(4 行,含结尾逗号)贴进 trust.rs 的 PINNED-KEYS 标记之间(标记本身别动)
# 那个形状就是 cargo fmt 会保持的形态 —— 贴完**不用**再跑 fmt(压成一行反而会被 fmt 拆开)
grep -n 'PINNED-KEYS' crates/core/src/update/trust.rs grep -n 'PINNED-KEYS' crates/core/src/update/trust.rs
# 贴完提交这次改动 —— 公钥进 git 是对的:它是信任锚,本来就该公开可审计 # 贴完提交这次改动 —— 公钥进 git 是对的:它是信任锚,本来就该公开可审计
@@ -78,7 +79,7 @@ python3 packaging/sign_manifest.py --verify-pinned dist/manifest.json # 期望
| 步 | 成功长什么样 | 失败长什么样 | | 步 | 成功长什么样 | 失败长什么样 |
|---|---|---| |---|---|---|
| ① | 打印 ① 私钥 JSON、② 一行 `PinnedKey`、③ 公钥十六进制;脚本自带「自己签自己验」自检 | `error: 自己签自己验不过` ⇒ 不要用这把钥,环境有问题 | | ① | 打印 ① 私钥 JSON、② 一段 4 行的 `PinnedKey`、③ 公钥十六进制;脚本自带「自己签自己验」自检 | `error: 自己签自己验不过` ⇒ 不要用这把钥,环境有问题 |
| ② | `gh secret list` 里能看到 `ULL_MANIFEST_SIGNING_KEY` | 列表里没有 / 名字拼错(大小写、下划线) | | ② | `gh secret list` 里能看到 `ULL_MANIFEST_SIGNING_KEY` | 列表里没有 / 名字拼错(大小写、下划线) |
| ③ | `check_signing_ready.py` 的 ① 从「未就绪」翻成「就绪」 | ① 仍说 0 条 ⇒ 贴到了标记**外面**(读数里会点名「另有 N 条在标记之外(key_id:…)」) | | ③ | `check_signing_ready.py` 的 ① 从「未就绪」翻成「就绪」 | ① 仍说 0 条 ⇒ 贴到了标记**外面**(读数里会点名「另有 N 条在标记之外(key_id:…)」) |
| ④ | `ok: 签名钥 … 已被钉住` | `签名钥(…)不在信任表里` ⇒ ②③ 不是同一把钥 | | ④ | `ok: 签名钥 … 已被钉住` | `签名钥(…)不在信任表里` ⇒ ②③ 不是同一把钥 |
@@ -104,7 +105,7 @@ sign_manifest.py --verify-pinned dist/manifest.json # 用钉住的公钥回验
secret,名字不对就是空串 ⇒ 签名步红,而错误文案会反过来指控「Secret 没配」。`release-publish.yml` secret,名字不对就是空串 ⇒ 签名步红,而错误文案会反过来指控「Secret 没配」。`release-publish.yml`
已经声明了这一个(`required: false`,缺省走 fail-closed),照抄名字即可。 已经声明了这一个(`required: false`,缺省走 fail-closed),照抄名字即可。
2. **只配 Secret 没钉表**:`--check-pinned-keyring` 报「信任表是空的(fail-closed)」,并**直接打印** 2. **只配 Secret 没钉表**:`--check-pinned-keyring` 报「信任表是空的(fail-closed)」,并**直接打印**
该贴的那一行 `PinnedKey`(只含公钥,安全)。照贴即可。 该贴的那一段 4 行 `PinnedKey`(只含公钥,安全)。照贴即可,注意连 `error:` 前缀一起删掉再贴。
3. **只钉表没配 Secret**:`--check-pinned-keyring` 报「没有签名密钥」。这不影响已有用户(表非空 ⇒ 3. **只钉表没配 Secret**:`--check-pinned-keyring` 报「没有签名密钥」。这不影响已有用户(表非空 ⇒
他们能验签),但发不了新版。 他们能验签),但发不了新版。
4. **Secret 与 trust.rs 是两把不同的钥**:最常见于「换过钥但只更新了一边」。判据是**公钥字节**, 4. **Secret 与 trust.rs 是两把不同的钥**:最常见于「换过钥但只更新了一边」。判据是**公钥字节**,
@@ -175,6 +176,10 @@ python3 packaging/sign_manifest.py --verify-pinned /tmp/verify/manifest.json
②③ 两步要动 GitHub Secret 与 Rust 常量,同理。所以本手册是「按现有脚本的接口写的步骤」, ②③ 两步要动 GitHub Secret 与 Rust 常量,同理。所以本手册是「按现有脚本的接口写的步骤」,
首次执行时**以脚本的实际输出为准**,跑完请回来修正本文档。 首次执行时**以脚本的实际输出为准**,跑完请回来修正本文档。
* **「用户机器上真的更新成功」**没有在本条里验:需要一次真 Release + 一台装着旧版本的机器。 * **「用户机器上真的更新成功」**没有在本条里验:需要一次真 Release + 一台装着旧版本的机器。
* **粘贴块「过一遍 fmt 不变形」只在有 `rustfmt` 的机器上是硬证据**:`sign_manifest.py --self-test` 会在
临时副本上真跑一遍 `rustfmt` 比对;PATH 里没有 `rustfmt` 时它会打一行 `note: ... 跳过`,只剩
「形状 == `trust.rs` 现存条目的形状」这条结构断言(抓得住回退成单行/缩进走样,抓不住 rustfmt 版本或
`.rustfmt.toml` 改配置带来的排版变化)。
* **ref 门只在本地验证过**(抽 `run:` 块用 Git Bash 跑 + 接线用例的文本变异),没有在真 Actions 上看过 * **ref 门只在本地验证过**(抽 `run:` 块用 Git Bash 跑 + 接线用例的文本变异),没有在真 Actions 上看过
`secrets` 的求值结果 —— 第一次真跑请确认非 main ref 的 ② 显示「未判定」。 `secrets` 的求值结果 —— 第一次真跑请确认非 main ref 的 ② 显示「未判定」。
@@ -187,4 +192,4 @@ python3 packaging/sign_manifest.py --verify-pinned /tmp/verify/manifest.json
| `packaging/tests/signing_ready_wiring_selftest.py` | 接线自测:不是门禁 / 不被 `uses:` / **ref 门** / 文档路径存在性 + 三处变异 | | `packaging/tests/signing_ready_wiring_selftest.py` | 接线自测:不是门禁 / 不被 `uses:` / **ref 门** / 文档路径存在性 + 三处变异 |
| `.github/workflows/signing-status.yml` | 把三行搬进 job summary 的**播报** workflow(不是门禁;种子只在 main 注入) | | `.github/workflows/signing-status.yml` | 把三行搬进 job summary 的**播报** workflow(不是门禁;种子只在 main 注入) |
| `packaging/docs/signing.md` | 为什么签名、威胁模型、轮换纪律、四条发布侧命令的红法对照表 | | `packaging/docs/signing.md` | 为什么签名、威胁模型、轮换纪律、四条发布侧命令的红法对照表 |
| `packaging/sign_manifest.py` | 生成钥 / 签名 / 对账 / 回验的唯一实现(`--self-test` 跑 RFC 8032 向量) | | `packaging/sign_manifest.py` | 生成钥 / 签名 / 对账 / 回验的唯一实现(`--self-test` 跑 RFC 8032 向量 + 粘贴块形态) |
+6 -3
View File
@@ -57,15 +57,18 @@ manifest.json(字节)──签名──> manifest.json.sig
## 3. 一次性录入(只做一次) ## 3. 一次性录入(只做一次)
```bash ```bash
# ① 生成:打印一段 Secret JSON + 一行可以直接贴进 trust.rs 的 PinnedKey # ① 生成:打印一段 Secret JSON + 一段可以直接贴进 trust.rs 的 PinnedKey(多行,4 行)
python3 packaging/sign_manifest.py --generate-key --key-id ull-release-2026a python3 packaging/sign_manifest.py --generate-key --key-id ull-release-2026a
``` ```
* 打印出来的 **第一行 JSON**(含 `seed_hex`)= 私钥,**只**贴进仓库 Secret: * 打印出来的 **第一行 JSON**(含 `seed_hex`)= 私钥,**只**贴进仓库 Secret:
Settings → Secrets and variables → Actions → New repository secret,名字 Settings → Secrets and variables → Actions → New repository secret,名字
**`ULL_MANIFEST_SIGNING_KEY`**。 **`ULL_MANIFEST_SIGNING_KEY`**。
* 打印出来的 **`PinnedKey { … }` 那一行**贴进 `crates/core/src/update/trust.rs` 的 * 打印出来的 **`PinnedKey { … }` 那一段(4 行,含结尾逗号)**贴进 `crates/core/src/update/trust.rs` 的
`>>> PINNED-KEYS >>>` 与 `<<< PINNED-KEYS <<<` 标记之间(标记本身不要动,脚本按它解析)。 `>>> PINNED-KEYS >>>` 与 `<<< PINNED-KEYS <<<` 标记之间(标记本身不要动,脚本按它解析)。
* 那个形状就是 `cargo fmt`(`.rustfmt.toml` 的 `max_width = 100`)**会保持**的形态:**贴完不用再跑
`cargo fmt`**。反过来,把它压成一行反而会被 fmt 拆开 —— 那种「粘贴 → 下一轮 fmt 改动 → 谁忘了跑 fmt
就把红带进 main」的复发路径由 `sign_manifest.py --self-test` 钉住(`render_pinned_key` 的形态断言)。
* 提交这次改动(公钥进 git 是对的:它是信任锚,本来就该公开可审计)。 * 提交这次改动(公钥进 git 是对的:它是信任锚,本来就该公开可审计)。
### 私钥纪律(违反任何一条都算事故) ### 私钥纪律(违反任何一条都算事故)
@@ -97,7 +100,7 @@ python3 packaging/sign_manifest.py --verify-pinned dist/manifest.json # ④ 用
| 现象 | 原因 | 怎么补 | | 现象 | 原因 | 怎么补 |
|---|---|---| |---|---|---|
| `没有签名密钥` | Secret `ULL_MANIFEST_SIGNING_KEY` 没配 / 名字拼错 | 按 §3 录入 | | `没有签名密钥` | Secret `ULL_MANIFEST_SIGNING_KEY` 没配 / 名字拼错 | 按 §3 录入 |
| `信任表是空的` | `trust.rs` 里还没钉任何公钥 | 把 §3 打印的 `PinnedKey` 那行贴进标记之间 | | `信任表是空的` | `trust.rs` 里还没钉任何公钥 | 把 §3 打印的 `PinnedKey` 那段(4 行)贴进标记之间 |
| `签名钥(…)不在信任表里` | Secret 与 `trust.rs` 是**两把不同的钥** | 用同一把钥重跑 §3,或回滚 `trust.rs` 的改动 | | `签名钥(…)不在信任表里` | Secret 与 `trust.rs` 是**两把不同的钥** | 用同一把钥重跑 §3,或回滚 `trust.rs` 的改动 |
| `签出来的签名自己验不过` | 实现/环境异常 | 不要发布;把 `--self-test` 输出贴进 issue | | `签出来的签名自己验不过` | 实现/环境异常 | 不要发布;把 `--self-test` 输出贴进 issue |
+112 -18
View File
@@ -25,7 +25,10 @@ import hashlib
import json import json
import os import os
import re import re
import shutil
import subprocess
import sys import sys
import tempfile
from pathlib import Path from pathlib import Path
FORMAT = 1 FORMAT = 1
@@ -33,8 +36,11 @@ ALG = "ed25519"
KEY_ENV = "ULL_MANIFEST_SIGNING_KEY" KEY_ENV = "ULL_MANIFEST_SIGNING_KEY"
SIG_SUFFIX = ".sig" SIG_SUFFIX = ".sig"
TRUST_RS = Path(__file__).resolve().parent.parent / "crates/core/src/update/trust.rs" 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_BEGIN = ">>> PINNED-KEYS >>>"
PIN_END = "<<< 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})"') 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 的参考实现搬到了 `packaging/ed25519_ref.py`(M190 r1:本文件贴着 400 行红线)。
# 目的与路径都写在那边的文件头:发布侧签名必须零第三方依赖,而仓里验签用的是 ed25519-dalek —— # 目的与路径都写在那边的文件头:发布侧签名必须零第三方依赖,而仓里验签用的是 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} 标记") raise SystemExit(f"error: {TRUST_RS} 里找不到 {PIN_BEGIN} / {PIN_END} 标记")
return KID_RE.findall(trust_text[begin: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: def do_generate_key(key_id: str) -> int:
seed = os.urandom(32) 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) secret = json.dumps({"key_id": key_id, "seed_hex": seed.hex()}, ensure_ascii=False)
print("# ① 私钥(**只**贴进仓库 Secret " + KEY_ENV + ",别写进任何文件/日志/提交):") print("# ① 私钥(**只**贴进仓库 Secret " + KEY_ENV + ",别写进任何文件/日志/提交):")
print(secret) print(secret)
print("# ② 公钥(贴进 crates/core/src/update/trust.rs 的 PINNED-KEYS 标记之间):") print("# ② 公钥(整段贴进 crates/core/src/update/trust.rs 的 PINNED-KEYS 标记之间;")
print(f' PinnedKey {{ key_id: "{key_id}", public_key_hex: "{pk_hex}" }},') print("# 形状就是 `cargo fmt` 会保持的那一种 —— 贴完直接提交,不用再跑 fmt):")
print(render_pinned_key(key_id, pk_hex))
print(f"# 公钥十六进制:{pk_hex}") print(f"# 公钥十六进制:{pk_hex}")
return 0 return 0
@@ -163,8 +229,9 @@ def do_check_pinned(key_path: str | None) -> int:
# 「说人话的红」:因为什么、缺什么、怎么补,一行一行写清楚(不涉密,只回显公钥)。 # 「说人话的红」:因为什么、缺什么、怎么补,一行一行写清楚(不涉密,只回显公钥)。
print("error: 信任表是空的(fail-closed):客户端一把公钥都没有,任何清单都验不过,所以拒绝发布。", file=sys.stderr) print("error: 信任表是空的(fail-closed):客户端一把公钥都没有,任何清单都验不过,所以拒绝发布。", file=sys.stderr)
print(f"error: 本次签名钥 = key_id {key_id} / 公钥 {pk_hex}", 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("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) 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) print("error: 手册:packaging/docs/signing.md §3", file=sys.stderr)
return 1 return 1
for pinned_id, pinned_hex in keys: for pinned_id, pinned_hex in keys:
@@ -235,36 +302,63 @@ def do_self_test() -> int:
"6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a"), "6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a"),
] ]
failures = 0 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): 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) seed, pk, message = bytes.fromhex(seed_hex), bytes.fromhex(pk_hex), bytes.fromhex(message_hex)
if public_key(seed).hex() != pk_hex: if public_key(seed).hex() != pk_hex:
print(f"FAIL vector {index}: public key mismatch", file=sys.stderr) fail(f"vector {index}: public key mismatch")
failures += 1
if sign(seed, message).hex() != sig_hex: if sign(seed, message).hex() != sig_hex:
print(f"FAIL vector {index}: signature mismatch", file=sys.stderr) fail(f"vector {index}: signature mismatch")
failures += 1
if not verify(pk, message, bytes.fromhex(sig_hex)): if not verify(pk, message, bytes.fromhex(sig_hex)):
print(f"FAIL vector {index}: verify rejected", file=sys.stderr) fail(f"vector {index}: verify rejected")
failures += 1
tampered = bytearray(message) tampered = bytearray(message)
tampered.append(0) tampered.append(0)
if verify(pk, bytes(tampered), bytes.fromhex(sig_hex)): if verify(pk, bytes(tampered), bytes.fromhex(sig_hex)):
print(f"FAIL vector {index}: tampered message still verifies", file=sys.stderr) fail(f"vector {index}: tampered message still verifies")
failures += 1
# 标记解析:信任表解析器必须在示例上命中一对,且在空表上返回空。 # 标记解析:信任表解析器必须在示例上命中一对,且在空表上返回空。
sample = f'{PIN_BEGIN}\n PinnedKey {{ key_id: "k1", public_key_hex: "{"ab" * 32}" }},\n{PIN_END}' sample = f'{PIN_BEGIN}\n PinnedKey {{ key_id: "k1", public_key_hex: "{"ab" * 32}" }},\n{PIN_END}'
if pinned_keys(sample) != [("k1", "ab" * 32)]: if pinned_keys(sample) != [("k1", "ab" * 32)]:
print("FAIL: pinned_keys() 没解析出示例条目", file=sys.stderr) fail("pinned_keys() 没解析出示例条目")
failures += 1
# 多行形态(M261):`trust.rs` 里那一条是 `cargo fmt` 生成的,字段一行一个 —— 靠 `KID_RE` 的 `\s*` # 多行形态(M261):`trust.rs` 里那一条是 `cargo fmt` 生成的,字段一行一个 —— 靠 `KID_RE` 的 `\s*`
# 跨行命中。若解析器退化成按行匹配,一次 fmt 就会把整张信任表**静默**读成空表(= 全量客户端验签失败)。 # 跨行命中。若解析器退化成按行匹配,一次 fmt 就会把整张信任表**静默**读成空表(= 全量客户端验签失败)。
multiline = f'{PIN_BEGIN}\n PinnedKey {{\n key_id: "k1",\n public_key_hex: "{"ab" * 32}",\n }},\n{PIN_END}' 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)]: if pinned_keys(multiline) != [("k1", "ab" * 32)]:
print("FAIL: pinned_keys() 没解析出多行(rustfmt)形态的条目", file=sys.stderr) fail("pinned_keys() 没解析出多行(rustfmt)形态的条目")
failures += 1
if pinned_keys(f"{PIN_BEGIN}\n{PIN_END}") != []: if pinned_keys(f"{PIN_BEGIN}\n{PIN_END}") != []:
print("FAIL: pinned_keys() 在空表上应返回空", file=sys.stderr) fail("pinned_keys() 在空表上应返回空")
failures += 1 # 粘贴块形态(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} 处失败") print("self-test:", "ok" if failures == 0 else f"{failures} 处失败")
return 0 if failures == 0 else 1 return 0 if failures == 0 else 1