ファイルのハッシュとチェックサム
ハッシュの長さ、衝突、照合値の信頼性を具体的な計算から説明します。
説明
ハッシュ関数は任意長のデータから固定長の値を計算します。ダウンロードしたファイルを照合するときは、比較するハッシュ値を信頼できる配布元から取得します。
短いバイト列のSHA-256と単純な加算チェックサムを比較します。並び替え、一文字の変更、照合値ごとのすり替えを試し、検出できる変化と信頼の置き場を調べます。
ハッシュの長さ、衝突、照合値の信頼性を具体的な計算から説明します。
13バイトのハッシュを計算する
SHA-256の出力は32バイトです。16進数では1バイトを2桁で表します。
import hashlib
original = b"abc"
digest = hashlib.sha256(original).digest()SHA-256の32バイトを取得する
print(original.hex(),len(original))
print(digest.hex()16進数で表示する,len(digest),len(digest.hex()))入力3バイトに対して出力は32バイト、表示は64桁です。64バイトのハッシュという意味ではありません。
2入力が長くても出力長は固定
同じ文字列を1000回つなげ、入力を3000バイトにします。
long = original * 1000入力を3000バイトにする
print(len(long),len(hashlib.sha256(long).digest()長い入力を要約する))入力長が変わっても出力は32バイトです。固定長に押し込むため、異なる入力で同じ値となる組は存在します。
3分割して読み込んでも同じ結果になる
ファイルを少しずつ読むときは、同じ状態へ順番にupdateします。各部分のハッシュを連結する方法ではありません。
h = hashlib.sha256()
h.update(b"a")先頭部分を追加する
h.update(b"bc")続きのバイトを追加する
print(h.hexdigest())
print(h.digest() == digest一括計算と照合する)連続したabcを処理した結果と一致します。分割位置が違っても、処理するバイト順が同じなら結果は変わりません。
4加算チェックサムで並び替えを調べる
バイト値を足して256で割った余りを、単純なチェックサムとして使います。暗号用の方式ではありません。
reordered = b"cba"
print(sum(original)%256加算チェックサムを求める,sum(reordered)%256)
print(hashlib.sha256(original).hexdigest())
print(hashlib.sha256(reordered)並び替えた入力をハッシュする.hexdigest())加算値は順序に依存しないため同じです。一方、このSHA-256の値は異なります。暗号学的な耐性の証明は一例の比較ではできません。
5一文字の変更で変わるビットを数える
abcの最後をdへ変え、32バイトのハッシュ同士をXORします。
changed = hashlib.sha256(b"abd").digest()一文字変えた入力を処理する
print(changed.hex())
print(sum((a^b).bit_count()異なるビットを数える for a,b in zip(digest,changed)))表示した数は、この入力対で変わったビット数です。常に同じ数だけ変化するという規則でも、衝突を発見できないことの証明でもありません。
6信頼できる照合値を保持する
元のdigestを別途確認済みの値とし、届いたファイルをabdに差し替えます。
trusted = digest比較する正しい値を保持する
received = b"abd"
print(hashlib.sha256(received).digest() == trusted受領したファイルを照合する)確認済みの値と一致しないので、このファイルを元のものとして受け入れません。
7ファイルと照合値を一緒に差し替える
同じ信頼できない場所からファイルとハッシュを取得した場合を考えます。
untrusted_digest =変更後のファイルで照合値を作り直す hashlib.sha256(received).digest()
print(hashlib.sha256(received).digest() == untrusted_digest)
print(untrusted_digest == trusted確認済みの値と比較する)攻撃者側の照合値とは一致しても、確認済みの値とは一致しません。ハッシュ値そのものの取得経路も保護する必要があります。
まとめ
SHA-256は入力長にかかわらず32バイトを返します。
チェックサムと暗号用ハッシュは想定する変更・攻撃が異なります。
照合値まで置き換えられると、単なるハッシュ比較では送信元を確認できません。
操作履歴と解説を振り返れます。
Python 3.15 / cryptography 50.0.2 / 公開試料と固定時刻
import hashlib
original = b"abc"
digest = hashlib.sha256(original).digest()
print(original.hex(),len(original))
print(digest.hex(),len(digest),len(digest.hex()))616263 3 ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad 32 64
long = original * 1000
print(len(long),len(hashlib.sha256(long).digest()))3000 32
h = hashlib.sha256()
h.update(b"a")
h.update(b"bc")
print(h.hexdigest())
print(h.digest() == digest)ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad True
reordered = b"cba"
print(sum(original)%256,sum(reordered)%256)
print(hashlib.sha256(original).hexdigest())
print(hashlib.sha256(reordered).hexdigest())38 38 ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad 6d970874d0db767a7058798973f22cf6589601edab57996312f2ef7b56e5584d
changed = hashlib.sha256(b"abd").digest()
print(changed.hex())
print(sum((a^b).bit_count() for a,b in zip(digest,changed)))a52d159f262b2c6ddb724a61840befc36eb30c88877a4030b65cbe86298449c9 122
trusted = digest
received = b"abd"
print(hashlib.sha256(received).digest() == trusted)False
untrusted_digest = hashlib.sha256(received).digest()
print(hashlib.sha256(received).digest() == untrusted_digest)
print(untrusted_digest == trusted)True False
演習完了