昨日(2014年2月24日)話題になったiOSのバグに対するソフトウェア技術者の視点での考察です。
「前代未聞」(Live doorニュース)、「かなりやばいぞ」(ギズモード・ジャパン)、「史上最悪」(Appllio)と日本語のニュースはかなり煽っています。
いったい何がどう危険なのでしょうか?
また、ソフトウェア・エンジニアリングで欠陥は防止可能でしょうか?
資料はGoogleのAdam Langleyさんのブログです。
何が危険か?
Adamさんのブログを読んで、私が読み取ったことは以下の通りです。
- サーバ証明を確認する処理を、途中で抜けている。
- 上により、SSLのハンドシェイクの際、サーバ側の証明書に付属された公開鍵と異なる鍵でもサインできる。(あるいは、まったくサインしなくてもよい。)
- 従って、サーバがその公開鍵を所有しているとは限らない。
(私はSSLについては無知なので、サインが何を表しているのかは不明。)
以上より、私は以下のような現象かと理解しました。
- 攻撃者が、ハンドシェイクのときにサーバからクライアントに送られる電文を改ざんして、自分の解読可能な公開鍵を送り込むことができる。
- ユーザーがSSLのつもりでも、攻撃者は通信を解読できる。
市民が「やばい」のは、当該のiOS(あるいはOS X)から攻撃者が潜んでいそうなサイトに、クレジットカード番号を書き込むときでしょうか?もちろん、いろんなアプリも裏でSSL通信していると思います。
(もし機密情報をメールでやりとりしている方がいましたら、そちらのほうが危ないですよ。)
ソフトウェア・エンジニアリング的な考察
当該の欠陥
Adamさんのブログより、欠陥部分のコードを引用します。
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; ... fail: SSLFreeBuffer(&signedHashes); SSLFreeBuffer(&hashCtx); return err; }
12行目の「goto fail;」が欠陥です。これにより実行がfail:ラベルに飛び、その後の検証をスキップして進んでしまうようです。
一見、20行でerrを返しており、呼び出し元がエラー判定しても良さそうに見えます。しかしerrは「OSStatus」という型(0と比較していることから、恐らく整数)の変数で、10行目の条件式の中で「成功」の値(0)が代入されています。
従って、20行では「成功」が返ります。
Adamさんの考察
この欠陥に対する、Adamさんのエンジニアリング的考察は以下です。
- ifのあとに{}を書くようなコーディング・スタイルは役に立つけど、変なインデントもあるよね。
- テスト・ケースはこのバグを捕捉するかもしれないけど、ハンドシェイクの奥深くだから難しいよね。
- コード・レビューはこの手のバグには有効だよね。Appleの文化はわからないけど、僕の職友は見つけると思うよ、僕が見逃したとしても。
(英語力のなさから、フィーリングで訳してます。^^;)
コーディング標準
1点目に関しては、Adamさんには説得力ないかもしれませんが、私は以下の様なコーディング標準を追加すればよいかと思います。
ifやwhileの後は、必ずブロック{}にする。
あるいは
ifやwhileの後にステートメントを書く場合は、改行しないでその後に続けて書く。
日本人は(C言語のK&R本の影響か?)前者に馴染みがあるかもしれません。
私の経験の範囲でのアメリカ人のコードでは、このコードのような書き方(ブロックを使わず、改行、インデント)をよく見ます。私もスクリプトを書くときはいちいちブロックを書きません。
しかし、その場合は後者のように改行をせず続けて書きます。このルールを適応すると、当該の欠陥箇所は以下のようになります。
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という変数を返しています。そこで以下の様なルールがあっても良いかもしれません。
catch{}やエラー処理のブロックからexitする場合のreturnには、変数ではなく明示的な値を指定する。
テストについて
欠陥の関数はstaticなので、xUnitのようなツールで関数の単体テストをすることは出来ません。
(私がC言語で製品のコードを書いていた時は、テストドライバにテスト対象をインクルードして、staticだろうが関数ごとに単体テストをしていました\(-o-)/。コンパイル後のオブジェクトが変わってしまうのでよろしくないのですが。)
ですので、Adamさんの言うとおり「(staticの付いていない公開関数から)テストするには深すぎる」のでしょう。
しかしカバレッジ計測をちゃんとやっていれば、欠陥以降のコードを通っていないことは目の子で分かります。
BSDのコンパイラが何かは知りませんが、gccならgcovがあります。
私はgcovの出力するテキストをHTMLに整形し、通っていないコード(あるいは分岐)を色付けするツールを自作して、視覚的にカバレッジが分かるようにしていました。
(phpUnitにはHTMLを出力する機能がありましたので、今のxUnitでは色付けHTML出力が標準機能かもしれませんね。もちろん数十万円のツールを使っても構いません。)
コード・レビューについて
これはAdamさんの言うとおりです。
エンジニアがセルフ・レビューをする規範を持っていたり、組織がピア・レビューをする文化があれば欠陥は検出可能です。
しかしなかなかこれが難しい。
私も社会的に信頼性の必要なソフトウェアの開発組織にいたのですが、上記はまったくありませんでした。(カバレッジを計測した単体テストも当時やっていたのは私だけでしたが…。^^;)
ましてや一般のソフトウェアについては言わんやをや、です。
(もしコード・レビューに興味のあるかたは、Watts S. Humphrey,「パーソナルソフトウェアプロセス技法―能力向上の決め手」,共立出版,1999 のなかにセルフ・レビューの仕方が書いてありますのでご参照下さい。高くて厚くて誤訳もある本ですが。^^;)
構造化プログラミングについて
25年くらい前の私がプログラミングを始めたときは、「構造化プログラミング」がよく言われていました。
「goto-less」という言葉に象徴される構造化プログラミンですが、上記のAppleのコードをみて、私は「Appleはgotoを書くんだなあ」と思いました。
これに関してfacebookでは「例外処理のないC言語ならいいんじゃないの」という意見を頂きました。
私も同意です。try{}catch{}の代わりにgotoを使うのは構わないと思います。エラー処理のラベルの前にreturnがあるのであれば、「構造化設計」はなされています。
「構造化プログラミング」では、gotoだけでなく、ループ中のcontinue、break、関数の途中のreturnも使わないのですが、私は多用します。理由としてはブロックの深さが浅くなり、コードの可読性が上がるからです。
このあたりの考え方はプログラマー個人や組織によってことなりますので、「どっちがよい」というつもりはありません。
とはいえ規格が「構造化プログラミング」を要求する場合はあります。それをチェックする人が(構造化設計でなく)単に「gotoがある」ことに目くじらを立てそうなので、注意が必要ですね。
(2014年2月27日付記)
冒頭であげましたAppllioさんの記事では、Wiredの記事を引用し、「BASIC初心者でも一目で分かるようなミス」(原文:“comprehensible almost at a glance to anyone who dabbled in BASIC as a kid”)と表現していますが、私はこの表現は適切であるとは思いません。(そもそもBASICにはブロックがないし。)
また別の記事では「偽のサーバー証明書もパスしてしまう」のような表現もありましたが、Adamさんのブログの範囲ではそのように読み取れません。
まあ、私もAdamさんのブログ記事を信じて、iOSのソースまで追っていないので同じ穴の狢ですが。
まとめ
ちょっと前にiPhoneでのvCardについて調べました。iPhone4にて不正なvCardファイルでメーラーがフリーズするとか、iPhone5でも知らないフィールド(MS拡張とか)があるとフィールドがずれるとか、結構安易な欠陥があるようでした。
再発防止されるかどうかは、Appleのエンジニアリングに対する考え方次第です。