适用人群:做完全量/差分 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 Boot(espsecure.py签名 + eFuse 烧公钥摘要),ROM 层面帮你验,比 DIY 更稳。
实操部分以 ESP-IDF 安全启动文档、mbedTLS 文档和 ST 的应用笔记(AN4992 / AN4657)为准。
六、公钥存哪?这是安全的最关键一环
验签安不安全,取决于公钥能不能被攻击者替换:
| 存放位置 | 攻击者能否改 | 结论 |
|---|---|---|
| 普通 Flash(和应用同片) | 能 | ❌ 攻击者可同时换公钥+恶意固件,签名形同虚设 |
| OTP / eFuse(一次性烧写) | 不能(烧死后不可改) | ✅ 正确做法 |
| ROM(芯片出厂固化) | 绝不可能 | ✅ 最强(平台自带安全启动) |
结论:公钥必须进 eFuse / OTP / 或用更高一级的密钥签名后锁死。配合 STM32 的 RDP 读保护 + WRP 写保护 + PCROP 代码保护,把 Bootloader 和密钥区锁成"读不出、改不了"。
七、各平台自带方案(优先用官方的,别自己造)
- ESP32:
CONFIG_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)记住"已接受的最高版本",更低的一律拒。
九、动手练一练
- 用上面的 OpenSSL 命令生成 P-256 密钥对,对你编译出的
firmware.bin签名,得到firmware.bin.sig。 - 用
openssl dgst -sha256 -verify验证:合法固件输出Verification OK。 - 故意把 firmware.bin 改 1 个字节(哪怕只是版本字符串),再验签 → 必须
Verification Failure。亲手感受"改一点就废"。 - 把
verify_firmware()接到 OTA_02 的jump前,验签失败的设备应当停在 bootloader 闪灯,绝不启动恶意固件。 - 讨论题:把公钥放"普通 Flash" vs “eFuse”,攻击者分别要多大代价才能得手?这步想通,你就懂了安全启动为什么强调"密钥锁死"。
小结
没签名的 OTA 是开了门没上锁。固件签名用非对称加密实现"只有你能签、谁都伪造不了":构建时用私钥对固件哈希盖章,设备用烧死在 eFuse 里的公钥验章,验不过就拒绝运行。记住三件事——公钥必须锁死在 eFuse/OTP(不能放普通 Flash)、验签失败必须硬拒绝、签名之外还要防回滚。平台自带 Secure Boot 优先开,自建验签只是兜底。
系列收官预告:OTA_05_量产落地 Checklist —— 版本号 / 防回滚 / 灰度发布 / 断点续传,把前面四篇串成一份真正能上线的大规模 OTA 方案清单。
下一篇预告:《量产落地 Checklist —— 版本号 / 防回滚 / 灰度 / 断点续传》
—— 拒绝非法固件&spm=1001.2101.3001.5002&articleId=163785110&d=1&t=3&u=480afdada53f4a78958ccdfa001c5e6d)
1370

被折叠的 条评论
为什么被折叠?



