4、安全升级机制:安全启动链、加密传输(TLS/HTTPS)、防回滚保护、安全存储密钥

OTA升级,说白了就是把新固件从云端送到设备里。但你想过没有——如果中间被人篡改了怎么办?如果设备被刷回了有漏洞的老版本怎么办?如果密钥被人读走了怎么办?

这些问题,我在第一个量产项目里全踩过一遍。嗯,那会儿真是焦头烂额。后来我总结了一句话:OTA升级,安全不是附加功能,而是地基。地基不稳,楼盖得再高也得塌。

这一章,我就把安全升级的四个核心支柱掰开揉碎讲给你听。分别是:安全启动链、加密传输、防回滚保护和安全存储密钥。

核心观点:安全升级不是单点防护,而是一条完整的信任链。从固件签名、传输加密、版本校验到密钥保管,任何一个环节出问题,整个升级过程都不安全。

OTA安全升级四支柱架构图 OTA 安全升级四支柱架构 安全启动链 逐级签名验证 加密传输 TLS/HTTPS 防回滚保护 版本号熔断 安全存储密钥 HSM/SE 信任链:签名 → 传输 → 版本 → 密钥,环环相扣 任何一个环节被攻破,整个升级系统都不安全 升级流程中的安全环节 固件签名 → HTTPS传输 → 版本校验 → 解密安装 → 安全启动验证 固件签名 HTTPS传输 版本校验 解密安装 安全启动 每个环节依赖前一个环节的信任,形成完整的信任链

4.1 安全启动链:从第一行代码开始信任

安全启动链,英文叫 Secure Boot Chain。它的核心思想很简单:从芯片上电的第一行代码开始,每一级代码都要验证下一级代码的签名。只有签名合法,才允许执行。

我举个例子你就明白了。芯片上电后,首先运行的是固化在ROM里的BootROM。BootROM会去验证Flash里的Bootloader签名。Bootloader验证通过后,再去验证OS Kernel的签名。Kernel再去验证应用程序的签名。一层套一层,像链条一样。

我的习惯:在设计安全启动链时,我通常把公钥直接烧死在芯片的一次性可编程(OTP)区域里。这样攻击者想改公钥?没门儿,物理上就改不了。

具体来说,安全启动链包含这几个关键步骤:

  1. BootROM验证Bootloader——这是信任的根。BootROM里的代码是只读的,改不了。
  2. Bootloader验证OS Kernel——Bootloader拿到固件包,先验签名,再解压加载。
  3. Kernel验证应用程序/服务——确保用户态程序也是经过签名的。
  4. 应用程序验证升级包——OTA下载的固件,安装前也要验签。

你想想看,如果BootROM被改了,那整个系统就彻底沦陷了。所以芯片厂商通常会把BootROM做成Mask ROM,出厂后无法修改。这就是信任的锚点。

我曾经踩过的坑:有一次,我发现设备启动时偶尔会跳过签名验证。查了两天才找到原因——Bootloader里有个调试模式,编译时没关掉。攻击者只要拉高某个GPIO引脚,就能绕过签名检查。从那以后,我要求所有量产固件必须强制关闭调试接口。

4.2 加密传输:TLS/HTTPS 不是可选项

固件在网络上传输,如果不加密,就等于把家门的钥匙挂在门外。攻击者可以轻松抓包、篡改、重放。

我个人习惯,所有OTA通信必须走HTTPS,底层是TLS 1.2或1.3。为什么?因为HTTPS提供了三重保护:

保护维度 说明 攻击方式
机密性 固件内容被加密,中间人看不到 窃听、抓包
完整性 固件在传输中无法被篡改 中间人篡改
身份认证 设备能确认服务器身份是真的 钓鱼服务器、DNS劫持

具体到嵌入式设备,TLS握手有个大问题——计算开销。尤其是非对称加密的握手阶段,对MCU来说挺吃力的。我建议的做法是:

  • 使用TLS会话复用——第一次握手后缓存会话票据,后续升级复用,省掉重复握手。
  • 选择合适的加密套件——优先选ECDHE + AES-GCM,性能好且安全。
  • 证书固定(Certificate Pinning)——把服务器证书或公钥直接烧在固件里,避免CA被攻破的风险。

关键点:不要只用HTTP!我见过有些产品为了省资源,用HTTP + 自定义加密。结果呢?自定义加密被逆向工程,固件被扒了个精光。HTTPS是经过全世界验证的标准,别自己造轮子。

下面是一个典型的HTTPS OTA请求流程:

设备 → 服务器: GET /firmware/v2.1.0.bin
设备 ← 服务器: TLS握手 + 证书验证
设备 → 服务器: HTTP请求头 (包含设备ID、当前版本)
设备 ← 服务器: 200 OK + 加密固件流
设备: 逐块接收、校验TLS MAC、写入临时分区

