247 lines
10 KiB
Markdown
247 lines
10 KiB
Markdown
# 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_<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`):
|
|
|
|
```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 <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 一键验签(推荐)
|
|
|
|
```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 端到端)
|