OTA 实战四:安全启动与固件签名(Secure Boot)—— 拒绝非法固件

适用人群:做完全量/差分 OTA、想堵住"别人能给你设备刷恶意固件"这个洞的同学
读完你能得到:① 搞懂"为什么没签名的 OTA 等于开门揖盗";② 理解非对称签名如何做到"只有你能签、谁都改不了";③ 拿到把验签接进 OTA_02 Bootloader 的具体代码。


一、一个没签名的 OTA,等于门没锁

前面三篇我们让设备能升级、能省流量,但都默认一件事:下到设备的固件就是厂家发的。现实不是。

你的智能设备卖到千家万户,连着网。某天:

  • 中间人攻击:同一 Wi-Fi / 4G 链路上有人截获你的 OTA 包,换成带木马的 evil.bin → 设备变成僵尸网络节点;
  • 物理接触:维修点、二手市场的人用串口/DFU 直接刷入竞品的固件 → 你的硬件被"洗"成别家的;
  • OTA 服务器被入侵:攻击者直接推送恶意固件给全网设备。

没有签名验证,以上全都能得手

安全启动 / 固件签名要解决的就一句话:只有用你私钥签过的固件,设备才认;别人的、改过的,bootloader 直接从源头拒绝,根本不运行。


二、前置概念(4 个词)

名词白话解释
非对称加密一对钥匙:私钥(你藏好)+ 公钥(烧进设备)。私钥签名,公钥验签;有公钥也推不出私钥
数字签名对"固件哈希"用私钥签名生成一段数据。设备用公钥验:① 确认是你签的( authenticity );② 确认固件没被改过( integrity )
安全启动 Secure Boot一条"信任链":不可改的 ROM 验 stage1 → stage1 验 app,每一级只跑下一级当且仅当签名合法
签名 ≠ 加密签名证明"谁发的、没被改",但不保密;想隐藏固件内容得另加 AES 加密

💡 核心直觉:你用私钥给固件"盖章"。设备用公钥验章。攻击者没有私钥,永远盖不出真章——哪怕他把固件改得亲妈都不认识,验签也过不了。


三、信任链:从 ROM 到 app 一级咬一级

┌─────────────┐   验签   ┌────────────────┐   验签   ┌──────────┐
│ ROM Boot    │ ───────▶ │ Stage-1 BL     │ ───────▶ │  App     │
│ (不可改)     │ 公钥A    │ (验 app 用公钥B)│ 公钥B    │ (你的业务)│
└─────────────┘          └────────────────┘          └──────────┘
   锁死                        锁死                        锁死
  • ROM 里的验签公钥/哈希是芯片出厂烧死的,改不了
  • Stage-1(就是 OTA_02 你写的 Bootloader)用自己的公钥验 app;
  • 任意一级验签失败 → 整条链断裂,设备不启动非法代码

我们 OTA_02 的 Bootloader,在这里就升级成了"会验签的 Stage-1"。


四、签名与验签:完整流程

4.1 构建时(服务器 / CI,用私钥)

firmware.bin ──SHA256──▶ hash
                      私钥 ──ECDSA签名──▶ signature.bin (约64~72字节, P-256)
最终镜像 = firmware.bin ‖ signature.bin

4.2 设备端(Bootloader,用公钥)

收到镜像 → 拆出 firmware 和 signature
        → SHA256(firmware) 算 hash
        → ecdsa_verify(公钥, hash, signature)
             合法 → 写 Flash / 跳转
             非法 → 拒绝,停在 bootloader

算法选型:ECDSA P-256 + SHA256 是嵌入式 IoT 的甜点(密钥短、验签快、生态全)。RSA-2048 也能用但更慢更占空间;资源足可选 Ed25519。


五、可跑的示例

5.1 服务端签名(OpenSSL,直接能敲)

