Files
Unlucky-Launcher/crates/core/src/update/trust.rs
T
AskaEth 528f5706a0 docs(m200): 补 update-test-key 的两处文档一致性(cherry-pick 之外)
`80b291c` 原样落盘(与本提交前一个 commit 逐字节相同),这里只补两处**文档一致性**,
不改任何判据、不改任何 feature 行为:

1. `crates/core/src/update/trust.rs`:`TEST_ONLY` 的文档注释还写着「**只**在 `cfg(test)` 与
   `update-test-source` 构建里存在」——同一文件里 `keyring()` 新增的表已经把三档(`cfg(test)` /
   `update-test-source` / `update-test-key`)讲清了,上面这句变成了自相矛盾的旧口径。改成三档,
   指向 `keyring()` 的说明。
2. `packaging/README.md` §12「文件清单」:新守门 `packaging/check_test_key_off.py` 已接进
   `release-guard.yml`,但清单表里没有它(同类守门 `check_test_source_off.py` 在表里),补一行。

验证:`cargo test --workspace --all-targets` exit 0(311 passed)、`cargo clippy --workspace
--all-targets -- -D warnings` exit 0、`cargo fmt --all --check` exit 0、
`packaging/check_test_key_off.py`(自检 + 正式扫描)exit 0、`packaging/check_workflows_yaml.py`
exit 0。
2026-11-09 07:56:33 +08:00

