ECサイトで商品を見た後、"レジへ進む"ボタンをクリックしたとき何が起こっているんでしょう。この記事では、Amazonとの間でHTTPS通信が確立されるときの最初の数ミリ秒を分析してみます。Amazonではレジへ進むと下記のような新しいページが表示されます。
この一瞬、220ミリ秒間に面白そうなことがたくさん起こった結果、FireFoxはアドレスバーの色を変え、右隅に鍵マークを表示します。私のお気に入りのネットワークツール、Wiresharkと少し改造してデバッグ用にビルドしたFireFoxを使って、一体何が起こっているので調査してみましょう。
RFC 2818の規定によって、Firefoxは"https"の場合、Amazon.comのport 443へ接続しなければならないことを知っています。
ほとんどの人がHTTPSとSSL (Secure Sockets Layer) を結びつけて考えます。SSLは1990年代半ばにNetscape社が開発した仕組みですが、今ではこの事実はあまり正確でないかもしれません。Netscape社が市場のシェアを失うにしたがって、SSLのメンテナンスはインターネット技術タスクフォース(IETF)へ移管されました。Netscape社から移管されて以降の初めてバージョンはTransport Layer Security (TLS)1.0と名付けられ、1999年1月にリリースされました。TLSが使われだして10年も経っているので、純粋な"SSL"のトラフィックを見ることはほとんどありません。
Client Hello
TLSはすべてのトラフィックを異なるタイプの"レコード"で包みます。ブラウザが出す先頭のバイト値は16進数表記で0x16 = 22。 これはこのレコードが"ハンドシェイク"レコードであることを意味します。
次の2バイトは0x0301です。これはこのレコードがバージョン3.1であることを示します。これはTLS 1.0が基本的にはSSL 3.1であることを示しています。
ハンドシェイクレコードはいくつかのメッセージに分解できます。最初は"Client Hello"メッセージ(0x01)です。これには重要な情報がいくつか含まれています。
- 乱数
現在の世界標準時(UTC)をUnixエポック形式で表す4バイトです。1970年1月1日から現在時までの総秒数になります。今回の調査の場合は0x4a2f07caです。そしてこの値に続いてランダムな28バイトが続きます、この値は後で利用されます。 - セッションID
値はempty/nullです。もし数秒前にもAmazon.comへアクセスしていたら、そのときのセッションを再開することでハンドシェイクをやらなくても済みます。 - 暗号アルゴリズム
ブラウザがサポートしている暗号アルゴリズムの一覧です。一覧の先頭はとても強力な"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA"で、このあとにブラウザが受け入れられる33のアルゴリズムが続きます。これらの意味がわからなくても心配はありません。Amazon.comが私たちの先頭の選択肢を取り上げてくれないことが後でわかります。 - server_name拡張
これはブラウザがhttps://www.amazon.com/へ接続しようとしていることをAmazon.comに対して伝える手段です。この手段がとても便利なのはTLSハンドシェイクがHTTPトラフィックのやり取りのはるか手前で行われるからです。HTTP には"Host"ヘッダがあります。これによって数百のサイトをひとつのIPアドレスにまとめることができるので、企業はインターネットホスティングで発生するコストを削減できます。SSLは従来、各サイトに対して別々のIPアドレスを要求していますが、この拡張を使えばサーバはブラウザが探している適切な証明書を返信することができます。
Server Hello
ブラウザのハンドシェイクに対して、Amazon.comもハンドシェイクレコードを返信します。このレコードのサイズ (2551バイト)は大きく、2つのパケットで送信されます。このレコードにはバージョンを表すバイト0x0301があり、これはクライアント側が要求したTLS 1.0を使うことについてAmazonが賛成したことを表します。またこのレコードは3つのメッセージがあり、それぞれ面白いデータが含まれています。
- "Server Hello" メッセージ (2):
- 4バイトのUnixエポック形式の秒数と28バイトのランダム値です。この値はあとで利用します。
- 32バイトのセッションID。面倒なハンドシェイクを行わずに再接続したい場合に使います。
- クライアント側が提供した34の暗号アルゴリズムの中から、Amazonは"TLS_RSA_WITH_RC4_128_MD5" (0x0004)を選びました。これは"RSA" 公開鍵アルゴリズムを利用して証明書の署名の検証や鍵の交換を行うことを意味します。また、RC4暗号アルゴリズムを使ってデータを暗号化し、MD5ハッシュ関数でメッセージの内容を検証します。これらの処理の詳細は後述します。私個人としては、Amazonがこの暗号アルゴリズムの組み合わせを選んだのには何か利己的な理由があると思います。Amazonが選択したのは一覧の中でも最もCPU負荷の少ない組み合わせなので、この組み合わせを採用することで各サーバが多くのコネクションを処理できるようにすることを考えているのかもしれません。また、ひょっとしたら、これら3つのアルゴリズムを考案したRon Rivestに対して特別な敬意を表明するためにこの組み合わせを採用しているのかもしれません。もっとも、この可能性は限りなくゼロに近いと思いますが。
-
Certificate メッセージ (11):
このメッセージは2464バイトの巨大なメッセージで、クライアント側がAmazonを検証するために使う証明書です。証明書は実体がないわけではありません。内容のほとんどはブラウザで確認できます。
-
"Server Hello Done" メッセージ (14):
これはゼロバイトのメッセージで、クライアントに"Hello"プロセスが終わったことを通知します。この後、サーバがクライアントに証明書について問い合わせることはありません。
証明書を検証する
次に、ブラウザはAmazon.comを信頼すべきかどうか判断しなければなりません。このときに証明書を利用します。ブラウザはAmazonから送られてきた証明書を見て、現在時が"発行日"の2008年8月26日以降であり、"有効期限"の2009年8月27日以前であることを確認します。また、証明書の公開鍵を使って鍵を秘密裏に交換してもいいのかどうかを確認します。
それにしても、なぜこの証明書を信頼すべきなのでしょうか。
この証明書に添付されている"署名"はビックエンディアン形式の非常に長い数値列に過ぎません。
こんな数値列なら誰だって送信できます。なぜこの署名は信頼できるのか。この疑問に答えるには数学の世界に寄り道する必要があります。
幕間劇: 短いが恐ろしくはないRSAガイド
ときどき、数学がプログラミングと関係あるかどうか疑う人がいますが、証明書に関する技術は数学のとても実践的な適用例といえるでしょう。Amazonの証明書が教えてくれるのは、この証明書の署名を検証するにはRSAアルゴリズムを使う必要があるということです。RSAは1970年代にMITの3人の教授、Ron Rivest氏、Adi Shamir氏、Len Adleman氏によって作られました。彼らは2000年に渡る数学の発展が生み出したいくつかのアイディアを組み合わせる優れた方法を見つけ出し、シンプルで美しいアルゴリズムを作りだしました。
まず、ふたつの巨大な素数、"p"と"q"を選び出します。そして、このふたつの値を掛け合わせて"n = p*q"を求めます。次に、小さな公開指数 "e"を選択します。この値は"暗号化指数"です。また、"e"を使って特別な方法で作られた逆数を"d"と呼び、"復号化指数"にします。"n"と"e"は公開し、"d"は徹底的に秘密にして、"p"と"q"は捨ててしまいます (または、"d"と同じように秘密にします)。"e"と"d"が互いの逆数になっていることを念頭におくことは重要です。
さて、なんらかのメッセージを持っているとします。まずはこのメッセージを表すバイトを単純な数値"M"として解釈します。このとき、メッセージを"暗号化"して"暗号文"を手に入れたい場合は、下記の計算をします。
C ≡ Me (mod n)
Meは"M"をM自身に"e"回掛け合わせるということです。"mod n"は"n"で割ったときの余りだけ(例."法")を取得するということです。例えば、午前11時 + 3時間 ≡ 2時(午後) (mod 12時間) となります。メッセージの受信者は"d"を知っていますので、下記のように暗号化されたメッセージを逆転させて、元の平文に復元します。
Cd ≡ (Me)d ≡ Me*d ≡ M1 ≡ M (mod n)
興味深いのは、指数"d"を知っている人だけがメッセージ"M"を指数"d"の数だけ累乗することで文書に"署名"できるということです。
Md ≡ S (mod n)
これがうまく動作するのは、"署名者"が"S"、"M"、"e"、"n"を公開しているからです。従って、下記の単純な計算をすれば、誰でも署名"S"を検証することができます。
Se ≡ (Md)e ≡ Md*e ≡ Me*d ≡ M1 ≡ M (mod n)
RSAのような公開鍵暗号アルゴリズムは"非対称"アルゴリズムと呼ばれます。これは暗号鍵(この場合では"e")が復号鍵"d"と等しくない("対称"でない)からです。すべての値を"mod n"を使って換算することで、私たちが使っている普通の対数表では扱えないようになっています。RSAの魔法がうまく動作するのは、C ≡ Me (mod n) を計算/暗号化するのはとても素早くできるのに対して、"d"を知らずにCd ≡ M (mod n) を計算/復号化するのはとても難しいという性質があるからです。前述したように、"d"は"n"を元にしていて、"n"は"p" と "q"に因数分解できますが、実際に因数分解するのはとても大変です。
署名を検証する
RSAを実世界で利用するときに気をつけておかなければならないのは、このアルゴリズムで使う数値はすべて巨大にする必要があるということです。これは最良のアルゴリズムを使っても、この暗号を破るのが困難にしておくためです。では実際はどのくらい巨大なのか。Amazon.comの署名は"VeriSign Class 3 Secure Server CA"によって"署名"されています。この証明書からVeriSign社の法"n"を見ることができますが、この値は2048ビット長で10進表記で下記のような617桁の値を含みます。
1890572922 9464742433 9498401781 6528521078 8629616064 3051642608 4317020197 7241822595 6075980039 8371048211 4887504542 4200635317 0422636532 2091550579 0341204005 1169453804 7325464426 0479594122 4167270607 6731441028 3698615569 9947933786 3789783838 5829991518 1037601365 0218058341 7944190228 0926880299 3425241541 4300090021 1055372661 2125414429 9349272172 5333752665 6605550620 5558450610 3253786958 8361121949 2417723618 5199653627 5260212221 0847786057 9342235500 9443918198 9038906234 1550747726 8041766919 1500918876 1961879460 3091993360 6376719337 6644159792 1249204891 7079005527 7689341573 9395596650 5484628101 0469658502 1566385762 0175231997 6268718746 7514321
(この"n"から"p" と "q"を見つけてみてください。もしそれができたら、VeriSign社の証明書の本当の姿を生成することができるでしょう。)
VeriSign社の"e"は2^16 + 1 = 65537です。もちろん"d"は秘密になっています。きっと、網膜スキャナと武装した警備員に守られた安全なハードウエアの中にあるのだと思います。署名をする前にVeriSign社はAmazon.comが自身の証明書として要求する内容の妥当性を検証します。この検証には実世界での"ハンドシェイク"を使い、Amazonの各種ビジネス文書を確認します。VeriSign社はこれらの文書を確認し終えると、SHA-1ハッシュアルゴリズムを使って、要求されたすべての内容を含む証明書のハッシュ値を算出します。Wiresharkでは、証明書の全体は"signedCertificate"という部分が該当します。
これらのバイト値には既に署名が含まれているのではなく、実際はこれから署名の対象になるバイト値なので、"signedCertificate"という名前は、厳密には正しいとは言えません。
Wiresharkでは実際の署名"S"は単純に"encrypted"と表示されます。"S"をVeriSign社の公開されている"e"の値、すなわち指数65537分累乗してから法"n"で割った余りを取得すると、16進数で下記のような"復号化"された署名が取得できます。
0001FFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFF00302130 0906052B0E03021A 05000414C19F8786 871775C60EFE0542 E4C2167C830539DB
PKCS #1 v1.5 標準によると、はじめの1バイト"00"は"暗号化されたブロックを数値に変換しても法より小さくなることを保証"しています。次のバイト"01"はこれが秘密鍵による暗号化であること(例えば、署名であること)を示します。そのあと、たくさんの"FF"バイトで埋められていますが、これは復号結果を十分に大きくするためです。"FF"によるパディングは"00"まで続きます。そのあとは"30 21 30 09 06 05 2B 0E 03 02 1A 05 00 04 14"と続きますが、これはPKCS #1 v2.1におけるSHA-1ハッシュアルゴリズムの定義です。最後の20バイトは"signedCertificate"に含まれるSHA-1ハッシュのダイジェスト値です。
復号化された値が適切な形式になっていて、最後のハッシュ値も同じアルゴリズムを使って独自に算出した値と一致しているので、"VeriSign Class 3 Secure Server CA"の秘密鍵を知っている誰かが"署名"をしたということがわかります。前提として、私たちはVeriSign社だけが"d"を知っていることを暗黙に信頼しています。
さらに"VeriSign Class 3 Secure Server CA"の証明書がVeriSign社の"Class 3 Public Primary Certification Authority"によって署名されていることを検証することもできます。
ではなぜ"Class 3 Public Primary Certification Authority"を信頼してもいいのでしょうか。信頼の連鎖にはもうこれ以上高いレベルはありません。
トップにある"VeriSign Class 3 Public Primary Certification Authority"は自分自身によって署名されています。この証明書は信頼できる優れた証明書としてNetwork Security Services (NSS)ライブラリのcertdata.txtがバージョン1.4になって以降、Mozillaの製品に組み込まれています。このファイルはNetscape社のRobert Relyea氏によって下記のコメントとともに2000年9月6日にチェックインされています。
"フレームワークのコンパイルに残りのNSSを含めること。また、オープンソースでの利用が許可された証明書のなかに'生きている'certdata.txtを含めること(所有者の許可が得られた証明書は順次加えていく)。"
この決定は比較的長期間にわたって影響を及ぼすことになりました。というのは、この組み込まれた証明書は1996年1月28日から2028年8月1日まで有効だからです。
Ken Thompson氏が"Reflections on Trusting Trust"という題名のペーパーでとても上手に説明している通り、究極的には誰かを無条件で信頼しなければなりません。この問題を回避する方法はありません。今回の場合に即して言えば、私たちはRobert Relyea氏の下した判断が正しかったことを無条件に信頼しています。また。組み込まれている他の証明書もMozillaの組み込み証明書に対するポリシーを遵守していることも信頼しています。
ここで念頭においておかなければならないのは、このような組み込みの証明書と署名はすべて信頼の連鎖を構成するのに使われるだけだということです。インターネットではどんなサイトを訪れようと、FirefoxはVeriSign社のルート証明書を暗黙に信頼しています。一方、自分の会社で独自のルート認証局(CA)を作って他人のマシンにその証明書をインストールすることもできます。
また、VeriSign社のような会社にお金を払わずに、かつ、証明書の信頼の連鎖をまったく無視する方法もあります。証明書は信頼された第三者(ここではVeriSign社)を利用して信頼関係を確立するときに使います。相手の耳元で長いパスワードをつぶやいて教えるような、秘密の "鍵"を共有する安全な方法を持っていれば、その事前共有鍵(PSK)を使って信頼関係を確立すればいいのです。この場合、信頼された第三者は必要ありません。TLSにはこの方法を実現するための拡張があります。例えば、TLS-PSKですが、私が気に入っているのはTLS with Secure Remote Password (SRP)です。しかし、残念ながらこれらの拡張はほとんど普及しておらずサポートもされていません。加えて、これらの方法を使う場合、秘密をやり取りを他の方法で実現しなければなりませんが、これは負担になります。標準的なTLSの方法よりも扱いにくいからです(そうでなければTLSがこんなに普及するでしょうか)。
さて、最後にやらなければならないのは証明書のホスト名が期待しているホスト名と同じかどうかを検証することです。この検証がなぜ必要なのか。Nelson Bolyard氏がSSL_AuthCertificate関数に書いたコメントを読めばわかります。
/* 証明書は問題なし。これはクライアント側のSSLコネクションです。
* 証明書の名前フィールドが期待しているものと一致するか調べてください。
* NB: この調査は中間者(MITM)攻撃に対する唯一の対策です! */
この検証は中間者攻撃を防ぐのに役立ちます。私たちは、証明書の信頼の連鎖に含まれている人は何も悪い事をしないと考えています。例えば、実際はAmazon.comから来ていない証明書に対して、あたかもAmazon.comから来ているように署名するというような悪い事は決してしないと、無条件に信頼しています。もし攻撃者がDNSキャッシュ・ポイズニングのような方法を使ってDNSサーバを改竄した場合、クライアント側は信頼されたサイト(Amazon.comのような)を閲覧していると騙されてしまうかもしれません。アドレスバーは普通に見えるからです。この最後の検証は認証局がうっかりこのような事態を発生させないことを暗黙に信頼しています。
プリマスタシークレット
さて、ここまででAmazon.comが送ってきた情報のいくつかを検証しました。また、公開されている暗号化指数"e"と法"n"もわかりました。しかし、トラフィックを監視している者なら誰でもこのふたつの値を知ることができます(その証拠に私たちはWiresharkを使ってキャプチャしています)。そこで、盗聴者/攻撃者が決して解明できないようなランダムな秘密鍵を生成する必要があります。これは言うほどには簡単ではありません。1996年、研究者たちはNetscape Navigator1.1が擬似乱数発生器で乱数を生成するときに、たった3つのソースだけしか使っていないことを突き止めました。それらのソースは、日時、プロセスID、そして親プロセスIDでした。研究者たちが示したようにこれらの"ランダム"なソースは実際にはランダムではなく比較的簡単に見つけられてしまうのです。
すべての値が3つだけの"ランダム"なソースから作られるので、1996年頃までのマシンでは、たった25秒間でSSLの"安全性"を"破る"ことが可能でした。こう聞いてもまだランダム性を解析するのが難しいと思っている方は、DebianのOpenSSLのメンテナに聞いてみてください。この乱数生成をいい加減に行うと、その上に構築されるすべての安全性が揺らいでしまいます。
Windowsでは、暗号化を目的にして乱数を生成するにはCryptGenRandom関数を使います。この関数は125を超えるソースから抽出したビット値をハッシュします。Firefoxはこの関数で得たビット値と 独自の関数から得たビット値を使って疑似乱数生成器で乱数を生成します。
これによって生成されるのが48バイトの"プリマスタシークレット"です。この値は直接は利用しませんが、この値から多くのことが生まれるので絶対に秘密にしておかなければなりません。もちろん、FireFoxもこの値を簡単に見つけられないようにしてます。今回の調査では、私はデバッグバージョンでコンパイルしてSSLDEBUGFILEとSSLTRACEのふたつの環境変数を設定することで、この値を確認できました。
今回の調査のセッションで、SSLDEBUGFILE変数の中に見られたプリマスタシークレットの値は下記の通りです。
4456: SSL[131491792]: Pre-Master Secret [Len: 48]
03 01 bb 7b 08 98 a7 49 de e8 e9 b8 91 52 ec 81 ...{...I.....R..
4c c2 39 7b f6 ba 1c 0a b1 95 50 29 be 02 ad e6 L.9{......P)....
ad 6e 11 3f 20 c4 66 f0 64 22 57 7e e1 06 7a 3b .n.? .f.d"W~..z;
この値が完全にはランダムでないことに注意してください。最初の2バイトは慣例に従ってTLSのバージョンを表しています(03 01)。
秘密を交換する
次に、この秘密の値をAmazon.comへ渡さなければなりません。Amazonは暗号アルゴリズムとして"TLS_RSA_WITH_RC4_128_MD5"を希望しています。これに従ってクライアントはRSAを使って受け渡しを行います。単純にこのプリマスタシークレットの値をそのままRSAの入力にしてもいいのですが、公開鍵暗号標準(PKCS)#1のバージョン1.5 RFCに従えば、入力の値を法の大きさ(1024ビット/128バイト)と同じにするためにランダムな値で埋めなければなりません。これは、攻撃者がプリマスタシークレットを判読するのを難しくするためです。また、この秘密の値を再利用してしまうような本当に間抜けなことをしてしまった場合の最終的な対策にもなります。万が一再利用してしまっても、ランダム値で埋められていれば、盗聴者にはネットワーク上にある別の値のように見えるからです。
繰り返しますが、Firefoxでこの値を確認するのは難しいです。私の場合はランダム値で埋める関数にデバッグ文を挿入して何が起こっているのか確認しました。
今回調査しているセッションでは、ランダム値で埋められた値の全体は下記のようになっていました。
00 02 12 A3 EA B1 65 D6 81 6C 13 14 13 62 10 53 23 B3 96 85 FF 24 FA CC 46 11 21 24 A4 81 EA 30 63 95 D4 DC BF 9C CC D0 2E DD 5A A6 41 6A 4E 82 65 7D 70 7D 50 09 17 CD 10 55 97 B9 C1 A1 84 F2 A9 AB EA 7D F4 CC 54 E4 64 6E 3A E5 91 A0 06 00 03 01 BB 7B 08 98 A7 49 DE E8 E9 B8 91 52 EC 81 4C C2 39 7B F6 BA 1C 0A B1 95 50 29 BE 02 AD E6 AD 6E 11 3F 20 C4 66 F0 64 22 57 7E E1 06 7A 3B
Firefoxはこの値を使って"C ≡ Me (mod n)"を計算し "C ≡ Me (mod n)""Client Key Exchange" レコードに見られる値を取得します。
最後にFirefoxは平文のメッセージ、 "Change Cipher Spec"レコードを送信します。
以上の方法で、FirefoxはAmazonに対して今後のメッセージを暗号化してやり取りするために、互いに合意した秘密を使うことを通知しました。
マスタシークレットを生成する
ここまですべて正常に動作していれば、クライアントとサーバは(そしてクライアントとサーバだけが)48バイト(256ビット)のプリマスタシークレットを知っています。しかし、Amazon側から見ると信頼性についてちょっとした問題があります。というのは、このプリマスタシークレットはクライアントで生成された単なるビット列でしかないので、サーバ側の情報や前述したクライアントとサーバの間のやり取りについては考慮されていません。この問題を解決するためには、"マスタシークレット"を算出する必要があります。仕様によれば、マスタシークレットは下記の計算で算出されます。
master_secret = PRF(pre_master_secret, "master secret", ClientHello.random + ServerHello.random)
"pre_master_secret"はクライアントが送信したプリマスタシークレットのことです。"master secret"は単なる文字列で、ASCIIコードのバイト(例えば"6d 61 73 74 65 72 ..."のような)が使われます。そして、最初に説明したClientHelloとServerHello(Amazon側からの)で送信したランダム値を連結します。
PRFは"疑似乱数関数(Pseudo-Random Function)"です。仕様が定められているこの関数はとても優秀です。この関数はMD5ハッシュ関数とSHA-1ハッシュ関数を両方利用したkeyed-Hash Message Authentication Code (HMAC)を使って秘密の情報とASCIIコードの文字列とランダムな値を結合します。入力値の半分ずづをそれぞれのハッシュ関数に渡すことで、攻撃に対する耐性がとても高くなっています。たとえ、MD5やSHA-1の弱点を突かれても強度は変わりません。また、この関数は必要なバイト数が生成されるまで繰り返されます。
この関数を実行することで下記の48バイトの"マスタシークレット"が取得できました。
4C AF 20 30 8F 4C AA C5 66 4A 02 90 F2 AC 10 00 39 DB 1D E0 1F CB E0 E0 9D D7 E6 BE 62 A4 6C 18 06 AD 79 21 DB 82 1D 53 84 DB 35 A7 1F C1 01 19
たくさんの鍵を生成する
さて、これでクライアントとサーバ双方が"マスタシークレット"を手に入れました。この状態で、どのようにして必要なセッションキーをすべて生成するのか。それは仕様に書いてあります。生成にはPRFを使ってデータの抽出元になる"キーブロック"を作る必要があります。
key_block = PRF(SecurityParameters.master_secret, "key expansion", SecurityParameters.server_random + SecurityParameters.client_random);
"キーブロック"から抽出した値を使って下記のキーを生成します。
client_write_MAC_secret[SecurityParameters.hash_size]
server_write_MAC_secret[SecurityParameters.hash_size]
client_write_key[SecurityParameters.key_material_length]
server_write_key[SecurityParameters.key_material_length]
client_write_IV[SecurityParameters.IV_size]
server_write_IV[SecurityParameters.IV_size]
Advanced Encryption Standard (AES)のようなブロック暗号の代わりにストリーム暗号を使っているので、初期化ベクタ(IV)は必要ありません。したがって必要なのは、双方のメッセージ認証コード (MAC)です。MD5のダイジェスト値のサイズが16バイトなので、この値のサイズは16バイト(128ビット)になります。加えて、後述するRC4暗号アルゴリズムが16バイト(128ビット)のキーを使います。これも双方に必要です。つまりキーブロックから抽出するのは合計で2*16 + 2*16 = 64バイトになります。
PRFを実行して下記の値を取得しました。
client_write_MAC_secret = 80 B8 F6 09 51 74 EA DB 29 28 EF 6F 9A B8 81 B0
server_write_MAC_secret = 67 7C 96 7B 70 C5 BC 62 9D 1D 1F 4A A6 79 81 61
client_write_key = 32 13 2C DD 1B 39 36 40 84 4A DE E5 6C 52 46 72
server_write_key = 58 36 C4 0D 8C 7C 74 DA 6D B7 34 0A 91 B6 8F A7
いよいよ暗号化!
クライアントが送信する最後のハンドシェイクメッセージは"Finishedメッセージ"です。このメッセージが優れているのは、ハンドシェイクが誰にも改竄されていないこととクライアント側がキーを知っていることを証明するからです。クライアントはすべてのハンドシェイクメッセージのバイト値を"handshake_messages"という名のバッファに入れています。さらにクライアント側はマスタキーと文字列"client finished"と"handshake_messages"バッファを入力にしたMD5とSHA-1の値を使って疑似乱数関数(PRF)を実行し、12バイトの"verify_data"を算出します。
verify_data = PRF(master_secret, "client finished", MD5(handshake_messages) + SHA-1(handshake_messages)) [12]
そして、算出結果の先頭に"finished"を表すバイト"0x14"を付加します。さらに12バイトの検証用データを送信中だということを表す"00 00 0c"を付加します。また、これから扱う暗号化されたメッセージと同じように、復号化したメッセージも改竄されていないか検証しなければなりません。利用している暗号アルゴリズムはTLS_RSA_WITH_RC4_128_MD5なので、この検証にはMD5ハッシュ関数を利用します。
MD5と聞くと極端に怪しむ人がいます。MD5にはいくつかの弱点があるからです。私もMD5をそのまま使うのはお勧めしません。しかし、TLSは賢いのでMD5を直接使わずにHMACのバージョンを使います。つまり、MD5(m)という計算をするかわりに下記の計算を行います。
HMAC_MD5(Key, m) = MD5((Key ⊕ opad) ++ MD5((Key ⊕ ipad) ++ m)
(⊕ は 排他的論理和を、++ は 結合、"opad" は"5c 5c ... 5c"というバイト列、そして、"ipad"は"36 36 ... 36"というバイト列を表します。)
具体的には、下記の計算をします。
HMAC_MD5(client_write_MAC_secret, seq_num + TLSCompressed.type + TLSCompressed.version + TLSCompressed.length + TLSCompressed.fragment));
見ての通り、この計算にはシーケンス値("seq_num")が平文の属性("TLSCompressed"のことです)と一緒に使われています。このシーケンス値は、以前に暗号化されたメッセージを手に入れてクライアントとサーバのやり取りに注入してくる攻撃者を撃退してくれます。こういう攻撃をしても、シーケンス値は絶対に一致しないからです。このシーケンス値も攻撃者からメッセージを守ってくれているのです。
さて、あとは実際のやり取りを暗号化するだけです。
RC4暗号
利用している暗号アルゴリズムはTLS_RSA_WITH_RC4_128_MD5でした。ということはトラフィックの暗号化に利用するのはRon's Code #4 (RC4)になります。Ron Rivest氏がこのRC4アルゴリズムを作ったのは、256バイトのキーからランダムなバイト列を生成するためでした。このアルゴリズムはとても単純なので数分もあれば記憶できます。
RC4は、まず256バイトのバイト配列"S"を作成し0から255までの値で埋めます。そして、キーとなる値を使ってこの配列全体を混ぜます。これは"ランダム"なバイト列を生成するステートマシンを作成するための処理です。ランダムなバイト列を生成するには配列"S"をごちゃ混ぜにする必要があります。
この処理を図にすると下記のようになります。
暗号化をするには、こうして作成したバイト列と暗号化したいバイト列の排他的論理和を計算します。あるビットと1を排他的論理和で計算するとその1が反転することを思い出してください。生成したのは乱数なので、平均すれば半数のビットが反転することになります。このように乱数を使ってビットを反転させる方法は暗号化方法としては効果的です。今まで見てきたようにそれほど複雑ではありませんし、計算も素早くできるからです。Amazonがこの暗号アルゴリズムを選んだのもこれらの長所を考慮したからだと思います。
さて、"client_write_key" と "server_write_key"を生成したことを思い出してください。2つのキーがあるので、RC4のインスタンスは2つ必要になります。ひとつはブラウザから送信するデータを暗号化し、もうひとつはサーバから送られてくるデータを復号化します。
RC4の"client_write"インスタンスの最初の数バイトは"7E 20 7A 4D FE FB 78 A7 33 ..."です。この値と暗号化されていないヘッダの排他的論理和を計算して、メッセージを表すバイト列"14 00 00 0C 98 F0 AE CB C4 ..."を検証すれば、Wireshark上で見られる暗号化された部分がどうなっているのかわかるはずです。
サーバもほとんど同じことをします。サーバは"Change Cipher Spec"を送信し、"Finishedメッセージ"を送信します。"Finishedメッセージ"にはすべてのハンドシェイクメッセージが含まれています。クライアントの"Finishedメッセージ"を復号化したものが含まれているので、このメッセージを受信したクライアントはサーバが復号化処理に成功したことがわかります。
アプリケーションレイヤへようこそ!
通信を始めて220ミリ秒経ちました。ついにアプリケーションレイヤの出番です。クライアントはTLSレイヤで暗号化用のRC4インスタンスを使って普通のHTTPトラフィックを暗号化し、受信したトラフィックを復号化用のRC4インスタンスで復号化します。さらに、TLSレイヤは各レコードが改竄されていないかどうかをレコードのHMAC_MD5ハッシュ値を算出して検証します。
この時点では既にハンドシェイクは終わっています。レコードのコンテンツタイプは23 (0x17)です。暗号化されたトラフィックは"17 03 01"で始まっていますが、これはこのレコードの種類とTLSのバージョンを表します。そして、その後に暗号化されたデータの大きさが続きます。この値にはHMACハッシュ値も含まれています。
次の平文を暗号化すると、
GET /gp/cart/view.html/ref=pd_luc_mri HTTP/1.1
Host: www.amazon.com
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.0.10) Gecko/2009060911 Minefield/3.0.10 (.NET CLR 3.5.30729)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
...
Wireshark上では、下記のようになります。
その他に注目すべき点としては、シーケンス値がレコードごとに増加していくことです(最初が1で次のレコードが2、というように)。
サーバ側もserver_write_keyを使って同じことをします。サーバ側の返信は下記の通りです。アプリケーションデータヘッダの値は隠されていません。
これを復号化すると、
HTTP/1.1 200 OK
Date: Wed, 10 Jun 2009 01:09:30 GMT
Server: Server
...
Cneonction: close
Transfer-Encoding: chunked
これは普通のHTTP応答で、非記述的な"Server: Server"を含んできます。また、Amazonのロードバランサが返す、スペルが間違っている"Cneonction: close"も含んでいます。
TLSはアプリケーションレイヤのすぐ上に位置します。したがって、HTTPサーバはまるで暗号化していないトラフィックを扱っているように振る舞うことができます。唯一違うのは暗号化処理を行うライブラリを使うことです。このようなライブラリとしてはOpenSSLがあります。OpenSSLは人気のあるオープンソースのTLS用ライブラリです。
一度開いたコネクションはクライアントとサーバが暗号化されたデータを送受信している限りは開いたままですが、どちらか一方が"closure alert"メッセージを送信すると閉じます。コネクションをとじだ後、すぐに再開した場合は、公開鍵暗号方式を使わずに暗号化通信に必要なキーを再利用できます(サーバ側がキャッシュしていた場合)。そうでなければ、もう一度ハンドシェイクをやり直す必要があります。
アプリケーションのデータレコードがどのようなものであっても暗号化できることを理解しておくのは重要です。"HTTPS"だけが特別に着目されるのはもっとも一般的だからです。TLSの上で動作するTCP/IPベースのプロトコルは他にもたくさんあります。例えば、FTPSやSMTPのセキュア拡張です。独自に安全性を確保する方法を実装するよりもTLSを使った方がいいのは間違いありません。また、詳細な安全性の分析に耐えてきたプロトコルを使うこと自体にもメリットがあります。
... ついに終わりました!
この記事では触れていない詳細について、TLS RFCにはとても読みやすく記述されています。この記事はTLS RFCの記述の中のひとつのパターンを、FirefoxとAmazonのサーバの間の220ミリ秒のやり取りを調査しながら追いかけてみました。ただし、この記事の調査はAmazon側がServerHelloメッセージを送信したときに暗号アルゴリズムにTLS_RSA_WITH_RC4_128_MD5を選んだことに大きく依存しています。安全性よりも処理速度をやや優先したい場合、この選択は合理的です。
上述したように、Amazonの法"n"を秘密裏に因数分解して"p"と"q"を導きだした人がいたら、その人はAmazonが証明書を変更するまで、すべての"安全な"トラフィックを復号化できてしまいます。Amazonはこの対策として証明書の有効期間を1年にしています。
暗号アルゴリズム一覧の中には"TLS_DHE_RSA_WITH_AES_256_CBC_SHA"がありましたが、これはディフィー・へルマン鍵交換を使います。このアルゴリズムには"Perfect Forward Secrecy"という優れた属性があります。この属性は、たとえあるセッションの鍵交換の処理が破られてしまってもその他のセッションの安全性には影響がでないようにします。ただし、このアルゴリズム使う場合は巨大な数値が必要ありますので、負荷の高いサーバにとっては余計な負担になってしまうという欠点があります。また、"Advanced Encryption Standard" (AES)アルゴリズムも暗号アルゴリズム一覧に何度も出てきました。RC4は単一のバイトを処理対象にしますが、このアルゴリズムは16バイトの"ブロック"を処理単位としています。鍵の長さを256ビットにできるので、RC4よりも安全だと考えられています。
以上、たった220ミリ秒の間にインターネット上のふたつのエンドポイントが互いに信頼し合うために必要な情報を提供しあい、暗号アルゴリズムを準備して暗号化したトラフィックを送り出すまでを観察しました。
この記事で説明したハンドシェイクを概観するためのプログラムを書きましたので、参考にしてください。
著者について
Jeff Moser氏はソフトウエア開発者であり、ブログhttp://www.moserware.com/の管理人でもあります。
注記:この記事ははじめJeff Moser氏のブログ、MOSERWAREに掲載されました。