单分区升级的致命问题是「升级失败 = 变砖」。A/B 双分区把风险从「能不能升级」转移到「升级失败能不能自动回滚」。本文从分区表、bootloader 回滚、签名校验到选型对比,一步步讲清如何把 A/B 升级做到生产标准。
📝 主旨内容
1. 为什么需要 A/B 双分区
单分区升级流程:下载镜像 → 覆盖 rootfs → 重启。一旦镜像损坏、中途断电、签名错误,板子直接变砖,只能拆机重烧。
A/B 的思路:系统盘做两份(A 槽 / B 槽),永远只写「非活动槽」。新版本写进非活动槽后,bootloader 尝试启动它;启动失败就自动回滚到旧槽。核心指标从「升级成功」变成「升级失败不砖」。
维度 | 单分区升级 | A/B 双分区 |
升级失败后果 | 变砖,需重烧 | 自动回滚旧槽 |
存储成本 | 1× rootfs | 2× rootfs |
升级窗口 | 停机 | 可后台写非活动槽 |
实现复杂度 | 低 | 中(bootloader + 状态管理) |
2. 三个核心概念
先定义清楚,后面全靠它们:
- 槽位(Slot):一份完整的系统分区(kernel + rootfs),A 和 B 互为镜像。
- BCB(Bootloader Control Block):写在独立 misc 分区里的一小块数据,记录「下次启动哪个槽」「当前槽状态」。bootloader 读它决定启动 A 还是 B。
- 槽位状态机:每个槽有 active(当前运行)/ inactive(备用)/ unbootable(已坏)三种状态。这是设计层面的概念模型,与具体工具无关——无论 RAUC、SWUpdate 还是手写 BCB,都要跟踪这三种状态。区别在谁来落地:RAUC 原生内置(mark-good / mark-bad 管理槽状态);SWUpdate 不自带,需自己用 bootloader 接口(U-Boot 的 bootcount/bootlimit + upgrade_available,或 BCB)实现。
3. 分区表设计(GPT)
用 GPT 而不是 MBR:支持分区名与类型 GUID,A/B 槽的识别更可靠。
要点:
- A、B 槽大小必须一致,否则升级包无法通用。
- misc 分区独立于 A/B,升级时永远不动它。
- data 分区独立,升级不清用户数据。
4. Bootloader 侧:bootcount 回滚
U-Boot 用三个环境变量实现「启动失败自动回滚」:
机制:每次启动 bootcount +1;系统正常启动后由应用层清零;若 bootcount 超过 bootlimit,说明新槽起不来,U-Boot 执行 altbootcmd 切回旧槽。
5. 升级包与签名校验
生产标准的最低要求:升级包必须签名,写入必须校验。
- 完整性:sha256 校验,防传输/写入损坏。
- 真实性:RSA/ECC 签名,防固件被替换。
- 可选加固:rootfs 用 dm-verity 做运行时完整性校验。
5.1 SWUpdate 如何实现发布者证书验证
核心一句话:SWUpdate 不签镜像,签的是更新清单 sw-description;设备端用内置信任锚验证签名和证书链,验证通过才允许解析清单、安装镜像。
信任链:签名认证清单 → 清单认证哈希(sha256)→ 哈希认证镜像内容。签一个文件,整包被认证。
两种签名模式
模式 | 机制 | 设备端验证什么 | 适用 |
RSA/ECDSA 公钥签名 | 私钥签 sw-description(openssl dgst -sha256 -sign priv.pem) | 内置公钥验签 | 单发布方、简单场景 |
CMS(PKCS#7 / X.509 证书链) | 私钥 + X.509 证书签(openssl cms -sign) | 内置 CA/信任锚:验签名 + 验证书链 + 验证书用途 | 发布者证书验证,支持 PKI、证书轮换、吊销 |
关键落地细节
- 验证时机:先验签 → 再解析清单 → 再逐镜像校验 sha256 → 最后安装,验签失败直接拒绝。
- 硬性规则:清单里每个镜像必须有 sha256 属性,缺一个整包视为未验证,SWUpdate 直接报错停止。
- 归档顺序:SWU 是 cpio 包,sw-description.sig 必须紧跟 sw-description。
- 密钥分布:私钥只在发布方/CI,永不进设备;公钥或 CA 证书固化在设备固件。
签名与验签交互图

5.2 公钥与 CA 认证锚如何烧入固件
一句话核心:公钥/CA 信任锚是「设备信任谁」的根,必须随固件一起、放在只读/受保护的位置烧进设备;私钥永远只留在 CI 侧。
方式 | 做法 | 防篡改能力 |
rootfs 固化(最常见) | 构建固件时把公钥/CA 证书放进固定路径(如 /etc/swupdate/、/etc/ssl/certs/),swupdate.cfg 配 public_key | 中——需配合只读分区 |
独立只读分区 | 放专门 keys/recovery 分区,与可更新 rootfs 分离,OTA 动不了信任锚 | 高 |
Bootloader 层 | 信任锚放 U-Boot 环境/启动镜像,SWUpdate 启动时读取 | 高 |
eFuse / 安全芯片 / TPM | 公钥哈希或证书固化进 OTP(一次性可编程熔丝),烧写不可逆 | 最高 |
关键机制
- 烧入时机:量产阶段随固件镜像一起烧写(量产工具链/烧录器),不是运行时下发。
- 为什么必须防篡改:信任锚是验签起点,能改设备上的公钥就能伪造签名通过验证,所以它必须在 OTA 更新范围之外。
- 信任锚更新:新信任锚只能靠「被旧锚签名的新固件」带入,或走安全通道/物理接触,形成信任根更新链;没有此机制,换 CA = 设备变砖。
- SWUpdate 落地:RSA/ECDSA 模式设备端放公钥文件(swupdate.cfg 的 public_key);CMS 模式放 CA 证书(信任锚),OpenSSL 把发布者证书链追到根。
面试一句话:SWUpdate 用「签名 sw-description + 镜像 sha256 + 设备端内置信任锚」实现发布者认证——发布方用私钥(CMS 场景是 X.509 证书)对更新清单签名,设备端用内置公钥/CA 验证签名和证书链,通过才解析清单;清单里每个镜像的 sha256 被认证,镜像即被认证。私钥永不上设备,信任锚固化在固件里。
5.3 CA 证书实操:格式 / 大小 / 生成 / 注入
四个问题逐个拆,最后给一条完整的落地链路。
格式
CA 证书就是标准的 X.509 证书,两种编码:
格式 | 说明 | 适用 |
PEM(文本) | Base64 + -----BEGIN CERTIFICATE----- 头尾,Linux/OpenSSL 默认 | 嵌入式 Linux 首选,SWUpdate 设备端用这个 |
DER(二进制) | X.509 原始二进制 | 少见,需转换 |
SWUpdate 官方文档明确:设备端要装的就是一个
.pem 证书文件(如 mycert.cert.pem),OpenSSL 直接处理。PEM 就对了,不用纠结。字节大小
X.509 证书就 1~2 KB 量级,对嵌入式是零负担:
密钥类型 | DER 大小 | PEM 大小(≈DER×1.33) |
RSA-2048 | ~700–1000 B | ~1–1.3 KB |
RSA-4096 | ~1.5–2 KB | ~2–2.7 KB |
ECC P-256 | ~350–500 B | ~0.5–0.7 KB |
大小主要取决于密钥长度、Subject 字段和扩展数量。设备端存一个 CA 证书,存储成本可以忽略——真正要防的不是「装不下」,而是「装错/被改」。
如何获取
嵌入式 OTA 的标准做法是自建内部 CA(自己当信任根),不是买公网证书。用 OpenSSL 三步:
signing.ext 里必须写(SWUpdate 用 OpenSSL 验 CMS 时默认强制检查):也可以买公网代码签名证书,但自建 CA 才让你完全掌控信任链和轮换策略——量产 OTA 基本都是这条路。
SWUpdate 如何注入(三层)
① 编译期(Kconfig)——决定「验不验、怎么验」:
CONFIG_SIGNED_IMAGES=y:启用签名镜像
CONFIG_SIGNATURE_TYPE="CMS":选 CMS 模式(或 RSA)
CONFIG_VERIFY_CERTIFICATE=y:开启证书链验证——设备端把发布方证书链追到内置 CA。不开的话只验签名、不验证书链,安全性打折。
② 运行期(swupdate.cfg)——告诉 SWUpdate 信任锚在哪:
③ 固件注入——证书怎么进设备:构建固件时把
ca.crt 放进 rootfs 固定路径(如 /etc/swupdate/),随固件镜像在量产烧录时固化。Yocto 侧对应 meta-swupdate 的变量:签名端 SWUPDATE_SIGNING="CMS" + SWUPDATE_CMS_CERT(签名证书)+ SWUPDATE_CMS_KEY(私钥,只在 CI),设备端把公钥证书烤进 rootfs。完整链路一句话:CI 用私钥 + 签名证书签 sw-description(CMS)→ 设备端 SWUpdate 读 swupdate.cfg 的
public_key 指向的内置 CA 证书 → OpenSSL 验签名 + 验证书链到信任锚 → 通过才解析清单、校验镜像 sha256、安装。5.4 SWUpdate 验签步骤与中间证书处理
常用验签步骤(CMS 模式)
- 解析 SWU(cpio)→ 定位 sw-description + sw-description.sig(sig 必须紧跟 description)
- 提取 CMS SignedData → 取出签名者证书 → 用其公钥验 sw-description 签名
- 证书链验证(CONFIG_VERIFY_CERTIFICATE=y)→ 从签名者向上逐级验 issuer+签名,追到设备内置信任锚
- 验签通过 → 才解析 sw-description(拿到镜像列表 + 各自 sha256)
- 逐镜像校验 sha256 → 全部通过才安装;任一失败整包拒绝
关键在 ③:验签名只证明「这个签名者确实签了」,验链才证明「这个签名者可信」。只开签名不开链验证(CONFIG_VERIFY_CERTIFICATE),等于只验了完整性没验来源。
中间证书如何处理(重点)
中间证书有两个来源,处理策略完全不同:
来源 | 机制 | 风险 |
CMS 里携带(additional certificates) | 发布方打包时塞进 SignedData,OpenSSL 用它搭链 | ⚠️ 攻击者也能塞自己的中间证书 |
设备信任库 | 中间 CA 证书提前烤进设备 | 安全,设备自己掌控 |
核心风险点:OpenSSL 对 CMS 里携带的额外证书的规则是「被用于搭链 ≠ 被信任」。它会把 CMS 里的中间证书拿来构造链条,但只要最终能追到设备内置的信任锚,验签就通过——这意味着:攻击者只要拿到一张能被你根 CA 信任的证书(哪怕用途不对、哪怕是内部泄露的),就能塞进恶意包里搭出一条合法链。
应对:CONFIG_CMS_IGNORE_ADDITIONAL_CERTS
SWUpdate 专门为这个加了选项:严格忽略 CMS 里携带的任何证书(除直接签名者本身),强制中间证书只能来自设备信任库。开启后,攻击者塞的中间证书直接被无视,链搭不起来,恶意包必拒。
但注意权衡——这个选项默认是关闭的,因为存在合法场景:设备信任库里的中间 CA 过期了,需要靠升级包顺带携带新的中间证书来续链。
场景 | 处理策略 |
生产标准(推荐) | 中间 CA 烤进设备信任库 + 开启 CONFIG_CMS_IGNORE_ADDITIONAL_CERTS |
需要随包带中间证书 | 关闭该选项,但必须叠加签名者身份白名单(只认指定签名证书的 CN/序列号),不能只靠「链到根」 |
签名侧(meta-swupdate) | SWUPDATE_CMS_EXTRA_CERTS 指定要塞进 CMS 的中间证书文件列表 |
一句话总结:中间证书要么提前烤进设备信任库(安全),要么由 CMS 携带(方便但有被利用风险)。生产标准做法:中间证书进信任库 + 开启 CONFIG_CMS_IGNORE_ADDITIONAL_CERTS 严格忽略 CMS 携带证书;若必须随包携带,则必须叠加签名者白名单,绝不能只验证「链到根」。
实践选型:扁平两级 vs CMS 携带中间证书
核心判断:一般实践选扁平两级——只用根证书 + 直接签发的签名证书,不引入中间证书。中间 CA 过期续链这个麻烦,本身就是中间证书带来的——没有中间证书,这个问题就不存在。
OTA 签名 PKI 是封闭内部系统(一家公司、一条产品线),不需要企业级 CA 的组织层级。扁平结构长这样:
- 链只有 2 跳,设备只存根,CMS 只带签名者证书(本来就在 SignedData 里)
- 可以一直开着严格模式(CONFIG_CMS_IGNORE_ADDITIONAL_CERTS),安全不打折
- 没有中间 CA,就没有「中间 CA 过期要续链」这个场景
签名证书过期怎么办:根 CA 重新签发一张新的签名证书,设备端根没变,新证书链照样追到同一个根 → 自动被信任,设备零改动。
维度 | 扁平两级(推荐) | CMS 携带中间证书 |
层级 | 根 → 签名证书(2 跳) | 根 → 中间 → 签名(3 跳+) |
设备端存储 | 只存根 | 只存根(中间靠 CMS 带) |
中间 CA 过期 | 不存在此问题 | 要续链,麻烦 |
签名证书过期 | 根重签,设备零改动 | 同左 |
安全模式 | 长期开严格模式 | 必须关严格模式 + 白名单 |
适用 | 单产品/单公司 OTA(绝大多数) | 多产品线/合规分层 |
一句话总结:实践上选扁平两级——根 CA 直接签签名证书,设备只存根,CMS 只带签名者证书,严格模式常开。签名证书过期靠根重签解决,设备零改动;中间证书是为组织层级付的复杂度,没有层级需求就不要引入。
6. 选型对比:SWUpdate vs RAUC vs 手写 BCB
维度 | SWUpdate | RAUC | 手写 BCB |
定位 | 通用更新框架 | 轻量 A/B 更新框架 | 自己实现 |
A/B 支持 | 通过 bootloader 接口 | 原生 slot 概念 | 自己设计 |
签名 | RSA/CMS 内置 | CMS/X.509 内置 | 自己实现 |
差分升级 | 生态成熟 | 较弱 | 自己实现 |
系统集成 | Yocto/Buildroot 好 | D-Bus/systemd 好 | 无依赖 |
学习成本 | 中高 | 中 | 低(但坑多) |
维护成本 | 低(社区维护) | 低 | 高 |
选型建议:
- 生产项目:RAUC(A/B 原生、设计简洁)或 SWUpdate(生态大、handler 灵活)。
- 学习/面试/简历项目:先手写 BCB 最小闭环(bootcount 回滚),理解机制后再上 SWUpdate 做签名与升级包。
- 关键判断变量:需要差分升级 → SWUpdate 生态更好;需要 D-Bus/系统集成 → RAUC;资源极度受限或纯学习 → 手写 BCB。
7. 完整升级流程(循序渐进)
每一步的注意点:
- 下载:断点续传 + 校验和。
- 校验签名:验签失败直接丢弃,不落盘。
- 写非活动槽:写完后 fsync,确保落盘。
- 校验写入完整性:读回比对 sha256。
- 更新 BCB:这是「切换」的唯一动作,必须原子(写一半断电要能恢复)。
- 重启:bootloader 读 BCB 启动新槽。
- commit:应用层确认启动成功后,新槽标记 active、bootcount 清零。
- 回滚:bootcount 超限自动切旧槽,标记新槽 unbootable。
⚠️ 最容易翻车的点:确认成功(commit)的时机
先交代前提:mark-good 是 RAUC 的术语。RAUC 的槽状态机里,新槽启动后若未执行 mark-good,会持续消耗尝试次数,耗尽即被标记 bad 并回滚;执行 mark-good 才把槽确认为 active。SWUpdate 不自带这套状态机的落地实现,A/B 回滚靠 bootloader 接口(典型是 U-Boot 的 bootcount/bootlimit + altbootcmd),对应的「确认成功」动作是:健康检查通过后清零 bootcount、清除 upgrade_available 标记,让 bootloader 不再回滚。两者机制不同,但时机的权衡是通用的:
- 内核启动完成 ≠ 产品已恢复:关键服务可能还没就绪、外设初始化可能失败、数据迁移可能只做了一半。
- 确认太早:坏版本被确认为可用,回滚机制形同虚设。
- 确认太晚:短暂掉电 + 看门狗复位会不断消耗尝试次数,把原本可用的新版本退回旧槽。
正确做法:定义项目级健康检查作为唯一判定标准。新系统进入用户态后,依次通过「核心服务就绪 → 外设自检 → 数据迁移完成」三层检查,全部通过才执行确认动作——RAUC 用 mark-good,SWUpdate 清零 bootcount / 清除 upgrade_available。判定标准写死在代码里,不靠人工肉眼确认。
8. 生产级加固清单
- 断电安全:BCB 更新必须原子(双份备份 + 校验和),写一半断电可恢复。
- 版本管理:升级包带版本号 + 硬件兼容性声明,拒绝降级或跨硬件升级。
- 日志:每次升级记录版本、结果、耗时,可追溯。
- 回滚策略:连续 N 次失败自动回滚并告警。
- 灰度发布:先小批量设备验证,再全量推送。
- 可观测:升级状态上报,失败率可监控。
9. 数据与配置的兼容性边界(最容易被忽略)
A/B 冗余的是系统槽,不会自动回滚共享数据库和配置。新版本改了数据格式再回退旧槽,旧程序可能已经读不了新数据——这不是回滚,是新的故障。
- 向后兼容:新版本写入的数据,旧版本必须能读(字段只增不删、缺省值兜底)。
- 迁移可逆:升级时做的数据迁移,回滚时要有对应的逆操作。
- 不可逆时限制升级跨度:数据格式一旦不可逆,就限制「只允许跨 N 个版本升级」,禁止跨大版本直接升级。
为什么企业看重这个:一次失败状态处理错误,可能把软件缺陷变成人工上门、返厂,甚至整批设备召回。数据兼容性是「升级失败不砖」之外的第二个底线。
10. 测试与验收
10.1 正常路径验收
用例 | 预期 |
正常升级 | 新槽启动,commit 成功 |
版本回退 | 按策略允许或拒绝 |
灰度发布 | 小批量验证后全量,失败率可监控 |
10.2 故障注入测试(重点)
故障注入测的不是「能不能升级」,而是「升级失败会不会砖」。每条用例都要回答三个问题:故障发生在哪个环节、系统如何检测、系统如何恢复。
故障注入矩阵
故障类型 | 注入方法 | 注入点 | 预期行为 |
断电 | 继电器控制电源,指定时刻断电 | 镜像写入中 | 恢复后旧槽可启动,新槽未提交 |
断电 | 同上 | BCB/环境保存中 | BCB 原子性生效,旧槽可启动 |
断电 | 同上 | 首次启动后、健康检查前 | 尝试次数被消耗,未确认成功,可回滚 |
断电 | 同上 | 健康检查通过后 | 新槽已 commit,保持 active |
坏镜像 | 位翻转 / 截断升级包 | 下载/写入 | 校验失败,拒绝写入 |
坏签名 | 错误密钥重签 / 篡改签名区 | 验签 | 拒绝升级,丢弃并告警 |
存储故障 | 坏块模拟 / 写满分区 | 写入 | 写入失败,旧槽仍可启动 |
BCB 损坏 | 向 misc 分区写垃圾数据 | 启动前 | 检测非法 BCB,回退默认槽 |
环境变量损坏 | 篡改 bootcount / bootlimit / altbootcmd | 启动前 | 按默认策略处理,不无限重启 |
健康检查失败 | 注入关键服务启动失败 | 首次启动 | 不 commit,尝试次数消耗,超限回滚 |
跨硬件/降级 | 强制安装不兼容包或旧版本 | 验签/安装 | 按策略拒绝 |
注入手段(怎么把故障造出来)
- 断电:串口继电器控制电源,脚本指定「写第 N 块时断电」这类精确时刻。
- 坏镜像/坏签名:对升级包做位翻转、截断、换错误密钥重签,比手动删文件更接近真实故障。
- 存储故障:用坏块模拟(如 device-mapper 映射错误扇区)或写满分区制造写入失败。
- BCB/环境变量损坏:直接向 misc 分区写垃圾、篡改 U-Boot env,验证「脏数据也能安全启动」。
断言状态,而不是断言命令
以 RAUC 为例,它自己的测试就补过这样一类缺口:早期只检查 mark-good / mark-bad / mark-active 的退出码和输出文字,后续提交增加了操作前后的槽位状态断言。命令说「成功」,不等于系统状态真的改变——这个区别是衡量项目质量的好标尺,对 SWUpdate 同样适用。
- 只截一张升级成功的终端图,证明的是界面输出。
- 记录槽位、尝试次数、启动原因、回退原因,才能证明你理解更新是一场跨 Bootloader、存储、用户态的事务。
- 每次注入前记录状态快照,注入后对比,用「状态是否达到预期」而非「命令是否报错」判定通过。
自动化:故障注入回归
- 串口 + 继电器电源,脚本驱动「注入 → 断电 → 重启 → 读状态」循环,全矩阵跑一遍即一次回归。
- 板端成本低,可纳入 CI;每次升级保留日志,失败率可监控。
🤗 总结归纳
A/B 双分区的本质是把「升级失败」从「变砖事故」变成「可自动恢复的异常」。生产标准 = 分区设计 + bootloader 回滚 + 签名校验 + 原子 BCB + 健康检查定时的确认成功 + 数据兼容性边界 + 完整测试。选型上:生产用 RAUC/SWUpdate,学习用最小闭环手写 BCB。
📎 参考文章
- SWUpdate 官方文档:https://sbabic.github.io/swupdate/
- RAUC 官方文档:https://rauc.readthedocs.io/
- U-Boot bootcount 文档:https://docs.u-boot.org/
- Author:felixfixit
- URL:http://www.felixmicrospace.top/article/ab-dual-partition-upgrade-best-practices
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!











