feat(m190): 产物署名(PE VERSIONINFO)+ 清单 Ed25519 多签名与更新前验签
两件事:让「这是谁发的」在产物与清单上都留下可验证的痕迹。
① PE 版本资源(两个 bin):`packaging/pe_version.rs` 是唯一口径(CompanyName
「Unlucky Launcher Project」/ ProductName、FileDescription「Unlucky Launcher」/
LegalCopyright「© 2026 … · Apache-2.0」/ 四段式版本号与 Cargo.toml 同源),被
`crates/{cli,app-iced}/build.rs` 各 include 一次;非 windows 目标整段不执行。
判据是读产物字节的 `packaging/check_pe_version.py`(CI 的 Windows 槽位)。
② 清单签名:`manifest.json` 旁边多一份 `manifest.json.sig`(分离签名,签的是清单**原始
字节**),信封是 `{format, signatures:[{alg,key_id,sig,manifest_sha256}]}` —— 多签名数组,
任意一条命中钉住的钥即放行(换钥窗口)。信任锚是 `crates/core/src/update/trust.rs`
里的编译期公钥表(正式表为空 = fail-closed,测试钥只在 cfg(test)/update-test-source 里)。
管线顺序改成「先验签、再解析」:`UpdateSource` 只交原文 + `.sig`,`verified_manifest`
是唯一收口点,`Job::apply` 因此在取包**之前**就拒。新增五类错误分类
(signature_missing / malformed / unknown_key / keyring_empty / invalid ),全部不可重试,
CLI 的 hint 单独说人话。
③ Ed25519 验签自带实现(`update/ed25519/*`,零新依赖):scope 不含 core 的 Cargo.toml,
用 RFC 8032 §7.1 向量 + 可锻/小阶/非规范编码的拒绝用例钉住;签名侧是
`packaging/sign_manifest.py`(纯 Python 参考实现,零 pip 依赖,`--self-test` 同向量)。
④ 守门:`release-publish.yml` 加签名步(`set +x`,只回显公钥;没 Secret / 钥没钉 / 签不上
一律红 ⇒ 不出 Release);`release-build.yml` 加 PE 资源读回;`release-guard.yml` 跑两个
脚本的 `--self-test`。密钥纪律与轮换(旧钥签新钥)写在 `packaging/docs/signing.md`。
Cargo.lock:新增 winresource(build-dependency)与 toml 系;toml_datetime 1.1.2→1.1.3 是
toml 1.2.1 的硬要求(`cargo update --precise 1.1.2` 已实测被拒)。
This commit is contained in:
1 parent
44a0c87a11
commit
b7d9b99e12
34 files changed
+2885
-104
No files matched your search
@@ -81,6 +81,65 @@ fn asset_name(version: &str) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
/// 测试签名钥的种子(**公开**:只对 `update-test-source` 构建有意义,正式信任表里没有这把钥,
|
||||
/// 见 `crates/core/src/update/trust.rs::TEST_ONLY`)。
|
||||
const TEST_KEY_SEED: &str = "ull-m190-test-key-seed-000000001";
|
||||
/// 测试钥的 `key_id`(与 `trust.rs::TEST_ONLY` 一致)。
|
||||
const TEST_KEY_ID: &str = "test-only-key";
|
||||
|
||||
/// 用 `packaging/sign_manifest.py` 给刚落盘的清单签一份 `.sig`(M190)。
|
||||
///
|
||||
/// 为什么走**真脚本**而不是在测试里再写一份签名:这样这套端到端用例同时是「发布侧签名脚本 ⇒
|
||||
/// 真二进制验签」的跨实现证据(Python 签、Rust 验)。脚本零第三方依赖,CI runner 与开发机都自带
|
||||
/// python3/python;找不到 python 时这套用例**直接红**(不静默跳过——跳过等于这条链没有证据)。
|
||||
fn write_signature(manifest_path: &Path) {
|
||||
let script = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../packaging/sign_manifest.py");
|
||||
assert!(script.is_file(), "找不到签名脚本:{}", script.display());
|
||||
let seed_hex: String = TEST_KEY_SEED
|
||||
.as_bytes()
|
||||
.iter()
|
||||
.map(|byte| format!("{byte:02x}"))
|
||||
.collect();
|
||||
let secret = format!(r#"{{"key_id":"{TEST_KEY_ID}","seed_hex":"{seed_hex}"}}"#);
|
||||
let mut command = None;
|
||||
for candidate in ["python3", "python"] {
|
||||
if std::process::Command::new(candidate)
|
||||
.arg("--version")
|
||||
.output()
|
||||
.is_ok_and(|output| output.status.success())
|
||||
{
|
||||
command = Some(candidate);
|
||||
break;
|
||||
}
|
||||
}
|
||||
let python = command.unwrap_or_else(|| panic!("这套用例需要 python3/python 跑 {script:?}"));
|
||||
let output = std::process::Command::new(python)
|
||||
.arg(&script)
|
||||
.arg("--sign")
|
||||
.arg(manifest_path)
|
||||
.env("ULL_MANIFEST_SIGNING_KEY", &secret)
|
||||
.output()
|
||||
.expect("跑签名脚本");
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"签名脚本失败:stdout={} stderr={}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
assert!(
|
||||
manifest_path
|
||||
.with_file_name(format!(
|
||||
"{}.sig",
|
||||
manifest_path
|
||||
.file_name()
|
||||
.map(|name| name.to_string_lossy().into_owned())
|
||||
.unwrap_or_default()
|
||||
))
|
||||
.is_file(),
|
||||
"签名脚本没产出 .sig"
|
||||
);
|
||||
}
|
||||
|
||||
/// 一份与 `packaging/tests/manifest.golden.json` **同字段**的清单。
|
||||
///
|
||||
/// 字段一个不多一个不少:core 的 `deny_unknown_fields` 会把多余字段当清单损坏拒掉,所以这条
|
||||
@@ -258,11 +317,11 @@ impl Fixture {
|
||||
/// 发一版,摘要/长度由调用方给(篡改用例用)。
|
||||
pub fn publish(&self, sha256: &str, size: u64) {
|
||||
std::fs::write(self.dist.path().join(&self.asset), &self.payload).expect("写产物");
|
||||
std::fs::write(
|
||||
self.dist.path().join("stable.json"),
|
||||
manifest(&self.version, &self.asset, size, sha256),
|
||||
)
|
||||
.expect("写清单");
|
||||
let path = self.dist.path().join("stable.json");
|
||||
std::fs::write(&path, manifest(&self.version, &self.asset, size, sha256)).expect("写清单");
|
||||
// M190:清单与签名成对落盘。篡改用例给的是「错的 sha/size」,签的是**改过之后**那份清单
|
||||
// —— 那正是「清单本身可信、包体不对」的现场(摘要判据要独立于签名判据被证到)。
|
||||
write_signature(&path);
|
||||
}
|
||||
|
||||
/// 跑一条 `update` 子命令:`--data-dir` 指向临时数据根(与 GUI 一样由数据根派生暂存目录)。
|
||||
|
||||
Reference in new issue
Block a user