# 1) 生成 ECDSA P-256 密钥对(私钥保密!公钥烧设备)
openssl ecparam -name prime256v1 -genkey -noout -out priv.key
openssl ec -in priv.key -pubout -out pub.key

# 2) 对固件哈希并用私钥签名(签的是 hash,不是整包)
openssl dgst -sha256 -sign priv.key -out firmware.bin.sig firmware.bin

# 3)(演示)验证:用公钥验 (固件, 签名)
openssl dgst -sha256 -verify pub.key -signature firmware.bin.sig firmware.bin
# 输出 Verification OK 即合法

5.2 设备端验签(基于 mbedTLS,示意)

生产请用成熟库(mbedTLS / wolfSSL / tinycrypt),不要自己手搓椭圆曲线。下面用 mbedTLS 的 pk(公钥)高层接口,自动兼容 P-256 与各版本差异:

#include "mbedtls/sha256.h"
#include "mbedtls/ecdsa.h"
#include "mbedtls/ecp.h"

// 返回 0 = 验签通过;非0 = 拒绝
int verify_firmware(const uint8_t *fw, uint32_t fw_len,
                    const uint8_t *sig, uint32_t sig_len,
                    const uint8_t *pubkey_pem, uint32_t key_len)
{
    uint8_t hash[32];
    mbedtls_sha256(fw, fw_len, hash, 0);          // 算固件哈希

    mbedtls_pk_context pk;
    mbedtls_pk_init(&pk);
    // 解析 PEM/DER 公钥:openssl ec -pubout 出的是 PEM,正好对上
    int ret = mbedtls_pk_parse_public_key(&pk, pubkey_pem, key_len);
    if (ret != 0) { mbedtls_pk_free(&pk); return ret; }

    // 高层验签接口,自动处理 P-256 曲线与 mbedTLS 版本差异
    ret = mbedtls_pk_verify(&pk, MBEDTLS_MD_SHA256, hash, 32, sig, sig_len);
    mbedtls_pk_free(&pk);
    return ret;   // 0 合法,其它拒绝
}

> ⚠️ **版本与格式坑**:① 旧写法用 `mbedtls_ecdsa_context` 直接访问 `ctx.grp/ctx.Q`——在 **mbedTLS 3.x(当前 ESP-IDF v5 自带)这些字段已 opaque,编不过**;上面改用 `mbedtls_pk_*` 高层接口可跨版本。② `openssl ec -pubout` 产出的是 **PEM(SubjectPublicKeyInfo)**,要用 `mbedtls_pk_parse_public_key` 解析,别用 `mbedtls_ecp_point_read_binary` 去喂 PEM 文件(那是裸 65 字节非压缩点格式)。

5.3 接回 OTA_02 的 Bootloader(关键一步)

把验签放在 app_is_valid 之后、jump_to_app 之前:

if (app_is_valid(APP_ADDR)) {
    if (verify_firmware((uint8_t *)APP_ADDR, app_size,
                        sig_addr, sig_len,
                        PUBKEY, PUBKEY_LEN) != 0) {
        // 验签失败:绝不下跳!停在 bootloader 等重传合法固件
        while (1) { LED_TOGGLE(); HAL_Delay(500); }
    }
    jump_to_app(APP_ADDR);   // 合法才跳
}

对 OTA_01 的 ESP32:要么在 esp_ota_end 后自己加一次 esp_ota_ops + 验签,要么直接开启 ESP-IDF 的 Secure Bootespsecure.py 签名 + eFuse 烧公钥摘要),ROM 层面帮你验,比 DIY 更稳。

实操部分以 ESP-IDF 安全启动文档、mbedTLS 文档和 ST 的应用笔记(AN4992 / AN4657)为准。


六、公钥存哪?这是安全的最关键一环

验签安不安全,取决于公钥能不能被攻击者替换

存放位置攻击者能否改结论
普通 Flash(和应用同片)❌ 攻击者可同时换公钥+恶意固件,签名形同虚设
OTP / eFuse(一次性烧写)不能(烧死后不可改)✅ 正确做法
ROM(芯片出厂固化)绝不可能✅ 最强(平台自带安全启动)

