第3章:哈希函数与消息认证码(MAC)——SHA-256、SM3、HMAC 在完整性校验中的应用

说到数据完整性,我第一个想到的就是哈希函数。你在平台工程里做镜像校验、包签名、日志防篡改,几乎处处都离不开它。说白了,哈希就是给数据打一个「数字指纹」——内容变了,指纹必定变。

但光有哈希还不够。你想想看,如果有人同时篡改了数据和哈希值,你怎么办?这就引出了消息认证码(MAC)的概念。MAC 在哈希的基础上加了密钥,只有持有密钥的人才能验证。这一章,我就把 SHA-256、SM3 和 HMAC 这三个家伙掰开揉碎了讲清楚。

3.1 哈希函数:数据完整性的基石

哈希函数有三个核心特性,我管它叫「三不放过」:

  • 不可逆:从哈希值反推原文,算不出来。这不是算力问题,是数学上就没这条路。
  • 抗碰撞:找不到两个不同的输入,产生相同的哈希值。嗯,这里要注意——理论上碰撞一定存在,但计算上找不到,就是安全的。
  • 雪崩效应:输入改一个比特,输出就面目全非。

我在评估一个哈希算法时,先看它的输出长度。SHA-256 输出 256 位,SM3 也是 256 位。为什么是 256 位?因为 128 位的 MD5 已经被撞烂了,160 位的 SHA-1 也基本宣告失效。256 位是目前工程界的「安全底线」。

核心观点: 完整性校验不是「防破解」,而是「防无感知篡改」。攻击者改了数据却改不了哈希值,系统就能在第一时间发现异常。

3.2 SHA-256:国际通用的硬通货

SHA-256 是 SHA-2 家族里使用最广的一员。为什么不是 SHA-512?因为 256 位的性能更好,安全强度在绝大多数场景下已经足够了。

它的处理流程大致是这样:

  1. 原始数据先做填充,使长度成为 512 位的倍数。
  2. 每 512 位分成 16 个 32 位字,再扩展成 64 个字。
  3. 用 8 个初始哈希值(H0~H7)做 64 轮压缩运算。
  4. 最后输出 8 个 32 位字拼成的 256 位哈希值。

我在项目中遇到过一个问题:有人在代码里直接用 Java 的 MessageDigest 做文件完整性校验,但忘了处理大文件分块读取的问题。结果内存溢出,生产环境直接挂了。用 SHA-256 校验大文件,一定要用流式更新,别一次性吞进去。

// 正确的流式 SHA-256 校验方式
MessageDigest md = MessageDigest.getInstance("SHA-256");
try (FileInputStream fis = new FileInputStream("large-file.bin");
     DigestInputStream dis = new DigestInputStream(fis, md)) {
    byte[] buffer = new byte[8192];
    while (dis.read(buffer) != -1) {
        // 不断更新摘要
    }
    byte[] hash = md.digest();
    System.out.println(bytesToHex(hash));
}

经验之谈: 我习惯把 SHA-256 的哈希值用 Base64 编码而不是 Hex 编码存储。Base64 只有 44 个字符,比 Hex 的 64 个字符省了将近三分之一的空间。在存大量校验和时,这个差异很明显。

3.3 SM3:国密标准的硬实力

SM3 是国家密码管理局发布的商用密码哈希算法,输出也是 256 位。它的结构和 SHA-256 类似,但做了不少改进。我仔细对比过两者的设计——SM3 的压缩函数使用了更多的异或操作和循环移位,在安全性上有一定优势。

但说实话,SM3 在平台工程里的推广一直有个痛点:生态不够完善。你用 SHA-256,几乎任何语言的标准库都直接支持。SM3 就需要额外引入 Bouncy Castle 或者国密的 SDK。我曾在给一个央企做 CI/CD 流水线时,就因为 SM3 的依赖问题折腾了大半天。

下面这张表让你快速看清两者的差异:

属性 SHA-256 SM3
输出长度 256 位(32 字节) 256 位(32 字节)
分组长度 512 位 512 位
压缩轮数 64 轮 64 轮
消息扩展 16→64 字 16→68 字 + 64 个派生字
标准制定 NIST(美国) 国密局(中国)
合规场景 国际通用 国内等保、密评要求
生态成熟度 极高 中等

选哪个?我的建议很简单:如果你的系统要过等保或密评,必须用 SM3,别犹豫。如果是国际项目或者 SaaS 服务,用 SHA-256 就够了。 双算法同时支持也是一种常见的折中方案。

