DevKitLab Logo DevKitLab
哈希 / 校验和 / 完整性 / 排查

校验和为什么对不上?哈希不匹配的排查指南

校验和对不上,感觉像警报,可这警报只有一档:两串字节不一样。它不告诉你为什么。真正的功夫不是把哈希重算一遍,而是把「对不上」收敛到三类原因里:数据真的不同、你把两个哈希比得不对等、或者你信的那个参考值本身就错了。这篇把三类都走一遍。

你下载了一个发布包——ISO、.tar.gz 或者安装程序——项目页面还贴心地在旁边给了一个 SHA-256。你在本地跑 sha256sum,把两串字符对齐一看,对不上。然后呢?大多数人跳过的一个事实是:光看「对不上」这件事,几乎什么都说明不了。哈希的设计就是这样——改一个比特,和换成一个完全不同的文件,算出来的摘要都会天差地别。没有「差一点」这回事。比对是全有或全无的,而比对失败只说明一件事:喂进这两个哈希的字节流不一样。不是损坏,不是被篡改,也不是坏掉了——只是「不一样」而已。真正有用的功夫,全在搞清楚「为什么」不一样。

(先说一下用词:本文里的「校验和」取下载场景的日常说法,就是你下完文件拿来比对的那串值。严格讲,SHA-256 是密码学哈希(也叫摘要),CRC32 那种才算非密码学校验和。MD5 仍然属于密码学哈希,只是抗碰撞性已经不够了——拿来做非对抗性的文件完整性校验,还能查出大多数偶然改动,但不适合安全认证。不管你手里是哪一种,下面的排查思路都一样。)

所以别把哈希重算五遍指望它变。它不会变。把「对不上」当成一次三岔分流的起点。一次校验和比对只有三个环节,对不上就意味着其中一个或多个出了岔——有时候还是几个一起——所以文末的清单会把三个都走一遍,而不是撞上第一个就停:

  1. 数据真的不一样。你哈希的那份字节,跟参考值当初计算的那份不是同一批——下载被截断或中断、拿错了文件、压缩包被重新打包,或者传输中真的损坏了。
  2. 两个哈希的比对方式不对等。字节可能是一样的,但你没把两个摘要放在同一个标准下比——算法不同、hex 和 base64 表示不一致,或者你盯着看的那串字符在复制时混进了杂质。
  3. 参考值本身就是错的或不可信。你拿来对照的那个数字过期了、来自另一个构建版本,或者出自一个你根本没理由信任的地方——这种情况下,「对上了」反而才是真问题。

本文按你实际该检查的顺序讲这三点。因为最省事、也最常见的原因——你把字符串比错了——恰恰是大家最后才想到的。

一个核心:哈希是指纹,指纹没有「差不多」

下面的一切都建立在一个性质上。加密哈希把任意输入——一个字节,或者一个 4 GB 的磁盘镜像——映射成一段定长的指纹,而且它刻意做到「雪崩」:输入改一个比特,输出大约一半的比特就翻转。这就是它的全部意义。正因如此,一段 64 个字符的字符串才能代表 1 GB 数据,还能抓出一个字节的损坏。

人们容易忘的是这份能力的另一面:输出完全不携带「两个输入差多少」的信息。差一个字节的两个文件,和毫无关系的两个文件,算出来的摘要看上去一样地毫不相干。所以你看到「对不上」,读不出严重程度。「哈希完全不一样」并不等于「文件坏得很厉害」——它也可能只是末尾多了一个换行。这就是为什么盯着两串 hex 看再久也没用,为什么排查从来不是「差多少」,而永远是「三个环节里哪个出了岔」。

记住这点:指纹只给你一个比特的信息,然后就闭嘴了。还要分清方向。对不上,就证明了字节流不同——这个方向铁板钉钉。对上了是它们相同的有力证据,但在抗碰撞算法下,那是证据,不是数学证明;两个不同输入撞出同一个摘要(也就是碰撞),对 SHA-256 来说概率低到离谱,但逻辑上并非不可能。对本文来说这点不对称几乎无所谓——你排查的是「对不上」,而「对不上」在「字节是否不同」这件事上从不说谎。它只是不告诉你「为什么」。剩下的,靠你自己。

