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
@@ -0,0 +1,101 @@
|
||||
// PE 版本资源(VERSIONINFO)的唯一口径:发布者/产品/版权/版本号。
|
||||
//
|
||||
// 这个文件被 `crates/cli/build.rs` 与 `crates/app-iced/build.rs` 各 `include!` 一次——两个产物
|
||||
// (`ull` 与 `unlucky-launcher-iced`)的署名必须逐字相同,复制两份表迟早会漂。
|
||||
//
|
||||
// # 字段口径(M190,塔已拍板)
|
||||
//
|
||||
// | 字段 | 值 | 为什么 |
|
||||
// |---|---|---|
|
||||
// | `CompanyName` | `Unlucky Launcher Project` | 发布者。**不得出现个人真实姓名**:这个名字随每个下载的 exe 传播,将来买 Authenticode 证书时 SmartScreen 显示的是证书主体,二者要对得上 |
|
||||
// | `ProductName` / `FileDescription` | `Unlucky Launcher` | Windows「属性 → 详细信息」里最显眼的两行 |
|
||||
// | `LegalCopyright` | `© 2026 Unlucky Launcher Project · Apache-2.0` | 版权 + 许可(仓库是 Apache-2.0) |
|
||||
// | `FileVersion` / `ProductVersion` | `CARGO_PKG_VERSION` 的四段式 | 与 `Cargo.toml` **同源**:不另写一份版本号 |
|
||||
// | `InternalName` / `OriginalFilename` | bin 名 / `<bin>.exe` | 供「这个 exe 到底叫什么」的取证 |
|
||||
// | `Comments` | 仓库地址 | 项目署名 `AskaEth` 的可读落点(GitHub 命名空间,不是个人姓名) |
|
||||
//
|
||||
// # 只在 windows 目标上真跑
|
||||
//
|
||||
// 版本资源是 PE 概念:`build.rs` 先看 `CARGO_CFG_TARGET_OS`,不是 `windows` 就整段返回,
|
||||
// Linux/macOS 构建一个字节都不多编译(它们的发布产物本来也没有 PE 头)。
|
||||
//
|
||||
// # 为什么不用 `windows_subsystem` 之外的资源
|
||||
//
|
||||
// 只要 VERSIONINFO 一块:图标、清单(DPI/权限)另有归属,本 mission 不碰(加了会改变运行时行为)。
|
||||
//
|
||||
// # 守门
|
||||
//
|
||||
// `packaging/check_pe_version.py` 是「写进源码不算,读回产物才算」的那道判据:它直接读 exe 字节,
|
||||
// 断言 `VS_VERSION_INFO` 结构在、上述字符串(UTF-16LE)在。CI 的构建槽位跑它。
|
||||
|
||||
/// 发布者(CompanyName)。**改它等于改署名**:同时要改 Authenticode 证书主体与 README 的署名口径。
|
||||
pub const COMPANY_NAME: &str = "Unlucky Launcher Project";
|
||||
|
||||
/// 产品名(`ProductName` 与 `FileDescription`)。
|
||||
pub const PRODUCT_NAME: &str = "Unlucky Launcher";
|
||||
|
||||
/// 版权行(`LegalCopyright`)。
|
||||
pub const COPYRIGHT: &str = "© 2026 Unlucky Launcher Project · Apache-2.0";
|
||||
|
||||
/// 项目署名(GitHub 命名空间;`Comments` 里随仓库地址一起出现)。
|
||||
pub const PROJECT_HANDLE: &str = "AskaEth";
|
||||
|
||||
/// 仓库地址(`Comments`)。
|
||||
pub const REPOSITORY: &str = "https://github.com/AskaEth/Unlucky-Launcher";
|
||||
|
||||
/// 把版本号补成四段式的数值(`0.2.3` → `0.2.3.0`;预发布段丢掉,因为 PE 的数值字段只认数字)。
|
||||
///
|
||||
/// 返回 `(四段元组, 打包成 FILEVERSION 用的 u64)`;任一数字大于 65535 时按 65535 截断
|
||||
/// (PE 的每个分量只有 16 位,真实版本号远小于它)。
|
||||
#[must_use]
|
||||
pub fn four_part(version: &str) -> ([u64; 4], u64) {
|
||||
// `0.2.3-beta.1` 的数值部分只有前三段;`+build` 元数据同理不计入版本资源。
|
||||
let numeric = version.split(['-', '+']).next().unwrap_or(version).trim();
|
||||
let mut parts = [0_u64; 4];
|
||||
for (index, piece) in numeric.split('.').take(4).enumerate() {
|
||||
parts[index] = piece.parse::<u64>().unwrap_or(0).min(0xFFFF);
|
||||
}
|
||||
let packed = (parts[0] << 48) | (parts[1] << 32) | (parts[2] << 16) | parts[3];
|
||||
(parts, packed)
|
||||
}
|
||||
|
||||
/// 给一个 bin 名(`ull` / `unlucky-launcher-iced`)写出版本资源。
|
||||
///
|
||||
/// 由两个 crate 的 `build.rs` 调用;非 windows 目标直接返回。
|
||||
pub fn emit_pe_version_resource(binary_name: &str) {
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
println!("cargo:rerun-if-env-changed=CARGO_PKG_VERSION");
|
||||
// 共享口径文件改了也要重编(`include!` 不被 cargo 追踪)。
|
||||
println!("cargo:rerun-if-changed=../../packaging/pe_version.rs");
|
||||
|
||||
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("windows") {
|
||||
// Linux/macOS:没有 PE 头,整段不跑(发布产物的形状不变)。
|
||||
return;
|
||||
}
|
||||
|
||||
let version = std::env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| "0.0.0".to_string());
|
||||
let (parts, packed) = four_part(&version);
|
||||
let dotted = format!("{}.{}.{}.{}", parts[0], parts[1], parts[2], parts[3]);
|
||||
|
||||
let mut resource = winresource::WindowsResource::new();
|
||||
resource.set("CompanyName", COMPANY_NAME);
|
||||
resource.set("ProductName", PRODUCT_NAME);
|
||||
resource.set("FileDescription", PRODUCT_NAME);
|
||||
resource.set("LegalCopyright", COPYRIGHT);
|
||||
resource.set("InternalName", binary_name);
|
||||
resource.set("OriginalFilename", &format!("{binary_name}.exe"));
|
||||
resource.set("Comments", REPOSITORY);
|
||||
// 字符串形态(「属性」页显示的)与数值形态(资源管理器/安装器排序用的)都要给。
|
||||
resource.set("FileVersion", &dotted);
|
||||
resource.set("ProductVersion", &dotted);
|
||||
resource.set_version_info(winresource::VersionInfo::FILEVERSION, packed);
|
||||
resource.set_version_info(winresource::VersionInfo::PRODUCTVERSION, packed);
|
||||
|
||||
if let Err(error) = resource.compile() {
|
||||
// 资源编不出来就**中断构建**:宁可不出产物,也不出一个「属性里什么都没有」的 exe
|
||||
// (那正是 M190 要修的病)。
|
||||
panic!("写 PE 版本资源失败({binary_name} {dotted}):{error}");
|
||||
}
|
||||
println!("cargo:warning=PE VERSIONINFO: {binary_name} {dotted} / {COMPANY_NAME}");
|
||||
let _ = PROJECT_HANDLE;
|
||||
}
|
||||
Reference in new issue
Block a user