你的更新 pipeline 显示绿色,bundle 已部署到边缘节点,设备也在检查更新。然后有人发现生成的 JavaScript 中存在可疑变更。被入侵的 build runner、被盗的开发者凭证或被篡改的 artifact,可能在测试结束后将恶意 payload 混入发布版本。如果没有 签名验证,客户端无法可靠地区分团队构建的 bundle 和攻击者在传输或交付层篡改过的 bundle。
签名更新改变了这一决策。设备在替换当前运行的 code 之前,会使用受信任的公钥验证 bundle。如果签名不匹配,更新将保持非活动状态。这听起来很简单,但生产环境中的故障通常发生在密码学周边,而非其内部。团队往往丢失密钥轮换记录、签错了 artifact、应用了未经验证的缓存文件,或者收集了太少的遥测数据,导致无法解释哪些设备拒绝了更新。
以下章节聚焦于这些操作边缘,从信任模型和验证流程,到 CI/CD 控制、事件响应,以及签名本身无法解决的局限性。
目录
- 为什么签名验证能防止灾难性更新
- 密码学签名如何建立信任
- 在移动和 Web 系统中的实际应用
- 构建完整的验证流程
- 大多数团队容易低估的密钥管理挑战
- 将验证集成到 CI/CD 和监控中
- 安全最佳实践与常见误区
签名验证如何防止灾难性更新
某热门 Capacitor 插件的 CI/CD 流水线在发布后期遭到入侵。攻击者篡改了 live update bundle,植入了读取应用数据的 code,并通过流水线的正常分发路径发布了该产物。设备端不会看到陌生的下载域名,只会看到一个等待安装的有效更新。
如果没有客户端加密校验,应用可能在更新策略允许时立即下载并执行被篡改的 bundle。潜在影响范围包括数据泄露、凭证窃取、支付流程篡改以及业务逻辑的静默操纵。调查过程也极为痛苦。工程师必须确定分发的是哪个产物、哪些渠道接收了它、哪些设备下载了它、哪些设备应用了它,以及恶意的 code 是否在撤回发布前已执行。

启用签名验证的团队会面临不同的故障模式。应用下载了同样的被污染 bundle,计算预期摘要,并对照可信密钥检查附带的签名。加密校验失败,更新器拒绝激活该 bundle,并在新的 code 运行前向安全团队发送事件。
生产环境规则: 在设备验证其身份和确切字节之前,应将更新视为具有敌意。
只有当验证器在设备上运行,且在提取或激活之前执行,并且可信密钥无法被更新本身替换时,保护才有效。这使得证书和密钥管理成为更新设计的一部分,而非行政细节。使用 Capacitor 的团队应将其与 证书管理流程一起记录,包括谁可以签名、密钥存储在哪里,以及客户端如何获知授权的后继密钥。
几十年来,现代研究一直将签名验证视为一门可衡量的工程学科。首批关于离线和在线签名验证的公开研究出现在 1977 年,后续工作扩展到了包括 HMM 和 FFT 在内的方法。一项被广泛引用的比较报告称,人类专家的 误接受率约为 0.5%,误拒绝率为 7%,而外行人的 误接受率为 6.5%,误拒绝率为 26%,正如 这是签名验证研究的历史概览。这些数字涉及手写签名,而非软件更新,但它们强化了一个有用的观点:验证质量取决于验证器、其参考数据及其决策策略。
加密签名如何建立信任
把发布 bundle 想象成一个密封的信封。构建系统会计算精确字节的摘要,并使用一个 私钥 为该摘要创建数字签名。App 会包含或安全地接收对应的 公钥,它就像信封上已知的徽记。如果攻击者改动了 bundle 的哪怕一小部分,App 计算出的摘要就会不同,签名便不再通过验证。

