MD5・SHA-1・SHA-256の違い:どのハッシュを使うべきか
セキュリティに関わる用途にはSHA-256を使いましょう。MD5とSHA-1が許されるのは、同じハッシュ値を偽造しても誰も得をしない場面だけです。
MD5、SHA-1、SHA-256はどれも同じ仕事をします。任意の長さの入力を、ダイジェストと呼ばれる短い固定長の「指紋」に変換することです。同じ入力からは必ず同じダイジェストが得られ、1ビット変えるだけで結果全体が変わります。3つの違いはダイジェストの長さ、速度、そして何より、同じダイジェストを持つ2つの異なる入力を意図的に作れるかどうかです。MD5とSHA-1ではそれが公開の場で実証されました。SHA-256では実現していません。
3つのアルゴリズムの比較表
| MD5 | SHA-1 | SHA-256 | |
|---|---|---|---|
| ダイジェスト長 | 128ビット、16進数32文字 | 160ビット、16進数40文字 | 256ビット、16進数64文字 |
| 公開 | 1992年(RFC 1321) | 1995年(FIPS 180-1) | 2001年、SHA-2ファミリーの一部として |
| 衝突 | 2004年から実用的に可能 | 2017年から実用的に可能 | 既知の攻撃なし |
| 特定のダイジェストに対応する入力の発見 | 実用的な攻撃なし | 実用的な攻撃なし | 実用的な攻撃なし |
| 現在の用途 | セキュリティ目的でないチェックサム、キャッシュキー | レガシーとの互換性のみ | 新規開発の標準 |
次は5バイトの文字列helloの完全なダイジェストで、md5sum、sha1sum、sha256sumが出力したものです。ハッシュ生成ツールでは、テキストや50 MBまでのファイルについて、同じ値をSHA-384やSHA-512とあわせて並べて表示できます。
MD5 5d41402abc4b2a76b9719d911017c592
SHA-1 aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d
SHA-256 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
ビット数が増えると何が変わるのか
ダイジェストの長さは、関数をブラックボックスとして扱う汎用的な攻撃のコストを決めます。誕生日のパラドックスにより、衝突する2つの入力を何でもよいから見つけるには、nビットのダイジェストに対して約2^(n/2)回の試行が必要です。MD5なら2^64、SHA-1なら2^80、SHA-256なら2^128です。特定の1つのダイジェストに一致する入力(原像)を見つけるには、約2^n回かかります。
ただし、これらの数字が成り立つのは設計に近道がない場合だけです。MD5とSHA-1にはどちらも衝突を見つける近道があり、長さだけでは安全性を語れなくなっています。SHA-256には既知の近道がなく、2^128は現存するどんな計算能力をもはるかに超えています。
MD5とSHA-1はどのように破られたのか
衝突攻撃でハッシュを逆算できるわけではありません。攻撃者ができるのは、同じダイジェストを持つ2つの異なる入力を事前に用意し、一方を信頼させてから、もう一方にすり替えることです。危険なのは選択プレフィックス衝突と呼ばれる種類で、攻撃者は名前の異なる2枚の証明書のような意味のある先頭部分を2つ選び、ダイジェストが一致するように追加のバイト列を計算します。
- MD5、2004年:Xiaoyun Wangらが、実用的なMD5の衝突を発表しました。
- MD5、2008年:研究者チームが選択プレフィックス型のMD5衝突を使い、ブラウザーが信頼してしまう不正な認証局証明書を作成しました。ある商用CAがまだMD5で証明書に署名していたためです。
- MD5、2012年:マルウェアFlameがMD5の衝突を利用して、Microsoftのコード署名証明書を偽造しました。
- SHA-1、2017年:GoogleとCWI Amsterdamが、同じSHA-1ダイジェストを持つ2つの異なるPDFファイル「SHAttered」を発表しました。チームによると、計算量はCPUで約6,500年分、GPUで110年分でした。
- SHA-1、2020年:Gaëtan LeurentとThomas Peyrinが実用的な選択プレフィックス型のSHA-1衝突を発表し、それを使ってPGP鍵のなりすましを実証しました。
ブラウザーは2017年にSHA-1のTLS証明書の受け入れをやめ、NISTは2022年12月に、SHA-1を2030年末までに完全に廃止すべきだと発表しました。一方で、MD5にもSHA-1にも実用的な原像攻撃はありません。ダイジェストだけを渡されても、それを生み出す入力は誰にも見つけられません。だからこそ、どちらも偶発的な破損の検出にはまだ使えるのです。
速度:思い込みではなく実測で
MD5は速いほうだとよく言われます。そこで、同じ256 MBのランダムデータを、1台のデスクトップコンピューター上のNode 24(OpenSSL 3.5)でハッシュ化し、それぞれ3回ずつ計測しました。
| アルゴリズム | そのマシンでのスループット |
|---|---|
| SHA-1 | 約1,000 MB/s |
| MD5 | 約730 MB/s |
| SHA-512 | 約690 MB/s |
| SHA-256 | 約460 MB/s |
この順位はプロセッサーによって変わります。このCPUにはSHA命令セット拡張がありません。拡張を持つプロセッサーはSHA-1とSHA-256をハードウェアで高速化するため、SHA-256がMD5を上回ることもあります。ここでSHA-512がSHA-256より速かったのは、64ビットのワード単位で処理するからです。短い入力では差が消えます。短いメールアドレスを100万回ハッシュ化すると、MD5、SHA-1、SHA-256のいずれでも1.8秒から2.1秒かかりました。ハッシュ計算そのものより、呼び出しごとのオーバーヘッドのほうが大きいためです。速度がMD5を選ぶ本当の理由になることは、ほとんどありません。
それぞれが今も使える用途
- MD5:偶発的な破損の検出、自分のファイルの重複排除、キャッシュキーやETag、MD5を前提とするレガシーシステムとのやり取り。署名、証明書、攻撃者がファイルを差し出せる状況での完全性チェックには決して使わないでください。
- SHA-1:まだ変更できない既存システム。Gitは長年SHA-1でオブジェクトに名前を付けてきましたが、既知の衝突攻撃を検出する強化版の実装を使っており、SHA-256を使うリポジトリにも対応しています。TOTP認証アプリのコードの既定アルゴリズムであるHMAC-SHA1は、衝突攻撃の影響を受けません。
- SHA-256:ダウンロードのチェックサム、デジタル署名、証明書、HMAC、コンテンツアドレス方式、そして新しく作るものすべて。改ざんに耐える必要があるチェックサムにはSHA-256を使い、照合が必要なレガシーの値にはMD5を残しておきましょう。
ダウンロードしたファイルを確認したいときは、ファイルのチェックサム確認がファイルをローカルでハッシュ化し、貼り付けたチェックサムがどのアルゴリズムと一致するかを教えてくれます。
パスワードの保存にはどれも使わない
3つとも高速に動くよう設計されていますが、何十億ものパスワードを試す攻撃者が求めているのはまさにその速さです。ソルトは事前計算テーブルを防げても、推測そのものを遅くはしません。パスワードの保存には、意図的に遅く、できればメモリも大量に消費する(メモリハードな)関数が必要です。上と同じマシンでの結果は次のとおりです。
| 関数と設定 | 1パスワードあたりの時間 |
|---|---|
| SHA-256、Nodeからの1回の呼び出し | 約2.5マイクロ秒 |
| Argon2id、メモリ19 MiB、反復2回、レーン1 | 約33ミリ秒 |
| PBKDF2-HMAC-SHA256、反復600,000回 | 約270ミリ秒 |
| scrypt、N=2^17、r=8、p=1 | 約310ミリ秒 |
これらの設定は、OWASP Password Storage Cheat Sheetが示す最小値です。同シートは第一にArgon2idを推奨し、次にscrypt、レガシーシステムにはワークファクター10以上のbcrypt、FIPS-140への準拠が必要な場合はPBKDF2を挙げています。単純なハッシュを組み合わせて自作するのではなく、フレームワークが提供するパスワードハッシュAPIを使ってください。
HMAC:ハッシュに鍵が必要なとき
ある秘密を持つ人からメッセージが送られたことを証明したいとき、sha256(secret + message)を計算してはいけません。MD5、SHA-1、SHA-256はいずれも伸長攻撃(length extension攻撃)に弱く、あるメッセージのダイジェストを知っていれば、秘密を知らなくても、そのメッセージの末尾にデータを追加したものに対する有効なダイジェストを計算できてしまいます。HMAC(RFC 2104)はこの問題を回避します。標準的な選択はHMAC-SHA256で、次のコードはどれも同じ値を出力します。
// Node.js
crypto.createHmac('sha256', 'secret').update('hello').digest('hex')
# Python
hmac.new(b'secret', b'hello', hashlib.sha256).hexdigest()
// PHP
hash_hmac('sha256', 'hello', 'secret')
// 88aab3ede8d3adf94d26ab90d3bafd4a2083070c3bcce9c014ee04a443847c0b
HMACの比較には==ではなく、crypto.timingSafeEqual、hmac.compare_digest、hash_equalsのような定数時間で比較する関数を使ってください。
コマンドラインとコードでハッシュを求める
# Linux (coreutils)
md5sum file.iso
sha1sum file.iso
sha256sum file.iso
# macOS
md5 file.iso
shasum -a 1 file.iso
shasum -a 256 file.iso
# Windows PowerShell
Get-FileHash file.iso -Algorithm MD5
Get-FileHash file.iso -Algorithm SHA1
Get-FileHash file.iso # SHA256 is the default
# Any system with OpenSSL
openssl dgst -sha256 file.iso
# Hashing a string: printf adds no newline, echo does
printf '%s' hello | sha256sum # 2cf24dba5fb0a30e...
echo hello | sha256sum # 5891b5b522d5df08...
PowerShellのGet-FileHashは、アルゴリズムを指定しなければSHA256を使います。最後の2行に注意してください。printfは改行を付けませんがechoは付けるため、ダイジェストが変わります。ブラウザーのcrypto.subtle.digest()はSHA-1、SHA-256、SHA-384、SHA-512に対応していますが、MD5には対応しておらず、MD5を指定するとNotSupportedErrorが発生します。そのため、このサイトのものを含むブラウザー上のツールは、MD5を別のJavaScript実装で計算しています。