# 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 生成(首次或轮换) ```powershell # 无密码(不推荐,当前生产就是这个状态) 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 个版本周期后,才切新私钥签发 ``` > 单管理员自用场景可简化:接受「一次手动重装」,直接换。对外分发则**必须走桥接版**。 --- ## 四、发布流程(一键脚本) ```powershell 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__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`): ```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。 --- ## 五、手工签名(脚本之外,补签/演练用) ```powershell # 无密码密钥 cargo tauri signer sign -f "$env:USERPROFILE\.tauri\devflow.key" -p "" --app-version 0.1.2 # 带密码密钥(口令走参数或环境变量,勿写进脚本) cargo tauri signer sign -f "$env:USERPROFILE\.tauri\devflow.key" -p "<口令>" --app-version 0.1.2 # 环境变量方式(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 一键验签(推荐) ```powershell node temp/verify_sig.mjs ``` 输出:配置公钥 keynum / 每个 exe 的 keynum 匹配性 / **ed25519 实际验签结果** / `latest.json` 与 `.sig` 内容一致性。 验签数学:payload 首 2 字节 `ED` 表示 **blake2b512 预哈希**模式,即 `verify(blake2b512(file), sig)`;`Ed` 表示直接对原文。 ### 6.2 密钥指纹自检 ```powershell python temp/sign_state_check.py # 三源 keynum 对照 python temp/key_layout_final.py # 私钥格式(106/158B)判定 ``` 判据:**配置公钥 keynum == 预期私钥对应 keynum**。不等 = 拿错钥匙,签出来的包客户端一律拒。 ### 6.3 「私钥是否设了密码」实测(唯一可信方法) ```powershell $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 端到端)