プロトコル自体を比べるとTLS 1.2よりもTLS 1.3の方が簡潔で洗練されている。しかし、APIの視点から見ると、TLS 1.3は非同期性が高くなる。同期性的なTLS 1.2のプロトコル設計には、妥当性があると気づいたので記録を残す。
以下のような API を考える
- new -- コンフィグを与えるとコンテキストを返す
- handshake -- コンテキストを与えると、ハンドシェイクを試みてTLSコネクションを作成する
- sendData -- コンテキストとデータを与えると、データを送信する
- recvData -- コンテキストを与えると、データを受信する
- bye -- アラートを送って、TLSコネクションを終了する
クライアントの疑似コードは、こんな感じ:
ctx = new(コンフィグ); handshake(ctx); sendData(ctx, "Hello world!"); rc = recvData(ctx); bye(ctx);
クライアント認証とNewSessionTicket
TLS 1.2のフルハンドシェイクでは、クライアントがサーバからFinishedを受け取って終了する。
Client Server handshake: ClientHello (empty SessionTicket extension)--------> : handshake ServerHello (empty SessionTicket extension) Certificate* ServerKeyExchange* CertificateRequest* <-------- ServerHelloDone Certificate* ClientKeyExchange CertificateVerify* [ChangeCipherSpec] Finished --------> NewSessionTicket [ChangeCipherSpec] <-------- Finished sendData: Application Data --------> : recvData recvData: <-------- Application Data: sendData bye : Alert -------->
TLS 1.2 のクライアントのhandshakeは、サーバのFinishedを待てるから:
- クライアントの handshake は NewSessionTicket を受け取れる
- クライアントの handshake はクライアント認証が失敗した際、Alert を受け取れる
クライアント認証が必要なくなるセッションの再開では、クライアントが Finished を送ることでハンドシェイクが完了する。
Client Server handshake: ClientHello (SessionTicket extension) --------> : handshake ServerHello (empty SessionTicket extension) NewSessionTicket [ChangeCipherSpec] <-------- Finished [ChangeCipherSpec] Finished --------> sendData: Application Data --------> : recvData recvData: <-------- Application Data: sendData bye : Alert -------->
実に同期的だ。非同期な要素としては、recvDataが正常終了や異常終了のアラートを受け取るぐらいである。
一方、TLS 1.3のハンドシェイクは以下の通り:
Client Server handshake: ClientHello + key_share* + signature_algorithms* + psk_key_exchange_modes* + pre_shared_key* --------> ServerHello: handshake + key_share* + pre_shared_key* EncryptedExtensions CertificateRequest* Certificate* CertificateVerify* <--------- Finished Certificate* CertificateVerify* Finished --------> sendData: Application Data --------> : recvData recvData: <-------- Application Data : sendData NewSessionTicket bye: Alert -------->
クライアントの handshake は、Finishedを送って終わるので、NewSessionTicketを受け取れない。このため、recvDataやbyeにNewSessionTicketを受け取る非同期的な工夫が必要となる。
同様にクライアントのhandshakeは、普通に実装したら、クライアント認証が失敗した際のアラートを受け取れない。handshakeがエラーを返さずに戻ってきたのに、recvDataがクライアント認証失敗のアラートを受け取るようなAPIは、とても使いずらい。よって、クライアントhandshakeがCertificateを送った場合は、一定期間アラートが戻ってこないか見張るべきだ。その間に、通常のデータを受信したら保存しておき、次にrecvDataが呼ばれた際に、そのデータを返すべきである。
余談だが、TLSレベルのクライアント認証は何かと問題が多いので、HTTPのレベルで証明書を使ったクライアント認証を実現しようという動きがある。
0-RTT
TLS 1.3 の 0-RTT は、とても非同期的だ。クライアントが、0-RTTでearly dataを送る場合、専用のAPIを与えると実用に乏しいことが経験的に分かった。そこで、これまでのsendDataでearly dataを送ることを考える。
handshake: ClientHello + early_data + key_share* + psk_key_exchange_modes + pre_shared_key sendData: Application Data --------> ServerHello: handshake + pre_shared_key + key_share* EncryptedExtensions + early_data* <-------- Finished sendData: EndOfEarlyData Finished Application Data --------> : recvData recvData: <-------- Application Data : sendData NewSessionTicket bye: Alert -------->
- クライアントのhandshakeは、ClientHelloを送ったら戻ってこないといけない。フルハンドシェイクのときは、戻らないことに注意。
- クライアントのsendDataは、ハンドシェクが完了したら EndOfEarlyData と Finished を送らないといけない
- サーバのrecvDataは、EndOfEarlyData と Finishedも受け取らないといけない