ようこそ、SSL/TLS Advent Calendarへ!
QiitaもようやくSSLになったので、このカレンダーを読んでいる人はほぼ確実にqiita.com
へのSSL接続を済ませていることかと思います。では、その接続とはどのような形で行われるものなのか、考えたことがある人はそんなに多くないかもしれません。
今回は、初回にふさわしく、SSL1の接続を開始してから成立するまでを考えていきます。
ハンドシェイクに要求されるもの
実際の流れを見ていく前に、SSLのハンドシェイクにはどのような特性が必要か考えてみましょう。両者で合意しないといけないものとしては、以下のようなものがあります。
- 使用する暗号化などの鍵スイート
- 使う暗号鍵
そして、ハンドシェイクにおいても次のような要件が必要です。
- (切断以外の)中間者攻撃に耐えられる
- 鍵は第三者から特定不能な形で共有する
- 正しく接続できなければ接続を成立させない
もちろん、これらの要求を満たすようにSSLは実装されています。
実際の流れ
TCPでも3-way Handshakeがありますが、SSLの場合も接続を成立させるまでには何往復かのラウンドトリップが必要となります。
概要
SSLのセッション構築は概ね、以下のような流れで進んでいきます。
-
ClientHello
として、クライアントからサーバに必要な情報を渡す -
ServerHello
から始まる一連のメッセージで、サーバ側の暗号形式などの決定・(必要なら)鍵共有に必要な情報の送信を行う -
ClientKeyExchange
等によるメッセージで、クライアント側からも鍵共有に必要な情報の送信を行う - サーバ側が
ChangeCipherSpec
を返して、鍵スイートが切り替わったことをクライアントに知らせる - 中身の通信を開始
ClientHello
SSL通信は、ClientHello
から始まります。これに含まれるデータは、以下のようなものです。
- Unixタイムと暗号学的擬似乱数からなる、ランダムデータ
- 希望するCipherSuiteのリスト
- SSL/TLSのバージョン2
- セッションID
- 圧縮アルゴリズム(CRIME脆弱性が出たので、事実上
NULL
一択です) - TLS拡張
ランダムデータは鍵を構築するベースになるものです。また、セッションIDについては、同じIDで再接続した場合に、セッション構築をすっ飛ばして接続を復元するという仕組みが存在します。
「TLS拡張」としては、以下のようなものがあります。
- 名前ベースのバーチャルホストを行っている場合、適切なドメイン名に相当する証明書をTLSレベルで提示する必要があるので、必要なドメインを伝えるSNI
- TLSのネゴシエーションをしてからプロトコルアップデートするとなるとさらに往復回数が増えてしまうので、TLSと同時にプロトコルネゴシエーションも済ませてしまう、ALPN
- TLSセッションの記憶をクライアントサイドに行わせる、TLS Session Ticket
ServerHello
~ServerHelloDone
ClientHello
に対して応答できるのであれば3、サーバーはServerHello
に始まる一連のメッセージを返します。
最初のServerHello
では、以下のようなものを返します。
- サーバー側のランダムデータ
- 合意したCipherSuite
- セッションID
- 合意した圧縮方式
- 拡張に関する情報(クライアント側から要求した拡張のみ)
なお、CipherSuiteの合意については、「サーバー/クライアントの両方が対応している」ことを前提に、サーバ側が決めることになっています。かつてはクライアント側の希望順序に従って決める例が多かったのですが、セキュリティ向上のためにサーバサイドの優先度に従って決めることが一般的になっています。ただ、「AESをハードウェア処理できないモバイル端末ではChacha20-Poly1305のほうが速い」みたいな事情もあって、部分的にクライアントサイドの要望を見る、という実装もあります。
そして、ServerHello
に引き続いて、ServerCertificate
が送られてきます4。これはASN.1形式の電子証明書を、ルートの手前までのチェーンで連ねたものとなっています。
さらに、Diffie-Hellmanなど、両者から情報を共有する形で鍵交換を行う場合は、ServerKeyExchange
としてその情報を送ります(RSA鍵交換など、そのような必要がない場合は不要です)。クライアント証明書が必要ならCertificateRequest
を、そして最後にServerHelloDone
を送って、必要な情報の送信は一段落します。
ClientKeyExchange
~ChangeCipherSpec
~Finished
ServerHelloDone
を受け取ったクライアントは、まず必要ならClientCertificate
でクライアント証明書を送信します。
次に(ClientCertificate
が不要なら、ServerHelloDone
の受信直後に)ClientKeyExchange
を送信します。これは、RSA鍵交換であれば、48バイト(うち2バイトはTLSのバージョン)のpre_master_secret
をサーバのRSA鍵で暗号化して送信します。一方、Diffie-Hellmanで共有する場合には、クライント側で生成した値を送信します。
クライアント証明書で署名が可能な場合、ClientKeyExchange
に引き続いて、CertificateVerify
で、これまでのヘッダのやり取りを署名して、クライアントがきちんとデータをやり取りしていることの証明とします。
ここまで来たところで、ChangeCipherSpec
を投げて、暗号通信に入ることを宣言します。そして、暗号化された状態でFinished
を送信して、クライアント側のハンドシェイクは完了です。このFinished
には、送受信したヘッダすべてのハッシュが入っているので、中間者にヘッダを改ざんされた場合にはこの時点で異常に気づくようになっています。
クライアント側はもはやすることがないので、Finished
を送信した直後に(サーバからのFinished
を待たずに)データを送信してしまうことも可能です。これには中間者介入の危険もありますが、「適切な暗号化方式を選べば実用上問題はない」ということで、TLS False StartとしてRFC 7918となっています。
ChangeCipherSpec
~Finished
(サーバ側)
クライアントのFinished
を受け取って、暗号方式・Finished
のハッシュ値が正しいことを確認してから、ChangeCipherSpec
で暗号通信に入って、Finished
を返すことで、こちらもハンドシェイクが完了します。