下载一份账单,网页同时给出“账单的 SHA-256”。算完发现两串十六进制相等:能说这份文件是银行发的吗?如果下载页和摘要都被同一个攻击者替换,不能。同样地,把一笔交易的哈希值打印出来,既不证明是谁授权交易,也不证明它已经写入共同历史。摘要比较首先回答的是已知预期摘要时,当前输入是否与它对应;这里的“已知”必须说清来源。

在 00 里,攻击者同时换掉合成订单与公开预期摘要,相等检查仍返回真。02 又展示不同的字段组可以拼出完全相同的字节,不论使用哪种哈希函数都救不了未定义的字段边界。这篇再看哈希本身承诺哪类困难,以及三个经常被混称为“找到碰撞”的不同问题。

输入、输出和可信起点

对固定算法 SHA-256,输入是一串确切字节,输出是 32 字节。例如 b"abc" 的摘要为 ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad。输出的十六进制有 64 个字符,只是每个字节写成两个字符后的显示形式。该算法不使用秘密密钥,任何拿到消息的人都能重复计算。

1
2
3
4
5
6
作者文件字节 -- SHA-256 --> 摘要 ----[通过可信渠道]----> 校验者
|
+---[不可信下载通道]---> 校验者算 SHA-256 并与可信摘要比较

如果两条通道都由攻击者控制:
攻击者文件 + 攻击者摘要 -----------------------------> 相等仍可成立

可信摘要可以来自已经认证的发布页、通过可信渠道公布的签名清单,或更高层已验证的协议结构;不能把一句“网页上有摘要”当作保证。即使预期摘要确实可信,比较结果也只针对文件内容,不自动保证程序运行安全或发行者可信。没有可信摘要时,哈希有助于文件索引和意外错误排查,却不是来源认证机制。要让公众能验证某人授权了明确消息,需要密钥与可信公钥绑定;12–14 再讲数字签名与身份。

三个困难问题不是同一道题

原像问题先给一个摘要,让攻击者找出任何能得到它的输入。第二原像问题先固定一个确切消息,攻击者必须找出另一个不同消息且摘要相同。碰撞问题允许攻击者同时自由选择两个不同消息,只要摘要相同即可。这些是关于攻击者可选择哪些输入的问题,不等于某次实验能证明普遍安全。

对于理想化的 n 位均匀摘要,找一个任意碰撞约需 2^(n/2) 量级的尝试,针对固定目标的一般搜索量级约为 2^n;这些是用于解释差别的近似模型,具体函数、候选空间与攻击算法还需另查。一个输出只有 16 位的玩具摘要,找任意两条相同值不难;这并不意味着要对某个指定目标找到替身也一样容易。现实输入若是弱密码,则敌人可以枚举小字典,所谓“很难反推摘要”也不能保护低熵秘密——08 再解释密码哈希。

这里还需要一个反常识的澄清:输入可以比摘要长得多,所以从数学上说碰撞一定存在;密码学追求的是在指定资源和威胁假设下难以找到合适碰撞。另一个澄清是,算法安全是随研究进展与参数选择变化的判断;本文不会凭一段 Python 循环宣称 SHA-256 在任何场景“绝对安全”。

可失败实验:只截取 16 位

examples/cryptography/03_hashes.py 用 Python 标准库 hashlib.sha256 计算真实 SHA-256,然后故意只取前 2 字节当作教学摘要。候选输入是公开的 demo-0, demo-1, ...,最多试 2000 个,枚举遇到的第一组不同消息的前缀相同就停止。

1
2
3
SHA-256(b"demo-270")[:2]  -> 24 5d
SHA-256(b"demo-309")[:2] -> 24 5d
完整 SHA-256 两份结果并不相同

仓库根目录运行:

1
2
python3 examples/cryptography/03_hashes.py
python3 -m unittest discover -s examples/cryptography -p 'test_03*.py' -v

2026-10-06 UTC,在 Python 3.12.3 上两条命令退出码均为 0,3 个测试通过。脚本检查已知 b"abc" 的 SHA-256 完整向量;测试还要求两条不同输入的截短摘要相同、完整 SHA-256 不同。若移除 [:2] 却仍声称那两个输入碰撞,测试便会失败。这个实验展示的是“把输出截短会缩小搜索空间”,不是破解 SHA-256,也不是成功攻击真实 TLS、Merkle 树或区块链。

让 demo-270 成为预先指定的目标,另去找同摘要的其他输入,才算尝试第二原像;本实验枚举时没有预先指定这个目标。让攻击者先拿到一个摘要、完全不知道相应消息,再找任意匹配输入,才是另一种原像问题。不要把枚举一对任意消息的实验标题写成“突破单向性”。

放回 HTTPS 和交易的两条路径

TLS 握手包含协议规定的消息摘要,但摘要本身并不认证服务端;证书链、绑定到握手内容的认证动作与密钥派生还各有职责。换一个握手字段通常会改变待验证的字节,但只有可信认证依据在场,浏览器才有资格把计算成功解释为“这是预期服务”。如何连接证书、握手签名和 Finished,要到 14–16 看具体消息和密钥持有人。

钱包对交易的某个确定表示产生授权证据,区块和 Merkle 结构用摘要关联内容。节点能重新计算交易摘要,不等于能替钱包授权转账;拿到一条 Merkle 路径也要先信任对应的根。30 篇再看“许多有效签名的候选历史,哪个成为共同历史”:共识和摘要属于不同问题。即使交易被验证、包含或执行,也不能推出“合成订单在现实世界已交付”。

下次遇见 hash(message) == expected,不要先问 SHA-256“强不强”,而要依次问:message 是哪些字节、expected 来自哪里、比较结果只覆盖内容还是还包括身份与时间?删除“可信摘要来源”后,攻击者连文件和摘要一起换掉就能通过;删除字节格式约定,两个参与者甚至可能不在核对同一个业务对象。

两道带答案的练习

推导题。 在上图中,攻击者控制不可信下载通道,却不能改掉另一路送来的已认证摘要。对一份新文件它需要完成哪类任务才能让比较成功?如果攻击者可以自由设计原文件和假文件,再诱导作者发布其中一份摘要,任务会怎样改变?

可核对答案: 前一种情况需针对已被固定的预期内容或摘要构造匹配替代物(具体是第二原像还是原像,取决于是否已知原内容);后一种可尝试先找任意两个不同输入但摘要相同的碰撞,再选择发布路径。可选择输入的范围决定攻击问题;但可信摘要若直接来自攻击者,两种高成本搜索都不需要。

变更题。 把 toy_digest 的截取长度从 2 字节改成 1 字节,运行实验;再改成 4 字节、保持 2000 次上限。分别记录退出码与失败位置,能否仅从“这次找不到”推断无碰撞?

可核对答案: 1 字节情况下通常更快找到不同输入的同前缀,测试仍要求完整摘要不同;4 字节的这组有限候选未必找到,会在 find_toy_collision 抛出 ValueError,相关测试失败。一次有限搜索没找到,不构成“没有碰撞”的证明;是否恰好找到以真实运行的确定性结果为准,不伪造实验输出。

资料与导航