ソフトウェアの配布サイトの隅に、SHA256 という添え書きとともに64文字の英数字が書いてあることがあります。あれは何のためにあり、どう使うのか。ダウンロードしたインストーラが配布元の置いたものと同じかどうかを、あの64文字で確かめられるのか。今日はそこが分かるところまで進みます。
OpenSSL で手を動かして学ぶ暗号の基礎、第1回はハッシュ関数です。環境の準備がまだの方は第0回からどうぞ。
コマンドを打つ前に、ハッシュ関数の性質を3コマにしておきます。
5文字でも、本1冊でも、出てくるのは64文字。
似た入力でも、出力は似ない。だから、1文字の改ざんでもばれる。
64文字から元の文は作れない。鍵もないし、復号もない。
openssl dgst -sha256 でハッシュを取る
echo -n "hello" | openssl dgst -sha256
# SHA2-256(stdin)= 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
dgst はダイジェスト(digest、要約)の略で、-sha256 はアルゴリズムの指定です。入力 hello を SHA-256 という関数に通すと、64文字の16進数が返ってきます。16進数1文字は4ビットなので、64文字で256ビット。SHA-256 の 256 はこの出力の長さです。echo -n の -n は末尾に改行を付けないという指定で、これを忘れると別の値になります(後述します)。
ここで大事なのは、この値が乱数ではないことです。同じ入力からは、いつ、誰が、どのマシンで実行しても、同じ値が出ます。つまりいま、あなたの画面にも上とまったく同じ 2cf24d… が並んでいるはずです。手元で確かめてみてください。
1文字変えると、すべて変わる
hello の最後の1文字だけ変えて、hellp にします。
echo -n "hellp" | openssl dgst -sha256
# SHA2-256(stdin)= fdd7585e08c4e2afd71dcabdb4636c89d557a3f42db9e2040c8bbd1708aa4ce7
2cf24d… だった値が fdd758… になりました。前半だけ似ているとか、1文字ぶんだけずれるとかではなく、64文字の全部が別物になります。入力のどこをどれだけ変えても、出力は予測のつかない別の値へ飛ぶ。この性質は雪崩効果(avalanche effect)と呼ばれます。
逆向きに言うと、こうなります。ハッシュ値が一致したなら、入力は同じだったと信じてよい。1文字の違いすら見逃さない関数が同じ値を返したのだから、と。この「値の一致=中身の一致」が、ハッシュ関数の使い道のほぼすべてを支えます。
入力がどれだけ大きくても、出力は64文字
hello は5文字でした。今度は大きなファイルを入れてみます。Mac には約250万バイトの英単語辞書が入っているので、これを使います(Linux の方は手元の適当なファイルでどうぞ)。
wc -c /usr/share/dict/words
# 2493885 /usr/share/dict/words
openssl dgst -sha256 /usr/share/dict/words
# SHA2-256(/usr/share/dict/words)= be41ad97963bf8dabedd5871d5d691596175269d540956b0f9965a885c2bbab9
5文字でも250万バイトでも、出てくるのは同じ64文字です。どんな大きさの入力も固定長に要約されるからこそ、後で出てくる「ハッシュ値だけ見比べる」という使い方が成り立ちます。
なお、250万バイトを64文字に押し込んでいる以上、元に戻す道はありません。ハッシュ値から入力は復元できませんし、そもそも復元という操作が定義されていません。ハッシュは暗号化ではなく、鍵もなければ復号もない。ここは後の回のためにも覚えておいてください。
改ざんは1文字でばれる
ファイルの中身を1文字だけ書き換えると何が起きるか、請求書で試します。
printf '請求金額は 100,000 円です。\n' > invoice.txt
openssl dgst -sha256 invoice.txt
# SHA2-256(invoice.txt)= b8819d4937c93e982de1500650e54d42bc89ce8d33322a6a3cb186c4e149c7a3
printf '請求金額は 900,000 円です。\n' > invoice.txt
openssl dgst -sha256 invoice.txt
# SHA2-256(invoice.txt)= 6bd2fd23483594c1e0367f2c8346f75e9d7453b8284d12e0363aae582b62aee4
変わったのは 1 が 9 になった1文字だけですが、ハッシュ値は全部変わりました。目視でファイルを見比べても、10万円と90万円の違いは案外すり抜けます。ハッシュ値の照合はすり抜けません。
実践:ダウンロードしたファイルを検証する
冒頭の64文字に戻ります。配布元がファイルと一緒に SHA-256 のハッシュ値を公表しているのは、受け取った側が手元で同じ計算をして、値を見比べるためです。一致すれば、ダウンロードしたファイルは配布元の置いたものと同じ。途中で壊れてもいないし、差し替えられてもいません。
配布元の値と手元の値が同じなら、同じファイル。
本物で試します。題材はこの連載の主役、OpenSSL 自身の配布ファイルです。OpenSSL のリリースはソースコードの tar.gz と一緒に、その SHA-256 ハッシュを書いた .sha256 ファイルを配っています。両方をダウンロードします(tar.gz は約55MB あります。バージョンは執筆時点の最新の 4.0.1 です。リリースページで最新版に読み替えてください)。
curl -sLO https://github.com/openssl/openssl/releases/download/openssl-4.0.1/openssl-4.0.1.tar.gz
curl -sLO https://github.com/openssl/openssl/releases/download/openssl-4.0.1/openssl-4.0.1.tar.gz.sha256
配布元が公表している値を見ます。
cat openssl-4.0.1.tar.gz.sha256
# 2db3f3a0d6ea4b59e1f094ace2c8cd536dffb87cdc39084c5afa1e6f7f37dd09 *openssl-4.0.1.tar.gz
手元で同じ計算をします。-r は「ハッシュ値が先」の形式で出力するオプションで、Linux の sha256sum コマンドや、いま見た .sha256 ファイルと同じ並びになります(-r なしの SHA2-256(...)= という表記は、ツールやバージョンによって変わります。見比べるのは16進数の部分だけで構いません)。
openssl dgst -sha256 -r openssl-4.0.1.tar.gz
# 2db3f3a0d6ea4b59e1f094ace2c8cd536dffb87cdc39084c5afa1e6f7f37dd09 *openssl-4.0.1.tar.gz
2db3f3… が一致しました。約5,500万バイトの tar.gz が、OpenSSL プロジェクトの置いたものと1ビットも違わないことを、いま自分の手で確かめたことになります。
目で見比べる代わりに、照合まで自動化もできます。macOS に最初から入っている shasum に、公表値のファイルを渡すだけです。
shasum -a 256 -c openssl-4.0.1.tar.gz.sha256
# openssl-4.0.1.tar.gz: OK
64文字を目で見比べる作業が、OK の2文字になりました。
ところで、リリースページには .sha256 の隣に .asc というファイルも並んでいます。正体はデジタル署名です。「公表されているハッシュ値そのものが差し替えられていたら?」という、今日の手順に残った疑問に答える仕組みで、詳しくは第4回で扱います。
パスワードが平文で保存されない理由
ハッシュの一方向性は、ダウンロードの検証以外のところでも毎日働いています。まともなサービスは、あなたのパスワードを平文、つまり入力したそのままの文字列では保存しません。保存されるのはパスワードのハッシュ値です。ログインのたびに、入力されたパスワードをハッシュして、保存してある値と照合する。値が一致すれば本人が来たと分かる仕組みで、サービス側はパスワードの原文を一度も持たずに済みます。データベースが流出しても、そこに並んでいるのは戻せないハッシュ値だけです。
サービスが持っているのはハッシュだけ。流出しても、元のパスワードには戻せない。
ただし、実際のパスワード保存に SHA-256 をそのまま使うことはありません。総当たりを遅くする仕掛けを足した専用の方式(bcrypt や Argon2 など)を使います。なぜ素のハッシュでは足りないのかは、第2回で登場するソルトの話につながっていきます。
注意点
echo の改行が混ざる
この回でいちばん実害のある落とし穴です。-n を付け忘れると、echo は末尾に改行を1文字付けます。
echo "hello" | openssl dgst -sha256
# SHA2-256(stdin)= 5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03
入力したつもりの hello は5文字、実際に流れたのは改行込みの6文字。ハッシュ値は当然すべて変わります。「公表されている値とどうしても一致しない」というとき、原因はたいていこれです。文字列のハッシュを取るときは echo -n、あるいは printf を使ってください。
MD5 と SHA-1 は改ざん検出に使わない
openssl dgst -md5 や -sha1 も今のところ動きますが、この2つは同じハッシュ値になる別の入力(衝突)を意図的に作る方法が公開されています。同じ値なら同じ中身、という今日の議論の土台が、攻撃者相手には成り立ちません。
これは昔話ではありません。たとえば WordPress の公式配布ファイルに付くチェックサムは、執筆時点でも .md5 と .sha1 だけで、SHA-256 は用意されていません。こうした値は転送中の破損チェック以上の意味はないと思って、改ざん検出の用途には SHA-256 以降を使ってください。
ハッシュ値は秘密ではない
ハッシュ値は堂々と公開してよい値です。値から入力へ逆走できないので、公開しても中身は漏れません。むしろ検証のためには公開されていなければ困ります。隠すべきものと公開してよいものの区別は、この連載を通じて何度も出てきます。
ハッシュ関数が約束するのは「同じ入力なら必ず同じ値、違う入力ならまるで違う値、そして値から入力へは戻れない」。これだけです。このそっけない約束が、ダウンロードの検証からパスワードの保存までを支えています。
次回は共通鍵暗号(AES)です。ハッシュと違って、今度は戻せる変換。ファイルを暗号化して、それから、鍵の受け渡しという厄介な問題に正面からぶつかります。
