密码学基础(上):哈希算法(SHA256)的原理与MCU实现
各位同学,咱们今天聊点硬核的——哈希算法。别一听“密码学”就头大,这东西在MCU安全启动里,说白了就是个“数字指纹”。你想想看,固件那么大,几兆甚至几十兆,你不可能把整个固件都加密签名吧?那太慢了。哈希的作用,就是把任意长度的数据,压缩成一个固定长度的摘要。
我个人习惯把哈希比作“榨汁机”。你扔进去一个苹果,出来一杯苹果汁;你扔进去一整个西瓜,出来的还是那杯果汁——只是浓缩程度不同。但有一点很关键:你永远没法从果汁里还原出原来的水果。这就是哈希的“单向性”。
SHA256 到底在算什么?
SHA256,输出是256位,也就是32个字节。我当年第一次看它的算法文档时,说实话,有点懵。一堆移位、异或、逻辑函数。但后来我把它拆解成几个步骤,就好理解了。
核心流程分三步:
- 填充:把原始数据补到512位的整数倍。末尾要加一个“1”比特,然后补一堆“0”,最后64位放原始长度。
- 分块:每512位(64字节)为一个处理块。
- 压缩:每个块经过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,签名时间就多几十毫秒。但先算哈希,再签名,不管固件多大,签名时间都是固定的。
下面这张图,是我在课程里经常画的——固件完整性校验的典型流程:
避坑指南: 我曾经在一个项目里,把参考哈希值直接放在固件头部。结果测试时发现,攻击者改了固件内容,顺手把头部哈希也改了,校验居然通过了!后来我把参考哈希放在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安全启动的场景下,碰撞攻击不是你需要担心的。
真正要防的是什么呢?是攻击者直接篡改固件,然后重新计算哈希。所以,哈希必须跟签名配合使用——签名保证了哈希值的来源可信,哈希保证了固件内容的完整性。
好了,这一节的内容就到这儿。哈希算法是安全启动的基石,理解透了,后面的签名验证才能学得扎实。