4. 签名验证实战:从密钥生成到Bootloader集成
签名验证,说白了就是给固件贴上一张「防伪标签」。MCU启动时先撕开标签验一下,是真的才让跑。这个流程在车规、工控、IoT设备里几乎是标配了。今天我就带你完整走一遍——从OpenSSL生成密钥对,到把验签代码塞进Bootloader。
4.1 整体流程概览
先看一张图,把整个链路串起来。你心里有个谱,后面动手才不会乱。
4.2 生成密钥对:私钥锁保险柜,公钥贴门上
密钥对分两种角色:私钥用来签名,公钥用来验签。私钥一旦泄露,整个安全体系就崩了。我习惯把私钥放在离线机器上,甚至用U盘物理隔离。
4.2.1 生成RSA密钥对
RSA是目前最通用的方案。MCU端验签时,公钥长度建议用2048位,太长MCU扛不住,太短不安全。
# 生成RSA私钥(2048位)
openssl genrsa -out private.pem 2048
# 从私钥提取公钥
openssl rsa -in private.pem -pubout -out public.pem
# 查看公钥内容(PEM格式)
cat public.pem
我的习惯:生成私钥时加 -aes256 参数加密存储。虽然每次签名要输密码,但安全等级高一个档次。
4.2.2 生成ECDSA密钥对(推荐)
ECDSA用更短的密钥达到同等安全强度。256位ECDSA ≈ 3072位RSA。在资源受限的MCU上,我强烈推荐ECDSA。
# 生成ECDSA私钥(prime256v1曲线)
openssl ecparam -genkey -name prime256v1 -out ecdsa_private.pem
# 提取公钥
openssl ec -in ecdsa_private.pem -pubout -out ecdsa_public.pem
# 查看公钥
openssl ec -in ecdsa_public.pem -pubin -text -noout
注意:ECDSA的随机数质量直接影响安全性。如果随机数生成器有缺陷,私钥可能被还原。MCU端验签时,确保硬件TRNG可用。
4.3 对固件进行签名:给固件盖个章
签名过程分两步:先对固件做哈希摘要,再用私钥加密这个摘要。我见过有人直接对固件签名,效率低且不安全——正确的做法是先哈希。
4.3.1 计算固件哈希
# 计算固件文件的SHA256哈希
openssl dgst -sha256 -binary firmware.bin > firmware.hash
# 查看哈希值(十六进制)
xxd firmware.hash
4.3.2 用私钥签名哈希
# RSA签名
openssl pkeyutl -sign -in firmware.hash -inkey private.pem -out firmware.sig
# ECDSA签名(推荐)
openssl pkeyutl -sign -in firmware.hash -inkey ecdsa_private.pem -out firmware.sig
# 查看签名长度
ls -l firmware.sig
关键点:签名文件长度取决于算法。RSA-2048签名固定256字节,ECDSA-P256签名固定64字节(r+s各32字节)。这个长度在Bootloader中要预留好。
4.3.3 打包固件
把固件、签名、元数据(版本号、芯片ID等)打包成一个文件。我习惯用自定义头部格式:
# 打包命令示例
# 头部结构:魔数(4B) + 版本(4B) + 长度(4B) + 签名(64B/256B) + 固件数据
# 这里用cat简单演示
cat firmware.bin firmware.sig > firmware_signed.bin
| 字段 | 大小 | 说明 |
|---|---|---|
| 魔数 | 4字节 | 0xDEADBEEF,用于校验头部完整性 |
| 固件版本 | 4字节 | 防止回滚攻击 |
| 固件长度 | 4字节 | 实际固件数据长度 |
| 签名数据 | 64/256字节 | ECDSA或RSA签名 |
| 固件数据 | 可变 | 实际固件二进制 |
4.4 Bootloader集成验签代码
这是最硬核的部分。Bootloader启动时,先读固件头部,提取签名,计算哈希,再用公钥验签。通过才跳转,不通过就原地等待或进入恢复模式。
4.4.1 公钥嵌入Bootloader
公钥不能从外部加载,否则攻击者可以替换。必须硬编码在Bootloader的只读区域。我一般放在链接脚本的.rodata段。
// 将公钥转换为C数组
// 使用openssl导出公钥的原始字节
openssl ec -in ecdsa_public.pem -pubin -outform DER | xxd -i
// 生成的C数组示例
const uint8_t public_key[] = {
0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86,
0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a,
// ... 省略
};
4.4.2 验签核心函数
这里以mbedTLS为例,它是MCU上最常用的加密库。代码量不大,但每一步都不能错。
#include <mbedtls/pk.h>
#include <mbedtls/sha256.h>
#include <mbedtls/rsa.h>
#include <mbedtls/ecdsa.h>
/**
* @brief 验签函数
* @param firmware 固件数据指针
* @param fw_len 固件长度
* @param signature 签名数据指针
* @param sig_len 签名长度
* @return 0成功,-1失败
*/
int verify_firmware(const uint8_t *firmware, uint32_t fw_len,
const uint8_t *signature, uint32_t sig_len)
{
int ret;
mbedtls_pk_context pk;
mbedtls_sha256_context sha256_ctx;
uint8_t hash[32];
// 1. 初始化公钥上下文
mbedtls_pk_init(&pk);
ret = mbedtls_pk_parse_public_key(&pk, public_key, sizeof(public_key));
if (ret != 0) {
// 公钥解析失败,直接返回
return -1;
}
// 2. 计算固件SHA256哈希
mbedtls_sha256_init(&sha256_ctx);
mbedtls_sha256_starts(&sha256_ctx, 0); // 0表示SHA256
mbedtls_sha256_update(&sha256_ctx, firmware, fw_len);
mbedtls_sha256_finish(&sha256_ctx, hash);
mbedtls_sha256_free(&sha256_ctx);
// 3. 验签
ret = mbedtls_pk_verify(&pk, MBEDTLS_MD_SHA256,
hash, 0, signature, sig_len);
mbedtls_pk_free(&pk);
return (ret == 0) ? 0 : -1;
}
我曾经踩过的坑:验签时忘记检查签名长度。攻击者传一个空签名,mbedtls内部可能直接返回成功。一定要在调用前校验sig_len是否等于预期值(ECDSA-P256为64,RSA-2048为256)。
4.4.3 Bootloader主流程集成
void bootloader_main(void)
{
// 假设固件存储在外部Flash的0x08020000地址
#define APP_ADDR 0x08020000
#define SIG_OFFSET (APP_ADDR + 12) // 跳过头部12字节
#define FW_OFFSET (APP_ADDR + 12 + 64) // ECDSA签名64字节
// 读取头部信息
uint32_t magic = *(volatile uint32_t *)APP_ADDR;
uint32_t fw_len = *(volatile uint32_t *)(APP_ADDR + 8);
// 检查魔数
if (magic != 0xDEADBEEF) {
// 固件无效,进入恢复模式
enter_recovery_mode();
return;
}
// 验签
int result = verify_firmware(
(const uint8_t *)FW_OFFSET,
fw_len,
(const uint8_t *)SIG_OFFSET,
64 // ECDSA-P256签名长度
);
if (result == 0) {
// 验签通过,跳转到应用
jump_to_app(APP_ADDR);
} else {
// 验签失败,进入恢复模式
enter_recovery_mode();
}
}
4.5 避坑指南
这部分是我这些年攒下来的血泪教训,你直接拿去用。
- 公钥保护:公钥虽然叫「公」,但不能被篡改。Bootloader的只读区域要加写保护,防止通过调试接口修改。
- 哈希碰撞:SHA256目前安全,但别用MD5或SHA1。我在一个老项目里见过用CRC32做签名的——那基本等于没锁门。
- 回滚攻击:固件版本号要递增,Bootloader检查新固件版本必须大于当前版本。否则攻击者可以刷回有漏洞的老版本。
- 时序攻击:验签函数不要返回详细的错误码(比如「签名格式错误」「哈希不匹配」),攻击者可以利用这些信息。统一返回「验签失败」即可。
- 内存清零:验签完成后,把临时缓冲区(哈希、签名副本)清零。防止侧信道攻击通过内存残留数据推断密钥信息。
一句话总结:签名验证不是加个函数就完事了。密钥管理、防回滚、抗侧信道,每个环节都要闭环。我见过太多「验签通过了但安全还是被攻破」的案例,问题往往出在细节上。
好了,这一章的内容就到这里。你可以在开发板上先跑通RSA验签,再切换到ECDSA对比一下性能差异。动手试一次,比看十遍文档都管用。