Rust 桌面应用自托管自动更新:latest.json 清单 + minisign 签名校验的完整做法

本文详细介绍了基于latest.json清单与minisign签名校验的Rust桌面应用自托管自动更新完整链路,涵盖密钥生成与公钥嵌入、CI签名生成清单、客户端轮询比版、安全下载、强验签机制、平台差异化换包(Windows重命名回滚、macOS整包swap)及进程重启等七步。核心强调信任锚固化于二进制、防CDN投毒、验签失败即终止、失败可回滚等安全设计原则。

Rust 桌面应用自托管自动更新:latest.json 清单 + minisign 签名校验的完整做法

学习目标:学完这篇,你会给自己的 Rust 桌面应用接上一套不依赖 Tauri updater / electron-updater 的自托管自动更新——清单轮询、下载、验签、换包、重启,全链路自己可控,还能防 CDN 投毒。步骤给全,可直接照抄。

0. 先说问题

我的 boundless 是个 Rust 写的桌面白板(GPUI 框架),发版后用户手里的版本就凝固了。靠用户手动下载新包,更新率惨不忍睹。Tauri 和 Electron 都有官方 updater,但纯 Rust 写的桌面应用没有现成轮子——那就自己搭一条链路。

boundless 的做法是:一个 latest.json 清单 + minisign 签名,src/updater.rs 几百行 Rust 全链路打通,外加一份 139 行的配置手册 UPDATER_CONFIG.md。今天把这套做法拆开讲。

1. 前置知识:更新链路只有五步

动手前先把链路想清楚,自动更新归根结底是五步:查(有没有新版)→ 下(下载产物)→ 验(签名对不对)→ 换(替换旧文件)→ 起(重启)。每一步的失败处理想清楚了,链路就稳了。

再科普一个概念「minisign」:轻量签名工具,一对密钥,签名就是 4 行文本的 .sig 文件。公钥可以编译进二进制——这是整套方案信任锚的关键。

2. 第一步:生成密钥对,公钥 bake 进二进制

用仓库自带的 examples/gen_minisign_keypair.rs 生成密钥对。注意一个坑:必须用这个 helper 生成,别用 minisign CLI——两者密钥 KDF 不一致,用 CLI 生成的密钥,签名端(examples/sign_release.rs,同一个 minisign crate)解不开。

公钥写成常量编译进二进制:

pub const MANIFEST_URL: &str = "https://boundlesscdn.xi-n.com/boundless-latest.json";
pub const MINISIGN_PUBKEY: &str = "RWTgy8P9e5EU/l+TFMe/JOCA/Dkn+5dDYdQgQySZUl959/zDM+NnbK9X";

私钥只存在 CI 的 secret 里,这辈子不出 CI。还有个守卫函数:

pub fn signing_configured() -> bool {
    !MINISIGN_PUBKEY.contains("REPLACE_ME")
}

公钥还是占位符 REPLACE_ME 时,验签直接拒绝——没配好密钥的构建,更新功能就当不存在。

翻译成人话:验签的"根信任"钉死在二进制里,不经过网络。CDN 被投毒也装不上坏包。

3. 第二步:CI 签名产物,生成 latest.json

发版时 CI 跑 examples/sign_release.rs:用私钥给产物签名,生成 <产物>.sig,把签名内容嵌进 latest.json:

{
  "version": "0.4.8",
  "notes": "...",
  "pubdate": "...",
  "platforms": {
    "windows-x86_64": { "signature": "<.sig 的 4 行文本>", "url": "https://..." },
    "darwin-aarch64": { "signature": "<.sig 的 4 行文本>", "url": "https://..." }
  }
}

对应代码里的 Manifest / PlatformEntry 结构(src/updater.rs)。schema 兼容 Tauri updater 那套(version、notes、pubdate、按平台分的 signature + url),latest.json 托管在 CDN 上。平台 key 按系统区分:windows-x86_64、darwin-aarch64、darwin-x86_64,不支持的平台直接标 unsupported。