该流程包含四个不同环节:
- 哈希 将 bundle 转换为固定长度的摘要。在这一完整性校验步骤中,SHA-256 和 SHA-512 是常用的选择。
- 密钥生成 创建非对称密钥对。私钥用于签名,公钥用于验证。
- 签名 将摘要与发布元数据绑定,最好包含版本、channel、平台和受众信息。
- 验证 从下载的字节重新计算摘要,并检查签名是否由受信任的私钥生成。
验证器必须将签名绑定到更新器将要应用的同一 payload。如果应用后续下载 bundle 时未检查 manifest 的哈希是否与 bundle 匹配,仅对 manifest 进行签名是不够的。同样,验证 bundle 的哈希并不能确定谁授权了它,除非哈希本身经过认证。
为移动端分发选择算法
RSA 依然熟悉且支持广泛,但通常需要更大的密钥材料和谨慎的填充选择。对于新的移动端更新协议,Ed25519 往往更具吸引力,因为其密钥和签名紧凑,且在移动处理器上的验证路径高效。当兼容性要求使得 RSA 成为必需时,RSA-PSS 也是合适的选择。选择应遵循平台加密库、支持的硬件、互操作性要求以及迁移计划,而不是从其他环境复制的基准测试。
一个有用的独立介绍是这份指南: 理解区块链中的加密签名。交易上下文与 OTA 分发不同,但关于私钥授权和公钥验证的解释可以直接迁移。
信任链与固定密钥
证书链通过中间机构将信任从根证书委托给叶子证书。该模型可以简化广泛的 PKI 操作,但应用更新客户端通常有更窄的需求:仅信任授权此更新的发布者密钥。将公钥或一组授权密钥直接嵌入应用二进制文件中是一种固定(pinning)形式。它减少了对外部证书机构的依赖,但会产生轮换问题,因为二进制文件必须已经信任替换密钥。
保持签名信封完整。您的发布元数据应标识工件、其摘要、目标通道以及密钥标识符。将此功能集成到 Capacitor 中的团队可以使用一个专注于 Capacitor 应用的令牌签名检查清单 在发布前审查密钥范围、存储和验证边界。
在移动和 Web 系统中的实际应用
签名验证出现在移动产品的多个层级中,每个层级回答不同的问题。应用商店签名帮助操作系统判断可安装包是否来自授权发布者。签名的 OTA bundle 回答 JavaScript 和资源负载是否来自更新器信任的发布权限。JWT 签名帮助 API 验证令牌是否由预期的身份服务颁发。
混淆这些层级会产生安全缺口。有效的 IPA 签名并不能自动认证后续的 web bundle。有效的 JWT 无法证明更新包是安全的。TLS 连接保护传输过程,但当 CDN、代理、缓存或构建系统成为篡改源头时,它无法替代产物签名。
| 上下文 | 签名机制 | 防止的故障模式 | 常见缺口 |
|---|---|---|---|
| 应用商店包 | 平台 code 签名及平台审核控制 | 重新签名或未授权的可安装包 | 团队假设商店签名覆盖了安装后的 web 资源 |
| Web bundle 和服务工作者 | 签名交换或 SRI 风格的完整性引用 | 被投毒的 CDN 响应或修改后的资源 | 仅检查入口文件,而导入的资源保持未验证状态 |
| API 令牌 | 使用受信任的公钥验证 JWT 签名,该公钥通常通过 JWKS 端点获取 | 伪造或篡改的 token | 服务器验证了签名,但忽略了签发者、受众、过期时间或 token 用途 |
| 空中更新 | 在设备上验证分离式或嵌入式 bundle 签名 | 中间人攻击或篡改的更新注入 | 客户端在执行决策前下载、缓存或解包内容 |
对于 Web 资源,子资源完整性(SRI)可以限制浏览器接受引用资源的内容,但它并不能自动解决动态导入、Service Worker 缓存或指向攻击者选定文件的更新清单问题。实现必须定义完整的工件集并验证将要执行的字节。
JWT 部署以不同的方式失败。工程师通常发布了正确的公钥,但接受了具有错误签发者或受众的 token,或者信任了由 token 头提供的算法选择。加密签名可能是有效的,但授权决策仍然是错误的。
对于依赖频繁发布和面向客户的移动体验的产品,运营环境也很重要。正在评估的 2026 年零售应用用户参与策略 应将更新完整性视为快速实验的前提条件。只有当发布渠道、制品和接收方都绑定到同一授权决策时,快速发布才有意义。
构建完整的验证流程
生产环境的更新器应将验证作为关卡,而不是在安装附近某处运行的回调。安全的顺序是确定性的:
- 通过经过身份验证的传输获取清单和签名。
- 验证清单结构、版本策略、渠道、有效期和制品身份。
- 下载清单中指定的确切 bundle。
- 在本地计算 bundle 摘要。
- 使用受信任的 Ed25519 或 RSA-PSS 公钥验证签名。
- 将已验证的制品存储在隔离位置。
- 原子性地应用它,然后保留回滚路径。

