3、安全固件更新(SOTA/FOTA):更新包加密与签名、回滚保护机制、Vector 的更新客户端实现
固件更新,说白了就是给车机“打补丁”。但车不是手机,更新包要是被篡改,轻则功能失效,重则刹车失灵。我参与过好几个量产项目,每次聊到 OTA 安全,团队都会吵上几轮。今天咱们就把它拆开揉碎了讲清楚。
3.1 更新包加密与签名:双重保险
更新包在传输过程中,可能被截获、篡改,甚至被替换成恶意固件。怎么防?两个手段:加密 和 签名。
- 加密:保证机密性。别人拿到包也解不开。
- 签名:保证完整性和真实性。确保包是官方发的,没被改过。
我习惯的做法是:先签名,再加密。为什么?你想想看,如果先加密再签名,签名验证时你得先解密,流程上多了一步,而且解密密钥一旦泄露,签名就形同虚设。
核心流程:
- 生成固件镜像的哈希值(比如 SHA-256)。
- 用私钥对哈希值签名,生成签名数据。
- 将固件镜像 + 签名数据打包。
- 用对称密钥(AES-256)加密整个包。
- 用接收方的公钥加密对称密钥(RSA 或 ECC)。
嗯,这里要注意:私钥必须硬件保护。我在一个项目里见过把私钥硬编码在代码里的,结果被逆向出来,整个 OTA 系统都得重做。那叫一个惨。
3.2 回滚保护机制:别让旧版本害了你
回滚攻击是什么?攻击者把车机固件降级到一个有已知漏洞的旧版本,然后利用漏洞入侵。这招很阴,但也很常见。
怎么防?核心思路就一句话:让旧版本无法被安装。
具体做法有两种:
| 方法 | 原理 | 我踩过的坑 |
|---|---|---|
| 版本号检查 | Bootloader 比较当前版本和新版本的版本号。新版本必须 ≥ 当前版本。 | 版本号用字符串比较?坑死你。一定要用整数,且单调递增。 |
| 安全计数器 | 硬件中维护一个只增不减的计数器。每次更新成功,计数器 +1。Bootloader 检查新固件的计数器值必须 ≥ 当前值。 | 计数器溢出怎么办?我建议用 64 位,这辈子都够用。 |
警告: 我曾经遇到过一个项目,回滚保护只检查了版本号,但没检查签名。结果攻击者把旧固件重新签名后刷了进去。记住:回滚保护必须和签名验证联动。旧固件的签名必须被吊销,否则一切白搭。
3.3 Vector 的更新客户端实现
Vector 的工具链里,更新客户端通常跑在 vFlash 或者 CANoe 的测试环境里。但真正量产时,客户端是嵌入在 ECU 中的。我拿 vFlash 的架构来举例,因为它最典型。
客户端主要干三件事:
- 接收更新包:通过 CAN、以太网或 FlexRay 接收。
- 验证包:解密、验签、检查版本号。
- 刷写固件:擦除 Flash、写入新固件、校验完整性。
代码逻辑大致如下(伪代码):
// 伪代码:更新客户端核心流程
int update_firmware(uint8_t* encrypted_package, uint32_t size) {
// 1. 解密
uint8_t* decrypted = decrypt_package(encrypted_package, size);
if (decrypted == NULL) return ERROR_DECRYPT_FAIL;
// 2. 验签
if (!verify_signature(decrypted)) {
free(decrypted);
return ERROR_SIGNATURE_INVALID;
}
// 3. 检查回滚
uint32_t new_version = get_version_from_package(decrypted);
uint32_t current_version = read_current_version();
if (new_version < current_version) {
free(decrypted);
return ERROR_ROLLBACK_DETECTED;
}
// 4. 刷写
if (flash_write(decrypted) != OK) {
free(decrypted);
return ERROR_FLASH_FAIL;
}
// 5. 更新安全计数器
increment_security_counter();
free(decrypted);
return OK;
}
你看,逻辑不复杂,但每一步都不能出错。我见过一个案例,解密步骤放在验签之前,结果攻击者用无效包把解密模块搞崩溃了。所以顺序很重要:先验签,再解密,能省掉很多不必要的计算,也安全。
小技巧: Vector 的 vFlash 支持 增量更新。只传输差异部分,能省带宽。但增量更新的回滚保护更复杂,因为旧版本和新版本之间可能有多个中间版本。我建议在增量场景下,强制要求全量更新作为基线,否则回滚保护容易出漏洞。
最后说一句:安全固件更新不是一锤子买卖。你得考虑密钥轮换、证书吊销、异常处理。我在一个项目里,就因为没处理好更新中断后的恢复逻辑,导致 ECU 变砖。嗯,从那以后,我每次都会在 Bootloader 里留一个“救援模式”,哪怕主固件刷坏了,还能通过 CAN 刷回来。
说白了,安全是设计出来的,不是测试出来的。你设计时多花一小时,产线上就能少熬一个通宵。