4. 第三步:客户端轮询清单,比版本

pub async fn fetch_manifest() -> Result<Manifest>

拉 latest.json 时,URL 后面拼 ?t={时间戳} 做缓存 bust——不然 CDN 缓存会让你"永远没新版"。is_newer() 做 semver 版本比较,current_version() 取编译进二进制的 CARGO_PKG_VERSION。

调试技巧:debug 构建认 BOUNDLESS_FAKE_VERSION 环境变量,可以假装自己是旧版本,把"标题栏图标 → 更新弹窗 → 重启提示"整条 UI 链路走一遍,不用真发版。

5. 第四步:下载到临时文件,带进度

pub async fn download<F>(url: &str, dest: &Path, progress: F) -> Result<()>

异步下载,带进度回调(给进度条用)。关键:先下到临时文件,不直接覆盖运行中的程序。下载和换包是两个阶段,中间隔着验签。

6. 第五步:验签,不通过绝不应用

pub fn verify(artifact: &Path, signature: &str) -> Result<()>

用编译进二进制的公钥验 .sig。签名不对、公钥是占位符,一律拒绝,更新流程到此为止,旧版本继续跑。

翻译成人话:这是整条链路最重要的一行纪律——验签失败就当没这回事,绝不"先装上再说"。自动更新最大的风险不是"更不上",是"更上了坏东西"。

7. 第六步:平台差异化换包

pub fn apply(artifact: &Path) -> Result<()>
  • Windows:运行中的 exe 不能被覆盖,但可以被改名。做法:从 zip 里解出新 exe 放到临时位置 → 把运行中的 exe 改名成 .old → 把新 exe 移进来 → 移动失败就回滚(把 .old 移回来,保证应用还能启动)。.old 下次启动时由 cleanup_old() 清理。
  • macOS:不能替换运行中的文件,那就整包 swap——把新的 Boundless.app 解到同级临时目录,整个目录换掉旧包。
  • 其他平台:直接 bail “not implemented”。不支持就是不支持,不装假。

8. 第七步:重启,退出旧进程

pub fn restart() -> Result<()>

起新进程,然后 std::process::exit(0)。新旧交接完成,用户下次看到的就是新版本。

9. 为什么这样设计

  1. 公钥编译进二进制:信任锚不经过网络,这是防投毒的根本。
  2. macOS 整包 swap:系统限制绕不过去,就顺着它——换整个 .app 而不是单个文件。
  3. Windows 两步换包 + 回滚:rename 旧文件、move 新文件,中间失败能回到旧版本,应用永远可启动。
  4. 占位符守卫:密钥没配好的构建,更新功能静默不存在,而不是带着假信任跑。

10. 避坑清单

  1. 密钥用配套 helper 生成,别用 minisign CLI(KDF 不一致,签名端解不开)。
  2. latest.json 一定要加缓存 bust,不然 CDN 缓存让你"永远没新版"。
  3. 验签失败的处理是"当没这回事",不是重试——签名不对重试 100 次也不对。
  4. Windows 换包顺序别反:先 rename 旧的,再 move 新的,失败回滚。直接覆盖运行中的 exe 会失败。

11. 总结

自托管自动更新七步:密钥对(公钥 bake 进二进制)→ CI 签名 + latest.json → 客户端轮询比版本 → 下载到临时文件 → 验签(不过就停)→ 平台差异化换包 → 重启。核心思想一句话:信任锚钉死在二进制里,失败时永远能回到可启动的旧版本。

给开发者的建议:如果你也在用 Rust 写桌面应用(GPUI、egui、iced 都一样),这套链路可以直接抄——updater.rs 的结构是平台无关的,换掉产物格式和平台分支就能用。

项目开源在 GitHub:tsingliuwin/boundless,觉得有用欢迎点个 star。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值