清单必须绑定所有影响决策的值。至少包括 bundle 摘要、版本、channel、平台以及密钥标识符。切勿允许下载器在验证后替换 URL、文件名或 channel。验证器应接收不可变的字节和不可变的元数据,然后返回单一的接受或拒绝结果。
简化的 TypeScript 结构如下:
type UpdateManifest = {
version: string
channel: string
platform: string
sha256: string
signature: string
keyId: string
}
async function verifyBundle(
bundle: Uint8Array,
manifest: UpdateManifest,
trustedKeys: Map<string, Uint8Array>
): Promise<boolean> {
const publicKey = trustedKeys.get(manifest.keyId)
if (!publicKey) return false
const digest = await sha256(bundle)
if (!constantTimeEqual(digest, hexToBytes(manifest.sha256))) {
return false
}
try {
return await ed25519Verify(
base64ToBytes(manifest.signature),
digest,
publicKey
)
} catch {
return false
}
}
该示例故意设置得较为严格。格式错误的 base64、未知的密钥标识符、摘要不匹配或签名失败都必须导致拒绝。下载过程中的超时不是应用之前部分文件的理由。请删除不完整的工件,保留最后已知良好的版本,并在有界策略下重试。
防止竞态条件和回滚错误
下载到临时路径。关闭并刷新文件,验证其完整内容,然后将其重命名到版本化的已验证存储中。激活步骤应仅引用该已验证路径。在 Capacitor 和 Electron 环境中,避免让异步下载完成事件独立于验证 Promise 触发激活。单个更新状态机应拥有诸如 downloading, verified, pending, active, rejected,以及 rolled_back.
恒定时间比较适用于敏感字节序列的比对,尤其是当攻击者能观察到重复的验证行为时。从运维角度看,更重要的是不要暴露一个捷径,仅因之前的尝试将版本标记为可用就接受该 bundle。可用性和真实性是两种独立的状态。
本指南中介绍的 Capacitor 更新的完整性检查 是保持哈希验证与激活逻辑分离的有用实现参考。请刻意测试失败分支,包括截断文件、格式错误的签名、未知密钥、过期的 manifest、重复版本以及激活过程中的进程终止。
关于自动签名验证的研究表明,在其他领域中阈值和参考数据为何重要。1994 年的一项在线研究测试了 22 个特征,选出了最佳的 10,并报告 99.5% 使用欧几里得距离方法正确分类了真实签名,同时拒绝了 86% 的伪造签名,根据 发表的统计方法研究。软件更新验证是确定性的,而非生物识别的,但这一教训依然相关:精确定义输入和决策边界。
大多数团队容易低估的密钥管理挑战
第一把签名密钥很容易。第二把密钥才是架构真正的考验。
团队可以在一天内生成密钥对,将公钥放入应用,并签署第一个 bundle。几个月后,一名工程师离职并带走了笔记本电脑的访问权限,CI 密钥泄露在构建日志中,或者签名任务需要从一台 runner 迁移到另一台。此时,“只需轮换密钥”可能意味着放弃那些尚未检查且无法识别替换密钥的设备。