结论:公钥必须进 eFuse / OTP / 或用更高一级的密钥签名后锁死。配合 STM32 的 RDP 读保护 + WRP 写保护 + PCROP 代码保护,把 Bootloader 和密钥区锁成"读不出、改不了"。


七、各平台自带方案(优先用官方的,别自己造)

  • ESP32CONFIG_SECURE_BOOT + eFuse 烧公钥摘要。Bootloader 和 app 都用 espsecure.py 签名,ROM 逐级验。直接开这个,比手搓可靠得多
  • STM32:部分系列(L5/U5/H5 带 TrustZone-M,或支持 SFI 安全固件安装)有官方安全启动;通用 F1/F4 用"自建验签 + RDP/WRP/PCROP 提门槛"。注意全功能安全启动不是所有线都原生支持。
  • nRF(Nordic):ARM TrustZone-M + CryptoCell,或 MCUboot + imgtool 签名验签,生态成熟。

经验法则:能开平台官方 Secure Boot 就开,不开才自建验签。自建只解决"验签逻辑",平台方案还顺带把密钥锁 eFuse、防回滚、防调试一锅端了。


八、新手必踩的 6 个坑

#后果正确做法
1公钥放可被改写的普通 Flash攻击者可换公钥+恶意固件,签名失效公钥进 eFuse/OTP,并锁区
2验签失败只"警告"不拒绝恶意固件照样跑验签非 0 必须硬拒绝、不下跳
3私钥进代码仓库/泄露等于没签名,谁都能签私钥只在 CI/离线机,绝不进 git;泄露即轮换
4自己手搓加密算法侧信道/实现漏洞被破用 mbedTLS / wolfSSL / tinycrypt
5混淆签名与加密固件内容仍可被抓包反编译要保密另加 AES(密钥也进 eFuse)
6只验签不防回滚攻击者可降级到你旧漏洞版本固件带单调版本号,设备拒跑更旧版本

第 6 条最容易被忽略:签名只证明"是你签的",不证明"是最新的"。攻击者拿你去年有漏洞的旧版本(合法签名)刷进去,设备照样认。所以固件里必须嵌版本号,设备用单调计数器(eFuse)记住"已接受的最高版本",更低的一律拒。


九、动手练一练

  1. 用上面的 OpenSSL 命令生成 P-256 密钥对,对你编译出的 firmware.bin 签名,得到 firmware.bin.sig
  2. openssl dgst -sha256 -verify 验证:合法固件输出 Verification OK
  3. 故意把 firmware.bin 改 1 个字节(哪怕只是版本字符串),再验签 → 必须 Verification Failure。亲手感受"改一点就废"。
  4. verify_firmware() 接到 OTA_02 的 jump 前,验签失败的设备应当停在 bootloader 闪灯,绝不启动恶意固件。
  5. 讨论题:把公钥放"普通 Flash" vs “eFuse”,攻击者分别要多大代价才能得手?这步想通,你就懂了安全启动为什么强调"密钥锁死"。

小结

没签名的 OTA 是开了门没上锁。固件签名用非对称加密实现"只有你能签、谁都伪造不了":构建时用私钥对固件哈希盖章,设备用烧死在 eFuse 里的公钥验章,验不过就拒绝运行。记住三件事——公钥必须锁死在 eFuse/OTP(不能放普通 Flash)、验签失败必须硬拒绝、签名之外还要防回滚。平台自带 Secure Boot 优先开,自建验签只是兜底。


系列收官预告:OTA_05_量产落地 Checklist —— 版本号 / 防回滚 / 灰度发布 / 断点续传,把前面四篇串成一份真正能上线的大规模 OTA 方案清单。

下一篇预告:《量产落地 Checklist —— 版本号 / 防回滚 / 灰度 / 断点续传》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值