Upgrade to Pro — share decks privately, control downloads, hide ads and more …

なぜ秘密の比較に hash_equals を使うのか ー内部実装と実践ガイド / why us...

なぜ秘密の比較に hash_equals を使うのか ー内部実装と実践ガイド / why use hash equals for secret comparison internals and practical guide

Avatar for コドモン開発チーム

コドモン開発チーム

February 23, 2026
Tweet

More Decks by コドモン開発チーム

Transcript

  1. 16 Linux man pageの警告 “ Do not use memcmp() to

    compare confidential data, such as cryptographic secrets, because the CPU time required for the comparison depends on the contents of the addresses compared, this function is subject to timing-based side-channel attacks. (機密データ(暗号シークレットなど)の比較にmemcmp()を使用しないでくだ さい。比較にかかるCPU時間は比較されるアドレスの内容に依存するため、こ の関数はタイミングベースのサイドチャネル攻撃を受けやすいです。)
  2. 18 攻撃の3ステップ Step 1 文字列長の特定 長さ一致時のみmemcmpが実行され、わずかに処理時間が長くなる ▼ Step 2 1文字目の特定

    正しい文字で2文字目比較に進み、処理時間が増加 ▼ Step 3 2文字目以降も同様 全文字を1文字ずつ特定していく
  3. 23 ベンチマーク環境 ⚫ 環境 : Docker(Apple Silicon) ⚫ 文字列長 :

    256文字 ⚫ 試行回数 : 1,000万回 × 7ラウンド(中央値を採用) ⚫ 測定対象 : 不一致位置を変えて === と hash_equals を比較
  4. 24 比較するPHPバージョン ⚫ PHP 5.6 : hash_equalsが初めて導入されたバージョン ⚫ PHP 7.4

    : PHP 7系の最終安定版 ⚫ PHP 8.4 : PHP 8系(JIT無効) ⚫ PHP 8.4 : PHP 8系(JIT有効)←ライブデモ 時間の都合上、ライブデモは PHP 8.4(JIT有効) のみ実施。 他のバージョンは事前に実行した結果を利用しています。 実行するコードはこちら
  5. 25 測定パターン ⚫ 1文字目不一致 : 最初の文字だけ違う → 最速 ⚫ 最後不一致

    : 256文字目で不一致 → 最遅 ⚫ 差分 : この2つの差がタイミング攻撃の根拠
  6. 27 ベンチマーク結果(=== 演算子) PHP Version 1文字目不一致 最後不一致 差分 5.6 14.09

    ns 23.20 ns 9.11 ns 7.4 6.47 ns 9.55 ns 3.08 ns 8.4(JIT無効) 6.01 ns 9.26 ns 3.25 ns 8.4(JIT有効) 2.74 ns 5.17 ns 2.43 ns
  7. 28 バージョンによる変化 ⚫ PHP 5.6 → 8.4(JIT) で差分は約3分の1に ⚫ しかし

    ゼロにはならない ⚫ JITを有効にしても差は残る
  8. 29 hash_equalsとの比較(PHP 8.4 JIT) ケース === hash_equals 長さ不一致 3.15 ns

    14.13 ns 1文字目不一致 2.74 ns 93.69 ns 中間不一致 3.63 ns 94.16 ns 最後不一致 5.17 ns 94.28 ns 完全一致 5.17 ns 94.52 ns
  9. 30 注目ポイント === の列 ⚫ 1文字目不一致 : 2.71ns ⚫ 最後不一致

    : 5.13ns ⚫ 約2.4ns の差がある ⚫ 「どこまで一致しているか」が   処理時間に現れている hash_equals の列 ⚫ 長さ一致ケースは全て 約94ns で安定 ⚫ これが 定数時間比較 の効果 ⚫ オーバーヘッドは約 90ns   (処理時間は無視できるレベル)
  10. 35 実装のキモ — XOR演算(^) 同じ文字 A ^ A = 0

    ビットが全て同じ → 0 違う文字 A ^ B ≠ 0 異なるビットがある →非0
  11. 39 なぜ長さのリークは許容されるのか? “ In general, it's not possible to prevent

    length leaks. So it's OK to leak the length. The important part is that it doesn't leak information about the difference of the two strings. CSRFトークンやHMACの出力長は 公開情報(固定長) 攻撃者が知りたいのは 内容 であり、長さだけでは内容を推測できない hash_equals も長さが異なると即座に false を返すが、問題ないか? — ircmaxell's Blog "It's All About Time"
  12. 41 RFC: timing_attack ⚫ 2013年12月: Rouven Weßling氏がRFCを提出 ⚫ RFCの名前が「timing_attack」 ⚫

    投票結果: 賛成22、反対1 で可決 — Request for Comments: Timing attack safe string comparison function
  13. 42 PHP 5.6(2014年8月) ⚫ タイミング攻撃対策として 初登場 ⚫ ext/hash に実装 導入の背景

    PHP 5.5の password_verify は定数時間比較を備えていたがパスワード検証専用。 CSRFトークンやHMAC署名など 文字列同士の汎用的な定数時間比較 が必要だった
  14. 47 迷ったときの判断基準 ⚫ 何か悪用できる → hash_equals ⚫ 特に問題ない → ===

    hash_equalsのオーバーヘッドは数十ナノ秒程度。 迷ったら hash_equals を選んでおけば安全 「この値が攻撃者に 1文字ずつ 漏れたら何が起きるか?」
  15. 54 今日のポイント ⚫ === は 早期リターン により処理時間が入力に依存する ⚫ hash_equals は

    全文字を必ず比較 して定数時間を実現 ⚫ 長さのリーク は許容される(内容の推測には使えない) ⚫ 使い分けの基準は「秘密情報をユーザー入力と照合しているか」 ⚫ 引数の順序は 既知の秘密が第1引数、ユーザー入力が第2引数 ⚫ パスワード検証は password_verify() を使う
  16. 55 多層防御の重要性 “ From a practical standpoint, I wouldn't worry

    about timing attacks until I was confident that the other potential vectors are secured. ⚫ Rate Limiting: 攻撃に必要な数万〜数十万の試行を制限 ⚫ トークンのローテーション: 有効期限を短くして試行回数を制限 ⚫ 他の対策も重要: SQLi、XSS対策が優先 — ircmaxell's Blog "It's All About Time" 「実用的には、他の脆弱性対策ができてからタイミング攻撃を心配すべき」
  17. 57 参考資料 ⚫ PHP RFC: timing_attack ⚫ PHP Manual: hash_equals

    ⚫ Linux man page: memcmp ⚫ ircmaxell's Blog ⚫ Remote Timing Attacks are Practical (PDF) - Rice大学の論文 ⚫ 本発表の元記事
  18. 59 Q1. password_verifyとの違いは何ですか? ケース hash_equals password_verify 比較対象 文字列 × 文字列

    平文パスワード × ハッシュ 内部処理 定数時間比較のみ ハッシュ化 → 定数時間比較 用途 CSRFトークン、HMAC署名、API キー等 パスワード検証専用 password_verify:平文パスワードをハッシュ化してから比較するパスワード専用関数 hash_equals:文字列同士をそのまま比較する汎用関数
  19. 60 Q2. hash_equalsができる以前はどうやって防御していたのか? ①同じロジックの自前実装(各フレームワーク等) RFCのReferencesに具体例が挙げられています ⚫ Symfony: StringUtils::equals() ⚫ Zend

    Framework 2: Zend\Crypt\Utils::compareStrings() ②別の戦略で回避 定数時間比較を正しく書くのは難しいため、比較の仕組み自体を変えるアプローチ ⚫ Double HMAC 戦略(Paragon Initiative Enterprises が提唱) ⚫ 比較する2つの文字列を毎回異なるランダムキーでHMAC値を計算してから === で比較 ⚫ 毎回結果が変わるため、タイミング差を統計的に蓄積できない
  20. 61 Q2. hash_equalsができる以前はどうやって防御していたのか? そもそも当時からメジャーな攻撃手法ではなかった “Timing attacks are not a widely

    recognized problem, since they require significant skill and resources of the attacker. OWASP Top 10 にも独立項目として掲載されたことがない ⚫ 2013年版〜2025年版すべてで不掲載 ⚫ SQLi・XSS 等と比べて対策の優先度が低かった サイドチャネル攻撃が広く意識されたのは hash_equals よりも後 ⚫ 2018年:Spectre / Meltdown、2022年:Hertzbleed いずれもCPUレベルの脆弱性であり、文字列比較のタイミング攻撃とはレイヤーが異なる ただし「処理の差から情報が漏れる」という原理は共通 — RFC 本文(2013年12月)
  21. 62 Q2. hash_equalsができる以前はどうやって防御していたのか? メジャーでない攻撃に対して、自前で対策を書く開発者は少ない → だからこそ言語が関数を提供し、使うだけで守れる状態にする必要があった “ this RFC aims

    to make it simpler for PHP developers to protect their applications. ⚫ 難しい定数時間比較の実装を開発者に任せない ⚫ hash_equals() を呼ぶだけで対策が完了する ⚫ 本発表のまとめでも紹介した「簡単に防げるなら防いでおく」はこの設計思想と同じ — RFC 本文(2013年12月)
  22. 63 Q3. libcなど他言語を読む時のソースコードを読むコツは? 生成AIにガイドしてもらいながら読み進めるのがおすすめ ⚫ ただし、AIの回答は鵜呑みにしない ⚫ 「なぜそう言えるのか?根拠となるコードはどこか?」を常に確認する 実際、今回libcを読んだのもこのアプローチの結果 ⚫

    hash_equalsの仕様をAIに質問しながら深掘りしていった ⚫ エビデンスを辿っていった先が、libcの内部実装だった ⚫ 普段C言語を書いているわけではないので苦戦はしたが、それもAIのガイドで乗り越える
  23. 64 Q4. hash_equalsという名前なのにhash使ってないんですね? 命名の経緯 RFC では元々 hash_compare という名前で提案 投票は hash_compare

    で可決(賛成22 / 反対1) 実装時に hash_equals にリネーム RFC「Differences between this RFC and the implementation」に記載 hash_ プレフィックスの理由 RFCに「as part of ext/hash」と明記されている通り、hashモジュールの一部として実装 同モジュールの関数は慣例として hash_ プレフィックスを持つ hash_hmac, hash_file, hash_pbkdf2 等 →内部でハッシュ化しているわけではなく、所属モジュールに由来する命名
  24. 65 ② 比較処理の外側で守る:多層防御 Q5. hash_equalsを使わずにタイミング攻撃を防ぐ方法は? ① 比較処理自体を守る(Q2で紹介した手法) ⚫ 定数時間比較の自前実装(Symfony, Zend

    Framework 2 等の実績あり) ⚫ Double HMAC 戦略 対策 効果 Rate Limiting 数万〜数十万回の試行を制限 トークンのローテーション 有効期限内にサンプル収集を困難に 十分なエントロピー 1文字漏れても探索空間が膨大 異常検知 規則的アクセスパターンを検知 → 現実的には①と②の組み合わせで防御する