【実務・中級編】 暗号学的ハッシュ関数とHMACの正しい使い分け – 暗号理論・認証基盤 & エンドポイントセキュリティ防御ガイド

ハッシュ関数を「認証」に使ってはいけない:HMACで守るべきデータの整合性

現場でコードレビューをしていると、未だに「改ざん検知」の文脈で、ただのハッシュ関数をそのまま使っている実装に出くわす。例えば、APIの署名検証やセッションデータの保護で、SHA256(secret + data) といった実装だ。

一見すると「秘密の鍵を混ぜているから大丈夫だろう」と思えるかもしれない。だが、これはプロの目から見れば「鍵を開けっ放しにして、泥棒に『入らないでください』と張り紙をしている」のと同じくらい無防備な状態だ。なぜこれが危険なのか、そして我々エンジニアがどう実装すべきか、現場の視点で解説しよう。

—

なぜ「単なるハッシュ」ではダメなのか?(長さ拡張攻撃の恐怖)

単純な連結 hash(secret + data) が危険な最大の理由は、「長さ拡張攻撃(Length Extension Attack)」という手法が存在するからだ。

SHA-256などのMD5系・SHA-2系ハッシュ関数は、データをブロック単位で処理する「マークル・ダンプガード構成」を採用している。この構造の弱点は、ハッシュ値さえ分かっていれば、元のメッセージの内容を知らなくても、元のハッシュ値から「メッセージが追記された新しいハッシュ値」を計算できてしまう点にある。

攻撃者は、あなたが発行した hash(secret + data) を傍受し、そのハッシュ値を初期状態(内部状態)として再利用することで、あなたの知らない間にデータを改ざんし、かつ「正しい署名」が付与された不正なリクエストを捏造できる。

これを防ぐための標準的な解法が HMAC (Hash-based Message Authentication Code) だ。HMACは二重のハッシュ処理(内部ハッシュと外部ハッシュを分ける構造)を行うことで、この数学的な脆弱性を完全に封じ込めている。

—

【実践】HMACを用いたセキュアな実装

「自分でハッシュ関数を組み合わせる」という発想は今すぐ捨ててほしい。セキュリティの世界では「車輪の再発明」は「脆弱性の発明」と同義だ。言語が提供する標準ライブラリの HMAC 関数を使い、正しい手順で実装する。

Pythonによる実装例

Pythonの hmac モジュールを使えば、数行で堅牢な署名検証ができる。

import hmac
import hashlib

# 秘密鍵(環境変数から読み込むこと。絶対にソースコードに直書きしない)
SECRET_KEY = b'super-secret-key-that-is-long-and-random'

def generate_signature(message: str) -> str:
    """メッセージに対するHMAC-SHA256署名を生成する"""
    return hmac.new(SECRET_KEY, message.encode(), hashlib.sha256).hexdigest()

def verify_signature(message: str, signature: str) -> bool:
    """署名を検証する(タイミング攻撃対策のため compare_digest を使用)"""
    expected = generate_signature(message)
    # 比較には必ず compare_digest を使うこと。
    # 通常の == 演算子は比較時間に差が出るため、サイドチャネル攻撃の対象になる。
    return hmac.compare_digest(expected, signature)

# 使用例
msg = "user_id=123&role=user"
sig = generate_signature(msg)
print(f"署名: {sig}")
print(f"検証結果: {verify_signature(msg, sig)}")

Node.js (JavaScript) による実装例

WebバックエンドでNode.jsを使う場合も同様だ。crypto モジュールを活用する。

const crypto = require('crypto');

const SECRET_KEY = process.env.API_SECRET;

function verifyHmac(data, signature) {
    const hmac = crypto.createHmac('sha256', SECRET_KEY);
    hmac.update(data);
    const expectedSignature = hmac.digest('hex');

    // timingSafeEqual を使うことで比較時間を一定に保つ
    return crypto.timingSafeEqual(
        Buffer.from(signature, 'hex'),
        Buffer.from(expectedSignature, 'hex')
    );
}

—

運用上の注意点:泥臭い現場の鉄則

コードが書けても、運用が甘ければインシデントは起きる。以下の3点を必ず守ってほしい。

1. 秘密鍵の管理(絶対厳守):
秘密鍵をGitにコミットするのは論外だが、設定ファイル(.env等)のパーミッション設定も確認せよ。IAMロールやAWS Secrets Manager、HashiCorp Vaultなど、動的に鍵を注入する仕組みを推奨する。
2. 比較演算は「一定時間」で:
サンプルコードで示した通り、== や === で文字列比較をしてはいけない。文字列の先頭から比較を行い、不一致の瞬間に処理を返す実装だと、攻撃者は「何文字目まで合っているか」を応答時間から逆算できる(タイミング攻撃)。必ず compare_digest や timingSafeEqual のような、定数時間で比較を完了させる関数を使うこと。
3. アルゴリズムの選定:
SHA-1はもはや衝突耐性が危うい。最低でも SHA-256 以上、可能なら SHA-384 や SHA-512 を選ぶのが、今後数年間の運用を見据えた賢明な選択だ。

まとめ

「ハッシュ関数」はデータの要約であり、「HMAC」はデータの保証書だ。
システムが受け取ったデータが「誰によって生成され、改ざんされていないか」を証明したいなら、迷わずHMACを選ぶこと。そして、比較処理には必ず「タイミング攻撃耐性」を持たせること。

この二つを守るだけで、あなたのシステムは攻撃者にとって格段に「割に合わない」ターゲットになる。セキュリティは、こうした些細な実装の積み重ねの上にしか成り立たない。さあ、今すぐプロジェクト内の署名検証ロジックをチェックしてほしい。

コメント

タイトルとURLをコピーしました