115 lines
6.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 钉进二进制的清单签名信任锚(keyring):**多把**公钥,`key_id` 只是查表用的标签。
//!
//! # 为什么是一张编译期常量表
//!
//! 自更新的信任根必须不可被「用户机器上的一枚文件」替换:清单与签名都能被能改 Release 的人一起
//! 换掉,唯一挡得住的是「客户端**编进二进制**的那几把公钥」。所以这张表是 `const`:
//!
//! * 改仓库里的本文件只会改变**下一次构建**的行为,不会改变任何**已发布二进制**的验签结果;
//! * 没有环境变量、没有设置键、没有「旁边的 keyring.json」这类运行期覆盖口子
//! (`tests/signature_cases.rs::no_runtime_keyring_override` 守这条)。
//!
//! # 为什么是「一组」而不是「一把」
//!
//! 换签名方(例如日后接 SignPath 的托管签名,或旧钥泄露后轮换)时,用户机器上装着的是**旧**二进制,
//! 它只认旧钥。做成数组才有两条路同时成立:新签出的清单可以同时带新旧两把钥的签名(旧二进制认旧的、
//! 新二进制认新的),以及新二进制可以一把一把地收紧。步骤见 `packaging/docs/signing.md`。
//!
//! # 唯一的加钥匙纪律:**旧钥签新钥**
//!
//! 往本表里加一把公钥,必须同时有「旧钥对新钥的背书」——即那次发布的清单同时带旧钥签名(见上),
//! 以及 changelog 里点名新钥的 `key_id` 与指纹。**不允许**在没有旧钥签名的情况下替换这张表:
//! 那等于让「能改仓库的人」变成「能改信任根的人」。
//!
//! # 正式构建的表当前是**空的**(fail-closed)
//!
//! 私钥只能来自仓库 Secret,本轮没有(也不该由代码仓库产生)私钥,所以正式表里一个条目都没有:
//! 任何正式构建拿到清单都会以 [`super::error::UpdateErrorKind::SignatureMissing`] 之类的分类拒绝更新,
//! 而 `packaging/sign_manifest.py --check-pinned-keyring` 会让发布流水线在「Secret 没配 / 公钥没钉」
//! 时**当场红**(宁可不出 Release,也不出未签名的 Release)。
//!
//! 录一把真钥的两步(细节见 `packaging/docs/signing.md`):
//!
//! ```text
//! python packaging/sign_manifest.py --generate-key --out-key release.key # ① 生成,私钥写进 Secret
//! # ② 把打印出来的 `PinnedKey { … }` 一行贴到下面 PINNED-KEYS 标记之间
//! ```
//!
//! 测试/验收构建另有一把**只在** `cfg(test)` 与 `update-test-source` feature 里存在的测试钥:
//! 它的私钥种子就写在测试夹具里、公开可见,因此它可以签出「本仓库自己认得」的清单,但对任何正式
//! 构建毫无意义(正式表里没有它)。这条也由 `packaging/check_test_source_off.py` 的同一道闸守着
//! ——正式发布流水线与打包脚本都不许打开 `update-test-source`。
use std::sync::OnceLock;
/// 一把钉死的公钥。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct PinnedKey {
/// 标签(清单签名里的 `key_id` 靠它查表;**改名就是换钥**)。
pub(crate) key_id: &'static str,
/// Ed25519 公钥的 64 位十六进制小写形态(32 字节)。
pub(crate) public_key_hex: &'static str,
}
/// 正式构建的信任锚(发布二进制里就是这一份)。
///
/// ── 加钥匙只改下面 `PINNED-KEYS` 标记之间的内容,别改标记本身 ──
/// (`packaging/sign_manifest.py --check-pinned-keyring` 按标记解析这一段,标记变了它就找不到表)。
pub(crate) const PRODUCTION: &[PinnedKey] = &[
// >>> PINNED-KEYS >>>
// <<< PINNED-KEYS <<<
];
/// 测试钥:**只**在 `cfg(test)` / `update-test-source` / `update-test-key` 三档构建里存在(正式表里没有它;
/// 三档的口径见下面 `keyring()`)。
///
/// 公钥由 `tests/support.rs` 里那串公开可见的 32 字节种子推出来(种子 = `ull-m190-test-key-seed-000000001`),
/// 两者一旦漂开,`tests/signature_cases.rs::pinned_test_key_matches_the_test_seed` 会当场红。
pub(crate) const TEST_ONLY: &[PinnedKey] = &[PinnedKey {
key_id: "test-only-key",
public_key_hex: "f3ea49a34ebda73d6500c9416f425e0358baf094b957baf3ea7daa6df63e876b",
}];
/// 本构建实际的信任锚(正式表 + 仅测试构建才有的那把测试钥)。
///
/// 三档「仅测试」的口径,逐档说明为什么需要它:
///
/// | 档 | 谁在用 | 为什么不能只靠 `cfg(test)` |
/// |---|---|---|
/// | `cfg(test)` | core 自己的单测 | 够用 |
/// | `update-test-source` | CLI 的端到端验收(`--features update-test-source`) | 那是**另一个 target**,`cfg(test)` 不生效 |
/// | `update-test-key` | `crates/presenter` 的 `[dev-dependencies]` | presenter 的更新链路用例编译 core 时既无 `cfg(test)`,也不该打开 `update-test-source`(那条会同时给出「本地目录当更新源」这条路)。这一档**只**加一把公开的测试公钥,不开任何数据源开关 |
///
/// 三者都进不了发布产物:`PRODUCTION` 之外没有任何路径能把 `TEST_ONLY` 带进 `--release` 构建
/// (官方构建三个 feature 都不开,且 `check_test_source_off.py` / `check_test_key_off.py` 守着)。
pub(crate) fn keyring() -> &'static [PinnedKey] {
static ALL: OnceLock<Vec<PinnedKey>> = OnceLock::new();
ALL.get_or_init(|| {
let mut keys = PRODUCTION.to_vec();
if cfg!(any(
test,
feature = "update-test-source",
feature = "update-test-key"
)) {
keys.extend_from_slice(TEST_ONLY);
}
keys
})
}
/// 十六进制解码(只收小写/大写十六进制,长度必须是偶数)。
pub(crate) fn decode_hex(text: &str) -> Option<Vec<u8>> {
if !text.len().is_multiple_of(2) {
return None;
}
let bytes = text.as_bytes();
let mut out = Vec::with_capacity(bytes.len() / 2);
let (pairs, _) = bytes.as_chunks::<2>();
for pair in pairs {
let hi = (pair[0] as char).to_digit(16)?;
let lo = (pair[1] as char).to_digit(16)?;
out.push(u8::try_from(hi * 16 + lo).ok()?);
}
Some(out)
}