密码学基础(上):哈希算法(SHA256)的原理与MCU实现

各位同学,咱们今天聊点硬核的——哈希算法。别一听“密码学”就头大,这东西在MCU安全启动里,说白了就是个“数字指纹”。你想想看,固件那么大,几兆甚至几十兆,你不可能把整个固件都加密签名吧?那太慢了。哈希的作用,就是把任意长度的数据,压缩成一个固定长度的摘要。

我个人习惯把哈希比作“榨汁机”。你扔进去一个苹果,出来一杯苹果汁;你扔进去一整个西瓜,出来的还是那杯果汁——只是浓缩程度不同。但有一点很关键:你永远没法从果汁里还原出原来的水果。这就是哈希的“单向性”。

SHA256 到底在算什么?

SHA256,输出是256位,也就是32个字节。我当年第一次看它的算法文档时,说实话,有点懵。一堆移位、异或、逻辑函数。但后来我把它拆解成几个步骤,就好理解了。

核心流程分三步:

  1. 填充:把原始数据补到512位的整数倍。末尾要加一个“1”比特,然后补一堆“0”,最后64位放原始长度。
  2. 分块:每512位(64字节)为一个处理块。
  3. 压缩:每个块经过64轮迭代,更新8个32位状态寄存器(A-H)。

嗯,这里要注意:SHA256的64轮迭代里,每轮都用到了不同的常量K和消息扩展字W。这些常量是固定的,算法标准里写死的。你在MCU上实现时,可以把它们放在Flash的常量区,别占RAM。

核心要点: SHA256的不可逆性来源于两个设计——非线性函数(如Σ0、Σ1)和模2^32加法。这两个操作组合在一起,让逆向推导变得几乎不可能。

MCU上的实现考量

在MCU上跑SHA256,跟PC上完全是两码事。PC上你调个OpenSSL就完事了,但在MCU上,你得考虑这几件事:

考量项 说明 我的建议
RAM占用 SHA256需要64字节的消息缓冲区 + 8个32位状态字 总共不到200字节,大部分MCU都扛得住
速度 软件实现通常每KB数据耗时几毫秒到几十毫秒 如果Bootloader里只校验几KB的签名,完全够用
代码大小 纯C实现大约2-3KB 建议用汇编优化关键轮函数,能省一半空间
硬件加速 部分MCU内置SHA引擎 有就用,没有也别慌,软件实现也能跑

我曾经在一个Cortex-M0的片子上做Bootloader,Flash总共才32KB。SHA256的C代码占了将近3KB,我心疼得不行。后来把64轮迭代里的关键路径用汇编重写了一遍,压缩到1.5KB,速度还快了30%。

小技巧: 如果你的MCU有DMA,可以把待校验的固件数据通过DMA喂给SHA引擎,CPU可以同时做其他事。我在STM32F4上这么干过,校验256KB固件只用了不到5ms。

哈希在固件完整性校验中的应用

咱们做安全启动,哈希主要用在两个地方:

  • 完整性校验:计算固件的哈希值,跟存储的参考值比对。不一样?说明固件被篡改了。
  • 数字签名的预处理:先对固件算哈希,再对哈希值做签名。这样签名速度快,而且签名长度固定。

你想想看,如果直接对整个固件做RSA签名,固件每大1KB,签名时间就多几十毫秒。但先算哈希,再签名,不管固件多大,签名时间都是固定的。

下面这张图,是我在课程里经常画的——固件完整性校验的典型流程:

固件完整性校验流程(SHA256 + 签名验证) 固件存储区 Flash / 外部存储 SHA256 计算 逐块读取 + 压缩 计算出的哈希值 32字节摘要 参考哈希值 安全存储区 哈希值比对 相等? ✅ 校验通过 启动固件 ❌ 校验失败 进入恢复模式 ⚠️ 安全启动关键点 参考哈希必须防篡改

避坑指南: 我曾经在一个项目里,把参考哈希值直接放在固件头部。结果测试时发现,攻击者改了固件内容,顺手把头部哈希也改了,校验居然通过了!后来我把参考哈希放在OTP区(一次性可编程区域),或者用签名保护起来。记住:参考哈希本身必须是可信的、不可篡改的。

MCU上SHA256的代码骨架

下面是一个精简的SHA256实现框架。实际项目中,我建议直接用mbedTLS或LibTomCrypt的移植版,但理解原理时,自己写一遍很有帮助。

// SHA256 上下文结构体
typedef struct {
    uint32_t state[8];    // A-H 状态寄存器
    uint32_t count;       // 已处理字节数
    uint8_t  buffer[64];  // 当前块缓冲区
} sha256_ctx;

// 初始化
void sha256_init(sha256_ctx *ctx) {
    ctx->state[0] = 0x6a09e667;
    ctx->state[1] = 0xbb67ae85;
    // ... 其他初始值
    ctx->count = 0;
}

// 更新(处理新数据)
void sha256_update(sha256_ctx *ctx, const uint8_t *data, uint32_t len) {
    // 填充缓冲区,满64字节就压缩一次
    // 这是最关键的循环
}

// 收尾(填充 + 输出摘要)
void sha256_final(sha256_ctx *ctx, uint8_t digest[32]) {
    // 补位、补长度
    // 输出state到digest
}

实际编码时,sha256_update里的循环是性能瓶颈。我建议用指针操作代替数组索引,能快不少。另外,注意字节序——SHA256是大端输出,而Cortex-M默认是小端,需要做转换。

调试技巧: 写完后,拿几个已知的测试向量验证一下。比如空字符串的SHA256是 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855。如果这个不对,说明你的实现有bug。

哈希碰撞——理论上存在,实际上不用怕

有人会问:SHA256会不会碰撞?理论上会,因为输入空间无限,输出空间只有2^256。但你想找到两个不同的输入,让它们的SHA256相等,需要大约2^128次尝试。这个数字有多大?

我用个比喻:你在地球上所有沙子里找一颗特定的沙子,比这还难2^128倍。所以,在MCU安全启动的场景下,碰撞攻击不是你需要担心的。

真正要防的是什么呢?是攻击者直接篡改固件,然后重新计算哈希。所以,哈希必须跟签名配合使用——签名保证了哈希值的来源可信,哈希保证了固件内容的完整性。

好了,这一节的内容就到这儿。哈希算法是安全启动的基石,理解透了,后面的签名验证才能学得扎实。