4.3 防回滚保护:别让设备退回到有漏洞的版本

防回滚,英文叫 Anti-Rollback。它的目的是防止攻击者把设备固件降级到有已知漏洞的老版本。

为什么会有人想回滚?举个例子:某个版本修复了一个安全漏洞,但攻击者手里有利用这个漏洞的工具。如果能把设备刷回老版本,漏洞就又可以利用了。

防回滚的实现方式,我总结下来有三种:

方案 原理 优点 缺点
版本号比较 新固件版本号必须大于当前版本号 实现简单 版本号可被篡改
版本号+签名 版本号被包含在签名数据中 防篡改 需要安全存储当前版本
硬件熔断 升级后烧断OTP位,不可逆 物理级防护 无法回滚到正常版本

我个人最推荐的是版本号+签名方案。具体做法是:

  1. 固件包头部包含版本号字段,比如 2.1.0。
  2. 整个固件包(包括版本号)被私钥签名。
  3. 设备收到固件后,先验签,再比较版本号。
  4. 如果版本号 ≤ 当前版本,拒绝安装。

我曾经踩过的坑:有一次,我发现设备升级后版本号没写对。结果下次升级时,版本号比较逻辑认为新固件比旧固件还老,直接拒绝了。后来我加了一个「强制升级标志」,用于工厂生产和紧急修复场景。但这个标志必须受签名保护,不能随便用。

4.4 安全存储密钥:把钥匙锁在保险柜里

前面讲了签名、加密、防回滚,这些全都依赖密钥。如果密钥被人读走了,那所有防护都是纸糊的。

密钥存储,我把它分为三个等级:

等级 存储方式 安全级别 成本
L1 Flash明文存储 低 零成本
L2 Flash加密存储 + MCU内部唯一ID派生密钥 中 低
L3 独立安全元件(SE)或HSM 高 较高

对于消费级产品,L2方案性价比最高。具体做法是:

  • 利用MCU内置的唯一ID(如STM32的96位Unique ID)。
  • 用这个ID作为种子,通过密钥派生函数(KDF)生成存储密钥。
  • 用存储密钥加密私钥或对称密钥,密文存在Flash里。
  • 使用时,读取唯一ID,重新派生密钥,解密出真正的密钥。

我的建议:如果产品对安全要求高(比如车规、金融支付),别省那几块钱,直接上独立安全元件。SE有物理防护,能防侧信道攻击、防探针、防电压扰动。我在一个车规项目里用过NXP的SE050,效果很好。

下面是一个基于MCU唯一ID的密钥派生示例:

// 伪代码:密钥派生与加解密
uint8_t unique_id[12];
HAL_GetUID(unique_id);  // 读取MCU唯一ID

// 使用HMAC-SHA256派生存储密钥
uint8_t storage_key[32];
hmac_sha256(unique_id, sizeof(unique_id), 
            "ota_key_derivation", 18, 
            storage_key);

// 用storage_key加密私钥
aes_gcm_encrypt(storage_key, private_key, 
                private_key_len, 
                encrypted_key, &tag);

// 将encrypted_key + tag写入Flash
flash_write(KEY_STORAGE_ADDR, encrypted_key, 
            sizeof(encrypted_key));
flash_write(KEY_STORAGE_ADDR + sizeof(encrypted_key), 
            tag, sizeof(tag));

重要提醒:密钥永远不要在代码里硬编码!我见过有人把私钥直接写在源码里,编译进固件。结果固件被反编译,私钥全网公开。那款产品的OTA系统直接报废。

4.5 四个支柱如何协同工作

这四个安全机制不是孤立的,它们必须协同工作,形成一条完整的信任链。我画个流程你就明白了:

  1. 固件打包阶段:用私钥对固件签名,版本号包含在签名数据中。
  2. 传输阶段:通过HTTPS(TLS)加密传输,防止中间人攻击。
  3. 设备接收阶段:验签、检查版本号(防回滚),通过后解密固件。
  4. 安装阶段:写入Flash,更新版本号记录。
  5. 启动阶段:安全启动链逐级验证,确保固件未被篡改。

你想想看,如果任何一个环节断了,比如密钥被偷了,那签名就形同虚设。或者TLS没配好,固件在传输中被换了,那后面验签也过不了。

我的经验:在设计安全系统时,我习惯先做威胁建模。问自己几个问题:攻击者能接触到设备吗?能抓包吗?能拆芯片吗?能侧信道攻击吗?根据威胁等级,选择合适的安全方案。别过度设计,也别偷工减料。