你实际会敲的命令

先给工具,再讲分类。macOS 或 Linux 上,你要么算一个哈希用肉眼比,要么让系统替你核对整个校验和文件:

sha256sum file.iso            # Linux:打印 SHA-256
shasum -a 256 file.iso        # macOS:同样的事,用 BSD 工具
sha256sum -c checksums.txt    # 逐个核对清单里每个文件的哈希

最后那种,在项目附带 checksums.txt(或 SHA256SUMS)时最值得用:它替你做字符串比对,逐个文件打印 OKFAILED,直接绕开了下一节要讲的那些复制粘贴错误。Windows PowerShell 上:

Get-FileHash .\file.iso -Algorithm SHA256

有了这些,下面看看摘要或比对还能在哪些地方翻车。

先查:两个摘要,是在同等条件下比较的吗?

这是最不起眼、却最常见的一类,所以在你去碰下载文件之前,先把它排掉。字节可能好好的,只是你没把两个摘要放在同一个标准下比。

复制时混进了杂质。你从网页上选中参考哈希,顺手带上了一个前导空格、一个末尾换行;或者那行折行了,你选中的其实被悄悄截断了一截。摘要是定长的,所以长度就是你的抓手:MD5 是 32 个 hex 字符,SHA-1 是 40,SHA-256 是 64,SHA-512 是 128。如果你粘出来的是 63 个字符、而它本该 64,那你拿到的不是另一个文件,而是一次不完整或错误的复制。下结论之前,先数一数字符数。

hex 大小写。一串大写、一串小写,很多人会慌。别慌:大小写不改变数值ABab 是同一个字节 0xAB。hex 只是摘要的一种写法,sha256sum 正好输出小写,而有些 Windows 工具和发布页面用大写。任何正确的比对,在 hex 上都是大小写不敏感的。真正的坑不在大小写本身——而在你手动「改大小写」、或者跨大小写复制的时候,有时会顺手漏掉或多出一个字符。数值跨大小写是一样的;脚本里一句 === 字面比对却不是,所以在代码里比之前,把两边都转成小写。

hex 还是 base64。同一个摘要,两套字母表。一个 SHA-256 是 32 个原始字节;写成 hex 是 64 个字符,写成 base64 则是一段 44 个字符、以 = 结尾的字符串。HTML 里的 Subresource Integrity 属性(integrity="sha256-…")用 base64,sha256sum 用 hex。如果一边长得像 47DEQpj8HBSa…、另一边像 e3b0c44298fc…,那你看到的可能是同一个摘要的两种编码,而不是两个不同的文件——但先确认两个参考值指的是同一个算法,再把一种编码转换成另一种去比。两串长得不一样的字符,本身并不能证明它们是同一个摘要;它们只是提醒你去核对。(base64 自己也有一堆「看起来不对」的理由,正好会在这里绊你一跤——那是另一个故事了。)

想把算法和 hex 大小写这两点的猜测彻底去掉,上传文件、或者粘贴你要计算的文本到 哈希生成器,选中参考值指明的那个算法,比对它输出的十六进制。你可以切换算法、也可以切换大写或小写 hex,这样就能精确对上参考值用的函数和大小写,而不是靠猜——注意这个工具只输出 hex、不输出 base64,所以参考值若是 base64,你得自己把一边转一下。

再查:你确定是同一个算法吗?

如果两个值都是同一种十六进制写法、长度却不同,这一条通常很好认——你用 SHA-256 算(64 个 hex 字符),页面上给的却是 MD5(32)或 SHA-512(128)。这里有个前提很关键:hex 和 base64 会把同一个摘要编码成不同长度,所以先排除编码不一致——一段 44 字符的 base64 SHA-256,可不是「某个更短的算法」。确认算法后,继续排查。

更阴的一种是长度相同、家族不同。SHA-256 和 SHA3-256 都产出 64 个 hex 字符,却是两个完全不同的函数。同样的问题也出在 SHA-224 和 SHA-384、各种截断版摘要,以及配置成 256 位输出的 BLAKE2 上——都很容易跟同长度的邻居搞混。页面只写「SHA256: …」的,一般是 SHA-2;但标签里带「SHA3」「Keccak」或「BLAKE」的,你就得选中那个确切的函数,而不是同长度的默认项。两个 64 字符的哈希对不上、文件却确实一模一样时,算法家族搞错是第一个该怀疑的——长度给了你一种「我在拿同类比同类」的假安全感。

