暗号通信のもう一つの柱は 「データが改ざんされていないこと」を保証する 完全性(integrity)。これを支えるのが ハッシュ関数 と MAC(Message Authentication Code) です。SHA-256 でファイルの fingerprint を取る、HMAC で API リクエストを認証する、bcrypt でパスワードを保管する、git でコミットを識別する ─ 現代のあらゆる場面でハッシュは登場します。本講では構造(Merkle-Damgård と Sponge)、衝突耐性、HMAC・CMAC・Poly1305 の使い分けを踏み込んで学びます。
本講を終えると、以下を達成できるようになります。
$ echo -n "hello" | sha256sum
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 -
$ echo -n "hello" | sha256sum
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 -
# ↑ 何度やっても同じ結果
$ echo -n "Hello" | sha256sum
185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969 -
# ↑ "h" → "H" の 1 ビット差で完全に違う出力
図の見方:入力サイズに関わらず、出力は固定長の 256 bit(SHA-256 の場合)。これを使えば「2 つのファイルが同じか?」は元データを比較せず、ハッシュ 32 バイトを比較するだけで済む。
確認: 暗号学的ハッシュ関数が「一方向(one-way)」であるとは、具体的にどういう性質か?
正解:B。一方向性は 「H から M を逆算できない」(原像計算困難性)を指す。出力は固定長で決定的(A・C)、入力差分が出力に拡散する(D、雪崩効果)、これらは別の重要性質。一方向性が崩れると、パスワードハッシュから元のパスワードが復元できてしまうので、もっとも基本かつ重要な性質。
| 性質 | 難易度(古典コンピュータ) | 用途 |
|---|---|---|
| 原像耐性 | 2^n(鍵長 n bit と同じ) | パスワードハッシュ |
| 第二原像耐性 | 2^n | ファイル整合性 |
| 衝突耐性 | 2^(n/2)(誕生日攻撃) | ディジタル署名 |
衝突耐性が 2^(n/2) なのは 誕生日のパラドックス から来ています。23 人の集まりで誰かと誰かの誕生日が一致する確率は 50% を超える ─ 同じ理由で、2^128 の入力を試せば 2^256 個のハッシュ空間でも衝突が見つかる可能性が高い。だから衝突耐性 128 bit を確保するには、ハッシュ長 256 bit が必要(SHA-256)。
SHA-1(160 bit 出力)は理論上 2^80 で衝突発見可能でしたが、長らく現実的攻撃は不可能とされていました。しかし 2017 年 2 月、Google と CWI Amsterdam が「SHAttered」攻撃 で SHA-1 衝突の実例を発表(同じ SHA-1 を持つ 2 つの異なる PDF ファイルを生成)。この時点で SHA-1 は 署名・証明書用途で禁止。git も SHA-256 への移行を進行中。
例:CA(認証局)が「example.com 用の証明書」に SHA-1 で署名するとき、攻撃者が「example.com 用と 同じハッシュを持つ 別の証明書(attacker.com 用や 偽の中間 CA)を作成可能なら、署名がそのまま偽証明書にも有効に。これが起こると 偽サイトが完全に正規の HTTPS として機能 してしまう。SHA-1 廃止が急がれた理由はここ。
確認: 「衝突耐性」と「2 番目原像耐性」の違いを最も適切に説明しているのはどれか。
正解:C。衝突耐性の方が攻撃者の自由度が高く、誕生日パラドックスの効果でビット長の半分の強度しか持たない(SHA-256 で 128 bit 強度)。2 番目原像耐性は M が固定なので「特定の M に衝突する M' を見つける」必要があり、ほぼ全数探索の困難さ(256 bit 強度)。証明書偽造は前者が崩れると現実化する ─ MD5 でこれが実証されたため廃止された。
入力を固定長ブロック(SHA-256 なら 512 bit)に分割し、各ブロックを順次「圧縮関数」で混ぜていく方式。MD5、SHA-1、SHA-2 がすべてこの構造です。
図の見方:メッセージを 512 bit ブロックに分割。各ブロックを「前の状態 + 現在のブロック → 新しい状態」と圧縮関数 f で処理し、最後の状態をハッシュ値として出力。実装が単純で、長い間標準だった構造。length extension attack(下記)という弱点がある。
2012 年に SHA-3 として採用された Keccak は Sponge(スポンジ) 構造。「吸収(absorb)」と「絞り出し(squeeze)」の 2 段階で動きます。Merkle-Damgård とは設計思想が根本から違う。
SHA-256 の出力 H(M) と M の長さが分かっていれば、攻撃者は M を知らなくても H(M ∥ padding ∥ M') を計算できる。これが MAC に SHA-256 をそのまま使うと危険な理由(対策が HMAC。後述)。SHA-3 は Sponge 構造のため最初からこの問題がない。
確認: SHA-3(Keccak)が「長さ拡張攻撃を構造的に持たない」のはなぜか?
正解:B。Merkle-Damgård(SHA-2)は最終チェーン値をそのままハッシュ値として返すため、攻撃者は H から内部状態を知って計算を継続できる。Sponge は r(rate) bit のみ出力し c(capacity) bit は隠蔽 するので、攻撃者は次の状態を再現できない。これが SHA-3 が そのまま MAC に使える(KMAC として標準化)一方、SHA-2 では HMAC で包む必要がある根本理由。
| 関数 | 出力長 | 構造 | 状態 | 主な用途 |
|---|---|---|---|---|
| MD5 | 128 bit | Merkle-Damgård | 破られている(廃止) | レガシー(チェックサム程度なら可) |
| SHA-1 | 160 bit | Merkle-Damgård | 破られている(2017 SHAttered) | git(移行中)以外は禁止 |
| SHA-256 | 256 bit | Merkle-Damgård | 安全 | 現代の主力(TLS, ブロックチェーン, git の次世代) |
| SHA-384 / SHA-512 | 384/512 bit | Merkle-Damgård | 安全 | 高セキュリティ・64 bit CPU で速い |
| SHA-3 / Keccak | 224〜512 bit | Sponge | 安全 | SHA-2 の代替。長期保険 |
| BLAKE2 | 128〜512 bit | BLAKE2 系 | 安全 | 高速。Argon2 内部、WireGuard が採用 |
| BLAKE3 | 任意 | Merkle ツリー + ChaCha | 安全 | 非常に高速・並列処理可。新しい標準候補 |
2024 年現在、ほとんどの用途で SHA-256 が安全かつ広く実装されており、第一選択です。長期保険を取るなら SHA-3 を併用。速度重視(ファイルチェック、コンテンツアドレス)なら BLAKE3 が有力。MD5・SHA-1 は新規採用しないこと。
| 性質 | MAC | ディジタル署名(第39回) |
|---|---|---|
| 鍵 | 共有秘密鍵 1 つ | 秘密鍵(署名) + 公開鍵(検証) |
| 検証可能な相手 | 鍵を共有している 1 人だけ | 公開鍵を知っている誰でも |
| 否認防止 | 不可(双方が同じ鍵で作れる) | 可能 |
| 速度 | 非常に高速 | 遅い(公開鍵演算) |
| 用途 | API 認証、TLS 内部 | 証明書、コードサイニング |
HMAC は SHA-256 などの普通のハッシュ関数を 「鍵 + メッセージ」を安全に混ぜる手順 で MAC 化したもの。素朴に H(K ∥ message) とするだけだと length extension attack が成立してしまうため、2 段階のハッシュ で防ぎます。
HMAC(K, m) = H( (K' ⊕ opad) ∥ H( (K' ⊕ ipad) ∥ m ) )
ここで:
K' = 鍵 K(短ければゼロパディング、長ければハッシュで縮める)
ipad = 0x36 を繰り返したバイト列
opad = 0x5C を繰り返したバイト列
H = ハッシュ関数(SHA-256 等)
∥ = 連結
つまり「鍵をパディングしてから内側ハッシュ → 外側ハッシュ」を 2 重に通す。
これにより length extension attack が成立しない。
Q. 自前で「ハッシュ関数を使った認証コード」を作るとして、もっとも素直そうな tag = SHA256(secret ∥ message)(秘密鍵を頭にくっつけて SHA-256 をかける)は なぜ安全でないのか?
SHA-256 は Merkle-Damgård 構造なので「長さ拡張攻撃」が成立する。攻撃者は tag と message だけ手に入れば、secret を知らなくても 任意の追加データ M' について SHA256(secret ∥ message ∥ padding ∥ M') を計算できる(SHA-256 の内部状態が tag そのものだから、そこから継続できる)。
つまり「秘密鍵を含むメッセージへの追記」を、秘密鍵を知らない攻撃者が 正しい認証コード付きで偽造可能。これは認証としての要件を完全に破る。
正解は HMAC = H((K ⊕ opad) ∥ H((K ⊕ ipad) ∥ M)) という二重ハッシュ構造。内側のハッシュで内部状態が秘匿され、外側で再ハッシュすることで長さ拡張を構造的に塞いでいる。「ハッシュは MAC ではない」「自前で組み合わせず HMAC を使え」という鉄則の根拠。
大きなソフトを公式サイトからダウンロードしたら、添付の SHA-256 値 と自分で計算した値を突き合わせる。途中でファイルが壊れたり改ざんされていないかを 1 行で検証できる。
$ sha256sum ubuntu-22.04-desktop-amd64.iso
b98dac940a82b110e6265ca78d1320f1f7103861e922247a4cdf6f17 ubuntu...
# 公式の値と比較
サーバはユーザのパスワードを そのまま保存しない。ハッシュだけ保存し、ログイン時に「入力されたパスワードのハッシュ == 保存されているハッシュ」を比較。サーバが侵害されてもパスワードそのものは露出しない。
ただし 素の SHA-256 だけでは不十分。GPU で 1 秒に何十億回もハッシュ計算できるので、辞書攻撃で破られる。対策は 意図的に遅いハッシュ関数:
git の commit a3f4b2... という ID は そのコミットの内容(変更・親コミット・著者・メッセージ)を全部結合したものの SHA-1 ハッシュ。コンテンツが 1 文字でも違えば全く違う ID になる ─ git の整合性の根幹。git は SHA-256 への移行を進行中。
Bitcoin の各ブロックは 「前のブロックのハッシュ + 取引データ + nonce」を SHA-256 した値 を持つ。マイナーは「特定の条件(先頭ゼロが多い)を満たすハッシュ」を見つけるため nonce を試行錯誤 ─ これが Proof of Work。ハッシュの一方向性と衝突耐性が PoW の安全性そのもの。
JWT の HS256 アルゴリズムは HMAC-SHA256。header.payload.signature という 3 部構成で、signature は HMAC-SHA256(secret, header + "." + payload)。受け取り側は同じ secret で再計算して検証 ─ 改ざんなしと送信元の認証を 1 つで実現。
IPFS・Docker イメージ・nix パッケージマネージャは「ファイル名ではなく ハッシュ値 でファイルを識別」する。同じハッシュなら同じ内容、自動的にデデュプリケーション。
つながる知識: ハッシュは「機密性なし、完全性あり」の暗号プリミティブ。鍵を必要とせず誰でも計算できるので「秘密」は守らないが、「同一性」を強力に保証する。これが多様な用途に使える理由です。
確認: Web サービスでユーザのパスワードを保存するとき、なぜ「単純に SHA-256 でハッシュ化」だけでは不十分なのか?
正解:D。汎用ハッシュは「速い」のが長所だが、パスワード保管では 致命的弱点。bcrypt / scrypt / Argon2 のような 計算的に重く・メモリも食う ハッシュ関数(KDF)を、ユーザごとの salt と組み合わせて使うのが現代の正解。Argon2 は 2015 年の Password Hashing Competition の優勝アルゴリズムで、OWASP の推奨。「正しい道具を正しい目的に」の典型例。
# SHA-256
echo -n "hello world" | openssl dgst -sha256
# (stdin)= b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9
# SHA-512
echo -n "hello world" | openssl dgst -sha512
# SHA-3
echo -n "hello world" | openssl dgst -sha3-256
# BLAKE2
echo -n "hello world" | openssl dgst -blake2b512
# ファイル全体のハッシュ
openssl dgst -sha256 myfile.tar.gz
# 利用可能なアルゴリズムを列挙
openssl dgst -list
# HMAC-SHA256(鍵を文字列で指定)
echo -n "important message" | \
openssl dgst -sha256 -hmac "supersecretkey"
# HMAC-SHA256(鍵をバイナリで指定)
KEY_HEX=$(openssl rand -hex 32)
echo "KEY=$KEY_HEX"
echo -n "important message" | \
openssl dgst -sha256 -mac HMAC -macopt "hexkey:$KEY_HEX"
# pip install argon2-cffi
from argon2 import PasswordHasher
ph = PasswordHasher()
# 保存時:ハッシュを生成(salt とパラメータも含む文字列)
hashed = ph.hash("user-password-123")
print(hashed)
# → $argon2id$v=19$m=65536,t=3,p=4$xxx$yyy
# ログイン時:検証
try:
ph.verify(hashed, "user-password-123")
print("OK")
except Exception:
print("Wrong password")
# pip install pyjwt
import jwt
secret = "my-shared-secret"
payload = {"user_id": 42, "exp": 1735689600}
# 生成
token = jwt.encode(payload, secret, algorithm="HS256")
print(token)
# → eyJhbGciOi...
# 検証(secret が違うと InvalidSignatureError)
decoded = jwt.decode(token, secret, algorithms=["HS256"])
print(decoded)
# → {'user_id': 42, 'exp': 1735689600}
| 用語 | 1行説明 |
|---|---|
| ハッシュ関数 / ダイジェスト | 任意長 → 固定長の暗号プリミティブ。fingerprint 用 |
| 原像耐性 | H(x) = h を満たす x を求めるのが困難 |
| 第二原像耐性 | x が与えられた時、H(x) = H(x') の x' を求めるのが困難 |
| 衝突耐性 | 任意の 2 つで H(x) = H(x') の対を見つけるのが困難 |
| 誕生日攻撃 | 衝突探索の難易度が 2^(n/2)(出力 n bit に対し) |
| Merkle-Damgård | SHA-2 までの構造。圧縮関数で順次混ぜる |
| Sponge / Keccak | SHA-3 の構造。吸収と絞り出しの 2 段階 |
| length extension attack | MD 構造の弱点。SHA-2 を素朴に MAC に使うと危険 |
| SHA-256 / SHA-3 | 現代の主力ハッシュ。256 bit 出力、衝突耐性 128 bit |
| BLAKE2 / BLAKE3 | 高速なモダンハッシュ。WireGuard 等で採用 |
| MAC(Message Auth Code) | 共有鍵で改ざん検知 + 認証するタグ |
| HMAC | RFC 2104。ハッシュ関数を 2 段階で MAC 化 |
| CMAC | AES ベースの MAC。NIST SP 800-38B |
| Poly1305 | 高速 MAC。ChaCha20-Poly1305 で使用 |
| bcrypt / scrypt / Argon2 | パスワード保管用の遅いハッシュ |