3. 哈希算法入门:MD5、SHA-1、SHA-256的原理与对比

做固件校验,绕不开哈希算法。说白了,哈希就是给一段数据算出一个固定长度的“指纹”。指纹变了,数据肯定被人动过。我在项目里见过太多因为选错哈希算法导致的校验事故,所以这一章咱们把MD5、SHA-1、SHA-256这三个家伙彻底聊透。

3.1 哈希算法到底在干什么?

你想想看,一个固件镜像动辄几兆甚至几十兆。你要验证它有没有被篡改,总不能逐字节去比对吧?哈希算法就是来解决这个问题的。它把任意长度的输入,通过一个数学函数,压缩成一个固定长度的输出——这个输出就叫哈希值、摘要或者指纹。

好的哈希算法有三个核心特性:

  • 单向性:从哈希值反推原始数据,理论上不可能。嗯,这里要注意,是“计算上不可行”,不是绝对不可能。
  • 抗碰撞性:很难找到两个不同的输入,算出相同的哈希值。
  • 雪崩效应:输入改一个比特,输出就面目全非。

核心概念:哈希不是加密。加密可以解密,哈希不行。哈希是“摘要”,不是“密文”。

3.2 MD5:曾经的老大哥,现在的过气网红

MD5(Message Digest 5)是1992年诞生的,输出128位,也就是32个十六进制字符。我在刚入行那会儿,MD5几乎是校验的标配。那时候大家觉得它又快又好用。

但好景不长。2004年,中国密码学家王小云团队宣布找到了MD5的碰撞——也就是说,他们能构造出两个不同的文件,但MD5值一模一样。我当时看到这个消息,心里咯噔一下。后来我在一个OTA升级项目里,亲眼看到有人用MD5做校验,结果被攻击者用碰撞攻击替换了固件。从那以后,我再也不敢在安全场景里用MD5了。

警告:MD5已经不安全。不要在任何涉及安全校验的场景中使用MD5。它只适合做非安全性的数据去重或校验和。

3.3 SHA-1:比MD5强一点,但也不行了

SHA-1(Secure Hash Algorithm 1)由美国国家安全局(NSA)设计,1995年发布为标准。输出160位,比MD5长32位。理论上,它的抗碰撞性比MD5强。

但2017年,Google和CWI Amsterdam联合宣布了SHAttered攻击——他们成功构造了SHA-1碰撞。我记得当时我们团队正在评估一个老产品的固件升级方案,用的就是SHA-1。看到这个消息后,我们连夜改了方案,全部迁移到SHA-256。

你可能会问:SHA-1现在还在用吗?说实话,在一些老旧设备或者非安全场景里,确实还有人用。但我个人建议,只要你的固件涉及安全校验,就别碰SHA-1了。

算法 输出长度 安全性状态 典型应用
MD5 128位(16字节) 已破解,不安全 非安全校验、数据去重
SHA-1 160位(20字节) 已破解,不推荐 老旧系统兼容
SHA-256 256位(32字节) 安全,主流选择 固件校验、数字签名、TLS

3.4 SHA-256:为什么它是主流?

SHA-256属于SHA-2家族,也是NSA设计的,2001年发布。输出256位,比SHA-1多了96位。别小看这96位,它让暴力破解的难度呈指数级上升。

我个人的习惯是:只要做固件镜像校验,默认就用SHA-256。原因有三:

  1. 安全性足够:到目前为止,没有公开的SHA-256碰撞攻击。理论上它的安全强度是128位,这在可预见的未来是够用的。
  2. 硬件支持好:现在的MCU和SoC,很多都内置了SHA-256硬件加速器。我在STM32和ESP32上都用过,硬件算起来比软件快一个数量级。
  3. 生态成熟:OpenSSL、mbedTLS、Linux内核、各种Bootloader,全都原生支持SHA-256。你不用操心兼容性问题。

经验之谈:在嵌入式环境里,如果MCU没有硬件加速,SHA-256的软件实现可能会有点慢。我曾经在一个Cortex-M0的芯片上跑SHA-256,算一个1MB的固件花了将近3秒。这种情况下,可以考虑用硬件加速或者优化实现。

3.5 三种算法的原理对比(通俗版)

