升级包制作与签名:升级包结构设计、差分算法选择、数字签名与验签流程

好,咱们进入第三个实战题目。说实话,升级包制作这块,是OTA系统里最容易出幺蛾子的环节。我见过太多团队,升级流程设计得花里胡哨,结果升级包本身结构混乱,签名验签漏洞百出。今天咱们就把这块掰开揉碎了讲清楚。

升级包结构设计

先聊聊结构。多域控制器升级,跟单芯片升级完全是两码事。你想想看,一个车里可能有七八个域控制器,每个域控制器里又有好几个MCU、SoC。升级包要是设计得不好,光解析就能把人搞疯。

我个人习惯,把升级包分成三层:

  • 元数据层:描述这个包是谁的、给谁用、版本号、校验信息
  • 载荷层:真正的固件数据,可能是完整镜像,也可能是差分补丁
  • 签名层:保证整个包没有被篡改过

具体到数据结构,我推荐用类似这样的方式组织:

// 升级包头部结构(简化版)
typedef struct {
    uint32_t magic;           // 魔数,0xOTA12345
    uint32_t version;         // 升级包格式版本
    uint32_t target_count;    // 目标域控制器数量
    uint32_t payload_offset;  // 载荷起始偏移
    uint32_t payload_size;    // 载荷总大小
    uint32_t signature_offset;// 签名数据偏移
    uint32_t signature_size;  // 签名数据大小
    uint32_t checksum;        // 头部校验和
} ota_package_header_t;

// 每个域控制器的描述块
typedef struct {
    uint8_t  domain_id;       // 域控制器ID
    uint8_t  update_type;     // 0:全量 1:差分
    uint16_t algo_type;       // 差分算法类型
    uint32_t old_version;     // 当前版本
    uint32_t new_version;     // 目标版本
    uint32_t data_offset;     // 该域数据在载荷中的偏移
    uint32_t data_size;       // 该域数据大小
    uint32_t data_hash[8];    // SHA-256哈希
} domain_descriptor_t;

这里有个坑,我踩过。魔数一定要选好,别用太常见的值。我曾经用0xDEADBEEF,结果跟某个调试工具冲突了,解析的时候老是报错。后来我改用0xOTA2024,再没出过问题。

核心原则:升级包结构要支持「按需下载」。什么意思?就是车机端不需要一次性下载整个包,可以根据自己的域控制器ID,只下载自己需要的那一段。这在带宽有限的车载环境下特别重要。

差分算法选择

差分升级,说白了就是只传变化的部分。为什么需要这个?你想想看,一个域控制器的固件可能50MB,每次升级只改了几KB的代码,全量下载太浪费流量了。

常见的差分算法有这么几种:

算法 适用场景 压缩率 内存消耗 我个人的评价
bsdiff 二进制文件差异大 高(需要大量内存) 效果好,但资源吃紧的MCU慎用
hdiffpatch 嵌入式固件 中高 中等 我项目里用得最多,平衡性好
xdelta 通用文件 适合资源受限的场景
自定义差分 特定格式固件 取决于实现 可控 如果你对固件结构非常了解,可以自己写

我建议这么选:

  • 如果目标域控制器是高性能SoC(比如座舱域),内存充裕,用bsdiff,压缩率最好
  • 如果是MCU(比如车身域、底盘域),内存只有几十KB,用xdelta或者自己写轻量级差分
  • 如果是中间件或者配置文件的升级,用hdiffpatch,它对小文件处理得不错

避坑指南:我曾经在一个项目里,对Bootloader用了bsdiff。结果差分补丁只有几百字节,但解压时需要32MB内存。Bootloader哪有那么多内存?直接挂了。后来我改成全量升级Bootloader,差分只用于Application区。记住:差分算法不是越高级越好,得看目标硬件的资源。

数字签名与验签流程

签名验签,这是OTA安全的最后一道防线。没有签名,你的升级包就是裸奔的。我见过一些团队,觉得「反正车机不联网,没人能攻击」,结果被物理接触攻击搞得很惨。

咱们用标准的做法:

  1. 签名端(云端)
    • 对升级包的载荷部分计算哈希(SHA-256)
    • 用私钥对哈希值进行签名(RSA-2048或ECDSA)
    • 把签名结果附加到升级包尾部
  2. 验签端(车端)
    • 收到升级包后,先校验头部checksum
    • 提取载荷数据,计算哈希
    • 用公钥解密签名,得到原始哈希
    • 对比两个哈希值,一致则通过

代码示例(伪代码,但逻辑是完整的):

// 云端签名流程
int sign_package(uint8_t* payload, uint32_t payload_size, 
                 uint8_t* signature, uint32_t* sig_size) {
    uint8_t hash[32];
    // 1. 计算载荷的SHA-256哈希
    sha256(payload, payload_size, hash);
    
    // 2. 用私钥签名哈希
    // 私钥存储在HSM或密钥管理服务中
    rsa_sign(hash, sizeof(hash), 
             private_key, private_key_len,
             signature, sig_size);
    
    return 0;
}

// 车端验签流程
int verify_package(uint8_t* payload, uint32_t payload_size,
                   uint8_t* signature, uint32_t sig_size) {
    uint8_t hash[32];
    uint8_t decrypted_hash[32];
    
    // 1. 计算载荷哈希
    sha256(payload, payload_size, hash);
    
    // 2. 用公钥解密签名
    // 公钥存储在安全存储区(如eFuse或OTP)
    rsa_verify(signature, sig_size,
               public_key, public_key_len,
               decrypted_hash);
    
    // 3. 比较哈希
    if (memcmp(hash, decrypted_hash, 32) != 0) {
        return -1; // 验签失败
    }
    
    return 0; // 验签通过
}

重要提醒:公钥的存储位置非常关键。千万别把公钥明文放在Flash里,那跟没签名一样。我建议:

  • SoC平台:放在eFuse或OTP中,一次性烧录,不可更改
  • MCU平台:放在安全Flash区,加上读保护
  • 密钥更新:通过安全通道下发,且需要旧密钥签名

还有一个细节,很多人会忽略——签名时效性。我建议在升级包的元数据里加入时间戳,验签时检查这个时间戳是否在有效期内。为什么要这么做?防止重放攻击。攻击者截获了一个旧版本的升级包,虽然签名是合法的,但版本太旧可能有已知漏洞。加上时间戳,就能避免这种问题。

嗯,关于升级包制作与签名,核心就是这些。结构要清晰,算法要匹配硬件,签名要严格。下一章咱们聊聊升级流程设计,到时候会讲到断点续传、失败回滚这些实战内容。

专注资料整理