Arduino-ESP32 签名 OTA 安全更新实战:RSA/ECDSA 固件签名验证完整指南
【免费下载链接】arduino-esp32Arduino core for the ESP32 family of SoCs项目地址: https://gitcode.com/GitHub_Trending/ar/arduino-esp32
本文基于 Arduino-ESP32 仓库中libraries/Update/examples/Signed_OTA_Update/示例及其配套源码,系统讲解如何在 ESP32 上实现带数字签名验证的 OTA(Over-The-Air)安全固件更新。读完后你将掌握:如何用bin_signing.py生成 RSA/ECDSA 密钥对、如何对应用固件二进制签名并烧录、如何在 Arduino 草图中安装签名验证器(installSignature),以及签名二进制在设备端的完整校验链路(mbedtls RSA-PSS / ECDSA 验证)。
1. 为什么需要签名 OTA:安全模型与能力概览
代码签名确保只有用你的私钥签过名的固件才会被设备接受。即使用户的攻击者接管了你的更新服务器,没有私钥他也无法让设备运行恶意固件——这正是签名 OTA 保护的核心边界。
示例文档(libraries/Update/examples/Signed_OTA_Update/README.md)声明的能力矩阵如下:
- RSA 签名验证:支持 RSA-2048、RSA-3072、RSA-4096;
- ECDSA 签名验证:支持 ECDSA-P256、ECDSA-P384;
- 多种哈希算法:SHA-256、SHA-384、SHA-512;
- 自动签名验证:OTA 更新过程中自动完成签名校验;
- 默认安全(Secure by Default):签名验证失败则更新直接失败,不会启动未验证固件。
前置条件:
- 安装
cryptography包的 Python 3:
pip install cryptography- 带 Update 库的 ESP32 Arduino Core(即本仓库)。
注意一个关键约束:签名验证功能由编译宏
UPDATE_SIGN控制。示例目录中的 build_opt.h 文件内容为-DUPDATE_SIGN,构建系统会自动为这个示例附加该宏,因此该示例无需额外配置即可启用签名验证;而如果你想在自己的草图中使用这套 API,需要自行确保定义了UPDATE_SIGN——Update.h 中installSignature()的声明和 Updater_Signing.h 整个头文件都被#ifdef UPDATE_SIGN包裹。
2. 签名二进制文件格式:固件 + 512 字节签名
理解整个流程的关键在于签名文件的结构。查看 tools/bin_signing.py 的sign_binary()实现:
# Pad signature to max size (512 bytes for RSA-4096) max_sig_size = 512 padded_signature = signature + b"\x00" * (max_sig_size - len(signature)) # Write signed binary (firmware + signature) with open(output_file, "wb") as f: f.write(binary_data) f.write(padded_signature)也就是说,签名后文件 =原始固件字节 + 固定 512 字节签名区(真实签名按方案不同只有 64~512 字节,其余用 0x00 填充)。这个 512 字节常数贯穿整条链路:
- 设备端:Updater.cpp 中
installSignature()将_signatureSize = 512写死; - 示例草图:Signed_OTA_Update.ino 中
const size_t signatureSize = 512;,并据此计算真实固件大小firmwareSize = contentLength - 512; - 验证时:Updater.cpp 在
end()阶段从分区偏移_size - 512处用ESP.partitionRead()把签名读回闪存,再调用verify()。
这个设计意味着Update.begin()必须传入固件 + 签名的总长度,哈希只覆盖固件部分(_size - 512字节),签名本身不参与哈希。
3. 快速上手:六步完成签名 OTA
以下流程完整继承自示例文档,并结合源码补齐了每个步骤的底层行为。
Step 1:生成密钥对
推荐使用 RSA-2048:
python <ARDUINO_ROOT>/tools/bin_signing.py --generate-key rsa-2048 --out private_key.pem python <ARDUINO_ROOT>/tools/bin_signing.py --extract-pubkey private_key.pem --out public_key.pem或者用更小的 ECDSA-P256(签名小、验证快):
python <ARDUINO_ROOT>/tools/bin_signing.py --generate-key ecdsa-p256 --out private_key.pem python <ARDUINO_ROOT>/tools/bin_signing.py --extract-pubkey private_key.pem --out public_key.pem<ARDUINO_ROOT>是你的 ESP32 Arduino 安装路径(如~/Arduino/hardware/espressif/esp32/),对应本仓库根目录下的 tools/bin_signing.py。
工具支持的全部密钥类型为rsa-2048 / rsa-3072 / rsa-4096 / ecdsa-p256 / ecdsa-p384(见 bin_signing.py 的 --generate-key 参数)。
一个容易忽略的细节:--extract-pubkey除了输出public_key.pem,还会自动生成同名.h头文件(见 extract_public_key)。该头文件把 PEM 公钥转成 C 数组,并追加一个\x00结尾符供 mbedtls 的 PEM 解析器使用:
const uint8_t PUBLIC_KEY[] PROGMEM = { 0x2d, 0x2d, /* ... PEM 字节 ... */ 0x00 }; const size_t PUBLIC_KEY_LEN = 452;仓库中已预置了一份仅用于测试的 RSA-2048 公钥public_key.h,文件内明确警告 "THIS IS A TEST KEY - DO NOT USE IN PRODUCTION!",生产环境必须替换为你自己生成的版本。
重要:妥善保管private_key.pem!任何拿到它的人都能给你的设备签发合法固件。
Step 2:配置示例草图
- 把生成的
public_key.h复制到示例目录,覆盖测试公钥; - 打开 Signed_OTA_Update.ino,修改 Wi-Fi 凭据:
const char *ssid = "YOUR_SSID"; const char *password = "YOUR_PASSWORD";- 修改固件 URL:
const char *firmwareUrl = "http://your-server.com/firmware_signed.bin";- 根据你的密钥类型取消对应宏注释(草图第 56~63 行):
// 密钥类型二选一(否则 #error 编译报错) #define USE_RSA // RSA 签名验证 //#define USE_ECDSA // ECDSA 签名验证 // 哈希算法三选一,必须与签名时使用的 --hash 一致 #define USE_SHA256 // SHA-256(推荐,默认) //#define USE_SHA384 //#define USE_SHA512宏选择最终映射为HASH_SHA256/384/512(Updater_Signing.h)并构造UpdaterRSAVerifier或UpdaterECDSAVerifier对象。
Step 3:编译并上传初始固件
- 编译并烧录该草图到 ESP32;
- 打开串口监视器(115200 波特率)确认运行正常。草图的
setup()会连接 Wi-Fi、打印 IP,然后直接执行一次performOTAUpdate()——这是一个"上电即尝试更新"的最简演示形态,实际项目中应加版本比较或触发条件。
Step 4:构建并签名新固件
- 修改草图(比如加版本号),重新构建并导出二进制:
- Arduino IDE:
Sketch→Export Compiled Binary; - 生成的 application
.bin位于草图目录的build文件夹下,例如build/espressif.esp32.esp32c6/Signed_OTA_Update.ino.bin。
- Arduino IDE:
- 签名:
python <ARDUINO_ROOT>/tools/bin_signing.py --bin <APPLICATION_BIN_FILE> --key private_key.pem --out firmware_signed.bin如需其他哈希算法(例如 SHA-384):
python <ARDUINO_ROOT>/tools/bin_signing.py --bin <APPLICATION_BIN_FILE> --key private_key.pem --out firmware_signed.bin --hash sha384从源码看,签名方案是自动适配密钥类型的(sign_binary):
- RSA:使用RSA-PSS填充,MGF1 与签名哈希相同,
salt_length=PSS.MAX_LENGTH(即key_len - hash_size - 2); - ECDSA:标准 ECDSA,输出 DER 编码签名。
设备端验证端与之一一对应:Updater_Signing.cpp 的 RSA 路径使用MBEDTLS_PK_SIGALG_RSA_PSS/mbedtls_pk_rsassa_pss_options(expected_salt_len = key_len - hash_size - 2),与 Python 侧PSS.MAX_LENGTH严格匹配;ECDSA 路径(第 198~243 行)则先解析 DER ASN.1 序列头求出真实签名长度,再调用mbedtls_pk_verify。
Step 5:托管签名固件
将firmware_signed.bin上传到你的 Web 服务器,使其可从 Step 2 配置的 URL 访问。
Step 6:执行 OTA 更新
复位 ESP32 后,设备将依次:连接 Wi-Fi → 下载签名固件 → 验证签名 → 签名有效则应用更新 → 以新固件重启。
4. 设备端实现剖析:签名验证如何嵌入 Update 状态机
示例草图的 performOTAUpdate() 展示了标准调用序列,顺序至关重要:
// 1. 构造验证器(公钥来自 public_key.h) #ifdef USE_RSA UpdaterRSAVerifier sign(PUBLIC_KEY, PUBLIC_KEY_LEN, hashType); #elif defined(USE_ECDSA) UpdaterECDSAVerifier sign(PUBLIC_KEY, PUBLIC_KEY_LEN, hashType); #endif // 2. 必须在 Update.begin() 之前安装签名验证 if (!Update.installSignature(&sign)) { /* 处理 "Failed to install signature verification" */ } // 3. 传入固件+签名的总大小 Update.begin(contentLength); // 4. 流式写入 HTTP 响应 while (http.connected() && written < contentLength) { /* Update.write(...) */ } // 5. end() 触发签名验证 if (Update.end()) { if (Update.isFinished()) { ESP.restart(); } } else if (Update.getError() == UPDATE_ERROR_SIGN) { Serial.println("SIGNATURE VERIFICATION FAILED!"); }在 Updater.cpp 内部,这套调用链对应的行为是:
| 阶段 | 源码行为 |
|---|---|
installSignature() | 保存验证器指针,固定_signatureSize = 512(L240-L267) |
begin(size) | 校验总大小不小于签名区(size < 512直接报错,L319-L326),并按验证器的getHashType()初始化SHA2Builder流式哈希 |
write() | 数据分两路:一路写 Flash,一路喂给哈希;但只哈希前_size - 512字节(L798-L812),签名字节不参与 |
end() | 从分区固件尾部偏移处读回 512 字节签名,调用verify();失败则置UPDATE_ERROR_SIGN并中止激活(L1007-L1042) |
验证器基类接口定义在 Updater_Signing.h:
class UpdaterVerifyClass { public: virtual bool verify(SHA2Builder *hash, const void *signature, size_t signatureLen) = 0; virtual int getHashType() const = 0; };两个具体实现UpdaterRSAVerifier/UpdaterECDSAVerifier构造时即解析 PEM 公钥并校验密钥类型(非 RSA/ECDSA 会置_valid = false),代码同时兼容 mbedtls 3.x 与 4.x 两套 API(4.x 走 PSA 路径,需先psa_crypto_init(),见 Updater_Signing.cpp 第 25~35 行)。
5. 签名方案与哈希算法选型
示例文档给出的签名方案对比表:
| 方案 | 密钥长度 | 签名长度 | 验证速度 | 安全强度 |
|---|---|---|---|---|
| RSA-2048 | 2048 bits | 256 bytes | 中等 | 高 |
| RSA-3072 | 3072 bits | 384 bytes | 较慢 | 很高 |
| RSA-4096 | 4096 bits | 512 bytes | 最慢 | 最高 |
| ECDSA-P256 | 256 bits | 64 bytes | 快 | 高 |
| ECDSA-P384 | 384 bits | 96 bytes | 快 | 很高 |
文档推荐:RSA-2048 或 ECDSA-P256 在安全性与性能间取得良好平衡。签名长度常量可在 Updater_Signing.h 中核对(RSA_2048_SIGNATURE_SIZE 256…ECDSA_P256_SIGNATURE_SIZE 64),注意它们都小于设备端统一的 512 字节签名缓冲区,短签名靠零填充对齐。
哈希算法对比表:
| 算法 | 输出长度 | 速度 | 安全强度 |
|---|---|---|---|
| SHA-256 | 32 bytes | 快 | 高 |
| SHA-384 | 48 bytes | 中等 | 很高 |
| SHA-512 | 64 bytes | 中等 | 很高 |
文档推荐:多数应用 SHA-256 已足够,且为 Python 工具与设备端的默认算法。
6. 安全管理建议与故障排查
私钥管理(来自文档 Security Considerations 一节)
- 绝不把私钥提交到版本控制;
- 加密存储或使用 HSM;
- 限制访问范围,仅授权人员可接触;
- 考虑开发与生产环境使用不同密钥。
推荐实践
- 使用 HTTPS:签名验证保护固件完整性与来源真实性,而 HTTPS 抵御中间人攻击,二者职责不同、应叠加使用;
- 密钥轮换:定期轮换密钥(注意:轮换需要一次包含新公钥的固件更新来"发布"新公钥)。
故障排查速查
"Signature verification failed"(错误码UPDATE_ERROR_SIGN,值为 14,见 Update.h 第 32 行)
- 确认固件确实用匹配的私钥签名;
- 确认草图中公钥与签名私钥成对;
- 确认签名方案(RSA/ECDSA)与哈希算法在签名侧(
--hash参数)和验证侧(USE_SHA*宏)一致; - 确认签名二进制在传输过程中未被损坏。
"Failed to install signature verification"
- 检查
installSignature()是否在Update.begin()之前调用; - 确认哈希与签名对象初始化正确(公钥 PEM 能被解析、
PUBLIC_KEY_LEN与实际长度匹配)。
"Public key parsing failed"(设备日志 "Failed to parse RSA/ECDSA public key")
- 确认公钥 PEM 格式正确(由
--extract-pubkey生成的.h文件可直接使用,其末尾 0x00 是 mbedtls 解析器所需); - 确认
PUBLIC_KEY_LEN与实际密钥长度一致。
7. 进阶用法与 API 参考
不烧录即可离线验证签名
python <ARDUINO_ROOT>/tools/bin_signing.py --verify firmware_signed.bin --pubkey public_key.pem该命令从文件尾部切出 512 字节签名区,去除零填充后按对应方案验签(verify_signature 实现),是发布流程中很好的 CI 检查点。
使用非默认哈希算法
签名侧与验证侧必须匹配:
# 签名侧:SHA-384 python <ARDUINO_ROOT>/tools/bin_signing.py --bin firmware.bin --key private_key.pem --out firmware_signed.bin --hash sha384// 草图侧:定义对应宏 #define USE_SHA384API 参考
类
UpdaterRSAVerifier:RSA 签名验证器(构造参数:pubkey, pubkeyLen, hashType,hashType缺省HASH_SHA256);UpdaterECDSAVerifier:ECDSA 签名验证器;- 二者均继承
UpdaterVerifyClass,内部持有 mbedtlspk_context(见 Updater_Signing.h)。
核心方法
// 安装签名验证,必须在 Update.begin() 之前调用 bool Update.installSignature(UpdaterVerifyClass *sign);相关错误码(Update.h)
UPDATE_ERROR_SIGN (14):签名验证失败;- 与之配合的还有
UPDATE_ERROR_SPACE (4)(OTA 分区剩余空间不足)、UPDATE_ERROR_STREAM (6)(流读取超时)等,可结合Update.errorString()定位问题。
8. 小结
Arduino-ESP32 的签名 OTA 方案把"密钥管理放在离线侧(Python 工具 + 私钥),验证逻辑放在设备侧(mbedtls + Update 库)"这条边界切得很清晰:bin_signing.py负责生成密钥、签名(固件 + 512 字节填充签名),Updater.installSignature()+begin/write/end状态机负责流式哈希、尾部签名回读与 RSA-PSS/ECDSA 验证,任何一环不匹配都会以UPDATE_ERROR_SIGN中止更新。按本文的六步流程,你可以把这套"未签名不启动"的安全机制直接落地到自己的 ESP32 产品线上。
【免费下载链接】arduino-esp32Arduino core for the ESP32 family of SoCs项目地址: https://gitcode.com/GitHub_Trending/ar/arduino-esp32
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考