news 2026/9/14 18:50:32

Arduino-ESP32 签名 OTA 安全更新实战:RSA/ECDSA 固件签名验证完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Arduino-ESP32 签名 OTA 安全更新实战:RSA/ECDSA 固件签名验证完整指南

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):签名验证失败则更新直接失败,不会启动未验证固件。

前置条件:

  1. 安装cryptography包的 Python 3:
pip install cryptography
  1. 带 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:配置示例草图

  1. 把生成的public_key.h复制到示例目录,覆盖测试公钥;
  2. 打开 Signed_OTA_Update.ino,修改 Wi-Fi 凭据:
const char *ssid = "YOUR_SSID"; const char *password = "YOUR_PASSWORD";
  1. 修改固件 URL:
const char *firmwareUrl = "http://your-server.com/firmware_signed.bin";
  1. 根据你的密钥类型取消对应宏注释(草图第 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)并构造UpdaterRSAVerifierUpdaterECDSAVerifier对象。

Step 3:编译并上传初始固件

  1. 编译并烧录该草图到 ESP32;
  2. 打开串口监视器(115200 波特率)确认运行正常。草图的setup()会连接 Wi-Fi、打印 IP,然后直接执行一次performOTAUpdate()——这是一个"上电即尝试更新"的最简演示形态,实际项目中应加版本比较或触发条件。

Step 4:构建并签名新固件

  1. 修改草图(比如加版本号),重新构建并导出二进制:
    • Arduino IDE:SketchExport Compiled Binary
    • 生成的 application.bin位于草图目录的build文件夹下,例如build/espressif.esp32.esp32c6/Signed_OTA_Update.ino.bin
  2. 签名:
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_optionsexpected_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-20482048 bits256 bytes中等
RSA-30723072 bits384 bytes较慢很高
RSA-40964096 bits512 bytes最慢最高
ECDSA-P256256 bits64 bytes
ECDSA-P384384 bits96 bytes很高

文档推荐:RSA-2048 或 ECDSA-P256 在安全性与性能间取得良好平衡。签名长度常量可在 Updater_Signing.h 中核对(RSA_2048_SIGNATURE_SIZE 256ECDSA_P256_SIGNATURE_SIZE 64),注意它们都小于设备端统一的 512 字节签名缓冲区,短签名靠零填充对齐。

哈希算法对比表:

算法输出长度速度安全强度
SHA-25632 bytes
SHA-38448 bytes中等很高
SHA-51264 bytes中等很高

文档推荐:多数应用 SHA-256 已足够,且为 Python 工具与设备端的默认算法。

6. 安全管理建议与故障排查

私钥管理(来自文档 Security Considerations 一节)

  • 绝不把私钥提交到版本控制;
  • 加密存储或使用 HSM;
  • 限制访问范围,仅授权人员可接触;
  • 考虑开发与生产环境使用不同密钥。

推荐实践

  1. 使用 HTTPS:签名验证保护固件完整性与来源真实性,而 HTTPS 抵御中间人攻击,二者职责不同、应叠加使用;
  2. 密钥轮换:定期轮换密钥(注意:轮换需要一次包含新公钥的固件更新来"发布"新公钥)。

故障排查速查

"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_SHA384

API 参考

  • UpdaterRSAVerifier:RSA 签名验证器(构造参数:pubkey, pubkeyLen, hashTypehashType缺省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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 18:50:30

MiGPT实战:3步把小爱音箱变成AI语音助手

MiGPT实战&#xff1a;3步把小爱音箱变成AI语音助手 【免费下载链接】mi-gpt &#x1f3e0; 将小爱音箱接入 ChatGPT 和豆包&#xff0c;改造成你的专属语音助手。 项目地址: https://gitcode.com/GitHub_Trending/mi/mi-gpt 问小爱"为什么天空是蓝的"&#x…

作者头像 李华
网站建设 2026/9/14 18:50:17

5分钟跑通自动驾驶软件栈:Autoware 安装与快速上手完整指南

5分钟跑通自动驾驶软件栈&#xff1a;Autoware 安装与快速上手完整指南 【免费下载链接】autoware Autoware - the worlds leading open-source software project for autonomous driving 项目地址: https://gitcode.com/GitHub_Trending/au/autoware 装自动驾驶环境&am…

作者头像 李华
网站建设 2026/9/14 18:47:28

小爱音箱接大模型:MiGPT 四步部署实录

小爱音箱接大模型&#xff1a;MiGPT 四步部署实录 【免费下载链接】mi-gpt &#x1f3e0; 将小爱音箱接入 ChatGPT 和豆包&#xff0c;改造成你的专属语音助手。 项目地址: https://gitcode.com/GitHub_Trending/mi/mi-gpt 你喊"小爱同学&#xff0c;今天天气怎么样…

作者头像 李华