From 06712ff6e9844a7ef545b77d1ede71098fc0d0b1 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Sat, 14 Nov 2026 00:15:13 +0800 Subject: [PATCH] =?UTF-8?q?fix(core,docs):=20M308=20r1=20=E2=80=94?= =?UTF-8?q?=E2=80=94=20=E8=AF=BB=E8=A1=A8=E5=B7=A5=E5=85=B7=E5=86=99?= =?UTF-8?q?=E9=94=99=EF=BC=88=E5=8F=88=E4=B8=80=E5=8F=A5=E3=80=8C=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E6=AF=94=E4=BB=A3=E7=A0=81=E5=A4=9A=E3=80=8D=EF=BC=89?= =?UTF-8?q?+=20`.exe`=20=E5=90=8D=E5=AD=97=E5=88=A4=E6=8D=AE=E7=A9=BA?= =?UTF-8?q?=E8=BD=AC=20+=20adopt=20=E4=B9=8B=E5=90=8E=E7=9A=84=E5=8F=96?= =?UTF-8?q?=E5=80=BC=E5=99=A8=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit p2-1 文档:`--check-pinned-keyring` **要私钥**(`do_check_pinned(args.key)`),语义是「Secret 里那把 钥是否已钉住」这条发布前门禁,不是读表工具。`docs/update-trust-anchor.md` 的「当场看一眼」与 `signing-runbook.md` §0 那句「现在 --check-pinned-keyring 绿」(与同文件 §:116 自相矛盾)都换成 `check_signing_ready.py` 的 ① 行(真只读,实测打「就绪 —— 标记之间 1 条:ull-release…」)。 p2-2 判据:`is_launcher_candidate` 的 `|| name.ends_with(".exe")` 此前**删掉也不红**(唯一含 `.exe` 的用例同时给了 0o755)。补 `a_windows_exe_without_the_exec_bit_is_still_the_launcher` (win-x64 + 归档内 `unlucky-launcher.exe` + MZ + **0644** 能装上),删那半条判据即红。 p2-3 文档:`StagedPackage::size()` / `integrity()` 的注释在 `adopt` 之后不再成立(前者是本体的字节数、 后者是**归档**的摘要),改成「这个文件写出来的字节数」/「为它背书的那份摘要」。 --- crates/core/src/update/tests/unpack_cases.rs | 24 ++++++++++++++++++++ crates/core/src/update/verify.rs | 11 +++++++-- docs/update-trust-anchor.md | 16 ++++++++----- packaging/docs/signing-runbook.md | 3 ++- 4 files changed, 45 insertions(+), 9 deletions(-) diff --git a/crates/core/src/update/tests/unpack_cases.rs b/crates/core/src/update/tests/unpack_cases.rs index ed80286..f6188ee 100644 --- a/crates/core/src/update/tests/unpack_cases.rs +++ b/crates/core/src/update/tests/unpack_cases.rs @@ -197,6 +197,30 @@ fn a_macos_tar_gz_runs_the_whole_chain() { assert_eq!(std::fs::read(job.dest()).expect("读目标"), MACHO); } +/// Windows 的名字兜底:归档里的 `unlucky-launcher.exe` 带 **0644**(没有可执行位)也认。 +/// +/// 为什么需要这一条:`is_launcher_candidate` 是两条判据取并集(可执行位 **或** `.exe` 名字)。 +/// 发布矩阵里 Windows 发裸 `.exe`,可「清单给 win-* 挂一个归档」是合法输入,而 tar 里没有 unix +/// 权限位、`make_artifacts.py` 也只给启动器打 0o755——名字这条兜底不能是空转的。 +/// +/// 抓得住:把 `is_launcher_candidate` 里 `|| name.to_ascii_lowercase().ends_with(".exe")` 删掉, +/// 本用例立刻红(归档里一个候选都没有 ⇒ `LauncherMissing`)。 +#[test] +fn a_windows_exe_without_the_exec_bit_is_still_the_launcher() { + let stage = "unlucky-launcher_9.9.9_windows_x64"; + let archive = tar_gz(&[ + (&format!("{stage}/unlucky-launcher.exe"), PE, 0o644), + (&format!("{stage}/LICENSE"), b"license text", 0o644), + ]); + let target = Target::new(OsFamily::Windows, Arch::X64); + let (_dir, job) = fixture(target, "portable-zip", &archive); + let applied = job + .apply(&Version::parse("0.2.2").expect("版本"), Channel::Stable) + .expect("应当装上(名字判据兜住没有可执行位的那份)"); + assert!(matches!(applied, Applied::Installed { .. })); + assert_eq!(std::fs::read(job.dest()).expect("读目标"), PE); +} + /// 归档里没有本体(只有随包文本)⇒ `LauncherMissing`。抓得住「只认可执行位」这条判据被放松。 #[test] fn an_archive_without_a_launcher_is_refused() { diff --git a/crates/core/src/update/verify.rs b/crates/core/src/update/verify.rs index 8c3829e..64d4547 100644 --- a/crates/core/src/update/verify.rs +++ b/crates/core/src/update/verify.rs @@ -56,13 +56,20 @@ impl StagedPackage { &self.path } - /// 实际字节数(已与清单核对过,等于清单声明的 `size`)。 + /// 实际字节数(**这个文件**写出来的字节数)。 + /// + /// `stage` 产出的那份就是清单声明的 `size`(双检已核对);M308 起 `unpack` 还会用 + /// [`Self::adopt`] 造出「从归档里拆出来的本体」那一份,那时它是**本体**的字节数, + /// 不是归档的。跨卷复制那一步靠它核对该搬多少字节,所以这个字段必须始终是「本文件的」。 #[must_use] pub const fn size(&self) -> u64 { self.size } - /// 校验用的摘要。 + /// 为这个文件背书的那份摘要。 + /// + /// `stage` 产出的是清单里写的那条摘要(对**下载到的字节**算的);`adopt` 产出的(拆出来的本体) + /// 沿用**归档**的摘要——拆出来的字节由「归档过了长度+摘要双检」背书,本模块不为它另算一份。 #[must_use] pub fn integrity(&self) -> &Integrity { &self.integrity diff --git a/docs/update-trust-anchor.md b/docs/update-trust-anchor.md index 27454c7..1a0f13d 100644 --- a/docs/update-trust-anchor.md +++ b/docs/update-trust-anchor.md @@ -33,16 +33,20 @@ PinnedKey { key_id: "ull-release", public_key_hex: "1c3687ec…" } ## 当场看一眼(只读命令) ```bash -# ① 信任表里有几条(标记之间) -python3 packaging/sign_manifest.py --check-pinned-keyring - -# ② 就绪度播报(三行:表非空 / 环境里有钥且同源 / 最近一次发布痕迹) +# ① 就绪度播报:第一行就是「标记之间有几条 PinnedKey」,只读、不需要任何私钥 python3 packaging/check_signing_ready.py - -# ③ 直接读表 +# ② 直接读表(最原始的那一眼) sed -n '/PINNED-KEYS/,/PINNED-KEYS/p' crates/core/src/update/trust.rs ``` +`check_signing_ready.py` 的 ① 那一行是**读表**的正确入口:它按标记解析本文件、数条目并点名 +`key_id` 与公钥前缀。它的退出码是**就绪度**(① 表非空 **且** ② 环境里有同源私钥才 exit 0), +不是「表读到了没有」——没配 Secret 的机器上它照样打出 ① 那一行,只是整体 exit 1。 + +⚠ **别拿 `sign_manifest.py --check-pinned-keyring` 当读表工具**:它要私钥 +(`ULL_MANIFEST_SIGNING_KEY` 或 `--key <文件>`),语义是「**Secret 里那把钥**是否已钉住」这条 +**发布前门禁**;没钥时它直接 `error: 没有签名密钥…` + exit 1(M308 r1 实测)。想只读看表就用上面两条。 + `check_signing_ready.py` 的 ① 那一行的判据是「标记之间有几条 `PinnedKey`」,**不是**「表是不是 空的」——所以哪天有人把标记之间清空,它会立刻喊未就绪(那句文案里的「空表」说的是那一天)。 diff --git a/packaging/docs/signing-runbook.md b/packaging/docs/signing-runbook.md index cfbef27..158162a 100644 --- a/packaging/docs/signing-runbook.md +++ b/packaging/docs/signing-runbook.md @@ -17,7 +17,8 @@ M254 审计当时读到 `crates/core/src/update/trust.rs` 的 `PRODUCTION` 表在 `PINNED-KEYS` 标记之间是 **空的**(空表 = `KeyringEmpty` 一律拒装 ⇒ 用户端表现为「更新永远失败」)。**那条读数已作废**: commit `68362e5` 钉入 `ull-release` 之后,正式构建认得这把钥签的清单。下表「今天」一列也随之为 -历史(现在 `--check-pinned-keyring` 绿,其余两行取决于环境里有没有配 Secret)。已存在的部分(都能用): +历史:表**不是**空的了(只读读数见 `python3 packaging/check_signing_ready.py` 的 ① 行,今天打 +「就绪 —— 标记之间 1 条:ull-release」),其余两行取决于环境里有没有配 Secret。已存在的部分(都能用): | 环节 | 在哪 | 今天的状态 | |---|---|---|