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
@@ -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 向量 + 粘贴块形态) |
|
||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user