つまり、このバグは中間者攻撃(man-in-the-middle attack)を引き起こし、バグの説明には、この問題は接続認証の正当性チェックをミスするステップに起因すると書かれています。
Impact: An attacker with a privileged network position may capture or modify data in sessions protected by SSL/TLS
|
ここでもう一つ、SSL/TLSに関連するバグやセキュリティ問題がインターネット上で発生したら、ユーザーであろうとプログラマーであろうと、細心の注意を払わなければなりません。なぜなら、このネットワーク通信用の暗号化プロトコルは、セキュリティが最も必要とされる多くの場所で広く使われており、SSL/TLSに問題が発生すれば、それは世界のコンピュータセキュリティシステムの崩壊を意味するからです。
コード バグの理由
Adam Langley氏のAppleのSSL/TLSバグに関する投稿で、バグの詳細が明らかになりました。
static OSStatus
SSLVerifySignedServerKeyExchange(SSLContext *ctx, bool isRsa, SSLBuffer signedParams,
uint8_t *signature, UInt16 signatureLen)
{
OSStatus err;
...
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;
err = sslRawVerify(ctx,
ctx->peerPubKey,
dataToSign, /* plaintext */
dataToSignLen, /* plaintext length */
signature,
signatureLen);
if(err) {
sslErrorLog("SSLDecodeSignedServerKeyExchange: sslRawVerify "
"returned %d
", (int)err);
goto fail;
}
fail:
SSLFreeBuffer(&signedHashes);
SSLFreeBuffer(&hashCtx);
return err;
}
if文には中括弧がないので、最初のgotoだけがifに属し、2番目のgotoは常に実行されます。つまり、前のifチェックがすべて失敗しても、goto fail;は実行されます。failタグ内のメモリを解放した後、return err;が表示されます。
SSLHashSHA1.update()が成功、つまり0を返したときに何が起こるか考えてみましょう。そう、本当の仕事をする sslRawVerify() はバイパスされるのです。そして関数SSLVerifySignedServerKeyExchange()も0を返し、これは成功を意味します!ニマ!おそらくCoolShell.comの記事 "A Space Causes a Fiasco "を思い浮かべていることでしょう。すべて低レベルのバグです。
この低レベルのバグはこの週末、インターネット上で話題になっており、Twiterで#gotofailのハッシュタグの祭典をチェックすることができます。
XKCDが好きなら、間違いなくこの漫画を思い浮かべるでしょう:
注:このバグはTLSバージョン1.2には影響しません。なぜならバージョン1.2はこの機能を使わず、別のメカニズムを経由するからです。しかし、クライアント側でバージョンを選択できることを忘れないでください。
あなたのブラウザに問題があるかどうかをテストしたい場合は、同じ日にライブになったhttps://。ウェブサイト
いくつかの考え
以下は、このテーマについての私の考えです。
0) コンパイルアラームについて
アップル社のコードにあるgoto文はデッドコード、つまり決して実行されることのないコードを作り出し、C/C++プログラマーは警告を受けるだろうと言う人がいます。しかし、実際、死んだコードはデフォルトでは警告されません。たとえ-Wallを追加しても、GCC 4.8.2もClang 3.3も、デフォルトの警告レベルのVisual Studio 2012も含めて、警告しません。gccとClangには、この状況を警告することができる-Wunreachable-codeというパラメータがありますが、-Wallには含まれていません。その理由は、コンパイラの最適化されたコードの振る舞いのために、このパラメータには多くの問題があり、このパラメータはすべての状況を正確に報告するわけではないからです。また、このパラメータは新しいバージョンのGCCでは削除されていることに注意してください。もちろん、他の静的コードチェック・ツールもこの低レベルの問題をチェックすることができます。
また、IDEのコード自動書式設定ツールを使うことで、少しは解決できるのではないでしょうか?少なくとも、そのインデントを一見間違って見えるようなものに変えてくれるかもしれません。
1) コードマージとコードレビューについて
このバグの差分を見るには、こちらのコードを比較するか、こちらにアクセスしてください。
diff -urN <(curl -s http://..com/source/Security/Security-.13/libsecurity_ssl/lib/.c\?txt) \ <(curl -s http://..com/source/Security/Security-55471/libsecurity_ssl/lib/.c\?txt) \
コードdiffを見ると、Appleがコードをリファクタリングしていることがわかります。
つまり、2つのgoto fail文は、おそらく異なるブランチ上のコードに対してマージを行ったために発生したと推測できます。これは、バージョン管理ツールを使ってコードをマージするときによくある問題です。コードにたくさんの差分がある場合、この問題を見逃しがちです。コードレビューがあったとしても、この問題のあるコードを見つけるのは困難です。次の diff を見て、このエラーに気づくでしょうか?
つまり、リファクタリングブランチのコードは正しいのですが、ブランチのマージ中にマージツールによってめちゃくちゃにされてしまうのです。ですから、コードマージをするときには注意深く慎重を期す必要があり、マージツールを完全に信用してはいけないのです。
2)テストについて
明らかに、このバグをコードレビューで見つけるのは非常に困難です。コードのリファクタリングや、コードマージでの多くの差分では、レビューされにくいのです。
もちろん、後知恵のある人たちが「検査で問題は発見できたはずだ」と言うのは簡単ですが、本当にそうでしょうか?
この関数は、非常に深い場所でネットワークハンドシェイクにあるため、この問題はまた、機能テストによって発見されることは非常に困難であり、機能テストは、必ずしもその深さをカバーすることはできません、あなたはそのようなケースを書きたい、あなたは、TLSスタックに非常に精通している必要があり、すべてのパラメータに精通している彼に非常に精通しており、各パラメータとテストケースの束を行うには、これらのパラメータの組み合わせのために書くことができる、この事はまた、非常に複雑なものです。非常に複雑なこと。全てのケースを書くのはとてもとても大変なことです。SSLVerifySignedServerKeyExchange()関数の詳細については、関連するServerKeyExchange RFC ドキュメントを参照してください。
問題だけを見れば、関数のユニットテストで問題が明らかになると言うでしょう。しかし、SSL/TLSは何年も前から存在しているので、基礎となる関数はかなり安定しているはずです。
十分な時間があれば、すべてのファンクションポイント、すべてのファンクションでUTを行い、コードカバレッジとブランチカバレッジを同じように探求することは可能です。しかし、絶対にできないことがあります。それは、ネガティブなケースをすべて洗い出すことです。ですから、テストでは極端なことはできませんし、より賢くテストする必要があります。私の記事QA専任の必要性」で述べたように、テストはコーディングよりもはるかに難しく、テストは上級開発者でなければうまくできない仕事です。私は、コードを書かない人がテストをうまくできるとは決して思いません。
ここで私が言いたいのは、テストによって問題が発見される可能性が低いということではなく、テストは重要であり、単一テストはさらに重要であるということです。しかし、すべてをカバーすることは不可能です。注意が払われていないところでは、必ず愚かなミスが起こります。
追記:大手サイトでのこの件に関する議論では、OS X の curl コマンドが認証されていない IP アドレスからの https リクエストを実際に受け付けることができることが確認されており、この理由はまだ誰も分かっていませんが、テストでチェックされなかった理由の一つかもしれません。
3)コーディングスタイルについて
もしブロック中括弧を強制的に使用すれば、両方のgoto失敗がifブロックの中に入ることになり、メンテナンスも読みやすさも向上します。もしブロック中括弧の使用を強制すれば、両方のgotoフェイルが1つのifブロックの中に入ることになり、これも保守しやすく読みやすくなります。
また、ステートメントブロック括弧を使用せず、単一のステートメントのみを使用することに固執する場合は、1つのステートメントであり、同じ行に配置する必要があるとも言われています。以下のように:
if (check_something) do_something();
しかしそうすると、シングル・ステップでコードをデバッグしているときに、STEP OVERしたときにifの条件がtrueなのかfalseなのかがわからなくなってしまい、ちょっと困ります。そこで、複数行に分割して中括弧を追加するのがよいでしょう。
同じような問題は、10年以上前にもありました。その時はもっと大きな問題があって、ユーザーに間違ったデータを提供してしまいました。その時は他の人のコードをメンテナンスしていたのですが、他の人のコードはAppleのコードのようにif文のブロック括弧がないだけでした。私はreturn zで関数を呼び出そうとしたのですが、それはお茶の子さいさいでした:
if ( ...... )
return x;
if ( ...... )
return y;
if ( ...... )
foo();
return z;
人間の脳は当たり前のように、インデントはすべて1つのステートメント・ブロックの中にあると思い込んでしまうので、このようなミスはうっかり犯してしまうのです。しかし、もし元のコードに中括弧が追加され、その後にインデントが普通に行われていたとしたら、後でそのコードをメンテナンスする人々にとっては非常に良いことだったでしょう。私が犯したような安直なミスは犯さなかったでしょう。下のコードのように、書くとちょっと言葉足らずですが、人のためになります。
if ( ...... ){
return x;
}
if ( ...... ){
return y;
}
if ( ...... ){
return z;
}
これと似たようなコードスタイルは以下の通りですが、どちらが読みやすいと思いますか?
- if 和 if
- if 和 if
- if そして、もし
- if ) and if == true )
また、switch文のifとしてcaseを使う人、つまりダフのDeviceのようにcaseの後にbreakをつけない人も多く、gotoを使ったコードは非常に見事です!
だから、コードはクールなところではクールではなく、他の人が読むためのものなのです。
また、このアップルのコードはなぜ以下のような形で書かれていないのでしょうか?ほら、この場合は以下のようにすっきりしているでしょう?
if ( (err = ReadyHash(&SSLHashSHA1, &hashCtx)) != 0 )
|| (err = SSLHashSHA1.update(&hashCtx, &clientRandom)) != 0)
|| (err = SSLHashSHA1.update(&hashCtx, &serverRandom) != 0)
|| (err = SSLHashSHA1.update(&hashCtx, &signedParams) != 0)
|| (err = SSLHashSHA1.final(&hashCtx, &hashOut)) != 0)) {
goto fail;
}
例えば、failタグにマクロとして記述することで、goto文を削除することができます。
4) goto文について
goto文については、1968年にエドガー・ダイクストラが『Communications of the ACM』に投稿した論文の原題が "A Case Against the Goto Statement "で、CACMの編集者であるニクラウス・ヴィルトに触発されて、おなじみの "Go To Statement Considered Harmful "というタイトルに変更されました。数年前、私はプログラマーの質がプログラム中のgoto文の密度に反比例することを観察しました。「それ以来、私はなぜgoto文の使用がこのような深刻な結果をもたらすのかを発見し、すべての高級言語はgotoを廃止すべきだと考えています。
goto文はなぜ悪いのか?ダイクストラは、変数が表す意味はその文脈によって異なると言っています。あるプログラムが部屋にいる人の数をNで管理しているとします。しかし、別の人が部屋に入るのを見た後にNをインクリメントする命令の前にあるプログラムのブロックでは、Nの値は「現在部屋にいる人の数プラス1」を表します。このように、プログラムの状態を正しく解釈するためには、プログラムの実行履歴を知ること、言い換えれば「現在地」を知ることが必要です。
どこに行ったか」をどう話すか?一直線に実行されるプログラムなら、そのステートメントを指差して「ここです」と言えばいいでしょう。ループのあるプログラムなら、「ループのこの時点で、ループはi回実行されました」と言う必要があるかもしれません。関数の中であれば、「関数pのこの時点で、pはqによって呼び出されたところです。
もしgoto文があったら?それは問題です。というのも、コンピュータは特定の命令を実行する前に、プログラム内の多くのgotoのうちの1つからジャンプしている可能性があるからです。また、特定の変数の性質について話すこともほとんど不可能になります。だからgoto文は問題なのです。
ダイクストラの記事は、私を含め、その後の多くのプログラマーに非常に深い影響を与えました。私を含め、多くのプログラマーは、goto文は使えるときに使うべきだと感じていました。しかし、10年前のプログラミング教養の第23回でも述べたように、私はgoto文が使えるケースは1つしかないと思っています。しかし、goto文は使えるのに使うべきではないというダイクストラの意見にも賛成です。このAppleの問題の場合、RAII技術を使ったより高度なC++では、このようなgoto文はもはやあまり意味がありません。
ダイクストラの論文は、構造化プログラミングの議論において最も有名な論文のひとつとなりました。19年後、フランク・ルービンは、「『有害とされる碁へ』Considered Harmful」と題する記事をCACMに寄稿しました。 ルービンは、「ダイクストラの主張は、あまりにも学術的で説得力に欠けるが、すべてのプログラマの心に焼き付いているようだ。ルービン氏は、「ダイクストラの主張はあまりに学術的で説得力に欠けるものの、すべてのプログラマの心に焼き付いているようです。ですから、誰かが "この問題を解くにはgoto文を使ったほうがいいかもしれない "と言うと、ひどく軽蔑されるのです」。そこでルービンは、XがN * N個の整数の配列である問題を思いつきました。Xの行iがすべて0ならiを出力し、複数の行がある場合は最小のiを出力します。
ルービンは、gotoを習慣的に使うプログラマーとそうでないプログラマーを調べたところ、gotoを使うプログラムは速く明快であることを発見しました。そして、gotoを使わない人は、通常、複雑な解決策を書くのに多くの時間を費やしていました。どう思いますか? また、あなたならこの問題のプログラムをどう書きますか?
私にとっては、goto文の欠点は長所をはるかに上回り、99%の場合、私は議論の反goto側です。javaとpythonはgoto文を提供していません!
まとめ
この問題を完全になくすことはできませんが、次のような方法で減らすことはできます:
1)実行時ではなく、コンパイル時にエラーが出るようにしてください。
2)コードは人が読むものであり、機械が動かすものです。トラブルを恐れず、良いコードスタイル、読みやすいコードは多くの問題を減らします。
3)コードレビューは真剣なものですが、コードレビューの前提条件は、コードが非常に読みやすいことです。
4)テストは、特に開発者が真剣に取り組むべき重要かつ難しいものです。
5)その場しのぎはやめましょう。その場しのぎで問題を解決するのは恥ずかしいことです!gotoを使わなくても、コードをうまくまとめる方法はたくさんあります。
最後に、私はこの一年間、淘宝網でP1/P2の失敗をいくつか経験し、特に昨年8-9月に失敗が頻発しましたが、P1/P2の失敗の70%はコードレビューが不十分で、テストがうまくいかず、問題を解決するために飛脚が大量に発生したためであり、これはビジネスの結果ばかりを重視し、技術に対して相応の厳密な態度と敬虔な気持ちを持っていないことに起因していることがわかりました。の中心は
アップルの失態が示唆するように、テクノロジーにふさわしい厳しさと敬意をもって扱わなければ、そうなってしまうのです。
Go To Fail !





