概要
ネットからソフトウェアをダウンロードするとき、配布ページに次のような長い英数字が書かれているのを見たことはないでしょうか。
例: e3b0c44298fc1c149afbf4c8996fb924...
初めて見ると、ただの不思議な文字列に見えるかもしれません。
しかし、この文字列は「ダウンロードしたファイルが途中で壊れていないか」「誰かにこっそり書き換えられていないか」を確認するために使われることがあります。
たとえば、公式サイトで公開されている値と、自分の手元で計算した値が一致すれば、少なくともファイルの内容が同じである可能性が高いと判断できます。
逆に、1文字だけでもファイルの中身が変わると、この値は大きく変わります。
このような「データの指紋」のような値を作る技術が、ハッシュ関数です。
ここで大切なのは、ハッシュ関数はデータを隠すための技術ではないという点です。
情報セキュリティを学び始めると、「暗号化」「ハッシュ化」「署名」「認証」など似た言葉がたくさん出てきます。
その中でも特に混同しやすいのが、ハッシュ化と暗号化です。
暗号化は、正しい鍵を持つ相手だけが元の内容を読めるようにする技術です。
一方、ハッシュ化は、基本的に元のデータへ戻すことを目的にしていません。
ハッシュ関数は、データそのものを隠すというより、データが変わっていないかを確認するための目印を作る技術です。
身近なところでは、ハッシュ関数は次のような場面に関係しています。
| 場面 | ハッシュ関数が関係すること |
|---|---|
| ファイルダウンロード | ファイルが壊れていないか、改ざんされていないかを確認する材料になる |
| スマホ決済・Web通信 | メッセージが途中で書き換えられていないかを確認する技術の土台になる |
| パスワード保存 | パスワードをそのまま保存しない考え方を理解する入口になる |
| Git | ファイルや履歴を識別するための値として使われる |
| ブロックチェーン | 取引やブロックをつなげ、改ざんに気づきやすくする仕組みに使われる |
ただし、ハッシュ関数だけで何でも安全になるわけではありません。
たとえば、普通のハッシュ関数だけでは「誰がその値を作ったのか」までは分かりません。
また、パスワード保存では、SHA-256のような通常の高速なハッシュ関数をそのまま使えば十分、とは考えない方がよいです。
本記事では、ハッシュ関数をいきなり数式やアルゴリズムから説明するのではなく、まずは身近な例から整理します。
そのうえで、「暗号化との違い」「どのような場面で使われるのか」「使うときに注意すること」を順番に見ていきます。
この記事を読み終えるころには、ハッシュ関数を単なる長い英数字ではなく、情報セキュリティを支える重要な道具としてイメージできるようになることを目指します。
この記事の立ち位置
この記事は、暗号技術の全体像を整理した記事 「スマホ決済の裏側から見る、情報セキュリティを支える暗号技術の全体像」 の派生記事として作成します。
派生元の記事では、暗号技術を次の3つの視点から整理しました。
- 何を守るのか
- どの道具を使うのか
- 実サービスではどう組み合わせるのか
本記事では、その中でも ハッシュ関数 に焦点を当てます。
ハッシュ関数は、暗号技術の中では「データの指紋を作る道具」として位置づけられます。
特に、データが途中で書き換えられていないことを確認する 完全性 の理解につながる技術です。
この図で示したように、ハッシュ関数は単独で使われるだけではありません。
MAC、デジタル署名、パスワード保存、ブロックチェーン、Gitなど、さまざまな仕組みの土台として登場します。
そのため本記事では、「ハッシュ関数とは何か」を用語として覚えるだけでなく、他の技術との違いやつながりまで整理します。
この記事で分かること
この記事では、次の内容を整理します。
- 身近な例から見たハッシュ関数の役割
- ハッシュ化と暗号化の違い
- ハッシュ関数に期待される基本的な性質
- SHA-256、SHA-3、MD5、SHA-1の位置づけ
- ファイル改ざん検知、Git、ブロックチェーンでの使われ方
- HMAC、デジタル署名、パスワード保存との関係
- ハッシュ関数を使うときに注意すべきこと
対象読者
この記事は、次のような人を想定しています。
- 情報セキュリティを学び始めた人
- 「暗号化」と「ハッシュ化」の違いがまだ曖昧な人
- SHA-256やハッシュ値という言葉を見たことはあるが、使いどころが分からない人
- パスワード保存、Git、ブロックチェーンなどで出てくるハッシュの意味を整理したい人
- 詳細な数式よりも、まずは役割とイメージを理解したい人
本記事で扱わないこと
本記事は、ハッシュ関数の入口として全体像をつかむことを目的にしています。
そのため、以下は深く扱いません。
- SHA-256やSHA-3の内部構造
- 圧縮関数、Merkle-Damgård構造、スポンジ構造の詳細
- 衝突攻撃の具体的な数学的手法
- 暗号ライブラリを使った実サービス向けの実装手順
- パスワード保存機能の本番実装
これらは重要なテーマですが、最初から詰め込みすぎると全体像が見えにくくなります。
本記事では、まず「ハッシュ関数は何のために使うのか」をつかむことを優先します。
1. 身近な例から見るハッシュ関数
この章では、ハッシュ関数をいきなり定義から覚えるのではなく、身近な場面から見ていきます。
「どこで役に立っているのか」を先に押さえると、後の暗号化・HMAC・署名との違いも理解しやすくなります。
ハッシュ関数は、普段の生活で名前を意識することはあまりありません。
しかし、裏側ではかなり多くの場面で使われています。
たとえば、ソフトウェアをダウンロードするときに、公式サイトが SHA-256 のような値を公開していることがあります。
これは、手元にあるファイルから計算した値と、公式サイトが示している値を比べることで、ファイルが途中で変わっていないかを確認するために使われます。
また、Gitでソースコードの履歴を管理するときにも、ハッシュ値は重要な役割を持ちます。
コミットやファイルの内容を識別するために、ハッシュ値が使われています。
ブロックチェーンでも、ハッシュ関数はよく登場します。
取引やブロックの内容からハッシュ値を作り、前のブロックとつなげることで、過去のデータが書き換えられたときに気づきやすい構造を作ります。
このように、ハッシュ関数は「暗号の世界だけに閉じた難しい道具」というより、データが変わっていないかを確認したい場面で使われる、かなり身近な技術です。
1.1 まずは「データの指紋」と考える
ハッシュ関数を最初に理解するときは、難しい数式よりも データの指紋 と考えると分かりやすいです。
人間の指紋は、人を見分けるための手がかりになります。
ハッシュ値もそれに近く、ファイルやメッセージを見分けるための手がかりになります。
もちろん、これはあくまでたとえです。
実際のハッシュ関数は、人間の指紋とは違い、入力データから決められた計算手順で値を作ります。
ここで大切なのは、ハッシュ値は 元データそのものではない という点です。
ハッシュ値は、元データを短く要約したような値であり、主に「同じデータか」「途中で変わっていないか」を確認するために使われます。
1.2 1文字変わるだけでも、ハッシュ値は大きく変わる
ハッシュ関数の面白いところは、入力が少し変わるだけでも、出力されるハッシュ値が大きく変わることです。
たとえば、次の2つの文章を考えます。
送金金額は1000円です
送金金額は9000円です
人間が見ると、違いは数字の部分だけです。
しかし、ハッシュ関数に入力すると、出力されるハッシュ値は大きく変わります。
この性質があるため、ハッシュ値は「どこかが少しでも変わったか」を見つける手がかりになります。
スマホ決済やネットショッピングのような場面では、金額や宛先が途中で変わっていないことがとても重要です。
実際のサービスではハッシュ関数だけでなく、HMAC、デジタル署名、TLSなど複数の技術が組み合わされますが、ハッシュ関数はその土台の一つになります。
1.3 ただし、ハッシュ関数は「隠す」ための技術ではない
ここで、最初に大切な注意点を確認しておきます。
ハッシュ関数は、データを読めない形にして隠すための技術ではありません。
データを第三者に読まれないようにするには、暗号化が必要です。
ハッシュ関数は、どちらかというと「中身を隠す」よりも、中身が変わっていないかを確認するために使われます。
| やりたいこと | 主に使う技術 |
|---|---|
| 通信内容や保存データを読まれないようにしたい | 暗号化 |
| ファイルやメッセージが変わっていないか確認したい | ハッシュ関数 |
| 鍵を知っている相手から来たことも確認したい | HMAC |
| 第三者にも検証できる形で本人性を示したい | デジタル署名 |
この違いを押さえておくと、次の章で扱う ハッシュ化と暗号化の違い がかなり理解しやすくなります。
2. ハッシュ化と暗号化の違い
この章では、初学者が特に混同しやすい 「ハッシュ化」と「暗号化」 の違いを整理します。
どちらも情報セキュリティでよく登場しますが、守りたいものと使いどころが異なります。
概要では、ハッシュ関数を「データの指紋を作る技術」と説明しました。
ここで一度、よく似た言葉である 暗号化 と比べておきます。
情報セキュリティを学び始めると、「パスワードを暗号化する」「ファイルをハッシュ化する」「通信を暗号化する」など、似た表現がたくさん出てきます。
日常会話ではまとめて「暗号っぽい処理」として扱われることもありますが、技術的には役割がかなり違います。
ざっくり言うと、次のように分けられます。
| 処理 | やさしく言うと | 主な目的 |
|---|---|---|
| 暗号化 | 鍵付きの箱に入れて、中身を読めないようにする | 機密性を守る |
| ハッシュ化 | データから指紋のような値を作る | 完全性や識別に使う |
ここでいう機密性は、「第三者に中身を読まれないこと」です。
一方、完全性は、「データが途中で変わっていないこと」です。
たとえば、スマホ決済で考えると、カード情報や通信内容を第三者に読まれないようにするには暗号化が重要です。
しかし、それだけでは「支払い金額が途中で書き換えられていないか」を確認するには不十分です。
そこで、ハッシュ関数、MAC、デジタル署名、認証付き暗号など、改ざん検知に関係する技術が組み合わされます。
暗号技術は「隠す」だけでなく、「変わっていないことを確認する」ためにも使われる、という感覚を持っておくと理解しやすくなります。
2.1 暗号化は「正しい鍵があれば戻せる」
暗号化は、読める状態のデータを、第三者には読みにくい形へ変換する処理です。
元の読めるデータを 平文、暗号化されたデータを 暗号文 と呼びます。
そして、正しい鍵を使って暗号文を元の平文に戻す処理を 復号 と呼びます。
身近な例でいうと、暗号化は「鍵付きの箱」に近いです。
箱に入れた中身は、鍵を持っていない人には読めません。
しかし、正しい鍵を持っている人なら箱を開けて中身を取り出せます。
たとえば、次のような場面では暗号化が重要になります。
| 場面 | 暗号化で守りたいもの |
|---|---|
| HTTPS通信 | ログイン情報や入力内容を第三者に読まれにくくする |
| クラウド保存 | 保存されたファイルの中身を読まれにくくする |
| メッセージアプリ | 送信したメッセージの内容を読まれにくくする |
NISTの用語集では、暗号化は平文を暗号文へ変換する処理として説明されています。
また、復号は暗号文を平文へ戻す処理として説明されています。
参考: NIST CSRC Glossary - Encryption
参考: NIST CSRC Glossary - Decryption
代表的な暗号化方式としては、AESがあります。
AESはNIST FIPS 197として標準化されている共通鍵暗号で、電子データを保護するために使われる代表的な方式です。
参考: NIST FIPS 197 - Advanced Encryption Standard
ここで大切なのは、暗号化は「戻せる」ことが前提になっている点です。
もちろん、誰でも戻せるわけではありません。
正しい鍵を持っている人だけが元に戻せる、というのがポイントです。
2.2 ハッシュ化は「基本的に戻さない」
一方、ハッシュ化は、入力データから固定長のハッシュ値を作る処理です。
暗号化と違い、ハッシュ値から元のデータへ戻すことを目的にしていません。
たとえるなら、ハッシュ化は「荷物の中身そのもの」ではなく、「荷物についた指紋」や「内容を表す確認用の印」を作るようなものです。
指紋を見れば、同じものかどうかを確認する手がかりになります。
しかし、指紋だけを見て荷物の中身を完全に復元することはできません。
たとえば、ソフトウェアをダウンロードしたあと、配布元が公開しているハッシュ値と、自分の手元で計算したハッシュ値を比べる場面を考えます。
この確認では、ファイルの中身を隠しているわけではありません。
目的は、「ファイルが途中で変わっていないか」を確認することです。
NISTの用語集では、ハッシュ関数は任意長のビット列を固定長のビット列へ対応させる関数として説明されています。
また、FIPS 180-4では、ハッシュアルゴリズムによって生成されるダイジェストは、メッセージが生成後に変更されたかどうかを検出するために使われると説明されています。
参考: NIST CSRC Glossary - Hash function
参考: NIST FIPS 180-4 - Secure Hash Standard
💡 豆知識
「ハッシュ値を復号する」という表現を見かけることがありますが、厳密には少し不自然です。
復号は、暗号文を元の平文へ戻す処理を指します。
ハッシュ値は基本的に元へ戻すためのものではないため、「復号する」よりも「同じ方法でハッシュ化して照合する」と考える方が正確です。
2.3 暗号化とハッシュ化を並べて整理する
ここまでの違いを表にまとめると、次のようになります。
| 比較項目 | 暗号化 | ハッシュ化 |
|---|---|---|
| 主な目的 | 中身を読まれないようにする | データが変わっていないか確認する、識別する |
| 守りたい性質 | 機密性 | 完全性、識別 |
| 元に戻せるか | 正しい鍵があれば戻せる | 基本的に戻さない |
| 鍵の有無 | 通常は鍵を使う | 通常のハッシュ関数自体は鍵を使わない |
| 出力 | 暗号文 | ハッシュ値、ダイジェスト |
| 代表例 | AES、ChaCha20 | SHA-256、SHA-3 |
| 身近な利用例 | HTTPS通信、ファイル暗号化 | ファイル検証、Git、ブロックチェーン、署名の前処理 |
暗号化とハッシュ化は、どちらも情報セキュリティで重要です。
ただし、役割は別です。
秘密にしたい情報があるなら、暗号化を考えます。
データが変わっていないことを確認したいなら、ハッシュ関数が関係します。
そして、「誰が作ったデータなのか」まで確認したい場合は、ハッシュ関数だけでは足りず、HMACやデジタル署名のような仕組みが必要になります。
この図のポイントは、ハッシュ関数が「暗号化の代わり」ではないことです。
それぞれの技術には得意な役割があります。
2.4 パスワードの話で混乱しやすい理由
ハッシュ化と暗号化の違いが特に混乱しやすいのが、パスワード保存の話です。
ログイン機能では、「パスワードはハッシュ化して保存する」と説明されることがあります。
この説明自体は方向性として重要ですが、ここで「では、ログイン時に保存されたハッシュ値を復号しているのか」と考えると、少しズレてしまいます。
実際の考え方は、ざっくり言うと次のような流れです。
つまり、保存された値を復号して元のパスワードを取り出すのではありません。
入力されたパスワード候補を同じ方法で処理し、保存されている値と一致するかを確認します。
ただし、ここで注意が必要です。
パスワード保存では、SHA-256のような通常の高速なハッシュ関数をそのまま使えば十分、とは考えない方がよいです。
攻撃者が漏えいしたハッシュ値に対して大量の候補を高速に試せる可能性があるためです。
この点は後の章で詳しく扱います。
ここではまず、次の2つだけ押さえておきます。
- ハッシュ化は、基本的に元へ戻す処理ではない
- パスワード保存では、通常のハッシュ関数とパスワード保存向けの方式を分けて考える
2.5 この章のまとめ
この章では、ハッシュ化と暗号化の違いを整理しました。
暗号化は、データを読めない形に変換し、正しい鍵を持つ人だけが元に戻せるようにする技術です。
主に、通信内容や保存データを第三者に読まれないようにするために使われます。
一方、ハッシュ化は、データから固定長のハッシュ値を作る処理です。
基本的に元へ戻すことを目的にせず、データが変わっていないかを確認したり、データを識別したりするために使われます。
| 覚えておきたいこと | 内容 |
|---|---|
| 暗号化 | 中身を隠す。鍵があれば戻せる。 |
| ハッシュ化 | データの指紋を作る。基本的に戻さない。 |
| ハッシュだけでは足りないこと | 誰が作ったかの確認にはHMACやデジタル署名が必要。 |
| パスワード保存 | 通常のハッシュ関数をそのまま使えばよいわけではない。 |
次の章では、ハッシュ関数をセキュリティ用途で使うときに重要になる性質を整理します。
「元に戻しにくい」「同じ指紋を持つ別データを作りにくい」といった性質を、できるだけ具体例を使いながら見ていきます。
3. ハッシュ関数に期待される性質
この章では、ハッシュ関数をセキュリティ用途で使うときに重要になる性質を整理します。
難しい用語も出てきますが、まずは「データの指紋として安心して使うには、どんな性質が必要なのか」という視点で見ていきます。
前の章では、ハッシュ化と暗号化の違いを整理しました。
暗号化は「鍵があれば元に戻せる」処理であるのに対し、ハッシュ化は「基本的に元に戻すことを目的にしない」処理でした。
では、ハッシュ関数はどのような性質を持っていれば、情報セキュリティで安心して使えるのでしょうか。
たとえば、ハッシュ関数を「データの指紋」と考えるなら、次のような性質がほしくなります。
- 指紋から元のデータを簡単に復元できない
- あるデータと同じ指紋を持つ別データを簡単に作れない
- 指紋が同じになる2つのデータを簡単に見つけられない
この3つは、暗号学的ハッシュ関数を理解するうえでとても重要です。
NISTの用語集でも、暗号学的ハッシュ関数には 衝突困難性、原像計算困難性、第2原像計算困難性 が期待される性質として整理されています。
参考: NIST CSRC Glossary - Cryptographic hash function
まずは全体像を表で見てみます。
| 性質 | やさしく言うと | 破られると困ること |
|---|---|---|
| 原像計算困難性 | ハッシュ値から元データを見つけにくい | ハッシュ値から元の内容を推測される |
| 第2原像計算困難性 | あるデータと同じハッシュ値になる別データを作りにくい | 正規ファイルと同じハッシュ値の偽物を作られる |
| 衝突困難性 | 同じハッシュ値になる2つの異なるデータを見つけにくい | 署名や検証の前提が崩れる可能性がある |
いきなり正式名称だけを見ると難しく感じますが、考えていることはシンプルです。
要するに、ハッシュ値を「確認用の指紋」として使うなら、攻撃者が都合のよい指紋を簡単に作れないことが重要になります。
3.1 まずはSHA-256でハッシュ値を見てみる
ここでは、Pythonの標準ライブラリである hashlib を使って、SHA-256のハッシュ値を確認してみます。
SHA-256はSHA-2系列のハッシュ関数の1つです。
NIST FIPS 180-4では、SHA-1やSHA-2系列のハッシュアルゴリズムが規定されており、生成されたメッセージダイジェストは、メッセージが変更されたかどうかを検出するために使われると説明されています。
参考: NIST FIPS 180-4 - Secure Hash Standard
import hashlib
# ハッシュ値を計算したい文字列を用意する
message = "hello"
# SHA-256でハッシュ値を計算する
# encode("utf-8") は、文字列をバイト列に変換する処理
digest = hashlib.sha256(message.encode("utf-8")).hexdigest()
# hexdigest() は、計算結果を16進数の文字列として表示するための処理
print(digest)
実行すると、次のような64文字の16進数文字列が出力されます。
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
SHA-256の出力は256ビットです。
16進数では1文字が4ビットを表すため、256ビットは64文字の16進数として表示されます。
ここで大切なのは、入力が短い "hello" でも、出力は固定長になることです。
ハッシュ関数は、入力の長さに関係なく、決まった長さのハッシュ値を出力します。
💡 豆知識
ハッシュ値は長い英数字に見えるため、最初はランダムな文字列のように感じます。
しかし、同じ入力を同じハッシュ関数に入れれば、基本的に同じハッシュ値が得られます。
そのため、ハッシュ値は「毎回変わる乱数」ではなく、「入力から決まる確認用の値」と考える方が近いです。
3.2 少しの違いでハッシュ値は大きく変わる
ハッシュ関数では、入力が少し変わるだけで出力が大きく変わります。
この性質は、直感的には「データが少しでも変わったら気づきやすい」ことにつながります。
次のコードでは、hello、Hello、hello! の3つをSHA-256でハッシュ化してみます。
import hashlib
# 比較したい入力をリストとして用意する
messages = ["hello", "Hello", "hello!"]
for message in messages:
# 文字列をUTF-8のバイト列に変換してからSHA-256に入力する
digest = hashlib.sha256(message.encode("utf-8")).hexdigest()
# 入力とハッシュ値を並べて表示する
print(f"{message}: {digest}")
実行結果は次のようになります。
hello: 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
Hello: 185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969
hello!: ce06092fb948d9ffac7d1a376e404b26b7575bcc11ee05a4615fef4fec3a308b
hello と Hello は、先頭の1文字が小文字か大文字か違うだけです。
hello と hello! も、最後に ! が付いただけです。
しかし、ハッシュ値は大きく変わっています。
このように、入力のわずかな違いが出力全体に広がるような性質は、よく 雪崩効果 と呼ばれます。
ただし、ここで注意したいのは、雪崩効果だけで安全性が決まるわけではないことです。
暗号学的ハッシュ関数として重要なのは、次に説明する原像計算困難性、第2原像計算困難性、衝突困難性のような性質です。
3.3 原像計算困難性:ハッシュ値から元データを見つけにくい
原像計算困難性 は、ハッシュ値から元の入力を見つけることが難しい、という性質です。
たとえば、次のハッシュ値だけを渡されたとします。
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
この値は、先ほどの例では "hello" のSHA-256ハッシュ値でした。
しかし、ハッシュ値だけを見て、元の入力が "hello" だと簡単に分かるわけではありません。
この性質があるからこそ、ハッシュ関数は「一方向の変換」として説明されることがあります。
一方向というのは、入力からハッシュ値を計算するのは簡単でも、ハッシュ値から入力を見つけるのは難しい、という意味です。
ただし、ここで誤解してはいけない点があります。
ハッシュ関数そのものが原像計算困難性を持っていても、入力が弱い場合は推測される可能性があります。
たとえば、元データが短いパスワードやよく使われる単語なら、攻撃者は候補を大量に試して、同じハッシュ値になるものを探せます。
そのため、パスワード保存では、通常のSHA-256をそのまま使うのではなく、Argon2id、bcrypt、PBKDF2のようなパスワード保存向けの方式を使うことが推奨されます。
この点は後の章で改めて詳しく扱います。
ここではまず、原像計算困難性は「どんな弱い入力でも絶対に守ってくれる魔法」ではなく、「十分に広い入力空間を前提に、元データを見つけにくくする性質」と理解しておくとよいです。
3.4 第2原像計算困難性:あるデータと同じ指紋を持つ別データを作りにくい
第2原像計算困難性 は、すでにある入力に対して、それと同じハッシュ値になる別の入力を見つけることが難しい、という性質です。
少し言葉が難しいので、ファイル配布の例で考えます。
公式サイトが、次の2つを公開しているとします。
- 正規のインストーラーファイル
- そのファイルのSHA-256ハッシュ値
利用者は、ダウンロードしたファイルからハッシュ値を計算し、公式サイトの値と一致するかを確認します。
ここで攻撃者にとって都合がよいのは、正規ファイルとは中身が違うのに、同じハッシュ値になる悪意あるファイルを作ることです。
もしそれが簡単にできてしまうと、ハッシュ値の確認をすり抜けられる可能性があります。
このような攻撃を難しくする性質が、第2原像計算困難性です。
| 観点 | 内容 |
|---|---|
| すでにあるもの | 正規ファイル |
| 攻撃者が作りたいもの | 正規ファイルと同じハッシュ値を持つ別ファイル |
| できてしまうと困ること | 悪意あるファイルを正規ファイルのように見せかけられる |
第2原像計算困難性は、ファイル検証、電子署名、ハッシュチェーンなどを考えるときに重要です。
「このデータと同じハッシュ値を持つ別データを、後から都合よく作れないこと」が、安全確認の前提になるからです。
3.5 衝突困難性:同じハッシュ値になる2つのデータを見つけにくい
衝突困難性 は、同じハッシュ値になる2つの異なる入力を見つけることが難しい、という性質です。
ここでいう 衝突 とは、異なる入力が同じハッシュ値になることです。
「同じハッシュ値になることなんて本当にあるのか」と感じるかもしれません。
理論上は、必ず起こりえます。
なぜなら、ハッシュ関数はどれだけ長い入力でも固定長の値に変換するからです。
入力の種類は非常に多い一方で、出力の種類は有限です。
そのため、異なる入力が同じ出力になる可能性自体は存在します。
ただし、暗号学的ハッシュ関数では、そのような組み合わせを現実的な時間で見つけることが難しいように設計されています。
ここで、衝突のイメージをつかむために、あえてSHA-256のハッシュ値を先頭4文字だけに短く切り詰める玩具例を見てみます。
⚠️ 注意
次のコードは、SHA-256そのものの衝突を見つけるコードではありません。
SHA-256の出力を先頭4文字だけに切り詰めることで、わざと衝突が起きやすい小さな世界を作っています。
「出力が短いと衝突が起きやすくなる」というイメージを確認するための実験です。
import hashlib
import itertools
import string
def short_hash(message: str) -> str:
"""SHA-256を計算し、説明用に先頭4文字だけを返す関数"""
digest = hashlib.sha256(message.encode("utf-8")).hexdigest()
# 本来のSHA-256は64文字だが、ここでは衝突を見つけやすくするために短く切る
return digest[:4]
# すでに見つけた短いハッシュ値を保存する辞書
seen = {}
# 衝突が見つかったかどうかを記録するフラグ
found = False
# a〜zの英小文字を使って、短い文字列を順番に試す
for length in range(1, 5):
for chars in itertools.product(string.ascii_lowercase, repeat=length):
message = "".join(chars)
h = short_hash(message)
# 同じ短縮ハッシュ値を持つ別の文字列がすでにあれば、衝突として表示する
if h in seen and seen[h] != message:
print("衝突を発見しました")
print(f"入力1: {seen[h]}")
print(f"入力2: {message}")
print(f"短縮ハッシュ値: {h}")
found = True
break
# まだ見つかっていない短縮ハッシュ値なら記録する
seen[h] = message
# 衝突が見つかったら、外側のループも終了する
if found:
break
実行すると、環境によって多少の探索過程は異なりますが、たとえば次のような結果が得られます。
衝突を発見しました
入力1: bj
入力2: ps
短縮ハッシュ値: 6527
この例では、bj と ps は異なる入力ですが、SHA-256の先頭4文字だけを見ると、どちらも 6527 になります。
もちろん、これはSHA-256全体の衝突ではありません。
完全なSHA-256の値を見ると、次のように異なります。
bj: 652742992acbaf9a5a3fbef60f7a51e853a93534fac251e375c662d6a5cd8d01
ps: 6527c9361a2f469c5275afcb5d06e53013367cd231995de13dc7218711388382
この実験から分かるのは、ハッシュ値を短くしすぎると衝突が見つかりやすくなるということです。
そのため、セキュリティ用途では、用途に応じた十分な長さと安全性を持つハッシュ関数を選ぶ必要があります。
NISTのHash Functionsプロジェクトでは、ハッシュ関数の衝突耐性の強度は一般に出力サイズの半分、原像計算困難性の強度は出力サイズに等しいと整理されています。
たとえばSHA-256の場合、出力は256ビットですが、衝突耐性の強度は128ビットとして整理されます。
参考: NIST CSRC - Hash Functions
💡 豆知識
「256ビットのハッシュ値なら、衝突に対しても256ビット安全」と思いたくなりますが、衝突探索では誕生日問題の影響が関係します。
そのため、一般に衝突耐性の強度は出力長の半分程度として考えられます。
このあたりは少し数学的な話になるため、本記事では「衝突では出力長をそのまま安全性として見ない」と押さえておけば十分です。
3.6 SHA-256の処理をざっくり見る
ここまで、SHA-256をブラックボックスのように使ってきました。
実際のSHA-256の内部では、入力メッセージに前処理を行い、固定長のブロックごとに処理して、最終的なハッシュ値を作ります。
FIPS 180-4では、SHA-256を含むSHA-2系列の具体的な処理が規定されています。
本記事では詳細な数式までは扱いませんが、流れだけを大まかに見ると次のようになります。
この図ではかなり簡略化していますが、ポイントは次の3つです。
| 処理 | やっていること |
|---|---|
| 前処理 | 入力を決まった形式に整える |
| ブロック処理 | 入力を一定サイズごとに処理する |
| 内部状態の更新 | 各ブロックの情報を混ぜ込み、最終的なハッシュ値を作る |
ここで大切なのは、SHA-256の安全性は「単に文字列を短くしている」わけではないという点です。
入力全体の情報が内部状態に混ぜ込まれ、少しの違いでも出力に大きな違いが出るように設計されています。
ただし、暗号アルゴリズムの内部構造を自分で実装することは、学習目的を除けば基本的にはおすすめしません。
実際の開発では、Pythonの hashlib のような標準ライブラリや、利用している言語・フレームワークの公式な暗号ライブラリを使うのが基本です。
3.7 3つの性質を使いどころから整理する
最後に、ここまで出てきた3つの性質を、使いどころと結びつけて整理します。
| 性質 | 関係しやすい場面 | 具体例 |
|---|---|---|
| 原像計算困難性 | ハッシュ値から元データを推測されにくくしたい場面 | ハッシュ値から元データを直接求められないようにする |
| 第2原像計算困難性 | 既存データと同じハッシュ値の偽物を作られたくない場面 | 正規ファイルと同じハッシュ値の悪意あるファイルを作られにくくする |
| 衝突困難性 | どの2つのデータでも同じハッシュ値を見つけられたくない場面 | 署名対象の文書や証明書で、同じハッシュ値の別データを作られにくくする |
これらの性質は、別々に暗記するよりも、「攻撃者が何をしたいのか」で考えると理解しやすくなります。
3.8 この章のまとめ
この章では、ハッシュ関数をセキュリティ用途で使うときに重要な性質を整理しました。
ハッシュ関数は、入力から固定長のハッシュ値を作るだけではありません。
情報セキュリティで使う場合は、攻撃者が都合のよい入力やハッシュ値を簡単に作れないことが重要になります。
特に大切なのは、次の3つです。
| 性質 | 一言でいうと |
|---|---|
| 原像計算困難性 | ハッシュ値から元データを見つけにくい |
| 第2原像計算困難性 | あるデータと同じハッシュ値を持つ別データを作りにくい |
| 衝突困難性 | 同じハッシュ値になる2つの異なるデータを見つけにくい |
また、PythonでSHA-256を試すことで、入力が少し変わるだけでもハッシュ値が大きく変わることを確認しました。
さらに、ハッシュ値を短く切り詰める玩具例を通じて、出力長と衝突の見つけやすさにも関係があることを見ました。
次の章では、今回少し触れたSHA-256をもう少し実験的に見ていきます。
実際にファイルのハッシュ値を計算し、ファイルの内容が少し変わったときにハッシュ値がどう変化するのかを確認します。
4. SHA-256でファイルの変化を見てみる
この章では、SHA-256を使って、実際にファイルのハッシュ値を計算してみます。
前の章で見た「少しの変化でハッシュ値が大きく変わる」という性質を、文字列ではなくファイルで確認します。
前の章では、ハッシュ関数に期待される性質を整理しました。
その中で、SHA-256を使うと、入力が1文字変わるだけでも出力されるハッシュ値が大きく変わることを見ました。
ここでは、もう少し実際の利用場面に近づけて、ファイルのハッシュ値 を計算してみます。
ソフトウェアをダウンロードするとき、配布ページにSHA-256の値が載っていることがあります。
これは、ダウンロードしたファイルが配布元の想定する内容と一致しているかを確認するための材料になります。
NIST FIPS 180-4では、ハッシュアルゴリズムで生成されるメッセージダイジェストは、メッセージが生成後に変更されたかどうかを検出するために使われると説明されています。
参考: NIST FIPS 180-4 - Secure Hash Standard
また、Pythonの hashlib は、SHA-256を含む複数のハッシュアルゴリズムを共通の方法で扱うための標準ライブラリです。
参考: Python Documentation - hashlib
4.1 今回確認すること
この章では、次の流れで確認します。
ここで見たいポイントは、次の2つです。
| 確認したいこと | 見るポイント |
|---|---|
| 同じファイルなら同じハッシュ値になるか | 同じ内容からは同じSHA-256値が得られる |
| ファイルが少し変わるとどうなるか | 1文字の違いでもハッシュ値が大きく変わる |
なお、この記事のコードは学習用です。
実際にソフトウェアの安全性を確認する場合は、ハッシュ値だけでなく、配布元の公式サイトであること、通信経路、デジタル署名の有無などもあわせて確認する必要があります。
4.2 まずは文字列をファイルに保存する
まず、確認用のテキストファイルを作成します。
from pathlib import Path
# 確認用ファイルの保存先を決める
file_path = Path("sample.txt")
# ファイルに書き込む内容を用意する
content = "これはハッシュ関数の確認用ファイルです。\n"
# テキストファイルとして保存する
# encoding="utf-8" を指定して、文字コードの違いによる混乱を避ける
file_path.write_text(content, encoding="utf-8")
print("sample.txt を作成しました")
このコードでは、sample.txt というファイルを作成し、確認用の文章を書き込んでいます。
ハッシュ値はファイルの内容から計算されるため、改行や文字コードの違いでも結果が変わることがあります。
そのため、ここでは encoding="utf-8" を明示しています。
4.3 ファイルのSHA-256を計算する
次に、作成したファイルのSHA-256ハッシュ値を計算します。
import hashlib
from pathlib import Path
# ハッシュ値を計算したいファイルを指定する
file_path = Path("sample.txt")
# ファイルの内容をバイト列として読み込む
# ハッシュ関数は文字列ではなく、最終的にはバイト列を入力として扱う
data = file_path.read_bytes()
# SHA-256でハッシュ値を計算する
digest = hashlib.sha256(data).hexdigest()
# 計算結果を16進数の文字列として表示する
print(digest)
実行すると、次のようなハッシュ値が得られます。
13bd9b2724cc809970fa8c230c8f723c28b866b13b937e6d58464a5369477259
この64文字の16進数が、今回作成した sample.txt のSHA-256ハッシュ値です。
ここで重要なのは、ハッシュ値が ファイル名 ではなく ファイルの中身 から計算されることです。
同じ内容なら、別のファイル名でも同じハッシュ値になります。
逆に、ファイル名が同じでも中身が変われば、ハッシュ値は変わります。
4.4 ファイルを1文字だけ変更してみる
次に、ファイルの中身を少しだけ変更します。
ここでは、句点 。 を感嘆符 ! に変えてみます。
from pathlib import Path
# 変更後の内容を用意する
modified_content = "これはハッシュ関数の確認用ファイルです!\n"
# 同じファイルに上書きする
Path("sample.txt").write_text(modified_content, encoding="utf-8")
print("sample.txt の内容を変更しました")
人間が読むと、ほとんど同じ文章に見えます。
しかし、ハッシュ関数にとっては入力データが変わっています。
もう一度、SHA-256ハッシュ値を計算してみます。
import hashlib
from pathlib import Path
# 変更後のファイルをバイト列として読み込む
data = Path("sample.txt").read_bytes()
# SHA-256でハッシュ値を再計算する
digest = hashlib.sha256(data).hexdigest()
# 変更後のハッシュ値を表示する
print(digest)
実行すると、次のような値になります。
d7bd3fff2c0079fbfe8fbbcc6c4d4f80bd2e07a0cc9ed0d3cff559f4d1174af1
変更前後を並べると、違いが分かりやすくなります。
| 状態 | 内容 | SHA-256 |
|---|---|---|
| 変更前 | これはハッシュ関数の確認用ファイルです。 |
13bd9b2724cc809970fa8c230c8f723c28b866b13b937e6d58464a5369477259 |
| 変更後 | これはハッシュ関数の確認用ファイルです! |
d7bd3fff2c0079fbfe8fbbcc6c4d4f80bd2e07a0cc9ed0d3cff559f4d1174af1 |
たった1文字の違いですが、ハッシュ値は大きく変わっています。
この性質があるため、ハッシュ値はファイルの変更検知に使えます。
ファイルが途中で壊れたり、意図せず変更されたり、悪意を持って書き換えられたりすると、計算されるハッシュ値が変わる可能性が高いからです。
4.5 大きなファイルでは少しずつ読み込む
ここまでの例では、read_bytes() でファイル全体を一度に読み込みました。
小さなファイルならこれで十分です。
しかし、大きなファイルを扱う場合、ファイル全体を一度にメモリへ読み込むのは効率がよくありません。
そのような場合は、ファイルを少しずつ読み込みながらハッシュ値を計算できます。
import hashlib
from pathlib import Path
def calculate_sha256(file_path: Path) -> str:
"""指定したファイルのSHA-256ハッシュ値を計算する関数"""
# SHA-256の計算オブジェクトを作成する
sha256 = hashlib.sha256()
# ファイルをバイナリモードで開く
with file_path.open("rb") as f:
while True:
# 8192バイトずつ読み込む
chunk = f.read(8192)
# 読み込むデータがなくなったらループを終了する
if not chunk:
break
# 読み込んだ部分をSHA-256の計算に追加する
sha256.update(chunk)
# 最終的なハッシュ値を16進数の文字列として返す
return sha256.hexdigest()
# sample.txt のSHA-256を計算して表示する
digest = calculate_sha256(Path("sample.txt"))
print(digest)
このコードでは、update() を使って、ファイルを分割して読み込みながらSHA-256を計算しています。
大きなファイルでも、全体を一度にメモリへ載せずに処理できるため、実用的な書き方に近くなります。
4.6 公開ハッシュ値と比較する
ファイル検証では、配布元が公開しているハッシュ値と、自分の手元で計算したハッシュ値を比較します。
ここでは、先ほどの変更後ファイルのハッシュ値を「期待する値」として比較してみます。
from pathlib import Path
import hashlib
def calculate_sha256(file_path: Path) -> str:
"""指定したファイルのSHA-256ハッシュ値を計算する関数"""
sha256 = hashlib.sha256()
with file_path.open("rb") as f:
while True:
chunk = f.read(8192)
if not chunk:
break
sha256.update(chunk)
return sha256.hexdigest()
# 配布元が公開している値、という想定のハッシュ値
expected_digest = "d7bd3fff2c0079fbfe8fbbcc6c4d4f80bd2e07a0cc9ed0d3cff559f4d1174af1"
# 手元のファイルからSHA-256を計算する
actual_digest = calculate_sha256(Path("sample.txt"))
# 期待する値と、手元で計算した値を比較する
if actual_digest == expected_digest:
print("ハッシュ値が一致しました")
print("ファイルの内容は期待したものと一致している可能性が高いです")
else:
print("ハッシュ値が一致しません")
print("ファイルの破損や改ざんの可能性を確認してください")
この確認の流れを図にすると、次のようになります。
ここで、あえて「安全です」と断定せず、一致している可能性が高い と書いている点が重要です。
ハッシュ値の比較は、ファイルの内容確認として有用ですが、それだけで配布元の信頼性まで保証するものではありません。
たとえば、攻撃者が偽サイトを作り、改ざん済みファイルとそのハッシュ値を一緒に載せていた場合、手元で計算したハッシュ値と偽サイト上の値は一致してしまいます。
そのため、実際には公式サイトであること、HTTPS接続、デジタル署名、パッケージマネージャの署名検証などもあわせて見る必要があります。
💡 豆知識
ハッシュ値の比較は、荷物の中身が配布元の説明と一致しているかを確かめる「照合」に近いです。
ただし、その説明をしている相手が本物かどうかまでは、ハッシュ値だけでは確認できません。
「中身の一致」と「配布元の信頼」は、分けて考える必要があります。
4.7 コマンドラインでも確認できる
Pythonを書かなくても、OSのコマンドでSHA-256を確認できる場合があります。
たとえば、LinuxやmacOSでは次のようなコマンドが使えます。
# LinuxでSHA-256を計算する例
sha256sum sample.txt
macOSでは、環境によって次のコマンドを使うこともあります。
# macOSでSHA-256を計算する例
shasum -a 256 sample.txt
WindowsのPowerShellでは、次のように確認できます。
# Windows PowerShellでSHA-256を計算する例
Get-FileHash .\sample.txt -Algorithm SHA256
出力形式はOSやコマンドによって少し異なりますが、目的は同じです。
ファイルの中身からSHA-256ハッシュ値を計算し、期待する値と比較します。
ただし、コマンドの使い方や出力形式は環境によって異なる場合があります。
実際に利用するときは、自分のOSの公式ドキュメントやヘルプも確認してください。
4.8 この章のまとめ
この章では、SHA-256を使ってファイルのハッシュ値を計算しました。
文字列だけでなく、ファイルに対してもハッシュ値を計算できます。
ファイルの中身が同じなら同じハッシュ値になり、ファイルの中身が少しでも変わるとハッシュ値も大きく変わります。
今回のポイントを整理すると、次のようになります。
| ポイント | 内容 |
|---|---|
| SHA-256 | 入力データから256ビットのハッシュ値を作る |
| ファイル検証 | 配布元の値と手元の値を比べることで、内容の一致を確認する材料になる |
| 1文字の変更 | 人間には小さな違いでも、ハッシュ値は大きく変わる |
| 大きなファイル |
update() を使い、少しずつ読み込みながら計算できる |
| 注意点 | ハッシュ値だけでは、配布元が本物かまでは確認できない |
ここまでで、ハッシュ関数が「データの変化に気づくための道具」として使えることが見えてきました。
次の章では、ファイル検証以外の使いどころも含めて、ハッシュ関数が実際にどのような場面で使われているのかを整理します。
5. ハッシュ関数の使いどころ
この章では、ハッシュ関数が実際にどのような場面で使われているのかを整理します。
前の章ではファイルの変化をSHA-256で確認しました。ここからは、ファイル検証だけでなく、HMAC、デジタル署名、Git、ブロックチェーン、監査ログなどに視野を広げます。
前の章では、ファイルの中身が少し変わるだけで、SHA-256のハッシュ値も大きく変わることを確認しました。
ここまで読んだ人の中には、次のように感じた人もいるかもしれません。
ファイルの変化を確認できるのは分かった。
でも、ハッシュ関数はそれ以外にどこで使われているの?
実は、ハッシュ関数は情報セキュリティのかなり広い範囲で使われています。
ただし、どの場面でも同じ意味で使われるわけではありません。
ある場面では、ファイルが変わっていないかを確認するために使われます。
別の場面では、メッセージ認証やデジタル署名の部品として使われます。
さらに、Gitやブロックチェーンのように、データ同士を結びつけるために使われることもあります。
まずは全体像を見てみます。
| 使いどころ | ハッシュ関数の役割 | 一言でいうと |
|---|---|---|
| ファイル検証 | ファイルの内容から確認用の値を作る | ダウンロードしたものが変わっていないか見る |
| HMAC | ハッシュ関数に共有鍵を組み合わせる | 鍵を知っている相手から来たか確認する |
| デジタル署名 | 署名対象データのダイジェストを作る | 大きなデータを扱いやすい形にして署名する |
| Git | オブジェクトを識別する値として使う | ファイルや履歴を区別する |
| ブロックチェーン | データ同士をハッシュでつなぐ | 後から書き換えにくい履歴を作る |
| 監査ログ | 前のログのハッシュ値を次のログへ入れる | 改ざんに気づきやすいログを作る |
この図を見ると、ハッシュ関数は「単独で安全を完成させる技術」というより、いろいろな仕組みの中で使われる土台の部品だと分かります。
5.1 ファイル検証:ダウンロードしたものが変わっていないか確認する
最もイメージしやすい使いどころは、前の章で扱ったファイル検証です。
ソフトウェアやISOイメージをダウンロードするとき、配布元がSHA-256のハッシュ値を公開していることがあります。
利用者は、自分の手元にあるファイルからハッシュ値を計算し、配布元の値と比較します。
NIST FIPS 180-4では、ハッシュアルゴリズムで生成されるメッセージダイジェストが、メッセージが変更されたかどうかの検出に使われると説明されています。
参考: NIST FIPS 180-4 - Secure Hash Standard
ただし、ここで注意が必要です。
ハッシュ値が一致することは、ファイルの内容が一致することを確認する材料にはなります。
しかし、それだけで「配布元が本物であること」までは確認できません。
たとえば、攻撃者が偽サイトを作り、改ざんしたファイルと、その改ざんファイルに対応するハッシュ値を一緒に置いていた場合、利用者の手元ではハッシュ値が一致してしまう可能性があります。
そのため、ファイル検証では次のように考えると安全です。
| 確認したいこと | ハッシュ値だけで確認できるか | 補足 |
|---|---|---|
| 手元のファイルが公開値と一致するか | 確認できる | ファイル破損や途中変更の確認に役立つ |
| 公開値そのものが本物か | それだけでは不十分 | 公式サイト、HTTPS、署名なども確認する |
| 配布者が誰か | それだけでは分からない | デジタル署名などが必要になる |
💡 豆知識
ファイルのハッシュ値を確認する作業は、荷物の伝票番号だけを見る作業に少し似ています。
伝票番号が一致すれば同じ荷物を追跡している可能性は高いですが、「その荷物を本当に信頼できる相手が送ったか」までは別の確認が必要です。
5.2 HMAC:ハッシュ関数に「秘密の鍵」を組み合わせる
普通のハッシュ関数は、基本的に誰でも計算できます。
たとえば、あるメッセージに対するSHA-256の値は、同じメッセージを持っている人なら誰でも計算できます。
これは便利ですが、同時に次のような限界もあります。
ハッシュ値だけでは、その値を誰が作ったのか分からない。
そこで使われる代表的な仕組みが HMAC です。
HMACは、ハッシュ関数に共有秘密鍵を組み合わせて、メッセージ認証のための値を作る仕組みです。
NIST FIPS 198-1では、HMACは暗号学的ハッシュ関数と共有秘密鍵を組み合わせて使うメッセージ認証の仕組みとして説明されています。
参考: NIST FIPS 198-1 - The Keyed-Hash Message Authentication Code
HMACの考え方は、次のように整理できます。
普通のハッシュ関数が「データの指紋」を作るものだとすると、HMACは「合言葉を知っている人だけが作れる確認印」に近いです。
たとえば、API通信では、サーバー同士があらかじめ共有した秘密鍵を使い、リクエストの内容に対するHMACを計算することがあります。
受信側も同じ秘密鍵を持っていれば、受け取ったリクエストが途中で変わっていないか、共有鍵を知っている相手から来たものかを確認できます。
Pythonでは、標準ライブラリの hmac と hashlib を使って、HMAC-SHA256を試せます。
Python公式ドキュメントでは、hmac モジュールはRFC 2104で説明されるHMACアルゴリズムを実装していると説明されています。
参考: Python Documentation - hmac
import hashlib
import hmac
# 学習用の共有秘密鍵です。
# 実サービスでは、ソースコードに秘密鍵を直接書かず、環境変数や安全な鍵管理の仕組みを使います。
secret_key = b"demo-secret-key"
# 認証したいメッセージです。
# ここではAPIリクエストの内容をかなり単純化した例にしています。
message = b"amount=1000&to=shop-a"
# HMAC-SHA256を計算します。
# 第1引数: 共有秘密鍵
# 第2引数: メッセージ
# 第3引数: 内部で使うハッシュ関数
mac = hmac.new(secret_key, message, hashlib.sha256).hexdigest()
print(mac)
受信側では、受け取ったメッセージに対して同じ計算を行い、送られてきたHMAC値と一致するかを確認します。
import hashlib
import hmac
secret_key = b"demo-secret-key"
# 送信者が送ったとされるメッセージとHMAC値です。
received_message = b"amount=1000&to=shop-a"
received_mac = hmac.new(secret_key, received_message, hashlib.sha256).hexdigest()
# 攻撃者が金額を書き換えたケースを想定します。
# メッセージが変わると、同じ秘密鍵で計算してもHMAC値は別物になります。
tampered_message = b"amount=9000&to=shop-a"
expected_mac = hmac.new(secret_key, tampered_message, hashlib.sha256).hexdigest()
# compare_digest() を使うと、比較処理に関する余計な情報漏えいを避けやすくなります。
# 一致すればTrue、不一致ならFalseです。
print(hmac.compare_digest(received_mac, expected_mac))
この例では、メッセージの金額部分を変えると、HMAC値も変わります。
そのため、受信側は「途中で書き換えられた可能性がある」と判断できます。
ただし、HMACにも注意点があります。
HMACを検証できる人は、基本的に同じ共有秘密鍵を持っている人です。
つまり、第三者に対して「この人が作った」と証明する用途には向きません。
そのような用途では、次に見るデジタル署名が関係してきます。
5.3 デジタル署名:署名する前にハッシュで短くまとめる
デジタル署名は、データの作成者や改ざんの有無を確認するための技術です。
秘密鍵で署名を作り、公開鍵で検証します。
NIST FIPS 186-5では、デジタル署名はデータの不正な変更を検出し、署名者の身元を認証するために使われると説明されています。
また、署名されたデータの受信者は、その署名が主張された署名者によって生成されたことを第三者に示す証拠として利用できます。
参考: NIST FIPS 186-5 - Digital Signature Standard
デジタル署名では、大きなデータそのものを直接扱うのではなく、まずハッシュ関数で短いダイジェストにまとめ、その値に対して署名する流れが使われることがあります。
ここでハッシュ関数が担うのは、データを扱いやすい固定長の値にまとめる役割です。
署名の検証では、受信したデータから再計算したハッシュ値と、署名に対応する情報を照合します。
ただし、初学者向けには次の点を押さえれば十分です。
| 技術 | 役割 |
|---|---|
| ハッシュ関数 | データから固定長の確認用の値を作る |
| デジタル署名 | 秘密鍵で署名し、公開鍵で検証できるようにする |
| 公開鍵 | 署名が正しいか確認するために使う |
| 秘密鍵 | 署名を作るために使う。外部に漏らしてはいけない |
💡 豆知識
デジタル署名は「サイン画像を貼ること」ではありません。
実際には、秘密鍵と公開鍵を使って検証できるデータを作る仕組みです。
そのため、見た目のサインよりも「検証できること」が重要です。
HMACとデジタル署名の違いは、次の章で改めて整理します。
5.4 Git:ファイルや履歴を識別する
ハッシュ関数は、暗号プロトコルだけでなく、開発者が普段使うツールにも登場します。
代表例が Git です。
Gitでは、ファイルの内容やコミットなどのオブジェクトを識別するためにハッシュ値が使われます。
そのため、Gitを使っている人は、普段から意識しないところでハッシュ関数のお世話になっています。
たとえば、GitのコミットIDとして表示される長い英数字は、オブジェクトを識別するための値です。
短く表示されることも多いですが、内部的にはハッシュ値に基づいています。
Git公式ドキュメントでは、SHA-256リポジトリ形式では、オブジェクト名やオブジェクト内の参照がSHA-1からSHA-256へ切り替わると説明されています。
また、SHA-256リポジトリは古いGitでは読めないなど、移行時の互換性にも注意が必要です。
参考: Git Documentation - hash-function-transition
ここで大切なのは、Gitでのハッシュ値は「暗号化して隠すため」ではなく、主にオブジェクトを識別し、内容に基づいた参照を行うために使われているという点です。
💡 豆知識
GitのコミットIDは、履歴の住所のようなものです。
「この変更はどれか」を指し示すために使われます。
ただし、Gitのハッシュ関数移行の話から分かるように、長く使われる仕組みでは、将来の暗号方式変更も現実的な課題になります。
5.5 ブロックチェーン:データをハッシュでつなげる
ブロックチェーンでも、ハッシュ関数は重要な役割を持ちます。
暗号資産という名前から、「取引内容が全部暗号化されて見えない」と思われることがあります。
しかし、多くのパブリックブロックチェーンでは、取引履歴は公開されています。
ここで中心になるのは、暗号化よりも、ハッシュ関数やデジタル署名です。
Bitcoinのホワイトペーパーでは、取引をハッシュベースのProof-of-Workのチェーンに入れてタイムスタンプし、記録を変更するにはProof-of-Workをやり直す必要がある構造が説明されています。
参考: Bitcoin: A Peer-to-Peer Electronic Cash System
かなり単純化すると、ブロックチェーンは次のように、前のブロックのハッシュ値を次のブロックへ含めることでつながります。
この仕組みでは、過去のブロックの内容を変えると、そのブロックのハッシュ値が変わります。
すると、次のブロックが参照しているハッシュ値とも合わなくなり、さらに後続のブロックにも影響します。
つまり、ハッシュ関数は「過去の記録を後から変えにくくする構造」の一部として使われています。
ただし、ハッシュでつながっているだけでブロックチェーン全体が安全になるわけではありません。
実際には、署名、合意形成、ネットワーク、経済的インセンティブなど、多くの要素が組み合わさっています。
5.6 監査ログ:ハッシュチェーンで改ざんに気づきやすくする
ハッシュ関数は、監査ログの改ざん検知にも応用できます。
監査ログとは、システム上で「誰が、いつ、何をしたか」を記録するログです。
たとえば、管理者が設定を変更した、ユーザーがログインした、APIが実行された、といった操作を記録します。
このログが後からこっそり書き換えられると、インシデント調査や不正調査が難しくなります。
そこで、前のログのハッシュ値を次のログに含める ハッシュチェーン の考え方を使うと、途中の改ざんに気づきやすくできます。
簡単なイメージをPythonで書くと、次のようになります。
import hashlib
import json
def calc_hash(record):
"""ログ1件をJSON文字列に変換し、SHA-256のハッシュ値を計算する関数"""
# sort_keys=True にすることで、辞書のキー順による出力差を減らします。
# ensure_ascii=False は、日本語を読みやすく扱うための指定です。
encoded = json.dumps(record, sort_keys=True, ensure_ascii=False).encode("utf-8")
# ログ内容からSHA-256のハッシュ値を計算します。
return hashlib.sha256(encoded).hexdigest()
# 最初のログには前のログがないため、便宜的に0を並べた値を入れます。
previous_hash = "0" * 64
# 学習用のログです。
# 実サービスでは、時刻、ユーザーID、操作対象、リクエストIDなども記録します。
actions = [
"user_login",
"change_setting",
"download_report",
]
records = []
for index, action in enumerate(actions, start=1):
# 各ログに「前のログのハッシュ値」を含めます。
record = {
"index": index,
"action": action,
"previous_hash": previous_hash,
}
# 現在のログ全体からハッシュ値を計算します。
current_hash = calc_hash(record)
# 表示・検証しやすいように、ログ本体と現在のハッシュ値をまとめて保存します。
records.append({
**record,
"current_hash": current_hash,
})
# 次のログが参照できるよう、現在のハッシュ値を保存します。
previous_hash = current_hash
for record in records:
print(record)
このコードでは、各ログに前のログのハッシュ値を含めています。
そのため、途中のログを書き換えると、そのログのハッシュ値が変わり、後ろのログとのつながりも崩れます。
ただし、これはあくまで学習用の簡易例です。
実際の監査ログでは、ログの保存場所、アクセス権限、時刻の信頼性、署名、外部保管、改ざん検知の運用手順なども考える必要があります。
💡 豆知識
ハッシュチェーンは、「前のページの内容を次のページにもメモしておくノート」に少し似ています。
途中のページだけを書き換えると、次のページに書かれたメモと合わなくなるため、変更に気づきやすくなります。
5.7 使いどころを「何を守りたいか」で整理する
ここまで見ると、ハッシュ関数はいろいろな場所で使われていることが分かります。
ただし、使いどころを覚えるだけでは、実際に設計するときに迷いやすくなります。
そこで、ハッシュ関数を使う場面は、何を守りたいのか から整理すると分かりやすくなります。
| 守りたいこと | ハッシュ関数の関係 | 追加で必要になること |
|---|---|---|
| データが変わっていないこと | ハッシュ値を比較する | 公開されたハッシュ値自体の信頼性確認 |
| 鍵を知る相手から来たこと | HMACで確認する | 共有秘密鍵の安全な管理 |
| 第三者にも署名者を示すこと | 署名前のダイジェスト生成に関係する | デジタル署名、公開鍵、証明書 |
| 履歴のつながりを守ること | 前のハッシュ値を次のデータへ含める | ログ保管、権限管理、運用設計 |
| データを識別すること | 内容に基づくIDとして使う | 衝突時の扱いや移行方針 |
ハッシュ関数はとても便利ですが、万能ではありません。
特に、「誰が作ったのか」「本当に信頼できる相手なのか」「秘密情報を安全に保存できているか」といった問題は、ハッシュ関数だけでは解決できません。
5.8 この章のまとめ
この章では、ハッシュ関数の代表的な使いどころを整理しました。
ファイル検証では、ダウンロードしたファイルが公開値と一致するかを確認する材料になります。
HMACでは、ハッシュ関数に共有秘密鍵を組み合わせることで、メッセージ認証に使われます。
デジタル署名では、データを短いダイジェストにまとめる役割として関係します。
Gitでは、ファイルやコミットなどのオブジェクト識別に使われます。
ブロックチェーンや監査ログでは、データ同士をハッシュでつなげることで、途中の変更に気づきやすくします。
ここまでで、ハッシュ関数が「データの指紋」を作るだけでなく、さまざまなセキュリティ技術の土台として使われていることが見えてきました。
一方で、ハッシュ関数だけではできないこともあります。
次の章では、ハッシュ関数の限界を整理しながら、HMACやデジタル署名との違いをもう少し明確にしていきます。
6. ハッシュ関数だけではできないこと
この章では、ハッシュ関数の限界を整理します。
ハッシュ関数はとても便利ですが、「データの指紋」を作るだけでは解決できない問題もあります。
前の章では、ハッシュ関数がファイル検証、HMAC、デジタル署名、Git、ブロックチェーン、監査ログなど、幅広い場面で使われていることを見ました。
ここまで読むと、ハッシュ関数はかなり万能な技術に見えるかもしれません。
たしかに、ハッシュ関数は情報セキュリティを支える重要な部品です。
しかし、ハッシュ関数だけで何でも守れるわけではありません。
たとえば、次のような疑問を考えてみます。
- このファイルは途中で変わっていないか
- このハッシュ値は本当に公式サイトが出したものなのか
- このメッセージは本当に正しい相手から届いたものなのか
- 後から第三者に「この人が署名した」と説明できるのか
- パスワードを安全に保存できているのか
このうち、ハッシュ関数だけで確認できる範囲は限られます。
ハッシュ関数は「データが変わったか」を確認する材料にはなりますが、「誰が作ったか」「信頼できる相手か」「秘密を守れているか」までは、単体では判断できません。
この章では、ハッシュ関数だけではできないことを整理し、HMAC、デジタル署名、暗号化、パスワード保存用の方式との違いを見ていきます。
6.1 ハッシュ値だけでは「誰が作ったか」は分からない
まず大切なのは、普通のハッシュ関数には鍵がないという点です。
SHA-256のような通常のハッシュ関数は、入力が同じであれば誰が計算しても同じハッシュ値になります。
これは便利な性質です。だからこそ、公式サイトが公開したハッシュ値と、自分の手元で計算したハッシュ値を比較できます。
一方で、誰でも同じ値を計算できるということは、ハッシュ値だけを見ても「誰が作ったのか」は分からないということでもあります。
たとえば、次のような状況を考えます。
メッセージ: 1000円を送金する
ハッシュ値: abc123...
このハッシュ値だけでは、「1000円を送金する」というメッセージが変わっていないかを確認する材料にはなります。
しかし、そのメッセージを本当に正しい送信者が作ったのかまでは分かりません。
ハッシュ関数は、データそのものの変化には敏感です。
でも、データの作成者までは見ていません。
💡 豆知識
ハッシュ関数は、誰でも使える「指紋採取キット」のようなものです。
指紋そのものは作れますが、「その指紋を誰が採取したか」を証明する仕組みは別に必要です。
6.2 送信者を確認したいならHMACやデジタル署名を使う
「誰が作ったのか」も確認したい場合は、ハッシュ関数に別の仕組みを組み合わせます。
代表的なのが、HMAC と デジタル署名 です。
HMACは、共有秘密鍵とハッシュ関数を組み合わせて、メッセージ認証を行う仕組みです。
NIST FIPS 198-1では、HMACは暗号学的ハッシュ関数と共有秘密鍵を組み合わせて使うメッセージ認証の仕組みとして説明されています。
参考: NIST FIPS 198-1 - The Keyed-Hash Message Authentication Code
一方、デジタル署名は、秘密鍵で署名を作り、公開鍵で検証する仕組みです。
NIST FIPS 186-5では、デジタル署名はデータの不正な変更を検出し、署名者の身元を認証し、第三者に対する証拠として使えると説明されています。
参考: NIST FIPS 186-5 - Digital Signature Standard
違いをざっくり整理すると、次のようになります。
| 技術 | 使う鍵 | 確認できること | 向いている場面 |
|---|---|---|---|
| ハッシュ関数 | なし | データが変わったかの確認材料 | ファイル検証、識別子、ハッシュチェーン |
| HMAC | 共有秘密鍵 | 鍵を持つ相手が作ったこと、途中で変わっていないこと | API通信、サーバー間通信 |
| デジタル署名 | 秘密鍵・公開鍵 | 署名者、改ざん有無、第三者への証明材料 | 電子署名、証明書、ブロックチェーン |
ここで、HMACとデジタル署名の違いも大切です。
HMACは、同じ秘密鍵を持っている相手同士で確認する仕組みです。
そのため、内部システム同士のAPI通信のように、あらかじめ秘密鍵を共有できる場面に向いています。
一方、デジタル署名は、公開鍵で検証できるため、第三者にも「この秘密鍵に対応する署名らしい」と示しやすい仕組みです。
電子契約、証明書、ブロックチェーンの取引検証などでは、この性質が重要になります。
6.3 コードで見る:普通のハッシュ値は誰でも再計算できる
普通のハッシュ関数では、同じ入力を使えば誰でも同じハッシュ値を計算できます。
この性質を、簡単なPythonコードで確認してみます。
import hashlib
# 送信したいメッセージを用意します。
message = "pay:1000yen".encode("utf-8")
# SHA-256でメッセージのハッシュ値を計算します。
# 通常のハッシュ関数には秘密鍵がないため、誰でも同じ計算ができます。
digest = hashlib.sha256(message).hexdigest()
print(digest)
このコードでは、pay:1000yen という文字列からSHA-256のハッシュ値を計算しています。
ただし、このハッシュ値は、メッセージを知っている人なら誰でも再計算できます。
つまり、次のようなことは確認できます。
このメッセージから計算したハッシュ値は、この値になる
しかし、次のことまでは確認できません。
このメッセージは、本当に正しい送信者が作った
送信者も確認したい場合は、HMACのように秘密鍵を組み合わせる必要があります。
import hmac
import hashlib
# 送信者と受信者だけが知っている共有秘密鍵です。
# 実サービスでは、ソースコードに直接書かず、安全な方法で管理します。
secret_key = b"shared-secret-key"
# 認証したいメッセージを用意します。
message = b"pay:1000yen"
# HMAC-SHA256を計算します。
# メッセージだけでなく共有秘密鍵も使うため、鍵を知らない人は同じ値を作りにくくなります。
tag = hmac.new(secret_key, message, hashlib.sha256).hexdigest()
print(tag)
この例では、メッセージに加えて共有秘密鍵を使っています。
そのため、同じメッセージを知っていても、秘密鍵を知らなければ同じHMAC値を作ることは難しくなります。
ただし、HMACで使う共有秘密鍵が漏えいすると、この前提は崩れます。
HMACを使えば自動的に安全になるのではなく、秘密鍵の生成、保存、更新、失効まで含めて考える必要があります。
実装上の注意
HMAC値を比較するときは、単純な==ではなく、タイミング攻撃への配慮がされた比較関数を使うことが推奨されます。
Pythonではhmac.compare_digest()が用意されています。
参考: Python Documentation - hmac
6.4 ハッシュ関数だけでは「中身を隠す」ことはできない
次に、ハッシュ関数は暗号化ではないという点をもう一度確認します。
ハッシュ関数は、データからハッシュ値を作ります。
しかし、元データそのものを読めない形で安全に保管するための技術ではありません。
たとえば、次のような状況を考えます。
秘密のメモ: 明日の会議資料は /secret/plan.pdf にある
SHA-256: 4f2a...
このハッシュ値を作ったからといって、元のメモが暗号化されるわけではありません。
元のメモが平文のまま保存されていれば、ファイルを読める人には普通に中身が見えてしまいます。
中身を読まれたくない場合は、ハッシュ関数ではなく暗号化を使います。
ここは、初学者が特に混同しやすいポイントです。
ハッシュ関数は「中身を隠す」ための技術ではありません。
ハッシュ値を作ったとしても、元データを別の場所に平文で置いていれば、そのデータは読まれる可能性があります。
💡 豆知識
ハッシュ化は「目隠し」ではなく「検品シール」に近いです。
目隠しをしたいなら暗号化、検品したいならハッシュ化、と考えると少しイメージしやすくなります。
6.5 ハッシュ値だけでは、公開された値そのものの信頼性は分からない
ファイル検証では、公式サイトが公開しているハッシュ値と、自分の手元で計算したハッシュ値を比較します。
しかし、ここには見落としやすい点があります。
それは、比較先のハッシュ値そのものを信頼できるのか という問題です。
たとえば、攻撃者がファイルだけでなく、配布ページに書かれたハッシュ値も一緒に書き換えた場合を考えます。
この場合、手元のファイルと公開されているハッシュ値は一致します。
しかし、それは「攻撃者が用意したファイル」と「攻撃者が用意したハッシュ値」が一致しているだけかもしれません。
そのため、実際のソフトウェア配布では、ハッシュ値の比較だけでなく、次のような確認も重要になります。
| 確認したいこと | 関係する仕組み |
|---|---|
| 本当に公式サイトから取得したか | HTTPS、証明書、公式ドメイン確認 |
| 配布元が署名したファイルか | デジタル署名、コード署名 |
| 公開されたハッシュ値が信頼できるか | 署名付きチェックサム、複数経路での確認 |
ここで大切なのは、ハッシュ値の一致は強力な確認材料ではあるものの、それだけで配布元の正当性まで保証するわけではないという点です。
6.6 ハッシュ関数だけではパスワード保存にも不十分
パスワード保存では、「パスワードはハッシュ化して保存する」と説明されることがあります。
この説明自体は大きく間違いではありませんが、少し補足が必要です。
パスワード保存に、SHA-256のような通常の高速なハッシュ関数をそのまま使うのは望ましくありません。
理由は、攻撃者も高速に大量の候補を試せてしまうからです。
OWASP Password Storage Cheat Sheetでは、パスワードは平文で保存せず、Argon2id、bcrypt、PBKDF2のような強く遅いハッシュアルゴリズムで保護するべきだと説明されています。
また、レインボーテーブルのような事前計算攻撃を防ぐために、パスワードごとに一意なソルトを加えることも説明されています。
参考: OWASP Password Storage Cheat Sheet
つまり、パスワード保存では単に「ハッシュ化する」だけでなく、次のような観点が必要です。
| 観点 | なぜ必要か |
|---|---|
| ソルト | 同じパスワードでも保存値を変え、事前計算攻撃を難しくする |
| 計算コスト | 攻撃者が大量の候補を高速に試すことを難しくする |
| メモリコスト | GPUなどによる大量並列推測のコストを上げる |
| 推奨方式の利用 | Argon2id、bcrypt、PBKDF2など、用途に合った方式を使う |
パスワード保存については重要なテーマなので、次の章で改めて詳しく扱います。
6.7 ハッシュ関数の限界をまとめる
ここまで見てきた内容を整理すると、ハッシュ関数だけでできることと、できないことは次のように分けられます。
| 確認したいこと | ハッシュ関数だけで十分か | 必要になりやすい技術 |
|---|---|---|
| データが変わっていないかを確認する | 条件付きで可能 | 信頼できるハッシュ値の入手経路 |
| データの中身を隠す | 不十分 | 暗号化 |
| 誰が作ったかを確認する | 不十分 | HMAC、デジタル署名 |
| 第三者に署名者を示す | 不十分 | デジタル署名、証明書 |
| パスワードを安全に保存する | 通常のハッシュ関数だけでは不十分 | Argon2id、bcrypt、PBKDF2、ソルト |
| 履歴の改ざんに気づきやすくする | 一部可能 | ハッシュチェーン、署名、ログ保管設計 |
ハッシュ関数は、情報セキュリティにおける「基本部品」です。
基本部品だからこそ、さまざまな技術の中で使われます。
ただし、基本部品をそのまま置くだけでは、システム全体の安全性は決まりません。
目的に応じて、暗号化、HMAC、デジタル署名、パスワード保存用の方式、鍵管理、運用設計などと組み合わせる必要があります。
6.8 この章のまとめ
この章では、ハッシュ関数だけではできないことを整理しました。
ハッシュ関数は、データの指紋を作る技術です。
データの変化に気づくための重要な材料になりますが、送信者の確認、中身の秘匿、第三者への証明、パスワード保存の安全性までは、単体では十分に実現できません。
送信者を確認したい場合はHMACやデジタル署名を使います。
中身を隠したい場合は暗号化を使います。
パスワード保存では、通常のSHA-256ではなく、Argon2id、bcrypt、PBKDF2のような専用の方式を検討します。
ここまでで、ハッシュ関数の役割と限界が見えてきました。
次の章では、特に誤解されやすい パスワード保存 に焦点を当て、「普通のハッシュ関数をそのまま使わない理由」をもう少し詳しく整理します。
7. パスワード保存では普通のハッシュ関数をそのまま使わない
この章では、ハッシュ関数の記事で特に誤解されやすい パスワード保存 を扱います。
「パスワードはハッシュ化して保存する」と言われることがありますが、SHA-256をそのまま使えばよい、という意味ではありません。
前の章では、ハッシュ関数だけではできないことを整理しました。
その中でも、実際のWebサービスやアプリ開発で特に注意したいのが パスワード保存 です。
ログイン機能を作るとき、パスワードをそのままデータベースに保存してしまうと、データベースが漏えいした瞬間に利用者のパスワードが丸見えになります。
そのため、パスワードは平文のまま保存しない、という考え方が重要です。
ここまでは直感的に分かりやすいと思います。
しかし、次の一歩でつまずきやすいポイントがあります。
では、パスワードをSHA-256でハッシュ化して保存すれば十分なのか?
結論から言うと、通常のSHA-256をそのまま使うだけでは、パスワード保存としては不十分です。
ここでは、その理由を「攻撃者が何をするのか」という視点から整理します。
OWASP Password Storage Cheat Sheetでは、パスワードは平文で保存せず、Argon2id、bcrypt、PBKDF2のような強く遅いハッシュアルゴリズムで保護すべきだと説明されています。
また、SHA-256のような高速なハッシュ関数は、攻撃者が大量の推測を素早く試せるため、パスワード保存には適さないと説明されています。
参考: OWASP Password Storage Cheat Sheet
NIST SP 800-63Bでも、パスワードはオフライン攻撃に耐えられる形で保存し、ソルトと適切なパスワードハッシュ方式を使うべきだと説明されています。
ここでいうパスワードハッシュ方式は、パスワード、ソルト、コスト係数を入力として、攻撃者の推測コストを高くすることを目的にしています。
参考: NIST SP 800-63B
7.1 パスワード保存で本当に怖いのは「漏えいした後」
ログイン画面だけを見ると、パスワードは次のように使われているように見えます。
このとき、通常のログイン試行であれば、アプリ側は試行回数制限や多要素認証などを組み合わせて守ることができます。
たとえば、何度も間違えたら一時的にロックする、といった対策です。
しかし、データベースが漏えいしてしまった場合は話が変わります。
攻撃者は、手元に持ち帰ったハッシュ値に対して、サービス側のログイン画面を通さずに大量の推測を試せます。
このような攻撃は、オフライン攻撃 と呼ばれます。
ここで重要なのは、攻撃者が「復号」しているわけではないことです。
ハッシュ値から元のパスワードを直接戻すのではなく、候補となるパスワードを次々にハッシュ化し、保存済みの値と一致するかを確認しています。
そのため、「ハッシュは元に戻せないから安心」とだけ考えるのは危険です。
元に戻せなくても、候補を大量に試せるなら、弱いパスワードは見つかってしまう可能性があります。
💡 豆知識
パスワード攻撃は、鍵のかかった箱を開けるというより、「合いそうな鍵を大量に試す」イメージに近いです。
そのため、パスワード保存では、攻撃者が1回試すたびのコストを上げることが重要になります。
7.2 なぜSHA-256をそのまま使うだけでは不十分なのか
SHA-256は、ファイルの改ざん確認やデータの識別などでよく使われる暗号学的ハッシュ関数です。
しかし、パスワード保存では「高速に計算できる」という性質が逆に問題になることがあります。
たとえば、次のコードは、パスワード文字列をSHA-256でハッシュ化するだけの例です。
import hashlib
# 学習用の例です。本番のパスワード保存では、このようにSHA-256だけを直接使わないでください。
password = "password123"
# 文字列をバイト列に変換し、SHA-256のハッシュ値を計算します。
digest = hashlib.sha256(password.encode("utf-8")).hexdigest()
print(digest)
このコード自体は、SHA-256の使い方としては分かりやすい例です。
しかし、パスワード保存として見ると問題があります。
攻撃者も同じように、候補パスワードを高速にSHA-256へ入力できます。
もし利用者が password123 や qwerty123 のような推測されやすいパスワードを使っていた場合、攻撃者は候補リストを順番に試すことで一致する値を探せてしまいます。
import hashlib
# 学習用の例です。攻撃を助長する目的ではなく、なぜ高速な通常ハッシュだけでは弱いのかを確認するための最小例です。
stored_hash = hashlib.sha256("password123".encode("utf-8")).hexdigest()
# 攻撃者が試しそうな候補の例です。
candidates = [
"123456",
"password",
"password123",
"qwerty123",
]
for candidate in candidates:
# 候補パスワードを同じ方法でSHA-256に通します。
candidate_hash = hashlib.sha256(candidate.encode("utf-8")).hexdigest()
# 保存されていたハッシュ値と一致するか確認します。
if candidate_hash == stored_hash:
print(f"一致した候補: {candidate}")
break
この例では候補が少ないので簡単に見えますが、実際には大量の候補や漏えい済みパスワードリストが使われることがあります。
そのため、パスワード保存では「ハッシュ関数が安全か」だけでなく、攻撃者がどれくらい速く試せるか を考える必要があります。
7.3 ソルト:同じパスワードでも保存値を変える
パスワード保存でよく出てくる重要な言葉が ソルト です。
ソルトは、パスワードに追加するランダムな値です。
料理の塩のように少し加えるもの、という名前ですが、セキュリティ上の役割はかなり重要です。
たとえば、2人の利用者が同じパスワードを使っていた場合、単純にSHA-256だけでハッシュ化すると、保存されるハッシュ値も同じになります。
しかし、ユーザーごとに異なるソルトを加えると、同じパスワードでも保存値を変えられます。
ソルトには、主に次のような効果があります。
| 効果 | 説明 |
|---|---|
| 同じパスワードでも保存値を変える | 複数ユーザーが同じパスワードを使っていても、保存値を見ただけでは分かりにくくする |
| 事前計算攻撃を難しくする | あらかじめ大量に作られたハッシュ表を使い回しにくくする |
| ユーザーごとの攻撃コストを上げる | 同じ候補でも、ソルトごとに計算し直す必要がある |
次のコードは、ソルトのイメージを理解するための学習用例です。
import hashlib
import secrets
# 学習用の例です。本番では、後述するArgon2id、bcrypt、PBKDF2などの方式を使うことを検討してください。
password = "same-password"
# ユーザーごとに異なるランダムなソルトを生成します。
# secrets.token_bytes() は、セキュリティ用途に適したランダムなバイト列を作るために使います。
salt_user_a = secrets.token_bytes(16)
salt_user_b = secrets.token_bytes(16)
# ソルトとパスワードを結合してからSHA-256を計算します。
# これはソルトの考え方を説明するための例であり、これだけで本番向けとは考えないでください。
hash_user_a = hashlib.sha256(salt_user_a + password.encode("utf-8")).hexdigest()
hash_user_b = hashlib.sha256(salt_user_b + password.encode("utf-8")).hexdigest()
print(hash_user_a)
print(hash_user_b)
print(hash_user_a == hash_user_b) # 多くの場合 False になります。
ここで大切なのは、ソルトは秘密情報ではない という点です。
通常、ソルトはパスワードハッシュと一緒に保存します。
「一緒に保存してよいなら意味がないのでは?」と思うかもしれません。
しかし、ソルトの主な目的は、同じパスワードの保存値を変えたり、事前計算された表を使い回しにくくしたりすることです。
ソルトを秘密にすることではありません。
💡 豆知識
ソルトは「隠し味」ではありますが、「秘密の鍵」ではありません。
隠すことよりも、ユーザーごとに違う値を混ぜることに意味があります。
7.4 計算コストとメモリコスト:攻撃者が大量に試しにくくする
ソルトを使っても、攻撃者は候補パスワードを1つずつ試すこと自体はできます。
そこで重要になるのが、1回の試行にかかるコストを上げる という考え方です。
パスワード保存向けの方式では、意図的に計算を重くしたり、メモリを多く使わせたりします。
これにより、正規のログイン処理では少し待つ程度で済みますが、攻撃者が何億回も試そうとすると大きな負担になります。
| 観点 | やさしく言うと | 目的 |
|---|---|---|
| ソルト | ユーザーごとに違う値を混ぜる | 同じパスワードでも保存値を変える |
| 計算コスト | わざと計算回数を増やす | 1回の推測に時間をかけさせる |
| メモリコスト | 計算に必要なメモリ量を増やす | GPUなどで大量並列に試しにくくする |
| パラメータ更新 | 時代に合わせて設定を強くする | 計算機性能の向上に追従する |
NIST SP 800-63Bでは、コスト係数は実用上可能な範囲で高くし、計算性能の向上に合わせて時間とともに増やすべきだと説明されています。
参考: NIST SP 800-63B
この考え方は、自転車の鍵に例えると分かりやすいです。
鍵を壊すことが理論上できるとしても、1台に数秒で済むのか、1台に何時間もかかるのかで、攻撃者にとっての現実的な難しさは大きく変わります。
パスワード保存も同じです。
完全に推測を不可能にするというより、攻撃者にとって割に合わない状態に近づけることが重要です。
7.5 代表的なパスワード保存向け方式
パスワード保存では、通常のSHA-256をそのまま使うのではなく、パスワード保存向けに設計された方式を使います。
代表的な方式には、次のようなものがあります。
| 方式 | ざっくりした特徴 | 記事での位置づけ |
|---|---|---|
| Argon2id | メモリコストも考慮した比較的新しい方式 | 現在の有力な選択肢として紹介 |
| bcrypt | 長く使われているパスワードハッシュ方式 | 既存システムや対応ライブラリが多い方式として紹介 |
| scrypt | メモリコストを重視する方式 | Argon2idが使えない場合の選択肢として紹介 |
| PBKDF2 | HMACを繰り返し使う鍵導出方式 | 標準ライブラリやFIPS要件との関係で紹介 |
OWASP Password Storage Cheat Sheetでは、調査時点でArgon2id、scrypt、bcrypt、PBKDF2に関する推奨設定が整理されています。
たとえばArgon2idについては、最小構成としてメモリ、反復回数、並列度の設定例が示されています。
ただし、実際の設定値はアプリケーションの性能要件や利用環境によって調整が必要です。
参考: OWASP Password Storage Cheat Sheet
RFC 9106では、Argon2はパスワードハッシュやProof-of-Work向けのメモリハード関数として説明されています。
また、Argon2idは、Argon2iとArgon2dの考え方を組み合わせ、サイドチャネル攻撃への耐性と総当たり攻撃へのコスト増加のバランスを狙った方式として説明されています。
参考: RFC 9106 - Argon2 Memory-Hard Function for Password Hashing and Proof-of-Work Applications
ここで大切なのは、方式名を暗記することではありません。
パスワード保存では、次のような観点を持つことが大切です。
7.6 Python標準ライブラリでPBKDF2の流れを見てみる
ここでは、Python標準ライブラリの hashlib.pbkdf2_hmac() を使って、パスワード保存の基本的な流れを見てみます。
ただし、最初に注意点を書いておきます。
このコードは、パスワード保存の考え方を理解するための学習用です。
本番のWebサービスでは、利用しているフレームワークやライブラリが提供するパスワードハッシュ機能を優先して使い、最新の推奨設定を確認してください。
可能であれば、Argon2idなどに対応した実績のあるライブラリの利用を検討してください。
Python公式ドキュメントでは、hashlib.pbkdf2_hmac() はPBKDF2を提供し、HMACを疑似乱数関数として使うと説明されています。
また、良いパスワードハッシュ関数は調整可能で、遅く、ソルトを含むべきだと説明されています。
参考: Python Documentation - hashlib
import base64
import hashlib
import hmac
import secrets
# 学習用の設定です。
# 実際の設定値は、利用環境・性能要件・最新の推奨に合わせて調整してください。
ALGORITHM = "sha256"
ITERATIONS = 600_000
SALT_SIZE = 16
def hash_password(password: str) -> str:
"""パスワードから保存用の文字列を作る学習用関数です。"""
# ユーザーごとに異なるランダムなソルトを作ります。
# secrets.token_bytes() は、セキュリティ用途に適したランダム値を作るために使います。
salt = secrets.token_bytes(SALT_SIZE)
# PBKDF2-HMAC-SHA256で、パスワード・ソルト・反復回数から保存用の値を作ります。
password_hash = hashlib.pbkdf2_hmac(
ALGORITHM,
password.encode("utf-8"),
salt,
ITERATIONS,
)
# バイナリ値のままだと保存しづらいため、Base64文字列に変換します。
salt_b64 = base64.b64encode(salt).decode("ascii")
hash_b64 = base64.b64encode(password_hash).decode("ascii")
# 後から検証できるよう、方式名・反復回数・ソルト・ハッシュ値をまとめて保存します。
# この形式は学習用の簡易形式です。
return f"pbkdf2_{ALGORITHM}${ITERATIONS}${salt_b64}${hash_b64}"
def verify_password(password: str, stored_value: str) -> bool:
"""入力されたパスワードが保存値と一致するか確認する学習用関数です。"""
# 保存されている文字列から、方式名・反復回数・ソルト・ハッシュ値を取り出します。
algorithm_name, iterations, salt_b64, hash_b64 = stored_value.split("$")
# "pbkdf2_sha256" から "sha256" の部分を取り出します。
_, hash_name = algorithm_name.split("_", 1)
# Base64文字列として保存していたソルトとハッシュ値を、バイト列に戻します。
salt = base64.b64decode(salt_b64)
expected_hash = base64.b64decode(hash_b64)
# 入力されたパスワードから、保存時と同じ条件でハッシュ値を再計算します。
actual_hash = hashlib.pbkdf2_hmac(
hash_name,
password.encode("utf-8"),
salt,
int(iterations),
)
# hmac.compare_digest() を使って比較します。
# 通常の == より、比較時間の差から情報が漏れるリスクを抑えやすい比較方法です。
return hmac.compare_digest(actual_hash, expected_hash)
stored = hash_password("correct horse battery staple")
print(stored)
print(verify_password("correct horse battery staple", stored)) # True
print(verify_password("wrong password", stored)) # False
このコードのポイントは、次の4つです。
| ポイント | 説明 |
|---|---|
| ソルトをランダムに作る | 同じパスワードでも保存値を変える |
| 反復回数を保存する | 後から検証でき、将来の移行もしやすくする |
| ソルトも保存する | 検証時に同じ条件で計算するために必要 |
compare_digest() を使う |
比較処理の細かな情報漏えいリスクを減らす |
Pythonの secrets モジュールは、パスワード、アカウント認証、セキュリティトークンなどを扱うための暗号学的に強い乱数を生成する用途に使われます。
また、secrets.compare_digest() や hmac.compare_digest() は、比較時間の差から情報が漏れるリスクを下げるための比較方法として説明されています。
参考: Python Documentation - secrets
7.7 保存する値には「方式名」と「パラメータ」も含める
パスワード保存では、ハッシュ値だけを保存すればよいわけではありません。
後から検証するためには、どの方式を使ったのか、ソルトは何か、反復回数などのパラメータは何かも必要です。
たとえば、先ほどの学習用コードでは、次のような形式で保存しました。
pbkdf2_sha256$600000$<salt>$<hash>
これは、次の情報をまとめたものです。
| 要素 | 意味 |
|---|---|
pbkdf2_sha256 |
PBKDF2とHMAC-SHA256を使ったこと |
600000 |
反復回数 |
<salt> |
ユーザーごとに異なるソルト |
<hash> |
パスワードから計算した保存用の値 |
なぜ方式名やパラメータまで保存するのでしょうか。
理由は、将来の移行に備えるためです。
たとえば、数年後に反復回数を増やしたり、方式をPBKDF2からArgon2idへ移行したりする場合、保存済みの値がどの条件で作られたものか分からないと検証できません。
NIST SP 800-63Bでも、パスワードハッシュ方式、コスト係数、ソルトなどを保存し、将来のアルゴリズムや作業係数の移行に備える考え方が示されています。
参考: NIST SP 800-63B
7.8 パスワード保存で避けたいこと
ここまでの内容を踏まえると、パスワード保存で避けたいことは次のように整理できます。
| 避けたいこと | なぜ危険か |
|---|---|
| パスワードを平文保存する | データベース漏えい時にそのまま悪用される |
| パスワードを復号可能な形で保存する | 鍵が漏えいすると元のパスワードが読める可能性がある |
| SHA-256だけで保存する | 攻撃者が大量の候補を高速に試せる |
| 全ユーザーで同じソルトを使う | 同じパスワードの保存値が同じになりやすい |
| ソルトを固定値にする | 事前計算攻撃への耐性が弱くなる |
| パラメータを記録しない | 将来の検証や移行が難しくなる |
| 自作方式を使う | 見落としや実装ミスに気づきにくい |
特に、独自の「オリジナル暗号」や「自作ハッシュ方式」を作るのは避けるべきです。
パスワード保存は、見た目には簡単そうに見えても、攻撃者側の計算性能、GPU、漏えい済みパスワードリスト、実装ミスなど、考えるべき点が多い分野です。
そのため、実務ではフレームワークや実績あるライブラリが提供する機能を使い、公式ドキュメントと最新の推奨を確認することが大切です。
7.9 この章のまとめ
この章では、パスワード保存では普通のハッシュ関数をそのまま使わない理由を整理しました。
パスワード保存で大切なのは、データベースが漏えいした後でも、攻撃者が元のパスワードを簡単に推測できないようにすることです。
そのためには、単にSHA-256でハッシュ化するだけでは不十分です。
重要なポイントは次の通りです。
| ポイント | 内容 |
|---|---|
| 平文保存しない | 漏えい時の被害を抑えるため |
| 通常のSHA-256だけに頼らない | 高速すぎて大量推測されやすいため |
| ソルトを使う | 同じパスワードでも保存値を変えるため |
| 計算コストを設定する | 攻撃者の試行コストを上げるため |
| メモリコストも考慮する | 大量並列推測を難しくするため |
| 推奨方式を使う | Argon2id、bcrypt、PBKDF2など用途に合った方式を使うため |
| パラメータも保存する | 将来の検証や移行に備えるため |
ここまでで、ハッシュ関数の基本的な役割、使いどころ、限界、そしてパスワード保存での注意点まで見てきました。
次の章では、代表的なハッシュ関数である MD5、SHA-1、SHA-2、SHA-3 の位置づけを整理します。
「昔よく使われた方式」と「現在のセキュリティ用途で使うべき方式」を分けて見ることで、ハッシュ関数の選び方がより分かりやすくなります。
8. MD5・SHA-1・SHA-2・SHA-3の位置づけ
この章では、代表的なハッシュ関数の名前を整理します。
ここまでの記事では主にSHA-256を使って説明してきましたが、実際に調べていると、MD5、SHA-1、SHA-2、SHA-3など、似た名前がたくさん出てきます。
前の章では、パスワード保存では普通のSHA-256をそのまま使うのではなく、Argon2id、bcrypt、PBKDF2のようなパスワード保存向け方式を使うべきだと整理しました。
ここまでで、ハッシュ関数は「何に使うか」によって選び方が変わることが見えてきました。
そこでこの章では、よく見かけるハッシュ関数の名前を、現在の記事でどう扱えばよいかという視点で整理します。
ハッシュ関数の名前は、少し歴史の話にも見えます。
しかし、この記事では年表を暗記することを目的にしません。
大切なのは、次のように考えることです。
そのハッシュ関数は、今のセキュリティ用途で使ってよいものなのか。
それとも、古い資料や既存システムを読むために知っておくべきものなのか。
8.1 まずは全体像を見る
代表的なハッシュ関数の位置づけを、かなりざっくり整理すると次のようになります。
| 名前 | 出力サイズの例 | 現在の記事での扱い | ひとことで言うと |
|---|---|---|---|
| MD5 | 128ビット | セキュリティ用途では基本的に避ける | 古い資料やチェックサムで見かけることがある |
| SHA-1 | 160ビット | 移行対象として扱う | 以前は広く使われたが、現在は使い続けない方向 |
| SHA-2 | 256ビット、384ビット、512ビットなど | 現在よく使われる標準的な系列 | SHA-256などを含む代表的な選択肢 |
| SHA-3 | 256ビット、512ビットなど | SHA-2とは別構造の標準 | SHAKEなどの発展的な関数も含む系列 |
図にすると、イメージとしては次のように整理できます。
ここで注意したいのは、「新しい名前ほど、どんな場面でも自動的に正解」というわけではないことです。
実務では、使うプロトコル、ライブラリ、標準、互換性、運用ルールに合わせて選ぶ必要があります。
ただし、初学者向けの記事としては、次の理解で大きく外れません。
- MD5とSHA-1は、現在のセキュリティ用途では避ける方向で考える
- SHA-256などのSHA-2系列は、現在もよく使われる代表例として理解する
- SHA-3は、SHA-2とは別の構造を持つ標準として理解する
- パスワード保存は、SHA-256やSHA-3をそのまま使うのではなく、専用の方式を使う
NIST FIPS 180-4では、SHA-1、SHA-224、SHA-256、SHA-384、SHA-512、SHA-512/224、SHA-512/256などがSecure Hash Standardとして扱われています。
ただし、NISTはFIPS 180-4を改訂し、SHA-1仕様を削除する方針を示しています。
参考: NIST FIPS 180-4 - Secure Hash Standard
参考: NIST - Decision to Revise FIPS 180-4, Secure Hash Standard
8.2 MD5:古い資料では見かけるが、衝突耐性が必要な用途では避ける
MD5は、昔から広く知られているハッシュ関数です。
出力は128ビットで、16進数で表すと32文字になります。
古い技術記事や古いシステム、ファイルのチェックサムなどで、MD5という名前を見ることがあります。
文字数も短いため、見た目としては扱いやすく感じるかもしれません。
しかし、現在のセキュリティ用途では注意が必要です。
RFC 6151では、MD5に対する既知の攻撃を踏まえ、衝突耐性が必要な場面でMD5を使うのは賢明ではないと説明されています。
また、デジタル署名のように衝突耐性が重要な用途では、MD5はもはや受け入れられないとされています。
参考: RFC 6151 - Updated Security Considerations for the MD5 Message-Digest and the HMAC-MD5 Algorithms
ここでいう衝突とは、異なる2つの入力から同じハッシュ値が出てしまうことです。
もちろん、どんなハッシュ関数でも理論上は衝突の可能性があります。
ただし、暗号学的ハッシュ関数では、その衝突を現実的に見つけにくいことが重要です。
MD5は、この衝突耐性の面で現在のセキュリティ用途には向きません。
そのため、新しく何かを設計する場合に、改ざん検知、署名、証明書、パスワード保存などでMD5を選ぶべきではありません。
💡 豆知識
MD5は今でも古い資料やファイル確認用の文字列として見かけることがあります。
ただし、「見かけることがある」と「安全な用途に使ってよい」は別です。
セキュリティ目的で使うなら、現在の推奨を確認する必要があります。
8.3 SHA-1:以前は広く使われたが、現在は移行対象
SHA-1も、過去にはとても広く使われていたハッシュ関数です。
出力は160ビットで、16進数で表すと40文字になります。
Gitの古いオブジェクトIDや、過去の証明書、古いシステムの仕様などでSHA-1という名前を見たことがある人もいるかもしれません。
しかし、SHA-1も現在では移行対象として扱うべき方式です。
NISTは、SHA-1を暗号保護用途から段階的に移行する計画を示しており、2030年12月31日までにSHA-1の利用から移行する方針を示しています。
また、NISTのニュース記事では、SHA-1に依存している場合は、SHA-2またはSHA-3へ移行することが推奨されています。
参考: NIST CSRC - Transitioning Away from SHA-1 for All Applications
参考: NIST - NIST Retires SHA-1 Cryptographic Algorithm
SHA-1については、次のように理解するとよいです。
| 観点 | 整理 |
|---|---|
| 学習上の位置づけ | 過去に広く使われた代表的なハッシュ関数 |
| 現在の新規利用 | セキュリティ用途では避ける |
| 既存システムで見かけた場合 | 移行対象かどうか確認する |
| 記事での扱い | 「歴史的に重要だが、現在の推奨対象ではない」と説明する |
ここで大切なのは、古い方式を見かけたときに、すぐに「全部危険」とだけ断定するのではなく、何の用途で使われているか を確認することです。
たとえば、互換性のために古い識別子として残っている場合と、デジタル署名や証明書の安全性に直接関わる場合では、リスクの意味が変わります。
ただし、新しくセキュリティ用途で選ぶ方式としては、SHA-1ではなくSHA-2やSHA-3を考えるのが基本です。
8.4 SHA-2:SHA-256を含む、現在よく使われる系列
SHA-2は、現在よく使われるハッシュ関数の系列です。
この記事で何度も登場した SHA-256 も、このSHA-2系列に含まれます。
SHA-2系列には、主に次のような種類があります。
| 名前 | 出力サイズ | よく見る場面の例 |
|---|---|---|
| SHA-224 | 224ビット | 特定の仕様で使われることがある |
| SHA-256 | 256ビット | ファイル検証、署名、ブロックチェーン、HMACなど |
| SHA-384 | 384ビット | TLSや署名関連の仕様で見かけることがある |
| SHA-512 | 512ビット | 64ビット環境での利用や高い出力長が必要な場面 |
| SHA-512/224 | 224ビット | SHA-512系から短い出力を得る方式 |
| SHA-512/256 | 256ビット | SHA-512系から256ビット出力を得る方式 |
NIST FIPS 180-4は、これらのハッシュアルゴリズムでメッセージダイジェストを生成し、そのダイジェストをメッセージが変更されたかどうかの検出に使えると説明しています。
参考: NIST FIPS 180-4 - Secure Hash Standard
初学者向けには、まずSHA-256を代表例として押さえるのが分かりやすいです。
SHA-256は名前の通り、256ビットのハッシュ値を出力します。
16進数で表すと64文字になります。
import hashlib
# 例としてハッシュ化したいメッセージを用意する
message = "Hello, hash!"
# 文字列をバイト列に変換し、SHA-256でハッシュ値を計算する
digest = hashlib.sha256(message.encode("utf-8")).hexdigest()
# SHA-256のハッシュ値は16進数表記で64文字になる
print(digest)
print(len(digest))
実行すると、SHA-256のハッシュ値が64文字の16進数として表示されます。
この「固定長の値になる」という性質が、ファイル検証やデータ識別で使いやすい理由の一つです。
ただし、SHA-256が便利だからといって、すべての場面でそのまま使えばよいわけではありません。
前章で説明したように、パスワード保存には専用の方式を使います。
また、メッセージの送信者確認にはHMACやデジタル署名と組み合わせる必要があります。
8.5 SHA-3:SHA-2とは別構造を持つ標準
SHA-3は、NIST FIPS 202で標準化されているハッシュ関数の系列です。
SHA-2と名前は似ていますが、内部構造は異なります。
FIPS 202では、SHA-3ファミリとして次の関数が定義されています。
| 種類 | 例 | 説明 |
|---|---|---|
| 固定長ハッシュ関数 | SHA3-224、SHA3-256、SHA3-384、SHA3-512 | 指定された長さのハッシュ値を出力する |
| XOF | SHAKE128、SHAKE256 | 必要な長さの出力を得られる拡張出力関数 |
NIST FIPS 202では、SHA-3ファミリはKECCAKを基にしており、SHA3-224、SHA3-256、SHA3-384、SHA3-512と、SHAKE128、SHAKE256が定義されています。
また、SHA-3はFIPS 180-4で定められているSHA-1およびSHA-2ファミリを補完する標準として説明されています。
参考: NIST FIPS 202 - SHA-3 Standard
ここで少し分かりにくいのが、SHA-3は「SHA-2が壊れたから急いで置き換えるもの」という単純な関係ではないことです。
SHA-3は、SHA-2とは異なる設計思想を持つ標準として見る方が自然です。
💡 豆知識
SHA-3の元になったKECCAKは、NISTのSHA-3暗号学的ハッシュアルゴリズムコンペティションで選ばれました。
SHA-2と似た名前ですが、内部の考え方は同じではありません。
初学者の段階では、「SHA-2とは別構造の標準」と押さえておくと十分です。
また、NISTは2025年に、FIPS 202を更新し、SHA-3派生関数を扱うSP 800-185を改訂する方針を示しています。
このように、ハッシュ関数の標準は一度決まったら終わりではなく、必要に応じて更新されます。
参考: NIST - SHA-3: NIST to Update FIPS 202 and Revise SP 800-185
8.6 Pythonで出力サイズの違いを見てみる
ここまで、MD5、SHA-1、SHA-2、SHA-3の位置づけを整理しました。
最後に、Pythonの hashlib を使って、出力されるハッシュ値の長さの違いを見てみます。
注意: このコードは、各方式の出力サイズを比較するための学習用です。
MD5やSHA-1の利用を推奨するものではありません。
import hashlib
# ハッシュ化するサンプルメッセージを用意する
message = "Hello, hash!"
# 比較したいハッシュ関数名を並べる
# MD5とSHA-1は、出力サイズ確認のために含めているだけで、利用推奨ではない
algorithms = ["md5", "sha1", "sha256", "sha3_256"]
for name in algorithms:
# hashlib.new() を使うと、文字列で指定したアルゴリズムを呼び出せる
h = hashlib.new(name)
# ハッシュ関数にはバイト列を渡す必要があるため、UTF-8でエンコードする
h.update(message.encode("utf-8"))
# hexdigest() で16進数表記のハッシュ値を取得する
digest = h.hexdigest()
# 16進数1文字は4ビットなので、文字数×4でビット長を確認できる
bit_length = len(digest) * 4
print(f"{name:8} {bit_length:3} bit {digest}")
出力例は、環境によって利用可能なアルゴリズムに差が出る場合がありますが、イメージとしては次のようになります。
md5 128 bit 0f31f5a1c3...
sha1 160 bit 90c98b1f1d...
sha256 256 bit 5f0d...
sha3_256 256 bit 0a6e...
ここで見てほしいのは、ハッシュ関数によって出力サイズが異なることです。
ただし、出力サイズだけで安全性を判断できるわけではありません。
どのような攻撃が知られているか、標準でどう扱われているか、どの用途で使うのかを合わせて考える必要があります。
Pythonの hashlib は、SHA-256やSHA-3などのハッシュアルゴリズムを扱うための標準ライブラリです。
実際に使用する場合は、利用しているPythonのバージョンや環境でサポートされているアルゴリズムを確認してください。
参考: Python Documentation - hashlib
8.7 どれを使えばよいのか
最後に、初学者が迷いやすい「結局どれを使えばよいのか」を整理します。
| やりたいこと | 考え方 | 例 |
|---|---|---|
| 学習用にハッシュ関数を試したい | SHA-256から始めると分かりやすい | hashlib.sha256() |
| ファイルの改ざん確認をしたい | 配布元が提供する強いハッシュ値と比較する | SHA-256、SHA-512など |
| APIメッセージの改ざんと送信元を確認したい | 鍵付きの仕組みを使う | HMAC-SHA256など |
| デジタル署名で使いたい | 署名方式や標準に従う | 仕様で指定されたハッシュ関数 |
| パスワードを保存したい | 通常のSHA-256をそのまま使わない | Argon2id、bcrypt、PBKDF2など |
| 古いシステムでMD5やSHA-1を見た | 用途を確認し、移行対象か検討する | 既存システムの棚卸し |
この記事の範囲では、次のようにまとめられます。
ハッシュ関数は、名前だけを覚えるよりも、用途ごとに整理した方が理解しやすいです。
MD5やSHA-1は、古い資料を読むためには知っておく価値があります。
一方で、新しくセキュリティ用途で設計するときには、現在の標準や推奨に沿った方式を選ぶ必要があります。
8.8 この章のまとめ
この章では、代表的なハッシュ関数の位置づけを整理しました。
MD5は、古い資料やチェックサムで見かけることがありますが、衝突耐性が必要なセキュリティ用途では避けるべきです。
SHA-1も過去には広く使われましたが、NISTは2030年末までに暗号保護用途から移行する計画を示しています。
一方、SHA-256を含むSHA-2系列は、現在よく使われる代表的なハッシュ関数です。
SHA-3系列は、SHA-2とは別構造を持つ標準であり、SHA3-256やSHAKE128、SHAKE256などを含みます。
ただし、どの方式を使うかは、単に名前の新しさだけでは決められません。
ファイル検証、HMAC、デジタル署名、パスワード保存など、用途に応じて適切な方式を選ぶことが重要です。
次の章では、ここまでの内容を踏まえて、ハッシュ関数について初学者が特に混同しやすいポイントを整理します。
9. よくある誤解
この章では、ハッシュ関数について初学者が混同しやすいポイントを整理します。
ここまでの章で扱った内容を、最後に「誤解しやすい言い方」と「より正確な理解」に分けて振り返ります。
前の章では、MD5、SHA-1、SHA-2、SHA-3の位置づけを整理しました。
ハッシュ関数にはいくつかの種類があり、用途や時代によって「使ってよい場面」「避けるべき場面」が変わることも見てきました。
ここまで読んできた内容を踏まえると、ハッシュ関数はとても便利な技術です。
しかし、便利な技術ほど、少し雑な理解のまま使うと危険な設計につながることがあります。
この章では、ハッシュ関数を学ぶときに特に誤解しやすいポイントを、Q&Aのような形で整理します。
9.1 「ハッシュ化」は「暗号化」と同じ?
まず一番よくある誤解が、ハッシュ化と暗号化を同じものとして扱ってしまうことです。
日常会話では、「パスワードを暗号化する」「ハッシュ化して隠す」のように、まとめて使われることがあります。
しかし、技術的には目的が違います。
暗号化は、正しい鍵を持つ人が元のデータに戻せることを前提にしています。
一方、ハッシュ関数は、基本的に元へ戻すことを目的にしていません。
| 比較 | 暗号化 | ハッシュ化 |
|---|---|---|
| 主な目的 | 中身を読まれないようにする | データの指紋を作る |
| 元に戻す処理 | 鍵があれば復号できる | 基本的に元へ戻さない |
| 鍵 | 通常は鍵を使う | 通常のハッシュ関数は鍵を使わない |
| 代表例 | AES、ChaCha20 | SHA-256、SHA-3 |
| 使う場面 | 通信内容や保存データの保護 | 改ざん検知、照合、識別 |
NISTの用語集では、ハッシュ関数は任意長のビット列を固定長のビット列に対応させる関数として説明されています。
また、暗号学的ハッシュ関数では、一方向性や衝突困難性などが重要な性質として扱われます。
参考: NIST CSRC Glossary - Hash function
この違いを短く表すなら、次のようになります。
つまり、ハッシュ化は「読めないように隠す処理」というより、同じデータかどうかを確認するための目印を作る処理と考える方が自然です。
💡 豆知識
「ハッシュ化されたパスワードを復号する」という表現を見かけることがあります。
しかし、ハッシュは基本的に復号するものではありません。
実際の確認では、入力されたパスワード候補を同じ方法で処理し、保存されている値と比べます。
9.2 ハッシュ値が一致すれば「絶対に」同じデータ?
ファイル検証では、公開されているハッシュ値と、自分の手元で計算したハッシュ値を比べることがあります。
このとき、ハッシュ値が一致すれば、実用上は「同じ内容である可能性が非常に高い」と判断できます。
ただし、厳密には 異なるデータが同じハッシュ値になる可能性はゼロではありません。
これを 衝突 と呼びます。
ハッシュ関数は、どれだけ大きなデータを入力しても、出力は固定長です。
たとえばSHA-256なら、出力は256ビットの値です。
入力の種類は無数にある一方で、出力の種類は有限なので、理論上はどこかで同じハッシュ値になる組み合わせが存在します。
大切なのは、暗号学的ハッシュ関数では、そのような衝突を現実的な時間で見つけにくいように設計されている、という点です。
NISTの用語集でも、暗号学的ハッシュ関数には衝突困難性や原像計算困難性が期待される性質として整理されています。
参考: NIST CSRC Glossary - Cryptographic hash function
| 表現 | 記事でのおすすめ表現 |
|---|---|
| ハッシュ値が同じなら絶対に同じファイル | 実用上、同じファイルである可能性が非常に高い |
| 衝突は絶対に起きない | 理論上は起き得るが、現実的に見つけにくいように設計されている |
| SHA-256なら何をしても安全 | 用途、実装、鍵管理、比較方法なども重要 |
記事では、「絶対に同じ」「絶対に安全」という表現は避けた方がよいです。
情報セキュリティでは、現実的な攻撃コストや用途を踏まえて安全性を考える必要があります。
9.3 SHA-256ならパスワード保存にそのまま使ってよい?
これも非常に大事な誤解です。
SHA-256は広く使われる暗号学的ハッシュ関数ですが、パスワード保存にそのまま使えばよい、という意味ではありません。
通常のSHA-256は高速に計算できます。
これはファイル検証やデータ識別では便利ですが、パスワード保存では問題になる場合があります。
なぜなら、データベースが漏えいした場合、攻撃者は手元の環境で大量のパスワード候補を試せるからです。
高速なハッシュ関数だけで保存していると、攻撃者も高速に推測を試せてしまいます。
OWASP Password Storage Cheat Sheetでは、パスワードは平文で保存せず、Argon2id、bcrypt、PBKDF2のような強く遅い方式で保護することが推奨されています。
また、SHA-256のような高速なハッシュ関数は、攻撃者が大量の推測を素早く試せるため、パスワード保存には適さないと説明されています。
参考: OWASP Password Storage Cheat Sheet
ここで大切なのは、「ハッシュ化しているかどうか」だけではなく、パスワード保存に適した方式を使っているかです。
💡 豆知識
パスワード保存では、速いことが必ずしも良いとは限りません。
普段の処理では少し遅くても、攻撃者が大量に試すときのコストを上げられる方が重要です。
9.4 ハッシュ値だけで「誰が作ったか」も分かる?
普通のハッシュ関数だけでは、そのハッシュ値を誰が作ったのかまでは分かりません。
たとえば、あるファイルのSHA-256ハッシュ値が公開されていたとします。
自分の手元で計算した値と一致すれば、ファイルの内容が同じである可能性は高いです。
しかし、その公開されているハッシュ値自体が攻撃者によって差し替えられていたらどうでしょうか。
ファイルとハッシュ値の両方を攻撃者が用意している場合、単に値が一致するだけでは安心できません。
このような「誰が作ったのか」「信頼できる相手から来たのか」を確認したい場合は、HMACやデジタル署名が関係します。
| 技術 | 何を確認できるか | 向いている場面 |
|---|---|---|
| 通常のハッシュ関数 | データが変わったかの確認材料 | ファイル検証、識別 |
| HMAC | 同じ秘密鍵を持つ相手が作ったか | API通信、サーバー間通信 |
| デジタル署名 | 秘密鍵の持ち主が署名したか | ソフトウェア配布、証明書、ブロックチェーン |
NIST FIPS 198-1では、HMACは暗号学的ハッシュ関数と共有秘密鍵を組み合わせて使うメッセージ認証の仕組みとして説明されています。
参考: NIST FIPS 198-1 - The Keyed-Hash Message Authentication Code
また、NIST FIPS 186-5では、デジタル署名はデータの不正な変更の検出や署名者の認証に使われると説明されています。
参考: NIST FIPS 186-5 - Digital Signature Standard
9.5 公開ハッシュ値があれば、配布ファイルは完全に安全?
公開ハッシュ値は、ファイル検証の重要な材料になります。
しかし、それだけで配布ファイルの安全性が完全に保証されるわけではありません。
たとえば、次のような場合を考えます。
- 攻撃者が配布ファイルを差し替える
- 同じページに掲載されているハッシュ値も差し替える
- 利用者は差し替え後のファイルと差し替え後のハッシュ値を比較する
- ハッシュ値は一致してしまう
この場合、利用者の手元では「一致している」と見えてしまいます。
しかし、それはあくまで 攻撃者が用意したファイルと攻撃者が用意したハッシュ値が一致している だけです。
そのため、実際には次のような点も重要になります。
| 確認したいこと | 関係するもの |
|---|---|
| ファイルが途中で壊れていないか | ハッシュ値 |
| 掲載元のページに安全につながっているか | TLS、証明書 |
| 正規の配布者が作ったものか | デジタル署名、署名検証 |
| 公開鍵や署名が本物か | 信頼できる配布経路、証明書、鍵管理 |
ハッシュ値は便利ですが、ハッシュ値をどこから取得したのか も同じくらい重要です。
9.6 MD5やSHA-1は、どんな場面でも絶対に使ってはいけない?
前章で、MD5やSHA-1は現在のセキュリティ用途では避けるべきだと説明しました。
ただし、ここでも少し丁寧に整理する必要があります。
MD5について、RFC 6151では、衝突耐性が必要な場面でMD5を使うのは賢明ではないと説明されています。
一方で、単なるエラー検出のチェックサムなど、セキュリティ性を期待しない用途では、文脈を分けて考える必要があります。
参考: RFC 6151 - Updated Security Considerations for MD5
SHA-1については、NISTが暗号保護用途から2030年12月31日までに移行する方針を示しています。
参考: NIST - Transitioning Away from SHA-1 for All Applications
そのため、記事では次のように整理すると誤解が少なくなります。
| 用途 | MD5・SHA-1の扱い |
|---|---|
| デジタル署名 | 避ける |
| 証明書 | 避ける |
| 改ざん耐性を期待するファイル検証 | 避ける |
| パスワード保存 | そもそも通常ハッシュ単体を使わない |
| 古いシステムの識別子・互換性 | 使われている場合はあるが、セキュリティ用途とは分けて考える |
「古い方式を見かけたら必ず即危険」と決めつけるよりも、どの用途で、どの安全性を期待しているのか を確認することが大切です。
9.7 ハッシュ関数は自作してもよい?
学習目的で簡単なハッシュ風の処理を作ってみることは、仕組みを理解するうえで役立つ場合があります。
しかし、実際のセキュリティ用途でハッシュ関数を自作するのは避けるべきです。
暗号学的ハッシュ関数には、原像計算困難性、第2原像計算困難性、衝突困難性などが求められます。
これらは、見た目としてランダムな値が出ていれば満たされる、というものではありません。
| 自作ハッシュで起こりやすい問題 | なぜ危険か |
|---|---|
| 出力に偏りがある | 衝突や推測が起きやすくなる |
| 入力の一部しか反映していない | 改ざんに気づけない可能性がある |
| 出力長が短すぎる | 衝突を見つけやすくなる |
| 十分な解析を受けていない | 既知の攻撃に弱い可能性がある |
| 用途に合わない | パスワード保存や認証に誤用される可能性がある |
実務では、標準化された方式や、利用する言語・フレームワークの信頼できるライブラリを使うことが基本です。
Pythonであれば、SHA-256などのハッシュ関数は標準ライブラリ hashlib から利用できます。
参考: Python Documentation - hashlib
💡 豆知識
暗号技術では、「自分だけの秘密の方式」は安全性の根拠になりません。
むしろ、多くの研究者や実務者に検証されてきた標準方式を、正しい用途で使うことが重要です。
9.8 この章のまとめ
ここまでの誤解をまとめると、次のようになります。
| 誤解 | より正確な理解 |
|---|---|
| ハッシュ化は暗号化と同じ | ハッシュ化は基本的に元へ戻すことを目的にしない |
| ハッシュ値が一致すれば絶対に同じ | 実用上は強い確認材料になるが、理論上は衝突の可能性がある |
| SHA-256ならパスワード保存にそのまま使える | パスワード保存にはArgon2id、bcrypt、PBKDF2などを使う |
| ハッシュ値だけで送信者も分かる | 送信者確認にはHMACやデジタル署名が必要 |
| 公開ハッシュ値があれば完全に安全 | ハッシュ値の取得元や署名検証も重要 |
| MD5やSHA-1はどんな場面でも同じ扱い | セキュリティ用途では避けるが、文脈を確認する |
| 自作ハッシュ関数でも十分 | 実用では標準方式と信頼できるライブラリを使う |
ハッシュ関数は、情報セキュリティの中でとても重要な道具です。
ただし、「ハッシュ化しているから安全」とだけ考えると、守りたいものを正しく守れない場合があります。
大切なのは、何を守りたいのか を先に考えることです。
データが変わっていないかを確認したいのか。
中身を読まれないようにしたいのか。
誰が作ったのかを確認したいのか。
パスワード漏えい時の被害を抑えたいのか。
目的が変われば、使うべき技術も変わります。
次の章では、ここまでの内容をまとめ、ハッシュ関数をどのように理解しておくとよいかを整理します。
10. まとめ
最後に、本記事で扱った内容を振り返ります。
ハッシュ関数は、情報セキュリティの中では地味に見えるかもしれませんが、改ざん検知、認証、署名、パスワード保存、Git、ブロックチェーンなど、多くの仕組みの土台になっています。
前の章では、ハッシュ関数についてよくある誤解を整理しました。
「ハッシュ化は暗号化と同じ」「SHA-256ならパスワード保存にそのまま使える」「ハッシュ値があれば送信者も分かる」といった考え方は、初学者のうちは特に混同しやすいポイントです。
ここまでの内容を踏まえると、ハッシュ関数は次のように整理できます。
ハッシュ関数は、データを隠すための技術ではなく、データの指紋を作る技術です。
その指紋を使って、データが変わっていないかを確認したり、他の暗号技術と組み合わせて安全な仕組みを作ったりします。
10.1 この記事で押さえたこと
本記事では、ハッシュ関数をいきなり数式や内部アルゴリズムから説明するのではなく、身近な例から順番に整理しました。
| 見てきた内容 | この記事でのポイント |
|---|---|
| ハッシュ関数のイメージ | データから「指紋」のような固定長の値を作る |
| 暗号化との違い | 暗号化は元に戻すことを前提にするが、ハッシュ化は基本的に戻さない |
| 期待される性質 | 元データを推測しにくい、同じハッシュ値になる別データを作りにくい |
| SHA-256の確認 | 入力が少し変わるだけで、ハッシュ値が大きく変わる |
| 使いどころ | ファイル検証、HMAC、署名、Git、ブロックチェーン、監査ログなど |
| 限界 | ハッシュ関数だけでは、送信者確認や中身の秘匿はできない |
| パスワード保存 | 通常の高速なハッシュ関数をそのまま使うのではなく、専用の方式を使う |
| 代表的な方式 | MD5やSHA-1は現在のセキュリティ用途では注意し、SHA-2やSHA-3などを理解する |
この表からも分かるように、ハッシュ関数は単体で完結する技術というより、さまざまな仕組みの中で使われる部品です。
たとえば、ファイル検証では「手元のファイルが配布元のものと同じか」を確認する材料になります。
HMACでは、共有秘密鍵と組み合わせることで「同じ鍵を持つ相手が作ったメッセージか」を確認できます。
デジタル署名では、署名対象データを扱いやすい形にする前処理として関係します。
ブロックチェーンや監査ログでは、データ同士をハッシュ値でつなげることで、途中の改ざんに気づきやすくなります。
このように見ると、ハッシュ関数は「長い英数字を作るだけの処理」ではなく、情報セキュリティの多くの場面で使われる基本部品だと分かります。
10.2 ハッシュ関数を使うときは「何を守りたいか」から考える
ハッシュ関数を学ぶときに大切なのは、アルゴリズム名だけを覚えることではありません。
もちろん、SHA-256、SHA-3、MD5、SHA-1といった名前を知ることは大切です。
しかし、実際に設計や実装で重要になるのは、何を守りたいのか を先に考えることです。
| 守りたいこと | ハッシュ関数だけで足りるか | 必要になる技術の例 |
|---|---|---|
| ファイルが変わっていないか確認したい | 場合によっては確認材料になる | SHA-256など |
| 誰が作ったメッセージか確認したい | ハッシュ関数だけでは足りない | HMAC、デジタル署名 |
| 通信内容を読まれないようにしたい | ハッシュ関数では目的が違う | 暗号化、TLS、認証付き暗号 |
| パスワード漏えい時の被害を抑えたい | 普通の高速ハッシュだけでは不十分 | Argon2id、bcrypt、PBKDF2など |
| 履歴の途中改ざんに気づきたい | ハッシュチェーンとして使える | ハッシュチェーン、監査ログ設計 |
このように、ハッシュ関数は便利ですが、目的によっては他の技術と組み合わせる必要があります。
特に注意したいのは、次の3点です。
1つ目は、ハッシュ関数は中身を隠す技術ではないということです。
中身を読まれないようにしたい場合は、暗号化を考える必要があります。
2つ目は、ハッシュ関数だけでは送信者を確認できないということです。
誰が作ったものかを確認したい場合は、HMACやデジタル署名を使います。
3つ目は、パスワード保存では普通のSHA-256をそのまま使わないということです。
パスワード保存では、攻撃者が大量に推測を試せることを前提に、計算コストやメモリコストを調整できる方式を使う必要があります。
10.3 「ハッシュ化しているから安全」で止まらない
ハッシュ関数を学ぶとき、最初は「ハッシュ化していれば安全」と考えてしまいがちです。
しかし、この記事で見てきたように、ハッシュ化という言葉だけでは十分ではありません。
たとえば、次のような確認が必要です。
| 確認したいこと | 見るべきポイント |
|---|---|
| どのハッシュ関数を使っているか | MD5やSHA-1をセキュリティ用途で使っていないか |
| 何のために使っているか | 改ざん検知なのか、認証なのか、パスワード保存なのか |
| 鍵が必要な用途ではないか | HMACやデジタル署名が必要ではないか |
| パスワード保存ではないか | Argon2id、bcrypt、PBKDF2などを使っているか |
| 比較処理は安全か | タイミング差などを考慮した比較関数を使っているか |
| 公開ハッシュ値を信頼してよいか | ハッシュ値の取得元や署名検証も確認しているか |
ここで大切なのは、ハッシュ関数そのものを怖がることではありません。
むしろ、ハッシュ関数は非常に重要で便利な技術です。
ただし、使いどころを間違えると、守りたいものを守れない可能性があります。
そのため、「どの方式が強いか」だけでなく、「どの目的で、どのように使っているか」を見ることが重要です。
10.4 今後深掘りしたいテーマ
本記事では、ハッシュ関数の全体像をつかむことを優先しました。
そのため、内部構造や数学的な詳細、本番サービス向けの実装までは深く扱っていません。
今後、さらに理解を深めるなら、次のようなテーマへ進むとよいと思います。
| 深掘りテーマ | 学べること |
|---|---|
| SHA-256の内部構造 | メッセージをどのように処理してハッシュ値を作るのか |
| SHA-2とSHA-3の違い | Merkle-Damgård構造とスポンジ構造の考え方 |
| HMACの仕組み | 共有秘密鍵とハッシュ関数を組み合わせる理由 |
| デジタル署名 | ハッシュ関数、秘密鍵、公開鍵の関係 |
| パスワード保存 | ソルト、ペッパー、計算コスト、メモリコスト |
| ハッシュチェーン | 監査ログやブロックチェーンでの改ざん検知 |
| 耐量子暗号とハッシュベース署名 | SLH-DSAなど、ハッシュ関数を使う署名方式 |
特に、セキュリティを実務や研究で扱う場合は、ハッシュ関数を単体で学ぶだけでなく、HMAC、デジタル署名、鍵管理、パスワード保存、監査ログとつなげて理解することが大切です。
派生元の記事で整理した暗号技術の全体像に戻ると、ハッシュ関数は「完全性」や「改ざん検知」を支える道具として位置づけられます。
そして、他の暗号技術と組み合わせることで、実際のサービスの安全性を支える仕組みになります。
10.5 最後に
ハッシュ関数は、最初は「よく分からない長い英数字を作るもの」に見えるかもしれません。
しかし、少しずつ整理していくと、ファイル検証、Webサービス、Git、ブロックチェーン、パスワード保存、監査ログなど、さまざまな場面に関係していることが分かります。
本記事で特に覚えておきたいことは、次の3つです。
- ハッシュ関数は、データの指紋を作る技術である
- 暗号化のように元へ戻すことを目的にしない
- 目的に応じて、HMAC、デジタル署名、パスワード保存向け方式などと使い分ける必要がある
ハッシュ関数を理解すると、暗号技術全体の見通しが少しよくなります。
「暗号化」「署名」「認証」「パスワード保存」などの言葉が出てきたときにも、どの部分でハッシュ関数が関係しているのかを考えられるようになります。
この記事が、情報セキュリティを学び始めるうえで、ハッシュ関数を身近に感じるきっかけになればうれしいです。