不要自作聪明「魔改」算法。 我曾经见过有人把 SM3 的输出截成 128 位用,说是为了「省空间」。这样做等于把算法的安全强度砍掉了一大半。256 位就是 256 位,别截断。

3.4 HMAC:给哈希加把锁

哈希函数只能证明数据没变,但不能证明这个哈希值是谁算出来的。这就是 MAC(消息认证码)登场的原因。HMAC 是一种基于哈希的 MAC 构造方式,它可以和任何哈希算法搭配使用——HMAC-SHA256、HMAC-SM3 都是常见组合。

HMAC 的构造公式我记了多年:

HMAC(K, m) = H((K' ⊕ opad) || H((K' ⊕ ipad) || m))

其中 K' 是经过填充的密钥,opad 和 ipad 分别是外填充和内填充常量。这个双层哈希结构能有效防止长度扩展攻击——嗯,这是 SHA-256 本身的一个弱点,HMAC 把它堵住了。

我在做 API 签名方案时,几乎不会直接用哈希,而是用 HMAC。为什么?因为哈希签名是「裸签名」,任何人只要知道哈希算法就能伪造签名。而 HMAC 绑定了密钥,攻击者看不到密钥,就算知道算法也签不出来。

来看一段 HMAC 签名的实际代码:

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public static String hmacSha256Sign(String data, String secret) {
    Mac mac = Mac.getInstance("HmacSHA256");
    SecretKeySpec keySpec = new SecretKeySpec(
        secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
    mac.init(keySpec);
    byte[] result = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
    return Base64.getEncoder().encodeToString(result);
}

关键区别: 哈希解决「数据是否被改」,MAC 解决「数据是否被改 + 谁签的」。如果把哈希比作指纹,那 MAC 就是带有签名人信息的指纹。

3.5 完整性校验的三个实践层级

在平台工程里,我用完整性校验分了三个层级,每个层级侧重点不同:

层级 典型场景 推荐算法 我的建议
文件级 镜像校验、包分发 SHA-256 / SM3 发布时生成哈希值,下载后重新计算比对
传输级 API 请求、消息队列 HMAC-SHA256 用时间戳 + 随机数做防重放,别只用数据签名
存储级 日志防篡改、数据库校验 HMAC-SM3 密钥定期轮换,旧密钥保留验证能力但不用于签名

你看,不同层级用的算法和策略都不一样。文件级只需要防篡改,用裸哈希就够了。但传输级和存储级必须防伪造,这时候 HMAC 就不可或缺了。

3.6 知识体系总览

下面这张图把本章的核心内容串在了一起。从哈希函数的两大分支(SHA-256 和 SM3)到 HMAC 的密钥加持,再到完整性校验的三个层级。你可以把它当一张「决策地图」来用:

哈希函数与消息认证码知识体系 哈希函数与消息认证码(MAC)知识体系 完整性校验核心 SHA-256 SM3 国密 HMAC 消息认证码 64 轮压缩 SP 结构 512 位分组 68+64 扩展 双字迭代 国密标准 双层哈希 密钥绑定 抗长度扩展 平台工程完整性校验三大场景 文件级·镜像校验 传输级·API 签名 存储级·日志防篡改 选型口诀:公开校验用哈希,防伪认证用 MAC,合规场景选 SM3

3.7 避坑总结

把这些年踩过的坑和看到过的教训总结一下:

  • 别用 MD5 和 SHA-1。就算是内部系统也不行。我见过太多人觉得「内网没事」,结果被内部员工利用碰撞攻击搞出了事故。
  • HMAC 的密钥别硬编码在代码里。 从环境变量或密钥管理服务(KMS)里取。我曾经接手过一个项目,密钥直接写在 Git 仓库里——那感觉就像看到有人把家门钥匙贴在门框上。
  • 哈希值比对要用常量时间比较。 字符串的 equals() 在发现第一个不同字符就会返回,攻击者可以利用这个时间差做侧信道攻击。用 MessageDigest.isEqual() 或者自己实现 XOR 后再判断。
  • SM3 的依赖版本要盯紧。 国密算法在不同库里的实现细节偶有差异,跨语言互操作时一定要做向量测试,踩过这个坑的人不在少数。

一个小工具推荐: 我习惯在 CI/CD 流水线里加一个「哈希校验」步骤,用 sha256sum 或 gmssl sm3 命令验证构建产物。这个步骤只需要几秒钟,但能拦住大部分的分发事故。

好了,关于哈希函数和消息认证码,该说的都在这里了。SHA-256 和 SM3 是完整性的「听诊器」,HMAC 是完整性的「签名笔」。选对工具,用对姿势,你的平台工程就能在安全上站稳脚跟。