文本输入:「内容」没变,字节却变了

到目前为止都假设你哈希的是一个文件——一串固定的字节,要么完整到手,要么没到。哈希文本则开了第二条战线,因为「同一段文本」可以是好几串不同的字节,而哈希看的是字节、不是含义。一段从字符串算出来的摘要,会在这些情况下发生分歧:

  • 字符编码不同(UTF-8 对 UTF-16,或者 UTF-8 对某个老式代码页),
  • 一边有字节序标记(BOM)、另一边没有,
  • 行尾一台机器是 LF、另一台是 CRLF
  • 一边末尾有换行、另一边没有——经典场景就是哈希一个光秃秃的字节,对上同一个字节后面跟一个换行:
printf x     | sha256sum   # 一个字节:'x'
printf 'x\n' | sha256sum   # 两个字节:'x' 加一个换行——摘要完全不同

echo "x" | sha256sum 掉的正是这个坑,因为 echo 会悄悄替你补上那个换行。)这些改动都改了输入的字节,却没改任何人类会称之为「内容」的东西,而对 SHA-256 这类密码学哈希来说,雪崩效应会把其中任何一个变成一个完全不同的摘要。文本编码和行尾的差异,牵扯得够多——跨两台机器时,「同一个输入、不同哈希」的意外也大多出在这里——另有一篇专门讲;这里你只要知道:如果你哈希的是文本而不是下载来的文件,编码和行尾这一层就是头号嫌疑。怎么修,取决于两边归谁管:两边都是你的,就在哈希之前把它们归一化到一致——同一种编码、同一种行尾,对末尾换行做一个明确的取舍。如果你是在对照一个已发布的参考值,你没法去「归一化」别人那串固定的值——你得复现出它当初计算时用的那批确切字节(对的编码、对的行尾约定),再去哈希。

到这一步再查数据:字节确实不一样

只有在你确认了「用同一个算法、对等地、在同一类输入上」比对之后,「字节真的不一样」这个结论才站得住。而当它真的不一样时,凶手通常是机械性的:

  • 传输被截断或中断。下载中途停了——连接断开、磁盘写满、代理关掉了流——你哈希的是半个文件。把磁盘上的文件大小,和可信来源声称的大小比一比:发布页面,或者 Content-Length 头(前提是这个值存在且可信)。文件明显小于可信大小,提示下载可能被截断——但只凭大小还不能断定,得结合重新下载和哈希结果来确认。
  • 你拿的文件跟校验和描述的不是同一个。校验和是按文件走的。app-linux-x64.tar.gz 旁边那个值,不会对上 app-linux-arm64.tar.gz2.4.0 的校验和,也不会对上你实际从某个镜像拉到的 2.4.1。确认一下参考值是为哪个文件名、哪个版本发布的。
  • 压缩包被重新打包过。两个 .zip.tar.gz 里的内容可以逐字节完全一致,哈希却不同,因为容器本身记录了时间戳、文件顺序和压缩设置,这些在不同构建之间会变。如果某个镜像重新压过这个发布包,它的校验和就对不上原始发布方的,哪怕解出来的每个文件都一样。要拿「产出这个压缩包的同一个来源」给的校验和去验。
  • 真的损坏了。在现代网络上比上面几种少见,但真会发生:磁盘不稳、内存出错、线缆坏了。重新下载一份、这次对上了,只能确认第一份不一样——不能确认为什么不一样。第一次也可能是拿错了文件或版本、镜像发的字节本就不同、压缩包被重打过,或者本地被改动过。损坏只是「重新下载这个结果与之相符」的一种假设,它自己证明不了什么。

留意一下:「数据不一样」是最后才值得深挖的一类,而不是第一类。对不上让人觉得像损坏,但真正的损坏,往往比「比对错」或「参考值错」都要少见。

最后:这个参考值,你到底信不信得过?

先从机制里抽身一下,因为有一种情况,是「对不上」在尽职,而「对上了」才该让你担心。校验和有意义的前提是:参考值来自一个你信任的来源,而且它到你手上走的那条通道,没法把文件和那个数字一起改掉。

