Appleのgoto failバグ
たった1行の重複したgoto failがTLS証明書検証を丸ごと無効化した経緯を史実に忠実に解剖し、波括弧省略の罠・静的解析・カバレッジという再発防止の勘所を持ち帰れる。
- 2014年、AppleのTLS実装へgoto failが重複し、後続の署名検証を飛ばして不正証明書を受理した(CVE-2014-1266)。中間者がHTTPS通信を盗聴・改ざんできた。
- 波括弧の無いif文では2つ目のgoto failが常に実行され、インデントは条件付きに見えても実際は無条件。errには直前の成功値が残るため検証は必ず成功扱いになった。
- コンパイラ警告-Wunreachable-codeは既定で無効、単体テストと文カバレッジも不足していた。負のテスト・静的解析・波括弧必須のコーディング規約が再発を防ぐ。
何のコードか
2014年2月21日、Apple は iOS 向けに緊急のセキュリティ更新(iOS 7.0.6 と 6.1.6)と Apple TV 6.0.2 を配布しました。数日後の2月25日には OS X 10.9.2 が続きます。修正されたのは CVE-2014-1266、通称「goto fail」バグです。舞台は Apple 独自の TLS/SSL 実装 Secure Transport の中核、libsecurity_ssl/lib/sslKeyExchange.c にある SSLVerifySignedServerKeyExchange 関数でした。
この関数はサーバーが送ってくる Server Key Exchange メッセージの署名を検証する、TLS ハンドシェイクの心臓部です。にもかかわらず、たった1行の重複したコードのせいで署名検証そのものを素通りし、正しい署名も不正な署名も等しく受理していました。攻撃者は中間者(MITM)の位置に立てば、任意の TLS サーバーになりすまし、HTTPS で守られていたはずのログイン情報やメールを盗聴・改ざんできます。Apple のコードは公開されており、誰でも該当箇所を読めたことも、この事件が「1行がもたらした世界規模の危機」として語り継がれる理由です。
コードと仕組みの解説
横にスクロール
問題のコードは、ハッシュ計算の途中でエラーが出たら後始末へ飛ぶ、C言語の典型的な goto によるエラー処理でした。
if ((err = SSLHashSHA1.update(&hashCtx, &serverRandom)) != 0)
goto fail;
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
goto fail;
goto fail;
if ((err = SSLHashSHA1.final(&hashCtx, &hashOut)) != 0)
goto fail;
/* ... この後に本来の署名検証が続く ... */
fail:
SSLFreeBuffer(&signedHashes);
SSLFreeBuffer(&hashCtx);
return err;
goto fail; が2行連続しているのが分かります。C言語では if に波括弧を付けない場合、条件に従うのは直後の1文だけです。したがって1つ目の goto fail; は if に紐づきますが、2つ目はインデントで条件付きに見えても、実際にはどんな場合でも必ず実行される無条件の文です。
ここが致命的でした。2つ目の goto fail; に到達するのは、直前の SSLHashSHA1.update が成功して err に0(成功)が入っている場合です。そのまま fail: ラベルへ飛び、err(=0)を return します。呼び出し側は戻り値0を「検証成功」と解釈するので、その先に書かれていた本命の署名照合コードは一度も実行されません。SHA1 の更新に成功しさえすれば、署名が正しかろうと偽物だろうと、関数は常に「OK」を返してしまうわけです。
このバグの本質は「見た目のインデント」と「実際の構文」のズレです。人間は字下げを制御構造として読みますが、C言語は空白を無視します。波括弧を省いた if の下に文を1つ足すと、書いた人の意図に反してブロックの外に落ちる。同じ罠は改行やコピー&ペーストのたびに再発します。
goto を使うこと自体が悪だという俗説がありますが、これは誤りです。リソース解放を1か所に集約する goto fail 型のエラー処理は C言語では定番で、Linux カーネルでも広く使われます。真犯人は goto ではなく、波括弧の省略と、コードの重複による混入でした。
逸話と経緯
正確な混入時期は今も特定されていません。分かっているのは、2012年5月公開の iOS 5.1.1 には無く、2012年9月公開の iOS 6 には存在したこと。つまり脆弱性はおよそ1年半、誰にも気づかれず iPhone や Mac の中で動き続けていました。誰が余分な1行を書いたのか、Apple は公式に明かしていません。マージ時のコンフリクト解消でコピー&ペーストがずれた、といった推測はありますが確証はなく、事実として語れるのは「重複した1行が入った」という結果だけです。
2014年2月21日の iOS 修正が引き金でした。セキュリティ研究者たちが公開済みのソースと差分を突き合わせ、原因がこの1行だと即座に特定します。ところが Apple は iOS を先に、OS X 版を数日遅れで出したため、Adam Langley をはじめとする研究者が該当コードを解説した時点で、Mac は未修正のまま脆弱性が世界に周知されるという危うい状況が生まれました。修正前のブラウザで動作を試せる gotofail.com が公開され、話題はさらに広がります。
戻り値0で検証成功と扱う設計が「バックドアではないか」という疑念も出ました。しかし波括弧を欠いた重複行という形は、意図的な仕込みにしては不自然で目立ちすぎます。研究者の大勢は単純なヒューマンエラーと結論づけました。断定できる証拠がない以上、俗説と事実は分けて扱うべき論点です。
遺産と教訓
この事件が突きつけたのは、なぜ混入から発見まで1年半もかかったのかという問いです。答えは開発プロセスの複数の穴にありました。
まずコンパイラ警告。2つ目の goto fail; の後ろにある if 文は決して実行されない到達不能コード(dead code)です。Clang には -Wunreachable-code があり、これを有効にしていれば警告できました。しかし当時この警告は -Wall(全警告)に含まれておらず、既定では黙殺されていたのです。後に GCC 6 は、まさにこの手のミスを狙った -Wmisleading-indentation(インデントが構造と食い違う場合の警告)を追加しました。
| 防御層 | この事件で機能したか | 有効化すれば防げたか |
|---|---|---|
| コンパイラ警告 | -Wall に到達不能コード検出が含まれず素通り | -Wunreachable-code / -Wmisleading-indentation で検出可 |
| 静的解析 | 到達不能コード解析が未適用 | 不到達コードを指摘するツールで即検出 |
| 単体テスト+負のテスト | 不正な署名を拒否するテストが不足 | 偽署名を1件でも流せば必ず失敗し発覚 |
| 文カバレッジ計測 | 検証本体が未実行でも気づけず | 未実行行として可視化され発覚 |
そして最大の教訓はテストです。この関数は外部依存を差し替えれば単体テスト可能で、不正な署名を1件でも投げれば必ず露見しました。「正しい入力を受理する」正のテストだけでなく、「不正な入力を拒否する」負のテストが欠けていたのです。加えて文カバレッジ(statement coverage)を計測していれば、本来の検証コードが一度も実行されていない事実が数字で見えました。同じハンドシェイク処理のパターンがコード内で何度も複製されていた点も、混入の温床でした。
再発防止の第一歩は規約で単純化できます。if・else・for・while の本体が1文でも必ず波括弧で囲む。多くのコーディング規約(MISRA-C など)が求めるこのルールを機械的な整形やリンタで強制すれば、2つ目の文が意図せずブロック外へ落ちる事故そのものが起きません。
TLS ハンドシェイクと証明書検証の原理はセキュリティを、静的解析やコードレビューを含む堅牢な開発手法はプログラミングを参照してください。
- 直接原因は重複した無条件の
goto fail;。波括弧の無いifの下に文を足すとブロック外に落ちる errに直前の成功値0が残るため、以降の署名検証を飛ばして常に「成功」を返した- 影響は MITM による TLS なりすまし(CVE-2014-1266)。iOS 6.x/7.x、OS X 10.9.x、Apple TV が対象
- 防げた理由: -Wunreachable-code、静的解析、負のテスト、文カバレッジ、if への波括弧必須
- 真犯人は
gotoではなく波括弧省略とコード重複。俗説(バックドア説)と事実は分けて扱う
一段で言うと
たった1行の重複した goto fail; が、iPhone と Mac の HTTPS を1年半にわたり無防備にしていました。C言語の波括弧省略という古典的な罠に、有効化されていない警告、欠けた負のテスト、計測されないカバレッジが重なった結果です。犯人は goto でも一人の開発者でもなく、1行の混入を無害化できなかった開発プロセスそのもの——それがこの1行が今も教え続ける教訓です。
コード遺産の記事ガイド
Appleのgoto failバグを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
TLS
比較で見る軸
難易度: advanced / カテゴリ: コード遺産 / タグ数: 6
導入後に効く点
波括弧の無いif文では2つ目のgoto failが常に実行され、インデントは条件付きに見えても実際は無条件。errには直前の成功値が残るため検証は必ず成功扱いになった。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- コード遺産
- タグ数
- 6
判断チェックリスト
- 自社の用途が「TLS / セキュリティ」に近いか確認する。
- 強みである「2014年、AppleのTLS実装へgoto failが重複し、後続の署名検証を飛ばして不正証明書を受理した(CVE-2014-1266)。中間者がHTTPS通信を盗聴・改ざんできた。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。