10 KiB
DevFlow 签名与发布 SOP
2026-10-10 制定。适用对象:上游发布者(你)。目标:让「签名 → 打包 → 发布清单 → 客户端自动升级」这条链可重复执行、可验证、密钥可控。 相关文档:《版本更新与自动升级-设计-2026-10-09.md》(链路设计)、《团队级基建-部署与运维-2026-10-05.md》(分发设施)。
一、密钥体系一句话
DevFlow 用 minisign / ed25519 非对称签名:
| 角色 | 文件 | 位置 | 作用 |
|---|---|---|---|
| 私钥(绝密) | devflow.key |
%USERPROFILE%\.tauri\ |
签名安装包,泄露=可伪造升级 |
| 公钥(公开) | devflow.key.pub |
同上 | 验签,同时硬编码进客户端 |
| 公钥副本 | pubkey 字段 |
src-tauri/tauri.conf.json plugins.updater.pubkey |
编译进 exe,客户端用它验签 |
核心约束:客户端 exe 里的公钥是编译期固化的。换密钥 = 换公钥 = 已分发的老客户端永远无法自动升级到新密钥签的包(只能手动重装)。所以密钥轮换是断链级操作,非必要不做。
二、密钥文件格式(排障必知)
Tauri 私钥文件是 base64 文本包 base64 的两层结构:
外层 base64 → 解码得文本,两行:
L1 untrusted comment: ...
L2 <内层 base64> → 解码得 payload 字节
payload 布局:
| 长度 | 类型 | 含义 |
|---|---|---|
| 106 B | 明文(未加密) | sig_alg(2)+keynum(8)+sk(64)+ck(32) |
| 158 B | 加密(Scrypt KDF 段) | sig_alg(2)+kdf_alg(2)+ck_alg(2)+salt(32)+opslimit(8)+memlimit(8)+keynum(8)+sk(64)+ck(32) |
⚠️
cargo tauri signer generate产出的都是 158 B(带 KDF 段)——即使你「没设密码」,结构上仍是加密布局,只是口令为空。所以不能用文件大小判断是否设了密码,必须用「空密码能否签名」实测(见 §六 自检)。
公钥 payload 固定 42 B:sig_alg(2)+keynum(8)+key(32)。keynum = payload[2:10],是密钥指纹,用于快速比对「配置公钥 ↔ 私钥 ↔ 签名」三者是否同一把钥匙。
三、密钥生命周期操作
3.1 生成(首次或轮换)
# 无密码(不推荐,当前生产就是这个状态)
cargo tauri signer generate -w "$env:USERPROFILE\.tauri\devflow.key"
# 带密码(推荐)
cargo tauri signer generate -p "<强口令>" -w "$env:USERPROFILE\.tauri\devflow.key"
生成后把 .pub 的内容填进 src-tauri/tauri.conf.json 的 plugins.updater.pubkey,然后重新编译客户端才生效。
3.2 备份(强制)
私钥丢失 = 无法再签发升级包 = 只能原地冻版。生成后立刻:
1. 复制 devflow.key / devflow.key.pub 到离线介质(U 盘 / 加密卷)
2. 记录密钥指纹(keynum),见 §六
3. 关键:口令单独记录(不要和私钥同处存储)
3.3 轮换(断链级,慎用)
代价:已分发的所有客户端自动升级链断裂 → 必须手动重装
步骤:
1. 生成新密钥对 → 旧密钥改名归档(不要删,历史包验签要用)
2. 更新 tauri.conf.json pubkey → 重新编译 → 发布「桥接版」
(桥接版仍用旧私钥签,老客户端能升上来;桥接版内含新公钥,之后再用新私钥签)
3. 桥接版铺开 ≥1 个版本周期后,才切新私钥签发
单管理员自用场景可简化:接受「一次手动重装」,直接换。对外分发则必须走桥接版。
四、发布流程(一键脚本)
cd E:\wk-lab\devflow
.\scripts\publish-release.ps1 # 完整发布(含 tauri build)
.\scripts\publish-release.ps1 -Notes "修复xx" # 手动指定更新说明
.\scripts\publish-release.ps1 -SkipBuild # 复用已有产物,只生成清单+上传
脚本六步(每步失败即 exit 1,不静默):
| 步 | 动作 | 失败含义 |
|---|---|---|
| 1 | 版本三源校验(Cargo.toml / tauri.conf.json / package.json) | 三处版本号不一致,先对齐(见 §七 坑 2) |
| 2 | 签名私钥就位检查(env 优先,回退 ~/.tauri/devflow.key) |
私钥缺失 |
| 3 | cargo tauri build(产出 devflow_<ver>_x64-setup.exe + .exe.sig) |
编译/打包失败 |
| 4 | 生成 latest.json(UTF-8 无 BOM) |
— |
| 5 | 上传 ssh-proxy → suke-dev:/var/www/app-site/download/devflow/ |
SSH/上传失败 |
| 6 | 远端 ls + cat latest.json 回显验证 |
上传未落盘 |
发布清单契约(latest.json):
{
"version": "0.1.2",
"notes": "...",
"pub_date": "2026-10-10T14:00:00Z",
"platforms": {
"windows-x86_64": {
"signature": "<.exe.sig 全文>",
"url": "https://app.1216.top/download/devflow/devflow_0.1.2_x64-setup.exe"
}
}
}
app.1216.top 已配 no-cache,改完即时生效,无需刷 CDN。
五、手工签名(脚本之外,补签/演练用)
# 无密码密钥
cargo tauri signer sign -f "$env:USERPROFILE\.tauri\devflow.key" -p "" --app-version 0.1.2 <file>
# 带密码密钥(口令走参数或环境变量,勿写进脚本)
cargo tauri signer sign -f "$env:USERPROFILE\.tauri\devflow.key" -p "<口令>" --app-version 0.1.2 <file>
# 环境变量方式(CI 友好)
$env:TAURI_SIGNING_PRIVATE_KEY = "$env:USERPROFILE\.tauri\devflow.key"
$env:TAURI_SIGNING_PRIVATE_KEY_PASSWORD = "<口令>"
务必带
--app-version:它把版本号写进签名的 trusted comment 并被签名覆盖。若客户端/服务端开启requireSignedVersion,清单里改版本号就会被拒——这是防「篡改清单把新版号配旧包」的关键绑定。cargo tauri build会自动带上,手工签要自己带。
六、验证(发布后必做)
6.1 一键验签(推荐)
node temp/verify_sig.mjs
输出:配置公钥 keynum / 每个 exe 的 keynum 匹配性 / ed25519 实际验签结果 / latest.json 与 .sig 内容一致性。
验签数学:payload 首 2 字节 ED 表示 blake2b512 预哈希模式,即 verify(blake2b512(file), sig);Ed 表示直接对原文。
6.2 密钥指纹自检
python temp/sign_state_check.py # 三源 keynum 对照
python temp/key_layout_final.py # 私钥格式(106/158B)判定
判据:配置公钥 keynum == 预期私钥对应 keynum。不等 = 拿错钥匙,签出来的包客户端一律拒。
6.3 「私钥是否设了密码」实测(唯一可信方法)
$probe = "$env:TEMP\pw_probe.txt"; "x" | Set-Content $probe -NoNewline
cargo tauri signer sign -f "$env:USERPROFILE\.tauri\devflow.key" -p "" "$probe" ; $LASTEXITCODE
# exit=0 → 无密码保护;exit≠0 且报 incorrect password → 有密码
七、踩坑记录(实证)
坑 1 — latest.json 的 UTF-8 BOM
Windows PowerShell 5.1 的 Out-File -Encoding utf8 / Set-Content -Encoding utf8 会写 BOM(EF BB BF)。JSON 解析器遇到 BOM 报 expected value at line 1 column 1,表现为「签名没错、包没错,updater 就是拉不到清单」。
✅ 修正:[System.IO.File]::WriteAllText($path, $json, (New-Object System.Text.UTF8Encoding($false)))。已固化进 publish-release.ps1。
坑 2 — 版本号三源分叉
版本号存在于三处:src-tauri/Cargo.toml(Rust crate / env!("CARGO_PKG_VERSION"))、src-tauri/tauri.conf.json(打包器实际使用)、package.json(前端)。
任一处漏改 → 设置页显示与实际安装版本不符(灵异现象)。
✅ 修正:publish-release.ps1 第 1 步硬校验三源一致,不一致直接拒发。2026-10-10 已修 package.json 0.1.1 → 0.1.2。
坑 3 — 发布脚本硬编码版本号
swap-install.ps1 曾硬编码 devflow_0.1.1_x64-setup.exe。升版忘改 → 若旧包残留同目录,静默装回旧版(伪成功,最危险)。
✅ 修正:改 glob 探测 + Sort-Object LastWriteTime -Descending | Select-Object -First 1。
坑 4 — 用文件大小判密码
cargo tauri signer generate 产出的私钥一律 158 B(带 Scrypt KDF 段),无密码时只是口令为空。按大小判断会误判。
✅ 修正:用 §6.3 空密码实签判定。
坑 5 — 密钥文件被误覆盖
2026-10-10 曾发生野密钥覆盖生产密钥,靠备份 *.bak-20261010-213046 恢复。
✅ 教训:动密钥前先归档(restore-key.ps1 的归档-恢复-校验三步式可复用);密钥目录加 ACL 防误改(见 §八)。
八、安全加固(已实施 2026-10-10)
威胁模型:任何能读到 devflow.key 的本地进程,都能签发被客户端信任的更新包 → 推恶意升级。
已实施的缓解(见 temp/harden_key_acl.ps1):
%USERPROFILE%\.tauri\ 阻断继承,仅 (当前用户 / SYSTEM)
%USERPROFILE%\.tauri\devflow.key* 阻断继承,仅 (当前用户 读写删 / SYSTEM 全控)
仍未消除的风险(重要):私钥当前无口令保护,文件本身即等价于签名能力;ACL 只挡了「非授权本地用户」,挡不住「以当前用户身份运行的程序」。
根治方案(需拍板,有代价):
| 方案 | 强度 | 代价 |
|---|---|---|
| A. 私钥加口令 | 中 | 必须重新生成密钥对(signer 无 reencrypt 子命令)→ 公钥变 → 换公钥 → 见 §3.3 断链(老客户端需手动重装) |
| B. 私钥迁离线介质 | 高 | 每次发布需插盘;CI 不可用 |
| C. A+B 组合 | 最高 | 同上,推荐对外分发场景 |
| D. 仅 ACL(现状) | 低 | 零代价,只防误改/误读 |
现状为 D。若 DevFlow 仅自用,D 可接受;一旦对外分发,建议升到 C。
九、应急:私钥泄露怎么办
1. 立即停发:删除远端 latest.json(客户端拉不到清单即停止升级链)
2. 评估:泄露时间窗内是否有未授权包被签(检查远端目录文件时间戳)
3. 轮换:按 §3.3 生成新密钥对 → 发布桥接版(旧私钥签)→ 铺开后切新私钥
4. 通告:对外分发场景需告知用户手动重装
注意:删除 latest.json 是唯一的「远端急停」手段——客户端只在 check 时拉清单,清单不在 = 无法升级。
十、日常检查清单
发布前:
- 版本三源一致(脚本第 1 步自动校验)
- 私钥就位且指纹与配置公钥一致(
node temp/verify_sig.mjs的 keynum 对照) - 密钥已备份(首次/轮换后)
发布后:
node temp/verify_sig.mjs全绿(验签 + latest.json 对账)- 远端
curl https://app.1216.top/download/devflow/latest.json可取且 version 正确 - 老客户端实测一次「检查更新 → 下载 → 安装 → 重启后版本号变新」(B3 端到端)