如果下载文件和它公布的哈希放在同一个页面、走的还是明文 HTTP,那么能篡改文件的人,也能顺手把哈希改成与之相符。检查会通过,却什么都证明不了。这里,校验和的用途就成了关键,也是一个常见误解绊人的地方:下载旁边那个朴素的 MD5 或 SHA-256,是一道完整性检查,防的是意外损坏——传输出错、镜像有问题、文件被截断。它不是一道能挡住蓄意攻击者的真实性保证。哈希本身不含任何秘密;谁都能对任意文件重算一遍,所以把校验和公布在它所描述的文件旁边,挡不住一个控制了那个页面、把两者一起替换掉的攻击者。要防篡改,你需要一个攻击者伪造不出来的东西,具体用哪个取决于你的场景:

  • 面向公开软件分发——一个谁都能拉取的下载页面——答案是数字签名:GPG,或者 Ed25519、minisign。发布方用私钥给发布包(或它的校验和文件)签名,你用他们广为公布的公钥去验。这就是为什么 Linux 发行版发的是一个带签名的校验和文件,而不是一个光秃秃的哈希。
  • HMAC 只有在双方已经共享一把密钥时才对——比如一个 API 在校验 webhook 载荷,或者两个你自己掌控的服务之间。它对公开下载没用,因为你得把密钥交给每一个下载者,一交出去它就不再是秘密,攻击者也就拿到了。

这个区分也顺带决定了算法本身有多要紧。要抓的是来自可信来源的随机、非对抗性损坏,那么哪怕是 MD5,也能可靠地揪出绝大多数意外改动。可一旦涉及有人可能蓄意构造一个碰撞文件,MD5 和 SHA-1 就已经被攻破了,你要用 SHA-256 或更强的;而要做到真正的防篡改,用签名,不是一个光秃秃的哈希。这个取舍——那些老算法什么时候还够用、什么时候绝对不能用——有单独一篇专门来比。眼下的一句话版本是:如果参考值和文件依赖同一个不可信的服务器和传输通道,那么「对上了」是一种虚假的安心,正确的做法是从一个独立、可信的地方另取参考值,而不是一遍遍重算哈希。

校验和对不上的排查清单

两串对不上时,忍住重算任何东西的冲动。按顺序走这一遍——它从最省事、最常见的原因,走到值得深挖的那些,而且它们任意几个可以同时成立:

  1. 先确保比较条件一致。数一数字符数(MD5/SHA-1/SHA-256/SHA-512 分别是 32/40/64/128)。两边都转小写——大小写从不改变数值。确认你不是在拿 hex 跟 base64 比。大多数「对不上」会当场死在这一步。
  2. 同一个算法吗?两边都是同一种 hex 写法时,长度不同就指向不同算法——但先确认这不是 hex 跟 base64 之别。长度相同也可能是不同家族——SHA-256、SHA3-256、BLAKE2 都给 64 个 hex 字符。对上参考值点名的那个确切函数。
  3. 哈希的是文本、不是文件?那么「内容」看着一样、字节却能不同:编码、BOM、LFCRLF、末尾换行。两边都归你,就归一化到一致;对照的是已发布的参考值,就复现它当初计算所用的那批确切字节。
  4. 这时再怀疑数据。把文件大小和公布的比一比(截断下载)、确认你拉的是校验和点名的那个确切文件和版本,并记住重新打包过的压缩包哪怕内容一致、哈希也不同。
  5. 这个参考值你到底信不信?如果文件和它的哈希来自同一个不可信的地方,对上了也证明不了什么。一个光秃秃的哈希是意外探测器,不是防篡改——公开下载靠的是数字签名(GPG、Ed25519),HMAC 只在你俩已经共享一把密钥的场合才用。

这五步底下,是开头那个核心:对不上,就证明了字节流不同;对上了,是相等的证据、而不是绝对的证明——而且两者都不告诉你为什么。「对不上」不是答案,是问题本身。你要做的,只是判断三个环节——数据、比对、参考值——里是哪一个动了,有时候还不止一个。上面每一种情况,都是这件事的一个具体版本。