チェックサムが一致しないのはなぜ?ハッシュ不一致の調べ方
チェックサムが合わないと警報のように感じます。でもこの警報は一段階しかありません。二つのバイト列が同じではない、それだけです。理由までは教えてくれません。大事なのはハッシュを取り直すことではなく、不一致を三つの原因に絞り込むこと。データが本当に違うのか、二つのハッシュを対等に比べていないのか、あるいは信じている参照値そのものが間違っているのか。この記事はその三つを順に見ていきます。
リリースを一つダウンロードする。ISO、.tar.gz、あるいはインストーラー。プロジェクトのページには親切に SHA-256 が添えてある。手元で sha256sum を走らせ、二つの文字列を並べてみると、合わない。さて、どうするか。多くの人が飛ばしてしまう事実がある。不一致それ自体は、ほとんど何も教えてくれない。ハッシュは、たった 1 ビットの反転でも、まるごと別のファイルでも、同じくらい別物のダイジェストになるよう作られている。「惜しい」はない。比較は全か無かで、失敗が示すのはただ一つ、二つのハッシュに入っていったバイト列が同じではなかった、それだけだ。壊れたわけでも、改ざんされたわけでも、故障でもない。ただ「同じではない」。役に立つのは全部、「なぜ」違うのかを突き止める側にある。
(言葉について一つ。この記事の「チェックサム」は、ダウンロード後に突き合わせるあの値、という日常的な意味で使う。厳密には、SHA-256 は暗号学的ハッシュ(ダイジェスト)で、CRC32 のようなものは非暗号的なチェックサムだ。MD5 も暗号学的ハッシュだが、衝突耐性はすでに破られている。敵対的でないファイル照合であれば偶発的な変化の大半は見つけられるが、セキュリティ用の認証には向かない。どれを手にしていても、以下の調べ方は同じだ。)
だから同じハッシュを五回取り直しても変わらない。変わらないのだ。不一致は、三方向への分岐の出発点だと考えてほしい。チェックサムの比較には可動部分がちょうど三つあり、不一致はそのうち一つ以上がずれていることを意味する。複数が同時にずれていることもある。だから記事末尾のチェックリストは、最初の一つで止めず、三つとも辿る。
- データが本当に違う。あなたがハッシュしたバイトが、参照値を計算したときのバイトと別物だ。途中で切れた・中断したダウンロード、取り違えたファイル、詰め直されたアーカイブ、あるいは転送中の本物の破損。
- 二つのハッシュを対等に比べていない。バイトは同じかもしれないのに、二つのダイジェストを同じ土俵に載せていない。アルゴリズムが違う、hex と base64 の表現がずれている、あるいは見比べている文字列にコピー時のノイズが混ざっている。
- 参照値そのものが間違っているか、信頼できない。突き合わせる相手の値が古い、別のビルドのもの、あるいは信じる理由のない場所から来ている。この場合、「一致」こそが本当の問題だったことになる。
この記事は、実際に確かめるべき順番でこの三つを辿る。いちばん手軽で、いちばんよくある原因――文字列を間違って比べていた――こそ、多くの人が最後に疑うものだからだ。
核心の一点:ハッシュは指紋、指紋に「だいたい」はない
以下のすべては、一つの性質の上に立っている。暗号学的ハッシュは、任意の入力――1 バイトでも 4 GB のディスクイメージでも――を固定長の指紋に写す。そして意図的に「雪崩」的だ。入力を 1 ビット変えると、出力の約半分のビットが反転する。それがすべての狙いだ。だからこそ 64 文字の文字列が 1 ギガのデータの代わりになり、なお 1 バイトの破損を捕まえられる。
見落とされがちなのは、その力の裏側だ。出力は「二つの入力がどれだけ違うか」の情報を一切持たない。1 バイトだけ違う二つのファイルと、まるで無関係な二つのファイルが、同じくらい互いに無関係に見えるダイジェストを生む。だから不一致を見ても、深刻さは読み取れない。「ハッシュがまったく違う」は「ファイルがひどく壊れている」を意味しない。末尾に改行が一つ増えただけでも、同じ見え方をする。二つの hex 文字列を長く睨んでも無駄なのはこのためで、調べ方が「どれだけ違うか」ではなく、いつも「三つの可動部分のどれがずれたか」になるのもこのためだ。
覚えておいてほしい。指紋は 1 ビットぶんの情報をくれて、あとは黙る。そして向きを取り違えないこと。不一致は、バイト列が違うことを証明する。この結論は確実だ。一致は、同じであることの強い証拠だが、衝突耐性のあるアルゴリズムでは、それは証拠であって数学的な証明ではない。異なる二つの入力が同じダイジェストになること(衝突)は、SHA-256 では天文学的に起こりにくいだけで、論理的に不可能ではない。この記事にとって、その非対称性はほとんど関係ない。調べているのは不一致であり、不一致は「バイトが違うかどうか」については決して嘘をつかない。ただ「なぜ」は教えてくれない。あとは自分次第だ。
実際に打つコマンド
分類の前に、道具から。macOS か Linux では、ハッシュを一つ計算して目視で比べるか、チェックサムのファイルをまるごとシステムに照合させるかのどちらかだ。
sha256sum file.iso # Linux:SHA-256 を出力
shasum -a 256 file.iso # macOS:同じこと、BSD 系のツール
sha256sum -c checksums.txt # 一覧の各ファイルを記録済みのハッシュと照合
最後の形は、プロジェクトが checksums.txt(や SHA256SUMS)を同梱しているときに真っ先に使いたい。文字列の突き合わせを代わりにやってくれて、ファイルごとに OK か FAILED を出すので、次の節で扱うコピペのミスをまるごと回避できる。Windows の PowerShell なら:
Get-FileHash .\file.iso -Algorithm SHA256
これらを手に、ダイジェストや比較がなお失敗しうる理由を見ていく。
まず確かめる:二つのハッシュを、対等に比べているか
これはいちばん地味で、いちばんよくある原因の一つだ。だからダウンロードに手を付ける前に、まず除外しよう。バイトは無傷かもしれず、ただ二つのダイジェストを同じ条件で比べていないだけかもしれない。
コピー時に混ざったノイズ。ウェブページから参照ハッシュを選択したとき、先頭のスペースや末尾の改行を一緒に拾ってしまった。あるいは行が折り返していて、選んだ範囲が知らないうちに途中で切れていた。ダイジェストは固定長なので、長さが味方になる。MD5 は 32 の hex 文字、SHA-1 は 40、SHA-256 は 64、SHA-512 は 128 だ。貼り付けた文字列が 64 のはずなのに 63 なら、別のファイルではなく、コピーが不完全だっただけだ。結論を出す前に、まず文字数を数えてほしい。
hex の大文字・小文字。片方が大文字、片方が小文字だと慌てる人がいる。慌てなくていい。大文字・小文字は値を変えない。AB と ab は同じ 1 バイト 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 には 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、 - 片方に末尾改行があり、もう片方にない――素の 1 バイトと、同じ 1 バイトに改行を足したものをハッシュする、という典型例だ。
printf x | sha256sum # 1 バイト:'x'
printf 'x\n' | sha256sum # 2 バイト:'x' と改行――まったく別のダイジェスト
(echo "x" | sha256sum がはまるのはまさにこれで、echo があの改行を黙って付け足すからだ。)どれも、人が「内容」と呼ぶものは変えずに入力バイトを変え、SHA-256 のような暗号学的ハッシュでは、雪崩効果がそのどれか一つを別のダイジェストに変える。文字コードと改行の差は、絡む要素が多く――二台のマシン間で「同じ入力なのに違うハッシュ」という驚きの多くもここから来る――別記事で詳しく扱っている。ここでは、ダウンロードしたファイルではなくテキストをハッシュしているなら、文字コードや改行の違いがまず疑うべき点だ、と分かれば十分だ。直し方は、二つの側を誰が握っているかで変わる。どちらも自分のものなら、ハッシュ前に揃える(同じ文字コード、同じ改行、末尾改行は意識して決める)。公開された参照値に照らしているなら、他人の固定値を「揃える」ことはできない。その値が計算されたときの、まさにそのバイト(正しい文字コード、正しい改行の流儀)を再現し、それをハッシュする。
ここで初めてデータを疑う:バイトが本当に違う
「同じアルゴリズムで、対等に、同じ種類の入力に対して」比べていると確認できて初めて、「バイトが本当に違う」という結論が立つ。そして本当に違うとき、犯人はたいてい機械的だ。
- 転送が途中で切れた・中断した。ダウンロードが早く止まった――接続断、ディスク満杯、ストリームを閉じたプロキシ――ので、半分のファイルをハッシュした。ディスク上のファイルサイズを、信頼できるソース(リリースページ、または値が存在して信頼できる場合は
Content-Lengthヘッダ)が示すサイズと比べてほしい。明らかに小さいファイルは途中で切れた可能性を示すが、サイズだけでは断定できない。取り直してハッシュを取り、その結果で確かめる。 - チェックサムが説明するのとは別の成果物を掴んだ。チェックサムはファイルごとだ。
app-linux-x64.tar.gzの隣の値はapp-linux-arm64.tar.gzには合わないし、2.4.0のチェックサムは、ミラーから実際に引いた2.4.1には合わない。参照値がどのファイル名・どのバージョン向けに公開されたかを確かめる。 - アーカイブが詰め直された。二つの
.zipや.tar.gzは、中身が 1 バイト単位で同一でも、ハッシュが違いうる。容器そのものがタイムスタンプ、ファイルの順序、圧縮設定を記録し、それらはビルド間で変わるからだ。あるミラーがそのリリースを圧縮し直していれば、展開後の各ファイルが同じでも、そのチェックサムは元の公開者のものに合わない。そのアーカイブを作った当のソースが出したチェックサムで検証する。 - 本物の破損。現代のネットワークでは上のどれより稀だが、実在する。不安定なディスク、壊れた RAM、断線したケーブル。良いソースから取り直して一致しても、確認できるのは最初のコピーが違ったことだけで、なぜかは分からない。最初にファイルやバージョンを取り違えた、ミラーが別のバイトを配っていた、アーカイブが詰め直されていた、ローカルで編集された――どれでもありうる。破損は、取り直しの結果と矛盾しない一つの仮説にすぎず、それ自体では何も証明しない。
気づいてほしいのは、「データが違う」は最後に深掘りする価値のある分類で、最初ではないということだ。不一致は破損のように感じるが、本物の破損は、比較の誤りや参照値の誤りより少ないことが多い。
最後に:その参照値、そもそも信頼できるか
いったん仕組みから離れよう。不一致が仕事をしていて、一致こそが心配な結果だった、という場合があるからだ。チェックサムに意味があるのは、参照値が信頼できるソースから来ていて、しかも届くまでの経路が、ファイルとその数字を一緒に書き換えられなかったときだけだ。
ダウンロードするファイルと、公開されたそのハッシュが同じページにあり、しかも平文の HTTP で配られているなら、ファイルを改ざんできる者は、ハッシュもそれに合うよう書き換えられる。検査は通り、何も証明しない。ここでチェックサムの用途が効いてくる。よくある誤解も整理しておこう。ダウンロードの隣の素朴な MD5 や SHA-256 は、偶発的な損傷に対する整合性の検査だ――転送の失敗、不良ミラー、切れたファイル。意図的な攻撃者に対する真正性の保証ではない。ハッシュ自体は秘密を含まず、誰でも任意のファイルについて再計算できる。だからチェックサムを、それが説明するファイルの隣に公開しても、そのページを支配し両方を同時に差し替える攻撃者は止められない。改ざんを防ぐには、攻撃者が偽造できない何かが要り、どれを使うかは状況で決まる。
- 公開ソフトウェア配布――誰でも取得できるダウンロードページ――なら、答えはデジタル署名だ。GPG や Ed25519、minisign などで、公開者が秘密鍵でリリース(またはそのチェックサムファイル)に署名し、あなたは広く公開された公開鍵で検証する。Linux ディストリビューションが素のハッシュではなく、署名付きのチェックサムファイルを配るのはこのためだ。
- HMAC が正しい道具なのは、両者がすでに秘密鍵を共有しているときだけだ――webhook のペイロードを検証する API、あるいは自分が握る二つのサービスの間。公開ダウンロードには使えない。鍵をダウンロードする全員に渡すことになり、渡した時点でそれは秘密でなくなり、攻撃者の手にも渡るからだ。
この区別は、アルゴリズムそのものがどれだけ重要かも決める。信頼できるソースからの、ランダムで敵対的でない損傷を捕まえるだけなら、MD5 でさえ、偶発的な変更の大多数を確実に炙り出す。しかし、誰かが意図的に衝突するファイルを作りうる場面では、MD5 と SHA-1 は破られており、SHA-256 かそれ以上が要る。そして本当の改ざん防止には、素のハッシュではなく署名を使う。この折り合い――古いアルゴリズムがまだ十分な場面と、絶対にだめな場面――は、それだけで別記事一本ぶんの比較になる。今の時点での一文版はこうだ。参照値とファイルが運命を共にしているなら――同じ信頼できないサーバー、同じ経路――「一致」は偽りの安心であり、直し方はハッシュを取り直し続けることではなく、独立した信頼できる場所から参照値を取り直すことだ。
チェックサムが合わないときのチェックリスト
二つの文字列が並ばないとき、何かを取り直したい衝動をこらえてほしい。この順で辿る。いちばん手軽でよくある原因から、もっと深く見るべきものへと進み、そのいくつもが同時に成り立ちうる。
- まず対等に比べる。文字数を数える(MD5/SHA-1/SHA-256/SHA-512 でそれぞれ 32/40/64/128)。両側を小文字に――大文字・小文字は決して値を変えない。hex と base64 を比べていないか確かめる。「不一致」の多くはここで決着する。
- 同じアルゴリズムか。両側が同じ hex 表記なら、長さの違いは別のアルゴリズムを指す――ただし、まず hex と base64 の違いでないことを確認する。同じ長さでも系統違いはありうる――SHA-256、SHA3-256、BLAKE2 はどれも 64 の hex 文字。参照値が名指す、その正確な関数に合わせる。
- ハッシュしているのはファイルでなくテキストか。なら「内容」は同じ見た目でもバイトは違いうる。文字コード、BOM、
LF対CRLF、末尾改行。両側が自分のものなら揃える。公開された参照値に照らすなら、それが計算されたときの、まさにそのバイトを再現する。 - ここで初めてデータを疑う。ファイルサイズを公開値と比べ(切れたダウンロード)、チェックサムが名指すそのファイルとバージョンを引いたか確かめ、詰め直したアーカイブは中身が同じでもハッシュが違うことを忘れない。
- その参照値をそもそも信頼するか。ファイルとそのハッシュが同じ信頼できない場所から来ているなら、一致しても何も証明しない。素のハッシュは事故検知器であって、改ざん防止ではない――公開ダウンロードならデジタル署名(GPG、Ed25519)、HMAC はすでに秘密鍵を共有している場面だけ。
この五つの底にあるのは、冒頭のあの一点だ。不一致はバイト列が違うことを証明し、一致は等しさの証拠であって絶対の証明ではない――そしてどちらも「なぜ」は教えてくれない。不一致は答えではなく、問いそのものだ。あなたの仕事は、三つの部分――データ、比較、参照値――のどれが動いたかを見極めることだけ。ときに動いたのは一つではない。上のどのケースも、この一つのことの具体例だ。