在首次发布之前,有三个问题值得在设计上重点关注:
- 无死胡同的轮换: 在强制要求新密钥之前,先为后继密钥建立信任。通过签署的密钥过渡记录,现有的受信任密钥可以授权下一把密钥,同时应用在定义的迁移窗口内继续接受旧密钥。
- 无假设的吊销: 应用内固定的密钥不会自动提供 OCSP 或 CRL 风格的吊销机制。客户端需要一个签署的拒绝列表、最低可接受的密钥版本,或一个在网络不可用时仍保持安全的服务器控制策略。
- 受限的签名访问权限: CI runner 应向 vault 或 HSM 发起签名操作,而不是通过明文环境变量接收可复用的私钥。日志必须对命令输出脱敏,来自不可信分支的 pull request 不得接触生产签名凭据。
运维现状: 密钥轮换本质上是一个更新问题。如果更新机制无法安全地传递信任变更,也就无法在密钥泄露后干净地恢复。
密钥层级可以缩小影响范围。根授权机构可以授权发布密钥,而开发、预发和生产 channel 分别由不同密钥签名。阈值签名(Threshold signing)可以要求多个授权方共同参与敏感的生产发布,有助于防止单个被盗凭据生成有效更新。这些控制会增加流程开销和延迟,因此团队应根据更新的影响程度和 channel 的敏感性来应用。
对于移动端更新,“首次使用信任”(Trust on first use)机制尤其脆弱。如果第一个密钥与 bundle 通过同一个 channel 到达,控制该 channel 的攻击者就能把两者都替换掉。初始信任锚点必须随 app 二进制提供、放在受平台保护的配置中,或通过其他独立认证的路径到达。
对于 Capacitor 团队,Capgo 文档中的方案包括公钥固定(public-key pinning)和密钥轮换支持。这套 安全 OTA 更新的密钥管理指南 为规划这一生命周期提供实用参考,而不是把密钥生成当成一次性设置。
将验证集成到 CI/CD 和监控中
签名应属于发布事务的一部分。Pipeline 不应先发布 bundle,再通过单独的手动步骤附加签名。应构建不可变 artifact,计算其 digest,对确切的字节签名,用独立的验证步骤校验签名,然后将 bundle 和元数据作为一个发布单元一起发布。
实际 pipeline 会产出以下 artifact:
- 不可变的 bundle: 客户端要下载的文件,而不是让 CDN 重新打包的目录。
- Manifest: 版本、channel、平台、digest、密钥标识符和 rollout policy。
- 分离签名: 对规范化 manifest 数据或精确定义的 digest 表示形式生成的签名。
- 验证结果: 机器可读的检查,用于确认签名能通过该环境预期公钥的验证。
GitHub Actions 和 GitLab CI 虽然语法不同,但都能落实同样的模式。如果私有签名服务不可用、签名格式错误,或新下载的测试副本无法通过验证,签名 job 就应失败。部署 job 应依赖该结果,而不只是 build 成功。
不要对路径签名后又允许后续任务改动它。签名后锁定 artifact,发布前比对 digest,并确保发布的 manifest 能根据 release 记录复现。这能发现一个出奇常见的集成缺口:CI 任务签名的是一份压缩包,交付层提供的却是重新压缩或重新生成的文件。
可观测性应放在设备端
成功的签名任务只能证明 pipeline 生成了有效签名。它无法证明设备收到了预期的字节,也无法证明 App 使用了正确的 key。请记录验证结果,并附带 App 版本、更新版本、channel、平台、key identifier、结果分类和隐私安全的 release correlation ID。不要记录 bundle、token、私有元数据或用户内容。
实用的 dashboard 应区分:
- 签名失败与下载失败,
- digest 不匹配与 manifest 格式错误,
- 未知 key 与策略拒绝,
- 按 App 版本、地区、channel 及发布时长统计的失败情况。
digest 不匹配突然集中出现,可能意味着缓存损坏、交付路径被改动或 artifact 发布出错。未知 key 事件可能表明密钥轮转不完整,或有人尝试未经授权发布。验证遥测本身无法识别攻击者,但能为响应人员提供调查时间线和受影响群体。
本指南中介绍的 Capacitor OTA 更新的 CI/CD 安全指南 可以帮助团队在同一个工作流中设置签名、验证和部署关卡。关键的设计选择是所有权。安全团队不应该需要询问工程团队验证是否已执行。发布记录和客户端遥测数据应直接回答这个问题。
安全最佳实践与常见误区
对于任何可以执行 code 的更新路径,签名验证都应是硬性要求。更新器必须在提取、安装或激活之前进行验证,并且在签名、摘要、密钥或策略检查不可用时必须失败关闭。
将此作为即时审查清单:
- 保护私钥: 将签名材料保存在 HSM 或托管保险库中。切勿将其提交到源代码控制、放入移动二进制文件或允许其进入 CI 日志。
- 固定受信任的公钥: 将初始信任锚点存储在更新负载之外。如果支持多个密钥,请定义其用途和过渡规则。
- 绑定完整的发布: 对标识确切 bundle、channel、平台和预期策略的规范元数据进行签名。
- 拒绝所有验证错误: 超时、格式错误的签名、未知密钥或缺失的 manifest 并不是继续使用前一个未验证结果的邀请。
- 测试妥协恢复: 在 staging 环境中演练密钥轮换、吊销密钥处理、回滚、中断下载以及陈旧设备场景。
- 审查验证器变更: 要求对加密库、规范化、解析、回退行为及调试配置进行以安全为重点的 code 审查。
- 保持调试行为隔离: 开发绕过机制必须无法通过默认标志或环境变量错误被打包进生产 build。
签名也无法证明新鲜度、接收方绑定、重放抵抗,或 payload 与提供商当前状态的兼容性。Webhook 安全指南明确区分了这一点,近期的漏洞报告也表明,畸形或空签名及规范化问题仍可能削弱狭窄的验证检查。在设计周围的授权规则时,请阅读 webhook 安全差距分析。
阈值选择在生物识别系统中也带来相关教训。一项使用 62 个参数特征 开启 来自 102 个人的 1,232 个签名 报告了依赖书写者的阈值为 2.8% 误拒率 以及 1.6% 误识率,而另一种方法报告了 2.68% 误拒率 以及 1.99% 误识率,正如 阈值选择研究所述。应用场景不同,但工作原理相同:验证者的决策策略与密码学原语同样重要。
对于需要把已签名的 Live Update bundle、设备端验证、发布控制、回滚保护和分发可观测性放在同一系统中的 Capacitor 和 Electron 团队,Capgo 可以作为实现方案之一。访问 Capgo 以评估其更新工作流是否满足你的密钥管理、CI/CD 和监控需求。