TLSセッション再開 (session resumption) のしくみ


SSL3.0~TLS1.2は、暗号通信が始まるまでのハンドシェイク手順がとても遅いので(それでもsshよりはだいぶマシだと思いますが)、これを多少なりとも高速化すべく、1回目のハンドシェイクは通常通りの手順(フルハンドシェイク)で行い、そこで得られるmaster_secret値をキャッシュして2回目以降は使い回すという方法があります。この2回目以降のハンドシェイク手順を「セッション再開」 (session resumption) と呼びます。

今のところ、Session IDを使う古典的な方法と、TLS Session Ticket拡張 (RFC5077, Google翻訳) を使う先進的な方法の2種類があります。

用語説明

セッションとコネクション

TLSでは、セッションとコネクションは明確に別物です。
セッションの一生はmaster_secretとともにあります。フルハンドシェイクでmaster_secretが算出されるときにセッションが生まれます。コネクションが終わっても、セッションはすぐには終わりません。セッションキャッシュと呼ばれる領域の中で生きています。このキャッシュされたセッションを使い回して別なコネクションを張るのが「セッション再開」です。

ここで1つの疑問があります。
我々が普段GET / HTTP/1.1とかの文字列を流し込み、HTTP/1.1 200 OKとかの文字列を吐き出しているストリーム状の概念がコネクションなんでしょうか。それとも、ハンドシェイクと、それに続く一連のApplication Dataの送受信がコネクションなんでしょうか。
再ネゴシエーションがなければ、これはどちらも同じです。
しかし再ネゴシエーションがある場合、1回目のハンドシェイクと2回目のハンドシェイクの間が1つのコネクションで、その後は別のコネクションでしょうか。それとも、それをつなぎ合わせた一連の「上位レイヤからソケットに見えるもの」一式全部がコネクションなんでしょうか。
いずれにしても、「コネクションでない方」は何と呼べばいいのでしょうか。

というわけでこの件、恐れ入りますが緩募とさせていただきたく存じます。

Session IDを使う古典的なフルハンドシェイク

さてここからが本題です。まずは普通のハンドシェイク。

                   ← HelloRequest*
       ClientHello → 
                   ← ServerHello
                   ← ServerCertificate
                   ← ServerKeyExchange*
                   ← CertificateRequest*
                   ← ServerHelloDone
ClientCertificate* → 
 ClientKeyExchange → 
CertificateVerify* → 
  ChangeCipherSpec → 
          Finished → 
                   ← ChangeCipherSpec 
                   ← Finished
  Application Data ⇔ Application Data

*は、送信されない場合もあるという意味です。

このときのServerHello.SessionIDを、このセッションのSessionIDと位置づけます。クライアントとサーバの双方で、このSessionIDとmaster_secretその他各種セッションパラメータをセットでセッションキャッシュ領域に保存します。

Session IDを使う古典的なセッション再開

ClientHelloとServerHelloの送受信まで進んだ段階でClientHello.SessionIDとServerHello.SessionIDが一致していたらセッション再開スタートです。ハンドシェイク手順はこうなります。

                   ← HelloRequest*
       ClientHello → 
                   ← ServerHello
                   ← ChangeCipherSpec 
                   ← Finished
  ChangeCipherSpec → 
          Finished → 
  Application Data ⇔ Application Data

だいぶ短くなったでしょ!これでパフォーマンスが稼げるわけです。

Q. Session IDに非対応のクライアントやサーバはどうすればいいのか。
A. 長さ0のSession IDを送ります。

Q. これはセキュアなのか。
A. 今のところSession IDが死んだという話は聞きませんので、セキュアと考えていいと思います。ただし、セッション再開の時にはサーバ証明書の検証をしませんので(クライアント証明書の検証もしません)、フルハンドシェイク時点では有効でも、セッション再開の時点では既に有効期限が切れていたとか、CRLが発行されていたとか、そういうのは検出できません(コネクション成功してしまいます)。これに対抗するにはセッションの生存時間を短めに設定するしかありません。RFC5246 (Google翻訳) (TLS1.2) の推奨は長くとも24時間となっています。まあそんなところでしょう。

