跳转至

ADR-0014:密钥生命周期——一对共用、备钥离线、双公钥轮换

日期:2026-07-24 状态:已接受

修订关系:细化 ADR-0003 中"私钥进 Jenkins 凭据库、预埋备用公钥"的具体规程;关闭开放问题 Q7

这份记录在讲什么

Ed25519 私钥就是热修通道的签发印章:盖过章的热修包,全网客户端才认。ADR-0003 定了"印章放 Jenkins、客户端预埋一枚备用印文",但怎么刻、怎么存、怎么换一直没写。这份记录把三件事定死。

决定一:全平台共用同一对密钥

实际有两处要盖章:Windows 侧我们自己验的清单/文件签名,和 macOS 侧 Sparkle 验的更新包签名(SUPublicEDKey,恰好同为 Ed25519,见 ADR-0010)。共用同一对密钥,不各刻一枚。

理由:两处属于同一个信任域(都是"官方热修"这一件事)、同一个保管地(Jenkins)、同一套轮换流程。刻两枚章不增加真实安全——Jenkins 被攻破时两枚一起丢——只把管理复杂度翻倍。

决定二:备用私钥离线封存,绝不进 Jenkins(要害)

生成与存放的规程:

  • 生成在离线机器上做(断网环境,用 Sparkle 自带的 generate_keys 或 libsodium),一次生成两对(主 + 备);
  • 主私钥导入 Jenkins 凭据库,日常签名用(使用权限已由 ADR-0013 的"权限收紧 + 操作留痕"覆盖);
  • 备用私钥加密后离线封存(加密介质 + 实体保险柜,口令与介质分开保管),永不进 Jenkins

为什么这条是要害:轮换机制的全部价值建立在"主钥被端时备钥还是干净的"。如果备用私钥也放进 Jenkins,攻击者端掉 Jenkins 就主备一锅端——轮换形同虚设,等于没有备份。

决定三:客户端双公钥验签,轮换当天生效、不用等发版

  • 客户端验签逻辑:先试主公钥,不过再试备用公钥(两枚印文都已预埋进二进制,ADR-0003)。
  • 轮换流程(怀疑主私钥泄露当天即可执行):取出离线备用私钥 → 导入 Jenkins → 此后签名全部改用它 → 飞书广播全员。在网客户端无感切换,不需要任何升级。
  • 收尾:下一个完整版本把泄露的公钥从客户端删掉、再预埋一枚新的备用——印文滚动,永远保持"一主一备"。
  • 丢失场景(主私钥丢了但未泄露):同样切备用,无伤;这也是为什么备份必须真实存在且真实离线。

这么定之后会怎样

  • Q7 关闭,只剩一个纯事务项线下定:保险柜的实体位置与保管人。
  • CI 侧无新增改造:签名 stage 从 Jenkins 凭据库取当前生效的私钥,主备切换对流水线透明。
  • 客户端验签从"单公钥"改为"公钥列表按序尝试",属决策引擎的既有验签环节的小扩展。