我不打算把数学公式贴出来,那玩意儿看着头疼。咱们用大白话聊聊它们的核心区别:

  • MD5:把数据分成512位的块,每块做4轮运算,每轮16步。说白了就是一顿乱搅,最后输出128位。结构简单,所以快,但也容易被攻破。
  • SHA-1:也是512位一块,但做80步运算,输出160位。比MD5多搅了几轮,安全性好一些,但终究没扛住。
  • SHA-256:块大小变成1024位,做64步运算,输出256位。每一步的运算逻辑比SHA-1复杂得多,用了更多的位运算和逻辑函数。说白了,搅得更狠,更难找到规律。

为什么会这样?因为攻击者要找的就是数学规律。MD5和SHA-1的数学结构相对简单,被找到了弱点。SHA-256的设计更保守,更“混乱”,所以目前还扛得住。

3.6 固件校验中的哈希算法选择建议

基于我这些年的经验,给你几个实际建议:

  • 新项目一律用SHA-256:别犹豫,这是最省心的选择。
  • 老项目迁移要评估性能:如果MCU太老,跑SHA-256太慢,可以考虑SHA-224(SHA-256的截断版)或者硬件加速。
  • 不要自己实现哈希算法:我曾经见过有人自己写了个“简化版SHA-256”,结果漏洞百出。用现成的库,比如mbedTLS、OpenSSL、或者芯片厂商提供的驱动。
  • 哈希校验要配合签名:哈希只能检测数据是否被篡改,但不能验证数据来源。要防伪造,还得加上数字签名(比如RSA或ECDSA)。这个后面章节会讲。

一句话总结:MD5已死,SHA-1垂危,SHA-256当立。做固件校验,选SHA-256就对了。

3.7 哈希算法在固件校验中的位置

下面这张图展示了哈希算法在整个固件校验流程中的位置。你可以看到,哈希是校验的核心环节,但不是全部。

固件校验流程中的哈希算法 固件镜像 哈希算法 哈希值 存储的参考哈希 比较 校验结果 说明: 哈希算法是固件校验的核心环节。固件经过哈希运算得到哈希值, 与安全存储的参考哈希比较,一致则固件未被篡改。

从图上你可以看到,哈希算法不是孤立存在的。它需要配合安全存储(存放参考哈希)和比较逻辑,才能构成完整的校验流程。我曾经在一个项目里踩过坑——参考哈希存在了Flash的明文区域,结果攻击者连固件带哈希一起改了,校验形同虚设。所以,哈希值本身也要保护好。

3.8 代码示例:用mbedTLS计算SHA-256

在嵌入式环境里,我常用mbedTLS。下面是一个计算SHA-256的示例代码:

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

int calculate_sha256(const uint8_t *data, size_t len, uint8_t output[32]) {
    mbedtls_sha256_context ctx;
    int ret;
    
    mbedtls_sha256_init(&ctx);
    ret = mbedtls_sha256_starts(&ctx, 0);  // 0表示SHA-256,1表示SHA-224
    if (ret != 0) {
        printf("SHA-256 starts failed: %d\n", ret);
        goto exit;
    }
    
    ret = mbedtls_sha256_update(&ctx, data, len);
    if (ret != 0) {
        printf("SHA-256 update failed: %d\n", ret);
        goto exit;
    }
    
    ret = mbedtls_sha256_finish(&ctx, output);
    if (ret != 0) {
        printf("SHA-256 finish failed: %d\n", ret);
        goto exit;
    }
    
exit:
    mbedtls_sha256_free(&ctx);
    return ret;
}

// 使用示例
void verify_firmware(const uint8_t *firmware, size_t fw_size) {
    uint8_t hash[32];
    uint8_t expected_hash[32] = { /* 从安全存储读取的参考哈希 */ };
    
    if (calculate_sha256(firmware, fw_size, hash) != 0) {
        printf("Hash calculation failed!\n");
        return;
    }
    
    if (memcmp(hash, expected_hash, 32) == 0) {
        printf("Firmware verified successfully.\n");
    } else {
        printf("Firmware verification FAILED!\n");
    }
}

小技巧:如果MCU支持硬件SHA-256,mbedTLS会自动检测并使用硬件加速。你不需要改代码,只需要在编译时启用对应的宏定义,比如MBEDTLS_SHA256_ALT。