Q. セッション再開するとApplication Data送受信に使う秘密鍵は前回のコネクションと同じになるのか。
A. いいえ。ClientHello.RandomとServerHello.Randomが前回と別物になるので、master_secretが同じでもkey_blockが変わります。そのためApplication Data送受信鍵は前回とは違うものになります。

Q. 再ネゴシエーションとセッション再開の関係は。
A. 再ネゴシエーションとセッション再開は独立です。再ネゴシエーションの2回目のハンドシェイクはフルハンドシェイクでもセッション再開でも構いません。また、セッション再開の場合、2回目のハンドシェイクは1回目と同じセッションでも別のセッションでも構いません。
余談ですが、Triple Handshake攻撃は、一部の製品で再ネゴシエーションの2回目がフルハンドシェイクだった場合のクライアント側の証明書検証が甘いというバグを突いた攻撃です。

Q. Session IDの何が悪いのか。
A. サーバが複数でロードバランスしている場合、1つのクライアントに対して1回目のコネクションと2回目のコネクションを別のサーバで受けるとセッション再開ではなくフルハンドシェイクになってしまうので効率が落ちるという話です。
昔はロードバランサの「スティッキーセッション」機能を使うことで対処したものですが、最近はサーバ間でmemcached等を駆使してセッションキャッシュを共有する方法か、後述するSession Ticketを使う方法が主流になってきているようです。

Session Ticket拡張を使う先進的なフルハンドシェイク

                   ← HelloRequest*
       ClientHello →                     # empty "session ticket" extension
                   ← ServerHello         # empty "session ticket" extension
                   ← ServerCertificate
                   ← ServerKeyExchange*
                   ← CertificateRequest*
                   ← ServerHelloDone
ClientCertificate* → 
 ClientKeyExchange → 
CertificateVerify* → 
  ChangeCipherSpec → 
          Finished → 
                   ← NewSessionTicket    # New!
                   ← ChangeCipherSpec 
                   ← Finished
  Application Data ⇔ Application Data

ClientHelloとServerHelloには「Session Ticket拡張」がつきます。
Session Ticket拡張はパラメータを取ることができますが、初期状態では、クライアントはなんの情報も持っていませんので、ClientHelloのパラメータはEmptyです。また、ServerHelloのパラメータは常にEmptyです。

NewSessionTicketというのが新機軸です。これはSession Ticket拡張を使う場合に限って登場するメッセージで、これがSession IDの代わりになります。

Q. Session TicketとやらはSession IDと何が違うのか。
A. 「長さ」と「発行タイミング」が違います。
長さとは、Session IDの0~32バイトに対し、Session Ticketは最大64キロバイトまで行けるということです。
発行タイミングとは、Session IDはServerHelloの段階で発行するのに対し、Session Ticketは鍵交換終了後、つまりmaster_secretその他各種セッションパラメータが出揃ってから発行するということです。
このことを利用して、サーバ側でセッションパラメータそのものを暗号化してSession Ticketに詰め込んで発行するということが行われます。こうすると、サーバがセッションキャッシュ領域を持つ必要がなくなります。セッション再開時にはサーバのキャッシュではなくSession Ticketそのものを解読して利用すればいいからです。

Session Ticket拡張を使う先進的なセッション再開

                   ← HelloRequest*
       ClientHello →                     # "session ticket" extension
                   ← ServerHello         # empty "session ticket" extension
                   ← NewSessionTicket*
                   ← ChangeCipherSpec 
                   ← Finished
  ChangeCipherSpec → 
          Finished → 
  Application Data ⇔ Application Data

