SSL/TLSのハンドシェイクってどんなの？

ようこそ、SSL/TLS Advent Calendarへ！

QiitaもようやくSSLになったので、このカレンダーを読んでいる人はほぼ確実に`qiita.com`へのSSL接続を済ませていることかと思います。では、その接続とはどのような形で行われるものなのか、考えたことがある人はそんなに多くないかもしれません。

今回は、初回にふさわしく、SSL[^1]の接続を開始してから成立するまでを考えていきます。

# ハンドシェイクに要求されるもの

実際の流れを見ていく前に、SSLのハンドシェイクにはどのような特性が必要か考えてみましょう。両者で合意しないといけないものとしては、以下のようなものがあります。

* 使用する暗号化などの鍵スイート
* 使う暗号鍵

そして、ハンドシェイクにおいても次のような要件が必要です。

* （切断以外の）中間者攻撃に耐えられる
* 鍵は第三者から特定不能な形で共有する
* 正しく接続できなければ接続を成立させない

もちろん、これらの要求を満たすようにSSLは実装されています。

# 実際の流れ

TCPでも3-way Handshakeがありますが、SSLの場合も接続を成立させるまでには何往復かのラウンドトリップが必要となります。

## 概要

SSLのセッション構築は概ね、以下のような流れで進んでいきます。

0. `ClientHello`として、クライアントからサーバに必要な情報を渡す
1. `ServerHello`から始まる一連のメッセージで、サーバ側の暗号形式などの決定・（必要なら）鍵共有に必要な情報の送信を行う
2. `ClientKeyExchange`等によるメッセージで、クライアント側からも鍵共有に必要な情報の送信を行う
3. サーバ側が`ChangeCipherSpec`を返して、鍵スイートが切り替わったことをクライアントに知らせる
4. 中身の通信を開始

## `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`を返すことで、こちらもハンドシェイクが完了します。


# 参考リンク

* [IPAによるRFC 2246（TLS 1.0）の翻訳](https://www.ipa.go.jp/security/rfc/RFC2246-00JA.html)

[^1]: いちいちSSL/TLSと書くのも面倒なので、特にTLS 1.0以上を前提とする場面以外は、SSLで代表させます。
[^2]: SSL 3.0より新しいことを示すために、TLSのバージョンには2.1を足して使います（TLS 1.2なら3.3）。
[^3]: フォーマットが合っていない、合意できるCipherSuiteがないなどでは継続不能です。
[^4]: 匿名方式ではもちろん証明書はありませんが、少なくともHTTPSでは匿名方式のSSLはほぼ実用されていません。