3、哈希算法基础:MD5、SHA-1、SHA-256算法原理、哈希在OTA中的应用、文件完整性校验

各位同学,咱们今天聊聊哈希算法。这东西在OTA升级里,就像一把看不见的锁。没有它,你升级包传过去是啥样,设备那边收到是啥样,你根本不知道。我刚开始做嵌入式那会儿,就吃过这个亏,后面会讲到。

3.1 哈希算法是个啥?

说白了,哈希算法就是一个“指纹生成器”。你给它一段数据,不管是一行文字还是一个几百兆的固件包,它都能算出一个固定长度的“摘要”。这个摘要,就是数据的唯一标识。

哈希算法有几个关键特性:

  • 单向性:从摘要反推原始数据,几乎不可能。嗯,这就是“哈希”名字的由来——像把肉剁碎了,你没法再拼回一头猪。
  • 抗碰撞性:两个不同的数据,算出同一个摘要,概率极低。我遇到过有人问“万一撞上了怎么办?”——理论上可能,但现实中你买彩票中头奖的概率都比它高几个数量级。
  • 固定输出长度:不管输入是1KB还是1GB,MD5输出永远是128位,SHA-256输出永远是256位。

核心理解:哈希不是加密。加密可以解密,哈希不行。哈希是用来“校验”的,不是用来“保密”的。

3.2 三大哈希算法对比

咱们做OTA,最常打交道的就三个:MD5、SHA-1、SHA-256。我一个个说。

3.2.1 MD5(Message Digest 5)

MD5出生最早,1992年就有了。输出128位,也就是32个十六进制字符。当年可是红极一时,几乎所有下载站都用它做校验。

但是——注意这个但是——MD5已经被证明不安全了。2004年,中国山东大学的王小云教授团队就找到了碰撞方法。什么意思?就是你能人为构造两个不同的文件,让它们MD5值一模一样。

警告:我在项目中见过有人还在用MD5做OTA升级包的完整性校验。我当时的反应是:“兄弟,你这是给黑客留后门啊。” 千万别在安全敏感的场景里用MD5。它现在只适合做非安全场景的快速校验,比如检查文件是否下载完整。

3.2.2 SHA-1(Secure Hash Algorithm 1)

SHA-1是MD5的升级版,输出160位。美国国家安全局(NSA)设计的,曾经是业界标准。GitHub早期就用SHA-1标识代码提交。

但SHA-1也老了。2017年,Google和CWI研究所联合宣布了世界上第一个SHA-1碰撞实例。两个不同的PDF文件,SHA-1值完全一样。这消息一出,整个行业都开始加速淘汰SHA-1。

我个人习惯是:SHA-1现在只用于兼容旧设备。新设计的OTA系统,直接跳过它。

3.2.3 SHA-256(SHA-2家族的代表)

SHA-256是目前的主流选择。输出256位,安全性远高于前两者。它是SHA-2家族的一员,同样由NSA设计。

为什么推荐它?三个原因:

  • 安全性足够:至今没有有效的碰撞攻击方法
  • 性能可接受:在ARM Cortex-M4上,计算1MB数据的SHA-256大概需要几十毫秒,完全可以接受
  • 行业标准:TLS 1.3、HTTPS证书、区块链,全都在用SHA-256
算法 输出长度 安全性 推荐场景
MD5 128位 已破解 非安全校验、快速检查
SHA-1 160位 已破解 兼容旧设备
SHA-256 256位 安全 OTA升级、签名验证

3.3 哈希在OTA中的应用

好了,理论讲完了。咱们看看哈希在OTA升级里到底怎么用。说白了就两个场景:

3.3.1 文件完整性校验

你想想看,固件包从服务器传到设备,中间要经过多少环节?网络传输、存储芯片写入、解压缩……任何一个环节出问题,固件就坏了。设备刷了坏固件,轻则死机,重则变砖。

哈希怎么解决?简单:

  1. 服务器计算固件包的哈希值(比如SHA-256)
  2. 把哈希值附在升级包元数据里,或者单独下发
  3. 设备下载完固件后,自己算一遍哈希
  4. 对比两个哈希值。一致就说明文件完整,不一致就说明出问题了

实战技巧:我建议在设备端做哈希校验时,采用“边下载边校验”的方式。不要等整个文件下载完再算哈希,那样太慢。可以分块计算,最后合并。很多MCU的硬件哈希加速器都支持这种模式。

3.3.2 数字签名的基石

哈希的另一个重要应用,是配合非对称加密做数字签名。这个咱们下一章会详细讲,这里先提一嘴:

  • 签名时,先对固件包算哈希,再对哈希值做加密签名
  • 验签时,先解密签名得到哈希值,再对比自己算的哈希

为什么要这么绕?因为非对称加密很慢。直接加密整个固件包,性能扛不住。先哈希再签名,既保证了安全性,又兼顾了性能。

3.4 代码实战:计算SHA-256

光说不练假把式。咱们写一段C代码,看看怎么在嵌入式设备上算SHA-256。这里用mbedTLS库,它是嵌入式领域最常用的加密库之一。

#include <mbedtls/sha256.h>
#include <stdio.h>
#include <string.h>

void calculate_sha256(const unsigned char *data, size_t len) {
    unsigned char hash[32];  // SHA-256输出32字节
    char hex_output[65];     // 十六进制字符串
    
    mbedtls_sha256_context ctx;
    mbedtls_sha256_init(&ctx);
    mbedtls_sha256_starts(&ctx, 0);  // 0表示SHA-224,1表示SHA-256
    
    // 分块计算,适合大文件
    size_t chunk_size = 1024;
    size_t offset = 0;
    while (offset < len) {
        size_t this_chunk = (len - offset > chunk_size) ? chunk_size : (len - offset);
        mbedtls_sha256_update(&ctx, data + offset, this_chunk);
        offset += this_chunk;
    }
    
    mbedtls_sha256_finish(&ctx, hash);
    mbedtls_sha256_free(&ctx);
    
    // 转成十六进制字符串
    for (int i = 0; i < 32; i++) {
        sprintf(hex_output + i * 2, "%02x", hash[i]);
    }
    hex_output[64] = '\0';
    
    printf("SHA-256: %s\n", hex_output);
}

int main() {
    const char *test_data = "Hello, OTA World!";
    calculate_sha256((unsigned char*)test_data, strlen(test_data));
    return 0;
}

避坑指南:我曾经在项目里犯过一个低级错误——忘了调用mbedtls_sha256_starts()初始化。结果每次算出来的哈希值都不一样,排查了半天。记住:任何加密库的上下文,都要先init再start,最后finish。顺序错了,结果全错。

3.5 哈希校验的常见陷阱

最后,我分享几个实战中容易踩的坑:

  • 大小写问题:哈希值的十六进制表示,大小写都可以。但比较时一定要统一。我习惯全部转成小写再比较。
  • 换行符差异:Windows用\r\n,Linux用\n。同一个文件在不同系统上算出的哈希值可能不同。解决办法:以二进制模式读取文件。
  • 内存对齐:有些MCU的硬件哈希加速器要求输入数据地址4字节对齐。不对齐的话,要么死机,要么算错。
  • 多线程安全:mbedTLS的哈希上下文不是线程安全的。如果多个任务同时算哈希,记得加锁或者每个任务用独立的上下文。

好了,哈希算法的基础就讲到这里。记住一句话:OTA升级,哈希先行。没有哈希校验的OTA,就像没锁门的房子——你永远不知道谁进去过。

下一章,咱们聊聊数字签名,看看哈希和RSA是怎么配合的。到时候我会分享一个我踩过的“签名验证失败”的大坑,保证让你印象深刻。