直近のNewSessionTicketでサーバからクライアントに送り出されたSession Ticketを、今度はClientHelloのSession Ticket拡張のパラメータに詰めてクライアントからサーバへと送ります。
サーバ側ではこれを解読し、有効な内容と認められれば、それを使って「セッション再開」手順によるハンドシェイクを行います。
この場合のNewSessionTicketは、Session Ticketの内容に変更がなければ省略して構いません。そんなわけでNewSessionTicketに*がつきます(*は送信されない場合もあるという意味です。覚えてますか?)。

Q. Session Ticketで使う暗号方式はCipher Suiteか何かで指定するのか。
A. Session Ticketは、サーバからサーバへ送信する、自分から自分への暗号文です。クライアントは単に中継するだけで中身には関知しません。つまり、サーバ内部で辻褄が合っていればいいのであって、Cipher Suiteでは何も指定しないのです。

Q. これはセキュアなのか。
A. 今のところセキュアです。Session Ticketではmaster_secretが暗号化されて流れますので、暗号化に使う秘密鍵の秘密保持には万全を期さなければなりません。TLSのセキュリティはmaster_secretが漏洩すると全部がぶっ壊れます。

Q. Session Ticketを使うにあたって注意すべきことはあるか。
A. ネットの怪しげな情報に惑わされないことです。Session Ticketの作成に使う秘密鍵は定期変更するのが望ましい、というのはいいのですが、ネットの怪しげな手順書の中には、設定ファイル上の秘密鍵データを書き換えても、それを稼働中のサーバプロセスに反映させる手順が抜けているものがあります。

Q. Session Ticketって何だか便利そうだ!ハンドシェイクを短縮する目的だけに使うのはもったいない。他の用途でも使えないか。
A. サーバ側から値を提示してクライアントはオウム返しに返すだけというのは、Cookieの仕組みに似ていますよね。というわけで、Session Ticketを使うまでもなく上位レイヤのHTTP Cookieか、HTML5 Web Storageを使うのが普通だと思います。

トラブルシューティング:WiresharkでキャプチャしたSession Ticketを何とか解読したい。鍵ならある

だいたい何でも入っているwiresharkですが、さすがにsession ticketの中身を解読する機能までは入っていないようです。

手元のnginx-1.10.1が生成するsession ticketをキャプチャして調べてみたところ、データフォーマットはRFC5077 Section 4の推奨形式にそこそこ近いものでした。つまり先頭16バイトがkey_name、次の16バイトがCBCのIV、真ん中が暗号化された本体、最後の32バイトがMACです。暗号方式はAES128-CBC、MAC方式はHMAC-SHA256です。

また、ssl_session_ticket_keyの乱数48バイトは16バイト×3であり、以下のように使われます。

  • 先頭16バイトはそのままsession ticketのkey_nameです。
  • 真ん中16バイトはAES128の秘密鍵です。
  • 末尾16バイトはHMAC-SHA256の秘密鍵です。RFC5077推奨はHMAC-SHA256秘密鍵を32バイトとしているのでちょっと足りませんが気にしない方向で。

この処理はngx_event_openssl.cに記述されています。

暗号方式やMAC方式そのものは素直なAES128-CBCとHMAC-SHA256なので、データフォーマットを自力で何とか処理すれば、あとはopenssl enc -aes-128-cbc -d -nosaltで復号化できます。ただし復号化された内容はRFC5077のStatePlaintextと全然違うASN.1です(なのでopenssl asnparse -inform DERで解読できます)。定義はopensslのssl/ssl.hのコメントに記載があります。

以上!幸運を祈る。

リファレンス

J. Salowey et al. (2008)
Transport Layer Security (TLS) Session Resumption without Server-Side State
https://tools.ietf.org/html/rfc5077 (Google翻訳)

新人さん著 (2015)
「細かすぎて伝わらないSSL/TLS」
Yahoo! Japan デベロッパーネットワーク
http://techblog.yahoo.co.jp/infrastructure/ssl-session-resumption/

不肖私 (2016)
「SSL/TLS(SSL3.0~TLS1.2)のハンドシェイクを復習する」
http://qiita.com/n-i-e/items/41673fd16d7bd1189a29